Fix the broken binary install; resolve the runtime from a prebuilt library - #317
Merged
Conversation
A prebuilt runtime library carries the full feature set, so it satisfies any subset of it. resolve_runtime_lib_dir consulted a prebuilt library only when the requested features equalled the full set exactly, but that set is derived from a program's imports - so any program not importing the entire std fell through to a source build and failed with "Could not locate mux-runtime source in cargo registry". That is every program on a binary install. The release tarball and scripts/install.sh ship bin/mux and lib/libmux_runtime.* with no runtime source, so a stock v0.6.0 install could not compile hello world. Local checkouts never saw it: a sibling ../mux-runtime, a cargo registry copy, and .cargo/config.toml's MUX_RUNTIME_LIB each mask the failure. A strict subset still prefers an exact-feature build when runtime source is available, so development builds keep unused std dependencies out of linked programs. The prebuilt library is the fallback, not the default, and a feature outside the full set still requires a source build. Add a packaged_artifact CI job covering the layout no other job does: it stages bin/ and lib/, moves the runtime checkout aside so the baked-in sibling path cannot mask the failure, and compiles with an empty CARGO_HOME.
The compiler shells out to a matching clang to link every program, but install.sh only checked for curl and tar. A user could complete the documented `curl | sh` install and then hit a linker error on their first program, with nothing in the install output pointing at the cause. Run `mux doctor` at the end of both installers. In user mode it checks clang and the runtime and already prints the per-platform install command for whatever is missing, so the gap surfaces while the user is still looking at the installer. A failing check is reported, not fatal: the install itself succeeded. install.ps1 needs one guard for this. Under `$ErrorActionPreference = "Stop"`, PowerShell 7.4+ turns a nonzero native exit code into a terminating error, which would abort the script instead of printing the guidance, so the doctor call opts out of that behavior and restores it afterwards.
Replace the `[patch.crates-io]` stopgap with a real git dependency on muxlang/mux-runtime `main`. A patch only applies to the workspace that declares it, so it was never going to survive publication; the compiler and the runtime are ABI-coupled and change together, so resolving from source is the honest expression of that. Cargo.lock still pins the exact commit, and it is byte-identical to what the patch already produced - `--locked` builds, CI and release tags included, resolve as before. Stamp the locked commit into MUX_RUNTIME_VERSION as semver build metadata (`0.5.0+g4e2dc14`, shown by `mux version`). A git-sourced runtime keeps one version across every commit, and MUX_RUNTIME_VERSION keys the runtime build cache - which reuses a library built from an immutable source without any freshness check. Without the commit in the key, two different runtimes share a cache entry and a program can link a stale one. ADR 0003 called this out; this implements it. Teach find_runtime_source about cargo's git checkouts, laid out as CARGO_HOME/git/checkouts/<repo>-<hash>/<short commit>, since the registry unpack a crates.io dependency produced no longer exists. Rename RuntimeSource::Registry to Immutable, which now covers both, and reword the failure message that named the registry specifically. Drop MUX_RUNTIME_SRC from release.yml: it pointed at a directory that workflow never creates, so it only produced a spurious warning and masked the real resolution path.
Cargo.lock pins one mux-runtime commit, which is what makes `--locked` builds reproducible - but it also means main stops tracking the runtime as soon as the runtime merges anything. This advances the pin on a schedule and opens a PR, so main keeps meaning "the most recent changes" without loosening the lock, and without pushing to a default branch. The job runs the test suite itself before opening the PR. A pull request created with GITHUB_TOKEN does not trigger other workflows, so the PR would otherwise arrive untested. When the suite fails the PR is opened as a draft naming the commit that broke it, which is the whole point: a runtime change that breaks the compiler surfaces against that change instead of on the next unrelated branch.
CONTRIBUTING.md never mentioned mux-runtime at all - the git dependency made it invisible, so nothing told a contributor that a separate repo was involved, that they do not need to clone it, or what to do when a change needs both sides. Add a section covering all three, plus the resolution order, because the top entries silently shadow the bottom ones: a stale sibling checkout or a leftover MUX_RUNTIME_LIB in .cargo/config.toml wins over the commit Cargo.lock names, and the mismatch surfaces as a confusing link error rather than as anything pointing at the cause. Fix two stale claims in AGENTS.md. Cargo builds the runtime as a dependency, not as a workspace member - the root Cargo.toml explicitly excludes it. And the project structure listed mux-runtime/src/ and mux-website/docs/ as directories in this tree; both are separate repos.
"No Rust or LLVM required" was misleading for the prebuilt install: the compiler invokes clang to link every program it builds, so clang matching the linked LLVM major is required to run anything. Say so, and point at the `mux doctor` check the installers now run automatically. Mark the crates.io option frozen and drop its version badge, which would otherwise keep advertising 0.6.0 as the current way to install.
Missed two occurrences in the previous commit. Clap rejects --version; the real --help has no version flag. Also correct the runtime line, which still described independent versioning rather than the pinned git dep.
Nothing ran install.sh or install.ps1, so a syntax error in either would ship silently and surface only when a user piped it into their shell - which matters more now that both scripts gained logic. Parse and analyze install.ps1 with pwsh, which is preinstalled on the Linux runner, and parse install.sh with shellcheck. Both verified against the current scripts: install.ps1 parses and is analyzer-clean at Error severity, and install.sh is shellcheck-clean at warning severity. Info severity only reports SC2016 on the awk program, where the single quotes are deliberate. Two details worth keeping. PSScriptAnalyzer is imported explicitly with -ErrorAction Stop: without it, a module that fails to load leaves the findings variable null and the step passes green having analyzed nothing, which is exactly what happened while testing this. And it is pinned to 1.22.0, because newer releases require pwsh 7.4.6+ while runner images can still be on 7.4.2.
It solved a problem already covered three ways. mux-runtime's CI builds mux-compiler main against the runtime source, and this repo's build.yml tests against runtime main on every PR, so an FFI break surfaces from both directions without the pin moving (ADR 0003). A coupled change moves the pin itself via `cargo update -p mux-runtime`, and a release settles it deliberately. A lock that trails runtime main between those moments costs nothing - that is what a lock is for. What remained was a daily CI run, PR noise, and a token question, for freshness nothing was waiting on. Reword the CONTRIBUTING section that pointed at it.
The CI leak job set MUX_RUNTIME_FEATURES and relied on the compiler to build a matching runtime from source. That made it the only thing depending on compiler-side feature builds, which is machinery worth removing: static linking already discards unreferenced archive members, so a feature-trimmed runtime produces a byte-identical binary to the full one. Build the leak runtime with an explicit cargo invocation in the runtime checkout and force it with MUX_RUNTIME_LIB, exactly as scripts/leak-check.sh already does locally. rc-leak-check sits outside `full` on purpose, so it stays the one runtime the compiler cannot produce on its own - which is why forcing it is the right mechanism rather than an accident. Drop the now-redundant MUX_RUNTIME_FEATURES export from leak-check.sh: MUX_RUNTIME_LIB is consulted first and returns immediately, so the features never affected resolution there. Reword the header comment, which explained the footgun in terms of a stale workspace-member build rather than the real reason.
The compiler built a feature-trimmed runtime on the user's machine so a program linked only the std features it imported. Measured, that buys nothing: static linking already discards archive members nothing references. Hello world is 16K against the trimmed runtime and 16K against the full one, which contains SQLite, Postgres, MySQL and an HTTP stack. Programs that do use those features link them either way (sqlite 47K, http 27K) and run identically. So the trimming was paying a large complexity bill for a saving that does not exist. Gone with it: the runtime build cache and its staleness fingerprints, the cargo-registry and git-checkout source lookups, the per-program feature sets, and a five-level resolution order whose upper entries silently shadowed the lower ones. That order is what let a release ship that could not compile hello world - every developer machine had a sibling checkout masking it. Runtime resolution is now three rules, all prebuilt: MUX_RUNTIME_LIB, then the library beside the binary, then the one cargo built into target/. MUX_RUNTIME_LIB stays first so scripts/leak-check.sh can force the rc-leak-check runtime, which sits outside `full` and is the one build the compiler cannot produce. CI drops the same apparatus. rust_checks, valgrind and benchmarks no longer check out mux-runtime or clear a cache that no longer exists: with a git dependency the locked commit is what a release ships, so linking anything else tests a configuration that never ships. FFI breaks against runtime `main` are still caught by mux-runtime's own CI, which builds this repo's `main` against its source. Net 637 lines out of main.rs. The vestigial feature tracking left in semantics/ now feeds nothing and should follow separately.
CONTRIBUTING described a five-level order that no longer exists, and AGENTS explained the rc-leak-check footgun in terms of a feature-built runtime being shadowed by a plain one - a mechanism that is also gone. Both now describe what the compiler actually does: three prebuilt sources, no source builds, always the full feature set. The rc-leak-check note is restated in the terms that survive - the feature sits outside `full`, so forcing the archive via MUX_RUNTIME_LIB is the only way to run the assertion at all.
With the compiler always linking the full runtime, nothing consumes the set of features a program requires. The analyzer still computed it: every std import inserted its module's features into a HashSet that was merged across submodule analyzers and then read by no one. Delete the chain end to end - the field and its initializers, the per-import tracking, the wildcard and namespace insertion loops, and the `runtime_features` column on StdModuleDef, which reduces the registry to what it is now for: which std modules exist and how each is implemented. `import_all_std_wildcard` loses its registry parameter along with it. Behavior is unchanged: full suite green, and std.math, std.sql and std.net.http still resolve and run from a packaged install, which is what the registry change could plausibly have broken.
The compiler no longer reads MUX_RUNTIME_SRC, so setting it in the compose file and the Dockerfile pointed the container at a path that no longer means anything - and, with the mux-runtime checkout gone from the job, at a path that no longer exists. The container needs none of it. CARGO_TARGET_DIR puts the runtime cargo builds as a dependency in /workspace/.docker-target, and the spawned mux binary sits in debug/ beside the uplifted libmux_runtime.a, which the second resolution rule finds. rc_leak_check is now the only job that checks out mux-runtime, because it genuinely needs source to build an archive with a feature outside `full`. Also fix a leak-check.sh comment still naming MUX_RUNTIME_FEATURES for the CI job; that variable is now MUX_RUNTIME_LEAK_FEATURES.
Removing the source-build path left a hole: cargo emits only a dependency's rlib, never its staticlib, so `cargo build -p mux-lang` alone produces no libmux_runtime.a and every compiled program fails to link with undefined references to core runtime symbols. Verified directly in a clean target directory - `-p mux-lang` leaves the archive absent, `-p mux-runtime -p mux-lang` produces it and hello world runs. Every local check I ran had missed this because a gitignored .cargo/config.toml sets MUX_RUNTIME_LIB, which is the first thing runtime resolution consults. That is the exact footgun CONTRIBUTING now warns about, and it hid a broken build from every check that was supposed to catch it. The Docker integration suite is what caught it, because it runs in an environment nothing else reproduces. Build both packages in run-checks.sh and valgrind-checks.sh - which covers the integration container and CI's Rust job, since both run run-checks.sh - and add an explicit step to the benchmarks job. The service suite needed its own fix: it compiles through `cargo run` with CARGO_TARGET_DIR=target/service-integration, so the compiler it builds looks for the archive in that directory rather than the main one. It now builds the runtime there once, guarded by a OnceLock. Those tests no-op unless MUX_RUN_SERVICE_TESTS=1, so their earlier "4 passed" was four tests doing nothing; they now genuinely compile and run against the live services. Correct the runtime-not-found message too: it said to run `cargo build`, which does not produce the archive. It now names `cargo build -p mux-runtime` and says why.
The integration image has no COPY or ADD - it installs a toolchain, and the repo arrives at runtime as a volume mount - so its build context is never read. Without a .dockerignore, docker still uploads one, and that is the entire working tree: target/ alone reaches tens of gigabytes here, plus the .docker-cache directories. A local run was still streaming context past 2.2GB when I stopped it. Ignoring everything is safe precisely because nothing in the context is consumed, and it takes the upload to a few kilobytes. Pre-existing, not introduced by the runtime work, but it slows every integration run in CI.
Review of the branch turned up three stale claims, all written before the source-build path came out. build.rs justified the commit stamp by the runtime build cache keying off it, and justified the seven-character length by cargo's git checkout directory names. Both mechanisms are gone. The stamp's purpose now is that `mux version` identifies which runtime a binary was built against. The worse one: CONTRIBUTING and AGENTS both described the third resolution rule as "the library cargo built into target/", with no mention that a plain `cargo build` does not produce it. That is precisely the trap that shipped a compiler unable to link anything, so the docs as written led straight back into it. Both now name `cargo build -p mux-runtime` and say why, and point at the link error as the symptom.
Quality pass over the branch, from the reuse and altitude angles. runtime_lib_from_build_config re-implemented the static-then-dynamic decision that find_runtime_lib_in_dir owns, despite runtime_static_lib_path documenting itself as the single source of the library filenames. It also disagreed with it: build.rs records `.so` as the dynamic path on macOS while runtime_dynamic_lib_path correctly uses `.dylib`, so the fallback was dead there. It now asks about the recorded directory instead, which puts the platform names back in one place. runtime_lib_near_executable repeated `find_runtime_lib_in_dir(d).and_then( |p| p.parent())` in two nested blocks, plus an `exists()` guard that find_runtime_lib_in_dir already performs. The containing directory is the one passed in, so dir_holding_runtime_lib says that directly and the search becomes a find_map over three candidate directories. That left MUX_RUNTIME_DYNAMIC and MUX_RUNTIME_DIR unread, so build.rs no longer emits them and detect_runtime_library returns the single path its caller uses. It still probes both libraries internally, because either one identifies the profile directory.
This was referenced Jul 29, 2026
Contributor
|
| Filename | Overview |
|---|---|
| .github/workflows/build.yml | Pins the previously mutable action references and adds packaged-install, installer, runtime-build, and leak-check coverage. |
| mux-compiler/src/main.rs | Replaces source builds and feature-specific runtime caching with prebuilt-library resolution. |
| mux-compiler/build.rs | Records the locked runtime commit in version metadata and simplifies generated runtime-library configuration. |
| mux-compiler/Cargo.toml | Makes mux-runtime an explicit Git dependency whose exact revision remains pinned by Cargo.lock. |
| scripts/install.sh | Updates the Unix installer for the packaged runtime layout and adds post-install diagnostics. |
| scripts/install.ps1 | Updates the Windows installer for the packaged runtime layout and adds post-install diagnostics. |
Reviews (2): Last reviewed commit: "Address PR review: lock the leak-runtime..." | Re-trigger Greptile
Valgrind Memory Checks - PASSEDpie showData
title Leg A: compiled programs under Valgrind (113 total)
"Clean" : 113
"Leaking" : 0
Full valgrind output (last 300 lines)Benchmarks (report-only) - report-onlyInformational only - shared runners are too noisy to gate on; full data is in the run artifacts. xychart-beta
title "Compile phases (median)"
x-axis ["lex", "parse", "semantics", "codegen"]
y-axis "microseconds"
bar [10.65, 26.19, 80.22, 347.1]
xychart-beta
title "Pipeline + execution (median)"
x-axis ["pipeline", "execution"]
y-axis "milliseconds"
bar [0.521, 53.01]
Full benchmark output (last 300 lines)Commit |
…e new helpers Three items from the first CI round on #317. Sonar githubactions:S8549 (the only new issue, and what dropped the security rating to C): the rc-leak-check runtime was built without `--locked`. That step runs in a clean checkout of mux-runtime with its committed lock, so resolving fresh would build something the repo never pinned. Added `--locked`. Greptile flagged the new jobs for using the movable `actions/checkout@v4` while the release workflow pins a SHA. Its premise is right but its framing was not: every checkout in build.yml used `@v4`, so pinning only the two new jobs would have left two SHAs among nine tags. Pinned all 19 first-party action references in the file to the commits `v4` currently resolves to, keeping the tag in a trailing comment for readability. Coverage on new code was 77.4% against an 80% gate. I removed eight tests with the source-build path and added none back, so this covers the helpers that replaced them: dir_holding_runtime_lib returning the directory it was asked about rather than the found file's parent, and the release install layout - the bin/ plus ../lib/ shape whose library was present but unreachable in v0.6.0.
|
Valgrind Memory Checks - PASSEDpie showData
title Leg A: compiled programs under Valgrind (113 total)
"Clean" : 113
"Leaking" : 0
Full valgrind output (last 300 lines)Benchmarks (report-only) - report-onlyInformational only - shared runners are too noisy to gate on; full data is in the run artifacts. xychart-beta
title "Compile phases (median)"
x-axis ["lex", "parse", "semantics", "codegen"]
y-axis "microseconds"
bar [13.67, 30.66, 101.7, 447]
xychart-beta
title "Pipeline + execution (median)"
x-axis ["pipeline", "execution"]
y-axis "milliseconds"
bar [0.6637, 68.13]
Full benchmark output (last 300 lines)Commit |
This was referenced Jul 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Closes #316. Fixes a release that could not compile hello world.
The bug
The published v0.6.0 tarball, installed exactly as
scripts/install.shlays it out, could not compileprint("hello"):resolve_runtime_lib_dirconsulted a prebuilt library only when a program's required features equalled the full set exactly. That set is derived from a program's imports, so any program not importing the entire std fell through to a source build - and a binary install ships no runtime source. The bundledlibmux_runtime.awas sitting in../libthe whole time.Every developer machine masked it three ways: a sibling
../mux-runtime, a cargo registry copy, andMUX_RUNTIME_LIBin a gitignored.cargo/config.toml.The fix, and why it went further
A prebuilt library carries the full feature set, so it satisfies any subset. But measuring the feature trimming showed it buys nothing: static linking already discards archive members nothing references. Hello world is 16K against the trimmed runtime and 16K against the full one, which contains SQLite, Postgres, MySQL and an HTTP stack. Programs that do use those link them either way (sqlite 47K, http 27K).
So the whole apparatus came out: the runtime build cache and its staleness fingerprints, the cargo-registry and git-checkout source lookups, per-program feature sets, and the five-level resolution order whose upper entries silently shadowed the lower ones. Net ~590 lines out of
main.rs.Resolution is now three rules, all prebuilt:
MUX_RUNTIME_LIB, the library beside the binary, the one intarget/.Other changes
[patch.crates-io]-> a real git dependency (closes [Chore] - Drop the mux-runtime [patch.crates-io] git patch and pin a published version before release #316).Cargo.lockis byte-identical - the patch already resolved to a git source.MUX_RUNTIME_VERSIONnow carries the locked commit (0.5.0+g4e2dc14, shown bymux version) so a bug report identifies its runtime.mux doctor. The compiler shells out to clang to link every program, butinstall.shonly checked forcurlandtar, so a clean install failed at first compile with nothing pointing at the cause.packaged_artifactruns the compiler from the bin/lib layout no other job covers - it fails against the released v0.6.0 binary and passes here.installer_scriptsparses both installers, which nothing did.cargo build -p mux-runtimeeverywhere programs are compiled. Cargo emits a dependency's rlib and never its staticlib, so removing the source-build path left a hole the Docker suite caught.Verification
Full suite green (169 tests, 11 binaries). clippy
-D warningsandfmtclean.leak-check.shreportsall programs clean. Docker integration suite passes including the four live service tests. Packaged install compiles and runs hello world,std.math,std.sqlandstd.net.http.Not run:
/code-review(user-triggered only) and CI itself -packaged_artifactandinstaller_scriptsare new, andrust_checks,valgrind,benchmarksandrc_leak_checkall changed, so expect a fixup pass on first run.Depends on the rationale in muxlang/mux-context#32.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.