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;
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<'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 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 .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 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 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
334pub 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}