Skip to main content

ferrum_native_ops_builder/
lib.rs

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