Skip to main content

ferrum_native_ops_builder/
source_build.rs

1//! Locked, independently runnable native source-build plans.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::fs::{self, File, OpenOptions};
5use std::io::{Read, Write};
6#[cfg(unix)]
7use std::os::unix::fs::OpenOptionsExt;
8use std::path::{Path, PathBuf};
9use std::process::{Command, ExitStatus, Stdio};
10use std::time::{Instant, SystemTime, UNIX_EPOCH};
11
12use ferrum_native_ops::{
13    CudaNativeBuildUnit, NativeBuildArtifactCache, NativeBuildArtifactLookup,
14    NativeBuildArtifactSpec,
15};
16use ferrum_types::{is_sha256_digest, NativeOperatorBackend, NativeOperatorSourcePackage};
17use serde::{Deserialize, Serialize};
18use tempfile::NamedTempFile;
19
20use super::{
21    read_json, require_file, sha256_bytes, sha256_file, symbol_slug, validate_relative_path,
22    write_json, NativeOperatorBuilderError, NativeOperatorEvidenceFile, Result,
23};
24
25pub const NATIVE_OPERATOR_SOURCE_DEFINITION_SCHEMA_VERSION: u32 = 3;
26pub const NATIVE_OPERATOR_SOURCE_BUILD_PLAN_SCHEMA_VERSION: u32 = 3;
27pub const NATIVE_OPERATOR_SOURCE_BUILD_RECEIPT_SCHEMA_VERSION: u32 = 7;
28pub const NATIVE_OPERATOR_SOURCE_OBJECT_BUILD_CONTRACT_VERSION: u32 = 7;
29pub const NATIVE_OPERATOR_CUDA_TOOLKIT_MANIFEST_SCHEMA_VERSION: u32 = 1;
30pub const NATIVE_OPERATOR_HOST_TOOLCHAIN_MANIFEST_SCHEMA_VERSION: u32 = 2;
31pub const NATIVE_OPERATOR_OBJECT_DEPENDENCY_PROOF_SCHEMA_VERSION: u32 = 3;
32pub const MAX_NVCC_THREADS: u32 = 8;
33const REQUIRED_CUDA_TOOLKIT_FILES: [&str; 6] = [
34    "bin/bin2c",
35    "bin/cudafe++",
36    "bin/fatbinary",
37    "bin/nvcc",
38    "bin/nvlink",
39    "bin/ptxas",
40];
41const REQUIRED_CUDA_TOOLKIT_SCOPES: [&str; 4] =
42    ["bin/crt", "include", "nvvm/bin", "nvvm/libdevice"];
43const HOST_TOOLCHAIN_PROGRAMS: [&str; 5] = ["as", "cc1", "cc1plus", "collect2", "ld"];
44const MAX_HOST_TOOLCHAIN_FILES: usize = 250_000;
45const MAX_DEPFILE_BYTES: usize = 16 * 1024 * 1024;
46const MAX_DEPFILE_DEPENDENCIES: usize = 250_000;
47const MAX_DEPFILE_WORD_BYTES: usize = 16 * 1024;
48const MAX_DEPENDENCY_PROOF_BYTES: usize = 64 * 1024 * 1024;
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct NativeOperatorSourceDefinition {
52    pub schema_version: u32,
53    pub operator: String,
54    pub source_package_kind: String,
55    pub source_package_revision: String,
56    pub upstream_sources: Vec<NativeOperatorUpstreamSource>,
57    pub translation_units: Vec<String>,
58    pub headers: Vec<String>,
59    pub dependency_closures: Vec<NativeOperatorTranslationUnitDependencies>,
60    pub include_dirs: Vec<String>,
61    pub defines: Vec<String>,
62    pub nvcc_policy: NativeOperatorNvccPolicy,
63    pub architecture: NativeOperatorCudaArchitecture,
64    pub archive_file: String,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68pub struct NativeOperatorUpstreamSource {
69    pub repository: String,
70    pub revision: String,
71    pub license: String,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct NativeOperatorTranslationUnitDependencies {
76    pub translation_unit: String,
77    pub headers: Vec<String>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct NativeOperatorSourceBuildPlan {
82    pub schema_version: u32,
83    pub operator: String,
84    pub source_package: NativeOperatorSourcePackage,
85    pub upstream_sources: Vec<NativeOperatorUpstreamSource>,
86    pub translation_units: Vec<NativeOperatorSourceFileLock>,
87    pub headers: Vec<NativeOperatorSourceFileLock>,
88    pub dependency_closures: Vec<NativeOperatorTranslationUnitDependencyLock>,
89    pub include_dirs: Vec<String>,
90    pub defines: Vec<String>,
91    pub nvcc_policy: NativeOperatorNvccPolicy,
92    pub architecture: NativeOperatorCudaArchitecture,
93    pub archive_file: String,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
97pub struct NativeOperatorSourceFileLock {
98    pub path: String,
99    pub sha256: String,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct NativeOperatorTranslationUnitDependencyLock {
104    pub translation_unit: String,
105    pub headers: Vec<NativeOperatorSourceFileLock>,
106    pub closure_sha256: String,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110#[serde(rename_all = "snake_case")]
111pub enum NativeOperatorCudaArchitecture {
112    DeviceComputeCapability,
113    Compute80Ptx,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "snake_case")]
118pub enum NativeOperatorCppStandard {
119    Cpp17,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub enum NativeOperatorOptimization {
125    O3,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129pub struct NativeOperatorNvccPolicy {
130    pub cpp_standard: NativeOperatorCppStandard,
131    pub optimization: NativeOperatorOptimization,
132    pub use_fast_math: bool,
133    pub relaxed_constexpr: bool,
134    pub extended_lambda: bool,
135    pub host_position_independent_code: bool,
136    pub host_default_visibility: bool,
137}
138
139#[derive(Debug, Clone)]
140pub struct NativeOperatorSourceBuildRequest {
141    pub plan_path: PathBuf,
142    pub source_root: PathBuf,
143    pub output_dir: PathBuf,
144    pub compute_capability: String,
145    pub builder_sha: String,
146    pub nvcc_path: PathBuf,
147    pub ccbin_path: PathBuf,
148    pub ar_path: PathBuf,
149    pub cuda_toolkit_root: PathBuf,
150    pub nvcc_threads: u32,
151    pub object_cache_dir: PathBuf,
152    pub plan_only: bool,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156pub struct NativeOperatorSourceBuildReceipt {
157    pub schema_version: u32,
158    pub status: NativeOperatorSourceBuildStatus,
159    pub operator: String,
160    pub plan_only: bool,
161    pub plan_sha256: String,
162    pub source_package: NativeOperatorSourcePackage,
163    pub builder_sha: String,
164    pub compute_capability: String,
165    pub architecture_argument: String,
166    pub nvcc_threads: u32,
167    pub object_cache_root: String,
168    pub toolchain: Option<NativeOperatorSourceBuildToolchain>,
169    pub effective_environment: BTreeMap<String, String>,
170    pub inputs_sha256: String,
171    pub commands: Vec<NativeOperatorSourceBuildCommand>,
172    pub compiled_translation_units: Vec<String>,
173    pub cache_hit_translation_units: Vec<String>,
174    pub archive_file: Option<String>,
175    pub archive_sha256: Option<String>,
176    pub started_unix_ms: u64,
177    pub elapsed_ms: u64,
178    pub failure_class: Option<String>,
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
182#[serde(rename_all = "snake_case")]
183pub enum NativeOperatorSourceBuildStatus {
184    Plan,
185    Pass,
186    Reject,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190pub struct NativeOperatorSourceBuildToolchain {
191    pub static_identity: NativeOperatorSourceBuildStaticToolchain,
192    pub miss_probe: Option<NativeOperatorSourceBuildToolchainProbe>,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196pub struct NativeOperatorSourceBuildStaticToolchain {
197    pub backend: NativeOperatorBackend,
198    pub compiler_driver: NativeOperatorSourceCompilerDriver,
199    pub cuda_toolkit: NativeOperatorCudaToolkitIdentity,
200    pub host_toolchain: NativeOperatorHostToolchainIdentity,
201    pub archiver: NativeOperatorToolFileIdentity,
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
205#[serde(rename_all = "snake_case")]
206pub enum NativeOperatorSourceCompilerDriver {
207    CudaNvcc,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211pub struct NativeOperatorCudaToolkitIdentity {
212    pub canonical_root: String,
213    pub invocation_root: String,
214    pub release_version: String,
215    pub nvcc: NativeOperatorToolFileIdentity,
216    pub manifest: NativeOperatorEvidenceFile,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
220pub struct NativeOperatorToolFileIdentity {
221    pub path: String,
222    pub sha256: String,
223    pub size_bytes: u64,
224}
225
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227pub struct NativeOperatorHostToolchainIdentity {
228    pub compiler: NativeOperatorToolFileIdentity,
229    pub compiler_version: String,
230    pub target: String,
231    pub manifest: NativeOperatorEvidenceFile,
232}
233
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235pub struct NativeOperatorHostToolchainManifest {
236    pub schema_version: u32,
237    pub compiler: NativeOperatorToolFileIdentity,
238    pub compiler_version: String,
239    pub target: String,
240    pub executable_inputs: Vec<NativeOperatorToolFileIdentity>,
241    pub include_roots: Vec<String>,
242    pub include_probe_sha256: String,
243    pub driver_probe_sha256: String,
244    pub discovery_roots: Vec<String>,
245    pub files: Vec<NativeOperatorHostToolchainFileIdentity>,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
249pub struct NativeOperatorHostToolchainFileIdentity {
250    pub logical_path: String,
251    pub resolved_path: String,
252    pub sha256: String,
253    pub size_bytes: u64,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
257pub struct NativeOperatorSourceBuildToolchainProbe {
258    pub nvcc_version: String,
259    pub host_compiler_version: String,
260    pub host_target: String,
261    pub archiver_version: String,
262    pub probed_for_misses: Vec<String>,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266pub struct NativeOperatorCudaToolkitManifest {
267    pub schema_version: u32,
268    pub canonical_root: String,
269    pub entries: Vec<NativeOperatorCudaToolkitFileIdentity>,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273pub struct NativeOperatorCudaToolkitFileIdentity {
274    pub logical_path: String,
275    pub resolved_path: String,
276    pub sha256: String,
277    pub size_bytes: u64,
278}
279
280#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
281pub struct NativeOperatorToolIdentity {
282    pub path: String,
283    pub sha256: String,
284    pub version: String,
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
288pub struct NativeOperatorObjectIdentity {
289    pub format: NativeOperatorObjectFormat,
290    pub class_bits: u8,
291    pub endianness: NativeOperatorObjectEndianness,
292    pub machine: u32,
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
296#[serde(rename_all = "snake_case")]
297pub enum NativeOperatorObjectFormat {
298    Elf,
299    MachO,
300    Coff,
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
304#[serde(rename_all = "snake_case")]
305pub enum NativeOperatorObjectEndianness {
306    Little,
307    Big,
308}
309
310#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
311pub struct NativeOperatorSourceBuildCommand {
312    pub translation_unit: Option<String>,
313    pub working_directory: String,
314    pub argv: Vec<String>,
315    pub object_file: Option<String>,
316    pub stdout_log: String,
317    pub stderr_log: String,
318    pub object_cache_key: Option<String>,
319    pub object_cache_status: Option<NativeOperatorSourceObjectCacheStatus>,
320    pub object_cache_entry: Option<String>,
321    pub object_sha256: Option<String>,
322    pub object_size_bytes: Option<u64>,
323    pub object_identity: Option<NativeOperatorObjectIdentity>,
324    pub dependency_closure_sha256: Option<String>,
325    pub dependency_validation: Option<NativeOperatorDependencyValidation>,
326    pub compiler_depfile: Option<String>,
327    pub compiler_depfile_sha256: Option<String>,
328    pub depfile: Option<String>,
329    pub depfile_sha256: Option<String>,
330    pub depfile_producer_working_directory: Option<String>,
331    pub depfile_producer_object_file: Option<String>,
332    pub depfile_bindings: Vec<NativeOperatorDepfileDependencyBinding>,
333    pub observed_dependencies: Vec<NativeOperatorObservedDependency>,
334    pub compiler_executed: bool,
335    pub elapsed_ms: Option<u64>,
336    pub return_code: Option<i32>,
337}
338
339#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
340#[serde(rename_all = "snake_case")]
341pub enum NativeOperatorDependencyValidation {
342    Plan,
343    Pending,
344    CacheProof,
345    Depfile,
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
349#[serde(rename_all = "snake_case")]
350pub enum NativeOperatorDependencyDomain {
351    Source,
352    BackendToolchain,
353    HostToolchain,
354}
355
356#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
357pub struct NativeOperatorObservedDependency {
358    pub domain: NativeOperatorDependencyDomain,
359    pub path: String,
360    pub sha256: String,
361}
362
363#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
364pub struct NativeOperatorDepfileDependencyBinding {
365    pub producer_path: String,
366    pub portable_path: String,
367    pub dependency: NativeOperatorObservedDependency,
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
371pub struct NativeOperatorObjectDependencyProof {
372    pub schema_version: u32,
373    pub object_cache_key: String,
374    pub object_sha256: String,
375    pub dependency_closure_sha256: String,
376    pub dependency_set_sha256: String,
377    pub compiler_depfile_sha256: String,
378    pub depfile_sha256: String,
379    pub producer_working_directory: String,
380    pub producer_object_file: String,
381    pub depfile_bindings: Vec<NativeOperatorDepfileDependencyBinding>,
382    pub observed_dependencies: Vec<NativeOperatorObservedDependency>,
383}
384
385#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
386#[serde(rename_all = "snake_case")]
387pub enum NativeOperatorSourceObjectCacheStatus {
388    Plan,
389    Pending,
390    Hit,
391    Miss,
392    Published,
393    Rejected,
394}
395
396#[derive(Debug, Serialize)]
397struct NativeOperatorSourceInventoryIdentity<'a> {
398    operator: &'a str,
399    upstream_sources: &'a [NativeOperatorUpstreamSource],
400    translation_units: &'a [NativeOperatorSourceFileLock],
401    headers: &'a [NativeOperatorSourceFileLock],
402}
403
404#[derive(Debug, Serialize)]
405struct NativeOperatorDependencyClosureIdentity<'a> {
406    translation_unit: &'a NativeOperatorSourceFileLock,
407    headers: &'a [NativeOperatorSourceFileLock],
408}
409
410#[derive(Debug, Serialize)]
411struct NativeOperatorBuildInputIdentity<'a> {
412    plan_sha256: &'a str,
413    source_package_sha256: &'a str,
414    builder_contract_version: u32,
415    architecture_argument: &'a str,
416    effective_environment: &'a BTreeMap<String, String>,
417    toolchain: Option<&'a NativeOperatorSourceBuildStaticToolchain>,
418}
419
420#[derive(Debug, Serialize)]
421struct NativeOperatorObjectInputIdentity<'a> {
422    schema_version: u32,
423    operator: &'a str,
424    translation_unit: &'a NativeOperatorSourceFileLock,
425    dependency_closure_sha256: &'a str,
426    headers: &'a [NativeOperatorSourceFileLock],
427    include_dirs: &'a [String],
428    defines: &'a [String],
429    nvcc_policy: &'a NativeOperatorNvccPolicy,
430    architecture_argument: &'a str,
431    builder_contract_version: u32,
432    effective_environment: &'a BTreeMap<String, String>,
433    toolchain: &'a NativeOperatorSourceBuildStaticToolchain,
434}
435
436pub fn lock_native_operator_source_definition(
437    definition_path: &Path,
438    source_root: &Path,
439    output_plan_path: &Path,
440) -> Result<NativeOperatorSourceBuildPlan> {
441    require_file(definition_path)?;
442    if output_plan_path.exists() {
443        return Err(NativeOperatorBuilderError::OutputExists(
444            output_plan_path.to_path_buf(),
445        ));
446    }
447    let definition: NativeOperatorSourceDefinition = read_json(definition_path)?;
448    validate_definition(&definition)?;
449    let canonical_root =
450        source_root
451            .canonicalize()
452            .map_err(|source| NativeOperatorBuilderError::Io {
453                path: source_root.to_path_buf(),
454                source,
455            })?;
456    if !canonical_root.is_dir() {
457        return Err(NativeOperatorBuilderError::Invalid(format!(
458            "source_root is not a directory: {}",
459            canonical_root.display()
460        )));
461    }
462    for include_dir in &definition.include_dirs {
463        let path = canonical_root.join(include_dir);
464        let canonical = path
465            .canonicalize()
466            .map_err(|source| NativeOperatorBuilderError::Io {
467                path: path.clone(),
468                source,
469            })?;
470        if !canonical.starts_with(&canonical_root) || !canonical.is_dir() {
471            return Err(NativeOperatorBuilderError::Invalid(format!(
472                "include directory escapes source root or does not exist: {}",
473                path.display()
474            )));
475        }
476    }
477    let translation_units = lock_source_files(&canonical_root, &definition.translation_units)?;
478    let headers = lock_source_files(&canonical_root, &definition.headers)?;
479    let translation_unit_by_path = translation_units
480        .iter()
481        .map(|locked| (locked.path.as_str(), locked))
482        .collect::<BTreeMap<_, _>>();
483    let header_by_path = headers
484        .iter()
485        .map(|locked| (locked.path.as_str(), locked))
486        .collect::<BTreeMap<_, _>>();
487    let dependency_closures = definition
488        .dependency_closures
489        .iter()
490        .map(|closure| {
491            let translation_unit = translation_unit_by_path
492                .get(closure.translation_unit.as_str())
493                .copied()
494                .ok_or_else(|| {
495                    NativeOperatorBuilderError::Invalid(format!(
496                        "dependency closure references unknown translation unit: {}",
497                        closure.translation_unit
498                    ))
499                })?;
500            let locked_headers = closure
501                .headers
502                .iter()
503                .map(|path| {
504                    header_by_path
505                        .get(path.as_str())
506                        .copied()
507                        .cloned()
508                        .ok_or_else(|| {
509                            NativeOperatorBuilderError::Invalid(format!(
510                                "dependency closure for {} references unknown header: {path}",
511                                closure.translation_unit
512                            ))
513                        })
514                })
515                .collect::<Result<Vec<_>>>()?;
516            let closure_sha256 =
517                dependency_closure_sha256(translation_unit, &locked_headers, output_plan_path)?;
518            Ok(NativeOperatorTranslationUnitDependencyLock {
519                translation_unit: closure.translation_unit.clone(),
520                headers: locked_headers,
521                closure_sha256,
522            })
523        })
524        .collect::<Result<Vec<_>>>()?;
525    let inventory_identity = NativeOperatorSourceInventoryIdentity {
526        operator: &definition.operator,
527        upstream_sources: &definition.upstream_sources,
528        translation_units: &translation_units,
529        headers: &headers,
530    };
531    let source_package_sha256 =
532        sha256_bytes(&serde_json::to_vec(&inventory_identity).map_err(|source| {
533            NativeOperatorBuilderError::Json {
534                path: output_plan_path.to_path_buf(),
535                source,
536            }
537        })?);
538    let plan = NativeOperatorSourceBuildPlan {
539        schema_version: NATIVE_OPERATOR_SOURCE_BUILD_PLAN_SCHEMA_VERSION,
540        operator: definition.operator,
541        source_package: NativeOperatorSourcePackage {
542            kind: definition.source_package_kind,
543            revision: definition.source_package_revision,
544            sha256: source_package_sha256,
545        },
546        upstream_sources: definition.upstream_sources,
547        translation_units,
548        headers,
549        dependency_closures,
550        include_dirs: definition.include_dirs,
551        defines: definition.defines,
552        nvcc_policy: definition.nvcc_policy,
553        architecture: definition.architecture,
554        archive_file: definition.archive_file,
555    };
556    validate_plan(&plan)?;
557    if let Some(parent) = output_plan_path.parent() {
558        fs::create_dir_all(parent).map_err(|source| NativeOperatorBuilderError::Io {
559            path: parent.to_path_buf(),
560            source,
561        })?;
562    }
563    write_json(output_plan_path, &plan)?;
564    Ok(plan)
565}
566
567pub fn run_native_operator_source_build(
568    request: &NativeOperatorSourceBuildRequest,
569) -> Result<NativeOperatorSourceBuildReceipt> {
570    require_file(&request.plan_path)?;
571    if request.output_dir.exists() {
572        return Err(NativeOperatorBuilderError::OutputExists(
573            request.output_dir.clone(),
574        ));
575    }
576    if !request.output_dir.is_absolute() {
577        return Err(NativeOperatorBuilderError::Invalid(
578            "source build output_dir must be absolute".to_string(),
579        ));
580    }
581    if !request.object_cache_dir.is_absolute() {
582        return Err(NativeOperatorBuilderError::Invalid(
583            "source build object_cache_dir must be absolute".to_string(),
584        ));
585    }
586    if [
587        &request.nvcc_path,
588        &request.ccbin_path,
589        &request.ar_path,
590        &request.cuda_toolkit_root,
591    ]
592    .iter()
593    .any(|path| !path.is_absolute())
594    {
595        return Err(NativeOperatorBuilderError::Invalid(
596            "source build tool paths and cuda_toolkit_root must be absolute".to_string(),
597        ));
598    }
599    validate_compute_capability(&request.compute_capability)?;
600    if request.nvcc_threads == 0 || request.nvcc_threads > MAX_NVCC_THREADS {
601        return Err(NativeOperatorBuilderError::Invalid(format!(
602            "nvcc_threads must be in [1,{MAX_NVCC_THREADS}]"
603        )));
604    }
605    if !is_git_oid(&request.builder_sha) {
606        return Err(NativeOperatorBuilderError::Invalid(
607            "builder_sha must be a lowercase 40- or 64-hex git object id".to_string(),
608        ));
609    }
610    let plan: NativeOperatorSourceBuildPlan = read_json(&request.plan_path)?;
611    validate_plan(&plan)?;
612    let plan_sha256 = sha256_file(&request.plan_path)?;
613    let canonical_root =
614        request
615            .source_root
616            .canonicalize()
617            .map_err(|source| NativeOperatorBuilderError::Io {
618                path: request.source_root.clone(),
619                source,
620            })?;
621    validate_locked_source_tree(&canonical_root, &plan)?;
622    let architecture_argument =
623        architecture_argument(plan.architecture, &request.compute_capability);
624    let started_unix_ms = unix_ms();
625    let started = Instant::now();
626    fs::create_dir_all(&request.output_dir).map_err(|source| NativeOperatorBuilderError::Io {
627        path: request.output_dir.clone(),
628        source,
629    })?;
630    let receipt_path = request.output_dir.join("source-build.receipt.json");
631    let logs_dir = request.output_dir.join("logs");
632    let objects_dir = request.output_dir.join("objects");
633    let depfiles_dir = request.output_dir.join("depfiles");
634    fs::create_dir_all(&logs_dir).map_err(|source| NativeOperatorBuilderError::Io {
635        path: logs_dir.clone(),
636        source,
637    })?;
638    if !request.plan_only {
639        fs::create_dir_all(&objects_dir).map_err(|source| NativeOperatorBuilderError::Io {
640            path: objects_dir.clone(),
641            source,
642        })?;
643        fs::create_dir_all(&depfiles_dir).map_err(|source| NativeOperatorBuilderError::Io {
644            path: depfiles_dir.clone(),
645            source,
646        })?;
647    }
648
649    let (toolchain, toolchain_failure) = if request.plan_only {
650        (None, None)
651    } else {
652        match resolve_static_toolchain(request) {
653            Ok(toolchain) => (Some(toolchain), None),
654            Err(error) => (None, Some(error.to_string())),
655        }
656    };
657    let effective_environment = effective_build_environment(request, toolchain.as_ref())?;
658    let inputs_sha256 = build_inputs_sha256(
659        &plan_sha256,
660        &plan.source_package.sha256,
661        &architecture_argument,
662        &effective_environment,
663        toolchain
664            .as_ref()
665            .map(|toolchain| &toolchain.static_identity),
666        &receipt_path,
667    )?;
668    let mut commands = build_commands(
669        request,
670        &plan,
671        &canonical_root,
672        &architecture_argument,
673        &objects_dir,
674        &logs_dir,
675        toolchain.as_ref(),
676        &effective_environment,
677    );
678    let initial_log = if request.plan_only {
679        b"plan-only: command was not executed\n".as_slice()
680    } else {
681        b"pending: command has not executed\n".as_slice()
682    };
683    for command in &commands {
684        write_command_stream(
685            &request.output_dir.join(&command.stdout_log),
686            "stdout",
687            &command.argv,
688            initial_log,
689        )?;
690        write_command_stream(
691            &request.output_dir.join(&command.stderr_log),
692            "stderr",
693            &command.argv,
694            initial_log,
695        )?;
696    }
697    let mut receipt = NativeOperatorSourceBuildReceipt {
698        schema_version: NATIVE_OPERATOR_SOURCE_BUILD_RECEIPT_SCHEMA_VERSION,
699        status: if request.plan_only {
700            NativeOperatorSourceBuildStatus::Plan
701        } else {
702            NativeOperatorSourceBuildStatus::Reject
703        },
704        operator: plan.operator.clone(),
705        plan_only: request.plan_only,
706        plan_sha256,
707        source_package: plan.source_package.clone(),
708        builder_sha: request.builder_sha.clone(),
709        compute_capability: request.compute_capability.clone(),
710        architecture_argument,
711        nvcc_threads: request.nvcc_threads,
712        object_cache_root: request.object_cache_dir.display().to_string(),
713        toolchain,
714        effective_environment,
715        inputs_sha256,
716        commands: commands.clone(),
717        compiled_translation_units: Vec::new(),
718        cache_hit_translation_units: Vec::new(),
719        archive_file: None,
720        archive_sha256: None,
721        started_unix_ms,
722        elapsed_ms: 0,
723        failure_class: None,
724    };
725    if request.plan_only {
726        receipt.elapsed_ms = millis(started.elapsed());
727        write_json(&receipt_path, &receipt)?;
728        return Ok(receipt);
729    }
730    if let Some(error) = toolchain_failure {
731        receipt.elapsed_ms = millis(started.elapsed());
732        return reject_source_build(
733            &receipt_path,
734            &mut receipt,
735            format!("toolchain_preflight_failed:{error}"),
736        );
737    }
738    let toolchain_dependency_scope = match load_toolchain_dependency_scope(
739        &request.output_dir,
740        &receipt
741            .toolchain
742            .as_ref()
743            .expect("actual source build has a resolved toolchain")
744            .static_identity,
745    ) {
746        Ok(scope) => scope,
747        Err(error) => {
748            receipt.elapsed_ms = millis(started.elapsed());
749            return reject_source_build(
750                &receipt_path,
751                &mut receipt,
752                format!("toolchain_dependency_scope_failed:{error}"),
753            );
754        }
755    };
756    let object_cache = match NativeBuildArtifactCache::new(&request.object_cache_dir) {
757        Ok(cache) => cache,
758        Err(error) => {
759            receipt.elapsed_ms = millis(started.elapsed());
760            return reject_source_build(
761                &receipt_path,
762                &mut receipt,
763                format!("object_cache_init_failed:{error}"),
764            );
765        }
766    };
767    let object_specs = match build_object_cache_specs(
768        &plan,
769        &receipt.architecture_argument,
770        receipt
771            .toolchain
772            .as_ref()
773            .map(|toolchain| &toolchain.static_identity)
774            .expect("actual source build has a resolved toolchain"),
775        &receipt.effective_environment,
776    ) {
777        Ok(specs) => specs,
778        Err(error) => {
779            receipt.elapsed_ms = millis(started.elapsed());
780            return reject_source_build(
781                &receipt_path,
782                &mut receipt,
783                format!("object_cache_spec_failed:{error}"),
784            );
785        }
786    };
787    for (command, spec) in commands.iter_mut().zip(object_specs.iter()) {
788        command.object_cache_key = Some(spec.input_signature_sha256().to_string());
789    }
790    receipt.commands = commands.clone();
791    receipt.failure_class = Some("build_incomplete".to_string());
792    write_json(&receipt_path, &receipt)?;
793
794    let mut expected_object_identity: Option<NativeOperatorObjectIdentity> = None;
795    let mut miss_indices = Vec::new();
796    for index in 0..plan.translation_units.len() {
797        let translation_unit = &plan.translation_units[index];
798        let object_path = PathBuf::from(
799            commands[index]
800                .object_file
801                .as_deref()
802                .expect("translation-unit command has an object file"),
803        );
804        let lookup_started = Instant::now();
805        match object_cache.restore(&object_specs[index], &object_path) {
806            Ok(NativeBuildArtifactLookup::Hit(cache_receipt)) => {
807                let compiler_depfile_relative = commands[index]
808                    .compiler_depfile
809                    .as_deref()
810                    .expect("translation-unit command has compiler depfile output")
811                    .to_string();
812                let compiler_depfile_path = request.output_dir.join(&compiler_depfile_relative);
813                let depfile_relative = commands[index]
814                    .depfile
815                    .as_deref()
816                    .expect("translation-unit command has depfile output")
817                    .to_string();
818                let depfile_path = request.output_dir.join(&depfile_relative);
819                let dependency_proof = match restore_object_dependency_proof(
820                    &cache_receipt.cache_entry,
821                    object_specs[index].input_signature_sha256(),
822                    &cache_receipt.artifact_sha256,
823                    &plan.dependency_closures[index],
824                    translation_unit,
825                    &object_path,
826                    &compiler_depfile_path,
827                    &depfile_path,
828                    &toolchain_dependency_scope,
829                ) {
830                    Ok(Some(proof)) => proof,
831                    Ok(None) => {
832                        fs::remove_file(&object_path).map_err(|source| {
833                            NativeOperatorBuilderError::Io {
834                                path: object_path.clone(),
835                                source,
836                            }
837                        })?;
838                        commands[index].object_cache_status =
839                            Some(NativeOperatorSourceObjectCacheStatus::Miss);
840                        append_command_stream(
841                            &request.output_dir.join(&commands[index].stdout_log),
842                            b"object-cache-miss: dependency-proof-absent\n",
843                        )?;
844                        miss_indices.push(index);
845                        continue;
846                    }
847                    Err(error) => {
848                        commands[index].object_cache_status =
849                            Some(NativeOperatorSourceObjectCacheStatus::Rejected);
850                        receipt.commands = commands.clone();
851                        receipt.elapsed_ms = millis(started.elapsed());
852                        return reject_source_build(
853                            &receipt_path,
854                            &mut receipt,
855                            format!("cached_dependency_proof_failed:{index}:{error}"),
856                        );
857                    }
858                };
859                commands[index].object_cache_status =
860                    Some(NativeOperatorSourceObjectCacheStatus::Hit);
861                commands[index].object_cache_entry =
862                    Some(cache_receipt.cache_entry.display().to_string());
863                commands[index].object_sha256 = Some(cache_receipt.artifact_sha256);
864                let object_size_bytes = match native_object_size(&object_path) {
865                    Ok(size_bytes) => size_bytes,
866                    Err(error) => {
867                        commands[index].object_cache_status =
868                            Some(NativeOperatorSourceObjectCacheStatus::Rejected);
869                        receipt.commands = commands.clone();
870                        receipt.elapsed_ms = millis(started.elapsed());
871                        return reject_source_build(
872                            &receipt_path,
873                            &mut receipt,
874                            format!("cached_object_size_failed:{index}:{error}"),
875                        );
876                    }
877                };
878                commands[index].object_size_bytes = Some(object_size_bytes);
879                let object_identity = match native_object_identity_file(&object_path) {
880                    Ok(identity) => identity,
881                    Err(error) => {
882                        commands[index].object_cache_status =
883                            Some(NativeOperatorSourceObjectCacheStatus::Rejected);
884                        receipt.commands = commands.clone();
885                        receipt.elapsed_ms = millis(started.elapsed());
886                        return reject_source_build(
887                            &receipt_path,
888                            &mut receipt,
889                            format!("cached_object_identity_failed:{index}:{error}"),
890                        );
891                    }
892                };
893                if expected_object_identity
894                    .as_ref()
895                    .is_some_and(|expected| expected != &object_identity)
896                {
897                    commands[index].object_cache_status =
898                        Some(NativeOperatorSourceObjectCacheStatus::Rejected);
899                    receipt.commands = commands.clone();
900                    receipt.elapsed_ms = millis(started.elapsed());
901                    return reject_source_build(
902                        &receipt_path,
903                        &mut receipt,
904                        format!("cached_object_target_mismatch:{index}"),
905                    );
906                }
907                expected_object_identity.get_or_insert_with(|| object_identity.clone());
908                commands[index].object_identity = Some(object_identity);
909                commands[index].dependency_validation =
910                    Some(NativeOperatorDependencyValidation::CacheProof);
911                commands[index].compiler_depfile_sha256 =
912                    Some(dependency_proof.compiler_depfile_sha256);
913                commands[index].depfile_sha256 = Some(dependency_proof.depfile_sha256);
914                commands[index].depfile_producer_working_directory =
915                    Some(dependency_proof.producer_working_directory);
916                commands[index].depfile_producer_object_file =
917                    Some(dependency_proof.producer_object_file);
918                commands[index].depfile_bindings = dependency_proof.depfile_bindings;
919                commands[index].observed_dependencies = dependency_proof.observed_dependencies;
920                commands[index].elapsed_ms = Some(millis(lookup_started.elapsed()));
921                append_command_stream(
922                    &request.output_dir.join(&commands[index].stdout_log),
923                    b"object-cache-hit: compiler and toolchain probes were not executed\n",
924                )?;
925                receipt
926                    .cache_hit_translation_units
927                    .push(translation_unit.path.clone());
928            }
929            Ok(NativeBuildArtifactLookup::Miss { reason }) => {
930                commands[index].object_cache_status =
931                    Some(NativeOperatorSourceObjectCacheStatus::Miss);
932                append_command_stream(
933                    &request.output_dir.join(&commands[index].stdout_log),
934                    format!("object-cache-miss: {reason}\n").as_bytes(),
935                )?;
936                miss_indices.push(index);
937            }
938            Err(error) => {
939                commands[index].object_cache_status =
940                    Some(NativeOperatorSourceObjectCacheStatus::Rejected);
941                receipt.commands = commands.clone();
942                receipt.elapsed_ms = millis(started.elapsed());
943                return reject_source_build(
944                    &receipt_path,
945                    &mut receipt,
946                    format!("object_cache_restore_failed:{index}:{error}"),
947                );
948            }
949        }
950    }
951    receipt.commands = commands.clone();
952    receipt.elapsed_ms = millis(started.elapsed());
953    write_json(&receipt_path, &receipt)?;
954
955    if !miss_indices.is_empty() {
956        let missed_translation_units = miss_indices
957            .iter()
958            .map(|index| plan.translation_units[*index].path.clone())
959            .collect::<Vec<_>>();
960        let static_identity = receipt
961            .toolchain
962            .as_ref()
963            .expect("actual source build has a resolved toolchain")
964            .static_identity
965            .clone();
966        let probe = match probe_source_toolchain(&static_identity, missed_translation_units) {
967            Ok(probe) => probe,
968            Err(error) => {
969                receipt.commands = commands.clone();
970                receipt.elapsed_ms = millis(started.elapsed());
971                return reject_source_build(
972                    &receipt_path,
973                    &mut receipt,
974                    format!("toolchain_miss_probe_failed:{error}"),
975                );
976            }
977        };
978        receipt
979            .toolchain
980            .as_mut()
981            .expect("actual source build has a resolved toolchain")
982            .miss_probe = Some(probe);
983        write_json(&receipt_path, &receipt)?;
984    }
985
986    for index in miss_indices {
987        let translation_unit = &plan.translation_units[index];
988        let object_path = PathBuf::from(
989            commands[index]
990                .object_file
991                .as_deref()
992                .expect("translation-unit command has an object file"),
993        );
994        let command_started = Instant::now();
995        let stdout_path = request.output_dir.join(&commands[index].stdout_log);
996        let stderr_path = request.output_dir.join(&commands[index].stderr_log);
997        commands[index].compiler_executed = true;
998        let status = match run_logged_command(
999            &commands[index].argv,
1000            &stdout_path,
1001            &stderr_path,
1002            &commands[index].working_directory,
1003            &receipt.effective_environment,
1004        ) {
1005            Ok(status) => status,
1006            Err(error) => {
1007                append_command_stream(&stderr_path, format!("spawn failed: {error}\n").as_bytes())?;
1008                receipt.commands = commands.clone();
1009                receipt.elapsed_ms = millis(started.elapsed());
1010                return reject_source_build(
1011                    &receipt_path,
1012                    &mut receipt,
1013                    format!("nvcc_translation_unit_{index}_spawn_failed"),
1014                );
1015            }
1016        };
1017        commands[index].elapsed_ms = Some(millis(command_started.elapsed()));
1018        commands[index].return_code = status.code();
1019        receipt
1020            .compiled_translation_units
1021            .push(translation_unit.path.clone());
1022        receipt.commands = commands.clone();
1023        receipt.elapsed_ms = millis(started.elapsed());
1024        if !status.success() {
1025            return reject_source_build(
1026                &receipt_path,
1027                &mut receipt,
1028                format!("nvcc_translation_unit_{index}_failed"),
1029            );
1030        }
1031        if let Err(error) = require_file(&object_path) {
1032            commands[index].object_cache_status =
1033                Some(NativeOperatorSourceObjectCacheStatus::Rejected);
1034            receipt.commands = commands.clone();
1035            receipt.elapsed_ms = millis(started.elapsed());
1036            return reject_source_build(
1037                &receipt_path,
1038                &mut receipt,
1039                format!("compiled_object_missing:{index}:{error}"),
1040            );
1041        }
1042        let compiler_depfile_relative = commands[index]
1043            .compiler_depfile
1044            .as_deref()
1045            .expect("cache-miss command has a compiler depfile")
1046            .to_string();
1047        let compiler_depfile_path = request.output_dir.join(&compiler_depfile_relative);
1048        let depfile_relative = commands[index]
1049            .depfile
1050            .as_deref()
1051            .expect("cache-miss command has a portable depfile")
1052            .to_string();
1053        let depfile_path = request.output_dir.join(&depfile_relative);
1054        let validated_depfile = match validate_translation_unit_depfile(
1055            &compiler_depfile_path,
1056            &depfile_path,
1057            &object_path,
1058            &canonical_root,
1059            translation_unit,
1060            &plan.dependency_closures[index],
1061            &toolchain_dependency_scope,
1062        ) {
1063            Ok(dependencies) => dependencies,
1064            Err(error) => {
1065                commands[index].object_cache_status =
1066                    Some(NativeOperatorSourceObjectCacheStatus::Rejected);
1067                receipt.commands = commands.clone();
1068                receipt.elapsed_ms = millis(started.elapsed());
1069                return reject_source_build(
1070                    &receipt_path,
1071                    &mut receipt,
1072                    format!("dependency_validation_failed:{index}:{error}"),
1073                );
1074            }
1075        };
1076        commands[index].compiler_depfile_sha256 = Some(validated_depfile.compiler_sha256.clone());
1077        commands[index].depfile_sha256 = Some(validated_depfile.portable_sha256.clone());
1078        commands[index].depfile_producer_working_directory =
1079            Some(commands[index].working_directory.clone());
1080        commands[index].depfile_producer_object_file = commands[index].object_file.clone();
1081        commands[index].depfile_bindings = validated_depfile.bindings.clone();
1082        commands[index].observed_dependencies = validated_depfile.observed_dependencies.clone();
1083        commands[index].dependency_validation = Some(NativeOperatorDependencyValidation::Depfile);
1084        let object_identity = match native_object_identity_file(&object_path) {
1085            Ok(identity) => identity,
1086            Err(error) => {
1087                commands[index].object_cache_status =
1088                    Some(NativeOperatorSourceObjectCacheStatus::Rejected);
1089                receipt.commands = commands.clone();
1090                receipt.elapsed_ms = millis(started.elapsed());
1091                return reject_source_build(
1092                    &receipt_path,
1093                    &mut receipt,
1094                    format!("compiled_object_identity_failed:{index}:{error}"),
1095                );
1096            }
1097        };
1098        if expected_object_identity
1099            .as_ref()
1100            .is_some_and(|expected| expected != &object_identity)
1101        {
1102            commands[index].object_cache_status =
1103                Some(NativeOperatorSourceObjectCacheStatus::Rejected);
1104            receipt.commands = commands.clone();
1105            receipt.elapsed_ms = millis(started.elapsed());
1106            return reject_source_build(
1107                &receipt_path,
1108                &mut receipt,
1109                format!("compiled_object_target_mismatch:{index}"),
1110            );
1111        }
1112        expected_object_identity.get_or_insert_with(|| object_identity.clone());
1113        commands[index].object_identity = Some(object_identity);
1114        let object_size_bytes = match native_object_size(&object_path) {
1115            Ok(size_bytes) => size_bytes,
1116            Err(error) => {
1117                commands[index].object_cache_status =
1118                    Some(NativeOperatorSourceObjectCacheStatus::Rejected);
1119                receipt.commands = commands.clone();
1120                receipt.elapsed_ms = millis(started.elapsed());
1121                return reject_source_build(
1122                    &receipt_path,
1123                    &mut receipt,
1124                    format!("compiled_object_size_failed:{index}:{error}"),
1125                );
1126            }
1127        };
1128        commands[index].object_size_bytes = Some(object_size_bytes);
1129        let cache_receipt = match object_cache.publish(&object_specs[index], &object_path) {
1130            Ok(cache_receipt) => cache_receipt,
1131            Err(error) => {
1132                commands[index].object_cache_status =
1133                    Some(NativeOperatorSourceObjectCacheStatus::Rejected);
1134                receipt.commands = commands.clone();
1135                receipt.elapsed_ms = millis(started.elapsed());
1136                return reject_source_build(
1137                    &receipt_path,
1138                    &mut receipt,
1139                    format!("object_cache_publish_failed:{index}:{error}"),
1140                );
1141            }
1142        };
1143        if let Err(error) = publish_object_dependency_proof(
1144            &cache_receipt.cache_entry,
1145            object_specs[index].input_signature_sha256(),
1146            &cache_receipt.artifact_sha256,
1147            translation_unit,
1148            &plan.dependency_closures[index],
1149            &validated_depfile.compiler_raw,
1150            &validated_depfile.compiler_sha256,
1151            &validated_depfile.portable_raw,
1152            &validated_depfile.portable_sha256,
1153            commands[index]
1154                .depfile_producer_working_directory
1155                .as_deref()
1156                .expect("compiled depfile records its producer working directory"),
1157            commands[index]
1158                .depfile_producer_object_file
1159                .as_deref()
1160                .expect("compiled depfile records its producer object"),
1161            &commands[index].depfile_bindings,
1162            &commands[index].observed_dependencies,
1163            &toolchain_dependency_scope,
1164        ) {
1165            commands[index].object_cache_status =
1166                Some(NativeOperatorSourceObjectCacheStatus::Rejected);
1167            receipt.commands = commands.clone();
1168            receipt.elapsed_ms = millis(started.elapsed());
1169            return reject_source_build(
1170                &receipt_path,
1171                &mut receipt,
1172                format!("object_dependency_proof_publish_failed:{index}:{error}"),
1173            );
1174        }
1175        commands[index].object_cache_status =
1176            Some(NativeOperatorSourceObjectCacheStatus::Published);
1177        commands[index].object_cache_entry = Some(cache_receipt.cache_entry.display().to_string());
1178        commands[index].object_sha256 = Some(cache_receipt.artifact_sha256);
1179        receipt.commands = commands.clone();
1180        write_json(&receipt_path, &receipt)?;
1181    }
1182
1183    let archive_index = plan.translation_units.len();
1184    let archive_command = commands
1185        .get_mut(archive_index)
1186        .expect("archive command follows translation units");
1187    let archive_started = Instant::now();
1188    let archive_stdout_path = request.output_dir.join(&archive_command.stdout_log);
1189    let archive_stderr_path = request.output_dir.join(&archive_command.stderr_log);
1190    let archive_status = match run_logged_command(
1191        &archive_command.argv,
1192        &archive_stdout_path,
1193        &archive_stderr_path,
1194        &archive_command.working_directory,
1195        &receipt.effective_environment,
1196    ) {
1197        Ok(status) => status,
1198        Err(error) => {
1199            append_command_stream(
1200                &archive_stderr_path,
1201                format!("spawn failed: {error}\n").as_bytes(),
1202            )?;
1203            receipt.commands = commands.clone();
1204            receipt.elapsed_ms = millis(started.elapsed());
1205            return reject_source_build(
1206                &receipt_path,
1207                &mut receipt,
1208                "archive_spawn_failed".to_string(),
1209            );
1210        }
1211    };
1212    archive_command.elapsed_ms = Some(millis(archive_started.elapsed()));
1213    archive_command.return_code = archive_status.code();
1214    receipt.commands = commands;
1215    receipt.elapsed_ms = millis(started.elapsed());
1216    if !archive_status.success() {
1217        return reject_source_build(&receipt_path, &mut receipt, "archive_failed".to_string());
1218    }
1219
1220    let archive_path = request.output_dir.join(&plan.archive_file);
1221    if let Err(error) = require_file(&archive_path) {
1222        return reject_source_build(
1223            &receipt_path,
1224            &mut receipt,
1225            format!("archive_output_missing:{error}"),
1226        );
1227    }
1228    let archive_sha256 = match sha256_file(&archive_path) {
1229        Ok(sha256) => sha256,
1230        Err(error) => {
1231            return reject_source_build(
1232                &receipt_path,
1233                &mut receipt,
1234                format!("archive_output_hash_failed:{error}"),
1235            );
1236        }
1237    };
1238    let static_identity = receipt
1239        .toolchain
1240        .as_ref()
1241        .expect("actual source build has a resolved toolchain")
1242        .static_identity
1243        .clone();
1244    if let Err(error) = validate_tool_file_unchanged(&static_identity.archiver) {
1245        return reject_source_build(
1246            &receipt_path,
1247            &mut receipt,
1248            format!("archiver_changed_during_build:{error}"),
1249        );
1250    }
1251    if receipt
1252        .toolchain
1253        .as_ref()
1254        .is_some_and(|toolchain| toolchain.miss_probe.is_some())
1255    {
1256        if let Err(error) = validate_host_toolchain_unchanged(
1257            &static_identity.host_toolchain,
1258            &request.output_dir,
1259            &receipt.effective_environment,
1260        )
1261        .and_then(|()| validate_tool_file_unchanged(&static_identity.cuda_toolkit.nvcc))
1262        .and_then(|()| validate_cuda_toolkit_unchanged(&static_identity.cuda_toolkit))
1263        {
1264            return reject_source_build(
1265                &receipt_path,
1266                &mut receipt,
1267                format!("compiler_toolchain_changed_during_build:{error}"),
1268            );
1269        }
1270    }
1271    receipt.archive_sha256 = Some(archive_sha256);
1272    receipt.archive_file = Some(plan.archive_file);
1273    receipt.status = NativeOperatorSourceBuildStatus::Pass;
1274    receipt.failure_class = None;
1275    receipt.elapsed_ms = millis(started.elapsed());
1276    write_json(&receipt_path, &receipt)?;
1277    Ok(receipt)
1278}
1279
1280fn validate_definition(definition: &NativeOperatorSourceDefinition) -> Result<()> {
1281    if definition.schema_version != NATIVE_OPERATOR_SOURCE_DEFINITION_SCHEMA_VERSION {
1282        return Err(NativeOperatorBuilderError::Invalid(format!(
1283            "source definition schema_version must be {NATIVE_OPERATOR_SOURCE_DEFINITION_SCHEMA_VERSION}"
1284        )));
1285    }
1286    validate_common(
1287        &definition.operator,
1288        &definition.upstream_sources,
1289        &definition.translation_units,
1290        &definition.headers,
1291        &definition.include_dirs,
1292        &definition.defines,
1293        &definition.archive_file,
1294    )?;
1295    validate_definition_dependency_closures(
1296        &definition.translation_units,
1297        &definition.headers,
1298        &definition.dependency_closures,
1299    )?;
1300    if definition.source_package_kind.trim().is_empty()
1301        || definition.source_package_revision.trim().is_empty()
1302    {
1303        return Err(NativeOperatorBuilderError::Invalid(
1304            "source package kind and revision must be non-empty".to_string(),
1305        ));
1306    }
1307    Ok(())
1308}
1309
1310fn validate_plan(plan: &NativeOperatorSourceBuildPlan) -> Result<()> {
1311    if plan.schema_version != NATIVE_OPERATOR_SOURCE_BUILD_PLAN_SCHEMA_VERSION {
1312        return Err(NativeOperatorBuilderError::Invalid(format!(
1313            "source build plan schema_version must be {NATIVE_OPERATOR_SOURCE_BUILD_PLAN_SCHEMA_VERSION}"
1314        )));
1315    }
1316    let translation_units = plan
1317        .translation_units
1318        .iter()
1319        .map(|file| file.path.clone())
1320        .collect::<Vec<_>>();
1321    let headers = plan
1322        .headers
1323        .iter()
1324        .map(|file| file.path.clone())
1325        .collect::<Vec<_>>();
1326    validate_common(
1327        &plan.operator,
1328        &plan.upstream_sources,
1329        &translation_units,
1330        &headers,
1331        &plan.include_dirs,
1332        &plan.defines,
1333        &plan.archive_file,
1334    )?;
1335    if plan.source_package.kind.trim().is_empty()
1336        || plan.source_package.revision.trim().is_empty()
1337        || !is_sha256_digest(&plan.source_package.sha256)
1338    {
1339        return Err(NativeOperatorBuilderError::Invalid(
1340            "source_package kind/revision must be non-empty and sha256 must be lowercase"
1341                .to_string(),
1342        ));
1343    }
1344    for file in plan.translation_units.iter().chain(plan.headers.iter()) {
1345        if !is_sha256_digest(&file.sha256) {
1346            return Err(NativeOperatorBuilderError::Invalid(format!(
1347                "{} has an invalid sha256",
1348                file.path
1349            )));
1350        }
1351    }
1352    validate_plan_dependency_closures(plan)?;
1353    let identity = NativeOperatorSourceInventoryIdentity {
1354        operator: &plan.operator,
1355        upstream_sources: &plan.upstream_sources,
1356        translation_units: &plan.translation_units,
1357        headers: &plan.headers,
1358    };
1359    let actual = sha256_bytes(&serde_json::to_vec(&identity).map_err(|source| {
1360        NativeOperatorBuilderError::Json {
1361            path: PathBuf::from("<source-build-plan>"),
1362            source,
1363        }
1364    })?);
1365    if actual != plan.source_package.sha256 {
1366        return Err(NativeOperatorBuilderError::Invalid(format!(
1367            "source_package.sha256 differs from locked file inventory: expected={} actual={actual}",
1368            plan.source_package.sha256
1369        )));
1370    }
1371    Ok(())
1372}
1373
1374fn validate_definition_dependency_closures(
1375    translation_units: &[String],
1376    headers: &[String],
1377    closures: &[NativeOperatorTranslationUnitDependencies],
1378) -> Result<()> {
1379    let closure_translation_units = closures
1380        .iter()
1381        .map(|closure| closure.translation_unit.as_str())
1382        .collect::<Vec<_>>();
1383    let expected_translation_units = translation_units
1384        .iter()
1385        .map(String::as_str)
1386        .collect::<Vec<_>>();
1387    if closure_translation_units != expected_translation_units {
1388        return Err(NativeOperatorBuilderError::Invalid(
1389            "dependency_closures must contain exactly one row per translation unit in translation_units order"
1390                .to_string(),
1391        ));
1392    }
1393    let declared_headers = headers.iter().map(String::as_str).collect::<BTreeSet<_>>();
1394    let mut attached_headers = BTreeSet::new();
1395    for closure in closures {
1396        require_sorted_optional_paths(
1397            &format!("dependency_closures[{}].headers", closure.translation_unit),
1398            &closure.headers,
1399        )?;
1400        for header in &closure.headers {
1401            if !declared_headers.contains(header.as_str()) {
1402                return Err(NativeOperatorBuilderError::Invalid(format!(
1403                    "dependency closure for {} references undeclared header: {header}",
1404                    closure.translation_unit
1405                )));
1406            }
1407            attached_headers.insert(header.as_str());
1408        }
1409    }
1410    if attached_headers != declared_headers {
1411        let missing = declared_headers
1412            .difference(&attached_headers)
1413            .copied()
1414            .collect::<Vec<_>>()
1415            .join(",");
1416        return Err(NativeOperatorBuilderError::Invalid(format!(
1417            "every declared header must belong to at least one dependency closure; unattached={missing}"
1418        )));
1419    }
1420    Ok(())
1421}
1422
1423fn validate_plan_dependency_closures(plan: &NativeOperatorSourceBuildPlan) -> Result<()> {
1424    let translation_units = plan
1425        .translation_units
1426        .iter()
1427        .map(|file| file.path.clone())
1428        .collect::<Vec<_>>();
1429    let headers = plan
1430        .headers
1431        .iter()
1432        .map(|file| file.path.clone())
1433        .collect::<Vec<_>>();
1434    let closures = plan
1435        .dependency_closures
1436        .iter()
1437        .map(|closure| NativeOperatorTranslationUnitDependencies {
1438            translation_unit: closure.translation_unit.clone(),
1439            headers: closure
1440                .headers
1441                .iter()
1442                .map(|header| header.path.clone())
1443                .collect(),
1444        })
1445        .collect::<Vec<_>>();
1446    validate_definition_dependency_closures(&translation_units, &headers, &closures)?;
1447
1448    let translation_unit_by_path = plan
1449        .translation_units
1450        .iter()
1451        .map(|locked| (locked.path.as_str(), locked))
1452        .collect::<BTreeMap<_, _>>();
1453    let header_by_path = plan
1454        .headers
1455        .iter()
1456        .map(|locked| (locked.path.as_str(), locked))
1457        .collect::<BTreeMap<_, _>>();
1458    for closure in &plan.dependency_closures {
1459        let translation_unit = translation_unit_by_path
1460            .get(closure.translation_unit.as_str())
1461            .copied()
1462            .expect("definition-shaped closure already validated");
1463        for header in &closure.headers {
1464            let global = header_by_path
1465                .get(header.path.as_str())
1466                .copied()
1467                .expect("definition-shaped closure already validated");
1468            if global != header {
1469                return Err(NativeOperatorBuilderError::Invalid(format!(
1470                    "dependency closure header lock differs from the global lock: {}",
1471                    header.path
1472                )));
1473            }
1474        }
1475        if !is_sha256_digest(&closure.closure_sha256) {
1476            return Err(NativeOperatorBuilderError::Invalid(format!(
1477                "dependency closure for {} has an invalid sha256",
1478                closure.translation_unit
1479            )));
1480        }
1481        let actual = dependency_closure_sha256(
1482            translation_unit,
1483            &closure.headers,
1484            Path::new("<source-build-plan>"),
1485        )?;
1486        if actual != closure.closure_sha256 {
1487            return Err(NativeOperatorBuilderError::Invalid(format!(
1488                "dependency closure hash mismatch for {}: expected={} actual={actual}",
1489                closure.translation_unit, closure.closure_sha256
1490            )));
1491        }
1492    }
1493    Ok(())
1494}
1495
1496fn dependency_closure_sha256(
1497    translation_unit: &NativeOperatorSourceFileLock,
1498    headers: &[NativeOperatorSourceFileLock],
1499    context: &Path,
1500) -> Result<String> {
1501    let identity = NativeOperatorDependencyClosureIdentity {
1502        translation_unit,
1503        headers,
1504    };
1505    let bytes =
1506        serde_json::to_vec(&identity).map_err(|source| NativeOperatorBuilderError::Json {
1507            path: context.to_path_buf(),
1508            source,
1509        })?;
1510    Ok(sha256_bytes(&bytes))
1511}
1512
1513pub(crate) fn verify_source_build_receipt_against_plan(
1514    receipt: &NativeOperatorSourceBuildReceipt,
1515    receipt_root: &Path,
1516    plan_path: &Path,
1517    source_root: &Path,
1518) -> Result<NativeOperatorSourceBuildPlan> {
1519    let plan = verify_source_build_receipt_against_plan_portable(receipt, plan_path)?;
1520    verify_source_build_evidence(receipt, receipt_root, &plan)?;
1521    let canonical_source_root =
1522        source_root
1523            .canonicalize()
1524            .map_err(|source| NativeOperatorBuilderError::Io {
1525                path: source_root.to_path_buf(),
1526                source,
1527            })?;
1528    validate_locked_source_tree(&canonical_source_root, &plan)?;
1529    Ok(plan)
1530}
1531
1532pub(crate) fn verify_source_build_receipt_against_plan_portable(
1533    receipt: &NativeOperatorSourceBuildReceipt,
1534    plan_path: &Path,
1535) -> Result<NativeOperatorSourceBuildPlan> {
1536    require_file(plan_path)?;
1537    let plan: NativeOperatorSourceBuildPlan = read_json(plan_path)?;
1538    validate_plan(&plan)?;
1539    let plan_sha256 = sha256_file(plan_path)?;
1540    if receipt.plan_sha256 != plan_sha256 {
1541        return Err(NativeOperatorBuilderError::Invalid(format!(
1542            "{} source-build receipt plan_sha256 mismatch: expected={plan_sha256} actual={}",
1543            receipt.operator, receipt.plan_sha256
1544        )));
1545    }
1546    if receipt.operator != plan.operator || receipt.source_package != plan.source_package {
1547        return Err(NativeOperatorBuilderError::Invalid(format!(
1548            "{} source-build receipt does not match its locked plan identity",
1549            receipt.operator
1550        )));
1551    }
1552
1553    let expected_architecture =
1554        architecture_argument(plan.architecture, &receipt.compute_capability);
1555    if receipt.architecture_argument != expected_architecture {
1556        return Err(NativeOperatorBuilderError::Invalid(format!(
1557            "{} source-build architecture argument differs from its plan: expected={expected_architecture} actual={}",
1558            receipt.operator, receipt.architecture_argument
1559        )));
1560    }
1561    let toolchain = receipt.toolchain.as_ref().ok_or_else(|| {
1562        NativeOperatorBuilderError::Invalid(format!(
1563            "{} source-build receipt is missing toolchain provenance",
1564            receipt.operator
1565        ))
1566    })?;
1567    validate_static_toolchain_identity(&receipt.operator, &toolchain.static_identity)?;
1568    let static_identity = &toolchain.static_identity;
1569    let expected_environment = effective_environment_for_tool_paths([
1570        static_identity.cuda_toolkit.nvcc.path.as_str(),
1571        static_identity.host_toolchain.compiler.path.as_str(),
1572        static_identity.archiver.path.as_str(),
1573    ])?;
1574    if receipt.effective_environment != expected_environment {
1575        return Err(NativeOperatorBuilderError::Invalid(format!(
1576            "{} source-build effective environment differs from the deterministic policy",
1577            receipt.operator
1578        )));
1579    }
1580    let expected_inputs_sha256 = build_inputs_sha256(
1581        &plan_sha256,
1582        &plan.source_package.sha256,
1583        &expected_architecture,
1584        &expected_environment,
1585        Some(static_identity),
1586        plan_path,
1587    )?;
1588    if receipt.inputs_sha256 != expected_inputs_sha256 {
1589        return Err(NativeOperatorBuilderError::Invalid(format!(
1590            "{} source-build inputs_sha256 mismatch: expected={expected_inputs_sha256} actual={}",
1591            receipt.operator, receipt.inputs_sha256
1592        )));
1593    }
1594    let object_specs = build_object_cache_specs(
1595        &plan,
1596        &expected_architecture,
1597        static_identity,
1598        &expected_environment,
1599    )?;
1600    if receipt.commands.len() != plan.translation_units.len() + 1 {
1601        return Err(NativeOperatorBuilderError::Invalid(format!(
1602            "{} source-build command count differs from its plan",
1603            receipt.operator
1604        )));
1605    }
1606    for (index, ((translation_unit, object_spec), command)) in plan
1607        .translation_units
1608        .iter()
1609        .zip(object_specs.iter())
1610        .zip(receipt.commands.iter())
1611        .enumerate()
1612    {
1613        let expected_object_file = object_file_name(index, translation_unit);
1614        let closure = &plan.dependency_closures[index];
1615        if command.translation_unit.as_deref() != Some(translation_unit.path.as_str())
1616            || command.object_cache_key.as_deref() != Some(object_spec.input_signature_sha256())
1617            || command.dependency_closure_sha256.as_deref() != Some(closure.closure_sha256.as_str())
1618            || command
1619                .object_file
1620                .as_deref()
1621                .and_then(|path| Path::new(path).file_name())
1622                != Some(std::ffi::OsStr::new(&expected_object_file))
1623        {
1624            return Err(NativeOperatorBuilderError::Invalid(format!(
1625                "{} source-build object identity for {} differs from its plan",
1626                receipt.operator, translation_unit.path
1627            )));
1628        }
1629        let stem = Path::new(&translation_unit.path)
1630            .file_stem()
1631            .and_then(|value| value.to_str())
1632            .unwrap_or("translation_unit");
1633        let expected_depfile = format!("{index:08}-{stem}.d");
1634        let expected_compiler_depfile = format!("{index:08}-{stem}.compiler.raw.d");
1635        let mut expected_argv = vec![
1636            static_identity.cuda_toolkit.nvcc.path.clone(),
1637            "-c".to_string(),
1638            translation_unit.path.clone(),
1639            "-o".to_string(),
1640            expected_object_file.clone(),
1641            expected_architecture.clone(),
1642            "-ccbin".to_string(),
1643            static_identity.host_toolchain.compiler.path.clone(),
1644            "-MMD".to_string(),
1645            "-MF".to_string(),
1646            expected_compiler_depfile.clone(),
1647            "-MT".to_string(),
1648            expected_object_file.clone(),
1649        ];
1650        expected_argv.extend(plan.include_dirs.iter().map(|path| format!("-I{path}")));
1651        expected_argv.extend(plan.defines.iter().map(|define| format!("-D{define}")));
1652        expected_argv.extend(nvcc_policy_flags(&plan.nvcc_policy));
1653        expected_argv.push("--threads".to_string());
1654        expected_argv.push(receipt.nvcc_threads.to_string());
1655        let mut actual_argv = command.argv.clone();
1656        if let Some(output) = actual_argv.get_mut(4) {
1657            *output = Path::new(output)
1658                .file_name()
1659                .unwrap_or_default()
1660                .to_string_lossy()
1661                .into_owned();
1662        }
1663        for argument in [10_usize, 12] {
1664            if let Some(path) = actual_argv.get_mut(argument) {
1665                *path = Path::new(path)
1666                    .file_name()
1667                    .unwrap_or_default()
1668                    .to_string_lossy()
1669                    .into_owned();
1670            }
1671        }
1672        if actual_argv != expected_argv {
1673            return Err(NativeOperatorBuilderError::Invalid(format!(
1674                "{} source-build argv for {} differs from its locked plan",
1675                receipt.operator, translation_unit.path
1676            )));
1677        }
1678        validate_observed_dependencies(
1679            &format!("{}:{}", receipt.operator, translation_unit.path),
1680            &command.observed_dependencies,
1681        )?;
1682        let expected_source = expected_source_dependencies(translation_unit, closure);
1683        let observed_source = command
1684            .observed_dependencies
1685            .iter()
1686            .filter(|dependency| dependency.domain == NativeOperatorDependencyDomain::Source)
1687            .cloned()
1688            .collect::<BTreeSet<_>>();
1689        if observed_source != expected_source {
1690            return Err(NativeOperatorBuilderError::Invalid(format!(
1691                "{} source-build source dependency evidence for {} differs from its locked closure",
1692                receipt.operator, translation_unit.path
1693            )));
1694        }
1695        let expected_depfile_relative = format!("depfiles/{expected_depfile}");
1696        let expected_compiler_depfile_relative = format!("depfiles/{expected_compiler_depfile}");
1697        let binding_dependencies = validate_depfile_bindings_basic(
1698            &format!("{}:{}", receipt.operator, translation_unit.path),
1699            &command.depfile_bindings,
1700        )?;
1701        match command.object_cache_status {
1702            Some(NativeOperatorSourceObjectCacheStatus::Published)
1703                if command.dependency_validation
1704                    == Some(NativeOperatorDependencyValidation::Depfile)
1705                    && command.compiler_depfile.as_deref()
1706                        == Some(expected_compiler_depfile_relative.as_str())
1707                    && command
1708                        .compiler_depfile_sha256
1709                        .as_deref()
1710                        .is_some_and(is_sha256_digest)
1711                    && command.depfile.as_deref() == Some(expected_depfile_relative.as_str())
1712                    && command
1713                        .depfile_sha256
1714                        .as_deref()
1715                        .is_some_and(is_sha256_digest)
1716                    && command.depfile_producer_working_directory.as_deref()
1717                        == Some(command.working_directory.as_str())
1718                    && command.depfile_producer_object_file.as_deref()
1719                        == command.object_file.as_deref()
1720                    && binding_dependencies == command.observed_dependencies => {}
1721            Some(NativeOperatorSourceObjectCacheStatus::Hit)
1722                if command.dependency_validation
1723                    == Some(NativeOperatorDependencyValidation::CacheProof)
1724                    && command.compiler_depfile.as_deref()
1725                        == Some(expected_compiler_depfile_relative.as_str())
1726                    && command
1727                        .compiler_depfile_sha256
1728                        .as_deref()
1729                        .is_some_and(is_sha256_digest)
1730                    && command.depfile.as_deref() == Some(expected_depfile_relative.as_str())
1731                    && command
1732                        .depfile_sha256
1733                        .as_deref()
1734                        .is_some_and(is_sha256_digest)
1735                    && command
1736                        .depfile_producer_working_directory
1737                        .as_deref()
1738                        .is_some_and(|path| Path::new(path).is_absolute())
1739                    && command
1740                        .depfile_producer_object_file
1741                        .as_deref()
1742                        .is_some_and(|path| {
1743                            Path::new(path).is_absolute()
1744                                && Path::new(path).file_name()
1745                                    == Some(std::ffi::OsStr::new(&expected_object_file))
1746                        })
1747                    && binding_dependencies == command.observed_dependencies => {}
1748            _ => {
1749                return Err(NativeOperatorBuilderError::Invalid(format!(
1750                    "{} source-build dependency evidence for {} differs from its locked plan",
1751                    receipt.operator, translation_unit.path
1752                )))
1753            }
1754        }
1755    }
1756    match &toolchain.miss_probe {
1757        Some(probe)
1758            if probe.probed_for_misses == receipt.compiled_translation_units
1759                && !receipt.compiled_translation_units.is_empty()
1760                && !probe.nvcc_version.trim().is_empty()
1761                && probe.host_compiler_version
1762                    == static_identity.host_toolchain.compiler_version
1763                && !probe.archiver_version.trim().is_empty()
1764                && probe.host_target == static_identity.host_toolchain.target => {}
1765        None if receipt.compiled_translation_units.is_empty() => {}
1766        _ => {
1767            return Err(NativeOperatorBuilderError::Invalid(format!(
1768                "{} source-build miss-only toolchain probe differs from compiled translation units",
1769                receipt.operator
1770            )))
1771        }
1772    }
1773    let archive_command = receipt.commands.last().expect("command count checked");
1774    let actual_archive_argv = archive_command
1775        .argv
1776        .iter()
1777        .enumerate()
1778        .map(|(index, value)| {
1779            if index >= 2 {
1780                Path::new(value)
1781                    .file_name()
1782                    .unwrap_or_default()
1783                    .to_string_lossy()
1784                    .into_owned()
1785            } else {
1786                value.clone()
1787            }
1788        })
1789        .collect::<Vec<_>>();
1790    let mut expected_archive_argv = vec![
1791        static_identity.archiver.path.clone(),
1792        "rcs".to_string(),
1793        plan.archive_file.clone(),
1794    ];
1795    expected_archive_argv.extend(
1796        plan.translation_units
1797            .iter()
1798            .enumerate()
1799            .map(|(index, translation_unit)| object_file_name(index, translation_unit)),
1800    );
1801    if actual_archive_argv != expected_archive_argv
1802        || receipt.archive_file.as_deref() != Some(plan.archive_file.as_str())
1803    {
1804        return Err(NativeOperatorBuilderError::Invalid(format!(
1805            "{} source-build archive command differs from its locked plan",
1806            receipt.operator
1807        )));
1808    }
1809    Ok(plan)
1810}
1811
1812pub(crate) fn verify_source_build_evidence(
1813    receipt: &NativeOperatorSourceBuildReceipt,
1814    receipt_root: &Path,
1815    plan: &NativeOperatorSourceBuildPlan,
1816) -> Result<()> {
1817    let toolchain = receipt.toolchain.as_ref().ok_or_else(|| {
1818        NativeOperatorBuilderError::Invalid(format!(
1819            "{} source-build receipt is missing toolchain provenance",
1820            receipt.operator
1821        ))
1822    })?;
1823    let manifest_evidence = &toolchain.static_identity.cuda_toolkit.manifest;
1824    let manifest_path =
1825        resolve_source_build_evidence_file(receipt_root, &receipt.operator, manifest_evidence)?;
1826    let manifest: NativeOperatorCudaToolkitManifest = read_json(&manifest_path)?;
1827    validate_cuda_toolkit_manifest(
1828        &receipt.operator,
1829        &toolchain.static_identity.cuda_toolkit,
1830        &manifest,
1831    )?;
1832    let host_identity = &toolchain.static_identity.host_toolchain;
1833    let host_manifest_path = resolve_source_build_evidence_file(
1834        receipt_root,
1835        &receipt.operator,
1836        &host_identity.manifest,
1837    )?;
1838    let host_manifest: NativeOperatorHostToolchainManifest = read_json(&host_manifest_path)?;
1839    validate_host_toolchain_manifest(&receipt.operator, &host_manifest)?;
1840    if host_manifest.compiler != host_identity.compiler
1841        || host_manifest.compiler_version != host_identity.compiler_version
1842        || host_manifest.target != host_identity.target
1843    {
1844        return Err(NativeOperatorBuilderError::Invalid(format!(
1845            "{} host toolchain manifest differs from its receipt identity",
1846            receipt.operator
1847        )));
1848    }
1849    let toolchain_dependency_scope =
1850        toolchain_dependency_scope(&toolchain.static_identity, &manifest, &host_manifest)?;
1851    for (index, command) in receipt
1852        .commands
1853        .iter()
1854        .take(plan.translation_units.len())
1855        .enumerate()
1856    {
1857        if !matches!(
1858            command.dependency_validation,
1859            Some(
1860                NativeOperatorDependencyValidation::Depfile
1861                    | NativeOperatorDependencyValidation::CacheProof
1862            )
1863        ) {
1864            continue;
1865        }
1866        let compiler_relative = command.compiler_depfile.as_deref().ok_or_else(|| {
1867            NativeOperatorBuilderError::Invalid(format!(
1868                "{} compiled source-build command is missing compiler depfile evidence",
1869                receipt.operator
1870            ))
1871        })?;
1872        let compiler_sha256 = command.compiler_depfile_sha256.as_deref().ok_or_else(|| {
1873            NativeOperatorBuilderError::Invalid(format!(
1874                "{} compiled source-build command is missing compiler depfile sha256",
1875                receipt.operator
1876            ))
1877        })?;
1878        let compiler_path = resolve_source_build_relative_file(receipt_root, compiler_relative)?;
1879        let compiler_bytes = read_bounded_regular_file(
1880            &compiler_path,
1881            MAX_DEPFILE_BYTES,
1882            "source-build compiler depfile",
1883        )?;
1884        let compiler_actual = sha256_bytes(&compiler_bytes);
1885        if compiler_actual != compiler_sha256 {
1886            return Err(NativeOperatorBuilderError::Invalid(format!(
1887                "{} source-build compiler depfile sha256 mismatch: path={compiler_relative} expected={compiler_sha256} actual={compiler_actual}",
1888                receipt.operator
1889            )));
1890        }
1891        let compiler_raw = std::str::from_utf8(&compiler_bytes).map_err(|_| {
1892            NativeOperatorBuilderError::Invalid(format!(
1893                "{} source-build compiler depfile is not UTF-8: {compiler_relative}",
1894                receipt.operator
1895            ))
1896        })?;
1897        let relative = command.depfile.as_deref().ok_or_else(|| {
1898            NativeOperatorBuilderError::Invalid(format!(
1899                "{} compiled source-build command is missing portable depfile evidence",
1900                receipt.operator
1901            ))
1902        })?;
1903        let sha256 = command.depfile_sha256.as_deref().ok_or_else(|| {
1904            NativeOperatorBuilderError::Invalid(format!(
1905                "{} compiled source-build command is missing depfile sha256",
1906                receipt.operator
1907            ))
1908        })?;
1909        let path = resolve_source_build_relative_file(receipt_root, relative)?;
1910        let bytes =
1911            read_bounded_regular_file(&path, MAX_DEPFILE_BYTES, "source-build portable depfile")?;
1912        let actual = sha256_bytes(&bytes);
1913        if actual != sha256 {
1914            return Err(NativeOperatorBuilderError::Invalid(format!(
1915                "{} source-build depfile sha256 mismatch: path={relative} expected={sha256} actual={actual}",
1916                receipt.operator
1917            )));
1918        }
1919        let raw = std::str::from_utf8(&bytes).map_err(|_| {
1920            NativeOperatorBuilderError::Invalid(format!(
1921                "{} source-build portable depfile is not UTF-8: {relative}",
1922                receipt.operator
1923            ))
1924        })?;
1925        let producer_object_file =
1926            command
1927                .depfile_producer_object_file
1928                .as_deref()
1929                .ok_or_else(|| {
1930                    NativeOperatorBuilderError::Invalid(format!(
1931                        "{} compiled source-build command is missing depfile producer object",
1932                        receipt.operator
1933                    ))
1934                })?;
1935        let producer_working_directory = command
1936            .depfile_producer_working_directory
1937            .as_deref()
1938            .ok_or_else(|| {
1939                NativeOperatorBuilderError::Invalid(format!(
1940                    "{} compiled source-build command is missing depfile producer working directory",
1941                    receipt.operator
1942                ))
1943            })?;
1944        command.object_file.as_deref().ok_or_else(|| {
1945            NativeOperatorBuilderError::Invalid(format!(
1946                "{} source-build command is missing object path for depfile validation",
1947                receipt.operator
1948            ))
1949        })?;
1950        let observed = validate_portable_depfile_pair(
1951            compiler_raw,
1952            &compiler_path,
1953            raw,
1954            &path,
1955            producer_object_file,
1956            producer_working_directory,
1957            &plan.translation_units[index],
1958            &plan.dependency_closures[index],
1959            &command.depfile_bindings,
1960            &toolchain_dependency_scope,
1961        )?;
1962        if observed != command.observed_dependencies {
1963            return Err(NativeOperatorBuilderError::Invalid(format!(
1964                "{} source-build depfile semantics differ from receipt evidence: path={relative}",
1965                receipt.operator
1966            )));
1967        }
1968    }
1969    Ok(())
1970}
1971
1972fn resolve_source_build_evidence_file(
1973    root: &Path,
1974    operator: &str,
1975    evidence: &NativeOperatorEvidenceFile,
1976) -> Result<PathBuf> {
1977    if !is_sha256_digest(&evidence.sha256) || evidence.size_bytes == 0 {
1978        return Err(NativeOperatorBuilderError::Invalid(format!(
1979            "{operator} source-build evidence identity is incomplete: {}",
1980            evidence.path
1981        )));
1982    }
1983    let path = resolve_source_build_relative_file(root, &evidence.path)?;
1984    let size_bytes = fs::metadata(&path)
1985        .map_err(|source| NativeOperatorBuilderError::Io {
1986            path: path.clone(),
1987            source,
1988        })?
1989        .len();
1990    let sha256 = sha256_file(&path)?;
1991    if size_bytes != evidence.size_bytes || sha256 != evidence.sha256 {
1992        return Err(NativeOperatorBuilderError::Invalid(format!(
1993            "{operator} source-build evidence mismatch: path={} expected_size={} actual_size={size_bytes} expected_sha256={} actual_sha256={sha256}",
1994            evidence.path, evidence.size_bytes, evidence.sha256
1995        )));
1996    }
1997    Ok(path)
1998}
1999
2000fn resolve_source_build_relative_file(root: &Path, relative: &str) -> Result<PathBuf> {
2001    validate_relative_path(relative)?;
2002    let canonical_root = root
2003        .canonicalize()
2004        .map_err(|source| NativeOperatorBuilderError::Io {
2005            path: root.to_path_buf(),
2006            source,
2007        })?;
2008    let path = canonical_root.join(relative);
2009    let canonical = path
2010        .canonicalize()
2011        .map_err(|source| NativeOperatorBuilderError::Io {
2012            path: path.clone(),
2013            source,
2014        })?;
2015    if !canonical.starts_with(&canonical_root) || !canonical.is_file() {
2016        return Err(NativeOperatorBuilderError::Invalid(format!(
2017            "source-build evidence escapes its root or is not a file: {relative}"
2018        )));
2019    }
2020    Ok(canonical)
2021}
2022
2023fn validate_cuda_toolkit_manifest(
2024    operator: &str,
2025    identity: &NativeOperatorCudaToolkitIdentity,
2026    manifest: &NativeOperatorCudaToolkitManifest,
2027) -> Result<()> {
2028    if manifest.schema_version != NATIVE_OPERATOR_CUDA_TOOLKIT_MANIFEST_SCHEMA_VERSION
2029        || manifest.canonical_root != identity.canonical_root
2030        || manifest.entries.is_empty()
2031        || manifest
2032            .entries
2033            .windows(2)
2034            .any(|pair| pair[0].logical_path >= pair[1].logical_path)
2035    {
2036        return Err(NativeOperatorBuilderError::Invalid(format!(
2037            "{operator} cuda toolkit manifest header/order is invalid"
2038        )));
2039    }
2040    let mut scopes = BTreeSet::new();
2041    let mut required_files = REQUIRED_CUDA_TOOLKIT_FILES
2042        .iter()
2043        .copied()
2044        .collect::<BTreeSet<_>>();
2045    let mut selected_nvcc = None;
2046    for entry in &manifest.entries {
2047        validate_relative_path(&entry.logical_path)?;
2048        validate_relative_path(&entry.resolved_path)?;
2049        if !is_sha256_digest(&entry.sha256) {
2050            return Err(NativeOperatorBuilderError::Invalid(format!(
2051                "{operator} cuda toolkit manifest entry is incomplete: {}",
2052                entry.logical_path
2053            )));
2054        }
2055        for scope in ["bin/crt/", "include/", "nvvm/bin/", "nvvm/libdevice/"] {
2056            if entry.logical_path.starts_with(scope) {
2057                scopes.insert(scope);
2058            }
2059        }
2060        required_files.remove(entry.logical_path.as_str());
2061        if Path::new(&identity.canonical_root).join(&entry.resolved_path)
2062            == Path::new(&identity.nvcc.path)
2063        {
2064            selected_nvcc = Some(entry);
2065        }
2066    }
2067    if scopes.len() != REQUIRED_CUDA_TOOLKIT_SCOPES.len() || !required_files.is_empty() {
2068        return Err(NativeOperatorBuilderError::Invalid(format!(
2069            "{operator} cuda toolkit manifest does not cover every required compiler input; missing={}",
2070            required_files.into_iter().collect::<Vec<_>>().join(",")
2071        )));
2072    }
2073    if selected_nvcc.is_none_or(|entry| {
2074        entry.sha256 != identity.nvcc.sha256 || entry.size_bytes != identity.nvcc.size_bytes
2075    }) {
2076        return Err(NativeOperatorBuilderError::Invalid(format!(
2077            "{operator} cuda toolkit manifest does not bind the selected nvcc"
2078        )));
2079    }
2080    Ok(())
2081}
2082
2083#[allow(clippy::too_many_arguments)]
2084fn validate_common(
2085    operator: &str,
2086    upstream_sources: &[NativeOperatorUpstreamSource],
2087    translation_units: &[String],
2088    headers: &[String],
2089    include_dirs: &[String],
2090    defines: &[String],
2091    archive_file: &str,
2092) -> Result<()> {
2093    symbol_slug(operator)?;
2094    if CudaNativeBuildUnit::from_artifact_operator(operator).is_none() {
2095        return Err(NativeOperatorBuilderError::Invalid(format!(
2096            "source build operator is not a registered CUDA build unit: {operator}"
2097        )));
2098    }
2099    if upstream_sources.is_empty() {
2100        return Err(NativeOperatorBuilderError::Invalid(
2101            "upstream_sources must be non-empty".to_string(),
2102        ));
2103    }
2104    if upstream_sources.windows(2).any(|pair| {
2105        (&pair[0].repository, &pair[0].revision) >= (&pair[1].repository, &pair[1].revision)
2106    }) {
2107        return Err(NativeOperatorBuilderError::Invalid(
2108            "upstream_sources must be sorted and unique by repository/revision".to_string(),
2109        ));
2110    }
2111    for upstream in upstream_sources {
2112        if upstream.repository.trim().is_empty()
2113            || upstream.revision.trim().is_empty()
2114            || upstream.license.trim().is_empty()
2115        {
2116            return Err(NativeOperatorBuilderError::Invalid(
2117                "upstream source repository, revision, and license must be non-empty".to_string(),
2118            ));
2119        }
2120    }
2121    require_sorted_paths("translation_units", translation_units)?;
2122    require_sorted_optional_paths("headers", headers)?;
2123    if translation_units.iter().any(|path| !path.ends_with(".cu")) {
2124        return Err(NativeOperatorBuilderError::Invalid(
2125            "translation_units must use .cu paths".to_string(),
2126        ));
2127    }
2128    let mut all_files = translation_units
2129        .iter()
2130        .chain(headers.iter())
2131        .collect::<Vec<_>>();
2132    all_files.sort();
2133    if all_files.windows(2).any(|pair| pair[0] == pair[1]) {
2134        return Err(NativeOperatorBuilderError::Invalid(
2135            "translation_units and headers overlap".to_string(),
2136        ));
2137    }
2138    require_sorted_optional_paths("include_dirs", include_dirs)?;
2139    if defines.windows(2).any(|pair| pair[0] >= pair[1])
2140        || defines
2141            .iter()
2142            .any(|define| define.is_empty() || define.chars().any(char::is_whitespace))
2143    {
2144        return Err(NativeOperatorBuilderError::Invalid(
2145            "defines must be sorted, unique, non-empty single arguments".to_string(),
2146        ));
2147    }
2148    validate_relative_path(archive_file)?;
2149    if Path::new(archive_file).parent() != Some(Path::new(""))
2150        || !archive_file.starts_with("lib")
2151        || !archive_file.ends_with(".a")
2152    {
2153        return Err(NativeOperatorBuilderError::Invalid(
2154            "archive_file must be a lib*.a filename without directories".to_string(),
2155        ));
2156    }
2157    Ok(())
2158}
2159
2160fn require_sorted_paths(field: &str, paths: &[String]) -> Result<()> {
2161    if paths.is_empty() {
2162        return Err(NativeOperatorBuilderError::Invalid(format!(
2163            "{field} must be sorted, unique, non-empty normalized relative paths"
2164        )));
2165    }
2166    require_sorted_optional_paths(field, paths)
2167}
2168
2169fn require_sorted_optional_paths(field: &str, paths: &[String]) -> Result<()> {
2170    if paths.windows(2).any(|pair| pair[0] >= pair[1])
2171        || paths
2172            .iter()
2173            .any(|path| validate_relative_path(path).is_err())
2174    {
2175        return Err(NativeOperatorBuilderError::Invalid(format!(
2176            "{field} must be sorted, unique normalized relative paths"
2177        )));
2178    }
2179    Ok(())
2180}
2181
2182fn lock_source_files(root: &Path, paths: &[String]) -> Result<Vec<NativeOperatorSourceFileLock>> {
2183    paths
2184        .iter()
2185        .map(|relative| {
2186            let path = locked_source_file(root, relative)?;
2187            Ok(NativeOperatorSourceFileLock {
2188                path: relative.clone(),
2189                sha256: sha256_file(&path)?,
2190            })
2191        })
2192        .collect()
2193}
2194
2195fn validate_locked_source_tree(root: &Path, plan: &NativeOperatorSourceBuildPlan) -> Result<()> {
2196    if !root.is_dir() {
2197        return Err(NativeOperatorBuilderError::Invalid(format!(
2198            "source_root is not a directory: {}",
2199            root.display()
2200        )));
2201    }
2202    for include_dir in &plan.include_dirs {
2203        let path = root.join(include_dir);
2204        let canonical = path
2205            .canonicalize()
2206            .map_err(|source| NativeOperatorBuilderError::Io {
2207                path: path.clone(),
2208                source,
2209            })?;
2210        if !canonical.starts_with(root) || !canonical.is_dir() {
2211            return Err(NativeOperatorBuilderError::Invalid(format!(
2212                "locked include directory escapes source root or is missing: {include_dir}"
2213            )));
2214        }
2215    }
2216    for locked in plan.translation_units.iter().chain(plan.headers.iter()) {
2217        let path = locked_source_file(root, &locked.path)?;
2218        let actual = sha256_file(&path)?;
2219        if actual != locked.sha256 {
2220            return Err(NativeOperatorBuilderError::Invalid(format!(
2221                "locked source drift: path={} expected={} actual={actual}",
2222                locked.path, locked.sha256
2223            )));
2224        }
2225    }
2226    Ok(())
2227}
2228
2229struct NativeOperatorToolchainDependencyScope {
2230    by_absolute_path: BTreeMap<String, NativeOperatorObservedDependency>,
2231}
2232
2233fn load_toolchain_dependency_scope(
2234    receipt_root: &Path,
2235    toolchain: &NativeOperatorSourceBuildStaticToolchain,
2236) -> Result<NativeOperatorToolchainDependencyScope> {
2237    let cuda_manifest_path = resolve_source_build_evidence_file(
2238        receipt_root,
2239        "<dependency-scope>",
2240        &toolchain.cuda_toolkit.manifest,
2241    )?;
2242    let cuda_manifest: NativeOperatorCudaToolkitManifest = read_json(&cuda_manifest_path)?;
2243    validate_cuda_toolkit_manifest(
2244        "<dependency-scope>",
2245        &toolchain.cuda_toolkit,
2246        &cuda_manifest,
2247    )?;
2248    let host_manifest_path = resolve_source_build_evidence_file(
2249        receipt_root,
2250        "<dependency-scope>",
2251        &toolchain.host_toolchain.manifest,
2252    )?;
2253    let host_manifest: NativeOperatorHostToolchainManifest = read_json(&host_manifest_path)?;
2254    validate_host_toolchain_manifest("<dependency-scope>", &host_manifest)?;
2255    toolchain_dependency_scope(toolchain, &cuda_manifest, &host_manifest)
2256}
2257
2258fn toolchain_dependency_scope(
2259    toolchain: &NativeOperatorSourceBuildStaticToolchain,
2260    cuda_manifest: &NativeOperatorCudaToolkitManifest,
2261    host_manifest: &NativeOperatorHostToolchainManifest,
2262) -> Result<NativeOperatorToolchainDependencyScope> {
2263    let mut by_absolute_path = BTreeMap::new();
2264    let cuda_roots = [
2265        Path::new(&toolchain.cuda_toolkit.canonical_root),
2266        Path::new(&toolchain.cuda_toolkit.invocation_root),
2267    ];
2268    for entry in &cuda_manifest.entries {
2269        let dependency = NativeOperatorObservedDependency {
2270            domain: NativeOperatorDependencyDomain::BackendToolchain,
2271            path: entry.logical_path.clone(),
2272            sha256: entry.sha256.clone(),
2273        };
2274        for root in cuda_roots {
2275            for relative in [&entry.logical_path, &entry.resolved_path] {
2276                insert_toolchain_dependency(
2277                    &mut by_absolute_path,
2278                    root.join(relative).display().to_string(),
2279                    dependency.clone(),
2280                )?;
2281            }
2282        }
2283    }
2284    for entry in &host_manifest.files {
2285        let dependency = NativeOperatorObservedDependency {
2286            domain: NativeOperatorDependencyDomain::HostToolchain,
2287            path: entry.resolved_path.clone(),
2288            sha256: entry.sha256.clone(),
2289        };
2290        for absolute in [&entry.logical_path, &entry.resolved_path] {
2291            insert_toolchain_dependency(
2292                &mut by_absolute_path,
2293                absolute.clone(),
2294                dependency.clone(),
2295            )?;
2296        }
2297    }
2298    Ok(NativeOperatorToolchainDependencyScope { by_absolute_path })
2299}
2300
2301fn insert_toolchain_dependency(
2302    dependencies: &mut BTreeMap<String, NativeOperatorObservedDependency>,
2303    absolute_path: String,
2304    dependency: NativeOperatorObservedDependency,
2305) -> Result<()> {
2306    validate_normalized_absolute_path(&absolute_path, "toolchain dependency path")?;
2307    if let Some(existing) = dependencies.get(&absolute_path) {
2308        if existing != &dependency {
2309            return Err(NativeOperatorBuilderError::Invalid(format!(
2310                "toolchain manifests ambiguously own dependency path: {absolute_path}"
2311            )));
2312        }
2313        return Ok(());
2314    }
2315    dependencies.insert(absolute_path, dependency);
2316    Ok(())
2317}
2318
2319fn expected_source_dependencies(
2320    translation_unit: &NativeOperatorSourceFileLock,
2321    closure: &NativeOperatorTranslationUnitDependencyLock,
2322) -> BTreeSet<NativeOperatorObservedDependency> {
2323    std::iter::once(translation_unit)
2324        .chain(closure.headers.iter())
2325        .map(|locked| NativeOperatorObservedDependency {
2326            domain: NativeOperatorDependencyDomain::Source,
2327            path: locked.path.clone(),
2328            sha256: locked.sha256.clone(),
2329        })
2330        .collect()
2331}
2332
2333fn validate_observed_dependency(
2334    context: &str,
2335    dependency: &NativeOperatorObservedDependency,
2336) -> Result<()> {
2337    let path_is_valid = match dependency.domain {
2338        NativeOperatorDependencyDomain::Source
2339        | NativeOperatorDependencyDomain::BackendToolchain => {
2340            validate_relative_path(&dependency.path).is_ok()
2341        }
2342        NativeOperatorDependencyDomain::HostToolchain => Path::new(&dependency.path).is_absolute(),
2343    };
2344    if !path_is_valid || !is_sha256_digest(&dependency.sha256) {
2345        return Err(NativeOperatorBuilderError::Invalid(format!(
2346            "{context} observed dependency identity is invalid: {:?}:{}",
2347            dependency.domain, dependency.path
2348        )));
2349    }
2350    Ok(())
2351}
2352
2353fn validate_observed_dependencies(
2354    context: &str,
2355    dependencies: &[NativeOperatorObservedDependency],
2356) -> Result<()> {
2357    if dependencies.is_empty()
2358        || dependencies.windows(2).any(|pair| pair[0] >= pair[1])
2359        || dependencies
2360            .iter()
2361            .any(|dependency| validate_observed_dependency(context, dependency).is_err())
2362    {
2363        return Err(NativeOperatorBuilderError::Invalid(format!(
2364            "{context} observed dependencies are empty, unsorted, duplicated, or invalid"
2365        )));
2366    }
2367    Ok(())
2368}
2369
2370fn observed_dependency_set_sha256(
2371    dependencies: &[NativeOperatorObservedDependency],
2372) -> Result<String> {
2373    validate_observed_dependencies("<dependency-set>", dependencies)?;
2374    let bytes =
2375        serde_json::to_vec(dependencies).map_err(|source| NativeOperatorBuilderError::Json {
2376            path: PathBuf::from("<dependency-set>"),
2377            source,
2378        })?;
2379    Ok(sha256_bytes(&bytes))
2380}
2381
2382fn validate_translation_unit_depfile(
2383    compiler_depfile_path: &Path,
2384    portable_depfile_path: &Path,
2385    object_path: &Path,
2386    source_root: &Path,
2387    translation_unit: &NativeOperatorSourceFileLock,
2388    closure: &NativeOperatorTranslationUnitDependencyLock,
2389    toolchain_scope: &NativeOperatorToolchainDependencyScope,
2390) -> Result<ValidatedTranslationUnitDepfile> {
2391    if closure.translation_unit != translation_unit.path {
2392        return Err(NativeOperatorBuilderError::Invalid(format!(
2393            "dependency closure translation unit mismatch: expected={} actual={}",
2394            translation_unit.path, closure.translation_unit
2395        )));
2396    }
2397    require_file(compiler_depfile_path)?;
2398    let raw =
2399        read_bounded_regular_file(compiler_depfile_path, MAX_DEPFILE_BYTES, "compiler depfile")?;
2400    let raw_text = std::str::from_utf8(&raw).map_err(|_| {
2401        NativeOperatorBuilderError::Invalid(format!(
2402            "compiler depfile is not valid UTF-8: {}",
2403            compiler_depfile_path.display()
2404        ))
2405    })?;
2406    let (target, dependencies) = parse_make_depfile(raw_text, compiler_depfile_path)?;
2407    let expected_target = object_path.display().to_string();
2408    validate_normalized_absolute_path(&expected_target, "compiler depfile object target")?;
2409    if target != expected_target {
2410        return Err(NativeOperatorBuilderError::Invalid(format!(
2411            "compiler depfile target differs from its exact -MT object: expected={expected_target} actual={target}"
2412        )));
2413    }
2414
2415    let mut observed = BTreeSet::new();
2416    let mut portable_paths = BTreeMap::new();
2417    let mut bindings = Vec::with_capacity(dependencies.len());
2418    for dependency in dependencies {
2419        let candidate = if Path::new(&dependency).is_absolute() {
2420            PathBuf::from(&dependency)
2421        } else {
2422            source_root.join(&dependency)
2423        };
2424        let canonical =
2425            candidate
2426                .canonicalize()
2427                .map_err(|source| NativeOperatorBuilderError::Io {
2428                    path: candidate.clone(),
2429                    source,
2430                })?;
2431        if !canonical.is_file() {
2432            return Err(NativeOperatorBuilderError::Invalid(format!(
2433                "compiler depfile dependency is not a file: {}",
2434                canonical.display()
2435            )));
2436        }
2437        let (identity, portable_path) = if canonical.starts_with(source_root) {
2438            let relative = canonical.strip_prefix(source_root).map_err(|_| {
2439                NativeOperatorBuilderError::Invalid(format!(
2440                    "compiler depfile dependency escapes the source root: {}",
2441                    canonical.display()
2442                ))
2443            })?;
2444            let identity = NativeOperatorObservedDependency {
2445                domain: NativeOperatorDependencyDomain::Source,
2446                path: path_with_forward_slashes(relative)?,
2447                sha256: sha256_file(&canonical)?,
2448            };
2449            let portable_path = identity.path.clone();
2450            (identity, portable_path)
2451        } else {
2452            let candidate_path = candidate.display().to_string();
2453            let canonical_path = canonical.display().to_string();
2454            let identity = toolchain_scope
2455                .by_absolute_path
2456                .get(&candidate_path)
2457                .or_else(|| toolchain_scope.by_absolute_path.get(&canonical_path))
2458                .ok_or_else(|| {
2459                    NativeOperatorBuilderError::Invalid(format!(
2460                        "compiler depfile dependency is outside the locked source and toolchain manifests: {}",
2461                        canonical.display()
2462                    ))
2463                })?
2464                .clone();
2465            let actual = sha256_file(&canonical)?;
2466            if actual != identity.sha256 {
2467                return Err(NativeOperatorBuilderError::Invalid(format!(
2468                    "toolchain dependency changed while compiling: path={} expected={} actual={actual}",
2469                    canonical.display(),
2470                    identity.sha256
2471                )));
2472            }
2473            validate_normalized_absolute_path(
2474                &canonical_path,
2475                "compiled depfile canonical toolchain dependency",
2476            )?;
2477            (identity, canonical_path)
2478        };
2479        validate_observed_dependency("<compiled-depfile>", &identity)?;
2480        if !observed.insert(identity.clone()) {
2481            return Err(NativeOperatorBuilderError::Invalid(format!(
2482                "compiler depfile contains a duplicate dependency identity: {:?}:{}",
2483                identity.domain, identity.path
2484            )));
2485        }
2486        portable_paths.insert(identity.clone(), portable_path.clone());
2487        bindings.push(NativeOperatorDepfileDependencyBinding {
2488            producer_path: dependency,
2489            portable_path,
2490            dependency: identity,
2491        });
2492    }
2493
2494    let expected = expected_source_dependencies(translation_unit, closure);
2495    let observed_source = observed
2496        .iter()
2497        .filter(|dependency| dependency.domain == NativeOperatorDependencyDomain::Source)
2498        .cloned()
2499        .collect::<BTreeSet<_>>();
2500    if observed_source != expected {
2501        let missing = expected
2502            .difference(&observed_source)
2503            .cloned()
2504            .collect::<Vec<_>>();
2505        let undeclared = observed_source
2506            .difference(&expected)
2507            .cloned()
2508            .collect::<Vec<_>>();
2509        return Err(NativeOperatorBuilderError::Invalid(format!(
2510            "depfile differs from the declared source dependency closure: missing={missing:?} undeclared={undeclared:?}"
2511        )));
2512    }
2513
2514    for locked in std::iter::once(translation_unit).chain(closure.headers.iter()) {
2515        let path = locked_source_file(source_root, &locked.path)?;
2516        let actual = sha256_file(&path)?;
2517        if actual != locked.sha256 {
2518            return Err(NativeOperatorBuilderError::Invalid(format!(
2519                "dependency changed while compiling: path={} expected={} actual={actual}",
2520                locked.path, locked.sha256
2521            )));
2522        }
2523    }
2524    let observed_dependencies = observed.into_iter().collect::<Vec<_>>();
2525    let portable_dependencies = observed_dependencies
2526        .iter()
2527        .map(|identity| {
2528            portable_paths.get(identity).cloned().ok_or_else(|| {
2529                NativeOperatorBuilderError::Invalid(format!(
2530                    "validated dependency has no portable depfile path: {:?}:{}",
2531                    identity.domain, identity.path
2532                ))
2533            })
2534        })
2535        .collect::<Result<Vec<_>>>()?;
2536    let portable_raw =
2537        serialize_portable_depfile(&object_path.display().to_string(), &portable_dependencies)?;
2538    let portable_text = std::str::from_utf8(&portable_raw).expect("portable depfile is UTF-8");
2539    let verified_dependencies = validate_portable_depfile_pair(
2540        raw_text,
2541        compiler_depfile_path,
2542        portable_text,
2543        portable_depfile_path,
2544        &object_path.display().to_string(),
2545        &source_root.display().to_string(),
2546        translation_unit,
2547        closure,
2548        &bindings,
2549        toolchain_scope,
2550    )?;
2551    if verified_dependencies != observed_dependencies {
2552        return Err(NativeOperatorBuilderError::Invalid(
2553            "producer depfile verification changed the typed dependency set".to_string(),
2554        ));
2555    }
2556    atomic_write_bytes(portable_depfile_path, &portable_raw)?;
2557    Ok(ValidatedTranslationUnitDepfile {
2558        compiler_sha256: sha256_bytes(&raw),
2559        compiler_raw: raw,
2560        portable_sha256: sha256_bytes(&portable_raw),
2561        portable_raw,
2562        bindings,
2563        observed_dependencies,
2564    })
2565}
2566
2567struct ValidatedTranslationUnitDepfile {
2568    compiler_raw: Vec<u8>,
2569    compiler_sha256: String,
2570    portable_raw: Vec<u8>,
2571    portable_sha256: String,
2572    bindings: Vec<NativeOperatorDepfileDependencyBinding>,
2573    observed_dependencies: Vec<NativeOperatorObservedDependency>,
2574}
2575
2576fn serialize_portable_depfile(target: &str, dependencies: &[String]) -> Result<Vec<u8>> {
2577    validate_normalized_absolute_path(target, "portable depfile target")?;
2578    if dependencies.is_empty() || dependencies.len() > MAX_DEPFILE_DEPENDENCIES {
2579        return Err(NativeOperatorBuilderError::Invalid(
2580            "portable depfile dependency count is invalid".to_string(),
2581        ));
2582    }
2583    let mut result = escape_make_word(target)?;
2584    result.push(':');
2585    for dependency in dependencies {
2586        if Path::new(dependency).is_absolute() {
2587            validate_normalized_absolute_path(dependency, "portable depfile dependency")?;
2588        } else {
2589            validate_relative_path(dependency)?;
2590        }
2591        result.push(' ');
2592        result.push_str(&escape_make_word(dependency)?);
2593    }
2594    result.push('\n');
2595    if result.len() > MAX_DEPFILE_BYTES {
2596        return Err(NativeOperatorBuilderError::Invalid(format!(
2597            "portable depfile exceeds {MAX_DEPFILE_BYTES} bytes"
2598        )));
2599    }
2600    let (parsed_target, parsed_dependencies) =
2601        parse_make_depfile(&result, Path::new("<portable-depfile>"))?;
2602    if parsed_target != target || parsed_dependencies != dependencies {
2603        return Err(NativeOperatorBuilderError::Invalid(
2604            "portable depfile serialization did not round-trip".to_string(),
2605        ));
2606    }
2607    Ok(result.into_bytes())
2608}
2609
2610fn escape_make_word(value: &str) -> Result<String> {
2611    if value.is_empty()
2612        || value.len() > MAX_DEPFILE_WORD_BYTES
2613        || value
2614            .chars()
2615            .any(|character| matches!(character, '\0' | '\n' | '\r'))
2616    {
2617        return Err(NativeOperatorBuilderError::Invalid(
2618            "make depfile word must be non-empty and single-line".to_string(),
2619        ));
2620    }
2621    let mut result = String::with_capacity(value.len());
2622    for character in value.chars() {
2623        if matches!(character, '\\' | ' ' | '\t' | ':' | '#' | '$') {
2624            result.push('\\');
2625        }
2626        result.push(character);
2627    }
2628    Ok(result)
2629}
2630
2631fn publish_object_dependency_proof(
2632    cache_entry: &Path,
2633    object_cache_key: &str,
2634    object_sha256: &str,
2635    translation_unit: &NativeOperatorSourceFileLock,
2636    closure: &NativeOperatorTranslationUnitDependencyLock,
2637    compiler_depfile_raw: &[u8],
2638    expected_compiler_depfile_sha256: &str,
2639    depfile_raw: &[u8],
2640    expected_depfile_sha256: &str,
2641    producer_working_directory: &str,
2642    producer_object_file: &str,
2643    depfile_bindings: &[NativeOperatorDepfileDependencyBinding],
2644    observed_dependencies: &[NativeOperatorObservedDependency],
2645    toolchain_scope: &NativeOperatorToolchainDependencyScope,
2646) -> Result<()> {
2647    let compiler_depfile_sha256 = sha256_bytes(compiler_depfile_raw);
2648    if compiler_depfile_sha256 != expected_compiler_depfile_sha256 {
2649        return Err(NativeOperatorBuilderError::Invalid(format!(
2650            "compiler depfile bytes differ from their recorded sha256: expected={expected_compiler_depfile_sha256} actual={compiler_depfile_sha256}"
2651        )));
2652    }
2653    let depfile_sha256 = sha256_bytes(depfile_raw);
2654    if depfile_sha256 != expected_depfile_sha256 {
2655        return Err(NativeOperatorBuilderError::Invalid(format!(
2656            "validated depfile bytes differ from their recorded sha256: expected={expected_depfile_sha256} actual={depfile_sha256}"
2657        )));
2658    }
2659    let proof = NativeOperatorObjectDependencyProof {
2660        schema_version: NATIVE_OPERATOR_OBJECT_DEPENDENCY_PROOF_SCHEMA_VERSION,
2661        object_cache_key: object_cache_key.to_string(),
2662        object_sha256: object_sha256.to_string(),
2663        dependency_closure_sha256: closure.closure_sha256.clone(),
2664        dependency_set_sha256: observed_dependency_set_sha256(observed_dependencies)?,
2665        compiler_depfile_sha256,
2666        depfile_sha256: depfile_sha256.clone(),
2667        producer_working_directory: producer_working_directory.to_string(),
2668        producer_object_file: producer_object_file.to_string(),
2669        depfile_bindings: depfile_bindings.to_vec(),
2670        observed_dependencies: observed_dependencies.to_vec(),
2671    };
2672    validate_object_dependency_proof(
2673        "<object-cache-publish>",
2674        &proof,
2675        object_cache_key,
2676        object_sha256,
2677        translation_unit,
2678        closure,
2679    )?;
2680    let proof_dir = cache_entry.join("dependency-proof");
2681    if proof_dir.exists() {
2682        return validate_existing_dependency_proof(
2683            &proof_dir,
2684            object_cache_key,
2685            object_sha256,
2686            translation_unit,
2687            closure,
2688            producer_object_file,
2689            toolchain_scope,
2690        );
2691    }
2692
2693    let staging = tempfile::Builder::new()
2694        .prefix(".dependency-proof-")
2695        .tempdir_in(cache_entry)
2696        .map_err(|source| NativeOperatorBuilderError::Io {
2697            path: cache_entry.to_path_buf(),
2698            source,
2699        })?;
2700    let staging_path = staging.path().to_path_buf();
2701    atomic_write_bytes(
2702        &staging_path.join("compiler-dependency.raw.d"),
2703        compiler_depfile_raw,
2704    )?;
2705    atomic_write_bytes(&staging_path.join("dependency.d"), depfile_raw)?;
2706    write_json(&staging_path.join("proof.json"), &proof)?;
2707    sync_directory(&staging_path)?;
2708    let staging_path = staging.keep();
2709    match fs::rename(&staging_path, &proof_dir) {
2710        Ok(()) => {
2711            sync_directory(cache_entry)?;
2712            validate_existing_dependency_proof(
2713                &proof_dir,
2714                object_cache_key,
2715                object_sha256,
2716                translation_unit,
2717                closure,
2718                producer_object_file,
2719                toolchain_scope,
2720            )
2721        }
2722        Err(_source) if proof_dir.exists() => {
2723            fs::remove_dir_all(&staging_path).map_err(|cleanup_source| {
2724                NativeOperatorBuilderError::Io {
2725                    path: staging_path.clone(),
2726                    source: cleanup_source,
2727                }
2728            })?;
2729            validate_existing_dependency_proof(
2730                &proof_dir,
2731                object_cache_key,
2732                object_sha256,
2733                translation_unit,
2734                closure,
2735                producer_object_file,
2736                toolchain_scope,
2737            )
2738        }
2739        Err(source) => {
2740            let _ = fs::remove_dir_all(&staging_path);
2741            Err(NativeOperatorBuilderError::Io {
2742                path: proof_dir,
2743                source,
2744            })
2745        }
2746    }
2747}
2748
2749fn validate_dependency_proof_directory(proof_dir: &Path) -> Result<()> {
2750    let metadata =
2751        fs::symlink_metadata(proof_dir).map_err(|source| NativeOperatorBuilderError::Io {
2752            path: proof_dir.to_path_buf(),
2753            source,
2754        })?;
2755    if !metadata.file_type().is_dir() || metadata.file_type().is_symlink() {
2756        return Err(NativeOperatorBuilderError::Invalid(format!(
2757            "object dependency proof is not a real directory: {}",
2758            proof_dir.display()
2759        )));
2760    }
2761    let mut entries = fs::read_dir(proof_dir)
2762        .map_err(|source| NativeOperatorBuilderError::Io {
2763            path: proof_dir.to_path_buf(),
2764            source,
2765        })?
2766        .map(|entry| {
2767            entry
2768                .map(|entry| entry.file_name())
2769                .map_err(|source| NativeOperatorBuilderError::Io {
2770                    path: proof_dir.to_path_buf(),
2771                    source,
2772                })
2773        })
2774        .collect::<Result<Vec<_>>>()?;
2775    entries.sort();
2776    let expected = [
2777        std::ffi::OsString::from("compiler-dependency.raw.d"),
2778        std::ffi::OsString::from("dependency.d"),
2779        std::ffi::OsString::from("proof.json"),
2780    ];
2781    if entries != expected {
2782        return Err(NativeOperatorBuilderError::Invalid(format!(
2783            "object dependency proof directory is incomplete or contains extra files: {}",
2784            proof_dir.display()
2785        )));
2786    }
2787    for name in expected {
2788        let path = proof_dir.join(name);
2789        let metadata =
2790            fs::symlink_metadata(&path).map_err(|source| NativeOperatorBuilderError::Io {
2791                path: path.clone(),
2792                source,
2793            })?;
2794        if !metadata.file_type().is_file() || metadata.file_type().is_symlink() {
2795            return Err(NativeOperatorBuilderError::Invalid(format!(
2796                "object dependency proof member is not a regular file: {}",
2797                path.display()
2798            )));
2799        }
2800    }
2801    Ok(())
2802}
2803
2804fn sync_directory(path: &Path) -> Result<()> {
2805    let directory = File::open(path).map_err(|source| NativeOperatorBuilderError::Io {
2806        path: path.to_path_buf(),
2807        source,
2808    })?;
2809    directory
2810        .sync_all()
2811        .map_err(|source| NativeOperatorBuilderError::Io {
2812            path: path.to_path_buf(),
2813            source,
2814        })
2815}
2816
2817struct ValidatedCachedDependencyProof {
2818    proof: NativeOperatorObjectDependencyProof,
2819    compiler_raw: Vec<u8>,
2820    portable_raw: Vec<u8>,
2821}
2822
2823#[allow(clippy::too_many_arguments)]
2824fn load_validated_dependency_proof(
2825    proof_dir: &Path,
2826    object_cache_key: &str,
2827    object_sha256: &str,
2828    translation_unit: &NativeOperatorSourceFileLock,
2829    closure: &NativeOperatorTranslationUnitDependencyLock,
2830    toolchain_scope: &NativeOperatorToolchainDependencyScope,
2831) -> Result<ValidatedCachedDependencyProof> {
2832    validate_dependency_proof_directory(proof_dir)?;
2833    let proof_path = proof_dir.join("proof.json");
2834    let proof_bytes = read_bounded_regular_file(
2835        &proof_path,
2836        MAX_DEPENDENCY_PROOF_BYTES,
2837        "object dependency proof",
2838    )?;
2839    let proof: NativeOperatorObjectDependencyProof =
2840        serde_json::from_slice(&proof_bytes).map_err(|source| {
2841            NativeOperatorBuilderError::Json {
2842                path: proof_path.clone(),
2843                source,
2844            }
2845        })?;
2846    validate_object_dependency_proof(
2847        "<object-cache-proof>",
2848        &proof,
2849        object_cache_key,
2850        object_sha256,
2851        translation_unit,
2852        closure,
2853    )?;
2854
2855    let compiler_depfile_path = proof_dir.join("compiler-dependency.raw.d");
2856    let compiler_raw = read_bounded_regular_file(
2857        &compiler_depfile_path,
2858        MAX_DEPFILE_BYTES,
2859        "cached compiler depfile",
2860    )?;
2861    if sha256_bytes(&compiler_raw) != proof.compiler_depfile_sha256 {
2862        return Err(NativeOperatorBuilderError::Invalid(format!(
2863            "cached object compiler depfile hash mismatch: {}",
2864            compiler_depfile_path.display()
2865        )));
2866    }
2867    let depfile_path = proof_dir.join("dependency.d");
2868    let portable_raw =
2869        read_bounded_regular_file(&depfile_path, MAX_DEPFILE_BYTES, "cached portable depfile")?;
2870    if sha256_bytes(&portable_raw) != proof.depfile_sha256 {
2871        return Err(NativeOperatorBuilderError::Invalid(format!(
2872            "cached object dependency depfile hash mismatch: {}",
2873            depfile_path.display()
2874        )));
2875    }
2876    let compiler_text = std::str::from_utf8(&compiler_raw).map_err(|_| {
2877        NativeOperatorBuilderError::Invalid(format!(
2878            "cached object compiler depfile is not UTF-8: {}",
2879            compiler_depfile_path.display()
2880        ))
2881    })?;
2882    let portable_text = std::str::from_utf8(&portable_raw).map_err(|_| {
2883        NativeOperatorBuilderError::Invalid(format!(
2884            "cached object dependency depfile is not UTF-8: {}",
2885            depfile_path.display()
2886        ))
2887    })?;
2888    let observed = validate_portable_depfile_pair(
2889        compiler_text,
2890        &compiler_depfile_path,
2891        portable_text,
2892        &depfile_path,
2893        &proof.producer_object_file,
2894        &proof.producer_working_directory,
2895        translation_unit,
2896        closure,
2897        &proof.depfile_bindings,
2898        toolchain_scope,
2899    )?;
2900    if observed != proof.observed_dependencies {
2901        return Err(NativeOperatorBuilderError::Invalid(format!(
2902            "cached object dependency proof observed list differs from depfile: {}",
2903            proof_path.display()
2904        )));
2905    }
2906    Ok(ValidatedCachedDependencyProof {
2907        proof,
2908        compiler_raw,
2909        portable_raw,
2910    })
2911}
2912
2913#[allow(clippy::too_many_arguments)]
2914fn validate_existing_dependency_proof(
2915    proof_dir: &Path,
2916    object_cache_key: &str,
2917    object_sha256: &str,
2918    translation_unit: &NativeOperatorSourceFileLock,
2919    closure: &NativeOperatorTranslationUnitDependencyLock,
2920    producer_object_file: &str,
2921    toolchain_scope: &NativeOperatorToolchainDependencyScope,
2922) -> Result<()> {
2923    let validated = load_validated_dependency_proof(
2924        proof_dir,
2925        object_cache_key,
2926        object_sha256,
2927        translation_unit,
2928        closure,
2929        toolchain_scope,
2930    )?;
2931    if Path::new(&validated.proof.producer_object_file).file_name()
2932        != Path::new(producer_object_file).file_name()
2933    {
2934        return Err(NativeOperatorBuilderError::Invalid(format!(
2935            "published dependency proof object name differs from the current object: {}",
2936            proof_dir.display()
2937        )));
2938    }
2939    Ok(())
2940}
2941
2942#[allow(clippy::too_many_arguments)]
2943fn restore_object_dependency_proof(
2944    cache_entry: &Path,
2945    object_cache_key: &str,
2946    object_sha256: &str,
2947    closure: &NativeOperatorTranslationUnitDependencyLock,
2948    translation_unit: &NativeOperatorSourceFileLock,
2949    object_path: &Path,
2950    output_compiler_depfile: &Path,
2951    output_depfile: &Path,
2952    toolchain_scope: &NativeOperatorToolchainDependencyScope,
2953) -> Result<Option<NativeOperatorObjectDependencyProof>> {
2954    let proof_dir = cache_entry.join("dependency-proof");
2955    if !proof_dir.exists() {
2956        return Ok(None);
2957    }
2958    let validated = load_validated_dependency_proof(
2959        &proof_dir,
2960        object_cache_key,
2961        object_sha256,
2962        translation_unit,
2963        closure,
2964        toolchain_scope,
2965    )?;
2966    if Path::new(&validated.proof.producer_object_file).file_name() != object_path.file_name() {
2967        return Err(NativeOperatorBuilderError::Invalid(format!(
2968            "cached dependency proof object name differs from restored object: {}",
2969            proof_dir.display()
2970        )));
2971    }
2972    atomic_write_bytes(output_compiler_depfile, &validated.compiler_raw)?;
2973    atomic_write_bytes(output_depfile, &validated.portable_raw)?;
2974    Ok(Some(validated.proof))
2975}
2976
2977fn validate_object_dependency_proof(
2978    context: &str,
2979    proof: &NativeOperatorObjectDependencyProof,
2980    object_cache_key: &str,
2981    object_sha256: &str,
2982    translation_unit: &NativeOperatorSourceFileLock,
2983    closure: &NativeOperatorTranslationUnitDependencyLock,
2984) -> Result<()> {
2985    let dependency_set_sha256 = observed_dependency_set_sha256(&proof.observed_dependencies)?;
2986    let binding_dependencies = validate_depfile_bindings_basic(context, &proof.depfile_bindings)?;
2987    let expected_source = expected_source_dependencies(translation_unit, closure);
2988    let observed_source = proof
2989        .observed_dependencies
2990        .iter()
2991        .filter(|dependency| dependency.domain == NativeOperatorDependencyDomain::Source)
2992        .cloned()
2993        .collect::<BTreeSet<_>>();
2994    validate_normalized_absolute_path(
2995        &proof.producer_working_directory,
2996        &format!("{context} producer_working_directory"),
2997    )?;
2998    validate_normalized_absolute_path(
2999        &proof.producer_object_file,
3000        &format!("{context} producer_object_file"),
3001    )?;
3002    if proof.schema_version != NATIVE_OPERATOR_OBJECT_DEPENDENCY_PROOF_SCHEMA_VERSION
3003        || proof.object_cache_key != object_cache_key
3004        || proof.object_sha256 != object_sha256
3005        || proof.dependency_closure_sha256 != closure.closure_sha256
3006        || proof.dependency_set_sha256 != dependency_set_sha256
3007        || !is_sha256_digest(&proof.object_cache_key)
3008        || !is_sha256_digest(&proof.object_sha256)
3009        || !is_sha256_digest(&proof.dependency_closure_sha256)
3010        || !is_sha256_digest(&proof.dependency_set_sha256)
3011        || !is_sha256_digest(&proof.compiler_depfile_sha256)
3012        || !is_sha256_digest(&proof.depfile_sha256)
3013        || binding_dependencies != proof.observed_dependencies
3014        || observed_source != expected_source
3015    {
3016        return Err(NativeOperatorBuilderError::Invalid(format!(
3017            "{context} object dependency proof is invalid"
3018        )));
3019    }
3020    Ok(())
3021}
3022
3023fn validate_depfile_bindings_basic(
3024    context: &str,
3025    bindings: &[NativeOperatorDepfileDependencyBinding],
3026) -> Result<Vec<NativeOperatorObservedDependency>> {
3027    if bindings.is_empty() || bindings.len() > MAX_DEPFILE_DEPENDENCIES {
3028        return Err(NativeOperatorBuilderError::Invalid(format!(
3029            "{context} depfile bindings are empty or exceed {MAX_DEPFILE_DEPENDENCIES}"
3030        )));
3031    }
3032    let mut dependencies = BTreeSet::new();
3033    for binding in bindings {
3034        if binding.producer_path.is_empty()
3035            || binding.producer_path.len() > MAX_DEPFILE_WORD_BYTES
3036            || binding
3037                .producer_path
3038                .chars()
3039                .any(|character| matches!(character, '\0' | '\n' | '\r'))
3040            || binding.portable_path.is_empty()
3041            || binding.portable_path.len() > MAX_DEPFILE_WORD_BYTES
3042        {
3043            return Err(NativeOperatorBuilderError::Invalid(format!(
3044                "{context} depfile binding path is invalid"
3045            )));
3046        }
3047        validate_observed_dependency(context, &binding.dependency)?;
3048        match binding.dependency.domain {
3049            NativeOperatorDependencyDomain::Source => {
3050                validate_relative_path(&binding.portable_path)?;
3051                if binding.portable_path != binding.dependency.path {
3052                    return Err(NativeOperatorBuilderError::Invalid(format!(
3053                        "{context} source depfile binding does not use its locked path"
3054                    )));
3055                }
3056            }
3057            NativeOperatorDependencyDomain::BackendToolchain
3058            | NativeOperatorDependencyDomain::HostToolchain => {
3059                validate_normalized_absolute_path(
3060                    &binding.portable_path,
3061                    &format!("{context} toolchain depfile binding"),
3062                )?;
3063            }
3064        }
3065        if !dependencies.insert(binding.dependency.clone()) {
3066            return Err(NativeOperatorBuilderError::Invalid(format!(
3067                "{context} depfile bindings duplicate a typed dependency"
3068            )));
3069        }
3070    }
3071    Ok(dependencies.into_iter().collect())
3072}
3073
3074#[allow(clippy::too_many_arguments)]
3075fn validate_portable_depfile_pair(
3076    compiler_raw: &str,
3077    compiler_depfile_path: &Path,
3078    portable_raw: &str,
3079    depfile_path: &Path,
3080    object_path: &str,
3081    working_directory: &str,
3082    translation_unit: &NativeOperatorSourceFileLock,
3083    closure: &NativeOperatorTranslationUnitDependencyLock,
3084    bindings: &[NativeOperatorDepfileDependencyBinding],
3085    toolchain_scope: &NativeOperatorToolchainDependencyScope,
3086) -> Result<Vec<NativeOperatorObservedDependency>> {
3087    let (compiler_target, compiler_dependencies) =
3088        parse_make_depfile(compiler_raw, compiler_depfile_path)?;
3089    let (target, dependencies) = parse_make_depfile(portable_raw, depfile_path)?;
3090    validate_normalized_absolute_path(object_path, "portable depfile producer object")?;
3091    if compiler_target != object_path || target != object_path {
3092        return Err(NativeOperatorBuilderError::Invalid(format!(
3093            "compiler or portable depfile target differs from object file: depfile={} compiler_target={compiler_target} portable_target={target}",
3094            depfile_path.display()
3095        )));
3096    }
3097    validate_normalized_absolute_path(working_directory, "portable depfile working directory")?;
3098    let working_directory = Path::new(working_directory);
3099    let expected = expected_source_dependencies(translation_unit, closure);
3100    let observed = validate_depfile_bindings_basic("<portable-depfile>", bindings)?;
3101    let expected_compiler_dependencies = bindings
3102        .iter()
3103        .map(|binding| binding.producer_path.as_str())
3104        .collect::<Vec<_>>();
3105    if compiler_dependencies
3106        != expected_compiler_dependencies
3107            .iter()
3108            .map(|value| (*value).to_string())
3109            .collect::<Vec<_>>()
3110    {
3111        return Err(NativeOperatorBuilderError::Invalid(
3112            "compiler depfile bytes differ from their ordered typed bindings".to_string(),
3113        ));
3114    }
3115    let portable_by_dependency = bindings
3116        .iter()
3117        .map(|binding| (binding.dependency.clone(), binding.portable_path.clone()))
3118        .collect::<BTreeMap<_, _>>();
3119    let expected_portable_dependencies = observed
3120        .iter()
3121        .map(|dependency| {
3122            portable_by_dependency
3123                .get(dependency)
3124                .cloned()
3125                .expect("binding dependencies were collected from the same rows")
3126        })
3127        .collect::<Vec<_>>();
3128    let canonical_portable =
3129        serialize_portable_depfile(object_path, &expected_portable_dependencies)?;
3130    if dependencies != expected_portable_dependencies
3131        || portable_raw.as_bytes() != canonical_portable.as_slice()
3132    {
3133        return Err(NativeOperatorBuilderError::Invalid(
3134            "portable depfile bytes differ from their canonical typed bindings".to_string(),
3135        ));
3136    }
3137    for binding in bindings {
3138        match binding.dependency.domain {
3139            NativeOperatorDependencyDomain::Source => {
3140                let producer = Path::new(&binding.producer_path);
3141                let relative = if producer.is_absolute() {
3142                    producer.strip_prefix(working_directory).map_err(|_| {
3143                        NativeOperatorBuilderError::Invalid(format!(
3144                            "source compiler depfile path escapes its recorded working directory: {}",
3145                            binding.producer_path
3146                        ))
3147                    })?
3148                } else {
3149                    producer
3150                };
3151                if normalize_portable_relative_path(relative)? != binding.dependency.path {
3152                    return Err(NativeOperatorBuilderError::Invalid(format!(
3153                        "source compiler depfile path differs from its locked identity: {}",
3154                        binding.producer_path
3155                    )));
3156                }
3157            }
3158            NativeOperatorDependencyDomain::BackendToolchain
3159            | NativeOperatorDependencyDomain::HostToolchain => {
3160                let normalized_producer = normalize_absolute_posix_path_lexically(
3161                    &binding.producer_path,
3162                    "toolchain compiler depfile binding",
3163                )?;
3164                if toolchain_scope.by_absolute_path.get(&normalized_producer)
3165                    != Some(&binding.dependency)
3166                    || toolchain_scope.by_absolute_path.get(&binding.portable_path)
3167                        != Some(&binding.dependency)
3168                {
3169                    return Err(NativeOperatorBuilderError::Invalid(format!(
3170                        "toolchain depfile binding is outside its typed manifest: producer={} portable={}",
3171                        binding.producer_path, binding.portable_path
3172                    )));
3173                }
3174            }
3175        }
3176    }
3177    let observed_source = observed
3178        .iter()
3179        .filter(|dependency| dependency.domain == NativeOperatorDependencyDomain::Source)
3180        .cloned()
3181        .collect::<BTreeSet<_>>();
3182    if observed_source != expected {
3183        return Err(NativeOperatorBuilderError::Invalid(format!(
3184            "portable depfile differs from locked source dependency closure: expected={expected:?} observed={observed_source:?}"
3185        )));
3186    }
3187    validate_observed_dependencies("<portable-depfile>", &observed)?;
3188    for dependency in &observed {
3189        if !bindings
3190            .iter()
3191            .any(|binding| &binding.dependency == dependency)
3192        {
3193            return Err(NativeOperatorBuilderError::Invalid(format!(
3194                "portable depfile is missing a binding for {:?}:{}",
3195                dependency.domain, dependency.path
3196            )));
3197        }
3198    }
3199    Ok(observed)
3200}
3201
3202fn normalize_absolute_posix_path_lexically(value: &str, label: &str) -> Result<String> {
3203    if !value.starts_with('/')
3204        || value.contains('\\')
3205        || value
3206            .chars()
3207            .any(|character| matches!(character, '\0' | '\n' | '\r'))
3208    {
3209        return Err(NativeOperatorBuilderError::Invalid(format!(
3210            "{label} must be an absolute POSIX path: {value}"
3211        )));
3212    }
3213    let mut components = Vec::new();
3214    for component in value.split('/') {
3215        match component {
3216            "" | "." => {}
3217            ".." => {
3218                if components.pop().is_none() {
3219                    return Err(NativeOperatorBuilderError::Invalid(format!(
3220                        "{label} escapes the filesystem root: {value}"
3221                    )));
3222                }
3223            }
3224            _ => components.push(component),
3225        }
3226    }
3227    if components.is_empty() {
3228        Ok("/".to_string())
3229    } else {
3230        Ok(format!("/{}", components.join("/")))
3231    }
3232}
3233
3234fn validate_normalized_absolute_path(value: &str, label: &str) -> Result<()> {
3235    if value == "/" {
3236        return Ok(());
3237    }
3238    if !value.starts_with('/')
3239        || value.contains('\\')
3240        || value.ends_with('/')
3241        || value[1..]
3242            .split('/')
3243            .any(|component| component.is_empty() || component == "." || component == "..")
3244    {
3245        return Err(NativeOperatorBuilderError::Invalid(format!(
3246            "{label} must be a normalized absolute POSIX path: {value}"
3247        )));
3248    }
3249    Ok(())
3250}
3251
3252fn normalize_portable_relative_path(path: &Path) -> Result<String> {
3253    let raw = path.to_str().ok_or_else(|| {
3254        NativeOperatorBuilderError::Invalid(format!(
3255            "depfile path is not valid UTF-8: {}",
3256            path.display()
3257        ))
3258    })?;
3259    if raw.contains('\\')
3260        || raw
3261            .chars()
3262            .any(|character| matches!(character, '\0' | '\n' | '\r'))
3263    {
3264        return Err(NativeOperatorBuilderError::Invalid(format!(
3265            "depfile path is not a relative POSIX path: {}",
3266            path.display()
3267        )));
3268    }
3269    let mut components = Vec::new();
3270    for component in path.components() {
3271        match component {
3272            std::path::Component::Normal(value) => {
3273                components.push(value.to_str().ok_or_else(|| {
3274                    NativeOperatorBuilderError::Invalid(format!(
3275                        "depfile path is not valid UTF-8: {}",
3276                        path.display()
3277                    ))
3278                })?)
3279            }
3280            std::path::Component::CurDir => {}
3281            std::path::Component::ParentDir => {
3282                if components.pop().is_none() {
3283                    return Err(NativeOperatorBuilderError::Invalid(format!(
3284                        "depfile path escapes its working directory: {}",
3285                        path.display()
3286                    )));
3287                }
3288            }
3289            _ => {
3290                return Err(NativeOperatorBuilderError::Invalid(format!(
3291                    "depfile path is not normalized beneath its working directory: {}",
3292                    path.display()
3293                )))
3294            }
3295        }
3296    }
3297    let normalized = components.join("/");
3298    validate_relative_path(&normalized)?;
3299    Ok(normalized)
3300}
3301
3302fn read_bounded_regular_file(path: &Path, max_bytes: usize, label: &str) -> Result<Vec<u8>> {
3303    let mut options = OpenOptions::new();
3304    options.read(true);
3305    #[cfg(unix)]
3306    options.custom_flags(libc::O_NOFOLLOW);
3307    let file = options
3308        .open(path)
3309        .map_err(|source| NativeOperatorBuilderError::Io {
3310            path: path.to_path_buf(),
3311            source,
3312        })?;
3313    let metadata = file
3314        .metadata()
3315        .map_err(|source| NativeOperatorBuilderError::Io {
3316            path: path.to_path_buf(),
3317            source,
3318        })?;
3319    if !metadata.is_file() || metadata.len() > max_bytes as u64 {
3320        return Err(NativeOperatorBuilderError::Invalid(format!(
3321            "{label} is not a regular file or exceeds {max_bytes} bytes: {}",
3322            path.display()
3323        )));
3324    }
3325    let mut bytes = Vec::with_capacity(metadata.len() as usize);
3326    file.take(max_bytes as u64 + 1)
3327        .read_to_end(&mut bytes)
3328        .map_err(|source| NativeOperatorBuilderError::Io {
3329            path: path.to_path_buf(),
3330            source,
3331        })?;
3332    if bytes.len() > max_bytes {
3333        return Err(NativeOperatorBuilderError::Invalid(format!(
3334            "{label} grew beyond {max_bytes} bytes while reading: {}",
3335            path.display()
3336        )));
3337    }
3338    Ok(bytes)
3339}
3340
3341fn atomic_write_bytes(destination: &Path, bytes: &[u8]) -> Result<()> {
3342    let parent = destination.parent().ok_or_else(|| {
3343        NativeOperatorBuilderError::Invalid(format!(
3344            "atomic write destination has no parent: {}",
3345            destination.display()
3346        ))
3347    })?;
3348    fs::create_dir_all(parent).map_err(|source| NativeOperatorBuilderError::Io {
3349        path: parent.to_path_buf(),
3350        source,
3351    })?;
3352    let mut temporary =
3353        NamedTempFile::new_in(parent).map_err(|source| NativeOperatorBuilderError::Io {
3354            path: parent.to_path_buf(),
3355            source,
3356        })?;
3357    temporary
3358        .write_all(bytes)
3359        .map_err(|source| NativeOperatorBuilderError::Io {
3360            path: temporary.path().to_path_buf(),
3361            source,
3362        })?;
3363    temporary
3364        .as_file_mut()
3365        .flush()
3366        .and_then(|()| temporary.as_file().sync_all())
3367        .map_err(|source| NativeOperatorBuilderError::Io {
3368            path: temporary.path().to_path_buf(),
3369            source,
3370        })?;
3371    temporary
3372        .persist(destination)
3373        .map_err(|error| NativeOperatorBuilderError::Io {
3374            path: destination.to_path_buf(),
3375            source: error.error,
3376        })?;
3377    Ok(())
3378}
3379
3380fn parse_make_depfile(raw: &str, path: &Path) -> Result<(String, Vec<String>)> {
3381    if raw.len() > MAX_DEPFILE_BYTES || raw.trim().is_empty() || raw.as_bytes().contains(&0) {
3382        return Err(NativeOperatorBuilderError::Invalid(format!(
3383            "depfile is too large, empty, or contains NUL: {}",
3384            path.display()
3385        )));
3386    }
3387    let normalized = raw.replace("\\\r\n", "").replace("\\\n", "");
3388    let normalized = normalized.trim_end_matches(&['\r', '\n'][..]);
3389    if normalized.contains('\n') || normalized.contains('\r') {
3390        return Err(NativeOperatorBuilderError::Invalid(format!(
3391            "depfile contains more than one make rule: {}",
3392            path.display()
3393        )));
3394    }
3395    let mut escaped = false;
3396    let mut delimiter = None;
3397    for (index, character) in normalized.char_indices() {
3398        if escaped {
3399            escaped = false;
3400            continue;
3401        }
3402        match character {
3403            '\\' => escaped = true,
3404            ':' => {
3405                delimiter = Some(index);
3406                break;
3407            }
3408            '\n' | '\r' => {
3409                return Err(NativeOperatorBuilderError::Invalid(format!(
3410                    "depfile contains multiple or unterminated rules: {}",
3411                    path.display()
3412                )))
3413            }
3414            _ => {}
3415        }
3416    }
3417    let delimiter = delimiter.ok_or_else(|| {
3418        NativeOperatorBuilderError::Invalid(format!(
3419            "depfile has no make target delimiter: {}",
3420            path.display()
3421        ))
3422    })?;
3423    let targets = parse_make_words(&normalized[..delimiter], path, 1)?;
3424    let dependencies =
3425        parse_make_words(&normalized[delimiter + 1..], path, MAX_DEPFILE_DEPENDENCIES)?;
3426    if targets.len() != 1 || dependencies.is_empty() {
3427        return Err(NativeOperatorBuilderError::Invalid(format!(
3428            "depfile target/dependency count or word size is invalid: {}",
3429            path.display()
3430        )));
3431    }
3432    Ok((targets[0].clone(), dependencies))
3433}
3434
3435fn parse_make_words(value: &str, path: &Path, max_words: usize) -> Result<Vec<String>> {
3436    let mut words = Vec::new();
3437    let mut word = String::new();
3438    let mut escaped = false;
3439    for character in value.chars() {
3440        if escaped {
3441            word.push(character);
3442            if word.len() > MAX_DEPFILE_WORD_BYTES {
3443                return Err(NativeOperatorBuilderError::Invalid(format!(
3444                    "depfile word exceeds {MAX_DEPFILE_WORD_BYTES} bytes: {}",
3445                    path.display()
3446                )));
3447            }
3448            escaped = false;
3449            continue;
3450        }
3451        match character {
3452            '\\' => escaped = true,
3453            '\n' | '\r' => unreachable!("newlines rejected before make-word parsing"),
3454            character if character.is_whitespace() => {
3455                if !word.is_empty() {
3456                    if words.len() >= max_words {
3457                        return Err(NativeOperatorBuilderError::Invalid(format!(
3458                            "depfile exceeds its word-count limit: {}",
3459                            path.display()
3460                        )));
3461                    }
3462                    words.push(std::mem::take(&mut word));
3463                }
3464            }
3465            _ => {
3466                word.push(character);
3467                if word.len() > MAX_DEPFILE_WORD_BYTES {
3468                    return Err(NativeOperatorBuilderError::Invalid(format!(
3469                        "depfile word exceeds {MAX_DEPFILE_WORD_BYTES} bytes: {}",
3470                        path.display()
3471                    )));
3472                }
3473            }
3474        }
3475    }
3476    if escaped {
3477        return Err(NativeOperatorBuilderError::Invalid(format!(
3478            "depfile ends with an incomplete escape: {}",
3479            path.display()
3480        )));
3481    }
3482    if !word.is_empty() {
3483        if words.len() >= max_words {
3484            return Err(NativeOperatorBuilderError::Invalid(format!(
3485                "depfile exceeds its word-count limit: {}",
3486                path.display()
3487            )));
3488        }
3489        words.push(word);
3490    }
3491    Ok(words)
3492}
3493
3494fn locked_source_file(root: &Path, relative: &str) -> Result<PathBuf> {
3495    validate_relative_path(relative)?;
3496    let path = root.join(relative);
3497    let metadata =
3498        fs::symlink_metadata(&path).map_err(|source| NativeOperatorBuilderError::Io {
3499            path: path.clone(),
3500            source,
3501        })?;
3502    if metadata.file_type().is_symlink() {
3503        return Err(NativeOperatorBuilderError::Invalid(format!(
3504            "locked source files must not be symlinks: {relative}"
3505        )));
3506    }
3507    let canonical = path
3508        .canonicalize()
3509        .map_err(|source| NativeOperatorBuilderError::Io {
3510            path: path.clone(),
3511            source,
3512        })?;
3513    if !canonical.starts_with(root) || !canonical.is_file() {
3514        return Err(NativeOperatorBuilderError::Invalid(format!(
3515            "locked source file escapes source root or is not a file: {relative}"
3516        )));
3517    }
3518    Ok(canonical)
3519}
3520
3521fn resolve_static_toolchain(
3522    request: &NativeOperatorSourceBuildRequest,
3523) -> Result<NativeOperatorSourceBuildToolchain> {
3524    let invocation_root_path = if request.cuda_toolkit_root.is_absolute() {
3525        request.cuda_toolkit_root.clone()
3526    } else {
3527        std::env::current_dir()
3528            .map_err(|source| NativeOperatorBuilderError::Io {
3529                path: PathBuf::from("."),
3530                source,
3531            })?
3532            .join(&request.cuda_toolkit_root)
3533    };
3534    let invocation_root = invocation_root_path.to_str().ok_or_else(|| {
3535        NativeOperatorBuilderError::Invalid(format!(
3536            "cuda_toolkit_root is not valid UTF-8: {}",
3537            invocation_root_path.display()
3538        ))
3539    })?;
3540    validate_normalized_absolute_path(invocation_root, "cuda_toolkit_root")?;
3541    let canonical_root = request.cuda_toolkit_root.canonicalize().map_err(|source| {
3542        NativeOperatorBuilderError::Io {
3543            path: request.cuda_toolkit_root.clone(),
3544            source,
3545        }
3546    })?;
3547    if !canonical_root.is_dir() {
3548        return Err(NativeOperatorBuilderError::Invalid(format!(
3549            "cuda_toolkit_root is not a directory: {}",
3550            canonical_root.display()
3551        )));
3552    }
3553    let canonical_root_string = canonical_root.to_str().ok_or_else(|| {
3554        NativeOperatorBuilderError::Invalid(format!(
3555            "canonical cuda_toolkit_root is not valid UTF-8: {}",
3556            canonical_root.display()
3557        ))
3558    })?;
3559    validate_normalized_absolute_path(canonical_root_string, "canonical cuda_toolkit_root")?;
3560    let nvcc = tool_file_identity(&request.nvcc_path)?;
3561    let canonical_nvcc = Path::new(&nvcc.path);
3562    if !canonical_nvcc.starts_with(&canonical_root) {
3563        return Err(NativeOperatorBuilderError::Invalid(format!(
3564            "nvcc must resolve inside cuda_toolkit_root: nvcc={} root={}",
3565            canonical_nvcc.display(),
3566            canonical_root.display()
3567        )));
3568    }
3569    let manifest = build_cuda_toolkit_manifest(&canonical_root)?;
3570    if !manifest.entries.iter().any(|entry| {
3571        canonical_root.join(&entry.resolved_path) == canonical_nvcc
3572            && entry.sha256 == nvcc.sha256
3573            && entry.size_bytes == nvcc.size_bytes
3574    }) {
3575        return Err(NativeOperatorBuilderError::Invalid(format!(
3576            "cuda toolkit manifest does not contain the selected nvcc: {}",
3577            canonical_nvcc.display()
3578        )));
3579    }
3580    let manifest_relative = "toolchain/cuda-static-manifest.json";
3581    let manifest_path = request.output_dir.join(manifest_relative);
3582    let manifest_parent = manifest_path
3583        .parent()
3584        .expect("cuda toolkit manifest has a parent directory");
3585    fs::create_dir_all(manifest_parent).map_err(|source| NativeOperatorBuilderError::Io {
3586        path: manifest_parent.to_path_buf(),
3587        source,
3588    })?;
3589    write_json(&manifest_path, &manifest)?;
3590    let manifest_size = fs::metadata(&manifest_path)
3591        .map_err(|source| NativeOperatorBuilderError::Io {
3592            path: manifest_path.clone(),
3593            source,
3594        })?
3595        .len();
3596    let manifest_evidence = NativeOperatorEvidenceFile {
3597        path: manifest_relative.to_string(),
3598        sha256: sha256_file(&manifest_path)?,
3599        size_bytes: manifest_size,
3600    };
3601    let host_toolchain = resolve_host_toolchain(request)?;
3602    Ok(NativeOperatorSourceBuildToolchain {
3603        static_identity: NativeOperatorSourceBuildStaticToolchain {
3604            backend: NativeOperatorBackend::Cuda,
3605            compiler_driver: NativeOperatorSourceCompilerDriver::CudaNvcc,
3606            cuda_toolkit: NativeOperatorCudaToolkitIdentity {
3607                canonical_root: canonical_root_string.to_string(),
3608                invocation_root: invocation_root.to_string(),
3609                release_version: cuda_toolkit_release_version(&canonical_root)?,
3610                nvcc,
3611                manifest: manifest_evidence,
3612            },
3613            host_toolchain,
3614            archiver: tool_file_identity(&request.ar_path)?,
3615        },
3616        miss_probe: None,
3617    })
3618}
3619
3620fn resolve_host_toolchain(
3621    request: &NativeOperatorSourceBuildRequest,
3622) -> Result<NativeOperatorHostToolchainIdentity> {
3623    let compiler = tool_file_identity(&request.ccbin_path)?;
3624    let cache_key = sha256_bytes(format!("{}\n{}\n", compiler.path, compiler.sha256).as_bytes());
3625    let cache_dir = request
3626        .object_cache_dir
3627        .join(".host-toolchains")
3628        .join(cache_key);
3629    fs::create_dir_all(&cache_dir).map_err(|source| NativeOperatorBuilderError::Io {
3630        path: cache_dir.clone(),
3631        source,
3632    })?;
3633    let cached_path = cache_dir.join("manifest.json");
3634    let environment = effective_environment_for_tool_paths([
3635        request.nvcc_path.to_str().unwrap_or(""),
3636        compiler.path.as_str(),
3637        request.ar_path.to_str().unwrap_or(""),
3638    ])?;
3639    let cached = cached_path
3640        .is_file()
3641        .then(|| read_json::<NativeOperatorHostToolchainManifest>(&cached_path))
3642        .transpose()
3643        .ok()
3644        .flatten()
3645        .filter(|manifest| {
3646            validate_host_toolchain_manifest("<host-toolchain-cache>", manifest).is_ok()
3647        });
3648    let manifest = match cached {
3649        Some(cached)
3650            if host_toolchain_manifest_matches_current(&cached, &compiler, &environment)? =>
3651        {
3652            cached
3653        }
3654        _ => {
3655            let probed = probe_host_toolchain_manifest(&compiler, &environment)?;
3656            write_json(&cached_path, &probed)?;
3657            probed
3658        }
3659    };
3660
3661    let relative = "toolchain/host-static-manifest.json";
3662    let output_path = request.output_dir.join(relative);
3663    write_json(&output_path, &manifest)?;
3664    let size_bytes = fs::metadata(&output_path)
3665        .map_err(|source| NativeOperatorBuilderError::Io {
3666            path: output_path.clone(),
3667            source,
3668        })?
3669        .len();
3670    Ok(NativeOperatorHostToolchainIdentity {
3671        compiler: manifest.compiler.clone(),
3672        compiler_version: manifest.compiler_version.clone(),
3673        target: manifest.target.clone(),
3674        manifest: NativeOperatorEvidenceFile {
3675            path: relative.to_string(),
3676            sha256: sha256_file(&output_path)?,
3677            size_bytes,
3678        },
3679    })
3680}
3681
3682fn probe_host_toolchain_manifest(
3683    compiler: &NativeOperatorToolFileIdentity,
3684    environment: &BTreeMap<String, String>,
3685) -> Result<NativeOperatorHostToolchainManifest> {
3686    let compiler_path = Path::new(&compiler.path);
3687    let compiler_version = host_compiler_output(compiler_path, &["--version"], b"", environment)?;
3688    let target = host_compiler_output(compiler_path, &["-dumpmachine"], b"", environment)?;
3689    if target.is_empty() || target.len() > 256 || target.chars().any(char::is_whitespace) {
3690        return Err(NativeOperatorBuilderError::Invalid(format!(
3691            "host compiler produced an invalid target: {}",
3692            compiler.path
3693        )));
3694    }
3695
3696    let discovery_probe = probe_host_compiler_discovery(compiler_path, environment)?;
3697
3698    let mut executable_inputs = BTreeMap::new();
3699    executable_inputs.insert(compiler.path.clone(), compiler.clone());
3700    for program in HOST_TOOLCHAIN_PROGRAMS {
3701        let value = host_compiler_output(
3702            compiler_path,
3703            &[&format!("-print-prog-name={program}")],
3704            b"",
3705            environment,
3706        )?;
3707        if let Some(path) = resolve_host_program(&value, environment)? {
3708            let identity = tool_file_identity(&path)?;
3709            executable_inputs.insert(identity.path.clone(), identity);
3710        }
3711    }
3712
3713    let mut discovery_roots = executable_inputs
3714        .values()
3715        .filter_map(|identity| Path::new(&identity.path).parent())
3716        .map(|path| path.display().to_string())
3717        .collect::<Vec<_>>();
3718    discovery_roots.sort();
3719    discovery_roots.dedup();
3720    let scope_roots = discovery_probe
3721        .include_roots
3722        .iter()
3723        .chain(discovery_roots.iter())
3724        .cloned()
3725        .collect::<Vec<_>>();
3726    let files = collect_host_toolchain_scope_files(&scope_roots)?;
3727    let manifest = NativeOperatorHostToolchainManifest {
3728        schema_version: NATIVE_OPERATOR_HOST_TOOLCHAIN_MANIFEST_SCHEMA_VERSION,
3729        compiler: compiler.clone(),
3730        compiler_version,
3731        target,
3732        executable_inputs: executable_inputs.into_values().collect(),
3733        include_roots: discovery_probe.include_roots,
3734        include_probe_sha256: discovery_probe.include_probe_sha256,
3735        driver_probe_sha256: discovery_probe.driver_probe_sha256,
3736        discovery_roots,
3737        files,
3738    };
3739    validate_host_toolchain_manifest("<host-toolchain-probe>", &manifest)?;
3740    Ok(manifest)
3741}
3742
3743fn rebuild_host_toolchain_manifest(
3744    recorded: &NativeOperatorHostToolchainManifest,
3745) -> Result<NativeOperatorHostToolchainManifest> {
3746    validate_host_toolchain_manifest("<host-toolchain-recorded>", recorded)?;
3747    let compiler = tool_file_identity(Path::new(&recorded.compiler.path))?;
3748    let executable_inputs = recorded
3749        .executable_inputs
3750        .iter()
3751        .map(|identity| tool_file_identity(Path::new(&identity.path)))
3752        .collect::<Result<Vec<_>>>()?;
3753    let scope_roots = recorded
3754        .include_roots
3755        .iter()
3756        .chain(recorded.discovery_roots.iter())
3757        .cloned()
3758        .collect::<Vec<_>>();
3759    let files = collect_host_toolchain_scope_files(&scope_roots)?;
3760    let current = NativeOperatorHostToolchainManifest {
3761        schema_version: NATIVE_OPERATOR_HOST_TOOLCHAIN_MANIFEST_SCHEMA_VERSION,
3762        compiler,
3763        compiler_version: recorded.compiler_version.clone(),
3764        target: recorded.target.clone(),
3765        executable_inputs,
3766        include_roots: recorded.include_roots.clone(),
3767        include_probe_sha256: recorded.include_probe_sha256.clone(),
3768        driver_probe_sha256: recorded.driver_probe_sha256.clone(),
3769        discovery_roots: recorded.discovery_roots.clone(),
3770        files,
3771    };
3772    validate_host_toolchain_manifest("<host-toolchain-current>", &current)?;
3773    Ok(current)
3774}
3775
3776struct HostCompilerDiscoveryProbe {
3777    include_roots: Vec<String>,
3778    include_probe_sha256: String,
3779    driver_probe_sha256: String,
3780}
3781
3782fn probe_host_compiler_discovery(
3783    compiler: &Path,
3784    environment: &BTreeMap<String, String>,
3785) -> Result<HostCompilerDiscoveryProbe> {
3786    let include_probe = host_compiler_raw_output(
3787        compiler,
3788        &["-E", "-x", "c++", "-", "-v"],
3789        b"\n",
3790        environment,
3791    )?;
3792    let include_roots = parse_host_compiler_include_roots(
3793        &String::from_utf8_lossy(&include_probe.stderr),
3794        compiler,
3795    )?;
3796    let driver_probe = host_compiler_raw_output(
3797        compiler,
3798        &["-###", "-pipe", "-x", "c++", "-c", "-", "-o", "/dev/null"],
3799        b"\n",
3800        environment,
3801    )?;
3802    Ok(HostCompilerDiscoveryProbe {
3803        include_roots,
3804        include_probe_sha256: compiler_probe_sha256(&include_probe),
3805        driver_probe_sha256: compiler_probe_sha256(&driver_probe),
3806    })
3807}
3808
3809fn compiler_probe_sha256(output: &std::process::Output) -> String {
3810    let mut identity = Vec::with_capacity(output.stdout.len() + output.stderr.len() + 16);
3811    identity.extend_from_slice(&(output.stdout.len() as u64).to_le_bytes());
3812    identity.extend_from_slice(&output.stdout);
3813    identity.extend_from_slice(&(output.stderr.len() as u64).to_le_bytes());
3814    identity.extend_from_slice(&output.stderr);
3815    sha256_bytes(&identity)
3816}
3817
3818fn host_toolchain_manifest_matches_current(
3819    recorded: &NativeOperatorHostToolchainManifest,
3820    compiler: &NativeOperatorToolFileIdentity,
3821    environment: &BTreeMap<String, String>,
3822) -> Result<bool> {
3823    if &recorded.compiler != compiler || rebuild_host_toolchain_manifest(recorded)? != *recorded {
3824        return Ok(false);
3825    }
3826    let discovery = probe_host_compiler_discovery(Path::new(&compiler.path), environment)?;
3827    Ok(discovery.include_roots == recorded.include_roots
3828        && discovery.include_probe_sha256 == recorded.include_probe_sha256
3829        && discovery.driver_probe_sha256 == recorded.driver_probe_sha256)
3830}
3831
3832fn host_compiler_output(
3833    compiler: &Path,
3834    args: &[&str],
3835    stdin: &[u8],
3836    environment: &BTreeMap<String, String>,
3837) -> Result<String> {
3838    let output = host_compiler_raw_output(compiler, args, stdin, environment)?;
3839    let value = format!(
3840        "{}{}",
3841        String::from_utf8_lossy(&output.stdout),
3842        String::from_utf8_lossy(&output.stderr)
3843    )
3844    .trim()
3845    .chars()
3846    .take(16_384)
3847    .collect::<String>();
3848    if value.is_empty() {
3849        return Err(NativeOperatorBuilderError::Invalid(format!(
3850            "host compiler produced no output for {:?}: {}",
3851            args,
3852            compiler.display()
3853        )));
3854    }
3855    Ok(value)
3856}
3857
3858fn host_compiler_raw_output(
3859    compiler: &Path,
3860    args: &[&str],
3861    stdin: &[u8],
3862    environment: &BTreeMap<String, String>,
3863) -> Result<std::process::Output> {
3864    let mut child = Command::new(compiler)
3865        .args(args)
3866        .env_clear()
3867        .envs(environment)
3868        .stdin(Stdio::piped())
3869        .stdout(Stdio::piped())
3870        .stderr(Stdio::piped())
3871        .spawn()
3872        .map_err(|source| NativeOperatorBuilderError::Io {
3873            path: compiler.to_path_buf(),
3874            source,
3875        })?;
3876    child
3877        .stdin
3878        .take()
3879        .expect("host compiler stdin is piped")
3880        .write_all(stdin)
3881        .map_err(|source| NativeOperatorBuilderError::Io {
3882            path: compiler.to_path_buf(),
3883            source,
3884        })?;
3885    let output = child
3886        .wait_with_output()
3887        .map_err(|source| NativeOperatorBuilderError::Io {
3888            path: compiler.to_path_buf(),
3889            source,
3890        })?;
3891    if !output.status.success() {
3892        return Err(NativeOperatorBuilderError::Invalid(format!(
3893            "host compiler probe failed for {:?}: path={} status={}",
3894            args,
3895            compiler.display(),
3896            output.status
3897        )));
3898    }
3899    Ok(output)
3900}
3901
3902fn parse_host_compiler_include_roots(stderr: &str, compiler: &Path) -> Result<Vec<String>> {
3903    let mut in_search_list = false;
3904    let mut roots = Vec::new();
3905    let mut seen = BTreeSet::new();
3906    for line in stderr.lines() {
3907        let trimmed = line.trim();
3908        if trimmed == "#include <...> search starts here:" {
3909            in_search_list = true;
3910            continue;
3911        }
3912        if in_search_list && trimmed == "End of search list." {
3913            break;
3914        }
3915        if !in_search_list || trimmed.is_empty() {
3916            continue;
3917        }
3918        let path = trimmed
3919            .strip_suffix(" (framework directory)")
3920            .unwrap_or(trimmed);
3921        validate_normalized_absolute_path(path, "host compiler include search entry")?;
3922        let canonical =
3923            Path::new(path)
3924                .canonicalize()
3925                .map_err(|source| NativeOperatorBuilderError::Io {
3926                    path: PathBuf::from(path),
3927                    source,
3928                })?;
3929        if !canonical.is_dir() {
3930            return Err(NativeOperatorBuilderError::Invalid(format!(
3931                "host compiler include search entry is not a directory: {}",
3932                canonical.display()
3933            )));
3934        }
3935        if seen.insert(path.to_string()) {
3936            roots.push(path.to_string());
3937        }
3938    }
3939    if roots.is_empty() {
3940        return Err(NativeOperatorBuilderError::Invalid(format!(
3941            "host compiler include search probe produced no roots: {}",
3942            compiler.display()
3943        )));
3944    }
3945    Ok(roots)
3946}
3947
3948fn resolve_host_program(
3949    value: &str,
3950    environment: &BTreeMap<String, String>,
3951) -> Result<Option<PathBuf>> {
3952    if value.is_empty()
3953        || value
3954            .chars()
3955            .any(|character| matches!(character, '\n' | '\r'))
3956    {
3957        return Err(NativeOperatorBuilderError::Invalid(format!(
3958            "host compiler returned an invalid program path: {value:?}"
3959        )));
3960    }
3961    let candidate = Path::new(value);
3962    if candidate.is_absolute() {
3963        return Ok(candidate.is_file().then(|| candidate.to_path_buf()));
3964    }
3965    let path = environment.get("PATH").ok_or_else(|| {
3966        NativeOperatorBuilderError::Invalid(
3967            "host compiler probe environment has no PATH".to_string(),
3968        )
3969    })?;
3970    Ok(std::env::split_paths(std::ffi::OsStr::new(path))
3971        .map(|directory| directory.join(candidate))
3972        .find(|candidate| candidate.is_file()))
3973}
3974
3975fn collect_host_toolchain_scope_files(
3976    roots: &[String],
3977) -> Result<Vec<NativeOperatorHostToolchainFileIdentity>> {
3978    let mut files = BTreeMap::new();
3979    for root in roots {
3980        collect_host_toolchain_files(Path::new(root), &mut BTreeSet::new(), &mut files)?;
3981    }
3982    if files.len() > MAX_HOST_TOOLCHAIN_FILES {
3983        return Err(NativeOperatorBuilderError::Invalid(format!(
3984            "host toolchain manifest must contain at most {MAX_HOST_TOOLCHAIN_FILES} sorted unique files"
3985        )));
3986    }
3987    Ok(files.into_values().collect())
3988}
3989
3990fn collect_host_toolchain_files(
3991    directory: &Path,
3992    active_directories: &mut BTreeSet<PathBuf>,
3993    files: &mut BTreeMap<String, NativeOperatorHostToolchainFileIdentity>,
3994) -> Result<()> {
3995    let resolved_directory =
3996        directory
3997            .canonicalize()
3998            .map_err(|source| NativeOperatorBuilderError::Io {
3999                path: directory.to_path_buf(),
4000                source,
4001            })?;
4002    if !resolved_directory.is_dir() {
4003        return Err(NativeOperatorBuilderError::Invalid(format!(
4004            "host toolchain scope is not a directory: {}",
4005            directory.display()
4006        )));
4007    }
4008    if !active_directories.insert(resolved_directory.clone()) {
4009        return Ok(());
4010    }
4011    let mut children = fs::read_dir(directory)
4012        .map_err(|source| NativeOperatorBuilderError::Io {
4013            path: directory.to_path_buf(),
4014            source,
4015        })?
4016        .collect::<std::result::Result<Vec<_>, _>>()
4017        .map_err(|source| NativeOperatorBuilderError::Io {
4018            path: directory.to_path_buf(),
4019            source,
4020        })?;
4021    children.sort_by_key(|entry| entry.file_name());
4022    for child in children {
4023        let logical = child.path();
4024        let resolved = logical
4025            .canonicalize()
4026            .map_err(|source| NativeOperatorBuilderError::Io {
4027                path: logical.clone(),
4028                source,
4029            })?;
4030        if resolved.is_dir() {
4031            collect_host_toolchain_files(&logical, active_directories, files)?;
4032        } else if resolved.is_file() {
4033            let logical_path = logical.display().to_string();
4034            if !files.contains_key(&logical_path) && files.len() >= MAX_HOST_TOOLCHAIN_FILES {
4035                return Err(NativeOperatorBuilderError::Invalid(format!(
4036                    "host toolchain manifest exceeds {MAX_HOST_TOOLCHAIN_FILES} files"
4037                )));
4038            }
4039            let size_bytes = fs::metadata(&resolved)
4040                .map_err(|source| NativeOperatorBuilderError::Io {
4041                    path: resolved.clone(),
4042                    source,
4043                })?
4044                .len();
4045            let identity = NativeOperatorHostToolchainFileIdentity {
4046                logical_path,
4047                resolved_path: resolved.display().to_string(),
4048                sha256: sha256_file(&resolved)?,
4049                size_bytes,
4050            };
4051            if let Some(existing) = files.insert(identity.logical_path.clone(), identity.clone()) {
4052                if existing != identity {
4053                    return Err(NativeOperatorBuilderError::Invalid(format!(
4054                        "host toolchain scope resolved inconsistently: {}",
4055                        identity.logical_path
4056                    )));
4057                }
4058            }
4059        } else {
4060            return Err(NativeOperatorBuilderError::Invalid(format!(
4061                "host toolchain scope contains a non-file entry: {}",
4062                logical.display()
4063            )));
4064        }
4065    }
4066    active_directories.remove(&resolved_directory);
4067    Ok(())
4068}
4069
4070fn validate_host_toolchain_manifest(
4071    context: &str,
4072    manifest: &NativeOperatorHostToolchainManifest,
4073) -> Result<()> {
4074    if manifest.schema_version != NATIVE_OPERATOR_HOST_TOOLCHAIN_MANIFEST_SCHEMA_VERSION
4075        || manifest.compiler_version.trim().is_empty()
4076        || manifest.target.trim().is_empty()
4077        || manifest.target.len() > 256
4078        || manifest.target.chars().any(char::is_whitespace)
4079        || manifest.executable_inputs.is_empty()
4080        || manifest.include_roots.is_empty()
4081        || !is_sha256_digest(&manifest.include_probe_sha256)
4082        || !is_sha256_digest(&manifest.driver_probe_sha256)
4083        || manifest.discovery_roots.is_empty()
4084        || manifest.files.is_empty()
4085        || manifest
4086            .executable_inputs
4087            .windows(2)
4088            .any(|pair| pair[0].path >= pair[1].path)
4089        || manifest.include_roots.iter().collect::<BTreeSet<_>>().len()
4090            != manifest.include_roots.len()
4091        || manifest
4092            .discovery_roots
4093            .windows(2)
4094            .any(|pair| pair[0] >= pair[1])
4095        || manifest
4096            .files
4097            .windows(2)
4098            .any(|pair| pair[0].logical_path >= pair[1].logical_path)
4099    {
4100        return Err(NativeOperatorBuilderError::Invalid(format!(
4101            "{context} host toolchain manifest header/order is invalid"
4102        )));
4103    }
4104    for tool in std::iter::once(&manifest.compiler).chain(manifest.executable_inputs.iter()) {
4105        validate_normalized_absolute_path(
4106            &tool.path,
4107            &format!("{context} host toolchain executable path"),
4108        )?;
4109        if !is_sha256_digest(&tool.sha256) || tool.size_bytes == 0 {
4110            return Err(NativeOperatorBuilderError::Invalid(format!(
4111                "{context} host toolchain executable identity is invalid: {}",
4112                tool.path
4113            )));
4114        }
4115    }
4116    for root in manifest
4117        .include_roots
4118        .iter()
4119        .chain(manifest.discovery_roots.iter())
4120    {
4121        validate_normalized_absolute_path(root, &format!("{context} host toolchain scope root"))?;
4122    }
4123    if !manifest
4124        .executable_inputs
4125        .iter()
4126        .any(|tool| tool == &manifest.compiler)
4127        || manifest.executable_inputs.iter().any(|tool| {
4128            Path::new(&tool.path).parent().map_or(true, |parent| {
4129                !manifest
4130                    .discovery_roots
4131                    .iter()
4132                    .any(|root| Path::new(root) == parent)
4133            })
4134        })
4135        || manifest.discovery_roots.iter().any(|root| {
4136            !manifest
4137                .executable_inputs
4138                .iter()
4139                .any(|tool| Path::new(&tool.path).parent() == Some(Path::new(root)))
4140        })
4141    {
4142        return Err(NativeOperatorBuilderError::Invalid(format!(
4143            "{context} host toolchain manifest does not bind its compiler/search roots"
4144        )));
4145    }
4146    for file in &manifest.files {
4147        validate_normalized_absolute_path(
4148            &file.logical_path,
4149            &format!("{context} host toolchain logical path"),
4150        )?;
4151        validate_normalized_absolute_path(
4152            &file.resolved_path,
4153            &format!("{context} host toolchain resolved path"),
4154        )?;
4155        if !is_sha256_digest(&file.sha256)
4156            || !manifest
4157                .include_roots
4158                .iter()
4159                .chain(manifest.discovery_roots.iter())
4160                .any(|root| Path::new(&file.logical_path).starts_with(root))
4161        {
4162            return Err(NativeOperatorBuilderError::Invalid(format!(
4163                "{context} host toolchain file identity is invalid: {}",
4164                file.logical_path
4165            )));
4166        }
4167    }
4168    Ok(())
4169}
4170
4171fn build_cuda_toolkit_manifest(root: &Path) -> Result<NativeOperatorCudaToolkitManifest> {
4172    let canonical_root = root
4173        .canonicalize()
4174        .map_err(|source| NativeOperatorBuilderError::Io {
4175            path: root.to_path_buf(),
4176            source,
4177        })?;
4178    let root = canonical_root.as_path();
4179    let mut entries = Vec::new();
4180    for relative in REQUIRED_CUDA_TOOLKIT_FILES {
4181        collect_cuda_toolkit_single_file(root, relative, &mut entries)?;
4182    }
4183    for optional in ["bin/cudafe", "bin/nvcc.profile"] {
4184        if root.join(optional).exists() {
4185            collect_cuda_toolkit_single_file(root, optional, &mut entries)?;
4186        }
4187    }
4188    for scope in REQUIRED_CUDA_TOOLKIT_SCOPES {
4189        let scope_path = root.join(scope);
4190        if !scope_path.is_dir() {
4191            return Err(NativeOperatorBuilderError::Invalid(format!(
4192                "cuda toolkit compiler scope is missing: {scope}"
4193            )));
4194        }
4195        collect_cuda_toolkit_files(root, &scope_path, &mut BTreeSet::new(), &mut entries)?;
4196    }
4197    entries.sort_by(|left, right| left.logical_path.cmp(&right.logical_path));
4198    if entries.is_empty()
4199        || entries
4200            .windows(2)
4201            .any(|pair| pair[0].logical_path >= pair[1].logical_path)
4202    {
4203        return Err(NativeOperatorBuilderError::Invalid(
4204            "cuda toolkit manifest entries must be non-empty, sorted, and unique".to_string(),
4205        ));
4206    }
4207    Ok(NativeOperatorCudaToolkitManifest {
4208        schema_version: NATIVE_OPERATOR_CUDA_TOOLKIT_MANIFEST_SCHEMA_VERSION,
4209        canonical_root: root.display().to_string(),
4210        entries,
4211    })
4212}
4213
4214fn collect_cuda_toolkit_single_file(
4215    root: &Path,
4216    relative: &str,
4217    entries: &mut Vec<NativeOperatorCudaToolkitFileIdentity>,
4218) -> Result<()> {
4219    validate_relative_path(relative)?;
4220    let logical = root.join(relative);
4221    let resolved = logical
4222        .canonicalize()
4223        .map_err(|source| NativeOperatorBuilderError::Io {
4224            path: logical.clone(),
4225            source,
4226        })?;
4227    if !resolved.starts_with(root) || !resolved.is_file() {
4228        return Err(NativeOperatorBuilderError::Invalid(format!(
4229            "cuda toolkit compiler input escapes root or is not a file: {relative}"
4230        )));
4231    }
4232    entries.push(cuda_toolkit_file_identity(root, &logical, &resolved)?);
4233    Ok(())
4234}
4235
4236fn cuda_toolkit_release_version(root: &Path) -> Result<String> {
4237    let cuda_header = root.join("include/cuda.h");
4238    require_file(&cuda_header)?;
4239    let contents =
4240        fs::read_to_string(&cuda_header).map_err(|source| NativeOperatorBuilderError::Io {
4241            path: cuda_header.clone(),
4242            source,
4243        })?;
4244    let encoded = contents
4245        .lines()
4246        .find_map(|line| {
4247            let mut fields = line.split_whitespace();
4248            match (fields.next(), fields.next(), fields.next(), fields.next()) {
4249                (Some("#define"), Some("CUDA_VERSION"), Some(value), None) => {
4250                    value.parse::<u32>().ok()
4251                }
4252                _ => None,
4253            }
4254        })
4255        .filter(|value| *value >= 1000)
4256        .ok_or_else(|| {
4257            NativeOperatorBuilderError::Invalid(format!(
4258                "cuda toolkit include/cuda.h has no valid CUDA_VERSION: {}",
4259                cuda_header.display()
4260            ))
4261        })?;
4262    Ok(format!(
4263        "{}.{}.{}",
4264        encoded / 1000,
4265        (encoded % 1000) / 10,
4266        encoded % 10
4267    ))
4268}
4269
4270fn collect_cuda_toolkit_files(
4271    root: &Path,
4272    directory: &Path,
4273    active_directories: &mut BTreeSet<PathBuf>,
4274    entries: &mut Vec<NativeOperatorCudaToolkitFileIdentity>,
4275) -> Result<()> {
4276    let resolved_directory =
4277        directory
4278            .canonicalize()
4279            .map_err(|source| NativeOperatorBuilderError::Io {
4280                path: directory.to_path_buf(),
4281                source,
4282            })?;
4283    if !resolved_directory.starts_with(root) || !resolved_directory.is_dir() {
4284        return Err(NativeOperatorBuilderError::Invalid(format!(
4285            "cuda toolkit compiler directory escapes its canonical root: {}",
4286            directory.display()
4287        )));
4288    }
4289    if !active_directories.insert(resolved_directory.clone()) {
4290        return Err(NativeOperatorBuilderError::Invalid(format!(
4291            "cuda toolkit compiler directory contains a symlink cycle: {}",
4292            directory.display()
4293        )));
4294    }
4295    let mut children = fs::read_dir(directory)
4296        .map_err(|source| NativeOperatorBuilderError::Io {
4297            path: directory.to_path_buf(),
4298            source,
4299        })?
4300        .collect::<std::result::Result<Vec<_>, _>>()
4301        .map_err(|source| NativeOperatorBuilderError::Io {
4302            path: directory.to_path_buf(),
4303            source,
4304        })?;
4305    children.sort_by_key(|entry| entry.file_name());
4306    for child in children {
4307        let logical_path = child.path();
4308        let resolved =
4309            logical_path
4310                .canonicalize()
4311                .map_err(|source| NativeOperatorBuilderError::Io {
4312                    path: logical_path.clone(),
4313                    source,
4314                })?;
4315        if !resolved.starts_with(root) {
4316            return Err(NativeOperatorBuilderError::Invalid(format!(
4317                "cuda toolkit symlink escapes its canonical root: {}",
4318                logical_path.display()
4319            )));
4320        }
4321        if resolved.is_dir() {
4322            collect_cuda_toolkit_files(root, &logical_path, active_directories, entries)?;
4323        } else if resolved.is_file() {
4324            entries.push(cuda_toolkit_file_identity(root, &logical_path, &resolved)?);
4325        } else {
4326            return Err(NativeOperatorBuilderError::Invalid(format!(
4327                "cuda toolkit compiler scope contains a non-file entry: {}",
4328                logical_path.display()
4329            )));
4330        }
4331    }
4332    active_directories.remove(&resolved_directory);
4333    Ok(())
4334}
4335
4336fn cuda_toolkit_file_identity(
4337    root: &Path,
4338    logical: &Path,
4339    resolved: &Path,
4340) -> Result<NativeOperatorCudaToolkitFileIdentity> {
4341    let logical_path = logical.strip_prefix(root).map_err(|_| {
4342        NativeOperatorBuilderError::Invalid(format!(
4343            "cuda toolkit logical path escapes root: {}",
4344            logical.display()
4345        ))
4346    })?;
4347    let resolved_path = resolved.strip_prefix(root).map_err(|_| {
4348        NativeOperatorBuilderError::Invalid(format!(
4349            "cuda toolkit resolved path escapes root: {}",
4350            resolved.display()
4351        ))
4352    })?;
4353    let size_bytes = fs::metadata(resolved)
4354        .map_err(|source| NativeOperatorBuilderError::Io {
4355            path: resolved.to_path_buf(),
4356            source,
4357        })?
4358        .len();
4359    Ok(NativeOperatorCudaToolkitFileIdentity {
4360        logical_path: path_with_forward_slashes(logical_path)?,
4361        resolved_path: path_with_forward_slashes(resolved_path)?,
4362        sha256: sha256_file(resolved)?,
4363        size_bytes,
4364    })
4365}
4366
4367fn path_with_forward_slashes(path: &Path) -> Result<String> {
4368    let components = path
4369        .components()
4370        .map(|component| match component {
4371            std::path::Component::Normal(value) => {
4372                value.to_str().map(str::to_string).ok_or_else(|| {
4373                    NativeOperatorBuilderError::Invalid(format!(
4374                        "native build path is not valid UTF-8: {}",
4375                        path.display()
4376                    ))
4377                })
4378            }
4379            _ => Err(NativeOperatorBuilderError::Invalid(format!(
4380                "native build path is not normalized and relative: {}",
4381                path.display()
4382            ))),
4383        })
4384        .collect::<Result<Vec<_>>>()?;
4385    Ok(components.join("/"))
4386}
4387
4388fn tool_file_identity(path: &Path) -> Result<NativeOperatorToolFileIdentity> {
4389    require_file(path)?;
4390    let canonical = path
4391        .canonicalize()
4392        .map_err(|source| NativeOperatorBuilderError::Io {
4393            path: path.to_path_buf(),
4394            source,
4395        })?;
4396    let size_bytes = fs::metadata(&canonical)
4397        .map_err(|source| NativeOperatorBuilderError::Io {
4398            path: canonical.clone(),
4399            source,
4400        })?
4401        .len();
4402    if size_bytes == 0 {
4403        return Err(NativeOperatorBuilderError::Invalid(format!(
4404            "source build tool is empty: {}",
4405            canonical.display()
4406        )));
4407    }
4408    Ok(NativeOperatorToolFileIdentity {
4409        path: canonical.display().to_string(),
4410        sha256: sha256_file(&canonical)?,
4411        size_bytes,
4412    })
4413}
4414
4415fn probe_source_toolchain(
4416    static_identity: &NativeOperatorSourceBuildStaticToolchain,
4417    missed_translation_units: Vec<String>,
4418) -> Result<NativeOperatorSourceBuildToolchainProbe> {
4419    Ok(NativeOperatorSourceBuildToolchainProbe {
4420        nvcc_version: tool_version(Path::new(&static_identity.cuda_toolkit.nvcc.path))?,
4421        host_compiler_version: static_identity.host_toolchain.compiler_version.clone(),
4422        host_target: static_identity.host_toolchain.target.clone(),
4423        archiver_version: tool_version(Path::new(&static_identity.archiver.path))?,
4424        probed_for_misses: missed_translation_units,
4425    })
4426}
4427
4428fn validate_static_toolchain_identity(
4429    operator: &str,
4430    toolchain: &NativeOperatorSourceBuildStaticToolchain,
4431) -> Result<()> {
4432    if toolchain.backend != NativeOperatorBackend::Cuda
4433        || toolchain.compiler_driver != NativeOperatorSourceCompilerDriver::CudaNvcc
4434    {
4435        return Err(NativeOperatorBuilderError::Invalid(format!(
4436            "{operator} source-build toolchain must use the CUDA nvcc driver"
4437        )));
4438    }
4439    validate_normalized_absolute_path(
4440        &toolchain.cuda_toolkit.canonical_root,
4441        &format!("{operator} cuda toolkit canonical_root"),
4442    )?;
4443    validate_normalized_absolute_path(
4444        &toolchain.cuda_toolkit.invocation_root,
4445        &format!("{operator} cuda toolkit invocation_root"),
4446    )?;
4447    if toolchain.cuda_toolkit.release_version.trim().is_empty()
4448        || toolchain
4449            .cuda_toolkit
4450            .release_version
4451            .chars()
4452            .any(|character| !(character.is_ascii_digit() || character == '.'))
4453    {
4454        return Err(NativeOperatorBuilderError::Invalid(format!(
4455            "{operator} cuda toolkit release_version is invalid"
4456        )));
4457    }
4458    for (name, tool) in [
4459        ("nvcc", &toolchain.cuda_toolkit.nvcc),
4460        ("host_compiler", &toolchain.host_toolchain.compiler),
4461        ("archiver", &toolchain.archiver),
4462    ] {
4463        if validate_normalized_absolute_path(
4464            &tool.path,
4465            &format!("{operator} source-build static {name} path"),
4466        )
4467        .is_err()
4468            || !is_sha256_digest(&tool.sha256)
4469            || tool.size_bytes == 0
4470        {
4471            return Err(NativeOperatorBuilderError::Invalid(format!(
4472                "{operator} source-build static {name} identity is incomplete"
4473            )));
4474        }
4475    }
4476    if !Path::new(&toolchain.cuda_toolkit.nvcc.path)
4477        .starts_with(&toolchain.cuda_toolkit.canonical_root)
4478    {
4479        return Err(NativeOperatorBuilderError::Invalid(format!(
4480            "{operator} nvcc identity escapes cuda toolkit root"
4481        )));
4482    }
4483    let manifest = &toolchain.cuda_toolkit.manifest;
4484    if manifest.path != "toolchain/cuda-static-manifest.json"
4485        || !is_sha256_digest(&manifest.sha256)
4486        || manifest.size_bytes == 0
4487    {
4488        return Err(NativeOperatorBuilderError::Invalid(format!(
4489            "{operator} cuda toolkit manifest evidence is incomplete"
4490        )));
4491    }
4492    let host = &toolchain.host_toolchain;
4493    if host.compiler_version.trim().is_empty()
4494        || host.target.trim().is_empty()
4495        || host.target.len() > 256
4496        || host.target.chars().any(char::is_whitespace)
4497        || host.manifest.path != "toolchain/host-static-manifest.json"
4498        || !is_sha256_digest(&host.manifest.sha256)
4499        || host.manifest.size_bytes == 0
4500    {
4501        return Err(NativeOperatorBuilderError::Invalid(format!(
4502            "{operator} host toolchain manifest evidence is incomplete"
4503        )));
4504    }
4505    Ok(())
4506}
4507
4508fn validate_tool_file_unchanged(identity: &NativeOperatorToolFileIdentity) -> Result<()> {
4509    let current = tool_file_identity(Path::new(&identity.path))?;
4510    if &current != identity {
4511        return Err(NativeOperatorBuilderError::Invalid(format!(
4512            "tool file changed after static identity was recorded: {}",
4513            identity.path
4514        )));
4515    }
4516    Ok(())
4517}
4518
4519fn validate_cuda_toolkit_unchanged(identity: &NativeOperatorCudaToolkitIdentity) -> Result<()> {
4520    let manifest_path = Path::new(&identity.canonical_root);
4521    let current = build_cuda_toolkit_manifest(manifest_path)?;
4522    validate_cuda_toolkit_manifest("<source-build-finalize>", identity, &current)?;
4523    let recorded_path = Path::new(&identity.canonical_root);
4524    if current.canonical_root != recorded_path.display().to_string() {
4525        return Err(NativeOperatorBuilderError::Invalid(
4526            "cuda toolkit canonical root changed during source build".to_string(),
4527        ));
4528    }
4529    let recorded_manifest = identity.manifest.sha256.as_str();
4530    let serialized_with_newline = {
4531        let mut bytes = serde_json::to_vec_pretty(&current).map_err(|source| {
4532            NativeOperatorBuilderError::Json {
4533                path: PathBuf::from("<cuda-static-manifest>"),
4534                source,
4535            }
4536        })?;
4537        bytes.push(b'\n');
4538        sha256_bytes(&bytes)
4539    };
4540    if serialized_with_newline != recorded_manifest {
4541        return Err(NativeOperatorBuilderError::Invalid(format!(
4542            "cuda toolkit manifest changed during source build: expected={recorded_manifest} actual={serialized_with_newline}"
4543        )));
4544    }
4545    Ok(())
4546}
4547
4548fn validate_host_toolchain_unchanged(
4549    identity: &NativeOperatorHostToolchainIdentity,
4550    receipt_root: &Path,
4551    environment: &BTreeMap<String, String>,
4552) -> Result<()> {
4553    let manifest_path = resolve_source_build_evidence_file(
4554        receipt_root,
4555        "<source-build-finalize>",
4556        &identity.manifest,
4557    )?;
4558    let recorded: NativeOperatorHostToolchainManifest = read_json(&manifest_path)?;
4559    validate_host_toolchain_manifest("<source-build-finalize>", &recorded)?;
4560    if recorded.compiler != identity.compiler
4561        || recorded.compiler_version != identity.compiler_version
4562        || recorded.target != identity.target
4563    {
4564        return Err(NativeOperatorBuilderError::Invalid(
4565            "host toolchain identity differs from its manifest".to_string(),
4566        ));
4567    }
4568    if !host_toolchain_manifest_matches_current(&recorded, &identity.compiler, environment)? {
4569        return Err(NativeOperatorBuilderError::Invalid(
4570            "host toolchain files or driver configuration changed during source build".to_string(),
4571        ));
4572    }
4573    Ok(())
4574}
4575
4576pub(crate) fn compiler_target(path: &Path) -> Result<String> {
4577    require_file(path)?;
4578    let canonical = path
4579        .canonicalize()
4580        .map_err(|source| NativeOperatorBuilderError::Io {
4581            path: path.to_path_buf(),
4582            source,
4583        })?;
4584    let output = Command::new(&canonical)
4585        .arg("-dumpmachine")
4586        .env_clear()
4587        .env("LANG", "C")
4588        .env("LC_ALL", "C")
4589        .env("TZ", "UTC")
4590        .output()
4591        .map_err(|source| NativeOperatorBuilderError::Io {
4592            path: canonical.clone(),
4593            source,
4594        })?;
4595    let target = String::from_utf8_lossy(&output.stdout).trim().to_string();
4596    if !output.status.success()
4597        || target.is_empty()
4598        || target.len() > 256
4599        || target.chars().any(char::is_whitespace)
4600    {
4601        return Err(NativeOperatorBuilderError::Invalid(format!(
4602            "compiler produced no valid target identity: {}",
4603            canonical.display()
4604        )));
4605    }
4606    Ok(target)
4607}
4608
4609pub(crate) fn native_object_identity_file(path: &Path) -> Result<NativeOperatorObjectIdentity> {
4610    let bytes = fs::read(path).map_err(|source| NativeOperatorBuilderError::Io {
4611        path: path.to_path_buf(),
4612        source,
4613    })?;
4614    native_object_identity_bytes(&bytes, &path.display().to_string())
4615}
4616
4617fn native_object_size(path: &Path) -> Result<u64> {
4618    let size_bytes = fs::metadata(path)
4619        .map_err(|source| NativeOperatorBuilderError::Io {
4620            path: path.to_path_buf(),
4621            source,
4622        })?
4623        .len();
4624    if size_bytes == 0 {
4625        return Err(NativeOperatorBuilderError::Invalid(format!(
4626            "native object is empty: {}",
4627            path.display()
4628        )));
4629    }
4630    Ok(size_bytes)
4631}
4632
4633pub(crate) fn native_object_identity_bytes(
4634    bytes: &[u8],
4635    context: &str,
4636) -> Result<NativeOperatorObjectIdentity> {
4637    if bytes.len() >= 20 && bytes.starts_with(b"\x7fELF") {
4638        let class_bits = match bytes[4] {
4639            1 => 32,
4640            2 => 64,
4641            value => {
4642                return Err(NativeOperatorBuilderError::Invalid(format!(
4643                    "unsupported ELF class in {context}: {value}"
4644                )))
4645            }
4646        };
4647        let endianness = match bytes[5] {
4648            1 => NativeOperatorObjectEndianness::Little,
4649            2 => NativeOperatorObjectEndianness::Big,
4650            value => {
4651                return Err(NativeOperatorBuilderError::Invalid(format!(
4652                    "unsupported ELF endianness in {context}: {value}"
4653                )))
4654            }
4655        };
4656        let machine = u32::from(read_u16(&bytes[18..20], endianness));
4657        let identity = NativeOperatorObjectIdentity {
4658            format: NativeOperatorObjectFormat::Elf,
4659            class_bits,
4660            endianness,
4661            machine,
4662        };
4663        validate_native_object_identity(&identity, context)?;
4664        return Ok(identity);
4665    }
4666
4667    if bytes.len() >= 8 {
4668        let (class_bits, endianness) = match &bytes[..4] {
4669            [0xce, 0xfa, 0xed, 0xfe] => (32, NativeOperatorObjectEndianness::Little),
4670            [0xfe, 0xed, 0xfa, 0xce] => (32, NativeOperatorObjectEndianness::Big),
4671            [0xcf, 0xfa, 0xed, 0xfe] => (64, NativeOperatorObjectEndianness::Little),
4672            [0xfe, 0xed, 0xfa, 0xcf] => (64, NativeOperatorObjectEndianness::Big),
4673            _ => (0, NativeOperatorObjectEndianness::Little),
4674        };
4675        if class_bits != 0 {
4676            let machine = read_u32(&bytes[4..8], endianness);
4677            let identity = NativeOperatorObjectIdentity {
4678                format: NativeOperatorObjectFormat::MachO,
4679                class_bits,
4680                endianness,
4681                machine,
4682            };
4683            validate_native_object_identity(&identity, context)?;
4684            return Ok(identity);
4685        }
4686    }
4687
4688    if bytes.len() >= 20 {
4689        let machine = u16::from_le_bytes([bytes[0], bytes[1]]);
4690        if matches!(machine, 0x014c | 0x01c0 | 0x01c4 | 0x8664 | 0xaa64) {
4691            let identity = NativeOperatorObjectIdentity {
4692                format: NativeOperatorObjectFormat::Coff,
4693                class_bits: if matches!(machine, 0x8664 | 0xaa64) {
4694                    64
4695                } else {
4696                    32
4697                },
4698                endianness: NativeOperatorObjectEndianness::Little,
4699                machine: u32::from(machine),
4700            };
4701            validate_native_object_identity(&identity, context)?;
4702            return Ok(identity);
4703        }
4704    }
4705
4706    Err(NativeOperatorBuilderError::Invalid(format!(
4707        "native object has no supported ELF, Mach-O, or COFF header: {context}"
4708    )))
4709}
4710
4711pub(crate) fn validate_native_object_identity(
4712    identity: &NativeOperatorObjectIdentity,
4713    context: &str,
4714) -> Result<()> {
4715    if !matches!(identity.class_bits, 32 | 64) || identity.machine == 0 {
4716        return Err(NativeOperatorBuilderError::Invalid(format!(
4717            "native object identity is incomplete for {context}: {identity:?}"
4718        )));
4719    }
4720    if identity.format == NativeOperatorObjectFormat::Coff
4721        && identity.endianness != NativeOperatorObjectEndianness::Little
4722    {
4723        return Err(NativeOperatorBuilderError::Invalid(format!(
4724            "COFF object must be little-endian for {context}"
4725        )));
4726    }
4727    Ok(())
4728}
4729
4730fn read_u16(bytes: &[u8], endianness: NativeOperatorObjectEndianness) -> u16 {
4731    let bytes = [bytes[0], bytes[1]];
4732    match endianness {
4733        NativeOperatorObjectEndianness::Little => u16::from_le_bytes(bytes),
4734        NativeOperatorObjectEndianness::Big => u16::from_be_bytes(bytes),
4735    }
4736}
4737
4738fn read_u32(bytes: &[u8], endianness: NativeOperatorObjectEndianness) -> u32 {
4739    let bytes = [bytes[0], bytes[1], bytes[2], bytes[3]];
4740    match endianness {
4741        NativeOperatorObjectEndianness::Little => u32::from_le_bytes(bytes),
4742        NativeOperatorObjectEndianness::Big => u32::from_be_bytes(bytes),
4743    }
4744}
4745
4746pub(crate) fn tool_identity(path: &Path) -> Result<NativeOperatorToolIdentity> {
4747    require_file(path)?;
4748    let canonical = path
4749        .canonicalize()
4750        .map_err(|source| NativeOperatorBuilderError::Io {
4751            path: path.to_path_buf(),
4752            source,
4753        })?;
4754    Ok(NativeOperatorToolIdentity {
4755        path: canonical.display().to_string(),
4756        sha256: sha256_file(&canonical)?,
4757        version: tool_version(&canonical)?,
4758    })
4759}
4760
4761fn tool_version(path: &Path) -> Result<String> {
4762    require_file(path)?;
4763    let canonical = path
4764        .canonicalize()
4765        .map_err(|source| NativeOperatorBuilderError::Io {
4766            path: path.to_path_buf(),
4767            source,
4768        })?;
4769    let output = Command::new(&canonical)
4770        .arg("--version")
4771        .env_clear()
4772        .env("LANG", "C")
4773        .env("LC_ALL", "C")
4774        .env("TZ", "UTC")
4775        .output()
4776        .map_err(|source| NativeOperatorBuilderError::Io {
4777            path: canonical.clone(),
4778            source,
4779        })?;
4780    let version = format!(
4781        "{}{}",
4782        String::from_utf8_lossy(&output.stdout),
4783        String::from_utf8_lossy(&output.stderr)
4784    )
4785    .trim()
4786    .chars()
4787    .take(4000)
4788    .collect::<String>();
4789    if version.is_empty() {
4790        return Err(NativeOperatorBuilderError::Invalid(format!(
4791            "tool produced no version identity: {}",
4792            canonical.display()
4793        )));
4794    }
4795    Ok(version)
4796}
4797
4798#[allow(clippy::too_many_arguments)]
4799fn build_inputs_sha256(
4800    plan_sha256: &str,
4801    source_package_sha256: &str,
4802    architecture_argument: &str,
4803    effective_environment: &BTreeMap<String, String>,
4804    toolchain: Option<&NativeOperatorSourceBuildStaticToolchain>,
4805    receipt_path: &Path,
4806) -> Result<String> {
4807    let identity = NativeOperatorBuildInputIdentity {
4808        plan_sha256,
4809        source_package_sha256,
4810        builder_contract_version: NATIVE_OPERATOR_SOURCE_OBJECT_BUILD_CONTRACT_VERSION,
4811        architecture_argument,
4812        effective_environment,
4813        toolchain,
4814    };
4815    let bytes =
4816        serde_json::to_vec(&identity).map_err(|source| NativeOperatorBuilderError::Json {
4817            path: receipt_path.to_path_buf(),
4818            source,
4819        })?;
4820    Ok(sha256_bytes(&bytes))
4821}
4822
4823fn effective_build_environment(
4824    request: &NativeOperatorSourceBuildRequest,
4825    toolchain: Option<&NativeOperatorSourceBuildToolchain>,
4826) -> Result<BTreeMap<String, String>> {
4827    let tool_paths = if let Some(toolchain) = toolchain {
4828        [
4829            toolchain.static_identity.cuda_toolkit.nvcc.path.as_str(),
4830            toolchain
4831                .static_identity
4832                .host_toolchain
4833                .compiler
4834                .path
4835                .as_str(),
4836            toolchain.static_identity.archiver.path.as_str(),
4837        ]
4838    } else {
4839        [
4840            request.nvcc_path.to_str().unwrap_or(""),
4841            request.ccbin_path.to_str().unwrap_or(""),
4842            request.ar_path.to_str().unwrap_or(""),
4843        ]
4844    };
4845    effective_environment_for_tool_paths(tool_paths)
4846}
4847
4848fn effective_environment_for_tool_paths(tool_paths: [&str; 3]) -> Result<BTreeMap<String, String>> {
4849    let mut path_entries = tool_paths
4850        .iter()
4851        .filter_map(|path| Path::new(path).parent())
4852        .map(Path::to_path_buf)
4853        .collect::<Vec<_>>();
4854    path_entries.extend([PathBuf::from("/bin"), PathBuf::from("/usr/bin")]);
4855    path_entries.sort();
4856    path_entries.dedup();
4857    if path_entries.iter().any(|path| path.as_os_str().is_empty()) {
4858        return Err(NativeOperatorBuilderError::Invalid(
4859            "source build tool paths must have parent directories".to_string(),
4860        ));
4861    }
4862    let path = std::env::join_paths(&path_entries)
4863        .map_err(|error| {
4864            NativeOperatorBuilderError::Invalid(format!(
4865                "source build tool PATH cannot be represented: {error}"
4866            ))
4867        })?
4868        .into_string()
4869        .map_err(|_| {
4870            NativeOperatorBuilderError::Invalid(
4871                "source build tool PATH is not valid UTF-8".to_string(),
4872            )
4873        })?;
4874    let mut environment = BTreeMap::new();
4875    environment.insert("LANG".to_string(), "C".to_string());
4876    environment.insert("LC_ALL".to_string(), "C".to_string());
4877    environment.insert("PATH".to_string(), path);
4878    environment.insert("SOURCE_DATE_EPOCH".to_string(), "0".to_string());
4879    environment.insert("TMPDIR".to_string(), "/tmp".to_string());
4880    environment.insert("TZ".to_string(), "UTC".to_string());
4881    environment.insert("ZERO_AR_DATE".to_string(), "1".to_string());
4882    Ok(environment)
4883}
4884
4885fn build_object_cache_specs(
4886    plan: &NativeOperatorSourceBuildPlan,
4887    architecture_argument: &str,
4888    toolchain: &NativeOperatorSourceBuildStaticToolchain,
4889    effective_environment: &BTreeMap<String, String>,
4890) -> Result<Vec<NativeBuildArtifactSpec>> {
4891    plan.translation_units
4892        .iter()
4893        .enumerate()
4894        .map(|(index, translation_unit)| {
4895            let closure = plan.dependency_closures.get(index).ok_or_else(|| {
4896                NativeOperatorBuilderError::Invalid(format!(
4897                    "missing dependency closure for {}",
4898                    translation_unit.path
4899                ))
4900            })?;
4901            if closure.translation_unit != translation_unit.path {
4902                return Err(NativeOperatorBuilderError::Invalid(format!(
4903                    "dependency closure order differs from translation units: expected={} actual={}",
4904                    translation_unit.path, closure.translation_unit
4905                )));
4906            }
4907            let identity = NativeOperatorObjectInputIdentity {
4908                schema_version: NATIVE_OPERATOR_SOURCE_OBJECT_BUILD_CONTRACT_VERSION,
4909                operator: &plan.operator,
4910                translation_unit,
4911                dependency_closure_sha256: &closure.closure_sha256,
4912                headers: &closure.headers,
4913                include_dirs: &plan.include_dirs,
4914                defines: &plan.defines,
4915                nvcc_policy: &plan.nvcc_policy,
4916                architecture_argument,
4917                builder_contract_version: NATIVE_OPERATOR_SOURCE_OBJECT_BUILD_CONTRACT_VERSION,
4918                effective_environment,
4919                toolchain,
4920            };
4921            let input_signature = serde_json::to_string(&identity).map_err(|source| {
4922                NativeOperatorBuilderError::Json {
4923                    path: PathBuf::from("<object-cache-input>"),
4924                    source,
4925                }
4926            })?;
4927            NativeBuildArtifactSpec::new(
4928                format!("{}.object.{index:02}", plan.operator),
4929                object_file_name(index, translation_unit),
4930                input_signature,
4931            )
4932            .map_err(NativeOperatorBuilderError::from)
4933        })
4934        .collect()
4935}
4936
4937fn object_file_name(index: usize, translation_unit: &NativeOperatorSourceFileLock) -> String {
4938    let stem = Path::new(&translation_unit.path)
4939        .file_stem()
4940        .and_then(|value| value.to_str())
4941        .unwrap_or("translation_unit");
4942    format!(
4943        "{index:08}_{}_{}.o",
4944        safe_component(stem),
4945        &translation_unit.sha256[..8]
4946    )
4947}
4948
4949fn nvcc_policy_flags(policy: &NativeOperatorNvccPolicy) -> Vec<String> {
4950    let mut flags = vec![
4951        match policy.cpp_standard {
4952            NativeOperatorCppStandard::Cpp17 => "-std=c++17",
4953        }
4954        .to_string(),
4955        match policy.optimization {
4956            NativeOperatorOptimization::O3 => "-O3",
4957        }
4958        .to_string(),
4959    ];
4960    if policy.use_fast_math {
4961        flags.push("--use_fast_math".to_string());
4962    }
4963    if policy.relaxed_constexpr {
4964        flags.push("--expt-relaxed-constexpr".to_string());
4965    }
4966    if policy.extended_lambda {
4967        flags.push("--expt-extended-lambda".to_string());
4968    }
4969    if policy.host_position_independent_code {
4970        flags.extend(["-Xcompiler".to_string(), "-fPIC".to_string()]);
4971    }
4972    if policy.host_default_visibility {
4973        flags.extend(["-Xcompiler".to_string(), "-fvisibility=default".to_string()]);
4974    }
4975    flags
4976}
4977
4978fn build_commands(
4979    request: &NativeOperatorSourceBuildRequest,
4980    plan: &NativeOperatorSourceBuildPlan,
4981    source_root: &Path,
4982    architecture_argument: &str,
4983    objects_dir: &Path,
4984    logs_dir: &Path,
4985    toolchain: Option<&NativeOperatorSourceBuildToolchain>,
4986    _effective_environment: &BTreeMap<String, String>,
4987) -> Vec<NativeOperatorSourceBuildCommand> {
4988    let nvcc_path = toolchain
4989        .map(|toolchain| toolchain.static_identity.cuda_toolkit.nvcc.path.as_str())
4990        .unwrap_or_else(|| request.nvcc_path.to_str().unwrap_or("<non-utf8-nvcc>"));
4991    let ccbin_path = toolchain
4992        .map(|toolchain| {
4993            toolchain
4994                .static_identity
4995                .host_toolchain
4996                .compiler
4997                .path
4998                .as_str()
4999        })
5000        .unwrap_or_else(|| request.ccbin_path.to_str().unwrap_or("<non-utf8-ccbin>"));
5001    let ar_path = toolchain
5002        .map(|toolchain| toolchain.static_identity.archiver.path.as_str())
5003        .unwrap_or_else(|| request.ar_path.to_str().unwrap_or("<non-utf8-ar>"));
5004    let mut commands = Vec::with_capacity(plan.translation_units.len() + 1);
5005    let mut object_paths = Vec::with_capacity(plan.translation_units.len());
5006    for (index, translation_unit) in plan.translation_units.iter().enumerate() {
5007        let stem = Path::new(&translation_unit.path)
5008            .file_stem()
5009            .and_then(|value| value.to_str())
5010            .unwrap_or("translation_unit");
5011        let object_name = object_file_name(index, translation_unit);
5012        let object_path = objects_dir.join(object_name);
5013        let depfile_name = format!("{index:08}-{stem}.d");
5014        let depfile_relative = format!("depfiles/{depfile_name}");
5015        let compiler_depfile_relative = format!("depfiles/{index:08}-{stem}.compiler.raw.d");
5016        let compiler_depfile_path = request.output_dir.join(&compiler_depfile_relative);
5017        object_paths.push(object_path.clone());
5018        let mut argv = vec![
5019            nvcc_path.to_string(),
5020            "-c".to_string(),
5021            translation_unit.path.clone(),
5022            "-o".to_string(),
5023            object_path.display().to_string(),
5024            architecture_argument.to_string(),
5025            "-ccbin".to_string(),
5026            ccbin_path.to_string(),
5027            "-MMD".to_string(),
5028            "-MF".to_string(),
5029            compiler_depfile_path.display().to_string(),
5030            "-MT".to_string(),
5031            object_path.display().to_string(),
5032        ];
5033        argv.extend(plan.include_dirs.iter().map(|path| format!("-I{path}")));
5034        argv.extend(plan.defines.iter().map(|define| format!("-D{define}")));
5035        argv.extend(nvcc_policy_flags(&plan.nvcc_policy));
5036        argv.push("--threads".to_string());
5037        argv.push(request.nvcc_threads.to_string());
5038        commands.push(NativeOperatorSourceBuildCommand {
5039            translation_unit: Some(translation_unit.path.clone()),
5040            working_directory: source_root.display().to_string(),
5041            argv,
5042            object_file: Some(object_path.display().to_string()),
5043            stdout_log: relative_log(logs_dir, &format!("{index:02}-{stem}.stdout.log")),
5044            stderr_log: relative_log(logs_dir, &format!("{index:02}-{stem}.stderr.log")),
5045            object_cache_key: None,
5046            object_cache_status: Some(if request.plan_only {
5047                NativeOperatorSourceObjectCacheStatus::Plan
5048            } else {
5049                NativeOperatorSourceObjectCacheStatus::Pending
5050            }),
5051            object_cache_entry: None,
5052            object_sha256: None,
5053            object_size_bytes: None,
5054            object_identity: None,
5055            dependency_closure_sha256: plan
5056                .dependency_closures
5057                .get(index)
5058                .map(|closure| closure.closure_sha256.clone()),
5059            dependency_validation: Some(if request.plan_only {
5060                NativeOperatorDependencyValidation::Plan
5061            } else {
5062                NativeOperatorDependencyValidation::Pending
5063            }),
5064            compiler_depfile: Some(compiler_depfile_relative),
5065            compiler_depfile_sha256: None,
5066            depfile: Some(depfile_relative),
5067            depfile_sha256: None,
5068            depfile_producer_working_directory: None,
5069            depfile_producer_object_file: None,
5070            depfile_bindings: Vec::new(),
5071            observed_dependencies: Vec::new(),
5072            compiler_executed: false,
5073            elapsed_ms: None,
5074            return_code: None,
5075        });
5076    }
5077    let archive_path = request.output_dir.join(&plan.archive_file);
5078    let mut archive_argv = vec![
5079        ar_path.to_string(),
5080        "rcs".to_string(),
5081        archive_path.display().to_string(),
5082    ];
5083    archive_argv.extend(object_paths.iter().map(|path| path.display().to_string()));
5084    commands.push(NativeOperatorSourceBuildCommand {
5085        translation_unit: None,
5086        working_directory: source_root.display().to_string(),
5087        argv: archive_argv,
5088        object_file: None,
5089        stdout_log: relative_log(logs_dir, "archive.stdout.log"),
5090        stderr_log: relative_log(logs_dir, "archive.stderr.log"),
5091        object_cache_key: None,
5092        object_cache_status: None,
5093        object_cache_entry: None,
5094        object_sha256: None,
5095        object_size_bytes: None,
5096        object_identity: None,
5097        dependency_closure_sha256: None,
5098        dependency_validation: None,
5099        compiler_depfile: None,
5100        compiler_depfile_sha256: None,
5101        depfile: None,
5102        depfile_sha256: None,
5103        depfile_producer_working_directory: None,
5104        depfile_producer_object_file: None,
5105        depfile_bindings: Vec::new(),
5106        observed_dependencies: Vec::new(),
5107        compiler_executed: false,
5108        elapsed_ms: None,
5109        return_code: None,
5110    });
5111    commands
5112}
5113
5114fn run_logged_command(
5115    argv: &[String],
5116    stdout_path: &Path,
5117    stderr_path: &Path,
5118    working_directory: &str,
5119    effective_environment: &BTreeMap<String, String>,
5120) -> Result<ExitStatus> {
5121    let (program, args) = argv.split_first().ok_or_else(|| {
5122        NativeOperatorBuilderError::Invalid("source build command is empty".to_string())
5123    })?;
5124    let stdout = append_command_file(stdout_path)?;
5125    let stderr = append_command_file(stderr_path)?;
5126    Command::new(program)
5127        .args(args)
5128        .current_dir(working_directory)
5129        .env_clear()
5130        .envs(effective_environment)
5131        .stdout(Stdio::from(stdout))
5132        .stderr(Stdio::from(stderr))
5133        .status()
5134        .map_err(|source| NativeOperatorBuilderError::Io {
5135            path: PathBuf::from(program),
5136            source,
5137        })
5138}
5139
5140fn append_command_file(path: &Path) -> Result<fs::File> {
5141    let mut file = OpenOptions::new()
5142        .append(true)
5143        .open(path)
5144        .map_err(|source| NativeOperatorBuilderError::Io {
5145            path: path.to_path_buf(),
5146            source,
5147        })?;
5148    file.write_all(b"execution-start\n")
5149        .and_then(|()| file.flush())
5150        .map_err(|source| NativeOperatorBuilderError::Io {
5151            path: path.to_path_buf(),
5152            source,
5153        })?;
5154    Ok(file)
5155}
5156
5157fn write_command_stream(path: &Path, stream: &str, argv: &[String], payload: &[u8]) -> Result<()> {
5158    let command =
5159        serde_json::to_string(argv).map_err(|source| NativeOperatorBuilderError::Json {
5160            path: path.to_path_buf(),
5161            source,
5162        })?;
5163    let mut bytes = format!("stream={stream}\nargv={command}\n").into_bytes();
5164    bytes.extend_from_slice(payload);
5165    if !payload.ends_with(b"\n") {
5166        bytes.push(b'\n');
5167    }
5168    fs::write(path, bytes).map_err(|source| NativeOperatorBuilderError::Io {
5169        path: path.to_path_buf(),
5170        source,
5171    })
5172}
5173
5174fn append_command_stream(path: &Path, payload: &[u8]) -> Result<()> {
5175    let mut file = OpenOptions::new()
5176        .append(true)
5177        .open(path)
5178        .map_err(|source| NativeOperatorBuilderError::Io {
5179            path: path.to_path_buf(),
5180            source,
5181        })?;
5182    file.write_all(payload)
5183        .and_then(|()| {
5184            if payload.ends_with(b"\n") {
5185                Ok(())
5186            } else {
5187                file.write_all(b"\n")
5188            }
5189        })
5190        .and_then(|()| file.flush())
5191        .map_err(|source| NativeOperatorBuilderError::Io {
5192            path: path.to_path_buf(),
5193            source,
5194        })
5195}
5196
5197fn reject_source_build<T>(
5198    receipt_path: &Path,
5199    receipt: &mut NativeOperatorSourceBuildReceipt,
5200    reason: String,
5201) -> Result<T> {
5202    receipt.status = NativeOperatorSourceBuildStatus::Reject;
5203    receipt.failure_class = Some(reason.clone());
5204    write_json(receipt_path, receipt)?;
5205    Err(NativeOperatorBuilderError::SourceBuildRejected {
5206        receipt_path: receipt_path.to_path_buf(),
5207        reason,
5208    })
5209}
5210
5211fn architecture_argument(
5212    architecture: NativeOperatorCudaArchitecture,
5213    compute_capability: &str,
5214) -> String {
5215    match architecture {
5216        NativeOperatorCudaArchitecture::DeviceComputeCapability => {
5217            format!("-arch={compute_capability}")
5218        }
5219        NativeOperatorCudaArchitecture::Compute80Ptx => "-arch=compute_80".to_string(),
5220    }
5221}
5222
5223fn validate_compute_capability(value: &str) -> Result<()> {
5224    if value.len() >= 5
5225        && value.starts_with("sm_")
5226        && value[3..].bytes().all(|byte| byte.is_ascii_digit())
5227    {
5228        Ok(())
5229    } else {
5230        Err(NativeOperatorBuilderError::Invalid(
5231            "compute_capability must use sm_<digits> form".to_string(),
5232        ))
5233    }
5234}
5235
5236fn is_git_oid(value: &str) -> bool {
5237    matches!(value.len(), 40 | 64)
5238        && value
5239            .bytes()
5240            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
5241}
5242
5243fn safe_component(value: &str) -> String {
5244    value
5245        .chars()
5246        .map(|character| {
5247            if character.is_ascii_alphanumeric() {
5248                character
5249            } else {
5250                '_'
5251            }
5252        })
5253        .collect()
5254}
5255
5256fn relative_log(logs_dir: &Path, file: &str) -> String {
5257    Path::new("logs")
5258        .join(logs_dir.join(file).file_name().expect("log file name"))
5259        .to_string_lossy()
5260        .replace('\\', "/")
5261}
5262
5263fn unix_ms() -> u64 {
5264    SystemTime::now()
5265        .duration_since(UNIX_EPOCH)
5266        .unwrap_or_default()
5267        .as_millis()
5268        .try_into()
5269        .unwrap_or(u64::MAX)
5270}
5271
5272fn millis(duration: std::time::Duration) -> u64 {
5273    duration.as_millis().try_into().unwrap_or(u64::MAX)
5274}
5275
5276#[cfg(test)]
5277mod tests {
5278    use std::os::unix::fs::{symlink, PermissionsExt};
5279
5280    use super::*;
5281
5282    fn definition() -> NativeOperatorSourceDefinition {
5283        NativeOperatorSourceDefinition {
5284            schema_version: NATIVE_OPERATOR_SOURCE_DEFINITION_SCHEMA_VERSION,
5285            operator: CudaNativeBuildUnit::Marlin.artifact_operator().to_string(),
5286            source_package_kind: "ferrum-native-source-bundle".to_string(),
5287            source_package_revision: "fixture".to_string(),
5288            upstream_sources: vec![NativeOperatorUpstreamSource {
5289                repository: "https://example.invalid/marlin.git".to_string(),
5290                revision: "abc123".to_string(),
5291                license: "Apache-2.0".to_string(),
5292            }],
5293            translation_units: vec!["kernels/marlin.cu".to_string()],
5294            headers: vec!["kernels/marlin.h".to_string()],
5295            dependency_closures: vec![NativeOperatorTranslationUnitDependencies {
5296                translation_unit: "kernels/marlin.cu".to_string(),
5297                headers: vec!["kernels/marlin.h".to_string()],
5298            }],
5299            include_dirs: vec!["kernels".to_string()],
5300            defines: vec!["FERRUM_FIXTURE=1".to_string()],
5301            nvcc_policy: NativeOperatorNvccPolicy {
5302                cpp_standard: NativeOperatorCppStandard::Cpp17,
5303                optimization: NativeOperatorOptimization::O3,
5304                use_fast_math: false,
5305                relaxed_constexpr: false,
5306                extended_lambda: false,
5307                host_position_independent_code: true,
5308                host_default_visibility: false,
5309            },
5310            architecture: NativeOperatorCudaArchitecture::Compute80Ptx,
5311            archive_file: "libmarlin.a".to_string(),
5312        }
5313    }
5314
5315    fn write_fixture(root: &Path) -> (PathBuf, PathBuf) {
5316        let source_root = root.join("source");
5317        fs::create_dir_all(source_root.join("kernels")).unwrap();
5318        fs::create_dir_all(source_root.join("kernels/core")).unwrap();
5319        fs::write(
5320            source_root.join("kernels/marlin.cu"),
5321            "#include \"marlin.h\"\n\
5322             int marlin_cuda(void) { return MARLIN - 1; }\n\
5323             int marlin_cuda_moe(void) { return 0; }\n",
5324        )
5325        .unwrap();
5326        fs::write(source_root.join("kernels/marlin.h"), "#define MARLIN 1\n").unwrap();
5327        let definition_path = root.join("source-definition.json");
5328        write_json(&definition_path, &definition()).unwrap();
5329        (source_root, definition_path)
5330    }
5331
5332    struct FakeCudaToolkit {
5333        root: PathBuf,
5334        nvcc: PathBuf,
5335        ccbin: PathBuf,
5336        empty_host_include_root: PathBuf,
5337        compile_counter: PathBuf,
5338        invocation_counter: PathBuf,
5339        host_compiler_invocation_counter: PathBuf,
5340        host_driver_config: PathBuf,
5341    }
5342
5343    #[derive(Clone, Copy)]
5344    enum FakeDepfileMode {
5345        Valid,
5346        MissingDeclaredHeader,
5347        UndeclaredExternal,
5348    }
5349
5350    fn write_fake_nvcc(root: &Path) -> FakeCudaToolkit {
5351        write_fake_nvcc_with_mode(root, FakeDepfileMode::Valid)
5352    }
5353
5354    fn write_fake_nvcc_with_mode(root: &Path, mode: FakeDepfileMode) -> FakeCudaToolkit {
5355        let toolkit_root = root.join("fake-cuda");
5356        for directory in ["bin/crt", "include", "nvvm/bin", "nvvm/libdevice"] {
5357            fs::create_dir_all(toolkit_root.join(directory)).unwrap();
5358        }
5359        for (relative, contents) in [
5360            ("bin/bin2c", "fake bin2c\n"),
5361            ("bin/crt/link.stub", "fake link stub\n"),
5362            ("bin/cudafe++", "fake cudafe\n"),
5363            ("bin/ptxas", "fake ptxas\n"),
5364            ("bin/fatbinary", "fake fatbinary\n"),
5365            ("bin/nvlink", "fake nvlink\n"),
5366            ("include/cuda.h", "#define CUDA_VERSION 12040\n"),
5367            ("nvvm/bin/cicc", "fake cicc\n"),
5368            ("nvvm/libdevice/libdevice.10.bc", "fake libdevice\n"),
5369        ] {
5370            fs::write(toolkit_root.join(relative), contents).unwrap();
5371        }
5372        let path = toolkit_root.join("bin/nvcc");
5373        let counter = root.join("fake-nvcc-compile-count");
5374        let invocation_counter = root.join("fake-nvcc-invocation-count");
5375        let host_root = root.join("fake-host-toolchain");
5376        let compile_tail = match mode {
5377            FakeDepfileMode::Valid => format!(
5378                "/usr/bin/cc -x c -c \"$src\" -o \"$out\" || exit $?\n\
5379                 declared_header=''\n\
5380                 case \"$src\" in */marlin.cu) declared_header=' kernels/core/../marlin.h' ;; esac\n\
5381                 printf '%s: %s%s %s %s\\n' \"$dep_target\" \"$src\" \"$declared_header\" '{}' '{}' > \"$depfile\"\n",
5382                toolkit_root.join("bin/../include/cuda.h").display(),
5383                host_root.join("include/stddef.h").display(),
5384            ),
5385            FakeDepfileMode::MissingDeclaredHeader => {
5386                "/usr/bin/cc -x c -MMD -MF \"$depfile\" -MT \"$dep_target\" -c \"$src\" -o \"$out\" || exit $?\n\
5387                 printf '%s: %s\\n' \"$dep_target\" \"$src\" > \"$depfile\"\n"
5388                    .to_string()
5389            }
5390            FakeDepfileMode::UndeclaredExternal => {
5391                "/usr/bin/cc -x c -MMD -MF \"$depfile\" -MT \"$dep_target\" -c \"$src\" -o \"$out\" || exit $?\n\
5392                 printf '%s: %s kernels/marlin.h /etc/hosts\\n' \"$dep_target\" \"$src\" > \"$depfile\"\n"
5393                    .to_string()
5394            }
5395        };
5396        fs::write(
5397            &path,
5398            format!(
5399                "#!/bin/sh\n\
5400             printf 'invoke:%s\\n' \"$*\" >> '{}'\n\
5401             if [ \"$1\" = \"--version\" ]; then echo 'fake nvcc 12.4'; exit 0; fi\n\
5402             src=''\n\
5403             out=''\n\
5404             depfile=''\n\
5405             dep_target=''\n\
5406             while [ \"$#\" -gt 0 ]; do\n\
5407               case \"$1\" in\n\
5408                 -c) src=\"$2\"; shift 2 ;;\n\
5409                 -o) out=\"$2\"; shift 2 ;;\n\
5410                 -MF) depfile=\"$2\"; shift 2 ;;\n\
5411                 -MT) dep_target=\"$2\"; shift 2 ;;\n\
5412                 *) shift ;;\n\
5413               esac\n\
5414             done\n\
5415             build_dir=$(dirname \"$(dirname \"$out\")\")\n\
5416             receipt=\"$build_dir/source-build.receipt.json\"\n\
5417             test -s \"$receipt\"\n\
5418             grep -q '\"status\": \"reject\"' \"$receipt\"\n\
5419             grep -q '\"failure_class\": \"build_incomplete\"' \"$receipt\"\n\
5420             printf 'compile\\n' >> '{}'\n\
5421             {}",
5422                invocation_counter.display(),
5423                counter.display(),
5424                compile_tail
5425            ),
5426        )
5427        .unwrap();
5428        let mut permissions = fs::metadata(&path).unwrap().permissions();
5429        permissions.set_mode(0o755);
5430        fs::set_permissions(&path, permissions).unwrap();
5431
5432        fs::create_dir_all(host_root.join("bin")).unwrap();
5433        fs::create_dir_all(host_root.join("include")).unwrap();
5434        let empty_host_include_root = host_root.join("empty-include");
5435        fs::create_dir_all(&empty_host_include_root).unwrap();
5436        fs::write(
5437            host_root.join("include/stddef.h"),
5438            "#define FAKE_SIZE_T 1\n",
5439        )
5440        .unwrap();
5441        for program in HOST_TOOLCHAIN_PROGRAMS {
5442            fs::write(
5443                host_root.join("bin").join(program),
5444                format!("fake host tool {program}\n"),
5445            )
5446            .unwrap();
5447        }
5448        fs::write(
5449            host_root.join("bin/driver.specs"),
5450            "fake host driver configuration\n",
5451        )
5452        .unwrap();
5453        let ccbin = host_root.join("bin/c++");
5454        let host_compiler_invocation_counter = root.join("fake-host-compiler-invocation-count");
5455        let host_driver_config = root.join("fake-host-driver.conf");
5456        fs::write(&host_driver_config, "external driver option v1\n").unwrap();
5457        fs::write(
5458            &ccbin,
5459            format!(
5460                "#!/bin/sh\n\
5461                 printf 'invoke:%s\\n' \"$*\" >> '{}'\n\
5462                 case \"$1\" in\n\
5463                   --version) echo 'fake host compiler 1.0'; exit 0 ;;\n\
5464                   -dumpmachine) echo 'x86_64-ferrum-linux-gnu'; exit 0 ;;\n\
5465                   -E) echo '#include <...> search starts here:' >&2; echo ' {}' >&2; echo ' {}' >&2; echo 'End of search list.' >&2; exit 0 ;;\n\
5466                   -###) test \"$2\" = '-pipe' || exit 3; echo 'fake cc1plus -O2 -x c++' >&2; cat '{}' >&2; exit 0 ;;\n\
5467                   -print-prog-name=*) name=${{1#*=}}; echo '{}/bin/'\"$name\"; exit 0 ;;\n\
5468                 esac\n\
5469                 exit 2\n",
5470                host_compiler_invocation_counter.display(),
5471                host_root.join("include").display(),
5472                empty_host_include_root.display(),
5473                host_driver_config.display(),
5474                host_root.display(),
5475            ),
5476        )
5477        .unwrap();
5478        let mut permissions = fs::metadata(&ccbin).unwrap().permissions();
5479        permissions.set_mode(0o755);
5480        fs::set_permissions(&ccbin, permissions).unwrap();
5481        FakeCudaToolkit {
5482            root: toolkit_root,
5483            nvcc: path,
5484            ccbin,
5485            empty_host_include_root,
5486            compile_counter: counter,
5487            invocation_counter,
5488            host_compiler_invocation_counter,
5489            host_driver_config,
5490        }
5491    }
5492
5493    #[test]
5494    fn object_file_names_preserve_lexical_order_past_one_hundred_units() {
5495        let translation_unit = NativeOperatorSourceFileLock {
5496            path: "kernels/unit.cu".to_string(),
5497            sha256: "a".repeat(64),
5498        };
5499
5500        assert!(object_file_name(99, &translation_unit) < object_file_name(100, &translation_unit));
5501    }
5502
5503    #[test]
5504    fn portable_depfile_serialization_round_trips_restricted_make_words() {
5505        let target = "/tmp/build path/object:name#$value.o";
5506        let dependencies = vec![
5507            "kernels/header name.h".to_string(),
5508            "/tmp/tool chain/header:name#$value.h".to_string(),
5509        ];
5510
5511        let raw = serialize_portable_depfile(target, &dependencies).unwrap();
5512        let text = std::str::from_utf8(&raw).unwrap();
5513        let (parsed_target, parsed_dependencies) =
5514            parse_make_depfile(text, Path::new("<round-trip>")).unwrap();
5515
5516        assert_eq!(parsed_target, target);
5517        assert_eq!(parsed_dependencies, dependencies);
5518        assert!(text.contains("\\ "));
5519        assert!(text.contains("\\:"));
5520        assert!(text.contains("\\#"));
5521        assert!(text.contains("\\$"));
5522    }
5523
5524    #[test]
5525    fn source_depfile_paths_lexically_normalize_without_escaping_working_directory() {
5526        assert_eq!(
5527            normalize_portable_relative_path(Path::new(
5528                "kernels/vllm_marlin_moe/core/../vllm_torch_shim.h"
5529            ))
5530            .unwrap(),
5531            "kernels/vllm_marlin_moe/vllm_torch_shim.h"
5532        );
5533        for path in ["../outside.h", "kernels/../../outside.h"] {
5534            let error = normalize_portable_relative_path(Path::new(path)).unwrap_err();
5535            assert!(error.to_string().contains("escapes its working directory"));
5536        }
5537        let error = normalize_portable_relative_path(Path::new("kernels\\outside.h")).unwrap_err();
5538        assert!(error.to_string().contains("relative POSIX path"));
5539    }
5540
5541    #[test]
5542    fn host_include_probe_preserves_search_order_and_deduplicates_first_occurrence() {
5543        let root = tempfile::tempdir().unwrap();
5544        let first = root.path().join("first include");
5545        let second = root.path().join("second include");
5546        fs::create_dir_all(&first).unwrap();
5547        fs::create_dir_all(&second).unwrap();
5548        let raw = format!(
5549            "#include <...> search starts here:\n {}\n {}\n {}\nEnd of search list.\n",
5550            second.display(),
5551            first.display(),
5552            second.display()
5553        );
5554
5555        let roots = parse_host_compiler_include_roots(&raw, Path::new("/fake/compiler")).unwrap();
5556
5557        assert_eq!(
5558            roots,
5559            [second.display().to_string(), first.display().to_string(),]
5560        );
5561    }
5562
5563    #[test]
5564    fn host_program_resolution_and_effective_path_support_spaces() {
5565        let root = tempfile::tempdir().unwrap();
5566        let tool_dir = root.path().join("tool chain");
5567        fs::create_dir_all(&tool_dir).unwrap();
5568        let helper = tool_dir.join("cc helper");
5569        fs::write(&helper, "fixture\n").unwrap();
5570        let path = std::env::join_paths([tool_dir.clone()])
5571            .unwrap()
5572            .into_string()
5573            .unwrap();
5574        let environment = BTreeMap::from([("PATH".to_string(), path)]);
5575
5576        assert_eq!(
5577            resolve_host_program("cc helper", &environment).unwrap(),
5578            Some(helper)
5579        );
5580
5581        let nvcc = tool_dir.join("nvcc");
5582        let ccbin = tool_dir.join("c++");
5583        let ar = tool_dir.join("ar");
5584        let effective = effective_environment_for_tool_paths([
5585            nvcc.to_str().unwrap(),
5586            ccbin.to_str().unwrap(),
5587            ar.to_str().unwrap(),
5588        ])
5589        .unwrap();
5590        assert!(
5591            std::env::split_paths(std::ffi::OsStr::new(&effective["PATH"]))
5592                .any(|entry| entry == tool_dir)
5593        );
5594    }
5595
5596    #[test]
5597    fn toolchain_dependency_scope_rejects_cross_domain_aliases() {
5598        let absolute = "/toolchain/include/shared.h".to_string();
5599        let mut dependencies = BTreeMap::new();
5600        insert_toolchain_dependency(
5601            &mut dependencies,
5602            absolute.clone(),
5603            NativeOperatorObservedDependency {
5604                domain: NativeOperatorDependencyDomain::BackendToolchain,
5605                path: "include/shared.h".to_string(),
5606                sha256: "a".repeat(64),
5607            },
5608        )
5609        .unwrap();
5610
5611        let error = insert_toolchain_dependency(
5612            &mut dependencies,
5613            absolute,
5614            NativeOperatorObservedDependency {
5615                domain: NativeOperatorDependencyDomain::HostToolchain,
5616                path: "/toolchain/include/shared.h".to_string(),
5617                sha256: "a".repeat(64),
5618            },
5619        )
5620        .unwrap_err();
5621
5622        assert!(error
5623            .to_string()
5624            .contains("toolchain manifests ambiguously own dependency path"));
5625    }
5626
5627    #[test]
5628    fn locks_self_contained_translation_unit_without_auxiliary_inputs() {
5629        let root = tempfile::tempdir().unwrap();
5630        let (source_root, _) = write_fixture(root.path());
5631        let mut definition = definition();
5632        definition.headers.clear();
5633        definition.dependency_closures[0].headers.clear();
5634        definition.include_dirs.clear();
5635        let definition_path = root.path().join("self-contained-definition.json");
5636        write_json(&definition_path, &definition).unwrap();
5637        let plan_path = root.path().join("source-build.plan.json");
5638
5639        let plan =
5640            lock_native_operator_source_definition(&definition_path, &source_root, &plan_path)
5641                .unwrap();
5642
5643        assert!(plan.headers.is_empty());
5644        assert!(plan.include_dirs.is_empty());
5645        assert_eq!(plan.translation_units.len(), 1);
5646    }
5647
5648    #[test]
5649    fn locks_files_and_rejects_source_drift_before_rendering_commands() {
5650        let root = tempfile::tempdir().unwrap();
5651        let (source_root, definition_path) = write_fixture(root.path());
5652        let plan_path = root.path().join("source-build.plan.json");
5653        lock_native_operator_source_definition(&definition_path, &source_root, &plan_path).unwrap();
5654        fs::write(source_root.join("kernels/marlin.h"), "#define MARLIN 2\n").unwrap();
5655
5656        let error = run_native_operator_source_build(&NativeOperatorSourceBuildRequest {
5657            plan_path,
5658            source_root,
5659            output_dir: root.path().join("build"),
5660            compute_capability: "sm_89".to_string(),
5661            builder_sha: "7".repeat(40),
5662            nvcc_path: PathBuf::from("/missing/nvcc"),
5663            cuda_toolkit_root: PathBuf::from("/missing/cuda"),
5664            ccbin_path: PathBuf::from("/missing/c++"),
5665            ar_path: PathBuf::from("/missing/ar"),
5666            nvcc_threads: 4,
5667            object_cache_dir: root.path().join("object-cache"),
5668            plan_only: true,
5669        })
5670        .unwrap_err();
5671
5672        assert!(error.to_string().contains("locked source drift"));
5673        assert!(!root.path().join("build").exists());
5674    }
5675
5676    #[test]
5677    fn plan_only_records_exact_commands_without_requiring_cuda_tools() {
5678        let root = tempfile::tempdir().unwrap();
5679        let (source_root, definition_path) = write_fixture(root.path());
5680        let plan_path = root.path().join("source-build.plan.json");
5681        lock_native_operator_source_definition(&definition_path, &source_root, &plan_path).unwrap();
5682
5683        let receipt = run_native_operator_source_build(&NativeOperatorSourceBuildRequest {
5684            plan_path,
5685            source_root,
5686            output_dir: root.path().join("plan"),
5687            compute_capability: "sm_89".to_string(),
5688            builder_sha: "7".repeat(40),
5689            nvcc_path: PathBuf::from("/missing/nvcc"),
5690            cuda_toolkit_root: PathBuf::from("/missing/cuda"),
5691            ccbin_path: PathBuf::from("/missing/c++"),
5692            ar_path: PathBuf::from("/missing/ar"),
5693            nvcc_threads: 4,
5694            object_cache_dir: root.path().join("object-cache"),
5695            plan_only: true,
5696        })
5697        .unwrap();
5698
5699        assert_eq!(receipt.status, NativeOperatorSourceBuildStatus::Plan);
5700        assert_eq!(receipt.architecture_argument, "-arch=compute_80");
5701        assert_eq!(receipt.commands.len(), 2);
5702        assert!(receipt.commands[0]
5703            .argv
5704            .windows(2)
5705            .any(|pair| pair == ["--threads", "4"]));
5706        assert!(receipt.toolchain.is_none());
5707        assert!(receipt.commands.iter().all(|command| {
5708            [
5709                root.path().join("plan").join(&command.stdout_log),
5710                root.path().join("plan").join(&command.stderr_log),
5711            ]
5712            .iter()
5713            .all(|path| fs::metadata(path).is_ok_and(|metadata| metadata.len() > 0))
5714        }));
5715    }
5716
5717    #[test]
5718    fn missing_toolchain_writes_reject_receipt_before_any_compiler_spawn() {
5719        let root = tempfile::tempdir().unwrap();
5720        let (source_root, definition_path) = write_fixture(root.path());
5721        let plan_path = root.path().join("source-build.plan.json");
5722        lock_native_operator_source_definition(&definition_path, &source_root, &plan_path).unwrap();
5723        let output_dir = root.path().join("toolchain-reject");
5724
5725        let error = run_native_operator_source_build(&NativeOperatorSourceBuildRequest {
5726            plan_path,
5727            source_root,
5728            output_dir: output_dir.clone(),
5729            compute_capability: "sm_89".to_string(),
5730            builder_sha: "7".repeat(40),
5731            nvcc_path: PathBuf::from("/missing/nvcc"),
5732            cuda_toolkit_root: PathBuf::from("/missing/cuda"),
5733            ccbin_path: PathBuf::from("/missing/c++"),
5734            ar_path: PathBuf::from("/missing/ar"),
5735            nvcc_threads: 4,
5736            object_cache_dir: root.path().join("object-cache"),
5737            plan_only: false,
5738        })
5739        .unwrap_err();
5740
5741        assert!(matches!(
5742            error,
5743            NativeOperatorBuilderError::SourceBuildRejected { .. }
5744        ));
5745        let receipt: NativeOperatorSourceBuildReceipt =
5746            read_json(&output_dir.join("source-build.receipt.json")).unwrap();
5747        assert_eq!(receipt.status, NativeOperatorSourceBuildStatus::Reject);
5748        assert!(receipt
5749            .failure_class
5750            .as_deref()
5751            .is_some_and(|failure| failure.starts_with("toolchain_preflight_failed:")));
5752        assert!(receipt.commands.iter().all(|command| {
5753            [
5754                output_dir.join(&command.stdout_log),
5755                output_dir.join(&command.stderr_log),
5756            ]
5757            .iter()
5758            .all(|path| fs::metadata(path).is_ok_and(|metadata| metadata.len() > 0))
5759        }));
5760    }
5761
5762    #[test]
5763    fn rejects_incomplete_or_undeclared_depfiles_before_cache_publish() {
5764        for (name, mode) in [
5765            ("missing-header", FakeDepfileMode::MissingDeclaredHeader),
5766            ("undeclared-external", FakeDepfileMode::UndeclaredExternal),
5767        ] {
5768            let root = tempfile::tempdir().unwrap();
5769            let (source_root, definition_path) = write_fixture(root.path());
5770            let plan_path = root.path().join("source-build.plan.json");
5771            lock_native_operator_source_definition(&definition_path, &source_root, &plan_path)
5772                .unwrap();
5773            let fake_cuda = write_fake_nvcc_with_mode(root.path(), mode);
5774            let output_dir = root.path().join(name);
5775            let object_cache_dir = root.path().join("object-cache");
5776
5777            let error = run_native_operator_source_build(&NativeOperatorSourceBuildRequest {
5778                plan_path,
5779                source_root,
5780                output_dir: output_dir.clone(),
5781                compute_capability: "sm_89".to_string(),
5782                builder_sha: "7".repeat(40),
5783                nvcc_path: fake_cuda.nvcc,
5784                cuda_toolkit_root: fake_cuda.root,
5785                ccbin_path: fake_cuda.ccbin.clone(),
5786                ar_path: PathBuf::from("/usr/bin/ar"),
5787                nvcc_threads: 2,
5788                object_cache_dir: object_cache_dir.clone(),
5789                plan_only: false,
5790            })
5791            .unwrap_err();
5792
5793            assert!(matches!(
5794                error,
5795                NativeOperatorBuilderError::SourceBuildRejected { .. }
5796            ));
5797            let receipt: NativeOperatorSourceBuildReceipt =
5798                read_json(&output_dir.join("source-build.receipt.json")).unwrap();
5799            assert!(receipt
5800                .failure_class
5801                .as_deref()
5802                .is_some_and(|failure| failure.starts_with("dependency_validation_failed:")));
5803            assert_eq!(
5804                receipt.commands[0].object_cache_status,
5805                Some(NativeOperatorSourceObjectCacheStatus::Rejected)
5806            );
5807            assert!(receipt.commands[0].object_cache_entry.is_none());
5808            assert!(
5809                fs::read_dir(object_cache_dir).unwrap().all(|entry| {
5810                    entry
5811                        .is_ok_and(|entry| entry.file_name() == ".host-toolchains")
5812                }),
5813                "invalid dependency evidence may cache toolchain inventory but must not publish an object"
5814            );
5815        }
5816    }
5817
5818    #[test]
5819    fn rejects_tampered_cached_depfile_proof_before_compiler_start() {
5820        let root = tempfile::tempdir().unwrap();
5821        let (source_root, definition_path) = write_fixture(root.path());
5822        let plan_path = root.path().join("source-build.plan.json");
5823        lock_native_operator_source_definition(&definition_path, &source_root, &plan_path).unwrap();
5824        let fake_cuda = write_fake_nvcc(root.path());
5825        let object_cache_dir = root.path().join("object-cache");
5826        let request = |name: &str| NativeOperatorSourceBuildRequest {
5827            plan_path: plan_path.clone(),
5828            source_root: source_root.clone(),
5829            output_dir: root.path().join(name),
5830            compute_capability: "sm_89".to_string(),
5831            builder_sha: "7".repeat(40),
5832            nvcc_path: fake_cuda.nvcc.clone(),
5833            cuda_toolkit_root: fake_cuda.root.clone(),
5834            ccbin_path: fake_cuda.ccbin.clone(),
5835            ar_path: PathBuf::from("/usr/bin/ar"),
5836            nvcc_threads: 2,
5837            object_cache_dir: object_cache_dir.clone(),
5838            plan_only: false,
5839        };
5840
5841        let cold = run_native_operator_source_build(&request("cold")).unwrap();
5842        let cache_entry = PathBuf::from(
5843            cold.commands[0]
5844                .object_cache_entry
5845                .as_deref()
5846                .expect("published object records its cache entry"),
5847        );
5848        let proof_dir = cache_entry.join("dependency-proof");
5849        fs::write(
5850            proof_dir.join("dependency.d"),
5851            "forged.o: kernels/marlin.cu\n",
5852        )
5853        .unwrap();
5854
5855        let error = run_native_operator_source_build(&request("tampered")).unwrap_err();
5856        assert!(matches!(
5857            error,
5858            NativeOperatorBuilderError::SourceBuildRejected { .. }
5859        ));
5860        let receipt: NativeOperatorSourceBuildReceipt =
5861            read_json(&root.path().join("tampered/source-build.receipt.json")).unwrap();
5862        assert!(receipt
5863            .failure_class
5864            .as_deref()
5865            .is_some_and(|failure| failure.starts_with("cached_dependency_proof_failed:")));
5866        assert_eq!(
5867            fs::read_to_string(&fake_cuda.compile_counter)
5868                .unwrap()
5869                .lines()
5870                .count(),
5871            1,
5872            "tampered cache proof must reject before another compiler starts"
5873        );
5874    }
5875
5876    #[test]
5877    fn rejects_tampered_cached_compiler_depfile_before_compiler_start() {
5878        let root = tempfile::tempdir().unwrap();
5879        let (source_root, definition_path) = write_fixture(root.path());
5880        let plan_path = root.path().join("source-build.plan.json");
5881        lock_native_operator_source_definition(&definition_path, &source_root, &plan_path).unwrap();
5882        let fake_cuda = write_fake_nvcc(root.path());
5883        let object_cache_dir = root.path().join("object-cache");
5884        let request = |name: &str| NativeOperatorSourceBuildRequest {
5885            plan_path: plan_path.clone(),
5886            source_root: source_root.clone(),
5887            output_dir: root.path().join(name),
5888            compute_capability: "sm_89".to_string(),
5889            builder_sha: "7".repeat(40),
5890            nvcc_path: fake_cuda.nvcc.clone(),
5891            cuda_toolkit_root: fake_cuda.root.clone(),
5892            ccbin_path: fake_cuda.ccbin.clone(),
5893            ar_path: PathBuf::from("/usr/bin/ar"),
5894            nvcc_threads: 2,
5895            object_cache_dir: object_cache_dir.clone(),
5896            plan_only: false,
5897        };
5898
5899        let cold = run_native_operator_source_build(&request("cold")).unwrap();
5900        let cache_entry = PathBuf::from(
5901            cold.commands[0]
5902                .object_cache_entry
5903                .as_deref()
5904                .expect("published object records its cache entry"),
5905        );
5906        let proof_path = cache_entry.join("dependency-proof/proof.json");
5907        let compiler_depfile_path = cache_entry.join("dependency-proof/compiler-dependency.raw.d");
5908        let mut proof: NativeOperatorObjectDependencyProof = read_json(&proof_path).unwrap();
5909        let backend_binding = proof
5910            .depfile_bindings
5911            .iter_mut()
5912            .find(|binding| {
5913                binding.dependency.domain == NativeOperatorDependencyDomain::BackendToolchain
5914            })
5915            .expect("fixture depfile contains a backend toolchain dependency");
5916        let original_producer = backend_binding.producer_path.clone();
5917        let forged_producer =
5918            original_producer.replace("/bin/../include/cuda.h", "/forged/include/cuda.h");
5919        assert_ne!(forged_producer, original_producer);
5920        backend_binding.producer_path = forged_producer.clone();
5921        let compiler_raw = fs::read_to_string(&compiler_depfile_path)
5922            .unwrap()
5923            .replace(&original_producer, &forged_producer);
5924        proof.compiler_depfile_sha256 = sha256_bytes(compiler_raw.as_bytes());
5925        fs::write(&compiler_depfile_path, compiler_raw).unwrap();
5926        write_json(&proof_path, &proof).unwrap();
5927
5928        let error = run_native_operator_source_build(&request("tampered")).unwrap_err();
5929        assert!(matches!(
5930            error,
5931            NativeOperatorBuilderError::SourceBuildRejected { .. }
5932        ));
5933        let receipt: NativeOperatorSourceBuildReceipt =
5934            read_json(&root.path().join("tampered/source-build.receipt.json")).unwrap();
5935        assert!(receipt
5936            .failure_class
5937            .as_deref()
5938            .is_some_and(|failure| failure.starts_with("cached_dependency_proof_failed:")));
5939        assert_eq!(
5940            fs::read_to_string(&fake_cuda.compile_counter)
5941                .unwrap()
5942                .lines()
5943                .count(),
5944            1,
5945            "tampered compiler depfile must reject before another compiler starts"
5946        );
5947    }
5948
5949    #[test]
5950    fn rejects_partial_cached_dependency_proof_before_compiler_start() {
5951        let root = tempfile::tempdir().unwrap();
5952        let (source_root, definition_path) = write_fixture(root.path());
5953        let plan_path = root.path().join("source-build.plan.json");
5954        lock_native_operator_source_definition(&definition_path, &source_root, &plan_path).unwrap();
5955        let fake_cuda = write_fake_nvcc(root.path());
5956        let object_cache_dir = root.path().join("object-cache");
5957        let request = |name: &str| NativeOperatorSourceBuildRequest {
5958            plan_path: plan_path.clone(),
5959            source_root: source_root.clone(),
5960            output_dir: root.path().join(name),
5961            compute_capability: "sm_89".to_string(),
5962            builder_sha: "7".repeat(40),
5963            nvcc_path: fake_cuda.nvcc.clone(),
5964            cuda_toolkit_root: fake_cuda.root.clone(),
5965            ccbin_path: fake_cuda.ccbin.clone(),
5966            ar_path: PathBuf::from("/usr/bin/ar"),
5967            nvcc_threads: 2,
5968            object_cache_dir: object_cache_dir.clone(),
5969            plan_only: false,
5970        };
5971
5972        let cold = run_native_operator_source_build(&request("cold")).unwrap();
5973        let cache_entry = PathBuf::from(
5974            cold.commands[0]
5975                .object_cache_entry
5976                .as_deref()
5977                .expect("published object records its cache entry"),
5978        );
5979        fs::remove_file(cache_entry.join("dependency-proof/proof.json")).unwrap();
5980
5981        let error = run_native_operator_source_build(&request("partial")).unwrap_err();
5982        assert!(matches!(
5983            error,
5984            NativeOperatorBuilderError::SourceBuildRejected { .. }
5985        ));
5986        let receipt: NativeOperatorSourceBuildReceipt =
5987            read_json(&root.path().join("partial/source-build.receipt.json")).unwrap();
5988        assert!(receipt
5989            .failure_class
5990            .as_deref()
5991            .is_some_and(|failure| failure.starts_with("cached_dependency_proof_failed:")));
5992        assert_eq!(
5993            fs::read_to_string(&fake_cuda.compile_counter)
5994                .unwrap()
5995                .lines()
5996                .count(),
5997            1,
5998            "partial proof publication must reject before another compiler starts"
5999        );
6000    }
6001
6002    #[test]
6003    fn rejects_tampered_cached_dependency_identity_before_compiler_start() {
6004        let root = tempfile::tempdir().unwrap();
6005        let (source_root, definition_path) = write_fixture(root.path());
6006        let plan_path = root.path().join("source-build.plan.json");
6007        lock_native_operator_source_definition(&definition_path, &source_root, &plan_path).unwrap();
6008        let fake_cuda = write_fake_nvcc(root.path());
6009        let object_cache_dir = root.path().join("object-cache");
6010        let request = |name: &str| NativeOperatorSourceBuildRequest {
6011            plan_path: plan_path.clone(),
6012            source_root: source_root.clone(),
6013            output_dir: root.path().join(name),
6014            compute_capability: "sm_89".to_string(),
6015            builder_sha: "7".repeat(40),
6016            nvcc_path: fake_cuda.nvcc.clone(),
6017            cuda_toolkit_root: fake_cuda.root.clone(),
6018            ccbin_path: fake_cuda.ccbin.clone(),
6019            ar_path: PathBuf::from("/usr/bin/ar"),
6020            nvcc_threads: 2,
6021            object_cache_dir: object_cache_dir.clone(),
6022            plan_only: false,
6023        };
6024
6025        let cold = run_native_operator_source_build(&request("cold")).unwrap();
6026        let cache_entry = PathBuf::from(
6027            cold.commands[0]
6028                .object_cache_entry
6029                .as_deref()
6030                .expect("published object records its cache entry"),
6031        );
6032        let proof_path = cache_entry.join("dependency-proof/proof.json");
6033        let mut proof: NativeOperatorObjectDependencyProof = read_json(&proof_path).unwrap();
6034        let backend_dependency = proof
6035            .observed_dependencies
6036            .iter_mut()
6037            .find(|dependency| {
6038                dependency.domain == NativeOperatorDependencyDomain::BackendToolchain
6039            })
6040            .expect("fixture depfile contains a backend toolchain dependency");
6041        backend_dependency.sha256 = "b".repeat(64);
6042        proof.dependency_set_sha256 =
6043            observed_dependency_set_sha256(&proof.observed_dependencies).unwrap();
6044        write_json(&proof_path, &proof).unwrap();
6045
6046        let error = run_native_operator_source_build(&request("tampered")).unwrap_err();
6047        assert!(matches!(
6048            error,
6049            NativeOperatorBuilderError::SourceBuildRejected { .. }
6050        ));
6051        let receipt: NativeOperatorSourceBuildReceipt =
6052            read_json(&root.path().join("tampered/source-build.receipt.json")).unwrap();
6053        assert!(receipt
6054            .failure_class
6055            .as_deref()
6056            .is_some_and(|failure| failure.starts_with("cached_dependency_proof_failed:")));
6057        assert_eq!(
6058            fs::read_to_string(&fake_cuda.compile_counter)
6059                .unwrap()
6060                .lines()
6061                .count(),
6062            1,
6063            "tampered typed dependency identity must reject before another compiler starts"
6064        );
6065    }
6066
6067    #[test]
6068    fn compiler_inputs_invalidate_object_cache_without_hidden_probe_hits() {
6069        let root = tempfile::tempdir().unwrap();
6070        let (source_root, definition_path) = write_fixture(root.path());
6071        let plan_path = root.path().join("source-build.plan.json");
6072        lock_native_operator_source_definition(&definition_path, &source_root, &plan_path).unwrap();
6073        let fake_cuda = write_fake_nvcc(root.path());
6074        let object_cache_dir = root.path().join("object-cache");
6075        let run_build = |name: &str| {
6076            run_native_operator_source_build(&NativeOperatorSourceBuildRequest {
6077                plan_path: plan_path.clone(),
6078                source_root: source_root.clone(),
6079                output_dir: root.path().join(name),
6080                compute_capability: "sm_89".to_string(),
6081                builder_sha: "7".repeat(40),
6082                nvcc_path: fake_cuda.nvcc.clone(),
6083                cuda_toolkit_root: fake_cuda.root.clone(),
6084                ccbin_path: fake_cuda.ccbin.clone(),
6085                ar_path: PathBuf::from("/usr/bin/ar"),
6086                nvcc_threads: 2,
6087                object_cache_dir: object_cache_dir.clone(),
6088                plan_only: false,
6089            })
6090            .unwrap()
6091        };
6092
6093        assert_eq!(run_build("cold").compiled_translation_units.len(), 1);
6094        assert_eq!(run_build("hit").cache_hit_translation_units.len(), 1);
6095        for (index, relative) in [
6096            "bin/ptxas",
6097            "include/cuda.h",
6098            "nvvm/libdevice/libdevice.10.bc",
6099        ]
6100        .iter()
6101        .enumerate()
6102        {
6103            let path = fake_cuda.root.join(relative);
6104            let mut contents = fs::read_to_string(&path).unwrap();
6105            contents.push_str(&format!("mutation-{index}\n"));
6106            fs::write(path, contents).unwrap();
6107            let receipt = run_build(&format!("mutation-{index}"));
6108            assert_eq!(receipt.compiled_translation_units, ["kernels/marlin.cu"]);
6109            assert!(receipt.cache_hit_translation_units.is_empty());
6110        }
6111        let host_root = fake_cuda
6112            .ccbin
6113            .parent()
6114            .and_then(Path::parent)
6115            .unwrap()
6116            .to_path_buf();
6117        for (index, relative) in ["include/stddef.h", "bin/cc1plus", "bin/driver.specs"]
6118            .iter()
6119            .enumerate()
6120        {
6121            let path = host_root.join(relative);
6122            let mut contents = fs::read_to_string(&path).unwrap();
6123            contents.push_str(&format!("host-mutation-{index}\n"));
6124            fs::write(path, contents).unwrap();
6125            let receipt = run_build(&format!("host-mutation-{index}"));
6126            assert_eq!(receipt.compiled_translation_units, ["kernels/marlin.cu"]);
6127            assert!(receipt.cache_hit_translation_units.is_empty());
6128        }
6129        fs::write(&fake_cuda.host_driver_config, "external driver option v2\n").unwrap();
6130        let external_config_receipt = run_build("external-driver-config-mutation");
6131        assert_eq!(
6132            external_config_receipt.compiled_translation_units,
6133            ["kernels/marlin.cu"]
6134        );
6135        assert!(external_config_receipt
6136            .cache_hit_translation_units
6137            .is_empty());
6138        assert_eq!(
6139            fs::read_to_string(&fake_cuda.compile_counter)
6140                .unwrap()
6141                .lines()
6142                .count(),
6143            8
6144        );
6145        assert_eq!(
6146            fs::read_to_string(&fake_cuda.invocation_counter)
6147                .unwrap()
6148                .lines()
6149                .count(),
6150            16,
6151            "each cache miss invokes one nvcc version probe and one compile; the full hit invokes zero"
6152        );
6153    }
6154
6155    #[test]
6156    fn empty_host_include_root_is_locked_and_new_header_invalidates_object_cache() {
6157        let root = tempfile::tempdir().unwrap();
6158        let (source_root, definition_path) = write_fixture(root.path());
6159        let plan_path = root.path().join("source-build.plan.json");
6160        lock_native_operator_source_definition(&definition_path, &source_root, &plan_path).unwrap();
6161        let fake_cuda = write_fake_nvcc(root.path());
6162        let object_cache_dir = root.path().join("object-cache");
6163        let run_build = |name: &str| {
6164            run_native_operator_source_build(&NativeOperatorSourceBuildRequest {
6165                plan_path: plan_path.clone(),
6166                source_root: source_root.clone(),
6167                output_dir: root.path().join(name),
6168                compute_capability: "sm_89".to_string(),
6169                builder_sha: "7".repeat(40),
6170                nvcc_path: fake_cuda.nvcc.clone(),
6171                cuda_toolkit_root: fake_cuda.root.clone(),
6172                ccbin_path: fake_cuda.ccbin.clone(),
6173                ar_path: PathBuf::from("/usr/bin/ar"),
6174                nvcc_threads: 2,
6175                object_cache_dir: object_cache_dir.clone(),
6176                plan_only: false,
6177            })
6178            .unwrap()
6179        };
6180
6181        assert_eq!(
6182            fs::read_dir(&fake_cuda.empty_host_include_root)
6183                .unwrap()
6184                .count(),
6185            0
6186        );
6187        let cold = run_build("cold-empty-root");
6188        assert_eq!(cold.compiled_translation_units, ["kernels/marlin.cu"]);
6189        let cold_manifest: NativeOperatorHostToolchainManifest = read_json(
6190            &root
6191                .path()
6192                .join("cold-empty-root/toolchain/host-static-manifest.json"),
6193        )
6194        .unwrap();
6195        let empty_root = fake_cuda.empty_host_include_root.display().to_string();
6196        assert!(cold_manifest.include_roots.contains(&empty_root));
6197        assert!(!cold_manifest
6198            .files
6199            .iter()
6200            .any(|file| Path::new(&file.logical_path).starts_with(&empty_root)));
6201
6202        let hit = run_build("unchanged-empty-root");
6203        assert!(hit.compiled_translation_units.is_empty());
6204        assert_eq!(hit.cache_hit_translation_units, ["kernels/marlin.cu"]);
6205        assert!(!hit.commands[0].compiler_executed);
6206
6207        let late_header = fake_cuda.empty_host_include_root.join("late-header.h");
6208        fs::write(&late_header, "#define LATE_HEADER 1\n").unwrap();
6209        let changed = run_build("populated-root");
6210        assert_eq!(changed.compiled_translation_units, ["kernels/marlin.cu"]);
6211        assert!(changed.cache_hit_translation_units.is_empty());
6212        let changed_manifest: NativeOperatorHostToolchainManifest = read_json(
6213            &root
6214                .path()
6215                .join("populated-root/toolchain/host-static-manifest.json"),
6216        )
6217        .unwrap();
6218        assert!(changed_manifest.files.iter().any(|file| {
6219            file.logical_path == late_header.display().to_string()
6220                && file.resolved_path == late_header.canonicalize().unwrap().display().to_string()
6221        }));
6222        assert_eq!(
6223            fs::read_to_string(&fake_cuda.compile_counter)
6224                .unwrap()
6225                .lines()
6226                .count(),
6227            2
6228        );
6229        assert_eq!(
6230            fs::read_to_string(&fake_cuda.invocation_counter)
6231                .unwrap()
6232                .lines()
6233                .count(),
6234            4,
6235            "the unchanged empty root is an nvcc-free hit; adding a header forces one probe and compile"
6236        );
6237    }
6238
6239    #[test]
6240    fn cuda_toolkit_manifest_accepts_internal_symlink_directories_and_rejects_escapes() {
6241        let root = tempfile::tempdir().unwrap();
6242        let fake_cuda = write_fake_nvcc(root.path());
6243        let internal_target = fake_cuda.root.join("targets/headers");
6244        fs::create_dir_all(&internal_target).unwrap();
6245        fs::write(internal_target.join("linked.h"), "#define LINKED 1\n").unwrap();
6246        let internal_link = fake_cuda.root.join("include/linked");
6247        symlink(&internal_target, &internal_link).unwrap();
6248
6249        let manifest = build_cuda_toolkit_manifest(&fake_cuda.root).unwrap();
6250        assert!(manifest.entries.iter().any(|entry| {
6251            entry.logical_path == "include/linked/linked.h"
6252                && entry.resolved_path == "targets/headers/linked.h"
6253        }));
6254
6255        fs::remove_file(&internal_link).unwrap();
6256        symlink("/etc", &internal_link).unwrap();
6257        let error = build_cuda_toolkit_manifest(&fake_cuda.root).unwrap_err();
6258        assert!(error.to_string().contains("escapes its canonical root"));
6259    }
6260
6261    #[test]
6262    fn bounded_fixture_build_writes_pass_receipt_and_archive_hash() {
6263        let root = tempfile::tempdir().unwrap();
6264        let (source_root, definition_path) = write_fixture(root.path());
6265        let plan_path = root.path().join("source-build.plan.json");
6266        lock_native_operator_source_definition(&definition_path, &source_root, &plan_path).unwrap();
6267        let output_dir = root.path().join("build");
6268        let fake_cuda = write_fake_nvcc(root.path());
6269        let object_cache_dir = root.path().join("object-cache");
6270
6271        let receipt = run_native_operator_source_build(&NativeOperatorSourceBuildRequest {
6272            plan_path: plan_path.clone(),
6273            source_root: source_root.clone(),
6274            output_dir: output_dir.clone(),
6275            compute_capability: "sm_89".to_string(),
6276            builder_sha: "7".repeat(40),
6277            nvcc_path: fake_cuda.nvcc.clone(),
6278            cuda_toolkit_root: fake_cuda.root.clone(),
6279            ccbin_path: fake_cuda.ccbin.clone(),
6280            ar_path: PathBuf::from("/usr/bin/ar"),
6281            nvcc_threads: 2,
6282            object_cache_dir: object_cache_dir.clone(),
6283            plan_only: false,
6284        })
6285        .unwrap();
6286
6287        assert_eq!(receipt.status, NativeOperatorSourceBuildStatus::Pass);
6288        let static_toolchain = &receipt
6289            .toolchain
6290            .as_ref()
6291            .expect("completed build records toolchain identity")
6292            .static_identity;
6293        assert_eq!(static_toolchain.backend, NativeOperatorBackend::Cuda);
6294        assert_eq!(
6295            static_toolchain.compiler_driver,
6296            NativeOperatorSourceCompilerDriver::CudaNvcc
6297        );
6298        assert!(is_sha256_digest(receipt.archive_sha256.as_deref().unwrap()));
6299        assert!(output_dir.join("libmarlin.a").is_file());
6300        assert!(output_dir.join("source-build.receipt.json").is_file());
6301        assert_eq!(receipt.compiled_translation_units, ["kernels/marlin.cu"]);
6302        assert!(receipt.cache_hit_translation_units.is_empty());
6303        assert_eq!(
6304            receipt.commands[0]
6305                .observed_dependencies
6306                .iter()
6307                .map(|dependency| dependency.domain)
6308                .collect::<Vec<_>>(),
6309            [
6310                NativeOperatorDependencyDomain::Source,
6311                NativeOperatorDependencyDomain::Source,
6312                NativeOperatorDependencyDomain::BackendToolchain,
6313                NativeOperatorDependencyDomain::HostToolchain,
6314            ]
6315        );
6316        assert!(receipt.commands[0]
6317            .observed_dependencies
6318            .iter()
6319            .all(|dependency| is_sha256_digest(&dependency.sha256)));
6320        let compiler_depfile = fs::read_to_string(
6321            output_dir.join(
6322                receipt.commands[0]
6323                    .compiler_depfile
6324                    .as_deref()
6325                    .expect("cold build records its compiler depfile"),
6326            ),
6327        )
6328        .unwrap();
6329        let portable_depfile = fs::read_to_string(
6330            output_dir.join(
6331                receipt.commands[0]
6332                    .depfile
6333                    .as_deref()
6334                    .expect("cold build records its portable depfile"),
6335            ),
6336        )
6337        .unwrap();
6338        assert!(compiler_depfile.contains("/bin/../include/cuda.h"));
6339        assert!(!portable_depfile.contains("/../"));
6340        let plan: NativeOperatorSourceBuildPlan = read_json(&plan_path).unwrap();
6341        let toolchain_scope =
6342            load_toolchain_dependency_scope(&output_dir, static_toolchain).unwrap();
6343        let cache_entry = PathBuf::from(
6344            receipt.commands[0]
6345                .object_cache_entry
6346                .as_deref()
6347                .expect("cold build records its cache entry"),
6348        );
6349        let object_name = Path::new(
6350            receipt.commands[0]
6351                .object_file
6352                .as_deref()
6353                .expect("cold build records its object"),
6354        )
6355        .file_name()
6356        .unwrap()
6357        .to_str()
6358        .unwrap();
6359        validate_existing_dependency_proof(
6360            &cache_entry.join("dependency-proof"),
6361            receipt.commands[0].object_cache_key.as_deref().unwrap(),
6362            receipt.commands[0].object_sha256.as_deref().unwrap(),
6363            &plan.translation_units[0],
6364            &plan.dependency_closures[0],
6365            &format!("/another-worktree/objects/{object_name}"),
6366            &toolchain_scope,
6367        )
6368        .expect("a concurrent cache winner from another worktree remains valid");
6369        assert!(receipt.commands.iter().all(|command| {
6370            [
6371                output_dir.join(&command.stdout_log),
6372                output_dir.join(&command.stderr_log),
6373            ]
6374            .iter()
6375            .all(|path| {
6376                fs::read_to_string(path).is_ok_and(|content| content.contains("execution-start"))
6377            })
6378        }));
6379        let host_probe_count = fs::read_to_string(&fake_cuda.host_compiler_invocation_counter)
6380            .unwrap()
6381            .lines()
6382            .count();
6383
6384        let cached_output_dir = root.path().join("cached-build");
6385        let cached_receipt = run_native_operator_source_build(&NativeOperatorSourceBuildRequest {
6386            plan_path: plan_path.clone(),
6387            source_root: source_root.clone(),
6388            output_dir: cached_output_dir.clone(),
6389            compute_capability: "sm_89".to_string(),
6390            builder_sha: "8".repeat(40),
6391            nvcc_path: fake_cuda.nvcc.clone(),
6392            cuda_toolkit_root: fake_cuda.root.clone(),
6393            ccbin_path: fake_cuda.ccbin.clone(),
6394            ar_path: PathBuf::from("/usr/bin/ar"),
6395            nvcc_threads: 8,
6396            object_cache_dir,
6397            plan_only: false,
6398        })
6399        .unwrap();
6400
6401        assert!(cached_receipt.compiled_translation_units.is_empty());
6402        assert_eq!(
6403            cached_receipt.cache_hit_translation_units,
6404            ["kernels/marlin.cu"]
6405        );
6406        assert!(!cached_receipt.commands[0].compiler_executed);
6407        assert_eq!(
6408            cached_receipt.commands[0].object_cache_status,
6409            Some(NativeOperatorSourceObjectCacheStatus::Hit)
6410        );
6411        assert_eq!(
6412            cached_receipt.commands[0].dependency_validation,
6413            Some(NativeOperatorDependencyValidation::CacheProof)
6414        );
6415        assert_eq!(
6416            cached_receipt.commands[0].observed_dependencies,
6417            receipt.commands[0].observed_dependencies
6418        );
6419        assert_ne!(
6420            cached_receipt.commands[0].object_file,
6421            cached_receipt.commands[0].depfile_producer_object_file,
6422            "portable cache proof must preserve the producer object while restoring to a new output root"
6423        );
6424        assert!(cached_receipt.commands[0]
6425            .depfile
6426            .as_deref()
6427            .is_some_and(|relative| cached_output_dir.join(relative).is_file()));
6428        assert_eq!(
6429            fs::read_to_string(&fake_cuda.compile_counter)
6430                .unwrap()
6431                .lines()
6432                .count(),
6433            1
6434        );
6435        assert_eq!(
6436            fs::read_to_string(&fake_cuda.invocation_counter)
6437                .unwrap()
6438                .lines()
6439                .count(),
6440            2,
6441            "cold build invokes one miss-only version probe plus one compile; full cache hit invokes neither"
6442        );
6443        assert_eq!(
6444            fs::read_to_string(&fake_cuda.host_compiler_invocation_counter)
6445                .unwrap()
6446                .lines()
6447                .count(),
6448            host_probe_count + 2,
6449            "full cache hit runs only the bounded include/driver configuration probes"
6450        );
6451        assert_eq!(
6452            receipt.archive_sha256, cached_receipt.archive_sha256,
6453            "worker-count changes must not change the object or archive"
6454        );
6455        assert_eq!(
6456            receipt.inputs_sha256, cached_receipt.inputs_sha256,
6457            "worker-count and provenance commit changes are not output-content inputs"
6458        );
6459
6460        fs::write(source_root.join("LICENSE"), "fixture license\n").unwrap();
6461        let package_spec = crate::NativeOperatorPackageSpec {
6462            schema_version: crate::NATIVE_OPERATOR_PACKAGE_SPEC_SCHEMA_VERSION,
6463            operator: CudaNativeBuildUnit::Marlin.artifact_operator().to_string(),
6464            operator_abi_version: "1".to_string(),
6465            backend: ferrum_types::NativeOperatorBackend::Cuda,
6466            compute_capabilities: vec!["sm_89".to_string()],
6467            operation_bindings: vec![ferrum_types::NativeOperatorBinding {
6468                operation_id: "operation.dense_linear".to_string(),
6469                operation_contract_version: ferrum_types::NativeOperatorContractVersion::new(1, 0),
6470                provider_id: "provider.cuda.dense_linear.f16.marlin".to_string(),
6471                provider_version: ferrum_types::NativeOperatorContractVersion::new(1, 0),
6472                provider_implementation_fingerprint: "a".repeat(64),
6473                entrypoints: CudaNativeBuildUnit::Marlin
6474                    .required_exports()
6475                    .iter()
6476                    .map(|value| (*value).to_string())
6477                    .collect(),
6478            }],
6479            required_exports: CudaNativeBuildUnit::Marlin
6480                .required_exports()
6481                .iter()
6482                .map(|value| (*value).to_string())
6483                .collect(),
6484            license_files: vec![crate::NativeOperatorLicenseInput {
6485                source_path: "LICENSE".to_string(),
6486                output_path: "licenses/LICENSE".to_string(),
6487            }],
6488            cuda_toolkit: Some("12.4".to_string()),
6489            cuda_runtime_min: Some("12.4".to_string()),
6490            system_libraries: vec![
6491                ferrum_native_ops::NativeOperatorSystemLibrary::CudaRuntime,
6492                ferrum_native_ops::NativeOperatorSystemLibrary::StdCxx,
6493            ],
6494        };
6495        let package_spec_path = root.path().join("package-spec.json");
6496        write_json(&package_spec_path, &package_spec).unwrap();
6497        let catalog_path = root.path().join("operation-catalog.json");
6498        let abi_path = root.path().join("native-abi.json");
6499        write_json(
6500            &catalog_path,
6501            &ferrum_types::NativeOperatorProviderCatalog {
6502                schema_version: ferrum_types::NATIVE_OPERATOR_PROVIDER_CATALOG_SCHEMA_VERSION,
6503                backend: ferrum_types::NativeOperatorBackend::Cuda,
6504                providers: vec![ferrum_types::NativeOperatorProviderCatalogRow {
6505                    operation_id: "operation.dense_linear".to_string(),
6506                    operation_contract_version: ferrum_types::NativeOperatorContractVersion::new(
6507                        1, 0,
6508                    ),
6509                    operation_fingerprint: "b".repeat(64),
6510                    provider_id: "provider.cuda.dense_linear.f16.marlin".to_string(),
6511                    provider_version: ferrum_types::NativeOperatorContractVersion::new(1, 0),
6512                    provider_implementation_fingerprint: "a".repeat(64),
6513                }],
6514            },
6515        )
6516        .unwrap();
6517        write_json(
6518            &abi_path,
6519            &ferrum_types::NativeOperatorAbiContract {
6520                schema_version: ferrum_types::NATIVE_OPERATOR_ABI_CONTRACT_SCHEMA_VERSION,
6521                ferrum_native_abi_version: ferrum_types::FERRUM_NATIVE_OPERATOR_ABI_VERSION
6522                    .to_string(),
6523                descriptor_struct: "FerrumNativeOperatorDescriptorV2".to_string(),
6524                descriptor_symbol_policy: "operator_namespaced".to_string(),
6525                descriptor_fields: [
6526                    ("struct_size", "uint32_t"),
6527                    ("ferrum_native_abi_version", "uint32_t"),
6528                    ("operator_name", "const char *"),
6529                    ("operator_abi_version", "const char *"),
6530                    ("g03_catalog_sha256", "const char *"),
6531                    ("abi_contract_sha256", "const char *"),
6532                ]
6533                .into_iter()
6534                .map(|(name, c_type)| ferrum_types::NativeOperatorAbiField {
6535                    name: name.to_string(),
6536                    c_type: c_type.to_string(),
6537                })
6538                .collect(),
6539            },
6540        )
6541        .unwrap();
6542        let package_output = root.path().join("package");
6543
6544        let package_receipt =
6545            crate::package_native_operator(&crate::NativeOperatorPackageRequest {
6546                spec_path: package_spec_path,
6547                source_root: source_root.clone(),
6548                license_root: source_root.clone(),
6549                source_build_receipt_path: output_dir.join("source-build.receipt.json"),
6550                source_build_plan_path: plan_path.clone(),
6551                g03_catalog_path: catalog_path,
6552                abi_contract_path: abi_path,
6553                output_dir: package_output.clone(),
6554                cc: PathBuf::from("/usr/bin/cc"),
6555                ar: PathBuf::from("/usr/bin/ar"),
6556            })
6557            .unwrap();
6558
6559        assert_eq!(
6560            package_receipt.source_build_plan.sha256,
6561            receipt.plan_sha256
6562        );
6563        assert_eq!(
6564            package_receipt.source_archive_sha256,
6565            receipt.archive_sha256.unwrap()
6566        );
6567        assert!(package_output.join("package.receipt.json").is_file());
6568
6569        let cached_package_spec_path = root.path().join("cached-package-spec.json");
6570        write_json(&cached_package_spec_path, &package_spec).unwrap();
6571        let cached_package_output = root.path().join("cached-package");
6572        crate::package_native_operator(&crate::NativeOperatorPackageRequest {
6573            spec_path: cached_package_spec_path,
6574            source_root: source_root.clone(),
6575            license_root: source_root,
6576            source_build_receipt_path: cached_output_dir.join("source-build.receipt.json"),
6577            source_build_plan_path: plan_path,
6578            g03_catalog_path: root.path().join("operation-catalog.json"),
6579            abi_contract_path: root.path().join("native-abi.json"),
6580            output_dir: cached_package_output.clone(),
6581            cc: PathBuf::from("/usr/bin/cc"),
6582            ar: PathBuf::from("/usr/bin/ar"),
6583        })
6584        .unwrap();
6585        let cached_manifest: ferrum_types::NativeOperatorManifest =
6586            read_json(&cached_package_output.join("native_operator_manifest.json")).unwrap();
6587        assert_eq!(
6588            cached_manifest.build_summary.nvcc_version.as_deref(),
6589            Some("cuda-toolkit-static 12.4.0")
6590        );
6591    }
6592
6593    #[test]
6594    fn changing_one_translation_unit_recompiles_only_that_unit() {
6595        let root = tempfile::tempdir().unwrap();
6596        let (source_root, _) = write_fixture(root.path());
6597        fs::write(
6598            source_root.join("kernels/other.cu"),
6599            "int other_cuda(void) { return 1; }\n",
6600        )
6601        .unwrap();
6602        let mut source_definition = definition();
6603        source_definition.translation_units = vec![
6604            "kernels/marlin.cu".to_string(),
6605            "kernels/other.cu".to_string(),
6606        ];
6607        source_definition.dependency_closures = vec![
6608            NativeOperatorTranslationUnitDependencies {
6609                translation_unit: "kernels/marlin.cu".to_string(),
6610                headers: vec!["kernels/marlin.h".to_string()],
6611            },
6612            NativeOperatorTranslationUnitDependencies {
6613                translation_unit: "kernels/other.cu".to_string(),
6614                headers: Vec::new(),
6615            },
6616        ];
6617        let definition_path = root.path().join("two-tu-definition.json");
6618        write_json(&definition_path, &source_definition).unwrap();
6619        let first_plan_path = root.path().join("first.plan.json");
6620        lock_native_operator_source_definition(&definition_path, &source_root, &first_plan_path)
6621            .unwrap();
6622        let fake_cuda = write_fake_nvcc(root.path());
6623        let object_cache_dir = root.path().join("object-cache");
6624
6625        let first_receipt = run_native_operator_source_build(&NativeOperatorSourceBuildRequest {
6626            plan_path: first_plan_path,
6627            source_root: source_root.clone(),
6628            output_dir: root.path().join("first-build"),
6629            compute_capability: "sm_89".to_string(),
6630            builder_sha: "7".repeat(40),
6631            nvcc_path: fake_cuda.nvcc.clone(),
6632            cuda_toolkit_root: fake_cuda.root.clone(),
6633            ccbin_path: fake_cuda.ccbin.clone(),
6634            ar_path: PathBuf::from("/usr/bin/ar"),
6635            nvcc_threads: 2,
6636            object_cache_dir: object_cache_dir.clone(),
6637            plan_only: false,
6638        })
6639        .unwrap();
6640        assert_eq!(first_receipt.compiled_translation_units.len(), 2);
6641        assert!(first_receipt.cache_hit_translation_units.is_empty());
6642
6643        fs::write(
6644            source_root.join("kernels/other.cu"),
6645            "int other_cuda(void) { return 2; }\n",
6646        )
6647        .unwrap();
6648        let second_plan_path = root.path().join("second.plan.json");
6649        lock_native_operator_source_definition(&definition_path, &source_root, &second_plan_path)
6650            .unwrap();
6651        let second_receipt = run_native_operator_source_build(&NativeOperatorSourceBuildRequest {
6652            plan_path: second_plan_path,
6653            source_root: source_root.clone(),
6654            output_dir: root.path().join("second-build"),
6655            compute_capability: "sm_89".to_string(),
6656            builder_sha: "8".repeat(40),
6657            nvcc_path: fake_cuda.nvcc.clone(),
6658            cuda_toolkit_root: fake_cuda.root.clone(),
6659            ccbin_path: fake_cuda.ccbin.clone(),
6660            ar_path: PathBuf::from("/usr/bin/ar"),
6661            nvcc_threads: 4,
6662            object_cache_dir: object_cache_dir.clone(),
6663            plan_only: false,
6664        })
6665        .unwrap();
6666
6667        assert_eq!(
6668            second_receipt.compiled_translation_units,
6669            ["kernels/other.cu"]
6670        );
6671        assert_eq!(
6672            second_receipt.cache_hit_translation_units,
6673            ["kernels/marlin.cu"]
6674        );
6675        assert_eq!(
6676            fs::read_to_string(&fake_cuda.compile_counter)
6677                .unwrap()
6678                .lines()
6679                .count(),
6680            3
6681        );
6682
6683        fs::write(source_root.join("kernels/marlin.h"), "#define MARLIN 2\n").unwrap();
6684        let third_plan_path = root.path().join("third.plan.json");
6685        lock_native_operator_source_definition(&definition_path, &source_root, &third_plan_path)
6686            .unwrap();
6687        let third_receipt = run_native_operator_source_build(&NativeOperatorSourceBuildRequest {
6688            plan_path: third_plan_path,
6689            source_root,
6690            output_dir: root.path().join("third-build"),
6691            compute_capability: "sm_89".to_string(),
6692            builder_sha: "9".repeat(40),
6693            nvcc_path: fake_cuda.nvcc,
6694            cuda_toolkit_root: fake_cuda.root,
6695            ccbin_path: fake_cuda.ccbin.clone(),
6696            ar_path: PathBuf::from("/usr/bin/ar"),
6697            nvcc_threads: 4,
6698            object_cache_dir,
6699            plan_only: false,
6700        })
6701        .unwrap();
6702        assert_eq!(
6703            third_receipt.compiled_translation_units,
6704            ["kernels/marlin.cu"]
6705        );
6706        assert_eq!(
6707            third_receipt.cache_hit_translation_units,
6708            ["kernels/other.cu"]
6709        );
6710        assert_eq!(
6711            fs::read_to_string(fake_cuda.compile_counter)
6712                .unwrap()
6713                .lines()
6714                .count(),
6715            4,
6716            "private header drift must recompile only its owning translation unit"
6717        );
6718    }
6719}