Skip to content

Commit 8c69648

Browse files
Merge pull request #534 from roc-lang/release/0.26-final-notes
Resolve final 0.26 feedback and complete the upgrade guide
2 parents 20ea37b + b7274ab commit 8c69648

15 files changed

Lines changed: 980 additions & 309 deletions

File tree

‎CONTRIBUTING.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,15 @@ with different arguments, stdin, environment, fixtures, helper servers,
170170
expected exit codes, or output assertions. `happy` is only a naming convention;
171171
an app can have any number of successful and failing cases.
172172

173+
The runner normally uses the compiler cache. A temporary workaround disables it
174+
for `x64musl` and `x64mac` builds with `nightly-2026-10-12-2664a27`: after
175+
`roc check` and `roc test` warm the cache, the command stress helper can SIGSEGV
176+
before its first write. The same build passes with `--no-cache`
177+
([controlled reproduction](https://github.com/roc-lang/basic-cli/actions/runs/39512913704)).
178+
TODO: remove this workaround once a compiler update passes that cached
179+
check/test/build sequence on both targets. Other Unix targets and check/test
180+
stages retain caching; the existing Windows workaround is unchanged.
181+
173182
CI separates source validation, cross-target compilation, and native execution.
174183
Every example is compiled for each target declared in `platform/main.roc`:
175184
`x64mac`, `arm64mac`, `x64win`, `x64musl`, and `arm64musl`. Target-specific

‎README.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,22 @@ These host behaviors may affect application design:
5252
- HTTP redirects are opt-in through trailing options, `{ max_redirects: n }`;
5353
the default is zero. The request deadline covers every hop and the final
5454
body. Cross-origin hops strip credentials; HTTPS downgrades are refused.
55+
- HTTP defaults to `User-Agent: roc-basic-cli/0.26`, preserving an explicit
56+
user agent. A non-empty body defaults to `Content-Type: text/plain` unless
57+
one is set; an empty body adds no content type.
58+
- A spawned `Cmd.Child` is killed and reaped when its last reference is
59+
released; an unused `_child` binding does not keep it alive. `Pipe` output
60+
is read through `child.read!(duration)` and is absent from `child.wait!()`;
61+
choose `Capture` to retain it for `wait!`.
62+
`.stdout(File(path, Append, {}))` or `.stderr(File(path, Truncate, {}))`
63+
writes directly to disk with the writer modes and creation permissions.
64+
`MergeWithStdout` shares stdout's open file; file-backed streams do not
65+
consume capture or pending-output budgets.
66+
- `Utc` supplies clock readings and signed epoch conversions. Use a package
67+
such as [roc-time](https://github.com/lukewilliamboswell/roc-time) for timestamp
68+
parsing, calendar calculations and richer formatting. Its documented profiles
69+
use microseconds, while `Utc` stores nanoseconds. `Utc.to_iso_8601` is a
70+
whole-second display; signed epoch nanoseconds preserve the stored instant.
5571
- `Signal.catch!` catches Ctrl-C and, with a `System.Unix` witness, SIGTERM
5672
(including default `systemctl stop`). Blocking effects return bare
5773
`Interrupted`. Call `Signal.poll!()` before effectful application cleanup;

‎ci/rust_http_server/src/main.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ async fn handle_redirect(req: Request<Incoming>) -> Result<Response<BoxBody>, Ge
196196
"cookie",
197197
"proxy-authorization",
198198
"content-type",
199+
"user-agent",
199200
"content-length",
200201
"x-hop",
201202
"x-public",

‎docs/releases/0.26.0.md‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ Until now each basic-cli module had grown its own habits. A timeout was a bare `
1212

1313
0.26.0 settles these questions once and applies the answers everywhere:
1414

15-
- **A timeout is a `Duration`**, and `Duration.zero` always expires immediately.
15+
- **Basic-cli timeout options use `Duration`**, and `Duration.zero` always expires immediately. The shared HTTP package keeps its existing request-timeout representation, described below.
1616
- **The end of input is `Err(EndOfInput)`**, on stdin, files and sockets alike. An empty `Ok` is real data.
1717
- **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.
18-
- **A module's failures arrive under one tag** (`TcpErr(IOErr)`, `CmdErr(Cmd.Err)`, `SqliteErr(Sqlite.Err)`), and every error can be compared with `==`.
18+
- **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 `==`.
1919

2020
The result is that code written against one module reads like code written against another:
2121

@@ -58,6 +58,8 @@ stale = Utc.now!().since(path.time_modified!()?) > Duration.minutes(5)
5858
- **File times** (`time_accessed!`, `time_modified!`, `time_created!`) return `Utc` and succeed for files older than 1970.
5959
- Sleep is now fallible: use `Sleep.millis!(250)?` or `duration.sleep!()?`. A caught stop request returns `Err(Interrupted)`.
6060

61+
`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.
62+
6163
## Loop over input
6264

6365
Reading lines used to mean hand-written recursion. Now it is a loop, and the same loop works on every source:
@@ -129,8 +131,10 @@ File.with_writer!(destination, Truncate, {}, |writer| writer.write_stream!(respo
129131

130132
- You see the status before any of the body is read, so you can decline a 404's body.
131133
- `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.
134+
- 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.
132135
- `response.read_to_end!(MaxBytes(1_048_576))` reads the rest with a bound, and `response.content_length()` is `Known(n)` or `Unknown`.
133136
- `Http.send!`, `send_json!`, `get!` and `get_utf8!` now take trailing options: pass `{}` to return the first response, as before.
137+
- 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)).
134138
- 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.
135139
- 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.
136140
- 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.
@@ -151,7 +155,7 @@ in_msg = conn.read_line!()?
151155
```
152156

153157
- `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.
154-
- 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`.
158+
- 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`.
155159
- **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.
156160
- 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.
157161
- `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.
@@ -210,7 +214,10 @@ child = Cmd.new("server").stdout(Pipe).spawn!()? # run in th
210214
- `.timeout(Duration.minutes(5))` bounds the whole run, including a spawned child, which is killed at the deadline.
211215
- **Errors and `to_inspect` no longer contain environment values**, so a secret passed with `.env` cannot leak into a log.
212216
- Compact `Cmd.call!(program, args)`, `capture!`, `run!`, and `spawn!` complement the reusable `Cmd.Command` builder. Builder execution uses `.call!()` too.
217+
- 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.
213218
- `cmd.inherit_stdio().run!()?.status` waits without recording output in memory. `.stdout(InheritStderr)` sends child stdout directly to parent stderr.
219+
- 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.
220+
- 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.
214221
- `.stderr(MergeWithStdout)` and `.output_limit(NoLimit)` say directly what used to be encoded in a `Bool` or a zero.
215222
- 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.
216223

@@ -255,6 +262,18 @@ The Windows x64 host keeps the MSVC ABI and now supplies the startup and runtime
255262

256263
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.
257264

265+
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.
266+
267+
**Target discovery**
268+
269+
| 0.25 | 0.26 |
270+
|---|---|
271+
| `Env.platform!()` returns a record | `System.target!()` returns an opaque `System.Target` |
272+
| `Env.platform!().arch` | `System.target!().arch()` returns `X64` or `Arm64`; `AARCH64` is now `Arm64` |
273+
| `Env.platform!().os` returns `LINUX`, `MACOS` or `WINDOWS` | `System.target!().os()` returns `Linux(os)`, `MacOS(os)` or `Windows(os)` |
274+
275+
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`.
276+
258277
**Time**
259278

260279
| 0.25 | 0.26 |
@@ -306,6 +325,7 @@ When upgrading an earlier 0.26 draft, add writer options: `File.open_writer!(pat
306325

307326
| 0.25 | 0.26 |
308327
|---|---|
328+
| `Cmd` as a command-builder type annotation | `Cmd.Command`; `Cmd` is the namespace |
309329
| `Cmd.exec!("cargo", ["build"])` | `Cmd.call!("cargo", ["build"])` |
310330
| `cmd.exec_cmd!()` | `cmd.call!()` |
311331
| `cmd.exec_output!()?.stdout_utf8` | `cmd.capture!()?` |
@@ -337,7 +357,9 @@ When upgrading an earlier 0.26 draft, add writer options: `File.open_writer!(pat
337357

338358
**Everything else**
339359

360+
- **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.
340361
- **`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.
362+
- **`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.
341363
- **`Path` and `OsStr` values that differed only in representation are now equal**, and collapse to one `Dict` or `Set` key.
342364
- **`Tty.enable_raw_mode!` and `disable_raw_mode!` return a `Try`**; add `?`.
343365
- **Write `True` and `False`**, not `Bool.True` and `Bool.False`, to match the platform's own docs and examples.
@@ -354,6 +376,8 @@ When upgrading an earlier 0.26 draft, add writer options: `File.open_writer!(pat
354376
- 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)).
355377
- 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.
356378

379+
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.
380+
357381
---
358382

359383
- [API documentation](https://roc-lang.github.io/basic-cli/0.26.0/)

0 commit comments

Comments
 (0)