Skip to main content

cargo/workspace/parser/
targets.rs

1//! This module implements Cargo conventions for directory layout:
2//!
3//!  * `src/lib.rs` is a library
4//!  * `src/main.rs` is a binary
5//!  * `src/bin/*.rs` are binaries
6//!  * `examples/*.rs` are examples
7//!  * `tests/*.rs` are integration tests
8//!  * `benches/*.rs` are benchmarks
9//!
10//! It is a bit tricky because we need match explicit information from `Cargo.toml`
11//! with implicit info in directory layout.
12
13use cargo_util::collections::{HashMap, HashSet};
14use std::fmt::Write;
15use std::fs::{self, DirEntry};
16use std::path::{Path, PathBuf};
17
18use anyhow::Context as _;
19use cargo_util::paths;
20use cargo_util_schemas::core::CrateType;
21use cargo_util_schemas::manifest::{
22    PathValue, StringOrVec, TomlBenchTarget, TomlBinTarget, TomlExampleTarget, TomlLibTarget,
23    TomlManifest, TomlPackageBuild, TomlTarget, TomlTestTarget,
24};
25
26use crate::compiler::rustdoc::RustdocScrapeExamples;
27use crate::util::{closest_msg, errors::CargoResult, restricted_names};
28use crate::workspace::parser::deprecated_underscore;
29use crate::workspace::{Edition, Feature, Features, Target};
30
31const DEFAULT_TEST_DIR_NAME: &'static str = "tests";
32const DEFAULT_BENCH_DIR_NAME: &'static str = "benches";
33const DEFAULT_EXAMPLE_DIR_NAME: &'static str = "examples";
34
35const TARGET_KIND_HUMAN_LIB: &str = "library";
36const TARGET_KIND_HUMAN_BIN: &str = "binary";
37const TARGET_KIND_HUMAN_EXAMPLE: &str = "example";
38const TARGET_KIND_HUMAN_TEST: &str = "test";
39const TARGET_KIND_HUMAN_BENCH: &str = "benchmark";
40
41const TARGET_KIND_LIB: &str = "lib";
42const TARGET_KIND_BIN: &str = "bin";
43const TARGET_KIND_EXAMPLE: &str = "example";
44const TARGET_KIND_TEST: &str = "test";
45const TARGET_KIND_BENCH: &str = "bench";
46
47#[tracing::instrument(skip_all)]
48pub(super) fn to_targets(
49    features: &Features,
50    original_toml: &TomlManifest,
51    normalized_toml: &TomlManifest,
52    package_root: &Path,
53    edition: Edition,
54    metabuild: &Option<StringOrVec>,
55    warnings: &mut Vec<String>,
56) -> CargoResult<Vec<Target>> {
57    let mut targets = Vec::new();
58
59    if let Some(target) = to_lib_target(
60        original_toml.lib.as_ref(),
61        normalized_toml.lib.as_ref(),
62        package_root,
63        edition,
64        warnings,
65    )? {
66        targets.push(target);
67    }
68
69    let package = normalized_toml
70        .package
71        .as_ref()
72        .ok_or_else(|| anyhow::format_err!("manifest has no `package` (or `project`)"))?;
73
74    targets.extend(to_bin_targets(
75        features,
76        normalized_toml.bin.as_deref().unwrap_or_default(),
77        package_root,
78        edition,
79        warnings,
80    )?);
81
82    targets.extend(to_example_targets(
83        normalized_toml.example.as_deref().unwrap_or_default(),
84        package_root,
85        edition,
86        warnings,
87    )?);
88
89    targets.extend(to_test_targets(
90        normalized_toml.test.as_deref().unwrap_or_default(),
91        package_root,
92        edition,
93        warnings,
94    )?);
95
96    targets.extend(to_bench_targets(
97        normalized_toml.bench.as_deref().unwrap_or_default(),
98        package_root,
99        edition,
100        warnings,
101    )?);
102
103    // processing the custom build script
104    if let Some(custom_build) = package.normalized_build().expect("previously normalized") {
105        if metabuild.is_some() {
106            anyhow::bail!("cannot specify both `metabuild` and `build`");
107        }
108        validate_unique_build_scripts(custom_build)?;
109        for script in custom_build {
110            let script_path = Path::new(script);
111            let name = format!(
112                "build-script-{}",
113                script_path
114                    .file_stem()
115                    .and_then(|s| s.to_str())
116                    .expect("previously normalized")
117            );
118            targets.push(Target::custom_build_target(
119                &name,
120                package_root.join(script_path),
121                edition,
122            ));
123        }
124    }
125    if let Some(metabuild) = metabuild {
126        // Verify names match available build deps.
127        let bdeps = normalized_toml.build_dependencies.as_ref();
128        for name in &metabuild.0 {
129            if !bdeps.map_or(false, |bd| bd.contains_key(name.as_str())) {
130                anyhow::bail!(
131                    "metabuild package `{}` must be specified in `build-dependencies`",
132                    name
133                );
134            }
135        }
136
137        targets.push(Target::metabuild_target(&format!(
138            "metabuild-{}",
139            package.normalized_name().expect("previously normalized")
140        )));
141    }
142
143    Ok(targets)
144}
145
146#[tracing::instrument(skip_all)]
147pub fn normalize_lib(
148    original_lib: Option<&TomlLibTarget>,
149    package_root: &Path,
150    package_name: &str,
151    edition: Edition,
152    autodiscover: Option<bool>,
153    warnings: &mut Vec<String>,
154) -> CargoResult<Option<TomlLibTarget>> {
155    if is_normalized(original_lib, autodiscover) {
156        let Some(mut lib) = original_lib.cloned() else {
157            return Ok(None);
158        };
159
160        // Check early to improve error messages
161        validate_lib_name(&lib, warnings)?;
162
163        validate_proc_macro(&lib, TARGET_KIND_HUMAN_LIB, edition, warnings)?;
164        validate_crate_types(&lib, TARGET_KIND_HUMAN_LIB, edition, warnings)?;
165
166        if let Some(PathValue(path)) = &lib.path {
167            lib.path = Some(PathValue(paths::normalize_path(path).into()));
168        }
169
170        Ok(Some(lib))
171    } else {
172        let inferred = inferred_lib(package_root);
173        let lib = original_lib.cloned().or_else(|| {
174            inferred.as_ref().map(|lib| TomlTarget {
175                path: Some(PathValue(lib.clone())),
176                ..TomlTarget::new()
177            })
178        });
179        let Some(mut lib) = lib else { return Ok(None) };
180        lib.name
181            .get_or_insert_with(|| package_name.replace("-", "_"));
182
183        // Check early to improve error messages
184        validate_lib_name(&lib, warnings)?;
185
186        validate_proc_macro(&lib, TARGET_KIND_HUMAN_LIB, edition, warnings)?;
187        validate_crate_types(&lib, TARGET_KIND_HUMAN_LIB, edition, warnings)?;
188
189        if lib.path.is_none() {
190            if let Some(inferred) = inferred {
191                lib.path = Some(PathValue(inferred));
192            } else {
193                let name = name_or_panic(&lib);
194                let legacy_path = Path::new("src").join(format!("{name}.rs"));
195                if edition == Edition::Edition2015 && package_root.join(&legacy_path).exists() {
196                    warnings.push(format!(
197                        "path `{}` was erroneously implicitly accepted for library `{name}`,\n\
198                     please rename the file to `src/lib.rs` or set lib.path in Cargo.toml",
199                        legacy_path.display(),
200                    ));
201                    lib.path = Some(PathValue(legacy_path));
202                } else {
203                    anyhow::bail!(
204                        "can't find library `{name}`, \
205                     rename file to `src/lib.rs` or specify lib.path",
206                    )
207                }
208            }
209        }
210
211        if let Some(PathValue(path)) = lib.path.as_ref() {
212            lib.path = Some(PathValue(paths::normalize_path(&path).into()));
213        }
214
215        Ok(Some(lib))
216    }
217}
218
219#[tracing::instrument(skip_all)]
220fn to_lib_target(
221    original_lib: Option<&TomlLibTarget>,
222    normalized_lib: Option<&TomlLibTarget>,
223    package_root: &Path,
224    edition: Edition,
225    warnings: &mut Vec<String>,
226) -> CargoResult<Option<Target>> {
227    let Some(lib) = normalized_lib else {
228        return Ok(None);
229    };
230
231    let path = lib.path.as_ref().expect("previously normalized");
232    let path = package_root.join(&path.0);
233
234    // Per the Macros 1.1 RFC:
235    //
236    // > Initially if a crate is compiled with the `proc-macro` crate type
237    // > (and possibly others) it will forbid exporting any items in the
238    // > crate other than those functions tagged #[proc_macro_derive] and
239    // > those functions must also be placed at the crate root.
240    //
241    // A plugin requires exporting plugin_registrar so a crate cannot be
242    // both at once.
243    let proc_macro = lib.proc_macro().unwrap_or(false);
244    let crate_types = match (lib.crate_types(), proc_macro) {
245        (Some(kinds), _)
246            if kinds.contains(&CrateType::Dylib.as_str().to_owned())
247                && kinds.contains(&CrateType::Cdylib.as_str().to_owned()) =>
248        {
249            anyhow::bail!(format!(
250                "library `{}` cannot set the crate type of both `dylib` and `cdylib`",
251                name_or_panic(lib)
252            ));
253        }
254        (Some(kinds), _) if kinds.contains(&"proc-macro".to_string()) => {
255            warnings.push(format!(
256                "library `{}` should only specify `proc-macro = true` instead of setting `crate-type`",
257                name_or_panic(lib)
258            ));
259            if kinds.len() > 1 {
260                anyhow::bail!("cannot mix `proc-macro` crate type with others");
261            }
262            vec![CrateType::ProcMacro]
263        }
264        (Some(kinds), _) => kinds.iter().map(|s| s.into()).collect(),
265        (None, true) => vec![CrateType::ProcMacro],
266        (None, false) => vec![CrateType::Lib],
267    };
268
269    if proc_macro != crate_types.contains(&CrateType::ProcMacro) {
270        let help = if proc_macro {
271            "remove `proc-macro` if this library is not intended to be a proc macro"
272        } else {
273            "set `proc-macro = true` and remove `crate-type` \
274             if this library is intended to be a proc macro"
275        };
276        warnings.push(format!(
277            "library `{}` has conflicting `proc-macro = {proc_macro}` and `crate-type` settings\n\
278             note: this was previously accepted but will become a hard error in the future; \
279             see <https://github.com/rust-lang/cargo/issues/17579>\n\
280             help: {help}",
281            name_or_panic(lib)
282        ));
283    }
284
285    let mut target = Target::lib_target(name_or_panic(lib), crate_types, path, edition);
286    configure(lib, &mut target, TARGET_KIND_HUMAN_LIB, warnings)?;
287    target.set_name_inferred(original_lib.map_or(true, |v| v.name.is_none()));
288    Ok(Some(target))
289}
290
291#[tracing::instrument(skip_all)]
292pub fn normalize_bins(
293    toml_bins: Option<&Vec<TomlBinTarget>>,
294    package_root: &Path,
295    package_name: &str,
296    edition: Edition,
297    autodiscover: Option<bool>,
298    warnings: &mut Vec<String>,
299    errors: &mut Vec<String>,
300    has_lib: bool,
301) -> CargoResult<Vec<TomlBinTarget>> {
302    if are_normalized(toml_bins, autodiscover) {
303        let mut toml_bins = toml_bins.cloned().unwrap_or_default();
304        for bin in toml_bins.iter_mut() {
305            validate_bin_name(bin, warnings)?;
306            validate_bin_crate_types(bin, edition, warnings, errors)?;
307            validate_bin_proc_macro(bin, edition, warnings, errors)?;
308
309            if let Some(PathValue(path)) = &bin.path {
310                bin.path = Some(PathValue(paths::normalize_path(path).into()));
311            }
312        }
313        Ok(toml_bins)
314    } else {
315        let inferred = inferred_bins(package_root, package_name);
316
317        let mut bins = toml_targets_and_inferred(
318            toml_bins,
319            &inferred,
320            package_root,
321            autodiscover,
322            edition,
323            warnings,
324            TARGET_KIND_HUMAN_BIN,
325            TARGET_KIND_BIN,
326            "autobins",
327        );
328
329        for bin in &mut bins {
330            // Check early to improve error messages
331            validate_bin_name(bin, warnings)?;
332
333            validate_bin_crate_types(bin, edition, warnings, errors)?;
334            validate_bin_proc_macro(bin, edition, warnings, errors)?;
335
336            let path = target_path(
337                bin,
338                &inferred,
339                TARGET_KIND_BIN,
340                package_root,
341                edition,
342                &mut |_| {
343                    if let Some(legacy_path) =
344                        legacy_bin_path(package_root, name_or_panic(bin), has_lib)
345                    {
346                        warnings.push(format!(
347                            "path `{}` was erroneously implicitly accepted for binary `{}`,\n\
348                     please set bin.path in Cargo.toml",
349                            legacy_path.display(),
350                            name_or_panic(bin)
351                        ));
352                        Some(legacy_path)
353                    } else {
354                        None
355                    }
356                },
357            );
358            let path = match path {
359                Ok(path) => paths::normalize_path(&path).into(),
360                Err(e) => anyhow::bail!("{}", e),
361            };
362            bin.path = Some(PathValue(path));
363        }
364
365        Ok(bins)
366    }
367}
368
369#[tracing::instrument(skip_all)]
370fn to_bin_targets(
371    features: &Features,
372    bins: &[TomlBinTarget],
373    package_root: &Path,
374    edition: Edition,
375    warnings: &mut Vec<String>,
376) -> CargoResult<Vec<Target>> {
377    // This loop performs basic checks on each of the TomlTarget in `bins`.
378    for bin in bins {
379        // For each binary, check if the `filename` parameter is populated. If it is,
380        // check if the corresponding cargo feature has been activated.
381        if bin.filename.is_some() {
382            features.require(Feature::different_binary_name())?;
383        }
384    }
385
386    validate_unique_names(&bins, TARGET_KIND_HUMAN_BIN)?;
387
388    let mut result = Vec::with_capacity(bins.len());
389    for bin in bins {
390        let path = package_root.join(&bin.path.as_ref().expect("previously normalized").0);
391        let mut target = Target::bin_target(
392            name_or_panic(bin),
393            bin.filename.clone(),
394            path,
395            bin.required_features.clone(),
396            edition,
397        );
398
399        configure(bin, &mut target, TARGET_KIND_HUMAN_BIN, warnings)?;
400        result.push(target);
401    }
402    Ok(result)
403}
404
405fn legacy_bin_path(package_root: &Path, name: &str, has_lib: bool) -> Option<PathBuf> {
406    if !has_lib {
407        let rel_path = Path::new("src").join(format!("{}.rs", name));
408        if package_root.join(&rel_path).exists() {
409            return Some(rel_path);
410        }
411    }
412
413    let rel_path = Path::new("src").join("main.rs");
414    if package_root.join(&rel_path).exists() {
415        return Some(rel_path);
416    }
417
418    let default_bin_dir_name = Path::new("src").join("bin");
419    let rel_path = default_bin_dir_name.join("main.rs");
420    if package_root.join(&rel_path).exists() {
421        return Some(rel_path);
422    }
423    None
424}
425
426#[tracing::instrument(skip_all)]
427pub fn normalize_examples(
428    toml_examples: Option<&Vec<TomlExampleTarget>>,
429    package_root: &Path,
430    edition: Edition,
431    autodiscover: Option<bool>,
432    warnings: &mut Vec<String>,
433    errors: &mut Vec<String>,
434) -> CargoResult<Vec<TomlExampleTarget>> {
435    let mut inferred = || infer_from_directory(&package_root, Path::new(DEFAULT_EXAMPLE_DIR_NAME));
436
437    let targets = normalize_targets(
438        TARGET_KIND_HUMAN_EXAMPLE,
439        TARGET_KIND_EXAMPLE,
440        toml_examples,
441        &mut inferred,
442        package_root,
443        edition,
444        autodiscover,
445        warnings,
446        errors,
447        "autoexamples",
448    )?;
449
450    Ok(targets)
451}
452
453#[tracing::instrument(skip_all)]
454fn to_example_targets(
455    targets: &[TomlExampleTarget],
456    package_root: &Path,
457    edition: Edition,
458    warnings: &mut Vec<String>,
459) -> CargoResult<Vec<Target>> {
460    validate_unique_names(&targets, TARGET_KIND_EXAMPLE)?;
461
462    let mut result = Vec::with_capacity(targets.len());
463    for toml in targets {
464        let path = package_root.join(&toml.path.as_ref().expect("previously normalized").0);
465        let crate_types = match toml.crate_types() {
466            Some(kinds) => kinds.iter().map(|s| s.into()).collect(),
467            None => Vec::new(),
468        };
469
470        let mut target = Target::example_target(
471            name_or_panic(&toml),
472            crate_types,
473            path,
474            toml.required_features.clone(),
475            edition,
476        );
477        configure(&toml, &mut target, TARGET_KIND_HUMAN_EXAMPLE, warnings)?;
478        result.push(target);
479    }
480
481    Ok(result)
482}
483
484#[tracing::instrument(skip_all)]
485pub fn normalize_tests(
486    toml_tests: Option<&Vec<TomlTestTarget>>,
487    package_root: &Path,
488    edition: Edition,
489    autodiscover: Option<bool>,
490    warnings: &mut Vec<String>,
491    errors: &mut Vec<String>,
492) -> CargoResult<Vec<TomlTestTarget>> {
493    let mut inferred = || infer_from_directory(&package_root, Path::new(DEFAULT_TEST_DIR_NAME));
494
495    let targets = normalize_targets(
496        TARGET_KIND_HUMAN_TEST,
497        TARGET_KIND_TEST,
498        toml_tests,
499        &mut inferred,
500        package_root,
501        edition,
502        autodiscover,
503        warnings,
504        errors,
505        "autotests",
506    )?;
507
508    Ok(targets)
509}
510
511#[tracing::instrument(skip_all)]
512fn to_test_targets(
513    targets: &[TomlTestTarget],
514    package_root: &Path,
515    edition: Edition,
516    warnings: &mut Vec<String>,
517) -> CargoResult<Vec<Target>> {
518    validate_unique_names(&targets, TARGET_KIND_TEST)?;
519
520    let mut result = Vec::with_capacity(targets.len());
521    for toml in targets {
522        let path = package_root.join(&toml.path.as_ref().expect("previously normalized").0);
523        let mut target = Target::test_target(
524            name_or_panic(&toml),
525            path,
526            toml.required_features.clone(),
527            edition,
528        );
529        configure(&toml, &mut target, TARGET_KIND_HUMAN_TEST, warnings)?;
530        result.push(target);
531    }
532    Ok(result)
533}
534
535#[tracing::instrument(skip_all)]
536pub fn normalize_benches(
537    toml_benches: Option<&Vec<TomlBenchTarget>>,
538    package_root: &Path,
539    edition: Edition,
540    autodiscover: Option<bool>,
541    warnings: &mut Vec<String>,
542    errors: &mut Vec<String>,
543) -> CargoResult<Vec<TomlBenchTarget>> {
544    let mut legacy_warnings = vec![];
545    let mut legacy_bench_path = |bench: &TomlTarget| {
546        let legacy_path = Path::new("src").join("bench.rs");
547        if !(name_or_panic(bench) == "bench" && package_root.join(&legacy_path).exists()) {
548            return None;
549        }
550        legacy_warnings.push(format!(
551            "path `{}` was erroneously implicitly accepted for benchmark `{}`,\n\
552                 please set bench.path in Cargo.toml",
553            legacy_path.display(),
554            name_or_panic(bench)
555        ));
556        Some(legacy_path)
557    };
558
559    let mut inferred = || infer_from_directory(&package_root, Path::new(DEFAULT_BENCH_DIR_NAME));
560
561    let targets = normalize_targets_with_legacy_path(
562        TARGET_KIND_HUMAN_BENCH,
563        TARGET_KIND_BENCH,
564        toml_benches,
565        &mut inferred,
566        package_root,
567        edition,
568        autodiscover,
569        warnings,
570        errors,
571        &mut legacy_bench_path,
572        "autobenches",
573    )?;
574    warnings.append(&mut legacy_warnings);
575
576    Ok(targets)
577}
578
579#[tracing::instrument(skip_all)]
580fn to_bench_targets(
581    targets: &[TomlBenchTarget],
582    package_root: &Path,
583    edition: Edition,
584    warnings: &mut Vec<String>,
585) -> CargoResult<Vec<Target>> {
586    validate_unique_names(&targets, TARGET_KIND_BENCH)?;
587
588    let mut result = Vec::with_capacity(targets.len());
589    for toml in targets {
590        let path = package_root.join(&toml.path.as_ref().expect("previously normalized").0);
591        let mut target = Target::bench_target(
592            name_or_panic(&toml),
593            path,
594            toml.required_features.clone(),
595            edition,
596        );
597        configure(&toml, &mut target, TARGET_KIND_HUMAN_BENCH, warnings)?;
598        result.push(target);
599    }
600
601    Ok(result)
602}
603
604fn is_normalized(toml_target: Option<&TomlTarget>, autodiscover: Option<bool>) -> bool {
605    are_normalized_(toml_target.map(std::slice::from_ref), autodiscover)
606}
607
608fn are_normalized(toml_targets: Option<&Vec<TomlTarget>>, autodiscover: Option<bool>) -> bool {
609    are_normalized_(toml_targets.map(|v| v.as_slice()), autodiscover)
610}
611
612fn are_normalized_(toml_targets: Option<&[TomlTarget]>, autodiscover: Option<bool>) -> bool {
613    if autodiscover != Some(false) {
614        return false;
615    }
616
617    let Some(toml_targets) = toml_targets else {
618        return true;
619    };
620    toml_targets
621        .iter()
622        .all(|t| t.name.is_some() && t.path.is_some())
623}
624
625fn normalize_targets(
626    target_kind_human: &str,
627    target_kind: &str,
628    toml_targets: Option<&Vec<TomlTarget>>,
629    inferred: &mut dyn FnMut() -> Vec<(String, PathBuf)>,
630    package_root: &Path,
631    edition: Edition,
632    autodiscover: Option<bool>,
633    warnings: &mut Vec<String>,
634    errors: &mut Vec<String>,
635    autodiscover_flag_name: &str,
636) -> CargoResult<Vec<TomlTarget>> {
637    normalize_targets_with_legacy_path(
638        target_kind_human,
639        target_kind,
640        toml_targets,
641        inferred,
642        package_root,
643        edition,
644        autodiscover,
645        warnings,
646        errors,
647        &mut |_| None,
648        autodiscover_flag_name,
649    )
650}
651
652fn normalize_targets_with_legacy_path(
653    target_kind_human: &str,
654    target_kind: &str,
655    toml_targets: Option<&Vec<TomlTarget>>,
656    inferred: &mut dyn FnMut() -> Vec<(String, PathBuf)>,
657    package_root: &Path,
658    edition: Edition,
659    autodiscover: Option<bool>,
660    warnings: &mut Vec<String>,
661    errors: &mut Vec<String>,
662    legacy_path: &mut dyn FnMut(&TomlTarget) -> Option<PathBuf>,
663    autodiscover_flag_name: &str,
664) -> CargoResult<Vec<TomlTarget>> {
665    if are_normalized(toml_targets, autodiscover) {
666        let mut toml_targets = toml_targets.cloned().unwrap_or_default();
667        for target in toml_targets.iter_mut() {
668            // Check early to improve error messages
669            validate_target_name(target, target_kind_human, target_kind, warnings)?;
670
671            validate_proc_macro(target, target_kind_human, edition, warnings)?;
672            validate_crate_types(target, target_kind_human, edition, warnings)?;
673
674            if let Some(PathValue(path)) = &target.path {
675                target.path = Some(PathValue(paths::normalize_path(path).into()));
676            }
677        }
678        Ok(toml_targets)
679    } else {
680        let inferred = inferred();
681        let toml_targets = toml_targets_and_inferred(
682            toml_targets,
683            &inferred,
684            package_root,
685            autodiscover,
686            edition,
687            warnings,
688            target_kind_human,
689            target_kind,
690            autodiscover_flag_name,
691        );
692
693        for target in &toml_targets {
694            // Check early to improve error messages
695            validate_target_name(target, target_kind_human, target_kind, warnings)?;
696
697            validate_proc_macro(target, target_kind_human, edition, warnings)?;
698            validate_crate_types(target, target_kind_human, edition, warnings)?;
699        }
700
701        let mut result = Vec::new();
702        for mut target in toml_targets {
703            let path = target_path(
704                &target,
705                &inferred,
706                target_kind,
707                package_root,
708                edition,
709                legacy_path,
710            );
711            let path = match path {
712                Ok(path) => path,
713                Err(e) => {
714                    errors.push(e);
715                    continue;
716                }
717            };
718            target.path = Some(PathValue(paths::normalize_path(&path).into()));
719            result.push(target);
720        }
721        Ok(result)
722    }
723}
724
725fn inferred_lib(package_root: &Path) -> Option<PathBuf> {
726    let lib = Path::new("src").join("lib.rs");
727    if package_root.join(&lib).exists() {
728        Some(lib)
729    } else {
730        None
731    }
732}
733
734fn inferred_bins(package_root: &Path, package_name: &str) -> Vec<(String, PathBuf)> {
735    let main = "src/main.rs";
736    let mut result = Vec::new();
737    if package_root.join(main).exists() {
738        let main = PathBuf::from(main);
739        result.push((package_name.to_string(), main));
740    }
741    let default_bin_dir_name = Path::new("src").join("bin");
742    result.extend(infer_from_directory(package_root, &default_bin_dir_name));
743
744    result
745}
746
747fn infer_from_directory(package_root: &Path, relpath: &Path) -> Vec<(String, PathBuf)> {
748    let directory = package_root.join(relpath);
749    let entries = match fs::read_dir(directory) {
750        Err(_) => return Vec::new(),
751        Ok(dir) => dir,
752    };
753
754    entries
755        .filter_map(|e| e.ok())
756        .filter(is_not_dotfile)
757        .filter_map(|d| infer_any(package_root, &d))
758        .collect()
759}
760
761fn infer_any(package_root: &Path, entry: &DirEntry) -> Option<(String, PathBuf)> {
762    if entry.file_type().map_or(false, |t| t.is_dir()) {
763        infer_subdirectory(package_root, entry)
764    } else if entry.path().extension().and_then(|p| p.to_str()) == Some("rs") {
765        infer_file(package_root, entry)
766    } else {
767        None
768    }
769}
770
771fn infer_file(package_root: &Path, entry: &DirEntry) -> Option<(String, PathBuf)> {
772    let path = entry.path();
773    let stem = path.file_stem()?.to_str()?.to_owned();
774    let path = path
775        .strip_prefix(package_root)
776        .map(|p| p.to_owned())
777        .unwrap_or(path);
778    Some((stem, path))
779}
780
781fn infer_subdirectory(package_root: &Path, entry: &DirEntry) -> Option<(String, PathBuf)> {
782    let path = entry.path();
783    let main = path.join("main.rs");
784    let name = path.file_name()?.to_str()?.to_owned();
785    if main.exists() {
786        let main = main
787            .strip_prefix(package_root)
788            .map(|p| p.to_owned())
789            .unwrap_or(main);
790        Some((name, main))
791    } else {
792        None
793    }
794}
795
796fn is_not_dotfile(entry: &DirEntry) -> bool {
797    entry.file_name().to_str().map(|s| s.starts_with('.')) == Some(false)
798}
799
800fn toml_targets_and_inferred(
801    toml_targets: Option<&Vec<TomlTarget>>,
802    inferred: &[(String, PathBuf)],
803    package_root: &Path,
804    autodiscover: Option<bool>,
805    edition: Edition,
806    warnings: &mut Vec<String>,
807    target_kind_human: &str,
808    target_kind: &str,
809    autodiscover_flag_name: &str,
810) -> Vec<TomlTarget> {
811    let inferred_targets = inferred_to_toml_targets(inferred);
812    let mut toml_targets = match toml_targets {
813        None => {
814            if let Some(false) = autodiscover {
815                vec![]
816            } else {
817                inferred_targets
818            }
819        }
820        Some(targets) => {
821            let mut targets = targets.clone();
822
823            let target_path =
824                |target: &TomlTarget| target.path.clone().map(|p| package_root.join(p.0));
825
826            let mut seen_names = HashSet::default();
827            let mut seen_paths = HashSet::default();
828            for target in targets.iter() {
829                seen_names.insert(target.name.clone());
830                seen_paths.insert(target_path(target));
831            }
832
833            let mut rem_targets = vec![];
834            for target in inferred_targets {
835                if !seen_names.contains(&target.name) && !seen_paths.contains(&target_path(&target))
836                {
837                    rem_targets.push(target);
838                }
839            }
840
841            let autodiscover = match autodiscover {
842                Some(autodiscover) => autodiscover,
843                None => {
844                    if edition == Edition::Edition2015 {
845                        if !rem_targets.is_empty() {
846                            let mut rem_targets_str = String::new();
847                            for t in rem_targets.iter() {
848                                if let Some(p) = t.path.clone() {
849                                    rem_targets_str.push_str(&format!("* {}\n", p.0.display()))
850                                }
851                            }
852                            warnings.push(format!(
853                                "\
854An explicit [[{section}]] section is specified in Cargo.toml which currently
855disables Cargo from automatically inferring other {target_kind_human} targets.
856This inference behavior will change in the Rust 2018 edition and the following
857files will be included as a {target_kind_human} target:
858
859{rem_targets_str}
860This is likely to break cargo build or cargo test as these files may not be
861ready to be compiled as a {target_kind_human} target today. You can future-proof yourself
862and disable this warning by adding `{autodiscover_flag_name} = false` to your [package]
863section. You may also move the files to a location where Cargo would not
864automatically infer them to be a target, such as in subfolders.
865
866For more information on this warning you can consult
867https://github.com/rust-lang/cargo/issues/5330",
868                                section = target_kind,
869                                target_kind_human = target_kind_human,
870                                rem_targets_str = rem_targets_str,
871                                autodiscover_flag_name = autodiscover_flag_name,
872                            ));
873                        };
874                        false
875                    } else {
876                        true
877                    }
878                }
879            };
880
881            if autodiscover {
882                targets.append(&mut rem_targets);
883            }
884
885            targets
886        }
887    };
888    // Ensure target order is deterministic, particularly for `cargo vendor` where re-vendoring
889    // should not cause changes.
890    //
891    // `unstable` should be deterministic because we enforce that `t.name` is unique
892    toml_targets.sort_unstable_by_key(|t| t.name.clone());
893    toml_targets
894}
895
896fn inferred_to_toml_targets(inferred: &[(String, PathBuf)]) -> Vec<TomlTarget> {
897    inferred
898        .iter()
899        .map(|(name, path)| TomlTarget {
900            name: Some(name.clone()),
901            path: Some(PathValue(path.clone())),
902            ..TomlTarget::new()
903        })
904        .collect()
905}
906
907/// Will check a list of toml targets, and make sure the target names are unique within a vector.
908fn validate_unique_names(targets: &[TomlTarget], target_kind: &str) -> CargoResult<()> {
909    let mut seen = HashSet::default();
910    for name in targets.iter().map(|e| name_or_panic(e)) {
911        if !seen.insert(name) {
912            anyhow::bail!(
913                "found duplicate {target_kind} name {name}, \
914                 but all {target_kind} targets must have a unique name",
915                target_kind = target_kind,
916                name = name
917            );
918        }
919    }
920    Ok(())
921}
922
923/// Will check a list of build scripts, and make sure script file stems are unique within a vector.
924fn validate_unique_build_scripts(scripts: &[String]) -> CargoResult<()> {
925    let mut seen = HashMap::default();
926    for script in scripts {
927        let stem = Path::new(script)
928            .file_stem()
929            .and_then(|s| s.to_str())
930            .expect("previously normalized");
931        seen.entry(stem)
932            .or_insert_with(Vec::new)
933            .push(script.as_str());
934    }
935    let mut conflict_file_stem = false;
936    let mut err_msg = String::from(
937        "found build scripts with duplicate file stems, but all build scripts must have a unique file stem",
938    );
939    for (stem, paths) in seen {
940        if paths.len() > 1 {
941            conflict_file_stem = true;
942            write!(&mut err_msg, "\n  for stem `{stem}`: {}", paths.join(", "))?;
943        }
944    }
945    if conflict_file_stem {
946        anyhow::bail!(err_msg);
947    }
948    Ok(())
949}
950
951fn configure(
952    toml: &TomlTarget,
953    target: &mut Target,
954    target_kind_human: &str,
955    warnings: &mut Vec<String>,
956) -> CargoResult<()> {
957    let t2 = target.clone();
958    target
959        .set_tested(toml.test.unwrap_or_else(|| t2.tested()))
960        .set_doc(toml.doc.unwrap_or_else(|| t2.documented()))
961        .set_doctest(toml.doctest.unwrap_or_else(|| t2.doctested()))
962        .set_benched(toml.bench.unwrap_or_else(|| t2.benched()))
963        .set_harness(toml.harness.unwrap_or_else(|| t2.harness()))
964        .set_proc_macro(toml.proc_macro().unwrap_or_else(|| t2.proc_macro()))
965        .set_doc_scrape_examples(match toml.doc_scrape_examples {
966            None => RustdocScrapeExamples::Unset,
967            Some(false) => RustdocScrapeExamples::Disabled,
968            Some(true) => RustdocScrapeExamples::Enabled,
969        })
970        .set_for_host(toml.proc_macro().unwrap_or_else(|| t2.for_host()));
971
972    if let Some(edition) = toml.edition.clone() {
973        let name = target.name();
974        warnings.push(format!(
975            "`edition` is set on {target_kind_human} `{name}` which is deprecated"
976        ));
977        target.set_edition(
978            edition
979                .parse()
980                .context("failed to parse the `edition` key")?,
981        );
982    }
983    Ok(())
984}
985
986/// Build an error message for a target path that cannot be determined either
987/// by auto-discovery or specifying.
988///
989/// This function tries to detect commonly wrong paths for targets:
990///
991/// test -> tests/*.rs, tests/*/main.rs
992/// bench -> benches/*.rs, benches/*/main.rs
993/// example -> examples/*.rs, examples/*/main.rs
994/// bin -> src/bin/*.rs, src/bin/*/main.rs
995///
996/// Note that the logic need to sync with [`infer_from_directory`] if changes.
997fn target_path_not_found_error_message(
998    package_root: &Path,
999    target: &TomlTarget,
1000    target_kind: &str,
1001    inferred: &[(String, PathBuf)],
1002) -> String {
1003    fn possible_target_paths(name: &str, kind: &str, commonly_wrong: bool) -> [PathBuf; 2] {
1004        let mut target_path = PathBuf::new();
1005        match (kind, commonly_wrong) {
1006            // commonly wrong paths
1007            ("test" | "bench" | "example", true) => target_path.push(kind),
1008            ("bin", true) => target_path.extend(["src", "bins"]),
1009            // default inferred paths
1010            ("test", false) => target_path.push(DEFAULT_TEST_DIR_NAME),
1011            ("bench", false) => target_path.push(DEFAULT_BENCH_DIR_NAME),
1012            ("example", false) => target_path.push(DEFAULT_EXAMPLE_DIR_NAME),
1013            ("bin", false) => target_path.extend(["src", "bin"]),
1014            _ => unreachable!("invalid target kind: {}", kind),
1015        }
1016
1017        let target_path_file = {
1018            let mut path = target_path.clone();
1019            path.push(format!("{name}.rs"));
1020            path
1021        };
1022        let target_path_subdir = {
1023            target_path.extend([name, "main.rs"]);
1024            target_path
1025        };
1026        return [target_path_file, target_path_subdir];
1027    }
1028
1029    let target_name = name_or_panic(target);
1030
1031    let commonly_wrong_paths = possible_target_paths(&target_name, target_kind, true);
1032    let possible_paths = possible_target_paths(&target_name, target_kind, false);
1033
1034    let msg = closest_msg(target_name, inferred.iter(), |(n, _p)| n, target_kind);
1035    if let Some((wrong_path, possible_path)) = commonly_wrong_paths
1036        .iter()
1037        .zip(possible_paths.iter())
1038        .filter(|(wp, _)| package_root.join(wp).exists())
1039        .next()
1040    {
1041        let [wrong_path, possible_path] = [wrong_path, possible_path].map(|p| p.display());
1042        format!(
1043            "can't find `{target_name}` {target_kind} at default paths, but found a file at `{wrong_path}`.\n\
1044             Perhaps rename the file to `{possible_path}` for target auto-discovery, \
1045             or specify {target_kind}.path if you want to use a non-default path.{msg}",
1046        )
1047    } else {
1048        let [path_file, path_dir] = possible_paths.each_ref().map(|p| p.display());
1049        format!(
1050            "can't find `{target_name}` {target_kind} at `{path_file}` or `{path_dir}`. \
1051             Please specify {target_kind}.path if you want to use a non-default path.{msg}"
1052        )
1053    }
1054}
1055
1056fn target_path(
1057    target: &TomlTarget,
1058    inferred: &[(String, PathBuf)],
1059    target_kind: &str,
1060    package_root: &Path,
1061    edition: Edition,
1062    legacy_path: &mut dyn FnMut(&TomlTarget) -> Option<PathBuf>,
1063) -> Result<PathBuf, String> {
1064    if let Some(ref path) = target.path {
1065        // Should we verify that this path exists here?
1066        return Ok(path.0.clone());
1067    }
1068    let name = name_or_panic(target).to_owned();
1069
1070    let mut matching = inferred
1071        .iter()
1072        .filter(|(n, _)| n == &name)
1073        .map(|(_, p)| p.clone());
1074
1075    let first = matching.next();
1076    let second = matching.next();
1077    match (first, second) {
1078        (Some(path), None) => Ok(path),
1079        (None, None) => {
1080            if edition == Edition::Edition2015 {
1081                if let Some(path) = legacy_path(target) {
1082                    return Ok(path);
1083                }
1084            }
1085            Err(target_path_not_found_error_message(
1086                package_root,
1087                target,
1088                target_kind,
1089                inferred,
1090            ))
1091        }
1092        (Some(p0), Some(p1)) => {
1093            if edition == Edition::Edition2015 {
1094                if let Some(path) = legacy_path(target) {
1095                    return Ok(path);
1096                }
1097            }
1098            Err(format!(
1099                "\
1100cannot infer path for `{}` {}
1101Cargo doesn't know which to use because multiple target files found at `{}` and `{}`.",
1102                name_or_panic(target),
1103                target_kind,
1104                p0.strip_prefix(package_root).unwrap_or(&p0).display(),
1105                p1.strip_prefix(package_root).unwrap_or(&p1).display(),
1106            ))
1107        }
1108        (None, Some(_)) => unreachable!(),
1109    }
1110}
1111
1112/// Returns the path to the build script if one exists for this crate.
1113#[tracing::instrument(skip_all)]
1114pub fn normalize_build(
1115    build: Option<&TomlPackageBuild>,
1116    package_root: &Path,
1117) -> CargoResult<Option<TomlPackageBuild>> {
1118    const BUILD_RS: &str = "build.rs";
1119    match build {
1120        None => {
1121            // If there is a `build.rs` file next to the `Cargo.toml`, assume it is
1122            // a build script.
1123            let build_rs = package_root.join(BUILD_RS);
1124            if build_rs.is_file() {
1125                Ok(Some(TomlPackageBuild::SingleScript(BUILD_RS.to_owned())))
1126            } else {
1127                Ok(Some(TomlPackageBuild::Auto(false)))
1128            }
1129        }
1130        // Explicitly no build script.
1131        Some(TomlPackageBuild::Auto(false)) => Ok(build.cloned()),
1132        Some(TomlPackageBuild::SingleScript(build_file)) => {
1133            validate_build_name(build_file)?;
1134            let build_file = paths::normalize_path(Path::new(build_file));
1135            let build = build_file.into_os_string().into_string().expect(
1136                "`build_file` started as a String and `normalize_path` shouldn't have changed that",
1137            );
1138            Ok(Some(TomlPackageBuild::SingleScript(build)))
1139        }
1140        Some(TomlPackageBuild::Auto(true)) => {
1141            Ok(Some(TomlPackageBuild::SingleScript(BUILD_RS.to_owned())))
1142        }
1143        Some(TomlPackageBuild::MultipleScript(scripts)) => {
1144            for script in scripts {
1145                validate_build_name(script)?;
1146            }
1147            Ok(build.cloned())
1148        }
1149    }
1150}
1151
1152fn validate_build_name(build: &str) -> CargoResult<()> {
1153    if Path::new(build).file_stem().is_none() {
1154        anyhow::bail!("invalid `package.build` file name");
1155    }
1156    Ok(())
1157}
1158
1159fn name_or_panic(target: &TomlTarget) -> &str {
1160    target
1161        .name
1162        .as_deref()
1163        .unwrap_or_else(|| panic!("target name is required"))
1164}
1165
1166fn validate_lib_name(target: &TomlTarget, warnings: &mut Vec<String>) -> CargoResult<()> {
1167    validate_target_name(target, TARGET_KIND_HUMAN_LIB, TARGET_KIND_LIB, warnings)?;
1168    let name = name_or_panic(target);
1169    if name.contains('-') {
1170        anyhow::bail!("library target names cannot contain hyphens: {}", name)
1171    }
1172
1173    Ok(())
1174}
1175
1176fn validate_bin_name(bin: &TomlTarget, warnings: &mut Vec<String>) -> CargoResult<()> {
1177    validate_target_name(bin, TARGET_KIND_HUMAN_BIN, TARGET_KIND_BIN, warnings)?;
1178    let name = name_or_panic(bin).to_owned();
1179    if restricted_names::is_conflicting_artifact_name(&name) {
1180        anyhow::bail!(
1181            "the binary target name `{name}` is forbidden, \
1182                 it conflicts with cargo's build directory names",
1183        )
1184    }
1185
1186    Ok(())
1187}
1188
1189fn validate_target_name(
1190    target: &TomlTarget,
1191    target_kind_human: &str,
1192    target_kind: &str,
1193    warnings: &mut Vec<String>,
1194) -> CargoResult<()> {
1195    match target.name {
1196        Some(ref name) => {
1197            if name.trim().is_empty() {
1198                anyhow::bail!("{} target names cannot be empty", target_kind_human)
1199            }
1200            if cfg!(windows) && restricted_names::is_windows_reserved(name) {
1201                warnings.push(format!(
1202                    "{} target `{}` is a reserved Windows filename, \
1203                        this target will not work on Windows platforms",
1204                    target_kind_human, name
1205                ));
1206            }
1207        }
1208        None => anyhow::bail!(
1209            "{} target {}.name is required",
1210            target_kind_human,
1211            target_kind
1212        ),
1213    }
1214
1215    Ok(())
1216}
1217
1218fn validate_bin_proc_macro(
1219    target: &TomlTarget,
1220    edition: Edition,
1221    warnings: &mut Vec<String>,
1222    errors: &mut Vec<String>,
1223) -> CargoResult<()> {
1224    if target.proc_macro() == Some(true) {
1225        let name = name_or_panic(target);
1226        errors.push(format!(
1227            "the target `{}` is a binary and can't have `proc-macro` \
1228                 set `true`",
1229            name
1230        ));
1231    } else {
1232        validate_proc_macro(target, TARGET_KIND_HUMAN_BIN, edition, warnings)?;
1233    }
1234    Ok(())
1235}
1236
1237fn validate_proc_macro(
1238    target: &TomlTarget,
1239    kind: &str,
1240    edition: Edition,
1241    warnings: &mut Vec<String>,
1242) -> CargoResult<()> {
1243    deprecated_underscore(
1244        &target.proc_macro2,
1245        &target.proc_macro,
1246        "proc-macro",
1247        name_or_panic(target),
1248        format!("{kind} target").as_str(),
1249        edition,
1250        warnings,
1251    )
1252}
1253
1254fn validate_bin_crate_types(
1255    target: &TomlTarget,
1256    edition: Edition,
1257    warnings: &mut Vec<String>,
1258    errors: &mut Vec<String>,
1259) -> CargoResult<()> {
1260    if let Some(crate_types) = target.crate_types() {
1261        if !crate_types.is_empty() {
1262            let name = name_or_panic(target);
1263            errors.push(format!(
1264                "the target `{}` is a binary and can't have any \
1265                     crate-types set (currently \"{}\")",
1266                name,
1267                crate_types.join(", ")
1268            ));
1269        } else {
1270            validate_crate_types(target, TARGET_KIND_HUMAN_BIN, edition, warnings)?;
1271        }
1272    }
1273    Ok(())
1274}
1275
1276fn validate_crate_types(
1277    target: &TomlTarget,
1278    kind: &str,
1279    edition: Edition,
1280    warnings: &mut Vec<String>,
1281) -> CargoResult<()> {
1282    deprecated_underscore(
1283        &target.crate_type2,
1284        &target.crate_type,
1285        "crate-type",
1286        name_or_panic(target),
1287        format!("{kind} target").as_str(),
1288        edition,
1289        warnings,
1290    )
1291}