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