1use std::path::PathBuf;
8use std::sync::OnceLock;
9
10use ferrum_native_ops::{
11 NativeOperatorArtifactFormat, NativeOperatorResolveError, NativeOperatorResolveRequest,
12 NativeOperatorResolver,
13};
14use ferrum_types::{
15 resolve_native_operator_manifest, NativeOperatorBackend, NativeOperatorBinding,
16 NativeOperatorLinkage, NativeOperatorProviderCatalog, NativeOperatorRequirement,
17 NATIVE_OPERATOR_MANIFEST_SCHEMA_VERSION,
18};
19
20pub use ferrum_types::CompiledNativeOperatorIdentity as CompiledNativeOperatorArtifact;
21
22pub const FA2_NATIVE_OPERATOR: &str = "fa2";
23pub const CUDA_NATIVE_SOURCE_BUNDLE_ID: &str = "ferrum-native-cuda-v1+sha256.\
247f6f35f91a85df6ea5374d5597f7f8ca4c159b5e567c1c1ef122a0ab88657613";
25
26pub fn compiled_native_operator_artifacts() -> &'static [CompiledNativeOperatorArtifact] {
27 static COMPILED: OnceLock<Vec<CompiledNativeOperatorArtifact>> = OnceLock::new();
28 COMPILED
29 .get_or_init(|| {
30 serde_json::from_str(
31 option_env!("FERRUM_COMPILED_NATIVE_OPERATOR_SET_JSON").unwrap_or("[]"),
32 )
33 .expect("build.rs emitted invalid native operator inventory JSON")
34 })
35 .as_slice()
36}
37
38pub fn validate_compiled_native_operator_provider_catalog(
39 catalog: &NativeOperatorProviderCatalog,
40 artifacts: &[CompiledNativeOperatorArtifact],
41) -> Result<(), String> {
42 catalog.validate()?;
43 if artifacts.is_empty() {
44 return Ok(());
45 }
46 let catalog_sha256 = catalog.canonical_sha256()?;
47 let mut binding_count = 0_usize;
48 let mut provenance_change_count = 0_usize;
49 let mut first_provenance_sha256 = None;
50 for artifact in artifacts {
51 if artifact.schema_version != NATIVE_OPERATOR_MANIFEST_SCHEMA_VERSION {
52 return Err(format!(
53 "compiled native operator {} uses schema {}, expected {}",
54 artifact.operator, artifact.schema_version, NATIVE_OPERATOR_MANIFEST_SCHEMA_VERSION
55 ));
56 }
57 if artifact.backend != catalog.backend {
58 return Err(format!(
59 "compiled native operator {} backend {:?} differs from live catalog {:?}",
60 artifact.operator, artifact.backend, catalog.backend
61 ));
62 }
63 let provenance_sha256 = artifact.g03_catalog_sha256.as_deref().ok_or_else(|| {
64 format!(
65 "compiled native operator {} has no provider catalog provenance",
66 artifact.operator
67 )
68 })?;
69 if !is_lowercase_sha256(provenance_sha256) {
70 return Err(format!(
71 "compiled native operator {} has invalid provider catalog provenance",
72 artifact.operator
73 ));
74 }
75 if provenance_sha256 != catalog_sha256 {
78 provenance_change_count = provenance_change_count.checked_add(1).ok_or_else(|| {
79 "compiled native operator provenance change count overflows usize".to_string()
80 })?;
81 first_provenance_sha256.get_or_insert(provenance_sha256);
82 }
83 for binding in &artifact.operation_bindings {
84 binding_count = binding_count.checked_add(1).ok_or_else(|| {
85 "compiled native operator binding count overflows usize".to_string()
86 })?;
87 let live = catalog
88 .providers
89 .iter()
90 .find(|provider| {
91 provider.operation_id == binding.operation_id
92 && provider.provider_id == binding.provider_id
93 })
94 .ok_or_else(|| {
95 format!(
96 "compiled native operator {} binds missing live provider {}/{}",
97 artifact.operator, binding.operation_id, binding.provider_id
98 )
99 })?;
100 if live.operation_contract_version != binding.operation_contract_version
101 || live.provider_version != binding.provider_version
102 || live.provider_implementation_fingerprint
103 != binding.provider_implementation_fingerprint
104 {
105 return Err(format!(
106 "compiled native operator {} binding {}/{} differs from the live provider identity",
107 artifact.operator, binding.operation_id, binding.provider_id
108 ));
109 }
110 }
111 }
112 if binding_count == 0 {
113 return Err(
114 "compiled native operator set does not bind any live G03 operation/provider"
115 .to_string(),
116 );
117 }
118 if provenance_change_count > 0 {
119 tracing::debug!(
120 target: "ferrum::native_ops",
121 event = "native_operator_catalog_provenance_changed",
122 artifact_count = artifacts.len(),
123 provenance_change_count,
124 validated_binding_count = binding_count,
125 artifact_catalog_sha256 = first_provenance_sha256.unwrap_or_default(),
126 live_catalog_sha256 = catalog_sha256,
127 "native operator catalog provenance changed; declared provider bindings remain compatible"
128 );
129 }
130 Ok(())
131}
132
133fn is_lowercase_sha256(value: &str) -> bool {
134 value.len() == 64
135 && value
136 .bytes()
137 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct CompiledFa2NativeOperatorArtifact {
142 pub manifest_path: String,
143 pub artifact_path: String,
144 pub source_package_sha256: String,
145 pub inputs_sha256: String,
146 pub binary_sha256: String,
147}
148
149pub fn compiled_fa2_native_operator_artifact_linked() -> bool {
150 compiled_fa2_native_operator_artifact().is_some()
151}
152
153pub fn compiled_fa2_native_operator_artifact_state() -> &'static str {
154 option_env!("FERRUM_FA2_NATIVE_ARTIFACT_COMPILE").unwrap_or("not_configured")
155}
156
157pub fn compiled_fa2_native_operator_artifact() -> Option<CompiledFa2NativeOperatorArtifact> {
158 Some(CompiledFa2NativeOperatorArtifact {
159 manifest_path: option_env!("FERRUM_COMPILED_FA2_NATIVE_MANIFEST")?.to_string(),
160 artifact_path: option_env!("FERRUM_COMPILED_FA2_NATIVE_ARTIFACT")?.to_string(),
161 source_package_sha256: option_env!("FERRUM_COMPILED_FA2_NATIVE_SOURCE_SHA256")?.to_string(),
162 inputs_sha256: option_env!("FERRUM_COMPILED_FA2_NATIVE_INPUTS_SHA256")?.to_string(),
163 binary_sha256: option_env!("FERRUM_COMPILED_FA2_NATIVE_BINARY_SHA256")?.to_string(),
164 })
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct NativeOperatorArtifactSpec {
169 pub operator: String,
170 pub backend: NativeOperatorBackend,
171 pub compute_capability: Option<String>,
172 pub manifest_path: PathBuf,
173 pub artifact_path: PathBuf,
174 pub source_package_sha256: Option<String>,
175 pub inputs_sha256: Option<String>,
176 pub binary_sha256: Option<String>,
177}
178
179impl NativeOperatorArtifactSpec {
180 pub fn cuda_fa2(
181 manifest_path: impl Into<PathBuf>,
182 artifact_path: impl Into<PathBuf>,
183 compute_capability: impl Into<String>,
184 ) -> Self {
185 Self {
186 operator: FA2_NATIVE_OPERATOR.to_string(),
187 backend: NativeOperatorBackend::Cuda,
188 compute_capability: Some(compute_capability.into()),
189 manifest_path: manifest_path.into(),
190 artifact_path: artifact_path.into(),
191 source_package_sha256: None,
192 inputs_sha256: None,
193 binary_sha256: None,
194 }
195 }
196
197 pub fn with_source_package_sha256(mut self, sha256: impl Into<String>) -> Self {
198 self.source_package_sha256 = Some(sha256.into());
199 self
200 }
201
202 pub fn with_inputs_sha256(mut self, sha256: impl Into<String>) -> Self {
203 self.inputs_sha256 = Some(sha256.into());
204 self
205 }
206
207 pub fn with_binary_sha256(mut self, sha256: impl Into<String>) -> Self {
208 self.binary_sha256 = Some(sha256.into());
209 self
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct NativeOperatorRuntimeSelection {
215 pub schema_version: u32,
216 pub operator: String,
217 pub operator_abi_version: String,
218 pub ferrum_native_abi_version: String,
219 pub backend: NativeOperatorBackend,
220 pub compute_capability: Option<String>,
221 pub linkage: NativeOperatorLinkage,
222 pub manifest_path: PathBuf,
223 pub artifact_path: PathBuf,
224 pub binary_sha256: String,
225 pub source_package_sha256: String,
226 pub inputs_sha256: String,
227 pub g03_catalog_sha256: Option<String>,
228 pub abi_contract_sha256: Option<String>,
229 pub descriptor_export: Option<String>,
230 pub operation_bindings: Vec<NativeOperatorBinding>,
231 pub artifact_format: NativeOperatorArtifactFormat,
232 pub archive_members: Vec<String>,
233 pub required_exports: Vec<String>,
234 pub matched_exports: Vec<String>,
235}
236
237pub fn resolve_native_operator_artifact(
238 spec: &NativeOperatorArtifactSpec,
239) -> Result<NativeOperatorRuntimeSelection, NativeOperatorResolveError> {
240 let mut request = NativeOperatorResolveRequest::new(
241 spec.operator.clone(),
242 spec.backend,
243 spec.manifest_path.clone(),
244 spec.artifact_path.clone(),
245 );
246 if let Some(compute_capability) = spec.compute_capability.clone() {
247 request = request.with_compute_capability(compute_capability);
248 }
249
250 let resolved = NativeOperatorResolver.resolve(&request)?;
251 let mut requirement = NativeOperatorRequirement {
252 operator: spec.operator.clone(),
253 backend: spec.backend,
254 operator_abi_version: resolved.manifest.operator_abi_version.clone(),
255 ferrum_native_abi_version: resolved.manifest.ferrum_native_abi_version.clone(),
256 compute_capability: spec.compute_capability.clone(),
257 source_package_sha256: spec.source_package_sha256.clone(),
258 inputs_sha256: spec.inputs_sha256.clone(),
259 binary_sha256: spec
260 .binary_sha256
261 .clone()
262 .or_else(|| Some(resolved.artifact_sha256.clone())),
263 g03_catalog_sha256: resolved.manifest.g03_catalog_sha256.clone(),
264 abi_contract_sha256: resolved.manifest.abi_contract_sha256.clone(),
265 descriptor_export: resolved.manifest.descriptor_export.clone(),
266 required_exports: resolved.manifest.exports.clone(),
267 operation_bindings: Some(resolved.manifest.operation_bindings.clone()),
268 };
269 if requirement.source_package_sha256.is_none() {
270 requirement.source_package_sha256 = Some(resolved.manifest.source_package.sha256.clone());
271 }
272 if requirement.inputs_sha256.is_none() {
273 requirement.inputs_sha256 = Some(resolved.manifest.inputs_sha256.clone());
274 }
275 resolve_native_operator_manifest(Some(&resolved.manifest), &requirement)
276 .map_err(NativeOperatorResolveError::ManifestInvalid)?;
277
278 Ok(NativeOperatorRuntimeSelection {
279 schema_version: resolved.manifest.schema_version,
280 operator: resolved.manifest.operator.clone(),
281 operator_abi_version: resolved.manifest.operator_abi_version.clone(),
282 ferrum_native_abi_version: resolved.manifest.ferrum_native_abi_version.clone(),
283 backend: resolved.manifest.backend,
284 compute_capability: spec.compute_capability.clone(),
285 linkage: resolved.manifest.linkage,
286 manifest_path: resolved.manifest_path,
287 artifact_path: resolved.artifact_path,
288 binary_sha256: resolved.artifact_sha256,
289 source_package_sha256: resolved.manifest.source_package.sha256,
290 inputs_sha256: resolved.manifest.inputs_sha256,
291 g03_catalog_sha256: resolved.manifest.g03_catalog_sha256,
292 abi_contract_sha256: resolved.manifest.abi_contract_sha256,
293 descriptor_export: resolved.manifest.descriptor_export,
294 operation_bindings: resolved.manifest.operation_bindings,
295 artifact_format: resolved.binary_validation.format,
296 archive_members: resolved.binary_validation.archive_members,
297 required_exports: resolved.binary_validation.required_exports,
298 matched_exports: resolved.binary_validation.matched_exports,
299 })
300}
301
302pub fn resolve_cuda_fa2_native_operator(
303 spec: &NativeOperatorArtifactSpec,
304) -> Result<NativeOperatorRuntimeSelection, NativeOperatorResolveError> {
305 if spec.operator != FA2_NATIVE_OPERATOR || spec.backend != NativeOperatorBackend::Cuda {
306 return Err(NativeOperatorResolveError::ManifestInvalid(
307 "FA2 native operator selection requires operator=fa2 backend=cuda".to_string(),
308 ));
309 }
310 resolve_native_operator_artifact(spec)
311}