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