1use std::{
11 collections::BTreeMap,
12 ffi::{OsStr, OsString},
13 fs::{self, OpenOptions},
14 io::{self, Write},
15 path::{Component, Path, PathBuf},
16 process::Command,
17 thread,
18 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
19};
20
21use nextest_metadata::TestListSummary;
22use serde::{Deserialize, Serialize};
23
24use crate::workspace::canonicalize_simplified;
25use crate::{
26 process_supervision::{
27 CommandSpec, ForwardedSignal, ProcessSupervisor, SupervisedOutput, SupervisionOptions,
28 },
29 rust_cargo_configuration::{RustCargoResolvedTargetRunner, RustCargoRunnerPlan},
30 rust_compiler_ctfe::{RustCompilerCtfeUnit, read_rust_compiler_ctfe},
31 rust_compiler_manifest::{NormalizedRustCompilerManifest, normalize_rust_compiler_candidates},
32 rust_compiler_selection::{SelectedRustCompilerCompanion, select_rust_compiler_companion},
33 rust_compiler_test_runner::{
34 RUST_CARGO_RUNNER_CONFIG_ENV, RUST_CARGO_RUNNER_VERSION, RustCargoRunnerArtifact,
35 RustCargoRunnerConfig, RustCargoRunnerUnit, read_cargo_runner_units,
36 },
37 rust_doctest::{
38 RustdocOutcomeResolution, join_rustdoc_outcomes, read_rustdoc_outcome_units,
39 resolve_merged_doctest_candidates,
40 },
41 rust_runner_attempt::parse_nextest_version_output,
42 rust_test_runner::{
43 RustCargoCommandKind, cargo_invocation, nextest_list_invocation, nextest_version_arguments,
44 rust_cargo_execution_selection,
45 },
46};
47
48fn inherited_environment(
49 overrides: impl IntoIterator<Item = (OsString, OsString)>,
50) -> Vec<(OsString, OsString)> {
51 let mut environment = std::env::vars_os().collect::<BTreeMap<_, _>>();
52 environment.extend(overrides);
53 environment.into_iter().collect()
54}
55
56fn supervised_success(output: &SupervisedOutput) -> bool {
57 output.result.status == Some(0)
58 && output.result.signal.is_none()
59 && !output.result.timed_out
60 && output.result.interrupted_signal.is_none()
61}
62
63fn interrupted_error(output: &SupervisedOutput) -> Option<RustCompilerOrchestrationError> {
64 output.result.interrupted_signal.map(|signal| {
65 let signal = match signal {
66 ForwardedSignal::Sighup => "SIGHUP",
67 ForwardedSignal::Sigint => "SIGINT",
68 ForwardedSignal::Sigterm => "SIGTERM",
69 };
70 RustCompilerOrchestrationError::Interrupted {
71 code: output.result.exit_code(),
72 signal: signal.into(),
73 }
74 })
75}
76
77fn cargo_runner_configuration_arguments(
78 wrapper: &Path,
79 plan: &RustCargoRunnerPlan,
80) -> Result<Vec<String>, RustCompilerOrchestrationError> {
81 let wrapper = wrapper.to_str().ok_or_else(|| {
82 RustCompilerOrchestrationError::InvalidRequest(
83 "the Cargo runner executable path is not UTF-8".into(),
84 )
85 })?;
86 let wrapper = serde_json::to_string(wrapper)
87 .map_err(|error| RustCompilerOrchestrationError::InvalidRequest(error.to_string()))?;
88 let mut seen = BTreeMap::new();
89 let mut arguments = Vec::with_capacity(plan.targets.len() * 2);
90 for target in &plan.targets {
91 if seen.insert(target.target.as_str(), ()).is_some() {
92 return Err(RustCompilerOrchestrationError::InvalidRequest(format!(
93 "Cargo runner plan contains duplicate target identity: {}",
94 target.target
95 )));
96 }
97 let target = serde_json::to_string(&target.target)
98 .map_err(|error| RustCompilerOrchestrationError::InvalidRequest(error.to_string()))?;
99 arguments.extend([
100 "--config".into(),
101 format!("target.{target}.runner=[{wrapper},\"__cargo-test-runner\",{target}]"),
102 ]);
103 }
104 if arguments.is_empty() {
105 return Err(RustCompilerOrchestrationError::InvalidRequest(
106 "Cargo runner plan has no selected targets".into(),
107 ));
108 }
109 Ok(arguments)
110}
111
112pub const RUST_COMPILER_WRAPPER_CONFIG_ENV: &str = "SUPERCOV_RUST_COMPILER_WRAPPER_CONFIG";
113pub const RUST_COMPILER_INNER_MODE_ENV: &str = "SUPERCOV_RUST_COMPILER_INNER_MODE";
114pub const RUST_ORIGINAL_COMPILER_ENV: &str = "SUPERCOV_RUST_ORIGINAL_COMPILER";
115pub const RUST_COMPILER_OUTPUT_ENV: &str = "SUPERCOV_RUST_COMPILER_OUTPUT";
116pub const RUST_SOURCE_ROOT_ENV: &str = "SUPERCOV_RUST_SOURCE_ROOT";
117pub const RUST_TARGET_ROOT_ENV: &str = "SUPERCOV_RUST_TARGET_ROOT";
118pub const RUST_INSTRUMENT_MIR_ENV: &str = "SUPERCOV_RUST_INSTRUMENT_MIR";
119pub const RUST_INSTRUMENT_CTFE_ENV: &str = "SUPERCOV_RUST_INSTRUMENT_CTFE";
120pub const RUST_STATIC_RUNTIME_DIRECTORY_ENV: &str = "SUPERCOV_RUST_STATIC_RUNTIME_DIRECTORY";
121pub const RUSTDOC_WRAPPER_MODE_ENV: &str = "SUPERCOV_RUSTDOC_WRAPPER_MODE";
122pub const RUST_REAL_RUSTDOC_ENV: &str = "SUPERCOV_RUST_REAL_RUSTDOC";
123pub const RUST_COMPANION_PATH_ENV: &str = "SUPERCOV_RUST_COMPANION_PATH";
124pub const RUSTDOC_CAPTURE_OUTCOMES_ENV: &str = "SUPERCOV_RUSTDOC_CAPTURE_OUTCOMES";
125pub const RUSTDOC_ENGINE_PATH_ENV: &str = "SUPERCOV_RUSTDOC_ENGINE_PATH";
126const SHARED_RUNTIME_TEMPLATE: &str = include_str!("../runtime-assets/rust-mmap-runtime.rs");
127const SHARED_RUNTIME_EXPORTS: &str = r#"
128#[unsafe(no_mangle)] pub extern "C" fn __supercov_rt_ordinal_hit(ordinal: u64) { __supercov_shared_runtime::ordinal_hit(ordinal) }
129#[unsafe(no_mangle)] pub extern "C" fn __supercov_rt_active_context() -> u64 { __supercov_shared_runtime::active_context() }
130#[unsafe(no_mangle)] pub extern "C" fn __supercov_rt_enter_context(context_id: u64) -> u64 { __supercov_shared_runtime::enter_context(context_id) }
131#[unsafe(no_mangle)] pub extern "C" fn __supercov_rt_exit_context(previous: u64) { __supercov_shared_runtime::exit_context(previous) }
132#[unsafe(no_mangle)] pub extern "C" fn __supercov_rt_exit_test_context(context_id: u64, previous: u64) { __supercov_shared_runtime::exit_test_context(context_id, previous) }
133#[unsafe(no_mangle)] pub extern "C" fn __supercov_rt_enter_assertion_context(id_high: u64, id_low: u32) -> u64 { __supercov_shared_runtime::enter_assertion_context(id_high, id_low) }
134#[unsafe(no_mangle)] pub extern "C" fn __supercov_rt_decision_start(id_high: u64, id_low: u32, conditions: u64) -> u64 { __supercov_shared_runtime::mir_decision_start(id_high, id_low, conditions) }
135#[unsafe(no_mangle)] pub extern "C" fn __supercov_rt_decision_condition(token: u64, index: u64, value: bool) { __supercov_shared_runtime::mir_decision_condition(token, index, value) }
136#[unsafe(no_mangle)] pub extern "C" fn __supercov_rt_decision_finish(token: u64, outcome: bool) { __supercov_shared_runtime::mir_decision_finish(token, outcome) }
137#[unsafe(no_mangle)] pub extern "C" fn __supercov_rt_branch_start() -> u64 { __supercov_shared_runtime::mir_branch_start() }
138#[unsafe(no_mangle)] pub extern "C" fn __supercov_rt_branch_hit(token: u64, ordinal: u64) { __supercov_shared_runtime::mir_branch_hit(token, ordinal) }
139"#;
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase", deny_unknown_fields)]
143pub struct RustCompilerWrapperConfig {
144 pub candidates: Vec<PathBuf>,
145 pub require_public_capabilities: bool,
146 pub selection_directory: PathBuf,
147 pub shared_runtime_directory: PathBuf,
148 pub target_runners: Vec<RustCargoResolvedTargetRunner>,
149 pub project_root: PathBuf,
150 pub compiler: crate::rust_cargo_configuration::RustCargoCompilerCommandPlan,
151 pub original_wrapper_environment: RustCompilerWrapperEnvironment,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155#[serde(tag = "encoding", rename_all = "kebab-case", deny_unknown_fields)]
156pub enum RustCompilerEnvironmentValue {
157 UnixBytes { value: Vec<u8> },
158 WindowsWide { value: Vec<u16> },
159}
160
161impl RustCompilerEnvironmentValue {
162 fn capture(value: &OsStr) -> Self {
163 #[cfg(unix)]
164 {
165 use std::os::unix::ffi::OsStrExt as _;
166 Self::UnixBytes {
167 value: value.as_bytes().to_vec(),
168 }
169 }
170 #[cfg(windows)]
171 {
172 use std::os::windows::ffi::OsStrExt as _;
173 Self::WindowsWide {
174 value: value.encode_wide().collect(),
175 }
176 }
177 #[cfg(not(any(unix, windows)))]
178 {
179 Self::UnixBytes {
180 value: value.to_string_lossy().as_bytes().to_vec(),
181 }
182 }
183 }
184
185 pub fn decode(&self) -> Result<OsString, RustCompilerOrchestrationError> {
186 match self {
187 Self::UnixBytes { value } => {
188 #[cfg(unix)]
189 {
190 use std::os::unix::ffi::OsStringExt as _;
191 Ok(OsString::from_vec(value.clone()))
192 }
193 #[cfg(not(unix))]
194 {
195 let _ = value;
196 Err(RustCompilerOrchestrationError::InvalidRequest(
197 "Unix compiler-wrapper environment was read on a non-Unix host".into(),
198 ))
199 }
200 }
201 Self::WindowsWide { value } => {
202 #[cfg(windows)]
203 {
204 use std::os::windows::ffi::OsStringExt as _;
205 Ok(OsString::from_wide(value))
206 }
207 #[cfg(not(windows))]
208 {
209 let _ = value;
210 Err(RustCompilerOrchestrationError::InvalidRequest(
211 "Windows compiler-wrapper environment was read on a non-Windows host"
212 .into(),
213 ))
214 }
215 }
216 }
217 }
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase", deny_unknown_fields)]
222pub struct RustCompilerWrapperEnvironment {
223 pub rustc_wrapper: Option<RustCompilerEnvironmentValue>,
224 pub rustc_workspace_wrapper: Option<RustCompilerEnvironmentValue>,
225}
226
227impl RustCompilerWrapperEnvironment {
228 fn capture() -> Self {
229 Self {
230 rustc_wrapper: std::env::var_os("RUSTC_WRAPPER")
231 .as_deref()
232 .map(RustCompilerEnvironmentValue::capture),
233 rustc_workspace_wrapper: std::env::var_os("RUSTC_WORKSPACE_WRAPPER")
234 .as_deref()
235 .map(RustCompilerEnvironmentValue::capture),
236 }
237 }
238
239 pub fn restore(&self, command: &mut Command) -> Result<(), RustCompilerOrchestrationError> {
240 for (name, value) in [
241 ("RUSTC_WRAPPER", &self.rustc_wrapper),
242 ("RUSTC_WORKSPACE_WRAPPER", &self.rustc_workspace_wrapper),
243 ] {
244 if let Some(value) = value {
245 command.env(name, value.decode()?);
246 } else {
247 command.env_remove(name);
248 }
249 }
250 Ok(())
251 }
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
255#[serde(rename_all = "camelCase", deny_unknown_fields)]
256pub struct RustCompilerBuildRequest {
257 pub project_root: PathBuf,
258 pub command: Vec<String>,
259 pub run_id: String,
260 pub wrapper_path: PathBuf,
261 pub companion_candidates: Vec<PathBuf>,
262 pub require_public_capabilities: bool,
263 pub cargo_runner_plan: RustCargoRunnerPlan,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
267#[serde(rename_all = "camelCase")]
268pub struct RustCompilerTestArtifact {
269 pub executable: PathBuf,
270 pub package: String,
271 pub target_name: String,
272 pub target_kinds: Vec<String>,
273 pub source_path: PathBuf,
274 pub test_harness: bool,
275}
276
277#[derive(Debug, Clone, PartialEq, Serialize)]
278#[serde(rename_all = "camelCase")]
279pub struct RustCompilerBuild {
280 pub selection: SelectedRustCompilerCompanion,
281 pub normalized: NormalizedRustCompilerManifest,
282 pub artifacts: Vec<RustCompilerTestArtifact>,
283 pub target_directory: PathBuf,
284 pub compiler_output_directory: PathBuf,
285 pub ctfe_units: Vec<RustCompilerCtfeUnit>,
286 pub doctest_outcomes: RustdocOutcomeResolution,
287 pub cargo_runner_units: Vec<RustCargoRunnerUnit>,
288 #[serde(skip)]
289 pub(crate) command_kind: RustCargoCommandKind,
290 #[serde(skip)]
291 pub(crate) nextest_version: Option<String>,
292 #[serde(skip)]
293 pub(crate) nextest_catalog: Option<TestListSummary>,
294 pub run_libtests: bool,
295 pub run_doctests: bool,
296 pub execution_exit_code: i32,
297 pub execution_stdout: Vec<u8>,
298 pub execution_stderr: Vec<u8>,
299 pub build_started_at_ms: i64,
300 pub build_ended_at_ms: i64,
301 pub build_ms: f64,
302 pub execution_ms: f64,
303}
304
305#[derive(Debug)]
306pub enum RustCompilerOrchestrationError {
307 InvalidRequest(String),
308 Io { path: PathBuf, reason: String },
309 Cargo(String),
310 CargoOutput(String),
311 CompilerOutput(String),
312 Selection(String),
313 Manifest(String),
314 UnverifiedExecution { code: i32, reason: String },
315 Interrupted { code: i32, signal: String },
316}
317
318impl std::fmt::Display for RustCompilerOrchestrationError {
319 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 match self {
321 Self::InvalidRequest(reason) => {
322 write!(formatter, "invalid Rust compiler build: {reason}")
323 }
324 Self::Io { path, reason } => write!(formatter, "{}: {reason}", path.display()),
325 Self::Cargo(reason) => write!(formatter, "Cargo compiler build failed: {reason}"),
326 Self::CargoOutput(reason) => {
327 write!(formatter, "invalid Cargo compiler output: {reason}")
328 }
329 Self::CompilerOutput(reason) => {
330 write!(formatter, "invalid Rust compiler output: {reason}")
331 }
332 Self::Selection(reason) => {
333 write!(formatter, "Rust compiler selection failed: {reason}")
334 }
335 Self::Manifest(reason) => write!(formatter, "Rust compiler manifest failed: {reason}"),
336 Self::UnverifiedExecution { code, reason } => write!(
337 formatter,
338 "Rust test command exited {code}, but Supercov could not authenticate complete coverage evidence: {reason}"
339 ),
340 Self::Interrupted { signal, .. } => {
341 write!(formatter, "Rust compiler run was interrupted by {signal}")
342 }
343 }
344 }
345}
346
347impl std::error::Error for RustCompilerOrchestrationError {}
348
349#[derive(Debug, Deserialize)]
350struct CargoMessage {
351 reason: String,
352 #[serde(default)]
353 manifest_path: Option<PathBuf>,
354 #[serde(default)]
355 target: Option<CargoTarget>,
356 #[serde(default)]
357 profile: Option<CargoProfile>,
358 executable: Option<PathBuf>,
359 #[serde(default)]
360 message: Option<CargoDiagnostic>,
361}
362
363#[derive(Debug, Deserialize)]
364struct CargoDiagnostic {
365 rendered: Option<String>,
366}
367
368#[derive(Debug, Deserialize)]
369struct CargoTarget {
370 name: String,
371 kind: Vec<String>,
372 src_path: PathBuf,
373}
374
375#[derive(Debug, Deserialize)]
376struct CargoProfile {
377 test: bool,
378}
379
380#[derive(Debug, Deserialize)]
381struct CargoMetadataOutput {
382 packages: Vec<CargoMetadataPackage>,
383}
384
385#[derive(Debug, Deserialize)]
386struct CargoMetadataPackage {
387 id: String,
388 name: String,
389 manifest_path: PathBuf,
390 targets: Vec<CargoMetadataTarget>,
391}
392
393#[derive(Debug, Deserialize)]
394struct CargoMetadataTarget {
395 name: String,
396 kind: Vec<String>,
397 src_path: PathBuf,
398}
399
400fn cargo_metadata_arguments(
401 invocation: &crate::rust_test_runner::CargoTestInvocation,
402) -> Result<Vec<String>, RustCompilerOrchestrationError> {
403 let command = invocation.command_position().ok_or_else(|| {
404 RustCompilerOrchestrationError::InvalidRequest(
405 "the Cargo invocation lost its test subcommand".into(),
406 )
407 })?;
408 let mut arguments = invocation.arguments[..command]
409 .iter()
410 .filter(|argument| argument.starts_with('+'))
411 .cloned()
412 .collect::<Vec<_>>();
413 arguments.extend([
414 "metadata".into(),
415 "--format-version=1".into(),
416 "--no-deps".into(),
417 ]);
418 let command_width = match invocation.kind {
419 RustCargoCommandKind::CargoTest => 1,
420 RustCargoCommandKind::NextestRun => 2,
421 };
422 let mut index = command + command_width;
423 while index < invocation.arguments.len() {
424 let argument = &invocation.arguments[index];
425 let name = argument
426 .split_once('=')
427 .map_or(argument.as_str(), |(name, _)| name);
428 let takes_value = match name {
429 "--manifest-path" | "--config" | "-Z" => Some(!argument.contains('=')),
430 "--frozen" | "--locked" | "--offline" | "--ignore-rust-version" => Some(false),
431 _ => None,
432 };
433 if let Some(takes_value) = takes_value {
434 arguments.push(argument.clone());
435 if takes_value {
436 index += 1;
437 let value = invocation.arguments.get(index).ok_or_else(|| {
438 RustCompilerOrchestrationError::InvalidRequest(format!(
439 "Cargo option {argument} has no value"
440 ))
441 })?;
442 arguments.push(value.clone());
443 }
444 }
445 index += 1;
446 }
447 Ok(arguments)
448}
449
450fn package_identity(
451 manifest_path: &Path,
452 project_root: &Path,
453) -> Result<String, RustCompilerOrchestrationError> {
454 let manifest_metadata =
455 fs::symlink_metadata(manifest_path).map_err(|error| io_error(manifest_path, error))?;
456 let manifest =
457 canonicalize_simplified(manifest_path).map_err(|error| io_error(manifest_path, error))?;
458 let package_root = manifest
459 .parent()
460 .and_then(|path| path.strip_prefix(project_root).ok())
461 .filter(|_| {
462 manifest_metadata.file_type().is_file()
463 && manifest
464 .file_name()
465 .is_some_and(|name| name == "Cargo.toml")
466 })
467 .ok_or_else(|| {
468 RustCompilerOrchestrationError::CargoOutput(format!(
469 "test artifact manifest escaped the owned project: {}",
470 manifest.display()
471 ))
472 })?;
473 if package_root.as_os_str().is_empty() {
474 Ok("package:.".into())
475 } else if package_root
476 .components()
477 .all(|component| matches!(component, Component::Normal(_)))
478 {
479 Ok(format!(
480 "package:{}",
481 package_root.to_string_lossy().replace('\\', "/")
482 ))
483 } else {
484 Err(RustCompilerOrchestrationError::CargoOutput(format!(
485 "test artifact has a noncanonical package root: {}",
486 package_root.display()
487 )))
488 }
489}
490
491fn nextest_artifacts(
492 catalog: &TestListSummary,
493 metadata: &CargoMetadataOutput,
494 target_directory: &Path,
495 project_root: &Path,
496) -> Result<Vec<RustCompilerTestArtifact>, RustCompilerOrchestrationError> {
497 let canonical_target = canonicalize_simplified(target_directory)
498 .map_err(|error| io_error(target_directory, error))?;
499 let canonical_project =
500 canonicalize_simplified(project_root).map_err(|error| io_error(project_root, error))?;
501 let packages = metadata
502 .packages
503 .iter()
504 .map(|package| (package.id.as_str(), package))
505 .collect::<BTreeMap<_, _>>();
506 let mut artifacts = Vec::new();
507 for (binary_id, suite) in &catalog.rust_suites {
508 if suite.binary.binary_id != *binary_id {
509 return Err(RustCompilerOrchestrationError::CargoOutput(format!(
510 "nextest suite key disagrees with binary identity {binary_id}"
511 )));
512 }
513 let package = packages
514 .get(suite.binary.package_id.as_str())
515 .ok_or_else(|| {
516 RustCompilerOrchestrationError::CargoOutput(format!(
517 "nextest binary {binary_id} names an unknown Cargo package {}",
518 suite.binary.package_id
519 ))
520 })?;
521 if package.name != suite.package_name {
522 return Err(RustCompilerOrchestrationError::CargoOutput(format!(
523 "nextest binary {binary_id} package name disagrees with Cargo metadata"
524 )));
525 }
526 let mut targets = package.targets.iter().filter(|target| {
527 target.name == suite.binary.binary_name
528 && target
529 .kind
530 .iter()
531 .any(|kind| kind == suite.binary.kind.as_str())
532 });
533 let target = targets.next().ok_or_else(|| {
534 RustCompilerOrchestrationError::CargoOutput(format!(
535 "nextest binary {binary_id} has no exact Cargo metadata target"
536 ))
537 })?;
538 if targets.next().is_some() {
539 return Err(RustCompilerOrchestrationError::CargoOutput(format!(
540 "nextest binary {binary_id} ambiguously matches Cargo metadata targets"
541 )));
542 }
543 let executable = canonicalize_simplified(suite.binary.binary_path.as_std_path())
544 .map_err(|error| io_error(suite.binary.binary_path.as_std_path(), error))?;
545 let executable_metadata =
546 fs::symlink_metadata(&executable).map_err(|error| io_error(&executable, error))?;
547 if !executable.starts_with(&canonical_target) || !executable_metadata.file_type().is_file()
548 {
549 return Err(RustCompilerOrchestrationError::CargoOutput(format!(
550 "nextest test artifact escaped the private target: {}",
551 executable.display()
552 )));
553 }
554 let source_path = canonicalize_simplified(&target.src_path)
555 .map_err(|error| io_error(&target.src_path, error))?;
556 if !source_path.starts_with(&canonical_project) {
557 return Err(RustCompilerOrchestrationError::CargoOutput(format!(
558 "nextest target source escaped the owned project: {}",
559 source_path.display()
560 )));
561 }
562 artifacts.push(RustCompilerTestArtifact {
563 executable,
564 package: package_identity(&package.manifest_path, &canonical_project)?,
565 target_name: target.name.clone(),
566 target_kinds: target.kind.clone(),
567 source_path,
568 test_harness: true,
569 });
570 }
571 artifacts.sort_by(|left, right| left.executable.cmp(&right.executable));
572 artifacts.dedup_by(|left, right| left.executable == right.executable);
573 Ok(artifacts)
574}
575
576fn io_error(path: &Path, error: impl std::fmt::Display) -> RustCompilerOrchestrationError {
577 RustCompilerOrchestrationError::Io {
578 path: path.to_path_buf(),
579 reason: error.to_string(),
580 }
581}
582
583fn epoch_ms() -> Result<i64, RustCompilerOrchestrationError> {
584 let millis = SystemTime::now()
585 .duration_since(UNIX_EPOCH)
586 .map_err(|error| RustCompilerOrchestrationError::Cargo(error.to_string()))?
587 .as_millis();
588 i64::try_from(millis).map_err(|error| RustCompilerOrchestrationError::Cargo(error.to_string()))
589}
590
591fn valid_run_id(value: &str) -> bool {
592 !value.is_empty()
593 && value != "."
594 && value != ".."
595 && value
596 .chars()
597 .all(|character| character.is_ascii_alphanumeric() || matches!(character, '-' | '_'))
598}
599
600fn ensure_directories(
601 root: &Path,
602 relative: &Path,
603) -> Result<PathBuf, RustCompilerOrchestrationError> {
604 if relative.is_absolute()
605 || relative
606 .components()
607 .any(|component| !matches!(component, Component::Normal(_)))
608 {
609 return Err(RustCompilerOrchestrationError::InvalidRequest(format!(
610 "unsafe storage path {}",
611 relative.display()
612 )));
613 }
614 let mut current = root.to_path_buf();
615 for component in relative.components() {
616 current.push(component.as_os_str());
617 match fs::symlink_metadata(¤t) {
618 Ok(metadata) if metadata.file_type().is_dir() => {}
619 Ok(_) => return Err(io_error(¤t, "expected a non-symlink directory")),
620 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
621 fs::create_dir(¤t).map_err(|error| io_error(¤t, error))?;
622 }
623 Err(error) => return Err(io_error(¤t, error)),
624 }
625 }
626 Ok(current)
627}
628
629fn regular_executable(path: &Path) -> Result<PathBuf, RustCompilerOrchestrationError> {
630 let path = canonicalize_simplified(path).map_err(|error| io_error(path, error))?;
631 let metadata = fs::symlink_metadata(&path).map_err(|error| io_error(&path, error))?;
632 if !metadata.file_type().is_file() {
633 return Err(io_error(&path, "expected a regular executable"));
634 }
635 Ok(path)
636}
637
638fn write_json_config<T: Serialize>(
639 path: &Path,
640 config: &T,
641) -> Result<(), RustCompilerOrchestrationError> {
642 let bytes = serde_json::to_vec(config)
643 .map_err(|error| RustCompilerOrchestrationError::InvalidRequest(error.to_string()))?;
644 let mut options = OpenOptions::new();
645 options.write(true).create_new(true);
646 #[cfg(unix)]
647 {
648 use std::os::unix::fs::OpenOptionsExt as _;
649 options.mode(0o600);
650 }
651 let mut file = options.open(path).map_err(|error| io_error(path, error))?;
652 file.write_all(&bytes)
653 .map_err(|error| io_error(path, error))?;
654 file.sync_all().map_err(|error| io_error(path, error))
655}
656
657pub fn publish_compiler_selection_attestation(
658 directory: &Path,
659 selection: &SelectedRustCompilerCompanion,
660) -> Result<PathBuf, RustCompilerOrchestrationError> {
661 if !fs::symlink_metadata(directory).is_ok_and(|metadata| metadata.file_type().is_dir()) {
662 return Err(io_error(
663 directory,
664 "compiler selection root is not a directory",
665 ));
666 }
667 let now = SystemTime::now()
668 .duration_since(UNIX_EPOCH)
669 .map_err(|error| RustCompilerOrchestrationError::Selection(error.to_string()))?
670 .as_nanos();
671 let stem = format!("selection-{}-{now}", std::process::id());
672 let partial = directory.join(format!(".{stem}.partial"));
673 let final_path = directory.join(format!("{stem}.json"));
674 let bytes = serde_json::to_vec(selection)
675 .map_err(|error| RustCompilerOrchestrationError::Selection(error.to_string()))?;
676 let mut options = OpenOptions::new();
677 options.write(true).create_new(true);
678 #[cfg(unix)]
679 {
680 use std::os::unix::fs::OpenOptionsExt as _;
681 options.mode(0o600);
682 }
683 let mut cleanup = RemoveFileOnDrop(Some(partial.clone()));
684 let mut file = options
685 .open(&partial)
686 .map_err(|error| io_error(&partial, error))?;
687 file.write_all(&bytes)
688 .map_err(|error| io_error(&partial, error))?;
689 file.sync_all().map_err(|error| io_error(&partial, error))?;
690 drop(file);
691 fs::rename(&partial, &final_path).map_err(|error| io_error(&final_path, error))?;
692 sync_directory(directory)?;
693 cleanup.0 = None;
694 Ok(final_path)
695}
696
697fn write_shared_runtime_source(directory: &Path) -> Result<(), RustCompilerOrchestrationError> {
698 let source = directory.join("runtime.rs");
699 let runtime = format!(
700 "{}\n{}",
701 SHARED_RUNTIME_TEMPLATE.replace("__SUPERCOV_MODULE__", "__supercov_shared_runtime"),
702 SHARED_RUNTIME_EXPORTS
703 );
704 let mut options = OpenOptions::new();
705 options.write(true).create_new(true);
706 #[cfg(unix)]
707 {
708 use std::os::unix::fs::OpenOptionsExt as _;
709 options.mode(0o600);
710 }
711 let mut file = options
712 .open(&source)
713 .map_err(|error| io_error(&source, error))?;
714 file.write_all(runtime.as_bytes())
715 .map_err(|error| io_error(&source, error))?;
716 file.sync_all().map_err(|error| io_error(&source, error))
717}
718
719fn shared_runtime_archive(directory: &Path) -> PathBuf {
720 #[cfg(windows)]
721 let name = "supercov_runtime.lib";
722 #[cfg(not(windows))]
723 let name = "libsupercov_runtime.a";
724 directory.join(name)
725}
726
727fn valid_shared_runtime_archive(path: &Path) -> bool {
728 fs::symlink_metadata(path)
729 .is_ok_and(|metadata| metadata.file_type().is_file() && metadata.len() != 0)
730}
731
732#[cfg(unix)]
733fn sync_directory(path: &Path) -> Result<(), RustCompilerOrchestrationError> {
734 let directory = OpenOptions::new()
735 .read(true)
736 .open(path)
737 .map_err(|error| io_error(path, error))?;
738 directory.sync_all().map_err(|error| io_error(path, error))
739}
740
741#[cfg(not(unix))]
742fn sync_directory(_path: &Path) -> Result<(), RustCompilerOrchestrationError> {
743 Ok(())
744}
745
746struct RemoveFileOnDrop(Option<PathBuf>);
747
748impl Drop for RemoveFileOnDrop {
749 fn drop(&mut self) {
750 if let Some(path) = self.0.take() {
751 let _ = fs::remove_file(path);
752 }
753 }
754}
755
756enum SharedRuntimeBuildFault {
757 None,
758 #[cfg(test)]
759 NoSpaceAfterCompile,
760 #[cfg(all(test, unix))]
762 WaitAfterLock {
763 ready: PathBuf,
764 },
765}
766
767pub fn prepare_shared_rust_runtime(
772 rustc: &Path,
773 directory: &Path,
774) -> Result<PathBuf, RustCompilerOrchestrationError> {
775 prepare_shared_rust_runtime_with_fault(rustc, directory, SharedRuntimeBuildFault::None)
776}
777
778fn prepare_shared_rust_runtime_with_fault(
779 rustc: &Path,
780 directory: &Path,
781 _fault: SharedRuntimeBuildFault,
782) -> Result<PathBuf, RustCompilerOrchestrationError> {
783 let metadata = fs::symlink_metadata(directory).map_err(|error| io_error(directory, error))?;
784 if !metadata.file_type().is_dir() {
785 return Err(io_error(
786 directory,
787 "shared Rust runtime root is not a directory",
788 ));
789 }
790 let source = directory.join("runtime.rs");
791 if !fs::symlink_metadata(&source).is_ok_and(|metadata| metadata.file_type().is_file()) {
792 return Err(io_error(
793 &source,
794 "shared Rust runtime source is not a regular file",
795 ));
796 }
797 let archive = shared_runtime_archive(directory);
798 if valid_shared_runtime_archive(&archive) {
799 return Ok(archive);
800 }
801 let lock = directory.join("build.lock");
802 let started = Instant::now();
803 loop {
804 let mut options = OpenOptions::new();
805 options.read(true).write(true).create(true);
806 #[cfg(unix)]
807 {
808 use std::os::unix::fs::OpenOptionsExt as _;
809 options.mode(0o600);
810 }
811 let mut lock_file = options
812 .open(&lock)
813 .map_err(|error| io_error(&lock, error))?;
814 match lock_file.try_lock() {
815 Ok(()) => {
816 if valid_shared_runtime_archive(&archive) {
817 return Ok(archive);
818 }
819 lock_file
820 .set_len(0)
821 .and_then(|()| writeln!(lock_file, "{}", std::process::id()))
822 .and_then(|()| lock_file.sync_all())
823 .map_err(|error| io_error(&lock, error))?;
824 #[cfg(all(test, unix))]
825 if let SharedRuntimeBuildFault::WaitAfterLock { ready } = &_fault {
826 fs::write(ready, b"locked\n").map_err(|error| io_error(ready, error))?;
827 loop {
828 thread::sleep(Duration::from_secs(1));
829 }
830 }
831 let partial = directory.join(format!(
832 ".supercov-runtime-{}-{}.partial",
833 std::process::id(),
834 SystemTime::now()
835 .duration_since(UNIX_EPOCH)
836 .map_err(|error| RustCompilerOrchestrationError::Cargo(error.to_string()))?
837 .as_nanos()
838 ));
839 let mut partial_cleanup = RemoveFileOnDrop(Some(partial.clone()));
840 let output = Command::new(rustc)
841 .args([
842 "--edition=2024",
843 "--crate-name=supercov_runtime",
844 "--crate-type=staticlib",
845 "-o",
846 ])
847 .arg(&partial)
848 .arg(&source)
849 .env_remove("RUSTC_WRAPPER")
850 .env_remove("RUSTC_WORKSPACE_WRAPPER")
851 .env_remove(RUST_COMPILER_WRAPPER_CONFIG_ENV)
852 .env_remove(RUST_INSTRUMENT_MIR_ENV)
853 .env_remove(RUST_INSTRUMENT_CTFE_ENV)
854 .output()
855 .map_err(|error| io_error(rustc, error));
856 let result = match output {
857 Ok(output) if output.status.success() => {
858 #[cfg(test)]
859 if matches!(_fault, SharedRuntimeBuildFault::NoSpaceAfterCompile) {
860 return Err(io_error(
861 &partial,
862 io::Error::from_raw_os_error(libc::ENOSPC),
863 ));
864 }
865 let file = OpenOptions::new()
870 .read(true)
871 .write(true)
872 .open(&partial)
873 .map_err(|error| io_error(&partial, error))?;
874 file.sync_all().map_err(|error| io_error(&partial, error))?;
875 drop(file);
876 fs::rename(&partial, &archive)
877 .map_err(|error| io_error(&archive, error))?;
878 sync_directory(directory)?;
879 partial_cleanup.0 = None;
880 Ok(archive.clone())
881 }
882 Ok(output) => Err(RustCompilerOrchestrationError::Cargo(format!(
883 "exact rustc could not compile the shared Supercov runtime: {}{}",
884 String::from_utf8_lossy(&output.stderr),
885 String::from_utf8_lossy(&output.stdout)
886 ))),
887 Err(error) => Err(error),
888 };
889 return result;
890 }
891 Err(fs::TryLockError::WouldBlock) => {
892 if valid_shared_runtime_archive(&archive) {
893 return Ok(archive);
894 }
895 if started.elapsed() >= Duration::from_secs(30) {
896 return Err(io_error(
897 &lock,
898 "timed out waiting for the exact shared Rust runtime build",
899 ));
900 }
901 thread::sleep(Duration::from_millis(10));
902 }
903 Err(fs::TryLockError::Error(error)) => return Err(io_error(&lock, error)),
904 }
905 }
906}
907
908fn compiler_candidates(
909 directory: &Path,
910) -> Result<crate::rust_doctest::RustdocResolvedCandidates, RustCompilerOrchestrationError> {
911 let mut manifests = BTreeMap::<String, PathBuf>::new();
912 let mut snapshots = BTreeMap::<String, PathBuf>::new();
913 let mut merged_maps = Vec::new();
914 let entries = fs::read_dir(directory)
915 .map_err(|error| io_error(directory, error))?
916 .collect::<Result<Vec<_>, _>>()
917 .map_err(|error| io_error(directory, error))?;
918 for entry in entries {
919 let path = entry.path();
920 let metadata = entry.file_type().map_err(|error| io_error(&path, error))?;
921 if !metadata.is_file() {
922 return Err(RustCompilerOrchestrationError::CompilerOutput(format!(
923 "compiler output contains a non-file entry: {}",
924 path.display()
925 )));
926 }
927 let name = entry.file_name().into_string().map_err(|_| {
928 RustCompilerOrchestrationError::CompilerOutput(
929 "compiler output contains a non-UTF-8 name".into(),
930 )
931 })?;
932 if name.starts_with("doctest-map-") && name.ends_with(".json") {
933 merged_maps.push(fs::read(&path).map_err(|error| io_error(&path, error))?);
934 continue;
935 }
936 let destination = if let Some(key) = name
937 .strip_prefix("manifest-")
938 .and_then(|name| name.strip_suffix(".json"))
939 {
940 Some((&mut manifests, key))
941 } else {
942 name.strip_prefix("sources-")
943 .and_then(|name| name.strip_suffix(".json"))
944 .map(|key| (&mut snapshots, key))
945 };
946 if let Some((destination, key)) = destination
947 && destination.insert(key.into(), path.clone()).is_some()
948 {
949 return Err(RustCompilerOrchestrationError::CompilerOutput(format!(
950 "duplicate compiler output identity {key}"
951 )));
952 }
953 }
954 if manifests.is_empty() || manifests.keys().ne(snapshots.keys()) {
955 return Err(RustCompilerOrchestrationError::CompilerOutput(format!(
956 "manifest/source snapshot identities differ (manifests: {}, snapshots: {})",
957 manifests.len(),
958 snapshots.len()
959 )));
960 }
961 let pairs = manifests
962 .into_iter()
963 .map(|(key, manifest)| {
964 let snapshot = &snapshots[&key];
965 let manifest = fs::read(&manifest).map_err(|error| io_error(&manifest, error))?;
966 let snapshot = fs::read(snapshot).map_err(|error| io_error(snapshot, error))?;
967 Ok((manifest, snapshot))
968 })
969 .collect::<Result<Vec<_>, RustCompilerOrchestrationError>>()?;
970 resolve_merged_doctest_candidates(pairs, merged_maps)
971 .map_err(|error| RustCompilerOrchestrationError::Manifest(error.to_string()))
972}
973
974pub fn verified_compiler_selection(
975 directory: &Path,
976 candidates: &[PathBuf],
977 require_public_capabilities: bool,
978 allow_in_progress: bool,
979) -> Result<Option<SelectedRustCompilerCompanion>, RustCompilerOrchestrationError> {
980 let mut attestations = Vec::new();
981 let entries = fs::read_dir(directory)
982 .map_err(|error| io_error(directory, error))?
983 .collect::<Result<Vec<_>, _>>()
984 .map_err(|error| io_error(directory, error))?;
985 for entry in entries {
986 let path = entry.path();
987 let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
988 return Err(RustCompilerOrchestrationError::CompilerOutput(
989 "selection output contains a non-UTF-8 name".into(),
990 ));
991 };
992 if name.starts_with(".selection-") && name.ends_with(".partial") && allow_in_progress {
993 continue;
994 }
995 if !name.starts_with("selection-") || !name.ends_with(".json") {
996 return Err(RustCompilerOrchestrationError::CompilerOutput(format!(
997 "unexpected selection output {name}"
998 )));
999 }
1000 if !entry
1001 .file_type()
1002 .map_err(|error| io_error(&path, error))?
1003 .is_file()
1004 {
1005 return Err(RustCompilerOrchestrationError::CompilerOutput(format!(
1006 "selection output is not a regular file: {}",
1007 path.display()
1008 )));
1009 }
1010 let bytes = fs::read(&path).map_err(|error| io_error(&path, error))?;
1011 attestations.push(
1012 serde_json::from_slice::<SelectedRustCompilerCompanion>(&bytes).map_err(|error| {
1013 RustCompilerOrchestrationError::Selection(format!(
1014 "invalid wrapper attestation {}: {error}",
1015 path.display()
1016 ))
1017 })?,
1018 );
1019 }
1020 let Some(first) = attestations.first() else {
1021 return Ok(None);
1022 };
1023 if attestations.iter().any(|selection| selection != first) {
1024 return Err(RustCompilerOrchestrationError::Selection(
1025 "Cargo used more than one compiler identity or companion".into(),
1026 ));
1027 }
1028 let verified =
1029 select_rust_compiler_companion(&first.rustc_path, candidates, require_public_capabilities)
1030 .map_err(|error| RustCompilerOrchestrationError::Selection(error.to_string()))?;
1031 if &verified != first {
1032 return Err(RustCompilerOrchestrationError::Selection(
1033 "wrapper attestation changed during post-build verification".into(),
1034 ));
1035 }
1036 Ok(Some(verified))
1037}
1038
1039fn cargo_artifacts(
1040 stdout: &[u8],
1041 target_directory: &Path,
1042 project_root: &Path,
1043) -> Result<Vec<RustCompilerTestArtifact>, RustCompilerOrchestrationError> {
1044 let canonical_target = canonicalize_simplified(target_directory)
1045 .map_err(|error| io_error(target_directory, error))?;
1046 let canonical_project =
1047 canonicalize_simplified(project_root).map_err(|error| io_error(project_root, error))?;
1048 let mut artifacts = Vec::new();
1049 for line in stdout
1050 .split(|byte| *byte == b'\n')
1051 .filter(|line| !line.is_empty())
1052 {
1053 let message: CargoMessage = serde_json::from_slice(line)
1054 .map_err(|error| RustCompilerOrchestrationError::CargoOutput(error.to_string()))?;
1055 if message.reason != "compiler-artifact"
1056 || !message.profile.as_ref().is_some_and(|profile| profile.test)
1057 {
1058 continue;
1059 }
1060 let (Some(executable), Some(manifest_path), Some(target)) =
1061 (message.executable, message.manifest_path, message.target)
1062 else {
1063 continue;
1064 };
1065 let executable =
1066 canonicalize_simplified(&executable).map_err(|error| io_error(&executable, error))?;
1067 let metadata =
1068 fs::symlink_metadata(&executable).map_err(|error| io_error(&executable, error))?;
1069 if !executable.starts_with(&canonical_target) || !metadata.file_type().is_file() {
1070 return Err(RustCompilerOrchestrationError::CargoOutput(format!(
1071 "test artifact escaped the private target: {}",
1072 executable.display()
1073 )));
1074 }
1075 let manifest_metadata = fs::symlink_metadata(&manifest_path)
1076 .map_err(|error| io_error(&manifest_path, error))?;
1077 let manifest = canonicalize_simplified(&manifest_path)
1078 .map_err(|error| io_error(&manifest_path, error))?;
1079 let package_root = manifest
1080 .parent()
1081 .and_then(|path| path.strip_prefix(&canonical_project).ok())
1082 .filter(|_| {
1083 manifest_metadata.file_type().is_file()
1084 && manifest
1085 .file_name()
1086 .is_some_and(|name| name == "Cargo.toml")
1087 })
1088 .ok_or_else(|| {
1089 RustCompilerOrchestrationError::CargoOutput(format!(
1090 "test artifact manifest escaped the owned project: {}",
1091 manifest.display()
1092 ))
1093 })?;
1094 let package = if package_root.as_os_str().is_empty() {
1095 "package:.".to_owned()
1096 } else if package_root
1097 .components()
1098 .all(|component| matches!(component, std::path::Component::Normal(_)))
1099 {
1100 format!(
1101 "package:{}",
1102 package_root.to_string_lossy().replace('\\', "/")
1103 )
1104 } else {
1105 return Err(RustCompilerOrchestrationError::CargoOutput(format!(
1106 "test artifact has a noncanonical package root: {}",
1107 package_root.display()
1108 )));
1109 };
1110 let test_harness = cargo_target_uses_test_harness(&manifest, &target)?;
1111 artifacts.push(RustCompilerTestArtifact {
1112 executable,
1113 package,
1114 target_name: target.name,
1115 target_kinds: target.kind,
1116 source_path: target.src_path,
1117 test_harness,
1118 });
1119 }
1120 artifacts.sort_by(|left, right| left.executable.cmp(&right.executable));
1121 artifacts.dedup_by(|left, right| left.executable == right.executable);
1122 if artifacts.is_empty() {
1123 return Err(RustCompilerOrchestrationError::CargoOutput(
1124 "Cargo emitted no executable test artifacts".into(),
1125 ));
1126 }
1127 Ok(artifacts)
1128}
1129
1130fn cargo_target_uses_test_harness(
1131 manifest: &Path,
1132 target: &CargoTarget,
1133) -> Result<bool, RustCompilerOrchestrationError> {
1134 let source = fs::read_to_string(manifest).map_err(|error| io_error(manifest, error))?;
1135 let document = toml::from_str::<toml::Value>(&source).map_err(|error| {
1136 RustCompilerOrchestrationError::CargoOutput(format!(
1137 "cannot classify test harness from {}: {error}",
1138 manifest.display()
1139 ))
1140 })?;
1141 let kind = match target.kind.as_slice() {
1142 [kind] => kind.as_str(),
1143 _ => {
1144 return Err(RustCompilerOrchestrationError::CargoOutput(format!(
1145 "Cargo target {} has ambiguous target kinds: {}",
1146 target.name,
1147 target.kind.join(", ")
1148 )));
1149 }
1150 };
1151 let harness = match kind {
1152 "lib" | "proc-macro" => document
1153 .get("lib")
1154 .and_then(toml::Value::as_table)
1155 .and_then(|table| table.get("harness")),
1156 "bin" | "test" | "bench" | "example" => document
1157 .get(kind)
1158 .and_then(toml::Value::as_array)
1159 .and_then(|targets| {
1160 targets.iter().find_map(|candidate| {
1161 let table = candidate.as_table()?;
1162 (table.get("name")?.as_str()? == target.name)
1163 .then(|| table.get("harness"))
1164 .flatten()
1165 })
1166 }),
1167 _ => {
1168 return Err(RustCompilerOrchestrationError::CargoOutput(format!(
1169 "Cargo test artifact {} has unsupported target kind {kind}",
1170 target.name
1171 )));
1172 }
1173 };
1174 match harness {
1175 None => Ok(true),
1176 Some(value) => value.as_bool().ok_or_else(|| {
1177 RustCompilerOrchestrationError::CargoOutput(format!(
1178 "Cargo target {} has a non-Boolean harness setting in {}",
1179 target.name,
1180 manifest.display()
1181 ))
1182 }),
1183 }
1184}
1185
1186fn rendered_cargo_diagnostics(stdout: &[u8]) -> String {
1187 stdout
1188 .split(|byte| *byte == b'\n')
1189 .filter(|line| !line.is_empty())
1190 .filter_map(|line| serde_json::from_slice::<CargoMessage>(line).ok())
1191 .filter_map(|message| message.message.and_then(|message| message.rendered))
1192 .collect::<Vec<_>>()
1193 .join("")
1194}
1195
1196pub fn build_with_rust_compiler_companion(
1197 request: &RustCompilerBuildRequest,
1198) -> Result<RustCompilerBuild, RustCompilerOrchestrationError> {
1199 let supervisor = ProcessSupervisor::new()
1200 .map_err(|error| RustCompilerOrchestrationError::Cargo(error.to_string()))?;
1201 let options = SupervisionOptions::from_environment()
1202 .map_err(|error| RustCompilerOrchestrationError::Cargo(error.to_string()))?;
1203 build_with_rust_compiler_companion_supervised(request, &supervisor, options, &mut io::sink())
1204}
1205
1206pub fn build_with_rust_compiler_companion_supervised(
1207 request: &RustCompilerBuildRequest,
1208 supervisor: &ProcessSupervisor,
1209 options: SupervisionOptions,
1210 diagnostics: &mut dyn Write,
1211) -> Result<RustCompilerBuild, RustCompilerOrchestrationError> {
1212 if request.command.is_empty()
1213 || !valid_run_id(&request.run_id)
1214 || request.companion_candidates.is_empty()
1215 {
1216 return Err(RustCompilerOrchestrationError::InvalidRequest(
1217 "command, safe run ID and companion candidates are required".into(),
1218 ));
1219 }
1220 if std::env::var_os("RUSTDOC").is_some() {
1221 return Err(RustCompilerOrchestrationError::InvalidRequest(
1222 "an existing RUSTDOC executable cannot yet be composed without changing rustdoc semantics"
1223 .into(),
1224 ));
1225 }
1226 let project_root = canonicalize_simplified(&request.project_root)
1227 .map_err(|error| io_error(&request.project_root, error))?;
1228 if !fs::symlink_metadata(&project_root).is_ok_and(|metadata| metadata.file_type().is_dir()) {
1229 return Err(io_error(&project_root, "expected a project directory"));
1230 }
1231 let wrapper = regular_executable(&request.wrapper_path)?;
1232 let run_root = ensure_directories(
1233 &project_root,
1234 &PathBuf::from(".supercov/work").join(&request.run_id),
1235 )?;
1236 let compiler_output_directory = run_root.join("rust-compiler");
1237 fs::create_dir(&compiler_output_directory)
1238 .map_err(|error| io_error(&compiler_output_directory, error))?;
1239 let selection_directory = compiler_output_directory.join("selections");
1240 let candidate_directory = compiler_output_directory.join("candidates");
1241 fs::create_dir(&selection_directory).map_err(|error| io_error(&selection_directory, error))?;
1242 fs::create_dir(&candidate_directory).map_err(|error| io_error(&candidate_directory, error))?;
1243 let target_directory = run_root.join("rust-target");
1244 fs::create_dir(&target_directory).map_err(|error| io_error(&target_directory, error))?;
1245 let shared_runtime_directory = compiler_output_directory.join("shared-runtime");
1246 fs::create_dir(&shared_runtime_directory)
1247 .map_err(|error| io_error(&shared_runtime_directory, error))?;
1248 let cargo_runner_directory = compiler_output_directory.join("cargo-runner");
1249 fs::create_dir(&cargo_runner_directory)
1250 .map_err(|error| io_error(&cargo_runner_directory, error))?;
1251 write_shared_runtime_source(&shared_runtime_directory)?;
1252 let target_runners = request
1253 .cargo_runner_plan
1254 .targets
1255 .iter()
1256 .map(|target| target.resolve(&project_root))
1257 .collect::<Vec<_>>();
1258 let config_path = compiler_output_directory.join("wrapper.json");
1259 write_json_config(
1260 &config_path,
1261 &RustCompilerWrapperConfig {
1262 candidates: request.companion_candidates.clone(),
1263 require_public_capabilities: request.require_public_capabilities,
1264 selection_directory: selection_directory.clone(),
1265 shared_runtime_directory: shared_runtime_directory.clone(),
1266 target_runners: target_runners.clone(),
1267 project_root: project_root.clone(),
1268 compiler: request.cargo_runner_plan.compiler.clone(),
1269 original_wrapper_environment: RustCompilerWrapperEnvironment::capture(),
1270 },
1271 )?;
1272 let cargo_runner_list_config_path = compiler_output_directory.join("cargo-runner-list.json");
1273 write_json_config(
1274 &cargo_runner_list_config_path,
1275 &RustCargoRunnerConfig {
1276 version: RUST_CARGO_RUNNER_VERSION,
1277 run_id: request.run_id.clone(),
1278 target_directory: target_directory.clone(),
1279 output_directory: cargo_runner_directory.clone(),
1280 target_runners: target_runners.clone(),
1281 artifacts: Vec::new(),
1282 },
1283 )?;
1284 let cargo_runner_config_path = compiler_output_directory.join("cargo-runner.json");
1285
1286 let mut invocation = cargo_invocation(&project_root, &request.command)
1287 .map_err(|error| RustCompilerOrchestrationError::InvalidRequest(error.to_string()))?;
1288 let execution = rust_cargo_execution_selection(&invocation)
1289 .map_err(|error| RustCompilerOrchestrationError::InvalidRequest(error.to_string()))?;
1290 let command_kind = invocation.kind;
1291 let execution_arguments = invocation.arguments.clone();
1292 let build_started_at_ms = epoch_ms()?;
1293 let started = Instant::now();
1294 let (nextest_version, nextest_catalog, nextest_metadata) = if command_kind
1295 == RustCargoCommandKind::NextestRun
1296 {
1297 let version_output = supervisor
1298 .supervise_captured(
1299 &CommandSpec {
1300 program: invocation.program.clone().into(),
1301 arguments: nextest_version_arguments(&invocation)
1302 .map_err(|error| {
1303 RustCompilerOrchestrationError::InvalidRequest(error.to_string())
1304 })?
1305 .into_iter()
1306 .map(OsString::from)
1307 .collect(),
1308 cwd: project_root.clone(),
1309 environment: Some(inherited_environment([(
1310 OsString::from("CARGO_TARGET_DIR"),
1311 target_directory.clone().into_os_string(),
1312 )])),
1313 captured_output: None,
1314 },
1315 options,
1316 diagnostics,
1317 )
1318 .map_err(|error| RustCompilerOrchestrationError::Cargo(error.to_string()))?;
1319 if let Some(error) = interrupted_error(&version_output) {
1320 return Err(error);
1321 }
1322 if !supervised_success(&version_output) {
1323 return Err(RustCompilerOrchestrationError::Cargo(
1324 format!(
1325 "{}{}",
1326 String::from_utf8_lossy(&version_output.stderr),
1327 String::from_utf8_lossy(&version_output.stdout)
1328 )
1329 .trim()
1330 .to_owned(),
1331 ));
1332 }
1333 let nextest_version = parse_nextest_version_output(&version_output.stdout)
1334 .map_err(|error| RustCompilerOrchestrationError::CargoOutput(error.to_string()))?;
1335 let projected = nextest_list_invocation(&invocation)
1336 .map_err(|error| RustCompilerOrchestrationError::InvalidRequest(error.to_string()))?;
1337 let mut list_arguments = projected.arguments;
1338 list_arguments.extend(cargo_runner_configuration_arguments(
1339 &wrapper,
1340 &request.cargo_runner_plan,
1341 )?);
1342 if !projected.runner_arguments.is_empty() {
1343 list_arguments.push("--".into());
1344 list_arguments.extend(projected.runner_arguments);
1345 }
1346 let list_output = supervisor
1347 .supervise_captured(
1348 &CommandSpec {
1349 program: invocation.program.clone().into(),
1350 arguments: list_arguments.into_iter().map(OsString::from).collect(),
1351 cwd: project_root.clone(),
1352 environment: Some(inherited_environment([
1353 (
1354 OsString::from("CARGO_TARGET_DIR"),
1355 target_directory.clone().into_os_string(),
1356 ),
1357 (
1358 OsString::from("RUSTC_WRAPPER"),
1359 wrapper.clone().into_os_string(),
1360 ),
1361 (
1362 OsString::from("RUSTC_WORKSPACE_WRAPPER"),
1363 wrapper.clone().into_os_string(),
1364 ),
1365 (
1366 OsString::from(RUST_COMPILER_WRAPPER_CONFIG_ENV),
1367 config_path.clone().into_os_string(),
1368 ),
1369 (
1370 OsString::from(RUST_COMPILER_OUTPUT_ENV),
1371 candidate_directory.clone().into_os_string(),
1372 ),
1373 (
1374 OsString::from(RUST_SOURCE_ROOT_ENV),
1375 project_root.clone().into_os_string(),
1376 ),
1377 (
1378 OsString::from(RUST_TARGET_ROOT_ENV),
1379 target_directory.clone().into_os_string(),
1380 ),
1381 (OsString::from(RUST_INSTRUMENT_MIR_ENV), OsString::from("1")),
1382 (
1383 OsString::from(RUST_INSTRUMENT_CTFE_ENV),
1384 OsString::from("1"),
1385 ),
1386 (
1387 OsString::from(RUST_STATIC_RUNTIME_DIRECTORY_ENV),
1388 shared_runtime_directory.clone().into_os_string(),
1389 ),
1390 (
1391 OsString::from(RUST_CARGO_RUNNER_CONFIG_ENV),
1392 cargo_runner_list_config_path.clone().into_os_string(),
1393 ),
1394 ])),
1395 captured_output: None,
1396 },
1397 options,
1398 diagnostics,
1399 )
1400 .map_err(|error| RustCompilerOrchestrationError::Cargo(error.to_string()))?;
1401 if let Some(error) = interrupted_error(&list_output) {
1402 return Err(error);
1403 }
1404 if !supervised_success(&list_output) {
1405 return Err(RustCompilerOrchestrationError::Cargo(
1406 format!(
1407 "{}{}",
1408 String::from_utf8_lossy(&list_output.stderr),
1409 String::from_utf8_lossy(&list_output.stdout)
1410 )
1411 .trim()
1412 .to_owned(),
1413 ));
1414 }
1415 let catalog = TestListSummary::parse_json(String::from_utf8_lossy(&list_output.stdout))
1416 .map_err(|error| {
1417 RustCompilerOrchestrationError::CargoOutput(format!(
1418 "invalid nextest JSON test catalog: {error}"
1419 ))
1420 })?;
1421
1422 let metadata_output = supervisor
1423 .supervise_captured(
1424 &CommandSpec {
1425 program: invocation.program.clone().into(),
1426 arguments: cargo_metadata_arguments(&invocation)?
1427 .into_iter()
1428 .map(OsString::from)
1429 .collect(),
1430 cwd: project_root.clone(),
1431 environment: Some(inherited_environment([(
1432 OsString::from("CARGO_TARGET_DIR"),
1433 target_directory.clone().into_os_string(),
1434 )])),
1435 captured_output: None,
1436 },
1437 options,
1438 diagnostics,
1439 )
1440 .map_err(|error| RustCompilerOrchestrationError::Cargo(error.to_string()))?;
1441 if let Some(error) = interrupted_error(&metadata_output) {
1442 return Err(error);
1443 }
1444 if !supervised_success(&metadata_output) {
1445 return Err(RustCompilerOrchestrationError::Cargo(
1446 String::from_utf8_lossy(&metadata_output.stderr)
1447 .trim()
1448 .to_owned(),
1449 ));
1450 }
1451 let metadata = serde_json::from_slice(&metadata_output.stdout).map_err(|error| {
1452 RustCompilerOrchestrationError::CargoOutput(format!(
1453 "invalid Cargo metadata for nextest: {error}"
1454 ))
1455 })?;
1456 (Some(nextest_version), Some(catalog), Some(metadata))
1457 } else {
1458 (None, None, None)
1459 };
1460 let output = if execution.run_libtests && command_kind == RustCargoCommandKind::CargoTest {
1461 invocation.arguments.retain(|argument| {
1462 argument != "--no-run" && !argument.starts_with("--message-format=")
1463 });
1464 invocation
1465 .arguments
1466 .extend(["--no-run".into(), "--message-format=json".into()]);
1467 let environment = inherited_environment([
1468 (
1469 OsString::from("CARGO_TARGET_DIR"),
1470 target_directory.clone().into_os_string(),
1471 ),
1472 (
1473 OsString::from("RUSTC_WRAPPER"),
1474 wrapper.clone().into_os_string(),
1475 ),
1476 (
1477 OsString::from("RUSTC_WORKSPACE_WRAPPER"),
1478 wrapper.clone().into_os_string(),
1479 ),
1480 (
1481 OsString::from(RUST_COMPILER_WRAPPER_CONFIG_ENV),
1482 config_path.clone().into_os_string(),
1483 ),
1484 (
1485 OsString::from(RUST_COMPILER_OUTPUT_ENV),
1486 candidate_directory.clone().into_os_string(),
1487 ),
1488 (
1489 OsString::from(RUST_SOURCE_ROOT_ENV),
1490 project_root.clone().into_os_string(),
1491 ),
1492 (
1493 OsString::from(RUST_TARGET_ROOT_ENV),
1494 target_directory.clone().into_os_string(),
1495 ),
1496 (OsString::from(RUST_INSTRUMENT_MIR_ENV), OsString::from("1")),
1497 (
1498 OsString::from(RUST_INSTRUMENT_CTFE_ENV),
1499 OsString::from("1"),
1500 ),
1501 ]);
1502 let output = supervisor
1503 .supervise_captured(
1504 &CommandSpec {
1505 program: invocation.program.clone().into(),
1506 arguments: invocation.arguments.iter().map(OsString::from).collect(),
1507 cwd: project_root.clone(),
1508 environment: Some(environment),
1509 captured_output: None,
1510 },
1511 options,
1512 diagnostics,
1513 )
1514 .map_err(|error| RustCompilerOrchestrationError::Cargo(error.to_string()))?;
1515 if let Some(error) = interrupted_error(&output) {
1516 return Err(error);
1517 }
1518 if !supervised_success(&output) {
1519 let rendered = rendered_cargo_diagnostics(&output.stdout);
1520 let stderr = String::from_utf8_lossy(&output.stderr);
1521 return Err(RustCompilerOrchestrationError::Cargo(
1522 format!("{stderr}{rendered}").trim().to_owned(),
1523 ));
1524 }
1525 Some(output)
1526 } else {
1527 None
1528 };
1529 let cargo_test_artifacts = output
1530 .as_ref()
1531 .map(|output| cargo_artifacts(&output.stdout, &target_directory, &project_root))
1532 .transpose()?
1533 .unwrap_or_default();
1534 let planned_artifacts = match (&nextest_catalog, &nextest_metadata) {
1535 (Some(catalog), Some(metadata)) => {
1536 nextest_artifacts(catalog, metadata, &target_directory, &project_root)?
1537 }
1538 (None, None) => cargo_test_artifacts,
1539 _ => {
1540 return Err(RustCompilerOrchestrationError::CargoOutput(
1541 "nextest catalog and Cargo metadata were only partially captured".into(),
1542 ));
1543 }
1544 };
1545 write_json_config(
1546 &cargo_runner_config_path,
1547 &RustCargoRunnerConfig {
1548 version: RUST_CARGO_RUNNER_VERSION,
1549 run_id: request.run_id.clone(),
1550 target_directory: target_directory.clone(),
1551 output_directory: cargo_runner_directory.clone(),
1552 target_runners,
1553 artifacts: planned_artifacts
1554 .iter()
1555 .map(|artifact| RustCargoRunnerArtifact {
1556 executable: artifact.executable.clone(),
1557 test_harness: artifact.test_harness,
1558 })
1559 .collect(),
1560 },
1561 )?;
1562 let mut full_arguments = execution_arguments;
1563 full_arguments.extend(cargo_runner_configuration_arguments(
1564 &wrapper,
1565 &request.cargo_runner_plan,
1566 )?);
1567 if !invocation.runner_arguments.is_empty() {
1568 full_arguments.push("--".into());
1569 full_arguments.extend(invocation.runner_arguments.iter().cloned());
1570 }
1571 let execution_started = Instant::now();
1572 let execution_output = supervisor
1573 .supervise_captured(
1574 &CommandSpec {
1575 program: invocation.program.clone().into(),
1576 arguments: full_arguments.into_iter().map(OsString::from).collect(),
1577 cwd: project_root.clone(),
1578 environment: Some(inherited_environment([
1579 (
1580 OsString::from("CARGO_TARGET_DIR"),
1581 target_directory.clone().into_os_string(),
1582 ),
1583 (
1584 OsString::from("RUSTC_WRAPPER"),
1585 wrapper.clone().into_os_string(),
1586 ),
1587 (
1588 OsString::from("RUSTC_WORKSPACE_WRAPPER"),
1589 wrapper.clone().into_os_string(),
1590 ),
1591 (
1592 OsString::from(RUST_COMPILER_WRAPPER_CONFIG_ENV),
1593 config_path.clone().into_os_string(),
1594 ),
1595 (
1596 OsString::from(RUST_COMPILER_OUTPUT_ENV),
1597 candidate_directory.clone().into_os_string(),
1598 ),
1599 (
1600 OsString::from(RUST_SOURCE_ROOT_ENV),
1601 project_root.clone().into_os_string(),
1602 ),
1603 (
1604 OsString::from(RUST_TARGET_ROOT_ENV),
1605 target_directory.clone().into_os_string(),
1606 ),
1607 (OsString::from(RUST_INSTRUMENT_MIR_ENV), OsString::from("1")),
1608 (
1609 OsString::from(RUST_INSTRUMENT_CTFE_ENV),
1610 OsString::from("1"),
1611 ),
1612 (
1613 OsString::from(RUST_STATIC_RUNTIME_DIRECTORY_ENV),
1614 shared_runtime_directory.clone().into_os_string(),
1615 ),
1616 (OsString::from("RUSTDOC"), wrapper.clone().into_os_string()),
1617 (
1618 OsString::from(RUSTDOC_WRAPPER_MODE_ENV),
1619 OsString::from("1"),
1620 ),
1621 (
1622 OsString::from(RUST_CARGO_RUNNER_CONFIG_ENV),
1623 cargo_runner_config_path.clone().into_os_string(),
1624 ),
1625 ])),
1626 captured_output: None,
1627 },
1628 options,
1629 diagnostics,
1630 )
1631 .map_err(|error| RustCompilerOrchestrationError::Cargo(error.to_string()))?;
1632 if let Some(error) = interrupted_error(&execution_output) {
1633 return Err(error);
1634 }
1635 let execution_ms = execution_started.elapsed().as_secs_f64() * 1000.0;
1636 let build_ms = started.elapsed().as_secs_f64() * 1000.0;
1637 let build_ended_at_ms = epoch_ms()?;
1638 let execution_exit_code = execution_output.result.exit_code();
1639 let selection = verified_compiler_selection(
1640 &selection_directory,
1641 &request.companion_candidates,
1642 request.require_public_capabilities,
1643 false,
1644 )?
1645 .ok_or_else(|| {
1646 RustCompilerOrchestrationError::Selection(
1647 "Cargo invoked no authenticated compiler companion".into(),
1648 )
1649 })?;
1650 let resolved = compiler_candidates(&candidate_directory)?;
1651 let normalized = normalize_rust_compiler_candidates(resolved.candidates)
1652 .map_err(|error| RustCompilerOrchestrationError::Manifest(error.to_string()))?;
1653 let ctfe_units =
1654 read_rust_compiler_ctfe(&candidate_directory, &normalized, build_started_at_ms)
1655 .map_err(|error| RustCompilerOrchestrationError::CompilerOutput(error.to_string()))?;
1656 let doctest_outcomes = read_rustdoc_outcome_units(&candidate_directory)
1657 .map_err(|error| RustCompilerOrchestrationError::CompilerOutput(error.to_string()))?;
1658 if doctest_outcomes
1659 .iter()
1660 .any(|unit| unit.companion_build_id != selection.handshake.companion_build_id)
1661 {
1662 return Err(RustCompilerOrchestrationError::CompilerOutput(
1663 "rustdoc outcome unit was produced by a different compiler companion".into(),
1664 ));
1665 }
1666 let doctest_outcomes = join_rustdoc_outcomes(resolved.merged_units, doctest_outcomes)
1667 .map_err(|error| RustCompilerOrchestrationError::CompilerOutput(error.to_string()))?;
1668 let artifacts = planned_artifacts;
1669 let expected_targets = request
1670 .cargo_runner_plan
1671 .targets
1672 .iter()
1673 .map(|target| target.target.clone())
1674 .collect::<Vec<_>>();
1675 let cargo_runner_units =
1676 read_cargo_runner_units(&cargo_runner_directory, &request.run_id, &expected_targets)
1677 .map_err(|error| {
1678 let stderr = String::from_utf8_lossy(&execution_output.stderr);
1679 RustCompilerOrchestrationError::UnverifiedExecution {
1680 code: if execution_exit_code == 0 {
1681 2
1682 } else {
1683 execution_exit_code
1684 },
1685 reason: format!("{error}\n{stderr}").trim().to_owned(),
1686 }
1687 })?;
1688 Ok(RustCompilerBuild {
1689 selection,
1690 normalized,
1691 artifacts,
1692 target_directory,
1693 compiler_output_directory,
1694 ctfe_units,
1695 doctest_outcomes,
1696 cargo_runner_units,
1697 command_kind,
1698 nextest_version,
1699 nextest_catalog,
1700 run_libtests: execution.run_libtests,
1701 run_doctests: execution.run_doctests,
1702 execution_exit_code,
1703 execution_stdout: execution_output.stdout,
1704 execution_stderr: execution_output.stderr,
1705 build_started_at_ms,
1706 build_ended_at_ms,
1707 build_ms,
1708 execution_ms,
1709 })
1710}
1711
1712#[cfg(test)]
1713mod tests {
1714 use std::{
1715 collections::BTreeSet,
1716 sync::atomic::{AtomicU64, Ordering},
1717 };
1718
1719 use super::*;
1720
1721 struct TemporaryDirectory(PathBuf);
1722
1723 static TEMPORARY_DIRECTORY_NONCE: AtomicU64 = AtomicU64::new(0);
1724
1725 impl TemporaryDirectory {
1726 fn new() -> Self {
1727 let path = std::env::temp_dir().join(format!(
1728 "supercov-shared-rust-runtime-{}-{}-{}",
1729 std::process::id(),
1730 SystemTime::now()
1731 .duration_since(UNIX_EPOCH)
1732 .unwrap()
1733 .as_nanos(),
1734 TEMPORARY_DIRECTORY_NONCE.fetch_add(1, Ordering::Relaxed)
1735 ));
1736 fs::create_dir(&path).unwrap();
1737 Self(path)
1738 }
1739 }
1740
1741 impl Drop for TemporaryDirectory {
1742 fn drop(&mut self) {
1743 let _ = fs::remove_dir_all(&self.0);
1744 }
1745 }
1746
1747 #[cfg(unix)]
1748 #[test]
1749 fn compiler_wrapper_environment_round_trips_non_utf8_and_exact_absence() {
1750 use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
1751
1752 let original = OsString::from_vec(vec![b'w', 0xff, b'r']);
1753 let snapshot = RustCompilerWrapperEnvironment {
1754 rustc_wrapper: Some(RustCompilerEnvironmentValue::capture(&original)),
1755 rustc_workspace_wrapper: None,
1756 };
1757 assert_eq!(
1758 snapshot.rustc_wrapper.as_ref().unwrap().decode().unwrap(),
1759 original
1760 );
1761 assert!(
1762 RustCompilerEnvironmentValue::WindowsWide { value: vec![1] }
1763 .decode()
1764 .unwrap_err()
1765 .to_string()
1766 .contains("non-Windows")
1767 );
1768
1769 let mut command = Command::new("rustc");
1770 command
1771 .env("RUSTC_WRAPPER", "temporary")
1772 .env("RUSTC_WORKSPACE_WRAPPER", "temporary");
1773 snapshot.restore(&mut command).unwrap();
1774 let environment = command
1775 .get_envs()
1776 .map(|(key, value)| {
1777 (
1778 key.as_bytes().to_vec(),
1779 value.map(|value| value.as_bytes().to_vec()),
1780 )
1781 })
1782 .collect::<BTreeMap<_, _>>();
1783 assert_eq!(
1784 environment.get(b"RUSTC_WRAPPER".as_slice()),
1785 Some(&Some(vec![b'w', 0xff, b'r']))
1786 );
1787 assert_eq!(
1788 environment.get(b"RUSTC_WORKSPACE_WRAPPER".as_slice()),
1789 Some(&None)
1790 );
1791 }
1792
1793 #[test]
1794 fn exact_rustc_concurrently_publishes_one_shared_runtime_without_debris() {
1795 let directory = TemporaryDirectory::new();
1796 write_shared_runtime_source(&directory.0).unwrap();
1797 let archives = std::thread::scope(|scope| {
1798 (0..4)
1799 .map(|_| {
1800 scope.spawn(|| {
1801 prepare_shared_rust_runtime(Path::new("rustc"), &directory.0).unwrap()
1802 })
1803 })
1804 .collect::<Vec<_>>()
1805 .into_iter()
1806 .map(|thread| thread.join().unwrap())
1807 .collect::<Vec<_>>()
1808 });
1809 assert!(archives.windows(2).all(|pair| pair[0] == pair[1]));
1810 assert!(valid_shared_runtime_archive(&archives[0]));
1811 let names = fs::read_dir(&directory.0)
1812 .unwrap()
1813 .map(|entry| entry.unwrap().file_name().into_string().unwrap())
1814 .collect::<BTreeSet<_>>();
1815 assert_eq!(
1816 names,
1817 BTreeSet::from([
1818 "build.lock".into(),
1819 "runtime.rs".into(),
1820 archives[0]
1821 .file_name()
1822 .unwrap()
1823 .to_str()
1824 .unwrap()
1825 .to_owned()
1826 ])
1827 );
1828 }
1829
1830 #[test]
1831 fn failed_shared_runtime_builder_releases_lock_and_leaves_no_partial_archive() {
1832 let directory = TemporaryDirectory::new();
1833 write_shared_runtime_source(&directory.0).unwrap();
1834 let missing_rustc = directory.0.join("missing-rustc");
1835 let error = prepare_shared_rust_runtime(&missing_rustc, &directory.0).unwrap_err();
1836 assert!(error.to_string().contains("missing-rustc"));
1837 let names = fs::read_dir(&directory.0)
1838 .unwrap()
1839 .map(|entry| entry.unwrap().file_name().into_string().unwrap())
1840 .collect::<BTreeSet<_>>();
1841 assert_eq!(
1842 names,
1843 BTreeSet::from(["build.lock".into(), "runtime.rs".into()])
1844 );
1845 let recovery_started = Instant::now();
1846 let archive = prepare_shared_rust_runtime(Path::new("rustc"), &directory.0).unwrap();
1847 assert!(recovery_started.elapsed() < Duration::from_secs(5));
1848 assert!(valid_shared_runtime_archive(&archive));
1849 }
1850
1851 #[test]
1852 fn shared_runtime_enospc_is_recoverable_without_partial_archive() {
1853 let directory = TemporaryDirectory::new();
1854 write_shared_runtime_source(&directory.0).unwrap();
1855 let error = prepare_shared_rust_runtime_with_fault(
1856 Path::new("rustc"),
1857 &directory.0,
1858 SharedRuntimeBuildFault::NoSpaceAfterCompile,
1859 )
1860 .unwrap_err();
1861 assert!(matches!(
1862 error,
1863 RustCompilerOrchestrationError::Io { reason, .. }
1864 if reason == io::Error::from_raw_os_error(libc::ENOSPC).to_string()
1865 ));
1866 assert!(!valid_shared_runtime_archive(&shared_runtime_archive(
1867 &directory.0
1868 )));
1869 assert!(fs::read_dir(&directory.0).unwrap().all(|entry| {
1870 !entry
1871 .unwrap()
1872 .file_name()
1873 .to_string_lossy()
1874 .ends_with(".partial")
1875 }));
1876
1877 let archive = prepare_shared_rust_runtime(Path::new("rustc"), &directory.0).unwrap();
1878 assert!(valid_shared_runtime_archive(&archive));
1879 }
1880
1881 #[cfg(unix)]
1882 #[test]
1883 fn shared_runtime_lock_holder_helper() {
1884 let Some(directory) = std::env::var_os("SUPERCOV_TEST_RUNTIME_LOCK_DIRECTORY") else {
1885 return;
1886 };
1887 let ready = std::env::var_os("SUPERCOV_TEST_RUNTIME_LOCK_READY")
1888 .expect("runtime lock helper ready path");
1889 let _ = prepare_shared_rust_runtime_with_fault(
1890 Path::new("rustc"),
1891 Path::new(&directory),
1892 SharedRuntimeBuildFault::WaitAfterLock {
1893 ready: PathBuf::from(ready),
1894 },
1895 );
1896 }
1897
1898 #[cfg(unix)]
1899 #[test]
1900 fn killed_shared_runtime_builder_releases_lock_immediately() {
1901 use std::process::Stdio;
1902
1903 let directory = TemporaryDirectory::new();
1904 write_shared_runtime_source(&directory.0).unwrap();
1905 let ready = directory.0.join("builder-ready");
1906 let mut child = Command::new(std::env::current_exe().unwrap())
1907 .args([
1908 "--exact",
1909 "rust_compiler_orchestration::tests::shared_runtime_lock_holder_helper",
1910 "--nocapture",
1911 ])
1912 .env("SUPERCOV_TEST_RUNTIME_LOCK_DIRECTORY", &directory.0)
1913 .env("SUPERCOV_TEST_RUNTIME_LOCK_READY", &ready)
1914 .stdin(Stdio::null())
1915 .stdout(Stdio::null())
1916 .stderr(Stdio::null())
1917 .spawn()
1918 .unwrap();
1919 let wait_started = Instant::now();
1920 while !ready.is_file() {
1921 assert!(
1922 wait_started.elapsed() < Duration::from_secs(10),
1923 "runtime lock helper did not acquire its kernel lock"
1924 );
1925 thread::sleep(Duration::from_millis(10));
1926 }
1927 assert_eq!(
1928 unsafe { libc::kill(child.id().try_into().unwrap(), libc::SIGKILL) },
1929 0
1930 );
1931 let status = child.wait().unwrap();
1932 assert_eq!(status.code(), None);
1933 fs::remove_file(&ready).unwrap();
1934
1935 let recovery_started = Instant::now();
1936 let archive = prepare_shared_rust_runtime(Path::new("rustc"), &directory.0).unwrap();
1937 assert!(recovery_started.elapsed() < Duration::from_secs(5));
1938 assert!(valid_shared_runtime_archive(&archive));
1939 assert!(fs::read_dir(&directory.0).unwrap().all(|entry| {
1940 !entry
1941 .unwrap()
1942 .file_name()
1943 .to_string_lossy()
1944 .ends_with(".partial")
1945 }));
1946 }
1947
1948 #[test]
1949 fn cargo_artifacts_bind_relocatable_workspace_package_identity() {
1950 fn fixture(root: &Path) -> (PathBuf, Vec<u8>) {
1951 let target = root.join("target");
1952 fs::create_dir(&target).unwrap();
1953 let mut messages = Vec::new();
1954 for (index, package_root) in [Path::new("."), Path::new("crates/sibling")]
1955 .into_iter()
1956 .enumerate()
1957 {
1958 let package = root.join(package_root);
1959 let source = package.join("src/lib.rs");
1960 fs::create_dir_all(source.parent().unwrap()).unwrap();
1961 fs::write(
1962 package.join("Cargo.toml"),
1963 "[package]\nname='fixture'\nversion='0.0.0'\n",
1964 )
1965 .unwrap();
1966 fs::write(&source, "#[test] fn same_name() {}\n").unwrap();
1967 let executable = target.join(format!("same-target-{index}"));
1968 fs::write(&executable, b"artifact").unwrap();
1969 messages.extend(
1970 serde_json::to_vec(&serde_json::json!({
1971 "reason": "compiler-artifact",
1972 "package_id": format!("opaque-{index}"),
1973 "manifest_path": package.join("Cargo.toml"),
1974 "target": {
1975 "name": "same_target",
1976 "kind": ["lib"],
1977 "src_path": source,
1978 },
1979 "profile": { "test": true },
1980 "executable": executable,
1981 }))
1982 .unwrap(),
1983 );
1984 messages.push(b'\n');
1985 }
1986 (target, messages)
1987 }
1988
1989 let first = TemporaryDirectory::new();
1990 let second = TemporaryDirectory::new();
1991 let (first_target, first_messages) = fixture(&first.0);
1992 let (second_target, second_messages) = fixture(&second.0);
1993 let first_artifacts = cargo_artifacts(&first_messages, &first_target, &first.0).unwrap();
1994 let second_artifacts =
1995 cargo_artifacts(&second_messages, &second_target, &second.0).unwrap();
1996 assert_eq!(
1997 first_artifacts
1998 .iter()
1999 .map(|artifact| artifact.package.as_str())
2000 .collect::<BTreeSet<_>>(),
2001 BTreeSet::from(["package:.", "package:crates/sibling"])
2002 );
2003 assert_eq!(
2004 first_artifacts
2005 .iter()
2006 .map(|artifact| &artifact.package)
2007 .collect::<Vec<_>>(),
2008 second_artifacts
2009 .iter()
2010 .map(|artifact| &artifact.package)
2011 .collect::<Vec<_>>(),
2012 "package identities changed when the workspace moved"
2013 );
2014 }
2015
2016 #[test]
2017 fn cargo_manifest_classifies_only_the_selected_custom_harness() {
2018 let directory = TemporaryDirectory::new();
2019 let manifest = directory.0.join("Cargo.toml");
2020 fs::write(
2021 &manifest,
2022 r#"
2023[package]
2024name = "fixture"
2025version = "0.0.0"
2026
2027[lib]
2028harness = true
2029
2030[[test]]
2031name = "custom"
2032harness = false
2033
2034[[test]]
2035name = "ordinary"
2036"#,
2037 )
2038 .unwrap();
2039 let target = |name: &str, kind: &str| CargoTarget {
2040 name: name.into(),
2041 kind: vec![kind.into()],
2042 src_path: directory.0.join("unused.rs"),
2043 };
2044 assert!(cargo_target_uses_test_harness(&manifest, &target("fixture", "lib")).unwrap());
2045 assert!(!cargo_target_uses_test_harness(&manifest, &target("custom", "test")).unwrap());
2046 assert!(cargo_target_uses_test_harness(&manifest, &target("ordinary", "test")).unwrap());
2047 assert!(cargo_target_uses_test_harness(&manifest, &target("implicit", "test")).unwrap());
2048 }
2049
2050 #[test]
2051 fn cargo_runner_configuration_is_target_indexed_and_rejects_aliases() {
2052 use crate::rust_cargo_configuration::{
2053 RustCargoCompilerCommandPlan, RustCargoRunnerProgram, RustCargoTargetRunnerPlan,
2054 };
2055
2056 let plan = RustCargoRunnerPlan {
2057 compiler: RustCargoCompilerCommandPlan {
2058 rustc: RustCargoRunnerProgram::SearchPath {
2059 value: "rustc".into(),
2060 },
2061 rustc_wrapper: None,
2062 rustc_workspace_wrapper: None,
2063 },
2064 targets: vec![
2065 RustCargoTargetRunnerPlan {
2066 target: "aarch64-apple-darwin".into(),
2067 underlying_runner: None,
2068 },
2069 RustCargoTargetRunnerPlan {
2070 target: "x86_64-unknown-linux-gnu".into(),
2071 underlying_runner: None,
2072 },
2073 ],
2074 };
2075 assert_eq!(
2076 cargo_runner_configuration_arguments(Path::new("/opt/super cov"), &plan).unwrap(),
2077 [
2078 "--config",
2079 "target.\"aarch64-apple-darwin\".runner=[\"/opt/super cov\",\"__cargo-test-runner\",\"aarch64-apple-darwin\"]",
2080 "--config",
2081 "target.\"x86_64-unknown-linux-gnu\".runner=[\"/opt/super cov\",\"__cargo-test-runner\",\"x86_64-unknown-linux-gnu\"]",
2082 ]
2083 );
2084 let duplicate = RustCargoRunnerPlan {
2085 compiler: plan.compiler.clone(),
2086 targets: vec![plan.targets[0].clone(), plan.targets[0].clone()],
2087 };
2088 assert!(
2089 cargo_runner_configuration_arguments(Path::new("/opt/supercov"), &duplicate)
2090 .unwrap_err()
2091 .to_string()
2092 .contains("duplicate target identity")
2093 );
2094 }
2095}