1
2use std::path::{Path, PathBuf};
3use std::io;
4use tokio::fs as tfs;
5use cargo_toml::Dependency;
6use std::sync::Arc;
7use crate::cache::{CacheManager, PyroductConfig, CacheError};
8use crate::artifacts::{ModuleSource, ModuleBinary, ModuleSpec};
9use crate::command::{run_command, format_syn_error, CommandError};
10use pyro_macro::module::generate_module_spec;
11use crate::cargo::ensure_cdylib;
12
13#[derive(Debug, thiserror::Error)]
14pub enum BuildError {
15 #[error("IO error — {context}: {error}")]
16 Io {
17 context: &'static str,
18 #[source]
19 error: std::io::Error,
20 },
21
22 #[error("Cargo error: {0}")]
23 Command(#[from] CommandError),
24
25 #[error("Manifest parse error: {0}")]
26 Manifest(String),
27
28 #[error("Documentation error: {0}")]
29 Documentation(String),
30
31 #[error("No build slot available: {0}")]
32 NoSlot(String),
33}
34
35impl From<std::io::Error> for BuildError {
36 fn from(e: std::io::Error) -> Self {
37 BuildError::Io {
38 context: "unexpected IO error",
39 error: e,
40 }
41 }
42}
43
44impl BuildError {
45 pub fn io(context: &'static str, error: std::io::Error) -> Self {
46 BuildError::Io { context, error }
47 }
48}
49
50pub struct Builder {
51 pub root: PathBuf,
52 pub target_dir: PathBuf,
53 pub pyroduct_dep: Dependency,
54 pub config: PyroductConfig,
55 pub build_slots: usize,
56 pub cache_manager: Arc<CacheManager>,
57}
58
59impl Builder {
60 pub async fn new(root: &Path, mut config: PyroductConfig, cache_manager: Arc<CacheManager>) -> Result<Self, CacheError> {
61 tfs::create_dir_all(root).await.map_err(|e| CacheError {
62 context: "Failed to create build root".to_string(),
63 error: e,
64 })?;
65
66
67 let pyroduct_dep = if let Some(dep) = &mut config.pyroduct {
68 crate::cache::resolve_dependency_path(dep, root);
69 dep.clone()
70 } else {
71 Dependency::Simple("*".to_string())
72 };
73
74 let target_dir = if let Some(target) = &config.target {
75 if target.is_relative() {
76 root.join(target)
77 } else {
78 target.clone()
79 }
80 } else {
81 root.join("target")
82 };
83
84 let build_slots = config.build_slots.unwrap_or(4).max(1);
85 tracing::info!(?root, "Setup Build directory");
86
87 let builder = Self {
88 root: root.to_path_buf(),
89 target_dir,
90 pyroduct_dep,
91 config,
92 build_slots,
93 cache_manager,
94 };
95
96 builder.init().await?;
97 Ok(builder)
98 }
99
100 pub async fn from_env(cache_manager: Arc<CacheManager>) -> Result<Self, CacheError> {
101 let root = std::env::var("PYRODUCT")
102 .map(PathBuf::from)
103 .unwrap_or_else(|_| {
104 let home = std::env::var("HOME")
105 .or_else(|_| std::env::var("USERPROFILE"))
106 .map(PathBuf::from)
107 .unwrap_or_else(|_| PathBuf::from("."));
108 home.join(".pyroduct")
109 });
110
111 let config_path = root.join("config.toml");
112 let content = tfs::read_to_string(&config_path)
113 .await
114 .map_err(|error| CacheError {
115 context: format!("Failed to read the configuration"),
116 error,
117 })?;
118 let config = toml::from_str::<PyroductConfig>(&content).map_err(|error| CacheError {
119 context: format!("Failed to parse the configuration"),
120 error: io::Error::new(io::ErrorKind::InvalidData, error),
121 })?;
122
123 Self::new(&root, config, cache_manager).await
124 }
125
126 fn build_base_dir(&self) -> &Path {
127 &self.root
128 }
129
130 async fn init(&self) -> Result<(), CacheError> {
131 let build_base = self.build_base_dir();
133 for i in 0..self.build_slots {
134 let slot_dir = build_base.join(i.to_string());
135 tfs::create_dir_all(&slot_dir)
136 .await
137 .map_err(|error| CacheError {
138 context: format!("Failed to create build slot dir {}", i),
139 error,
140 })?;
141 }
142
143 let cargo_dir = self.root.join(".cargo");
144 tfs::create_dir_all(&cargo_dir)
145 .await
146 .map_err(|error| CacheError {
147 context: "Failed to create .cargo dir".to_string(),
148 error,
149 })?;
150
151 tfs::write(
152 cargo_dir.join("config.toml"),
153 format!("[build]\ntarget-dir = \"{}\"", self.target_dir.display()),
154 )
155 .await
156 .map_err(|error| CacheError {
157 context: "Failed to write target config.toml".to_string(),
158 error,
159 })?;
160 Ok(())
161 }
162
163 #[cfg(feature = "compiler")]
164 pub async fn compile(&self, source: &ModuleSource) -> Result<ModuleBinary, BuildError> {
165 let hash = source.hash();
166
167 if let Ok(binary) = self.cache_manager.get_binary(&hash).await {
169 return Ok(binary);
170 }
171
172 let slot = BuildSlot::acquire_any(&self.build_base_dir(), self.build_slots).await?;
174 tracing::info!(slot = slot.index, hash = %hash, "Compiling in build slot");
175
176 let build_dir = &slot.dir;
177 let src_dir = build_dir.join("src");
178 tfs::create_dir_all(&src_dir)
179 .await
180 .map_err(|e| BuildError::io("create src dir", e))?;
181 tfs::write(src_dir.join("lib.rs"), &source.source)
182 .await
183 .map_err(|e| BuildError::io("write lib.rs", e))?;
184
185 let crate_name = format!("mod_slot{}", slot.index);
186 let basic_toml = format!(
187 r#"
188[package]
189name = "{crate_name}"
190version = "0.1.0"
191author = "anon"
192edition = "2024"
193
194[workspace]
195
196[lib]
197name = "mod_slot"
198
199[dependencies]
200"#
201 );
202
203 let mut manifest: cargo_toml::Manifest = toml::from_str(&basic_toml)
204 .map_err(|e| BuildError::Manifest(format!("Couldn't build base manifest: {}", e)))?;
205 let mut pyro_dep = self.pyroduct_dep.clone();
206 pyro_dep.detail_mut().features.push("module".to_string());
207 manifest
208 .dependencies
209 .insert("pyroduct".to_string(), pyro_dep);
210 for (dep_name, dep) in source.dependencies.dependencies.iter() {
211 manifest.dependencies.insert(dep_name.clone(), dep.clone());
212 }
213 for cap in source.dependencies.capabilities.iter() {
214 let path = self.cache_manager.interface_dir(&cap.author, &cap.package, &cap.version)
215 .to_string_lossy()
216 .into();
217 let dep = Dependency::Detailed(Box::new(cargo_toml::DependencyDetail {
218 path: Some(path),
219 ..Default::default()
220 }));
221 manifest.dependencies.insert(cap.package.clone(), dep);
222 }
223 manifest.lib = ensure_cdylib(manifest.lib.take());
224
225 let mut crates_io_patch = std::collections::BTreeMap::new();
227 crates_io_patch.insert("pyroduct".to_string(), self.pyroduct_dep.clone());
228 manifest.patch.insert("crates-io".to_string(), crates_io_patch);
229
230 let cargo_toml_content =
231 toml::to_string_pretty(&manifest).map_err(|e| BuildError::Manifest(e.to_string()))?;
232 tfs::write(build_dir.join("Cargo.toml"), &cargo_toml_content)
233 .await
234 .map_err(|e| BuildError::io("write Cargo.toml", e))?;
235
236 run_command(
237 build_dir,
238 &["build", "--release", "--target", "wasm32-unknown-unknown"],
239 true,
240 )
241 .await?;
242
243 let wasm_path = self
244 .target_dir
245 .join("wasm32-unknown-unknown")
246 .join("release")
247 .join("mod_slot.wasm");
248
249 let wasm = tfs::read(wasm_path)
250 .await
251 .map_err(|e| BuildError::io("read compiled wasm", e))?;
252
253 drop(slot);
254
255 let func = generate_module_spec(&source.source)
256 .map_err(|s| {
257 BuildError::Documentation(format_syn_error("Cannot generate docstring", s))
258 })?
259 .ok_or(BuildError::Documentation(
260 "Module main functions is missing".to_string(),
261 ))?;
262 let spec = ModuleSpec {
263 hash,
264 func,
265 capabilities: source.dependencies.capabilities.clone(),
266 };
267
268 let binary = ModuleBinary { wasm, spec };
269
270 let _ = self.cache_manager.write_artifacts(&source.clone().into()).await;
272 let _ = self.cache_manager.write_artifacts(&binary.clone().into()).await;
273
274 Ok(binary)
275 }
276}
277
278pub struct BuildSlot {
279 pub index: usize,
280 pub dir: PathBuf,
281 _lock_file: std::fs::File,
282}
283
284impl BuildSlot {
285 fn try_acquire(build_base: &Path, index: usize) -> io::Result<Option<Self>> {
286 use fs2::FileExt;
287
288 let slot_dir = build_base.join(index.to_string());
289 std::fs::create_dir_all(&slot_dir)?;
290
291 let lock_path = slot_dir.join(".lock");
292 let lock_file = std::fs::OpenOptions::new()
293 .create(true)
294 .write(true)
295 .truncate(false)
296 .open(&lock_path)?;
297
298 if lock_file.try_lock_exclusive().is_ok() {
299 Ok(Some(BuildSlot {
300 index,
301 dir: slot_dir,
302 _lock_file: lock_file,
303 }))
304 } else {
305 Ok(None)
306 }
307 }
308
309 async fn acquire_any(build_base: &Path, slot_count: usize) -> Result<Self, BuildError> {
310 loop {
311 for i in 0..slot_count {
312 match Self::try_acquire(build_base, i) {
313 Ok(Some(slot)) => {
314 tracing::info!(slot = i, "Acquired build slot");
315 return Ok(slot);
316 }
317 Ok(None) => continue,
318 Err(e) => {
319 return Err(BuildError::NoSlot(format!(
320 "Failed to probe slot {}: {}",
321 i, e
322 )));
323 }
324 }
325 }
326 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
327 }
328 }
329}