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.\
24885762babdde73807bdacfe1348890a170e1a002be7c7f5ad1fd66df7e90190e";
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 !contract_version_satisfies(
101 live.operation_contract_version,
102 binding.operation_contract_version,
103 ) || !contract_version_satisfies(live.provider_version, binding.provider_version)
104 {
105 return Err(format!(
106 "compiled native operator {} binding {}/{} is incompatible with the live versioned contract",
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 contract_version_satisfies(
134 available: ferrum_types::NativeOperatorContractVersion,
135 required: ferrum_types::NativeOperatorContractVersion,
136) -> bool {
137 available.major == required.major && available.minor >= required.minor
138}
139
140fn is_lowercase_sha256(value: &str) -> bool {
141 value.len() == 64
142 && value
143 .bytes()
144 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct CompiledFa2NativeOperatorArtifact {
149 pub manifest_path: String,
150 pub artifact_path: String,
151 pub source_package_sha256: String,
152 pub inputs_sha256: String,
153 pub binary_sha256: String,
154}
155
156pub fn compiled_fa2_native_operator_artifact_linked() -> bool {
157 compiled_fa2_native_operator_artifact().is_some()
158}
159
160pub fn compiled_fa2_native_operator_artifact_state() -> &'static str {
161 option_env!("FERRUM_FA2_NATIVE_ARTIFACT_COMPILE").unwrap_or("not_configured")
162}
163
164pub fn compiled_fa2_native_operator_artifact() -> Option<CompiledFa2NativeOperatorArtifact> {
165 Some(CompiledFa2NativeOperatorArtifact {
166 manifest_path: option_env!("FERRUM_COMPILED_FA2_NATIVE_MANIFEST")?.to_string(),
167 artifact_path: option_env!("FERRUM_COMPILED_FA2_NATIVE_ARTIFACT")?.to_string(),
168 source_package_sha256: option_env!("FERRUM_COMPILED_FA2_NATIVE_SOURCE_SHA256")?.to_string(),
169 inputs_sha256: option_env!("FERRUM_COMPILED_FA2_NATIVE_INPUTS_SHA256")?.to_string(),
170 binary_sha256: option_env!("FERRUM_COMPILED_FA2_NATIVE_BINARY_SHA256")?.to_string(),
171 })
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct NativeOperatorArtifactSpec {
176 pub operator: String,
177 pub backend: NativeOperatorBackend,
178 pub compute_capability: Option<String>,
179 pub manifest_path: PathBuf,
180 pub artifact_path: PathBuf,
181 pub source_package_sha256: Option<String>,
182 pub inputs_sha256: Option<String>,
183 pub binary_sha256: Option<String>,
184}
185
186impl NativeOperatorArtifactSpec {
187 pub fn cuda_fa2(
188 manifest_path: impl Into<PathBuf>,
189 artifact_path: impl Into<PathBuf>,
190 compute_capability: impl Into<String>,
191 ) -> Self {
192 Self {
193 operator: FA2_NATIVE_OPERATOR.to_string(),
194 backend: NativeOperatorBackend::Cuda,
195 compute_capability: Some(compute_capability.into()),
196 manifest_path: manifest_path.into(),
197 artifact_path: artifact_path.into(),
198 source_package_sha256: None,
199 inputs_sha256: None,
200 binary_sha256: None,
201 }
202 }
203
204 pub fn with_source_package_sha256(mut self, sha256: impl Into<String>) -> Self {
205 self.source_package_sha256 = Some(sha256.into());
206 self
207 }
208
209 pub fn with_inputs_sha256(mut self, sha256: impl Into<String>) -> Self {
210 self.inputs_sha256 = Some(sha256.into());
211 self
212 }
213
214 pub fn with_binary_sha256(mut self, sha256: impl Into<String>) -> Self {
215 self.binary_sha256 = Some(sha256.into());
216 self
217 }
218}
219
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct NativeOperatorRuntimeSelection {
222 pub schema_version: u32,
223 pub operator: String,
224 pub operator_abi_version: String,
225 pub ferrum_native_abi_version: String,
226 pub backend: NativeOperatorBackend,
227 pub compute_capability: Option<String>,
228 pub linkage: NativeOperatorLinkage,
229 pub manifest_path: PathBuf,
230 pub artifact_path: PathBuf,
231 pub binary_sha256: String,
232 pub source_package_sha256: String,
233 pub inputs_sha256: String,
234 pub g03_catalog_sha256: Option<String>,
235 pub abi_contract_sha256: Option<String>,
236 pub descriptor_export: Option<String>,
237 pub operation_bindings: Vec<NativeOperatorBinding>,
238 pub artifact_format: NativeOperatorArtifactFormat,
239 pub archive_members: Vec<String>,
240 pub required_exports: Vec<String>,
241 pub matched_exports: Vec<String>,
242}
243
244pub fn resolve_native_operator_artifact(
245 spec: &NativeOperatorArtifactSpec,
246) -> Result<NativeOperatorRuntimeSelection, NativeOperatorResolveError> {
247 let mut request = NativeOperatorResolveRequest::new(
248 spec.operator.clone(),
249 spec.backend,
250 spec.manifest_path.clone(),
251 spec.artifact_path.clone(),
252 );
253 if let Some(compute_capability) = spec.compute_capability.clone() {
254 request = request.with_compute_capability(compute_capability);
255 }
256
257 let resolved = NativeOperatorResolver.resolve(&request)?;
258 let mut requirement = NativeOperatorRequirement {
259 operator: spec.operator.clone(),
260 backend: spec.backend,
261 operator_abi_version: resolved.manifest.operator_abi_version.clone(),
262 ferrum_native_abi_version: resolved.manifest.ferrum_native_abi_version.clone(),
263 compute_capability: spec.compute_capability.clone(),
264 source_package_sha256: spec.source_package_sha256.clone(),
265 inputs_sha256: spec.inputs_sha256.clone(),
266 binary_sha256: spec
267 .binary_sha256
268 .clone()
269 .or_else(|| Some(resolved.artifact_sha256.clone())),
270 g03_catalog_sha256: resolved.manifest.g03_catalog_sha256.clone(),
271 abi_contract_sha256: resolved.manifest.abi_contract_sha256.clone(),
272 descriptor_export: resolved.manifest.descriptor_export.clone(),
273 required_exports: resolved.manifest.exports.clone(),
274 operation_bindings: Some(resolved.manifest.operation_bindings.clone()),
275 };
276 if requirement.source_package_sha256.is_none() {
277 requirement.source_package_sha256 = Some(resolved.manifest.source_package.sha256.clone());
278 }
279 if requirement.inputs_sha256.is_none() {
280 requirement.inputs_sha256 = Some(resolved.manifest.inputs_sha256.clone());
281 }
282 resolve_native_operator_manifest(Some(&resolved.manifest), &requirement)
283 .map_err(NativeOperatorResolveError::ManifestInvalid)?;
284
285 Ok(NativeOperatorRuntimeSelection {
286 schema_version: resolved.manifest.schema_version,
287 operator: resolved.manifest.operator.clone(),
288 operator_abi_version: resolved.manifest.operator_abi_version.clone(),
289 ferrum_native_abi_version: resolved.manifest.ferrum_native_abi_version.clone(),
290 backend: resolved.manifest.backend,
291 compute_capability: spec.compute_capability.clone(),
292 linkage: resolved.manifest.linkage,
293 manifest_path: resolved.manifest_path,
294 artifact_path: resolved.artifact_path,
295 binary_sha256: resolved.artifact_sha256,
296 source_package_sha256: resolved.manifest.source_package.sha256,
297 inputs_sha256: resolved.manifest.inputs_sha256,
298 g03_catalog_sha256: resolved.manifest.g03_catalog_sha256,
299 abi_contract_sha256: resolved.manifest.abi_contract_sha256,
300 descriptor_export: resolved.manifest.descriptor_export,
301 operation_bindings: resolved.manifest.operation_bindings,
302 artifact_format: resolved.binary_validation.format,
303 archive_members: resolved.binary_validation.archive_members,
304 required_exports: resolved.binary_validation.required_exports,
305 matched_exports: resolved.binary_validation.matched_exports,
306 })
307}
308
309pub fn resolve_cuda_fa2_native_operator(
310 spec: &NativeOperatorArtifactSpec,
311) -> Result<NativeOperatorRuntimeSelection, NativeOperatorResolveError> {
312 if spec.operator != FA2_NATIVE_OPERATOR || spec.backend != NativeOperatorBackend::Cuda {
313 return Err(NativeOperatorResolveError::ManifestInvalid(
314 "FA2 native operator selection requires operator=fa2 backend=cuda".to_string(),
315 ));
316 }
317 resolve_native_operator_artifact(spec)
318}