#![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,
);
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,
}
}
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"])
.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);
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 {
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)
}
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();
}
pub fn main() {
use clap::Parser;
init_tracing();
Cli::parse().run().unwrap();
}