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