1use anyhow::{Context, Result};
2use async_channel::{Receiver, Sender, bounded};
3use libloading::{Library, Symbol};
4use log::{debug, error, info};
5use ranim::Scene;
6use std::{
7 path::{Path, PathBuf},
8 process::Command,
9 sync::Arc,
10 thread::{self, JoinHandle},
11};
12
13use crate::{cli::Args, workspace::Workspace};
14
15pub mod cli;
16pub mod workspace;
17
18#[derive(Clone)]
19pub struct BuildProcess {
20 workspace: Arc<Workspace>,
21 package_name: String,
22 args: Args,
23 current_dir: PathBuf,
24
25 res_tx: Sender<Result<RanimUserLibrary>>,
26 cancel_rx: Receiver<()>,
27}
28
29impl BuildProcess {
30 pub fn build(self) {
31 if let Err(err) = cargo_build(
32 &self.current_dir,
33 &self.package_name,
34 &self.args,
35 Some(self.cancel_rx),
36 ) {
37 error!("Failed to build package: {err:?}");
38 self.res_tx
39 .send_blocking(Err(anyhow::anyhow!("Failed to build package: {err:?}")))
40 .unwrap();
41 } else {
42 let dylib_path = get_dylib_path(&self.workspace, &self.package_name, &self.args.args);
43 info!("loading {dylib_path:?}...");
45
46 let lib = RanimUserLibrary::load(dylib_path);
47 self.res_tx.send_blocking(Ok(lib)).unwrap();
48 }
49 }
50}
51
52pub struct RanimUserLibraryBuilder {
53 pub res_rx: Receiver<Result<RanimUserLibrary>>,
54 cancel_tx: Sender<()>,
55
56 build_process: BuildProcess,
57 building_handle: Option<JoinHandle<()>>,
58}
59
60impl RanimUserLibraryBuilder {
61 pub fn new(
62 workspace: Arc<Workspace>,
63 package_name: String,
64 args: Args,
65 current_dir: PathBuf,
66 ) -> Self {
67 let (res_tx, res_rx) = bounded(1);
68 let (cancel_tx, cancel_rx) = bounded(1);
69
70 let build_process = BuildProcess {
71 workspace,
72 package_name,
73 args,
74 current_dir,
75 res_tx,
76 cancel_rx,
77 };
78
79 Self {
80 res_rx,
81 cancel_tx,
82 build_process,
83 building_handle: None,
84 }
85 }
86
87 pub fn start_build(&mut self) {
89 info!("Start build");
90 self.cancel_previous_build();
91 let builder = self.build_process.clone();
92 self.building_handle = Some(thread::spawn(move || builder.build()));
93 }
94
95 pub fn cancel_previous_build(&mut self) {
96 if let Some(building_handle) = self.building_handle.take()
97 && !building_handle.is_finished()
98 {
99 info!("Canceling previous build...");
100 if let Err(err) = self.cancel_tx.try_send(())
101 && err.is_closed()
102 {
103 panic!("Failed to cancel build: {err:?}");
104 }
105 building_handle.join().unwrap();
106 }
107 }
108}
109
110impl Drop for RanimUserLibraryBuilder {
111 fn drop(&mut self) {
112 self.cancel_previous_build();
113 }
114}
115
116pub struct RanimUserLibrary {
117 inner: Option<Library>,
118 temp_path: PathBuf,
119}
120
121impl RanimUserLibrary {
122 pub fn load(dylib_path: impl AsRef<Path>) -> Self {
123 let dylib_path = dylib_path.as_ref();
124
125 let temp_dir = std::env::temp_dir();
126 let file_name = dylib_path.file_name().unwrap();
127
128 let timestamp = std::time::SystemTime::now()
130 .duration_since(std::time::UNIX_EPOCH)
131 .unwrap()
132 .as_nanos();
133 let temp_path = temp_dir.join(format!(
134 "ranim_{}_{}_{}",
135 std::process::id(),
136 timestamp,
137 file_name.to_string_lossy()
138 ));
139
140 std::fs::copy(dylib_path, &temp_path).unwrap();
141
142 let lib = unsafe { Library::new(&temp_path).unwrap() };
143 Self {
144 inner: Some(lib),
145 temp_path,
146 }
147 }
148
149 pub fn scenes(&self) -> &[Scene] {
151 let scene_cnt: Symbol<extern "C" fn() -> usize> =
152 unsafe { self.inner.as_ref().unwrap().get(b"scene_cnt").unwrap() };
153
154 let scenes: Symbol<extern "C" fn() -> *const Scene> =
155 unsafe { self.inner.as_ref().unwrap().get(b"scenes").unwrap() };
156
157 let scene_cnt = scene_cnt();
158 debug!("scene_cnt: {scene_cnt}");
159 let scenes = scenes();
160
161 unsafe { std::slice::from_raw_parts(scenes, scene_cnt) }
162 }
163
164 pub fn get_preview_func(&self) -> Result<&Scene> {
165 self.scenes()
166 .iter()
167 .find(|s| s.preview)
168 .context("no scene marked with `#[preview]` found")
169 }
170}
171
172impl Drop for RanimUserLibrary {
173 fn drop(&mut self) {
174 println!("Dropping RanimUserLibrary...");
175
176 drop(self.inner.take());
177 std::fs::remove_file(&self.temp_path).unwrap();
178 }
179}
180
181fn cargo_build(
182 path: impl AsRef<Path>,
183 package: &str,
184 args: &Args,
185 cancel_rx: Option<Receiver<()>>,
186) -> Result<()> {
187 let path = path.as_ref();
188 let mut cmd = Command::new("cargo");
189 cmd.args([
190 "build",
191 "-p",
192 package,
193 "--lib",
194 "--color=always",
195 ])
197 .current_dir(path);
198 cmd.args(&args.args);
199
200 let mut child = match cmd.spawn() {
202 Ok(child) => child,
203 Err(e) => {
204 anyhow::bail!("Failed to start cargo build: {}", e)
205 }
206 };
207
208 loop {
209 if cancel_rx
210 .as_ref()
211 .and_then(|rx| rx.try_recv().ok())
212 .is_some()
213 {
214 child.kill().unwrap();
215 child.wait().unwrap();
216
217 anyhow::bail!("build cancelled");
218 }
219 match child.try_wait() {
220 Ok(res) => {
221 if let Some(status) = res {
222 if status.success() {
223 info!("Build successful!");
224 return Ok(());
225 } else {
226 anyhow::bail!("Build failed with exit code: {:?}", status.code());
227 }
228 }
229 }
230 Err(err) => {
231 anyhow::bail!("build process error: {}", err);
232 }
233 }
234 }
235}
236
237fn get_dylib_path(workspace: &Workspace, package_name: &str, args: &[String]) -> PathBuf {
238 let target_dir = workspace
240 .krates
241 .workspace_root()
242 .as_std_path()
243 .join("target")
244 .join(if args.contains(&"--release".to_string()) {
245 "release"
246 } else {
247 "debug"
248 });
249
250 #[cfg(target_os = "windows")]
251 let dylib_name = format!("{}.dll", package_name.replace("-", "_"));
252
253 #[cfg(target_os = "macos")]
254 let dylib_name = format!("lib{}.dylib", package_name.replace("-", "_"));
255
256 #[cfg(target_os = "linux")]
257 let dylib_name = format!("lib{}.so", package_name.replace("-", "_"));
258
259 target_dir.join(dylib_name)
260}