Skip to content

test(embed): the empty-environment acceptance matrix and the embed CI budget (#661, #662) - #717

Open
espetro wants to merge 4 commits into
work/emb659from
work/acc661
Open

espetro wants to merge 4 commits into
work/emb659from
work/acc661

Conversation

@espetro

@espetro espetro commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Closes #661
Closes #662

Stacked on #716 (base work/emb659).

Gap analysis: what #661 and #662 already had, and what was missing

Read from the issues (gh api repos/sigilco/pyscriptc/issues/661 and /662) and compared against the tree after #707 and #716.

#661, empty-environment acceptance test on macOS arm64 and Linux x86_64

Already met before this PR:

  • The locked runtime pack for both platforms: packaging/runtime.lock.toml with macos-arm64 and linux-x86_64 entries, checked by tests/test_embed.py:58 (test_lock_pins_the_two_slice_platforms) and tests/test_embed.py:66.
  • pyscriptc embed fetch with sha256 verification: src/pyscriptc/embed.py:236 (fetch), src/pyscriptc/embed.py:146 (verify_sha256), pinned by tests/test_embed.py:79 (a flipped byte is refused).
  • The relative rpath in the link line: src/pyscriptc/embed.py:370 (link_flags), pinned by tests/test_embed.py:86 and tests/test_embed.py:97.
  • The bundle layout (name/bin/name plus name/lib/): src/pyscriptc/embed.py:392 (stage_bundle), pinned by tests/test_embed.py:146.
  • A single-program empty-environment run: tests/test_embed.py:227 (test_bundle_runs_without_system_python), which ran one program with env={}.

Missing, and implemented here:

  • The three-program matrix. Before this PR there was one program (the stdlib island fractions fixture). The pure-Python dependency program (embed: bundle pure-Python dependencies into site-packages, refuse native wheels #660) and the data-file program (embed: relocated program data files and __file__ under the program-wide island #665) had no bundle-level acceptance at all: tests/test_sitedeps.py only checked the staging against a fake site-packages tree, and tests/test_island_program.py:202 checked the data-file relocation for a host-linked island, not for a bundle. Now tests/test_embed_acceptance.py:96 runs all three.
  • The relocation with the build tree and the pack cache hidden. The old test ran the bundle in place, next to the compile output.
  • stdout, stderr and exit code compared with CPython. The old test compared stdout only (tests/test_embed.py:257).
  • The loader assertions (otool -L/otool -l on macOS, readelf -d on Linux): no absolute build path, only the relative rpath. Nothing asserted this.
  • The size gates (macOS 40 MB, Linux 60 MB, uncompressed). Nothing asserted this.
  • The pack-less refusal without a traceback. pyscriptc compile --embed-python=bundle with a missing pack ended in a Python traceback; it now prints one line naming pyscriptc embed fetch (src/pyscriptc/cli.py:694).
  • The staged data tree travelling with the bundle. stage_bundle moved the binary into bin/ but left the pyscriptc-data tree beside the old binary path, so a relocated bundle died with FileNotFoundError on the data file. Fixed in src/pyscriptc/embed.py:443.

#662, CI jobs, caches and runner cost budget

Already met before this PR:

  • The two cells: Linux x86_64 on ubuntu-24.04 and macOS arm64 on macos-14, .github/workflows/embed.yml:52.
  • The PR path filter, .github/workflows/embed.yml:78 (the grep over the changed files).
  • The nightly schedule and the unconditional runs: schedule at .github/workflows/embed.yml:30 and the push/schedule/always_run cases in the gate step, .github/workflows/embed.yml:74.
  • The pack cache keyed on the lock hash: .github/workflows/embed.yml:113, with the prefix fallback at .github/workflows/embed.yml:114.
  • The budget numbers in the workflow header (4 minutes per cell target, 6 hard timeout, about 12 runner minutes per full run) and the EMBED_PR_SKIP_MACOS switch, which was already read by the gate step at .github/workflows/embed.yml:69.

Missing, and implemented here:

  • The acceptance step itself ran one program and compared stdout only (.github/workflows/embed.yml:133 before this PR). It now runs the full embed: empty-environment acceptance test on macOS arm64 and Linux x86_64 #661 matrix on both cells: three programs, relocated, with the pack cache moved away and the source fixtures renamed away, run under env -i and compared with CPython's stdout, stderr and exit code, plus the loader and size-gate assertions.
  • The required check names and the measured minutes, which issue embed: CI jobs, caches and runner cost budget #662 asks to be recorded at close: the check names are in this PR body and the measured minutes are in the issue comment below.

What changed

  • src/pyscriptc/embed.py: the staged pyscriptc-data tree moves into bin/ with the binary, so a relocated bundle finds its data files; the "fetching the runtime pack" notice is skipped when PYSC_RUNTIME_DIR already holds a pack.
  • src/pyscriptc/cli.py: embed.EmbedError and sitedeps.SiteError are caught at the CLI boundary and printed as one line without a traceback.
  • tests/test_embed_acceptance.py (new): the acceptance matrix, the loader assertions, the size gates and the pack-less refusal.
  • tests/fixtures/embed/island_dependency.py (new) and tests/fixtures/embed/datafile/ (new): the two acceptance programs. pathspec moved into the dev group in pyproject.toml so the compiler interpreter can stage it.
  • .github/workflows/embed.yml: the acceptance step runs the full matrix on both cells; the header documents the nightly-versus-per-PR split and the runner-minute budget.
  • tests/test_census_self.py: the self-host fence pin moves with the code (3204 to 3177).

No existing check was weakened.

Tests

Real output of the targeted runs on this machine (macOS arm64, uv run pytest -n 2 on the targeted files only, not the full suite):

$ uv run pytest -n 2 tests/test_embed_acceptance.py -v
[gw1] [ 25%] PASSED tests/test_embed_acceptance.py::test_the_bundle_stays_inside_the_size_gate
[gw1] [ 50%] PASSED tests/test_embed_acceptance.py::test_a_missing_pack_is_refused_without_a_traceback
[gw0] [ 75%] PASSED tests/test_embed_acceptance.py::test_acceptance_matrix_runs_in_an_empty_environment
[gw0] [100%] PASSED tests/test_embed_acceptance.py::test_the_bundle_carries_only_a_relative_rpath
4 passed in 40.76s

$ uv run pytest -n 2 tests/test_embed.py tests/test_embed_acceptance.py tests/test_sitedeps.py \
    tests/test_stdlibzip.py tests/test_island_program.py tests/test_census_self.py \
    tests/test_cli_dx.py tests/test_config.py
110 passed, 0 failed, 1 skipped in 106.78s

$ uv run pytest -n 2 tests/test_compat_json.py tests/test_support_docs.py tests/test_what_will_fail.py
39 passed in 78.67s

$ uv run ruff check src/ tests/ scripts/
Ruff: No issues found
$ uv run ruff format --check
821 files already formatted

The CI acceptance step was also run verbatim on this machine (macOS arm64), with the matrix expressions resolved to macos-14:

stdlib island fractions: rc=0 stdout=[11/12] (matches CPython)
bundled pure dependency: rc=0 stdout=[1] (matches CPython)
program data file: rc=0 stdout=[hello-data datafile] (matches CPython)
bundle size: 36372 KB (limit 40 MB)
EXIT=0

The bundle size measured locally is 36372 KB (36.4 MB), under the 40 MB macOS gate.

Required checks

  • embed acceptance (ubuntu-24.04)
  • embed acceptance (macos-14)

Both are path-filtered on pull requests and run unconditionally on the nightly schedule, on pushes to master and in the release rehearsal (workflow_dispatch with always_run=true).

Not done here

Two defects the empty-environment acceptance matrix of issue #661 found on
macOS arm64.

The staged program data tree (#665) sits beside the binary the compile
produced, and `stage_bundle` moves that binary into `bin/`. The island
bootstrap resolves the tree from the running image (the binary's own
directory), so a relocated bundle looked for `bin/pyscriptc-data/<program>`
and found nothing: the run died with `FileNotFoundError` on the data file.
The tree now travels into `bin/` with the binary.

A bundle compile that cannot get its pack (or that carries a native
dependency) ended in a Python traceback. It is a user-facing refusal, so
`embed.EmbedError` and `sitedeps.SiteError` are caught at the CLI boundary
and printed as one line, naming `pyscriptc embed fetch` for the missing
pack. The "fetching the runtime pack" notice is skipped when
`PYSC_RUNTIME_DIR` points at an unpacked pack, where no fetch happens.

The census fence pin moves with the code (3204 to 3177).
Three programs compiled into bundles, relocated to a fresh directory, run
under `env -i` (no PATH, no HOME, no PYTHONHOME) and compared with CPython
byte for byte (stdout, stderr and exit code):

1. the stdlib-only island fractions program,
2. a program importing a bundled pure-Python dependency (`pathspec`,
   issue #660, added to the dev group so the interpreter can stage it),
3. a program reading a staged data file (issue #665).

Loader assertions: the relative rpath (`@executable_path/../lib` on macOS,
`$ORIGIN/../lib` on Linux) is present and no absolute build path survives
in the image. Size gates: macOS 40 MB, Linux 60 MB, uncompressed. A
pack-less compile refuses with one line naming `pyscriptc embed fetch`,
no traceback.

The module skips unless the pinned pack is unpacked (cache or
`PYSC_RUNTIME_DIR`); the CI cell in `.github/workflows/embed.yml` runs the
same matrix on Linux x86_64 and macOS arm64.
The embed job's acceptance step now runs the issue #661 matrix on both
cells (Linux x86_64 on ubuntu-24.04, macOS arm64 on macos-14): three
programs (stdlib-only island, bundled pure dependency, staged data file)
compiled into bundles, relocated, with the pack cache moved away and the
source fixtures renamed away, run under `env -i` and compared with CPython's
stdout, stderr and exit code; loader and size-gate assertions as in the
test module.

Issue #662 acceptance, unchanged in kind, now spelled out:

- path filter: pull requests run only when the embedding surface changed
  (the gate step's grep, extended with `packaging/third-party.toml`,
  `scripts/gen_third_party_notices` and the workflow files);
- nightly versus per-PR split: schedule, push to master and the release
  rehearsal (`workflow_dispatch` with always_run) run unconditionally;
- pack cache keyed on the lock hash with a prefix fallback;
- runner-minute budget documented in the header: 4 minutes per cell target,
  6 hard timeout, about 12 runner minutes per full run, with the
  `EMBED_PR_SKIP_MACOS` repo variable flipping the macOS PR cell to nightly
  and rehearsal only. Measured minutes get recorded in issue #662 at close.

A missing pack keeps the clean-skip behavior for the pre-#657 world and
fails the step clearly once the lock exists.
@espetro espetro added this to the v0.7.0 milestone Oct 26, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Deploying pyscriptc with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9429d9f
Status:🚫  Build failed.

View logs

@espetro

espetro commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Measured runner minutes for the embed jobs, first CI run of PR #717 (branch work/acc661, 2026-10-26):

cell runner wall time (job) acceptance step
embed acceptance (ubuntu-24.04) ubuntu-24.04 42 s about 18 s (from step timestamps)
embed acceptance (macos-14) macos-14 44 s about 15 s

So the full two-cell run costs under 2 runner minutes of Linux-equivalent time, well inside the documented budget (4 minutes per cell target, 6 hard timeout, about 12 minutes per full run ceiling). The macOS PR cell stays on pull requests; EMBED_PR_SKIP_MACOS is not needed. Local measurement on the author machine agreed (about 8 minutes for the whole matrix including three bundle compiles; the CI cache and the faster runners cut it to under a minute).

Acceptance run results (both cells green, same output):

stdlib island fractions: rc=0 stdout=[11/12] (matches CPython)
bundled pure dependency: rc=0 stdout=[1] (matches CPython)
program data file: rc=0 stdout=[hello-data datafile] (matches CPython)
bundle size (Linux): 50396 KB (limit 60 MB)
bundle size (macOS): 36424 KB (limit 40 MB)

Linux x86_64 bundle: 50.4 MB uncompressed, inside the 60 MB gate. macOS arm64: 36.4 MB, inside the 40 MB gate.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant