Software

Rust 1.99 breaks one thing and quietly changes your CI

By · Thu Oct 15 2026 · 9 min read · 0 views

View as a Web Story

Software#rust#ci#rust 1.99#cargo#upgrade guide

Rust 1.99.0 upgrade guide showing breaking changes and CI cache effect

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.

Rust 1.99.0 breaking changes ranked by how likely they are to stop a build

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

Cold build time, one-line rebuild time and target folder size for three Rust CI setups

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:

  1. 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 as printf-style logging.
  2. Size and alignment from raw pointers. Layout::for_value_raw, mem::size_of_val_raw and mem::align_of_val_raw are stable.
  3. Vec::into_parts and Vec::from_parts. Both are stable, and they split a vector into pointer, length and capacity.
  4. A debug profile. Cargo has a built-in debug profile. Today it is identical to dev. The notes say it prepares for moving dev away from debug info.
  5. Box::leak guidance. The docs now warn against freeing leaked memory. They suggest Box::into_raw or Box::into_non_null instead.

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:

  1. Run rustup update stable on a branch, then cargo build --all-targets.
  2. Search for no_mangle on generic functions, using the command above.
  3. Replace std::i32::MAX-style paths with i32::MAX.
  4. Decide on CARGO_INCREMENTAL for your CI, based on whether you cache target/.
  5. Check your logs for the ignored default-features warning.

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