1use anyhow::{Context, 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::CliArgs, 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 target: Target,
23 args: CliArgs,
24 current_dir: PathBuf,
25
26 res_tx: Sender<Result<RanimUserLibrary>>,
27 cancel_rx: Receiver<()>,
28}
29
30impl BuildProcess {
31 pub fn build(self) {
32 if let Err(err) = cargo_build(
33 &self.current_dir,
34 &self.package_name,
35 &self.target,
36 &self.args,
37 Some(self.cancel_rx),
38 ) {
39 error!("Failed to build package: {err:?}");
40 self.res_tx
41 .send_blocking(Err(anyhow::anyhow!("Failed to build package: {err:?}")))
42 .unwrap();
43 } else {
44 let dylib_path = get_dylib_path(
45 &self.workspace,
46 &self.package_name,
47 &self.target,
48 &self.args.args,
49 );
50 info!("loading {dylib_path:?}...");
52
53 let lib = RanimUserLibrary::load(dylib_path);
54 self.res_tx.send_blocking(Ok(lib)).unwrap();
55 }
56 }
57}
58
59pub struct RanimUserLibraryBuilder {
60 pub res_rx: Receiver<Result<RanimUserLibrary>>,
61 cancel_tx: Sender<()>,
62
63 build_process: BuildProcess,
64 building_handle: Option<JoinHandle<()>>,
65}
66
67impl RanimUserLibraryBuilder {
68 pub fn new(
69 workspace: Arc<Workspace>,
70 package_name: String,
71 target: Target,
72 args: CliArgs,
73 current_dir: PathBuf,
74 ) -> Self {
75 let (res_tx, res_rx) = bounded(1);
76 let (cancel_tx, cancel_rx) = bounded(1);
77
78 let build_process = BuildProcess {
79 workspace,
80 package_name,
81 target,
82 args,
83 current_dir,
84 res_tx,
85 cancel_rx,
86 };
87
88 Self {
89 res_rx,
90 cancel_tx,
91 build_process,
92 building_handle: None,
93 }
94 }
95
96 pub fn start_build(&mut self) {
98 info!("Start build");
99 self.cancel_previous_build();
100 let builder = self.build_process.clone();
101 self.building_handle = Some(thread::spawn(move || builder.build()));
102 }
103
104 pub fn cancel_previous_build(&mut self) {
105 if let Some(building_handle) = self.building_handle.take()
106 && !building_handle.is_finished()
107 {
108 info!("Canceling previous build...");
109 if let Err(err) = self.cancel_tx.try_send(())
110 && err.is_closed()
111 {
112 panic!("Failed to cancel build: {err:?}");
113 }
114 building_handle.join().unwrap();
115 }
116 }
117}
118
119impl Drop for RanimUserLibraryBuilder {
120 fn drop(&mut self) {
121 self.cancel_previous_build();
122 }
123}
124
125pub struct RanimUserLibrary {
126 inner: Option<Library>,
127 temp_path: PathBuf,
128}
129
130pub struct RanimUserLibrarySceneIter<'a> {
131 lib: &'a RanimUserLibrary,
132 idx: usize,
133}
134
135impl<'a> Iterator for RanimUserLibrarySceneIter<'a> {
136 type Item = &'a Scene;
137
138 fn next(&mut self) -> Option<Self::Item> {
139 let res = self.lib.get_scene(self.idx);
140 self.idx += 1;
141 res
142 }
143}
144
145impl RanimUserLibrary {
146 pub fn load(dylib_path: impl AsRef<Path>) -> Self {
147 let dylib_path = dylib_path.as_ref();
148
149 let temp_dir = std::env::temp_dir();
150 let file_name = dylib_path.file_name().unwrap();
151
152 let timestamp = std::time::SystemTime::now()
154 .duration_since(std::time::UNIX_EPOCH)
155 .unwrap()
156 .as_nanos();
157 let temp_path = temp_dir.join(format!(
158 "ranim_{}_{}_{}",
159 std::process::id(),
160 timestamp,
161 file_name.to_string_lossy()
162 ));
163
164 std::fs::copy(dylib_path, &temp_path).unwrap();
165
166 let lib = unsafe { Library::new(&temp_path).unwrap() };
167 Self {
168 inner: Some(lib),
169 temp_path,
170 }
171 }
172
173 pub fn scene_cnt(&self) -> usize {
174 let scene_cnt: Symbol<extern "C" fn() -> usize> =
175 unsafe { self.inner.as_ref().unwrap().get(b"scene_cnt").unwrap() };
176 scene_cnt()
177 }
178
179 pub fn get_scene(&self, idx: usize) -> Option<&Scene> {
180 let get_scene: Symbol<extern "C" fn(usize) -> *const Scene> =
181 unsafe { self.inner.as_ref().unwrap().get(b"get_scene").unwrap() };
182 if self.scene_cnt() <= idx {
183 None
184 } else {
185 Some(unsafe { &*get_scene(idx) })
186 }
187 }
188
189 pub fn scenes(&self) -> impl Iterator<Item = &Scene> {
190 RanimUserLibrarySceneIter { lib: self, idx: 0 }
191 }
192
193 pub fn get_preview_func(&self) -> Result<&Scene> {
194 self.scenes().next().context("no scene found")
195 }
196}
197
198impl Drop for RanimUserLibrary {
199 fn drop(&mut self) {
200 println!("Dropping RanimUserLibrary...");
201
202 drop(self.inner.take());
203 std::fs::remove_file(&self.temp_path).unwrap();
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, Default)]
208pub enum Target {
209 #[default]
210 Lib,
211 Example(String),
212}
213
214impl From<cli::TargetArg> for Target {
215 fn from(arg: cli::TargetArg) -> Self {
216 if arg.lib {
217 Target::Lib
218 } else if let Some(example) = arg.example {
219 Target::Example(example)
220 } else {
221 Self::default()
222 }
223 }
224}
225
226pub fn cargo_build(
227 path: impl AsRef<Path>,
228 package: &str,
229 target: &Target,
230 args: &CliArgs,
231 cancel_rx: Option<Receiver<()>>,
232) -> Result<()> {
233 let path = path.as_ref();
234 let mut cmd = Command::new("cargo");
235 cmd.args(["build", "-p", package, "--color=always"])
236 .current_dir(path);
238 match target {
239 Target::Lib => {
240 cmd.arg("--lib");
241 }
242 Target::Example(x) => {
243 cmd.args(["--example", x]);
244 }
245 }
246 cmd.args(&args.args);
247
248 let mut child = match cmd.spawn() {
250 Ok(child) => child,
251 Err(e) => {
252 anyhow::bail!("Failed to start cargo build: {}", e)
253 }
254 };
255
256 loop {
257 if cancel_rx
258 .as_ref()
259 .and_then(|rx| rx.try_recv().ok())
260 .is_some()
261 {
262 child.kill().unwrap();
263 child.wait().unwrap();
264
265 anyhow::bail!("build cancelled");
266 }
267 match child.try_wait() {
268 Ok(res) => {
269 if let Some(status) = res {
270 if status.success() {
271 info!("Build successful!");
272 return Ok(());
273 } else {
274 anyhow::bail!("Build failed with exit code: {:?}", status.code());
275 }
276 }
277 }
278 Err(err) => {
279 anyhow::bail!("build process error: {}", err);
280 }
281 }
282 }
283}
284
285fn get_dylib_path(
286 workspace: &Workspace,
287 package_name: &str,
288 target: &Target,
289 args: &[String],
290) -> PathBuf {
291 let mut target_dir = workspace
293 .krates
294 .workspace_root()
295 .as_std_path()
296 .join("target")
297 .join(if args.contains(&"--release".to_string()) {
298 "release"
299 } else {
300 "debug"
301 });
302 if let Target::Example(_) = target {
303 target_dir = target_dir.join("examples");
304 }
305
306 let artifact_name = match target {
307 Target::Lib => package_name,
308 Target::Example(example) => example,
309 };
310
311 #[cfg(target_os = "windows")]
312 let dylib_name = format!("{}.dll", artifact_name.replace("-", "_"));
313
314 #[cfg(target_os = "macos")]
315 let dylib_name = format!("lib{}.dylib", artifact_name.replace("-", "_"));
316
317 #[cfg(target_os = "linux")]
318 let dylib_name = format!("lib{}.so", artifact_name.replace("-", "_"));
319
320 target_dir.join(dylib_name)
321}