chore: devcontainer on the CI runner image, with LLVM install docs and CI validation#525
chore: devcontainer on the CI runner image, with LLVM install docs and CI validation#525nebasuke wants to merge 23 commits into
Conversation
|
Build the Docker container: https://github.com/NomicFoundation/solx/actions/runs/29089933569/job/86352587385 |
| ccache --set-config=max_size=20G | ||
|
|
||
| echo "==> Initializing submodules (shallow, as in CI)" | ||
| git submodule update --init --recursive --depth 1 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 underdeps/) - 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.
There was a problem hiding this comment.
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.
| # 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| // 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", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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-llvmacross 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.
|
|
||
| # ── Rust ───────────────────────────────────────────────────────────────────── | ||
| # Version must match rust-toolchain.toml in the solx repo. | ||
| ARG RUST_VERSION=1.93.0 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
| # 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 |
There was a problem hiding this comment.
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.
| 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 \ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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 \ |
There was a problem hiding this comment.
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)".
| devcontainer: | ||
| - '.devcontainer/**' | ||
| - '.github/docker/**' | ||
| - 'rust-toolchain.toml' |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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`. |
There was a problem hiding this comment.
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_tests → LLVM_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).
There was a problem hiding this comment.
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.
| > 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. |
There was a problem hiding this comment.
"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".
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.
|
Build of fixed up container: https://github.com/NomicFoundation/solx/actions/runs/29209314411 |
…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.
…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.
There was a problem hiding this comment.
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/cistages and wire the devcontainer to build--target devlocally. - 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. |
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:
apt getinstallations, 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/cistage split in.github/docker/Dockerfile—baseis the shared system toolchain (system packages, LLVM-21 apt, libm/libstdc++ fixes, Node);devadds the devcontainer bits on top (UID-mapped non-rootdevuser with sudo, shared rustup at/usr/local/{rustup,cargo}with the toolchain resolved lazily fromrust-toolchain.toml, apt valgrind — dev needs a working valgrind, not thecistage's pinned from-source build, and keeping it in the image stops LLVM's baked-in cmake valgrind detection from flapping across rebuilds);ciadds the CI-only extras (the root-homed Rust install CI jobs use — pinned 1.96.0, kept in lockstep withrust-toolchain.tomlby a new renovate customManager — plus valgrind from source and cargo coverage/report tools) and stays the final stage, so the publishedsolx-ci-runnerimage is unchanged (docker-ci-image.yamlbuilds the default target, and buildx skips thedevstage when targetingci).--target devdirectly (devcontainer.jsonpoints at the runner Dockerfile) — ~5–10 min the first time, layer-cached afterwards. All sources are public, so the privatesolx-ci-runnerpackage 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 viainitializeCommand, 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 fortarget/,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);hostRequirementsfor the LLVM build (8 CPU / 16 GB / 64 GB); rust-analyzer + clangd extensions, clangd pointed at thecompile_commands.jsonsolx-devexports intotarget-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) → buildsolx-dev→solx-dev llvm build --enable-assertions --enable-mlir --ccache-variant ccache→solx-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 forcargo test-slang;--no-mlirto opt out);--cleanwipestarget-llvm/first, for build trees configured in a previous container image (CMake caches toolchain facts).CCACHE_COMPILERCHECK=contentmatches CI so image rebuilds with an unchanged clang don't cold-miss the cache. Deliberately not run frompostCreateCommand: the cold build is ~1h and should be a step you choose, not a surprise.post-create.shonly does container plumbing and prints the pointer. (An earlier revision git-excluded the Boost treessolx-devdrops into thesolx-soliditysubmodule; the fork's own.gitignorenow covers them — see the submodule bump below.)validate-devcontainerjob intest.yaml(mirroring slang's) — path-gated on.devcontainer/**,.github/docker/**(the devcontainer builds its base stage from it),rust-toolchain.toml, andtest.yamlitself (so edits to the validation harness get validated too). Shellchecks the scripts, then builds the container via the pinneddevcontainers/ciaction — same auth-free local-build flow as a developer machine — and runs.devcontainer/smoke-test.shinside: non-root user + sudo, writable workspace/volumes, system toolchain on PATH, lazy resolution of therust-toolchain.tomlpin. 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 thesolx-llvmfork from inside the container, and an orientation section for Slang contributors (no Hermit;infraCLI role split betweenbootstrap.shandsolx-dev).Why
A natural question from contributors coming from Slang is whether
solx-ci-runnercan be used as a devcontainer. As-is it can't, for non-obvious reasons: the package is private (pull needs aread:packagestoken), rustup is homed in/rootwith 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 withllvm-sysbuild-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 --release→cargo test-slang); the Boost fix, the watcher excludes, the submodule git-exclude, and most of the troubleshooting section are findings from that run.Notes
base(it was ~1.5 GB of/root-homed toolchain the dev user couldn't reach) intoci, 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.base, CI-only tooling inci(noted in the Dockerfile header).solx-ci-runneris 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.target-llvm/target-finalartifact from main (reusing the existingbuild-llvmcache key) so the bootstrap can download instead of building — would cut first-open from ~1h to minutes.solx-llvmdeclaring the solx devcontainer the canonical dev environment for the fork (its regression CI already builds through a solx checkout).— 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/boost*in thesolx-solidityfork's own.gitignore0.8.34), merged; this PR bumps the submodule one commit (5323a55dd→79519a9bb, exactly that fix) and drops the bootstrap's local-exclude workaround.basestage itself, thinning the devcontainer layer.