
Bundle R Programs for Submission with pkglite: The pack/unpack Workflow
Turn your analysis R package into the single, reviewable ASCII text file an eCTD submission expects — the collate, pack, verify_ascii, and unpack round-trip, why a flat text artifact is used, and how it fits the ADRG’s Submission of Programs inventory
A regulatory submission ships the datasets and the metadata — but it also ships the analysis programs that produced the results, and the eCTD wants them as a single, flat, reviewable text file, not a folder tree of .R files. This lesson uses pkglite (an R package from Merck) to bundle an R package’s source into one ASCII text artifact and restore it: the collate → pack → verify_ascii → unpack round-trip, run end to end on a throwaway package so every line executes. It shows the pkglite.txt wire format, the ASCII submission gate, why a single-file text bundle fits the eCTD program-submission constraints, how it slots into the ADRG’s Submission of Programs section, and where pkglite’s job ends (it does not write .xpt or Define-XML).
- pkglite turns an R package’s source into ONE text file, and back. pkglite (an R package from Merck) packs a package directory —
DESCRIPTION,R/,man/, and the rest — into a singlepkglite.txt, andunpack()restores the folder tree from it. - A submission ships programs as a flat text artifact, not a folder. The eCTD (electronic Common Technical Document) wants the analysis programs as a single, reviewable, self-contained ASCII (plain 7-bit text) file — which is exactly what
pkglite.txtis. - The grammar is three verbs:
collate→pack→unpack.collate()selects the package files into a file collection,pack()writes them to the text file, andunpack()rebuilds the package. Usefile_ectd()(called with no arguments) as the spec so the collection matches the eCTD program-submission layout. verify_ascii()is the submission gate — run it, it is not automatic.pack()happily writes non-ASCII bytes;verify_ascii()scans the packed file and returnsTRUEonly when every byte is ASCII, so make it your pre-submission check.- The round-trip is content-faithful, not byte-identical, and its scope is narrow.
unpack()reproduces the source content exactly (a POSIX trailing newline may differ by one byte); pkglite does not write.xptfiles (that isxportr) or Define-XML (that ismetacore) — it only bundles the programs.
Introduction
You have exported the XPT (SAS Transport) datasets, written the Define-XML, and drafted the reviewer’s guides. There is one more thing a regulator receives: the analysis programs themselves — the R code that produced the tables, listings, and figures. The ADRG (Analysis Data Reviewer’s Guide) inventories them in its Submission of Programs section, and the eCTD (electronic Common Technical Document) expects them in the m5 analysis folder. But it does not want them as a folder tree of loose .R files. It wants a single, flat, self-contained, reviewable text file — one artifact a reviewer can open, read, and (if they choose) run.
That is the gap pkglite fills. pkglite is an R package from Merck — “a tool, grammar, and standard to represent and exchange R package source code as text files.” It takes your analysis R package (a real package directory: DESCRIPTION, an R/ folder of functions, maybe man/ and data/) and packs it into one ASCII (plain 7-bit text) file, conventionally pkglite.txt. A reviewer unpacks that file back into a working package. No archive, no binary, no folder tree — just text.
This lesson runs the whole round-trip on a small throwaway package so every line executes: collate the files, pack them to pkglite.txt, look at the wire format, verify it is ASCII, and unpack it back. Then it places pkglite in the submission — why a single-file text bundle, how it feeds the ADRG’s program inventory, and where its job stops.
What pkglite is
pkglite is an R package from Merck (source at github.com/Merck/pkglite, docs at merck.github.io/pkglite), written for exactly this regulatory workflow. Its CRAN title is “Compact Package Representations,” and its job is narrow and precise: convert one or more R package directories into a single text file, and restore them. The reference paper is Zhao et al., Electronic common technical document submission with analysis using R, Clinical Trials (2023) — the eCTD-with-R workflow pkglite standardizes.
The design is a small grammar of three steps:
| Verb | What it does | Produces |
|---|---|---|
collate(pkg, spec) |
Select which files of the package to include, using a file specification | a file collection |
pack(collection, output = ...) |
Serialize the collection to a single text file | pkglite.txt |
unpack(input, output = ...) |
Read the text file and rebuild the package on disk | a restored <pkg>/ folder |
The spec in collate() decides which files travel. For a submission bundle you use file_ectd() — the built-in specification tuned to the eCTD program-submission layout (root text files like DESCRIPTION and NAMESPACE; R/ source as text; man/ help as text with figures as binary; src/, data/, and so on). Below, we run the three verbs end to end.
Build a throwaway package to pack
pack() consumes a package directory, so first we need one. The block below writes a minimal but real package into a temporary folder — a DESCRIPTION file and a single function under R/ — so the round-trip has something to carry. This stands in for your analysis package (the ADaM (Analysis Data Model) derivation programs, the TLF — tables, listings, and figures — programs) without needing a full study on disk.
library(pkglite)
pkg <- file.path(tempdir(), "analysis")
dir.create(file.path(pkg, "R"), recursive = TRUE, showWarnings = FALSE)
# A minimal DESCRIPTION — the one file every R package must have.
writeLines(c(
"Package: analysis",
"Type: Package",
"Title: Example Analysis Programs",
"Version: 0.1.0",
"Description: Throwaway package for a pkglite demo.",
"License: MIT"
), file.path(pkg, "DESCRIPTION"))
# One analysis function under R/ — counts adverse events per subject.
writeLines(c(
"#' Count adverse events per subject",
"ae_count <- function(ae) {",
" tapply(ae$AEDECOD, ae$USUBJID, length)",
"}"
), file.path(pkg, "R", "ae_count.R"))
list.files(pkg, recursive = TRUE) # the package tree we will pack[1] "DESCRIPTION" "R/ae_count.R"
Two files: DESCRIPTION and R/ae_count.R. That is a valid package directory.
Collate: select the files
collate() walks the package and applies a file specification, returning a file collection — an object that records which files were selected and whether each is text or binary. Pass the package path as the first argument and file_ectd() — called with no arguments — as the spec:
fc <- collate(pkg, file_ectd())
fc-- File collection -------------------------------------------------------------
-- Package: analysis -----------------------------------------------------------
path_rel format
1 DESCRIPTION text
2 R/ae_count.R text
The printed collection lists each selected file and its format. Both of our files are text (an eCTD program bundle is overwhelmingly text; binaries like data/*.rda would show as binary). This is the object pack() serializes — not the directory path, the collection.
Pack: write the single text file
pack() takes the collection and writes it to the output path. This is the artifact you submit:
out_txt <- file.path(tempdir(), "pkglite.txt")
pack(fc, output = out_txt)-- Packing into pkglite file ---------------------------------------------------
-- Reading package: analysis ---------------------------------------------------
Reading "DESCRIPTION"
Reading "R/ae_count.R"
Writing to: "/tmp/RtmpX46HhB/pkglite.txt"
file.exists(out_txt) # the bundle was written[1] TRUE
file.info(out_txt)$size # its size, in bytes[1] 482
One file on disk. Now look inside it — the reason a reviewer accepts it. pkglite.txt is a plain-text wire format: a header saying not to edit it by hand, then one block per file with Package:, File:, Format: (text or binary), and a Content: section whose lines are indented two spaces. It is entirely human-readable:
writeLines(readLines(out_txt))# Generated by pkglite: do not edit by hand
# Use pkglite::unpack() to restore the packages
Package: analysis
File: DESCRIPTION
Format: text
Content:
Package: analysis
Type: Package
Title: Example Analysis Programs
Version: 0.1.0
Description: Throwaway package for a pkglite demo.
License: MIT
Package: analysis
File: R/ae_count.R
Format: text
Content:
#' Count adverse events per subject
ae_count <- function(ae) {
tapply(ae$AEDECOD, ae$USUBJID, length)
}
Read it top to bottom and you can see the whole package: the DESCRIPTION block, then the R/ae_count.R block with the function source indented under Content:. Nothing is compressed or encoded — a reviewer opens this in any text editor and reads your programs directly. That transparency is the point of a text bundle.
Verify: the ASCII submission gate
The eCTD requires the programs file to be ASCII — plain 7-bit characters, no accented letters, no smart quotes, no Greek symbols. pack() does not enforce this; it will write a µ or a naïve straight into the bundle. The explicit check is verify_ascii(), which scans the packed file and returns TRUE only when every byte is ASCII:
verify_ascii(out_txt)[1] TRUE
TRUE — our bundle is clean and submission-ready. Now see the gate do its job. If a program contains a non-ASCII character — a stray µ in a comment is the classic culprit — verify_ascii() returns FALSE and points at the offending line so you can fix it:
# A package whose source hides a non-ASCII character.
badpkg <- file.path(tempdir(), "badpkg")
dir.create(file.path(badpkg, "R"), recursive = TRUE, showWarnings = FALSE)
writeLines(c("Package: badpkg", "Type: Package", "Title: Non ASCII Demo",
"Version: 0.1.0", "Description: Demo.", "License: MIT"),
file.path(badpkg, "DESCRIPTION"))
writeLines(c("# effect size in µg (micrograms)", # a non-ASCII micro sign
"dose <- function(x) x * 1000"),
file.path(badpkg, "R", "dose.R"))
bad_txt <- file.path(tempdir(), "badpkg.txt")
pack(collate(badpkg, file_ectd()), output = bad_txt)-- Packing into pkglite file ---------------------------------------------------
-- Reading package: badpkg -----------------------------------------------------
Reading "DESCRIPTION"
Reading "R/dose.R"
Writing to: "/tmp/RtmpX46HhB/badpkg.txt"
verify_ascii(bad_txt) # FALSE — and it names the offending line19: # effect size in <c2><b5>g (micrograms)
[1] FALSE
FALSE, with the line number and the non-ASCII bytes reported. Run verify_ascii() before you ever hand off a bundle: the ASCII rule is a hard eCTD constraint, but nothing enforces it for you. (For cleaning, pkglite also ships sanitize() to scrub a collection and prune() to drop files by path.)
Unpack: restore the package
The other half of the round-trip is the reviewer’s side. unpack() reads the text file and rebuilds the package under the output directory — note the argument is output, not output_dir, and it recreates a <package-name>/ subfolder:
restore <- file.path(tempdir(), "restored")
dir.create(restore, showWarnings = FALSE)
unpack(out_txt, output = restore)-- Unpacking from pkglite file -------------------------------------------------
-- Reading file: "/tmp/RtmpX46HhB/pkglite.txt" ---------------------------------
Writing to: "/tmp/RtmpX46HhB/restored"
Writing "analysis/DESCRIPTION"
Writing "analysis/R/ae_count.R"
list.files(restore, recursive = TRUE) # the package tree, reconstructed[1] "analysis/DESCRIPTION" "analysis/R/ae_count.R"
The analysis/ package is back — analysis/DESCRIPTION and analysis/R/ae_count.R, the same tree we started from. The round-trip is content-faithful: every source line is reproduced exactly. It is not guaranteed byte-identical — unpack() appends a POSIX trailing newline, so the restored file can be a byte (a blank final line) longer than the original. Verify the round-trip by comparing the content lines, not the file bytes:
original <- readLines(file.path(pkg, "R", "ae_count.R"))
recovered <- readLines(file.path(restore, "analysis", "R", "ae_count.R"))
length(original) # the source lines[1] 4
length(recovered) # one more — unpack appends a trailing newline[1] 5
identical(original, recovered[seq_along(original)]) # TRUE — every source line matches[1] TRUE
The source has four lines and the restored file five — the extra one is the trailing newline unpack() adds — but the four source lines match exactly (TRUE). That is what content-faithful means: the code is reproduced line for line, even when the byte count differs by a newline, so verify the content rather than a file hash. (unpack() has an install = FALSE default; leaving it FALSE keeps the teaching cell reproducible — set it TRUE only when you actually want R CMD INSTALL to run on the restored package.)
Why a single ASCII text file for the eCTD
Every property of pkglite.txt maps to a submission requirement:
- Single file. The eCTD places the programs in one location in the
m5analysis folder; one text artifact is far simpler to reference, hash, and archive than a nested directory of loose scripts. - Plain text, no archive. A reviewer opens it in any editor on any platform — no unzip step, no tool, no trust decision about an executable archive. The
# Generated by pkgliteheader tells them exactly what it is and how to restore it. - ASCII. Guaranteed to survive any transport and render identically everywhere — which is why
verify_ascii()exists as a hard gate. - Reversible. Because
unpack()restores a working package, the bundle is not a read-only dump — a reviewer can reconstruct and run your analysis programs, which is the whole reproducibility promise.
That is why a submission ships programs as a pkglite.txt rather than a zip or a folder.
Where pkglite fits: the ADRG and the R Consortium pilots
pkglite is the mechanism behind the ADRG’s Submission of Programs section. That section inventories the analysis programs — the ADaM programs, the TLF programs, the macro and utility programs — and pkglite is how those programs are packaged into the single submittable text file that physically sits in the eCTD m5/.../analysis/adam/programs/ location.
The R Consortium submission pilots to the FDA demonstrated this end to end: an entirely R-based analysis package delivered in eCTD format. Their m5 programs folder documents a “proprietary R package in txt format” — the analysis programs packaged as one submission text file. That is precisely the workflow pkglite standardizes: bundle the R analysis package into a single reviewable text artifact for the eCTD. (The pilot’s README describes the text-format package; treat pkglite as the tool that produces that artifact, and read the Zhao et al. Clinical Trials paper and the pilot repository for the full end-to-end context.)
What pkglite does not do
Keep its scope tight so you reach for the right tool at each step. pkglite bundles the R package source as text — that is its entire job. It does not:
- write
.xptfiles — exporting a validated ADaM dataset to SAS Transport v5 isxportr, an earlier lesson in this series; - produce Define-XML — the machine-readable dataset metadata comes from
metacore/metatools, not pkglite; - validate metadata or check CDISC conformance — that is Pinnacle 21 / CDISC CORE and the
xportrconformance gate.
pkglite operates on programs, not data and not metadata. In the packaging sequence it is the step that turns your analysis code into a submittable artifact, sitting alongside — never replacing — the tools that package the datasets and their metadata.
Ask Prova “I have an R package of analysis programs — pack it into a single submission text file with pkglite, confirm it is ASCII, then show me how a reviewer unpacks it back to a working package.” — it answers grounded in this pillar’s lessons, with runnable code you can try on an example package. The runtime is the judge. Ask Prova →
Common issues
verify_ascii() never ran, and a non-ASCII character shipped. pack() writes non-ASCII bytes without complaint, so a stray µ, an accented name, or a smart quote in a comment slips into the bundle silently. The eCTD requires ASCII. Always run verify_ascii(bundle) before handing off — it returns TRUE/FALSE and names the offending line — and clean the source (or use sanitize()) until it is TRUE.
Passing a directory to pack(), or an argument to file_ectd(). The grammar is collate() then pack(): collate(pkg, file_ectd()) builds the file collection, and pack() consumes that collection, not a folder path. And file_ectd() takes no arguments — the package path goes to collate(), so it is collate(pkg, file_ectd()), never file_ectd(pkg).
Expecting a byte-identical round-trip. unpack() reproduces the source content exactly but appends a POSIX trailing newline, so the restored file can be a byte (a blank final line) longer than the original. Do not assert byte-identical file hashes — verify the round-trip by comparing the content lines (readLines()), which match exactly. That is what “content-faithful” means.
Frequently asked questions
Package the analysis R programs as a single ASCII text file and place it in the eCTD m5 analysis folder, inventoried by the ADRG’s Submission of Programs section. In R, the tool for producing that file is pkglite: collate() your package’s files, pack() them to a pkglite.txt, and verify_ascii() it before handoff. The R Consortium FDA pilots demonstrate a fully R-based analysis package delivered this way.
pkglite is an R package from Merck — “a tool, grammar, and standard to represent and exchange R package source code as text files.” It converts one or more R package directories into a single text file (pkglite.txt) with pack() and restores the package from it with unpack(), which makes it the standard way to bundle analysis programs for a regulatory submission. Its documentation is at merck.github.io/pkglite.
It is a plain-text wire format: a header (# Generated by pkglite: do not edit by hand), then one block per file with Package:, File:, Format: (text or binary), and a Content: section holding the file’s contents with each line indented two spaces. Nothing is compressed or encoded — a reviewer reads your programs directly in any text editor, and unpack() reconstructs the package folder from those blocks.
Call unpack(input, output = "<dir>") — the destination argument is output, not output_dir. It reads the text bundle and rebuilds the package under that directory, creating a <package-name>/ subfolder with the DESCRIPTION, R/, and other files restored. Leave install = FALSE (the default) to just recreate the files; set install = TRUE only if you also want R CMD INSTALL to run on the restored package.
No. pkglite bundles the R package source (your analysis programs) as text — that is all it does. Exporting a dataset to a SAS Transport .xpt file is xportr, and producing the Define-XML metadata is metacore/metatools. A submission uses all three: xportr for the datasets, metacore for the metadata, and pkglite for the programs — they are complementary, not interchangeable.
Test your understanding
Build a tiny package in a temporary directory (a DESCRIPTION and one function under R/), then collate it with the eCTD spec, pack it to a pkglite.txt, and confirm with a single call that the bundle is ASCII and therefore submission-ready. Which spec do you pass to collate(), and which function is the ASCII gate?
The spec for a submission bundle is file_ectd() — call it with no arguments; the package path is the first argument to collate(). Chain the grammar collate(pkg, file_ectd()) into pack(output = ...), then run verify_ascii() on the packed file. verify_ascii() returns TRUE when every byte is ASCII.
library(pkglite)
# Build a minimal package
pkg <- file.path(tempdir(), "myprog")
dir.create(file.path(pkg, "R"), recursive = TRUE, showWarnings = FALSE)
writeLines(c("Package: myprog", "Type: Package", "Title: Demo",
"Version: 0.1.0", "Description: Demo.", "License: MIT"),
file.path(pkg, "DESCRIPTION"))
writeLines(c("mean_age <- function(x) mean(x$AGE)"),
file.path(pkg, "R", "mean_age.R"))
# collate -> pack -> verify
out <- file.path(tempdir(), "myprog.txt")
collate(pkg, file_ectd()) |> pack(output = out)
verify_ascii(out) # TRUE — the bundle is ASCII, submission-readycollate(pkg, file_ectd()) builds the file collection using the eCTD specification, pack() writes it to the single text file, and verify_ascii() is the gate — TRUE means every byte is ASCII, so the bundle meets the eCTD constraint. (pkglite exports %>%; the native |> pipe works too.)
A. pack() refuses to write a file that contains non-ASCII characters, so no separate check is needed. B. collate(pkg, file_ectd()) builds a file collection, pack() writes it to one text file, and verify_ascii() must be run explicitly to confirm the bundle is ASCII; unpack() restores content-faithfully, not byte-identically. C. pkglite writes the .xpt datasets and the Define-XML along with the programs, in one bundle.
B. The grammar is collate() → pack() → unpack(), verify_ascii() is an explicit gate (pkglite does not enforce ASCII on pack()), and the round-trip is content-faithful rather than byte-identical (a trailing-newline difference is possible). A is wrong — pack() writes non-ASCII bytes happily, which is exactly why you must call verify_ascii(). C is wrong — pkglite bundles only the R package source; .xpt files come from xportr and Define-XML from metacore.
Conclusion
Shipping the analysis programs is its own last-mile step, and pkglite is the tool for it. The grammar is three verbs: collate() selects the package’s files with the file_ectd() spec, pack() serializes them into a single, human-readable pkglite.txt, and unpack() rebuilds the package on the reviewer’s side. Run verify_ascii() before every handoff — the eCTD’s ASCII rule is a hard constraint that nothing enforces automatically — and trust the round-trip as content-faithful, not byte-identical. That single text artifact is what the ADRG’s Submission of Programs section inventories and what the R Consortium pilots delivered to the FDA. Keep pkglite’s scope in mind: it bundles the programs, while xportr packages the datasets and metacore produces the metadata — three complementary tools for one submission.
This lesson is reproducible: every result on this page was produced by the code shown — copy any block and run it to reproduce them. The runtime is the judge.
References
- pkglite — CRAN — “Compact Package Representations”; the Merck package that packs an R package’s source into a single text file and restores it.
- pkglite documentation — the concept guide and full API reference (
collate,pack,unpack,file_ectd,verify_ascii,sanitize,prune). - Merck/pkglite on GitHub — the package source and issue tracker.
- Zhao et al., Electronic common technical document submission with analysis using R, Clinical Trials (2023) — the eCTD-with-R workflow that motivates pkglite.
- R Consortium submissions-pilot1-to-fda — a fully R-based analysis package delivered to the FDA in eCTD format, documenting the R package in text form.
Reuse
Citation
@online{2026,
author = {},
title = {Bundle {R} {Programs} for {Submission} with Pkglite: {The}
Pack/Unpack {Workflow}},
date = {2026-07-01},
url = {https://www.datanovia.com/learn/pharma-clinical/07-submission-packaging/bundling-programs-pkglite},
langid = {en}
}