cpclib_bndbuild/
lib.rs

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), // true for clearable, false for others
61    (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    // TODO use query_shell https://crates.io/crates/query-shell to get the proper shell
272
273    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
326/// Expand glob patterns
327/// {a,b} expension is always done even if file does not exists
328/// *.a is done only when file exists
329fn 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("." /* std::env::current_dir().unwrap() */)
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}