Compiling Rust Crates with C++ Dependencies in Nix

Building Rust crates that compile C++ dependencies fails mysteriously in Nix environments. The error message claims your C++ compiler doesn’t support -std=c++17. It does. The real issue: conflicting compiler flags between Nix and the cc-rs crate.

The Error

You’re building a Rust project that depends on wasm-opt-rs or similar crates. Everything works fine outside Nix. Inside a devenv shell:

error: failed to run custom build command for `wasm-opt-sys v0.116.0`
...
Error: C++ compiler does not support `-std=c++17` flag

The compiler definitely supports C++17. Running clang++ --version confirms it. The build script even detects the compiler correctly. Yet it fails.

The Root Cause

The cc-rs crate automatically adds compiler flags based on target architecture. These defaults work well in standard environments but clash with Nix’s carefully controlled build settings.

When cc-rs adds flags like -mcpu=cortex-a15 and Nix provides -march=armv7-a+fp, the compiler rejects the conflicting directives. On Apple Silicon Macs, similar conflicts occur with architecture-specific optimizations.

The cc-rs build probe tests compiler features by attempting compilation with various flags. When these conflict with Nix-provided flags, the probe fails—not because the compiler lacks C++17 support, but because the flag combination is invalid.

The Solution

Set CRATE_CC_NO_DEFAULTS=1 in your environment. This tells cc-rs to skip adding default compiler flags:

# devenv.nix
{
  # Prevent cc-rs from adding conflicting compiler flags
  env.CRATE_CC_NO_DEFAULTS = "1";

  languages.rust.enable = true;
  languages.cplusplus.enable = true;
}

The variable name is all that matters—cc-rs only checks for its existence, not its value.

Understanding wasm-opt-sys

The wasm-opt-sys crate demonstrates this issue perfectly. It builds Binaryen’s C++ codebase from source, requiring:

  • A C++ compiler with C++17 support
  • No CMake (intentionally, to keep builds simple)
  • Complex flag management for cross-platform builds

During the build, cc-rs runs compiler detection probes. With default flags enabled, these probes fail in Nix environments due to flag conflicts. The error message about C++17 support is misleading—it’s really about incompatible flag combinations.

Why This Works

By disabling cc-rs defaults, you allow Nix to maintain complete control over compiler invocation. Nix provides:

  • Proper sysroot configuration
  • Architecture-specific optimizations
  • Reproducible build flags

The cc-rs crate still handles compiler detection and invocation but skips the problematic default flags. This separation of concerns resolves the conflict.

When You Need This

Set CRATE_CC_NO_DEFAULTS=1 when:

  • Building crates with C/C++ dependencies in Nix
  • Encountering mysterious compiler capability errors
  • Cross-compiling in Nix environments
  • Using devenv.sh or similar Nix-based development tools

Common affected crates include:

  • wasm-opt-sys / wasm-opt-rs
  • ring (cryptography)
  • tree-sitter language parsers
  • Any crate using cc-rs for native compilation

Alternative Approaches

If setting the environment variable doesn’t work:

  1. Check compiler detection: Ensure languages.cplusplus.enable = true in devenv.nix
  2. Verify toolchain: Nix should provide clang or gcc with proper wrappers
  3. Debug cc-rs: Set CC_ENABLE_DEBUG_OUTPUT=1 to see detailed compiler invocation

For cargo-based solutions without devenv:

CRATE_CC_NO_DEFAULTS=1 cargo build

Or in .cargo/config.toml:

[env]
CRATE_CC_NO_DEFAULTS = "1"

The Broader Pattern

This issue extends beyond wasm-opt-sys. Any Rust crate that:

  • Uses cc-rs to compile native code
  • Targets multiple architectures
  • Runs in controlled build environments

Can hit this flag conflict. The Yocto Project officially adopted CRATE_CC_NO_DEFAULTS for all Rust builds, confirming this is a widespread pattern.

The conflict arises from different philosophies: cc-rs assumes it should optimize for the target platform, while Nix assumes it controls the entire build. Both are reasonable positions that clash in practice.

Understanding this pattern helps diagnose similar build failures quickly. When a build claims your compiler lacks basic features it clearly supports, check for flag conflicts first.