From cf6a48ba7d55d51a44bf117cfebf2ed87e4769ac Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Wed, 21 Oct 2026 08:55:47 +1100 Subject: [PATCH 1/5] Complete 0.25 to 0.26 release migration notes --- docs/releases/0.26.0.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/releases/0.26.0.md b/docs/releases/0.26.0.md index 5824fac4..22ae90f2 100644 --- a/docs/releases/0.26.0.md +++ b/docs/releases/0.26.0.md @@ -12,10 +12,10 @@ Until now each basic-cli module had grown its own habits. A timeout was a bare ` 0.26.0 settles these questions once and applies the answers everywhere: -- **A timeout is a `Duration`**, and `Duration.zero` always expires immediately. +- **Basic-cli timeout options use `Duration`**, and `Duration.zero` always expires immediately. The shared HTTP package keeps its existing request-timeout representation, described below. - **The end of input is `Err(EndOfInput)`**, on stdin, files and sockets alike. An empty `Ok` is real data. - **Anything you can read, you can loop over**: `lines()` and `chunks()` return a `Stream` on stdin, file readers, TCP connections and HTTP responses, and `rows` does the same for SQLite. -- **A module's failures arrive under one tag** (`TcpErr(IOErr)`, `CmdErr(Cmd.Err)`, `SqliteErr(Sqlite.Err)`), and every error can be compared with `==`. +- **A module's failures arrive under one tag** (`TcpErr(IOErr)`, `CmdErr(Cmd.Err)`, `SqliteErr(Sqlite.Err)`), while conditions such as `EndOfInput` and `Interrupted` remain bare tags. Every error can be compared with `==`. The result is that code written against one module reads like code written against another: @@ -129,6 +129,7 @@ File.with_writer!(destination, Truncate, {}, |writer| writer.write_stream!(respo - You see the status before any of the body is read, so you can decline a 404's body. - `idle_timeout` (30 seconds unless you set it) bounds each single wait, not the whole download. A slow but healthy download keeps going; a stalled one stops. After an idle timeout the response is still usable. +- The shared HTTP `Request` keeps its existing timeout representation: `TimeoutMilliseconds(n)` sets the overall deadline, while `NoTimeout` (the default) and `TimeoutMilliseconds(0)` mean no deadline. Streaming `idle_timeout` uses `After(Duration)` or `NoTimeout`; `After(Duration.zero)` expires immediately. - `response.read_to_end!(MaxBytes(1_048_576))` reads the rest with a bound, and `response.content_length()` is `Known(n)` or `Unknown`. - `Http.send!`, `send_json!`, `get!` and `get_utf8!` now take trailing options: pass `{}` to return the first response, as before. - Redirects are opt-in: `Http.send!(request, { max_redirects: 5 })` follows 301, 302, 303, 307 and 308. Streaming accepts the same `max_redirects` option. Intermediate bodies are closed without collecting them; the final status, including a 404, is still a response. @@ -151,7 +152,7 @@ in_msg = conn.read_line!()? ``` - `Tcp.Stream` is now `Tcp.Connection`. It holds its I/O timeout and line limit; `conn.with_timeout(d)` and `conn.with_line_limit(n)` return the same connection with another setting. -- Every failure is `TcpErr(IOErr)`, so `result == Err(TcpErr(TimedOut))` and `Err(TcpErr(ConnectionRefused))` can be matched directly. Windows takes about two seconds to report a refused connection, so a shorter `connect_timeout` gives `TimedOut` there. `IOErr` gains `TimedOut`, `ConnectionRefused`, `ConnectionReset`, `AddrInUse` and `AddrNotAvailable`. +- TCP I/O failures use `TcpErr(IOErr)`, so `result == Err(TcpErr(TimedOut))` and `Err(TcpErr(ConnectionRefused))` can be matched directly. Conditions such as `EndOfInput`, `BadUtf8`, `ReadLimitExceeded` and `Interrupted` remain bare tags. Windows takes about two seconds to report a refused connection, so a shorter `connect_timeout` gives `TimedOut` there. `IOErr` gains `TimedOut`, `ConnectionRefused`, `ConnectionReset`, `AddrInUse` and `AddrNotAvailable`. - **A read that times out or is interrupted consumes nothing.** The bytes that had arrived are returned by the next read, so a client that tolerates idle periods just calls `conn.lines()` again. Caught pending signals return bare `Interrupted`; a failed write may have sent a prefix. - A line longer than the limit is `Err(ReadLimitExceeded(n))`; the rest of it is discarded and the next read starts at the next line. - `listener.accept!({ accept_timeout: NoTimeout, io_timeout: Duration.seconds(30) })` separates how long to wait for a client from the timeout that client's connection gets. @@ -255,6 +256,16 @@ The Windows x64 host keeps the MSVC ABI and now supplies the startup and runtime Update the platform URL and rebuild: the native host interface changed, and mixing hosts and platform sources from different versions is not supported. Then work through the modules you use. +**Target discovery** + +| 0.25 | 0.26 | +|---|---| +| `Env.platform!()` returns a record | `System.target!()` returns an opaque `System.Target` | +| `Env.platform!().arch` | `System.target!().arch()` returns `X64` or `Arm64`; `AARCH64` is now `Arm64` | +| `Env.platform!().os` returns `LINUX`, `MACOS` or `WINDOWS` | `System.target!().os()` returns `Linux(os)`, `MacOS(os)` or `Windows(os)` | + +Keep the witness returned by an OS match when calling OS-specific operations: `os.unix()` on Linux or macOS supplies the evidence required by `Cmd.exec!` and `Terminate`. + **Time** | 0.25 | 0.26 | @@ -306,6 +317,7 @@ When upgrading an earlier 0.26 draft, add writer options: `File.open_writer!(pat | 0.25 | 0.26 | |---|---| +| `Cmd` as a command-builder type annotation | `Cmd.Command`; `Cmd` is the namespace | | `Cmd.exec!("cargo", ["build"])` | `Cmd.call!("cargo", ["build"])` | | `cmd.exec_cmd!()` | `cmd.call!()` | | `cmd.exec_output!()?.stdout_utf8` | `cmd.capture!()?` | @@ -337,7 +349,9 @@ When upgrading an earlier 0.26 draft, add writer options: `File.open_writer!(pat **Everything else** +- **Caught signals add bare `Interrupted` results** to sleep, standard I/O, command waits and pipe I/O, TCP/Unix sockets, HTTP and SQLite operations. Update exhaustive error matches. Before effectful cleanup or retry, acknowledge pending events with `Signal.poll!()`; propagating `Interrupted` to `main!` exits without running application cleanup. - **`IOErr` has five new variants** (`TimedOut`, `ConnectionRefused`, `ConnectionReset`, `AddrInUse`, `AddrNotAvailable`). A `match` that lists every `IOErr` variant needs new arms, even in code that never uses TCP. +- **`Path` representation tags are renamed**: `Unix(bytes)` becomes `UnixBytes(bytes)`, and `Windows(units)` becomes `WindowsU16s(units)`, matching `OsStr`. `Utf8` and the `Path.unix_bytes`/`Path.windows_u16s` constructors keep their names. - **`Path` and `OsStr` values that differed only in representation are now equal**, and collapse to one `Dict` or `Set` key. - **`Tty.enable_raw_mode!` and `disable_raw_mode!` return a `Try`**; add `?`. - **Write `True` and `False`**, not `Bool.True` and `Bool.False`, to match the platform's own docs and examples. From d39e0f97596b99b97476864cd3ea16ce6875c656 Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Wed, 21 Oct 2026 09:25:16 +1100 Subject: [PATCH 2/5] Document rc3 feedback and current HTTP, command, and UTC limits --- README.md | 15 +++++++++++++++ docs/releases/0.26.0.md | 10 ++++++++++ platform/Cmd.roc | 15 +++++++++++++++ platform/Http.roc | 17 +++++++++++++++++ platform/Utc.roc | 11 +++++++++++ 5 files changed, 68 insertions(+) diff --git a/README.md b/README.md index 444b8c50..46c6bfb6 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,21 @@ These host behaviors may affect application design: - HTTP redirects are opt-in through trailing options, `{ max_redirects: n }`; the default is zero. The request deadline covers every hop and the final body. Cross-origin hops strip credentials; HTTPS downgrades are refused. +- HTTP sends no default `User-Agent`. For APIs that require one, such as + GitHub's REST API, build a request with `.add_header("User-Agent", "my-roc-cli")` + and use `Http.send!` or `Http.send_streaming!`. A missing `Content-Type` + currently becomes `text/plain`, even for a bodyless GET; an explicit header + is preserved ([issue #531](https://github.com/roc-lang/basic-cli/issues/531)). +- A spawned `Cmd.Child` is killed and reaped when its last reference is + released; an unused `_child` binding does not keep it alive. `Pipe` output + is read through `child.read!(duration)`, and is absent + from `child.wait!()`'s output; choose `Capture` to keep it for `wait!`. + Direct child-output redirection to a file is not supported yet + ([issue #532](https://github.com/roc-lang/basic-cli/issues/532)). +- `Utc` stores nanoseconds, but `to_iso_8601` formats whole seconds and + floors any fractional second. No ISO 8601/RFC 3339 parser is supplied yet. + Use signed epoch nanoseconds for exact timestamp storage + ([issue #533](https://github.com/roc-lang/basic-cli/issues/533)). - `Signal.catch!` catches Ctrl-C and, with a `System.Unix` witness, SIGTERM (including default `systemctl stop`). Blocking effects return bare `Interrupted`. Call `Signal.poll!()` before effectful application cleanup; diff --git a/docs/releases/0.26.0.md b/docs/releases/0.26.0.md index 22ae90f2..b674e17b 100644 --- a/docs/releases/0.26.0.md +++ b/docs/releases/0.26.0.md @@ -212,6 +212,8 @@ child = Cmd.new("server").stdout(Pipe).spawn!()? # run in th - **Errors and `to_inspect` no longer contain environment values**, so a secret passed with `.env` cannot leak into a log. - Compact `Cmd.call!(program, args)`, `capture!`, `run!`, and `spawn!` complement the reusable `Cmd.Command` builder. Builder execution uses `.call!()` too. - `cmd.inherit_stdio().run!()?.status` waits without recording output in memory. `.stdout(InheritStderr)` sends child stdout directly to parent stderr. +- Keep a spawned `Child` for later reads, waits or cleanup: releasing its last reference kills and reaps a running child. An unused binding such as `_child` can release it immediately. +- A spawned child's `Pipe` output is available only through `child.read!(duration)`; it is absent from `child.wait!()`'s output. Choose `Capture` to retain that stream for `wait!`, subject to the output limit. - `.stderr(MergeWithStdout)` and `.output_limit(NoLimit)` say directly what used to be encoded in a `Bool` or a zero. - Unix `Cmd.exec!(unix, executable_path, args)` replaces the current process and preserves its PID. It uses an exact path with no PATH lookup or shell fallback; success never returns. Finish buffered writers, transactions, terminal raw mode and child cleanup first. A failed replacement leaves the Roc process running. @@ -368,6 +370,14 @@ When upgrading an earlier 0.26 draft, add writer options: `File.open_writer!(pat - CI now formats, lints and unit-tests the Rust host on Linux, macOS and Windows ([#514](https://github.com/roc-lang/basic-cli/pull/514)). - New examples: [`file-write-stream.roc`](https://github.com/roc-lang/basic-cli/blob/0.26.0/examples/file-write-stream.roc) and [`http-download.roc`](https://github.com/roc-lang/basic-cli/blob/0.26.0/examples/http-download.roc). The TCP, command, SQLite, stdin and file examples are rewritten for the new APIs. +## Current limitations + +- **HTTP request headers:** no default `User-Agent` is sent. For APIs that require one, such as GitHub's REST API, construct a `Request` with `.add_header("User-Agent", "my-roc-cli")` and pass it to `Http.send!` or `send_streaming!`. The platform also currently adds `Content-Type: text/plain` when none is set, even for a bodyless GET; an explicit content type is preserved. Changes to these defaults are tracked in [#531](https://github.com/roc-lang/basic-cli/issues/531). +- **Child output to files:** there is no direct file-redirection mode yet ([#532](https://github.com/roc-lang/basic-cli/issues/532)). `Pipe` requires the app to read and write the chunks; for logging independently of the app, use the child's own log-file option or an explicitly invoked operating-system shell. `Capture` and `Tee` retain output in memory. +- **Timestamp strings:** `Utc.to_iso_8601` formats whole seconds, flooring any fractional second, and there is no ISO 8601/RFC 3339 parser yet ([#533](https://github.com/roc-lang/basic-cli/issues/533)). The stored instant still has nanosecond precision; use `to_nanos_since_epoch` and `from_nanos_since_epoch` for exact timestamp storage. + +When copying an example from an RC tag or a source checkout, replace its pinned platform URL with the 0.26.0 bundle URL above before running it. The stable release workflow opens a follow-up PR to update checked-in example URLs after publication. + --- - [API documentation](https://roc-lang.github.io/basic-cli/0.26.0/) diff --git a/platform/Cmd.roc b/platform/Cmd.roc index 888704ae..4bff4ce1 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -37,6 +37,13 @@ import Duration ## Programs and arguments are passed to the operating system as they are: no ## shell is involved, so spaces, `$FOO` and `*.txt` stay literal. ## +## There is no output mode that writes directly to a file or a `File.Writer` +## ([issue #532](https://github.com/roc-lang/basic-cli/issues/532)). `Pipe` +## requires the application to drain [Child.read!] and write each chunk; +## `Capture` and `Tee` retain output in memory. For independent file logging, +## use the child's own log-file option or explicitly invoke an appropriate +## operating-system shell with redirection. +## ## Errors, [Command.to_str] and `Str.inspect` never contain the value of an environment ## variable, so a command that carries a secret can be logged. Cmd :: [].{ @@ -381,6 +388,8 @@ Cmd :: [].{ ## ## 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)) { @@ -553,6 +562,8 @@ Cmd :: [].{ ## When the last reference to a child is released, a child that is still ## running is killed and reaped. Use [Child.close!] to do that at a chosen ## point. After `close!` every other operation returns `Err(Closed)`. + ## An unused binding, including `_child`, can release the last reference + ## immediately. Keep a reference that a later read, wait or cleanup uses. Child :: { host : Resource.Child, pid_value : U32, command : Str, program : OsStr }.{ # A tag union that crosses the host boundary is closed by design, so @@ -568,6 +579,10 @@ Cmd :: [].{ ## return its status and captured output. A piped standard input is ## closed first, so exchange data with [write_bytes!] and [read!] before ## waiting. Waiting again returns the same result. + ## + ## `Pipe` output is available only through [read!]; its output field in + ## this result is empty. Choose `Capture` to retain that stream for + ## `wait!`, subject to the command's [Command.output_limit]. wait! : Child => Try(Output, [CmdErr(Cmd.Err), Closed, Interrupted]) wait! = |child| match Host.child_wait!(child.host) { diff --git a/platform/Http.roc b/platform/Http.roc index 186a7084..96ae9ade 100644 --- a/platform/Http.roc +++ b/platform/Http.roc @@ -18,6 +18,23 @@ import http.Response ## have arrived and hands the body over as a `Stream` of chunks, for a ## download larger than memory or one whose progress is shown. ## +## No default `User-Agent` is sent. APIs such as +## [GitHub's REST API](https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api#user-agent) +## require one; add it to the request before calling [Http.send!] or +## [Http.send_streaming!]: +## +## ```roc +## request = Request.from_method(GET) +## .with_uri("https://api.github.com/repos/roc-lang/basic-cli") +## .add_header("User-Agent", "my-roc-cli") +## response = Http.send!(request, {})? +## ``` +## +## If the request has no `Content-Type` header, the platform currently adds +## `Content-Type: text/plain`, including for a bodyless GET. An explicit +## content type is preserved. [Issue #531](https://github.com/roc-lang/basic-cli/issues/531) +## tracks changes to these defaults. +## ## See the [host runtime behavior](https://github.com/roc-lang/basic-cli#host-runtime-behavior) ## for HTTP protocol, TLS trust-store, and timeout details. Http :: [].{ diff --git a/platform/Utc.roc b/platform/Utc.roc index 8ad394f8..2ad3d518 100644 --- a/platform/Utc.roc +++ b/platform/Utc.roc @@ -13,6 +13,13 @@ nanos_per_milli = 1_000_000 ## [Utc.since]. To measure how long something took, prefer [Monotonic], which ## is unaffected by changes to the wall clock. ## +## There is currently no ISO 8601 or RFC 3339 string parser. [Utc.to_iso_8601] +## formats whole seconds and floors any fractional second; it does not change +## the stored instant. For exact timestamp storage, use signed epoch +## nanoseconds with [Utc.to_nanos_since_epoch] and [Utc.from_nanos_since_epoch]. +## [Issue #533](https://github.com/roc-lang/basic-cli/issues/533) tracks string +## parsing and fractional-second formatting. +## ## Every result is clamped to what its type holds, so nothing here crashes on ## overflow: a `Utc` to the [I128] range, a [Duration] to zero and its maximum. Utc :: { nanos : I128 }.{ @@ -71,6 +78,10 @@ Utc :: { nanos : I128 }.{ ## Format this instant as an ISO 8601 UTC string with second precision, ## flooring any partial second: `2026-10-13T09:30:00Z`. ## + ## The stored nanoseconds are unchanged, but this string omits them; use + ## [to_nanos_since_epoch] when the full precision must round-trip. There + ## is currently no corresponding ISO 8601 or RFC 3339 parser. + ## ## Instants before 1970 are formatted too, in the proleptic Gregorian ## calendar. Years outside 0000 to 9999 use the expanded form with a sign, ## such as `-0001-12-31T23:59:59Z`. From 8da3c86dfd7812ea1f7715351af051a1773c7c0a Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Wed, 21 Oct 2026 09:37:10 +1100 Subject: [PATCH 3/5] Resolve HTTP defaults and add direct child output files for 0.26 --- README.md | 27 +- ci/rust_http_server/src/main.rs | 1 + docs/releases/0.26.0.md | 10 +- platform/Cmd.roc | 49 +- platform/Host.roc | 8 +- platform/Http.roc | 58 ++- platform/Utc.roc | 18 +- src/cmd.rs | 219 +++++++-- src/file_writer.rs | 2 +- src/process_service.rs | 35 +- src/roc_platform_abi.rs | 766 ++++++++++++++++++++++---------- tests/cmd-api.roc | 41 ++ tests/http-redirects.roc | 9 + 13 files changed, 893 insertions(+), 350 deletions(-) diff --git a/README.md b/README.md index 46c6bfb6..bb9ea10e 100644 --- a/README.md +++ b/README.md @@ -52,21 +52,22 @@ These host behaviors may affect application design: - HTTP redirects are opt-in through trailing options, `{ max_redirects: n }`; the default is zero. The request deadline covers every hop and the final body. Cross-origin hops strip credentials; HTTPS downgrades are refused. -- HTTP sends no default `User-Agent`. For APIs that require one, such as - GitHub's REST API, build a request with `.add_header("User-Agent", "my-roc-cli")` - and use `Http.send!` or `Http.send_streaming!`. A missing `Content-Type` - currently becomes `text/plain`, even for a bodyless GET; an explicit header - is preserved ([issue #531](https://github.com/roc-lang/basic-cli/issues/531)). +- HTTP defaults to `User-Agent: roc-basic-cli/0.26`, preserving an explicit + user agent. A non-empty body defaults to `Content-Type: text/plain` unless + one is set; an empty body adds no content type. - A spawned `Cmd.Child` is killed and reaped when its last reference is released; an unused `_child` binding does not keep it alive. `Pipe` output - is read through `child.read!(duration)`, and is absent - from `child.wait!()`'s output; choose `Capture` to keep it for `wait!`. - Direct child-output redirection to a file is not supported yet - ([issue #532](https://github.com/roc-lang/basic-cli/issues/532)). -- `Utc` stores nanoseconds, but `to_iso_8601` formats whole seconds and - floors any fractional second. No ISO 8601/RFC 3339 parser is supplied yet. - Use signed epoch nanoseconds for exact timestamp storage - ([issue #533](https://github.com/roc-lang/basic-cli/issues/533)). + is read through `child.read!(duration)` and is absent from `child.wait!()`; + choose `Capture` to retain it for `wait!`. + `.stdout(File(path, Append, {}))` or `.stderr(File(path, Truncate, {}))` + writes directly to disk with the writer modes and creation permissions. + `MergeWithStdout` shares stdout's open file; file-backed streams do not + consume capture or pending-output budgets. +- `Utc` supplies clock readings and signed epoch conversions. Use a package + such as [roc-time](https://github.com/lukewilliamboswell/roc-time) for timestamp + parsing, calendar calculations and richer formatting. Its documented profiles + use microseconds, while `Utc` stores nanoseconds. `Utc.to_iso_8601` is a + whole-second display; signed epoch nanoseconds preserve the stored instant. - `Signal.catch!` catches Ctrl-C and, with a `System.Unix` witness, SIGTERM (including default `systemctl stop`). Blocking effects return bare `Interrupted`. Call `Signal.poll!()` before effectful application cleanup; diff --git a/ci/rust_http_server/src/main.rs b/ci/rust_http_server/src/main.rs index f68fb888..af727ed3 100644 --- a/ci/rust_http_server/src/main.rs +++ b/ci/rust_http_server/src/main.rs @@ -196,6 +196,7 @@ async fn handle_redirect(req: Request) -> Result, Ge "cookie", "proxy-authorization", "content-type", + "user-agent", "content-length", "x-hop", "x-public", diff --git a/docs/releases/0.26.0.md b/docs/releases/0.26.0.md index b674e17b..c5aa47ea 100644 --- a/docs/releases/0.26.0.md +++ b/docs/releases/0.26.0.md @@ -58,6 +58,8 @@ stale = Utc.now!().since(path.time_modified!()?) > Duration.minutes(5) - **File times** (`time_accessed!`, `time_modified!`, `time_created!`) return `Utc` and succeed for files older than 1970. - Sleep is now fallible: use `Sleep.millis!(250)?` or `duration.sleep!()?`. A caught stop request returns `Err(Interrupted)`. +`Utc` supplies clock readings and signed epoch conversion. Timestamp parsing, calendar calculations and richer formatting belong in packages such as [roc-time](https://github.com/lukewilliamboswell/roc-time), rather than the native host. See its [supported profiles](https://lukewilliamboswell.github.io/roc-time/profiles.html#profiles): roc-time 0.3 uses microseconds, while `Utc` stores nanoseconds. Convert units explicitly between them. `Utc.to_iso_8601` remains a whole-second display; `to_nanos_since_epoch` and `from_nanos_since_epoch` preserve the full stored precision. + ## Loop over input Reading lines used to mean hand-written recursion. Now it is a loop, and the same loop works on every source: @@ -132,6 +134,7 @@ File.with_writer!(destination, Truncate, {}, |writer| writer.write_stream!(respo - The shared HTTP `Request` keeps its existing timeout representation: `TimeoutMilliseconds(n)` sets the overall deadline, while `NoTimeout` (the default) and `TimeoutMilliseconds(0)` mean no deadline. Streaming `idle_timeout` uses `After(Duration)` or `NoTimeout`; `After(Duration.zero)` expires immediately. - `response.read_to_end!(MaxBytes(1_048_576))` reads the rest with a bound, and `response.content_length()` is `Known(n)` or `Unknown`. - `Http.send!`, `send_json!`, `get!` and `get_utf8!` now take trailing options: pass `{}` to return the first response, as before. +- Requests send `User-Agent: roc-basic-cli/0.26` by default, so APIs that require a user agent can be called directly. An explicit `User-Agent` overrides it regardless of header-name casing. `Content-Type: text/plain` is added only for a non-empty body without an explicit content type; bodyless GETs add none ([#531](https://github.com/roc-lang/basic-cli/issues/531)). - Redirects are opt-in: `Http.send!(request, { max_redirects: 5 })` follows 301, 302, 303, 307 and 308. Streaming accepts the same `max_redirects` option. Intermediate bodies are closed without collecting them; the final status, including a 404, is still a response. - Redirects use one overall request deadline, including the final body. Cross-origin hops remove authorization and cookie headers, and HTTPS-to-HTTP redirects fail with `HttpErr(InsecureRedirect)`. A POST becomes GET after 301/302, and a non-HEAD request becomes GET after 303; 307/308 preserve the method and body. - A positive redirect budget that runs out is `HttpErr(RedirectLimitExceeded(n))`. Duplicate `Location` headers and invalid destinations return `AmbiguousRedirectLocation` and `InvalidRedirectUrl(Url.ParseErr)` under `HttpErr`; a missing `Location` returns the original 3xx response. With the default budget of zero, the host returns the first response without validating redirects. @@ -211,6 +214,7 @@ child = Cmd.new("server").stdout(Pipe).spawn!()? # run in th - `.timeout(Duration.minutes(5))` bounds the whole run, including a spawned child, which is killed at the deadline. - **Errors and `to_inspect` no longer contain environment values**, so a secret passed with `.env` cannot leak into a log. - Compact `Cmd.call!(program, args)`, `capture!`, `run!`, and `spawn!` complement the reusable `Cmd.Command` builder. Builder execution uses `.call!()` too. +- Write a child's output directly to disk with `.stdout(File("server.log", Append, {}))` or `.stderr(File(path, Truncate, {}))` ([#532](https://github.com/roc-lang/basic-cli/issues/532)). The modes and options are the same as `File.Writer`, including `RequireNew` and `{ create_permissions: OwnerOnly }`. Relative paths use the command's working directory. With `.stderr(MergeWithStdout)`, both streams share the same open file. No output is captured or queued in memory for a file-backed stream, so capture and pending-output limits do not restrict it. Files are opened before the child starts; a failure returns `CmdErr(StartFailed(...))` and may leave newly created or truncated files. - `cmd.inherit_stdio().run!()?.status` waits without recording output in memory. `.stdout(InheritStderr)` sends child stdout directly to parent stderr. - Keep a spawned `Child` for later reads, waits or cleanup: releasing its last reference kills and reaps a running child. An unused binding such as `_child` can release it immediately. - A spawned child's `Pipe` output is available only through `child.read!(duration)`; it is absent from `child.wait!()`'s output. Choose `Capture` to retain that stream for `wait!`, subject to the output limit. @@ -370,12 +374,6 @@ When upgrading an earlier 0.26 draft, add writer options: `File.open_writer!(pat - CI now formats, lints and unit-tests the Rust host on Linux, macOS and Windows ([#514](https://github.com/roc-lang/basic-cli/pull/514)). - New examples: [`file-write-stream.roc`](https://github.com/roc-lang/basic-cli/blob/0.26.0/examples/file-write-stream.roc) and [`http-download.roc`](https://github.com/roc-lang/basic-cli/blob/0.26.0/examples/http-download.roc). The TCP, command, SQLite, stdin and file examples are rewritten for the new APIs. -## Current limitations - -- **HTTP request headers:** no default `User-Agent` is sent. For APIs that require one, such as GitHub's REST API, construct a `Request` with `.add_header("User-Agent", "my-roc-cli")` and pass it to `Http.send!` or `send_streaming!`. The platform also currently adds `Content-Type: text/plain` when none is set, even for a bodyless GET; an explicit content type is preserved. Changes to these defaults are tracked in [#531](https://github.com/roc-lang/basic-cli/issues/531). -- **Child output to files:** there is no direct file-redirection mode yet ([#532](https://github.com/roc-lang/basic-cli/issues/532)). `Pipe` requires the app to read and write the chunks; for logging independently of the app, use the child's own log-file option or an explicitly invoked operating-system shell. `Capture` and `Tee` retain output in memory. -- **Timestamp strings:** `Utc.to_iso_8601` formats whole seconds, flooring any fractional second, and there is no ISO 8601/RFC 3339 parser yet ([#533](https://github.com/roc-lang/basic-cli/issues/533)). The stored instant still has nanosecond precision; use `to_nanos_since_epoch` and `from_nanos_since_epoch` for exact timestamp storage. - When copying an example from an RC tag or a source checkout, replace its pinned platform URL with the 0.26.0 bundle URL above before running it. The stable release workflow opens a follow-up PR to update checked-in example URLs after publication. --- diff --git a/platform/Cmd.roc b/platform/Cmd.roc index 4bff4ce1..9cfd39fe 100644 --- a/platform/Cmd.roc +++ b/platform/Cmd.roc @@ -6,6 +6,7 @@ import Env import System import Path import Duration +import File ## Build and run child processes with native-safe programs, arguments, and ## environment values. @@ -37,12 +38,10 @@ import Duration ## Programs and arguments are passed to the operating system as they are: no ## shell is involved, so spaces, `$FOO` and `*.txt` stay literal. ## -## There is no output mode that writes directly to a file or a `File.Writer` -## ([issue #532](https://github.com/roc-lang/basic-cli/issues/532)). `Pipe` -## requires the application to drain [Child.read!] and write each chunk; -## `Capture` and `Tee` retain output in memory. For independent file logging, -## use the child's own log-file option or explicitly invoke an appropriate -## operating-system shell with redirection. +## Redirect output directly to disk with `File(path, mode, options)`, using +## [File.WriteMode] and [File.WriterOptions]. Files are opened before the child +## starts; failures are `StartFailed`. Relative paths use the command's working +## directory. Redirected bytes are neither captured nor queued in memory. ## ## Errors, [Command.to_str] and `Str.inspect` never contain the value of an environment ## variable, so a command that carries a secret can be logged. @@ -66,16 +65,18 @@ Cmd :: [].{ ## - `Capture`: kept in memory and returned in the [Output]. ## - `Tee`: captured and also forwarded to the parent's standard output. The ## child sees a pipe, not a terminal. + ## - `File(path, mode, options)`: open a file once and write directly to it. + ## `MergeWithStdout` shares the same open file, including its append mode. ## - `Pipe`: queued as chunks for [Child.read!]. Only [spawn!] returns a ## [Child], so with [call!], [capture!] and [run!] it means `Capture`. - StdoutMode : [Inherit, InheritStderr, Null, Capture, Tee, Pipe] + StdoutMode : [Inherit, InheritStderr, Null, Capture, Tee, Pipe, File(Path, File.WriteMode, File.WriterOptions)] ## Where the child's standard error goes: the modes of [StdoutMode], or ## `MergeWithStdout`, which sends it into the same OS pipe as standard output. ## That keeps the order in which the child wrote to the two streams, which two ## separate streams cannot. The merged stream follows the standard output ## mode and is reported as standard output. - StderrMode : [Inherit, Null, Capture, Tee, Pipe, MergeWithStdout] + StderrMode : [Inherit, Null, Capture, Tee, Pipe, File(Path, File.WriteMode, File.WriterOptions), MergeWithStdout] ## How a child ended: `Exited(code)` with its exit code, or `Signaled(signal)` ## when a signal terminated it. `Signaled` is reported on Unix only. @@ -175,8 +176,8 @@ Cmd :: [].{ program : OsStr, cwd_value : [Inherit, Dir(Path)], stdin_value : [Unset, Set([Inherit, Null, Bytes(List(U8)), Pipe])], - stdout_value : [Unset, Set([Inherit, InheritStderr, Null, Capture, Tee, Pipe])], - stderr_value : [Unset, Set([Inherit, Null, Capture, Tee, Pipe, MergeWithStdout])], + 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, @@ -746,14 +747,14 @@ to_host_cmd = |cmd, entry| { (Unset, _) => Inherit (Set(Pipe), Spawn) => Pipe (Set(Pipe), _) => Capture - (Set(mode), _) => mode + (Set(mode), _) => stdout_to_host(mode) }, stderr: match (cmd.stderr_value, entry) { (Unset, Run) => Capture (Unset, _) => Inherit (Set(Pipe), Spawn) => Pipe (Set(Pipe), _) => Capture - (Set(mode), _) => mode + (Set(mode), _) => stderr_to_host(mode) }, timeout: match cmd.timeout_value { NoTimeout => NoTimeout @@ -1034,3 +1035,27 @@ expect { path = OsStr.windows_u16s([0x43, 0x3B, 0x44]) path_dirs(path, True) == [Path.windows_u16s([0x43]), Path.windows_u16s([0x44])] } + +stdout_to_host : Cmd.StdoutMode -> Host.CmdStdout +stdout_to_host = |mode| + match mode { + Inherit => Inherit + InheritStderr => InheritStderr + Null => Null + Capture => Capture + Tee => Tee + Pipe => Pipe + File(path, write_mode, options) => File({ path: Path.to_raw(path), mode: write_mode, create_permissions: options.create_permissions }) + } + +stderr_to_host : Cmd.StderrMode -> Host.CmdStderr +stderr_to_host = |mode| + match mode { + Inherit => Inherit + Null => Null + Capture => Capture + Tee => Tee + Pipe => Pipe + MergeWithStdout => MergeWithStdout + File(path, write_mode, options) => File({ path: Path.to_raw(path), mode: write_mode, create_permissions: options.create_permissions }) + } diff --git a/platform/Host.roc b/platform/Host.roc index 69d204ff..61f89b01 100644 --- a/platform/Host.roc +++ b/platform/Host.roc @@ -9,6 +9,10 @@ Host :: [].{ NativeOsStr : [Utf8(Str), UnixBytes(List(U8)), WindowsU16s(List(U16))] NativePath : [Utf8(Str), UnixBytes(List(U8)), WindowsU16s(List(U16))] + CmdFile : { path : NativePath, mode : [Append, RequireNew, Truncate], create_permissions : [Default, OwnerOnly, PublicRead] } + CmdStdout : [Inherit, InheritStderr, Null, Capture, Tee, Pipe, File(CmdFile)] + CmdStderr : [Inherit, Null, Capture, Tee, Pipe, File(CmdFile), MergeWithStdout] + # A command whose streams, deadline and limits are already resolved: `Cmd.roc` # applies the defaults of the entry point before the command crosses. Cmd : { @@ -16,8 +20,8 @@ Host :: [].{ clear_envs : Bool, cwd : [Inherit, Dir(NativePath)], stdin : [Inherit, Null, Bytes(List(U8)), Pipe], - stdout : [Inherit, InheritStderr, Null, Capture, Tee, Pipe], - stderr : [Inherit, Null, Capture, Tee, Pipe, MergeWithStdout], + stdout : CmdStdout, + stderr : CmdStderr, timeout : [NoTimeout, AfterNanos(U64)], output_limit : [NoLimit, MaxBytes(U64)], pending_limit : U64, diff --git a/platform/Http.roc b/platform/Http.roc index 96ae9ade..326e7b9d 100644 --- a/platform/Http.roc +++ b/platform/Http.roc @@ -18,22 +18,10 @@ import http.Response ## have arrived and hands the body over as a `Stream` of chunks, for a ## download larger than memory or one whose progress is shown. ## -## No default `User-Agent` is sent. APIs such as -## [GitHub's REST API](https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api#user-agent) -## require one; add it to the request before calling [Http.send!] or -## [Http.send_streaming!]: -## -## ```roc -## request = Request.from_method(GET) -## .with_uri("https://api.github.com/repos/roc-lang/basic-cli") -## .add_header("User-Agent", "my-roc-cli") -## response = Http.send!(request, {})? -## ``` -## -## If the request has no `Content-Type` header, the platform currently adds -## `Content-Type: text/plain`, including for a bodyless GET. An explicit -## content type is preserved. [Issue #531](https://github.com/roc-lang/basic-cli/issues/531) -## tracks changes to these defaults. +## Requests default to `User-Agent: roc-basic-cli/0.26`. An explicit +## `User-Agent` is preserved, regardless of the header name's casing. +## A non-empty body defaults to `Content-Type: text/plain`; an empty body +## adds no content type. Explicit content types are always preserved. ## ## See the [host runtime behavior](https://github.com/roc-lang/basic-cli#host-runtime-behavior) ## for HTTP protocol, TLS trust-store, and timeout details. @@ -182,13 +170,7 @@ Http :: [].{ send_streaming! = |request, options| { url = Url.parse(Request.uri(request)) ? InvalidUrl canonical = Url.without_fragment(url) - headers = Request.headers(request) - initial_headers = if headers.any(|header| header.name.with_ascii_lowercased() == "content-type") { - headers - } else { - headers.append({ name: "Content-Type", value: "text/plain" }) - } - canonical_request = request.with_uri(canonical.to_str()).with_headers(initial_headers) + canonical_request = request_with_defaults(request.with_uri(canonical.to_str())) deadline = match Request.timeout(request) { NoTimeout | TimeoutMilliseconds(0) => NoTimeout TimeoutMilliseconds(ms) => At(Host.monotonic_now!().plus_saturated(ms.times_saturated(1_000_000))) @@ -455,3 +437,33 @@ expect { next = redirected_request(request, "https://a.example/", "https://b.example/", 302) Request.method(next) == GET and Request.body(next) == [] and Request.headers(next) == [{ name: "X-Public", value: "kept" }] } + +## Apply request defaults once, before redirect rewriting can remove body headers. +request_with_defaults : Request -> Request +request_with_defaults = |request| { + headers = Request.headers(request) + with_agent = if headers.any(|header| header.name.with_ascii_lowercased() == "user-agent") { + headers + } else { + headers.append({ name: "User-Agent", value: "roc-basic-cli/0.26" }) + } + resolved = if !Request.body(request).is_empty() and !headers.any(|header| header.name.with_ascii_lowercased() == "content-type") { + with_agent.append({ name: "Content-Type", value: "text/plain" }) + } else { + with_agent + } + request.with_headers(resolved) +} + +expect { + request = request_with_defaults(Request.from_method(GET)) + request.headers() == [{ name: "User-Agent", value: "roc-basic-cli/0.26" }] +} +expect { + request = request_with_defaults(Request.from_method(POST).with_body([1])) + request.headers().contains({ name: "Content-Type", value: "text/plain" }) +} +expect { + headers = [{ name: "uSeR-aGeNt", value: "my-cli" }, { name: "content-TYPE", value: "application/custom" }] + request_with_defaults(Request.from_method(GET).with_headers(headers)).headers() == headers +} diff --git a/platform/Utc.roc b/platform/Utc.roc index 2ad3d518..a67af49c 100644 --- a/platform/Utc.roc +++ b/platform/Utc.roc @@ -13,12 +13,16 @@ nanos_per_milli = 1_000_000 ## [Utc.since]. To measure how long something took, prefer [Monotonic], which ## is unaffected by changes to the wall clock. ## -## There is currently no ISO 8601 or RFC 3339 string parser. [Utc.to_iso_8601] -## formats whole seconds and floors any fractional second; it does not change -## the stored instant. For exact timestamp storage, use signed epoch -## nanoseconds with [Utc.to_nanos_since_epoch] and [Utc.from_nanos_since_epoch]. -## [Issue #533](https://github.com/roc-lang/basic-cli/issues/533) tracks string -## parsing and fractional-second formatting. +## This platform supplies clock effects and signed epoch conversions. Use a +## package such as [roc-time](https://github.com/lukewilliamboswell/roc-time) +## for timestamp parsing, calendar operations and richer formatting. Its +## [supported profiles](https://lukewilliamboswell.github.io/roc-time/profiles.html#profiles) +## describe the accepted formats and precision; roc-time 0.3 uses microseconds, +## while `Utc` stores nanoseconds. Convert units explicitly at that boundary. +## [Utc.to_iso_8601] is a convenience display at whole-second precision. +## Use [Utc.to_nanos_since_epoch] and [Utc.from_nanos_since_epoch] for exact +## timestamp storage. Parsing and richer formatting are outside this module's +## scope rather than planned additions to the native host. ## ## Every result is clamped to what its type holds, so nothing here crashes on ## overflow: a `Utc` to the [I128] range, a [Duration] to zero and its maximum. @@ -80,7 +84,7 @@ Utc :: { nanos : I128 }.{ ## ## The stored nanoseconds are unchanged, but this string omits them; use ## [to_nanos_since_epoch] when the full precision must round-trip. There - ## is currently no corresponding ISO 8601 or RFC 3339 parser. + ## is no corresponding parser here; use a time package for text interchange. ## ## Instants before 1970 are formatted too, in the proleptic Gregorian ## calendar. Years outside 0000 to 9999 use the expanded form with a sign, diff --git a/src/cmd.rs b/src/cmd.rs index 3e00fc81..e8b78c93 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -29,12 +29,11 @@ type ChildUnitResult = HostChildKillResult; type ChildUnitResultPayload = HostChildKillResultPayload; type ChildUnitResultTag = HostChildKillResultTag; -// RustGlue does not currently emit a semantic return alias for an error-only -// hosted function. This is the generated [Failed(IOErr), Interrupted] result of -// Host.cmd_exec!; regenerate-glue --check detects changes to its generated name. -type ExecFailure = FailedOrInterruptedType384; -type ExecFailureTag = FailedOrInterruptedType384Tag; -type ExecFailurePayload = FailedOrInterruptedType384Payload; +// Generated aliases for the error-only [Failed(IOErr), Interrupted] result +// of Host.cmd_exec!, kept independent of the generator's numbered type name. +type ExecFailure = FailedOrInterrupted; +type ExecFailureTag = FailedOrInterruptedTag; +type ExecFailurePayload = FailedOrInterruptedPayload; fn take_arg_list( list: &RocList, @@ -90,15 +89,40 @@ fn command_config(cmd: Cmd, roc_host: &RocHost) -> io::Result { BytesOrInheritOrNullOrPipeTag::Pipe => StdinMode::Pipe, }; - let mut command = std::process::Command::new(program?); - command.args(args?); + // Convert both path payloads before propagating any conversion failure, + // so every Roc-owned field is consumed even when another field is invalid. + let stdout = stdout_config(cmd.stdout, roc_host); + let stderr = stderr_config(cmd.stderr, roc_host); + let program = program?; + let args = args?; + let envs = envs?; + let cwd = cwd?; + let (stdout, stdout_spec) = stdout?; + let (stderr, stderr_spec) = stderr?; + let open = |spec: Option| -> io::Result> { + if crate::signals::is_pending() { + return Err(io::ErrorKind::Interrupted.into()); + } + spec.map(|spec| { + let path = match &cwd { + Some(directory) if spec.path.is_relative() => directory.join(&spec.path), + _ => spec.path, + }; + crate::file_writer::open_file(&path, spec.mode, spec.policy) + }) + .transpose() + }; + let stdout_file = open(stdout_spec)?; + let stderr_file = open(stderr_spec)?; + let mut command = std::process::Command::new(program); + command.args(args); if cmd.clear_envs { command.env_clear(); } - if let Some(cwd) = cwd? { + if let Some(cwd) = cwd { command.current_dir(cwd); } - for pair in envs?.chunks(2) { + for pair in envs.chunks(2) { if let [key, value] = pair { command.env(key, value); } @@ -107,27 +131,10 @@ fn command_config(cmd: Cmd, roc_host: &RocHost) -> io::Result { Ok(Config { command, stdin, - stdout: match cmd.stdout { - CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee::Capture => OutputMode::Capture, - CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee::Inherit => OutputMode::Inherit, - CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee::InheritStderr => { - OutputMode::InheritStderr - } - CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee::Null => OutputMode::Null, - CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee::Pipe => OutputMode::Pipe, - CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee::Tee => OutputMode::Tee, - }, - stderr: { - use CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee as Mode; - match cmd.stderr { - Mode::Capture => StderrMode::Separate(OutputMode::Capture), - Mode::Inherit => StderrMode::Separate(OutputMode::Inherit), - Mode::Null => StderrMode::Separate(OutputMode::Null), - Mode::Pipe => StderrMode::Separate(OutputMode::Pipe), - Mode::Tee => StderrMode::Separate(OutputMode::Tee), - Mode::MergeWithStdout => StderrMode::MergeWithStdout, - } - }, + stdout, + stderr, + stdout_file, + stderr_file, timeout: match cmd.timeout.tag { AfterNanosOrNoTimeoutTag::AfterNanos => { Some(Duration::from_nanos(ManuallyDrop::into_inner(unsafe { @@ -149,6 +156,76 @@ fn command_config(cmd: Cmd, roc_host: &RocHost) -> io::Result { }) } +struct FileOutput { + path: std::path::PathBuf, + mode: AppendOrRequireNewOrTruncate, + policy: crate::file_security::Policy, +} +fn file_output( + file: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeFile, + host: &RocHost, +) -> io::Result { + let path = path_from_native(file.path, host)?; + let policy = match file.create_permissions { + DefaultOrOwnerOnlyOrPublicRead::Default => crate::file_security::Policy::Default, + DefaultOrOwnerOnlyOrPublicRead::OwnerOnly => crate::file_security::Policy::OwnerOnly, + DefaultOrOwnerOnlyOrPublicRead::PublicRead => crate::file_security::Policy::PublicRead, + }; + Ok(FileOutput { + path, + mode: file.mode, + policy, + }) +} +fn stdout_config( + mode: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee, + host: &RocHost, +) -> io::Result<(OutputMode, Option)> { + use CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag as Tag; + let mode = match mode.tag { + Tag::Capture => OutputMode::Capture, + Tag::Inherit => OutputMode::Inherit, + Tag::InheritStderr => OutputMode::InheritStderr, + Tag::Null => OutputMode::Null, + Tag::Pipe => OutputMode::Pipe, + Tag::Tee => OutputMode::Tee, + Tag::File => { + return Ok(( + OutputMode::File, + Some(file_output( + ManuallyDrop::into_inner(unsafe { mode.payload.file }), + host, + )?), + )) + } + }; + Ok((mode, None)) +} +fn stderr_config( + mode: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee, + host: &RocHost, +) -> io::Result<(StderrMode, Option)> { + use CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag as Tag; + let mode = match mode.tag { + Tag::Capture => OutputMode::Capture, + Tag::Inherit => OutputMode::Inherit, + Tag::Null => OutputMode::Null, + Tag::Pipe => OutputMode::Pipe, + Tag::Tee => OutputMode::Tee, + Tag::MergeWithStdout => return Ok((StderrMode::MergeWithStdout, None)), + Tag::File => { + return Ok(( + StderrMode::Separate(OutputMode::File), + Some(file_output( + ManuallyDrop::into_inner(unsafe { mode.payload.file }), + host, + )?), + )) + } + }; + Ok((StderrMode::Separate(mode), None)) +} + fn same_command(cmd: HostCmdSpawnArgs) -> Cmd { Cmd { args: cmd.args, @@ -169,7 +246,14 @@ fn same_command(cmd: HostCmdSpawnArgs) -> Cmd { /// Start a command. A command that cannot be converted for this operating /// system never starts, which is the same failure as a program that is missing. fn start(cmd: Cmd) -> Result { - Child::spawn(command_config(cmd, roc_host()).map_err(Error::Start)?) + let config = command_config(cmd, roc_host()).map_err(|error| { + if error.kind() == io::ErrorKind::Interrupted && crate::signals::is_pending() { + Error::Interrupted + } else { + Error::Start(error) + } + })?; + Child::spawn(config) } fn partial_output(partial: &Partial) -> CmdPartialOutput { @@ -750,8 +834,16 @@ mod ownership_tests { }, clear_envs: true, manage_tree: true, - stderr: CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee::MergeWithStdout, - stdout: CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee::Tee, + stderr: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee { + tag: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::MergeWithStdout, + payload: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeePayload { + merge_with_stdout: [], + }, + }, + stdout: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee { + tag: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::Tee, + payload: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeePayload { tee: [] }, + }, } } @@ -810,7 +902,10 @@ mod ownership_tests { tag: MaxBytesOrNoLimitTag::NoLimit, payload: MaxBytesOrNoLimitPayload { no_limit: [] }, }; - cmd.stderr = CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee::Pipe; + cmd.stderr = CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee { + tag: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::Pipe, + payload: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeePayload { pipe: [] }, + }; let config = command_config(cmd, &host).unwrap(); assert_eq!(config.command.get_current_dir(), None); assert_eq!(config.timeout, None); @@ -841,10 +936,62 @@ mod ownership_tests { }), }, }; - let error = command_config(heap_command(foreign, &host), &host) + let mut command = heap_command(foreign, &host); + let file = |path| CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeFile { + path: utf8(path, &host), + mode: AppendOrRequireNewOrTruncate::RequireNew, + create_permissions: DefaultOrOwnerOnlyOrPublicRead::OwnerOnly, + }; + command.stdout = CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee { + tag: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::File, + payload: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeePayload { + file: ManuallyDrop::new(file("a stdout pathname needing a heap allocation")), + }, + }; + command.stderr = CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee { + tag: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::File, + payload: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeePayload { + file: ManuallyDrop::new(file("a stderr pathname needing a heap allocation")), + }, + }; + let error = command_config(command, &host) .err() .expect("a foreign program name must not convert"); assert_eq!(error.kind(), io::ErrorKind::Unsupported); + finish(host, 10); + } + + #[test] + fn pending_stop_does_not_truncate_a_redirected_file() { + let host = host(); + let path = std::env::temp_dir().join(format!( + "roc-cmd-pending-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::write(&path, b"keep this log").unwrap(); + let mut command = heap_command(utf8("some program", &host), &host); + command.stdout = CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee { + tag: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::File, + payload: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeePayload { + file: ManuallyDrop::new( + CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeFile { + path: utf8(path.to_str().unwrap(), &host), + mode: AppendOrRequireNewOrTruncate::Truncate, + create_permissions: DefaultOrOwnerOnlyOrPublicRead::Default, + }, + ), + }, + }; + crate::signals::set_test_pending(true); + let result = command_config(command, &host); + crate::signals::set_test_pending(false); + assert_eq!(result.err().unwrap().kind(), io::ErrorKind::Interrupted); + assert_eq!(std::fs::read(&path).unwrap(), b"keep this log"); + std::fs::remove_file(path).unwrap(); finish(host, 8); } diff --git a/src/file_writer.rs b/src/file_writer.rs index 18aa289e..3fc98700 100644 --- a/src/file_writer.rs +++ b/src/file_writer.rs @@ -24,7 +24,7 @@ pub struct FileWriter(BufWriter); /// What `File.WriteMode` asks of the file that is opened. type WriteMode = AppendOrRequireNewOrTruncate; -fn open_file(path: &Path, mode: WriteMode, policy: Policy) -> io::Result { +pub(crate) fn open_file(path: &Path, mode: WriteMode, policy: Policy) -> io::Result { open_file_with_create(path, mode, policy, file_security::open_new) } diff --git a/src/process_service.rs b/src/process_service.rs index 7f0cc61c..2cae0822 100644 --- a/src/process_service.rs +++ b/src/process_service.rs @@ -81,6 +81,8 @@ pub enum OutputMode { Inherit, /// Inherit the parent's standard error without capturing output. InheritStderr, + /// Write through a file descriptor installed before spawning. + File, Null, Capture, /// Captured and forwarded to the parent's stream. @@ -99,6 +101,8 @@ pub struct Config { pub stdin: StdinMode, pub stdout: OutputMode, pub stderr: StderrMode, + pub stdout_file: Option, + pub stderr_file: Option, /// `None` is no deadline; zero expires immediately. pub timeout: Option, /// Combined capture budget; `None` is unlimited. @@ -190,7 +194,7 @@ fn output_stdio(mode: OutputMode) -> Stdio { match mode { OutputMode::Inherit => Stdio::inherit(), OutputMode::InheritStderr => Stdio::from(std::io::stderr()), - OutputMode::Null => Stdio::null(), + OutputMode::Null | OutputMode::File => Stdio::null(), OutputMode::Capture | OutputMode::Tee | OutputMode::Pipe => Stdio::piped(), } } @@ -479,7 +483,10 @@ impl Pump { stop(&shared, &mut state, Stop::OutputLimit); } } - OutputMode::Inherit | OutputMode::InheritStderr | OutputMode::Null => {} + OutputMode::Inherit + | OutputMode::InheritStderr + | OutputMode::Null + | OutputMode::File => {} } shared.changed.notify_all(); } @@ -511,14 +518,34 @@ async fn spawn(mut config: Config) -> io::Result { StdinMode::Null => Stdio::null(), StdinMode::Bytes(_) | StdinMode::Pipe => Stdio::piped(), }); - config.command.stdout(output_stdio(config.stdout)); + let stdout_file = config.stdout_file.take(); + if config.stdout == OutputMode::File { + let file = stdout_file.ok_or_else(|| io::Error::other("missing stdout file"))?; + if config.stderr == StderrMode::MergeWithStdout { + // Duplicate the same open file description: offset and O_APPEND are + // shared, so stderr cannot reopen/truncate the stdout destination. + config.command.stderr(Stdio::from(file.try_clone()?)); + } + config.command.stdout(Stdio::from(file)); + } else { + config.command.stdout(output_stdio(config.stdout)); + } let merge = config.stderr == StderrMode::MergeWithStdout; // Both descriptors share one kernel pipe, preserving their actual write ordering. let merged = match config.stderr { + StderrMode::Separate(OutputMode::File) => { + let file = config + .stderr_file + .take() + .ok_or_else(|| io::Error::other("missing stderr file"))?; + config.command.stderr(Stdio::from(file)); + None + } StderrMode::Separate(mode) => { config.command.stderr(output_stdio(mode)); None } + StderrMode::MergeWithStdout if config.stdout == OutputMode::File => None, StderrMode::MergeWithStdout if config.stdout == OutputMode::Null => { config.command.stderr(Stdio::null()); None @@ -764,6 +791,8 @@ mod tests { stdin: StdinMode::Null, stdout: OutputMode::Capture, stderr: StderrMode::Separate(OutputMode::Capture), + stdout_file: None, + stderr_file: None, timeout: Some(Duration::from_secs(3)), output_limit: Some(16 * 1024 * 1024), pending_limit: 1024 * 1024, diff --git a/src/roc_platform_abi.rs b/src/roc_platform_abi.rs index d25105b0..8777fa71 100644 --- a/src/roc_platform_abi.rs +++ b/src/roc_platform_abi.rs @@ -1358,11 +1358,11 @@ const _: () = assert!(core::mem::size_of::() == 24, #[cfg(target_pointer_width = "32")] const _: () = assert!(core::mem::align_of::() == 4, "AnonStruct3e7554e024207e25 alignment mismatch"); -/// Element type for __AnonStruct_d681562afa848954 +/// Element type for __AnonStruct_1027aaeb0344dea1 #[cfg(target_pointer_width = "32")] #[repr(C)] #[derive(Clone, Copy)] -pub struct AnonStructD681562afa848954 { +pub struct AnonStruct1027aaeb0344dea1 { pub output_limit: MaxBytesOrNoLimit, pub pending_limit: u64, pub timeout: AfterNanosOrNoTimeout, @@ -1370,18 +1370,18 @@ pub struct AnonStructD681562afa848954 { pub cwd: DirOrInherit, pub envs: RocList, pub program: UnixBytesOrUtf8OrWindowsU16s, + pub stderr: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee, pub stdin: BytesOrInheritOrNullOrPipe, + pub stdout: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee, pub clear_envs: bool, pub manage_tree: bool, - pub stderr: CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee, - pub stdout: CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee, } -/// Element type for __AnonStruct_d681562afa848954 +/// Element type for __AnonStruct_1027aaeb0344dea1 #[cfg(not(target_pointer_width = "32"))] #[repr(C)] #[derive(Clone, Copy)] -pub struct AnonStructD681562afa848954 { +pub struct AnonStruct1027aaeb0344dea1 { pub output_limit: MaxBytesOrNoLimit, pub pending_limit: u64, pub timeout: AfterNanosOrNoTimeout, @@ -1389,21 +1389,50 @@ pub struct AnonStructD681562afa848954 { pub cwd: DirOrInherit, pub envs: RocList, pub program: UnixBytesOrUtf8OrWindowsU16s, + pub stderr: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee, pub stdin: BytesOrInheritOrNullOrPipe, + pub stdout: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee, pub clear_envs: bool, pub manage_tree: bool, - pub stderr: CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee, - pub stdout: CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee, } #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 200, "AnonStructD681562afa848954 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 296, "AnonStruct1027aaeb0344dea1 size mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::align_of::() == 8, "AnonStructD681562afa848954 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct1027aaeb0344dea1 alignment mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 120, "AnonStructD681562afa848954 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 168, "AnonStruct1027aaeb0344dea1 size mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::align_of::() == 8, "AnonStructD681562afa848954 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "AnonStruct1027aaeb0344dea1 alignment mismatch"); + +/// Element type for __AnonStruct_b1229292e7a43aa6 +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStructB1229292e7a43aa6 { + pub path: UnixBytesOrUtf8OrWindowsU16s, + pub create_permissions: DefaultOrOwnerOnlyOrPublicRead, + pub mode: AppendOrRequireNewOrTruncate, +} + +/// Element type for __AnonStruct_b1229292e7a43aa6 +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct AnonStructB1229292e7a43aa6 { + pub path: UnixBytesOrUtf8OrWindowsU16s, + pub create_permissions: DefaultOrOwnerOnlyOrPublicRead, + pub mode: AppendOrRequireNewOrTruncate, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 40, "AnonStructB1229292e7a43aa6 size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "AnonStructB1229292e7a43aa6 alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 20, "AnonStructB1229292e7a43aa6 size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "AnonStructB1229292e7a43aa6 alignment mismatch"); /// Element type for __AnonStruct_3e0cd494b019cad5 #[cfg(target_pointer_width = "32")] @@ -8394,26 +8423,142 @@ const _: () = assert!(core::mem::align_of::() == 8, "MaxBytes #[cfg(target_pointer_width = "32")] const _: () = assert!(core::mem::offset_of!(MaxBytesOrNoLimit, tag) == 8, "MaxBytesOrNoLimit tag offset mismatch"); -/// Tag union: CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee +/// Tag discriminant for CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee { +pub enum CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag { Capture = 0, - Inherit = 1, - MergeWithStdout = 2, - Null = 3, - Pipe = 4, - Tee = 5, + File = 1, + Inherit = 2, + MergeWithStdout = 3, + Null = 4, + Pipe = 5, + Tee = 6, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeePayload { + pub capture: [u8; 0], + pub file: core::mem::ManuallyDrop, + pub inherit: [u8; 0], + pub merge_with_stdout: [u8; 0], + pub null: [u8; 0], + pub pipe: [u8; 0], + pub tee: [u8; 0], +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeePayloadAlignment; + +/// Tag union: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee { + pub _payload_alignment: [CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeePayloadAlignment; 0], + pub payload: [u8; 20], + pub tag: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag, +} + +/// Tag union: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee { + pub payload: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeePayload, + pub tag: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag, +} + +impl CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee { + /// Borrow the `File` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::File` and the payload must still be initialized. + #[cfg(target_pointer_width = "32")] + pub unsafe fn borrow_payload_file_unchecked(&self) -> &AnonStructB1229292e7a43aa6 { + unsafe { &*(self.payload.as_ptr() as *const AnonStructB1229292e7a43aa6) } + } + + /// Borrow the `File` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::File` and the payload must still be initialized. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn borrow_payload_file_unchecked(&self) -> &AnonStructB1229292e7a43aa6 { + unsafe { &*(&self.payload.file as *const core::mem::ManuallyDrop as *const AnonStructB1229292e7a43aa6) } + } + + /// Move the `File` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::File`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(target_pointer_width = "32")] + pub unsafe fn take_payload_file_unchecked(&mut self) -> AnonStructB1229292e7a43aa6 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const AnonStructB1229292e7a43aa6) } + } + + /// Move the `File` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::File`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn take_payload_file_unchecked(&mut self) -> AnonStructB1229292e7a43aa6 { + unsafe { core::mem::ManuallyDrop::take(&mut self.payload.file) } + } + +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 48, "CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee alignment mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::offset_of!(CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee, tag) == 40, "CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 24, "CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 4, "CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::offset_of!(CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee, tag) == 20, "CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee tag offset mismatch"); + +/// Tag union: DefaultOrOwnerOnlyOrPublicRead +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DefaultOrOwnerOnlyOrPublicRead { + Default = 0, + OwnerOnly = 1, + PublicRead = 2, } #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 1, "CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee size mismatch"); +const _: () = assert!(core::mem::size_of::() == 1, "DefaultOrOwnerOnlyOrPublicRead size mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::align_of::() == 1, "CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 1, "DefaultOrOwnerOnlyOrPublicRead alignment mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 1, "CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee size mismatch"); +const _: () = assert!(core::mem::size_of::() == 1, "DefaultOrOwnerOnlyOrPublicRead size mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::align_of::() == 1, "CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 1, "DefaultOrOwnerOnlyOrPublicRead alignment mismatch"); + +/// Tag union: AppendOrRequireNewOrTruncate +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AppendOrRequireNewOrTruncate { + Append = 0, + RequireNew = 1, + Truncate = 2, +} + +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::size_of::() == 1, "AppendOrRequireNewOrTruncate size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 1, "AppendOrRequireNewOrTruncate alignment mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 1, "AppendOrRequireNewOrTruncate size mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::align_of::() == 1, "AppendOrRequireNewOrTruncate alignment mismatch"); /// Tag discriminant for BytesOrInheritOrNullOrPipe. #[repr(u8)] @@ -8510,26 +8655,106 @@ const _: () = assert!(core::mem::align_of::() == 4, #[cfg(target_pointer_width = "32")] const _: () = assert!(core::mem::offset_of!(BytesOrInheritOrNullOrPipe, tag) == 12, "BytesOrInheritOrNullOrPipe tag offset mismatch"); -/// Tag union: CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee +/// Tag discriminant for CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee { +pub enum CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag { Capture = 0, - Inherit = 1, - InheritStderr = 2, - Null = 3, - Pipe = 4, - Tee = 5, + File = 1, + Inherit = 2, + InheritStderr = 3, + Null = 4, + Pipe = 5, + Tee = 6, +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub union CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeePayload { + pub capture: [u8; 0], + pub file: core::mem::ManuallyDrop, + pub inherit: [u8; 0], + pub inherit_stderr: [u8; 0], + pub null: [u8; 0], + pub pipe: [u8; 0], + pub tee: [u8; 0], +} + +#[cfg(target_pointer_width = "32")] +#[repr(align(4))] +#[derive(Clone, Copy)] +pub struct CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeePayloadAlignment; + +/// Tag union: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee +#[cfg(target_pointer_width = "32")] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee { + pub _payload_alignment: [CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeePayloadAlignment; 0], + pub payload: [u8; 20], + pub tag: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag, +} + +/// Tag union: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee +#[cfg(not(target_pointer_width = "32"))] +#[repr(C)] +#[derive(Clone, Copy)] +pub struct CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee { + pub payload: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeePayload, + pub tag: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag, +} + +impl CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee { + /// Borrow the `File` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::File` and the payload must still be initialized. + #[cfg(target_pointer_width = "32")] + pub unsafe fn borrow_payload_file_unchecked(&self) -> &AnonStructB1229292e7a43aa6 { + unsafe { &*(self.payload.as_ptr() as *const AnonStructB1229292e7a43aa6) } + } + + /// Borrow the `File` payload without creating another owner. + /// + /// # Safety + /// `self.tag` must be `CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::File` and the payload must still be initialized. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn borrow_payload_file_unchecked(&self) -> &AnonStructB1229292e7a43aa6 { + unsafe { &*(&self.payload.file as *const core::mem::ManuallyDrop as *const AnonStructB1229292e7a43aa6) } + } + + /// Move the `File` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::File`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(target_pointer_width = "32")] + pub unsafe fn take_payload_file_unchecked(&mut self) -> AnonStructB1229292e7a43aa6 { + unsafe { core::ptr::read(self.payload.as_ptr() as *const AnonStructB1229292e7a43aa6) } + } + + /// Move the `File` payload out of one owned tag-union shell. + /// + /// # Safety + /// `self.tag` must be `CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::File`. After this call, `self` is logically uninitialized and must not be read or destroyed. + #[cfg(not(target_pointer_width = "32"))] + pub unsafe fn take_payload_file_unchecked(&mut self) -> AnonStructB1229292e7a43aa6 { + unsafe { core::mem::ManuallyDrop::take(&mut self.payload.file) } + } + } #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 1, "CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee size mismatch"); +const _: () = assert!(core::mem::size_of::() == 48, "CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee size mismatch"); +#[cfg(target_pointer_width = "64")] +const _: () = assert!(core::mem::align_of::() == 8, "CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee alignment mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::align_of::() == 1, "CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee alignment mismatch"); +const _: () = assert!(core::mem::offset_of!(CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee, tag) == 40, "CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee tag offset mismatch"); +#[cfg(target_pointer_width = "32")] +const _: () = assert!(core::mem::size_of::() == 24, "CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee size mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 1, "CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee size mismatch"); +const _: () = assert!(core::mem::align_of::() == 4, "CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee alignment mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::align_of::() == 1, "CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee alignment mismatch"); +const _: () = assert!(core::mem::offset_of!(CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee, tag) == 20, "CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee tag offset mismatch"); /// Tag discriminant for AfterNanosOrNoTimeout. #[repr(u8)] @@ -11308,42 +11533,6 @@ const _: () = assert!(core::mem::align_of::() == 4, "H #[cfg(target_pointer_width = "32")] const _: () = assert!(core::mem::offset_of!(HostFileOpenWriterResult, tag) == 16, "HostFileOpenWriterResult tag offset mismatch"); -/// Tag union: AppendOrRequireNewOrTruncate -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AppendOrRequireNewOrTruncate { - Append = 0, - RequireNew = 1, - Truncate = 2, -} - -#[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 1, "AppendOrRequireNewOrTruncate size mismatch"); -#[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::align_of::() == 1, "AppendOrRequireNewOrTruncate alignment mismatch"); -#[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 1, "AppendOrRequireNewOrTruncate size mismatch"); -#[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::align_of::() == 1, "AppendOrRequireNewOrTruncate alignment mismatch"); - -/// Tag union: DefaultOrOwnerOnlyOrPublicRead -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DefaultOrOwnerOnlyOrPublicRead { - Default = 0, - OwnerOnly = 1, - PublicRead = 2, -} - -#[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 1, "DefaultOrOwnerOnlyOrPublicRead size mismatch"); -#[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::align_of::() == 1, "DefaultOrOwnerOnlyOrPublicRead alignment mismatch"); -#[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 1, "DefaultOrOwnerOnlyOrPublicRead size mismatch"); -#[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::align_of::() == 1, "DefaultOrOwnerOnlyOrPublicRead alignment mismatch"); - /// Tag discriminant for Try. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -12149,14 +12338,14 @@ const _: () = assert!(core::mem::offset_of!(HostSignalCatchResult, tag) == 16, " /// Tag discriminant for InterruptOrTerminate. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum InterruptOrTerminateType379Tag { +pub enum InterruptOrTerminateType380Tag { Interrupt = 0, Terminate = 1, } #[repr(C)] #[derive(Clone, Copy)] -pub union InterruptOrTerminateType379Payload { +pub union InterruptOrTerminateType380Payload { pub interrupt: [u8; 0], pub terminate: core::mem::ManuallyDrop, } @@ -12164,32 +12353,32 @@ pub union InterruptOrTerminateType379Payload { #[cfg(target_pointer_width = "32")] #[repr(align(1))] #[derive(Clone, Copy)] -pub struct InterruptOrTerminateType379PayloadAlignment; +pub struct InterruptOrTerminateType380PayloadAlignment; /// Tag union: InterruptOrTerminate #[cfg(target_pointer_width = "32")] #[repr(C)] #[derive(Clone, Copy)] -pub struct InterruptOrTerminateType379 { - pub _payload_alignment: [InterruptOrTerminateType379PayloadAlignment; 0], +pub struct InterruptOrTerminateType380 { + pub _payload_alignment: [InterruptOrTerminateType380PayloadAlignment; 0], pub payload: [u8; 1], - pub tag: InterruptOrTerminateType379Tag, + pub tag: InterruptOrTerminateType380Tag, } /// Tag union: InterruptOrTerminate #[cfg(not(target_pointer_width = "32"))] #[repr(C)] #[derive(Clone, Copy)] -pub struct InterruptOrTerminateType379 { - pub payload: InterruptOrTerminateType379Payload, - pub tag: InterruptOrTerminateType379Tag, +pub struct InterruptOrTerminateType380 { + pub payload: InterruptOrTerminateType380Payload, + pub tag: InterruptOrTerminateType380Tag, } -impl InterruptOrTerminateType379 { +impl InterruptOrTerminateType380 { /// Borrow the `Terminate` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateType379Tag::Terminate` and the payload must still be initialized. + /// `self.tag` must be `InterruptOrTerminateType380Tag::Terminate` and the payload must still be initialized. #[cfg(target_pointer_width = "32")] pub unsafe fn borrow_payload_terminate_unchecked(&self) -> &SystemUnix { unsafe { &*(self.payload.as_ptr() as *const SystemUnix) } @@ -12198,7 +12387,7 @@ impl InterruptOrTerminateType379 { /// Borrow the `Terminate` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateType379Tag::Terminate` and the payload must still be initialized. + /// `self.tag` must be `InterruptOrTerminateType380Tag::Terminate` and the payload must still be initialized. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn borrow_payload_terminate_unchecked(&self) -> &SystemUnix { unsafe { &*(&self.payload.terminate as *const core::mem::ManuallyDrop as *const SystemUnix) } @@ -12207,7 +12396,7 @@ impl InterruptOrTerminateType379 { /// Move the `Terminate` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateType379Tag::Terminate`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `InterruptOrTerminateType380Tag::Terminate`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(target_pointer_width = "32")] pub unsafe fn take_payload_terminate_unchecked(&mut self) -> SystemUnix { unsafe { core::ptr::read(self.payload.as_ptr() as *const SystemUnix) } @@ -12216,7 +12405,7 @@ impl InterruptOrTerminateType379 { /// Move the `Terminate` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `InterruptOrTerminateType379Tag::Terminate`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `InterruptOrTerminateType380Tag::Terminate`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn take_payload_terminate_unchecked(&mut self) -> SystemUnix { unsafe { core::mem::ManuallyDrop::take(&mut self.payload.terminate) } @@ -12225,46 +12414,46 @@ impl InterruptOrTerminateType379 { } #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 2, "InterruptOrTerminateType379 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 2, "InterruptOrTerminateType380 size mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::align_of::() == 1, "InterruptOrTerminateType379 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 1, "InterruptOrTerminateType380 alignment mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::offset_of!(InterruptOrTerminateType379, tag) == 1, "InterruptOrTerminateType379 tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(InterruptOrTerminateType380, tag) == 1, "InterruptOrTerminateType380 tag offset mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 2, "InterruptOrTerminateType379 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 2, "InterruptOrTerminateType380 size mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::align_of::() == 1, "InterruptOrTerminateType379 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 1, "InterruptOrTerminateType380 alignment mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::offset_of!(InterruptOrTerminateType379, tag) == 1, "InterruptOrTerminateType379 tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(InterruptOrTerminateType380, tag) == 1, "InterruptOrTerminateType380 tag offset mismatch"); /// Tag union: InterruptOrTerminate #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum InterruptOrTerminateType383 { +pub enum InterruptOrTerminateType384 { Interrupt = 0, Terminate = 1, } #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 1, "InterruptOrTerminateType383 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 1, "InterruptOrTerminateType384 size mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::align_of::() == 1, "InterruptOrTerminateType383 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 1, "InterruptOrTerminateType384 alignment mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 1, "InterruptOrTerminateType383 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 1, "InterruptOrTerminateType384 size mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::align_of::() == 1, "InterruptOrTerminateType383 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 1, "InterruptOrTerminateType384 alignment mismatch"); /// Tag discriminant for FailedOrInterrupted. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FailedOrInterruptedType384Tag { +pub enum FailedOrInterruptedType385Tag { Failed = 0, Interrupted = 1, } #[repr(C)] #[derive(Clone, Copy)] -pub union FailedOrInterruptedType384Payload { +pub union FailedOrInterruptedType385Payload { pub failed: core::mem::ManuallyDrop, pub interrupted: [u8; 0], } @@ -12272,32 +12461,32 @@ pub union FailedOrInterruptedType384Payload { #[cfg(target_pointer_width = "32")] #[repr(align(4))] #[derive(Clone, Copy)] -pub struct FailedOrInterruptedType384PayloadAlignment; +pub struct FailedOrInterruptedType385PayloadAlignment; /// Tag union: FailedOrInterrupted #[cfg(target_pointer_width = "32")] #[repr(C)] #[derive(Clone, Copy)] -pub struct FailedOrInterruptedType384 { - pub _payload_alignment: [FailedOrInterruptedType384PayloadAlignment; 0], +pub struct FailedOrInterruptedType385 { + pub _payload_alignment: [FailedOrInterruptedType385PayloadAlignment; 0], pub payload: [u8; 16], - pub tag: FailedOrInterruptedType384Tag, + pub tag: FailedOrInterruptedType385Tag, } /// Tag union: FailedOrInterrupted #[cfg(not(target_pointer_width = "32"))] #[repr(C)] #[derive(Clone, Copy)] -pub struct FailedOrInterruptedType384 { - pub payload: FailedOrInterruptedType384Payload, - pub tag: FailedOrInterruptedType384Tag, +pub struct FailedOrInterruptedType385 { + pub payload: FailedOrInterruptedType385Payload, + pub tag: FailedOrInterruptedType385Tag, } -impl FailedOrInterruptedType384 { +impl FailedOrInterruptedType385 { /// Borrow the `Failed` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `FailedOrInterruptedType384Tag::Failed` and the payload must still be initialized. + /// `self.tag` must be `FailedOrInterruptedType385Tag::Failed` and the payload must still be initialized. #[cfg(target_pointer_width = "32")] pub unsafe fn borrow_payload_failed_unchecked(&self) -> &IOErr { unsafe { &*(self.payload.as_ptr() as *const IOErr) } @@ -12306,7 +12495,7 @@ impl FailedOrInterruptedType384 { /// Borrow the `Failed` payload without creating another owner. /// /// # Safety - /// `self.tag` must be `FailedOrInterruptedType384Tag::Failed` and the payload must still be initialized. + /// `self.tag` must be `FailedOrInterruptedType385Tag::Failed` and the payload must still be initialized. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn borrow_payload_failed_unchecked(&self) -> &IOErr { unsafe { &*(&self.payload.failed as *const core::mem::ManuallyDrop as *const IOErr) } @@ -12315,7 +12504,7 @@ impl FailedOrInterruptedType384 { /// Move the `Failed` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `FailedOrInterruptedType384Tag::Failed`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `FailedOrInterruptedType385Tag::Failed`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(target_pointer_width = "32")] pub unsafe fn take_payload_failed_unchecked(&mut self) -> IOErr { unsafe { core::ptr::read(self.payload.as_ptr() as *const IOErr) } @@ -12324,7 +12513,7 @@ impl FailedOrInterruptedType384 { /// Move the `Failed` payload out of one owned tag-union shell. /// /// # Safety - /// `self.tag` must be `FailedOrInterruptedType384Tag::Failed`. After this call, `self` is logically uninitialized and must not be read or destroyed. + /// `self.tag` must be `FailedOrInterruptedType385Tag::Failed`. After this call, `self` is logically uninitialized and must not be read or destroyed. #[cfg(not(target_pointer_width = "32"))] pub unsafe fn take_payload_failed_unchecked(&mut self) -> IOErr { unsafe { core::mem::ManuallyDrop::take(&mut self.payload.failed) } @@ -12333,17 +12522,17 @@ impl FailedOrInterruptedType384 { } #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 40, "FailedOrInterruptedType384 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 40, "FailedOrInterruptedType385 size mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::align_of::() == 8, "FailedOrInterruptedType384 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 8, "FailedOrInterruptedType385 alignment mismatch"); #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::offset_of!(FailedOrInterruptedType384, tag) == 32, "FailedOrInterruptedType384 tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(FailedOrInterruptedType385, tag) == 32, "FailedOrInterruptedType385 tag offset mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 20, "FailedOrInterruptedType384 size mismatch"); +const _: () = assert!(core::mem::size_of::() == 20, "FailedOrInterruptedType385 size mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::align_of::() == 4, "FailedOrInterruptedType384 alignment mismatch"); +const _: () = assert!(core::mem::align_of::() == 4, "FailedOrInterruptedType385 alignment mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::offset_of!(FailedOrInterruptedType384, tag) == 16, "FailedOrInterruptedType384 tag offset mismatch"); +const _: () = assert!(core::mem::offset_of!(FailedOrInterruptedType385, tag) == 16, "FailedOrInterruptedType385 tag offset mismatch"); /// Tag union: OwnerOnlyOrPublicRead #[repr(u8)] @@ -14097,7 +14286,7 @@ pub struct HostCmdExecArgs { } /// Arguments for Host.cmd_run! -/// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, cwd : [Dir([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), Inherit], envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), manage_tree : Bool, output_limit : [MaxBytes(U64), NoLimit], pending_limit : U64, program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], stderr : [Capture, Inherit, MergeWithStdout, Null, Pipe, Tee], stdin : [Bytes(List(U8)), Inherit, Null, Pipe], stdout : [Capture, Inherit, InheritStderr, Null, Pipe, Tee], timeout : [AfterNanos(U64), NoTimeout] } => Try({ status : [Exited(I32), Signaled(I32)], stderr_bytes : List(U8), stdout_bytes : List(U8) }, [CmdErr([IOErr(IOErr), OutputLimitExceeded({ stderr_bytes : List(U8), stdout_bytes : List(U8) }), StartFailed(IOErr), TimedOut({ stderr_bytes : List(U8), stdout_bytes : List(U8) })]), Interrupted]) +/// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, cwd : [Dir([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), Inherit], envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), manage_tree : Bool, output_limit : [MaxBytes(U64), NoLimit], pending_limit : U64, program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], stderr : [Capture, File({ create_permissions : [Default, OwnerOnly, PublicRead], mode : [Append, RequireNew, Truncate], path : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] }), Inherit, MergeWithStdout, Null, Pipe, Tee], stdin : [Bytes(List(U8)), Inherit, Null, Pipe], stdout : [Capture, File({ create_permissions : [Default, OwnerOnly, PublicRead], mode : [Append, RequireNew, Truncate], path : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] }), Inherit, InheritStderr, Null, Pipe, Tee], timeout : [AfterNanos(U64), NoTimeout] } => Try({ status : [Exited(I32), Signaled(I32)], stderr_bytes : List(U8), stdout_bytes : List(U8) }, [CmdErr([IOErr(IOErr), OutputLimitExceeded({ stderr_bytes : List(U8), stdout_bytes : List(U8) }), StartFailed(IOErr), TimedOut({ stderr_bytes : List(U8), stdout_bytes : List(U8) })]), Interrupted]) /// Refcounted fields are owned by the hosted function. #[cfg(target_pointer_width = "32")] #[repr(C)] @@ -14110,15 +14299,15 @@ pub struct HostCmdRunArgs { pub cwd: DirOrInherit, pub envs: RocList, pub program: UnixBytesOrUtf8OrWindowsU16s, + pub stderr: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee, pub stdin: BytesOrInheritOrNullOrPipe, + pub stdout: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee, pub clear_envs: bool, pub manage_tree: bool, - pub stderr: CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee, - pub stdout: CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee, } /// Arguments for Host.cmd_run! -/// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, cwd : [Dir([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), Inherit], envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), manage_tree : Bool, output_limit : [MaxBytes(U64), NoLimit], pending_limit : U64, program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], stderr : [Capture, Inherit, MergeWithStdout, Null, Pipe, Tee], stdin : [Bytes(List(U8)), Inherit, Null, Pipe], stdout : [Capture, Inherit, InheritStderr, Null, Pipe, Tee], timeout : [AfterNanos(U64), NoTimeout] } => Try({ status : [Exited(I32), Signaled(I32)], stderr_bytes : List(U8), stdout_bytes : List(U8) }, [CmdErr([IOErr(IOErr), OutputLimitExceeded({ stderr_bytes : List(U8), stdout_bytes : List(U8) }), StartFailed(IOErr), TimedOut({ stderr_bytes : List(U8), stdout_bytes : List(U8) })]), Interrupted]) +/// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, cwd : [Dir([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), Inherit], envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), manage_tree : Bool, output_limit : [MaxBytes(U64), NoLimit], pending_limit : U64, program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], stderr : [Capture, File({ create_permissions : [Default, OwnerOnly, PublicRead], mode : [Append, RequireNew, Truncate], path : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] }), Inherit, MergeWithStdout, Null, Pipe, Tee], stdin : [Bytes(List(U8)), Inherit, Null, Pipe], stdout : [Capture, File({ create_permissions : [Default, OwnerOnly, PublicRead], mode : [Append, RequireNew, Truncate], path : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] }), Inherit, InheritStderr, Null, Pipe, Tee], timeout : [AfterNanos(U64), NoTimeout] } => Try({ status : [Exited(I32), Signaled(I32)], stderr_bytes : List(U8), stdout_bytes : List(U8) }, [CmdErr([IOErr(IOErr), OutputLimitExceeded({ stderr_bytes : List(U8), stdout_bytes : List(U8) }), StartFailed(IOErr), TimedOut({ stderr_bytes : List(U8), stdout_bytes : List(U8) })]), Interrupted]) /// Refcounted fields are owned by the hosted function. #[cfg(not(target_pointer_width = "32"))] #[repr(C)] @@ -14131,19 +14320,19 @@ pub struct HostCmdRunArgs { pub cwd: DirOrInherit, pub envs: RocList, pub program: UnixBytesOrUtf8OrWindowsU16s, + pub stderr: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee, pub stdin: BytesOrInheritOrNullOrPipe, + pub stdout: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee, pub clear_envs: bool, pub manage_tree: bool, - pub stderr: CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee, - pub stdout: CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee, } #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 200, "HostCmdRunArgs size mismatch"); +const _: () = assert!(core::mem::size_of::() == 296, "HostCmdRunArgs size mismatch"); #[cfg(target_pointer_width = "64")] const _: () = assert!(core::mem::align_of::() == 8, "HostCmdRunArgs alignment mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 120, "HostCmdRunArgs size mismatch"); +const _: () = assert!(core::mem::size_of::() == 168, "HostCmdRunArgs size mismatch"); #[cfg(target_pointer_width = "32")] const _: () = assert!(core::mem::align_of::() == 8, "HostCmdRunArgs alignment mismatch"); @@ -14160,8 +14349,8 @@ impl HostCmdRunArgs { unsafe { value.cwd.decref(roc_host); } unsafe { decref_list_of_unix_bytes_or_utf8or_windows_u16s(value.envs, roc_host); } unsafe { value.program.decref(roc_host); } - unsafe { value.stdin.decref(roc_host); } unsafe { value.stderr.decref(roc_host); } + unsafe { value.stdin.decref(roc_host); } unsafe { value.stdout.decref(roc_host); } } @@ -14178,8 +14367,8 @@ impl HostCmdRunArgs { unsafe { value.cwd.incref(amount); } unsafe { value.envs.incref(amount); } unsafe { value.program.incref(amount); } - unsafe { value.stdin.incref(amount); } unsafe { value.stderr.incref(amount); } + unsafe { value.stdin.incref(amount); } unsafe { value.stdout.incref(amount); } } } @@ -14193,7 +14382,7 @@ unsafe impl RocRelease for HostCmdRunArgsRelease { } /// Arguments for Host.cmd_spawn! -/// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, cwd : [Dir([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), Inherit], envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), manage_tree : Bool, output_limit : [MaxBytes(U64), NoLimit], pending_limit : U64, program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], stderr : [Capture, Inherit, MergeWithStdout, Null, Pipe, Tee], stdin : [Bytes(List(U8)), Inherit, Null, Pipe], stdout : [Capture, Inherit, InheritStderr, Null, Pipe, Tee], timeout : [AfterNanos(U64), NoTimeout] } => Try(Resource.Handle([ChildKind]), [CmdErr([IOErr(IOErr), OutputLimitExceeded({ stderr_bytes : List(U8), stdout_bytes : List(U8) }), StartFailed(IOErr), TimedOut({ stderr_bytes : List(U8), stdout_bytes : List(U8) })]), Interrupted]) +/// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, cwd : [Dir([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), Inherit], envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), manage_tree : Bool, output_limit : [MaxBytes(U64), NoLimit], pending_limit : U64, program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], stderr : [Capture, File({ create_permissions : [Default, OwnerOnly, PublicRead], mode : [Append, RequireNew, Truncate], path : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] }), Inherit, MergeWithStdout, Null, Pipe, Tee], stdin : [Bytes(List(U8)), Inherit, Null, Pipe], stdout : [Capture, File({ create_permissions : [Default, OwnerOnly, PublicRead], mode : [Append, RequireNew, Truncate], path : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] }), Inherit, InheritStderr, Null, Pipe, Tee], timeout : [AfterNanos(U64), NoTimeout] } => Try(Resource.Handle([ChildKind]), [CmdErr([IOErr(IOErr), OutputLimitExceeded({ stderr_bytes : List(U8), stdout_bytes : List(U8) }), StartFailed(IOErr), TimedOut({ stderr_bytes : List(U8), stdout_bytes : List(U8) })]), Interrupted]) /// Refcounted fields are owned by the hosted function. #[cfg(target_pointer_width = "32")] #[repr(C)] @@ -14206,15 +14395,15 @@ pub struct HostCmdSpawnArgs { pub cwd: DirOrInherit, pub envs: RocList, pub program: UnixBytesOrUtf8OrWindowsU16s, + pub stderr: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee, pub stdin: BytesOrInheritOrNullOrPipe, + pub stdout: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee, pub clear_envs: bool, pub manage_tree: bool, - pub stderr: CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee, - pub stdout: CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee, } /// Arguments for Host.cmd_spawn! -/// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, cwd : [Dir([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), Inherit], envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), manage_tree : Bool, output_limit : [MaxBytes(U64), NoLimit], pending_limit : U64, program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], stderr : [Capture, Inherit, MergeWithStdout, Null, Pipe, Tee], stdin : [Bytes(List(U8)), Inherit, Null, Pipe], stdout : [Capture, Inherit, InheritStderr, Null, Pipe, Tee], timeout : [AfterNanos(U64), NoTimeout] } => Try(Resource.Handle([ChildKind]), [CmdErr([IOErr(IOErr), OutputLimitExceeded({ stderr_bytes : List(U8), stdout_bytes : List(U8) }), StartFailed(IOErr), TimedOut({ stderr_bytes : List(U8), stdout_bytes : List(U8) })]), Interrupted]) +/// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, cwd : [Dir([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), Inherit], envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), manage_tree : Bool, output_limit : [MaxBytes(U64), NoLimit], pending_limit : U64, program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], stderr : [Capture, File({ create_permissions : [Default, OwnerOnly, PublicRead], mode : [Append, RequireNew, Truncate], path : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] }), Inherit, MergeWithStdout, Null, Pipe, Tee], stdin : [Bytes(List(U8)), Inherit, Null, Pipe], stdout : [Capture, File({ create_permissions : [Default, OwnerOnly, PublicRead], mode : [Append, RequireNew, Truncate], path : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] }), Inherit, InheritStderr, Null, Pipe, Tee], timeout : [AfterNanos(U64), NoTimeout] } => Try(Resource.Handle([ChildKind]), [CmdErr([IOErr(IOErr), OutputLimitExceeded({ stderr_bytes : List(U8), stdout_bytes : List(U8) }), StartFailed(IOErr), TimedOut({ stderr_bytes : List(U8), stdout_bytes : List(U8) })]), Interrupted]) /// Refcounted fields are owned by the hosted function. #[cfg(not(target_pointer_width = "32"))] #[repr(C)] @@ -14227,19 +14416,19 @@ pub struct HostCmdSpawnArgs { pub cwd: DirOrInherit, pub envs: RocList, pub program: UnixBytesOrUtf8OrWindowsU16s, + pub stderr: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee, pub stdin: BytesOrInheritOrNullOrPipe, + pub stdout: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee, pub clear_envs: bool, pub manage_tree: bool, - pub stderr: CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee, - pub stdout: CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee, } #[cfg(target_pointer_width = "64")] -const _: () = assert!(core::mem::size_of::() == 200, "HostCmdSpawnArgs size mismatch"); +const _: () = assert!(core::mem::size_of::() == 296, "HostCmdSpawnArgs size mismatch"); #[cfg(target_pointer_width = "64")] const _: () = assert!(core::mem::align_of::() == 8, "HostCmdSpawnArgs alignment mismatch"); #[cfg(target_pointer_width = "32")] -const _: () = assert!(core::mem::size_of::() == 120, "HostCmdSpawnArgs size mismatch"); +const _: () = assert!(core::mem::size_of::() == 168, "HostCmdSpawnArgs size mismatch"); #[cfg(target_pointer_width = "32")] const _: () = assert!(core::mem::align_of::() == 8, "HostCmdSpawnArgs alignment mismatch"); @@ -14256,8 +14445,8 @@ impl HostCmdSpawnArgs { unsafe { value.cwd.decref(roc_host); } unsafe { decref_list_of_unix_bytes_or_utf8or_windows_u16s(value.envs, roc_host); } unsafe { value.program.decref(roc_host); } - unsafe { value.stdin.decref(roc_host); } unsafe { value.stderr.decref(roc_host); } + unsafe { value.stdin.decref(roc_host); } unsafe { value.stdout.decref(roc_host); } } @@ -14274,8 +14463,8 @@ impl HostCmdSpawnArgs { unsafe { value.cwd.incref(amount); } unsafe { value.envs.incref(amount); } unsafe { value.program.incref(amount); } - unsafe { value.stdin.incref(amount); } unsafe { value.stderr.incref(amount); } + unsafe { value.stdin.incref(amount); } unsafe { value.stdout.incref(amount); } } } @@ -14807,7 +14996,7 @@ pub struct HostPathTypeArgs { #[repr(C)] #[derive(Clone, Copy)] pub struct HostSignalCatchArgs { - pub arg0: RocListWith, + pub arg0: RocListWith, } /// Arguments for Host.sleep_nanos! @@ -15239,10 +15428,10 @@ pub type HostChildWriteErrCmdErrTimedOut = AnonStruct3e7554e024207e25; pub type HostCmdExecArg2 = UnixBytesOrUtf8OrWindowsU16s; pub type HostCmdExecArg2Payload = UnixBytesOrUtf8OrWindowsU16sPayload; pub type HostCmdExecArg2Tag = UnixBytesOrUtf8OrWindowsU16sTag; -pub type FailedOrInterrupted = FailedOrInterruptedType384; -pub type FailedOrInterruptedPayload = FailedOrInterruptedType384Payload; -pub type FailedOrInterruptedTag = FailedOrInterruptedType384Tag; -pub type HostCmdRunArg0 = AnonStructD681562afa848954; +pub type FailedOrInterrupted = FailedOrInterruptedType385; +pub type FailedOrInterruptedPayload = FailedOrInterruptedType385Payload; +pub type FailedOrInterruptedTag = FailedOrInterruptedType385Tag; +pub type HostCmdRunArg0 = AnonStruct1027aaeb0344dea1; pub type HostCmdRunArg0Args = UnixBytesOrUtf8OrWindowsU16s; pub type HostCmdRunArg0ArgsPayload = UnixBytesOrUtf8OrWindowsU16sPayload; pub type HostCmdRunArg0ArgsTag = UnixBytesOrUtf8OrWindowsU16sTag; @@ -15252,6 +15441,8 @@ pub type DirOrInheritDirTag = UnixBytesOrUtf8OrWindowsU16sTag; pub type HostCmdRunArg0Envs = UnixBytesOrUtf8OrWindowsU16s; pub type HostCmdRunArg0EnvsPayload = UnixBytesOrUtf8OrWindowsU16sPayload; pub type HostCmdRunArg0EnvsTag = UnixBytesOrUtf8OrWindowsU16sTag; +pub type CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeFile = AnonStructB1229292e7a43aa6; +pub type CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeFile = AnonStructB1229292e7a43aa6; pub type HostCmdRunErr = CmdErrOrInterrupted; pub type HostCmdRunErrPayload = CmdErrOrInterruptedPayload; pub type HostCmdRunErrTag = CmdErrOrInterruptedTag; @@ -15266,7 +15457,7 @@ pub type CmdErrOrInterruptedCmdErrPayload = IOErrOrOutputLimitExceededOrStartFai pub type CmdErrOrInterruptedCmdErrTag = IOErrOrOutputLimitExceededOrStartFailedOrTimedOutTag; pub type CmdErrOrInterruptedCmdErrOutputLimitExceeded = AnonStruct3e7554e024207e25; pub type CmdErrOrInterruptedCmdErrTimedOut = AnonStruct3e7554e024207e25; -pub type HostCmdSpawnArg0 = AnonStructD681562afa848954; +pub type HostCmdSpawnArg0 = AnonStruct1027aaeb0344dea1; pub type HostCmdSpawnArg0Args = UnixBytesOrUtf8OrWindowsU16s; pub type HostCmdSpawnArg0ArgsPayload = UnixBytesOrUtf8OrWindowsU16sPayload; pub type HostCmdSpawnArg0ArgsTag = UnixBytesOrUtf8OrWindowsU16sTag; @@ -15435,15 +15626,15 @@ pub type HostPathSymlinkResult = HostEnvSetCwdResult; pub type HostPathSymlinkResultPayload = HostEnvSetCwdResultPayload; pub type HostPathSymlinkResultTag = HostEnvSetCwdResultTag; pub type HostPathTypeOk = DirOrFileOrOtherOrSymLink; -pub type HostSignalCatchArg0 = InterruptOrTerminateType379; -pub type HostSignalCatchArg0Payload = InterruptOrTerminateType379Payload; -pub type HostSignalCatchArg0Tag = InterruptOrTerminateType379Tag; +pub type HostSignalCatchArg0 = InterruptOrTerminateType380; +pub type HostSignalCatchArg0Payload = InterruptOrTerminateType380Payload; +pub type HostSignalCatchArg0Tag = InterruptOrTerminateType380Tag; pub type HostSignalCatchArg0Terminate = SystemUnix; -pub type InterruptOrTerminate = InterruptOrTerminateType379; -pub type InterruptOrTerminatePayload = InterruptOrTerminateType379Payload; -pub type InterruptOrTerminateTag = InterruptOrTerminateType379Tag; +pub type InterruptOrTerminate = InterruptOrTerminateType380; +pub type InterruptOrTerminatePayload = InterruptOrTerminateType380Payload; +pub type InterruptOrTerminateTag = InterruptOrTerminateType380Tag; pub type InterruptOrTerminateTerminate = SystemUnix; -pub type HostSignalPoll = InterruptOrTerminateType383; +pub type HostSignalPoll = InterruptOrTerminateType384; pub type HostSqliteCloseErr = AnonStruct8dda646697632dbf; pub type HostSqliteExecuteArg2 = BytesOrIntegerOrNullOrRealOrString; pub type HostSqliteExecuteArg2Payload = BytesOrIntegerOrNullOrRealOrStringPayload; @@ -18660,7 +18851,7 @@ unsafe impl RocRelease for AnonStruct3e7554e024207e2 } } -impl AnonStructD681562afa848954 { +impl AnonStruct1027aaeb0344dea1 { /// Recursively decrement Roc-owned fields. /// /// # Safety @@ -18673,8 +18864,8 @@ impl AnonStructD681562afa848954 { unsafe { value.cwd.decref(roc_host); } unsafe { decref_list_of_unix_bytes_or_utf8or_windows_u16s(value.envs, roc_host); } unsafe { value.program.decref(roc_host); } - unsafe { value.stdin.decref(roc_host); } unsafe { value.stderr.decref(roc_host); } + unsafe { value.stdin.decref(roc_host); } unsafe { value.stdout.decref(roc_host); } } @@ -18691,16 +18882,16 @@ impl AnonStructD681562afa848954 { unsafe { value.cwd.incref(amount); } unsafe { value.envs.incref(amount); } unsafe { value.program.incref(amount); } - unsafe { value.stdin.incref(amount); } unsafe { value.stderr.incref(amount); } + unsafe { value.stdin.incref(amount); } unsafe { value.stdout.incref(amount); } } } -pub struct AnonStructD681562afa848954Release; +pub struct AnonStruct1027aaeb0344dea1Release; -unsafe impl RocRelease for AnonStructD681562afa848954Release { - unsafe fn release(value: AnonStructD681562afa848954, roc_host: &RocHost) { +unsafe impl RocRelease for AnonStruct1027aaeb0344dea1Release { + unsafe fn release(value: AnonStruct1027aaeb0344dea1, roc_host: &RocHost) { unsafe { value.decref(roc_host); } } } @@ -18785,7 +18976,93 @@ unsafe impl RocRelease for MaxBytesOrNoLimitRelease { } } -impl CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee { +impl CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let mut value = self; + let _ = roc_host; + match value.tag { + CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::Capture => {}, + CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::File => { + let payload = unsafe { value.take_payload_file_unchecked() }; + unsafe { payload.decref(roc_host); } + }, + CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::Inherit => {}, + CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::MergeWithStdout => {}, + CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::Null => {}, + CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::Pipe => {}, + CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::Tee => {}, + } + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + let _ = amount; + match value.tag { + CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::Capture => {}, + CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::File => { + let payload = unsafe { core::ptr::read(value.borrow_payload_file_unchecked()) }; + unsafe { payload.incref(amount); } + }, + CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::Inherit => {}, + CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::MergeWithStdout => {}, + CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::Null => {}, + CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::Pipe => {}, + CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeTag::Tee => {}, + } + } +} + +pub struct CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeRelease; + +unsafe impl RocRelease for CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeRelease { + unsafe fn release(value: CaptureOrFileOrInheritOrMergeWithStdoutOrNullOrPipeOrTee, roc_host: &RocHost) { + unsafe { value.decref(roc_host); } + } +} + +impl AnonStructB1229292e7a43aa6 { + /// Recursively decrement Roc-owned fields. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted field. + pub unsafe fn decref(self, roc_host: &RocHost) { + let value = self; + unsafe { value.path.decref(roc_host); } + unsafe { value.create_permissions.decref(roc_host); } + unsafe { value.mode.decref(roc_host); } + } + + /// Increment Roc-owned fields. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let value = self; + unsafe { value.path.incref(amount); } + unsafe { value.create_permissions.incref(amount); } + unsafe { value.mode.incref(amount); } + } +} + +pub struct AnonStructB1229292e7a43aa6Release; + +unsafe impl RocRelease for AnonStructB1229292e7a43aa6Release { + unsafe fn release(value: AnonStructB1229292e7a43aa6, roc_host: &RocHost) { + unsafe { value.decref(roc_host); } + } +} + +impl DefaultOrOwnerOnlyOrPublicRead { /// Recursively decrement Roc-owned payloads. /// /// # Safety @@ -18806,10 +19083,39 @@ impl CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee { } } -pub struct CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeRelease; +pub struct DefaultOrOwnerOnlyOrPublicReadRelease; -unsafe impl RocRelease for CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTeeRelease { - unsafe fn release(value: CaptureOrInheritOrMergeWithStdoutOrNullOrPipeOrTee, roc_host: &RocHost) { +unsafe impl RocRelease for DefaultOrOwnerOnlyOrPublicReadRelease { + unsafe fn release(value: DefaultOrOwnerOnlyOrPublicRead, roc_host: &RocHost) { + unsafe { value.decref(roc_host); } + } +} + +impl AppendOrRequireNewOrTruncate { + /// Recursively decrement Roc-owned payloads. + /// + /// # Safety + /// `self` must own one live Roc reference for each refcounted payload. + pub unsafe fn decref(self, roc_host: &RocHost) { + let _ = self; + let _ = roc_host; + } + + /// Increment Roc-owned payloads. + /// + /// # Safety + /// `self` must point at live Roc allocations. The retained references must + /// be balanced by later decrefs. + pub unsafe fn incref(self, amount: isize) { + let _ = self; + let _ = amount; + } +} + +pub struct AppendOrRequireNewOrTruncateRelease; + +unsafe impl RocRelease for AppendOrRequireNewOrTruncateRelease { + unsafe fn release(value: AppendOrRequireNewOrTruncate, roc_host: &RocHost) { unsafe { value.decref(roc_host); } } } @@ -18861,14 +19167,26 @@ unsafe impl RocRelease for BytesOrInheritOrNullOrPip } } -impl CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee { +impl CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee { /// Recursively decrement Roc-owned payloads. /// /// # Safety /// `self` must own one live Roc reference for each refcounted payload. pub unsafe fn decref(self, roc_host: &RocHost) { - let _ = self; + let mut value = self; let _ = roc_host; + match value.tag { + CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::Capture => {}, + CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::File => { + let payload = unsafe { value.take_payload_file_unchecked() }; + unsafe { payload.decref(roc_host); } + }, + CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::Inherit => {}, + CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::InheritStderr => {}, + CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::Null => {}, + CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::Pipe => {}, + CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::Tee => {}, + } } /// Increment Roc-owned payloads. @@ -18877,15 +19195,27 @@ impl CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee { /// `self` must point at live Roc allocations. The retained references must /// be balanced by later decrefs. pub unsafe fn incref(self, amount: isize) { - let _ = self; + let value = self; let _ = amount; + match value.tag { + CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::Capture => {}, + CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::File => { + let payload = unsafe { core::ptr::read(value.borrow_payload_file_unchecked()) }; + unsafe { payload.incref(amount); } + }, + CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::Inherit => {}, + CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::InheritStderr => {}, + CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::Null => {}, + CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::Pipe => {}, + CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeTag::Tee => {}, + } } } -pub struct CaptureOrInheritOrInheritStderrOrNullOrPipeOrTeeRelease; +pub struct CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeRelease; -unsafe impl RocRelease for CaptureOrInheritOrInheritStderrOrNullOrPipeOrTeeRelease { - unsafe fn release(value: CaptureOrInheritOrInheritStderrOrNullOrPipeOrTee, roc_host: &RocHost) { +unsafe impl RocRelease for CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTeeRelease { + unsafe fn release(value: CaptureOrFileOrInheritOrInheritStderrOrNullOrPipeOrTee, roc_host: &RocHost) { unsafe { value.decref(roc_host); } } } @@ -20062,64 +20392,6 @@ unsafe impl RocRelease for HostFileOpenWriterResultRel } } -impl AppendOrRequireNewOrTruncate { - /// Recursively decrement Roc-owned payloads. - /// - /// # Safety - /// `self` must own one live Roc reference for each refcounted payload. - pub unsafe fn decref(self, roc_host: &RocHost) { - let _ = self; - let _ = roc_host; - } - - /// Increment Roc-owned payloads. - /// - /// # Safety - /// `self` must point at live Roc allocations. The retained references must - /// be balanced by later decrefs. - pub unsafe fn incref(self, amount: isize) { - let _ = self; - let _ = amount; - } -} - -pub struct AppendOrRequireNewOrTruncateRelease; - -unsafe impl RocRelease for AppendOrRequireNewOrTruncateRelease { - unsafe fn release(value: AppendOrRequireNewOrTruncate, roc_host: &RocHost) { - unsafe { value.decref(roc_host); } - } -} - -impl DefaultOrOwnerOnlyOrPublicRead { - /// Recursively decrement Roc-owned payloads. - /// - /// # Safety - /// `self` must own one live Roc reference for each refcounted payload. - pub unsafe fn decref(self, roc_host: &RocHost) { - let _ = self; - let _ = roc_host; - } - - /// Increment Roc-owned payloads. - /// - /// # Safety - /// `self` must point at live Roc allocations. The retained references must - /// be balanced by later decrefs. - pub unsafe fn incref(self, amount: isize) { - let _ = self; - let _ = amount; - } -} - -pub struct DefaultOrOwnerOnlyOrPublicReadRelease; - -unsafe impl RocRelease for DefaultOrOwnerOnlyOrPublicReadRelease { - unsafe fn release(value: DefaultOrOwnerOnlyOrPublicRead, roc_host: &RocHost) { - unsafe { value.decref(roc_host); } - } -} - impl HostHttpSendStreamingResult { /// Recursively decrement Roc-owned payloads. /// @@ -20489,7 +20761,7 @@ unsafe impl RocRelease for HostSignalCatchResultRelease { } } -impl InterruptOrTerminateType379 { +impl InterruptOrTerminateType380 { /// Recursively decrement Roc-owned payloads. /// /// # Safety @@ -20498,8 +20770,8 @@ impl InterruptOrTerminateType379 { let mut value = self; let _ = roc_host; match value.tag { - InterruptOrTerminateType379Tag::Interrupt => {}, - InterruptOrTerminateType379Tag::Terminate => { + InterruptOrTerminateType380Tag::Interrupt => {}, + InterruptOrTerminateType380Tag::Terminate => { let payload = unsafe { value.take_payload_terminate_unchecked() }; unsafe { payload.decref(roc_host); } }, @@ -20515,8 +20787,8 @@ impl InterruptOrTerminateType379 { let value = self; let _ = amount; match value.tag { - InterruptOrTerminateType379Tag::Interrupt => {}, - InterruptOrTerminateType379Tag::Terminate => { + InterruptOrTerminateType380Tag::Interrupt => {}, + InterruptOrTerminateType380Tag::Terminate => { let payload = unsafe { core::ptr::read(value.borrow_payload_terminate_unchecked()) }; unsafe { payload.incref(amount); } }, @@ -20524,10 +20796,10 @@ impl InterruptOrTerminateType379 { } } -pub struct InterruptOrTerminateType379Release; +pub struct InterruptOrTerminateType380Release; -unsafe impl RocRelease for InterruptOrTerminateType379Release { - unsafe fn release(value: InterruptOrTerminateType379, roc_host: &RocHost) { +unsafe impl RocRelease for InterruptOrTerminateType380Release { + unsafe fn release(value: InterruptOrTerminateType380, roc_host: &RocHost) { unsafe { value.decref(roc_host); } } } @@ -20563,7 +20835,7 @@ unsafe impl RocRelease for SystemUnixRelease { } } -impl InterruptOrTerminateType383 { +impl InterruptOrTerminateType384 { /// Recursively decrement Roc-owned payloads. /// /// # Safety @@ -20584,15 +20856,15 @@ impl InterruptOrTerminateType383 { } } -pub struct InterruptOrTerminateType383Release; +pub struct InterruptOrTerminateType384Release; -unsafe impl RocRelease for InterruptOrTerminateType383Release { - unsafe fn release(value: InterruptOrTerminateType383, roc_host: &RocHost) { +unsafe impl RocRelease for InterruptOrTerminateType384Release { + unsafe fn release(value: InterruptOrTerminateType384, roc_host: &RocHost) { unsafe { value.decref(roc_host); } } } -impl FailedOrInterruptedType384 { +impl FailedOrInterruptedType385 { /// Recursively decrement Roc-owned payloads. /// /// # Safety @@ -20601,11 +20873,11 @@ impl FailedOrInterruptedType384 { let mut value = self; let _ = roc_host; match value.tag { - FailedOrInterruptedType384Tag::Failed => { + FailedOrInterruptedType385Tag::Failed => { let payload = unsafe { value.take_payload_failed_unchecked() }; unsafe { payload.decref(roc_host); } }, - FailedOrInterruptedType384Tag::Interrupted => {}, + FailedOrInterruptedType385Tag::Interrupted => {}, } } @@ -20618,19 +20890,19 @@ impl FailedOrInterruptedType384 { let value = self; let _ = amount; match value.tag { - FailedOrInterruptedType384Tag::Failed => { + FailedOrInterruptedType385Tag::Failed => { let payload = unsafe { core::ptr::read(value.borrow_payload_failed_unchecked()) }; unsafe { payload.incref(amount); } }, - FailedOrInterruptedType384Tag::Interrupted => {}, + FailedOrInterruptedType385Tag::Interrupted => {}, } } } -pub struct FailedOrInterruptedType384Release; +pub struct FailedOrInterruptedType385Release; -unsafe impl RocRelease for FailedOrInterruptedType384Release { - unsafe fn release(value: FailedOrInterruptedType384, roc_host: &RocHost) { +unsafe impl RocRelease for FailedOrInterruptedType385Release { + unsafe fn release(value: FailedOrInterruptedType385, roc_host: &RocHost) { unsafe { value.decref(roc_host); } } } @@ -21684,10 +21956,10 @@ unsafe extern "C" { /// unsafe { arg1.decref(roc_host); } /// unsafe { decref_list_of_unix_bytes_or_utf8or_windows_u16s(arg2, roc_host); } /// The result is owned by Roc: return exactly one owned reference. - pub fn hosted_cmd_exec(arg0: SystemUnix, arg1: UnixBytesOrUtf8OrWindowsU16s, arg2: RocList) -> FailedOrInterruptedType384; + pub fn hosted_cmd_exec(arg0: SystemUnix, arg1: UnixBytesOrUtf8OrWindowsU16s, arg2: RocList) -> FailedOrInterruptedType385; /// Hosted symbol for Host.cmd_run! - /// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, cwd : [Dir([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), Inherit], envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), manage_tree : Bool, output_limit : [MaxBytes(U64), NoLimit], pending_limit : U64, program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], stderr : [Capture, Inherit, MergeWithStdout, Null, Pipe, Tee], stdin : [Bytes(List(U8)), Inherit, Null, Pipe], stdout : [Capture, Inherit, InheritStderr, Null, Pipe, Tee], timeout : [AfterNanos(U64), NoTimeout] } => Try({ status : [Exited(I32), Signaled(I32)], stderr_bytes : List(U8), stdout_bytes : List(U8) }, [CmdErr([IOErr(IOErr), OutputLimitExceeded({ stderr_bytes : List(U8), stdout_bytes : List(U8) }), StartFailed(IOErr), TimedOut({ stderr_bytes : List(U8), stdout_bytes : List(U8) })]), Interrupted]) + /// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, cwd : [Dir([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), Inherit], envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), manage_tree : Bool, output_limit : [MaxBytes(U64), NoLimit], pending_limit : U64, program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], stderr : [Capture, File({ create_permissions : [Default, OwnerOnly, PublicRead], mode : [Append, RequireNew, Truncate], path : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] }), Inherit, MergeWithStdout, Null, Pipe, Tee], stdin : [Bytes(List(U8)), Inherit, Null, Pipe], stdout : [Capture, File({ create_permissions : [Default, OwnerOnly, PublicRead], mode : [Append, RequireNew, Truncate], path : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] }), Inherit, InheritStderr, Null, Pipe, Tee], timeout : [AfterNanos(U64), NoTimeout] } => Try({ status : [Exited(I32), Signaled(I32)], stderr_bytes : List(U8), stdout_bytes : List(U8) }, [CmdErr([IOErr(IOErr), OutputLimitExceeded({ stderr_bytes : List(U8), stdout_bytes : List(U8) }), StartFailed(IOErr), TimedOut({ stderr_bytes : List(U8), stdout_bytes : List(U8) })]), Interrupted]) /// Owned arguments. Release each exactly once before returning, unless it is /// moved into storage or into the result: /// unsafe { arg0.decref(roc_host); } @@ -21695,7 +21967,7 @@ unsafe extern "C" { pub fn hosted_cmd_run(arg0: HostCmdRunArgs) -> HostCmdRunResult; /// Hosted symbol for Host.cmd_spawn! - /// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, cwd : [Dir([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), Inherit], envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), manage_tree : Bool, output_limit : [MaxBytes(U64), NoLimit], pending_limit : U64, program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], stderr : [Capture, Inherit, MergeWithStdout, Null, Pipe, Tee], stdin : [Bytes(List(U8)), Inherit, Null, Pipe], stdout : [Capture, Inherit, InheritStderr, Null, Pipe, Tee], timeout : [AfterNanos(U64), NoTimeout] } => Try(Resource.Handle([ChildKind]), [CmdErr([IOErr(IOErr), OutputLimitExceeded({ stderr_bytes : List(U8), stdout_bytes : List(U8) }), StartFailed(IOErr), TimedOut({ stderr_bytes : List(U8), stdout_bytes : List(U8) })]), Interrupted]) + /// Roc signature: { args : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), clear_envs : Bool, cwd : [Dir([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), Inherit], envs : List([UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))]), manage_tree : Bool, output_limit : [MaxBytes(U64), NoLimit], pending_limit : U64, program : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))], stderr : [Capture, File({ create_permissions : [Default, OwnerOnly, PublicRead], mode : [Append, RequireNew, Truncate], path : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] }), Inherit, MergeWithStdout, Null, Pipe, Tee], stdin : [Bytes(List(U8)), Inherit, Null, Pipe], stdout : [Capture, File({ create_permissions : [Default, OwnerOnly, PublicRead], mode : [Append, RequireNew, Truncate], path : [UnixBytes(List(U8)), Utf8(Str), WindowsU16s(List(U16))] }), Inherit, InheritStderr, Null, Pipe, Tee], timeout : [AfterNanos(U64), NoTimeout] } => Try(Resource.Handle([ChildKind]), [CmdErr([IOErr(IOErr), OutputLimitExceeded({ stderr_bytes : List(U8), stdout_bytes : List(U8) }), StartFailed(IOErr), TimedOut({ stderr_bytes : List(U8), stdout_bytes : List(U8) })]), Interrupted]) /// Owned arguments. Release each exactly once before returning, unless it is /// moved into storage or into the result: /// unsafe { arg0.decref(roc_host); } @@ -22153,12 +22425,12 @@ unsafe extern "C" { /// moved into storage or into the result: /// unsafe { arg0.decref(roc_host); } /// The result is owned by Roc: return exactly one owned reference. - pub fn hosted_signal_catch(arg0: RocListWith) -> HostSignalCatchResult; + pub fn hosted_signal_catch(arg0: RocListWith) -> HostSignalCatchResult; /// Hosted symbol for Host.signal_poll! /// Roc signature: {} => List([Interrupt, Terminate]) /// The result is owned by Roc: return exactly one owned reference. - pub fn hosted_signal_poll() -> RocListWith; + pub fn hosted_signal_poll() -> RocListWith; /// Hosted symbol for Host.sleep_nanos! /// Roc signature: U64 => Try({}, [Interrupted]) diff --git a/tests/cmd-api.roc b/tests/cmd-api.roc index 315045b9..53d23bfd 100644 --- a/tests/cmd-api.roc +++ b/tests/cmd-api.roc @@ -22,6 +22,7 @@ main! = |args| { running!()? errors!()? streams!()? + file_outputs!()? child!()? deadlines!()? compact!()? @@ -65,6 +66,12 @@ as_helper! = |args| Sleep.millis!(30_000)? Ok({}) } + ["flood"] => { + for _ in List.repeat(0, 4096) { + Stdout.write_bytes!("0123456789abcdef".to_utf8())? + } + Ok({}) + } ["echo"] => Stdout.write_bytes!(Stdin.read_to_end!()?) ["env", name] => match Env.var!(OsStr.from_str(name)) { @@ -446,3 +453,37 @@ is_unix! = || match System.target!().os() { Windows(_) => False Linux(_) | MacOS(_) => True } + +## File descriptors are opened once before spawn, without capture budgets. +file_outputs! : () => Try({}, _) +file_outputs! = || Env.with_temp_dir!( + |directory| { + log = directory.join("server.log") + result = helper!(["both", "out", "err"]) + .cwd(directory) + .stdout(File("server.log", RequireNew, { create_permissions: OwnerOnly })) + .stderr(MergeWithStdout) + .output_limit(MaxBytes(1)) + .pending_limit(1) + .spawn!()?.wait!()?.require_success()? + check!("file stdout is not captured", result.stdout_bytes.is_empty() and result.stderr_bytes.is_empty())? + check!("merged stderr shares the stdout file and offset", log.read_utf8!()? == "out\nerr\n")? + helper!(["print", "append"]).stdout(File(log, Append, {})).call!()? + check!("file append preserves contents", log.read_utf8!()? == "out\nerr\nappend\n")? + helper!(["print", "truncate"]).stdout(File(log, Truncate, {})).call!()? + check!("file truncate replaces contents", log.read_utf8!()? == "truncate\n")? + blocked = helper!(["print", "must not run"]).stdout(File(log, RequireNew, {})).run!() + match blocked { + Err(CmdErr(StartFailed(AlreadyExists, _))) => {} + _ => return Err(CheckFailed("RequireNew must reject an existing output")) + } + check!("failed redirect leaves existing file intact", log.read_utf8!()? == "truncate\n")? + separate = directory.join("stderr.log") + captured = helper!(["both", "captured", "redirected"]).stdout(Capture).stderr(File(separate, RequireNew, {})).run!()?.require_success()? + check!("stderr file with captured stdout", captured.stdout_bytes == "captured\n".to_utf8() and captured.stderr_bytes.is_empty() and separate.read_utf8!()? == "redirected\n")? + large = directory.join("large.log") + _large_output = helper!(["flood"]).stdout(File(large, RequireNew, {})).output_limit(MaxBytes(1)).pending_limit(1).spawn!()?.wait!()?.require_success()? + check!("file output does not consume capture or pending budgets", large.size_in_bytes!()? == 65_536)? + Ok({}) + }, +) diff --git a/tests/http-redirects.roc b/tests/http-redirects.roc index 17165a78..0acfb7a0 100644 --- a/tests/http-redirects.roc +++ b/tests/http-redirects.roc @@ -21,6 +21,15 @@ text = |response| Str.from_utf8_lossy(Response.body(response)) main! : List(OsStr) => Try({}, _) main! = |_| { + # Defaults and explicit overrides must reach the wire for both entry points. + plain = text(Http.send!(request("/redirect/inspect"), {})?) + check(plain.contains("user-agent: roc-basic-cli/0.26\n") and plain.contains("content-type: \n"), "bodyless GET defaults")? + post = text(Http.send!(request("/redirect/inspect").with_method(POST).with_body("payload".to_utf8()), {})?) + check(post.contains("content-type: text/plain\n"), "non-empty body default")? + custom = request("/redirect/inspect").add_header("uSeR-aGeNt", "my-cli").add_header("content-TYPE", "application/custom") + streamed_custom = Http.send_streaming!(custom, {})? + custom_report = Str.from_utf8_lossy(streamed_custom.read_to_end!(NoLimit)?) + check(custom_report.contains("user-agent: my-cli\n") and custom_report.contains("content-type: application/custom\n"), "explicit headers preserved")? # Disabled redirects return the original status, including invalid Location. for path in ["/redirect/malformed", "/redirect/duplicate", "/redirect/unsupported", "/redirect/non-utf8"] { response = Http.send_streaming!(request(path), {})? From 27c81b93fd00c2b07dfc1f0a7688eae45de1a735 Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Wed, 21 Oct 2026 10:14:21 +1100 Subject: [PATCH 4/5] Compile release validation without stale Roc caches --- CONTRIBUTING.md | 7 +++++++ docs/releases/0.26.0.md | 2 +- scripts/test.py | 5 ++++- src/process_service.rs | 17 +++++++++++++++++ tests/cmd-api.roc | 5 ++++- 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a41704cf..1b569708 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -170,6 +170,13 @@ with different arguments, stdin, environment, fixtures, helper servers, expected exit codes, or output assertions. `happy` is only a naming convention; an app can have any number of successful and failing cases. +Example checks, tests and builds use `--no-cache` on every operating system so +validation compiles the current platform sources against the current native ABI. +The pinned nightly can produce invalid cached loop code after check/test +warm-up; do not re-enable these caches until an upstream fix is verified. +When upgrading a platform URL, rebuild your application with +`roc build --no-cache your-app.roc` as well. + CI separates source validation, cross-target compilation, and native execution. Every example is compiled for each target declared in `platform/main.roc`: `x64mac`, `arm64mac`, `x64win`, `x64musl`, and `arm64musl`. Target-specific diff --git a/docs/releases/0.26.0.md b/docs/releases/0.26.0.md index c5aa47ea..2a04243e 100644 --- a/docs/releases/0.26.0.md +++ b/docs/releases/0.26.0.md @@ -260,7 +260,7 @@ The Windows x64 host keeps the MSVC ABI and now supplies the startup and runtime ## Upgrading from 0.25 -Update the platform URL and rebuild: the native host interface changed, and mixing hosts and platform sources from different versions is not supported. Then work through the modules you use. +Update the platform URL and rebuild with `roc build --no-cache your-app.roc`: the native host interface changed, and mixing hosts and platform sources from different versions is not supported. Then work through the modules you use. **Target discovery** diff --git a/scripts/test.py b/scripts/test.py index c5b2ec11..a991c2a4 100755 --- a/scripts/test.py +++ b/scripts/test.py @@ -91,7 +91,10 @@ def binary_stem(source: Path) -> str: def roc_extra_args() -> tuple[str, ...]: args = ("--max-transitive-mb=256",) - return (*args, "--no-cache") if platform.system() == "Windows" else args + # Validate the current Roc sources against the current native ABI on every + # target. The pinned compiler can reuse invalid cached loop code after + # check/test warm-up (reproduced on Linux and Intel macOS). + return (*args, "--no-cache") def load_spec() -> tuple[dict[str, bool], list[dict[str, object]]]: diff --git a/src/process_service.rs b/src/process_service.rs index 2cae0822..72735780 100644 --- a/src/process_service.rs +++ b/src/process_service.rs @@ -1067,6 +1067,23 @@ mod tests { assert!(!result.timed_out()); assert!(state.done); } + #[test] + fn repeated_file_writes_share_offsets_without_using_capture_budgets() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("child.log"); + let mut config = + shell("i=0; while [ $i -lt 4096 ]; do printf out; printf err >&2; i=$((i+1)); done"); + config.stdout = OutputMode::File; + config.stdout_file = Some(std::fs::File::create(&path).unwrap()); + config.stderr = StderrMode::MergeWithStdout; + config.output_limit = Some(1); + config.pending_limit = 1; + let output = run(config).unwrap(); + assert_eq!(output.status, Status::Exited(0)); + assert!(output.stdout.is_empty() && output.stderr.is_empty()); + assert_eq!(std::fs::read(path).unwrap(), b"outerr".repeat(4096)); + } + #[test] fn merged_output_preserves_kernel_pipe_order() { let mut config = shell("printf a; printf b >&2; printf c; printf d >&2"); diff --git a/tests/cmd-api.roc b/tests/cmd-api.roc index 53d23bfd..0e757c52 100644 --- a/tests/cmd-api.roc +++ b/tests/cmd-api.roc @@ -72,6 +72,7 @@ as_helper! = |args| } Ok({}) } + ["large-block"] => Stdout.write_bytes!(List.repeat('x', 65_536)) ["echo"] => Stdout.write_bytes!(Stdin.read_to_end!()?) ["env", name] => match Env.var!(OsStr.from_str(name)) { @@ -483,7 +484,9 @@ file_outputs! = || Env.with_temp_dir!( check!("stderr file with captured stdout", captured.stdout_bytes == "captured\n".to_utf8() and captured.stderr_bytes.is_empty() and separate.read_utf8!()? == "redirected\n")? large = directory.join("large.log") _large_output = helper!(["flood"]).stdout(File(large, RequireNew, {})).output_limit(MaxBytes(1)).pending_limit(1).spawn!()?.wait!()?.require_success()? - check!("file output does not consume capture or pending budgets", large.size_in_bytes!()? == 65_536)? + check!("file output does not consume capture or pending budgets", large.read_bytes!()? == List.repeat("0123456789abcdef".to_utf8(), 4096).join())? + _large_block = helper!(["large-block"]).stdout(File(large, Truncate, {})).output_limit(MaxBytes(1)).pending_limit(1).spawn!()?.wait!()?.require_success()? + check!("large file writes preserve every byte", large.read_bytes!()? == List.repeat('x', 65_536))? Ok({}) }, ) From b7274ab251a1d65230e4d8a07f9db65f0e16dfb0 Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Wed, 21 Oct 2026 10:45:35 +1100 Subject: [PATCH 5/5] Scope cache workaround to affected compiler and x64 builds --- CONTRIBUTING.md | 14 ++++++++------ docs/releases/0.26.0.md | 4 +++- scripts/test.py | 26 +++++++++++++++++++------- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1b569708..86d351bb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -170,12 +170,14 @@ with different arguments, stdin, environment, fixtures, helper servers, expected exit codes, or output assertions. `happy` is only a naming convention; an app can have any number of successful and failing cases. -Example checks, tests and builds use `--no-cache` on every operating system so -validation compiles the current platform sources against the current native ABI. -The pinned nightly can produce invalid cached loop code after check/test -warm-up; do not re-enable these caches until an upstream fix is verified. -When upgrading a platform URL, rebuild your application with -`roc build --no-cache your-app.roc` as well. +The runner normally uses the compiler cache. A temporary workaround disables it +for `x64musl` and `x64mac` builds with `nightly-2026-10-12-2664a27`: after +`roc check` and `roc test` warm the cache, the command stress helper can SIGSEGV +before its first write. The same build passes with `--no-cache` +([controlled reproduction](https://github.com/roc-lang/basic-cli/actions/runs/39512913704)). +TODO: remove this workaround once a compiler update passes that cached +check/test/build sequence on both targets. Other Unix targets and check/test +stages retain caching; the existing Windows workaround is unchanged. CI separates source validation, cross-target compilation, and native execution. Every example is compiled for each target declared in `platform/main.roc`: diff --git a/docs/releases/0.26.0.md b/docs/releases/0.26.0.md index 2a04243e..015173d4 100644 --- a/docs/releases/0.26.0.md +++ b/docs/releases/0.26.0.md @@ -260,7 +260,9 @@ The Windows x64 host keeps the MSVC ABI and now supplies the startup and runtime ## Upgrading from 0.25 -Update the platform URL and rebuild with `roc build --no-cache your-app.roc`: the native host interface changed, and mixing hosts and platform sources from different versions is not supported. Then work through the modules you use. +Update the platform URL and rebuild: the native host interface changed, and mixing hosts and platform sources from different versions is not supported. Then work through the modules you use. + +With `nightly-2026-10-12-2664a27`, Linux x64 and Intel macOS builds can crash when reusing a cache warmed by `roc check`/`roc test`. For that compiler bug, use `roc build --no-cache your-app.roc` ([reproduction](https://github.com/roc-lang/basic-cli/actions/runs/39512913704)). This is a temporary compiler workaround, not a requirement of the platform upgrade. **Target discovery** diff --git a/scripts/test.py b/scripts/test.py index a991c2a4..c5dd5a5e 100755 --- a/scripts/test.py +++ b/scripts/test.py @@ -89,12 +89,24 @@ def binary_stem(source: Path) -> str: return "-".join(parts[1:]) -def roc_extra_args() -> tuple[str, ...]: +@functools.cache +def roc_version() -> str: + return subprocess.check_output([ROC, "version"], text=True).strip() + + +def roc_extra_args(*, build_target: str | None = None) -> tuple[str, ...]: args = ("--max-transitive-mb=256",) - # Validate the current Roc sources against the current native ABI on every - # target. The pinned compiler can reuse invalid cached loop code after - # check/test warm-up (reproduced on Linux and Intel macOS). - return (*args, "--no-cache") + if platform.system() == "Windows": + return (*args, "--no-cache") + # TODO: Remove this workaround after a compiler update passes the cached + # check -> test -> build reproduction on x64musl and x64mac. With 2664a27, + # cmd-api's flood helper SIGSEGVs before its first write; --no-cache passes. + # Reproduction: https://github.com/roc-lang/basic-cli/actions/runs/39512913704 + if build_target in ("x64musl", "x64mac") and roc_version() == ( + "Roc compiler version nightly-2026-10-12-2664a27" + ): + return (*args, "--no-cache") + return args def load_spec() -> tuple[dict[str, bool], list[dict[str, object]]]: @@ -619,7 +631,7 @@ def run_stage( binary.parent.mkdir(parents=True, exist_ok=True) command( ROC, "build", source, f"--target={target}", - f"--output={binary}", *roc_extra_args(), + f"--output={binary}", *roc_extra_args(build_target=target), ) binaries[path] = binary elif stage == "run": @@ -793,7 +805,7 @@ def main() -> None: if shutil.which(ROC) is None: raise SystemExit(f"Roc executable '{ROC}' was not found") - print(f"Using roc version: {subprocess.check_output([ROC, 'version'], text=True).strip()}") + print(f"Using roc version: {roc_version()}") if not args.no_build and not args.bundle_path: print("\n=== Building platform ===") command(sys.executable, ROOT / "scripts" / "build.py", "--target", target)