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