Skip to content

Commit ceead8d

Browse files
committed
[POC] rustc_session: {cwd} placeholder in --remap-path-prefix
Proof-of-concept for an alternative to the incremental-cache invalidation caused by remapping the current working directory. Validates feasibility ahead of a Major Change Proposal; not meant to merge as-is. Today `-Zremap-cwd-prefix=V` resolves the absolute cwd at parse time and stores it in `remap_path_prefix` (`[TRACKED_NO_CRATE_HASH]`), so the working directory enters the incremental command-line hash and a build from a different directory (e.g. a sandbox or per-task worktree) purges the cache even though the remapped output is identical. This lets a `--remap-path-prefix` FROM contain a `{cwd}` placeholder, recognised only as a whole path component, with `format!`-style `{{`/`}}` escaping. The FROM is stored verbatim (so `{cwd}` and the escaped literal `{{cwd}}` stay distinct with no new type), and the placeholder is expanded only when the `FilePathMapping` is built. The stored, tracked value is therefore stable across build directories and keeps the incremental cache valid. Unknown placeholders are rejected during parsing. `-Zremap-cwd-prefix=V` becomes exactly sugar for `--remap-path-prefix={cwd}=V`, providing a path to sunset it. Adding placeholder semantics to a stable flag is a stable-surface change and needs sign-off (MCP).
1 parent d595fce commit ceead8d

3 files changed

Lines changed: 127 additions & 10 deletions

File tree

compiler/rustc_interface/src/tests.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use rustc_session::utils::{CanonicalizedPath, NativeLib};
2525
use rustc_session::{CompilerIO, EarlyDiagCtxt, Session, build_session, getopts};
2626
use rustc_span::edition::{DEFAULT_EDITION, Edition};
2727
use rustc_span::source_map::{RealFileLoader, SourceMapInputs};
28-
use rustc_span::{FileName, SourceFileHashAlgorithm, sym};
28+
use rustc_span::{FileName, RealFileName, RemapPathScopeComponents, SourceFileHashAlgorithm, sym};
2929
use rustc_target::spec::{
3030
CodeModel, FramePointer, LinkerFlavorCli, MergeFunctions, OnBrokenPipe, PanicStrategy,
3131
RelocModel, RelroLevel, SanitizerSet, SplitDebuginfo, StackProtector, TlsModel,
@@ -175,6 +175,59 @@ fn test_can_print_warnings() {
175175
});
176176
}
177177

178+
// `--remap-path-prefix={cwd}=...` stores the `{cwd}` placeholder verbatim (so the absolute cwd
179+
// never enters the tracked option and the incremental cache survives across build directories,
180+
// see #132132) and expands it only when the mapping is applied.
181+
#[test]
182+
fn test_remap_path_prefix_cwd_placeholder() {
183+
sess_and_cfg(&["--remap-path-prefix={cwd}/sub=mapped"], |sess, _cfg| {
184+
// Stored verbatim as the placeholder text, not the resolved cwd.
185+
assert_eq!(
186+
sess.opts.remap_path_prefix,
187+
vec![(PathBuf::from("{cwd}/sub"), PathBuf::from("mapped"))],
188+
);
189+
// ... but expanded + applied to real paths under the cwd.
190+
let cwd = std::env::current_dir().unwrap();
191+
let remapped = sess
192+
.opts
193+
.file_path_mapping()
194+
.to_real_filename(&RealFileName::empty(), cwd.join("sub").join("foo.rs"))
195+
.path(RemapPathScopeComponents::DEBUGINFO)
196+
.to_path_buf();
197+
assert_eq!(remapped, PathBuf::from("mapped/foo.rs"));
198+
});
199+
}
200+
201+
// `{{cwd}}` is an escaped literal `{cwd}` directory, NOT the placeholder, and is not expanded.
202+
#[test]
203+
fn test_remap_path_prefix_escaped_braces_are_literal() {
204+
sess_and_cfg(&["--remap-path-prefix={{cwd}}/x=mapped"], |sess, _cfg| {
205+
assert_eq!(
206+
sess.opts.remap_path_prefix,
207+
vec![(PathBuf::from("{{cwd}}/x"), PathBuf::from("mapped"))],
208+
);
209+
// Maps the literal prefix `{cwd}/x`, and does not touch the real cwd.
210+
let remapped = sess
211+
.opts
212+
.file_path_mapping()
213+
.to_real_filename(&RealFileName::empty(), PathBuf::from("{cwd}/x/foo.rs"))
214+
.path(RemapPathScopeComponents::DEBUGINFO)
215+
.to_path_buf();
216+
assert_eq!(remapped, PathBuf::from("mapped/foo.rs"));
217+
});
218+
}
219+
220+
// `-Zremap-cwd-prefix` is sugar for `--remap-path-prefix={cwd}=VALUE`: same stored placeholder.
221+
#[test]
222+
fn test_remap_cwd_prefix_stores_placeholder() {
223+
sess_and_cfg(&["-Zremap-cwd-prefix=mapped"], |sess, _cfg| {
224+
assert_eq!(
225+
sess.opts.remap_path_prefix,
226+
vec![(PathBuf::from("{cwd}"), PathBuf::from("mapped"))],
227+
);
228+
});
229+
}
230+
178231
#[test]
179232
fn test_output_types_tracking_hash_different_paths() {
180233
let mut v1 = Options::default();

compiler/rustc_session/src/config.rs

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::collections::btree_map::{
77
use std::collections::{BTreeMap, BTreeSet};
88
use std::ffi::OsStr;
99
use std::hash::Hash;
10-
use std::path::{Path, PathBuf};
10+
use std::path::{Component, Path, PathBuf};
1111
use std::str::{self, FromStr};
1212
use std::sync::LazyLock;
1313
use std::{cmp, fs, iter};
@@ -1373,11 +1373,53 @@ pub fn host_tuple() -> &'static str {
13731373
(option_env!("CFG_COMPILER_HOST_TRIPLE")).expect("CFG_COMPILER_HOST_TRIPLE")
13741374
}
13751375

1376+
/// If `component` is a whole-component `{name}` placeholder, returns `Some(name)`.
1377+
///
1378+
/// Only single-brace components qualify; `{{`/`}}`-escaped components are literal and
1379+
/// return `None` (so a literal directory named `{cwd}` is written `{{cwd}}`).
1380+
fn remap_placeholder_name(component: &OsStr) -> Option<&str> {
1381+
let inner = component.to_str()?.strip_prefix('{')?.strip_suffix('}')?;
1382+
(!inner.is_empty() && !inner.contains(['{', '}'])).then_some(inner)
1383+
}
1384+
1385+
/// Expands placeholders in a `--remap-path-prefix` FROM path.
1386+
///
1387+
/// A path component equal to `{cwd}` is replaced with the current working directory;
1388+
/// other components are literal, with `{{`/`}}` unescaped to `{`/`}`. Unknown
1389+
/// placeholders are rejected during parsing (see `parse_remap_path_prefix`).
1390+
///
1391+
/// The placeholder text -- not the resolved cwd -- is what is stored in the tracked
1392+
/// `remap_path_prefix` option; expansion happens here, at apply time. So the option's
1393+
/// dependency hash is stable across build directories and does not invalidate the
1394+
/// incremental cache. See #132132.
1395+
fn expand_remap_path_prefix(from: &Path) -> Option<PathBuf> {
1396+
let mut out = PathBuf::new();
1397+
for comp in from.components() {
1398+
match comp {
1399+
Component::Normal(seg) => match remap_placeholder_name(seg) {
1400+
Some("cwd") => out.push(std::env::current_dir().ok()?),
1401+
// Unknown placeholders are rejected during parsing; be literal defensively.
1402+
Some(_) => out.push(seg),
1403+
None => match seg.to_str() {
1404+
Some(s) => out.push(s.replace("{{", "{").replace("}}", "}")),
1405+
None => out.push(seg),
1406+
},
1407+
},
1408+
other => out.push(other),
1409+
}
1410+
}
1411+
Some(out)
1412+
}
1413+
13761414
fn file_path_mapping(
13771415
remap_path_prefix: Vec<(PathBuf, PathBuf)>,
13781416
remap_path_scope: RemapPathScopeComponents,
13791417
) -> FilePathMapping {
1380-
FilePathMapping::new(remap_path_prefix.clone(), remap_path_scope)
1418+
let mapping = remap_path_prefix
1419+
.into_iter()
1420+
.filter_map(|(from, to)| Some((expand_remap_path_prefix(&from)?, to)))
1421+
.collect();
1422+
FilePathMapping::new(mapping, remap_path_scope)
13811423
}
13821424

13831425
impl Default for Options {
@@ -2396,13 +2438,28 @@ fn parse_remap_path_prefix(
23962438
Some((from, to)) => (PathBuf::from(from), PathBuf::from(to)),
23972439
})
23982440
.collect();
2399-
match &unstable_opts.remap_cwd_prefix {
2400-
Some(to) => match std::env::current_dir() {
2401-
Ok(cwd) => mapping.push((cwd, to.clone())),
2402-
Err(_) => (),
2403-
},
2404-
None => (),
2405-
};
2441+
// `-Zremap-cwd-prefix=VALUE` is sugar for `--remap-path-prefix={cwd}=VALUE`: store the `{cwd}`
2442+
// placeholder (expanded in `file_path_mapping`) rather than the absolute cwd, so the
2443+
// incremental cache stays valid across build directories. See #132132.
2444+
if let Some(to) = &unstable_opts.remap_cwd_prefix {
2445+
mapping.push((PathBuf::from("{cwd}"), to.clone()));
2446+
}
2447+
// Reject unknown placeholders early. Only `{cwd}` is recognised; a literal `{name}`
2448+
// directory must be written `{{name}}`.
2449+
for (from, _) in &mapping {
2450+
for comp in from.components() {
2451+
if let Component::Normal(seg) = comp
2452+
&& let Some(name) = remap_placeholder_name(seg)
2453+
&& name != "cwd"
2454+
{
2455+
let placeholder = ["{", name, "}"].concat();
2456+
let literal = ["{{", name, "}}"].concat();
2457+
early_dcx.early_fatal(format!(
2458+
"unknown placeholder `{placeholder}` in `--remap-path-prefix`; write `{literal}` for a literal `{placeholder}`"
2459+
));
2460+
}
2461+
}
2462+
}
24062463
mapping
24072464
}
24082465

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Unknown `{name}` placeholders in `--remap-path-prefix` are rejected.
2+
// A literal `{foo}` directory would be written `{{foo}}`.
3+
//
4+
//@ compile-flags: --remap-path-prefix={foo}=bar
5+
//@ error-pattern: unknown placeholder `{foo}` in `--remap-path-prefix`
6+
7+
fn main() {}

0 commit comments

Comments
 (0)