1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
name: CI
on:
push:
branches:
pull_request:
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Test & lint (${{ matrix.os }}, ${{ matrix.backend.name }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
# Two TLS backends ship in the crate (purecrypto-tls is the default,
# rustls-tls is opt-in). Build & test both on every PR so neither
# rots. The flags are stored on the matrix entry to keep the steps
# below shell-portable across Linux/macOS/Windows runners.
backend:
- name: default
flags: --all-features
- name: rustls
flags: --no-default-features --features rustls-tls
# HTTP-only: no SSH, no BitTorrent (so no `puressh` dependency) — the
# minimal surface an HTTP source driver builds against. Guards the
# feature gating so an ungated `ssh`/`bittorrent` reference can't
# sneak back in unnoticed.
- name: http-only
flags: --no-default-features --features purecrypto-tls,idn
steps:
- uses: actions/checkout@v6
- name: Install Rust
# Pinned to the MSRV (`rust-version` in Cargo.toml): a floating stable
# breaks `clippy -D warnings` whenever std deprecates something whose
# replacement is newer than our MSRV.
uses: dtolnay/rust-toolchain@1.95
with:
components: rustfmt, clippy
- name: Cache
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.os }}-${{ matrix.backend.name }}
- name: Format
# Formatting is platform- and backend-independent; only run it once.
if: runner.os == 'Linux' && matrix.backend.name == 'default'
run: cargo fmt --all --check
- name: Clippy (warnings denied)
run: cargo clippy --all-targets ${{ matrix.backend.flags }} -- -D warnings
- name: Test (release)
# `--release` so the integration tests in tests/ (which spin up a
# local HTTP/1.1 server and round-trip real bytes through rsurl)
# exercise the same code paths CI ships. The unit tests are cheap
# enough that running them in release adds no meaningful time.
run: cargo test --release ${{ matrix.backend.flags }}
c_abi:
name: C ABI smoke test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache
uses: Swatinem/rust-cache@v2
- name: Build the C library (static + shared)
# Cargo.toml declares crate-type = ["rlib", "cdylib", "staticlib"], so
# the build produces librsurl.a and librsurl.so. The `rsurl_*` C ABI
# symbols are gated behind the (off-by-default) `ffi` feature, so it must
# be enabled here for the smoke tests below to link.
run: cargo build --release --features ffi
- name: Compile and run the C smoke test (static link)
run: |
cc tests/ffi_smoke.c -I include target/release/librsurl.a \
-lpthread -ldl -lm -o ffi_smoke_static
./ffi_smoke_static
- name: Compile and run the C smoke test (shared link)
# Same source, linked against the cdylib instead of the staticlib.
# LD_LIBRARY_PATH points the loader at target/release so the
# binary can find librsurl.so at run time without installing it.
run: |
cc tests/ffi_smoke.c -I include \
-L target/release -lrsurl \
-o ffi_smoke_shared
LD_LIBRARY_PATH=target/release ./ffi_smoke_shared
- name: Install clippy
run: rustup component add clippy
- name: Build, lint & test the curl-compat shim
# curl-compat is a standalone sub-crate (this repo is not a workspace),
# so the steps above never touch it. Gate it here too — a non-exhaustive
# match or other breakage in the libcurl-ABI shim must turn CI red.
run: |
cargo clippy --manifest-path curl-compat/Cargo.toml --all-targets -- -D warnings
cargo build --manifest-path curl-compat/Cargo.toml --release
cargo test --manifest-path curl-compat/Cargo.toml
linux32:
name: 32-bit (i686, pure-Rust backends)
runs-on: ubuntu-latest
# rsurl's pure-Rust path (the purecrypto default and the http-only build)
# targets 32-bit/ILP32 embedded devices. x86-64 runners execute i686
# binaries natively with multilib, so the tests actually run. The rustls
# backend is intentionally excluded: its `ring` provider compiles C/asm and
# needs an i686 cross-toolchain, which is a `ring` concern, not rsurl's.
strategy:
fail-fast: false
matrix:
backend:
- name: default
flags: ""
- name: http-only
flags: "--no-default-features --features purecrypto-tls,idn"
steps:
- uses: actions/checkout@v6
- name: Install Rust (with i686 target)
# Pinned to the MSRV, like the test & lint matrix above.
uses: dtolnay/rust-toolchain@1.95
with:
targets: i686-unknown-linux-gnu
components: clippy
- name: Install 32-bit link support
# rustc shells out to gcc to link the i686 binaries; gcc-multilib
# provides the 32-bit crt/libc and `-m32` support.
run: sudo apt-get update && sudo apt-get install -y gcc-multilib
- name: Cache
uses: Swatinem/rust-cache@v2
with:
key: i686-${{ matrix.backend.name }}
- name: Clippy (warnings denied)
run: cargo clippy --target i686-unknown-linux-gnu --all-targets ${{ matrix.backend.flags }} -- -D warnings
- name: Test (release)
run: cargo test --release --target i686-unknown-linux-gnu ${{ matrix.backend.flags }}
- name: Build, lint & test the curl-compat shim (i686)
# Exercises the pointer-width gate: CURLOPT_POSTFIELDSIZE_LARGE rejects
# with CURLE_NOT_BUILT_IN on ILP32 instead of truncating the curl_off_t.
if: matrix.backend.name == 'default'
run: |
cargo clippy --manifest-path curl-compat/Cargo.toml --target i686-unknown-linux-gnu --all-targets -- -D warnings
cargo build --manifest-path curl-compat/Cargo.toml --release --target i686-unknown-linux-gnu
cargo test --manifest-path curl-compat/Cargo.toml --target i686-unknown-linux-gnu
docs:
name: Docs build (warnings denied)
runs-on: ubuntu-latest
env:
RUSTDOCFLAGS: -D warnings -D rustdoc::broken-intra-doc-links
steps:
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache
uses: Swatinem/rust-cache@v2
- name: cargo doc --no-deps --all-features
run: cargo doc --no-deps --all-features
wasm:
name: wasm32 (browser) build & lint
runs-on: ubuntu-latest
# The browser backend (`crate::aio` over Fetch + the native WebSocket) is a
# whole separate compile target the matrix above never exercises: on
# `wasm32-unknown-unknown` the entire net/proto/tls socket stack is
# cfg'd out and the wasm bindings take over. Build & lint it here so that
# gating can't silently rot (e.g. an ungated `std::net` reference sneaking
# back into the wasm surface). Tests need a browser harness (wasm-bindgen-test)
# and are out of scope for this job — this guards compilation and lints only.
steps:
- uses: actions/checkout@v6
- name: Install Rust (with wasm32 target)
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
components: clippy
- name: Cache
uses: Swatinem/rust-cache@v2
with:
key: wasm32
- name: Clippy (warnings denied)
# The wasm build is HTTP/WebSocket-only: `idn` for host normalisation,
# no default features (which would pull the native-only ssh/tls/bittorrent
# stack). `--lib` because test/bin targets don't link on bare wasm32.
run: cargo clippy --target wasm32-unknown-unknown --no-default-features --features idn --lib -- -D warnings
- name: Build (lib + bin)
# Also builds the bin wrapper (an empty `main` on wasm) to prove the whole
# crate — not just the lib — compiles for the browser target.
run: cargo build --target wasm32-unknown-unknown --no-default-features --features idn
fullrust:
name: fullrust (libc-free static Linux)
runs-on: ubuntu-latest
# `x86_64-unknown-linux-fullrust` is a patched toolchain whose std talks to
# the kernel through raw syscalls instead of libc, so an ordinary crate
# links into a fully static ELF with no interpreter and no NEEDED entries.
# rsurl is a natural fit — the whole dependency graph is pure Rust already —
# but the target is deliberately NOT in the `unix` family, so `cfg(unix)` is
# false while `cfg(windows)` is too: the `#[cfg(not(any(unix, windows)))]`
# arms (positional file I/O in src/tmpfile.rs, most of all) are the ones
# that actually run there, and no other job in this matrix compiles them
# against a real network stack. Build and test here so they can't rot.
#
# The image is pinned to Rust 1.95, this crate's `rust-version`. Keep the
# two in step: fullrust publishes one image per Rust minor.
steps:
- uses: actions/checkout@v6
- name: Build the CLI for the fullrust target
uses: KarpelesLab/fullrust@master
with:
image: ghcr.io/karpeleslab/fullrust:1.95
args: --release --bin rsurl
- name: Test (release)
uses: KarpelesLab/fullrust@master
with:
image: ghcr.io/karpeleslab/fullrust:1.95
command: test
# No doctests: the image ships no rustdoc, and they run in the
# host test & lint matrix anyway.
args: --release --lib --bins --tests
- name: Prove the binary links no libc
run: |
set -euo pipefail
bin=target/x86_64-unknown-linux-fullrust/release/rsurl
file "$bin"
# A libc-free static ELF has no PT_INTERP and no .dynamic section.
readelf -d "$bin" | grep -q 'There is no dynamic section' \
|| { echo "::error::binary has a dynamic section"; readelf -d "$bin"; exit 1; }
# `ldd` exits non-zero on a static binary, so capture rather than pipe
# (`pipefail` would read that exit status as the step failing).
ldd_out=$(ldd "$bin" 2>&1 || true)
case "$ldd_out" in
*"not a dynamic executable"*) ;;
*) echo "::error::binary is dynamically linked"; echo "$ldd_out"; exit 1 ;;
esac
# And it runs: version banner plus a real HTTPS fetch through
# purecrypto's TLS stack, on a binary whose only ABI is the syscall.
"$bin" --version
"$bin" -sS -o /dev/null -w 'https fetch: %{http_code}\n' https://example.com/