Summary
A links (-sys) crate whose build script exposes an OUT_DIR-relative path through the metadata convention (e.g. cargo:include=$OUT_DIR/include, or cargo::metadata=include=$OUT_DIR/include) propagates that value to a dependent crate's build script via DEP_<LINKS>_INCLUDE with the producing crate's OUT_DIR rewritten to the literal, unresolved ${out_dir} token.
The dependent build script therefore sees something like:
DEP_LZ4_INCLUDE=<execroot>/${out_dir}/include
The ${out_dir} substring is never resolved, the directory does not exist, and C/C++ includes that consume DEP_<LINKS>_INCLUDE fail. Concretely, librocksdb-sys cannot find lz4.h when built against lz4-sys.
Affected versions
This is a regression from the path-mapping work, not present in older releases:
Root cause
In cargo/private/cargo_build_script_runner/lib.rs, outputs_to_dep_env redacts dep-env values with redact_paths, which rewrites the producing crate's out_dir to the generic ${out_dir} token:
Self::escape_for_serializing(Self::redact_paths(env, exec_root, out_dir))
That generic token is only resolved by process_wrapper's --out-dir flag, for the rustc action of the crate that directly owns the build script. But a dep-env (.depenv) file is consumed by a dependent crate's build-script runner (bin.rs), which only substitutes ${pwd}:
command.env(key, value.replace("${pwd}", &exec_root.to_string_lossy()));
It has no notion of the producing crate's out_dir and no ${out_dir} binding, so the token survives unresolved.
This is the same transitive-consumption scenario that redact_flags was deliberately written to handle (per-build-script unique tokens plus matching --subst entries added on the Starlark side). The dep-env path was left on the generic-token codepath, which has no resolution mechanism on the consuming side.
Reproduction
test/cargo_build_script/metadata_dep_env already exercises DEP_<links>_<KEY> propagation. Extending the producer to expose an OUT_DIR-relative include dir and asserting the dependent receives a resolved, existing path reproduces it (red before the fix, green after). A real-world trigger is librocksdb-sys with the bundled lz4 feature.
Proposed fix
In outputs_to_dep_env, redact only the exec root (${pwd}) and keep the producing crate's real out_dir-relative path. This is correct and safe because:
- The consumer is the dependent's build-script runner (
bin.rs), which already resolves ${pwd}.
- The producing crate's
out_dir tree is already a declared input of the dependent build-script action at that same exec-root-relative path.
- Build-script actions do not advertise
supports-path-mapping, so --experimental_output_paths=strip never rewrites that path out from under the literal value.
- The producing crate's own
.env file (outputs_to_env) still uses the ${out_dir} token, so path-mapping cache-shareability for the owning target is unchanged.
PR with the one-line fix plus unit and end-to-end regression tests to follow.
Reported downstream as hermeticbuild/rules_rs#163.
Summary
A
links(-sys) crate whose build script exposes anOUT_DIR-relative path through the metadata convention (e.g.cargo:include=$OUT_DIR/include, orcargo::metadata=include=$OUT_DIR/include) propagates that value to a dependent crate's build script viaDEP_<LINKS>_INCLUDEwith the producing crate'sOUT_DIRrewritten to the literal, unresolved${out_dir}token.The dependent build script therefore sees something like:
The
${out_dir}substring is never resolved, the directory does not exist, and C/C++ includes that consumeDEP_<LINKS>_INCLUDEfail. Concretely,librocksdb-syscannot findlz4.hwhen built againstlz4-sys.Affected versions
This is a regression from the path-mapping work, not present in older releases:
experimental_output_paths#4011 ("Add support forexperimental_output_paths", merged 2026-05-07), which added the${out_dir}tokenization of build-script outputs; refined by FixOUT_DIRsanitization incargo_build_script#4050.DEP_<links>_<KEY>metadata propagation that carries the include path comes from cargo_build_script_runner: parse cargo::metadata key/value pairs #3877.0.70.0(2026-04-22), which predates Add support forexperimental_output_paths#4011 and is unaffected. The bug currently exists onmainonly (not yet in a release).Root cause
In
cargo/private/cargo_build_script_runner/lib.rs,outputs_to_dep_envredacts dep-env values withredact_paths, which rewrites the producing crate'sout_dirto the generic${out_dir}token:That generic token is only resolved by
process_wrapper's--out-dirflag, for the rustc action of the crate that directly owns the build script. But a dep-env (.depenv) file is consumed by a dependent crate's build-script runner (bin.rs), which only substitutes${pwd}:It has no notion of the producing crate's
out_dirand no${out_dir}binding, so the token survives unresolved.This is the same transitive-consumption scenario that
redact_flagswas deliberately written to handle (per-build-script unique tokens plus matching--substentries added on the Starlark side). The dep-env path was left on the generic-token codepath, which has no resolution mechanism on the consuming side.Reproduction
test/cargo_build_script/metadata_dep_envalready exercisesDEP_<links>_<KEY>propagation. Extending the producer to expose anOUT_DIR-relative include dir and asserting the dependent receives a resolved, existing path reproduces it (red before the fix, green after). A real-world trigger islibrocksdb-syswith the bundledlz4feature.Proposed fix
In
outputs_to_dep_env, redact only the exec root (${pwd}) and keep the producing crate's realout_dir-relative path. This is correct and safe because:bin.rs), which already resolves${pwd}.out_dirtree is already a declared input of the dependent build-script action at that same exec-root-relative path.supports-path-mapping, so--experimental_output_paths=stripnever rewrites that path out from under the literal value..envfile (outputs_to_env) still uses the${out_dir}token, so path-mapping cache-shareability for the owning target is unchanged.PR with the one-line fix plus unit and end-to-end regression tests to follow.
Reported downstream as hermeticbuild/rules_rs#163.