build: 1.99.0, resolver 3, three std-API fixes, and what the router flags are worth
Some checks failed
Check / Test & lint (linux) (push) Has been cancelled
Check / Artifact round-trip (push) Has been cancelled
Check / Test & lint (windows) (push) Has been cancelled
Check / Supply chain (push) Has been cancelled

The pin moves 1.98.1 -> 1.99.0, which is what the edition-2024 tree had been
waiting on: `resolver = "3"` is read from `[workspace]` alone (a virtual
workspace does not inherit it from the members' edition), and `rust-version =
"1.99"` in `[workspace.package]`, opted in by both crates, is what gives v3's
MSRV-aware pick something to filter by. `Cargo.lock` is untouched — the resolved
set is the one 1.98.1 built, checked with `git diff` rather than assumed.

Four rewrites the bump unlocked, each confirmed by the compiler rather than by a
changelog:
- `net/sysinfo/os.rs`: `chunk.len() == 16` + fallible `<&[u8; 16]>::try_from` is
  `chunk.as_array::<16>()` (1.93);
- `screens/post_run.rs`: `strip_prefix(..).and_then(strip_suffix(..))` is
  `strip_circumfix(.., ..)` (1.98);
- `state.rs`: the three attribute-gated `platform_dirs` wrappers are one
  `cfg_select!` block (1.95), the shape `classify/classifier.rs` already uses;
- `net/sysinfo/mod.rs`: `from_utf8_lossy_owned` (1.99) at the one site that
  already owns a `Vec<u8>`. The other seven `from_utf8_lossy(..).into_owned()`
  sites cannot use it: the API consumes `Vec<u8>`, so a borrow would need
  `.to_vec()` — a copy rather than a saving. A review pass claimed six mechanical
  sites; the compiler refuted that in a minute, which is why the count is one.

Docs that name the pin move with it (`README.md`, `docs/CI.md`,
`docs/PROFILES_AND_PROVIDER.md`), and two recorded claims the pinned toolchain
has outgrown are corrected rather than left to drift: §2.6's unwind-table
mechanics (since 1.92 codegen emits `.eh_frame` under `panic = "abort"` on Linux
by default, so `-C force-unwind-tables=no` is load-bearing — the release.yml
comment said the opposite) and `.cargo/config.toml`'s `embed-metadata` note (on
1.99.0 the key is a parse error, `expected a table, but found a boolean for
target.x86_64-pc-windows-msvc.embed-metadata`, where 1.98.1 called it an unused
key; the lever is the nightly-only pair `cargo -Z embed-metadata` /
`rustc -Z embed-metadata=val`, both listed by nightly-2026-09-17).

§2.12 records what the router build flags are worth, measured on the real router
targets with the workflow's own RUSTFLAGS, one tree and one difference per row:
the bump itself +4 160 B (+0.1 %) and still not static-PIE; `-Z build-std`
−5.2 %; `-Cpanic=immediate-abort` −11.4 % on top of that, −15.9 % against 1.99.0;
`-Cjump-tables=no` −3.2 % on armv7 and −3.1 % on mipsel. The mechanism is read off
the bytes: the shipped mipsel binary carries `panicked at`, `RUST_BACKTRACE`,
`index out of bounds`, `capacity overflow` and source paths, while the
immediate-abort armv7 binary carries none of them — one `panicked at ` fragment
survives because `main.rs`'s panic hook calls `PanicHookInfo::to_string()`, which
is both the residual size and the reason the strategy is not adopted: it takes the
localized message, `dpi_detector_crash.log` and the terminal restore with it.
None of the three lands here, and not for a size reason: the panic strategy is a
product decision, and `-Cjump-tables=no` / `-Z build-std` either move a row onto a
nightly toolchain or trade the row's stated priority (CPU first, then RSS, then
size) for a size win whose CPU half is unmeasured. §2.12 states what a router pass
of §2.6's shape would have to show for them to land.

Also recorded there: the nightly pin and the cross image no longer agree for the
MIPS rows. `ghcr.io/cross-rs/mipsel-unknown-linux-musl:0.2.5` ships glibc 2.27
while `nightly-2026-09-17`'s host `std` needs >= 2.30 (`gettid@GLIBC_2.30`, the
thread ID 1.91 added to panic messages), so the row's build scripts cannot run
inside the container — from this box and from CI alike, which mount the same
toolchain into the same image. `nightly-2026-09-02` builds every mipsel row in
§2.12, so the fix is the pin (four places, `docs/CI.md` §3) or the image.

Verified: `cargo clippy --workspace --all-targets --locked` = 0 warnings, the
`-p dpi-core --features live-network` clippy row = 0, `cargo test --workspace
--locked` = 289 + 90 passed / 0 failed, `RUSTDOCFLAGS=-D warnings cargo doc`
clean, a live `-t 2` run draws both columns, and `--json` parses into the same
frozen shape.
This commit is contained in:
Kirill Minovsky 2026-10-03 03:42:55 +03:00
parent dd1c589291
commit 54b19de8eb
14 changed files with 82 additions and 42 deletions

View file

@ -15,17 +15,19 @@ linker = "rust-lld"
# Rejected here, with the reason, so nobody re-tries them:
#
# embed-metadata = false Cargo 1.98.1 reports the key as unused — `cargo
# metadata` prints "unused config key" on every
# invocation, and it is part of the CI cache key, so a
# stray key would bust the runners' caches for nothing.
# The real lever is the nightly-only
# `-Z no-embed-metadata` (cargo 1.98.1 lists it, but
# stable rejects every `-Z` and neither installed
# nightly knows the rustc option). Measured for scale:
# a clean dev build is 1078 MiB, of which 434 MiB is
# `.rlib` and 165 MiB `.rmeta` — the win would be a
# fraction of that, and it cannot ship on the pinned
# toolchain.
# embed-metadata = false Not a config key on the pinned toolchain: 1.99.0
# fails the parse with `expected a table, but found a
# boolean for
# target.x86_64-pc-windows-msvc.embed-metadata` where
# 1.98.1 only called it an unused key — which is what
# this note recorded first. The lever itself exists as
# the nightly-only pair `cargo -Z embed-metadata` /
# `rustc -Z embed-metadata=val`, both listed by
# nightly-2026-09-17; stable rejects every `-Z`.
# Measured for scale: a clean dev build is 1078 MiB, of
# which 434 MiB is `.rlib` and 165 MiB `.rmeta` — the
# win is a fraction of that, in intermediate artifacts
# rather than in the shipped binary, so it does not pay
# for a nightly build.
# sccache, mold no benefit at one codegen unit with fat LTO, and
# mold is ELF-only while these rows link with rust-lld.

View file

@ -200,8 +200,9 @@ jobs:
name: dpi-detector-linux-x86_64
use_cross: false
smoke: true
# `panic = "abort"` already leaves our own code without unwind
# tables; this drops the ones codegen would still emit, for size.
# Since 1.92 codegen emits unwind tables under `panic = "abort"` on
# Linux by default, so this flag is what removes them — load-bearing,
# not belt-and-braces (docs/OPTIMIZATIONS.md §2.6).
rustflags: "-C force-unwind-tables=no"
# 3. ARM64 Routers & SBCs (Keenetic Hopper/Titan 2, Xiaomi AX3000T, Raspberry Pi)

View file

@ -1,5 +1,5 @@
[workspace]
resolver = "2"
resolver = "3"
members = [
"crates/dpi-core",
"crates/dpi-detector",
@ -28,6 +28,12 @@ version = "5.0.0-alpha.21"
# collapsed by hand — a Windows clippy cannot compile them, and CI's Linux clippy
# would have failed on them.
edition = "2024"
# The floor for `resolver = "3"`'s MSRV-aware pick, and a statement of what the
# sources may use. It is a floor, not the pin: `rust-toolchain.toml` pins the
# exact compiler the project builds with (1.99.0), which is strictly stronger.
# Members opt in with `rust-version.workspace = true`; the field is inert in
# `[workspace.package]` alone.
rust-version = "1.99"
authors = ["runnin"]
license = "MIT"

View file

@ -378,7 +378,7 @@ NFQWS_EXTRA_ARGS="$MODE_ALL"
## 🏗️ Сборка из исходников
Для самостоятельной сборки нужен Rust: версия закреплена в `rust-toolchain.toml`
(сейчас 1.98.1), и `rustup` поставит её сам при первой сборке — ничего выбирать не надо.
(сейчас 1.99.0), и `rustup` поставит её сам при первой сборке — ничего выбирать не надо.
Если `rustup` не используется, компилятор должен быть не старее версии из этого файла.
```bash

View file

@ -2,6 +2,7 @@
name = "dpi-core"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
# Never published: this is the engine of a binary that ships as a GitHub release

View file

@ -115,7 +115,7 @@ fn run_cmd(program: &str, args: &[&str], timeout_dur: Duration) -> Option<String
let _ = tx.send(out);
});
match rx.recv_timeout(timeout_dur) {
Ok(Ok(o)) => Some(String::from_utf8_lossy(&o.stdout).into_owned()),
Ok(Ok(o)) => Some(String::from_utf8_lossy_owned(o.stdout)),
_ => None,
}
}

View file

@ -183,9 +183,7 @@ fn read_dns_entries(guid: &str) -> Vec<(String, DnsSource)> {
&& rv.vtype == REG_BINARY
{
for chunk in rv.bytes.chunks(16) {
if chunk.len() == 16
&& let Ok(arr) = <&[u8; 16]>::try_from(chunk)
{
if let Some(arr) = chunk.as_array::<16>() {
cand.push(Ipv6Addr::from(*arr).to_string());
}
}

View file

@ -2,6 +2,7 @@
name = "dpi-detector"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
# Never published: a binary crate that ships as a GitHub release asset. Same

View file

@ -148,8 +148,7 @@ mod tests {
fn the_generated_report_name_carries_the_stamp() {
let name = default_report_name();
let stamp = name
.strip_prefix("dpi_detector_results-")
.and_then(|rest| rest.strip_suffix(".txt"))
.strip_circumfix("dpi_detector_results-", ".txt")
.unwrap_or_else(|| panic!("the name is not the report name plus a stamp: {name}"));
assert_eq!(stamp.len(), 15, "yyyymmdd-hhmmss: {name}");
assert!(

View file

@ -169,23 +169,26 @@ fn platform_dirs_unix(xdg: Option<&str>, home: Option<&str>, prefix: Option<&str
}
/// The directories this build may use, most preferred first.
#[cfg(windows)]
///
/// One `cfg_select!` block rather than three attribute pairs: the arms are the
/// same call shape, and a half-migrated pair is what a reader cannot see — the
/// argument `classify/classifier.rs` records for its constant table.
fn platform_dirs() -> Vec<PathBuf> {
platform_dirs_windows(var("APPDATA").as_deref())
}
#[cfg(target_os = "macos")]
fn platform_dirs() -> Vec<PathBuf> {
platform_dirs_macos(var("HOME").as_deref())
}
#[cfg(all(unix, not(target_os = "macos")))]
fn platform_dirs() -> Vec<PathBuf> {
platform_dirs_unix(
var("XDG_CONFIG_HOME").as_deref(),
var("HOME").as_deref(),
var("PREFIX").as_deref(),
)
cfg_select! {
windows => {
platform_dirs_windows(var("APPDATA").as_deref())
},
target_os = "macos" => {
platform_dirs_macos(var("HOME").as_deref())
},
_ => {
platform_dirs_unix(
var("XDG_CONFIG_HOME").as_deref(),
var("HOME").as_deref(),
var("PREFIX").as_deref(),
)
}
}
}
/// Every directory the file may live in, most preferred first.

View file

@ -51,7 +51,7 @@ a runner for the six-hour default.
`--profile release` for the rest — no row builds `--release`. Three rows build on
`nightly-2026-09-17` with `-Z build-std` (win7 and both MIPS — the flag is
nightly-only; `+toolchain` overrides `rust-toolchain.toml`, so the rest of the
matrix stays on 1.98.1). That date lives in four places — the win7 row's
matrix stays on 1.99.0). That date lives in four places — the win7 row's
`cargo_args`, both MIPS rows' `cross_args`, and the
`rustup toolchain install` step — move them together. Every built binary is
smoke-tested by running it (`--version` and `--legend`); foreign architectures run

View file

@ -127,7 +127,7 @@ Size is a measured goal, not a budget: the desktop `release` profile trades some
### 2.6. Cutting Stack Unwind Tables on Linux (`-C force-unwind-tables=no`)
* **File:** `.github/workflows/release.yml` — the `Build with cross` step, whose `RUSTFLAGS` is `--cfg rustix_use_libc -C force-unwind-tables=no ${{ matrix.rustflags }}`. Every `use_cross` row gets it, the four router rows and the two Android ones alike, and `Cross.toml` passes `RUSTFLAGS` into the container. It came in with 2597cc2 and is in the released tag; the x86_64-musl row sets the same flag by itself, because that row does not go through `cross`.
* **Mechanics:** With `panic = "abort"` the stack unwind tables (`.eh_frame` and `.eh_frame_hdr`) are not used while the program runs, so the flag stops codegen from emitting them.
* **Mechanics:** With `panic = "abort"` the stack unwind tables (`.eh_frame` and `.eh_frame_hdr`) are never used while the program runs, so the flag stops codegen from emitting them. Since 1.92 that is the *only* thing that stops it: codegen now emits them under `panic = "abort"` on Linux by default, so the flag is load-bearing rather than belt-and-braces. The pinned toolchain (1.99.0) is past 1.92, so every row that passes it is on the new default, and the −352 KB below was measured there.
* **What it is worth:** measured on the target for `mipsel-unknown-linux-musl` as a local A/B — one tree, built and run with and without the flag, three interleaved passes: the file drops from 5 370 720 to 5 018 392 bytes (**−352 KB**), and the resident memory with it — steady RSS −216, −408 and −388 KB, peak RSS −180, −456 and −696 KB. Processor time unchanged, as it must be for tables that are never executed. No shipped row changed here; the measurement is what the flag is worth.
* The Windows and macOS rows do not set it, and nothing measured says they should: their tables are the platform's own (SEH on MSVC), produced from the target's CFI rather than by this flag.
@ -195,6 +195,35 @@ Measured on the target (MT7621, musl, `--release`). §2.5's two guesses about wh
`split-debuginfo = "unpacked"` changes nothing on MSVC — the default already
writes the `.pdb` separately.
### 2.12. Router Builds: `std` from Source, the Panic Strategy, Jump Tables
* **File:** `Cargo.toml` (`[profile.release-router]`), `.github/workflows/release.yml` (the three router rows), `Cross.toml`.
* **How it was measured:** one target at a time (`armv7-unknown-linux-musleabihf`, `mipsel-unknown-linux-musl`), `--profile release-router` (`-Oz`, fat LTO, cgu 1, `panic = "abort"`, `strip = true`), `cross` with the workflow's own `RUSTFLAGS`, `stat -c%s` of the linked ELF. Every row of the tables is one tree, one difference from the row above it.
| armv7, `release-router` | file | Δ |
| --- | --- | --- |
| 1.98.1, prebuilt `std` (the tree before the pin moved) | 3 762 124 B | — |
| 1.99.0, prebuilt `std` | 3 766 284 B | +4 160 (+0.1 %) |
| nightly-2026-09-17, prebuilt `std` | 3 766 292 B | +8 (the nightly itself is worth nothing) |
| nightly + `-Z build-std=std,panic_abort` | 3 569 644 B | −196 648 (−5.2 %) |
| nightly + `-Z build-std=std,panic_abort` + `-Cpanic=immediate-abort` | 3 162 620 B | −407 024 (−11.4 %), −599 504 (−15.9 %) against 1.99.0 |
| 1.99.0 + `-Cjump-tables=no` (its own A/B, not a step of the chain above) | 3 647 500 B | −118 784 against 1.99.0 (−3.2 %) |
| mipsel, `release-router` | file | Δ |
| --- | --- | --- |
| nightly + `-Z build-std` (`Cross.toml`, as shipped) | 4 804 684 B | — |
| + `-Cjump-tables=no` | 4 657 220 B | −147 464 (−3.1 %) |
| + `-Cpanic=immediate-abort` | 4 135 460 B | −521 760 (−11.2 %) against the row above, −669 224 (−13.9 %) against the baseline |
* **The toolchain bump is free on this row, and it does not make it static-PIE.** 1.99.0 against 1.98.1 on armv7: +4 160 B (+0.1 %) — and the nightly-2026-09-17 row with the shipped `std` lands 8 B away, so the +4 160 B is the toolchain rather than the three std-API edits of the commit that moved the pin (the 1.98.1 row is the tree before them: `from_utf8_lossy_owned` is 1.99-only, so it cannot be built there). `file` still reports `ELF 32-bit LSB executable, ARM … statically linked`: 1.99's `static_position_independent_executables` on the gnu/musl targets did not change this artifact's shape.
* **Every armv7 row was measured twice — once while the review's edits were still landing, once on the frozen tree — and the three sizes are byte-identical across the two passes** (3 766 284 / 3 647 500 / 3 162 620). The mipsel rows were built after the edits and only once; their two deltas are each a single flag against the row above.
* **`std` from source is worth 5.2 %.** `-Z build-std=std,panic_abort` rebuilds `std`/`core` with the profile's `-Oz` + fat LTO + one codegen unit instead of the shipped `std`'s own flags. `armv7` is the row that does not use it; both MIPS rows already do (`Cross.toml`).
* **The panic strategy is the larger half, and since 1.99 it is a strategy, not a feature.** `-Z build-std-features=panic_immediate_abort` now fails inside `core` with `panic_immediate_abort is now a real panic strategy! Enable it with panic = "immediate-abort" in Cargo.toml, or with the compiler flags -Zunstable-options -Cpanic=immediate-abort`. The profile spelling additionally needs `cargo-features = ["panic-immediate-abort"]` — nightly **cargo** for every build of that profile; the flag spelling needs nightly rustc only.
* **The mechanism, read off the bytes rather than argued:** the shipped `mipsel` binary carries `panicked at`, `RUST_BACKTRACE` (×3), `index out of bounds`, `capacity overflow` and `crates/dpi-core/src/net/tls.rs`; the `immediate-abort` `armv7` binary carries none of them. One `panicked at ` fragment survives because `main.rs`'s panic hook calls `PanicHookInfo::to_string()` — `set_hook` keeps that formatting reachable although the immediate strategy never calls the hook.
* **`-Cjump-tables=no` is a size win on both router arches** (−3.2 % / −3.1 %; 1.93 stabilized the flag, it was `-Zno-jump-tables`). It cannot be a profile key, so it belongs in a row's `rustflags` — never in `.cargo/config.toml`'s `[build] rustflags`, which would hit dev and test builds too.
* **None of the three is adopted yet, and not for a size reason.** The panic strategy takes the panic message, `dpi_detector_crash.log` and the terminal restore with it — a product decision, not a codegen one. `-Cjump-tables=no` and `-Z build-std` either move a row onto a nightly toolchain or trade the row's stated priority (`CPU first, then RSS, then size`) for a size win whose **CPU half is unmeasured**: the numbers here are file sizes, and what would let them land is a router pass of §2.6's shape — one tree, built and run with and without the flag, tests 1/2/6, CPU + RSS + file.
* **The nightly pin and the cross image do not currently agree for the MIPS rows.** `ghcr.io/cross-rs/mipsel-unknown-linux-musl:0.2.5` ships glibc 2.27 while `nightly-2026-09-17`'s host `std` needs ≥ 2.30 (`gettid@GLIBC_2.30`, the thread ID 1.91 added to panic messages), so the row's *build scripts* cannot run inside the container and the build fails there — from a Windows box and from CI alike, since both mount that toolchain into that image. `nightly-2026-09-02` builds both tables above, so the fix is the pin (four places, `docs/CI.md` §3) or the image.
---
## 3. Router Specifics (MIPS, ARM, OpenWrt, Keenetic, Entware)

View file

@ -46,7 +46,7 @@ measured is marked as such; there is no extrapolated figure presented as data.
| `webpki-roots` | 1.0 | trust anchors; independent of the provider |
| `ring` | 0.17.14 | the provider: a normal dependency of `dpi-core` since the switch |
| `cc` | 1.4.4 in this lock (1.5.1 resolved elsewhere) | `ring`'s build-dependency — enters the graph with `ring` |
| Toolchain | 1.98.1 (pinned) | `rust-toolchain.toml`; MIPS rows additionally use `nightly-2026-09-17` (`-Z build-std`) |
| Toolchain | 1.99.0 (pinned) | `rust-toolchain.toml`; MIPS rows additionally use `nightly-2026-09-17` (`-Z build-std`) |
| `cross` | 0.2.5 | MIPS/ARM release rows |
| Cross GCC | `mipsel-linux-muslsf-gcc (GCC) 9.2.0` | inside `ghcr.io/cross-rs/mipsel-unknown-linux-musl:0.2.5` |
| Router under test | Keenetic, MediaTek MT7621, mipsel, 4 cores, Linux 4.9 | binaries run from `/tmp`, measured with busybox `/opt/bin/time -v` |

View file

@ -15,6 +15,6 @@
# win7 and `cross +nightly …` for MIPS, whose Cross.toml enables build-std.
# `+toolchain` overrides this file, which is why those rows still work.
[toolchain]
channel = "1.98.1"
channel = "1.99.0"
profile = "minimal"
components = ["clippy"]