diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 763ca398..bbacd271 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,7 @@ jobs: - uses: roc-lang/setup-roc@cbe782d6f165b89c87d99f50a59ac4f5f73b4427 # ratchet:roc-lang/setup-roc@v0.3.0 with: version: nightly-new-compiler - nightly-tag: nightly-2026-11-06-34f137a + nightly-tag: nightly-2026-11-13-03dce41 - name: Install Rust toolchain run: | rustup toolchain install "${{ needs.rust-version.outputs.version }}" --profile minimal --target x86_64-unknown-linux-musl --component llvm-tools-preview @@ -116,7 +116,7 @@ jobs: - uses: roc-lang/setup-roc@cbe782d6f165b89c87d99f50a59ac4f5f73b4427 # ratchet:roc-lang/setup-roc@v0.3.0 with: version: nightly-new-compiler - nightly-tag: nightly-2026-11-06-34f137a + nightly-tag: nightly-2026-11-13-03dce41 - name: Install Rust toolchain shell: bash run: | @@ -164,7 +164,7 @@ jobs: - uses: roc-lang/setup-roc@cbe782d6f165b89c87d99f50a59ac4f5f73b4427 # ratchet:roc-lang/setup-roc@v0.3.0 with: version: nightly-new-compiler - nightly-tag: nightly-2026-11-06-34f137a + nightly-tag: nightly-2026-11-13-03dce41 - name: Install Rust toolchain shell: bash run: | diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 7fe031d8..174e49e7 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -40,7 +40,7 @@ jobs: uses: roc-lang/setup-roc@cbe782d6f165b89c87d99f50a59ac4f5f73b4427 # ratchet:roc-lang/setup-roc@v0.3.0 with: version: nightly-new-compiler - nightly-tag: nightly-2026-11-06-34f137a + nightly-tag: nightly-2026-11-13-03dce41 - name: Restore released docs env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2dc5331f..bb5ddf39 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -126,7 +126,7 @@ jobs: uses: roc-lang/setup-roc@cbe782d6f165b89c87d99f50a59ac4f5f73b4427 # ratchet:roc-lang/setup-roc@v0.3.0 with: version: nightly-new-compiler - nightly-tag: nightly-2026-11-06-34f137a + nightly-tag: nightly-2026-11-13-03dce41 - name: Install Zig uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f @@ -235,7 +235,7 @@ jobs: uses: roc-lang/setup-roc@cbe782d6f165b89c87d99f50a59ac4f5f73b4427 # ratchet:roc-lang/setup-roc@v0.3.0 with: version: nightly-new-compiler - nightly-tag: nightly-2026-11-06-34f137a + nightly-tag: nightly-2026-11-13-03dce41 - name: Install Rust shell: bash @@ -280,7 +280,7 @@ jobs: uses: roc-lang/setup-roc@cbe782d6f165b89c87d99f50a59ac4f5f73b4427 # ratchet:roc-lang/setup-roc@v0.3.0 with: version: nightly-new-compiler - nightly-tag: nightly-2026-11-06-34f137a + nightly-tag: nightly-2026-11-13-03dce41 - name: Create docs staging tree run: mkdir -p docs-site @@ -453,7 +453,7 @@ jobs: uses: roc-lang/setup-roc@cbe782d6f165b89c87d99f50a59ac4f5f73b4427 # ratchet:roc-lang/setup-roc@v0.3.0 with: version: nightly-new-compiler - nightly-tag: nightly-2026-11-06-34f137a + nightly-tag: nightly-2026-11-13-03dce41 - name: Download release metadata uses: actions/download-artifact@v8 diff --git a/.github/workflows/pie-compatibility.yml b/.github/workflows/static-runtime.yml similarity index 60% rename from .github/workflows/pie-compatibility.yml rename to .github/workflows/static-runtime.yml index c6de79a2..dcf5bad2 100644 --- a/.github/workflows/pie-compatibility.yml +++ b/.github/workflows/static-runtime.yml @@ -1,6 +1,4 @@ -# TODO for the release after 0.28.0: pin a Roc nightly containing #12949, -# remove this workflow, and retire the fixed-address musl startup patch. -name: Static runtime compatibility +name: Static runtime validation on: pull_request: workflow_dispatch: @@ -13,7 +11,6 @@ jobs: fail-fast: false matrix: target: [x64musl, arm64musl, x64mac, arm64mac, x64win] - compiler: [pinned, pie] include: - {target: x64musl, os: ubuntu-24.04} - {target: arm64musl, os: ubuntu-24.04-arm} @@ -23,36 +20,10 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v7 - - uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f - if: matrix.compiler == 'pie' - with: - version: 0.17.1 - uses: roc-lang/setup-roc@cbe782d6f165b89c87d99f50a59ac4f5f73b4427 - if: matrix.compiler == 'pinned' with: version: nightly-new-compiler - nightly-tag: nightly-2026-11-06-34f137a - - uses: actions/checkout@v7 - if: matrix.compiler == 'pie' - with: - repository: roc-lang/roc - ref: c080bd1bc565531389f74444d3736d1bfb8e5352 - path: target/roc-pie - - uses: actions/cache@v5 - if: matrix.compiler == 'pie' - id: compiler-cache - with: - path: target/roc-pie/zig-out/bin - key: roc-pie-${{ runner.os }}-${{ runner.arch }}-c080bd1bc565531389f74444d3736d1bfb8e5352-zig-0.17.1-strip - - name: Build the pinned PIE compiler - if: matrix.compiler == 'pie' && steps.compiler-cache.outputs.cache-hit != 'true' - shell: bash - working-directory: target/roc-pie - run: zig build roc -Doptimize=ReleaseFast -Dstrip -j2 - - name: Select the PIE compiler - shell: bash - if: matrix.compiler == 'pie' - run: echo "ROC=$GITHUB_WORKSPACE/target/roc-pie/zig-out/bin/roc${{ runner.os == 'Windows' && '.exe' || '' }}" >> "$GITHUB_ENV" + nightly-tag: nightly-2026-11-13-03dce41 - uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f with: version: 0.17.0 @@ -82,7 +53,7 @@ jobs: dist/dev-compat/tcp-server > target/dev-tcp-server.log "$compiler" build tests/resource-lifetimes/resource-lifetime.roc --opt=dev --target="$TARGET" --output=dist/dev-compat/resources dist/dev-compat/resources - python3 scripts/check_static_executables.py dist/dev-compat --format ${{ matrix.compiler }} + python3 scripts/check_static_executables.py dist/dev-compat --format pie - name: Verify executable format if: endsWith(matrix.target, 'musl') - run: python3 scripts/check_static_executables.py dist/example-binaries/${{ matrix.target }} --format ${{ matrix.compiler }} + run: python3 scripts/check_static_executables.py dist/example-binaries/${{ matrix.target }} --format pie diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index abdcd763..21a98418 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -138,9 +138,9 @@ and run `python3 scripts/pin_linker_inputs.py /path/to/linker-inputs.lock.json`, then regenerate third-party notices and commit the lock and text metadata. Generated target objects and libraries must stay outside Git. -Linux startup supports fixed-address executables from the pinned Roc nightly -and static PIE from the compiler's ASLR change. The compatibility workflow tests -both compilers against the same pinned runtime release. Linux Rust and C host +The pinned Roc nightly emits static PIE on Linux. The Static runtime validation +workflow checks randomized loading, development-backend callbacks, and native +execution against the pinned runtime release. Linux Rust and C host code is built as PIC. The producer also validates startup with stored function pointers, thread-local data, and threads on both Linux architectures. diff --git a/Cargo.lock b/Cargo.lock index 319e5ddc..074d96d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -502,7 +502,7 @@ dependencies = [ [[package]] name = "roc_host" -version = "0.28.0" +version = "0.28.1" dependencies = [ "bytes", "getrandom 0.2.16", diff --git a/Cargo.toml b/Cargo.toml index 245dada6..f5fadf74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "roc_host" -version = "0.28.0" +version = "0.28.1" authors = ["The Roc Contributors"] edition = "2021" rust-version = "1.97" diff --git a/docs/releases/0.28.1.md b/docs/releases/0.28.1.md new file mode 100644 index 00000000..895c60d5 --- /dev/null +++ b/docs/releases/0.28.1.md @@ -0,0 +1,35 @@ +# basic-cli 0.28.1 + +```roc +app [main!] { pf: platform "{{PLATFORM_URL}}" } +``` + +Use Roc **`nightly-2026-11-13-03dce41`** or later. CI, release builds, and the +Nix development shell now use this nightly. + +## Fixes + +- `Cmd` and `Path` check without duplicate-definition warnings on the new + nightly ([#556](https://github.com/roc-lang/basic-cli/issues/556)). + `Cmd.Command` remains opaque and keeps all its builder and execution methods; + the `Cmd.call!`, `capture!`, `run!`, and `spawn!` convenience functions keep + their existing signatures. +- The pinned compiler includes the Windows unwind-table fix in + [roc#12996](https://github.com/roc-lang/roc/pull/12996). This resolves the + compile-time failures of quoted `Locale` and `Path` values reported in + [#555](https://github.com/roc-lang/basic-cli/issues/555). Applications using + 0.27.0 or 0.28.0 also benefit from upgrading the compiler; their literals do + not need changing. +- The platform header now explicitly types `main!` arguments as `List(OsStr)`, + matching the native arguments supplied by the launcher + ([#554](https://github.com/roc-lang/basic-cli/pull/554)). + +Source validation now checks the platform with `--no-cache` on Linux and +Windows, so dependency warnings and compile-time literal failures fail CI. +Linux runtime checks use the official nightly and verify static PIE loading +and development-backend callbacks. + +--- + +- [API documentation](https://roc-lang.github.io/basic-cli/0.28.1/) +- [Full changelog since 0.28.0](https://github.com/roc-lang/basic-cli/compare/0.28.0...0.28.1) diff --git a/flake.lock b/flake.lock index 92271dcc..6dfd0ce0 100644 --- a/flake.lock +++ b/flake.lock @@ -42,17 +42,17 @@ ] }, "locked": { - "lastModified": 1793980758, - "narHash": "sha256-l9jXUZklQEX32xK2dKQGOnfRAJRLCbwdzbcPyqDJ4nQ=", + "lastModified": 1794554775, + "narHash": "sha256-ZB7yiDEs11sNrCjT2O73hyGSbfbwEGJz1jqfhLPc7l0=", "owner": "roc-lang", "repo": "roc-overlay", - "rev": "57a4b47c16545bf87a7d6b7e4480e7100fe74dc2", + "rev": "67491987eb6ef2423d9a6e5901f6a8e439d8566c", "type": "github" }, "original": { "owner": "roc-lang", "repo": "roc-overlay", - "rev": "57a4b47c16545bf87a7d6b7e4480e7100fe74dc2", + "rev": "67491987eb6ef2423d9a6e5901f6a8e439d8566c", "type": "github" } }, diff --git a/flake.nix b/flake.nix index 36811a77..ae7dc20e 100644 --- a/flake.nix +++ b/flake.nix @@ -6,7 +6,7 @@ # nixos-unstable no longer supports Intel macOS. nixpkgs-x86-darwin.url = "github:NixOS/nixpkgs/nixpkgs-26.05-darwin"; roc-overlay = { - url = "github:roc-lang/roc-overlay/57a4b47c16545bf87a7d6b7e4480e7100fe74dc2"; + url = "github:roc-lang/roc-overlay/67491987eb6ef2423d9a6e5901f6a8e439d8566c"; inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs-darwin.follows = "nixpkgs-x86-darwin"; }; @@ -56,16 +56,35 @@ inherit system; overlays = [ roc-overlay.overlays.default + # Until roc-overlay records the November 13 nightly. + (import ./nix/roc-nightly.nix) rust-overlay.overlays.default ]; }; releaseFor = system: import ./nix/release.nix { pkgs = pkgsFor system; }; + # Keep in sync with the nightly pinned in .github/workflows. + developmentCompilerFor = system: (pkgsFor system).rocpkgs."nightly-2026-11-13-03dce41"; in { formatter = forAllSystems (system: (pkgsFor system).nixfmt); packages = forAllSystems (system: (releaseFor system).packages); - checks = forAllSystems (system: (releaseFor system).checks); + checks = forAllSystems ( + system: + let + pkgs = pkgsFor system; + compiler = developmentCompilerFor system; + in + (releaseFor system).checks + // { + development-compiler = pkgs.runCommand "basic-cli-development-compiler" { + nativeBuildInputs = [ compiler ]; + } '' + test "$(roc version)" = "Roc compiler version ${compiler.version}" + touch "$out" + ''; + } + ); devShells = forAllSystems ( system: @@ -78,8 +97,7 @@ { default = pkgs.mkShell { packages = [ - # Keep in sync with the nightly pinned in .github/workflows. - pkgs.rocpkgs."nightly-2026-11-06-34f137a" + (developmentCompilerFor system) pkgs.python3 rustToolchain pkgs.simple-http-server diff --git a/native/linker-inputs/README.md b/native/linker-inputs/README.md index 0b5386df..fc7ddd3f 100644 --- a/native/linker-inputs/README.md +++ b/native/linker-inputs/README.md @@ -26,9 +26,13 @@ python3 scripts/linker_inputs.py build --target x64musl --directory dist/linker- Publication requires each target to have passed validation on its native OS and architecture. The workflow publishes only after all three producers succeed. -## Removal in the release after 0.28.0 +## Fixed-address compatibility removal -Move the Roc pin to a nightly containing roc-lang/roc#12949, remove the temporary -Static runtime compatibility workflow and the fixed-address startup patch in -the producer, then publish and pin new linker inputs using unpatched `rcrt1.o`. -Update the producer's fixed-address checks and documentation with that change. +0.28.1 moves the Roc pin past roc-lang/roc#12949. Static runtime validation now +uses that official nightly for static PIE, development-backend callbacks, and +native execution. The pinned linker inputs still include the fixed-address +startup compatibility patch; it is harmless for PIE and does not need rebuilding +for this source compatibility patch release. + +At the next linker-input update, remove the fixed-address startup patch and its +producer checks, then publish and pin new inputs using unpatched `rcrt1.o`. diff --git a/nix/roc-nightly-sources.json b/nix/roc-nightly-sources.json new file mode 100644 index 00000000..2695bc68 --- /dev/null +++ b/nix/roc-nightly-sources.json @@ -0,0 +1,18 @@ +{ + "x86_64-linux": { + "url": "https://github.com/roc-lang/nightlies/releases/download/nightly-2026-11-13-03dce41/roc_nightly-linux_x86_64-2026-11-13-03dce41.tar.gz", + "hash": "sha256-GlL8PRRm/IUeajPCg4+xJFRGS8WjwLyV7ceEqg5CXlI=" + }, + "aarch64-linux": { + "url": "https://github.com/roc-lang/nightlies/releases/download/nightly-2026-11-13-03dce41/roc_nightly-linux_arm64-2026-11-13-03dce41.tar.gz", + "hash": "sha256-nb/NzPo2ecel+PTij7dLtOcUZRQAyXiQHGCYSCl2Nmw=" + }, + "aarch64-darwin": { + "url": "https://github.com/roc-lang/nightlies/releases/download/nightly-2026-11-13-03dce41/roc_nightly-macos_apple_silicon-2026-11-13-03dce41.tar.gz", + "hash": "sha256-SXzKhKYwHht0HAb7ZK5uAYZszwu6moGX/ckcnvPHAXE=" + }, + "x86_64-darwin": { + "url": "https://github.com/roc-lang/nightlies/releases/download/nightly-2026-11-13-03dce41/roc_nightly-macos_x86_64-2026-11-13-03dce41.tar.gz", + "hash": "sha256-Vu3fny503mZGCvHITqqUoHQ8kJ4RLqR1UjQwRBxSLIA=" + } +} diff --git a/nix/roc-nightly.nix b/nix/roc-nightly.nix new file mode 100644 index 00000000..d4402dac --- /dev/null +++ b/nix/roc-nightly.nix @@ -0,0 +1,23 @@ +# Temporary supplement until roc-overlay records this nightly. Reuse its +# installation and wrapping recipe, with the official archives and digests. +final: prev: +let + source = (builtins.fromJSON (builtins.readFile ./roc-nightly-sources.json)).${prev.stdenv.hostPlatform.system}; + tag = builtins.elemAt (prev.lib.splitString "/" source.url) 7; +in +{ + rocpkgs = prev.rocpkgs // { + ${tag} = prev.rocpkgs.default.overrideAttrs (old: { + version = tag; + src = final.fetchurl source; + passthru = old.passthru // { + inherit tag source; + compilerVersion = tag; + compilerCommit = "03dce412cc30ec237e4829b633f98549d01faf14"; + }; + meta = old.meta // { + changelog = "https://github.com/roc-lang/nightlies/releases/tag/${tag}"; + }; + }); + }; +} diff --git a/platform/Cmd.roc b/platform/Cmd.roc index bc08958a..68713d4e 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -169,271 +169,7 @@ Cmd :: [].{ } ## A reusable command configuration. - Command :: { - args : List(OsStr), - clear_envs : Bool, - envs : List((OsStr, OsStr)), - program : OsStr, - cwd_value : [Inherit, Dir(Path)], - stdin_value : [Unset, Set([Inherit, Null, Bytes(List(U8)), Pipe])], - stdout_value : [Unset, Set(StdoutMode)], - stderr_value : [Unset, Set(StderrMode)], - timeout_value : [NoTimeout, After(Duration)], - output_limit_value : [NoLimit, MaxBytes(U64)], - pending_limit_value : U64, - manage_tree_value : Bool, - }.{ - - ## Add a single argument to the command. - ## ❗ Shell features like variable substitution (e.g. `$FOO`), glob patterns (e.g. `*.txt`), ... are not available. - ## - ## ```roc - ## cmd = Cmd.new("ls").arg("-l") - ## ``` - arg : Command, OsStr -> Command - arg = |cmd, a| { - ..cmd, - args: cmd.args.append(a), - } - - ## Add multiple arguments to the command. - ## ❗ Shell features like variable substitution (e.g. `$FOO`), glob patterns (e.g. `*.txt`), ... are not available. - ## - ## ```roc - ## cmd = Cmd.new("ls").args(["-l", "-a"]) - ## ``` - ## - ## For a `List(Str)` use `cmd.args(names.map(OsStr.from_str))`. - args : Command, List(OsStr) -> Command - args = |cmd, new_args| { - ..cmd, - args: cmd.args.concat(new_args), - } - - ## Add a single environment variable to the command. - ## - ## ```roc - ## cmd = Cmd.new("env").env("FOO", "bar") # add the environment variable "FOO" with value "bar" - ## ``` - env : Command, OsStr, OsStr -> Command - env = |cmd, key, value| { - { ..cmd, envs: cmd.envs.append((key, value)) } - } - - ## Add multiple environment variables to the command. - ## - ## ```roc - ## cmd = Cmd.new("env").envs([("FOO", "bar"), ("BAZ", "qux")]) - ## ``` - envs : Command, List((OsStr, OsStr)) -> Command - envs = |cmd, pairs| { ..cmd, envs: cmd.envs.concat(pairs) } - - ## Clear all environment variables before running the command. - ## Only environment variables added via `env` or `envs` will be available. - ## Useful if you want a clean command run that does not behave unexpectedly if the user has some env var set. - ## - ## ```roc - ## cmd = - ## Cmd.new("env") - ## .clear_envs() - ## .env("ONLY_THIS", "visible") - ## ``` - clear_envs : Command -> Command - clear_envs = |cmd| { ..cmd, clear_envs: True } - - ## Set the child working directory without changing the parent directory. - ## Use an absolute executable path or a bare PATH name: resolving a relative - ## executable against cwd is platform-specific. - cwd : Command, Path -> Command - cwd = |cmd, path| { ..cmd, cwd_value: Dir(path) } - - ## Choose the child's standard input; see [StdinMode]. - ## - ## When this is not called, [run!] gives the child `Null`, and [call!], - ## [capture!] and [spawn!] let it inherit the parent's standard input. - stdin : Command, StdinMode -> Command - stdin = |cmd, mode| { ..cmd, stdin_value: Set(mode) } - - ## Choose where the child's standard output goes; see [StdoutMode]. - ## - ## When this is not called, [run!] and [capture!] capture it, and [call!] and - ## [spawn!] let it go to the parent's standard output. - stdout : Command, StdoutMode -> Command - stdout = |cmd, mode| { ..cmd, stdout_value: Set(mode) } - - ## Choose where the child's standard error goes; see [StderrMode]. - ## - ## When this is not called, [run!] captures it, and [call!], [capture!] and - ## [spawn!] let it go to the parent's standard error. - stderr : Command, StderrMode -> Command - stderr = |cmd, mode| { ..cmd, stderr_value: Set(mode) } - - ## Let the child use the parent's standard input, output and error, however - ## the command is run. With [run!] this gives the exit status of a command - ## that prints to the terminal: - ## - ## ```roc - ## status = Cmd.new("roc").args(["check", "main.roc"]).inherit_stdio().run!()?.status - ## ``` - inherit_stdio : Command -> Command - inherit_stdio = |cmd| { ..cmd, stdin_value: Set(Inherit), stdout_value: Set(Inherit), stderr_value: Set(Inherit) } - - ## Stop the command when it has run for longer than `duration`. The deadline - ## covers the whole run, including reading the rest of its output. - ## - ## A command that reaches its deadline is killed, and the result is - ## `Err(CmdErr(TimedOut(partial)))` with the output captured so far. That is - ## also what [Child.wait!] and [Child.poll!] return for a child started with - ## [spawn!]. When `timeout` is not called there is no deadline; - ## `Duration.zero` expires immediately. - timeout : Command, Duration -> Command - timeout = |cmd, duration| { ..cmd, timeout_value: After(duration) } - - ## Limit how many bytes of standard output and standard error are captured, - ## counted together. The default is `MaxBytes(16777216)`, 16 MiB. - ## - ## A command that prints more is killed, and the result is - ## `Err(CmdErr(OutputLimitExceeded(partial)))` with the output captured up to - ## the limit. `NoLimit` captures whatever the command prints. - output_limit : Command, [NoLimit, MaxBytes(U64)] -> Command - output_limit = |cmd, limit| { ..cmd, output_limit_value: limit } - - ## Limit how many bytes of `Pipe` output may wait to be read with - ## [Child.read!], counted over both streams (default 1 MiB). A child that - ## gets further ahead of its reader is killed, and [Child.wait!] returns - ## `Err(CmdErr(OutputLimitExceeded(partial)))`. - pending_limit : Command, U64 -> Command - pending_limit = |cmd, bytes| { ..cmd, pending_limit_value: bytes } - - ## Also terminate descendants on cancellation, using a Unix process group - ## or Windows Job Object. Disabled by default; descendants must not escape it. - manage_tree : Command, Bool -> Command - manage_tree = |cmd, enabled| { ..cmd, manage_tree_value: enabled } - - # A tag union that crosses the host boundary is closed by design, so the - # four entry points below, like the `Child` methods, rebuild the error they - # forward from the host. That gives callers an open union, which `?` can - # combine with other error types however the function is called. - # `tests/cmd-api.roc` covers the function-value form. - - ## Run the command on the parent's terminal and require it to succeed. - ## - ## Standard input, output and error are inherited unless the command sets - ## them. Any status other than exit code 0 is `Err(BadExitStatus(output))`. - ## - ## ```roc - ## Cmd.new("cargo").args(["build", "--release"]).call!()? - ## ``` - call! : Command => Try({}, [CmdErr(Err), BadExitStatus(Output), Interrupted]) - call! = |cmd| - match finish!(cmd, Call) { - Ok(output) => - match output.require_success() { - Ok(_) => Ok({}) - Err(BadExitStatus(failed)) => Err(BadExitStatus(failed)) - } - Err(CmdErr(err)) => Err(CmdErr(err)) - Err(Interrupted) => Err(Interrupted) - } - - ## Run the command, require it to succeed, and return its standard output as - ## text. - ## - ## Standard output is captured unless the command sets it. Standard input and - ## standard error are inherited unless the command sets them, so warnings - ## still reach the terminal, as with a shell's `$(..)`. Any status other than - ## exit code 0 is `Err(BadExitStatus(output))`, and output that is not UTF-8 - ## is `Err(BadUtf8(..))`. - ## - ## The text is returned as printed, including a final newline: - ## - ## ```roc - ## sha = Cmd.new("git").args(["rev-parse", "${branch}"]).capture!()?.trim() - ## ``` - capture! : Command => Try(Str, [CmdErr(Err), BadExitStatus(Output), BadUtf8({ problem : Str.Utf8Problem, index : U64 }), Interrupted]) - capture! = |cmd| - match finish!(cmd, Capture) { - Ok(output) => - match output.require_success() { - Ok(succeeded) => - match succeeded.stdout_utf8() { - Ok(text) => Ok(text) - Err(BadUtf8(problem)) => Err(BadUtf8(problem)) - } - Err(BadExitStatus(failed)) => Err(BadExitStatus(failed)) - } - Err(CmdErr(err)) => Err(CmdErr(err)) - Err(Interrupted) => Err(Interrupted) - } - - ## Run the command and return its exit status and output as data. A non-zero - ## exit code or a signal is not an error here: match on `output.status`, or - ## call [Output.require_success]. - ## - ## Standard output and standard error are captured, and standard input is - ## `Null`, unless the command sets them. The result is returned once the - ## command has exited and all of its output has been read. - ## - ## ```roc - ## output = Cmd.new("roc").args(["check", "main.roc"]).run!()? - ## ``` - run! : Command => Try(Output, [CmdErr(Err), Interrupted]) - run! = |cmd| - match finish!(cmd, Run) { - Ok(output) => Ok(output) - Err(CmdErr(err)) => Err(CmdErr(err)) - Err(Interrupted) => Err(Interrupted) - } - - ## Start the command and return a [Child] without waiting for it. - ## - ## Standard input, output and error are inherited unless the command sets - ## them. Set a stream to `Pipe` to exchange data with the child while it runs. - ## Keep the returned child for later reads, waits or cleanup; an unused - ## binding such as `_child` does not keep the process alive. See [Child]. - spawn! : Command => Try(Child, [CmdErr(Err), Interrupted]) - spawn! = |cmd| - match Host.cmd_spawn!(to_host_cmd(cmd, Spawn)) { - Ok(handle) => - Ok( - Child.{ - host: handle, - pid_value: Host.child_pid!(handle), - command: to_str(cmd), - program: cmd.program, - }, - ) - Err(CmdErr(err)) => Err(CmdErr(from_host_err(err, cmd.program))) - Err(Interrupted) => Err(Interrupted) - } - - ## The program and its arguments, separated by spaces, for messages and logs. - ## An argument that is empty or contains a space or a quote is written as a - ## quoted, escaped string. The environment, the working directory and the - ## other settings are not included. - ## - ## ```roc - ## expect Cmd.new("git").args(["commit", "-m", "first draft"]).to_str() == "git commit -m \"first draft\"" - ## ``` - to_str : Command -> Str - to_str = |cmd| - Str.join_with([cmd.program].concat(cmd.args).map(display_word), " ") - - ## Customize command output for `Str.inspect`: the full configuration, with - ## the names of the environment variables it sets but never their values, and - ## the length of `Bytes` input but not its content. - to_inspect : Command -> Str - to_inspect = |cmd| { - env_names = cmd.envs.map(|(name, _)| name) - stdin_text = - match cmd.stdin_value { - Set(Bytes(bytes)) => "Set(Bytes(<${bytes.len().to_str()} bytes>))" - other => Str.inspect(other) - } - - "Cmd.Command({ program: ${Str.inspect(cmd.program)}, args: ${Str.inspect(cmd.args)}, env_names: ${Str.inspect(env_names)}, clear_envs: ${Str.inspect(cmd.clear_envs)}, cwd: ${Str.inspect(cmd.cwd_value)}, stdin: ${stdin_text}, stdout: ${Str.inspect(cmd.stdout_value)}, stderr: ${Str.inspect(cmd.stderr_value)}, timeout: ${Str.inspect(cmd.timeout_value)}, output_limit: ${Str.inspect(cmd.output_limit_value)}, pending_limit: ${Str.inspect(cmd.pending_limit_value)}, manage_tree: ${Str.inspect(cmd.manage_tree_value)} })" - } - } + Command : CommandConfig ## Report whether `command` can be found on the system as something runnable. ## @@ -679,6 +415,275 @@ Cmd :: [].{ } } +# Keep the opaque implementation outside Cmd so its execution methods do not +# shadow the namespace convenience functions. Cmd.Command is its public alias. +## A reusable command configuration. +CommandConfig :: { + args : List(OsStr), + clear_envs : Bool, + envs : List((OsStr, OsStr)), + program : OsStr, + cwd_value : [Inherit, Dir(Path)], + stdin_value : [Unset, Set([Inherit, Null, Bytes(List(U8)), Pipe])], + stdout_value : [Unset, Set(Cmd.StdoutMode)], + stderr_value : [Unset, Set(Cmd.StderrMode)], + timeout_value : [NoTimeout, After(Duration)], + output_limit_value : [NoLimit, MaxBytes(U64)], + pending_limit_value : U64, + manage_tree_value : Bool, +}.{ + + ## Add a single argument to the command. + ## ❗ Shell features like variable substitution (e.g. `$FOO`), glob patterns (e.g. `*.txt`), ... are not available. + ## + ## ```roc + ## cmd = Cmd.new("ls").arg("-l") + ## ``` + arg : CommandConfig, OsStr -> CommandConfig + arg = |cmd, a| { + ..cmd, + args: cmd.args.append(a), + } + + ## Add multiple arguments to the command. + ## ❗ Shell features like variable substitution (e.g. `$FOO`), glob patterns (e.g. `*.txt`), ... are not available. + ## + ## ```roc + ## cmd = Cmd.new("ls").args(["-l", "-a"]) + ## ``` + ## + ## For a `List(Str)` use `cmd.args(names.map(OsStr.from_str))`. + args : CommandConfig, List(OsStr) -> CommandConfig + args = |cmd, new_args| { + ..cmd, + args: cmd.args.concat(new_args), + } + + ## Add a single environment variable to the command. + ## + ## ```roc + ## cmd = Cmd.new("env").env("FOO", "bar") # add the environment variable "FOO" with value "bar" + ## ``` + env : CommandConfig, OsStr, OsStr -> CommandConfig + env = |cmd, key, value| { + { ..cmd, envs: cmd.envs.append((key, value)) } + } + + ## Add multiple environment variables to the command. + ## + ## ```roc + ## cmd = Cmd.new("env").envs([("FOO", "bar"), ("BAZ", "qux")]) + ## ``` + envs : CommandConfig, List((OsStr, OsStr)) -> CommandConfig + envs = |cmd, pairs| { ..cmd, envs: cmd.envs.concat(pairs) } + + ## Clear all environment variables before running the command. + ## Only environment variables added via `env` or `envs` will be available. + ## Useful if you want a clean command run that does not behave unexpectedly if the user has some env var set. + ## + ## ```roc + ## cmd = + ## Cmd.new("env") + ## .clear_envs() + ## .env("ONLY_THIS", "visible") + ## ``` + clear_envs : CommandConfig -> CommandConfig + clear_envs = |cmd| { ..cmd, clear_envs: True } + + ## Set the child working directory without changing the parent directory. + ## Use an absolute executable path or a bare PATH name: resolving a relative + ## executable against cwd is platform-specific. + cwd : CommandConfig, Path -> CommandConfig + cwd = |cmd, path| { ..cmd, cwd_value: Dir(path) } + + ## Choose the child's standard input; see [Cmd.StdinMode]. + ## + ## When this is not called, [run!] gives the child `Null`, and [call!], + ## [capture!] and [spawn!] let it inherit the parent's standard input. + stdin : CommandConfig, Cmd.StdinMode -> CommandConfig + stdin = |cmd, mode| { ..cmd, stdin_value: Set(mode) } + + ## Choose where the child's standard output goes; see [Cmd.StdoutMode]. + ## + ## When this is not called, [run!] and [capture!] capture it, and [call!] and + ## [spawn!] let it go to the parent's standard output. + stdout : CommandConfig, Cmd.StdoutMode -> CommandConfig + stdout = |cmd, mode| { ..cmd, stdout_value: Set(mode) } + + ## Choose where the child's standard error goes; see [Cmd.StderrMode]. + ## + ## When this is not called, [run!] captures it, and [call!], [capture!] and + ## [spawn!] let it go to the parent's standard error. + stderr : CommandConfig, Cmd.StderrMode -> CommandConfig + stderr = |cmd, mode| { ..cmd, stderr_value: Set(mode) } + + ## Let the child use the parent's standard input, output and error, however + ## the command is run. With [run!] this gives the exit status of a command + ## that prints to the terminal: + ## + ## ```roc + ## status = Cmd.new("roc").args(["check", "main.roc"]).inherit_stdio().run!()?.status + ## ``` + inherit_stdio : CommandConfig -> CommandConfig + inherit_stdio = |cmd| { ..cmd, stdin_value: Set(Inherit), stdout_value: Set(Inherit), stderr_value: Set(Inherit) } + + ## Stop the command when it has run for longer than `duration`. The deadline + ## covers the whole run, including reading the rest of its output. + ## + ## A command that reaches its deadline is killed, and the result is + ## `Err(CmdErr(TimedOut(partial)))` with the output captured so far. That is + ## also what [Cmd.Child.wait!] and [Cmd.Child.poll!] return for a child started with + ## [spawn!]. When `timeout` is not called there is no deadline; + ## `Duration.zero` expires immediately. + timeout : CommandConfig, Duration -> CommandConfig + timeout = |cmd, duration| { ..cmd, timeout_value: After(duration) } + + ## Limit how many bytes of standard output and standard error are captured, + ## counted together. The default is `MaxBytes(16777216)`, 16 MiB. + ## + ## A command that prints more is killed, and the result is + ## `Err(CmdErr(OutputLimitExceeded(partial)))` with the output captured up to + ## the limit. `NoLimit` captures whatever the command prints. + output_limit : CommandConfig, [NoLimit, MaxBytes(U64)] -> CommandConfig + output_limit = |cmd, limit| { ..cmd, output_limit_value: limit } + + ## Limit how many bytes of `Pipe` output may wait to be read with + ## [Cmd.Child.read!], counted over both streams (default 1 MiB). A child that + ## gets further ahead of its reader is killed, and [Cmd.Child.wait!] returns + ## `Err(CmdErr(OutputLimitExceeded(partial)))`. + pending_limit : CommandConfig, U64 -> CommandConfig + pending_limit = |cmd, bytes| { ..cmd, pending_limit_value: bytes } + + ## Also terminate descendants on cancellation, using a Unix process group + ## or Windows Job Object. Disabled by default; descendants must not escape it. + manage_tree : CommandConfig, Bool -> CommandConfig + manage_tree = |cmd, enabled| { ..cmd, manage_tree_value: enabled } + + # A tag union that crosses the host boundary is closed by design, so the + # four entry points below, like the `Cmd.Child` methods, rebuild the error they + # forward from the host. That gives callers an open union, which `?` can + # combine with other error types however the function is called. + # `tests/cmd-api.roc` covers the function-value form. + + ## Run the command on the parent's terminal and require it to succeed. + ## + ## Standard input, output and error are inherited unless the command sets + ## them. Any status other than exit code 0 is `Err(BadExitStatus(output))`. + ## + ## ```roc + ## Cmd.new("cargo").args(["build", "--release"]).call!()? + ## ``` + call! : CommandConfig => Try({}, [CmdErr(Cmd.Err), BadExitStatus(Cmd.Output), Interrupted]) + call! = |cmd| + match finish!(cmd, Call) { + Ok(output) => + match output.require_success() { + Ok(_) => Ok({}) + Err(BadExitStatus(failed)) => Err(BadExitStatus(failed)) + } + Err(CmdErr(err)) => Err(CmdErr(err)) + Err(Interrupted) => Err(Interrupted) + } + + ## Run the command, require it to succeed, and return its standard output as + ## text. + ## + ## Standard output is captured unless the command sets it. Standard input and + ## standard error are inherited unless the command sets them, so warnings + ## still reach the terminal, as with a shell's `$(..)`. Any status other than + ## exit code 0 is `Err(BadExitStatus(output))`, and output that is not UTF-8 + ## is `Err(BadUtf8(..))`. + ## + ## The text is returned as printed, including a final newline: + ## + ## ```roc + ## sha = Cmd.new("git").args(["rev-parse", "${branch}"]).capture!()?.trim() + ## ``` + capture! : CommandConfig => Try(Str, [CmdErr(Cmd.Err), BadExitStatus(Cmd.Output), BadUtf8({ problem : Str.Utf8Problem, index : U64 }), Interrupted]) + capture! = |cmd| + match finish!(cmd, Capture) { + Ok(output) => + match output.require_success() { + Ok(succeeded) => + match succeeded.stdout_utf8() { + Ok(text) => Ok(text) + Err(BadUtf8(problem)) => Err(BadUtf8(problem)) + } + Err(BadExitStatus(failed)) => Err(BadExitStatus(failed)) + } + Err(CmdErr(err)) => Err(CmdErr(err)) + Err(Interrupted) => Err(Interrupted) + } + + ## Run the command and return its exit status and output as data. A non-zero + ## exit code or a signal is not an error here: match on `output.status`, or + ## call [Cmd.Output.require_success]. + ## + ## Standard output and standard error are captured, and standard input is + ## `Null`, unless the command sets them. The result is returned once the + ## command has exited and all of its output has been read. + ## + ## ```roc + ## output = Cmd.new("roc").args(["check", "main.roc"]).run!()? + ## ``` + run! : CommandConfig => Try(Cmd.Output, [CmdErr(Cmd.Err), Interrupted]) + run! = |cmd| + match finish!(cmd, Run) { + Ok(output) => Ok(output) + Err(CmdErr(err)) => Err(CmdErr(err)) + Err(Interrupted) => Err(Interrupted) + } + + ## Start the command and return a [Cmd.Child] without waiting for it. + ## + ## Standard input, output and error are inherited unless the command sets + ## them. Set a stream to `Pipe` to exchange data with the child while it runs. + ## Keep the returned child for later reads, waits or cleanup; an unused + ## binding such as `_child` does not keep the process alive. See [Cmd.Child]. + spawn! : CommandConfig => Try(Cmd.Child, [CmdErr(Cmd.Err), Interrupted]) + spawn! = |cmd| + match Host.cmd_spawn!(to_host_cmd(cmd, Spawn)) { + Ok(handle) => + Ok( + Cmd.Child.{ + host: handle, + pid_value: Host.child_pid!(handle), + command: to_str(cmd), + program: cmd.program, + }, + ) + Err(CmdErr(err)) => Err(CmdErr(from_host_err(err, cmd.program))) + Err(Interrupted) => Err(Interrupted) + } + + ## The program and its arguments, separated by spaces, for messages and logs. + ## An argument that is empty or contains a space or a quote is written as a + ## quoted, escaped string. The environment, the working directory and the + ## other settings are not included. + ## + ## ```roc + ## expect Cmd.new("git").args(["commit", "-m", "first draft"]).to_str() == "git commit -m \"first draft\"" + ## ``` + to_str : CommandConfig -> Str + to_str = |cmd| + Str.join_with([cmd.program].concat(cmd.args).map(display_word), " ") + + ## Customize command output for `Str.inspect`: the full configuration, with + ## the names of the environment variables it sets but never their values, and + ## the length of `Bytes` input but not its content. + to_inspect : CommandConfig -> Str + to_inspect = |cmd| { + env_names = cmd.envs.map(|(name, _)| name) + stdin_text = + match cmd.stdin_value { + Set(Bytes(bytes)) => "Set(Bytes(<${bytes.len().to_str()} bytes>))" + other => Str.inspect(other) + } + + "Cmd.Command({ program: ${Str.inspect(cmd.program)}, args: ${Str.inspect(cmd.args)}, env_names: ${Str.inspect(env_names)}, clear_envs: ${Str.inspect(cmd.clear_envs)}, cwd: ${Str.inspect(cmd.cwd_value)}, stdin: ${stdin_text}, stdout: ${Str.inspect(cmd.stdout_value)}, stderr: ${Str.inspect(cmd.stderr_value)}, timeout: ${Str.inspect(cmd.timeout_value)}, output_limit: ${Str.inspect(cmd.output_limit_value)}, pending_limit: ${Str.inspect(cmd.pending_limit_value)}, manage_tree: ${Str.inspect(cmd.manage_tree_value)} })" + } +} + ## Run a command to completion with the stream defaults of one entry point. finish! : Cmd.Command, [Run, Call, Capture, Spawn] => Try(Cmd.Output, [CmdErr(Cmd.Err), Interrupted]) finish! = |cmd, entry| diff --git a/platform/Path.roc b/platform/Path.roc index ba1e314c..92c84e65 100644 --- a/platform/Path.roc +++ b/platform/Path.roc @@ -411,8 +411,8 @@ Path := [ parent = |path| match path { Utf8(str) => parent_slash_units(Str.to_utf8(str)).map_ok(|bytes| Utf8(str_from_valid_utf8(bytes))) - UnixBytes(bytes) => parent_slash_units(bytes).map_ok(|parent| UnixBytes(parent)) - WindowsU16s(units) => parent_windows_units(units).map_ok(|parent| WindowsU16s(parent)) + UnixBytes(bytes) => parent_slash_units(bytes).map_ok(|parent_bytes| UnixBytes(parent_bytes)) + WindowsU16s(units) => parent_windows_units(units).map_ok(|parent_units| WindowsU16s(parent_units)) } ## Iterate over this path and its successive [parent] paths, starting with diff --git a/scripts/test.py b/scripts/test.py index 5cc8013a..62277ef6 100755 --- a/scripts/test.py +++ b/scripts/test.py @@ -749,6 +749,9 @@ def run_suite( if "validate" in operations: print("\n=== RUST TEST ===") command("cargo", "test", "--locked", "--lib") + print("\n=== PLATFORM CHECK ===") + # Reevaluate dependency constants and fail on warnings as consumers do. + command(ROC, "check", ROOT / "platform" / "main.roc", "--no-cache", "--max-transitive-mb=256") print("\n=== PLATFORM TEST ===") command(ROC, "test", ROOT / "platform" / "main.roc", *roc_extra_args()) for stage in ("fmt", "check", "test"):