Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
gcc g++ libc6-dev make cmake ninja-build meson golang-go binutils llvm \
gcc g++ clang ccache libc6-dev make cmake ninja-build meson golang-go binutils llvm \
podman nodejs npm zstd

- name: Install Bazel
Expand Down
2 changes: 1 addition & 1 deletion Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ ENV PATH=/opt/uv/bin:/opt/buildprof-venv/bin:/opt/cargo/bin:${PATH}
# Install every build system covered by the conformance suite.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates curl gcc g++ libc6-dev python3 python3-venv git \
ca-certificates curl gcc g++ clang ccache libc6-dev python3 python3-venv git \
make cmake ninja-build meson golang-go binutils llvm podman nodejs npm zstd \
&& rm -rf /var/lib/apt/lists/*

Expand Down
17 changes: 17 additions & 0 deletions src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ pub fn run_wrapper() -> Option<ExitCode> {
));
command.arg(format!("-Wl,--time-trace={}", output.display()));
}
hide_wrappers(&profile_dir, &mut command);
command
}
_ => return None,
Expand All @@ -574,6 +575,22 @@ pub fn run_wrapper() -> Option<ExitCode> {
Some(ExitCode::from(WRAPPER_FAILURE_EXIT_CODE))
}

/// Takes the wrapper directory back out of `PATH` before the real compiler
/// runs. The directory is there to intercept what the build launches, not what
/// the compiler launches, and a compiler launcher such as ccache resolves its
/// own name through `PATH`: leaving the directory in place lets it find this
/// wrapper again, which sends it straight back to the launcher, forever.
fn hide_wrappers(profile_dir: &OsStr, command: &mut Command) {
let Some(path) = env::var_os("PATH") else {
return;
};
let wrappers = Path::new(profile_dir).join(WRAPPER_DIRECTORY_NAME);
let kept = env::split_paths(&path).filter(|directory| *directory != wrappers);
if let Ok(path) = env::join_paths(kept) {
command.env("PATH", path);
}
}

fn find_on_path(program: &str) -> Option<PathBuf> {
env::split_paths(&env::var_os("PATH")?)
.map(|directory| directory.join(program))
Expand Down
55 changes: 52 additions & 3 deletions tests/conformance/test_compiler_traces.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

Rust self-profile data needs a nightly compiler. The build decides which
toolchain runs, so the wrapper must find out from the compiler it launches
rather than from whatever `rustc` is the default at startup.
rather than from whatever `rustc` is the default at startup. The build also
decides what stands in for the compiler, which may be a launcher that goes
looking for the compiler itself.
"""

from __future__ import annotations
Expand All @@ -19,6 +21,10 @@

from .model import count_compiler_events, load_perfetto, load_trace_attributes

# Long enough for a cold ccache to compile a one-line program, short enough
# that a wrapper that never stops is reported rather than waited on.
CCACHE_TIMEOUT_SECONDS = 60

# How a build can pick the nightly toolchain without changing the default.
NIGHTLY_SELECTIONS = {
"cargo-plus-nightly": (["cargo", "+nightly", "build", "--offline"], {}, None),
Expand Down Expand Up @@ -70,15 +76,21 @@ def _cargo_project(root: Path, toolchain_file: str | None) -> None:
(root / "rust-toolchain.toml").write_text(toolchain_file)


def _record(buildprof: Path, project: Path, command: list[str], extra_env: dict) -> Path:
def _record(
buildprof: Path,
project: Path,
command: list[str],
extra_env: dict,
timeout: int = 300,
) -> Path:
trace = project.parent / f"{project.name}.pftrace"
env = dict(os.environ, LC_ALL="C", **extra_env)
result = subprocess.run(
[str(buildprof), "--compiler-traces", "-o", str(trace), "--", *command],
cwd=project,
text=True,
capture_output=True,
timeout=300,
timeout=timeout,
env=env,
)
assert result.returncode == 0, f"build failed:\n{result.stdout}{result.stderr}"
Expand Down Expand Up @@ -131,3 +143,40 @@ def test_default_toolchain_builds_and_traces_only_if_nightly(
assert events > 0, "the default nightly rustc produced no compiler events"
else:
assert events == 0, f"a stable rustc produced {events} compiler events"


def test_ccache_wrappers_on_path_do_not_loop(buildprof: Path, tmp_path: Path):
"""A compiler launcher must not find the wrapper that launched it.

ccache is normally used through a directory of symlinks named after the
compilers, early on `PATH`. It works out which compiler to run by looking
its own name up on `PATH` again, skipping only itself, so it finds the
wrapper directory and runs the wrapper, which hands straight back to
ccache. Left alone the two never stop.
"""
_require("ccache")
_require("clang")
project = tmp_path / "ccache"
project.mkdir()
(project / "hello.c").write_text("int main(void) { return 0; }\n")
launchers = tmp_path / "ccache-bin"
launchers.mkdir()
ccache = shutil.which("ccache")
for name in ("clang", "clang++"):
(launchers / name).symlink_to(ccache)

trace = _record(
buildprof,
project,
["clang", "hello.c", "-o", "hello"],
{
"PATH": f"{launchers}{os.pathsep}{os.environ['PATH']}",
"CCACHE_DIR": str(tmp_path / "cache"),
},
timeout=CCACHE_TIMEOUT_SECONDS,
)

assert (project / "hello").is_file(), "the build produced nothing"
assert count_compiler_events(trace, "Clang") > 0, (
"compiling through ccache recorded no Clang events"
)
Loading