ranim_cli/
lib.rs

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            // let tmp_dir = std::env::temp_dir();
51            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    /// This will cancel the previous build
97    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        // 使用时间戳和随机数确保每次都有唯一的临时文件名
153        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        // .env("RUSTFLAGS", "-C prefer_dynamic")
237        .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    // Start an async task to wait for completion
249    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    // Construct the dylib path
292    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}