ranim-cli 0.2.1

The command line tool for Ranim.
Documentation
// #![warn(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![allow(rustdoc::private_intra_doc_links)]
#![doc(
    html_logo_url = "https://raw.githubusercontent.com/AzurIce/ranim/refs/heads/main/assets/ranim.svg",
    html_favicon_url = "https://raw.githubusercontent.com/AzurIce/ranim/refs/heads/main/assets/ranim.svg"
)]

use anyhow::{Context, Result};
use async_channel::{Receiver, Sender, bounded};
use libloading::{Library, Symbol};
use ranim::{Scene, StaticScene};
use std::{
    path::{Path, PathBuf},
    process::Command,
    sync::Arc,
    thread::{self, JoinHandle},
};
use tracing::{error, info};

use crate::{
    cli::{Cli, CliArgs},
    workspace::Workspace,
};

pub mod cli;
pub mod workspace;

#[derive(Clone)]
pub struct BuildProcess {
    workspace: Arc<Workspace>,
    package_name: String,
    target: Target,
    args: CliArgs,
    current_dir: PathBuf,

    res_tx: Sender<Result<RanimUserLibrary>>,
    cancel_rx: Receiver<()>,
}

impl BuildProcess {
    pub fn build(self) {
        if let Err(err) = cargo_build(
            &self.current_dir,
            &self.package_name,
            &self.target,
            &self.args,
            Some(self.cancel_rx),
        ) {
            error!("Failed to build package: {err:?}");
            self.res_tx
                .send_blocking(Err(anyhow::anyhow!("Failed to build package: {err:?}")))
                .unwrap();
        } else {
            let dylib_path = get_dylib_path(
                &self.workspace,
                &self.package_name,
                &self.target,
                &self.args.args,
            );
            // let tmp_dir = std::env::temp_dir();
            info!("loading {dylib_path:?}...");

            let lib = RanimUserLibrary::load(dylib_path);
            self.res_tx.send_blocking(Ok(lib)).unwrap();
        }
    }
}

pub struct RanimUserLibraryBuilder {
    pub res_rx: Receiver<Result<RanimUserLibrary>>,
    cancel_tx: Sender<()>,

    build_process: BuildProcess,
    building_handle: Option<JoinHandle<()>>,
}

impl RanimUserLibraryBuilder {
    pub fn new(
        workspace: Arc<Workspace>,
        package_name: String,
        target: Target,
        args: CliArgs,
        current_dir: PathBuf,
    ) -> Self {
        let (res_tx, res_rx) = bounded(1);
        let (cancel_tx, cancel_rx) = bounded(1);

        let build_process = BuildProcess {
            workspace,
            package_name,
            target,
            args,
            current_dir,
            res_tx,
            cancel_rx,
        };

        Self {
            res_rx,
            cancel_tx,
            build_process,
            building_handle: None,
        }
    }

    /// This will cancel the previous build
    pub fn start_build(&mut self) {
        info!("Start build");
        self.cancel_previous_build();
        let builder = self.build_process.clone();
        self.building_handle = Some(thread::spawn(move || builder.build()));
    }

    pub fn cancel_previous_build(&mut self) {
        if let Some(building_handle) = self.building_handle.take()
            && !building_handle.is_finished()
        {
            info!("Canceling previous build...");
            if let Err(err) = self.cancel_tx.try_send(())
                && err.is_closed()
            {
                panic!("Failed to cancel build: {err:?}");
            }
            building_handle.join().unwrap();
        }
    }
}

impl Drop for RanimUserLibraryBuilder {
    fn drop(&mut self) {
        self.cancel_previous_build();
    }
}

pub struct RanimUserLibrary {
    inner: Option<Library>,
    temp_path: PathBuf,
}

pub struct RanimUserLibrarySceneIter<'a> {
    lib: &'a RanimUserLibrary,
    idx: usize,
}

impl Iterator for RanimUserLibrarySceneIter<'_> {
    type Item = Scene;

    fn next(&mut self) -> Option<Self::Item> {
        let res = self.lib.get_scene(self.idx);
        self.idx += 1;
        res
    }
}

impl RanimUserLibrary {
    pub fn load(dylib_path: impl AsRef<Path>) -> Self {
        let dylib_path = dylib_path.as_ref();

        let temp_dir = std::env::temp_dir();
        let file_name = dylib_path.file_name().unwrap();

        // 使用时间戳和随机数确保每次都有唯一的临时文件名
        let timestamp = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let temp_path = temp_dir.join(format!(
            "ranim_{}_{}_{}",
            std::process::id(),
            timestamp,
            file_name.to_string_lossy()
        ));

        std::fs::copy(dylib_path, &temp_path).unwrap();

        let lib = unsafe { Library::new(&temp_path).unwrap() };
        Self {
            inner: Some(lib),
            temp_path,
        }
    }

    pub fn scene_cnt(&self) -> usize {
        let scene_cnt: Symbol<extern "C" fn() -> usize> =
            unsafe { self.inner.as_ref().unwrap().get(b"scene_cnt").unwrap() };
        scene_cnt()
    }

    pub fn get_scene(&self, idx: usize) -> Option<Scene> {
        let get_scene: Symbol<extern "C" fn(usize) -> *const StaticScene> =
            unsafe { self.inner.as_ref().unwrap().get(b"get_scene").unwrap() };
        if self.scene_cnt() <= idx {
            None
        } else {
            Some(Scene::from(unsafe { &*get_scene(idx) }))
        }
    }

    pub fn scenes(&self) -> impl Iterator<Item = Scene> {
        RanimUserLibrarySceneIter { lib: self, idx: 0 }
    }

    pub fn get_preview_func(&self) -> Result<Scene> {
        self.scenes().next().context("no scene found")
    }
}

impl Drop for RanimUserLibrary {
    fn drop(&mut self) {
        println!("Dropping RanimUserLibrary...");

        drop(self.inner.take());
        std::fs::remove_file(&self.temp_path).unwrap();
    }
}

#[derive(Debug, Clone, PartialEq, Default)]
pub enum Target {
    #[default]
    Lib,
    Example(String),
}

impl From<cli::TargetArg> for Target {
    fn from(arg: cli::TargetArg) -> Self {
        if arg.lib {
            Target::Lib
        } else if let Some(example) = arg.example {
            Target::Example(example)
        } else {
            Self::default()
        }
    }
}

pub fn cargo_build(
    path: impl AsRef<Path>,
    package: &str,
    target: &Target,
    args: &CliArgs,
    cancel_rx: Option<Receiver<()>>,
) -> Result<()> {
    let path = path.as_ref();
    let mut cmd = Command::new("cargo");
    cmd.args(["build", "-p", package, "--color=always"])
        // .env("RUSTFLAGS", "-C prefer_dynamic")
        .current_dir(path);
    match target {
        Target::Lib => {
            cmd.arg("--lib");
        }
        Target::Example(x) => {
            cmd.args(["--example", x]);
        }
    }

    if !args.features.is_empty() {
        cmd.args(std::iter::once("--features").chain(args.features.iter().map(|s| s.as_str())));
    }

    cmd.args(&args.args);

    // Start an async task to wait for completion
    let mut child = match cmd.spawn() {
        Ok(child) => child,
        Err(e) => {
            anyhow::bail!("Failed to start cargo build: {}", e)
        }
    };

    loop {
        if cancel_rx
            .as_ref()
            .and_then(|rx| rx.try_recv().ok())
            .is_some()
        {
            child.kill().unwrap();
            child.wait().unwrap();

            anyhow::bail!("build cancelled");
        }
        match child.try_wait() {
            Ok(res) => {
                if let Some(status) = res {
                    if status.success() {
                        info!("Build successful!");
                        return Ok(());
                    } else {
                        anyhow::bail!("Build failed with exit code: {:?}", status.code());
                    }
                }
            }
            Err(err) => {
                anyhow::bail!("build process error: {}", err);
            }
        }
    }
}

fn get_dylib_path(
    workspace: &Workspace,
    package_name: &str,
    target: &Target,
    args: &[String],
) -> PathBuf {
    // Construct the dylib path
    let mut target_dir = workspace
        .krates
        .workspace_root()
        .as_std_path()
        .join("target")
        .join(if args.contains(&"--release".to_string()) {
            "release"
        } else {
            "debug"
        });
    if let Target::Example(_) = target {
        target_dir = target_dir.join("examples");
    }

    let artifact_name = match target {
        Target::Lib => package_name,
        Target::Example(example) => example,
    };

    #[cfg(target_os = "windows")]
    let dylib_name = format!("{}.dll", artifact_name.replace("-", "_"));

    #[cfg(target_os = "macos")]
    let dylib_name = format!("lib{}.dylib", artifact_name.replace("-", "_"));

    #[cfg(target_os = "linux")]
    let dylib_name = format!("lib{}.so", artifact_name.replace("-", "_"));

    target_dir.join(dylib_name)
}

/// This should only be called once per process.
///
/// If you use [`main`], this is called automatically.
pub fn init_tracing() {
    use tracing::level_filters::LevelFilter;
    use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};

    fn build_filter() -> EnvFilter {
        const DEFAULT_DIRECTIVES: &[(&str, LevelFilter)] = &[
            ("ranim_cli", LevelFilter::INFO),
            ("ranim", LevelFilter::INFO),
        ];
        let mut filter = EnvFilter::from_default_env();
        let env = std::env::var("RUST_LOG").unwrap_or_default();
        for (name, level) in DEFAULT_DIRECTIVES
            .iter()
            .filter(|(name, _)| !env.contains(name))
        {
            filter = filter.add_directive(format!("{name}={level}").parse().unwrap());
        }
        filter
    }

    let indicatif_layer = tracing_indicatif::IndicatifLayer::new();

    tracing_subscriber::registry()
        .with(fmt::layer().with_writer(indicatif_layer.get_stderr_writer()))
        .with(indicatif_layer)
        .with(build_filter())
        .init();
}

/// main
pub fn main() {
    use clap::Parser;
    init_tracing();
    Cli::parse().run().unwrap();
}