diff --git a/compiler/rustc_interface/src/tests.rs b/compiler/rustc_interface/src/tests.rs index 0498d835df5f5..136c458b3a4f9 100644 --- a/compiler/rustc_interface/src/tests.rs +++ b/compiler/rustc_interface/src/tests.rs @@ -25,7 +25,7 @@ use rustc_session::utils::{CanonicalizedPath, NativeLib}; use rustc_session::{CompilerIO, EarlyDiagCtxt, Session, build_session, getopts}; use rustc_span::edition::{DEFAULT_EDITION, Edition}; use rustc_span::source_map::{RealFileLoader, SourceMapInputs}; -use rustc_span::{FileName, SourceFileHashAlgorithm, sym}; +use rustc_span::{FileName, RealFileName, RemapPathScopeComponents, SourceFileHashAlgorithm, sym}; use rustc_target::spec::{ CodeModel, FramePointer, LinkerFlavorCli, MergeFunctions, OnBrokenPipe, PanicStrategy, RelocModel, RelroLevel, SanitizerSet, SplitDebuginfo, StackProtector, TlsModel, @@ -175,6 +175,59 @@ fn test_can_print_warnings() { }); } +// `--remap-path-prefix={cwd}=...` stores the `{cwd}` placeholder verbatim (so the absolute cwd +// never enters the tracked option and the incremental cache survives across build directories, +// see #132132) and expands it only when the mapping is applied. +#[test] +fn test_remap_path_prefix_cwd_placeholder() { + sess_and_cfg(&["--remap-path-prefix={cwd}/sub=mapped"], |sess, _cfg| { + // Stored verbatim as the placeholder text, not the resolved cwd. + assert_eq!( + sess.opts.remap_path_prefix, + vec![(PathBuf::from("{cwd}/sub"), PathBuf::from("mapped"))], + ); + // ... but expanded + applied to real paths under the cwd. + let cwd = std::env::current_dir().unwrap(); + let remapped = sess + .opts + .file_path_mapping() + .to_real_filename(&RealFileName::empty(), cwd.join("sub").join("foo.rs")) + .path(RemapPathScopeComponents::DEBUGINFO) + .to_path_buf(); + assert_eq!(remapped, PathBuf::from("mapped/foo.rs")); + }); +} + +// `{{cwd}}` is an escaped literal `{cwd}` directory, NOT the placeholder, and is not expanded. +#[test] +fn test_remap_path_prefix_escaped_braces_are_literal() { + sess_and_cfg(&["--remap-path-prefix={{cwd}}/x=mapped"], |sess, _cfg| { + assert_eq!( + sess.opts.remap_path_prefix, + vec![(PathBuf::from("{{cwd}}/x"), PathBuf::from("mapped"))], + ); + // Maps the literal prefix `{cwd}/x`, and does not touch the real cwd. + let remapped = sess + .opts + .file_path_mapping() + .to_real_filename(&RealFileName::empty(), PathBuf::from("{cwd}/x/foo.rs")) + .path(RemapPathScopeComponents::DEBUGINFO) + .to_path_buf(); + assert_eq!(remapped, PathBuf::from("mapped/foo.rs")); + }); +} + +// `-Zremap-cwd-prefix` is sugar for `--remap-path-prefix={cwd}=VALUE`: same stored placeholder. +#[test] +fn test_remap_cwd_prefix_stores_placeholder() { + sess_and_cfg(&["-Zremap-cwd-prefix=mapped"], |sess, _cfg| { + assert_eq!( + sess.opts.remap_path_prefix, + vec![(PathBuf::from("{cwd}"), PathBuf::from("mapped"))], + ); + }); +} + #[test] fn test_output_types_tracking_hash_different_paths() { let mut v1 = Options::default(); diff --git a/compiler/rustc_session/src/config.rs b/compiler/rustc_session/src/config.rs index e82f67eac5e9f..c0e8818b9e68b 100644 --- a/compiler/rustc_session/src/config.rs +++ b/compiler/rustc_session/src/config.rs @@ -7,7 +7,7 @@ use std::collections::btree_map::{ use std::collections::{BTreeMap, BTreeSet}; use std::ffi::OsStr; use std::hash::Hash; -use std::path::{Path, PathBuf}; +use std::path::{Component, Path, PathBuf}; use std::str::{self, FromStr}; use std::sync::LazyLock; use std::{cmp, fs, iter}; @@ -1373,11 +1373,43 @@ pub fn host_tuple() -> &'static str { (option_env!("CFG_COMPILER_HOST_TRIPLE")).expect("CFG_COMPILER_HOST_TRIPLE") } +/// If `component` is a whole `{name}` placeholder, returns `Some(name)`. `{{`/`}}`-escaped +/// components (e.g. `{{cwd}}`) are not placeholders and return `None`. +fn remap_placeholder_name(component: &OsStr) -> Option<&str> { + let inner = component.to_str()?.strip_prefix('{')?.strip_suffix('}')?; + (!inner.is_empty() && !inner.contains(['{', '}'])).then_some(inner) +} + +/// Expands a `--remap-path-prefix` FROM path: a `{cwd}` component becomes the working +/// directory, and `{{`/`}}` are unescaped to `{`/`}`. Expansion happens here, at apply time; +/// the FROM is stored with the placeholder intact, so the tracked option is stable across +/// build directories and the incremental cache survives. See #132132. +fn expand_remap_path_prefix(from: &Path) -> Option { + let mut out = PathBuf::new(); + for comp in from.components() { + match comp { + Component::Normal(seg) if remap_placeholder_name(seg) == Some("cwd") => { + out.push(std::env::current_dir().ok()?) + } + Component::Normal(seg) => match seg.to_str() { + Some(s) => out.push(s.replace("{{", "{").replace("}}", "}")), + None => out.push(seg), + }, + other => out.push(other), + } + } + Some(out) +} + fn file_path_mapping( remap_path_prefix: Vec<(PathBuf, PathBuf)>, remap_path_scope: RemapPathScopeComponents, ) -> FilePathMapping { - FilePathMapping::new(remap_path_prefix.clone(), remap_path_scope) + let mapping = remap_path_prefix + .into_iter() + .filter_map(|(from, to)| Some((expand_remap_path_prefix(&from)?, to))) + .collect(); + FilePathMapping::new(mapping, remap_path_scope) } impl Default for Options { @@ -2396,13 +2428,23 @@ fn parse_remap_path_prefix( Some((from, to)) => (PathBuf::from(from), PathBuf::from(to)), }) .collect(); - match &unstable_opts.remap_cwd_prefix { - Some(to) => match std::env::current_dir() { - Ok(cwd) => mapping.push((cwd, to.clone())), - Err(_) => (), - }, - None => (), - }; + // `-Zremap-cwd-prefix=VALUE` is sugar for `--remap-path-prefix={cwd}=VALUE`. See #132132. + if let Some(to) = &unstable_opts.remap_cwd_prefix { + mapping.push((PathBuf::from("{cwd}"), to.clone())); + } + // Only `{cwd}` is a recognised placeholder; reject typos like `{cdw}`. + for (from, _) in &mapping { + for comp in from.components() { + if let Component::Normal(seg) = comp + && let Some(name) = remap_placeholder_name(seg) + && name != "cwd" + { + early_dcx.early_fatal(format!( + "unknown placeholder `{{{name}}}` in `--remap-path-prefix`" + )); + } + } + } mapping } diff --git a/tests/ui/errors/remap-path-prefix-unknown-placeholder.rs b/tests/ui/errors/remap-path-prefix-unknown-placeholder.rs new file mode 100644 index 0000000000000..ff9cd9128b527 --- /dev/null +++ b/tests/ui/errors/remap-path-prefix-unknown-placeholder.rs @@ -0,0 +1,8 @@ +// Unknown `{name}` placeholders in `--remap-path-prefix` are rejected. +// A literal `{foo}` directory would be written `{{foo}}`. +// +//@ compile-flags: --remap-path-prefix={foo}=bar + +fn main() {} + +//~? ERROR unknown placeholder `{foo}` in `--remap-path-prefix` diff --git a/tests/ui/errors/remap-path-prefix-unknown-placeholder.stderr b/tests/ui/errors/remap-path-prefix-unknown-placeholder.stderr new file mode 100644 index 0000000000000..70763b9eb2c26 --- /dev/null +++ b/tests/ui/errors/remap-path-prefix-unknown-placeholder.stderr @@ -0,0 +1,2 @@ +error: unknown placeholder `{foo}` in `--remap-path-prefix` +