Skip to main content

ferrum_native_ops_builder/
lib.rs

1//! Isolated packaging and set assembly for source-built native operators.
2
3pub mod source_build;
4
5use std::collections::BTreeMap;
6use std::ffi::OsStr;
7use std::fs;
8use std::io::{self, Write};
9use std::path::{Component, Path, PathBuf};
10use std::process::Command;
11use std::time::Instant;
12
13use ferrum_native_ops::{
14    CudaNativeBuildUnit, NativeOperatorArtifactLock, NativeOperatorArtifactSetLock,
15    NativeOperatorEvidenceFile, NativeOperatorResolveRequest, NativeOperatorResolver,
16    NativeOperatorSystemLibrary, NATIVE_OPERATOR_ARTIFACT_SET_SCHEMA_VERSION,
17};
18use ferrum_types::{
19    is_sha256_digest, NativeOperatorAbiContract, NativeOperatorBackend, NativeOperatorBinding,
20    NativeOperatorBuildSummary, NativeOperatorLinkage, NativeOperatorManifest,
21    NativeOperatorProviderCatalog, FERRUM_NATIVE_OPERATOR_ABI_VERSION,
22    NATIVE_OPERATOR_MANIFEST_SCHEMA_VERSION,
23};
24use serde::{de::DeserializeOwned, Deserialize, Serialize};
25use sha2::{Digest, Sha256};
26use tempfile::{Builder as TempBuilder, NamedTempFile};
27use thiserror::Error;
28
29pub const NATIVE_OPERATOR_PACKAGE_DEFINITION_SCHEMA_VERSION: u32 = 1;
30pub const NATIVE_OPERATOR_PACKAGE_SPEC_SCHEMA_VERSION: u32 = 3;
31pub const NATIVE_OPERATOR_PACKAGE_RECEIPT_SCHEMA_VERSION: u32 = 5;
32
33pub use source_build::*;
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct NativeOperatorPackageSpec {
38    pub schema_version: u32,
39    pub operator: String,
40    pub operator_abi_version: String,
41    pub backend: NativeOperatorBackend,
42    pub compute_capabilities: Vec<String>,
43    pub operation_bindings: Vec<NativeOperatorBinding>,
44    pub required_exports: Vec<String>,
45    pub license_files: Vec<NativeOperatorLicenseInput>,
46    pub cuda_toolkit: Option<String>,
47    pub cuda_runtime_min: Option<String>,
48    pub system_libraries: Vec<NativeOperatorSystemLibrary>,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(deny_unknown_fields)]
53pub struct NativeOperatorPackageDefinition {
54    pub schema_version: u32,
55    pub operator: String,
56    pub operator_abi_version: String,
57    pub backend: NativeOperatorBackend,
58    pub compute_capabilities: Vec<String>,
59    pub provider_bindings: Vec<NativeOperatorProviderBindingDefinition>,
60    pub required_exports: Vec<String>,
61    pub license_files: Vec<NativeOperatorLicenseInput>,
62    pub cuda_toolkit: Option<String>,
63    pub cuda_runtime_min: Option<String>,
64    pub system_libraries: Vec<NativeOperatorSystemLibrary>,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(deny_unknown_fields)]
69pub struct NativeOperatorProviderBindingDefinition {
70    pub operation_id: String,
71    pub provider_id: String,
72    pub entrypoints: Vec<String>,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76pub struct NativeOperatorLicenseInput {
77    pub source_path: String,
78    pub output_path: String,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct NativeOperatorPackageReceipt {
84    pub schema_version: u32,
85    pub operator: String,
86    pub package_spec: NativeOperatorEvidenceFile,
87    pub g03_catalog: NativeOperatorEvidenceFile,
88    pub abi_contract: NativeOperatorEvidenceFile,
89    pub source_build_receipt: NativeOperatorEvidenceFile,
90    pub source_build_plan: NativeOperatorEvidenceFile,
91    pub source_build_inputs: Vec<NativeOperatorEvidenceFile>,
92    pub source_build_logs: Vec<NativeOperatorEvidenceFile>,
93    pub source_archive_sha256: String,
94    pub source_archive_members: Vec<NativeOperatorArchiveMemberEvidence>,
95    pub source_archive_verification: NativeOperatorEvidenceFile,
96    pub descriptor_object: NativeOperatorArchiveMemberEvidence,
97    pub final_archive_members: Vec<NativeOperatorArchiveMemberEvidence>,
98    pub final_archive_verification: NativeOperatorEvidenceFile,
99    pub manifest_file: String,
100    pub artifact_file: String,
101    pub manifest_sha256: String,
102    pub binary_sha256: String,
103    pub g03_catalog_sha256: String,
104    pub abi_contract_sha256: String,
105    pub descriptor_export: String,
106    pub license_files: Vec<NativeOperatorEvidenceFile>,
107    pub system_libraries: Vec<NativeOperatorSystemLibrary>,
108    pub package_toolchain: NativeOperatorPackageToolchain,
109    pub package_environment: BTreeMap<String, String>,
110    pub package_commands: Vec<NativeOperatorPackageCommand>,
111    pub package_build_logs: Vec<NativeOperatorEvidenceFile>,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115pub struct NativeOperatorPackageToolchain {
116    pub descriptor_compiler: NativeOperatorToolIdentity,
117    pub descriptor_target: String,
118    pub archiver: NativeOperatorToolIdentity,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub struct NativeOperatorPackageCommand {
123    pub argv: Vec<String>,
124    pub working_directory: String,
125    pub stdout_log: String,
126    pub stderr_log: String,
127    pub return_code: i32,
128    pub elapsed_ms: u64,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132pub struct NativeOperatorArchiveMemberEvidence {
133    pub member: String,
134    pub sha256: String,
135    pub size_bytes: u64,
136    pub object_identity: NativeOperatorObjectIdentity,
137}
138
139#[derive(Debug, Clone)]
140pub struct NativeOperatorPackageRequest {
141    pub spec_path: PathBuf,
142    pub source_root: PathBuf,
143    pub license_root: PathBuf,
144    pub source_build_receipt_path: PathBuf,
145    pub source_build_plan_path: PathBuf,
146    pub g03_catalog_path: PathBuf,
147    pub abi_contract_path: PathBuf,
148    pub output_dir: PathBuf,
149    pub cc: PathBuf,
150    pub ar: PathBuf,
151}
152
153#[derive(Debug, Clone)]
154pub struct NativeOperatorPackageSpecRequest {
155    pub definition_path: PathBuf,
156    pub g03_catalog_path: PathBuf,
157    pub output_path: PathBuf,
158}
159
160#[derive(Debug, Clone)]
161pub struct NativeOperatorSetRequest {
162    pub receipt_paths: Vec<PathBuf>,
163    pub expected_receipt_sha256: Vec<String>,
164    pub expected_g03_catalog_sha256: String,
165    pub output_lock_path: PathBuf,
166    pub compute_capability: String,
167}
168
169#[derive(Debug, Error)]
170pub enum NativeOperatorBuilderError {
171    #[error("invalid native operator package request: {0}")]
172    Invalid(String),
173    #[error("path does not exist or is not a file: {0}")]
174    MissingFile(PathBuf),
175    #[error("output already exists: {0}")]
176    OutputExists(PathBuf),
177    #[error("failed to access {path}: {source}")]
178    Io { path: PathBuf, source: io::Error },
179    #[error("failed to parse JSON {path}: {source}")]
180    Json {
181        path: PathBuf,
182        source: serde_json::Error,
183    },
184    #[error("native operator tool failed: tool={tool} status={status} stderr={stderr}")]
185    Tool {
186        tool: String,
187        status: String,
188        stderr: String,
189    },
190    #[error("native operator artifact validation failed: {0}")]
191    Resolve(#[from] ferrum_native_ops::NativeOperatorResolveError),
192    #[error("native operator artifact-set validation failed: {0}")]
193    ArtifactSet(#[from] ferrum_native_ops::NativeOperatorArtifactSetError),
194    #[error("native build artifact cache failed: {0}")]
195    BuildCache(#[from] ferrum_native_ops::NativeBuildArtifactCacheError),
196    #[error("native operator source build rejected: receipt={receipt_path} reason={reason}")]
197    SourceBuildRejected {
198        receipt_path: PathBuf,
199        reason: String,
200    },
201}
202
203pub type Result<T> = std::result::Result<T, NativeOperatorBuilderError>;
204
205pub fn materialize_native_operator_package_spec(
206    request: &NativeOperatorPackageSpecRequest,
207) -> Result<NativeOperatorPackageSpec> {
208    require_file(&request.definition_path)?;
209    require_file(&request.g03_catalog_path)?;
210    if request.output_path.exists() {
211        return Err(NativeOperatorBuilderError::OutputExists(
212            request.output_path.clone(),
213        ));
214    }
215    let definition: NativeOperatorPackageDefinition = read_json(&request.definition_path)?;
216    validate_package_definition(&definition)?;
217    let (catalog, catalog_sha256): (NativeOperatorProviderCatalog, String) =
218        read_json_with_sha256(&request.g03_catalog_path)?;
219    catalog
220        .validate()
221        .map_err(NativeOperatorBuilderError::Invalid)?;
222    if catalog_sha256
223        != catalog
224            .canonical_sha256()
225            .map_err(NativeOperatorBuilderError::Invalid)?
226    {
227        return Err(NativeOperatorBuilderError::Invalid(format!(
228            "live G03 catalog is not in canonical JSON form: {}",
229            request.g03_catalog_path.display()
230        )));
231    }
232    if catalog.backend != definition.backend {
233        return Err(NativeOperatorBuilderError::Invalid(format!(
234            "package definition backend {:?} differs from live G03 catalog backend {:?}",
235            definition.backend, catalog.backend
236        )));
237    }
238
239    let mut operation_bindings = Vec::with_capacity(definition.provider_bindings.len());
240    for requested in &definition.provider_bindings {
241        let provider = catalog
242            .providers
243            .iter()
244            .find(|provider| {
245                provider.operation_id == requested.operation_id
246                    && provider.provider_id == requested.provider_id
247            })
248            .ok_or_else(|| {
249                NativeOperatorBuilderError::Invalid(format!(
250                    "live G03 catalog does not contain operation/provider {}/{}",
251                    requested.operation_id, requested.provider_id
252                ))
253            })?;
254        operation_bindings.push(NativeOperatorBinding {
255            operation_id: provider.operation_id.clone(),
256            operation_contract_version: provider.operation_contract_version,
257            provider_id: provider.provider_id.clone(),
258            provider_version: provider.provider_version,
259            provider_implementation_fingerprint: provider
260                .provider_implementation_fingerprint
261                .clone(),
262            entrypoints: requested.entrypoints.clone(),
263        });
264    }
265    operation_bindings.sort_by(|left, right| {
266        left.operation_id
267            .cmp(&right.operation_id)
268            .then(left.provider_id.cmp(&right.provider_id))
269    });
270
271    let spec = NativeOperatorPackageSpec {
272        schema_version: NATIVE_OPERATOR_PACKAGE_SPEC_SCHEMA_VERSION,
273        operator: definition.operator,
274        operator_abi_version: definition.operator_abi_version,
275        backend: definition.backend,
276        compute_capabilities: definition.compute_capabilities,
277        operation_bindings,
278        required_exports: definition.required_exports,
279        license_files: definition.license_files,
280        cuda_toolkit: definition.cuda_toolkit,
281        cuda_runtime_min: definition.cuda_runtime_min,
282        system_libraries: definition.system_libraries,
283    };
284    validate_package_spec(&spec)?;
285    let parent = request.output_path.parent().ok_or_else(|| {
286        NativeOperatorBuilderError::Invalid(format!(
287            "package spec output has no parent: {}",
288            request.output_path.display()
289        ))
290    })?;
291    fs::create_dir_all(parent).map_err(|source| NativeOperatorBuilderError::Io {
292        path: parent.to_path_buf(),
293        source,
294    })?;
295    write_json(&request.output_path, &spec)?;
296    Ok(spec)
297}
298
299pub fn package_native_operator(
300    request: &NativeOperatorPackageRequest,
301) -> Result<NativeOperatorPackageReceipt> {
302    require_file(&request.spec_path)?;
303    require_file(&request.source_build_receipt_path)?;
304    require_file(&request.source_build_plan_path)?;
305    require_file(&request.g03_catalog_path)?;
306    require_file(&request.abi_contract_path)?;
307    if !request.cc.is_absolute() || !request.ar.is_absolute() {
308        return Err(NativeOperatorBuilderError::Invalid(
309            "package compiler and archiver paths must be absolute".to_string(),
310        ));
311    }
312    if request.output_dir.exists() {
313        return Err(NativeOperatorBuilderError::OutputExists(
314            request.output_dir.clone(),
315        ));
316    }
317    let (spec, spec_sha256): (NativeOperatorPackageSpec, String) =
318        read_json_with_sha256(&request.spec_path)?;
319    validate_package_spec(&spec)?;
320    let source_root =
321        request
322            .source_root
323            .canonicalize()
324            .map_err(|source| NativeOperatorBuilderError::Io {
325                path: request.source_root.clone(),
326                source,
327            })?;
328    if !source_root.is_dir() {
329        return Err(NativeOperatorBuilderError::Invalid(format!(
330            "source_root is not a directory: {}",
331            source_root.display()
332        )));
333    }
334    let license_root =
335        request
336            .license_root
337            .canonicalize()
338            .map_err(|source| NativeOperatorBuilderError::Io {
339                path: request.license_root.clone(),
340                source,
341            })?;
342    if !license_root.is_dir() {
343        return Err(NativeOperatorBuilderError::Invalid(format!(
344            "license_root is not a directory: {}",
345            license_root.display()
346        )));
347    }
348    let source_build = load_source_build_for_package(
349        &request.source_build_receipt_path,
350        &request.source_build_plan_path,
351        &spec,
352        &source_root,
353    )?;
354    let package_toolchain = NativeOperatorPackageToolchain {
355        descriptor_compiler: tool_identity(&request.cc)?,
356        descriptor_target: compiler_target(&request.cc)?,
357        archiver: tool_identity(&request.ar)?,
358    };
359    let package_environment = package_build_environment(&package_toolchain)?;
360
361    let (g03_catalog, g03_catalog_sha256): (NativeOperatorProviderCatalog, String) =
362        read_json_with_sha256(&request.g03_catalog_path)?;
363    g03_catalog
364        .validate()
365        .map_err(NativeOperatorBuilderError::Invalid)?;
366    if g03_catalog_sha256
367        != g03_catalog
368            .canonical_sha256()
369            .map_err(NativeOperatorBuilderError::Invalid)?
370    {
371        return Err(NativeOperatorBuilderError::Invalid(format!(
372            "live G03 catalog is not in canonical JSON form: {}",
373            request.g03_catalog_path.display()
374        )));
375    }
376    validate_spec_against_catalog(&spec, &g03_catalog)?;
377    let (abi_contract, abi_contract_sha256): (NativeOperatorAbiContract, String) =
378        read_json_with_sha256(&request.abi_contract_path)?;
379    abi_contract
380        .validate()
381        .map_err(NativeOperatorBuilderError::Invalid)?;
382    if abi_contract_sha256
383        != abi_contract
384            .canonical_sha256()
385            .map_err(NativeOperatorBuilderError::Invalid)?
386    {
387        return Err(NativeOperatorBuilderError::Invalid(format!(
388            "native ABI contract is not in canonical JSON form: {}",
389            request.abi_contract_path.display()
390        )));
391    }
392    let identity_suffix = &sha256_bytes(spec.operator.as_bytes())[..12];
393    let symbol_slug = symbol_slug(&spec.operator)?;
394    let descriptor_export = format!("ferrum_native_{symbol_slug}_{identity_suffix}_descriptor_v2");
395    let artifact_file = format!("libferrum_native_{symbol_slug}_{identity_suffix}.a");
396    let manifest_file = "native_operator_manifest.json".to_string();
397    let receipt_file = "package.receipt.json";
398
399    let output_parent = request
400        .output_dir
401        .parent()
402        .unwrap_or_else(|| Path::new("."));
403    fs::create_dir_all(output_parent).map_err(|source| NativeOperatorBuilderError::Io {
404        path: output_parent.to_path_buf(),
405        source,
406    })?;
407    let staging = TempBuilder::new()
408        .prefix(".ferrum-native-package-")
409        .tempdir_in(output_parent)
410        .map_err(|source| NativeOperatorBuilderError::Io {
411            path: output_parent.to_path_buf(),
412            source,
413        })?;
414    let package_spec = copy_evidence_file(
415        &request.spec_path,
416        staging.path(),
417        "provenance/package.spec.json",
418    )?;
419    if package_spec.sha256 != spec_sha256 {
420        return Err(NativeOperatorBuilderError::Invalid(format!(
421            "{} package spec changed while packaging",
422            spec.operator
423        )));
424    }
425    let g03_catalog = copy_evidence_file(
426        &request.g03_catalog_path,
427        staging.path(),
428        "provenance/g03-provider-catalog.json",
429    )?;
430    if g03_catalog.sha256 != g03_catalog_sha256 {
431        return Err(NativeOperatorBuilderError::Invalid(format!(
432            "{} live G03 catalog changed while packaging",
433            spec.operator
434        )));
435    }
436    let abi_contract = copy_evidence_file(
437        &request.abi_contract_path,
438        staging.path(),
439        "provenance/native-abi-contract.json",
440    )?;
441    if abi_contract.sha256 != abi_contract_sha256 {
442        return Err(NativeOperatorBuilderError::Invalid(format!(
443            "{} native ABI contract changed while packaging",
444            spec.operator
445        )));
446    }
447    let source_build_receipt = copy_evidence_file(
448        &request.source_build_receipt_path,
449        staging.path(),
450        "provenance/source-build.receipt.json",
451    )?;
452    if source_build_receipt.sha256 != source_build.receipt_sha256 {
453        return Err(NativeOperatorBuilderError::Invalid(format!(
454            "{} source-build receipt changed while packaging",
455            spec.operator
456        )));
457    }
458    let source_build_plan = copy_evidence_file(
459        &source_build.plan_path,
460        staging.path(),
461        "provenance/source-build.plan.json",
462    )?;
463    if source_build_plan.sha256 != source_build.receipt.plan_sha256 {
464        return Err(NativeOperatorBuilderError::Invalid(format!(
465            "{} source-build plan changed while packaging",
466            spec.operator
467        )));
468    }
469    let source_build_logs = copy_source_build_logs(
470        &request.source_build_receipt_path,
471        &source_build.receipt,
472        staging.path(),
473    )?;
474    let source_build_inputs = copy_source_build_inputs(
475        &request.source_build_receipt_path,
476        &source_build.receipt,
477        staging.path(),
478    )?;
479    let source_plan = verify_source_build_receipt_against_plan_portable(
480        &source_build.receipt,
481        &staging.path().join(&source_build_plan.path),
482    )?;
483    verify_source_build_evidence(
484        &source_build.receipt,
485        staging.path().join("provenance").as_path(),
486        &source_plan,
487    )?;
488    let artifact_path = staging.path().join(&artifact_file);
489    fs::copy(&source_build.archive_path, &artifact_path).map_err(|source| {
490        NativeOperatorBuilderError::Io {
491            path: artifact_path.clone(),
492            source,
493        }
494    })?;
495    let copied_source_archive_sha256 = sha256_file(&artifact_path)?;
496    if copied_source_archive_sha256 != source_build.source_archive_sha256 {
497        return Err(NativeOperatorBuilderError::Invalid(format!(
498            "{} source archive changed while packaging: expected={} actual={copied_source_archive_sha256}",
499            spec.operator, source_build.source_archive_sha256
500        )));
501    }
502    let source_archive_expected = source_archive_member_expectations(&source_build.receipt)?;
503    let (source_archive_members, source_archive_verification) = verify_archive_members(
504        &artifact_path,
505        &source_build.receipt.operator,
506        &source_archive_expected,
507        &package_toolchain.archiver.path,
508        staging.path(),
509        &package_environment,
510        "build-logs/source-archive-verify.log",
511    )?;
512
513    let descriptor_source = staging.path().join("descriptor.c");
514    let descriptor_object = staging.path().join("descriptor.o");
515    fs::write(
516        &descriptor_source,
517        render_descriptor_source(
518            &descriptor_export,
519            &spec.operator,
520            &spec.operator_abi_version,
521            &g03_catalog_sha256,
522            &abi_contract_sha256,
523        ),
524    )
525    .map_err(|source| NativeOperatorBuilderError::Io {
526        path: descriptor_source.clone(),
527        source,
528    })?;
529    let descriptor_compile = run_package_command(
530        &package_toolchain.descriptor_compiler.path,
531        vec![
532            "-std=c11".to_string(),
533            "-O2".to_string(),
534            "-fno-ident".to_string(),
535            "-fvisibility=hidden".to_string(),
536            "-c".to_string(),
537            "descriptor.c".to_string(),
538            "-o".to_string(),
539            "descriptor.o".to_string(),
540        ],
541        staging.path(),
542        "build-logs/descriptor-compile.stdout.log",
543        "build-logs/descriptor-compile.stderr.log",
544        &package_environment,
545    )?;
546    let descriptor_object_evidence =
547        archive_member_evidence_file("descriptor.o", &descriptor_object)?;
548    let mut final_archive_expected = source_archive_members.clone();
549    final_archive_expected.push(descriptor_object_evidence.clone());
550    final_archive_expected.sort_by(|left, right| left.member.cmp(&right.member));
551    validate_archive_member_evidence(&spec.operator, &final_archive_expected)?;
552    let descriptor_archive = run_package_command(
553        &package_toolchain.archiver.path,
554        vec![
555            "rcs".to_string(),
556            artifact_file.clone(),
557            "descriptor.o".to_string(),
558        ],
559        staging.path(),
560        "build-logs/descriptor-archive.stdout.log",
561        "build-logs/descriptor-archive.stderr.log",
562        &package_environment,
563    )?;
564    let (final_archive_members, final_archive_verification) = verify_archive_members(
565        &artifact_path,
566        &spec.operator,
567        &final_archive_expected,
568        &package_toolchain.archiver.path,
569        staging.path(),
570        &package_environment,
571        "build-logs/final-archive-verify.log",
572    )?;
573    let package_commands = vec![descriptor_compile, descriptor_archive];
574    let mut package_build_logs = vec![
575        source_archive_verification.clone(),
576        final_archive_verification.clone(),
577    ];
578    package_build_logs.extend(
579        package_commands
580            .iter()
581            .flat_map(|command| [&command.stdout_log, &command.stderr_log])
582            .map(|relative| evidence_file_at(staging.path(), relative))
583            .collect::<Result<Vec<_>>>()?,
584    );
585    package_build_logs.sort_by(|left, right| left.path.cmp(&right.path));
586
587    let mut license_files = Vec::with_capacity(spec.license_files.len());
588    let mut license_evidence = Vec::with_capacity(spec.license_files.len());
589    for license in &spec.license_files {
590        let source = resolve_relative_file(&license_root, &license.source_path)?;
591        license_evidence.push(copy_evidence_file(
592            &source,
593            staging.path(),
594            &license.output_path,
595        )?);
596        license_files.push(license.output_path.clone());
597    }
598
599    let mut exports = spec.required_exports.clone();
600    exports.push(descriptor_export.clone());
601    exports.sort();
602    let binary_sha256 = sha256_file(&artifact_path)?;
603    let manifest = NativeOperatorManifest {
604        schema_version: NATIVE_OPERATOR_MANIFEST_SCHEMA_VERSION,
605        operator: spec.operator.clone(),
606        operator_abi_version: spec.operator_abi_version.clone(),
607        ferrum_native_abi_version: FERRUM_NATIVE_OPERATOR_ABI_VERSION.to_string(),
608        backend: spec.backend,
609        cuda_toolkit: spec.cuda_toolkit.clone(),
610        cuda_runtime_min: spec.cuda_runtime_min.clone(),
611        compute_capabilities: spec.compute_capabilities.clone(),
612        source_package: source_build.receipt.source_package.clone(),
613        inputs_sha256: source_build.receipt.inputs_sha256.clone(),
614        binary_sha256: binary_sha256.clone(),
615        linkage: NativeOperatorLinkage::Static,
616        g03_catalog_sha256: Some(g03_catalog_sha256.clone()),
617        abi_contract_sha256: Some(abi_contract_sha256.clone()),
618        descriptor_export: Some(descriptor_export.clone()),
619        operation_bindings: spec.operation_bindings.clone(),
620        exports: exports.clone(),
621        license_files,
622        build_summary: source_build.build_summary.clone(),
623    };
624    manifest
625        .validate()
626        .map_err(NativeOperatorBuilderError::Invalid)?;
627    let manifest_path = staging.path().join(&manifest_file);
628    write_json(&manifest_path, &manifest)?;
629    NativeOperatorResolver.resolve(
630        &NativeOperatorResolveRequest::new(
631            spec.operator.clone(),
632            spec.backend,
633            &manifest_path,
634            &artifact_path,
635        )
636        .with_compute_capability(source_build.receipt.compute_capability.clone())
637        .with_operator_abi_version(spec.operator_abi_version.clone())
638        .with_ferrum_native_abi_version(FERRUM_NATIVE_OPERATOR_ABI_VERSION)
639        .with_g03_catalog_sha256(g03_catalog_sha256.clone())
640        .with_abi_contract_sha256(abi_contract_sha256.clone())
641        .with_descriptor_export(descriptor_export.clone())
642        .with_required_exports(exports)
643        .with_operation_bindings(spec.operation_bindings.clone()),
644    )?;
645
646    let manifest_sha256 = sha256_file(&manifest_path)?;
647    let receipt = NativeOperatorPackageReceipt {
648        schema_version: NATIVE_OPERATOR_PACKAGE_RECEIPT_SCHEMA_VERSION,
649        operator: spec.operator.clone(),
650        package_spec,
651        g03_catalog,
652        abi_contract,
653        source_build_receipt,
654        source_build_plan,
655        source_build_inputs,
656        source_build_logs,
657        source_archive_sha256: source_build.source_archive_sha256.clone(),
658        source_archive_members,
659        source_archive_verification,
660        descriptor_object: descriptor_object_evidence,
661        final_archive_members,
662        final_archive_verification,
663        manifest_file,
664        artifact_file,
665        manifest_sha256,
666        binary_sha256,
667        g03_catalog_sha256,
668        abi_contract_sha256,
669        descriptor_export,
670        license_files: license_evidence,
671        system_libraries: spec.system_libraries.clone(),
672        package_toolchain,
673        package_environment,
674        package_commands,
675        package_build_logs,
676    };
677    validate_package_receipt(&receipt)?;
678    validate_package_semantic_links(
679        &receipt,
680        &spec,
681        &source_build.receipt,
682        &source_plan,
683        &manifest,
684    )?;
685    write_json(&staging.path().join(receipt_file), &receipt)?;
686    fs::remove_file(&descriptor_source).map_err(|source| NativeOperatorBuilderError::Io {
687        path: descriptor_source,
688        source,
689    })?;
690    fs::remove_file(&descriptor_object).map_err(|source| NativeOperatorBuilderError::Io {
691        path: descriptor_object,
692        source,
693    })?;
694
695    let staging_path = staging.keep();
696    fs::rename(&staging_path, &request.output_dir).map_err(|source| {
697        NativeOperatorBuilderError::Io {
698            path: request.output_dir.clone(),
699            source,
700        }
701    })?;
702    Ok(receipt)
703}
704
705struct ValidatedSourceBuildForPackage {
706    receipt: NativeOperatorSourceBuildReceipt,
707    receipt_sha256: String,
708    plan_path: PathBuf,
709    archive_path: PathBuf,
710    source_archive_sha256: String,
711    build_summary: NativeOperatorBuildSummary,
712}
713
714fn load_source_build_for_package(
715    receipt_path: &Path,
716    plan_path: &Path,
717    spec: &NativeOperatorPackageSpec,
718    source_root: &Path,
719) -> Result<ValidatedSourceBuildForPackage> {
720    let bytes = fs::read(receipt_path).map_err(|source| NativeOperatorBuilderError::Io {
721        path: receipt_path.to_path_buf(),
722        source,
723    })?;
724    let receipt_sha256 = sha256_bytes(&bytes);
725    let receipt: NativeOperatorSourceBuildReceipt =
726        serde_json::from_slice(&bytes).map_err(|source| NativeOperatorBuilderError::Json {
727            path: receipt_path.to_path_buf(),
728            source,
729        })?;
730    validate_source_build_for_package(&receipt, spec)?;
731    let receipt_root = receipt_path.parent().unwrap_or_else(|| Path::new("."));
732    verify_source_build_receipt_against_plan(&receipt, receipt_root, plan_path, source_root)?;
733
734    let archive_file = receipt
735        .archive_file
736        .as_deref()
737        .expect("validated PASS receipt has archive_file");
738    let receipt_root = receipt_path.parent().unwrap_or_else(|| Path::new("."));
739    let archive_path = resolve_relative_file(receipt_root, archive_file)?;
740    let source_archive_sha256 = sha256_file(&archive_path)?;
741    let expected_archive_sha256 = receipt
742        .archive_sha256
743        .as_deref()
744        .expect("validated PASS receipt has archive_sha256");
745    if source_archive_sha256 != expected_archive_sha256 {
746        return Err(NativeOperatorBuilderError::Invalid(format!(
747            "{} source-build archive sha256 differs from its receipt: expected={expected_archive_sha256} actual={source_archive_sha256}",
748            receipt.operator
749        )));
750    }
751
752    let build_summary = source_build_summary(&receipt)?;
753
754    Ok(ValidatedSourceBuildForPackage {
755        receipt,
756        receipt_sha256,
757        plan_path: plan_path.to_path_buf(),
758        archive_path,
759        source_archive_sha256,
760        build_summary,
761    })
762}
763
764fn source_build_summary(
765    receipt: &NativeOperatorSourceBuildReceipt,
766) -> Result<NativeOperatorBuildSummary> {
767    let toolchain = receipt.toolchain.as_ref().ok_or_else(|| {
768        NativeOperatorBuilderError::Invalid(format!(
769            "{} PASS source-build receipt is missing toolchain provenance",
770            receipt.operator
771        ))
772    })?;
773    let host_version = toolchain
774        .miss_probe
775        .as_ref()
776        .map(|probe| normalize_tool_version(&probe.host_compiler_version))
777        .unwrap_or_else(|| {
778            normalize_tool_version(&toolchain.static_identity.host_toolchain.compiler_version)
779        });
780    Ok(NativeOperatorBuildSummary {
781        builder_sha: receipt.builder_sha.clone(),
782        elapsed_ms: receipt.elapsed_ms,
783        nvcc_version: Some(
784            toolchain
785                .miss_probe
786                .as_ref()
787                .map(|probe| normalize_tool_version(&probe.nvcc_version))
788                .unwrap_or_else(|| {
789                    format!(
790                        "cuda-toolkit-static {}",
791                        toolchain.static_identity.cuda_toolkit.release_version
792                    )
793                }),
794        ),
795        host_compiler: format!(
796            "path={};sha256={};version={}",
797            toolchain.static_identity.host_toolchain.compiler.path,
798            toolchain.static_identity.host_toolchain.compiler.sha256,
799            host_version
800        ),
801    })
802}
803
804fn validate_source_build_for_package(
805    receipt: &NativeOperatorSourceBuildReceipt,
806    spec: &NativeOperatorPackageSpec,
807) -> Result<()> {
808    if receipt.schema_version != NATIVE_OPERATOR_SOURCE_BUILD_RECEIPT_SCHEMA_VERSION {
809        return Err(NativeOperatorBuilderError::Invalid(format!(
810            "source-build receipt schema_version must be {NATIVE_OPERATOR_SOURCE_BUILD_RECEIPT_SCHEMA_VERSION}"
811        )));
812    }
813    if receipt.status != NativeOperatorSourceBuildStatus::Pass
814        || receipt.plan_only
815        || receipt.failure_class.is_some()
816    {
817        return Err(NativeOperatorBuilderError::Invalid(format!(
818            "package requires a terminal PASS source-build receipt: status={:?} plan_only={} failure_class={:?}",
819            receipt.status, receipt.plan_only, receipt.failure_class
820        )));
821    }
822    if receipt.operator != spec.operator {
823        return Err(NativeOperatorBuilderError::Invalid(format!(
824            "source-build operator differs from package spec: expected={} actual={}",
825            spec.operator, receipt.operator
826        )));
827    }
828    if spec.compute_capabilities.as_slice() != std::slice::from_ref(&receipt.compute_capability) {
829        return Err(NativeOperatorBuilderError::Invalid(format!(
830            "{} package compute_capabilities must exactly equal the source-build target [{}]",
831            receipt.operator, receipt.compute_capability
832        )));
833    }
834    for (field, digest) in [
835        ("plan_sha256", receipt.plan_sha256.as_str()),
836        (
837            "source_package.sha256",
838            receipt.source_package.sha256.as_str(),
839        ),
840        ("inputs_sha256", receipt.inputs_sha256.as_str()),
841    ] {
842        if !is_sha256_digest(digest) {
843            return Err(NativeOperatorBuilderError::Invalid(format!(
844                "{} source-build receipt {field} is not a lowercase sha256 digest",
845                receipt.operator
846            )));
847        }
848    }
849    if receipt.source_package.kind.trim().is_empty()
850        || receipt.source_package.revision.trim().is_empty()
851    {
852        return Err(NativeOperatorBuilderError::Invalid(format!(
853            "{} source-build source_package kind and revision must be non-empty",
854            receipt.operator
855        )));
856    }
857    if !is_git_object_id(&receipt.builder_sha) {
858        return Err(NativeOperatorBuilderError::Invalid(format!(
859            "{} source-build builder_sha must be a lowercase 40- or 64-hex git object id",
860            receipt.operator
861        )));
862    }
863    if receipt.nvcc_threads == 0 {
864        return Err(NativeOperatorBuilderError::Invalid(format!(
865            "{} source-build nvcc_threads must be greater than zero",
866            receipt.operator
867        )));
868    }
869    if receipt.architecture_argument.trim().is_empty()
870        || receipt.object_cache_root.trim().is_empty()
871        || receipt.effective_environment.is_empty()
872    {
873        return Err(NativeOperatorBuilderError::Invalid(format!(
874            "{} source-build architecture, object cache, and effective environment must be recorded",
875            receipt.operator
876        )));
877    }
878
879    let archive_file = receipt.archive_file.as_deref().ok_or_else(|| {
880        NativeOperatorBuilderError::Invalid(format!(
881            "{} PASS source-build receipt is missing archive_file",
882            receipt.operator
883        ))
884    })?;
885    validate_relative_path(archive_file)?;
886    if Path::new(archive_file).parent() != Some(Path::new(""))
887        || Path::new(archive_file).extension() != Some(OsStr::new("a"))
888    {
889        return Err(NativeOperatorBuilderError::Invalid(format!(
890            "{} source-build archive_file must be a .a filename without directories",
891            receipt.operator
892        )));
893    }
894    if !receipt
895        .archive_sha256
896        .as_deref()
897        .is_some_and(is_sha256_digest)
898    {
899        return Err(NativeOperatorBuilderError::Invalid(format!(
900            "{} PASS source-build receipt is missing a valid archive_sha256",
901            receipt.operator
902        )));
903    }
904
905    let toolchain = receipt.toolchain.as_ref().ok_or_else(|| {
906        NativeOperatorBuilderError::Invalid(format!(
907            "{} PASS source-build receipt is missing toolchain provenance",
908            receipt.operator
909        ))
910    })?;
911    let static_identity = &toolchain.static_identity;
912    if spec.cuda_toolkit.as_deref().is_none_or(|declared| {
913        !cuda_toolkit_version_matches(declared, &static_identity.cuda_toolkit.release_version)
914    }) {
915        return Err(NativeOperatorBuilderError::Invalid(format!(
916            "{} package cuda_toolkit does not match source-build toolkit release {}",
917            receipt.operator, static_identity.cuda_toolkit.release_version
918        )));
919    }
920    for (name, tool) in [
921        ("nvcc", &static_identity.cuda_toolkit.nvcc),
922        ("host_compiler", &static_identity.host_toolchain.compiler),
923        ("archiver", &static_identity.archiver),
924    ] {
925        if !Path::new(&tool.path).is_absolute()
926            || !is_sha256_digest(&tool.sha256)
927            || tool.size_bytes == 0
928        {
929            return Err(NativeOperatorBuilderError::Invalid(format!(
930                "{} source-build {name} identity is incomplete",
931                receipt.operator
932            )));
933        }
934    }
935    if !Path::new(&static_identity.cuda_toolkit.canonical_root).is_absolute()
936        || static_identity
937            .cuda_toolkit
938            .release_version
939            .trim()
940            .is_empty()
941        || static_identity
942            .cuda_toolkit
943            .release_version
944            .chars()
945            .any(|character| !(character.is_ascii_digit() || character == '.'))
946        || !Path::new(&static_identity.cuda_toolkit.nvcc.path)
947            .starts_with(&static_identity.cuda_toolkit.canonical_root)
948        || static_identity.cuda_toolkit.manifest.path != "toolchain/cuda-static-manifest.json"
949        || !is_sha256_digest(&static_identity.cuda_toolkit.manifest.sha256)
950        || static_identity.cuda_toolkit.manifest.size_bytes == 0
951        || static_identity
952            .host_toolchain
953            .compiler_version
954            .trim()
955            .is_empty()
956        || static_identity.host_toolchain.target.trim().is_empty()
957        || static_identity.host_toolchain.target.len() > 256
958        || static_identity
959            .host_toolchain
960            .target
961            .chars()
962            .any(char::is_whitespace)
963        || static_identity.host_toolchain.manifest.path != "toolchain/host-static-manifest.json"
964        || !is_sha256_digest(&static_identity.host_toolchain.manifest.sha256)
965        || static_identity.host_toolchain.manifest.size_bytes == 0
966    {
967        return Err(NativeOperatorBuilderError::Invalid(format!(
968            "{} source-build cuda toolkit identity is invalid",
969            receipt.operator
970        )));
971    }
972    match &toolchain.miss_probe {
973        Some(probe)
974            if !probe.nvcc_version.trim().is_empty()
975                && !probe.host_compiler_version.trim().is_empty()
976                && !probe.archiver_version.trim().is_empty()
977                && !probe.host_target.trim().is_empty()
978                && probe.host_target.len() <= 256
979                && !probe.host_target.chars().any(char::is_whitespace)
980                && probe.host_compiler_version
981                    == static_identity.host_toolchain.compiler_version
982                && probe.host_target == static_identity.host_toolchain.target
983                && probe.probed_for_misses == receipt.compiled_translation_units => {}
984        None if receipt.compiled_translation_units.is_empty() => {}
985        _ => {
986            return Err(NativeOperatorBuilderError::Invalid(format!(
987            "{} source-build miss-only toolchain probe does not match compiled translation units",
988            receipt.operator
989        )))
990        }
991    }
992
993    if receipt.commands.len() < 2 {
994        return Err(NativeOperatorBuilderError::Invalid(format!(
995            "{} PASS source-build receipt must contain translation-unit and archive commands",
996            receipt.operator
997        )));
998    }
999    let (archive_command, translation_unit_commands) = receipt
1000        .commands
1001        .split_last()
1002        .expect("commands length checked above");
1003    let expected_working_directory = translation_unit_commands[0].working_directory.clone();
1004    if expected_working_directory.trim().is_empty()
1005        || !Path::new(&expected_working_directory).is_absolute()
1006    {
1007        return Err(NativeOperatorBuilderError::Invalid(format!(
1008            "{} source-build working directory must be a recorded absolute path",
1009            receipt.operator
1010        )));
1011    }
1012    let mut observed_compiled = Vec::new();
1013    let mut observed_cache_hits = Vec::new();
1014    let mut previous_translation_unit: Option<&str> = None;
1015    for command in translation_unit_commands {
1016        let translation_unit = command.translation_unit.as_deref().ok_or_else(|| {
1017            NativeOperatorBuilderError::Invalid(format!(
1018                "{} source-build translation-unit command is missing its source path",
1019                receipt.operator
1020            ))
1021        })?;
1022        validate_relative_path(translation_unit)?;
1023        if previous_translation_unit.is_some_and(|previous| previous >= translation_unit) {
1024            return Err(NativeOperatorBuilderError::Invalid(format!(
1025                "{} source-build translation-unit commands must be sorted and unique",
1026                receipt.operator
1027            )));
1028        }
1029        previous_translation_unit = Some(translation_unit);
1030        validate_source_build_command_common(receipt, command, &expected_working_directory)?;
1031        if command.object_file.as_deref().is_none_or(str::is_empty)
1032            || !command
1033                .object_cache_key
1034                .as_deref()
1035                .is_some_and(is_sha256_digest)
1036            || !command
1037                .object_sha256
1038                .as_deref()
1039                .is_some_and(is_sha256_digest)
1040            || command.object_size_bytes.is_none_or(|size| size == 0)
1041            || command.object_identity.is_none()
1042            || !command
1043                .dependency_closure_sha256
1044                .as_deref()
1045                .is_some_and(is_sha256_digest)
1046            || command
1047                .object_cache_entry
1048                .as_deref()
1049                .is_none_or(str::is_empty)
1050            || command.elapsed_ms.is_none()
1051        {
1052            return Err(NativeOperatorBuilderError::Invalid(format!(
1053                "{} source-build command for {translation_unit} has incomplete object evidence",
1054                receipt.operator
1055            )));
1056        }
1057        validate_native_object_identity(
1058            command
1059                .object_identity
1060                .as_ref()
1061                .expect("validated object identity"),
1062            translation_unit,
1063        )?;
1064        match command.object_cache_status {
1065            Some(NativeOperatorSourceObjectCacheStatus::Hit)
1066                if !command.compiler_executed
1067                    && command.return_code.is_none()
1068                    && command.dependency_validation
1069                        == Some(NativeOperatorDependencyValidation::CacheProof)
1070                    && command
1071                        .compiler_depfile
1072                        .as_deref()
1073                        .is_some_and(|path| validate_relative_path(path).is_ok())
1074                    && command
1075                        .compiler_depfile_sha256
1076                        .as_deref()
1077                        .is_some_and(is_sha256_digest)
1078                    && command
1079                        .depfile
1080                        .as_deref()
1081                        .is_some_and(|path| validate_relative_path(path).is_ok())
1082                    && command
1083                        .depfile_sha256
1084                        .as_deref()
1085                        .is_some_and(is_sha256_digest)
1086                    && command
1087                        .depfile_producer_working_directory
1088                        .as_deref()
1089                        .is_some_and(|path| Path::new(path).is_absolute())
1090                    && command
1091                        .depfile_producer_object_file
1092                        .as_deref()
1093                        .is_some_and(|path| Path::new(path).is_absolute())
1094                    && !command.depfile_bindings.is_empty()
1095                    && !command.observed_dependencies.is_empty() =>
1096            {
1097                observed_cache_hits.push(translation_unit.to_string());
1098            }
1099            Some(NativeOperatorSourceObjectCacheStatus::Published)
1100                if command.compiler_executed
1101                    && command.return_code == Some(0)
1102                    && command.dependency_validation
1103                        == Some(NativeOperatorDependencyValidation::Depfile)
1104                    && command
1105                        .compiler_depfile
1106                        .as_deref()
1107                        .is_some_and(|path| validate_relative_path(path).is_ok())
1108                    && command
1109                        .compiler_depfile_sha256
1110                        .as_deref()
1111                        .is_some_and(is_sha256_digest)
1112                    && command
1113                        .depfile
1114                        .as_deref()
1115                        .is_some_and(|path| validate_relative_path(path).is_ok())
1116                    && command
1117                        .depfile_sha256
1118                        .as_deref()
1119                        .is_some_and(is_sha256_digest)
1120                    && command
1121                        .depfile_producer_working_directory
1122                        .as_deref()
1123                        .is_some_and(|path| Path::new(path).is_absolute())
1124                    && command
1125                        .depfile_producer_object_file
1126                        .as_deref()
1127                        .is_some_and(|path| Path::new(path).is_absolute())
1128                    && !command.depfile_bindings.is_empty()
1129                    && !command.observed_dependencies.is_empty() =>
1130            {
1131                observed_compiled.push(translation_unit.to_string());
1132            }
1133            _ => {
1134                return Err(NativeOperatorBuilderError::Invalid(format!(
1135                    "{} source-build command for {translation_unit} is not terminal",
1136                    receipt.operator
1137                )));
1138            }
1139        }
1140    }
1141    validate_source_build_command_common(receipt, archive_command, &expected_working_directory)?;
1142    if archive_command.translation_unit.is_some()
1143        || archive_command.object_file.is_some()
1144        || archive_command.object_cache_status.is_some()
1145        || archive_command.object_cache_key.is_some()
1146        || archive_command.object_sha256.is_some()
1147        || archive_command.object_size_bytes.is_some()
1148        || archive_command.object_identity.is_some()
1149        || archive_command.dependency_closure_sha256.is_some()
1150        || archive_command.dependency_validation.is_some()
1151        || archive_command.compiler_depfile.is_some()
1152        || archive_command.compiler_depfile_sha256.is_some()
1153        || archive_command.depfile.is_some()
1154        || archive_command.depfile_sha256.is_some()
1155        || archive_command.depfile_producer_working_directory.is_some()
1156        || archive_command.depfile_producer_object_file.is_some()
1157        || !archive_command.depfile_bindings.is_empty()
1158        || !archive_command.observed_dependencies.is_empty()
1159        || archive_command.return_code != Some(0)
1160        || archive_command.elapsed_ms.is_none()
1161    {
1162        return Err(NativeOperatorBuilderError::Invalid(format!(
1163            "{} source-build archive command is not terminal",
1164            receipt.operator
1165        )));
1166    }
1167    if observed_compiled != receipt.compiled_translation_units
1168        || observed_cache_hits != receipt.cache_hit_translation_units
1169    {
1170        return Err(NativeOperatorBuilderError::Invalid(format!(
1171            "{} source-build compiled/cache-hit summaries do not match command evidence",
1172            receipt.operator
1173        )));
1174    }
1175    Ok(())
1176}
1177
1178fn validate_source_build_command_common(
1179    receipt: &NativeOperatorSourceBuildReceipt,
1180    command: &NativeOperatorSourceBuildCommand,
1181    expected_working_directory: &str,
1182) -> Result<()> {
1183    if command.working_directory != expected_working_directory || command.argv.is_empty() {
1184        return Err(NativeOperatorBuilderError::Invalid(format!(
1185            "{} source-build command has an unexpected working directory or empty argv",
1186            receipt.operator
1187        )));
1188    }
1189    validate_relative_path(&command.stdout_log)?;
1190    validate_relative_path(&command.stderr_log)?;
1191    Ok(())
1192}
1193
1194fn is_git_object_id(value: &str) -> bool {
1195    matches!(value.len(), 40 | 64)
1196        && value
1197            .bytes()
1198            .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
1199}
1200
1201fn normalize_tool_version(value: &str) -> String {
1202    value.split_whitespace().collect::<Vec<_>>().join(" ")
1203}
1204
1205fn copy_source_build_logs(
1206    receipt_path: &Path,
1207    receipt: &NativeOperatorSourceBuildReceipt,
1208    package_root: &Path,
1209) -> Result<Vec<NativeOperatorEvidenceFile>> {
1210    let source_build_root = receipt_path.parent().unwrap_or_else(|| Path::new("."));
1211    let mut logs = receipt
1212        .commands
1213        .iter()
1214        .flat_map(|command| [&command.stdout_log, &command.stderr_log])
1215        .cloned()
1216        .collect::<Vec<_>>();
1217    logs.sort();
1218    logs.dedup();
1219    if logs.is_empty() {
1220        return Err(NativeOperatorBuilderError::Invalid(format!(
1221            "{} source-build receipt contains no command logs",
1222            receipt.operator
1223        )));
1224    }
1225    logs.into_iter()
1226        .map(|relative| {
1227            let source = resolve_relative_file(source_build_root, &relative)?;
1228            let evidence =
1229                copy_evidence_file(&source, package_root, &format!("provenance/{relative}"))?;
1230            if evidence.size_bytes == 0 {
1231                return Err(NativeOperatorBuilderError::Invalid(format!(
1232                    "{} source-build log is empty: {relative}",
1233                    receipt.operator
1234                )));
1235            }
1236            Ok(evidence)
1237        })
1238        .collect()
1239}
1240
1241fn copy_source_build_inputs(
1242    receipt_path: &Path,
1243    receipt: &NativeOperatorSourceBuildReceipt,
1244    package_root: &Path,
1245) -> Result<Vec<NativeOperatorEvidenceFile>> {
1246    let source_build_root = receipt_path.parent().unwrap_or_else(|| Path::new("."));
1247    let toolchain = receipt.toolchain.as_ref().ok_or_else(|| {
1248        NativeOperatorBuilderError::Invalid(format!(
1249            "{} source-build receipt is missing toolchain provenance",
1250            receipt.operator
1251        ))
1252    })?;
1253    let mut inputs = vec![
1254        toolchain.static_identity.cuda_toolkit.manifest.path.clone(),
1255        toolchain
1256            .static_identity
1257            .host_toolchain
1258            .manifest
1259            .path
1260            .clone(),
1261    ];
1262    inputs.extend(
1263        receipt
1264            .commands
1265            .iter()
1266            .filter(|command| {
1267                matches!(
1268                    command.dependency_validation,
1269                    Some(
1270                        NativeOperatorDependencyValidation::Depfile
1271                            | NativeOperatorDependencyValidation::CacheProof
1272                    )
1273                )
1274            })
1275            .flat_map(|command| {
1276                [command.compiler_depfile.clone(), command.depfile.clone()]
1277                    .into_iter()
1278                    .flatten()
1279            }),
1280    );
1281    inputs.sort();
1282    inputs.dedup();
1283    if inputs.is_empty() {
1284        return Err(NativeOperatorBuilderError::Invalid(format!(
1285            "{} source-build receipt contains no input evidence",
1286            receipt.operator
1287        )));
1288    }
1289    inputs
1290        .into_iter()
1291        .map(|relative| {
1292            let source = resolve_relative_file(source_build_root, &relative)?;
1293            copy_evidence_file(&source, package_root, &format!("provenance/{relative}"))
1294        })
1295        .collect()
1296}
1297
1298fn copy_evidence_file(
1299    source: &Path,
1300    package_root: &Path,
1301    output_relative: &str,
1302) -> Result<NativeOperatorEvidenceFile> {
1303    require_file(source)?;
1304    validate_relative_path(output_relative)?;
1305    let destination = package_root.join(output_relative);
1306    if let Some(parent) = destination.parent() {
1307        fs::create_dir_all(parent).map_err(|source| NativeOperatorBuilderError::Io {
1308            path: parent.to_path_buf(),
1309            source,
1310        })?;
1311    }
1312    fs::copy(source, &destination).map_err(|source| NativeOperatorBuilderError::Io {
1313        path: destination.clone(),
1314        source,
1315    })?;
1316    let size_bytes = fs::metadata(&destination)
1317        .map_err(|source| NativeOperatorBuilderError::Io {
1318            path: destination.clone(),
1319            source,
1320        })?
1321        .len();
1322    if size_bytes == 0 {
1323        return Err(NativeOperatorBuilderError::Invalid(format!(
1324            "native operator evidence file is empty: {}",
1325            source.display()
1326        )));
1327    }
1328    Ok(NativeOperatorEvidenceFile {
1329        path: output_relative.to_string(),
1330        sha256: sha256_file(&destination)?,
1331        size_bytes,
1332    })
1333}
1334
1335fn evidence_file_at(package_root: &Path, relative: &str) -> Result<NativeOperatorEvidenceFile> {
1336    let path = resolve_relative_file(package_root, relative)?;
1337    let size_bytes = fs::metadata(&path)
1338        .map_err(|source| NativeOperatorBuilderError::Io {
1339            path: path.clone(),
1340            source,
1341        })?
1342        .len();
1343    if size_bytes == 0 {
1344        return Err(NativeOperatorBuilderError::Invalid(format!(
1345            "package build log is empty: {relative}"
1346        )));
1347    }
1348    Ok(NativeOperatorEvidenceFile {
1349        path: relative.to_string(),
1350        sha256: sha256_file(&path)?,
1351        size_bytes,
1352    })
1353}
1354
1355fn source_archive_member_expectations(
1356    receipt: &NativeOperatorSourceBuildReceipt,
1357) -> Result<Vec<NativeOperatorArchiveMemberEvidence>> {
1358    let translation_unit_commands = receipt
1359        .commands
1360        .get(..receipt.commands.len().saturating_sub(1))
1361        .ok_or_else(|| {
1362            NativeOperatorBuilderError::Invalid(format!(
1363                "{} source-build receipt has no archive command",
1364                receipt.operator
1365            ))
1366        })?;
1367    let expected = translation_unit_commands
1368        .iter()
1369        .map(|command| {
1370            let object_file = command.object_file.as_deref().ok_or_else(|| {
1371                NativeOperatorBuilderError::Invalid(format!(
1372                    "{} source-build command is missing object_file",
1373                    receipt.operator
1374                ))
1375            })?;
1376            let member = Path::new(object_file)
1377                .file_name()
1378                .and_then(|value| value.to_str())
1379                .ok_or_else(|| {
1380                    NativeOperatorBuilderError::Invalid(format!(
1381                        "{} source-build object has no UTF-8 file name: {object_file}",
1382                        receipt.operator
1383                    ))
1384                })?
1385                .to_string();
1386            validate_relative_path(&member)?;
1387            let sha256 = command.object_sha256.clone().ok_or_else(|| {
1388                NativeOperatorBuilderError::Invalid(format!(
1389                    "{} source-build command is missing object_sha256: {member}",
1390                    receipt.operator
1391                ))
1392            })?;
1393            let size_bytes = command
1394                .object_size_bytes
1395                .filter(|size| *size > 0)
1396                .ok_or_else(|| {
1397                    NativeOperatorBuilderError::Invalid(format!(
1398                        "{} source-build command is missing object_size_bytes: {member}",
1399                        receipt.operator
1400                    ))
1401                })?;
1402            let object_identity = command.object_identity.clone().ok_or_else(|| {
1403                NativeOperatorBuilderError::Invalid(format!(
1404                    "{} source-build command is missing object_identity: {member}",
1405                    receipt.operator
1406                ))
1407            })?;
1408            validate_native_object_identity(&object_identity, &member)?;
1409            Ok(NativeOperatorArchiveMemberEvidence {
1410                member,
1411                sha256,
1412                size_bytes,
1413                object_identity,
1414            })
1415        })
1416        .collect::<Result<Vec<_>>>()?;
1417    validate_archive_member_evidence(&receipt.operator, &expected)?;
1418    Ok(expected)
1419}
1420
1421fn verify_archive_members(
1422    archive_path: &Path,
1423    operator: &str,
1424    expected: &[NativeOperatorArchiveMemberEvidence],
1425    archiver: &str,
1426    package_root: &Path,
1427    environment: &BTreeMap<String, String>,
1428    verification_log: &str,
1429) -> Result<(
1430    Vec<NativeOperatorArchiveMemberEvidence>,
1431    NativeOperatorEvidenceFile,
1432)> {
1433    validate_relative_path(verification_log)?;
1434    let (evidence, log) = inspect_archive_members(
1435        archive_path,
1436        operator,
1437        expected,
1438        archiver,
1439        package_root,
1440        environment,
1441    )?;
1442    let verification_path = package_root.join(verification_log);
1443    if let Some(parent) = verification_path.parent() {
1444        fs::create_dir_all(parent).map_err(|source| NativeOperatorBuilderError::Io {
1445            path: parent.to_path_buf(),
1446            source,
1447        })?;
1448    }
1449    fs::write(&verification_path, log).map_err(|source| NativeOperatorBuilderError::Io {
1450        path: verification_path,
1451        source,
1452    })?;
1453    Ok((evidence, evidence_file_at(package_root, verification_log)?))
1454}
1455
1456fn inspect_archive_members(
1457    archive_path: &Path,
1458    operator: &str,
1459    expected: &[NativeOperatorArchiveMemberEvidence],
1460    archiver: &str,
1461    package_root: &Path,
1462    environment: &BTreeMap<String, String>,
1463) -> Result<(Vec<NativeOperatorArchiveMemberEvidence>, String)> {
1464    validate_archive_member_evidence(operator, expected)?;
1465    let archive_file = archive_path
1466        .file_name()
1467        .and_then(|value| value.to_str())
1468        .ok_or_else(|| {
1469            NativeOperatorBuilderError::Invalid(format!(
1470                "native archive has no UTF-8 file name: {}",
1471                archive_path.display()
1472            ))
1473        })?;
1474    let list_output = Command::new(archiver)
1475        .args(["t", archive_file])
1476        .current_dir(package_root)
1477        .env_clear()
1478        .envs(environment)
1479        .output()
1480        .map_err(|source| NativeOperatorBuilderError::Io {
1481            path: PathBuf::from(archiver),
1482            source,
1483        })?;
1484    if !list_output.status.success() {
1485        return Err(NativeOperatorBuilderError::Tool {
1486            tool: archiver.to_string(),
1487            status: list_output.status.to_string(),
1488            stderr: String::from_utf8_lossy(&list_output.stderr)
1489                .trim()
1490                .chars()
1491                .take(2000)
1492                .collect(),
1493        });
1494    }
1495    let raw_listed = std::str::from_utf8(&list_output.stdout)
1496        .map_err(|_| {
1497            NativeOperatorBuilderError::Invalid(format!(
1498                "{operator} native archive member list is not UTF-8"
1499            ))
1500        })?
1501        .lines()
1502        .map(str::trim)
1503        .filter(|value| !value.is_empty())
1504        .map(ToOwned::to_owned)
1505        .collect::<Vec<_>>();
1506    let listed = raw_listed
1507        .iter()
1508        .filter(|member| !is_archive_metadata_member(member))
1509        .cloned()
1510        .collect::<Vec<_>>();
1511    let expected_names = expected
1512        .iter()
1513        .map(|evidence| evidence.member.clone())
1514        .collect::<Vec<_>>();
1515    if listed != expected_names {
1516        return Err(NativeOperatorBuilderError::Invalid(format!(
1517            "{operator} native archive members differ from expected evidence: expected={expected_names:?} actual={listed:?}"
1518        )));
1519    }
1520
1521    let mut evidence = Vec::with_capacity(expected.len());
1522    let mut log = format!(
1523        "operator={}\narchive={archive_file}\narchive_sha256={}\nargv={} t {archive_file}\nraw_members={}\nmembers={}\n",
1524        operator,
1525        sha256_file(archive_path)?,
1526        archiver,
1527        raw_listed.join(","),
1528        expected_names.join(",")
1529    );
1530    for expected_member in expected {
1531        let member = &expected_member.member;
1532        let output = Command::new(archiver)
1533            .args(["p", archive_file, &member])
1534            .current_dir(package_root)
1535            .env_clear()
1536            .envs(environment)
1537            .output()
1538            .map_err(|source| NativeOperatorBuilderError::Io {
1539                path: PathBuf::from(archiver),
1540                source,
1541            })?;
1542        if !output.status.success() {
1543            return Err(NativeOperatorBuilderError::Tool {
1544                tool: archiver.to_string(),
1545                status: output.status.to_string(),
1546                stderr: String::from_utf8_lossy(&output.stderr)
1547                    .trim()
1548                    .chars()
1549                    .take(2000)
1550                    .collect(),
1551            });
1552        }
1553        let actual_sha256 = sha256_bytes(&output.stdout);
1554        let actual_size = u64::try_from(output.stdout.len()).unwrap_or(u64::MAX);
1555        let object_identity = native_object_identity_bytes(&output.stdout, member)?;
1556        if actual_sha256 != expected_member.sha256
1557            || actual_size != expected_member.size_bytes
1558            || object_identity != expected_member.object_identity
1559        {
1560            return Err(NativeOperatorBuilderError::Invalid(format!(
1561                "{operator} native archive member evidence mismatch: member={member} expected_sha256={} actual_sha256={actual_sha256} expected_size={} actual_size={actual_size} expected_identity={:?} actual_identity={object_identity:?}",
1562                expected_member.sha256,
1563                expected_member.size_bytes,
1564                expected_member.object_identity
1565            )));
1566        }
1567        log.push_str(&format!(
1568            "argv={archiver} p {archive_file} {member}\nmember={member} sha256={actual_sha256} size_bytes={actual_size} object_identity={object_identity:?}\n"
1569        ));
1570        evidence.push(NativeOperatorArchiveMemberEvidence {
1571            member: member.clone(),
1572            sha256: actual_sha256,
1573            size_bytes: actual_size,
1574            object_identity,
1575        });
1576    }
1577    Ok((evidence, log))
1578}
1579
1580fn validate_archive_member_evidence(
1581    operator: &str,
1582    members: &[NativeOperatorArchiveMemberEvidence],
1583) -> Result<()> {
1584    if members.is_empty()
1585        || members
1586            .windows(2)
1587            .any(|pair| pair[0].member >= pair[1].member)
1588    {
1589        return Err(NativeOperatorBuilderError::Invalid(format!(
1590            "{operator} archive members must be sorted, unique, and non-empty"
1591        )));
1592    }
1593    let expected_identity = &members[0].object_identity;
1594    for member in members {
1595        validate_relative_path(&member.member)?;
1596        if Path::new(&member.member).parent() != Some(Path::new(""))
1597            || !is_sha256_digest(&member.sha256)
1598            || member.size_bytes == 0
1599        {
1600            return Err(NativeOperatorBuilderError::Invalid(format!(
1601                "{operator} archive member evidence is incomplete: {}",
1602                member.member
1603            )));
1604        }
1605        validate_native_object_identity(&member.object_identity, &member.member)?;
1606        if member.object_identity != *expected_identity {
1607            return Err(NativeOperatorBuilderError::Invalid(format!(
1608                "{operator} archive mixes object targets: first={expected_identity:?} member={} actual={:?}",
1609                member.member, member.object_identity
1610            )));
1611        }
1612    }
1613    Ok(())
1614}
1615
1616fn archive_member_evidence_file(
1617    member: &str,
1618    path: &Path,
1619) -> Result<NativeOperatorArchiveMemberEvidence> {
1620    validate_relative_path(member)?;
1621    require_file(path)?;
1622    let bytes = fs::read(path).map_err(|source| NativeOperatorBuilderError::Io {
1623        path: path.to_path_buf(),
1624        source,
1625    })?;
1626    let size_bytes = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
1627    if size_bytes == 0 {
1628        return Err(NativeOperatorBuilderError::Invalid(format!(
1629            "archive member object is empty: {member}"
1630        )));
1631    }
1632    Ok(NativeOperatorArchiveMemberEvidence {
1633        member: member.to_string(),
1634        sha256: sha256_bytes(&bytes),
1635        size_bytes,
1636        object_identity: native_object_identity_bytes(&bytes, member)?,
1637    })
1638}
1639
1640fn is_archive_metadata_member(member: &str) -> bool {
1641    matches!(
1642        member,
1643        "/" | "//" | "__.SYMDEF" | "__.SYMDEF SORTED" | "__.SYMDEF_64" | "__.SYMDEF_64 SORTED"
1644    )
1645}
1646
1647fn validate_recorded_tool_binary(
1648    operator: &str,
1649    name: &str,
1650    tool: &NativeOperatorToolIdentity,
1651) -> Result<()> {
1652    let path = Path::new(&tool.path);
1653    require_file(path)?;
1654    let canonical = path
1655        .canonicalize()
1656        .map_err(|source| NativeOperatorBuilderError::Io {
1657            path: path.to_path_buf(),
1658            source,
1659        })?;
1660    let actual_path = canonical.display().to_string();
1661    let actual_sha256 = sha256_file(&canonical)?;
1662    if actual_path != tool.path || actual_sha256 != tool.sha256 {
1663        return Err(NativeOperatorBuilderError::Invalid(format!(
1664            "{operator} recorded {name} binary changed before artifact-set assembly"
1665        )));
1666    }
1667    Ok(())
1668}
1669
1670fn package_build_environment(
1671    toolchain: &NativeOperatorPackageToolchain,
1672) -> Result<BTreeMap<String, String>> {
1673    let mut path_entries = [
1674        toolchain.descriptor_compiler.path.as_str(),
1675        toolchain.archiver.path.as_str(),
1676    ]
1677    .iter()
1678    .filter_map(|path| Path::new(path).parent())
1679    .map(Path::to_path_buf)
1680    .collect::<Vec<_>>();
1681    path_entries.extend([PathBuf::from("/bin"), PathBuf::from("/usr/bin")]);
1682    path_entries.sort();
1683    path_entries.dedup();
1684    if path_entries.iter().any(|path| path.as_os_str().is_empty()) {
1685        return Err(NativeOperatorBuilderError::Invalid(
1686            "package tool paths must have parent directories".to_string(),
1687        ));
1688    }
1689    let path = std::env::join_paths(&path_entries)
1690        .map_err(|error| {
1691            NativeOperatorBuilderError::Invalid(format!(
1692                "package tool PATH cannot be represented: {error}"
1693            ))
1694        })?
1695        .into_string()
1696        .map_err(|_| {
1697            NativeOperatorBuilderError::Invalid("package tool PATH is not valid UTF-8".to_string())
1698        })?;
1699    Ok(BTreeMap::from([
1700        ("LANG".to_string(), "C".to_string()),
1701        ("LC_ALL".to_string(), "C".to_string()),
1702        ("PATH".to_string(), path),
1703        ("SOURCE_DATE_EPOCH".to_string(), "0".to_string()),
1704        ("TMPDIR".to_string(), "/tmp".to_string()),
1705        ("TZ".to_string(), "UTC".to_string()),
1706        ("ZERO_AR_DATE".to_string(), "1".to_string()),
1707    ]))
1708}
1709
1710fn run_package_command(
1711    program: &str,
1712    args: Vec<String>,
1713    working_directory: &Path,
1714    stdout_log: &str,
1715    stderr_log: &str,
1716    environment: &BTreeMap<String, String>,
1717) -> Result<NativeOperatorPackageCommand> {
1718    validate_relative_path(stdout_log)?;
1719    validate_relative_path(stderr_log)?;
1720    for relative in [stdout_log, stderr_log] {
1721        let parent = working_directory
1722            .join(relative)
1723            .parent()
1724            .expect("validated log path has parent")
1725            .to_path_buf();
1726        fs::create_dir_all(&parent).map_err(|source| NativeOperatorBuilderError::Io {
1727            path: parent,
1728            source,
1729        })?;
1730    }
1731    let argv = std::iter::once(program.to_string())
1732        .chain(args.iter().cloned())
1733        .collect::<Vec<_>>();
1734    let started = Instant::now();
1735    let output = Command::new(program)
1736        .args(&args)
1737        .current_dir(working_directory)
1738        .env_clear()
1739        .envs(environment)
1740        .output()
1741        .map_err(|source| NativeOperatorBuilderError::Io {
1742            path: PathBuf::from(program),
1743            source,
1744        })?;
1745    let elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX);
1746    let write_log = |relative: &str, stream: &str, bytes: &[u8]| -> Result<()> {
1747        let path = working_directory.join(relative);
1748        let mut content = format!(
1749            "stream={stream}\nworking_directory=.\nargv={}\n",
1750            argv.join(" ")
1751        )
1752        .into_bytes();
1753        content.extend_from_slice(bytes);
1754        if !content.ends_with(b"\n") {
1755            content.push(b'\n');
1756        }
1757        fs::write(&path, content).map_err(|source| NativeOperatorBuilderError::Io { path, source })
1758    };
1759    write_log(stdout_log, "stdout", &output.stdout)?;
1760    write_log(stderr_log, "stderr", &output.stderr)?;
1761    let return_code = output.status.code().unwrap_or(-1);
1762    if !output.status.success() {
1763        return Err(NativeOperatorBuilderError::Tool {
1764            tool: program.to_string(),
1765            status: output.status.to_string(),
1766            stderr: String::from_utf8_lossy(&output.stderr)
1767                .trim()
1768                .chars()
1769                .take(2000)
1770                .collect(),
1771        });
1772    }
1773    Ok(NativeOperatorPackageCommand {
1774        argv,
1775        working_directory: ".".to_string(),
1776        stdout_log: stdout_log.to_string(),
1777        stderr_log: stderr_log.to_string(),
1778        return_code,
1779        elapsed_ms,
1780    })
1781}
1782
1783pub fn assemble_native_operator_set(
1784    request: &NativeOperatorSetRequest,
1785) -> Result<NativeOperatorArtifactSetLock> {
1786    if request.receipt_paths.is_empty() {
1787        return Err(NativeOperatorBuilderError::Invalid(
1788            "artifact set requires at least one package receipt".to_string(),
1789        ));
1790    }
1791    if request.expected_receipt_sha256.len() != request.receipt_paths.len()
1792        || request
1793            .expected_receipt_sha256
1794            .iter()
1795            .any(|digest| !is_sha256_digest(digest))
1796    {
1797        return Err(NativeOperatorBuilderError::Invalid(
1798            "artifact set requires one external receipt sha256 pin per package".to_string(),
1799        ));
1800    }
1801    if !is_sha256_digest(&request.expected_g03_catalog_sha256) {
1802        return Err(NativeOperatorBuilderError::Invalid(
1803            "artifact set requires an external g03 catalog sha256 pin".to_string(),
1804        ));
1805    }
1806    if request.output_lock_path.exists() {
1807        return Err(NativeOperatorBuilderError::OutputExists(
1808            request.output_lock_path.clone(),
1809        ));
1810    }
1811    if !request.compute_capability.starts_with("sm_") {
1812        return Err(NativeOperatorBuilderError::Invalid(
1813            "compute_capability must use sm_xx form".to_string(),
1814        ));
1815    }
1816    let root = request
1817        .output_lock_path
1818        .parent()
1819        .unwrap_or_else(|| Path::new("."));
1820    fs::create_dir_all(root).map_err(|source| NativeOperatorBuilderError::Io {
1821        path: root.to_path_buf(),
1822        source,
1823    })?;
1824    let canonical_root = root
1825        .canonicalize()
1826        .map_err(|source| NativeOperatorBuilderError::Io {
1827            path: root.to_path_buf(),
1828            source,
1829        })?;
1830
1831    let mut artifacts = Vec::with_capacity(request.receipt_paths.len());
1832    for (receipt_path, expected_receipt_sha256) in request
1833        .receipt_paths
1834        .iter()
1835        .zip(&request.expected_receipt_sha256)
1836    {
1837        require_file(receipt_path)?;
1838        let actual_receipt_sha256 = sha256_file(receipt_path)?;
1839        if &actual_receipt_sha256 != expected_receipt_sha256 {
1840            return Err(NativeOperatorBuilderError::Invalid(format!(
1841                "package receipt differs from its external sha256 pin: path={} expected={expected_receipt_sha256} actual={actual_receipt_sha256}",
1842                receipt_path.display()
1843            )));
1844        }
1845        let receipt: NativeOperatorPackageReceipt = read_json(receipt_path)?;
1846        validate_package_receipt(&receipt)?;
1847        if receipt.g03_catalog_sha256 != request.expected_g03_catalog_sha256 {
1848            return Err(NativeOperatorBuilderError::Invalid(format!(
1849                "package {} catalog differs from the external artifact-set pin: expected={} actual={}",
1850                receipt.operator,
1851                request.expected_g03_catalog_sha256,
1852                receipt.g03_catalog_sha256
1853            )));
1854        }
1855        let package_root = receipt_path.parent().unwrap_or_else(|| Path::new("."));
1856        let package_receipt = resolve_file_evidence(receipt_path, &canonical_root)?;
1857        let manifest_path = resolve_relative_file(package_root, &receipt.manifest_file)?;
1858        let manifest_evidence = resolve_file_evidence(&manifest_path, &canonical_root)?;
1859        let artifact_path = resolve_relative_file(package_root, &receipt.artifact_file)?;
1860        let package_spec =
1861            resolve_package_evidence(package_root, &canonical_root, &receipt.package_spec)?;
1862        let g03_catalog =
1863            resolve_package_evidence(package_root, &canonical_root, &receipt.g03_catalog)?;
1864        let abi_contract =
1865            resolve_package_evidence(package_root, &canonical_root, &receipt.abi_contract)?;
1866        let source_build_receipt =
1867            resolve_package_evidence(package_root, &canonical_root, &receipt.source_build_receipt)?;
1868        let source_build_plan =
1869            resolve_package_evidence(package_root, &canonical_root, &receipt.source_build_plan)?;
1870        let source_build_inputs = receipt
1871            .source_build_inputs
1872            .iter()
1873            .map(|evidence| resolve_package_evidence(package_root, &canonical_root, evidence))
1874            .collect::<Result<Vec<_>>>()?;
1875        let source_build_logs = receipt
1876            .source_build_logs
1877            .iter()
1878            .map(|evidence| resolve_package_evidence(package_root, &canonical_root, evidence))
1879            .collect::<Result<Vec<_>>>()?;
1880        let package_build_logs = receipt
1881            .package_build_logs
1882            .iter()
1883            .map(|evidence| resolve_package_evidence(package_root, &canonical_root, evidence))
1884            .collect::<Result<Vec<_>>>()?;
1885        let license_files = receipt
1886            .license_files
1887            .iter()
1888            .map(|evidence| resolve_package_evidence(package_root, &canonical_root, evidence))
1889            .collect::<Result<Vec<_>>>()?;
1890        let package_spec_path = resolve_relative_file(package_root, &receipt.package_spec.path)?;
1891        let g03_catalog_path = resolve_relative_file(package_root, &receipt.g03_catalog.path)?;
1892        let abi_contract_path = resolve_relative_file(package_root, &receipt.abi_contract.path)?;
1893        let source_build_receipt_path =
1894            resolve_relative_file(package_root, &receipt.source_build_receipt.path)?;
1895        let source_build_plan_path =
1896            resolve_relative_file(package_root, &receipt.source_build_plan.path)?;
1897        let packaged_spec: NativeOperatorPackageSpec = read_json(&package_spec_path)?;
1898        let packaged_g03_catalog: NativeOperatorProviderCatalog = read_json(&g03_catalog_path)?;
1899        let packaged_abi_contract: NativeOperatorAbiContract = read_json(&abi_contract_path)?;
1900        let packaged_source_build: NativeOperatorSourceBuildReceipt =
1901            read_json(&source_build_receipt_path)?;
1902        validate_package_spec(&packaged_spec)?;
1903        packaged_g03_catalog
1904            .validate()
1905            .map_err(NativeOperatorBuilderError::Invalid)?;
1906        packaged_abi_contract
1907            .validate()
1908            .map_err(NativeOperatorBuilderError::Invalid)?;
1909        validate_spec_against_catalog(&packaged_spec, &packaged_g03_catalog)?;
1910        if packaged_g03_catalog
1911            .canonical_sha256()
1912            .map_err(NativeOperatorBuilderError::Invalid)?
1913            != receipt.g03_catalog_sha256
1914            || packaged_abi_contract
1915                .canonical_sha256()
1916                .map_err(NativeOperatorBuilderError::Invalid)?
1917                != receipt.abi_contract_sha256
1918        {
1919            return Err(NativeOperatorBuilderError::Invalid(format!(
1920                "{} packaged catalog/ABI semantic hashes differ from their receipt pins",
1921                receipt.operator
1922            )));
1923        }
1924        validate_source_build_for_package(&packaged_source_build, &packaged_spec)?;
1925        let packaged_source_plan = verify_source_build_receipt_against_plan_portable(
1926            &packaged_source_build,
1927            &source_build_plan_path,
1928        )?;
1929        verify_source_build_evidence(
1930            &packaged_source_build,
1931            source_build_receipt_path
1932                .parent()
1933                .unwrap_or_else(|| Path::new(".")),
1934            &packaged_source_plan,
1935        )?;
1936        if manifest_evidence.sha256 != receipt.manifest_sha256 {
1937            return Err(NativeOperatorBuilderError::Invalid(format!(
1938                "{} manifest sha256 differs from its package receipt",
1939                receipt.operator
1940            )));
1941        }
1942        if sha256_file(&artifact_path)? != receipt.binary_sha256 {
1943            return Err(NativeOperatorBuilderError::Invalid(format!(
1944                "{} artifact sha256 differs from its package receipt",
1945                receipt.operator
1946            )));
1947        }
1948        validate_recorded_tool_binary(
1949            &receipt.operator,
1950            "archiver",
1951            &receipt.package_toolchain.archiver,
1952        )?;
1953        let package_environment = package_build_environment(&receipt.package_toolchain)?;
1954        inspect_archive_members(
1955            &artifact_path,
1956            &receipt.operator,
1957            &receipt.final_archive_members,
1958            &receipt.package_toolchain.archiver.path,
1959            package_root,
1960            &package_environment,
1961        )?;
1962        let manifest: NativeOperatorManifest = read_json(&manifest_path)?;
1963        manifest
1964            .validate()
1965            .map_err(NativeOperatorBuilderError::Invalid)?;
1966        validate_package_semantic_links(
1967            &receipt,
1968            &packaged_spec,
1969            &packaged_source_build,
1970            &packaged_source_plan,
1971            &manifest,
1972        )?;
1973        NativeOperatorResolver.resolve(
1974            &NativeOperatorResolveRequest::new(
1975                manifest.operator.clone(),
1976                manifest.backend,
1977                &manifest_path,
1978                &artifact_path,
1979            )
1980            .with_compute_capability(request.compute_capability.clone())
1981            .with_operator_abi_version(manifest.operator_abi_version.clone())
1982            .with_ferrum_native_abi_version(manifest.ferrum_native_abi_version.clone())
1983            .with_g03_catalog_sha256(receipt.g03_catalog_sha256.clone())
1984            .with_abi_contract_sha256(receipt.abi_contract_sha256.clone())
1985            .with_descriptor_export(receipt.descriptor_export.clone())
1986            .with_required_exports(manifest.exports.clone())
1987            .with_operation_bindings(manifest.operation_bindings.clone()),
1988        )?;
1989
1990        artifacts.push(NativeOperatorArtifactLock {
1991            operator: manifest.operator,
1992            backend: manifest.backend,
1993            manifest_path: relative_path(&canonical_root, &manifest_path)?,
1994            manifest: manifest_evidence,
1995            artifact_path: relative_path(&canonical_root, &artifact_path)?,
1996            operator_abi_version: manifest.operator_abi_version,
1997            ferrum_native_abi_version: manifest.ferrum_native_abi_version,
1998            source_package_sha256: manifest.source_package.sha256,
1999            inputs_sha256: manifest.inputs_sha256,
2000            package_spec,
2001            g03_catalog,
2002            abi_contract,
2003            source_build_receipt,
2004            source_build_plan,
2005            source_build_inputs,
2006            source_build_logs,
2007            source_archive_sha256: receipt.source_archive_sha256,
2008            package_receipt,
2009            package_build_logs,
2010            license_files,
2011            binary_sha256: receipt.binary_sha256,
2012            abi_contract_sha256: receipt.abi_contract_sha256,
2013            descriptor_export: receipt.descriptor_export,
2014            required_exports: manifest.exports,
2015            operation_bindings: manifest.operation_bindings,
2016            system_libraries: receipt.system_libraries,
2017        });
2018    }
2019    artifacts.sort_by(|left, right| left.operator.cmp(&right.operator));
2020    if artifacts
2021        .windows(2)
2022        .any(|pair| pair[0].operator == pair[1].operator)
2023    {
2024        return Err(NativeOperatorBuilderError::Invalid(
2025            "artifact set contains duplicate operators".to_string(),
2026        ));
2027    }
2028    let lock = NativeOperatorArtifactSetLock {
2029        schema_version: NATIVE_OPERATOR_ARTIFACT_SET_SCHEMA_VERSION,
2030        g03_catalog_sha256: request.expected_g03_catalog_sha256.clone(),
2031        artifacts,
2032    };
2033
2034    let mut temporary =
2035        NamedTempFile::new_in(root).map_err(|source| NativeOperatorBuilderError::Io {
2036            path: root.to_path_buf(),
2037            source,
2038        })?;
2039    serde_json::to_writer_pretty(&mut temporary, &lock).map_err(|source| {
2040        NativeOperatorBuilderError::Json {
2041            path: temporary.path().to_path_buf(),
2042            source,
2043        }
2044    })?;
2045    temporary
2046        .write_all(b"\n")
2047        .map_err(|source| NativeOperatorBuilderError::Io {
2048            path: temporary.path().to_path_buf(),
2049            source,
2050        })?;
2051    temporary
2052        .flush()
2053        .map_err(|source| NativeOperatorBuilderError::Io {
2054            path: temporary.path().to_path_buf(),
2055            source,
2056        })?;
2057    NativeOperatorArtifactSetLock::load_and_resolve(
2058        temporary.path(),
2059        Some(&request.compute_capability),
2060    )?;
2061    temporary
2062        .persist(&request.output_lock_path)
2063        .map_err(|error| NativeOperatorBuilderError::Io {
2064            path: request.output_lock_path.clone(),
2065            source: error.error,
2066        })?;
2067    Ok(lock)
2068}
2069
2070fn validate_package_definition(definition: &NativeOperatorPackageDefinition) -> Result<()> {
2071    if definition.schema_version != NATIVE_OPERATOR_PACKAGE_DEFINITION_SCHEMA_VERSION {
2072        return Err(NativeOperatorBuilderError::Invalid(format!(
2073            "package definition schema_version must be {NATIVE_OPERATOR_PACKAGE_DEFINITION_SCHEMA_VERSION}"
2074        )));
2075    }
2076    let common = NativeOperatorPackageSpec {
2077        schema_version: NATIVE_OPERATOR_PACKAGE_SPEC_SCHEMA_VERSION,
2078        operator: definition.operator.clone(),
2079        operator_abi_version: definition.operator_abi_version.clone(),
2080        backend: definition.backend,
2081        compute_capabilities: definition.compute_capabilities.clone(),
2082        operation_bindings: Vec::new(),
2083        required_exports: definition.required_exports.clone(),
2084        license_files: definition.license_files.clone(),
2085        cuda_toolkit: definition.cuda_toolkit.clone(),
2086        cuda_runtime_min: definition.cuda_runtime_min.clone(),
2087        system_libraries: definition.system_libraries.clone(),
2088    };
2089    validate_package_spec(&common)?;
2090    let mut previous: Option<(&str, &str)> = None;
2091    for binding in &definition.provider_bindings {
2092        if !valid_contract_identifier(&binding.operation_id, "operation.")
2093            || !valid_contract_identifier(&binding.provider_id, "provider.")
2094        {
2095            return Err(NativeOperatorBuilderError::Invalid(
2096                "package definition operation/provider identities are invalid".to_string(),
2097            ));
2098        }
2099        let key = (binding.operation_id.as_str(), binding.provider_id.as_str());
2100        if previous.is_some_and(|prior| prior >= key) {
2101            return Err(NativeOperatorBuilderError::Invalid(
2102                "package definition provider_bindings must be sorted and unique".to_string(),
2103            ));
2104        }
2105        previous = Some(key);
2106        require_sorted_unique_non_empty("provider binding entrypoints", &binding.entrypoints)?;
2107        if binding
2108            .entrypoints
2109            .iter()
2110            .any(|entrypoint| !definition.required_exports.contains(entrypoint))
2111        {
2112            return Err(NativeOperatorBuilderError::Invalid(
2113                "package definition provider entrypoint is absent from required_exports"
2114                    .to_string(),
2115            ));
2116        }
2117    }
2118    Ok(())
2119}
2120
2121fn validate_spec_against_catalog(
2122    spec: &NativeOperatorPackageSpec,
2123    catalog: &NativeOperatorProviderCatalog,
2124) -> Result<()> {
2125    if spec.backend != catalog.backend {
2126        return Err(NativeOperatorBuilderError::Invalid(format!(
2127            "package spec backend {:?} differs from live G03 catalog backend {:?}",
2128            spec.backend, catalog.backend
2129        )));
2130    }
2131    for binding in &spec.operation_bindings {
2132        let Some(provider) = catalog.providers.iter().find(|provider| {
2133            provider.operation_id == binding.operation_id
2134                && provider.provider_id == binding.provider_id
2135        }) else {
2136            return Err(NativeOperatorBuilderError::Invalid(format!(
2137                "package spec binding {}/{} is absent from the live G03 catalog",
2138                binding.operation_id, binding.provider_id
2139            )));
2140        };
2141        if provider.operation_contract_version != binding.operation_contract_version
2142            || provider.provider_version != binding.provider_version
2143            || provider.provider_implementation_fingerprint
2144                != binding.provider_implementation_fingerprint
2145        {
2146            return Err(NativeOperatorBuilderError::Invalid(format!(
2147                "package spec binding {}/{} differs from the live G03 catalog identity",
2148                binding.operation_id, binding.provider_id
2149            )));
2150        }
2151    }
2152    Ok(())
2153}
2154
2155fn validate_package_spec(spec: &NativeOperatorPackageSpec) -> Result<()> {
2156    if spec.schema_version != NATIVE_OPERATOR_PACKAGE_SPEC_SCHEMA_VERSION {
2157        return Err(NativeOperatorBuilderError::Invalid(format!(
2158            "package spec schema_version must be {NATIVE_OPERATOR_PACKAGE_SPEC_SCHEMA_VERSION}"
2159        )));
2160    }
2161    if spec.backend != NativeOperatorBackend::Cuda {
2162        return Err(NativeOperatorBuilderError::Invalid(
2163            "source-build packager currently accepts CUDA artifacts only".to_string(),
2164        ));
2165    }
2166    if spec
2167        .cuda_toolkit
2168        .as_deref()
2169        .is_none_or(|version| parse_cuda_version(version).is_none())
2170    {
2171        return Err(NativeOperatorBuilderError::Invalid(
2172            "CUDA package spec must declare a numeric major.minor[.patch] cuda_toolkit".to_string(),
2173        ));
2174    }
2175    symbol_slug(&spec.operator)?;
2176    if spec.operator_abi_version.trim().is_empty() {
2177        return Err(NativeOperatorBuilderError::Invalid(
2178            "operator_abi_version must be non-empty".to_string(),
2179        ));
2180    }
2181    require_sorted_unique_non_empty("compute_capabilities", &spec.compute_capabilities)?;
2182    if spec
2183        .compute_capabilities
2184        .iter()
2185        .any(|capability| !capability.starts_with("sm_"))
2186    {
2187        return Err(NativeOperatorBuilderError::Invalid(
2188            "compute_capabilities must use sm_xx form".to_string(),
2189        ));
2190    }
2191    require_sorted_unique_non_empty("required_exports", &spec.required_exports)?;
2192    let mut previous_binding: Option<(&str, &str)> = None;
2193    for binding in &spec.operation_bindings {
2194        if !valid_contract_identifier(&binding.operation_id, "operation.")
2195            || !valid_contract_identifier(&binding.provider_id, "provider.")
2196            || binding.operation_contract_version.major == 0
2197            || binding.provider_version.major == 0
2198            || !is_sha256_digest(&binding.provider_implementation_fingerprint)
2199        {
2200            return Err(NativeOperatorBuilderError::Invalid(
2201                "package spec operation binding identity or version is invalid".to_string(),
2202            ));
2203        }
2204        let key = (binding.operation_id.as_str(), binding.provider_id.as_str());
2205        if previous_binding.is_some_and(|previous| previous >= key) {
2206            return Err(NativeOperatorBuilderError::Invalid(
2207                "package spec operation_bindings must be sorted and unique".to_string(),
2208            ));
2209        }
2210        previous_binding = Some(key);
2211        require_sorted_unique_non_empty("operation binding entrypoints", &binding.entrypoints)?;
2212        if binding
2213            .entrypoints
2214            .iter()
2215            .any(|entrypoint| !spec.required_exports.contains(entrypoint))
2216        {
2217            return Err(NativeOperatorBuilderError::Invalid(
2218                "package spec operation binding entrypoint is absent from required_exports"
2219                    .to_string(),
2220            ));
2221        }
2222    }
2223    if let Some(unit) = CudaNativeBuildUnit::from_artifact_operator(&spec.operator) {
2224        for required in unit.required_exports() {
2225            if !spec
2226                .required_exports
2227                .iter()
2228                .any(|export| export == required)
2229            {
2230                return Err(NativeOperatorBuilderError::Invalid(format!(
2231                    "{} package spec is missing build-unit export {required}",
2232                    unit.as_str()
2233                )));
2234            }
2235        }
2236    }
2237    if spec.license_files.is_empty() {
2238        return Err(NativeOperatorBuilderError::Invalid(
2239            "license_files must be non-empty".to_string(),
2240        ));
2241    }
2242    let mut previous_license: Option<&str> = None;
2243    for license in &spec.license_files {
2244        validate_relative_path(&license.source_path)?;
2245        validate_relative_path(&license.output_path)?;
2246        if previous_license.is_some_and(|previous| previous >= license.output_path.as_str()) {
2247            return Err(NativeOperatorBuilderError::Invalid(
2248                "license_files must be sorted and unique by output_path".to_string(),
2249            ));
2250        }
2251        previous_license = Some(&license.output_path);
2252    }
2253    if spec
2254        .system_libraries
2255        .windows(2)
2256        .any(|pair| pair[0] >= pair[1])
2257    {
2258        return Err(NativeOperatorBuilderError::Invalid(
2259            "system_libraries must be sorted and unique".to_string(),
2260        ));
2261    }
2262    Ok(())
2263}
2264
2265fn valid_contract_identifier(value: &str, prefix: &str) -> bool {
2266    value.starts_with(prefix)
2267        && value.len() > prefix.len()
2268        && value.len() <= 160
2269        && value.bytes().all(|byte| {
2270            byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-' | b':' | b'/')
2271        })
2272}
2273
2274fn cuda_toolkit_version_matches(declared: &str, actual: &str) -> bool {
2275    let Some(declared) = parse_cuda_version(declared) else {
2276        return false;
2277    };
2278    let Some(actual) = parse_cuda_version(actual) else {
2279        return false;
2280    };
2281    declared.len() <= actual.len() && declared.iter().zip(actual.iter()).all(|(a, b)| a == b)
2282}
2283
2284fn parse_cuda_version(value: &str) -> Option<Vec<u32>> {
2285    let parts = value
2286        .split('.')
2287        .map(str::parse::<u32>)
2288        .collect::<std::result::Result<Vec<_>, _>>()
2289        .ok()?;
2290    matches!(parts.len(), 2 | 3)
2291        .then_some(parts)
2292        .filter(|parts| parts.iter().all(|part| *part <= 999))
2293}
2294
2295fn validate_package_receipt(receipt: &NativeOperatorPackageReceipt) -> Result<()> {
2296    if receipt.schema_version != NATIVE_OPERATOR_PACKAGE_RECEIPT_SCHEMA_VERSION {
2297        return Err(NativeOperatorBuilderError::Invalid(format!(
2298            "{} package receipt schema_version must be {}",
2299            receipt.operator, NATIVE_OPERATOR_PACKAGE_RECEIPT_SCHEMA_VERSION
2300        )));
2301    }
2302    if receipt.operator.trim().is_empty() || receipt.descriptor_export.trim().is_empty() {
2303        return Err(NativeOperatorBuilderError::Invalid(
2304            "package receipt operator and descriptor_export must be non-empty".to_string(),
2305        ));
2306    }
2307    validate_relative_path(&receipt.manifest_file)?;
2308    validate_relative_path(&receipt.artifact_file)?;
2309    validate_evidence_record("package_spec", &receipt.package_spec)?;
2310    validate_evidence_record("g03_catalog", &receipt.g03_catalog)?;
2311    validate_evidence_record("abi_contract", &receipt.abi_contract)?;
2312    validate_evidence_record("source_build_receipt", &receipt.source_build_receipt)?;
2313    validate_evidence_record("source_build_plan", &receipt.source_build_plan)?;
2314    if receipt.source_build_inputs.is_empty()
2315        || receipt
2316            .source_build_inputs
2317            .windows(2)
2318            .any(|pair| pair[0].path >= pair[1].path)
2319    {
2320        return Err(NativeOperatorBuilderError::Invalid(format!(
2321            "{} package receipt source_build_inputs must be sorted, unique, and non-empty",
2322            receipt.operator
2323        )));
2324    }
2325    for evidence in &receipt.source_build_inputs {
2326        validate_evidence_record("source_build_input", evidence)?;
2327    }
2328    if receipt.source_build_logs.is_empty()
2329        || receipt
2330            .source_build_logs
2331            .windows(2)
2332            .any(|pair| pair[0].path >= pair[1].path)
2333    {
2334        return Err(NativeOperatorBuilderError::Invalid(format!(
2335            "{} package receipt source_build_logs must be sorted, unique, and non-empty",
2336            receipt.operator
2337        )));
2338    }
2339    for evidence in &receipt.source_build_logs {
2340        validate_evidence_record("source_build_log", evidence)?;
2341    }
2342    validate_evidence_record(
2343        "source_archive_verification",
2344        &receipt.source_archive_verification,
2345    )?;
2346    validate_archive_member_evidence(&receipt.operator, &receipt.source_archive_members)?;
2347    validate_archive_member_evidence(
2348        &receipt.operator,
2349        std::slice::from_ref(&receipt.descriptor_object),
2350    )?;
2351    if receipt.descriptor_object.member != "descriptor.o" {
2352        return Err(NativeOperatorBuilderError::Invalid(format!(
2353            "{} package descriptor object must be descriptor.o",
2354            receipt.operator
2355        )));
2356    }
2357    let mut expected_final_members = receipt.source_archive_members.clone();
2358    expected_final_members.push(receipt.descriptor_object.clone());
2359    expected_final_members.sort_by(|left, right| left.member.cmp(&right.member));
2360    if receipt.final_archive_members != expected_final_members {
2361        return Err(NativeOperatorBuilderError::Invalid(format!(
2362            "{} final archive members must exactly equal source members plus descriptor.o",
2363            receipt.operator
2364        )));
2365    }
2366    validate_archive_member_evidence(&receipt.operator, &receipt.final_archive_members)?;
2367    validate_evidence_record(
2368        "final_archive_verification",
2369        &receipt.final_archive_verification,
2370    )?;
2371    if receipt.license_files.is_empty()
2372        || receipt
2373            .license_files
2374            .windows(2)
2375            .any(|pair| pair[0].path >= pair[1].path)
2376    {
2377        return Err(NativeOperatorBuilderError::Invalid(format!(
2378            "{} package receipt license_files must be sorted, unique, and non-empty",
2379            receipt.operator
2380        )));
2381    }
2382    for evidence in &receipt.license_files {
2383        validate_evidence_record("license_file", evidence)?;
2384    }
2385    for (field, digest) in [
2386        ("source_archive_sha256", &receipt.source_archive_sha256),
2387        ("manifest_sha256", &receipt.manifest_sha256),
2388        ("binary_sha256", &receipt.binary_sha256),
2389        ("g03_catalog_sha256", &receipt.g03_catalog_sha256),
2390        ("abi_contract_sha256", &receipt.abi_contract_sha256),
2391    ] {
2392        if !is_sha256_digest(digest) {
2393            return Err(NativeOperatorBuilderError::Invalid(format!(
2394                "{} package receipt {field} is not a sha256 digest",
2395                receipt.operator
2396            )));
2397        }
2398    }
2399    if receipt.g03_catalog.sha256 != receipt.g03_catalog_sha256
2400        || receipt.abi_contract.sha256 != receipt.abi_contract_sha256
2401    {
2402        return Err(NativeOperatorBuilderError::Invalid(format!(
2403            "{} package receipt catalog/ABI evidence differs from its semantic hash pins",
2404            receipt.operator
2405        )));
2406    }
2407    if receipt
2408        .system_libraries
2409        .windows(2)
2410        .any(|pair| pair[0] >= pair[1])
2411    {
2412        return Err(NativeOperatorBuilderError::Invalid(format!(
2413            "{} package receipt system_libraries must be sorted and unique",
2414            receipt.operator
2415        )));
2416    }
2417    for (name, tool) in [
2418        (
2419            "descriptor_compiler",
2420            &receipt.package_toolchain.descriptor_compiler,
2421        ),
2422        ("archiver", &receipt.package_toolchain.archiver),
2423    ] {
2424        if !Path::new(&tool.path).is_absolute()
2425            || tool.version.trim().is_empty()
2426            || !is_sha256_digest(&tool.sha256)
2427        {
2428            return Err(NativeOperatorBuilderError::Invalid(format!(
2429                "{} package receipt {name} identity is incomplete",
2430                receipt.operator
2431            )));
2432        }
2433    }
2434    if receipt
2435        .package_toolchain
2436        .descriptor_target
2437        .trim()
2438        .is_empty()
2439        || receipt.package_toolchain.descriptor_target.len() > 256
2440        || receipt
2441            .package_toolchain
2442            .descriptor_target
2443            .chars()
2444            .any(char::is_whitespace)
2445    {
2446        return Err(NativeOperatorBuilderError::Invalid(format!(
2447            "{} package descriptor target identity is invalid",
2448            receipt.operator
2449        )));
2450    }
2451    let expected_environment = package_build_environment(&receipt.package_toolchain)?;
2452    if receipt.package_environment != expected_environment {
2453        return Err(NativeOperatorBuilderError::Invalid(format!(
2454            "{} package environment differs from deterministic policy",
2455            receipt.operator
2456        )));
2457    }
2458    let expected_commands = [
2459        (
2460            vec![
2461                receipt.package_toolchain.descriptor_compiler.path.clone(),
2462                "-std=c11".to_string(),
2463                "-O2".to_string(),
2464                "-fno-ident".to_string(),
2465                "-fvisibility=hidden".to_string(),
2466                "-c".to_string(),
2467                "descriptor.c".to_string(),
2468                "-o".to_string(),
2469                "descriptor.o".to_string(),
2470            ],
2471            "build-logs/descriptor-compile.stdout.log",
2472            "build-logs/descriptor-compile.stderr.log",
2473        ),
2474        (
2475            vec![
2476                receipt.package_toolchain.archiver.path.clone(),
2477                "rcs".to_string(),
2478                receipt.artifact_file.clone(),
2479                "descriptor.o".to_string(),
2480            ],
2481            "build-logs/descriptor-archive.stdout.log",
2482            "build-logs/descriptor-archive.stderr.log",
2483        ),
2484    ];
2485    if receipt.package_commands.len() != expected_commands.len() {
2486        return Err(NativeOperatorBuilderError::Invalid(format!(
2487            "{} package receipt must contain exactly {} package commands",
2488            receipt.operator,
2489            expected_commands.len()
2490        )));
2491    }
2492    for (command, (expected_argv, expected_stdout, expected_stderr)) in
2493        receipt.package_commands.iter().zip(expected_commands)
2494    {
2495        validate_relative_path(&command.stdout_log)?;
2496        validate_relative_path(&command.stderr_log)?;
2497        if command.argv != expected_argv
2498            || command.working_directory != "."
2499            || command.stdout_log != expected_stdout
2500            || command.stderr_log != expected_stderr
2501            || command.return_code != 0
2502        {
2503            return Err(NativeOperatorBuilderError::Invalid(format!(
2504                "{} package command differs from deterministic policy: {:?}",
2505                receipt.operator, command.argv
2506            )));
2507        }
2508    }
2509    if receipt.package_build_logs.is_empty()
2510        || receipt
2511            .package_build_logs
2512            .windows(2)
2513            .any(|pair| pair[0].path >= pair[1].path)
2514    {
2515        return Err(NativeOperatorBuilderError::Invalid(format!(
2516            "{} package_build_logs must be sorted, unique, and non-empty",
2517            receipt.operator
2518        )));
2519    }
2520    for evidence in &receipt.package_build_logs {
2521        validate_evidence_record("package_build_log", evidence)?;
2522    }
2523    let mut expected_log_paths = vec![
2524        receipt.source_archive_verification.path.clone(),
2525        receipt.final_archive_verification.path.clone(),
2526    ];
2527    expected_log_paths.extend(
2528        receipt
2529            .package_commands
2530            .iter()
2531            .flat_map(|command| [command.stdout_log.clone(), command.stderr_log.clone()]),
2532    );
2533    expected_log_paths.sort();
2534    let actual_log_paths = receipt
2535        .package_build_logs
2536        .iter()
2537        .map(|evidence| evidence.path.clone())
2538        .collect::<Vec<_>>();
2539    if actual_log_paths != expected_log_paths
2540        || !receipt
2541            .package_build_logs
2542            .contains(&receipt.source_archive_verification)
2543        || !receipt
2544            .package_build_logs
2545            .contains(&receipt.final_archive_verification)
2546    {
2547        return Err(NativeOperatorBuilderError::Invalid(format!(
2548            "{} package build-log evidence does not match recorded commands",
2549            receipt.operator
2550        )));
2551    }
2552    Ok(())
2553}
2554
2555fn validate_package_semantic_links(
2556    receipt: &NativeOperatorPackageReceipt,
2557    spec: &NativeOperatorPackageSpec,
2558    source_build: &NativeOperatorSourceBuildReceipt,
2559    source_plan: &NativeOperatorSourceBuildPlan,
2560    manifest: &NativeOperatorManifest,
2561) -> Result<()> {
2562    validate_package_spec(spec)?;
2563    validate_source_build_for_package(source_build, spec)?;
2564
2565    if receipt.operator != spec.operator
2566        || source_plan.operator != spec.operator
2567        || source_plan.source_package != source_build.source_package
2568    {
2569        return Err(NativeOperatorBuilderError::Invalid(format!(
2570            "{} package spec, source receipt, and source plan identities differ",
2571            receipt.operator
2572        )));
2573    }
2574    if receipt.package_spec.path != "provenance/package.spec.json"
2575        || receipt.g03_catalog.path != "provenance/g03-provider-catalog.json"
2576        || receipt.abi_contract.path != "provenance/native-abi-contract.json"
2577        || receipt.source_build_receipt.path != "provenance/source-build.receipt.json"
2578        || receipt.source_build_plan.path != "provenance/source-build.plan.json"
2579    {
2580        return Err(NativeOperatorBuilderError::Invalid(format!(
2581            "{} package provenance paths differ from the deterministic layout",
2582            receipt.operator
2583        )));
2584    }
2585
2586    let expected_source_log_paths = source_build
2587        .commands
2588        .iter()
2589        .flat_map(|command| [&command.stdout_log, &command.stderr_log])
2590        .map(|relative| format!("provenance/{relative}"))
2591        .collect::<std::collections::BTreeSet<_>>()
2592        .into_iter()
2593        .collect::<Vec<_>>();
2594    let actual_source_log_paths = receipt
2595        .source_build_logs
2596        .iter()
2597        .map(|evidence| evidence.path.clone())
2598        .collect::<Vec<_>>();
2599    if actual_source_log_paths != expected_source_log_paths {
2600        return Err(NativeOperatorBuilderError::Invalid(format!(
2601            "{} packaged source-build logs do not exactly match its command graph",
2602            receipt.operator
2603        )));
2604    }
2605    let toolchain = source_build.toolchain.as_ref().ok_or_else(|| {
2606        NativeOperatorBuilderError::Invalid(format!(
2607            "{} source-build receipt is missing toolchain provenance",
2608            receipt.operator
2609        ))
2610    })?;
2611    let mut expected_source_input_paths = vec![
2612        format!(
2613            "provenance/{}",
2614            toolchain.static_identity.cuda_toolkit.manifest.path
2615        ),
2616        format!(
2617            "provenance/{}",
2618            toolchain.static_identity.host_toolchain.manifest.path
2619        ),
2620    ];
2621    expected_source_input_paths.extend(
2622        source_build
2623            .commands
2624            .iter()
2625            .filter(|command| {
2626                matches!(
2627                    command.dependency_validation,
2628                    Some(
2629                        NativeOperatorDependencyValidation::Depfile
2630                            | NativeOperatorDependencyValidation::CacheProof
2631                    )
2632                )
2633            })
2634            .flat_map(|command| {
2635                [command.compiler_depfile.as_ref(), command.depfile.as_ref()]
2636                    .into_iter()
2637                    .flatten()
2638            })
2639            .map(|relative| format!("provenance/{relative}")),
2640    );
2641    expected_source_input_paths.sort();
2642    expected_source_input_paths.dedup();
2643    let actual_source_input_paths = receipt
2644        .source_build_inputs
2645        .iter()
2646        .map(|evidence| evidence.path.clone())
2647        .collect::<Vec<_>>();
2648    if actual_source_input_paths != expected_source_input_paths {
2649        return Err(NativeOperatorBuilderError::Invalid(format!(
2650            "{} packaged source-build inputs do not exactly match toolkit/depfile evidence",
2651            receipt.operator
2652        )));
2653    }
2654
2655    if receipt.source_archive_sha256 != source_build.archive_sha256.as_deref().unwrap_or_default() {
2656        return Err(NativeOperatorBuilderError::Invalid(format!(
2657            "{} package source archive pin differs from the source-build receipt",
2658            receipt.operator
2659        )));
2660    }
2661    let expected_source_members = source_archive_member_expectations(source_build)?;
2662    if receipt.source_archive_members != expected_source_members {
2663        return Err(NativeOperatorBuilderError::Invalid(format!(
2664            "{} package source archive members differ from source-build object evidence",
2665            receipt.operator
2666        )));
2667    }
2668
2669    let identity_suffix = &sha256_bytes(spec.operator.as_bytes())[..12];
2670    let symbol_slug = symbol_slug(&spec.operator)?;
2671    let expected_descriptor =
2672        format!("ferrum_native_{symbol_slug}_{identity_suffix}_descriptor_v2");
2673    let expected_artifact = format!("libferrum_native_{symbol_slug}_{identity_suffix}.a");
2674    let mut expected_exports = spec.required_exports.clone();
2675    expected_exports.push(expected_descriptor.clone());
2676    expected_exports.sort();
2677    let expected_license_paths = spec
2678        .license_files
2679        .iter()
2680        .map(|license| license.output_path.clone())
2681        .collect::<Vec<_>>();
2682    let actual_license_paths = receipt
2683        .license_files
2684        .iter()
2685        .map(|evidence| evidence.path.clone())
2686        .collect::<Vec<_>>();
2687
2688    if receipt.descriptor_export != expected_descriptor
2689        || receipt.manifest_file != "native_operator_manifest.json"
2690        || receipt.artifact_file != expected_artifact
2691        || receipt.system_libraries != spec.system_libraries
2692        || actual_license_paths != expected_license_paths
2693    {
2694        return Err(NativeOperatorBuilderError::Invalid(format!(
2695            "{} package receipt differs from its deterministic spec projection",
2696            receipt.operator
2697        )));
2698    }
2699
2700    let expected_build_summary = source_build_summary(source_build)?;
2701    if manifest.operator != spec.operator
2702        || manifest.operator_abi_version != spec.operator_abi_version
2703        || manifest.ferrum_native_abi_version != FERRUM_NATIVE_OPERATOR_ABI_VERSION
2704        || manifest.backend != spec.backend
2705        || manifest.cuda_toolkit != spec.cuda_toolkit
2706        || manifest.cuda_runtime_min != spec.cuda_runtime_min
2707        || manifest.compute_capabilities != spec.compute_capabilities
2708        || manifest.source_package != source_build.source_package
2709        || manifest.inputs_sha256 != source_build.inputs_sha256
2710        || manifest.binary_sha256 != receipt.binary_sha256
2711        || manifest.linkage != NativeOperatorLinkage::Static
2712        || manifest.g03_catalog_sha256.as_deref() != Some(&receipt.g03_catalog_sha256)
2713        || manifest.abi_contract_sha256.as_deref() != Some(&receipt.abi_contract_sha256)
2714        || manifest.descriptor_export.as_deref() != Some(expected_descriptor.as_str())
2715        || manifest.operation_bindings != spec.operation_bindings
2716        || manifest.exports != expected_exports
2717        || manifest.license_files != expected_license_paths
2718        || manifest.build_summary != expected_build_summary
2719    {
2720        return Err(NativeOperatorBuilderError::Invalid(format!(
2721            "{} manifest is not the exact semantic projection of its package and source evidence",
2722            receipt.operator
2723        )));
2724    }
2725    Ok(())
2726}
2727
2728fn validate_evidence_record(field: &str, evidence: &NativeOperatorEvidenceFile) -> Result<()> {
2729    validate_relative_path(&evidence.path)?;
2730    if !is_sha256_digest(&evidence.sha256) || evidence.size_bytes == 0 {
2731        return Err(NativeOperatorBuilderError::Invalid(format!(
2732            "{field} must record a non-empty file and lowercase sha256"
2733        )));
2734    }
2735    Ok(())
2736}
2737
2738fn resolve_package_evidence(
2739    package_root: &Path,
2740    artifact_set_root: &Path,
2741    evidence: &NativeOperatorEvidenceFile,
2742) -> Result<NativeOperatorEvidenceFile> {
2743    validate_evidence_record("package evidence", evidence)?;
2744    let path = resolve_relative_file(package_root, &evidence.path)?;
2745    let actual_size = fs::metadata(&path)
2746        .map_err(|source| NativeOperatorBuilderError::Io {
2747            path: path.clone(),
2748            source,
2749        })?
2750        .len();
2751    let actual_sha256 = sha256_file(&path)?;
2752    if actual_sha256 != evidence.sha256 || actual_size != evidence.size_bytes {
2753        return Err(NativeOperatorBuilderError::Invalid(format!(
2754            "package evidence differs from its receipt: {}",
2755            evidence.path
2756        )));
2757    }
2758    Ok(NativeOperatorEvidenceFile {
2759        path: relative_path(artifact_set_root, &path)?,
2760        sha256: actual_sha256,
2761        size_bytes: actual_size,
2762    })
2763}
2764
2765fn resolve_file_evidence(
2766    path: &Path,
2767    artifact_set_root: &Path,
2768) -> Result<NativeOperatorEvidenceFile> {
2769    require_file(path)?;
2770    let canonical = path
2771        .canonicalize()
2772        .map_err(|source| NativeOperatorBuilderError::Io {
2773            path: path.to_path_buf(),
2774            source,
2775        })?;
2776    let size_bytes = fs::metadata(&canonical)
2777        .map_err(|source| NativeOperatorBuilderError::Io {
2778            path: canonical.clone(),
2779            source,
2780        })?
2781        .len();
2782    if size_bytes == 0 {
2783        return Err(NativeOperatorBuilderError::Invalid(format!(
2784            "native operator evidence file is empty: {}",
2785            canonical.display()
2786        )));
2787    }
2788    Ok(NativeOperatorEvidenceFile {
2789        path: relative_path(artifact_set_root, &canonical)?,
2790        sha256: sha256_file(&canonical)?,
2791        size_bytes,
2792    })
2793}
2794
2795fn render_descriptor_source(
2796    descriptor_export: &str,
2797    operator: &str,
2798    operator_abi_version: &str,
2799    g03_catalog_sha256: &str,
2800    abi_contract_sha256: &str,
2801) -> String {
2802    format!(
2803        "#include <stdint.h>\n\
2804         typedef struct {{\n\
2805           uint32_t struct_size;\n\
2806           uint32_t ferrum_native_abi_version;\n\
2807           const char *operator_name;\n\
2808           const char *operator_abi_version;\n\
2809           const char *g03_catalog_sha256;\n\
2810           const char *abi_contract_sha256;\n\
2811         }} FerrumNativeOperatorDescriptorV2;\n\
2812         static const FerrumNativeOperatorDescriptorV2 descriptor = {{\n\
2813           sizeof(FerrumNativeOperatorDescriptorV2),\n\
2814           {FERRUM_NATIVE_OPERATOR_ABI_VERSION},\n\
2815           \"{}\",\n\
2816           \"{}\",\n\
2817           \"{g03_catalog_sha256}\",\n\
2818           \"{abi_contract_sha256}\"\n\
2819         }};\n\
2820         __attribute__((visibility(\"default\")))\n\
2821         const FerrumNativeOperatorDescriptorV2 *{descriptor_export}(void) {{\n\
2822           return &descriptor;\n\
2823         }}\n",
2824        c_string(operator),
2825        c_string(operator_abi_version),
2826    )
2827}
2828
2829fn c_string(value: &str) -> String {
2830    value
2831        .replace('\\', "\\\\")
2832        .replace('"', "\\\"")
2833        .replace('\n', "\\n")
2834        .replace('\r', "\\r")
2835}
2836
2837fn symbol_slug(operator: &str) -> Result<String> {
2838    if operator.is_empty()
2839        || !operator
2840            .bytes()
2841            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
2842    {
2843        return Err(NativeOperatorBuilderError::Invalid(
2844            "operator must use ASCII alphanumeric, dot, underscore, or hyphen characters"
2845                .to_string(),
2846        ));
2847    }
2848    Ok(operator
2849        .chars()
2850        .map(|character| {
2851            if character.is_ascii_alphanumeric() {
2852                character
2853            } else {
2854                '_'
2855            }
2856        })
2857        .collect())
2858}
2859
2860fn require_sorted_unique_non_empty(field: &str, values: &[String]) -> Result<()> {
2861    if values.is_empty()
2862        || values.iter().any(|value| value.trim().is_empty())
2863        || values.windows(2).any(|pair| pair[0] >= pair[1])
2864    {
2865        return Err(NativeOperatorBuilderError::Invalid(format!(
2866            "{field} must be sorted, unique, and non-empty"
2867        )));
2868    }
2869    Ok(())
2870}
2871
2872fn validate_relative_path(path: &str) -> Result<()> {
2873    let path = Path::new(path);
2874    if path.as_os_str().is_empty()
2875        || path.is_absolute()
2876        || path
2877            .components()
2878            .any(|component| !matches!(component, Component::Normal(_)))
2879    {
2880        return Err(NativeOperatorBuilderError::Invalid(format!(
2881            "path must be a non-empty normalized relative path: {}",
2882            path.display()
2883        )));
2884    }
2885    Ok(())
2886}
2887
2888fn resolve_relative_file(root: &Path, relative: &str) -> Result<PathBuf> {
2889    validate_relative_path(relative)?;
2890    let path = root.join(relative);
2891    let canonical = path
2892        .canonicalize()
2893        .map_err(|source| NativeOperatorBuilderError::Io {
2894            path: path.clone(),
2895            source,
2896        })?;
2897    let canonical_root = root
2898        .canonicalize()
2899        .map_err(|source| NativeOperatorBuilderError::Io {
2900            path: root.to_path_buf(),
2901            source,
2902        })?;
2903    if !canonical.starts_with(&canonical_root) || !canonical.is_file() {
2904        return Err(NativeOperatorBuilderError::Invalid(format!(
2905            "relative file escapes its root or is not a file: {relative}"
2906        )));
2907    }
2908    Ok(canonical)
2909}
2910
2911fn relative_path(root: &Path, path: &Path) -> Result<String> {
2912    let canonical = path
2913        .canonicalize()
2914        .map_err(|source| NativeOperatorBuilderError::Io {
2915            path: path.to_path_buf(),
2916            source,
2917        })?;
2918    let relative = canonical.strip_prefix(root).map_err(|_| {
2919        NativeOperatorBuilderError::Invalid(format!(
2920            "artifact-set input {} is outside {}",
2921            canonical.display(),
2922            root.display()
2923        ))
2924    })?;
2925    let value = relative.to_string_lossy().replace('\\', "/");
2926    validate_relative_path(&value)?;
2927    Ok(value)
2928}
2929
2930fn require_file(path: &Path) -> Result<()> {
2931    if path.is_file() {
2932        Ok(())
2933    } else {
2934        Err(NativeOperatorBuilderError::MissingFile(path.to_path_buf()))
2935    }
2936}
2937
2938fn read_json<T: DeserializeOwned>(path: &Path) -> Result<T> {
2939    let bytes = fs::read(path).map_err(|source| NativeOperatorBuilderError::Io {
2940        path: path.to_path_buf(),
2941        source,
2942    })?;
2943    serde_json::from_slice(&bytes).map_err(|source| NativeOperatorBuilderError::Json {
2944        path: path.to_path_buf(),
2945        source,
2946    })
2947}
2948
2949fn read_json_with_sha256<T: DeserializeOwned>(path: &Path) -> Result<(T, String)> {
2950    let bytes = fs::read(path).map_err(|source| NativeOperatorBuilderError::Io {
2951        path: path.to_path_buf(),
2952        source,
2953    })?;
2954    let value =
2955        serde_json::from_slice(&bytes).map_err(|source| NativeOperatorBuilderError::Json {
2956            path: path.to_path_buf(),
2957            source,
2958        })?;
2959    Ok((value, sha256_bytes(&bytes)))
2960}
2961
2962fn write_json(path: &Path, value: &impl Serialize) -> Result<()> {
2963    let mut bytes =
2964        serde_json::to_vec_pretty(value).map_err(|source| NativeOperatorBuilderError::Json {
2965            path: path.to_path_buf(),
2966            source,
2967        })?;
2968    bytes.push(b'\n');
2969    let parent = path.parent().ok_or_else(|| {
2970        NativeOperatorBuilderError::Invalid(format!(
2971            "JSON output has no parent directory: {}",
2972            path.display()
2973        ))
2974    })?;
2975    let mut temporary =
2976        NamedTempFile::new_in(parent).map_err(|source| NativeOperatorBuilderError::Io {
2977            path: parent.to_path_buf(),
2978            source,
2979        })?;
2980    temporary
2981        .as_file_mut()
2982        .write_all(&bytes)
2983        .and_then(|()| temporary.as_file_mut().flush())
2984        .and_then(|()| temporary.as_file().sync_all())
2985        .map_err(|source| NativeOperatorBuilderError::Io {
2986            path: temporary.path().to_path_buf(),
2987            source,
2988        })?;
2989    temporary
2990        .persist(path)
2991        .map_err(|error| NativeOperatorBuilderError::Io {
2992            path: path.to_path_buf(),
2993            source: error.error,
2994        })?;
2995    Ok(())
2996}
2997
2998fn sha256_file(path: &Path) -> Result<String> {
2999    let bytes = fs::read(path).map_err(|source| NativeOperatorBuilderError::Io {
3000        path: path.to_path_buf(),
3001        source,
3002    })?;
3003    Ok(sha256_bytes(&bytes))
3004}
3005
3006fn sha256_bytes(bytes: &[u8]) -> String {
3007    format!("{:x}", Sha256::digest(bytes))
3008}
3009
3010#[cfg(test)]
3011mod tests {
3012    use super::*;
3013    use ferrum_native_ops::NativeOperatorArtifactSetLock;
3014    use std::os::unix::fs::PermissionsExt;
3015
3016    fn digest(character: char) -> String {
3017        std::iter::repeat(character).take(64).collect()
3018    }
3019
3020    fn write_executable_script(path: &Path, contents: &str) {
3021        fs::write(path, contents).unwrap();
3022        let mut permissions = fs::metadata(path).unwrap().permissions();
3023        permissions.set_mode(0o755);
3024        fs::set_permissions(path, permissions).unwrap();
3025    }
3026
3027    #[test]
3028    fn checked_in_native_package_definitions_and_abi_are_canonical() {
3029        let repository_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
3030        let abi_path = repository_root.join("native-operators/abi/ferrum-native-abi-v2.json");
3031        let abi: NativeOperatorAbiContract = read_json(&abi_path).unwrap();
3032        assert_eq!(
3033            fs::read(&abi_path).unwrap(),
3034            abi.canonical_json_bytes().unwrap()
3035        );
3036
3037        for name in [
3038            "marlin",
3039            "vllm-marlin",
3040            "vllm-moe-marlin",
3041            "vllm-paged-attention-v2",
3042        ] {
3043            let definition_path = repository_root.join(format!(
3044                "native-operators/cuda/package-definitions/{name}.json"
3045            ));
3046            let definition: NativeOperatorPackageDefinition = read_json(&definition_path).unwrap();
3047            validate_package_definition(&definition).unwrap();
3048            let mut canonical = serde_json::to_vec_pretty(&definition).unwrap();
3049            canonical.push(b'\n');
3050            assert_eq!(fs::read(&definition_path).unwrap(), canonical);
3051
3052            let source_definition: NativeOperatorSourceDefinition =
3053                read_json(&repository_root.join(format!(
3054                    "native-operators/cuda/source-definitions/{name}.json"
3055                )))
3056                .unwrap();
3057            let source_plan: NativeOperatorSourceBuildPlan = read_json(&repository_root.join(
3058                format!("native-operators/cuda/source-locks/{name}.plan.json"),
3059            ))
3060            .unwrap();
3061            assert_eq!(definition.operator, source_definition.operator);
3062            assert_eq!(definition.operator, source_plan.operator);
3063        }
3064    }
3065
3066    fn run_source_build_fixture(
3067        root: &Path,
3068        source_root: &Path,
3069        operator: &str,
3070        name: &str,
3071        exports: &[&str],
3072    ) -> (PathBuf, PathBuf, PathBuf) {
3073        let source = exports
3074            .iter()
3075            .enumerate()
3076            .map(|(index, export)| format!("int {export}(void) {{ return {index}; }}\n"))
3077            .collect::<String>();
3078        fs::write(source_root.join("fixture.cu"), source).unwrap();
3079        let definition = NativeOperatorSourceDefinition {
3080            schema_version: NATIVE_OPERATOR_SOURCE_DEFINITION_SCHEMA_VERSION,
3081            operator: operator.to_string(),
3082            source_package_kind: "fixture-source".to_string(),
3083            source_package_revision: "fixture".to_string(),
3084            upstream_sources: vec![NativeOperatorUpstreamSource {
3085                repository: "https://example.invalid/native-op.git".to_string(),
3086                revision: "fixture".to_string(),
3087                license: "Apache-2.0".to_string(),
3088            }],
3089            translation_units: vec!["fixture.cu".to_string()],
3090            headers: Vec::new(),
3091            dependency_closures: vec![NativeOperatorTranslationUnitDependencies {
3092                translation_unit: "fixture.cu".to_string(),
3093                headers: Vec::new(),
3094            }],
3095            include_dirs: Vec::new(),
3096            defines: Vec::new(),
3097            nvcc_policy: NativeOperatorNvccPolicy {
3098                cpp_standard: NativeOperatorCppStandard::Cpp17,
3099                optimization: NativeOperatorOptimization::O3,
3100                use_fast_math: false,
3101                relaxed_constexpr: false,
3102                extended_lambda: false,
3103                host_position_independent_code: true,
3104                host_default_visibility: false,
3105            },
3106            architecture: NativeOperatorCudaArchitecture::DeviceComputeCapability,
3107            archive_file: format!("lib{name}.a"),
3108        };
3109        let definition_path = root.join(format!("{name}.source-definition.json"));
3110        let plan_path = root.join(format!("{name}.source-build.plan.json"));
3111        write_json(&definition_path, &definition).unwrap();
3112        lock_native_operator_source_definition(&definition_path, source_root, &plan_path).unwrap();
3113
3114        let fake_cuda_root = root.join(format!("fake-cuda-{name}"));
3115        for directory in ["bin/crt", "include", "nvvm/bin", "nvvm/libdevice"] {
3116            fs::create_dir_all(fake_cuda_root.join(directory)).unwrap();
3117        }
3118        for (relative, contents) in [
3119            ("bin/bin2c", "fake bin2c\n"),
3120            ("bin/crt/link.stub", "fake link stub\n"),
3121            ("bin/cudafe++", "fake cudafe\n"),
3122            ("bin/ptxas", "fake ptxas\n"),
3123            ("bin/fatbinary", "fake fatbinary\n"),
3124            ("bin/nvlink", "fake nvlink\n"),
3125            ("include/cuda.h", "#define CUDA_VERSION 12040\n"),
3126            ("nvvm/bin/cicc", "fake cicc\n"),
3127            ("nvvm/libdevice/libdevice.10.bc", "fake libdevice\n"),
3128        ] {
3129            fs::write(fake_cuda_root.join(relative), contents).unwrap();
3130        }
3131        let fake_nvcc = fake_cuda_root.join("bin/nvcc");
3132        fs::write(
3133            &fake_nvcc,
3134            "#!/bin/sh\n\
3135             if [ \"$1\" = \"--version\" ]; then echo 'fake nvcc 12.4'; exit 0; fi\n\
3136             src=''\n\
3137             out=''\n\
3138             depfile=''\n\
3139             dep_target=''\n\
3140             while [ \"$#\" -gt 0 ]; do\n\
3141               case \"$1\" in\n\
3142                 -c) src=\"$2\"; shift 2 ;;\n\
3143                 -o) out=\"$2\"; shift 2 ;;\n\
3144                 -MF) depfile=\"$2\"; shift 2 ;;\n\
3145                 -MT) dep_target=\"$2\"; shift 2 ;;\n\
3146                 *) shift ;;\n\
3147               esac\n\
3148             done\n\
3149             exec /usr/bin/cc -x c -MMD -MF \"$depfile\" -MT \"$dep_target\" -c \"$src\" -o \"$out\"\n",
3150        )
3151        .unwrap();
3152        let mut permissions = fs::metadata(&fake_nvcc).unwrap().permissions();
3153        permissions.set_mode(0o755);
3154        fs::set_permissions(&fake_nvcc, permissions).unwrap();
3155
3156        let fake_host_root = root.join(format!("fake-host-{name}"));
3157        fs::create_dir_all(fake_host_root.join("bin")).unwrap();
3158        fs::create_dir_all(fake_host_root.join("include")).unwrap();
3159        fs::write(
3160            fake_host_root.join("include/stddef.h"),
3161            "#define FAKE_HOST 1\n",
3162        )
3163        .unwrap();
3164        for program in ["as", "cc1", "cc1plus", "collect2", "ld"] {
3165            fs::write(
3166                fake_host_root.join("bin").join(program),
3167                format!("fake host tool {program}\n"),
3168            )
3169            .unwrap();
3170        }
3171        let fake_ccbin = fake_host_root.join("bin/c++");
3172        write_executable_script(
3173            &fake_ccbin,
3174            &format!(
3175                "#!/bin/sh\n\
3176                 case \"$1\" in\n\
3177                   --version) echo 'fake host compiler 1.0'; exit 0 ;;\n\
3178                   -dumpmachine) echo 'x86_64-ferrum-linux-gnu'; exit 0 ;;\n\
3179                   -E) echo '#include <...> search starts here:' >&2; echo ' {}' >&2; echo 'End of search list.' >&2; exit 0 ;;\n\
3180                   -###) echo 'fake cc1plus -O2 -x c++' >&2; exit 0 ;;\n\
3181                   -print-prog-name=*) name=${{1#*=}}; echo '{}/bin/'\"$name\"; exit 0 ;;\n\
3182                 esac\n\
3183                 exit 2\n",
3184                fake_host_root.join("include").display(),
3185                fake_host_root.display(),
3186            ),
3187        );
3188
3189        let output_dir = root.join("source-builds").join(name);
3190        run_native_operator_source_build(&NativeOperatorSourceBuildRequest {
3191            plan_path: plan_path.clone(),
3192            source_root: source_root.to_path_buf(),
3193            output_dir: output_dir.clone(),
3194            compute_capability: "sm_89".to_string(),
3195            builder_sha: "7".repeat(40),
3196            nvcc_path: fake_nvcc,
3197            cuda_toolkit_root: fake_cuda_root,
3198            ccbin_path: fake_ccbin,
3199            ar_path: PathBuf::from("/usr/bin/ar"),
3200            nvcc_threads: 2,
3201            object_cache_dir: root.join("object-cache"),
3202            plan_only: false,
3203        })
3204        .unwrap();
3205        (
3206            output_dir.join("source-build.receipt.json"),
3207            plan_path,
3208            output_dir.join(format!("lib{name}.a")),
3209        )
3210    }
3211
3212    fn package_spec(
3213        operator: &str,
3214        operation_id: &str,
3215        provider_id: &str,
3216        exports: &[&str],
3217    ) -> NativeOperatorPackageSpec {
3218        NativeOperatorPackageSpec {
3219            schema_version: NATIVE_OPERATOR_PACKAGE_SPEC_SCHEMA_VERSION,
3220            operator: operator.to_string(),
3221            operator_abi_version: "1".to_string(),
3222            backend: NativeOperatorBackend::Cuda,
3223            compute_capabilities: vec!["sm_89".to_string()],
3224            operation_bindings: vec![NativeOperatorBinding {
3225                operation_id: operation_id.to_string(),
3226                operation_contract_version: ferrum_types::NativeOperatorContractVersion::new(1, 0),
3227                provider_id: provider_id.to_string(),
3228                provider_version: ferrum_types::NativeOperatorContractVersion::new(1, 0),
3229                provider_implementation_fingerprint: digest('c'),
3230                entrypoints: exports.iter().map(|value| (*value).to_string()).collect(),
3231            }],
3232            required_exports: exports.iter().map(|value| (*value).to_string()).collect(),
3233            license_files: vec![NativeOperatorLicenseInput {
3234                source_path: "LICENSE".to_string(),
3235                output_path: "licenses/LICENSE".to_string(),
3236            }],
3237            cuda_toolkit: Some("12.4".to_string()),
3238            cuda_runtime_min: Some("12.4".to_string()),
3239            system_libraries: vec![
3240                NativeOperatorSystemLibrary::CudaRuntime,
3241                NativeOperatorSystemLibrary::StdCxx,
3242            ],
3243        }
3244    }
3245
3246    fn fixture_files(root: &Path) -> (PathBuf, PathBuf, PathBuf) {
3247        let source_root = root.join("source");
3248        fs::create_dir_all(&source_root).unwrap();
3249        fs::write(source_root.join("LICENSE"), "fixture license\n").unwrap();
3250        let catalog_path = root.join("operation-catalog.json");
3251        let abi_path = root.join("native-abi.json");
3252        let version = ferrum_types::NativeOperatorContractVersion::new(1, 0);
3253        write_json(
3254            &catalog_path,
3255            &NativeOperatorProviderCatalog {
3256                schema_version: ferrum_types::NATIVE_OPERATOR_PROVIDER_CATALOG_SCHEMA_VERSION,
3257                backend: NativeOperatorBackend::Cuda,
3258                providers: vec![
3259                    ferrum_types::NativeOperatorProviderCatalogRow {
3260                        operation_id: "operation.dense_linear".to_string(),
3261                        operation_contract_version: version,
3262                        operation_fingerprint: digest('d'),
3263                        provider_id: "provider.cuda.dense_linear.f16.marlin".to_string(),
3264                        provider_version: version,
3265                        provider_implementation_fingerprint: digest('c'),
3266                    },
3267                    ferrum_types::NativeOperatorProviderCatalogRow {
3268                        operation_id: "operation.quantized_linear".to_string(),
3269                        operation_contract_version: version,
3270                        operation_fingerprint: digest('e'),
3271                        provider_id: "provider.cuda.quantized_linear.gptq_marlin".to_string(),
3272                        provider_version: version,
3273                        provider_implementation_fingerprint: digest('c'),
3274                    },
3275                ],
3276            },
3277        )
3278        .unwrap();
3279        write_json(
3280            &abi_path,
3281            &NativeOperatorAbiContract {
3282                schema_version: ferrum_types::NATIVE_OPERATOR_ABI_CONTRACT_SCHEMA_VERSION,
3283                ferrum_native_abi_version: FERRUM_NATIVE_OPERATOR_ABI_VERSION.to_string(),
3284                descriptor_struct: "FerrumNativeOperatorDescriptorV2".to_string(),
3285                descriptor_symbol_policy: "operator_namespaced".to_string(),
3286                descriptor_fields: vec![
3287                    abi_field("struct_size", "uint32_t"),
3288                    abi_field("ferrum_native_abi_version", "uint32_t"),
3289                    abi_field("operator_name", "const char *"),
3290                    abi_field("operator_abi_version", "const char *"),
3291                    abi_field("g03_catalog_sha256", "const char *"),
3292                    abi_field("abi_contract_sha256", "const char *"),
3293                ],
3294            },
3295        )
3296        .unwrap();
3297        (source_root, catalog_path, abi_path)
3298    }
3299
3300    fn abi_field(name: &str, c_type: &str) -> ferrum_types::NativeOperatorAbiField {
3301        ferrum_types::NativeOperatorAbiField {
3302            name: name.to_string(),
3303            c_type: c_type.to_string(),
3304        }
3305    }
3306
3307    #[test]
3308    fn materializes_package_bindings_from_the_live_catalog_and_allows_unbound_leaves() {
3309        let root = tempfile::tempdir().unwrap();
3310        let (_, catalog_path, _) = fixture_files(root.path());
3311        let definition_path = root.path().join("definition.json");
3312        let output_path = root.path().join("package-spec.json");
3313        let mut definition = NativeOperatorPackageDefinition {
3314            schema_version: NATIVE_OPERATOR_PACKAGE_DEFINITION_SCHEMA_VERSION,
3315            operator: CudaNativeBuildUnit::Marlin.artifact_operator().to_string(),
3316            operator_abi_version: "1".to_string(),
3317            backend: NativeOperatorBackend::Cuda,
3318            compute_capabilities: vec!["sm_89".to_string()],
3319            provider_bindings: vec![NativeOperatorProviderBindingDefinition {
3320                operation_id: "operation.dense_linear".to_string(),
3321                provider_id: "provider.cuda.dense_linear.f16.marlin".to_string(),
3322                entrypoints: vec!["marlin_cuda".to_string()],
3323            }],
3324            required_exports: CudaNativeBuildUnit::Marlin
3325                .required_exports()
3326                .iter()
3327                .map(|value| (*value).to_string())
3328                .collect(),
3329            license_files: vec![NativeOperatorLicenseInput {
3330                source_path: "LICENSE".to_string(),
3331                output_path: "licenses/LICENSE".to_string(),
3332            }],
3333            cuda_toolkit: Some("12.4".to_string()),
3334            cuda_runtime_min: Some("12.4".to_string()),
3335            system_libraries: vec![
3336                NativeOperatorSystemLibrary::CudaRuntime,
3337                NativeOperatorSystemLibrary::StdCxx,
3338            ],
3339        };
3340        write_json(&definition_path, &definition).unwrap();
3341        let spec = materialize_native_operator_package_spec(&NativeOperatorPackageSpecRequest {
3342            definition_path: definition_path.clone(),
3343            g03_catalog_path: catalog_path.clone(),
3344            output_path: output_path.clone(),
3345        })
3346        .unwrap();
3347        assert_eq!(spec.operation_bindings.len(), 1);
3348        assert_eq!(
3349            spec.operation_bindings[0].provider_implementation_fingerprint,
3350            digest('c')
3351        );
3352        assert!(output_path.is_file());
3353
3354        definition.provider_bindings.clear();
3355        let unbound_definition = root.path().join("unbound-definition.json");
3356        let unbound_output = root.path().join("unbound-package-spec.json");
3357        write_json(&unbound_definition, &definition).unwrap();
3358        let unbound = materialize_native_operator_package_spec(&NativeOperatorPackageSpecRequest {
3359            definition_path: unbound_definition,
3360            g03_catalog_path: catalog_path,
3361            output_path: unbound_output,
3362        })
3363        .unwrap();
3364        assert!(unbound.operation_bindings.is_empty());
3365    }
3366
3367    #[test]
3368    fn package_spec_materialization_rejects_a_provider_absent_from_the_live_catalog() {
3369        let root = tempfile::tempdir().unwrap();
3370        let (_, catalog_path, _) = fixture_files(root.path());
3371        let definition_path = root.path().join("definition.json");
3372        let output_path = root.path().join("package-spec.json");
3373        write_json(
3374            &definition_path,
3375            &NativeOperatorPackageDefinition {
3376                schema_version: NATIVE_OPERATOR_PACKAGE_DEFINITION_SCHEMA_VERSION,
3377                operator: CudaNativeBuildUnit::Marlin.artifact_operator().to_string(),
3378                operator_abi_version: "1".to_string(),
3379                backend: NativeOperatorBackend::Cuda,
3380                compute_capabilities: vec!["sm_89".to_string()],
3381                provider_bindings: vec![NativeOperatorProviderBindingDefinition {
3382                    operation_id: "operation.dense_linear".to_string(),
3383                    provider_id: "provider.cuda.missing".to_string(),
3384                    entrypoints: vec!["marlin_cuda".to_string()],
3385                }],
3386                required_exports: CudaNativeBuildUnit::Marlin
3387                    .required_exports()
3388                    .iter()
3389                    .map(|value| (*value).to_string())
3390                    .collect(),
3391                license_files: vec![NativeOperatorLicenseInput {
3392                    source_path: "LICENSE".to_string(),
3393                    output_path: "licenses/LICENSE".to_string(),
3394                }],
3395                cuda_toolkit: Some("12.4".to_string()),
3396                cuda_runtime_min: Some("12.4".to_string()),
3397                system_libraries: vec![
3398                    NativeOperatorSystemLibrary::CudaRuntime,
3399                    NativeOperatorSystemLibrary::StdCxx,
3400                ],
3401            },
3402        )
3403        .unwrap();
3404
3405        let error = materialize_native_operator_package_spec(&NativeOperatorPackageSpecRequest {
3406            definition_path,
3407            g03_catalog_path: catalog_path,
3408            output_path: output_path.clone(),
3409        })
3410        .unwrap_err();
3411        assert!(error
3412            .to_string()
3413            .contains("live G03 catalog does not contain"));
3414        assert!(!output_path.exists());
3415    }
3416
3417    fn package_fixture(
3418        root: &Path,
3419        operator: &str,
3420        operation_id: &str,
3421        provider_id: &str,
3422        exports: &[&str],
3423        output_name: &str,
3424    ) -> Result<(PathBuf, NativeOperatorPackageReceipt)> {
3425        let (source_root, catalog_path, abi_path) = fixture_files(root);
3426        let (source_build_receipt, source_build_plan, _) =
3427            run_source_build_fixture(root, &source_root, operator, output_name, exports);
3428        let spec = package_spec(operator, operation_id, provider_id, exports);
3429        let spec_path = root.join(format!("{output_name}.package.json"));
3430        write_json(&spec_path, &spec)?;
3431        let output_dir = root.join("packages").join(output_name);
3432        let receipt = package_native_operator(&NativeOperatorPackageRequest {
3433            spec_path,
3434            source_root: source_root.clone(),
3435            license_root: source_root,
3436            source_build_receipt_path: source_build_receipt,
3437            source_build_plan_path: source_build_plan,
3438            g03_catalog_path: catalog_path,
3439            abi_contract_path: abi_path,
3440            output_dir: output_dir.clone(),
3441            cc: PathBuf::from("/usr/bin/cc"),
3442            ar: PathBuf::from("/usr/bin/ar"),
3443        })?;
3444        Ok((output_dir, receipt))
3445    }
3446
3447    #[test]
3448    fn packages_archive_with_namespaced_descriptor_and_verified_manifest() {
3449        let root = tempfile::tempdir().unwrap();
3450        let exports = ["marlin_cuda", "marlin_cuda_moe"];
3451        let (output_dir, receipt) = package_fixture(
3452            root.path(),
3453            CudaNativeBuildUnit::Marlin.artifact_operator(),
3454            "operation.dense_linear",
3455            "provider.cuda.dense_linear.f16.marlin",
3456            &exports,
3457            "marlin",
3458        )
3459        .unwrap();
3460
3461        assert!(output_dir.join("package.receipt.json").is_file());
3462        assert!(output_dir.join("licenses/LICENSE").is_file());
3463        assert!(!output_dir.join("descriptor.c").exists());
3464        assert!(!output_dir.join("descriptor.o").exists());
3465        let manifest: NativeOperatorManifest =
3466            read_json(&output_dir.join(&receipt.manifest_file)).unwrap();
3467        assert_eq!(
3468            manifest.descriptor_export.as_deref(),
3469            Some(receipt.descriptor_export.as_str())
3470        );
3471        assert!(manifest
3472            .exports
3473            .iter()
3474            .any(|export| export == &receipt.descriptor_export));
3475        assert!(manifest
3476            .exports
3477            .iter()
3478            .any(|export| export == "marlin_cuda"));
3479        assert_eq!(
3480            sha256_file(&output_dir.join(&receipt.artifact_file)).unwrap(),
3481            receipt.binary_sha256
3482        );
3483        assert!(is_sha256_digest(&manifest.source_package.sha256));
3484        assert!(is_sha256_digest(&manifest.inputs_sha256));
3485        assert_eq!(manifest.build_summary.builder_sha, "7".repeat(40));
3486        assert_eq!(
3487            manifest.build_summary.nvcc_version.as_deref(),
3488            Some("fake nvcc 12.4")
3489        );
3490        assert!(is_sha256_digest(&receipt.source_build_receipt.sha256));
3491        assert!(is_sha256_digest(&receipt.source_build_plan.sha256));
3492        assert_eq!(receipt.source_build_inputs.len(), 4);
3493        assert!(receipt
3494            .source_build_inputs
3495            .iter()
3496            .any(|evidence| evidence.path.ends_with("cuda-static-manifest.json")));
3497        assert!(receipt
3498            .source_build_inputs
3499            .iter()
3500            .any(|evidence| evidence.path.ends_with("host-static-manifest.json")));
3501        assert!(receipt
3502            .source_build_inputs
3503            .iter()
3504            .any(|evidence| evidence.path.ends_with(".compiler.raw.d")));
3505        assert!(receipt.source_build_inputs.iter().any(|evidence| {
3506            evidence.path.ends_with(".d") && !evidence.path.ends_with(".compiler.raw.d")
3507        }));
3508        assert!(!receipt.source_build_logs.is_empty());
3509        assert!(is_sha256_digest(&receipt.source_archive_sha256));
3510        assert_eq!(receipt.source_archive_members.len(), 1);
3511        assert!(receipt
3512            .package_build_logs
3513            .contains(&receipt.source_archive_verification));
3514        assert_eq!(receipt.package_commands.len(), 2);
3515    }
3516
3517    #[test]
3518    fn rejects_archive_members_with_mixed_object_abis() {
3519        let members = vec![
3520            NativeOperatorArchiveMemberEvidence {
3521                member: "00000000_alpha.o".to_string(),
3522                sha256: digest('a'),
3523                size_bytes: 1,
3524                object_identity: NativeOperatorObjectIdentity {
3525                    format: NativeOperatorObjectFormat::Elf,
3526                    class_bits: 64,
3527                    endianness: NativeOperatorObjectEndianness::Little,
3528                    machine: 62,
3529                },
3530            },
3531            NativeOperatorArchiveMemberEvidence {
3532                member: "descriptor.o".to_string(),
3533                sha256: digest('b'),
3534                size_bytes: 1,
3535                object_identity: NativeOperatorObjectIdentity {
3536                    format: NativeOperatorObjectFormat::Elf,
3537                    class_bits: 64,
3538                    endianness: NativeOperatorObjectEndianness::Little,
3539                    machine: 183,
3540                },
3541            },
3542        ];
3543
3544        let error = validate_archive_member_evidence("mixed-abi", &members).unwrap_err();
3545
3546        assert!(error.to_string().contains("archive mixes object targets"));
3547    }
3548
3549    #[test]
3550    fn package_spec_rejects_legacy_self_reported_build_provenance() {
3551        let mut value = serde_json::to_value(package_spec(
3552            CudaNativeBuildUnit::Marlin.artifact_operator(),
3553            "operation.dense_linear",
3554            "provider.cuda.dense_linear.f16.marlin",
3555            CudaNativeBuildUnit::Marlin.required_exports(),
3556        ))
3557        .unwrap();
3558        value
3559            .as_object_mut()
3560            .unwrap()
3561            .insert("inputs_sha256".to_string(), serde_json::json!(digest('9')));
3562
3563        assert!(serde_json::from_value::<NativeOperatorPackageSpec>(value).is_err());
3564    }
3565
3566    #[test]
3567    fn accepts_source_build_receipt_after_checkout_relocation() {
3568        let root = tempfile::tempdir().unwrap();
3569        let (source_root, catalog_path, abi_path) = fixture_files(root.path());
3570        let exports = CudaNativeBuildUnit::Marlin.required_exports();
3571        let (source_build_receipt, source_build_plan, _) = run_source_build_fixture(
3572            root.path(),
3573            &source_root,
3574            CudaNativeBuildUnit::Marlin.artifact_operator(),
3575            "relocated",
3576            exports,
3577        );
3578        let original_source_build_root = source_build_receipt.parent().unwrap();
3579        let relocated_source_build_root = root.path().join("relocated-source-build-evidence");
3580        fs::rename(original_source_build_root, &relocated_source_build_root).unwrap();
3581        let source_build_receipt = relocated_source_build_root.join("source-build.receipt.json");
3582        let spec = package_spec(
3583            CudaNativeBuildUnit::Marlin.artifact_operator(),
3584            "operation.dense_linear",
3585            "provider.cuda.dense_linear.f16.marlin",
3586            exports,
3587        );
3588        let spec_path = root.path().join("marlin.package.json");
3589        write_json(&spec_path, &spec).unwrap();
3590        let output_dir = root.path().join("packages/marlin");
3591
3592        let receipt = package_native_operator(&NativeOperatorPackageRequest {
3593            spec_path,
3594            source_root: source_root.clone(),
3595            license_root: source_root,
3596            source_build_receipt_path: source_build_receipt,
3597            source_build_plan_path: source_build_plan,
3598            g03_catalog_path: catalog_path,
3599            abi_contract_path: abi_path,
3600            output_dir: output_dir.clone(),
3601            cc: PathBuf::from("/usr/bin/cc"),
3602            ar: PathBuf::from("/usr/bin/ar"),
3603        })
3604        .unwrap();
3605
3606        assert!(is_sha256_digest(&receipt.source_build_receipt.sha256));
3607        assert!(output_dir.join("package.receipt.json").is_file());
3608    }
3609
3610    #[test]
3611    fn accepts_descriptor_target_alias_when_object_abi_matches() {
3612        let root = tempfile::tempdir().unwrap();
3613        let (source_root, catalog_path, abi_path) = fixture_files(root.path());
3614        let exports = CudaNativeBuildUnit::Marlin.required_exports();
3615        let (source_build_receipt, source_build_plan, _) = run_source_build_fixture(
3616            root.path(),
3617            &source_root,
3618            CudaNativeBuildUnit::Marlin.artifact_operator(),
3619            "target-mismatch",
3620            exports,
3621        );
3622        let spec = package_spec(
3623            CudaNativeBuildUnit::Marlin.artifact_operator(),
3624            "operation.dense_linear",
3625            "provider.cuda.dense_linear.f16.marlin",
3626            exports,
3627        );
3628        let spec_path = root.path().join("target-mismatch.package.json");
3629        write_json(&spec_path, &spec).unwrap();
3630        let output_dir = root.path().join("packages/target-mismatch");
3631        let mismatched_cc = root.path().join("mismatched-cc");
3632        write_executable_script(
3633            &mismatched_cc,
3634            "#!/bin/sh\n\
3635             if [ \"$1\" = \"-dumpmachine\" ]; then echo 'forged-unknown-target'; exit 0; fi\n\
3636             exec /usr/bin/cc \"$@\"\n",
3637        );
3638
3639        let receipt = package_native_operator(&NativeOperatorPackageRequest {
3640            spec_path,
3641            source_root: source_root.clone(),
3642            license_root: source_root,
3643            source_build_receipt_path: source_build_receipt,
3644            source_build_plan_path: source_build_plan,
3645            g03_catalog_path: catalog_path,
3646            abi_contract_path: abi_path,
3647            output_dir: output_dir.clone(),
3648            cc: mismatched_cc,
3649            ar: PathBuf::from("/usr/bin/ar"),
3650        })
3651        .unwrap();
3652
3653        assert_eq!(
3654            receipt.package_toolchain.descriptor_target,
3655            "forged-unknown-target"
3656        );
3657        assert!(output_dir.exists());
3658        assert!(receipt
3659            .final_archive_members
3660            .iter()
3661            .all(|member| member.object_identity == receipt.descriptor_object.object_identity));
3662    }
3663
3664    #[test]
3665    fn rejects_package_that_overclaims_source_build_compute_capabilities() {
3666        let root = tempfile::tempdir().unwrap();
3667        let (source_root, _, _) = fixture_files(root.path());
3668        let exports = CudaNativeBuildUnit::Marlin.required_exports();
3669        let (source_build_receipt, _, _) = run_source_build_fixture(
3670            root.path(),
3671            &source_root,
3672            CudaNativeBuildUnit::Marlin.artifact_operator(),
3673            "overclaimed-sm",
3674            exports,
3675        );
3676        let source_build: NativeOperatorSourceBuildReceipt =
3677            read_json(&source_build_receipt).unwrap();
3678        let mut spec = package_spec(
3679            CudaNativeBuildUnit::Marlin.artifact_operator(),
3680            "operation.dense_linear",
3681            "provider.cuda.dense_linear.f16.marlin",
3682            exports,
3683        );
3684        spec.compute_capabilities.push("sm_90".to_string());
3685
3686        let error = validate_source_build_for_package(&source_build, &spec).unwrap_err();
3687
3688        assert!(error.to_string().contains("must exactly equal"));
3689    }
3690
3691    #[test]
3692    fn rejects_package_toolkit_version_that_differs_from_source_build() {
3693        let root = tempfile::tempdir().unwrap();
3694        let (source_root, _, _) = fixture_files(root.path());
3695        let exports = CudaNativeBuildUnit::Marlin.required_exports();
3696        let (source_build_receipt, _, _) = run_source_build_fixture(
3697            root.path(),
3698            &source_root,
3699            CudaNativeBuildUnit::Marlin.artifact_operator(),
3700            "wrong-toolkit",
3701            exports,
3702        );
3703        let source_build: NativeOperatorSourceBuildReceipt =
3704            read_json(&source_build_receipt).unwrap();
3705        let mut spec = package_spec(
3706            CudaNativeBuildUnit::Marlin.artifact_operator(),
3707            "operation.dense_linear",
3708            "provider.cuda.dense_linear.f16.marlin",
3709            exports,
3710        );
3711        spec.cuda_toolkit = Some("12.6".to_string());
3712
3713        let error = validate_source_build_for_package(&source_build, &spec).unwrap_err();
3714
3715        assert!(error
3716            .to_string()
3717            .contains("package cuda_toolkit does not match source-build toolkit release 12.4.0"));
3718    }
3719
3720    #[test]
3721    fn rejects_non_pass_source_build_without_publishing_output() {
3722        let root = tempfile::tempdir().unwrap();
3723        let (source_root, catalog_path, abi_path) = fixture_files(root.path());
3724        let exports = CudaNativeBuildUnit::Marlin.required_exports();
3725        let (source_build_receipt, source_build_plan, _) = run_source_build_fixture(
3726            root.path(),
3727            &source_root,
3728            CudaNativeBuildUnit::Marlin.artifact_operator(),
3729            "marlin-plan",
3730            exports,
3731        );
3732        let mut source_build: NativeOperatorSourceBuildReceipt =
3733            read_json(&source_build_receipt).unwrap();
3734        source_build.status = NativeOperatorSourceBuildStatus::Plan;
3735        source_build.plan_only = true;
3736        write_json(&source_build_receipt, &source_build).unwrap();
3737        let spec = package_spec(
3738            CudaNativeBuildUnit::Marlin.artifact_operator(),
3739            "operation.dense_linear",
3740            "provider.cuda.dense_linear.f16.marlin",
3741            exports,
3742        );
3743        let spec_path = root.path().join("marlin.package.json");
3744        write_json(&spec_path, &spec).unwrap();
3745        let output_dir = root.path().join("packages/marlin");
3746
3747        let error = package_native_operator(&NativeOperatorPackageRequest {
3748            spec_path,
3749            source_root: source_root.clone(),
3750            license_root: source_root,
3751            source_build_receipt_path: source_build_receipt,
3752            source_build_plan_path: source_build_plan,
3753            g03_catalog_path: catalog_path,
3754            abi_contract_path: abi_path,
3755            output_dir: output_dir.clone(),
3756            cc: PathBuf::from("/usr/bin/cc"),
3757            ar: PathBuf::from("/usr/bin/ar"),
3758        })
3759        .unwrap_err();
3760
3761        assert!(error.to_string().contains("terminal PASS"));
3762        assert!(!output_dir.exists());
3763    }
3764
3765    #[test]
3766    fn rejects_source_build_for_another_operator_without_publishing_output() {
3767        let root = tempfile::tempdir().unwrap();
3768        let (source_root, catalog_path, abi_path) = fixture_files(root.path());
3769        let exports = CudaNativeBuildUnit::Marlin.required_exports();
3770        let (source_build_receipt, source_build_plan, _) = run_source_build_fixture(
3771            root.path(),
3772            &source_root,
3773            CudaNativeBuildUnit::VllmMarlin.artifact_operator(),
3774            "wrong-operator",
3775            exports,
3776        );
3777        let spec = package_spec(
3778            CudaNativeBuildUnit::Marlin.artifact_operator(),
3779            "operation.dense_linear",
3780            "provider.cuda.dense_linear.f16.marlin",
3781            exports,
3782        );
3783        let spec_path = root.path().join("marlin.package.json");
3784        write_json(&spec_path, &spec).unwrap();
3785        let output_dir = root.path().join("packages/marlin");
3786
3787        let error = package_native_operator(&NativeOperatorPackageRequest {
3788            spec_path,
3789            source_root: source_root.clone(),
3790            license_root: source_root,
3791            source_build_receipt_path: source_build_receipt,
3792            source_build_plan_path: source_build_plan,
3793            g03_catalog_path: catalog_path,
3794            abi_contract_path: abi_path,
3795            output_dir: output_dir.clone(),
3796            cc: PathBuf::from("/usr/bin/cc"),
3797            ar: PathBuf::from("/usr/bin/ar"),
3798        })
3799        .unwrap_err();
3800
3801        assert!(error.to_string().contains("operator differs"));
3802        assert!(!output_dir.exists());
3803    }
3804
3805    #[test]
3806    fn rejects_archive_tampered_after_source_build_without_publishing_output() {
3807        let root = tempfile::tempdir().unwrap();
3808        let (source_root, catalog_path, abi_path) = fixture_files(root.path());
3809        let exports = CudaNativeBuildUnit::Marlin.required_exports();
3810        let (source_build_receipt, source_build_plan, archive) = run_source_build_fixture(
3811            root.path(),
3812            &source_root,
3813            CudaNativeBuildUnit::Marlin.artifact_operator(),
3814            "tampered",
3815            exports,
3816        );
3817        fs::write(&archive, b"tampered after source build\n").unwrap();
3818        let spec = package_spec(
3819            CudaNativeBuildUnit::Marlin.artifact_operator(),
3820            "operation.dense_linear",
3821            "provider.cuda.dense_linear.f16.marlin",
3822            exports,
3823        );
3824        let spec_path = root.path().join("marlin.package.json");
3825        write_json(&spec_path, &spec).unwrap();
3826        let output_dir = root.path().join("packages/marlin");
3827
3828        let error = package_native_operator(&NativeOperatorPackageRequest {
3829            spec_path,
3830            source_root: source_root.clone(),
3831            license_root: source_root,
3832            source_build_receipt_path: source_build_receipt,
3833            source_build_plan_path: source_build_plan,
3834            g03_catalog_path: catalog_path,
3835            abi_contract_path: abi_path,
3836            output_dir: output_dir.clone(),
3837            cc: PathBuf::from("/usr/bin/cc"),
3838            ar: PathBuf::from("/usr/bin/ar"),
3839        })
3840        .unwrap_err();
3841
3842        assert!(error.to_string().contains("archive sha256 differs"));
3843        assert!(!output_dir.exists());
3844    }
3845
3846    #[test]
3847    fn rejects_archiver_success_without_a_final_descriptor_member() {
3848        let root = tempfile::tempdir().unwrap();
3849        let (source_root, catalog_path, abi_path) = fixture_files(root.path());
3850        let exports = CudaNativeBuildUnit::Marlin.required_exports();
3851        let (source_build_receipt, source_build_plan, _) = run_source_build_fixture(
3852            root.path(),
3853            &source_root,
3854            CudaNativeBuildUnit::Marlin.artifact_operator(),
3855            "false-archiver-success",
3856            exports,
3857        );
3858        let spec = package_spec(
3859            CudaNativeBuildUnit::Marlin.artifact_operator(),
3860            "operation.dense_linear",
3861            "provider.cuda.dense_linear.f16.marlin",
3862            exports,
3863        );
3864        let spec_path = root.path().join("false-archiver-success.package.json");
3865        write_json(&spec_path, &spec).unwrap();
3866        let output_dir = root.path().join("packages/false-archiver-success");
3867        let false_success_ar = root.path().join("false-success-ar");
3868        write_executable_script(
3869            &false_success_ar,
3870            "#!/bin/sh\n\
3871             if [ \"$1\" = \"rcs\" ] && [ \"$3\" = \"descriptor.o\" ]; then exit 0; fi\n\
3872             exec /usr/bin/ar \"$@\"\n",
3873        );
3874
3875        let error = package_native_operator(&NativeOperatorPackageRequest {
3876            spec_path,
3877            source_root: source_root.clone(),
3878            license_root: source_root,
3879            source_build_receipt_path: source_build_receipt,
3880            source_build_plan_path: source_build_plan,
3881            g03_catalog_path: catalog_path,
3882            abi_contract_path: abi_path,
3883            output_dir: output_dir.clone(),
3884            cc: PathBuf::from("/usr/bin/cc"),
3885            ar: false_success_ar,
3886        })
3887        .unwrap_err();
3888
3889        assert!(error
3890            .to_string()
3891            .contains("native archive members differ from expected evidence"));
3892        assert!(!output_dir.exists());
3893    }
3894
3895    #[test]
3896    fn rejects_source_archive_member_hash_mismatch_with_updated_archive_pin() {
3897        let root = tempfile::tempdir().unwrap();
3898        let (source_root, catalog_path, abi_path) = fixture_files(root.path());
3899        let exports = CudaNativeBuildUnit::Marlin.required_exports();
3900        let (source_build_receipt, source_build_plan, archive) = run_source_build_fixture(
3901            root.path(),
3902            &source_root,
3903            CudaNativeBuildUnit::Marlin.artifact_operator(),
3904            "forged-member",
3905            exports,
3906        );
3907        let mut source_build: NativeOperatorSourceBuildReceipt =
3908            read_json(&source_build_receipt).unwrap();
3909        let object_path = PathBuf::from(
3910            source_build.commands[0]
3911                .object_file
3912                .as_deref()
3913                .expect("fixture object path"),
3914        );
3915        let replacement_source = root.path().join("replacement.c");
3916        fs::write(
3917            &replacement_source,
3918            "int marlin_cuda(void) { return 91; }\nint marlin_cuda_moe(void) { return 92; }\n",
3919        )
3920        .unwrap();
3921        assert!(Command::new("/usr/bin/cc")
3922            .args(["-c"])
3923            .arg(&replacement_source)
3924            .arg("-o")
3925            .arg(&object_path)
3926            .status()
3927            .unwrap()
3928            .success());
3929        fs::remove_file(&archive).unwrap();
3930        assert!(Command::new("/usr/bin/ar")
3931            .arg("rcs")
3932            .arg(&archive)
3933            .arg(&object_path)
3934            .status()
3935            .unwrap()
3936            .success());
3937        source_build.archive_sha256 = Some(sha256_file(&archive).unwrap());
3938        write_json(&source_build_receipt, &source_build).unwrap();
3939
3940        let spec = package_spec(
3941            CudaNativeBuildUnit::Marlin.artifact_operator(),
3942            "operation.dense_linear",
3943            "provider.cuda.dense_linear.f16.marlin",
3944            exports,
3945        );
3946        let spec_path = root.path().join("marlin.package.json");
3947        write_json(&spec_path, &spec).unwrap();
3948        let output_dir = root.path().join("packages/marlin");
3949
3950        let error = package_native_operator(&NativeOperatorPackageRequest {
3951            spec_path,
3952            source_root: source_root.clone(),
3953            license_root: source_root,
3954            source_build_receipt_path: source_build_receipt,
3955            source_build_plan_path: source_build_plan,
3956            g03_catalog_path: catalog_path,
3957            abi_contract_path: abi_path,
3958            output_dir: output_dir.clone(),
3959            cc: PathBuf::from("/usr/bin/cc"),
3960            ar: PathBuf::from("/usr/bin/ar"),
3961        })
3962        .unwrap_err();
3963
3964        assert!(error
3965            .to_string()
3966            .contains("native archive member evidence mismatch"));
3967        assert!(!output_dir.exists());
3968    }
3969
3970    #[test]
3971    fn rejects_archive_missing_a_build_unit_export_without_publishing_output() {
3972        let root = tempfile::tempdir().unwrap();
3973        let (source_root, catalog_path, abi_path) = fixture_files(root.path());
3974        let (source_build_receipt, source_build_plan, _) = run_source_build_fixture(
3975            root.path(),
3976            &source_root,
3977            CudaNativeBuildUnit::Marlin.artifact_operator(),
3978            "marlin-incomplete",
3979            &["marlin_cuda"],
3980        );
3981        let spec = package_spec(
3982            CudaNativeBuildUnit::Marlin.artifact_operator(),
3983            "operation.dense_linear",
3984            "provider.cuda.dense_linear.f16.marlin",
3985            &["marlin_cuda", "marlin_cuda_moe"],
3986        );
3987        let spec_path = root.path().join("marlin.package.json");
3988        write_json(&spec_path, &spec).unwrap();
3989        let output_dir = root.path().join("packages/marlin");
3990
3991        let error = package_native_operator(&NativeOperatorPackageRequest {
3992            spec_path,
3993            source_root: source_root.clone(),
3994            license_root: source_root,
3995            source_build_receipt_path: source_build_receipt,
3996            source_build_plan_path: source_build_plan,
3997            g03_catalog_path: catalog_path,
3998            abi_contract_path: abi_path,
3999            output_dir: output_dir.clone(),
4000            cc: PathBuf::from("/usr/bin/cc"),
4001            ar: PathBuf::from("/usr/bin/ar"),
4002        })
4003        .unwrap_err();
4004
4005        assert!(matches!(
4006            error,
4007            NativeOperatorBuilderError::Resolve(
4008                ferrum_native_ops::NativeOperatorResolveError::ArtifactMissingExports { .. }
4009            )
4010        ));
4011        assert!(!output_dir.exists());
4012    }
4013
4014    #[test]
4015    fn identical_inputs_produce_identical_archive_and_manifest_hashes() {
4016        let root = tempfile::tempdir().unwrap();
4017        let (source_root, catalog_path, abi_path) = fixture_files(root.path());
4018        let exports = ["marlin_cuda", "marlin_cuda_moe"];
4019        let (source_build_receipt, source_build_plan, _) = run_source_build_fixture(
4020            root.path(),
4021            &source_root,
4022            CudaNativeBuildUnit::Marlin.artifact_operator(),
4023            "marlin-deterministic",
4024            &exports,
4025        );
4026        let spec = package_spec(
4027            CudaNativeBuildUnit::Marlin.artifact_operator(),
4028            "operation.dense_linear",
4029            "provider.cuda.dense_linear.f16.marlin",
4030            &exports,
4031        );
4032        let spec_path = root.path().join("marlin.package.json");
4033        write_json(&spec_path, &spec).unwrap();
4034        let package = |name: &str| {
4035            package_native_operator(&NativeOperatorPackageRequest {
4036                spec_path: spec_path.clone(),
4037                source_root: source_root.clone(),
4038                license_root: source_root.clone(),
4039                source_build_receipt_path: source_build_receipt.clone(),
4040                source_build_plan_path: source_build_plan.clone(),
4041                g03_catalog_path: catalog_path.clone(),
4042                abi_contract_path: abi_path.clone(),
4043                output_dir: root.path().join("packages").join(name),
4044                cc: PathBuf::from("/usr/bin/cc"),
4045                ar: PathBuf::from("/usr/bin/ar"),
4046            })
4047            .unwrap()
4048        };
4049
4050        let first = package("first");
4051        let second = package("second");
4052
4053        assert_eq!(first.binary_sha256, second.binary_sha256);
4054        assert_eq!(first.manifest_sha256, second.manifest_sha256);
4055        assert_eq!(first.descriptor_export, second.descriptor_export);
4056    }
4057
4058    #[test]
4059    fn artifact_set_rejects_semantically_forged_source_receipt_with_updated_file_pin() {
4060        let root = tempfile::tempdir().unwrap();
4061        let exports = CudaNativeBuildUnit::Marlin.required_exports();
4062        let (package_dir, _) = package_fixture(
4063            root.path(),
4064            CudaNativeBuildUnit::Marlin.artifact_operator(),
4065            "operation.dense_linear",
4066            "provider.cuda.dense_linear.f16.marlin",
4067            exports,
4068            "semantic-forgery",
4069        )
4070        .unwrap();
4071        let package_receipt_path = package_dir.join("package.receipt.json");
4072        let external_receipt_sha256 = sha256_file(&package_receipt_path).unwrap();
4073        let mut package_receipt: NativeOperatorPackageReceipt =
4074            read_json(&package_receipt_path).unwrap();
4075        let source_receipt_path = package_dir.join(&package_receipt.source_build_receipt.path);
4076        let mut source_receipt: NativeOperatorSourceBuildReceipt =
4077            read_json(&source_receipt_path).unwrap();
4078        source_receipt.builder_sha = "8".repeat(40);
4079        write_json(&source_receipt_path, &source_receipt).unwrap();
4080        package_receipt.source_build_receipt.sha256 = sha256_file(&source_receipt_path).unwrap();
4081        package_receipt.source_build_receipt.size_bytes =
4082            fs::metadata(&source_receipt_path).unwrap().len();
4083        write_json(&package_receipt_path, &package_receipt).unwrap();
4084        let forged_receipt_sha256 = sha256_file(&package_receipt_path).unwrap();
4085        let anchor_lock_path = root.path().join("packages/anchor-reject.lock.json");
4086        let anchor_error = assemble_native_operator_set(&NativeOperatorSetRequest {
4087            receipt_paths: vec![package_receipt_path.clone()],
4088            expected_receipt_sha256: vec![external_receipt_sha256],
4089            expected_g03_catalog_sha256: package_receipt.g03_catalog_sha256.clone(),
4090            output_lock_path: anchor_lock_path.clone(),
4091            compute_capability: "sm_89".to_string(),
4092        })
4093        .unwrap_err();
4094        assert!(anchor_error
4095            .to_string()
4096            .contains("differs from its external sha256 pin"));
4097        assert!(!anchor_lock_path.exists());
4098        let lock_path = root.path().join("packages/native-operators.lock.json");
4099
4100        let error = assemble_native_operator_set(&NativeOperatorSetRequest {
4101            receipt_paths: vec![package_receipt_path],
4102            expected_receipt_sha256: vec![forged_receipt_sha256],
4103            expected_g03_catalog_sha256: package_receipt.g03_catalog_sha256.clone(),
4104            output_lock_path: lock_path.clone(),
4105            compute_capability: "sm_89".to_string(),
4106        })
4107        .unwrap_err();
4108
4109        assert!(error
4110            .to_string()
4111            .contains("manifest is not the exact semantic projection"));
4112        assert!(!lock_path.exists());
4113    }
4114
4115    #[test]
4116    fn artifact_set_rejects_tampered_toolkit_manifest_with_updated_outer_pin() {
4117        let root = tempfile::tempdir().unwrap();
4118        let exports = CudaNativeBuildUnit::Marlin.required_exports();
4119        let (package_dir, mut receipt) = package_fixture(
4120            root.path(),
4121            CudaNativeBuildUnit::Marlin.artifact_operator(),
4122            "operation.dense_linear",
4123            "provider.cuda.dense_linear.f16.marlin",
4124            exports,
4125            "tampered-toolkit",
4126        )
4127        .unwrap();
4128        let evidence = receipt
4129            .source_build_inputs
4130            .iter_mut()
4131            .find(|evidence| evidence.path.ends_with("cuda-static-manifest.json"))
4132            .expect("package carries the cuda toolkit manifest");
4133        let manifest_path = package_dir.join(&evidence.path);
4134        let mut bytes = fs::read(&manifest_path).unwrap();
4135        bytes.extend_from_slice(b" \n");
4136        fs::write(&manifest_path, &bytes).unwrap();
4137        evidence.sha256 = sha256_bytes(&bytes);
4138        evidence.size_bytes = bytes.len().try_into().unwrap();
4139        let receipt_path = package_dir.join("package.receipt.json");
4140        write_json(&receipt_path, &receipt).unwrap();
4141        let lock_path = root.path().join("packages/tampered-toolkit.lock.json");
4142
4143        let error = assemble_native_operator_set(&NativeOperatorSetRequest {
4144            receipt_paths: vec![receipt_path.clone()],
4145            expected_receipt_sha256: vec![sha256_file(&receipt_path).unwrap()],
4146            expected_g03_catalog_sha256: receipt.g03_catalog_sha256,
4147            output_lock_path: lock_path.clone(),
4148            compute_capability: "sm_89".to_string(),
4149        })
4150        .unwrap_err();
4151
4152        assert!(error.to_string().contains("source-build evidence mismatch"));
4153        assert!(!lock_path.exists());
4154    }
4155
4156    #[test]
4157    fn artifact_set_reparses_depfile_after_coherent_outer_rehash() {
4158        let root = tempfile::tempdir().unwrap();
4159        let exports = CudaNativeBuildUnit::Marlin.required_exports();
4160        let (package_dir, mut receipt) = package_fixture(
4161            root.path(),
4162            CudaNativeBuildUnit::Marlin.artifact_operator(),
4163            "operation.dense_linear",
4164            "provider.cuda.dense_linear.f16.marlin",
4165            exports,
4166            "tampered-depfile",
4167        )
4168        .unwrap();
4169        let source_receipt_path = package_dir.join(&receipt.source_build_receipt.path);
4170        let mut source_receipt: NativeOperatorSourceBuildReceipt =
4171            read_json(&source_receipt_path).unwrap();
4172        let command = &mut source_receipt.commands[0];
4173        let depfile_relative = command
4174            .depfile
4175            .as_deref()
4176            .expect("compiled command carries a depfile")
4177            .to_string();
4178        let depfile_path = source_receipt_path
4179            .parent()
4180            .unwrap()
4181            .join(&depfile_relative);
4182        let producer_object = command
4183            .depfile_producer_object_file
4184            .as_deref()
4185            .expect("compiled command records the depfile producer object");
4186        let forged = format!("{producer_object}: fixture.cu forged.h\n");
4187        fs::write(&depfile_path, forged.as_bytes()).unwrap();
4188        command.depfile_sha256 = Some(sha256_bytes(forged.as_bytes()));
4189        write_json(&source_receipt_path, &source_receipt).unwrap();
4190
4191        receipt.source_build_receipt.sha256 = sha256_file(&source_receipt_path).unwrap();
4192        receipt.source_build_receipt.size_bytes = fs::metadata(&source_receipt_path).unwrap().len();
4193        let packaged_depfile = format!("provenance/{depfile_relative}");
4194        let depfile_evidence = receipt
4195            .source_build_inputs
4196            .iter_mut()
4197            .find(|evidence| evidence.path == packaged_depfile)
4198            .expect("package carries the portable depfile");
4199        depfile_evidence.sha256 = sha256_file(&depfile_path).unwrap();
4200        depfile_evidence.size_bytes = fs::metadata(&depfile_path).unwrap().len();
4201
4202        let receipt_path = package_dir.join("package.receipt.json");
4203        write_json(&receipt_path, &receipt).unwrap();
4204        let lock_path = root.path().join("packages/tampered-depfile.lock.json");
4205        let error = assemble_native_operator_set(&NativeOperatorSetRequest {
4206            receipt_paths: vec![receipt_path.clone()],
4207            expected_receipt_sha256: vec![sha256_file(&receipt_path).unwrap()],
4208            expected_g03_catalog_sha256: receipt.g03_catalog_sha256,
4209            output_lock_path: lock_path.clone(),
4210            compute_capability: "sm_89".to_string(),
4211        })
4212        .unwrap_err();
4213
4214        assert!(error
4215            .to_string()
4216            .contains("portable depfile bytes differ from their canonical typed bindings"));
4217        assert!(!lock_path.exists());
4218    }
4219
4220    #[test]
4221    fn artifact_set_rejects_replaced_final_member_with_updated_outer_pins() {
4222        let root = tempfile::tempdir().unwrap();
4223        let exports = CudaNativeBuildUnit::Marlin.required_exports();
4224        let (package_dir, _) = package_fixture(
4225            root.path(),
4226            CudaNativeBuildUnit::Marlin.artifact_operator(),
4227            "operation.dense_linear",
4228            "provider.cuda.dense_linear.f16.marlin",
4229            exports,
4230            "final-member-forgery",
4231        )
4232        .unwrap();
4233        let package_receipt_path = package_dir.join("package.receipt.json");
4234        let mut package_receipt: NativeOperatorPackageReceipt =
4235            read_json(&package_receipt_path).unwrap();
4236        let member_name = package_receipt.source_archive_members[0].member.clone();
4237        let replacement_source = root.path().join("replacement-final-member.c");
4238        let replacement_object = root.path().join(&member_name);
4239        fs::write(
4240            &replacement_source,
4241            "int marlin_cuda(void) { return 101; }\n\
4242             int marlin_cuda_moe(void) { return 102; }\n",
4243        )
4244        .unwrap();
4245        assert!(Command::new("/usr/bin/cc")
4246            .args(["-c"])
4247            .arg(&replacement_source)
4248            .arg("-o")
4249            .arg(&replacement_object)
4250            .status()
4251            .unwrap()
4252            .success());
4253        let artifact_path = package_dir.join(&package_receipt.artifact_file);
4254        assert!(Command::new("/usr/bin/ar")
4255            .arg("rcs")
4256            .arg(&artifact_path)
4257            .arg(&replacement_object)
4258            .status()
4259            .unwrap()
4260            .success());
4261        let forged_binary_sha256 = sha256_file(&artifact_path).unwrap();
4262        let manifest_path = package_dir.join(&package_receipt.manifest_file);
4263        let mut manifest: NativeOperatorManifest = read_json(&manifest_path).unwrap();
4264        manifest.binary_sha256 = forged_binary_sha256.clone();
4265        write_json(&manifest_path, &manifest).unwrap();
4266        package_receipt.binary_sha256 = forged_binary_sha256;
4267        package_receipt.manifest_sha256 = sha256_file(&manifest_path).unwrap();
4268        write_json(&package_receipt_path, &package_receipt).unwrap();
4269        let forged_receipt_sha256 = sha256_file(&package_receipt_path).unwrap();
4270        let lock_path = root.path().join("packages/native-operators.lock.json");
4271
4272        let error = assemble_native_operator_set(&NativeOperatorSetRequest {
4273            receipt_paths: vec![package_receipt_path],
4274            expected_receipt_sha256: vec![forged_receipt_sha256],
4275            expected_g03_catalog_sha256: package_receipt.g03_catalog_sha256.clone(),
4276            output_lock_path: lock_path.clone(),
4277            compute_capability: "sm_89".to_string(),
4278        })
4279        .unwrap_err();
4280
4281        assert!(error
4282            .to_string()
4283            .contains("native archive member evidence mismatch"));
4284        assert!(!lock_path.exists());
4285    }
4286
4287    #[test]
4288    fn assembles_multiple_verified_packages_into_one_resolvable_lock() {
4289        let root = tempfile::tempdir().unwrap();
4290        let marlin_exports = CudaNativeBuildUnit::Marlin.required_exports();
4291        let (alpha_dir, alpha_receipt) = package_fixture(
4292            root.path(),
4293            CudaNativeBuildUnit::Marlin.artifact_operator(),
4294            "operation.dense_linear",
4295            "provider.cuda.dense_linear.f16.marlin",
4296            marlin_exports,
4297            "alpha",
4298        )
4299        .unwrap();
4300        let vllm_marlin_exports = CudaNativeBuildUnit::VllmMarlin.required_exports();
4301        let (beta_dir, beta_receipt) = package_fixture(
4302            root.path(),
4303            CudaNativeBuildUnit::VllmMarlin.artifact_operator(),
4304            "operation.quantized_linear",
4305            "provider.cuda.quantized_linear.gptq_marlin",
4306            vllm_marlin_exports,
4307            "beta",
4308        )
4309        .unwrap();
4310        let lock_path = root.path().join("packages/native-operators.lock.json");
4311        let alpha_receipt_path = alpha_dir.join("package.receipt.json");
4312        let beta_receipt_path = beta_dir.join("package.receipt.json");
4313
4314        let lock = assemble_native_operator_set(&NativeOperatorSetRequest {
4315            receipt_paths: vec![alpha_receipt_path.clone(), beta_receipt_path.clone()],
4316            expected_receipt_sha256: vec![
4317                sha256_file(&alpha_receipt_path).unwrap(),
4318                sha256_file(&beta_receipt_path).unwrap(),
4319            ],
4320            expected_g03_catalog_sha256: alpha_receipt.g03_catalog_sha256.clone(),
4321            output_lock_path: lock_path.clone(),
4322            compute_capability: "sm_89".to_string(),
4323        })
4324        .unwrap();
4325
4326        assert_eq!(
4327            alpha_receipt.g03_catalog_sha256,
4328            beta_receipt.g03_catalog_sha256
4329        );
4330        assert_eq!(lock.artifacts.len(), 2);
4331        let resolved =
4332            NativeOperatorArtifactSetLock::load_and_resolve(&lock_path, Some("sm_89")).unwrap();
4333        assert_eq!(resolved.artifacts.len(), 2);
4334        assert_eq!(
4335            resolved.artifacts[0].resolved.manifest.operator,
4336            CudaNativeBuildUnit::Marlin.artifact_operator()
4337        );
4338        assert_eq!(
4339            resolved.artifacts[1].resolved.manifest.operator,
4340            CudaNativeBuildUnit::VllmMarlin.artifact_operator()
4341        );
4342    }
4343}