1use anyhow::Result;
2use async_channel::{Receiver, Sender, bounded};
3use libloading::{Library, Symbol};
4use log::{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) -> &'static [Scene] {
151 let scenes: Symbol<extern "C" fn() -> &'static [Scene]> =
152 unsafe { self.inner.as_ref().unwrap().get(b"scenes").unwrap() };
153 scenes()
154 }
155
156 pub fn get_preview_func(&self) -> &'static Scene {
157 self.scenes()
158 .iter()
159 .find(|s| s.preview)
160 .expect("no scene marked with `#[preview]` found")
161 }
162}
163
164impl Drop for RanimUserLibrary {
165 fn drop(&mut self) {
166 println!("Dropping RanimUserLibrary...");
167
168 drop(self.inner.take());
169 std::fs::remove_file(&self.temp_path).unwrap();
170 }
171}
172
173fn cargo_build(
174 path: impl AsRef<Path>,
175 package: &str,
176 args: &Args,
177 cancel_rx: Option<Receiver<()>>,
178) -> Result<()> {
179 let path = path.as_ref();
180 let mut cmd = Command::new("cargo");
181 cmd.args([
182 "build",
183 "-p",
184 package,
185 "--lib",
186 "--color=always",
187 ])
189 .current_dir(path);
190 cmd.args(&args.args);
191
192 let mut child = match cmd.spawn() {
194 Ok(child) => child,
195 Err(e) => {
196 anyhow::bail!("Failed to start cargo build: {}", e)
197 }
198 };
199
200 loop {
201 if cancel_rx
202 .as_ref()
203 .and_then(|rx| rx.try_recv().ok())
204 .is_some()
205 {
206 child.kill().unwrap();
207 child.wait().unwrap();
208
209 anyhow::bail!("build cancelled");
210 }
211 match child.try_wait() {
212 Ok(res) => {
213 if let Some(status) = res {
214 if status.success() {
215 info!("Build successful!");
216 return Ok(());
217 } else {
218 anyhow::bail!("Build failed with exit code: {:?}", status.code());
219 }
220 }
221 }
222 Err(err) => {
223 anyhow::bail!("build process error: {}", err);
224 }
225 }
226 }
227}
228
229fn get_dylib_path(workspace: &Workspace, package_name: &str, args: &[String]) -> PathBuf {
230 let target_dir = workspace
232 .krates
233 .workspace_root()
234 .as_std_path()
235 .join("target")
236 .join(if args.contains(&"--release".to_string()) {
237 "release"
238 } else {
239 "debug"
240 });
241
242 #[cfg(target_os = "windows")]
243 let dylib_name = format!("{}.dll", package_name.replace("-", "_"));
244
245 #[cfg(target_os = "macos")]
246 let dylib_name = format!("lib{}.dylib", package_name.replace("-", "_"));
247
248 #[cfg(target_os = "linux")]
249 let dylib_name = format!("lib{}.so", package_name.replace("-", "_"));
250
251 target_dir.join(dylib_name)
252}