Short, dated lessons: facts we discovered, surprises, gotchas. Newest first. Each entry links to its evidence. If a lesson changes a decision, also write an ADR (see ADR-0001).
Format: - **YYYY-MM-DD** · <area> · <lesson>. Evidence: <link>.
-
2026-10-22 · review · Using the released tool as a user would found main-path bugs that eleven code-reading reviews missed, and none of them showed in CI:
uv python installlocks$UV_PYTHON_INSTALL_DIR/.lockitself, so a caller holding that file deadlocks uv until its 300 s timeout. CI never downloads a Python.- A lock file held shared by every running copy can't also be the unpack lock: one running copy blocks every re-unpack.
- A script lock without
requires-pythonis stamped with uv's default Python. The bundle's range must respect the lock's top-levelrequires-python, or marker-gated backports (tomli,exceptiongroup) go missing. That stamp also turns "stale" when a newer Python is installed, unlessuv export --python <the lock's version>is passed. - Apple's
/usr/bin/python3keeps itspycache_prefixunder-s -S, so compileall writes nothing into the payload (-X pycache_prefix=turns it off). uv pip install --python-version 3.9means 3.9.0 for markers: a lock forked atpython_full_version <= '3.9'installs the other side for a real 3.9.6. Pass the full version.- An atexit handler that imports anything loses Ctrl-C's exit status on Python 3.9-3.12 (exit 1, not 130): import beforehand.
- A payload built for another Python than the build's own skips compiling and analysis, so anything it claims needs its own check; claims nothing ran are where bundles break.
Evidence: twelfth review.
-
2026-10-21 · platforms · Testing our multi-Python skill bundling on Windows (
x86_64-pc-windows-msvc) and Intel macOS (x86_64-apple-darwin) uncovered two more real-world wheel packaging patterns: (1)pywin32on Windows ships 59 untagged.pyd/.dll/.exefiles (win32/win32api.pyd,pythonwin/win32ui.pyd,win32/pythonservice.exe) at identical relative paths across itscp310–cp314wheels instead of using.cp3XX-win_amd64.pydfilenames, andPillowonx86_64-apple-darwinshipsPIL/.dylibs/*.dylibbuilt with differentMACOSX_DEPLOYMENT_TARGETheaders (10_10for 3.10–3.11,10_13for 3.12–3.13,10_15for 3.14)—whenmerge_native_wheels()stops on such a file conflict (merge_conflict=True),_lock_fork_pythons()must layer the remaining Python versions or groups sowrite_layers()stores shared files once and only separates the conflicting binaries per Python version; (2)uv python find --no-projectstill returns an active$VIRTUAL_ENVunless--systemis also passed; and (3) on Windows,Path.write_text()withoutnewline="\n"writes CRLF into modifiedWHEEL/RECORDfiles andos.path.realpath()preserves drive/path case variation unless wrapped inos.path.normcase(). Evidence: ADR-0052,.github/workflows/ci.yml(skillandskill-run),tests/test_config.py. -
2026-10-20 · ecosystem · Five Python packaging gotchas surfaced when bundling a real skill (
playwright): (1)uv init --barestamps the build host's Python (requires-python = ">=3.13") intopyproject.toml, silently excluding Python 3.10–3.12 on virtual dependency-only projects; (2) wheels likeplaywright(py3-none-<platform>) andcryptography(cp3X-abi3-<platform>) work across Python versions as-is, whilecp3XXwheels (greenlet,lxml,Pillow,numpy) can stack.cpython-310through.cpython-314extensions in one payload if build metadata files (numpy/__config__.py,libnpymath.a) are ignored when checking non-binary diffs; (3) universal locks fork pure-Python dependencies across Python versions (pyee 13.0.1on<3.12vs14.0.0on>=3.12inplaywright), requiring automatic fork layering (_lock_fork_pythons) to cover the full range with zero flags; and (4) macOS users who install Python viauv python installoften keep only/usr/bin/python3(3.9) onPATH, so runtime re-exec must fall back touv python find --no-project --no-python-downloads. Evidence: ADR-0052,tests/test_config.py::test_multi_python_abi3_py3_none_wheel_ranges_and_python_specifiers. -
2026-10-15 · design · Studying six winning JS bundlers (
esbuild,rollup,parcel,@vercel/ncc,tsup,rolldown) showed whybundleup's two-zip extract-to-cache architecture avoidsncc's fragile AST__dirname/.noderelocation, while highlighting six high-leverage features to adopt:--external PKG(uv export --prune+BUNDLEUP_INHERIT_PATH=1at runtime so host-provided packages likeboto3on Lambda ortorchin sandboxes can be left out cleanly), comma-separated list flags (--python-platform linux,windows), muscle-memory flag hints (esbuildstyle), source code frames (line | code+^) on diagnostics,Requires-Distdependency chains (required_by) and percentage size shares incheck -v, and lock-keyed caching ofuv export. Evidence: JS bundlers synthesis, ADR-0051. -
2026-10-13 · review · Three consecutive adversarial review passes after 0.1.0 caught subtle cross-cutting edge cases:
compileall -fre-compiling every cached.pycon warm builds (skipping already-cached.pycfiles cuts warm compile time by ~50 ms and avoids spawning Python at all when only non-code or already-cached files are present);python -Ienabling-sand-S, which hid site-installed dependencies insidesetuptoolsbuild backends onpylock.tomlsdist builds (use-s -Sonly for bytecode/syntax subprocesses); macOS/var→/private/varsymlinks causingabspathvsrealpathmismatches between_loader,_runtime,_sitecustomize, and_verify; and PEP 263 encoding cookies (tokenize.open), UTF-8 BOMs (utf-8-sig), and non-canonical project names (My_Appvsmy-app) acrosscheckandverify. Evidence: ninth, tenth, eleventh, UX & speed findings. -
2026-10-13 · build · uv keys cached non-editable wheels of local directories on
pyproject.tomlalone, so a secondbundleup buildafter editingsrc/re-bundled the first build's wheel unless--refresh-packageis passed for everydirectorypackage in the lock; tests that build each fixture only once in a fresh temp folder can't catch stale-cache bugs. Also:argparse'snargs="?"treats--smoke "--version"as an option ofbuildunless joined with=, andzipfile.ZipFileon a--format exebinary sees only the.pyzat the tail, not the launcher or Python archive ahead of it. Evidence:tests/test_bundle.py,tests/test_smoke.py,tests/test_exe.py. -
2026-10-10 · process · Changing a fallback breaks whoever relied on it: making
BUNDLEUP_CACHEthe only cache broke the gauntlet's noexec condition,--smokeon noexec hosts andbundleup cache's root filter, and the Mac-only local run couldn't see the first (ST_NOEXECis Linux). And compiling a whole payload to save milliseconds per start costs seconds where every start is cold: compile what was imported, at exit. Evidence: eighth review. -
2026-10-10 · review · When one output wraps another (an executable around a
.pyz), every report has to describe the outer one: the seventh review found the executable's target, smoke test and size check all describing the.pyzinside. And a "first place to look" cache setting isn't an override: a copy elsewhere won, which also hid a crash from this session's own manual test (now ADR-0048). Evidence: seventh review. -
2026-10-09 · executables · python-build-standalone ships no stdlib bytecode for macOS and Linux (3 of 1,090 modules), so an executable built on it compiles the stdlib on every run under
PYTHONDONTWRITEBYTECODE(181 ms warm instead of 62): compile it at build time. Unpacking the Linux archive on a Mac fails (terminfo names differ only in case); copy it entry by entry. For reproducible bytecode, strip the build folder from recorded paths (compileall -s) and keep gzip's header free of time and name. And a scie can't be signed on macOS at all (appended data). Evidence: findings. -
2026-10-09 · python · Python reads and tokenizes a whole script before running it, so data carried in a
.pycosts time on every start: about 1.7 ms per MB as#comment lines, twice that in a string literal (M-series Mac, 3.12; 1, 10 and 40 MB of base64). A main script's bytecode is never cached either, so an 800-line loader adds ~3 ms. And a.pycopied with Windows line endings breaks any marker that includes\n: match markers without line ends. Evidence: ADR-0046, tests/test_py_format.py. -
2026-10-09 · packaging · A zip can be cut between files into several ordinary zips that unpack in turn into one folder; the compressed sizes are known after one write, so the cut needs no second guess (
--split). Hash each part before unpacking: the cache folder is named after the build, so a part from another build would otherwise be unpacked under the wrong name. Evidence: ADR-0045. -
2026-10-09 · ecosystem · Facts from rounds 6 and 7 worth keeping (checked by the research against primary sources unless noted): pip 26.0 (Jan 2026) installs a PEP 723 script's dependencies (
--requirements-from-script) and 26.1 (Apr 2026) installspylock.toml(experimental); uv's macOS and Windows executables are code-signed since Sept 2026; uv keeps uv.lock because pylock "is not sufficient to replace" it (uv #12584), so pylock is an exchange format; PEP 711 (PyBI) is still Draft and python-build-standalone fills its role; PEP 817/825 (wheel variants for GPU builds) is in progress; Terminal-Bench 2.1 found that pinned Docker images still broke because tasks fetched things from the internet (nine tasks; scores moved up to 12 points), the strongest argument for offline artifacts; agents set up only 6.69% of Python repositories on EnvBench (2025 models); 97% of llms.txt files are never requested (Ahrefs, June 2026). Evidence: round 6, round 7, 7b. -
2026-10-09 · research · Rounds 6 and 7 (four Compass runs, no attachments) converge: Python's empty seat is "project → artifact that runs elsewhere" (uv #5802 is still a "wish"), offline portable environments and "will it run there?" checks are what an independent tool can own, and uv shipping an executable builder is the main risk. Agent code fails elsewhere through its environment (68.3% ran from a clean environment; 13.5× more runtime than declared dependencies, arXiv 2512.22387). Evidence: synthesis.
-
2026-10-08 · process · A claim in a findings write-up ("no false warnings across the gauntlet") came from a scan that grepped the text output for the diagnostic's code, which only
--jsonshows, so it couldn't have found anything. Check that a scan can find a known case before reporting that it found none. Evidence: fifth review. -
2026-10-08 · process · The fourth review found the founding case still failed on a typical Mac (Homebrew's newer
python3first on PATH, Apple's 3.9 only aspython3), that undeclared imports were the most common run-time failure yet unchecked, and that "work on main" plus CI's cancel-in-progress left commits with no completed run. Evidence: fourth review. -
2026-10-08 · tooling ·
uv pip compile --universal --format pylock.tomlresolves a requirements.txt, pyproject.toml or setup.py for every platform at once, with markers and hashes, the same shape asuv export's pylock. With no dependencies it writespackages = [], so appending a[[packages]]table is invalid TOML. Reading a setup.py project's metadata (compile) or building it (install) writesbuild/and*.egg-infointo the project, even with a lock. Evidence:tests/test_inputs.py, ADR-0041. -
2026-10-08 · runtime ·
python -(a program on stdin) hassys.argv == ["-"], piped input with no arguments[""], like a prompt. ADR-0037's child check treated neither as the bundle's code, so click'stest_light_imports(which runspython -) couldn't import click in a bundle; the nightly suites caught it on every OS, but kept only pytest's last 15 lines, so the failing test's name took a re-run with full failure lists. Evidence:tests/test_bundle.py::test_a_child_reading_its_program_from_stdin_sees_the_bundle. -
2026-10-07 · security · bundleup exported the lock as bare
name==versionlines and letuv pip installfind them on the default index: a package locked from a private index wasn't found, a same-named one on PyPI would have been bundled (dependency confusion), and hashes were dropped. Installing from uv'spylock.tomlexport uses the locked URLs and checks hashes. Two uv quirks: uv 0.12.23 writes local paths relative to the export file as spelled but reads them from its real path (macOS's temp folder is behind/var -> /private/var), and joins absolute paths onto the file's folder; uv 0.12.7 writes them relative to the workspace root. uv never readspip.conf. Evidence:tests/test_index.py,src/bundleup/_uv.pyrelocate_paths. -
2026-10-07 · build · A multi-platform
.pyzof the founding example (python-pptx, lxml, Pillow) for macOS, Linux and Windows on Python 3.11 and 3.12 is 84 MiB and builds in 7.7 s from warm caches; each payload repeats the pure-Python packages.BUNDLEUP_CACHEis only the first place a bundle looks: a copy already unpacked in the user cache is used from there. Evidence: ADR-0038. -
2026-10-07 · runtime ·
sys.argvis already set whensitecustomizeruns (3.9-3.13), so a child can tell-c,-m(module name fromsys.orig_argvon 3.10+) and a script path apart.ast.parse(feature_version=)misses tokenizer changes (PEP 701 f-strings parse withfeature_version=(3, 10)); only a real interpreter is exact.uv python installinto a privateUV_PYTHON_INSTALL_DIRwith--no-bin --no-registryleaves the user's Pythons alone. Darwin 25 is macOS 26. Evidence: third review. -
2026-10-07 · process ·
git checkout -- .discards every uncommitted change in the tree, with no undo (a stash only helps if made first). An agent ran it while tidying and lost an hour of fixes, redone from its own record. Commit to a working branch as soon as a fix passes, and merge tomainonly when CI is green (the second review also found red commits onmain). Evidence: second review. -
2026-10-07 · security · A cache directory next to a bundle must get the same ownership check as a shared temp directory: a bundle in
/tmpor on a team drive otherwise runs a copy another user planted beside it. Internal environment variables need their own prefix: the runtime'sBUNDLEUP_PYTHONdoubled as--python's default and broke bundleup inside bundles. Evidence: second review. -
2026-10-07 · benchmarks · Against pex's fastest path (
--venv-repositoryfrom a uv-synced venv,--venv prepend), bundleup's build lead shrinks to 1.1-2.4× (uv does most of the work for both); its start-up lead stays (warm = venv, first run 2.3-15× faster). pex can't subset uv'spylock.toml(no dependency metadata) and refuses--pythonwith some--venv-repositoryvenvs. Evidence: findings. -
2026-10-07 · platforms · uv's platform names map to wheel levels (
uv help pip install):x86_64-unknown-linux-gnuismanylinux_2_28, macOS targets 13.0 unlessMACOSX_DEPLOYMENT_TARGETsays otherwise; uv offers manylinux 2_17, 2_28 and 2_31-2_40 only.packaging.tags.cpython_tags/compatible_tags/mac_platformsgive the exact tag set for a target, so wheel coverage can be decided from a pylock's wheel filenames. Evidence: ADR-0031. -
2026-10-07 · runtime · uv's universal lock forks per Python version (gauntlet 03 gets other package versions on 3.9; Django 4.2 vs 5 in gauntlet 21), so "does a pure bundle run on 3.X?" is answered by evaluating the pylock markers for 3.X and comparing the selection, plus every package's
Requires-Python. A bundle's unchecked-hash.pycfiles for one version sit next to ones other versions compile on first import; nothing conflicts. Evidence: ADR-0030. -
2026-10-07 · runtime · A
sitecustomize.pyreached throughPYTHONPATHruns in every Python a program starts, so it can decide per interpreter: comparesys.prefix, version and ABI flags with the parent's and only then activate the bundle, then import thesitecustomizeit shadows (importlib.machinery.PathFinder.find_specon the rest ofsys.path). The oldPYTHONPATH-to-the-payload approach made unrelated Pythons import the bundle's packages. Evidence: ADR-0027. -
2026-10-07 · ecosystem · Some wheels built on Windows write
RECORDpaths with backslashes (ormsgpack 1.12.2); pip and uv install them, so a strict checker must normalise\\to/. Found by the first scheduled smoke run (langchain on Windows).uv exporton a project withoutuv.lockwrites one;uv export --scriptnever writes a script lock. Pillow 12 publishes no wheels belowmanylinux_2_27, so amanylinux_2_17target can't use it. Evidence: review findings. -
2026-10-07 · process · An independent, adversarial review found real bugs the gauntlet and CI missed (a leak into other Pythons, a broken preset, a silently written lockfile) and docs that oversold the code. Worth repeating before releases; the prompt is in the findings. Evidence: review findings.
-
2026-10-06 · testing · A real test suite can run from a bundle: make a throwaway project depending on the package, pytest and the test deps, bundle it with
--entry pytest:console_main, and run it from a clone with asrc/layout (so the clone can't shadow the package) and--import-mode=importlib. Read pytest's summary line by pattern: other output can follow it. Lambda's runtime interface emulator image runs locally in Docker, so Lambda zips can be tested in CI; there apython -cchild finds the function's packages via the working directory (/var/task). Evidence: findings. -
2026-10-06 · ecosystem · uv 0.12 installs a
pylock.tomldirectly, without a preview flag, but refuses it alongside other requirements ("Cannot specify additional requirements alongside apylock.tomlfile") and rejects one with nopackagesarray.pip lock's output lists the project itself as a directory entry, and bundles fine. Evidence: ADR-0026. -
2026-10-06 · platforms · AWS Lambda runs Python 3.10/3.11 on Amazon Linux 2 (glibc 2.26) and 3.12+ on Amazon Linux 2023 (glibc 2.34); both CPUs everywhere; 3.15 in preview. uv's
--python-platformhas nomanylinux_2_26, so AL2 targets usemanylinux_2_17. Anthropic's code execution sandbox is Python 3.11 on Linux x86_64 with no network; its glibc isn't documented. Evidence: ADR-0025. -
2026-10-05 · build ·
compileallwithquiet=2skips files that don't compile without a word, so a bundle built for 3.9 from code with amatchstatement built fine and crashed on import. The build now treats "a.pywith no.pycafter compiling" as a candidate and asks the target interpreter why (bundleup check, ADR-0024). Across gauntlet 03, 10, 13-15 and 20-23 on 3.9 and 3.12 (torch, sympy, Django, boto3) no dependency file failed, so the check is quiet in practice. Evidence: ADR-0024. -
2026-10-05 · runtime · Python runs
.pthfiles only in site directories, never inPYTHONPATHorsys.path.insertentries, so a bundler must process them itself. Without that, setuptools' distutils shim and pywin32's directories are missing. pex, shiv and a plain zipapp all fail; some old namespace-package.pthlines readsitedirfrom their caller's frame, so the loader's function keeps that name. Evidence: gauntlet 23-pth-files, ADR-0023. -
2026-10-05 · runtime · Some wheels are mostly a program: ruff's (and uv's, ninja's, cmake's) binary is a wheel script, installed to
bin/(Scripts/on Windows), and the Python wrapper finds it next to the packages when installed with--target. bundleup dropped all ofbin/as console-script launchers, sofind_ruff_bin()failed; now only each distribution's own entry-point launchers are dropped. pex and zipapps fail the same way; shiv passes. Evidence: gauntlet 22-wheel-executables. -
2026-10-05 · tooling · This repo's Actions token can't open pull requests: the repo setting "Allow GitHub Actions to create and approve pull requests" is off (
gh api repos/funkyfunc/bundleup/actions/permissions/workflowshowscan_approve_pull_request_reviews: false). The weekly summary job pushes a branch and falls back to an issue with a "open a pull request" link. Evidence: weekly.yml. -
2026-10-05 · runtime · A PEP 420 namespace package (directories without
__init__.py) loses to a regular package of the same name anywhere later onsys.path: Python keeps scanning after a namespace portion and takes the first regular package. So withBUNDLEUP_INHERIT_PATH=1a stalepip install --usercopy can shadow a bundle's namespace package despite the bundle coming first; the default isolation (ADR-0021) prevents it. Found by theuser-site-conflictcondition on gauntlet 08 in CI. Evidence: gauntlet/run_bundlers.py. -
2026-10-05 · testing · A test that picked "the first directory entry" in the cache broke once the unpack lock added
.lock-*files beside the unpacked copies (and passed on macOS by directory-order luck). Select cache entries by their name pattern. Evidence:tests/test_verify_command.py. -
2026-10-05 · runtime · Without a lock, 16 simultaneous first runs of the NumPy bundle each unpacked a full copy (3.2 s wall, 28 s CPU; correct thanks to the atomic rename, but wasteful). A best-effort lock file in the cache root (flock / msvcrt, released by the OS if the holder dies, 120 s timeout) makes one process unpack while the rest wait: 0.55 s wall, 1.1 s CPU. Evidence:
src/bundleup/_loader.py_lock(); measured with two bundles built from consecutive commits. -
2026-10-05 · build ·
zipfile.writestr(ZipInfo, ...)ignores the archive'scompresslevel, so "level 1" payloads were really level 6.pathlib.Path.relative_to()costs ~60 µs a call (over a second for 18,000 files); useos.walkstrings in hot loops. Set literals make.pycbytes depend on the hash seed: compile withPYTHONHASHSEED=0for reproducible builds. Evidence: findings. -
2026-10-05 · cross-target ·
uv pip install --target --python-platform Xinstalls another platform's wheels from any machine, and builds pure-Python sdists fine. A compiled sdist is built for this machine: uv itself rejects the result ("not compatible with the target"), and bundleup also checks every wheel tag against the target. Bytecode only depends on the Python version, so a local interpreter of the target version compiles it. Evidence:_platforms.py,tests/test_platforms.py. -
2026-10-05 · testing · A corpus oracle that compares raw output flags programs that print their install path (
--version"from …/site-packages"), list things in set order (twine), or differ only in traceback frames (the loader's vs a console-script launcher's). The first corpus run filed six such false positives (#1-#6); normalise those, and pinPYTHONHASHSEED. Evidence: gauntlet/corpus.py. -
2026-10-05 · testing · The 100 most-downloaded PyPI packages (incl. pandas, scipy, pyarrow, grpcio, cryptography) bundle and match a normal install on Linux x64/arm64, macOS and Windows with no failures. Breadth beyond that and real program runs are the next risk. Evidence: findings.
-
2026-10-05 · testing · argparse's error wording changes even between patch releases (
(choose from 'build')vs(choose from build)across 3.12.x), so CLI snapshots must only contain bundleup's own words. A corruption test that flipped a byte at a fixed offset landed in the loader, not the payload, and showedverifywasn't checking the loader: target bytes by zip offsets, and hash everything that runs. Evidence: tests/test_verify_command.py. -
2026-10-05 · cli ·
uv export --lockedrefuses a PEP 723 script that has no lockfile, so "CI implies--locked" must only apply when a lockfile exists. argparse's help layout and error wording change between Python versions (3.13:-o, --output FILE), so help snapshots are pinned to the dev Python (.python-version). An-X importtimetest caughtdifflib(a top-level import) on the--versionpath. Evidence: tests/test_cli.py. -
2026-10-05 · ecosystem · Runtime hooks inside bundles are normal: shiv bundles read 10
SHIV_*variables and pex ~30PEX_*(incl.PEX_TOOLS); neither has a verify mode. bundleup verifies with a command (bundleup verify) instead, keeping the loader minimal. Evidence: the baseline's built bundles (_bootstrap/environment.py,.bootstrap/pex/variables.py). -
2026-10-05 · runtime · A wheel's data files (the
.data/data/scheme, e.g. sympy'sshare/man/man1/isympy.1) go to<venv>/share/...in a venv but to the payload root with--target(uv and pip alike), whereshare/even imports as a namespace package. Harmless for man pages; an app that looks for its data undersys.prefixwon't find it in a bundle. Found by thematches-venvcondition on gauntlet 21; candidate for its own gauntlet project if a real package depends on it. Evidence: gauntlet/snapshot.py. -
2026-10-05 · platforms · First runs on CI: every gauntlet project passed on Linux x64 and arm64 (3.9/3.11/3.12) and Windows x64 (3.11/3.12) with no code changes, including the hostile conditions each platform supports. Windows can't block network for a process (recorded per result) and ignores read-only on directories (those conditions are skipped there). Evidence: CI artifacts of the first green run; gauntlet README "CI".
-
2026-10-05 · tooling ·
astral-sh/setup-uvpublishes exact version tags only (v10.2.0), not a floatingv10;actions/checkoutandactions/upload-artifactdo havev7. Evidence:.github/workflows/ci.yml. -
2026-10-05 · tooling ·
uv export --format pylock.tomlgives each locked package's name, version, marker and wheel hashes, which is what a lock-vs-bundle check needs. Butuv pip install -r pylock.tomlis a preview feature and resolves local directories relative to the file, so installs keep using the requirements format. Evidence:src/bundleup/build.pyexport(). -
2026-10-05 · testing · Inside a project,
uv python find 3.12returns the project's own.venveven with--managed-python --no-project; only running it from outside the repo (and withoutVIRTUAL_ENV) finds a plain interpreter. Since milestone 1 the harness had run "3.12" bundles on the repo venv, whosepackaging/pygmentscould have masked a package missing from a bundle. Fixed (the harness now refuses venvs); a clean re-run passed all 21 projects and every hostile condition, so no result changed. Evidence:gauntlet/run_bundlers.pypython_path(). -
2026-10-05 · process · Other sessions edit this repo concurrently: a
git add -Acommit swept in someone else's in-progress ADR and roadmap edits (undone before pushing). Stage explicit paths, and re-read docs from disk before relying on them. Evidence: CLAUDE.md "Working rules". -
2026-10-04 · runtime · Importing
typingcosts 6.7 ms on macOS's Python 3.9 and 3.9 ms on 3.12, more than the loader's whole margin over a venv. pyright honours a module-levelTYPE_CHECKING = False(no import), so the loader is fully typed for free with quoted annotations and# type:comments. Evidence:python -X importtime -c "import typing";src/bundleup/_loader.py. -
2026-10-04 · tooling · Ruff doesn't read
# type:comments, so names used only there look unused (F401) and need an explainednoqa. Variable annotations (x: int = 1) are Python 3.6+ syntax even in a function body.ast.parse(src, feature_version=(3, 5))checks a file against an older grammar. Evidence:tests/test_bundle.py::test_loader_parses_on_any_python_3. -
2026-10-04 · tooling · pre-commit hooks without
stagesrun at every installed stage, so commit-time checks also re-run on push unless pinned tostages: [pre-commit]. Ruff skips excluded paths passed explicitly by a hook only withforce-exclude = true. Evidence:.pre-commit-config.yaml. -
2026-10-04 · tooling · The
pyrightPyPI wrapper ships the matching pyright release in the wheel (souv.lockpins it), but needs Node.js: it usesnodefromPATH, otherwise downloads one with nodeenv at run time. Thenodejsextra brings Node in as a wheel instead, which is why the dev group usespyright[nodejs]. Evidence: the package's README (PyPIpyright1.1.414). -
2026-10-04 · testing · The gauntlet proves bundles run and behave; nothing yet proves a bundle contains exactly the locked files. Planned: lock-vs-bundle check, wheel
RECORDhash verification, differential test against auv syncvenv, a top-PyPI import smoke test. Evidence: roadmap "Next up" item 4. -
2026-10-04 · research · Deep-research tools don't always receive attachments (round 5's Compass report didn't); check the report's own "assumptions" section before trusting project-specific details. Evidence: round 5 synthesis.
-
2026-10-04 · platforms · AWS Lambda: only
/tmpis writable, the handler ismodule.function, and Lambda already unzips the package, so a.pyzwould re-extract on every cold start. A native Lambda zip is the right output. Limits: 50 MB zipped / 250 MB unzipped incl. layers. Evidence: verification notes. -
2026-10-04 · platforms · Host applications (Splunk, Blender, QGIS) want a vendored directory or wheels built for their Python and platform, not a zipapp. Evidence: round 4 synthesis.
-
2026-10-04 · platforms · Some platforms now build from
uv.lockthemselves (Vercel; Cloud Run from Python 3.14), so bundleup adds little there. Evidence: round 4 synthesis. -
2026-10-04 · design · A
.pyzcarrying a PEP 723 header would run different dependency sets underpython(locked wheels) anduv run(fresh resolve). Never make that the default. Evidence: round 4 synthesis. -
2026-10-04 · agents · Claude API Skills have no network and no runtime installs (Python 3.11, Linux x86_64), but the Agent Skills guide recommends PEP 723 +
uv run, which needs network. That contradiction is bundleup's clearest agent use case. Evidence: round 3 synthesis. -
2026-10-04 · agents · MCPB (MCP Bundles) gave up vendoring Python: compiled deps like pydantic can't be bundled portably, so it added a host-side
uvserver type. A.pyzhas the same limit unless the target platform is known in advance. Evidence: verification notes. -
2026-10-04 · ecosystem · uv has turned bundling requests away: #13503 closed as not planned, #5802 labelled wish; #12035 (Lambda) was closed by its own author, not implemented. Evidence: verification notes.
-
2026-10-04 · research · Re-running round 2 without round 1 attached reached the same conclusions, so they weren't an artifact of anchoring. Evidence: round 3/2b synthesis.
-
2026-10-04 · ecosystem · Astral on bundling (uv#7419, 2024-10-08): "definitely something we're interested in doing someday. I'm not sure zipapp is the ideal format for us." Still open, labelled
wish. Evidence: verification notes. -
2026-10-04 · environment · macOS's
/usr/bin/python3is a stub until the Xcode Command Line Tools are installed; with them it's Python 3.9.6. "Use whatever python3 exists" is solid for developers, servers, CI, Lambda and agents, weak for non-technical end users. Evidence: round 1b synthesis. -
2026-10-04 · positioning · Lead with what bundleup literally does (self-contained Python files); use cases are examples, never the definition, so nobody concludes "not for me". Evidence: ADR-0012.
-
2026-10-04 · research · Check which brief is pasted before running a deep-research job: the "round 3" run re-used the round 1 brief. Evidence: round 1b synthesis.
-
2026-10-04 · build · On a large pure-Python project (21: 44 MB of source) bundleup's build is no faster than pex (5.2 vs 5.5 s): zip 3.1 s and bytecode compile 1.3 s dominate; uv's install is 0.2 s. zlib releases the GIL, so threads cut compression 2–5×; compiling must be done by the target Python, so the fix there is caching per wheel, not another language. Evidence: large project.
-
2026-10-04 · analyzer · Just
ast.parse+ walking every node takes 5.2 s for 21 and 10.5 s for PyTorch's 98 MB of source in one process (0.8 s / 1.6 s on 8 processes): a pure-Python scanner sits at ADR-0008's Rust trigger before doing any analysis. Evidence: large project. -
2026-10-04 · ecosystem · Django finds its translation catalogs through its package directory, so it fails inside a zip ("No translation files found for default language en") with zipapps and plain zipapp. Evidence: gauntlet 21.
-
2026-10-04 · environment ·
/usr/bin/python3on macOS is anxcrunshim: ~5 ms slower to start than the interpreter it launches (16.5 vs 11 ms for-c pass). A venv's console script skips it;python3 app.pyzcan't. Compare start-up with the same binary. Evidence: milestone 1. -
2026-10-04 · build · For PyTorch (21,942 files, 190 MB bundle), single-threaded zip compression is 11.3 s of a 14.5 s build: the next build-speed bottleneck, fixable with threads (zlib releases the GIL) rather than Rust. Evidence: milestone 1.
-
2026-10-04 · runtime · Running a
.pyzmakeszipimportparse the whole zip directory on every start: ~1.5–2 µs per entry, so a 20,000-file bundle adds 27 ms (3.12) to 43 ms (3.9) to every run. A two-entry outer zip with a stored inner payload makes warm start independent of size. Evidence: milestone 1. -
2026-10-04 · environment · macOS's
/usr/bin/python3setssys.pycache_prefixto~/Library/Caches/com.apple.python: it ignores every__pycache__directory, and its standard library ships no.pyc, so a freshHOMEcompiles the stdlib on the first run of any program (~130 ms for click's imports). Precompiled.pycmust be placed under the prefix to be used. Evidence: milestone 1. -
2026-10-04 · tooling · uv caches what it learns about a real interpreter but re-runs a shim like
/usr/bin/python3on everyuv pip install --python(+120 ms); resolvesys.executablefirst. Importingplatformcosts ~30 ms on Apple's Python. Evidence: milestone 1. -
2026-10-04 · tooling ·
compileallwithworkers=0is slower than one process for small trees on macOS (workers are spawned interpreters): 317 ms vs 125 ms for 03 on 3.9; worth it from a few hundred files.compileallembeds the source path in each.pycunless givenddir, which breaks reproducible builds. Evidence: milestone 1. -
2026-10-04 · tooling ·
uv pip install -rresolves relative paths in the requirements file against the working directory, not the file's location;uv export --no-editableemits the project and workspace members as././packages/x.uv python findcan't combine a version request with--script. Evidence: ADR-0011. -
2026-10-04 · tooling · zlib level 1 deflates NumPy's 33 MB install 2.5× faster than level 6 (183 vs 463 ms) for 14% more size; decompression time is the same. Evidence: milestone 1.
-
2026-10-04 · measurement · The baseline's speed table (59 ms venv, 106 ms shiv) isn't reproducible from
run_bundlers.py, which runs six builds in parallel; its method wasn't recorded. Re-measured sequentially withgauntlet/bench.py: the venv floor for 03 is ~31 ms on 3.12. Compare tools from the same run, not across sessions. Evidence: milestone 1. -
2026-10-04 · environment · The development machine's shell sets
PYTHONDONTWRITEBYTECODE=1. The gauntlet scripts pass a clean environment, so results aren't affected, but ad-hoc timings in the shell are. -
2026-10-03 · naming · Names built on "zip" read as "this makes zip files"; name the experience (ready to run), not the container format. Evidence: ADR-0009.
-
2026-10-03 · naming ·
bundle/bundlerare Ruby commands preinstalled on macOS (/usr/bin/bundle);packeris HashiCorp's;packis Cloud Native Buildpacks' CLI. Check command clashes, not just PyPI. Evidence: ADR-0009. -
2026-10-03 · naming · PyPI
packis an empty placeholder (0.0.1, no files, Paul J. Davis): likely eligible for PEP 541 transfer, but transfers take months (owner contacted, ~6-week wait, small review team). Evidence: PEP 541. -
2026-10-03 · prior art · Abandoned attempts at the same idea exist on PyPI:
carryon(0.1.3, Nov 2024: "Pack your Python script with its dependencies", appends a zip to the script) andcarton(2016: "make self-extracting virtualenvs"). More evidence of demand and of nobody sticking with it. -
2026-10-03 · tools · pex is the correctness bar: it passed every applicable gauntlet project except child processes (19), and every hostile condition. Evidence: baseline.
-
2026-10-03 · tools · pex adds ~210 ms to every start by default (273 ms vs 59 ms for an installed venv).
--venv --sh-bootgets 32 ms, but an--sh-bootpex runs the firstpython3onPATH, and on macOS that's 3.9, so a 3.12 pex fails with a confusing "no cp39 distributions" error. Evidence: baseline. -
2026-10-03 · tools · On Python 3.9, pex resolves with a vendored pip 20.3.4 and its legacy resolver: builds take ~12 s vs ~2 s on 3.12. Evidence: baseline.
-
2026-10-03 · tools · shiv always extracts to
~/.shivwith no fallback; every bundle fails whenHOMEisn't writable. Evidence: baseline. -
2026-10-03 · tools · zipapps deletes all
.dist-infoby default (--rm-patterns), breakingimportlib.metadataand entry points; it unpacks native code into./zipapps_cachein the working directory, so it fails from read-only directories and races on simultaneous first runs. Evidence: baseline. -
2026-10-03 · tools · A naive
pip --target+zipappbundle silently loses compiled accelerators (PyYAML's LibYAML, mypyc'd charset-normalizer) and falls back to pure Python with no warning. Evidence: gauntlet project 10 in the baseline. -
2026-10-03 · runtime · Child interpreters started with
sys.executabledon't inherit a bundle'ssys.path; only zipapps handles this (via the environment). Evidence: gauntlet 19. -
2026-10-03 · runtime · Extract-everything tools avoid
__file__, metadata and native failures; run-from-zip tools fail more on files/metadata than on native code. Answers the research's open question. Evidence: baseline. -
2026-10-03 · ecosystem · Every tool refused the 3.12-only project on 3.9 only because pip checked
requires-python; none inspects the code, and none gave a plain-language message. Evidence: baseline. -
2026-10-03 · ecosystem · python-pptx reads its default template via
__file__(zip-unsafe); pytz tries__file__and falls back toimportlib.resources; docopt 0.6.2 ships only an sdist. Evidence: gauntlet projects 05, 14, 18. -
2026-10-03 · environment · macOS's
/usr/bin/python3is 3.9.6 (Command Line Tools), a realistic "user's Python" target. -
2026-10-03 · environment ·
sandbox-exec -p '(version 1)(allow default)(deny network*)'blocks network for a process on macOS; used by the harness to prove bundles need no network. -
2026-10-03 · environment · No Docker on the development machine; Linux/cross-platform runs need CI or another host.
-
2026-10-03 · tooling ·
uv export --script file.pyexports a PEP 723 script's resolved dependencies as requirements. -
2026-10-03 · research · The "gemini" research reports hallucinate and recycle sources (round 2 cites round 1 as its main source). Trust the "compass" reports, and verify anything decisive. Evidence: verification notes.
-
2026-10-03 · research · uv's
uv bundlerequest (#5802) is labelled "wish – Not on the immediate roadmap"; theuv run --watchPR (#12847) was closed unmerged. Evidence: verification notes.