cpclib_bndbuild/
task.rs

1use std::fmt::Display;
2use std::ops::{Deref, DerefMut};
3use std::str::FromStr;
4use std::sync::atomic::AtomicUsize;
5use std::sync::{Arc, LazyLock};
6
7use camino::Utf8Path;
8use cpclib_common::itertools::Itertools;
9use cpclib_runner::emucontrol::EMUCTRL_CMD;
10use cpclib_runner::runner::assembler::uz80::UZ80_CMD;
11use cpclib_runner::runner::assembler::{RASM_CMD, RasmVersion, SJASMPLUS_CMD, VASM_CMD};
12use cpclib_runner::runner::convgeneric::CONVGENERIC_CMD;
13use cpclib_runner::runner::disassembler::ExternDisassembler;
14use cpclib_runner::runner::disassembler::disark::{DISARK_CMD, DisarkVersion};
15use cpclib_runner::runner::emulator::caprice_forever::CAPRICEFOREVER_CMD;
16use cpclib_runner::runner::emulator::cpcemupower::CPCEMUPOWER_CMD;
17use cpclib_runner::runner::emulator::{
18    ACE_CMD, AMSPIRIT_CMD, CPCEC_CMD, SUGARBOX_V2_CMD, WINAPE_CMD
19};
20#[cfg(feature = "fap")]
21use cpclib_runner::runner::fap::FAP_CMD;
22use cpclib_runner::runner::hspcompiler::HSPC_CMD;
23use cpclib_runner::runner::impdisc::IMPDISC_CMD;
24use cpclib_runner::runner::martine::MARTINE_CMD;
25use cpclib_runner::runner::tracker::at3::AT_CMD;
26use cpclib_runner::runner::tracker::at3::extra::{
27    SongToAkg, SongToAkm, SongToAky, SongToEvents, SongToRaw, SongToSoundEffects, SongToVgm,
28    SongToWav, SongToYm
29};
30use cpclib_runner::runner::tracker::chipnsfx::CHIPNSFX_CMD;
31use fancy_regex::Regex;
32use serde::de::{Error, Visitor};
33use serde::{Deserialize, Deserializer};
34
35use crate::event::BndBuilderObserver;
36use crate::execute;
37use crate::runners::assembler::Assembler;
38use crate::runners::disassembler::Disassembler;
39use crate::runners::emulator::Emulator;
40use crate::runners::hideur::HIDEUR_CMD;
41use crate::runners::tracker::{SongConverter, Tracker};
42
43#[derive(Clone, Debug, PartialEq, Eq, Hash)]
44pub enum InnerTask {
45    Assembler(Assembler, StandardTaskArguments),
46    BndBuild(StandardTaskArguments),
47    Convgeneric(StandardTaskArguments),
48    Cp(StandardTaskArguments),
49    Crunch(StandardTaskArguments),
50    Disassembler(Disassembler, StandardTaskArguments),
51    Disc(StandardTaskArguments),
52    Echo(StandardTaskArguments),
53    Emulator(Emulator, StandardTaskArguments),
54    Extern(StandardTaskArguments),
55    #[cfg(feature = "fap")]
56    Fap(StandardTaskArguments),
57    Hideur(StandardTaskArguments),
58    HspCompiler(StandardTaskArguments),
59    ImgConverter(StandardTaskArguments),
60    ImpDsk(StandardTaskArguments),
61    Martine(StandardTaskArguments),
62    Mkdir(StandardTaskArguments),
63    Rm(StandardTaskArguments),
64    Snapshot(StandardTaskArguments),
65    SongConverter(SongConverter, StandardTaskArguments),
66    Tracker(Tracker, StandardTaskArguments),
67    Xfer(StandardTaskArguments)
68}
69
70#[derive(Debug, PartialEq, Eq, Hash, Clone)]
71pub struct Task {
72    inner: InnerTask,
73    id: usize
74}
75
76impl<'de> Deserialize<'de> for Task {
77    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
78    where D: Deserializer<'de> {
79        InnerTask::deserialize(deserializer).map(|t| t.into())
80    }
81}
82impl Display for Task {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        write!(f, "{}", &self.inner)
85    }
86}
87
88impl From<InnerTask> for Task {
89    fn from(value: InnerTask) -> Self {
90        Self {
91            inner: value,
92            id: Self::next_id()
93        }
94    }
95}
96
97impl Task {
98    pub fn execute<E: BndBuilderObserver + 'static>(
99        &self,
100        observer: &Arc<E>
101    ) -> Result<(), String> {
102        execute(self, observer)
103    }
104
105    fn next_id() -> usize {
106        static COUNTER: AtomicUsize = AtomicUsize::new(1);
107
108        unsafe { COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed) }
109    }
110
111    pub fn id(&self) -> usize {
112        self.id
113    }
114
115    pub fn new_basm(args: &str) -> Self {
116        InnerTask::new_basm(args).into()
117    }
118
119    pub fn new_bndbuild(args: &str) -> Self {
120        InnerTask::new_bndbuild(args).into()
121    }
122
123    pub fn new_echo(args: &str) -> Self {
124        InnerTask::new_echo(args).into()
125    }
126
127    pub fn new_imgconverter(args: &str) -> Self {
128        InnerTask::new_imgconverter(args).into()
129    }
130
131    pub fn new_rm(args: &str) -> Self {
132        InnerTask::new_rm(args).into()
133    }
134
135    pub fn set_ignore_errors(mut self, flag: bool) -> Self {
136        let new = self.inner.clone().set_ignore_errors(flag);
137        self.inner = new;
138        self
139    }
140}
141
142impl Deref for Task {
143    type Target = InnerTask;
144
145    fn deref(&self) -> &Self::Target {
146        &self.inner
147    }
148}
149
150impl DerefMut for Task {
151    fn deref_mut(&mut self) -> &mut Self::Target {
152        &mut self.inner
153    }
154}
155
156// list of keywords; do not forget to add them to bndbuild/lib.rs
157pub const EMUCTRL_CMDS: &[&str] = &[EMUCTRL_CMD, "emu", "emuctrl", "emucontrol"];
158pub const ACE_CMDS: &[&str] = &[ACE_CMD, "acedl"];
159pub const WINAPE_CMDS: &[&str] = &[WINAPE_CMD];
160pub const CPCEC_CMDS: &[&str] = &[CPCEC_CMD];
161pub const AMSPIRIT_CMDS: &[&str] = &[AMSPIRIT_CMD];
162pub const SUGARBOX_CMDS: &[&str] = &[SUGARBOX_V2_CMD];
163pub const CPCEMUPOWER_CMDS: &[&str] = &[CPCEMUPOWER_CMD];
164pub const CAPRICEFOREVER_CMDS: &[&str] = &[CAPRICEFOREVER_CMD];
165
166pub const BASM_CMDS: &[&str] = &["basm", "assemble"];
167pub const ORGAMS_CMDS: &[&str] = &["orgams"];
168pub const RASM_CMDS: &[&str] = &[RASM_CMD];
169pub const SJASMPLUS_CMDS: &[&str] = &[SJASMPLUS_CMD];
170pub const UZ80_CMDS: &[&str] = &[UZ80_CMD];
171pub const VASM_CMDS: &[&str] = &[VASM_CMD];
172
173pub const BDASM_CMDS: &[&str] = &["bdasm", "dz80"];
174pub const DISARK_CMDS: &[&str] = &[DISARK_CMD];
175
176pub const AT_CMDS: &[&str] = &[AT_CMD, "ArkosTracker3"];
177pub const CHIPNSFX_CMDS: &[&str] = &[CHIPNSFX_CMD];
178
179pub const HSPC_CMDS: &[&str] = &[HSPC_CMD, "hspc"];
180
181pub const CP_CMDS: &[&str] = &["cp", "copy"];
182pub const MKDIR_CMDS: &[&str] = &["mkdir"];
183pub const RM_CMDS: &[&str] = &["rm", "del"];
184
185pub const BNDBUILD_CMDS: &[&str] = &["bndbuild", "build"];
186pub const CONVGENERIC_CMDS: &[&str] = &[CONVGENERIC_CMD];
187pub const DISC_CMDS: &[&str] = &["dsk", "disc"];
188pub const ECHO_CMDS: &[&str] = &["echo", "print"];
189pub const EXTERN_CMDS: &[&str] = &["extern"];
190#[cfg(feature = "fap")]
191pub const FAP_CMDS: &[&str] = &[FAP_CMD];
192pub const IMG2CPC_CMDS: &[&str] = &["img2cpc", "imgconverter"];
193pub const HIDEUR_CMDS: &[&str] = &[HIDEUR_CMD];
194pub const IMPDISC_CMDS: &[&str] = &[IMPDISC_CMD, "impdisc"];
195pub const MARTINE_CMDS: &[&str] = &[MARTINE_CMD];
196pub const SNA_CMDS: &[&str] = &["sna", "snpashot"];
197pub const XFER_CMDS: &[&str] = &["xfer", "cpcwifi", "m4"];
198
199pub const CRUNCH_CMDS: &[&str] = &["crunch", "compress"];
200
201pub const SONG2AKM_CMDS: &[&str] = &[SongToAkm::CMD];
202pub const SONG2AKG_CMDS: &[&str] = &[SongToAkg::CMD];
203pub const SONG2AKY_CMDS: &[&str] = &[SongToAky::CMD];
204pub const SONG2EVENTS_CMDS: &[&str] = &[SongToEvents::CMD];
205pub const SONG2RAW_CMDS: &[&str] = &[SongToRaw::CMD];
206pub const SONG2SOUNDEFFECTS_CMDS: &[&str] = &[SongToSoundEffects::CMD];
207pub const SONG2VGM_CMDS: &[&str] = &[SongToVgm::CMD];
208pub const SONG2WAV_CMDS: &[&str] = &[SongToWav::CMD];
209pub const SONG2YM_CMDS: &[&str] = &[SongToYm::CMD];
210
211impl Display for InnerTask {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        let (cmd, s) = match self {
214            Self::Assembler(a, s) => (a.get_command(), s),
215            Self::BndBuild(s) => (BNDBUILD_CMDS[0], s),
216            Self::Convgeneric(s) => (CONVGENERIC_CMDS[0], s),
217            Self::Cp(s) => (CP_CMDS[0], s),
218            Self::Crunch(s) => (CRUNCH_CMDS[0], s),
219            Self::Disassembler(d, s) => (d.get_command(), s),
220            Self::Disc(s) => (DISC_CMDS[0], s),
221            Self::Echo(s) => (ECHO_CMDS[0], s),
222            Self::Emulator(e, s) => (e.get_command(), s),
223            Self::Extern(s) => (EXTERN_CMDS[0], s),
224            #[cfg(feature = "fap")]
225            Self::Fap(s) => (FAP_CMDS[0], s),
226            Self::Hideur(s) => (HIDEUR_CMDS[0], s),
227            Self::HspCompiler(s) => (HSPC_CMDS[0], s),
228            Self::ImgConverter(s) => (IMG2CPC_CMDS[0], s),
229            Self::ImpDsk(s) => (IMPDISC_CMDS[0], s),
230            Self::Martine(s) => (MARTINE_CMDS[0], s),
231            Self::Mkdir(s) => (MKDIR_CMDS[0], s),
232            Self::Rm(s) => (RM_CMDS[0], s),
233            Self::Snapshot(s) => (SNA_CMDS[0], s),
234            Self::SongConverter(t, s) => (t.get_command(), s),
235            Self::Tracker(t, s) => (t.get_command(), s),
236            Self::Xfer(s) => (XFER_CMDS[0], s),
237            #[cfg(feature = "fap")]
238            Self::Fap(s) => (FAP_CMDS[0], s),
239            Self::Snapshot(s) => (SNA_CMDS[0], s)
240        };
241
242        write!(
243            f,
244            "{}{} {}",
245            if s.ignore_error { "-" } else { "" },
246            cmd,
247            s.args
248        )
249    }
250}
251
252macro_rules! is_some_cmd {
253    ($($code:ident), *) => {
254        $(
255            paste::paste! {
256                #[inline]
257                pub fn [<is_ $code:lower _cmd>](code: &str) -> bool {
258                    [< $code:upper _CMDS>] .iter().contains(&code)
259                }
260            }
261        )*
262
263    };
264}
265
266#[rustfmt::skip]
267is_some_cmd!(
268    ace, amspirit, at,
269    basm, bdasm, bndbuild,
270    capriceforever, chipnsfx, convgeneric, crunch, cp, cpcec, cpcemupower,
271    disark, disc,
272    echo, emuctrl, r#extern,
273    hideur,hspc,
274    img2cpc, impdisc,
275    martine, mkdir,
276    orgams,
277    rasm, rm,
278    sjasmplus, sna, sugarbox,
279    song2akm,
280    song2akg,
281    song2aky,
282    song2events,
283    song2raw,
284    song2soundeffects,
285    song2vgm,
286    song2wav,
287    song2ym,
288    uz80,
289    vasm,
290    winape,
291    xfer
292);
293
294#[cfg(feature = "fap")]
295is_some_cmd!(fap);
296
297#[cfg(not(feature = "fap"))]
298pub fn is_fap_cmd(code: &str) -> bool {
299    false
300}
301
302impl<'de> Deserialize<'de> for InnerTask {
303    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
304    where D: Deserializer<'de> {
305        struct Line;
306        impl Visitor<'_> for Line {
307            type Value = InnerTask;
308
309            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
310            where E: serde::de::Error {
311                let (code, next) = v.split_once(" ").unwrap_or((v, ""));
312                let (code, ignore) = if code.starts_with("-") {
313                    (&code[1..], true)
314                }
315                else {
316                    (code, false)
317                };
318                let std = StandardTaskArguments {
319                    args: next.to_owned(),
320                    ignore_error: ignore
321                };
322
323                if is_ace_cmd(code) {
324                    Ok(InnerTask::Emulator(Emulator::new_ace_default(), std))
325                }
326                else if is_at_cmd(code) {
327                    Ok(InnerTask::Tracker(Tracker::new_at3_default(), std))
328                }
329                else if is_chipnsfx_cmd(code) {
330                    Ok(InnerTask::Tracker(Tracker::new_chipnsfx_default(), std))
331                }
332                else if is_song2akm_cmd(code) {
333                    Ok(InnerTask::SongConverter(
334                        SongConverter::new_song_to_akm_default(),
335                        std
336                    ))
337                }
338                else if is_song2aky_cmd(code) {
339                    Ok(InnerTask::SongConverter(
340                        SongConverter::new_song_to_aky_default(),
341                        std
342                    ))
343                }
344                else if is_song2akg_cmd(code) {
345                    Ok(InnerTask::SongConverter(
346                        SongConverter::new_song_to_akg_default(),
347                        std
348                    ))
349                }
350                else if is_song2events_cmd(code) {
351                    Ok(InnerTask::SongConverter(
352                        SongConverter::new_song_to_events_default(),
353                        std
354                    ))
355                }
356                else if is_song2raw_cmd(code) {
357                    Ok(InnerTask::SongConverter(
358                        SongConverter::new_song_to_raw_default(),
359                        std
360                    ))
361                }
362                else if is_song2soundeffects_cmd(code) {
363                    Ok(InnerTask::SongConverter(
364                        SongConverter::new_song_to_sound_effects_default(),
365                        std
366                    ))
367                }
368                else if is_song2vgm_cmd(code) {
369                    Ok(InnerTask::SongConverter(
370                        SongConverter::new_song_to_vgm_default(),
371                        std
372                    ))
373                }
374                else if is_song2wav_cmd(code) {
375                    Ok(InnerTask::SongConverter(
376                        SongConverter::new_song_to_wav_default(),
377                        std
378                    ))
379                }
380                else if is_song2ym_cmd(code) {
381                    Ok(InnerTask::SongConverter(
382                        SongConverter::new_song_to_ym_default(),
383                        std
384                    ))
385                }
386                else if is_crunch_cmd(code) {
387                    Ok(InnerTask::Crunch(std))
388                }
389                else if is_convgeneric_cmd(code) {
390                    Ok(InnerTask::Convgeneric(std))
391                }
392                else if is_cpcec_cmd(code) {
393                    Ok(InnerTask::Emulator(Emulator::new_cpcec_default(), std))
394                }
395                else if is_amspirit_cmd(code) {
396                    Ok(InnerTask::Emulator(Emulator::new_amspirit_default(), std))
397                }
398                else if is_sugarbox_cmd(code) {
399                    Ok(InnerTask::Emulator(Emulator::new_sugarbox_default(), std))
400                }
401                else if is_winape_cmd(code) {
402                    Ok(InnerTask::Emulator(Emulator::new_winape_default(), std))
403                }
404                else if is_cpcemupower_cmd(code) {
405                    Ok(InnerTask::Emulator(
406                        Emulator::new_cpcemupower_default(),
407                        std
408                    ))
409                }
410                else if is_capriceforever_cmd(code) {
411                    Ok(InnerTask::Emulator(
412                        Emulator::new_capriceforever_default(),
413                        std
414                    ))
415                }
416                else if is_emuctrl_cmd(code) {
417                    Ok(InnerTask::Emulator(Emulator::new_facade(), std))
418                }
419                else if is_basm_cmd(code) {
420                    Ok(InnerTask::Assembler(Assembler::Basm, std))
421                }
422                else if is_bdasm_cmd(code) {
423                    Ok(InnerTask::Disassembler(Disassembler::Bdasm, std))
424                }
425                else if is_disark_cmd(code) {
426                    Ok(InnerTask::Disassembler(
427                        Disassembler::Extern(ExternDisassembler::Disark(DisarkVersion::default())),
428                        std
429                    ))
430                }
431                else if is_fap_cmd(code) {
432                    #[cfg(feature = "fap")]
433                    let res = Ok(InnerTask::Fap(std));
434
435                    #[cfg(not(feature = "fap"))]
436                    let res = unreachable!();
437
438                    res
439                }
440                else if is_orgams_cmd(code) {
441                    Ok(InnerTask::Assembler(Assembler::Orgams, std))
442                }
443                else if is_rasm_cmd(code) {
444                    Ok(InnerTask::Assembler(
445                        Assembler::Extern(cpclib_runner::runner::assembler::ExternAssembler::Rasm(
446                            RasmVersion::default()
447                        )),
448                        std
449                    ))
450                }
451                else if is_uz80_cmd(code) {
452                    Ok(InnerTask::Assembler(
453                        Assembler::Extern(cpclib_runner::runner::assembler::ExternAssembler::Uz80(
454                            Default::default()
455                        )),
456                        std
457                    ))
458                }
459                else if is_sjasmplus_cmd(code) {
460                    Ok(InnerTask::Assembler(
461                        Assembler::Extern(
462                            cpclib_runner::runner::assembler::ExternAssembler::Sjasmplus(
463                                Default::default()
464                            )
465                        ),
466                        std
467                    ))
468                }
469                else if is_vasm_cmd(code) {
470                    Ok(InnerTask::Assembler(
471                        Assembler::Extern(cpclib_runner::runner::assembler::ExternAssembler::Vasm(
472                            Default::default()
473                        )),
474                        std
475                    ))
476                }
477                else if is_sna_cmd(code) {
478                    Ok(InnerTask::Snapshot(std))
479                }
480                else if is_bndbuild_cmd(code) {
481                    Ok(InnerTask::BndBuild(std))
482                }
483                else if is_disc_cmd(code) {
484                    Ok(InnerTask::Disc(std))
485                }
486                else if is_echo_cmd(code) {
487                    Ok(InnerTask::Echo(std))
488                }
489                else if is_extern_cmd(code) {
490                    Ok(InnerTask::Extern(std))
491                }
492                else if is_hideur_cmd(code) {
493                    Ok(InnerTask::Hideur(std))
494                }
495                else if is_img2cpc_cmd(code) {
496                    Ok(InnerTask::ImgConverter(std))
497                }
498                else if is_impdisc_cmd(code) {
499                    Ok(InnerTask::ImpDsk(std))
500                }
501                else if is_hspc_cmd(code) {
502                    Ok(InnerTask::HspCompiler(std))
503                }
504                else if is_martine_cmd(code) {
505                    Ok(InnerTask::Martine(std))
506                }
507                else if is_xfer_cmd(code) {
508                    Ok(InnerTask::Xfer(std))
509                }
510                else if is_cp_cmd(code) {
511                    Ok(InnerTask::Cp(std))
512                }
513                else if is_mkdir_cmd(code) {
514                    Ok(InnerTask::Mkdir(std))
515                }
516                else if is_rm_cmd(code) {
517                    Ok(InnerTask::Rm(std))
518                }
519                else {
520                    Err(Error::custom(format!("{code} is an invalid command")))
521                }
522            }
523
524            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
525                formatter.write_str("Expecting a command")
526            }
527        }
528
529        deserializer.deserialize_str(Line)
530    }
531}
532
533impl FromStr for Task {
534    type Err = String;
535
536    fn from_str(s: &str) -> Result<Self, Self::Err> {
537        InnerTask::from_str(s).map(|t| t.into())
538    }
539}
540impl FromStr for InnerTask {
541    type Err = String;
542
543    fn from_str(s: &str) -> Result<Self, Self::Err> {
544        serde_yaml::from_str(s).map_err(|e| e.to_string())
545    }
546}
547
548impl InnerTask {
549    pub fn new_basm(args: &str) -> Self {
550        Self::Assembler(Assembler::Basm, StandardTaskArguments::new(args))
551    }
552
553    pub fn new_bndbuild(args: &str) -> Self {
554        Self::BndBuild(StandardTaskArguments::new(args))
555    }
556
557    pub fn new_dsk(args: &str) -> Self {
558        Self::Disc(StandardTaskArguments::new(args))
559    }
560
561    pub fn new_rm(args: &str) -> Self {
562        Self::Rm(StandardTaskArguments::new(args))
563    }
564
565    pub fn new_echo(args: &str) -> Self {
566        Self::Echo(StandardTaskArguments::new(args))
567    }
568
569    pub fn new_imgconverter(args: &str) -> Self {
570        Self::ImgConverter(StandardTaskArguments::new(args))
571    }
572
573    pub fn replace_automatic_variables(
574        &mut self,
575        first_dep: Option<&Utf8Path>,
576        first_tgt: Option<&Utf8Path>
577    ) -> Result<(), String> {
578        self.standard_task_arguments_mut()
579            .replace_automatic_variables(first_dep, first_tgt)
580    }
581
582    fn standard_task_arguments(&self) -> &StandardTaskArguments {
583        match self {
584            InnerTask::Assembler(_, t)
585            | InnerTask::BndBuild(t)
586            | InnerTask::Convgeneric(t)
587            | InnerTask::Crunch(t)
588            | InnerTask::Cp(t)
589            | InnerTask::Disassembler(_, t)
590            | InnerTask::Disc(t)
591            | InnerTask::ImpDsk(t)
592            | InnerTask::Echo(t)
593            | InnerTask::Extern(t)
594            | InnerTask::Hideur(t)
595            | InnerTask::HspCompiler(t)
596            | InnerTask::ImgConverter(t)
597            | InnerTask::Martine(t)
598            | InnerTask::Mkdir(t)
599            | InnerTask::Rm(t)
600            | InnerTask::Xfer(t)
601            | InnerTask::Emulator(_, t)
602            | InnerTask::Snapshot(t)
603            | InnerTask::SongConverter(_, t)
604            | InnerTask::Tracker(_, t) => t,
605            #[cfg(feature = "fap")]
606            InnerTask::Fap(t) => t
607        }
608    }
609
610    fn standard_task_arguments_mut(&mut self) -> &mut StandardTaskArguments {
611        match self {
612            InnerTask::Assembler(_, t)
613            | InnerTask::BndBuild(t)
614            | InnerTask::Convgeneric(t)
615            | InnerTask::Crunch(t)
616            | InnerTask::Cp(t)
617            | InnerTask::Disassembler(_, t)
618            | InnerTask::Disc(t)
619            | InnerTask::Echo(t)
620            | InnerTask::Emulator(_, t)
621            | InnerTask::Extern(t)
622            | InnerTask::Hideur(t)
623            | InnerTask::HspCompiler(t)
624            | InnerTask::ImgConverter(t)
625            | InnerTask::ImpDsk(t)
626            | InnerTask::BndBuild(t)
627            | InnerTask::Martine(t)
628            | InnerTask::Mkdir(t)
629            | InnerTask::Rm(t)
630            | InnerTask::Snapshot(t)
631            | InnerTask::SongConverter(_, t)
632            | InnerTask::Tracker(_, t)
633            | InnerTask::Xfer(t) => t,
634            #[cfg(feature = "fap")]
635            InnerTask::Fap(t) => t
636        }
637    }
638
639    pub fn args(&self) -> &str {
640        &self.standard_task_arguments().args
641    }
642
643    pub fn ignore_errors(&self) -> bool {
644        self.standard_task_arguments().ignore_error
645    }
646
647    pub fn set_ignore_errors(mut self, ignore: bool) -> Self {
648        self.standard_task_arguments_mut().ignore_error = ignore;
649        self
650    }
651
652    // TODO deeply check the arguments of the commands because here we may be wrong ...
653    pub fn is_phony(&self) -> bool {
654        match self {
655            InnerTask::Assembler(..) => false, // wrong when displaying stuff
656            InnerTask::BndBuild(_) => false,
657            InnerTask::Convgeneric(_) => false,
658            InnerTask::Cp(_) => false,
659            InnerTask::Crunch(_) => false,
660            InnerTask::Disassembler(..) => false,
661            InnerTask::Disc(_) => false,
662            InnerTask::Echo(_) => true,
663            InnerTask::Emulator(..) => true,
664            InnerTask::Extern(_) => false,
665            #[cfg(feature = "fap")]
666            InnerTask::Fap(..) => true,
667            InnerTask::Hideur(_) => false,
668            InnerTask::HspCompiler(_) => false,
669            InnerTask::ImgConverter(_) => false,
670            InnerTask::ImpDsk(_) => false,
671            InnerTask::Martine(t) => false,
672            InnerTask::Mkdir(_) => false,
673            InnerTask::Rm(_s) => false, // wrong when downloading files
674            InnerTask::Snapshot(_) => false,
675            InnerTask::SongConverter(_, t) => false,
676            InnerTask::Tracker(_, t) => true, // XXX think if false is better
677            InnerTask::Xfer(_) => true
678        }
679    }
680}
681
682#[derive(Deserialize, Clone, PartialEq, Debug, Eq, Hash)]
683pub struct StandardTaskArguments {
684    args: String,
685    ignore_error: bool
686}
687
688impl StandardTaskArguments {
689    pub fn new<S: Into<String>>(args: S) -> Self {
690        Self {
691            args: args.into(),
692            ignore_error: false
693        }
694    }
695
696    /// This method modify the args to replace automatic variables by the expected values
697    /// TODO keep the original argument for display and error purposes ?
698    fn replace_automatic_variables(
699        &mut self,
700        first_dep: Option<&Utf8Path>,
701        first_tgt: Option<&Utf8Path>
702    ) -> Result<(), String> {
703        static RE_FIRST_DEP: LazyLock<Regex> =
704            LazyLock::new(|| Regex::new(r"\${1}(?!\$)<").unwrap()); // 1 repetition does not seem to work :(
705        static RE_FIRST_TGT: LazyLock<Regex> =
706            LazyLock::new(|| Regex::new(r"\${1}(?!\$)@").unwrap());
707
708        let initial = self.args.clone();
709
710        if let Some(first_dep) = first_dep {
711            self.args = RE_FIRST_DEP
712                .replace_all(&self.args, first_dep.as_str())
713                .into_owned();
714        }
715        else if RE_FIRST_DEP.is_match(&self.args).unwrap() {
716            self.args = initial;
717            return Err(format!(
718                "{} contains $<, but there are no available dependencies.",
719                self.args
720            ));
721        }
722
723        if let Some(first_tgt) = first_tgt {
724            self.args = RE_FIRST_TGT
725                .replace_all(&self.args, first_tgt.as_str())
726                .into_owned();
727        }
728        else if RE_FIRST_TGT.is_match(&self.args).unwrap() {
729            self.args = initial;
730            return Err(format!(
731                "{} contains $@, but there are no available targets.",
732                self.args
733            ));
734        }
735        Ok(())
736    }
737}
738
739#[cfg(test)]
740mod test {
741    use super::InnerTask;
742    use crate::task::StandardTaskArguments;
743
744    #[test]
745    fn test_automatic_arguments() {
746        // no replacement expected
747        let mut no_args = StandardTaskArguments::new("a b");
748        assert!(dbg!(
749            no_args.replace_automatic_variables(None, None).is_ok()
750        ));
751        assert_eq!(no_args.args, "a b");
752
753        let mut no_args = StandardTaskArguments::new("a b");
754        assert!(dbg!(
755            no_args
756                .replace_automatic_variables(Some("a".into()), None)
757                .is_ok()
758        ));
759        assert_eq!(no_args.args, "a b");
760
761        let mut no_args = StandardTaskArguments::new("a b");
762        assert!(dbg!(
763            no_args
764                .replace_automatic_variables(None, Some("b".into()))
765                .is_ok()
766        ));
767        assert_eq!(no_args.args, "a b");
768
769        let mut no_args = StandardTaskArguments::new("a b");
770        assert!(dbg!(
771            no_args
772                .replace_automatic_variables(Some("a".into()), Some("b".into()))
773                .is_ok()
774        ));
775        assert_eq!(no_args.args, "a b");
776
777        // tgt replacement expected
778        let mut no_args = StandardTaskArguments::new("$@ b");
779        assert!(dbg!(
780            no_args.replace_automatic_variables(None, None).is_err()
781        ));
782        assert_eq!(no_args.args, "$@ b");
783
784        let mut no_args = StandardTaskArguments::new("$@ b");
785        assert!(dbg!(
786            no_args
787                .replace_automatic_variables(Some("a".into()), None)
788                .is_err()
789        ));
790        assert_eq!(no_args.args, "$@ b");
791
792        let mut no_args = StandardTaskArguments::new("$@ b");
793        assert!(dbg!(
794            no_args
795                .replace_automatic_variables(None, Some("b".into()))
796                .is_ok()
797        ));
798        assert_eq!(no_args.args, "b b");
799
800        let mut no_args = StandardTaskArguments::new("$@ b");
801        assert!(dbg!(
802            no_args
803                .replace_automatic_variables(Some("a".into()), Some("b".into()))
804                .is_ok()
805        ));
806        assert_eq!(no_args.args, "b b");
807
808        // tgt and dep replacements expected
809        let mut no_args = StandardTaskArguments::new("$@ $<");
810        assert!(dbg!(
811            no_args.replace_automatic_variables(None, None).is_err()
812        ));
813        assert_eq!(no_args.args, "$@ $<");
814
815        let mut no_args = StandardTaskArguments::new("$@ $<");
816        assert!(dbg!(
817            no_args
818                .replace_automatic_variables(Some("a".into()), None)
819                .is_err()
820        ));
821        assert_eq!(no_args.args, "$@ $<");
822
823        let mut no_args = StandardTaskArguments::new("$@ $<");
824        assert!(dbg!(
825            no_args
826                .replace_automatic_variables(None, Some("b".into()))
827                .is_err()
828        ));
829        assert_eq!(no_args.args, "$@ $<");
830
831        let mut no_args = StandardTaskArguments::new("$@ $<");
832        assert!(dbg!(
833            no_args
834                .replace_automatic_variables(Some("a".into()), Some("b".into()))
835                .is_ok()
836        ));
837        assert_eq!(no_args.args, "b a");
838
839        // duplicated $ change nothing
840        //        this onefails but i do not understand why
841        // let mut no_args = StandardTaskArguments::new("$$@ $$<");
842        // assert!(dbg!(no_args.replace_automatic_variables(Some("a".into()), Some("b".into())).is_ok()));
843        // assert_eq!(no_args.args, "$$@ $$<");
844        //
845    }
846
847    #[test]
848    fn test_deserialize_task() {
849        let yaml = "basm toto.asm -o toto.o";
850        let task: InnerTask = serde_yaml::from_str(yaml).unwrap();
851        assert_eq!(
852            task,
853            InnerTask::Assembler(
854                crate::runners::assembler::Assembler::Basm,
855                StandardTaskArguments {
856                    args: "toto.asm -o toto.o".to_owned(),
857                    ignore_error: false
858                }
859            )
860        );
861
862        let yaml = "-basm toto.asm -o toto.o";
863        let task: InnerTask = serde_yaml::from_str(yaml).unwrap();
864        assert_eq!(
865            task,
866            InnerTask::Assembler(
867                crate::runners::assembler::Assembler::Basm,
868                StandardTaskArguments {
869                    args: "toto.asm -o toto.o".to_owned(),
870                    ignore_error: true
871                }
872            )
873        );
874    }
875}