use color_eyre::eyre::{Report, Result}; use path_absolutize::Absolutize; use shulkerbox::virtual_fs::{VFile, VFolder}; use crate::{ config::ProjectConfig, error::Error, terminal_output::{print_error, print_info, print_success, print_warning}, }; use std::{ borrow::Cow, fs, path::{Path, PathBuf}, }; #[derive(Debug, clap::Args, Clone)] pub struct BuildArgs { /// The path of the project to build. #[clap(default_value = ".")] pub path: PathBuf, /// The path of the directory to place the compiled datapack. #[clap(short, long, env = "DATAPACK_DIR")] pub output: Option, /// The path of a folder which files and subfolders will be copied to the root of the datapack. /// Overrides the `assets` field in the pack.toml file. #[clap(short, long)] pub assets: Option, /// Whether to package the project to a zip file. #[clap(short, long)] pub zip: bool, } impl Default for BuildArgs { fn default() -> Self { Self { path: PathBuf::from("."), output: None, assets: None, zip: false, } } } pub fn build(_verbose: bool, args: &BuildArgs) -> Result<()> { if args.zip && !cfg!(feature = "zip") { print_error("The zip feature is not enabled. Please install with the `zip` feature enabled to use the `--zip` option."); return Err(Report::from(Error::FeatureNotEnabledError( "zip".to_string(), ))); } let path = args.path.as_path(); let dist_path = args .output .as_ref() .map(Cow::Borrowed) .unwrap_or_else(|| Cow::Owned(path.join("dist"))); let and_package_msg = if args.zip { " and packaging" } else { "" }; print_info(format!( "Building{and_package_msg} project at {}", path.absolutize()?.display() )); let (project_config, toml_path) = get_pack_config(path)?; let script_paths = get_script_paths( &toml_path .parent() .ok_or(Error::InvalidPackPathError(path.to_path_buf()))? .join("src"), )?; let mut compiled = shulkerscript::compile(&script_paths)?; let icon_path = toml_path.parent().unwrap().join("pack.png"); if icon_path.is_file() { if let Ok(icon_data) = fs::read(icon_path) { compiled.add_file("pack.png", VFile::Binary(icon_data)); } } let assets_path = args.assets.clone().or(project_config .compiler .as_ref() .and_then(|c| c.assets.as_ref().map(|p| path.join(p)))); let output = if let Some(assets_path) = assets_path { let assets = VFolder::try_from(assets_path.as_path()); if assets.is_err() { print_error(format!( "The specified assets path does not exist: {}", assets_path.display() )); } let mut assets = assets?; let replaced = assets.merge(compiled); for replaced in replaced { print_warning(format!( "Template file {} was replaced by a file in the compiled datapack", replaced )); } assets } else { compiled }; let dist_extension = if args.zip { ".zip" } else { "" }; let dist_path = dist_path.join(project_config.pack.name + dist_extension); #[cfg(feature = "zip")] if args.zip { output.zip(&dist_path)?; } else { output.place(&dist_path)?; } #[cfg(not(feature = "zip"))] output.place(&dist_path)?; print_success(format!( "Finished building{and_package_msg} project to {}", dist_path.absolutize_from(path)?.display() )); Ok(()) } /// Recursively get all script paths in a directory. pub(super) fn get_script_paths(path: &Path) -> std::io::Result> { _get_script_paths(path, "") } fn _get_script_paths(path: &Path, prefix: &str) -> std::io::Result> { if path.exists() && path.is_dir() { let contents = path.read_dir()?; let mut paths = Vec::new(); for entry in contents { let path = entry?.path(); if path.is_dir() { let prefix = path .absolutize()? .file_name() .unwrap() .to_str() .expect("Invalid folder name") .to_string() + "/"; paths.extend(_get_script_paths(&path, &prefix)?); } else if path.extension().unwrap_or_default() == "shu" { paths.push(( prefix.to_string() + path .file_stem() .expect("ShulkerScript files are not allowed to have empty names") .to_str() .expect("Invalid characters in filename"), path, )); } } Ok(paths) } else { Ok(Vec::new()) } } /// Get the pack config and config path from a project path. /// /// # Errors /// - If the specified path does not exist. /// - If the specified directory does not contain a pack.toml file. pub(super) fn get_pack_config(path: &Path) -> Result<(ProjectConfig, PathBuf)> { let toml_path = if !path.exists() { print_error("The specified path does not exist."); return Err(Error::PathNotFoundError(path.to_path_buf()))?; } else if path.is_dir() { let toml_path = path.join("pack.toml"); if !toml_path.exists() { print_error("The specified directory does not contain a pack.toml file."); Err(Error::InvalidPackPathError(path.to_path_buf()))?; } toml_path } else if path.is_file() && path .file_name() .ok_or(Error::InvalidPackPathError(path.to_path_buf()))? == "pack.toml" { path.to_path_buf() } else { print_error("The specified path is neither a directory nor a pack.toml file."); return Err(Error::InvalidPackPathError(path.to_path_buf()))?; }; let toml_content = fs::read_to_string(&toml_path)?; let project_config = toml::from_str::(&toml_content)?; Ok((project_config, toml_path)) }