1#![feature(cfg_select)]
2#![feature(closure_lifetime_binder)]
3
4use std::env::current_dir;
5use std::sync::OnceLock;
6
7use app::BndBuilderApp;
8use clap_complete::Shell;
9use cpclib_common::camino::{Utf8Path, Utf8PathBuf};
10use cpclib_common::clap;
11use cpclib_common::clap::*;
12use cpclib_common::itertools::Itertools;
13use lazy_regex::regex_captures;
14#[cfg(feature = "fap")]
15use task::FAP_CMDS;
16use task::{
17 ACE_CMDS, AMSPIRIT_CMDS, AT_CMDS, BASM_CMDS, BDASM_CMDS, BNDBUILD_CMDS, CAPRICEFOREVER_CMDS,
18 CHIPNSFX_CMDS, CONVGENERIC_CMDS, CP_CMDS, CPCEC_CMDS, CPCEMUPOWER_CMDS, DISARK_CMDS, DISC_CMDS,
19 ECHO_CMDS, EMUCTRL_CMDS, EXTERN_CMDS, HIDEUR_CMDS, HSPC_CMDS, IMG2CPC_CMDS, IMPDISC_CMDS,
20 MARTINE_CMDS, ORGAMS_CMDS, RASM_CMDS, RM_CMDS, SJASMPLUS_CMDS, SONG2AKM_CMDS, SUGARBOX_CMDS,
21 UZ80_CMDS, VASM_CMDS, WINAPE_CMDS, XFER_CMDS
22};
23use thiserror::Error;
24
25pub use crate::BndBuilder;
26use crate::event::BndBuilderObserverRc;
27use crate::executor::*;
28
29pub mod app;
30pub mod builder;
31pub mod constraints;
32pub mod event;
33pub mod executor;
34pub mod rules;
35pub mod runners;
36pub mod task;
37
38pub use builder::*;
39pub use cpclib_common;
40
41pub mod built_info {
42 include!(concat!(env!("OUT_DIR"), "/built.rs"));
43}
44
45pub fn process_matches(matches: &ArgMatches) -> Result<(), BndBuilderError> {
46 let cmd = BndBuilderApp::from_matches(matches.clone());
47 cmd.command()?.execute()
48}
49
50pub fn process_matches_with_observer(
51 matches: &ArgMatches,
52 o: BndBuilderObserverRc
53) -> Result<(), BndBuilderError> {
54 let mut cmd = BndBuilderApp::from_matches(matches.clone());
55 cmd.add_observer(o);
56 cmd.command()?.execute()
57}
58
59pub const ALL_APPLICATIONS: &[(&[&str], bool)] = &[
60 (ACE_CMDS, true), (AMSPIRIT_CMDS, true),
62 (AT_CMDS, true),
63 (BASM_CMDS, false),
64 (BDASM_CMDS, false),
65 (BNDBUILD_CMDS, false),
66 (CHIPNSFX_CMDS, true),
67 (CONVGENERIC_CMDS, true),
68 (CP_CMDS, false),
69 (CPCEC_CMDS, true),
70 (CPCEMUPOWER_CMDS, true),
71 (CAPRICEFOREVER_CMDS, true),
72 (DISC_CMDS, false),
73 (DISARK_CMDS, true),
74 (ECHO_CMDS, false),
75 (EMUCTRL_CMDS, false),
76 (EXTERN_CMDS, false),
77 #[cfg(feature = "fap")]
78 (FAP_CMDS, true),
79 (HIDEUR_CMDS, false),
80 (HSPC_CMDS, true),
81 (IMG2CPC_CMDS, false),
82 (IMPDISC_CMDS, true),
83 (MARTINE_CMDS, true),
84 (ORGAMS_CMDS, false),
85 (RASM_CMDS, true),
86 (RM_CMDS, false),
87 (SJASMPLUS_CMDS, true),
88 (SONG2AKM_CMDS, true),
89 (SUGARBOX_CMDS, true),
90 (UZ80_CMDS, true),
91 (VASM_CMDS, true),
92 (WINAPE_CMDS, true),
93 (XFER_CMDS, false)
94];
95
96pub fn commands_list() -> &'static (Vec<&'static str>, Vec<&'static str>) {
97 static COMMANDS_LIST: OnceLock<(Vec<&str>, Vec<&str>)> = OnceLock::new();
98 COMMANDS_LIST.get_or_init(|| {
99 let all_applications = ALL_APPLICATIONS;
100
101 let mut all = Vec::with_capacity(all_applications.iter().map(|l| l.0.len()).sum());
102 let mut clearable = Vec::with_capacity(
103 all_applications
104 .iter()
105 .map(|l| if l.1 { l.0.len() } else { 0 })
106 .sum()
107 );
108 for l in all_applications.iter() {
109 all.extend_from_slice(l.0);
110 if l.1 {
111 clearable.extend_from_slice(l.0);
112 }
113 }
114
115 all.sort();
116 clearable.sort();
117
118 (all, clearable)
119 })
120}
121
122pub fn build_args_parser() -> clap::Command {
123 static COMMANDS_LIST: OnceLock<(Vec<&str>, Vec<&str>)> = OnceLock::new();
124 let (commands_list, clearable_list) = commands_list();
125 let updatable_list = clearable_list;
126
127 let cmd = Command::new("bndbuilder")
128 .about("Benediction CPC demo project builder")
129 .before_help("Can be used as a project builder similar to Make, but using a yaml project description, or can be used as any Benediction crossdev tool (basm, img2cpc, xfer, disc). This way only bndbuild needs to be installed.")
130 .author("Krusty/Benediction")
131 .version(built_info::PKG_VERSION)
132 .disable_help_flag(true)
133 .disable_version_flag(true)
134 ;
135
136 #[cfg(feature = "self-update")]
137 let cmd = cmd.arg(
138 Arg::new("update")
139 .long("update")
140 .short('u')
141 .num_args(0..=1)
142 .value_parser(updatable_list.iter().chain(&["self", "all", "installed"]).collect_vec())
143 .help("Update (or install) bndbuild or a given embedded application if provided. There are specific cases: if `all` is provided, it update all applications and bndbuild itself, if `installed` is provided it update only installed applications, if `self` is provide, it updates bndbuild itself.")
144 .exclusive(true)
145 );
146
147 let cmd = cmd
148 .arg(
149 Arg::new("help")
150 .long("help")
151 .short('h')
152 .value_name("CMD")
153 .value_parser(commands_list.clone())
154 .default_missing_value_os("bndbuild")
155 .default_value("bndbuild")
156 .num_args(0..=1)
157 .help("Show the help of the given subcommand CMD.")
158 )
159 .arg(
160 Arg::new("direct")
161 .action(ArgAction::SetTrue)
162 .long("direct")
163 .help(format!("Bypass the task file and directly execute a command along: [{}].", commands_list.iter().join(", ")))
164 .conflicts_with_all(["list", "init", "add"])
165 )
166 .arg(
167 Arg::new("version")
168 .long("version")
169 .short('V')
170 .help("Print version")
171 .action(ArgAction::SetTrue)
172 )
173 .arg(
174 Arg::new("dot")
175 .long("dot")
176 .alias("grapĥviz")
177 .num_args(0..=1)
178 .value_hint(ValueHint::FilePath)
179 .help("Generate the graphviz representation of the selected bndbuild.yml file. If no file is provided, it prints the .dot representation. Otherwise it saves it on disc (only .dot, .png and .svg are possible. dot command MUST be installed and available in PATH)")
180 )
181 .arg(
182 Arg::new("show")
183 .long("show")
184 .help("Show the file AFTER interpreting the templates")
185 .action(ArgAction::SetTrue)
186 .conflicts_with("dot")
187 )
188 .arg(
189 Arg::new("file")
190 .short('f')
191 .long("file")
192 .action(ArgAction::Set)
193 .value_name("FILE")
194 .value_hint(ValueHint::FilePath)
195 .help("Provide the YAML file for the given project.")
196 )
197 .arg(
198 Arg::new("watch")
199 .short('w')
200 .long("watch")
201 .action(ArgAction::SetTrue)
202 .help("Watch the targets and permanently rebuild them when needed.")
203 .conflicts_with_all(["dot", "show"])
204 )
205 .arg(
206 Arg::new("list")
207 .short('l')
208 .long("list")
209 .action(ArgAction::SetTrue)
210 .help("List the available targets")
211 .conflicts_with("dot")
212 )
213 .arg(
214 Arg::new("DEFINE_SYMBOL")
215 .help("Provide a symbol with its value (default set to 1)")
216 .long("define")
217 .short('D')
218 .action(ArgAction::Append)
219 .number_of_values(1)
220 )
221 .arg(
222 Arg::new("clear")
223 .long("clear-cache")
224 .alias("clear")
225 .short('c')
226 .num_args(0..=1)
227 .value_parser(clearable_list.clone())
228 .help("Clear cache folder that contains all automatically downloaded executables. Can optionally take one argument to clear the cache of the corresponding executable.")
229 .exclusive(true)
230 )
231 .arg(
232 Arg::new("init")
233 .long("init")
234 .action(ArgAction::SetTrue)
235 .help("Init a new project by creating it")
236 .conflicts_with("dot")
237 )
238 .arg(
239 Arg::new("add")
240 .long("add")
241 .short('a')
242 .help("Add a new basm target in an existing bndbuild.yml (or create it)")
243 .conflicts_with("dot")
244 .action(ArgAction::Set)
245 )
246 .arg(
247 Arg::new("dep")
248 .help("The source files")
249 .long("dep")
250 .short('d')
251 .requires("add")
252 )
253 .arg(
254 Arg::new("kind")
255 .help("The kind of command to be added in the yaml file")
256 .long("kind")
257 .short('k')
258 .value_parser(commands_list.clone())
259 .requires("add")
260 .default_missing_value("basm")
261 )
262
263 .arg(
264 Arg::new("target")
265 .action(ArgAction::Append)
266 .value_name("TARGET")
267 .help("Provide the target(s) to run.")
268 .conflicts_with_all(["list", "init", "add"])
269 );
270
271 cmd.arg(
274 Arg::new("completion")
275 .long("completion")
276 .action(ArgAction::Set)
277 .help("Generate autocompletion configuration")
278 .value_parser(value_parser!(Shell))
279 )
280}
281
282pub fn init_project(path: Option<&Utf8Path>) -> Result<(), BndBuilderError> {
283 let path = path
284 .map(|p| p.to_owned())
285 .unwrap_or_else(|| Utf8PathBuf::from_path_buf(current_dir().unwrap()).unwrap());
286
287 if !path.is_dir() {
288 return Err(BndBuilderError::AnyError(format!(
289 "{path} is not a valid directory"
290 )));
291 }
292
293 let bndbuild_yml = path.join("bndbuild.yml");
294 if bndbuild_yml.exists() {
295 return Err(BndBuilderError::AnyError(format!(
296 "{bndbuild_yml} already exists"
297 )));
298 }
299
300 let main_asm = path.join("main.asm");
301 if main_asm.exists() {
302 return Err(BndBuilderError::AnyError(format!(
303 "{main_asm} already exists"
304 )));
305 }
306
307 let data_asm = path.join("data.asm");
308 if main_asm.exists() {
309 return Err(BndBuilderError::AnyError(format!(
310 "{data_asm} already exists"
311 )));
312 }
313
314 std::fs::write(&bndbuild_yml, include_bytes!("default_bndbuild.yml"))
315 .map_err(|e| BndBuilderError::AnyError(e.to_string()))?;
316
317 std::fs::write(&main_asm, include_bytes!("default_main.asm"))
318 .map_err(|e| BndBuilderError::AnyError(e.to_string()))?;
319
320 std::fs::write(&data_asm, include_bytes!("default_data.asm"))
321 .map_err(|e| BndBuilderError::AnyError(e.to_string()))?;
322
323 Ok(())
324}
325
326fn expand_glob(p: &str) -> Vec<String> {
330 let expended = if let Some((_, start, middle, end)) = regex_captures!(r"^(.*)\{(.*)\}(.*)$", p)
331 {
332 middle
333 .split(",")
334 .map(|component| format!("{start}{component}{end}"))
335 .collect_vec()
336 }
337 else {
338 vec![p.to_owned()]
339 };
340
341 expended
342 .into_iter()
343 .flat_map(|p| {
344 globmatch::Builder::new(p.as_str())
345 .build("." )
346 .map(|builder| {
347 builder
348 .into_iter()
349 .map(|p2| {
350 match p2 {
351 Ok(p) => {
352 let p = Utf8PathBuf::from_path_buf(p).unwrap();
353 let s = p.to_string();
354 if s.starts_with(".\\") {
355 s[2..].to_owned()
356 }
357 else {
358 s
359 }
360 },
361 Err(_e) => p.clone()
362 }
363 })
364 .collect_vec()
365 })
366 .map(|v| if v.is_empty() { vec![p.clone()] } else { v })
367 .unwrap_or(vec![p])
368 })
369 .collect_vec()
370}
371
372#[derive(Error, Debug)]
373pub enum BndBuilderError {
374 #[error("Unable to access file {fname}: {error}.")]
375 InputFileError {
376 fname: String,
377 error: std::io::Error
378 },
379 #[error("Unable to setup working directory {fname}: {error}.")]
380 WorkingDirectoryError {
381 fname: String,
382 error: std::io::Error
383 },
384 #[error("Unable to deserialize rules {0}.")]
385 ParseError(serde_yaml::Error),
386 #[error("Unable to build the dependency graph {0}.")]
387 DependencyError(String),
388 #[error("Unable to build {fname}: {msg}.")]
389 ExecuteError { fname: String, msg: String },
390 #[error("Unable to build default target.\n{source}")]
391 DefaultTargetError { source: Box<BndBuilderError> },
392 #[error("The file does not contain a target.")]
393 NoTargets,
394 #[error("The target {0} is disabled.")]
395 DisabledTarget(String),
396 #[error("Target {0} is not buildable.")]
397 UnknownTarget(String),
398 #[error("{0}")]
399 AnyError(String),
400 #[cfg(feature = "self-update")]
401 #[error("Self-update error: {0}")]
402 SelfUpdateError(self_update::errors::Error),
403 #[error("Udate error: {0}")]
404 UpdateError(String)
405}
406
407#[cfg(feature = "self-update")]
408impl From<self_update::errors::Error> for BndBuilderError {
409 fn from(value: self_update::errors::Error) -> Self {
410 Self::SelfUpdateError(value)
411 }
412}