Conditional Compilation and cfg

28 min read
On this page

cfg determines whether certain code participates in the build, making it suitable for platform-specific modules, optional dependencies, and test helper code. It differs from runtime selection based on the program's startup environment and cannot replace compile-time checks for all supported configurations.

Key Differences Between cfg and cfg!

#[cfg(unix)] fn platform()Disabled code skips semantic checksif cfg!(unix) { ... } else { ... }Both branches still require valid names and typesSelects which code items existSuitable for platform-specific APIsSelects a constant-condition branchSuitable for expressions that compile on both sidescfg attribute removes code; cfg! only produces a boolean constantConditional compilation does not allow arbitrary unparsable syntax in disabled regions.
#[cfg(unix)]
fn platform_name() -> &'static str { "unix" }

#[cfg(not(unix))]
fn platform_name() -> &'static str { "other" }

fn main() {
    println!("{}", platform_name());
    let word = if cfg!(debug_assertions) { "checks" } else { "optimized" };
    println!("{word}");
}

cfg! expands to true/false, but the two branches of the surrounding ordinary if are still subject to name resolution and type checking. Functions that only exist on Linux cannot be hidden from other platforms using if cfg!(target_os="linux"); you should use #[cfg] on items or blocks. Conditional Compilation Rules

#[cfg] is a syntax-level configuration removal; source files must still be readable as valid Rust syntax. You cannot place arbitrarily malformed tokens in disabled regions. For module-level platform implementations, you can select different files using cfg, allowing each implementation to maintain the same public interface.

Ensure Branch Coverage First, Then Implement Platform Differences

Multiple implementations of the same function must satisfy two conditions: at least one must exist under any supported configuration, and no more than one should be selected simultaneously. The easiest-to-maintain binary split is between condition P and not(P), rather than manually listing "other platforms currently thought of."

#[cfg(all(unix, target_pointer_width = "64"))]
fn implementation() -> &'static str { "unix-64" }

#[cfg(not(all(unix, target_pointer_width = "64")))]
fn implementation() -> &'static str { "portable" }

fn main() {
    assert_eq!(implementation(),
        if cfg!(all(unix, target_pointer_width = "64")) {
            "unix-64"
        } else {
            "portable"
        });
}

Here, both implementations have the same signature, so callers don't need to repeat platform checks. all means all conditions must be true, any means at least one is true, and not inverts a condition; an empty all() is true, and an empty any() is false. These empty conditions are also suitable for constructing minimal experiments: marking a function with an undefined name as #[cfg(any())] prevents it from participating in name checking; changing it to a regular if cfg!(any()) will still report an undefined name.

When selecting implementations by operating system, do not define functions with the same name using two overlapping conditions, such as unix and target_os="linux", which would both be true. When selecting by feature, consider unions as well: two user features might be enabled simultaneously. Only use compile_error! to reject combinations if the product contract explicitly forbids them; additive features should generally be allowed to coexist. Configuration Predicates

Why does a false branch still fail checking?

Runtime reachability does not determine compile-time checking. With linux_only defined only on Linux, compare what remains at name checking. #[cfg] removes the call, whereas cfg! merely expands to a Boolean constant.

Preparing the visual
Why does a false branch still fail checking?

#[cfg] removes disabled code; if cfg! is a Boolean branch whose arms still undergo name and type checking.

Common Conditions by Dimension

ConditionFact ExpressedNote
target_osTarget OS like linux, windows, macosNot the host system where the build script runs
target_archTarget architecture like x86_64, aarch64, wasm32Not sufficient to represent all ABIs
target_envTarget environment like gnu, musl, msvcSome targets have empty values
target_pointer_widthPointer widthNot equal to all integer or address implementation widths
target_featureInstruction features enabled for this compilation targetNot equal to support on any deployed machine
debug_assertionsCorresponding compilation switchCan be overridden by profile; not equivalent to fixed dev/release labels
testThe crate is compiled for testingIntegration tests depending on regular libraries do not automatically gain cfg(test)
feature="json"Current package feature enabled by CargoNot a CPU feature

View current target configuration:

rustc --print cfg
rustc --print cfg --target aarch64-unknown-linux-gnu

The second command views the target configuration known to the compiler; it does not install the target standard library or complete linking for you. Finding a name for a target does not mean the environment can already run the artifact.

cfg_attr Selects Attributes, Not Runtime Flow

#[cfg_attr(debug_assertions, derive(Debug))]
struct State { value: u32 }

fn main() {
    let state = State { value: 7 };
    assert_eq!(state.value, 7);
    #[cfg(debug_assertions)]
    println!("{state:?}");
}

When the condition is met, derive(Debug) is attached; otherwise, the attribute is not attached. The usage of Debug in the example is also controlled by the same condition; if println! is placed inside if cfg!(debug_assertions), builds with debug assertions disabled will still require State to implement Debug. Similarly, you can select link attributes, documentation annotations, or representation attributes, but if changing repr across platforms, consider FFI, storage formats, and the layout expectations of dependents.

When the same set of conditions appears repeatedly, centralize them at the module boundary; scattered cfgs might cause methods, imports, or trait implementations to be missed in certain configurations. Compiling the default configuration only proves that this one branch combination is valid.

Compile-Time CPU Features and Runtime Dispatch

#[cfg(target_feature="avx2")] indicates that AVX2 is enabled in the current compilation environment, not that it performs detection on the running machine. Distributing binaries built with target-cpu=native to different CPUs may also result in lost compatibility.

When you need a universal binary to select specialized implementations based on the machine, use the target platform's runtime feature detection, such as is_x86_feature_detected! for x86, and then call functions that meet the safety conditions of #[target_feature], while retaining a baseline implementation. Do not unconditionally execute specialized instructions first and then check for support with if. x86 Feature Detection

build.rs Generates Custom cfg

For example, whether a native library provides an API needs to be determined by the build script. You should declare both the valid name and value of the cfg so that rustc's checks can catch typos. The following demonstrates explicit environment input, without claiming to have detected a real system library:

fn main() {
    println!("cargo::rustc-check-cfg=cfg(has_demo_api)");
    println!("cargo::rerun-if-env-changed=DEMO_API");
    if std::env::var("DEMO_API").as_deref() == Ok("1") {
        println!("cargo::rustc-cfg=has_demo_api");
    }
}
Host: Run build.rsReads DEMO_API or target library infoCargo's Target ConfigurationTARGET / CARGO_CFG_TARGET_OSrustc-check-cfgDeclares valid name has_demo_apirustc-cfg (conditional output)Actually enables has_demo_apibuild.rs runs on the host, output affects the target code of the current packageTarget Code: Validity checks and conditional selection happen separatelybuild.rs's own cfg!(target_os) describes the host; reading target info is required to guide cross-compilation.

Save the script as build.rs in the project root, and use the complete two-branch interface in src/main.rs:

#[cfg(has_demo_api)]
fn capability() -> &'static str { "enabled" }

#[cfg(not(has_demo_api))]
fn capability() -> &'static str { "fallback" }

fn main() {
    println!("{}", capability());
}

Run the following commands sequentially in a POSIX shell. The second and third runs do not require manual cleanup of target; changes in environment input will cause the script to re-run:

DEMO_API=0 cargo run --quiet
DEMO_API=1 cargo run --quiet
DEMO_API=0 cargo run --quiet

The output will be fallback, enabled, fallback in order. The script explicitly stipulates that only the string "1" enables the feature; "true", empty values, and missing values will not enable it. Real library detection usually needs to distinguish between "not supported" and "detection failed"; otherwise, a missing toolchain might be mistaken for a missing feature, silently selecting a degraded implementation.

The main code uses #[cfg(has_demo_api)]. check-cfg only declares what conditions are valid, it does not enable conditions; rustc-cfg is what actually enables them. The cargo::KEY form requires support from the corresponding Cargo version; older projects can use the cargo:KEY form according to their MSRV. Build Script Output

If the object of detection is a cross-compilation target, you should check the target library and its headers; you cannot detect the host-installed library and directly write the conclusion to the target. Custom cfgs do not automatically propagate to other crates that depend on them.

Spelling Checks and Enable Switches Are Independently Verified

Copy the above main.rs to a temporary directory to directly verify the two types of rustc inputs:

rustc --edition 2024 --check-cfg 'cfg(has_demo_api)' -D unexpected_cfgs main.rs -o cfg-demo
./cfg-demo
rustc --edition 2024 --check-cfg 'cfg(has_demo_api)' --cfg has_demo_api -D unexpected_cfgs main.rs -o cfg-demo
./cfg-demo

The first run only registers the valid condition and prints fallback; the second run additionally enables the condition and prints enabled. If you misspell the name in the source code as has_demo_ap, unexpected_cfgs will reject it, preventing typos from being silently interpreted as "not supported." Declaring all possible names should be done unconditionally; do not declare them only within the branch where detection succeeds, otherwise, when the feature is disabled, it will generate unexpected configuration warnings itself. rustc Configuration Checking

Configuration Matrix Should Cover Interface Combinations

For a library claiming to support optional features, at least check the default, default disabled, single critical features, and promised combinations separately. all-features is only a union and cannot replace tests with default features disabled; helper imports in cfg(test) might also mask missing features in regular builds.

You can combine cargo check, cargo test, and --target to check different levels. Cross-compilation cargo check does not verify the target linker, native dependencies, or runtime behavior; these conclusions should be accurately distinguished.

Arrange the Matrix by Failure Reasons, Not Just by Increasing Command Count

Build DimensionSpecific Issues to DiscoverUseful Checks
Default features disabledDefault imports masking missing dependencies, fallback implementations undefinedcargo check --no-default-features
Single feature enabledThis feature incorrectly depending on another featureSeparate --no-default-features --features <name>
Allowed feature combinationsRedefinitions, conflicting implementations, merged dependenciesCheck/test promised combinations separately
debug_assertions disabledUsage points of attributes like Debug not synchronously protectedRelease build or explicitly override this switch
Custom condition true/falseFallback branches, declaration omissions, input changes not rebuildingThe above 0 → 1 → 0 experiment
Different targetsPlatform APIs, type widths, native library linkingTarget check, then link and run in available environments

For example, "json feature + has_demo_api" are two independent axes; you must consider four configurations: both disabled, only json, only API, and both enabled. If the actual contract allows one axis to appear only on specific targets, clearly write down the restrictions and verify diagnostics for unsupported combinations. Do not artificially inject built-in conditions like unix into rustc just to make it pass on the current machine; this will not change the target ABI or install the corresponding standard library.