/ crates / xtask / src / main.rs
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  }