cli/descriptor/
mod.rs

1//! # descriptor
2//!
3//! Loads the tasks descriptor.<br>
4//! It will first load the default descriptor which is defined in cargo-make
5//! internally and afterwards tries to find the external descriptor and load it
6//! as well.<br> If an external descriptor exists, it will be loaded and extend
7//! the default descriptor.
8
9#[cfg(test)]
10#[path = "mod_test.rs"]
11mod mod_test;
12
13mod cargo_alias;
14pub(crate) mod descriptor_deserializer;
15mod env;
16mod makefiles;
17
18use crate::descriptor::env::{merge_env, merge_env_files, merge_env_scripts};
19use crate::environment;
20use crate::error::CargoMakeError;
21use crate::plugin::descriptor::merge_plugins_config;
22use crate::types::{
23    Config, ConfigSection, EnvFile, EnvFileInfo, EnvValue, Extend, ExternalConfig, ModifyConfig,
24    Task,
25};
26use crate::{io, scriptengine, version};
27use fsio::path::as_path::AsPath;
28use fsio::path::from_path::FromPath;
29use indexmap::IndexMap;
30use std::path::{Path, PathBuf};
31
32#[derive(Debug)]
33enum RelativeTo {
34    Makefile,
35    GitRoot,
36    CrateRoot,
37    WorkspaceRoot,
38}
39
40fn merge_tasks(
41    base: &mut IndexMap<String, Task>,
42    extended: &mut IndexMap<String, Task>,
43    merge_task_env: bool,
44) -> IndexMap<String, Task> {
45    let mut merged = IndexMap::<String, Task>::new();
46
47    for (key, value) in base.iter() {
48        let key_str = key.to_string();
49        merged.insert(key_str, value.clone());
50    }
51
52    for (key, value) in extended.iter() {
53        let key_str = key.to_string();
54        let mut task = value.clone();
55
56        task = match base.get(key) {
57            Some(ref value) => {
58                let mut merged_task = Task::new();
59
60                merged_task.extend(value);
61                merged_task.extend(&task);
62
63                if merge_task_env && value.env.is_some() && task.env.is_some() {
64                    let extended_env = task.env.clone().unwrap();
65                    let clear = task.clear.clone().unwrap_or(false);
66                    if extended_env.len() == 2
67                        && extended_env.contains_key("CARGO_MAKE_CURRENT_TASK_INITIAL_MAKEFILE")
68                        && extended_env
69                            .contains_key("CARGO_MAKE_CURRENT_TASK_INITIAL_MAKEFILE_DIRECTORY")
70                        && !clear
71                    {
72                        let base_env = value.env.clone().unwrap();
73                        merged_task.env = Some(base_env);
74                    }
75                }
76
77                merged_task
78            }
79            _ => task,
80        };
81
82        merged.insert(key_str, task);
83    }
84
85    merged
86}
87
88fn add_file_location_info(
89    mut external_config: ExternalConfig,
90    file_path_string: &str,
91) -> ExternalConfig {
92    let file_path = file_path_string.as_path();
93    let base_directory = match file_path.parent() {
94        Some(directory) => FromPath::from_path(directory),
95        None => "".to_string(),
96    };
97
98    match external_config.env_files {
99        Some(env_files) => {
100            let mut modified_env_files = vec![];
101
102            for env_file in env_files {
103                match env_file {
104                    EnvFile::Path(path) => {
105                        let mut info = EnvFileInfo::new(path);
106                        info.base_path = Some(base_directory.clone());
107
108                        modified_env_files.push(EnvFile::Info(info));
109                    }
110                    EnvFile::Info(mut info) => {
111                        if info.base_path.is_none() {
112                            info.base_path = Some(base_directory.clone());
113                        }
114
115                        modified_env_files.push(EnvFile::Info(info));
116                    }
117                }
118            }
119
120            external_config.env_files = Some(modified_env_files);
121        }
122        None => (),
123    };
124
125    let mut tasks_map = IndexMap::new();
126    if let Some(tasks) = external_config.tasks.clone() {
127        for (task_name, task) in tasks {
128            let mut env = match task.env.clone() {
129                Some(env) => env,
130                None => IndexMap::new(),
131            };
132
133            env.insert(
134                "CARGO_MAKE_CURRENT_TASK_INITIAL_MAKEFILE".to_string(),
135                EnvValue::Value(file_path_string.to_string()),
136            );
137            env.insert(
138                "CARGO_MAKE_CURRENT_TASK_INITIAL_MAKEFILE_DIRECTORY".to_string(),
139                EnvValue::Value(base_directory.to_string()),
140            );
141
142            let mut updated_task = task.clone();
143            updated_task.env = Some(env);
144            tasks_map.insert(task_name, updated_task);
145        }
146
147        external_config.tasks = Some(tasks_map);
148    };
149
150    external_config
151}
152
153fn run_load_script(external_config: &ExternalConfig) -> Result<bool, CargoMakeError> {
154    match external_config.config {
155        Some(ref config) => {
156            let load_script = config.get_load_script();
157
158            match load_script {
159                Some(ref script) => {
160                    debug!("Load script found.");
161
162                    scriptengine::invoke_script_pre_flow(script, None, None, None, true, &vec![])?;
163
164                    Ok(true)
165                }
166                None => {
167                    debug!("No load script defined.");
168                    Ok(false)
169                }
170            }
171        }
172        None => {
173            debug!("No load script defined.");
174            Ok(false)
175        }
176    }
177}
178
179fn merge_external_configs(
180    config: ExternalConfig,
181    parent_config: ExternalConfig,
182) -> Result<ExternalConfig, CargoMakeError> {
183    // merge env files
184    let mut parent_env_files = match parent_config.env_files {
185        Some(env_files) => env_files,
186        None => vec![],
187    };
188    let mut extended_env_files = match config.env_files {
189        Some(env_files) => env_files,
190        None => vec![],
191    };
192    let all_env_files = merge_env_files(&mut parent_env_files, &mut extended_env_files);
193
194    // merge env
195    let mut parent_env = match parent_config.env {
196        Some(env) => env,
197        None => IndexMap::new(),
198    };
199    let mut extended_env = match config.env {
200        Some(env) => env,
201        None => IndexMap::new(),
202    };
203    let all_env = merge_env(&mut parent_env, &mut extended_env)?;
204
205    // merge env scripts
206    let mut parent_env_scripts = match parent_config.env_scripts {
207        Some(env_scripts) => env_scripts,
208        None => vec![],
209    };
210    let mut extended_env_scripts = match config.env_scripts {
211        Some(env_scripts) => env_scripts,
212        None => vec![],
213    };
214    let all_env_scripts = merge_env_scripts(&mut parent_env_scripts, &mut extended_env_scripts);
215
216    // merge tasks
217    let mut parent_tasks = match parent_config.tasks {
218        Some(tasks) => tasks,
219        None => IndexMap::new(),
220    };
221    let mut extended_tasks = match config.tasks {
222        Some(tasks) => tasks,
223        None => IndexMap::new(),
224    };
225    let all_tasks = merge_tasks(&mut parent_tasks, &mut extended_tasks, false);
226
227    let mut config_section = ConfigSection::new();
228    if parent_config.config.is_some() {
229        let mut config_section_data = parent_config.config.unwrap();
230        debug!("Adding parent config section: {:#?}", &config_section_data);
231        config_section.extend(&mut config_section_data);
232    }
233    if config.config.is_some() {
234        let mut config_section_data = config.config.unwrap();
235        debug!("Adding config section: {:#?}", &config_section_data);
236        config_section.extend(&mut config_section_data);
237    }
238
239    let plugins = merge_plugins_config(parent_config.plugins, config.plugins);
240
241    let config = ExternalConfig {
242        extend: None,
243        config: Some(config_section),
244        env_files: Some(all_env_files),
245        env: Some(all_env),
246        env_scripts: Some(all_env_scripts),
247        tasks: Some(all_tasks),
248        plugins,
249    };
250
251    Ok(config)
252}
253
254fn load_descriptor_extended_makefiles(
255    parent_path: &str,
256    extend_struct: &Extend,
257) -> Result<ExternalConfig, CargoMakeError> {
258    match extend_struct {
259        Extend::Path(base_file) => {
260            load_external_descriptor(parent_path, &base_file, true, false, RelativeTo::Makefile)
261        }
262        Extend::Options(extend_options) => {
263            let force = !extend_options.optional.unwrap_or(false);
264            let relative_to_str = extend_options
265                .relative
266                .clone()
267                .unwrap_or("makefile".to_string());
268            let relative_to = match relative_to_str.as_str() {
269                "git" => RelativeTo::GitRoot,
270                "crate" => RelativeTo::CrateRoot,
271                "workspace" => RelativeTo::WorkspaceRoot,
272                "makefile" => RelativeTo::Makefile,
273                _ => {
274                    warn!(
275                        "Unknown relative-to value: {}, defaulting to makefile",
276                        &relative_to_str
277                    );
278                    RelativeTo::Makefile
279                }
280            };
281            load_external_descriptor(parent_path, &extend_options.path, force, false, relative_to)
282        }
283        Extend::List(extend_list) => {
284            let mut ordered_list_config = ExternalConfig::new();
285
286            for entry in extend_list.iter() {
287                let extend_options = entry.clone();
288                let entry_config = load_descriptor_extended_makefiles(
289                    parent_path,
290                    &Extend::Options(extend_options),
291                )?;
292
293                // merge configs
294                ordered_list_config = merge_external_configs(entry_config, ordered_list_config)?;
295            }
296
297            Ok(ordered_list_config)
298        }
299    }
300}
301
302/// Ensure the Makefile's min_version, if present, is older than cargo-make's
303/// currently running version.
304fn check_makefile_min_version(external_descriptor: &str) -> Result<(), CargoMakeError> {
305    let value: toml::Value = match toml::from_str(&external_descriptor) {
306        Ok(value) => value,
307        // If there's an error parsing the file, let the caller function figure
308        // it out
309        Err(_) => return Ok(()),
310    };
311
312    let min_version = value
313        .get("config")
314        .and_then(|config| config.get("min_version"))
315        .and_then(|min_ver| min_ver.as_str());
316
317    if let Some(ref min_version) = min_version {
318        if version::is_newer_found(&min_version) {
319            return Err(CargoMakeError::VersionTooOld(min_version.to_string()));
320        }
321    }
322
323    Ok(())
324}
325
326fn load_external_descriptor(
327    base_path: &str,
328    file_name: &str,
329    force: bool,
330    set_env: bool,
331    relative_to: RelativeTo,
332) -> Result<ExternalConfig, CargoMakeError> {
333    debug!(
334        "Loading tasks from file: {} base directory: {}, relative to: {:#?}",
335        &file_name, &base_path, &relative_to,
336    );
337
338    let descriptor_dir = match relative_to {
339        RelativeTo::Makefile => base_path.to_string(),
340        RelativeTo::GitRoot => {
341            let git_root = environment::find_git_root(&PathBuf::from(base_path));
342            debug!("git root: {:#?}", &git_root);
343            match git_root {
344                Some(git_root_dir) => git_root_dir.clone(),
345                None => base_path.to_string(),
346            }
347        }
348        RelativeTo::CrateRoot => {
349            let project_root = environment::get_project_root_for_path(&PathBuf::from(base_path));
350            debug!("project root: {:#?}", &project_root);
351            match project_root {
352                Some(crate_dir) => crate_dir.clone(),
353                None => base_path.to_string(),
354            }
355        }
356        RelativeTo::WorkspaceRoot => {
357            let base_path_buf = PathBuf::from(base_path);
358            let project_root = environment::get_project_root_for_path(&base_path_buf);
359            debug!("project root: {:#?}", &project_root);
360            match project_root {
361                Some(crate_dir) => {
362                    let crate_parent_path = PathBuf::from(&crate_dir).join("..");
363                    let workspace_root = environment::get_project_root_for_path(&crate_parent_path);
364                    debug!("workspace root: {:#?}", &workspace_root);
365                    match workspace_root {
366                        Some(workspace_dir) => workspace_dir.clone(),
367                        None => crate_dir.clone(),
368                    }
369                }
370                None => base_path.to_string(),
371            }
372        }
373    };
374    let file_path = Path::new(&descriptor_dir).join(file_name);
375
376    if file_path.exists() && file_path.is_file() {
377        let file_path_string: String = FromPath::from_path(&file_path);
378        let absolute_file_path = io::canonicalize_to_string(&file_path_string);
379
380        if set_env {
381            envmnt::set("CARGO_MAKE_MAKEFILE_PATH", &absolute_file_path);
382        }
383
384        let external_descriptor = io::read_text_file(&file_path)?;
385
386        check_makefile_min_version(&external_descriptor)?;
387
388        let mut file_config =
389            descriptor_deserializer::load_external_config(&external_descriptor, &file_path_string)?;
390        debug!("Loaded external config: {:#?}", &file_config);
391
392        file_config = add_file_location_info(file_config, &absolute_file_path);
393
394        run_load_script(&file_config)?;
395
396        match file_config.extend {
397            Some(ref extend_struct) => {
398                let parent_path_buf = Path::new(&file_path_string).join("..");
399                let parent_path = file_path
400                    .parent()
401                    .unwrap_or(&parent_path_buf)
402                    .to_str()
403                    .unwrap_or(".");
404                debug!("External config parent path: {}", &parent_path);
405
406                let base_file_config =
407                    load_descriptor_extended_makefiles(&parent_path, extend_struct)?;
408
409                merge_external_configs(file_config.clone(), base_file_config)
410            }
411            None => Ok(file_config),
412        }
413    } else if force {
414        error!("Descriptor file: {:#?} not found.", &file_path);
415        Err(CargoMakeError::NotFound(format!(
416            "Descriptor file: {:#?} not found.",
417            &file_path
418        )))
419    } else {
420        debug!("External file not found or is not a file, skipping.");
421
422        Ok(ExternalConfig::new())
423    }
424}
425
426pub(crate) fn load_internal_descriptors(
427    stable: bool,
428    experimental: bool,
429    modify_config: Option<ModifyConfig>,
430) -> Result<Config, CargoMakeError> {
431    debug!("Loading base tasks.");
432
433    let base_descriptor = if stable {
434        makefiles::STABLE
435    } else {
436        makefiles::BASE
437    };
438
439    let mut base_config = descriptor_deserializer::load_config(&base_descriptor, false)?;
440    debug!("Loaded base config: {:#?}", &base_config);
441
442    if experimental {
443        debug!("Loading experimental tasks.");
444        let experimental_descriptor = makefiles::BETA;
445
446        let experimental_config =
447            descriptor_deserializer::load_config(&experimental_descriptor, false)?;
448        debug!("Loaded experimental config: {:#?}", &experimental_config);
449
450        let mut base_tasks = base_config.tasks;
451        let mut experimental_tasks = experimental_config.tasks;
452        let all_tasks = merge_tasks(&mut base_tasks, &mut experimental_tasks, false);
453
454        base_config.tasks = all_tasks;
455    }
456
457    // reset
458    envmnt::set("CARGO_MAKE_CORE_TASK_NAMESPACE", "");
459    envmnt::set("CARGO_MAKE_CORE_TASK_NAMESPACE_PREFIX", "");
460
461    match modify_config {
462        Some(props) => {
463            base_config.apply(&props);
464
465            match props.namespace {
466                Some(ref namespace) => {
467                    let prefix = props.get_namespace_prefix();
468
469                    envmnt::set("CARGO_MAKE_CORE_TASK_NAMESPACE", &namespace);
470                    envmnt::set("CARGO_MAKE_CORE_TASK_NAMESPACE_PREFIX", &prefix);
471                }
472                None => (),
473            };
474        }
475        None => (),
476    };
477
478    Ok(base_config)
479}
480
481fn merge_base_config_and_external_config(
482    base_config: Config,
483    external_config: ExternalConfig,
484    env_map: Option<Vec<String>>,
485    late_merge: bool,
486) -> Result<Config, CargoMakeError> {
487    let mut external_tasks = match external_config.tasks {
488        Some(tasks) => tasks,
489        None => IndexMap::new(),
490    };
491    let mut base_tasks = base_config.tasks;
492
493    let env_files = match external_config.env_files {
494        Some(env_files) => env_files,
495        None => vec![],
496    };
497
498    let env_scripts = match external_config.env_scripts {
499        Some(env_scripts) => env_scripts,
500        None => vec![],
501    };
502
503    let mut external_env = match external_config.env {
504        Some(env) => env,
505        None => IndexMap::new(),
506    };
507    let mut base_env = base_config.env;
508
509    // merge env
510    let mut all_env = merge_env(&mut base_env, &mut external_env)?;
511    all_env = match env_map {
512        Some(values) => {
513            let mut cli_env = IndexMap::new();
514
515            for env_pair in &values {
516                debug!("Checking env pair: {}", &env_pair);
517                let env_parts: Option<(&str, &str)> = split_once(env_pair, '=');
518
519                if let Some((key, value)) = env_parts {
520                    cli_env.insert(key.to_string(), EnvValue::Value(value.to_string()));
521                }
522            }
523
524            // cli env should be ordered first and not overwritten
525            // to enable composite/mapped env vars in makefiles to work correctly
526            for (key, value) in all_env.iter() {
527                let key_str = key.to_string();
528
529                if !cli_env.contains_key(&key_str) {
530                    cli_env.insert(key_str, value.clone());
531                }
532            }
533            cli_env
534        }
535        None => all_env,
536    };
537
538    let all_tasks = merge_tasks(&mut base_tasks, &mut external_tasks, late_merge);
539
540    let mut config_section = base_config.config.clone();
541    config_section.extend(&mut external_config.config.unwrap_or(ConfigSection::new()));
542
543    let plugins = merge_plugins_config(base_config.plugins, external_config.plugins);
544
545    let config = Config {
546        config: config_section,
547        env_files,
548        env: all_env,
549        env_scripts,
550        tasks: all_tasks,
551        plugins,
552    };
553
554    Ok(config)
555}
556
557fn split_once(value: &str, delimiter: char) -> Option<(&str, &str)> {
558    let mut parts = value.splitn(2, delimiter);
559    let part1 = parts.next()?;
560    let part2 = parts.next()?;
561    Some((part1, part2))
562}
563
564/// Loads the tasks descriptor.<br>
565/// It will first load the default descriptor which is defined in cargo-make
566/// internally and afterwards tries to find the external descriptor and load it
567/// as well.<br> If an external descriptor exists, it will be loaded and extend
568/// the default descriptor. If one of the descriptor requires a newer version of
569/// cargo-make, returns an error with the minimum version required by the
570/// descriptor.
571fn load_descriptors(
572    file_name: &str,
573    force: bool,
574    env_map: Option<Vec<String>>,
575    stable: bool,
576    experimental: bool,
577    modify_core_tasks: Option<ModifyConfig>,
578) -> Result<Config, CargoMakeError> {
579    let default_config = load_internal_descriptors(stable, experimental, modify_core_tasks)?;
580
581    let mut external_config =
582        load_external_descriptor(".", file_name, force, true, RelativeTo::Makefile)?;
583
584    external_config = match std::env::var("CARGO_MAKE_WORKSPACE_MAKEFILE") {
585        Ok(workspace_makefile) => {
586            let mut pathbuf = PathBuf::from(workspace_makefile);
587            match pathbuf.clone().file_name() {
588                Some(workspace_file_name) => match workspace_file_name.to_str() {
589                    Some(workspace_file_name_str) => {
590                        pathbuf.pop();
591
592                        match pathbuf.to_str() {
593                            Some(directory) => {
594                                let workspace_config = load_external_descriptor(
595                                    directory,
596                                    workspace_file_name_str,
597                                    false,
598                                    false,
599                                    RelativeTo::Makefile,
600                                )?;
601                                merge_external_configs(external_config, workspace_config)?
602                            }
603                            _ => external_config,
604                        }
605                    }
606                    _ => external_config,
607                },
608                _ => external_config,
609            }
610        }
611        _ => external_config,
612    };
613
614    let config =
615        merge_base_config_and_external_config(default_config, external_config, env_map, false)?;
616
617    debug!("Loaded merged config: {:#?}", &config);
618
619    Ok(config)
620}
621
622fn load_cargo_aliases(config: &mut Config) -> Result<(), CargoMakeError> {
623    if let Some(load_cargo_aliases) = config.config.load_cargo_aliases {
624        if load_cargo_aliases {
625            let alias_tasks = cargo_alias::load()?;
626            for (name, task) in alias_tasks {
627                match config.tasks.get(&name) {
628                    None => {
629                        debug!("Creating cargo alias task: {}", &name);
630                        config.tasks.insert(name, task);
631                    }
632                    Some(_) => debug!("Ignoring cargo alias task: {}", &name),
633                }
634            }
635        }
636    }
637    Ok(())
638}
639
640/// Loads the tasks descriptor.<br>
641/// It will first load the default descriptor which is defined in cargo-make
642/// internally and afterwards tries to find the external descriptor and load it
643/// as well.<br> If an external descriptor exists, it will be loaded and extend
644/// the default descriptor. <br> If one of the descriptor requires a newer
645/// version of cargo-make, returns an error with the minimum version required by
646/// the descriptor.
647pub fn load(
648    file_name: &str,
649    force: bool,
650    env_map: Option<Vec<String>>,
651    experimental: bool,
652) -> Result<Config, CargoMakeError> {
653    // load extended descriptor only
654    let mut config = load_descriptors(&file_name, force, env_map.clone(), false, false, None)?;
655
656    // need to load core tasks as well
657    if !config.config.skip_core_tasks.unwrap_or(false) {
658        let modify_core_tasks = config.config.modify_core_tasks.clone();
659
660        match modify_core_tasks {
661            Some(modify_config) => {
662                if modify_config.is_modifications_defined() {
663                    // reload everything with core modifications
664                    config = load_descriptors(
665                        &file_name,
666                        force,
667                        env_map.clone(),
668                        true,
669                        experimental,
670                        Some(modify_config),
671                    )?;
672                }
673            }
674            None => {
675                let core_config = load_internal_descriptors(true, experimental, modify_core_tasks)?;
676                let external_config = ExternalConfig {
677                    extend: None,
678                    config: Some(config.config),
679                    env_files: Some(config.env_files),
680                    env: Some(config.env),
681                    env_scripts: Some(config.env_scripts),
682                    tasks: Some(config.tasks),
683                    plugins: config.plugins,
684                };
685
686                config = merge_base_config_and_external_config(
687                    core_config,
688                    external_config,
689                    env_map.clone(),
690                    true,
691                )?;
692            }
693        };
694    }
695
696    load_cargo_aliases(&mut config)?;
697
698    if let Some(unstable_features) = &config.config.unstable_features {
699        for feature in unstable_features {
700            config
701                .env
702                .insert(feature.to_env_name(), EnvValue::Boolean(true));
703        }
704    }
705
706    Ok(config)
707}