Skip to content

chore: devcontainer on the CI runner image, with LLVM install docs and CI validation#525

Open
nebasuke wants to merge 23 commits into
mainfrom
devcontainer
Open

chore: devcontainer on the CI runner image, with LLVM install docs and CI validation#525
nebasuke wants to merge 23 commits into
mainfrom
devcontainer

Conversation

@nebasuke

@nebasuke nebasuke commented Jul 9, 2026

Copy link
Copy Markdown
Member

I've run this locally and resolved a whole bunch of UX issues (too much files getting checked by VSCode, interrupted builds struggling, Boost not working well, etc.).

Key change:

  • Split the Docker container into a base, dev and a CI only part. The devcontainer then constructs a definition by importing dev (built on base). Note that this causes ~10 minutes of build time (instead of 5 min for ghcr.io) the first install due to apt get installations, but avoids having to access ghcr.io. This will be cached and avoided in subsequent runs (due to the separate dev image). Cached startup was ~10 seconds locally within vscode.

Claude summary

What

Adds a .devcontainer/ to solx plus a Developer Guide page, so opening the repo in VS Code (or Codespaces / the devcontainer CLI) lands you in the CI toolchain environment — with no registry authentication — and a documented one-command path to building the custom LLVM backend.

  • base/dev/ci stage split in .github/docker/Dockerfilebase is the shared system toolchain (system packages, LLVM-21 apt, libm/libstdc++ fixes, Node); dev adds the devcontainer bits on top (UID-mapped non-root dev user with sudo, shared rustup at /usr/local/{rustup,cargo} with the toolchain resolved lazily from rust-toolchain.toml, apt valgrind — dev needs a working valgrind, not the ci stage's pinned from-source build, and keeping it in the image stops LLVM's baked-in cmake valgrind detection from flapping across rebuilds); ci adds the CI-only extras (the root-homed Rust install CI jobs use — pinned 1.96.0, kept in lockstep with rust-toolchain.toml by a new renovate customManager — plus valgrind from source and cargo coverage/report tools) and stays the final stage, so the published solx-ci-runner image is unchanged (docker-ci-image.yaml builds the default target, and buildx skips the dev stage when targeting ci).
  • The devcontainer builds --target dev directly (devcontainer.json points at the runner Dockerfile) — ~5–10 min the first time, layer-cached afterwards. All sources are public, so the private solx-ci-runner package never needs to be pulled and there is no digest to keep in sync: the container tracks the checkout's Dockerfile by construction. Single build, single cache domain — an earlier iteration built a separately-tagged base via initializeCommand, which busted the child layer cache on every rebuild (containerd image store re-IDs images via provenance timestamps even on full cache hits).
  • .devcontainer/devcontainer.json — named volumes for target/, target-slang/, target-llvm/ (per-checkout via ${devcontainerId}, so parallel checkouts/worktrees never share a build tree), plus shared rustup/cargo/ccache volumes (survive container rebuilds, stay off the slow bind mount on macOS/Windows; the rustup share's first-run concurrency caveat is documented in the guide); hostRequirements for the LLVM build (8 CPU / 16 GB / 64 GB); rust-analyzer + clangd extensions, clangd pointed at the compile_commands.json solx-dev exports into target-llvm/build-final/; VS Code watcher/search excludes for the build trees (without them the server crawls the built LLVM tree and integrated terminals lag by seconds).
  • .devcontainer/bootstrap.sh — the explicit "install LLVM" step: branch-aware shallow submodule init (a submodule left on a branch — the fork-hacking flow — is left untouched with its nested submodules still synced; a detached pin is reset with an announcement) → build solx-devsolx-dev llvm build --enable-assertions --enable-mlir --ccache-variant ccachesolx-dev solc build --build-boost --ccache-variant ccache --enable-mlir (the image ships no system Boost, matching CI's build-solc action). MLIR is on by default (required for cargo test-slang; --no-mlir to opt out); --clean wipes target-llvm/ first, for build trees configured in a previous container image (CMake caches toolchain facts). CCACHE_COMPILERCHECK=content matches CI so image rebuilds with an unchanged clang don't cold-miss the cache. Deliberately not run from postCreateCommand: the cold build is ~1h and should be a step you choose, not a surprise. post-create.sh only does container plumbing and prints the pointer. (An earlier revision git-excluded the Boost trees solx-dev drops into the solx-solidity submodule; the fork's own .gitignore now covers them — see the submodule bump below.)
  • validate-devcontainer job in test.yaml (mirroring slang's) — path-gated on .devcontainer/**, .github/docker/** (the devcontainer builds its base stage from it), rust-toolchain.toml, and test.yaml itself (so edits to the validation harness get validated too). Shellchecks the scripts, then builds the container via the pinned devcontainers/ci action — same auth-free local-build flow as a developer machine — and runs .devcontainer/smoke-test.sh inside: non-root user + sudo, writable workspace/volumes, system toolchain on PATH, lazy resolution of the rust-toolchain.toml pin. Deliberately does not build LLVM. The smoke script doubles as a user-facing health check (linked from the guide's troubleshooting section).
  • docs/src/developer-guide/00-development-container.md — the grounding doc: what the base image contains vs. what is deliberately not baked in (the LLVM backend — per-submodule-bump churn + GB weight), how the LLVM install actually works and where it lands, rebuild-after-bump via the persistent ccache volume, a troubleshooting section grown from dogfooding (interrupted toolchain download, bind-mount hazards, stale base image, VS Code sluggishness), working on the solx-llvm fork from inside the container, and an orientation section for Slang contributors (no Hermit; infra CLI role split between bootstrap.sh and solx-dev).

Why

A natural question from contributors coming from Slang is whether solx-ci-runner can be used as a devcontainer. As-is it can't, for non-obvious reasons: the package is private (pull needs a read:packages token), rustup is homed in /root with no non-root user, and — the real gap — the custom LLVM isn't in the image and is only reachable in CI via the Actions cache, so a fresh container greets you with llvm-sys build-script failures and an undocumented hour-long build. This PR closes the gap with a supported, documented setup; the stage split removes the auth problem entirely by building the developer-relevant part of the image locally from public sources.

The whole flow was dogfooded end-to-end (bootstrap → cargo build --releasecargo test-slang); the Boost fix, the watcher excludes, the submodule git-exclude, and most of the troubleshooting section are findings from that run.

Notes

  • Post-review hardening: review feedback plus a self-review pass moved Rust out of base (it was ~1.5 GB of /root-homed toolchain the dev user couldn't reach) into ci, caught the pin up to 1.96.0, and corrected the LLVM-pairing comment along the way — rustc 1.96.0 embeds LLVM 22, one major ahead of the image's LLVM-21 tools; they still read its profraw output, and the comment now says to re-check the pairing on every toolchain bump.
  • Local base vs. frozen CI image: a locally built base re-resolves apt packages, so it is not bit-for-bit identical to the image CI pulls. Fine for development; CI itself still uses the frozen published image. Keep the stage split honest: dev-relevant tools go in base, CI-only tooling in ci (noted in the Dockerfile header).
  • Image visibility: with the local-build flow the devcontainer no longer cares that solx-ci-runner is private. Making the package public remains worth considering separately — it would let non-org contributors reproduce the exact CI environment — but nothing in this PR depends on it.
  • Not done here (possible follow-ups):
    • Publishing the target-llvm/target-final artifact from main (reusing the existing build-llvm cache key) so the bootstrap can download instead of building — would cut first-open from ~1h to minutes.
    • A README pointer in solx-llvm declaring the solx devcontainer the canonical dev environment for the fork (its regression CI already builds through a solx checkout).
    • /boost* in the solx-solidity fork's own .gitignore — done in fix: add boost to .gitignore (#111) solx-solidity#158 (cherry-pick of the fork's fix: add a placeholder object for gasEstimates #111 onto 0.8.34), merged; this PR bumps the submodule one commit (5323a55dd79519a9bb, exactly that fix) and drops the bootstrap's local-exclude workaround.
    • Moving the shared-rustup layout into the base stage itself, thinning the devcontainer layer.

Comment thread docs/src/developer-guide/00-development-container.md Outdated
@nebasuke

Copy link
Copy Markdown
Member Author

@nebasuke nebasuke marked this pull request as ready for review July 10, 2026 15:13
@nebasuke nebasuke requested review from a team and OmarTawfik July 10, 2026 15:14
Comment thread .devcontainer/bootstrap.sh Outdated
ccache --set-config=max_size=20G

echo "==> Initializing submodules (shallow, as in CI)"
git submodule update --init --recursive --depth 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rerunning bootstrap.sh silently resets a manually checked-out submodule branch: git submodule update --init --recursive checks out the recorded gitlink SHA on every run, not just the first. A developer who followed the guide's fork-hacking step 1 (git -C solx-llvm checkout <branch>) and later reruns bootstrap — as the header's "Idempotent — rerun it after a solx-llvm submodule bump" invites — gets a clean solx-llvm tree moved back to the recorded detached SHA, then rebuilds upstream LLVM believing they built their branch.

Consider initializing only uninitialized submodules (lines prefixed - in git submodule status), or documenting the interaction here and in the guide's fork-hacking section.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 0d39af0 + 6635c52, though not via init-only-uninitialized: that would
break the other documented flow (rerunning after a submodule bump, where the
tree is initialized but stale and should move). The loop now distinguishes:

  • on a branch → left untouched, with a notice; nested submodules still sync to
    whatever you have checked out (solx-solidity carries three under deps/)
  • detached at a different SHA → still reset, but announced. A deliberately
    pinned detached checkout is indistinguishable from a stale post-bump tree, so
    a branch is the protection signal — the fork-hacking section says so now.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Follow-up: I've walked this back and taken your init-only suggestion after all
(147d2d8). The branch/detached heuristic bought one convenience — auto-refresh
on rerun after a bump — at the cost of a policy rule ("a branch protects your
checkout, detached doesn't") that users only learn from a log line, plus the
subtle bash to implement it. Making the refresh explicit dissolves the
ambiguity the heuristic existed to resolve: the bootstrap now only initializes
never-initialized submodules and never moves an existing checkout. If one
differs from the recorded commit it warns with the exact
git submodule update --init --recursive --depth 1 to run, and the guide's
submodule-bump section leads with that command.

Comment thread .devcontainer/bootstrap.sh Outdated
# fork's .gitignore; without this local exclude, git status enumerates the
# extracted Boost tree (tens of thousands of files) and VS Code's git
# extension drowns in it ("too many changes").
EXCLUDE_FILE=$(git -C solx-solidity rev-parse --git-path info/exclude)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This works around a gap that is fixable at the source: the org owns the solx-solidity fork, and its .gitignore has no boost entry today. Adding /boost* to the fork's .gitignore (at the next submodule bump) fixes the noisy git status for every consumer — host checkouts, CI, other IDEs — after which this git-plumbing workaround and its justification comment can be deleted.

@nebasuke nebasuke Jul 12, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed, turns out you already fixed it on the fork's main in #111; it just
never reached 0.8.34. Cherry-picked onto that branch as
NomicFoundation/solx-solidity#158, now merged.

Comment thread .devcontainer/devcontainer.json Outdated
// Build state lives in named volumes: it survives container rebuilds and
// stays off the bind mount, which is slow on macOS/Windows hosts.
"mounts": [
"source=solx-target,target=${containerWorkspaceFolder}/target,type=volume",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These named volumes are docker-global, so every checkout of this repo on one machine shares the same build state. Two checkouts (clone + fork review, or two worktrees) opened in devcontainers mount the same cargo target/registry and rustup volumes: concurrent use contends on cargo locks and mixes artifacts from different sources at identical container paths; alternating use thrashes fingerprints and can surface stale artifacts.

The spec's ${devcontainerId} variable exists for exactly this — e.g. source=solx-${devcontainerId}-target.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Took ${devcontainerId} for the three target volumes (230b86f). I kept
rustup/cargo/ccache shared deliberately: cargo's registry is lock-protected,
and cross-checkout sharing of the LLVM ccache is the point of persisting it —
that's what makes a second checkout's build "minutes, not an hour".

Two things that came out of double-checking this:

  • sharing target-llvm across checkouts wouldn't even have worked: CMake bakes
    ${containerWorkspaceFolder} into the cache, and that path differs per
    checkout basename, so per-checkout is correct there regardless.
  • rustup is not concurrency-safe (rustup#988 — no cross-process locking), so
    the shared-volume caveat is now documented in the guide's Persistence section
    (e30ca90) rather than claimed away.

Comment thread .github/docker/Dockerfile Outdated

# ── Rust ─────────────────────────────────────────────────────────────────────
# Version must match rust-toolchain.toml in the solx repo.
ARG RUST_VERSION=1.93.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This violates the invariant stated right above it: rust-toolchain.toml pins 1.96.0, not 1.93.0. The baked default toolchain is dead weight — every CI job in the ci image re-downloads and installs 1.96.0 at its first cargo invocation — and the "1.93.0 → LLVM 21" rationale in the LLVM section rots silently with it.

Pre-existing, but this PR adds a lazily-resolving rustup in the dev stage precisely because baked pins go stale, and its new CI filter watches rust-toolchain.toml while nothing guards this pin. Bump to 1.96.0, or make the base install lazy the same way the dev stage does.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Bumped to 1.96.0 (dd903ad). The lazy-resolve alternative can't work in this
stage: the ci stage's cargo install runs at image build time with no checkout,
so there's no rust-toolchain.toml for rustup to resolve — the pin has to stay
concrete. To stop it drifting again, a renovate regex customManager now emits
the same rust/rust-version dependency as the built-in rust-toolchain
manager, so the ARG rides the same update PR (e5416bc; verified with a local
extraction dry-run).

Checking your premise also surfaced that the adjacent LLVM comment was already
wrong: rustc 1.96.0 embeds LLVM 22, not 21. The image's LLVM-21
llvm-cov/llvm-profdata still read its profraw output, so coverage works — the
comment now states that actual pairing and tells toolchain bumps to re-check it
(ab7f23e).

Comment thread .github/docker/Dockerfile Outdated
# Keeping it in the image (rather than ad-hoc installs) also keeps LLVM's
# cmake valgrind detection stable across container rebuilds; the persistent
# target-llvm build tree bakes it in.
ARG USERNAME=dev

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ARG USERNAME is dead parameterization: devcontainer.json hardcodes "remoteUser": "dev" and passes no build.args, so overriding this ARG only desyncs the two files — the image gets some other user while the devcontainer still remotes in as dev, and creation fails or falls back to root. Inline dev.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Inlined (dd903ad).

Comment thread .github/docker/Dockerfile
ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:${PATH}
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The dev image still carries base's full root-homed Rust install (~1.5 GB: rustup + 1.93.0 default-profile toolchain + rust-src in /root) that this stage's own comment declares unreachable — which is why a second rustup gets installed here. The only consumers of the /root install are in the ci stage (the cargo-tools build, and CI jobs running as root).

Moving base's Rust section (and the /root/.cargo/bin PATH component) into ci removes the dead weight from the image every developer builds locally; nothing else in base needs cargo. rust-src looks doubly vestigial: jobs run 1.96.0 per rust-toolchain.toml, whose default profile does not include it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Moved base's Rust section into ci (dd903ad) — agreed the split had it in the
wrong stage. One correction on the aside: rust-src isn't vestigial. The
build-rust and rust-unit-tests actions request it explicitly for -Zbuild-std;
they were installing it onto the resolved toolchain themselves, so only the
baked 1.93.0 copy was dead. With the pin now matching rust-toolchain.toml,
baking rust-src means jobs find it preinstalled instead of downloading it.

Comment thread .github/docker/Dockerfile Outdated
RUN curl -fsSL -o valgrind-${VALGRIND_VERSION}.tar.bz2 \
https://sourceware.org/pub/valgrind/valgrind-${VALGRIND_VERSION}.tar.bz2 \
&& tar -xf valgrind-${VALGRIND_VERSION}.tar.bz2 \
&& cd valgrind-${VALGRIND_VERSION} && ./configure && make && make install \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Still compiles single-threaded — make with no -j. Every CI-image build (monthly cron, every .github/docker change, amd64 and QEMU-emulated arm64) pays minutes of avoidable build time, worst under QEMU. make -j"$(nproc)".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done (dd903ad). Also moved the valgrind block above the Rust layer in the ci
stage (ab7f23e): its pin effectively never changes, while RUST_VERSION bumps
every few weeks and was invalidating the valgrind build for nothing.

devcontainer:
- '.devcontainer/**'
- '.github/docker/**'
- 'rust-toolchain.toml'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The filter does not cover the validation harness itself: a PR that only edits the validate-devcontainer job or bumps the devcontainers/ci pin in this file yields devcontainer == 'false', the job is skipped, and a broken harness merges — surfacing on the next unrelated devcontainer PR. Adding .github/workflows/test.yaml here costs a ~2-minute job on test.yaml edits.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added (6b2a9dc). One expectation-setting note: it's pricier than ~2 minutes —
the job builds the base+dev stages from scratch on the runner (no layer cache),
so more like 5–10. It runs in parallel and is never the long pole on a
test.yaml edit, so accepted as is.

The devcontainer is also the intended environment for hacking on `solx-llvm`: the fork is not built standalone — `solx-dev` owns the CMake configuration (in `solx-dev/src/llvm/`), and `solx-llvm`'s own regression CI drives its builds through a **solx** checkout in the same runner image.

1. Point the submodule at your branch: `git -C solx-llvm checkout <branch>` (after `git -C solx-llvm fetch --unshallow origin <branch>` if needed).
2. Rebuild: `./target/release/solx-dev llvm build --enable-assertions --enable-mlir --ccache-variant ccache`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This workflow cannot run the LLVM regression tests it exists to support: neither bootstrap.sh nor this rebuild command passes --enable-tests, so the build has LLVM_INCLUDE_TESTS=Off and LLVM_BUILD_UTILS=Off — no FileCheck, no llvm-lit, no check-* targets (solx-dev maps enable_testsLLVM_INCLUDE_TESTS and promotes it to enable_utils; both default off).

A developer following this section rebuilds their branch, has no way to run lit locally, and discovers breakage only in solx-llvm's CI — despite the paragraph above calling the container "the intended environment for hacking on solx-llvm". Document --enable-tests here (and/or add a bootstrap passthrough flag).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Documented --enable-tests in the rebuild command (b9a648e), plus the
-DLLVM_PARALLEL_LINK_JOBS='2' cap that bootstrap already uses — the full
toolset's uncapped links would OOM the guide's own 16 GB host minimum
(e30ca90). I passed on a bootstrap passthrough flag: by that point in the flow
you're invoking solx-dev directly anyway.

Comment thread docs/src/user-guide/01-installation.md Outdated
> Deployment and production use cases should rely only on [the officially released executables](#static-executables).

> For development, the repository also ships a [devcontainer](../developer-guide/00-development-container.md)
> that provides all dependencies below on the exact toolchain image CI uses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"the exact toolchain image CI uses" overclaims: the devcontainer builds the image locally from the same definition, and the developer guide itself says it is "not bit-for-bit identical to the frozen image CI pulls". A user chasing an apt-drift discrepancy gets sent the wrong way. Suggest: "the same toolchain image definition CI uses".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Taken as suggested (b9a648e).

nebasuke added 20 commits July 12, 2026 19:44
Adds a .devcontainer/ that layers a UID-mapped non-root user and a shared
rustup (official rust-image layout, toolchain resolved lazily from
rust-toolchain.toml) on top of ghcr.io/nomicfoundation/solx-ci-runner, so
local dev runs on the exact toolchain image CI uses.

The custom LLVM backend stays out of the image by design (it changes with
every solx-llvm bump and weighs GBs); instead bootstrap.sh wraps the same
solx-dev flow CI uses (submodules -> solx-dev -> llvm build --enable-mlir ->
solc build), kept out of postCreate so the ~1h cold build is an explicit,
documented step. Build state (target dirs, rustup/cargo, ccache) persists in
named volumes across container rebuilds; MLIR is on by default for the Slang
frontend pipeline; clangd is wired to the compile_commands.json that solx-dev
already exports for the solx-llvm submodule.

New Developer Guide page documents the LLVM install flow, the image-content
rationale, rebuild-after-bump, and a Slang-contributor orientation (no
Hermit; infra CLI role split between bootstrap.sh and solx-dev).
Adds a validate-devcontainer job to test.yaml (mirroring slang's), gated on
.devcontainer/**, .github/workflows/** (they pin the same runner image), and
rust-toolchain.toml. Cheapest checks first: the devcontainer's runner-image
digest must match the one pinned across workflows (enforcing the
KEEP-IN-SYNC repin note), shellcheck over the scripts, then the container is
built via the pinned devcontainers/ci action and .devcontainer/smoke-test.sh
runs inside: non-root user + passwordless sudo, writable workspace and named
volumes, system toolchain on PATH, executable scripts, and lazy resolution of
the rust-toolchain.toml pin. LLVM is deliberately not built.

The devcontainer filter is a separate paths-filter step because the existing
one uses predicate-quantifier 'every' (needed for its negation-style code
filter), which would AND the OR-list of positive patterns into never-true.

smoke-test.sh doubles as a user-facing health check, linked from the guide's
troubleshooting section.
…zard

Found by dogfooding the bootstrap end-to-end: the runner image ships no
system Boost (CI's build-solc action always passes --build-boost), so the
bare `solx-dev solc build` died at cmake's find_package(Boost). Pass
--build-boost (static Boost into solx-solidity/boost/, default 1.83.0
matching the action), --ccache-variant, and mirror the MLIR flag as
slang-tests.yaml does.

Also from the same run: a host-side branch switch mid-bootstrap broke the
build inside the container (shared bind mount). New troubleshooting bullet
covers it.
Found by dogfooding right after the first successful bootstrap: VS Code's
default watcherExclude has no Rust entries, so once target-llvm/build-final
exists (hundreds of thousands of files) the server crawls and watches it,
starving its event loop — observed as ~20s keystroke latency in integrated
terminals. Exclude all three cargo target dirs plus solc's build/boost trees
from both the watcher and search.
Dogfooding follow-up to the VS Code unresponsiveness: the fork's .gitignore
covers /build*/ but not the Boost trees solx-dev drops into the submodule
(boost/, boost_1_83_0/, boost_1_83_0.tar.gz — the extracted source tree is
tens of thousands of untracked files). VS Code's git extension enumerates
inside untracked dirs and drowns ("too many changes"), rescanning on every
FS event. Bootstrap now appends /boost* to the submodule's local
info/exclude (resolved via rev-parse — the module dir is internally named
"solidity", not "solx-solidity"), keeping git status clean without touching
the fork.
…op GHCR auth

Split .github/docker/Dockerfile into base/ci stages (pure reorder): base is
what a developer needs (system packages, LLVM-21 apt, libm/libstdc++ fixes,
rustup, node); ci adds the CI-only extras (valgrind from source, cargo
coverage/report tools — the expensive layers) and stays the final stage, so
docker-ci-image.yaml publishes an unchanged image.

The devcontainer now builds `--target base` locally via initializeCommand
(~5-10 min once, layer-cached after) instead of pulling the private
solx-ci-runner image: no registry auth at all, and the base tracks the
checkout's Dockerfile by construction — the digest KEEP-IN-SYNC problem
disappears, so the digest-sync CI step and the GHCR login step retire. The
validate-devcontainer path filter swaps .github/workflows/** (digest-sync
rationale) for .github/docker/** (the new dependency). Trade-off, documented:
a locally built base re-resolves apt, so it is not bit-for-bit the frozen CI
image; CI itself still pulls the frozen artifact.

Docs: auth prerequisite section replaced by the local-build story; new
troubleshooting bullet for a stale base layer cache.
… stage

Dogfooding found the initializeCommand seam busts the dev layer cache on
every rebuild: re-tagging solx-devcontainer-base can yield a new image ID
even on a full cache hit (containerd image store embeds provenance
timestamps), invalidating the child layers — the userdel/apt/rustup steps
re-ran on every container rebuild.

Move the dev layer (non-root user, shared rustup) into
.github/docker/Dockerfile as a `dev` stage on base, and point
devcontainer.json straight at it (dockerfile + target: dev). One build, one
cache domain: rebuilds are now cache hits end-to-end. .devcontainer/Dockerfile
and initializeCommand are gone; `ci` remains the final stage so the published
image is unchanged (buildx skips the dev stage when targeting ci).
…LLVM tree

Dogfooding the image redesign hit the class directly: target-llvm/ persists
in a volume, so CMake's cache carries toolchain facts from the image that
configured it — here HAVE_VALGRIND_VALGRIND_H=1 from the old runner image,
failing Valgrind.cpp in the valgrind-less dev stage.

- bootstrap.sh grows a --clean passthrough (wipes target-llvm before
  building) and the guide gets a symptom-indexed troubleshooting bullet.
- CCACHE_COMPILERCHECK=content in containerEnv (matches CI): an image
  rebuild refreshes mtimes on an unchanged clang, which would otherwise
  cold-miss the whole ccache under the default mtime check.
Restores capability parity with the CI image (LLVM regression tests under
valgrind work in the container) without the ci stage's from-source build of
a pinned version — apt is seconds and dev doesn't need CI's exact valgrind.
Just as important, it makes valgrind a stable fact of the image: LLVM's
cmake bakes valgrind detection into the persistent target-llvm build tree,
so ad-hoc installs that vanish on container rebuild flap the config (the
exact failure the previous commit documents).
The --no-mlir override was mentioned only under the raw solx-dev llvm
build command, where it isn't a valid flag (solx-dev takes a boolean
--enable-mlir; clap rejects --no-mlir). Surface --no-mlir on the
bootstrap.sh wrapper in Getting Started, and disambiguate the two
layers in How LLVM is installed.
Repin the pinned ghcr.io/nomicfoundation/solx-ci-runner digest to
sha256:90f34266...6f6c, the latest image built from this PR's Dockerfile.
Review fixes on the runner Dockerfile:

- The base-stage Rust install violated its own "must match
  rust-toolchain.toml" invariant (1.93.0 vs 1.96.0) and was dead weight
  in the dev image (~1.5 GB of /root-homed toolchain the non-root user
  cannot reach). Move it to the ci stage — its only consumers are the
  cargo-tools build and CI jobs running as root — and pin 1.96.0.
  rust-src stays: the -Zbuild-std actions request it explicitly.
- Inline the dead USERNAME parameterization: devcontainer.json
  hardcodes remoteUser dev and passes no build args.
- Build valgrind with -j$(nproc) instead of single-threaded.
- Fix the stale 1.93.0 reference in CLAUDE.md.
git submodule update unconditionally detaches every submodule back to
the recorded SHA, so rerunning bootstrap.sh after the guide's
fork-hacking step silently reset a manually checked-out solx-llvm
branch and rebuilt upstream LLVM. Skip any submodule sitting on a
branch (with a notice) and update the rest, preserving both the
submodule-bump rerun flow and the fork-hacking flow.

Also TODO the boost info/exclude workaround for removal once the
submodule pin includes the fork-side .gitignore fix (cherry-pick of
NomicFoundation/solx-solidity#111 onto 0.8.34).
The named target volumes were docker-global, so two checkouts or
worktrees opened as devcontainers on one machine mounted the same
cargo target trees: concurrent use contends on cargo locks and mixes
artifacts from different sources; alternating use thrashes
fingerprints. Suffix them with ${devcontainerId}.

The rustup/cargo/ccache volumes stay shared across checkouts on
purpose: they are concurrency-safe caches, and sharing the LLVM
ccache across checkouts is the point of persisting it.
…self

A PR editing only the validate-devcontainer job or the devcontainers/ci
pin skipped its own validation, so a broken harness would merge and
surface on the next unrelated devcontainer PR. Add test.yaml to the
filter; the cost is one extra parallel job on test.yaml edits.
- The fork-hacking rebuild command built with LLVM_INCLUDE_TESTS=Off,
  leaving no FileCheck/llvm-lit/check-* targets to run the regression
  suite locally. Document --enable-tests and note bootstrap.sh reruns
  no longer reset a submodule left on a branch.
- The installation guide claimed the devcontainer is "the exact
  toolchain image CI uses"; it is built locally from the same
  definition and the developer guide itself disclaims bit-for-bit
  identity.
…bmodules

Review follow-ups on the bootstrap submodule loop:

- A detached checkout of a pinned commit (verification, bisect) was
  still silently reset — it is indistinguishable from a stale
  post-bump tree, so it is still reset, but now announced; a branch
  remains the way to protect a checkout.
- A skipped (branched) submodule no longer skips its nested
  submodules: solx-solidity carries three under deps/, and they now
  follow whatever the developer checked out.
- </dev/null on the update calls so a git prompt can never swallow
  the loop's remaining input.
- rustc 1.96.0 embeds LLVM 22, not 21 (rustc -vV); the LLVM section
  comment claimed a version match that does not exist. State the real
  pairing: the LLVM 21 tools still read 1.96's profraw format, and the
  pairing must be re-checked on every toolchain bump.
- Order valgrind above Rust in the ci stage: the valgrind pin
  effectively never changes, while RUST_VERSION bumps every few weeks
  and was invalidating the valgrind layer for nothing.
- Trim the Rust-placement rationale to one canonical comment (the ci
  stage's) instead of three cross-referencing copies.
…ription

- rustup has no cross-process locking (rust-lang/rustup#988), so the
  volume comment's "concurrency-safe caches" overclaimed; the docs
  Persistence section now carries the accurate rationale (cargo and
  ccache lock; rustup first-run races are possible) and the json
  comment shrinks to a pointer.
- "What the image contains" still listed rustup in the base stage and
  described ci without Rust — stale since the base→ci move.
- The fork-hacking rebuild command gains the -DLLVM_PARALLEL_LINK_JOBS
  cap bootstrap already uses (--enable-tests links the full toolset;
  uncapped links OOM a 16 GB minimum host) and documents the
  detached-checkout reset behavior.
- Deduplicate the volume-cleanup command into one place.
The built-in rust-toolchain manager only bumps rust-toolchain.toml,
which is how the Dockerfile's ARG RUST_VERSION drifted 1.93.0→1.96.0
across three toolchain updates. A regex customManager emitting the
same `rust` dependency folds the ARG into the same update PR.
@nebasuke

nebasuke commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

Build of fixed up container: https://github.com/NomicFoundation/solx/actions/runs/29209314411

nebasuke added 2 commits July 12, 2026 21:04
…clude

The 0.8.34 branch now carries the boost ignore entries
(NomicFoundation/solx-solidity#158, cherry-pick of #111), so
bootstrap.sh no longer needs to append /boost* to the submodule's
info/exclude. One-commit bump: 5323a55dd → 79519a9bb contains exactly
that fix.
@nebasuke nebasuke marked this pull request as draft July 13, 2026 08:25
…es them

The branch-vs-detached heuristic existed to guess whether a divergent
submodule checkout was deliberate or stale, because the bootstrap updated
unconditionally. Making the post-bump refresh an explicit step
(git submodule update --init --recursive --depth 1, now documented in the
guide) removes the ambiguity: the bootstrap fetches only never-initialized
submodules and prints a warning with that command when an existing checkout
differs from the recorded commit.

Copilot AI 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.

Pull request overview

This PR adds a VS Code devcontainer setup that locally builds the same toolchain image definition used by CI (without needing access to the private GHCR runner image), documents the LLVM/solc build bootstrap flow, and adds CI validation for devcontainer changes while updating the CI runner image digest/toolchain references.

Changes:

  • Split the runner Dockerfile into base/dev/ci stages and wire the devcontainer to build --target dev locally.
  • Add .devcontainer/ scripts + a smoke test and a new Developer Guide page describing bootstrap/install/persistence.
  • Add a devcontainer validation job in CI and update runner image digests across workflows; add a Renovate rule to keep the Dockerfile Rust ARG in sync with rust-toolchain.toml.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
renovate.json Adds a custom regex manager to keep ARG RUST_VERSION in the runner Dockerfile in sync with rust-toolchain.toml.
docs/src/user-guide/01-installation.md Adds a user-guide pointer to the devcontainer documentation for development installs.
docs/src/SUMMARY.md Adds the new “Development Container” page to the Developer Guide sidebar.
docs/src/developer-guide/00-development-container.md Introduces detailed devcontainer/LLVM+solc bootstrap docs, troubleshooting, and persistence model.
CLAUDE.md Updates the documented Rust toolchain version to 1.96.0.
.github/workflows/test.yaml Adds path-gated validate-devcontainer job and updates CI runner image digest.
.github/workflows/slang-tests.yaml Updates CI runner image digest used for Slang tests.
.github/workflows/sanitizer.yaml Updates CI runner image digest for sanitizer runs.
.github/workflows/release.yaml Updates CI runner image digest used for Linux release builds/bundling.
.github/workflows/integration-tests.yaml Updates CI runner image digest for integration tests.
.github/workflows/coverage.yaml Updates CI runner image digest for coverage job.
.github/workflows/compile-benchmark.yaml Updates CI runner image digest for benchmark compile workflow.
.github/workflows/cache-warmup.yaml Updates CI runner image digest for cache warmup workflows.
.github/docker/Dockerfile Refactors into base/dev/ci, moves Node into base, adds devcontainer user + shared rustup, and bumps CI Rust to 1.96.0.
.devcontainer/smoke-test.sh Adds a fast health check script for devcontainer correctness (non-root, mounts writable, tools present, Rust pin resolves).
.devcontainer/post-create.sh Adds post-create setup for volume ownership + git safe.directory configuration and prints next steps.
.devcontainer/devcontainer.json Defines the devcontainer build (targeting dev stage), mounts, env, and VS Code excludes/extensions.
.devcontainer/bootstrap.sh Adds a bootstrap wrapper to init submodules and build LLVM + solc via solx-dev, with MLIR/clean flags and ccache config.

Comment thread .github/docker/Dockerfile
Comment thread .devcontainer/post-create.sh
@nebasuke nebasuke marked this pull request as ready for review July 13, 2026 11:22
@nebasuke nebasuke requested a review from hedgar2017 July 13, 2026 11:22
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