ranim_cli/
lib.rs

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            // let tmp_dir = std::env::temp_dir();
44            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    /// This will cancel the previous build
88    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        // 使用时间戳和随机数确保每次都有唯一的临时文件名
129        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    /// Safety: dylib has a `scenes` and a `scene_cnt` fn with the correct signature and safe implementation
150    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        // "--message-format=json-render-diagnostics",
196    ])
197    .current_dir(path);
198    cmd.args(&args.args);
199
200    // Start an async task to wait for completion
201    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    // Construct the dylib path
239    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}