diff --git a/.github/scripts/id-pr-checks.txt b/.github/scripts/id-pr-checks.txt index af8e56652..856883def 100644 --- a/.github/scripts/id-pr-checks.txt +++ b/.github/scripts/id-pr-checks.txt @@ -37,6 +37,7 @@ test-shard-3 test-web-unit test-web-typecheck roc-test +roc-examples crash-gate-sampled crash-gate-sweep crash-gate-control diff --git a/pkgs/id/apps/tic-tac-toe/program/build.sh b/pkgs/id/apps/tic-tac-toe/program/build.sh index fb2bb6d87..02a4261c8 100755 --- a/pkgs/id/apps/tic-tac-toe/program/build.sh +++ b/pkgs/id/apps/tic-tac-toe/program/build.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" -ROC="${ROC:-roc}" +export ROC="${ROC:-roc}" +source ../../../examples/roc-toolchain.sh if [ -z "${WASM_TOOLS:-}" ]; then if command -v wasm-tools >/dev/null 2>&1; then WASM_TOOLS="wasm-tools" @@ -23,7 +24,7 @@ if [ -z "${WASM_OPT:-}" ]; then if [ -n "$STORE_OPT" ] && [ -x "$STORE_OPT" ]; then WASM_OPT="$STORE_OPT" else - WASM_OPT="nix run nixpkgs#binaryen --" + WASM_OPT="nix shell nixpkgs#binaryen --command wasm-opt" fi fi fi diff --git a/pkgs/id/examples/lounge/build.sh b/pkgs/id/examples/lounge/build.sh index 9df83f291..9cf695cbf 100755 --- a/pkgs/id/examples/lounge/build.sh +++ b/pkgs/id/examples/lounge/build.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" -ROC="${ROC:-roc}" +export ROC="${ROC:-roc}" +source ../roc-toolchain.sh if [ -z "${WASM_TOOLS:-}" ]; then if command -v wasm-tools >/dev/null 2>&1; then WASM_TOOLS="wasm-tools" @@ -23,7 +24,7 @@ if [ -z "${WASM_OPT:-}" ]; then if [ -n "$STORE_OPT" ] && [ -x "$STORE_OPT" ]; then WASM_OPT="$STORE_OPT" else - WASM_OPT="nix run nixpkgs#binaryen --" + WASM_OPT="nix shell nixpkgs#binaryen --command wasm-opt" fi fi fi diff --git a/pkgs/id/examples/roc-chat/build.sh b/pkgs/id/examples/roc-chat/build.sh index 14f1e3217..3377c5c4d 100755 --- a/pkgs/id/examples/roc-chat/build.sh +++ b/pkgs/id/examples/roc-chat/build.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" -ROC="${ROC:-roc}" +export ROC="${ROC:-roc}" +source ../roc-toolchain.sh if [ -z "${WASM_TOOLS:-}" ]; then if command -v wasm-tools >/dev/null 2>&1; then @@ -24,7 +25,7 @@ if [ -z "${WASM_OPT:-}" ]; then if [ -n "$STORE_OPT" ] && [ -x "$STORE_OPT" ]; then WASM_OPT="$STORE_OPT" else - WASM_OPT="nix run nixpkgs#binaryen --" + WASM_OPT="nix shell nixpkgs#binaryen --command wasm-opt" fi fi fi diff --git a/pkgs/id/examples/roc-counter/build.sh b/pkgs/id/examples/roc-counter/build.sh index 2599dfc53..a54b431ef 100755 --- a/pkgs/id/examples/roc-counter/build.sh +++ b/pkgs/id/examples/roc-counter/build.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" -ROC="${ROC:-roc}" +export ROC="${ROC:-roc}" +source ../roc-toolchain.sh WASM_TOOLS="${WASM_TOOLS:-nix run nixpkgs#wasm-tools --}" if [ -z "${WASM_OPT:-}" ]; then @@ -12,7 +13,7 @@ if [ -z "${WASM_OPT:-}" ]; then if [ -n "$STORE_OPT" ] && [ -x "$STORE_OPT" ]; then WASM_OPT="$STORE_OPT" else - WASM_OPT="nix run nixpkgs#binaryen --" + WASM_OPT="nix shell nixpkgs#binaryen --command wasm-opt" fi fi fi diff --git a/pkgs/id/examples/roc-grid/build.sh b/pkgs/id/examples/roc-grid/build.sh index 43dc7c451..7bba26cd1 100755 --- a/pkgs/id/examples/roc-grid/build.sh +++ b/pkgs/id/examples/roc-grid/build.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" -ROC="${ROC:-roc}" +export ROC="${ROC:-roc}" +source ../roc-toolchain.sh if [ -z "${WASM_TOOLS:-}" ]; then if command -v wasm-tools >/dev/null 2>&1; then @@ -24,7 +25,7 @@ if [ -z "${WASM_OPT:-}" ]; then if [ -n "$STORE_OPT" ] && [ -x "$STORE_OPT" ]; then WASM_OPT="$STORE_OPT" else - WASM_OPT="nix run nixpkgs#binaryen --" + WASM_OPT="nix shell nixpkgs#binaryen --command wasm-opt" fi fi fi diff --git a/pkgs/id/examples/roc-grid/roc-grid.wasm b/pkgs/id/examples/roc-grid/roc-grid.wasm index 5c45c669f..daed4efc0 100755 Binary files a/pkgs/id/examples/roc-grid/roc-grid.wasm and b/pkgs/id/examples/roc-grid/roc-grid.wasm differ diff --git a/pkgs/id/examples/roc-screen/build.sh b/pkgs/id/examples/roc-screen/build.sh index 93107cf17..9826314d1 100755 --- a/pkgs/id/examples/roc-screen/build.sh +++ b/pkgs/id/examples/roc-screen/build.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" -ROC="${ROC:-roc}" +export ROC="${ROC:-roc}" +source ../roc-toolchain.sh if [ -z "${WASM_TOOLS:-}" ]; then if command -v wasm-tools >/dev/null 2>&1; then @@ -24,7 +25,7 @@ if [ -z "${WASM_OPT:-}" ]; then if [ -n "$STORE_OPT" ] && [ -x "$STORE_OPT" ]; then WASM_OPT="$STORE_OPT" else - WASM_OPT="nix run nixpkgs#binaryen --" + WASM_OPT="nix shell nixpkgs#binaryen --command wasm-opt" fi fi fi diff --git a/pkgs/id/examples/roc-screen/roc-screen.wasm b/pkgs/id/examples/roc-screen/roc-screen.wasm index c61a05675..6239a48fc 100755 Binary files a/pkgs/id/examples/roc-screen/roc-screen.wasm and b/pkgs/id/examples/roc-screen/roc-screen.wasm differ diff --git a/pkgs/id/examples/roc-toolchain.sh b/pkgs/id/examples/roc-toolchain.sh new file mode 100644 index 000000000..2cf44c41f --- /dev/null +++ b/pkgs/id/examples/roc-toolchain.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Shared compiler guard for every checked-in Roc example module. + +ID_ROC_VERSION_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/nix/roc-version" +ID_ROC_EXPECTED_VERSION="$(<"$ID_ROC_VERSION_FILE")" +ID_ROC_EXPECTED_OUTPUT="Roc compiler version $ID_ROC_EXPECTED_VERSION" +ID_ROC_BIN="${ROC:-roc}" + +if ! ID_ROC_ACTUAL_OUTPUT="$("$ID_ROC_BIN" version 2>&1)"; then + printf 'Roc toolchain check failed: could not run %s version:\n%s\n' \ + "$ID_ROC_BIN" "$ID_ROC_ACTUAL_OUTPUT" >&2 + exit 1 +fi + +if [[ $ID_ROC_ACTUAL_OUTPUT != "$ID_ROC_EXPECTED_OUTPUT" ]]; then + printf 'Roc toolchain mismatch: expected %s from %s, got %s from %s; refusing to build.\n' \ + "$ID_ROC_EXPECTED_OUTPUT" "$ID_ROC_VERSION_FILE" "$ID_ROC_ACTUAL_OUTPUT" "$ID_ROC_BIN" >&2 + exit 1 +fi + +echo "Using pinned Roc compiler $ID_ROC_EXPECTED_VERSION" diff --git a/pkgs/id/examples/territory/build.sh b/pkgs/id/examples/territory/build.sh index 5e6141281..1e6686be4 100755 --- a/pkgs/id/examples/territory/build.sh +++ b/pkgs/id/examples/territory/build.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")" -ROC="${ROC:-roc}" +export ROC="${ROC:-roc}" +source ../roc-toolchain.sh if [ -z "${WASM_TOOLS:-}" ]; then if command -v wasm-tools >/dev/null 2>&1; then WASM_TOOLS="wasm-tools" @@ -23,7 +24,7 @@ if [ -z "${WASM_OPT:-}" ]; then if [ -n "$STORE_OPT" ] && [ -x "$STORE_OPT" ]; then WASM_OPT="$STORE_OPT" else - WASM_OPT="nix run nixpkgs#binaryen --" + WASM_OPT="nix shell nixpkgs#binaryen --command wasm-opt" fi fi fi diff --git a/pkgs/id/examples/territory/territory.wasm b/pkgs/id/examples/territory/territory.wasm index 2336528b8..2677b50e9 100755 Binary files a/pkgs/id/examples/territory/territory.wasm and b/pkgs/id/examples/territory/territory.wasm differ diff --git a/pkgs/id/flake.nix b/pkgs/id/flake.nix index 11a024a4f..c8c1c6a43 100644 --- a/pkgs/id/flake.nix +++ b/pkgs/id/flake.nix @@ -159,9 +159,18 @@ ''; }; - # Helper to create a check that runs a just command - mkCheck = - name: justCmd: + # Helper to create a check that runs a just command or direct test + # command, with an optional pre-check step and check-specific tools. + mkCheck = name: justCmd: mkCheckWith { inherit name justCmd; }; + + mkCheckWith = + { + name, + justCmd ? null, + checkCommand ? null, + extraNativeBuildInputs ? [ ], + beforeCheck ? "", + }: pkgs.stdenv.mkDerivation { name = "id-${name}"; # Every check builds its own debug test binaries in parallel on a @@ -173,10 +182,13 @@ CARGO_PROFILE_DEV_DEBUG = "line-tables-only"; src = ./.; inherit buildInputs; - nativeBuildInputs = nativeBuildInputs ++ [ - bun2nixPkg.hook - pkgs.rustPlatform.cargoSetupHook - ]; + nativeBuildInputs = + nativeBuildInputs + ++ extraNativeBuildInputs + ++ [ + bun2nixPkg.hook + pkgs.rustPlatform.cargoSetupHook + ]; inherit cargoDeps; inherit (opensslEnv) OPENSSL_DIR; inherit (opensslEnv) OPENSSL_LIB_DIR; @@ -204,7 +216,9 @@ ${buildWasmHost} - just ${justCmd} + ${beforeCheck} + + ${if checkCommand != null then checkCommand else "just ${justCmd}"} ''; installPhase = '' mkdir -p $out @@ -307,7 +321,7 @@ # Uses 'ci' command (read-only, no auto-fix modifications) # ======================================================================= checks = { - # CI-safe checks (read-only): cargo-fmt-check web-fmt-check clippy-lint web-lint test-sandbox test-web-unit test-web-typecheck roc-test doc build release + # CI-safe checks (read-only): cargo-fmt-check web-fmt-check clippy-lint web-lint test-sandbox test-web-unit test-web-typecheck roc-test roc-examples doc build release # NixOS VM integration tests (Linux only): nixos-serve nixos-e2e default = mkCheck "ci" "ci"; @@ -336,6 +350,25 @@ # nativeBuildInputs); fails on a missing compiler or a failing # expect — the world_compile.rs skip path is not reused here. roc-test = mkCheck "roc-test" "roc-test"; + # Rebuild all checked-in Roc examples with the pinned compiler, then + # run the sandbox behavior and fuel-budget tests against them. + roc-examples = mkCheckWith { + name = "roc-examples"; + checkCommand = '' + cargo test --no-default-features --features sandbox,world --lib sandbox + cargo test --test toolchain_pin + ''; + extraNativeBuildInputs = [ + pkgs.binaryen + pkgs.wasm-tools + ]; + beforeCheck = '' + ROC=${rocCompiler}/bin/roc \ + WASM_TOOLS=${pkgs.wasm-tools}/bin/wasm-tools \ + WASM_OPT=${pkgs.binaryen}/bin/wasm-opt \ + bash scripts/rebuild-roc-examples.sh + ''; + }; doc = mkCheck "doc" "doc"; cargo-check = mkCheck "cargo-check" "cargo-check"; crash-gate = mkCheck "crash-gate" "crash-gate"; diff --git a/pkgs/id/nix/roc-version b/pkgs/id/nix/roc-version new file mode 100644 index 000000000..abd004634 --- /dev/null +++ b/pkgs/id/nix/roc-version @@ -0,0 +1 @@ +nightly-2026-10-04-130536d diff --git a/pkgs/id/nix/roc.nix b/pkgs/id/nix/roc.nix index f504d45f4..6f47bd874 100644 --- a/pkgs/id/nix/roc.nix +++ b/pkgs/id/nix/roc.nix @@ -12,15 +12,18 @@ }: let - tag = "nightly-2026-10-04-130536d"; + # Shared with the example build scripts so the executable guard and Nix + # package cannot silently pin different Roc nightlies. + tag = lib.removeSuffix "\n" (builtins.readFile ./roc-version); + tagSuffix = lib.removePrefix "nightly-" tag; sources = { x86_64-linux = { - asset = "roc_nightly-linux_x86_64-2026-10-04-130536d.tar.gz"; + asset = "roc_nightly-linux_x86_64-${tagSuffix}.tar.gz"; hash = "sha256-iTyG0toKTDkMvb1CWM5F1obl7BW0Lu64Ef0axEU3cU8="; }; aarch64-linux = { - asset = "roc_nightly-linux_arm64-2026-10-04-130536d.tar.gz"; + asset = "roc_nightly-linux_arm64-${tagSuffix}.tar.gz"; hash = "sha256-9FMtKQyDkg0j8ZHaRlCxmqgwJ0PtNcq06a3IO+rwwds="; }; }; diff --git a/pkgs/id/scripts/rebuild-roc-examples.sh b/pkgs/id/scripts/rebuild-roc-examples.sh new file mode 100644 index 000000000..1a0739b29 --- /dev/null +++ b/pkgs/id/scripts/rebuild-roc-examples.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")/.." + +examples=( + examples/lounge + examples/roc-chat + examples/roc-counter + examples/roc-grid + examples/roc-screen + examples/territory + apps/tic-tac-toe/program +) + +for example in "${examples[@]}"; do + echo "=== Rebuilding Roc example: $example ===" + (cd "$example" && bash ./build.sh) +done diff --git a/pkgs/id/tests/toolchain_pin.rs b/pkgs/id/tests/toolchain_pin.rs index 7f8ce9a8a..36d40b92a 100644 --- a/pkgs/id/tests/toolchain_pin.rs +++ b/pkgs/id/tests/toolchain_pin.rs @@ -8,6 +8,17 @@ use std::fs; use std::path::Path; +use std::process::Command; + +const ROC_EXAMPLE_BUILDS: [&str; 7] = [ + "examples/lounge/build.sh", + "examples/roc-chat/build.sh", + "examples/roc-counter/build.sh", + "examples/roc-grid/build.sh", + "examples/roc-screen/build.sh", + "examples/territory/build.sh", + "apps/tic-tac-toe/program/build.sh", +]; /// The Nix side of the pin: a literal version, or a value read from the file. #[derive(Debug, PartialEq, Eq)] @@ -70,6 +81,78 @@ fn read_pin_files() -> (String, String) { (toml, nix) } +#[test] +fn test_roc_examples_and_nix_package_share_the_compiler_pin() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let pin = fs::read_to_string(root.join("nix/roc-version")).unwrap(); + let pinned_version = pin.trim(); + assert!(!pinned_version.is_empty(), "nix/roc-version is empty"); + + let roc_nix = fs::read_to_string(root.join("nix/roc.nix")).unwrap(); + assert!( + roc_nix.contains("tag = lib.removeSuffix \"\\n\" (builtins.readFile ./roc-version);") + && roc_nix.contains("lib.removePrefix \"nightly-\" tag"), + "nix/roc.nix must derive its download tag from nix/roc-version" + ); + + for build_script in ROC_EXAMPLE_BUILDS { + let script = fs::read_to_string(root.join(build_script)).unwrap(); + assert!( + script.contains("roc-toolchain.sh"), + "{build_script} must check the shared Roc compiler pin" + ); + } +} + +#[cfg(unix)] +#[test] +fn test_example_build_refuses_a_different_roc_before_building() { + use std::os::unix::fs::PermissionsExt; + + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let temp = tempfile::tempdir().unwrap(); + let bash = std::env::split_paths(&std::env::var_os("PATH").unwrap()) + .map(|directory| directory.join("bash")) + .find(|candidate| candidate.is_file()) + .and_then(|candidate| fs::canonicalize(candidate).ok()) + .expect("bash is available on PATH"); + let roc_stub = temp.path().join("roc-stub"); + let build_marker = temp.path().join("build-was-run"); + fs::write( + &roc_stub, + format!( + "#!{}\nif [[ \"${{1:-}}\" == version ]]; then\n echo 'Roc compiler version unpinned-test'\nelse\n : > \"$ROC_BUILD_MARKER\"\nfi\n", + bash.display() + ), + ) + .unwrap(); + fs::set_permissions(&roc_stub, fs::Permissions::from_mode(0o755)).unwrap(); + + let output = Command::new("bash") + .arg("examples/territory/build.sh") + .current_dir(root) + .env("ROC", &roc_stub) + .env("ROC_BUILD_MARKER", &build_marker) + .output() + .unwrap(); + + assert!( + !output.status.success(), + "an unpinned Roc compiler was accepted" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + let pinned_version = fs::read_to_string(root.join("nix/roc-version")).unwrap(); + assert!(stderr.contains(pinned_version.trim()), "{stderr}"); + assert!( + stderr.contains("Roc compiler version unpinned-test"), + "{stderr}" + ); + assert!( + !build_marker.exists(), + "the build command ran before refusal" + ); +} + #[test] fn nix_check_installs_the_channel_pinned_in_rust_toolchain_toml() { let (toml, nix) = read_pin_files();