Skip to content

fix: make the coreutils toolchain work in WORKSPACE mode and with aspect_bazel_lib 1.x - #409

Closed
shimmerjs wants to merge 16 commits into
mainfrom
fix-380-workspace-coreutils
Closed

shimmerjs wants to merge 16 commits into
mainfrom
fix-380-workspace-coreutils

Conversation

@shimmerjs

Copy link
Copy Markdown
Contributor

Please read: this stacks on #380

This branch contains #380 (@xnox) unchanged, including the #379 commits it carries. Everything after 2500938 is new: three commits (c36c17f, f4ffb22, 2fbfbce). If #380 lands first, this rebases down to those three. If it is easier, the three commits can be cherry-picked into #380 instead.

#380 makes the apko and staging actions take mkdir/cp/mktemp/rm from aspect_bazel_lib's coreutils toolchain. That works under bzlmod. It breaks WORKSPACE users on Bazel 7 and 8 in two ways, and this fixes both.

  1. Analysis fails with No matching toolchains found for types @@aspect_bazel_lib//lib:coreutils_toolchain_type, because the documented WORKSPACE setup never registers the toolchain. The aspect_bazel_lib 1.34.5 that rules_apko_dependencies() fetches already defines the toolchain and register_coreutils_toolchains(), so no version bump is needed. The fix adds one load and one call to the setup in e2e/smoke/WORKSPACE.bazel. Registration can't move into rules_apko_dependencies() itself, because repositories.bzl is loaded before @aspect_bazel_lib exists.
  2. With the toolchain registered, every apko_image fails: failed to read ".../cache/https%3A%2F%2Fdl-cdn.alpinelinux.org%2F.../busybox-1.37.0-r14.apk" in offline cache: ... no such file or directory. aspect_bazel_lib 1.x registers uutils coreutils 0.0.16. That release copies cp -R src/. dst to dst/<basename>, so the cache mirror lands one level too deep. copy_to_workdir nests directory inputs the same way. The fix copies to a destination that doesn't exist yet. That form behaves the same on every uutils release aspect_bazel_lib has shipped (0.0.16, 0.0.23, 0.0.26, 0.0.27, 0.1.0) and on GNU cp. The mirror is still one symlink per file inside real directories.

Also:

  • e2e/smoke/WORKSPACE.bazel loaded local_repository from @bazel//..., which does not exist in WORKSPACE mode, so the file could not load at all. It now loads from @bazel_tools.
  • CI only ran with --enable_bzlmod. A new step runs e2e/smoke with --noenable_bzlmod --enable_workspace on Bazel 7.6.1, 8.3.0 and 8.6.0.
  • setup-bazel's tags have no v prefix, so the # v0.19.0 pin comments pointed at an unknown ref (zizmor ref-version-mismatch). They now read # 0.19.0. That also clears the same open alert on main (ci.yaml:89).

Breaking change? WORKSPACE users who do not already register the coreutils toolchain must add two lines. Analysis fails until they do, and the error names the toolchain type. Users who call aspect_bazel_lib_dependencies() from aspect_bazel_lib 1.x already have it, because that function always registers coreutils. Bzlmod users are unaffected. Bumping the pinned aspect_bazel_lib to 2.x would not avoid the edit. In 2.x, @aspect_bazel_lib//lib:coreutils_toolchain_type is an alias into @bazel_lib, and maybe() means a user's existing pin wins anyway.

Evidence (macOS arm64 host and an arm64 Linux container):

Before, #380 head 2500938, e2e/smoke, Bazel 7.6.1, --noenable_bzlmod --enable_workspace:

ERROR: .../multifile_config/BUILD:17:11: While resolving toolchains for target //multifile_config:image_from_generated_config (096dcc8): No matching toolchains found for types @@aspect_bazel_lib//lib:coreutils_toolchain_type.

Before, same, with register_coreutils_toolchains() added to the WORKSPACE:

Error: building "amd64" layer: failed installation from lockfile ../example_multifile_lock/lockfile_copy: installing packages: expanding {busybox ...}: ... failed to read "/var/folders/.../T/tmp.gMJYVqeBHW/cache/https%3A%2F%2Fdl-cdn.alpinelinux.org%2Falpine%2Fv3.21%2Fmain/x86_64/busybox-1.37.0-r14.apk" in offline cache: ... no such file or directory
Executed 1 out of 4 tests: 1 test passes, 1 fails to build and 2 were skipped.

After, this branch, e2e/smoke:

bazel 7.6.1 test //... --noenable_bzlmod --enable_workspace   Executed 4 out of 4 tests: 4 tests pass.   (macOS and Linux)
bazel 8.6.0 test //... --noenable_bzlmod --enable_workspace   Executed 4 out of 4 tests: 4 tests pass.   (macOS and Linux)
bazel 9.1.0 test //...                                        Executed 4 out of 4 tests: 4 tests pass.   (macOS and Linux)
bazel 7.6.1 test //... (bzlmod)                               Executed 4 out of 4 tests: 4 tests pass.   (macOS)

Root workspace, Bazel 9.1.0, Linux container with clang: //docs:update_0_test, //docs:update_1_test, //apko/tests:versions_test_test_0, //examples/lock:test and //examples/wolfi-base:test pass. //examples/simple:test_apko_genrule fails there, and fails the same way on main, because the container's busybox grep rejects --fixed-strings. On macOS, //apko/... //examples/... gives 4/4. bazel run //.github/workflows:buildifier.check is clean.

Hermeticity probe, Linux container, Bazel 9.1.0: fetch, delete mkdir/mktemp/ln/find from PATH, then bazel build //... --spawn_strategy=local in e2e/smoke. This branch builds (INFO: Build completed successfully, 61 total actions). main fails (/bin/bash: line 2: mkdir: command not found). cp and rm could not be removed in that container, so the probe covers the other four tools only.

Not verified here: the actiond remote-execution job (it needs x86_64 KVM; CI runs it). The directory branch of copy_to_workdir was checked with each uutils binary directly, not through a Bazel target, because no test target stages a tree-artifact config input.

codysoyland
codysoyland previously approved these changes Oct 26, 2026

@codysoyland codysoyland left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, with four non-blocking nits.

Nits (non-blocking)

  • Pin the downloaded actiond binary digest: .github/workflows/ci.yaml:173.
  • Exercise tree-artifact config staging in a regression test: apko/private/apko_config.bzl:119.
  • Describe the actual HOME fallback condition: apko/private/apko_image.bzl:116.
  • Remove the issue-history reference from the regression-gate comment: .github/workflows/ci.yaml:215.

What I verified

  • At 2fbfbce, WORKSPACE registration follows dependency creation in e2e/smoke/WORKSPACE.bazel:28; CI run 40409341233 logs show all four smoke tests passing in WORKSPACE mode on Bazel 7.6.1, 8.3.0, and 8.6.0.
  • The same run's actiond job completed with five remote actions; .github/workflows/ci.yaml:230 disables local fallback.
  • Tested uutils 0.0.16 and 0.1.0 directly: copying a read-only source directory with cp -Rs creates a writable destination with per-file symlinks, matching apko/private/apko_image.bzl:133.
  • Scratch cleanup uses an absolute tool path and an EXIT trap, and apko arguments retain their boundaries at apko/private/apko_image.bzl:128-136.
  • e2e/smoke/BUILD:15 exercises use_default_shell_env = False. No local Bazel run was possible because Bazel is not installed.

Security

  • CI: cleared; read-only token, no persisted checkout credentials, localhost worker, and bounded runtime (.github/workflows/ci.yaml:131-147). Independent check confirmed.
  • Dependencies: cleared at blocking severity within the read-only CI job. The actiond checksum detects corruption but is fetched beside a mutable release asset; pinning its digest is a non-blocking improvement. Independent check confirmed.

Comment thread .github/workflows/ci.yaml Outdated
Comment thread apko/private/apko_config.bzl
Comment thread apko/private/apko_image.bzl Outdated
Comment thread .github/workflows/ci.yaml Outdated
@shimmerjs

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I am not folding the four nits into this PR. Any push dismisses the existing approval (dismiss_stale_reviews_on_push) and this PR also needs the extra approval for unattributed changes, so pushing now would delay a fix that remote-execution users are waiting on. They are all worth doing and I will send them as a follow-up PR once this lands:

  • pin the actiond binary by digest instead of the checksum file served beside it (ci.yaml)
  • add a regression test that stages a tree-artifact config input through copy_to_workdir (apko_config.bzl), which no target covers today
  • describe the real condition under which the HOME fallback applies (apko_image.bzl)
  • drop the issue-history reference from the regression-gate comment in ci.yaml

jeffpignataro and others added 16 commits October 26, 2026 19:17
The apko build action was pinned to local execution in #332 to work
around #331. The underlying incompatibility is that apko writes into
its --cache-dir even with --offline (it creates directories on cache
lookup and expands packages into the cache), while Bazel provides the
prepopulated cache tree artifact as a read-only action input, which
remote executors enforce strictly. Local sandboxes keep directories
writable, so this only surfaced under remote execution.

- Copy the cache input into a writable scratch directory inside the
  action and point --cache-dir at it, so the action works on
  read-only input trees.
- Give HOME a writable fallback when the executor does not provide
  one.
- Bind the apko_image and apko_show_config actions to the resolved
  apko toolchain so an executor never receives a binary built for a
  different platform than the action is scheduled on.
- Remove the no-remote-exec execution requirement from #332.
- Add an opt-in use_default_shell_env attribute (default True, the
  previous behavior) so users who want a hermetic action environment
  can drop host env inheritance per target.

Verified locally (all tests, examples, and e2e/smoke) and against a
Buildbarn-based remote executor: both e2e/smoke image targets build
with the apko actions executing on remote linux-amd64 workers from a
macOS host.
apko cross-builds images for any target arch, so the comment's framing
about arch mismatches was wrong; the toolchain param needs no
explanation beyond what it says.
Mirror the read-only cache tree artifact into the writable scratch
directory as real directories containing per-file symlinks rather than
cp -RL'ing the whole cache. apko only adds new entries (package
expansions) to its --cache-dir and never rewrites existing files, so
new writes land in the scratch directories while existing entries are
read through the symlinks. This avoids duplicating the full APK cache
on every image build. The chmod is no longer needed since apko never
writes to the symlinked files.

Verified locally (darwin-sandbox) and on a Buildbarn-based remote
executor with linux-amd64 workers; layer digests are identical to the
copy-based version.
Adds an x86_64-only workflow that starts actiond, a local hermetic
Remote Execution API worker that runs Bazel actions inside a Linux VM,
and points Bazel at it to build //examples/wolfi-base.

x86_64 only: GitHub's arm64 Linux runners have no /dev/kvm because the
underlying Azure DPDsv6 SKU does not support nested virtualization, and
the macOS arm64 runners cannot nest under Apple's Virtualization
Framework. actiond's own CI has the same split.
CopyToWorkdir fails at exit 127 before the apko action is ever
scheduled, which hides what #379 changed. Add a non-blocking follow-up
build that runs the staging actions locally, so the apko action reaches
the VM and its own missing-coreutils failure shows up in the log.

Also trigger on actiond** branches so experiments do not have to land
on main.
So the workflow triggers on the branch #379 is developed on, not just
on main and actiond** experiment branches.
The run_shell actions in copy_to_workdir and apko_image call mkdir, cp,
mktemp, find, ln, and rm, all resolved from the executor's PATH. That
holds on a conventional RBE image and fails on an executor that ships no
userspace: against actiond, a local RE worker that runs each action in
an empty chroot, staging dies with "mkdir: command not found" (exit 127)
and the apko action with "mktemp: command not found".

Take the binary from aspect_bazel_lib's coreutils toolchain and declare
it in tools, so it is uploaded as an action input and travels to
whichever executor the action lands on. aspect_bazel_lib is already a
bazel_dep and registers the toolchain itself, so this adds no dependency
and needs no registration. The Linux assets are statically linked
against musl, so they need nothing from the executor either.

The find/ln mirror collapses into a single cp -Rs, which has the same
effect - real directories, per-file symlinks into the read-only input
tree - without needing find, which uutils does not ship.

These actions still need a shell; making them fully shell-free would
mean replacing copy_to_workdir with copy_to_directory and giving the
apko action a wrapper binary.
Without a pull_request trigger the job never runs on a PR: a fork PR
generates no push event in the upstream repo, so the workflow this PR
proposes as a check would not have fired on the PR proposing it.
At the default spawn strategy an action that opts out of remote
execution just runs on the host and the build still passes, so a
regression of the kind #332 introduced would not have been caught.
--spawn_strategy=remote with no local fallback turns that into a hard
failure.

Drop the diagnostic step: it existed to surface the apko action's
failure when staging failed first, which no longer happens, and a
continue-on-error step reads as a passing check when it is not one.
needs: can only reference jobs in the same workflow, so a separate
actiond.yaml could never be a dependency of the summary gate. Moving the
job into ci.yaml and adding it to test-roundup's needs means branch
protection keeps requiring exactly one context while remote execution
becomes part of what that context covers.
aspect_bazel_lib 1.x, the version rules_apko_dependencies() fetches for
WORKSPACE users, registers uutils coreutils 0.0.16. That release treats
"cp -R src/. dst" as a copy of a directory named after src and nests it
at dst/<basename>. With it, the apko action's cache mirror lands one
level too deep and apko fails at --offline with "failed to read ... in
offline cache", and copy_to_workdir nests directory inputs the same way.
uutils 0.0.23 and later, and GNU cp, are not affected.

Copy to a destination that does not exist yet instead, which every
uutils release aspect_bazel_lib has shipped (0.0.16, 0.0.23, 0.0.26,
0.0.27, 0.1.0) and GNU cp handle the same way: the apko action no longer
pre-creates the cache mirror directory, and copy_to_workdir removes the
pre-created output directory before copying a directory into its place.

Resolve the cache source with pwd -P so cp -Rs always starts from a real
directory rather than copying a symlink to one.
Taking mkdir/cp/mktemp from aspect_bazel_lib's coreutils toolchain makes
analysis fail in WORKSPACE mode with "No matching toolchains found for
@@aspect_bazel_lib//lib:coreutils_toolchain_type", because nothing in
the documented WORKSPACE setup registers it. Bzlmod users are not
affected: aspect_bazel_lib registers it from its MODULE.bazel.

The aspect_bazel_lib 1.34.5 that rules_apko_dependencies() fetches
already defines the toolchain and register_coreutils_toolchains(), so no
version bump is needed. Registration cannot move into
rules_apko_dependencies() itself, since repositories.bzl is loaded before
@aspect_bazel_lib exists, so the setup gains one load and one call.

Also load local_repository from @bazel_tools. @bazel does not exist in
WORKSPACE mode, so this file could not be loaded at all.
CI only ever ran with --enable_bzlmod, so neither the missing coreutils
registration nor the unloadable e2e/smoke WORKSPACE file showed up. Run
e2e/smoke a second time with --noenable_bzlmod --enable_workspace on the
Bazel versions that still support WORKSPACE.

Also fix the setup-bazel pin comments: the action's tags have no "v"
prefix, so zizmor reported "v0.19.0" as an unknown ref.
Pin the actiond binary by its SHA-256 in ci.yaml instead of reading the
checksum from a file served beside the release asset. Add e2e/smoke
tree_staging, which stages a directory through copy_to_workdir and checks
its contents land at the destination; it fails with the previous
"src/." copy form on uutils 0.0.16. Describe the condition under which
apko_image's HOME fallback applies, and drop the issue reference from the
remote-execution gate comment.
@xnox

xnox commented Oct 27, 2026

Copy link
Copy Markdown
Member

i actually dislike the coreutils toolchain thing.

I wonder if instead apko should learn to deal with "rules_apko generated cache which is readonly". Cause it can handle "apko generated cache readonly".

As in we might be able to fix all of this with adding test case in apko, that generates cache with rules_apko, hops into a docker or bubble wrap container with said rules_apko style cache mounted readonly.

That way we might avoid the need for all the coreutils toolchain shenanigans.

@codysoyland codysoyland left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by unblok using Codex · gpt-6-astra · medium reasoning effort

Fixing apko could remove the writable cache mirror, but it would not by itself remove the current staging actions. Those still need tools on an empty remote executor. Can the proposed approach also replace that staging?

Verified at 2fbfbce: apko_config.bzl:116 uses mkdir/cp for config staging; apko_image.bzl:104-105 stages the binary separately from the cache mirror. I haven't tested the proposed apko change.

@shimmerjs

Copy link
Copy Markdown
Contributor Author

The four review nits are now in this PR (87b9ff9), so there is no follow-up PR:

  • actiond is pinned by SHA-256 in ci.yaml (ACTIOND_SHA256), checked against both the downloaded v0.0.6 asset and the release's SHA256.txt.
  • e2e/smoke/tree_staging stages a tree artifact through copy_to_workdir and fails if it nests. With fix: carry coreutils as action inputs so apko actions run on minimal executors #380's previous copy form it fails on Bazel 7.6.1 in WORKSPACE mode (uutils 0.0.16), so it catches the bug fixed here.
  • The HOME comment now says the fallback applies when the action environment leaves HOME unset or empty.
  • The #332 reference is gone from the regression-gate comment.

I also stripped the Co-Authored-By: Claude and Claude-Session trailers from the commit messages and rebased onto current main. Authors are unchanged, and the diff against main is smaller because the branch had fallen behind the apko version bump. Pushing dismissed the earlier approval, so @codysoyland and @xnox, please review again.

@shimmerjs
shimmerjs requested a review from codysoyland October 27, 2026 16:09

@codysoyland codysoyland left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by unblok using Codex · gpt-6-astra · medium reasoning effort

Re-reviewed after 87b9ff9. LGTM; all four implementation nits are addressed.

  • Digest pin: addressed at .github/workflows/ci.yaml:136,173; the checked-in SHA-256 is verified before execution and matches the release asset digest.
  • Directory regression test: addressed at e2e/smoke/tree_staging/tree.bzl:20,28; it exercises the helper with a tree artifact and checks the expected path and absence of nesting.
  • HOME comment: addressed at apko/private/apko_image.bzl:116; it now describes unset or empty HOME.
  • History reference: addressed at .github/workflows/ci.yaml:215; removed.
  • The earlier staging-design question remains open and non-blocking; this push keeps the existing approach.

What I verified

  • //tree_staging:test depends on the marker action consuming the staged tree, so failed path checks fail the build. CI run 40538033389 for this head reports five passing smoke tests in both bzlmod and WORKSPACE mode on Bazel 7.6.1, including this new test.
  • The download step uses set -euxo pipefail and checks the pinned digest before chmod or execution. The pin matches the GitHub release asset API.
  • The version-related changes match current main; the substantive update is limited to the workflow pin, comment corrections, and tree fixture. No local suite or build was run.

Security

  • CI: cleared at 87b9ff9; read-only token, no persisted checkout credentials, localhost worker, bounded runtime. Fresh independent check confirmed.
  • Dependencies: cleared at 87b9ff9; the actiond digest is now checked against a repository pin before execution. Fresh independent check confirmed.

@shimmerjs

Copy link
Copy Markdown
Contributor Author

Closing. #411 merged and shipped in v1.5.59: it removes the no-remote-exec pin, and apko v1.4.3 and later builds offline from a read-only cache, so the cache mirror and the coreutils toolchain are not needed for the remote-execution fix. This PR existed to repair #380's WORKSPACE regressions, and with that approach no longer going forward it has nothing left to land.

If someone hits an executor with no mkdir or cp on its PATH, the branch fix-380-workspace-coreutils carries the working version of that approach (WORKSPACE registration, the uutils 0.0.16 copy fix, the WORKSPACE-mode CI step and the tree-staging test). #380 is xnox's and I have left it alone.

@shimmerjs shimmerjs closed this Oct 27, 2026
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.

4 participants