From a8e2e1c9df16adf98dbabaf8cdbfa3c88c70b581 Mon Sep 17 00:00:00 2001 From: Jamie Cho Date: Thu, 6 Aug 2026 05:29:11 -0400 Subject: [PATCH 1/6] build: fetch upstream sources shallowly and pin every revision 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 ` + `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 ` 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 ` would not work here: it populates only FETCH_HEAD without creating refs/tags/, 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) --- Dockerfile | 58 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7e6bfd1..eba3780 100644 --- a/Dockerfile +++ b/Dockerfile @@ -99,9 +99,9 @@ RUN --mount=type=cache,target=/root/.ccache,sharing=shared \ # Install Toolshed RUN --mount=type=cache,target=/root/.ccache,sharing=shared \ - git clone https://github.com/nitros9project/toolshed.git && \ + git clone --depth=1 --branch v2_5 \ + https://github.com/nitros9project/toolshed.git && \ cd toolshed && \ - git checkout v2_5 && \ make -j -C build/unix CC="ccache gcc" && \ make -C build/unix install && \ cd /root && rm -rf toolshed @@ -114,18 +114,24 @@ RUN --mount=type=cache,target=/root/.ccache,sharing=shared \ # Install preprocessor FROM foundation AS preproc -RUN git clone https://github.com/yggdrasilradio/preprocessor.git && \ +RUN git init -q preprocessor && \ (cd preprocessor && \ - git checkout 62c4ace79eeffa48817f429363816d79abea77c3 && \ + git remote add origin https://github.com/yggdrasilradio/preprocessor.git && \ + git fetch --depth=1 origin 62c4ace79eeffa48817f429363816d79abea77c3 && \ + git checkout -q FETCH_HEAD && \ mkdir -p /staging/usr/local/bin && \ cp decbpp /staging/usr/local/bin/) && \ - (yes | rm -r preprocessor) + rm -rf preprocessor # Install ZX0 data compressor FROM foundation AS zx0 RUN --mount=type=cache,target=/root/.ccache,sharing=shared \ - git clone https://github.com/einar-saukas/ZX0 && \ - cd "ZX0/src" && \ + git init -q ZX0 && \ + cd ZX0 && \ + git remote add origin https://github.com/einar-saukas/ZX0 && \ + git fetch --depth=1 origin ecde3a2ae05061fe06469ed46df81a33b7de7d86 && \ + git checkout -q FETCH_HEAD && \ + cd src && \ make -j CC="ccache gcc" CFLAGS=-O3 EXTENSION= && \ mkdir -p /staging/usr/local/bin && \ cp zx0 dzx0 /staging/usr/local/bin && \ @@ -133,9 +139,11 @@ RUN --mount=type=cache,target=/root/.ccache,sharing=shared \ # Install salvador (fast near-optimal ZX0 compressor) FROM foundation AS salvador -RUN git clone https://github.com/emmanuel-marty/salvador && \ +RUN git init -q salvador && \ cd salvador && \ - git checkout 1662b625a8dcd6f3f7e3491c88840611776533f5 && \ + git remote add origin https://github.com/emmanuel-marty/salvador && \ + git fetch --depth=1 origin 1662b625a8dcd6f3f7e3491c88840611776533f5 && \ + git checkout -q FETCH_HEAD && \ mkdir clang-hack && \ ln -s /usr/bin/cc clang-hack/clang && \ (PATH=./clang-hack:$PATH make -j) && \ @@ -146,9 +154,11 @@ RUN git clone https://github.com/emmanuel-marty/salvador && \ # Install key OS-9 defs from nitros-9 FROM foundation AS nitros9 -RUN git clone https://github.com/nitros9project/nitros9.git && \ +RUN git init -q nitros9 && \ cd nitros9 && \ - git checkout 27c67d5c445db631abfd5b45d49870364d9eacb6 && \ + git remote add origin https://github.com/nitros9project/nitros9.git && \ + git fetch --depth=1 origin 27c67d5c445db631abfd5b45d49870364d9eacb6 && \ + git checkout -q FETCH_HEAD && \ mkdir -p /staging/usr/local/share/lwasm && \ cp -R defs/* /staging/usr/local/share/lwasm/ && \ cd /root && rm -rf nitros9 @@ -156,15 +166,21 @@ RUN git clone https://github.com/nitros9project/nitros9.git && \ # Install java grinder (and naken_asm, which builds it and runs the tests) FROM foundation AS jgrinder RUN --mount=type=cache,target=/root/.ccache,sharing=shared \ - git clone https://github.com/mikeakohn/naken_asm.git && \ - git clone https://github.com/mikeakohn/java_grinder && \ + git init -q naken_asm && \ + (cd naken_asm && \ + git remote add origin https://github.com/mikeakohn/naken_asm.git && \ + git fetch --depth=1 origin b6e83f1976a5fa0b1a371bd4d6db935a386b95ef && \ + git checkout -q FETCH_HEAD) && \ + git init -q java_grinder && \ + (cd java_grinder && \ + git remote add origin https://github.com/mikeakohn/java_grinder && \ + git fetch --depth=1 origin 4dca222bae458766c320f045c015754aa6c17376 && \ + git checkout -q FETCH_HEAD) && \ cd naken_asm && \ - git checkout b6e83f1976a5fa0b1a371bd4d6db935a386b95ef && \ ./configure && \ make CC="ccache gcc" && \ make install && \ cd ../java_grinder && \ - git checkout 4dca222bae458766c320f045c015754aa6c17376 && \ make -j CC="ccache gcc" CXX="ccache g++" && \ make java && \ (cd samples/trs80_coco && make -j grind) && \ @@ -177,9 +193,11 @@ RUN --mount=type=cache,target=/root/.ccache,sharing=shared \ # Install tasm6801 FROM foundation AS tasm -RUN git clone https://github.com/gregdionne/tasm6801.git && \ +RUN git init -q tasm6801 && \ cd tasm6801 && \ - git checkout 0820625bf8e78053ced348a3d747191d54e5e24f && \ + git remote add origin https://github.com/gregdionne/tasm6801.git && \ + git fetch --depth=1 origin 0820625bf8e78053ced348a3d747191d54e5e24f && \ + git checkout -q FETCH_HEAD && \ cd src && \ make -j && \ mkdir -p /staging/usr/local/bin && \ @@ -188,9 +206,11 @@ RUN git clone https://github.com/gregdionne/tasm6801.git && \ # Install mcbasic FROM foundation AS mcbasic -RUN git clone https://github.com/gregdionne/mcbasic.git && \ +RUN git init -q mcbasic && \ cd mcbasic && \ - git checkout 1030ec4413df400e07709a9aabffcaaf4772eb82 && \ + git remote add origin https://github.com/gregdionne/mcbasic.git && \ + git fetch --depth=1 origin 1030ec4413df400e07709a9aabffcaaf4772eb82 && \ + git checkout -q FETCH_HEAD && \ make -j && \ mkdir -p /staging/usr/local/bin && \ cp mcbasic /staging/usr/local/bin && \ From ec2e87444fc6624f99b9f57701ea0b1777e5dcd4 Mon Sep 17 00:00:00 2001 From: Jamie Cho Date: Thu, 6 Aug 2026 05:29:33 -0400 Subject: [PATCH 2/6] build: upgrade CMOC 0.1.98 -> 0.1.99 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) --- Dockerfile | 8 ++++---- README.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index eba3780..37bb728 100644 --- a/Dockerfile +++ b/Dockerfile @@ -219,13 +219,13 @@ RUN git init -q mcbasic && \ # Install CMOC FROM foundation AS cmoc RUN --mount=type=cache,target=/root/.ccache,sharing=shared \ - curl -LO http://sarrazip.com/dev/cmoc-0.1.98.tar.gz && \ - tar -zxpvf cmoc-0.1.98.tar.gz && \ - cd cmoc-0.1.98 && \ + curl -LO http://sarrazip.com/dev/cmoc-0.1.99.tar.gz && \ + tar -zxpvf cmoc-0.1.99.tar.gz && \ + cd cmoc-0.1.99 && \ ./configure CC="ccache gcc" CXX="ccache g++" && \ make && \ make install DESTDIR=/staging && \ - cd /root && rm -rf cmoc-0.1.98 cmoc-0.1.98.tar.gz + cd /root && rm -rf cmoc-0.1.99 cmoc-0.1.99.tar.gz # Build and install BASIC-To-6809 FROM foundation AS basto diff --git a/README.md b/README.md index 18628a5..deb81c9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ applications. It implements a Docker image that includes the following tools: * CoCo Languages and Libraries * [BasTo6809 V5.28](https://github.com/nowhereman999/BASIC-To-6809) - * [CMOC 0.1.98](http://sarrazip.com/dev/cmoc.html) + * [CMOC 0.1.99](http://sarrazip.com/dev/cmoc.html) * [Java Grinder](http://www.mikekohn.net/micro/java_grinder.php) * [LWTOOLS 4.24](http://lwtools.projects.l-w.ca) * [naken](http://www.mikekohn.net/micro/naken_asm.php) From 1af788ffe4416267dba7287bfd8e15fee1bba946 Mon Sep 17 00:00:00 2001 From: Jamie Cho Date: Thu, 6 Aug 2026 05:30:23 -0400 Subject: [PATCH 3/6] build: upgrade BASIC-To-6809 to v5.33 and fetch it sparsely 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) --- Dockerfile | 26 +++++++++++++++----------- README.md | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index 37bb728..ff114e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -228,21 +228,25 @@ RUN --mount=type=cache,target=/root/.ccache,sharing=shared \ cd /root && rm -rf cmoc-0.1.99 cmoc-0.1.99.tar.gz # Build and install BASIC-To-6809 +# +# Upstream keeps every historical release binary in the tree, so even a depth-1 +# checkout is ~1.9GB. Fetch blobs lazily (--filter=blob:none) and sparse-check +# out just the manual and this architecture's zip, which pulls ~43MB instead. FROM foundation AS basto -RUN git clone https://github.com/nowhereman999/BASIC-To-6809.git && \ +RUN if [ "$(uname -m)" = "aarch64" ]; then ARCH=arm64; else ARCH=x86_64; fi && \ + ZIP="BASIC-To-6809_v5.33_Linux_$ARCH.zip" && \ + git init -q BASIC-To-6809 && \ cd BASIC-To-6809 && \ - git checkout 0e60e91fae063324fb9608f0117f6e9ac0582125 && \ + git remote add origin https://github.com/nowhereman999/BASIC-To-6809.git && \ + git config remote.origin.promisor true && \ + git config remote.origin.partialclonefilter blob:none && \ + git sparse-checkout set --no-cone /Manual.pdf "/Binary_Versions/$ZIP" && \ + git fetch --depth=1 --filter=blob:none origin 1fd2f46923e61b60f54a16e5b317a633eaa43c80 && \ + git checkout -q FETCH_HEAD && \ mkdir -p /staging/usr/local/share/doc && \ cp Manual.pdf /staging/usr/local/share/doc/basto6809.pdf && \ - cd Binary_Versions && \ - if [ "$(uname -m)" = "aarch64" ]; then \ - unzip BASIC-To-6809_v5.28_Linux_arm64.zip -d /tmp/basto6809 && \ - mv /tmp/basto6809/BASIC-To-6809_Linux_arm64 /staging/usr/local/share/basto6809; \ - else \ - unzip BASIC-To-6809_v5.28_Linux_x86_64.zip -d /tmp/basto6809 && \ - mv /tmp/basto6809/BASIC-To-6809_Linux_x86_64 /staging/usr/local/share/basto6809; \ - fi && \ - mv "/staging/usr/local/share/basto6809/BasTo6809.2.Compile copy" "/staging/usr/local/share/basto6809/BasTo6809.2.Compile" && \ + unzip -q "Binary_Versions/$ZIP" -d /tmp/basto6809 && \ + mv "/tmp/basto6809/BASIC-To-6809_Linux_$ARCH" /staging/usr/local/share/basto6809 && \ chmod -R o+rx /staging/usr/local/share/basto6809 && \ cd /root && rm -rf BASIC-To-6809 /tmp/basto6809 diff --git a/README.md b/README.md index deb81c9..e55f2dc 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Color Computer](https://en.wikipedia.org/wiki/TRS-80_Color_Computer) applications. It implements a Docker image that includes the following tools: * CoCo Languages and Libraries - * [BasTo6809 V5.28](https://github.com/nowhereman999/BASIC-To-6809) + * [BasTo6809 V5.33](https://github.com/nowhereman999/BASIC-To-6809) * [CMOC 0.1.99](http://sarrazip.com/dev/cmoc.html) * [Java Grinder](http://www.mikekohn.net/micro/java_grinder.php) * [LWTOOLS 4.24](http://lwtools.projects.l-w.ca) From 024b7523bd5cd9e295215298ba4c73f25434a0a4 Mon Sep 17 00:00:00 2001 From: Jamie Cho Date: Thu, 6 Aug 2026 05:31:02 -0400 Subject: [PATCH 4/6] test: cover tasm6801, ZX0, salvador, decbpp and the nitros9 defs 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) --- Makefile | 31 ++++++++++++++++++++++++++----- README.md | 3 ++- tests/tasm6801/HELLO.asm | 10 ++++++++++ 3 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 tests/tasm6801/HELLO.asm diff --git a/Makefile b/Makefile index 4c12182..454f10b 100644 --- a/Makefile +++ b/Makefile @@ -26,25 +26,46 @@ build: test: docker run --rm -v "$(CURDIR)/tests:/sources:ro" $(TAG) bash -euc '\ - echo "[1/5] java_grinder -> .bin"; \ + echo "[1/10] java_grinder -> .bin"; \ work=$$(mktemp -d); cp /sources/java_grinder/* $$work/; cd $$work; \ javac Hello.java; \ java_grinder Hello.class Hello.asm trs80_coco; \ naken_asm -l -type bin -o Hello.bin Hello.asm; \ test -s Hello.bin; \ - echo "[2/5] basto6809todsk -> .DSK"; \ + echo "[2/10] basto6809todsk -> .DSK"; \ work=$$(mktemp -d); cp /sources/basto6809/* $$work/; cd $$work; \ basto6809todsk HELLO.BAS; \ test -s HELLO.DSK; \ - echo "[3/5] mcbasic -> .c10"; \ + echo "[3/10] mcbasic -> .c10"; \ work=$$(mktemp -d); cp /sources/mcbasic/* $$work/; cd $$work; \ mcbasic MC10HELLO.BAS; \ test -s MC10HELLO.c10; \ - echo "[4/5] cmoc --os9 -> OS-9 module"; \ + echo "[4/10] cmoc --os9 -> OS-9 module"; \ work=$$(mktemp -d); cp /sources/cmoc-os9/* $$work/; cd $$work; \ cmoc --os9 hello.c; \ test -s hello; \ - echo "[5/5] mame coco3 driver present + headless"; \ + echo "[5/10] tasm6801 -> .c10"; \ + work=$$(mktemp -d); cp /sources/tasm6801/* $$work/; cd $$work; \ + tasm6801 -obj -c10 HELLO.asm; \ + test -s HELLO.c10; \ + test -s HELLO.obj; \ + echo "[6/10] zx0 -> dzx0 round trip"; \ + work=$$(mktemp -d); cd $$work; \ + i=0; while [ $$i -lt 200 ]; do printf "HELLO COCO "; i=$$((i+1)); done > orig.bin; \ + zx0 orig.bin packed.zx0 >/dev/null; \ + dzx0 packed.zx0 unpacked.bin >/dev/null; \ + cmp orig.bin unpacked.bin; \ + test "$$(wc -c dzx0 cross-check"; \ + salvador -c orig.bin packed.sal >/dev/null; \ + dzx0 packed.sal unpacked2.bin >/dev/null; \ + cmp orig.bin unpacked2.bin; \ + echo "[8/10] decbpp minifies DECB BASIC"; \ + printf "print \"HELLO COCO\"\nend\n" | decbpp 2>/dev/null | grep -q "^1PRINT.*:END$$"; \ + echo "[9/10] nitros9 defs installed for lwasm"; \ + test -s /usr/local/share/lwasm/os9.d; \ + test -s /usr/local/share/lwasm/rbf.d; \ + echo "[10/10] mame coco3 driver present + headless"; \ mame -listfull coco3 | grep -qw coco3; \ mame -validate coco3 >/dev/null; \ echo "All smoke tests passed."' diff --git a/README.md b/README.md index e55f2dc..847fabe 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,8 @@ make build Run `make help` to see the available targets. After building, `make test` runs a quick smoke test that exercises CMOC, BasTo6809, mcbasic, Java -Grinder, and the CoCo 3 MAME build against the built image. +Grinder, tasm6801, ZX0/salvador, decbpp, the nitros9 defs, and the CoCo 3 +MAME build against the built image. The image is a multi-stage build: a shared `foundation` stage (apt packages, the Python venv, lwtools and toolshed) followed by one stage per tool, which diff --git a/tests/tasm6801/HELLO.asm b/tests/tasm6801/HELLO.asm new file mode 100644 index 0000000..766a573 --- /dev/null +++ b/tests/tasm6801/HELLO.asm @@ -0,0 +1,10 @@ +; Minimal MC-10 program: store 'A' at the top-left of the text screen. +scrn .equ $4000 + + .org $4400 + +start ldaa #$41 + staa scrn + rts + + .end start From 299a05f7830593f21e2c0a6884f4c80788d3ab79 Mon Sep 17 00:00:00 2001 From: Jamie Cho Date: Thu, 6 Aug 2026 05:31:33 -0400 Subject: [PATCH 5/6] build: lint the Dockerfile with hadolint `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) --- .dockerignore | 1 + .hadolint.yaml | 28 ++++++++++++++++++++++++++++ Dockerfile | 2 +- Makefile | 5 ++++- README.md | 3 ++- 5 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 .hadolint.yaml diff --git a/.dockerignore b/.dockerignore index 477752a..63092f7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,7 @@ .git .github .gitignore +.hadolint.yaml Makefile README.md bump-info.json diff --git a/.hadolint.yaml b/.hadolint.yaml new file mode 100644 index 0000000..785b3b7 --- /dev/null +++ b/.hadolint.yaml @@ -0,0 +1,28 @@ +# hadolint configuration for the coco-dev image. +# +# Every ignore below is deliberate; please justify any addition here rather +# than sprinkling inline `# hadolint ignore=` pragmas through the Dockerfile. +ignored: + # "Use WORKDIR to switch to a directory". + # + # Each tool stage is a single RUN that clones, builds, installs into + # /staging, and deletes its source tree. The `cd`s are scoped to that one + # layer on purpose -- switching to WORKDIR would leak the directories into + # the image as persistent state and split each stage across several layers, + # which is exactly what the multi-stage refactor set out to avoid. + - DL3003 + + # "Pin versions in apt-get install". + # + # This is a devcontainer that intentionally tracks whatever Ubuntu 24.04 + # ships. Pinning ~40 apt versions would break on every upstream point + # release. The reproducibility that matters here -- the 6809 toolchain -- is + # pinned by git SHA and tarball version instead. + - DL3008 + + # "Avoid additional packages by specifying --no-install-recommends". + # + # Not yet measured. Several of the packages below (default-jdk, the SDL and + # Mesa dev packages) can pull needed runtime libraries in via recommends, so + # this needs a build plus `make test` on both arches to adopt safely. + - DL3015 diff --git a/Dockerfile b/Dockerfile index ff114e0..0f8d27d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -66,7 +66,7 @@ WORKDIR /root RUN python -m venv venv ENV VIRTUAL_ENV=/root/venv ENV PATH="$VIRTUAL_ENV/bin:$PATH" -RUN pip install \ +RUN pip install --no-cache-dir \ coco-tools==0.27 \ milliluk-tools==0.1 \ mc10-tools==0.10 \ diff --git a/Makefile b/Makefile index 454f10b..9fefa79 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ IMAGE := jamieleecho/coco-dev VERSION := $(shell sed -nE 's/.*"version" *: *"([^"]+)".*/\1/p' package.json) TAG ?= $(IMAGE):$(VERSION) SHELLCHECK_IMAGE := koalaman/shellcheck-alpine:stable +HADOLINT_IMAGE := hadolint/hadolint:v2.15.1-alpine help: @echo "coco-dev Makefile targets:" @@ -12,7 +13,7 @@ help: @echo " make build Build the docker image and tag as $(TAG)" @echo " make test Run smoke tests inside the built image" @echo " make shell Drop into a one-off bash shell in the image" - @echo " make lint Run shellcheck on the shell scripts" + @echo " make lint Run shellcheck on the shell scripts + hadolint on the Dockerfile" @echo " make size Print the size of the built image" @echo " make push Push the image to Docker Hub" @echo " make clean Remove the local image" @@ -76,6 +77,8 @@ shell: lint: docker run --rm -v "$(CURDIR):/work" -w /work $(SHELLCHECK_IMAGE) \ shellcheck coco-dev utils/basto6809todsk + docker run --rm -v "$(CURDIR):/work" -w /work $(HADOLINT_IMAGE) \ + hadolint Dockerfile size: @bytes=$$(docker image inspect --format='{{.Size}}' $(TAG)); \ diff --git a/README.md b/README.md index 847fabe..68bf7ba 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,8 @@ make build Run `make help` to see the available targets. After building, `make test` runs a quick smoke test that exercises CMOC, BasTo6809, mcbasic, Java Grinder, tasm6801, ZX0/salvador, decbpp, the nitros9 defs, and the CoCo 3 -MAME build against the built image. +MAME build against the built image. `make lint` runs shellcheck over the +shell scripts and hadolint over the Dockerfile. The image is a multi-stage build: a shared `foundation` stage (apt packages, the Python venv, lwtools and toolshed) followed by one stage per tool, which From c6c9c2af5063dc2815d14aa1a3ebb34ce2da6803 Mon Sep 17 00:00:00 2001 From: Jamie Cho Date: Thu, 6 Aug 2026 05:31:56 -0400 Subject: [PATCH 6/6] refactor: build toolshed as its own parallel stage 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) --- Dockerfile | 42 +++++++++++++++++++++++++++++------------- README.md | 8 +++++--- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0f8d27d..172267a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,12 +3,15 @@ # Multi-stage build. # # `foundation` holds everything shared by every later build: the apt packages, -# the Python venv, and the two foundational toolchain pieces (lwtools and -# toolshed) that other builds may rely on. Every remaining tool is built in its -# own stage `FROM foundation`, so BuildKit compiles them concurrently and a -# change to one tool no longer invalidates the others' cache. Each tool stage -# installs into an isolated `/staging` prefix; the `final` stage assembles the -# image with one `COPY --from= /staging/ /` per tool. +# the Python venv, and lwtools. lwtools is the one genuine build-time +# dependency -- CMOC's configure aborts without `lwasm` (>= 4.11). Toolshed is +# only needed at run time, so it builds as its own parallel stage below. +# +# Every remaining tool is built in its own stage `FROM foundation`, so BuildKit +# compiles them concurrently and a change to one tool no longer invalidates the +# others' cache. Each tool stage installs into an isolated `/staging` prefix; +# the `final` stage assembles the image with one `COPY --from= /staging/ /` +# per tool. # # Compile-heavy stages (mame, cmoc, java_grinder, lwtools, toolshed, zx0) use a # ccache BuildKit cache mount, so recompiling the same source (e.g. after a @@ -86,7 +89,7 @@ RUN pip install --no-cache-dir \ ENV CCACHE_DIR=/root/.ccache \ CCACHE_MAXSIZE=2G -# --- Foundational toolchain (other build stages may depend on these) --- +# --- Foundational toolchain (other build stages depend on this) --- # Install lwtools RUN --mount=type=cache,target=/root/.ccache,sharing=shared \ @@ -97,21 +100,33 @@ RUN --mount=type=cache,target=/root/.ccache,sharing=shared \ make install && \ cd /root && rm -rf lwtools-4.24 lwtools-4.24.tar.gz + +# =========================================================================== +# Parallel tool stages. Each is independent and FROM foundation, and installs +# into /staging (mirroring the final layout) so `final` can COPY it in. +# =========================================================================== + # Install Toolshed +# +# Nothing builds against this -- it is a run-time dependency of the image +# (basto6809todsk shells out to `decb`), so it belongs here rather than in +# foundation. Its Makefile maps DESTDIR to $(DESTDIR)/usr/bin instead of +# /usr/local/bin, so install normally and copy the paths into /staging, the +# same way the jgrinder stage handles naken_asm. +FROM foundation AS toolshed RUN --mount=type=cache,target=/root/.ccache,sharing=shared \ git clone --depth=1 --branch v2_5 \ https://github.com/nitros9project/toolshed.git && \ cd toolshed && \ make -j -C build/unix CC="ccache gcc" && \ make -C build/unix install && \ + mkdir -p /staging/usr/local/bin /staging/usr/local/share && \ + for t in ar2 os9 mamou cecb decb tocgen makewav dis68 lst2cmt cocofuse; do \ + cp "/usr/local/bin/$t" /staging/usr/local/bin/; \ + done && \ + cp -R /usr/local/share/toolshed /staging/usr/local/share/ && \ cd /root && rm -rf toolshed - -# =========================================================================== -# Parallel tool stages. Each is independent and FROM foundation, and installs -# into /staging (mirroring the final layout) so `final` can COPY it in. -# =========================================================================== - # Install preprocessor FROM foundation AS preproc RUN git init -q preprocessor && \ @@ -294,6 +309,7 @@ RUN --mount=type=cache,target=/root/.ccache,sharing=shared \ # =========================================================================== FROM foundation AS final +COPY --from=toolshed /staging/ / COPY --from=preproc /staging/ / COPY --from=zx0 /staging/ / COPY --from=salvador /staging/ / diff --git a/README.md b/README.md index 68bf7ba..af55862 100644 --- a/README.md +++ b/README.md @@ -115,9 +115,11 @@ MAME build against the built image. `make lint` runs shellcheck over the shell scripts and hadolint over the Dockerfile. The image is a multi-stage build: a shared `foundation` stage (apt packages, -the Python venv, lwtools and toolshed) followed by one stage per tool, which -BuildKit compiles in parallel. Compile-heavy stages use a `ccache` cache mount, -so rebuilding unchanged sources locally is fast. +the Python venv and lwtools) followed by one stage per tool, which BuildKit +compiles in parallel. lwtools lives in `foundation` because CMOC's configure +requires `lwasm`; every other tool, toolshed included, is only needed at run +time and so builds as its own parallel stage. Compile-heavy stages use a +`ccache` cache mount, so rebuilding unchanged sources locally is fast. Building MAME from source is the slow part of the image build. A few of MAME's core source files need ~2 GB of RAM each in the compiler, so the job count is