ranim_cli/
lib.rs

1// #![warn(missing_docs)]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![allow(rustdoc::private_intra_doc_links)]
4#![doc(
5    html_logo_url = "https://raw.githubusercontent.com/AzurIce/ranim/refs/heads/main/assets/ranim.svg",
6    html_favicon_url = "https://raw.githubusercontent.com/AzurIce/ranim/refs/heads/main/assets/ranim.svg"
7)]
8
9use anyhow::{Context, Result};
10use async_channel::{Receiver, Sender, bounded};
11use libloading::{Library, Symbol};
12use ranim::Scene;
13use std::{
14    path::{Path, PathBuf},
15    process::Command,
16    sync::Arc,
17    thread::{self, JoinHandle},
18};
19use tracing::{error, info};
20
21use crate::{
22    cli::{Cli, CliArgs},
23    workspace::Workspace,
24};
25
26pub mod cli;
27pub mod workspace;
28
29#[derive(Clone)]
30pub struct BuildProcess {
31    workspace: Arc<Workspace>,
32    package_name: String,
33    target: Target,
34    args: CliArgs,
35    current_dir: PathBuf,
36
37    res_tx: Sender<Result<RanimUserLibrary>>,
38    cancel_rx: Receiver<()>,
39}
40
41impl BuildProcess {
42    pub fn build(self) {
43        if let Err(err) = cargo_build(
44            &self.current_dir,
45            &self.package_name,
46            &self.target,
47            &self.args,
48            Some(self.cancel_rx),
49        ) {
50            error!("Failed to build package: {err:?}");
51            self.res_tx
52                .send_blocking(Err(anyhow::anyhow!("Failed to build package: {err:?}")))
53                .unwrap();
54        } else {
55            let dylib_path = get_dylib_path(
56                &self.workspace,
57                &self.package_name,
58                &self.target,
59                &self.args.args,
60            );
61            // let tmp_dir = std::env::temp_dir();
62            info!("loading {dylib_path:?}...");
63
64            let lib = RanimUserLibrary::load(dylib_path);
65            self.res_tx.send_blocking(Ok(lib)).unwrap();
66        }
67    }
68}
69
70pub struct RanimUserLibraryBuilder {
71    pub res_rx: Receiver<Result<RanimUserLibrary>>,
72    cancel_tx: Sender<()>,
73
74    build_process: BuildProcess,
75    building_handle: Option<JoinHandle<()>>,
76}
77
78impl RanimUserLibraryBuilder {
79    pub fn new(
80        workspace: Arc<Workspace>,
81        package_name: String,
82        target: Target,
83        args: CliArgs,
84        current_dir: PathBuf,
85    ) -> Self {
86        let (res_tx, res_rx) = bounded(1);
87        let (cancel_tx, cancel_rx) = bounded(1);
88
89        let build_process = BuildProcess {
90            workspace,
91            package_name,
92            target,
93            args,
94            current_dir,
95            res_tx,
96            cancel_rx,
97        };
98
99        Self {
100            res_rx,
101            cancel_tx,
102            build_process,
103            building_handle: None,
104        }
105    }
106
107    /// This will cancel the previous build
108    pub fn start_build(&mut self) {
109        info!("Start build");
110        self.cancel_previous_build();
111        let builder = self.build_process.clone();
112        self.building_handle = Some(thread::spawn(move || builder.build()));
113    }
114
115    pub fn cancel_previous_build(&mut self) {
116        if let Some(building_handle) = self.building_handle.take()
117            && !building_handle.is_finished()
118        {
119            info!("Canceling previous build...");
120            if let Err(err) = self.cancel_tx.try_send(())
121                && err.is_closed()
122            {
123                panic!("Failed to cancel build: {err:?}");
124            }
125            building_handle.join().unwrap();
126        }
127    }
128}
129
130impl Drop for RanimUserLibraryBuilder {
131    fn drop(&mut self) {
132        self.cancel_previous_build();
133    }
134}
135
136pub struct RanimUserLibrary {
137    inner: Option<Library>,
138    temp_path: PathBuf,
139}
140
141pub struct RanimUserLibrarySceneIter<'a> {
142    lib: &'a RanimUserLibrary,
143    idx: usize,
144}
145
146impl<'a> Iterator for RanimUserLibrarySceneIter<'a> {
147    type Item = &'a Scene;
148
149    fn next(&mut self) -> Option<Self::Item> {
150        let res = self.lib.get_scene(self.idx);
151        self.idx += 1;
152        res
153    }
154}
155
156impl RanimUserLibrary {
157    pub fn load(dylib_path: impl AsRef<Path>) -> Self {
158        let dylib_path = dylib_path.as_ref();
159
160        let temp_dir = std::env::temp_dir();
161        let file_name = dylib_path.file_name().unwrap();
162
163        // 使用时间戳和随机数确保每次都有唯一的临时文件名
164        let timestamp = std::time::SystemTime::now()
165            .duration_since(std::time::UNIX_EPOCH)
166            .unwrap()
167            .as_nanos();
168        let temp_path = temp_dir.join(format!(
169            "ranim_{}_{}_{}",
170            std::process::id(),
171            timestamp,
172            file_name.to_string_lossy()
173        ));
174
175        std::fs::copy(dylib_path, &temp_path).unwrap();
176
177        let lib = unsafe { Library::new(&temp_path).unwrap() };
178        Self {
179            inner: Some(lib),
180            temp_path,
181        }
182    }
183
184    pub fn scene_cnt(&self) -> usize {
185        let scene_cnt: Symbol<extern "C" fn() -> usize> =
186            unsafe { self.inner.as_ref().unwrap().get(b"scene_cnt").unwrap() };
187        scene_cnt()
188    }
189
190    pub fn get_scene(&self, idx: usize) -> Option<&Scene> {
191        let get_scene: Symbol<extern "C" fn(usize) -> *const Scene> =
192            unsafe { self.inner.as_ref().unwrap().get(b"get_scene").unwrap() };
193        if self.scene_cnt() <= idx {
194            None
195        } else {
196            Some(unsafe { &*get_scene(idx) })
197        }
198    }
199
200    pub fn scenes(&self) -> impl Iterator<Item = &Scene> {
201        RanimUserLibrarySceneIter { lib: self, idx: 0 }
202    }
203
204    pub fn get_preview_func(&self) -> Result<&Scene> {
205        self.scenes().next().context("no scene found")
206    }
207}
208
209impl Drop for RanimUserLibrary {
210    fn drop(&mut self) {
211        println!("Dropping RanimUserLibrary...");
212
213        drop(self.inner.take());
214        std::fs::remove_file(&self.temp_path).unwrap();
215    }
216}
217
218#[derive(Debug, Clone, PartialEq, Default)]
219pub enum Target {
220    #[default]
221    Lib,
222    Example(String),
223}
224
225impl From<cli::TargetArg> for Target {
226    fn from(arg: cli::TargetArg) -> Self {
227        if arg.lib {
228            Target::Lib
229        } else if let Some(example) = arg.example {
230            Target::Example(example)
231        } else {
232            Self::default()
233        }
234    }
235}
236
237pub fn cargo_build(
238    path: impl AsRef<Path>,
239    package: &str,
240    target: &Target,
241    args: &CliArgs,
242    cancel_rx: Option<Receiver<()>>,
243) -> Result<()> {
244    let path = path.as_ref();
245    let mut cmd = Command::new("cargo");
246    cmd.args(["build", "-p", package, "--color=always"])
247        // .env("RUSTFLAGS", "-C prefer_dynamic")
248        .current_dir(path);
249    match target {
250        Target::Lib => {
251            cmd.arg("--lib");
252        }
253        Target::Example(x) => {
254            cmd.args(["--example", x]);
255        }
256    }
257    cmd.args(&args.args);
258
259    // Start an async task to wait for completion
260    let mut child = match cmd.spawn() {
261        Ok(child) => child,
262        Err(e) => {
263            anyhow::bail!("Failed to start cargo build: {}", e)
264        }
265    };
266
267    loop {
268        if cancel_rx
269            .as_ref()
270            .and_then(|rx| rx.try_recv().ok())
271            .is_some()
272        {
273            child.kill().unwrap();
274            child.wait().unwrap();
275
276            anyhow::bail!("build cancelled");
277        }
278        match child.try_wait() {
279            Ok(res) => {
280                if let Some(status) = res {
281                    if status.success() {
282                        info!("Build successful!");
283                        return Ok(());
284                    } else {
285                        anyhow::bail!("Build failed with exit code: {:?}", status.code());
286                    }
287                }
288            }
289            Err(err) => {
290                anyhow::bail!("build process error: {}", err);
291            }
292        }
293    }
294}
295
296fn get_dylib_path(
297    workspace: &Workspace,
298    package_name: &str,
299    target: &Target,
300    args: &[String],
301) -> PathBuf {
302    // Construct the dylib path
303    let mut target_dir = workspace
304        .krates
305        .workspace_root()
306        .as_std_path()
307        .join("target")
308        .join(if args.contains(&"--release".to_string()) {
309            "release"
310        } else {
311            "debug"
312        });
313    if let Target::Example(_) = target {
314        target_dir = target_dir.join("examples");
315    }
316
317    let artifact_name = match target {
318        Target::Lib => package_name,
319        Target::Example(example) => example,
320    };
321
322    #[cfg(target_os = "windows")]
323    let dylib_name = format!("{}.dll", artifact_name.replace("-", "_"));
324
325    #[cfg(target_os = "macos")]
326    let dylib_name = format!("lib{}.dylib", artifact_name.replace("-", "_"));
327
328    #[cfg(target_os = "linux")]
329    let dylib_name = format!("lib{}.so", artifact_name.replace("-", "_"));
330
331    target_dir.join(dylib_name)
332}
333
334/// main
335pub fn main() {
336    use clap::Parser;
337    use tracing::level_filters::LevelFilter;
338    use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
339
340    fn build_filter() -> EnvFilter {
341        const DEFAULT_DIRECTIVES: &[(&str, LevelFilter)] = &[
342            ("ranim_cli", LevelFilter::INFO),
343            ("ranim", LevelFilter::INFO),
344        ];
345        let mut filter = EnvFilter::from_default_env();
346        let env = std::env::var("RUST_LOG").unwrap_or_default();
347        for (name, level) in DEFAULT_DIRECTIVES
348            .iter()
349            .filter(|(name, _)| !env.contains(name))
350        {
351            filter = filter.add_directive(format!("{name}={level}").parse().unwrap());
352        }
353        filter
354    }
355
356    let indicatif_layer = tracing_indicatif::IndicatifLayer::new();
357
358    tracing_subscriber::registry()
359        .with(fmt::layer().with_writer(indicatif_layer.get_stderr_writer()))
360        .with(indicatif_layer)
361        .with(build_filter())
362        .init();
363
364    Cli::parse().run().unwrap();
365}