hara_native/
native_link.rs1use hara_abi::NativeIdentity;
4use std::collections::BTreeMap;
5use std::path::{Path, PathBuf};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct NativeArtifact {
9 pub identity: NativeIdentity,
10 pub version: String,
11 pub archive_sha256: String,
12 pub package_root: PathBuf,
13 pub crate_path: PathBuf,
14}
15
16impl NativeArtifact {
17 pub fn verified(
18 identity: NativeIdentity,
19 version: impl Into<String>,
20 archive_sha256: impl Into<String>,
21 package_root: impl Into<PathBuf>,
22 crate_relative: impl AsRef<Path>,
23 ) -> Result<Self, String> {
24 let version = version.into();
25 let archive_sha256 = archive_sha256.into();
26 if version.is_empty() {
27 return Err("native artifact version must not be empty".into());
28 }
29 let digest = archive_sha256
30 .strip_prefix("sha256:")
31 .ok_or("native artifact digest must start with sha256:")?;
32 if digest.len() != 64 || !digest.chars().all(|value| value.is_ascii_hexdigit()) {
33 return Err("native artifact digest must contain 64 hexadecimal characters".into());
34 }
35 let package_root = package_root.into();
36 let relative = crate_relative.as_ref();
37 if relative.is_absolute()
38 || relative
39 .components()
40 .any(|component| matches!(component, std::path::Component::ParentDir))
41 {
42 return Err("native crate path must remain beneath the package root".into());
43 }
44 let crate_path = package_root.join(relative);
45 if !crate_path.join("Cargo.toml").is_file() {
46 return Err(format!(
47 "native crate has no Cargo.toml: {}",
48 crate_path.display()
49 ));
50 }
51 Ok(Self {
52 identity,
53 version,
54 archive_sha256,
55 package_root,
56 crate_path,
57 })
58 }
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct LinkPlan {
63 pub artifacts: Vec<NativeArtifact>,
64}
65
66impl LinkPlan {
67 pub fn new(mut artifacts: Vec<NativeArtifact>) -> Result<Self, String> {
68 artifacts.sort_by(|left, right| left.identity.cmp(&right.identity));
69 for pair in artifacts.windows(2) {
70 if pair[0].identity == pair[1].identity {
71 return Err(format!(
72 "duplicate native adapter {} {}",
73 pair[0].identity.package, pair[0].identity.export
74 ));
75 }
76 }
77 Ok(Self { artifacts })
78 }
79
80 pub fn cargo_dependencies(&self) -> String {
81 let mut dependencies = BTreeMap::new();
82 for artifact in &self.artifacts {
83 dependencies.insert(
84 artifact.identity.crate_name.as_str(),
85 artifact.crate_path.to_string_lossy(),
86 );
87 }
88 let mut output = String::from("[dependencies]\n");
89 for (crate_name, path) in dependencies {
90 output.push_str(&format!(
91 "{crate_name} = {{ path = {:?} }}\n",
92 toml_string(&path)
93 ));
94 }
95 output
96 }
97
98 pub fn registration_source(&self, runtime_expression: &str) -> String {
100 let mut output = String::new();
101 for artifact in &self.artifacts {
102 let crate_name = artifact.identity.crate_name.replace('-', "_");
103 output.push_str(&format!(
104 "{runtime_expression}.install_native_module({crate_name}::module());\n"
105 ));
106 }
107 output
108 }
109}
110
111fn toml_string(value: &str) -> String {
112 value.replace('\\', "\\\\").replace('"', "\\\"")
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use std::fs;
119 use std::time::{SystemTime, UNIX_EPOCH};
120
121 #[test]
122 fn verified_artifacts_generate_deterministic_cargo_dependencies() {
123 let root = std::env::temp_dir().join(format!(
124 "hara-native-link-{}",
125 SystemTime::now()
126 .duration_since(UNIX_EPOCH)
127 .unwrap()
128 .as_nanos()
129 ));
130 fs::create_dir_all(root.join("crate")).unwrap();
131 fs::write(
132 root.join("crate/Cargo.toml"),
133 "[package]\nname='adapter'\nversion='0.1.0'\n",
134 )
135 .unwrap();
136 let identity = NativeIdentity::new(
137 "gh:example:adapter",
138 "service/store",
139 "adapter",
140 "service/1",
141 )
142 .unwrap();
143 let artifact = NativeArtifact::verified(
144 identity,
145 "0.1.0",
146 format!("sha256:{}", "a".repeat(64)),
147 &root,
148 "crate",
149 )
150 .unwrap();
151 let plan = LinkPlan::new(vec![artifact]).unwrap();
152 assert!(plan.cargo_dependencies().contains("adapter = { path ="));
153 assert_eq!(
154 plan.registration_source("runtime"),
155 "runtime.install_native_module(adapter::module());\n"
156 );
157 fs::remove_dir_all(root).unwrap();
158 }
159}