1#![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, StaticScene};
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 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 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 Iterator for RanimUserLibrarySceneIter<'_> {
147 type Item = 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 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 StaticScene> =
192 unsafe { self.inner.as_ref().unwrap().get(b"get_scene").unwrap() };
193 if self.scene_cnt() <= idx {
194 None
195 } else {
196 Some(Scene::from(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 .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
258 if !args.features.is_empty() {
259 cmd.args(std::iter::once("--features").chain(args.features.iter().map(|s| s.as_str())));
260 }
261
262 cmd.args(&args.args);
263
264 let mut child = match cmd.spawn() {
266 Ok(child) => child,
267 Err(e) => {
268 anyhow::bail!("Failed to start cargo build: {}", e)
269 }
270 };
271
272 loop {
273 if cancel_rx
274 .as_ref()
275 .and_then(|rx| rx.try_recv().ok())
276 .is_some()
277 {
278 child.kill().unwrap();
279 child.wait().unwrap();
280
281 anyhow::bail!("build cancelled");
282 }
283 match child.try_wait() {
284 Ok(res) => {
285 if let Some(status) = res {
286 if status.success() {
287 info!("Build successful!");
288 return Ok(());
289 } else {
290 anyhow::bail!("Build failed with exit code: {:?}", status.code());
291 }
292 }
293 }
294 Err(err) => {
295 anyhow::bail!("build process error: {}", err);
296 }
297 }
298 }
299}
300
301fn get_dylib_path(
302 workspace: &Workspace,
303 package_name: &str,
304 target: &Target,
305 args: &[String],
306) -> PathBuf {
307 let mut target_dir = workspace
309 .krates
310 .workspace_root()
311 .as_std_path()
312 .join("target")
313 .join(if args.contains(&"--release".to_string()) {
314 "release"
315 } else {
316 "debug"
317 });
318 if let Target::Example(_) = target {
319 target_dir = target_dir.join("examples");
320 }
321
322 let artifact_name = match target {
323 Target::Lib => package_name,
324 Target::Example(example) => example,
325 };
326
327 #[cfg(target_os = "windows")]
328 let dylib_name = format!("{}.dll", artifact_name.replace("-", "_"));
329
330 #[cfg(target_os = "macos")]
331 let dylib_name = format!("lib{}.dylib", artifact_name.replace("-", "_"));
332
333 #[cfg(target_os = "linux")]
334 let dylib_name = format!("lib{}.so", artifact_name.replace("-", "_"));
335
336 target_dir.join(dylib_name)
337}
338
339pub fn init_tracing() {
343 use tracing::level_filters::LevelFilter;
344 use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
345
346 fn build_filter() -> EnvFilter {
347 const DEFAULT_DIRECTIVES: &[(&str, LevelFilter)] = &[
348 ("ranim_cli", LevelFilter::INFO),
349 ("ranim", LevelFilter::INFO),
350 ];
351 let mut filter = EnvFilter::from_default_env();
352 let env = std::env::var("RUST_LOG").unwrap_or_default();
353 for (name, level) in DEFAULT_DIRECTIVES
354 .iter()
355 .filter(|(name, _)| !env.contains(name))
356 {
357 filter = filter.add_directive(format!("{name}={level}").parse().unwrap());
358 }
359 filter
360 }
361
362 let indicatif_layer = tracing_indicatif::IndicatifLayer::new();
363
364 tracing_subscriber::registry()
365 .with(fmt::layer().with_writer(indicatif_layer.get_stderr_writer()))
366 .with(indicatif_layer)
367 .with(build_filter())
368 .init();
369}
370
371pub fn main() {
373 use clap::Parser;
374 init_tracing();
375 Cli::parse().run().unwrap();
376}