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

Pharma & Clinical

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).

Published

July 1, 2026

Modified

July 7, 2026

TipKey takeaways
  • 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 single pkglite.txt, and unpack() 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.txt is.
  • The grammar is three verbs: collatepackunpack. collate() selects the package files into a file collection, pack() writes them to the text file, and unpack() rebuilds the package. Use file_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 returns TRUE only 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 .xpt files (that is xportr) or Define-XML (that is metacore) — 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.

A left-to-right flow schematic of the pkglite round-trip in five boxes joined by arrows. Box 1 (light blue): an R package, listing DESCRIPTION, R/, man/, src/ as a folder tree. Box 2 (mid blue): pack(), described as collate then write to one ASCII text file. Box 3 (dark navy, the centrepiece): pkglite.txt, a single submittable ASCII artifact. Box 4 (mid blue): unpack(), described as restore the package. Box 5 (brand azure): the R package again, reconstructed and content-faithful. The visual point is that pkglite compresses a package's source folder into one flat text file for submission and restores the same folder tree on the other side.

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 line
19:   # 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 m5 analysis 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 pkglite header 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 .xpt files — exporting a validated ADaM dataset to SAS Transport v5 is xportr, 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 xportr conformance 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.

🟢 With an AI agent

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-ready

collate(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.

Note

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

Reuse

Citation

BibTeX 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}
}
For attribution, please cite this work as:
“Bundle R Programs for Submission with Pkglite: The Pack/Unpack Workflow.” 2026. July 1. https://www.datanovia.com/learn/pharma-clinical/07-submission-packaging/bundling-programs-pkglite.