Skip to content

fix: make builds reproducible by moving rust-i18n to 3.1.5 - #65

Merged
animeshchaudhri merged 1 commit into
rust-man-org:masterfrom
barnumbirr:fix/reproducible-builds-rust-i18n
Oct 21, 2026
Merged

animeshchaudhri merged 1 commit into
rust-man-org:masterfrom
barnumbirr:fix/reproducible-builds-rust-i18n

Conversation

@barnumbirr

Copy link
Copy Markdown
Contributor

Two release builds of the same commit produce different binaries. The cause is rust-i18n-macro 3.1.2, which writes out the translation tables of the editor widget's i18n! call by iterating a HashMap. Its seed is random in every process, so every build compiles the tables in a different order, and with LTO the rest of the binary shifts around them. rust-i18n fixed this in 3.1.3 ("Replace HashMap with BTreeMap to remove non-determinism in build", longbridge/rust-i18n#104), but the lock is still on 3.1.2.

A plain cargo update -p rust-i18n cannot move it: proc-macro-crate 2.0.2 pins toml_datetime = "=0.6.3", below what the toml in newer rust-i18n needs. num_enum_derive accepts proc-macro-crate up to 3, and 3.5.0 is already in the lock, so the change is:

cargo update -p proc-macro-crate@2.0.2
cargo update -p rust-i18n

It only touches Cargo.lock:

  • rust-i18n, rust-i18n-macro and rust-i18n-support go from 3.1.2 to 3.1.5. The runtime rust-i18n crate is identical between the two; rust-i18n-support changes only in the locale loader that the macro runs at compile time.
  • toml 0.7.8 to 0.8.23, with toml_edit, toml_datetime, winnow and a new toml_write, all reached only through the macro at compile time.
  • proc-macro-crate 2.0.2, serde_yml and libyml are removed. The last two clear two cargo audit warnings, RUSTSEC-2025-0068 and RUSTSEC-2025-0067 (unsound, unmaintained); rust-i18n 3.1.4 had already moved to serde_yaml over earlier concerns about serde_yml (Concerns with serde_yml rustsec/advisory-db#2212).

Testing

  • cargo build --release, twice per tree from a clean target at the same path, Rust 1.98.1: on master the two binaries differ (3de8eb66… and d0bb5bf4…, 23,495,224 and 23,495,480 bytes); with this change both are cec019ef….
  • In a --no-default-features build, comparing every crate's .rlib and .rmeta between two builds found iced_code_editor the only crate that differed. With this change they all match.
  • cargo test: 139 passed and 3 ignored with default features, 131 and 3 with --no-default-features, the same as master.
  • cargo check --all-targets --target x86_64-pc-windows-gnu passes with master's one warning (APP_ID is never used).
  • Rust 1.88.0 checks clean with and without default features. That is the oldest toolchain master builds with: the README says 1.85, but iced 0.14 and calamine 0.36 need 1.88. The per-crate rust-version floors are the same 21 as on master, so this change does not raise it.
  • Under Xvfb, the response editor's find bar and context menu show the same English strings as on master: "Search...", "Case sensitive", Undo, Redo, Cut, Copy, Paste and Select All.

Not tested on macOS.

Two release builds of the same commit produce different binaries.
rust-i18n-macro 3.1.2 writes out the translation tables of the editor
widget's i18n! call by iterating a HashMap, whose seed is random in
every process, so each build compiles them in a different order.
3.1.3 and later iterate a BTreeMap.

A plain cargo update cannot reach 3.1.3: proc-macro-crate 2.0.2 pins
toml_datetime exactly, below what newer rust-i18n's toml needs.
num_enum_derive accepts proc-macro-crate 3, already in the lock, so
the change is

    cargo update -p proc-macro-crate@2.0.2
    cargo update -p rust-i18n

Lock-only. The runtime rust-i18n crate is identical from 3.1.2 to
3.1.5, and rust-i18n-support changes only in the locale loader the
macro runs at compile time. The update also drops serde_yml and
libyml, which cargo audit reports as unsound (RUSTSEC-2025-0068,
RUSTSEC-2025-0067).
@animeshchaudhri

animeshchaudhri commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

@barnumbirr Really interesting find. This is especially important for security as Rustman grows. Thanks for the detailed investigation. Merging this!

Also did you use any tool for figuring what was different for LTO for both builds or just the sha hash ?

@barnumbirr

Copy link
Copy Markdown
Contributor Author

Thanks! No dedicated tool, mostly hashes and comparing the right files. I noticed it because debrebuild, which rebuilds a Debian package and compares it with the published one, reported only size differs. sha256sum then showed the original and two rebuilds were all different from each other.

Diffing the final binaries didn't get far. With LTO and codegen-units = 1, a small change early on shifts everything after it, so about 11 MB of the 23 MB binary differed. readelf -S gave the first real hint: .text changed size between builds, by up to 384 bytes, while .rodata stayed exactly the same size. So the same code was being compiled slightly differently, rather than different data being embedded.

What found it was comparing the intermediate artifacts instead of the binary: two builds from a clean target in the same directory, hashing every crate's .rlib and .rmeta.

for n in 1 2; do
    rm -rf target
    cargo build --release --locked -q
    (cd target/release/deps && sha256sum *.rlib *.rmeta | sort -k2) > /tmp/rlibs-$n.txt
done
diff /tmp/rlibs-1.txt /tmp/rlibs-2.txt

Only iced_code_editor differed. Its .rmeta differed as well, which means its expanded source changed between builds, so I looked at the macros it runs at compile time. The rust_i18n::i18n!("locales", fallback = "en") call stood out, and rust-i18n-macro 3.1.2 generates its output by iterating a HashMap. If every rlib had matched, the variation would have been in the final LTO step instead.

diffoscope, from the Reproducible Builds project, is the usual tool for looking inside two differing builds, but I didn't need it here.

On security: I've opened #66 for the four advisories cargo audit reports against rustls 0.23.43. It's independent of this one and merges cleanly in either order.

@animeshchaudhri
animeshchaudhri merged commit b12b215 into rust-man-org:master Oct 21, 2026
2 checks passed
@barnumbirr
barnumbirr deleted the fix/reproducible-builds-rust-i18n branch October 24, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants