Rust 1.99 breaks one thing and quietly changes your CI
By Nihar Ranjan Das · Thu Oct 15 2026 · 9 min read · 0 views
View as a Web StorySoftware#rust#ci#rust 1.99#cargo#upgrade guide

Rust 1.99 breaks one thing and quietly changes your CI
What breaks when you upgrade to Rust 1.99.0?
One thing can stop a build: a generic function with #[no_mangle] is now a hard error. Three other changes only warn. The rest of the release adds features and does not break code. Rust 1.99.0 is a stable compiler release, published on October 1, 2026 according to the official announcement and its detailed release notes.
Our test setup was Rust 1.98.1 and 1.99.0 installed side by side on an Apple Silicon Mac. We ran the same small test crates through both. Every result below comes from those runs, so each one shows the exact message you will see.

Which Rust 1.99.0 breaking changes need a fix?
Rust 1.99.0 has one hard error, three warnings and one behaviour change. The table lists each with the version where it first appears and the fix. Read the "Stops a build?" column first, because that is what decides whether you upgrade today or next week.
| Change | On 1.98.1 | On 1.99.0 | Stops a build? | Fix |
|---|---|---|---|---|
Generic function with #[no_mangle] |
Warning | Error | Yes | Remove the generic or mangle it |
std::i32::MAX and other legacy modules |
Silent | Warning | No | Use i32::MAX |
| Macro that ends in a semicolon, from another crate | Silent | Warning | No | Report it to the macro's crate |
default-features = false on a workspace dependency |
Error | Works on edition 2024 | Only on older editions | Move to edition 2024 |
Incremental builds when CI is set |
On | Off | No | Set CARGO_INCREMENTAL=1 |
The #[no_mangle] error
Name mangling is the step where the compiler turns a function's name and types into a unique symbol for the linker. A function that is generic over types has no single symbol, so Rust now refuses to compile a #[no_mangle] one. Rust 1.98.1 printed a warning for this. Rust 1.99.0 prints an error. Here is the smallest code that triggers it:
#[no_mangle]
pub fn generic<T>(_x: T) {}
On 1.99.0 the compiler stops with this line:
error: functions generic over types or consts must be mangled
Remove #[no_mangle] from the generic function. If C code must call it, write a non-generic wrapper for each type you need and put #[no_mangle] on the wrapper. The release notes list this as "Upgrade no_mangle_generic_items into hard error".
Most projects never hit this. Crates that expose a C interface are the exception. Search your tree for it before you bump the toolchain:
grep -rn --include='*.rs' -A1 'no_mangle' . | grep -E 'fn [a-z_0-9]+<'
The legacy integer modules
std::i32::MAX now produces a deprecation warning: "use of deprecated constant std::i32::MAX: replaced by the MAX associated constant on this type". The fix is i32::MAX. Rust has offered the short form since 1.43, so the change is safe on any toolchain you still support. If your CI runs with -D warnings, this warning becomes a failure. Fix it with one search and replace.
The macro semicolon warning
A macro whose body ends in a stray semicolon now warns even when it comes from another crate. We built a tiny crate with macro_rules! m { () => { 1; } } and called it from a second crate. Rust 1.98.1 printed nothing. Rust 1.99.0 printed "warning: trailing semicolon in macro used in expression position". The release notes ask you to report it to the crate that owns the macro, not silence it in your own code. If you cannot wait for a fix, add an #[allow] on the call site and leave a comment.
Advertisement
Does Rust 1.99.0 change how Cargo behaves in CI?
Yes. Cargo now turns off incremental compilation by default when the CI environment variable is set. The release notes state it in one line: "Incremental compilation is now disabled by default when running in CI". GitHub Actions, GitLab CI and CircleCI all set CI=true, so the change reaches most pipelines without a config edit.
Incremental compilation is a compiler mode that saves intermediate results to disk, so a small edit recompiles only what changed. It speeds up your laptop. It also makes target/ much larger. On a CI runner that starts clean, the saved data is never reused.
We measured both sides. The test crate has 60 modules and 4,800 small functions. We timed a cold build, then changed one constant and rebuilt. Each setup ran five times with CI=true, and the figures are medians.
| Setup | Cold build | One-line rebuild | target/ size |
|---|---|---|---|
| Rust 1.98.1 | 6.68 s | 0.90 s | 340 MB |
| Rust 1.99.0 | 5.35 s | 4.43 s | 129 MB |
Rust 1.99.0 with CARGO_INCREMENTAL=1 |
4.97 s | 0.79 s | 337 MB |

Three things stand out. The target/ folder is 62% smaller, because the incremental data was most of the 340 MB. A cold build was faster on 1.99.0 in both rows, but we cannot credit the CI change for that, since the row with incremental switched back on was just as fast. The cost is the one-line rebuild, which is 5.6 times slower, because the compiler redoes the whole crate.
Should you keep incremental builds in CI?
Keep the new default if each CI job starts from a clean checkout. A clean job never reuses incremental data, so the setting only saves disk and cache upload time. A smaller cache also restores faster, although we did not time cache restores.
Turn it back on if your pipeline restores a cached target/ and then rebuilds after small commits. The rebuild penalty applies only to crates you edit. Registry dependencies stay cached either way. In a GitHub Actions workflow, one line restores the old behaviour:
env:
CARGO_INCREMENTAL: "1"
CARGO_INCREMENTAL is an environment variable that forces incremental compilation on when set to 1 and off when set to 0, according to the Cargo environment variables reference. We confirmed that it overrides the new default even when CI=true. The target/ folder returned to 337 MB, which proves the flag took effect. Then compare your own cache size and job time for a week before you decide. Our crate is synthetic, and a real workspace with many small crates will land between the two extremes.
Why does default-features = false now work for workspace members?
Rust 1.99.0 lets a workspace member on edition 2024 or later switch off a dependency's default features, even when the workspace turns them on. This closes a long-standing gap. Before, you could not write default-features = false on a dependency inherited with workspace = true.
A Cargo workspace is a group of packages that share one Cargo.lock and one target/ folder, according to the Cargo workspaces reference. Teams add a [workspace.dependencies] table to pin versions in one place. The new rule is RFC 3945, which the release notes name.
We tested it with a workspace dependency named dep that has a default feature called fancy. Crate a uses edition 2021 and crate b uses edition 2024. Both write this:
dep = { workspace = true, default-features = false }
On Rust 1.98.1 the build failed for both crates with "default-features = false cannot override workspace's default-features". On 1.99.0 crate b built, and cargo tree -e features showed dep without fancy. Crate a built too, but Cargo printed this warning: "default-features is ignored for dep, since default-features was not specified for workspace.dependencies.dep; overriding workspace default-features to false requires Rust 1.99+ and the 2024 edition".
That warning matters. On edition 2021 the line compiles, the feature stays on, and nothing fails. You get a bigger dependency tree than you wrote. Grep your CI logs for "is ignored for" after you upgrade. Moving a crate to edition 2024 has its own traps, which we cover in a Rust 2024 edition migration guide.
What else changed in Rust 1.99.0?
The new features are small and mostly for systems code. None of them forces a change. Here is the short list from the release notes, with the reason each may matter:
- C-variadic functions. Rust can now define functions that take
..., using the"C"or"C-unwind"ABI. This helps when you implement a C API, such asprintf-style logging. - Size and alignment from raw pointers.
Layout::for_value_raw,mem::size_of_val_rawandmem::align_of_val_raware stable. Vec::into_partsandVec::from_parts. Both are stable, and they split a vector into pointer, length and capacity.- A
debugprofile. Cargo has a built-indebugprofile. Today it is identical todev. The notes say it prepares for movingdevaway from debug info. Box::leakguidance. The docs now warn against freeing leaked memory. They suggestBox::into_raworBox::into_non_nullinstead.
We checked the profile claim. cargo build --profile debug fails on 1.98.1 and finishes on 1.99.0. The output goes to target/debug, the same folder as dev. If your scripts call --profile dev, nothing changes.
How do you test the upgrade in CI without risk?
Run both toolchains side by side for a week, and make only the new one strict. The old toolchain keeps your pipeline green, while the new one shows every warning you will have to fix. This job does that without changing your release builds:
jobs:
toolchain-matrix:
runs-on: ubuntu-latest
strategy:
matrix:
toolchain: ["1.98.1", "1.99.0"]
steps:
- uses: actions/checkout@v4
- run: rustup toolchain install ${{ matrix.toolchain }} --profile minimal
- run: cargo +${{ matrix.toolchain }} build --all-targets
env:
RUSTFLAGS: ${{ matrix.toolchain == '1.99.0' && '-D warnings' || '' }}
If the 1.99.0 leg fails, the log shows the exact lines to fix, such as the std::i32::MAX warning or the no_mangle error. Because CI=true is set on the runner, the 1.99.0 leg also uses the new non-incremental default, so you see its real build time. Compare the two legs' durations before you decide on CARGO_INCREMENTAL.
Once the 1.99.0 leg is green, pin the new version in rust-toolchain.toml and remove the matrix. Do not leave a strict -D warnings leg on a floating stable channel, because every future release will then break your build on its own schedule.
Should you upgrade to Rust 1.99.0 now?
Upgrade now if you ship an application and do not export generic #[no_mangle] functions. The compile-time risk is low, and the warnings are fixable in minutes. Wait a week if you publish a library that others build with -D warnings, because the macro and integer warnings will hit your users first.
Here is the order we would follow:
- Run
rustup update stableon a branch, thencargo build --all-targets. - Search for
no_mangleon generic functions, using the command above. - Replace
std::i32::MAX-style paths withi32::MAX. - Decide on
CARGO_INCREMENTALfor your CI, based on whether you cachetarget/. - Check your logs for the ignored
default-featureswarning.
The previous release had its own breaking risk, which we covered in our Rust 1.98 PartialOrd change guide. Budget about an hour for a mid-sized workspace. Read the compatibility notes before the headline features on every toolchain bump, because that is where the breakage lives.
Advertisement
FAQ
What are the breaking changes in Rust 1.99.0?
One hard error: generic functions marked `#[no_mangle]` no longer compile. Three warnings: legacy modules such as `std::i32::MAX`, trailing semicolons in macros from other crates, and a changed `default-features` rule for workspace dependencies on edition 2021. Everything else is additive.
Why is my Rust CI build slower after 1.99.0?
Cargo 1.99 disables incremental compilation when the `CI` variable is set. A one-line rebuild took 4.43 seconds instead of 0.79 in our test. Set `CARGO_INCREMENTAL=1` in the job environment to restore the old behaviour, at the cost of a larger `target/` folder.
How do I fix "functions generic over types or consts must be mangled"?
Remove `#[no_mangle]` from the generic function. If foreign code must call it, add a non-generic wrapper per type and mark each wrapper `#[no_mangle]`. A generic has no single symbol name, so the compiler cannot export it by name.
Can I use default-features = false with workspace dependencies now?
Yes, on edition 2024 or later with Rust 1.99. On older editions Cargo ignores the setting and prints a warning, so the default features stay enabled. On Rust 1.98.1 the same line is a hard error.
Is the new Cargo debug profile different from dev?
Not yet. The release notes say the two profiles are identical today. The `debug` profile exists so a future release can change what `dev` does. We built with `--profile debug` and the output landed in `target/debug`.
Comments
Loading…
Sign in to join the conversation.
Related posts

Rust is dropping 32-bit Windows hosts. Your i686 builds can stay
Rust 1.100 stops installing toolchains on 32-bit Windows, but i686 binaries keep building. Check if you are affected and cross-compile in three ways.
Thu Oct 15 2026 · 9 min read · 0 views

Does your GitHub Actions cache leak secrets? A Miri audit
Rust's Miri saved CI environment variables into target/, which GitHub Actions caches. Run this four-question audit and a cache scanner to check your repo.
Thu Oct 15 2026 · 8 min read · 0 views

Rust 2024 migration: cargo fix keeps your deadlock alive
Rust 2024 edition migration, tested on 1.99.0: cargo fix rewrites if let into match, which still deadlocks. Fix the if let and drop-order traps by hand.
Thu Oct 15 2026 · 9 min read · 0 views