Skip to content

Pin and shallow-fetch every upstream source; build toolshed in parallel - #75

Merged
jamieleecho merged 6 commits into
mainfrom
build/pin-and-parallelize-toolchain
Aug 6, 2026
Merged

Pin and shallow-fetch every upstream source; build toolshed in parallel#75
jamieleecho merged 6 commits into
mainfrom
build/pin-and-parallelize-toolchain

Conversation

@jamieleecho

Copy link
Copy Markdown
Owner

Summary

Six focused commits covering how the image fetches its upstream sources, two
toolchain upgrades, test coverage for the tools that had none, and Dockerfile
linting.

The largest single change is mechanical: every stage cloned its upstream repo in
full and then checked out a pinned SHA, downloading an entire history to use one
snapshot. These now fetch only the pinned commit.

Commits

Commit What
a8e2e1c Shallow, pinned fetches for all nine git sources; pin ZX0
ec2e874 CMOC 0.1.98 → 0.1.99
1af788f BASIC-To-6809 → v5.33, fetched sparsely
024b752 Smoke tests for tasm6801, ZX0, salvador, decbpp, nitros9 defs
299a05f hadolint in make lint; pip --no-cache-dir
c6c9c2a Toolshed moved to its own parallel stage

The two toolchain upgrades are deliberately isolated so that a miscompile
bisects to the compiler change rather than to build plumbing.

Things worth a second opinion

git fetch <sha> is not depth-limited. Fetching into a shallow clone
deepens it back to full history, so --depth=1 on the fetch is load-bearing.
On nitros9 the difference is 39MB/3669 commits versus 18MB/1 commit.

Toolshed cannot use git fetch origin <tag>. That populates only
FETCH_HEAD without creating refs/tags/<tag>, so a following git checkout v2_5 fails with "pathspec did not match". It uses clone --depth=1 --branch v2_5 instead.

BASIC-To-6809 keeps every historical release binary in-tree, so even a
depth-1 checkout pulls ~1.9GB and takes ~2.5 minutes. A blob-filtered sparse
checkout of just the manual and one architecture's zip brings that to ~43MB in
~5 seconds against the same immutable SHA. Its version bump and fetch rewrite
share a commit because the archive filenames embed the release — the
sparse-checkout path cannot be written without picking a version.

Nothing built against toolshed. The only build-time dependency on the
foundational toolchain is CMOC's configure requiring lwasm. Moving toolshed
out of foundation removes a serialization point for all eleven tool stages and
stops a toolshed bump from invalidating their caches. Its Makefile maps DESTDIR
to $(DESTDIR)/usr/bin rather than /usr/local/bin, so the stage installs
normally and copies the paths into /staging.

Verification

make build, make test (10/10) and make lint all pass at the branch tip.
The toolshed move was checked further: all ten binaries land at their original
paths reporting Toolshed 2.5, and --target cmoc no longer references toolshed
at all.

This was all verified on arm64 only. The x86_64 leg — including the other
BASIC-To-6809 v5.33 archive, whose contents I inspected but never executed — is
unproven until CI's matrix runs.

The nitros9 test step is only a presence check; asserting on it properly means
assembling a source that uses os9.d through lwasm.

No version bump here — bump-version.yml owns package.json, coco-dev:10
and docker-compose.build.yml:4.

🤖 Generated with Claude Code

jamieleecho and others added 6 commits August 6, 2026 05:29
Each tool stage cloned its upstream repo in full and then checked out a
pinned SHA, downloading the entire history to use a single snapshot.

Replace that with `git init` + `git remote add` + `git fetch --depth=1
<sha>` + `git checkout FETCH_HEAD`, which fetches only the pinned commit
and skips the throwaway default-branch download entirely. Measured on
nitros9: 39MB of history down to 18MB, 3669 commits down to 1.

Note that plain `git fetch <sha>` is NOT depth-limited -- fetching into a
shallow clone deepens it back to full history -- so the `--depth=1` on
the fetch is load-bearing, not decorative.

Toolshed is a tag rather than a SHA, so it uses `git clone --depth=1
--branch v2_5` instead. `git fetch origin <tag>` would not work here: it
populates only FETCH_HEAD without creating refs/tags/<tag>, so a
subsequent `git checkout v2_5` fails with "pathspec did not match".

ZX0 was the one unpinned source in the file; pin it at ecde3a2, which is
the current default-branch HEAD, so this locks in what the build already
produced rather than moving it.

Also drop the `yes | rm -r` guard on the preprocessor tree, which existed
to auto-confirm write-protected files and is unnecessary now that the
directory is one we created.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Upstream release bump. Isolated in its own commit so a miscompile in the
6809 C compiler bisects here rather than to a build-plumbing change.

0.1.98 is still published, so this is a deliberate upgrade rather than a
forced one. Verified that `cmoc --os9` still builds a working OS-9
module via the smoke test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Move the pin from 0e60e91 to 1fd2f46. The version bump and the fetch
rewrite land together because they are genuinely coupled: the archive
filenames embed the release, so the sparse-checkout path cannot be
written without choosing a version.

Three things change as a consequence of the new pin:

  - The in-tree archives are now v5.33, so the v5.28 filenames would no
    longer resolve.
  - v5.33 ships `BasTo6809.2.Compile` under its final name, so the
    rename of "BasTo6809.2.Compile copy" is gone. Confirmed against both
    the arm64 and x86_64 archives.
  - The arch branches collapse into one unzip/mv pair driven by an ARCH
    variable, since only the filename differed.

Upstream keeps every historical release binary in the tree, so even a
--depth=1 checkout pulls ~1.9GB and takes ~2.5 minutes. Fetching blobs
lazily and sparse-checking out only the manual plus this architecture's
zip brings that to ~43MB in ~5 seconds, against the same immutable SHA.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The smoke suite exercised five tools and left five others with no
runtime coverage at all -- a stage could build and install a broken
binary without any test noticing.

Add five steps:

  - tasm6801 assembles a minimal MC-10 source to .c10/.obj
  - zx0 -> dzx0 round trip, asserting the output actually shrank
  - salvador compressing and dzx0 decompressing, which cross-checks the
    two compressors against each other rather than trusting either alone
  - decbpp minifying a line of DECB BASIC (it reads stdin, not a file
    argument -- passing a filename silently produces nothing)
  - the nitros9 defs being present for lwasm

The nitros9 step is only a presence check. Asserting on it properly
means assembling a source that `use`s os9.d through lwasm, which is
worth doing later.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`make lint` covered the two shell scripts but nothing checked the
Dockerfile itself, which is where most of this image's complexity lives.

Wire hadolint into the lint target alongside shellcheck, pinned to
v2.15.1 rather than a floating tag.

Fix the one finding worth fixing: pip was baking its wheel cache into
the image layer, so `--no-cache-dir` (DL3042).

Ignore three rules in .hadolint.yaml, each with its reasoning recorded
there rather than as inline pragmas:

  - DL3003 (use WORKDIR, not cd) works against the multi-stage design.
    Each stage is deliberately one RUN that clones, builds, stages and
    deletes its tree; WORKDIR would leak those directories into the
    image and split each stage across layers.
  - DL3008 (pin apt versions) is impractical for a devcontainer tracking
    Ubuntu 24.04. The reproducibility that matters here -- the 6809
    toolchain -- is pinned by SHA and tarball version instead.
  - DL3015 (--no-install-recommends) is unmeasured. default-jdk and the
    SDL/Mesa dev packages may pull needed runtime libraries in via
    recommends, so adopting it needs a build plus `make test` on both
    architectures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Toolshed sat in `foundation`, which every tool stage derives from. That
made it a serialization point -- all eleven tool stages waited on it --
and a cache liability, since bumping toolshed invalidated every one of
them.

Nothing actually builds against it. Checking each stage's build files,
the only build-time dependency on the foundational toolchain is CMOC's
configure, which aborts without `lwasm` (>= 4.11). Toolshed is needed
only at run time, because basto6809todsk shells out to `decb`.

So move it to `FROM foundation AS toolshed` alongside the other tools
and pull it into `final` with one more COPY. lwtools stays where it is.

Its Makefile maps DESTDIR to $(DESTDIR)/usr/bin rather than
/usr/local/bin, so a plain `make install DESTDIR=/staging` would
relocate the binaries. Install normally and copy the paths into
/staging, the same way the jgrinder stage handles naken_asm.

Verified against a full build: all ten binaries land at their original
paths reporting Toolshed 2.5, `make test` passes (step 2 exercises
`decb` end to end), and building --target cmoc no longer references
toolshed at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jamieleecho
jamieleecho merged commit c9ef70f into main Aug 6, 2026
9 checks passed
@jamieleecho
jamieleecho deleted the build/pin-and-parallelize-toolchain branch August 6, 2026 23:31
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