shulkerscript_cli/subcommands/
build.rs1use anyhow::Result;
2use path_absolutize::Absolutize;
3use shulkerscript::{
4 base::{FsProvider, PrintHandler},
5 shulkerbox::{
6 util::compile::CompileOptions,
7 virtual_fs::{VFile, VFolder},
8 },
9};
10
11use crate::{
12 config::ProjectConfig,
13 error::Error,
14 terminal_output::{print_error, print_info, print_success, print_warning},
15 util,
16};
17use std::{
18 borrow::Cow,
19 fs,
20 path::{Path, PathBuf},
21};
22
23#[derive(Debug, clap::Args, Clone)]
24pub struct BuildArgs {
25 #[arg(default_value = ".")]
27 pub path: PathBuf,
28 #[arg(short, long, env = "DATAPACK_DIR")]
32 pub output: Option<PathBuf>,
33 #[arg(short, long)]
38 pub assets: Option<PathBuf>,
39 #[arg(short, long)]
41 pub zip: bool,
42 #[arg(long)]
44 pub no_validate: bool,
45 #[arg(long, conflicts_with_all = ["output", "zip"])]
47 pub check: bool,
48}
49
50pub fn build(args: &BuildArgs) -> Result<()> {
51 if args.zip && !cfg!(feature = "zip") {
52 print_error("The zip feature is not enabled. Please install with the `zip` feature enabled to use the `--zip` option.");
53 return Err(Error::FeatureNotEnabledError("zip".to_string()).into());
54 }
55
56 let path = util::get_project_path(&args.path).unwrap_or(args.path.clone());
57 let dist_path = args
58 .output
59 .as_ref()
60 .map(Cow::Borrowed)
61 .unwrap_or_else(|| Cow::Owned(path.join("dist")));
62
63 let and_package_msg = if args.zip { " and packaging" } else { "" };
64
65 let mut path_display = format!("{}", path.display());
66 if path_display.is_empty() {
67 path_display.push('.');
68 }
69
70 print_info(format!(
71 "Building{and_package_msg} project at {path_display}"
72 ));
73
74 let (project_config, toml_path) = get_pack_config(&path)?;
75
76 let script_paths = get_script_paths(
77 &toml_path
78 .parent()
79 .ok_or(Error::InvalidPackPathError(path.to_path_buf()))?
80 .join("src"),
81 )?;
82
83 let datapack = shulkerscript::transpile(
84 &PrintHandler::new(),
85 &FsProvider::default(),
86 project_config.pack.pack_format,
87 &script_paths,
88 )?;
89
90 if !args.no_validate && !datapack.validate() {
91 print_warning(format!(
92 "The datapack is not compatible with the specified pack format: {}",
93 project_config.pack.pack_format
94 ));
95 return Err(Error::IncompatiblePackVersionError.into());
96 }
97
98 let mut compiled = datapack.compile(&CompileOptions::default());
99
100 let icon_path = toml_path.parent().unwrap().join("pack.png");
101
102 if icon_path.is_file() {
103 if let Ok(icon_data) = fs::read(icon_path) {
104 compiled.add_file("pack.png", VFile::Binary(icon_data));
105 }
106 }
107
108 let assets_path = args.assets.clone().or(project_config
109 .compiler
110 .as_ref()
111 .and_then(|c| c.assets.as_ref().map(|p| path.join(p))));
112
113 let output = if let Some(assets_path) = assets_path {
114 let assets = VFolder::try_from(assets_path.as_path());
115 if assets.is_err() {
116 print_error(format!(
117 "The specified assets path does not exist: {}",
118 assets_path.display()
119 ));
120 }
121 let mut assets = assets?;
122 let replaced = assets.merge(compiled);
123
124 for replaced in replaced {
125 print_warning(format!(
126 "Template file {} was replaced by a file in the compiled datapack",
127 replaced
128 ));
129 }
130
131 assets
132 } else {
133 compiled
134 };
135
136 let dist_extension = if args.zip { ".zip" } else { "" };
137
138 let dist_path = dist_path.join(project_config.pack.name + dist_extension);
139
140 if args.check {
141 print_success("Project is valid and can be built.");
142 } else {
143 #[cfg(feature = "zip")]
144 if args.zip {
145 output.zip_with_comment(
146 &dist_path,
147 format!(
148 "{} - v{}",
149 &project_config.pack.description, &project_config.pack.version
150 ),
151 )?;
152 } else {
153 output.place(&dist_path)?;
154 }
155
156 #[cfg(not(feature = "zip"))]
157 output.place(&dist_path)?;
158
159 print_success(format!(
160 "Finished building{and_package_msg} project to {}",
161 dist_path.absolutize_from(path)?.display()
162 ));
163 }
164
165 Ok(())
166}
167
168pub(super) fn get_script_paths(path: &Path) -> std::io::Result<Vec<(String, PathBuf)>> {
170 _get_script_paths(path, "")
171}
172
173fn _get_script_paths(path: &Path, prefix: &str) -> std::io::Result<Vec<(String, PathBuf)>> {
174 if path.exists() && path.is_dir() {
175 let contents = path.read_dir()?;
176
177 let mut paths = Vec::new();
178
179 for entry in contents {
180 let path = entry?.path();
181 if path.is_dir() {
182 let prefix = path
183 .absolutize()?
184 .file_name()
185 .unwrap()
186 .to_str()
187 .expect("Invalid folder name")
188 .to_string()
189 + "/";
190 paths.extend(_get_script_paths(&path, &prefix)?);
191 } else if path.extension().unwrap_or_default() == "shu" {
192 paths.push((
193 prefix.to_string()
194 + path
195 .file_stem()
196 .expect("Shulkerscript files are not allowed to have empty names")
197 .to_str()
198 .expect("Invalid characters in filename"),
199 path,
200 ));
201 }
202 }
203
204 Ok(paths)
205 } else {
206 Ok(Vec::new())
207 }
208}
209
210pub(super) fn get_pack_config(path: &Path) -> Result<(ProjectConfig, PathBuf)> {
216 let path = path.absolutize()?;
217 let toml_path = if !path.exists() {
218 print_error("The specified path does not exist.");
219 return Err(Error::PathNotFoundError(path.to_path_buf()))?;
220 } else if path.is_dir() {
221 let toml_path = path.join("pack.toml");
222 if !toml_path.exists() {
223 print_error("The specified directory does not contain a pack.toml file.");
224 Err(Error::InvalidPackPathError(path.to_path_buf()))?;
225 }
226 toml_path
227 } else if path.is_file()
228 && path
229 .file_name()
230 .ok_or(Error::InvalidPackPathError(path.to_path_buf()))?
231 == "pack.toml"
232 {
233 path.to_path_buf()
234 } else {
235 print_error("The specified path is neither a directory nor a pack.toml file.");
236 return Err(Error::InvalidPackPathError(path.to_path_buf()))?;
237 };
238
239 let toml_content = fs::read_to_string(&toml_path)?;
240 let project_config = toml::from_str::<ProjectConfig>(&toml_content)?;
241
242 Ok((project_config, toml_path))
243}