Skip to content

Commit fd46a6a

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 fd46a6a

4 files changed

Lines changed: 115 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: 51 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,43 @@ 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 `{name}` placeholder, returns `Some(name)`. `{{`/`}}`-escaped
1377+
/// components (e.g. `{{cwd}}`) are not placeholders and return `None`.
1378+
fn remap_placeholder_name(component: &OsStr) -> Option<&str> {
1379+
let inner = component.to_str()?.strip_prefix('{')?.strip_suffix('}')?;
1380+
(!inner.is_empty() && !inner.contains(['{', '}'])).then_some(inner)
1381+
}
1382+
1383+
/// Expands a `--remap-path-prefix` FROM path: a `{cwd}` component becomes the working
1384+
/// directory, and `{{`/`}}` are unescaped to `{`/`}`. Expansion happens here, at apply time;
1385+
/// the FROM is stored with the placeholder intact, so the tracked option is stable across
1386+
/// build directories and the incremental cache survives. See #132132.
1387+
fn expand_remap_path_prefix(from: &Path) -> Option<PathBuf> {
1388+
let mut out = PathBuf::new();
1389+
for comp in from.components() {
1390+
match comp {
1391+
Component::Normal(seg) if remap_placeholder_name(seg) == Some("cwd") => {
1392+
out.push(std::env::current_dir().ok()?)
1393+
}
1394+
Component::Normal(seg) => match seg.to_str() {
1395+
Some(s) => out.push(s.replace("{{", "{").replace("}}", "}")),
1396+
None => out.push(seg),
1397+
},
1398+
other => out.push(other),
1399+
}
1400+
}
1401+
Some(out)
1402+
}
1403+
13761404
fn file_path_mapping(
13771405
remap_path_prefix: Vec<(PathBuf, PathBuf)>,
13781406
remap_path_scope: RemapPathScopeComponents,
13791407
) -> FilePathMapping {
1380-
FilePathMapping::new(remap_path_prefix.clone(), remap_path_scope)
1408+
let mapping = remap_path_prefix
1409+
.into_iter()
1410+
.filter_map(|(from, to)| Some((expand_remap_path_prefix(&from)?, to)))
1411+
.collect();
1412+
FilePathMapping::new(mapping, remap_path_scope)
13811413
}
13821414

13831415
impl Default for Options {
@@ -2396,13 +2428,23 @@ fn parse_remap_path_prefix(
23962428
Some((from, to)) => (PathBuf::from(from), PathBuf::from(to)),
23972429
})
23982430
.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-
};
2431+
// `-Zremap-cwd-prefix=VALUE` is sugar for `--remap-path-prefix={cwd}=VALUE`. See #132132.
2432+
if let Some(to) = &unstable_opts.remap_cwd_prefix {
2433+
mapping.push((PathBuf::from("{cwd}"), to.clone()));
2434+
}
2435+
// Only `{cwd}` is a recognised placeholder; reject typos like `{cdw}`.
2436+
for (from, _) in &mapping {
2437+
for comp in from.components() {
2438+
if let Component::Normal(seg) = comp
2439+
&& let Some(name) = remap_placeholder_name(seg)
2440+
&& name != "cwd"
2441+
{
2442+
early_dcx.early_fatal(format!(
2443+
"unknown placeholder `{{{name}}}` in `--remap-path-prefix`"
2444+
));
2445+
}
2446+
}
2447+
}
24062448
mapping
24072449
}
24082450

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
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+
6+
fn main() {}
7+
8+
//~? ERROR unknown placeholder `{foo}` in `--remap-path-prefix`
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
error: unknown placeholder `{foo}` in `--remap-path-prefix`
2+

0 commit comments

Comments
 (0)