ranim_cli/
lib.rs

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            // 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`` fn with the correct signature
150    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        // "--message-format=json-render-diagnostics",
188    ])
189    .current_dir(path);
190    cmd.args(&args.args);
191
192    // Start an async task to wait for completion
193    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    // Construct the dylib path
231    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}