main.rs
1 use std::{ 2 env, fs, io, 3 path::{Path, PathBuf}, 4 process, 5 }; 6 7 const WINDOWS_ONLY: &str = "This command currently only supports windows"; 8 9 #[derive(PartialEq)] 10 enum BuildMode { 11 Release, 12 Dev, 13 } 14 15 /// # Examples 16 /// 17 /// `cargo xtask run` 18 /// 19 /// `cargo xtask` to print all commands to the stdout 20 fn main() { 21 let task = env::args().nth(1); 22 match task { 23 None => print_help(), 24 Some(t) => match t.as_str() { 25 "blender" => match env::args().nth(2) { 26 None => blender(BuildMode::Dev), 27 Some(t) => match t.as_str() { 28 "-r" => blender(BuildMode::Release), 29 "-h" => blender_help(), 30 _ => blender(BuildMode::Dev), 31 }, 32 }, 33 "python" => match env::args().nth(2) { 34 None => python(BuildMode::Dev), 35 Some(t) => match t.as_str() { 36 "-r" => python(BuildMode::Release), 37 "-h" => python_help(), 38 _ => python(BuildMode::Dev), 39 }, 40 }, 41 "run" => run(), 42 "wheel" => match env::args().nth(2) { 43 None => wheel(BuildMode::Dev), 44 Some(t) => match t.as_str() { 45 "-r" => wheel(BuildMode::Release), 46 "-h" => wheel_help(), 47 _ => wheel(BuildMode::Dev), 48 }, 49 }, 50 _ => run_plugin(&t, None), 51 }, 52 } 53 } 54 55 fn blender_help() { 56 let descriptions = &[ 57 "Blender task flags:\n", 58 "-r Build the blender extension with blender's CLI for distribution\n", 59 "-h Shows this message\n", 60 ]; 61 eprintln!("{}", descriptions.join("")) 62 } 63 64 fn python_help() { 65 let descriptions = &[ 66 "Python task flags:\n", 67 "-r Develop with the emtk-py python bindings using maturin in release mode\n", 68 "-h Shows this message\n", 69 ]; 70 eprintln!("{}", descriptions.join("")) 71 } 72 73 fn wheel_help() { 74 let descriptions = &[ 75 "Wheel task flags:\n", 76 "-r Build emtk-py wheel with maturin in release mode\n", 77 "-h Shows this message\n", 78 ]; 79 eprintln!("{}", descriptions.join("")) 80 } 81 82 /// Parses the examples folder to print out to the stdout as a xtask command 83 fn print_help() { 84 let mut descriptions = vec![ 85 "Tasks:\n".to_string(), 86 "blender Copy emtk-py's blender folder to blender's extension folder (-h for help)\n".to_string(), 87 "python Develop with emtk-py python bindings using maturin (-h for help)\n".to_string(), 88 "run Run all example plugins\n".to_string(), 89 "wheel Build emtk-py wheel with maturin (-h for help)\n".to_string(), 90 ]; 91 92 let project_root = project_root(); 93 let examples_path = project_root.join("examples"); 94 for entry in examples_path 95 .read_dir() 96 .expect("error while reading examples folder") 97 .flatten() 98 { 99 let path = entry.path(); 100 if path.is_file() { 101 continue; 102 } 103 let name = entry 104 .file_name() 105 .to_str() 106 .expect("error while getting name of entry") 107 .to_string(); 108 109 descriptions.push(format!( 110 "{}{}Run only the {} plugin\n", 111 name, 112 " ".repeat(19 - name.len()), 113 name 114 )); 115 } 116 117 eprintln!("{}", descriptions.join("")); 118 } 119 120 /// Helps show exactly what command ran with what arguments in the panic 121 fn panic_command(cmd: &str, args: Option<&[&str]>, e: io::Error) -> process::ExitStatus { 122 match args { 123 None => { 124 panic!(r#"error while running "{}": {}"#, cmd, e) 125 } 126 Some(a) => { 127 panic!(r#"error while running "{} {}": {}"#, cmd, a.join(" "), e) 128 } 129 } 130 } 131 132 /// This will usually return "cargo" 133 fn cargo_env() -> String { 134 env::var("CARGO").unwrap_or("cargo".to_string()) 135 } 136 137 fn project_root() -> &'static Path { 138 Path::new(&env!("CARGO_MANIFEST_DIR")) 139 .parent() 140 .unwrap() 141 .parent() 142 .unwrap() 143 } 144 145 /// Reads the EXANIMA_EXE environment variable and returns it as a [`PathBuf`]. 146 /// 147 /// A default install of Steam will install Exanima at this path: 148 /// 149 /// ```rust 150 /// "C:/Program Files (x86)/Steam/steamapps/common/Exanima/Exanima.exe" 151 /// ``` 152 /// 153 /// # Panics 154 /// 155 /// - EXANIMA_EXE environment variable **must** be set. 156 /// - EXANIMA_EXE **must** point to an existing file. The file should be the game's binary. 157 fn exe_path() -> PathBuf { 158 let exanima_exe = PathBuf::from( 159 env::var("EXANIMA_EXE").expect("environment variable, EXANIMA_EXE, must be set"), 160 ); 161 if !exanima_exe.exists() { 162 panic!("Could not find Exanima.exe\nSet EXANIMA_EXE to the full path to Exanima.exe") 163 } 164 165 exanima_exe 166 } 167 168 fn setup_python() { 169 let project_root = project_root(); 170 let uv_cmd = "uv"; 171 let uv_venv_args = &["venv"]; 172 let uv_install_args = &[ 173 "pip", 174 "install", 175 "-r", 176 "./bindings/python/emtk-py/requirements.txt", 177 ]; 178 let venv_cmd = "./.venv/Scripts/activate.bat"; 179 180 process::Command::new(uv_cmd) 181 .current_dir(project_root) 182 .args(uv_venv_args) 183 .status() 184 .unwrap_or_else(|e| panic_command(uv_cmd, Some(uv_venv_args), e)); 185 process::Command::new(uv_cmd) 186 .current_dir(project_root) 187 .args(uv_install_args) 188 .status() 189 .unwrap_or_else(|e| panic_command(uv_cmd, Some(uv_install_args), e)); 190 process::Command::new(venv_cmd) 191 .current_dir(project_root) 192 .status() 193 .unwrap_or_else(|e| panic_command(venv_cmd, None, e)); 194 } 195 196 /// A zip file will be created when building for release. 197 /// When building in dev mode, emtk-py's blender folder is copied to blender's extension folder. 198 /// 199 /// Blender 4.2 supported only 200 /// 201 /// # Panics 202 /// 203 /// - The uv command **must** be in the PATH environment variable 204 /// - The blender binary **must** be in the PATH environment variable when building in release 205 fn blender(build_mode: BuildMode) { 206 if !cfg!(windows) { 207 return eprintln!("{}", WINDOWS_ONLY); 208 } 209 210 let project_root = project_root(); 211 // NOTE: emtk-py wheel 212 let wheel_pkg = "wheels/emtk-0.1.0b1-cp311-abi3-win_amd64.whl"; 213 214 // Copy the emtk-py wheel file into emtk's blender extension "wheels" folder 215 let bl_dep_path = PathBuf::from(&wheel_pkg); 216 let wheel_name = bl_dep_path.file_name().unwrap().to_str().unwrap(); 217 let wheel_path = project_root.join(format!("target/wheels/{}", &wheel_name)); 218 let target_wheel_path = project_root.join(format!( 219 "bindings/python/emtk-py/emtk/blender/wheels/{}", 220 &wheel_name 221 )); 222 let target_wheel_parent = target_wheel_path.parent().unwrap(); 223 if !target_wheel_parent.exists() { 224 fs::create_dir_all(target_wheel_parent).unwrap(); 225 } 226 if !wheel_path.exists() { 227 wheel(BuildMode::Release); 228 } 229 fs::copy(&wheel_path, &target_wheel_path).unwrap(); 230 231 if build_mode == BuildMode::Release { 232 // Bundle extension for distribution 233 let blender_cmd = "blender"; 234 let blender_args = &[ 235 "--command", 236 "extension", 237 "build", 238 "--source-dir", 239 "./bindings/python/emtk-py/emtk/blender/", 240 "--output-dir", 241 "./target/", 242 ]; 243 process::Command::new(blender_cmd) 244 .current_dir(project_root) 245 .args(blender_args) 246 .status() 247 .unwrap_or_else(|e| panic_command(blender_cmd, Some(blender_args), e)); 248 } else if build_mode == BuildMode::Dev { 249 // Check if the extension folder exists first 250 // WARN: Be careful modifying the path of data_dir as fs::remove_dir_all is called with it 251 let mut data_dir = PathBuf::from(env::var("APPDATA").unwrap()) 252 .join("Blender Foundation/Blender/4.2/extensions/user_default"); 253 if !data_dir.exists() { 254 fs::create_dir_all(&data_dir).unwrap(); 255 } 256 257 // Manage the blender extension folder 258 data_dir.push("emtk"); 259 if !data_dir.exists() { 260 fs::create_dir(&data_dir).unwrap(); 261 } else { 262 fs::remove_dir_all(&data_dir).unwrap(); 263 fs::create_dir(&data_dir).unwrap(); 264 } 265 266 // Copy emtk's blender extension folder to blender's extension folder 267 fn copy(source: &Path, target: &Path) { 268 for entry in source 269 .read_dir() 270 .expect("error while reading emtk blender extension folder") 271 .flatten() 272 { 273 let path = entry.path(); 274 if path.is_dir() { 275 fs::create_dir(target.join(path.file_name().unwrap())).unwrap(); 276 copy(&path, &target.join(path.file_name().unwrap())); 277 continue; 278 } else if path.is_file() { 279 fs::copy(&path, target.join(path.file_name().unwrap())).unwrap(); 280 } 281 } 282 } 283 copy( 284 &project_root.join("bindings/python/emtk-py/emtk/blender"), 285 &data_dir, 286 ); 287 println!("Re-toggle the extension inside blender"); 288 } 289 } 290 291 fn python(build_mode: BuildMode) { 292 if !cfg!(windows) { 293 return eprintln!("{}", WINDOWS_ONLY); 294 } 295 296 setup_python(); 297 298 let maturin_cmd = "maturin"; 299 let maturin_args = match build_mode { 300 BuildMode::Release => vec![ 301 "develop", 302 "-r", 303 "--uv", 304 "-m", 305 "./bindings/python/emtk-py/Cargo.toml", 306 ], 307 BuildMode::Dev => vec![ 308 "develop", 309 "--uv", 310 "-m", 311 "./bindings/python/emtk-py/Cargo.toml", 312 ], 313 }; 314 315 process::Command::new(maturin_cmd) 316 .args(&maturin_args) 317 .status() 318 .unwrap_or_else(|e| panic_command(maturin_cmd, Some(&maturin_args), e)); 319 } 320 321 /// Run all example plugins 322 fn run() { 323 let cargo = cargo_env(); 324 let project_root = project_root(); 325 let examples_path = project_root.join("examples"); 326 let exe_path = exe_path(); 327 328 let cargo_build_args = &["build", "-p", "emf"]; 329 let cargo_run_args = &["run", "-p", "emtk"]; 330 331 process::Command::new(&cargo) 332 .current_dir(project_root) 333 .args(cargo_build_args) 334 .status() 335 .unwrap_or_else(|e| panic_command(&cargo, Some(cargo_build_args), e)); 336 337 for entry in examples_path 338 .read_dir() 339 .expect("error while reading examples folder") 340 .flatten() 341 { 342 let path = entry.path(); 343 if path.is_file() { 344 continue; 345 } 346 let name = entry 347 .file_name() 348 .to_str() 349 .expect("error while getting name of entry") 350 .to_string(); 351 352 run_plugin(&name, Some(exe_path.clone())); 353 } 354 355 process::Command::new(&cargo) 356 .current_dir(project_root) 357 .args(cargo_run_args) 358 .status() 359 .unwrap_or_else(|e| panic_command(&cargo, Some(cargo_run_args), e)); 360 } 361 362 /// Run only one plugin by name 363 fn run_plugin(name: &str, exanima_exe_path: Option<PathBuf>) { 364 let cargo = cargo_env(); 365 let project_root = project_root(); 366 let example_path = project_root.join(format!("examples/{}", name)); 367 if !example_path.exists() { 368 eprintln!("\"{}\" is an invalid command\n", name); 369 print_help(); 370 return; 371 } 372 let exe_path = match exanima_exe_path.clone() { 373 Some(path) => path, 374 None => exe_path(), 375 }; 376 let build_path = project_root.join("target/debug"); 377 let plugin_path = exe_path.parent().unwrap().join(format!("mods/{}", name)); 378 379 // Skip when using "cargo xtask run" 380 if exanima_exe_path.is_none() { 381 let cargo_build_args = &["build", "-p", "emf"]; 382 process::Command::new(&cargo) 383 .current_dir(project_root) 384 .args(cargo_build_args) 385 .status() 386 .unwrap_or_else(|e| panic_command(&cargo, Some(cargo_build_args), e)); 387 } 388 389 let cargo_build_args = &["build", "-p", name]; 390 process::Command::new(&cargo) 391 .current_dir(project_root) 392 .args(cargo_build_args) 393 .status() 394 .unwrap_or_else(|e| panic_command(&cargo, Some(cargo_build_args), e)); 395 396 fs::create_dir_all(plugin_path.clone()) 397 .unwrap_or_else(|e| panic!("error while creating {} folder at mods path: {}", name, e)); 398 fs::copy( 399 build_path.join(format!("{}.dll", name.replace("-", "_"))), 400 plugin_path.join(format!("{}.dll", name)), 401 ) 402 .unwrap_or_else(|e| panic!("error while copying {} dll to mods folder: {}", name, e)); 403 // Do not overwrite config if it exists 404 if !plugin_path.join("config.toml").exists() { 405 fs::copy( 406 example_path.join("config.toml"), 407 plugin_path.join("config.toml"), 408 ) 409 .unwrap_or_else(|e| panic!("error while copying {} config to mods folder: {}", name, e)); 410 } 411 412 // Skip when using "cargo xtask run" 413 if exanima_exe_path.is_none() { 414 let cargo_run_args = &["run", "-p", "emtk"]; 415 process::Command::new(&cargo) 416 .current_dir(project_root) 417 .args(cargo_run_args) 418 .status() 419 .unwrap_or_else(|e| panic_command(&cargo, Some(cargo_run_args), e)); 420 } 421 } 422 423 /// Builds the python wheel for emtk-py. 424 /// 425 /// # Panics 426 /// 427 /// - The uv command **must** be in the PATH environment variable 428 fn wheel(build_mode: BuildMode) { 429 if !cfg!(windows) { 430 return eprintln!("{}", WINDOWS_ONLY); 431 } 432 433 setup_python(); 434 435 let project_root = project_root(); 436 let maturin_cmd = "maturin"; 437 let build_args = match build_mode { 438 BuildMode::Release => vec!["build", "-r", "-m", "./bindings/python/emtk-py/Cargo.toml"], 439 BuildMode::Dev => vec!["build", "-m", "./bindings/python/emtk-py/Cargo.toml"], 440 }; 441 442 process::Command::new(maturin_cmd) 443 .current_dir(project_root) 444 .args(&build_args) 445 .status() 446 .unwrap_or_else(|e| panic_command(maturin_cmd, Some(&build_args), e)); 447 }