1#![forbid(unsafe_code)]
2
3mod callbacks;
4mod check_release;
5mod config;
6mod data_generation;
7mod manifest;
8mod query;
9mod rustdoc_gen;
10mod templating;
11mod util;
12mod witness_gen;
13
14use anyhow::Context;
15use cargo_metadata::PackageId;
16use clap::ValueEnum;
17use data_generation::{DataStorage, IntoTerminalResult as _, TerminalError};
18use directories::ProjectDirs;
19use itertools::Itertools;
20use serde::Serialize;
21
22use std::collections::{BTreeMap, HashSet};
23use std::io::Write as _;
24use std::path::{Path, PathBuf};
25use std::time::Duration;
26
27use check_release::{LintResult, run_check_release};
28use rustdoc_gen::CrateDataForRustdoc;
29
30pub use config::{FeatureFlag, GlobalConfig};
31pub use query::{
32 ActualSemverUpdate, LintLevel, OverrideMap, OverrideStack, QueryOverride, RequiredSemverUpdate,
33 SemverQuery, Witness,
34};
35
36#[non_exhaustive]
38#[derive(Debug, PartialEq, Eq, Serialize)]
39pub struct Check {
40 scope: Scope,
42 current: Rustdoc,
43 baseline: Rustdoc,
44 release_type: Option<ReleaseType>,
45 current_feature_config: rustdoc_gen::FeatureConfig,
46 baseline_feature_config: rustdoc_gen::FeatureConfig,
47 build_target: Option<String>,
49 witness_generation: WitnessGeneration,
51}
52
53#[non_exhaustive]
58#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize)]
59pub enum ReleaseType {
60 Major,
61 Minor,
62 Patch,
63}
64
65#[non_exhaustive]
66#[derive(Debug, PartialEq, Eq, Serialize)]
67pub struct Rustdoc {
68 source: RustdocSource,
69}
70
71impl Rustdoc {
72 pub fn from_path(rustdoc_path: impl Into<PathBuf>) -> Self {
74 Self {
75 source: RustdocSource::Rustdoc(rustdoc_path.into()),
76 }
77 }
78
79 pub fn from_root(project_root: impl Into<PathBuf>) -> Self {
84 Self {
85 source: RustdocSource::Root(project_root.into()),
86 }
87 }
88
89 pub fn from_git_revision(
91 project_root: impl Into<PathBuf>,
92 revision: impl Into<String>,
93 ) -> Self {
94 Self {
95 source: RustdocSource::Revision(project_root.into(), revision.into()),
96 }
97 }
98
99 pub fn from_registry_latest_crate_version() -> Self {
103 Self {
104 source: RustdocSource::VersionFromRegistry(None),
105 }
106 }
107
108 pub fn from_registry(crate_version: impl Into<String>) -> Self {
110 Self {
111 source: RustdocSource::VersionFromRegistry(Some(crate_version.into())),
112 }
113 }
114}
115
116#[derive(Debug, PartialEq, Eq, Serialize)]
117enum RustdocSource {
118 Rustdoc(PathBuf),
121 Root(PathBuf),
124 Revision(PathBuf, String),
126 VersionFromRegistry(Option<String>),
131}
132
133#[derive(Default, Debug, PartialEq, Eq, Serialize)]
135struct Scope {
136 mode: ScopeMode,
137}
138
139#[derive(Debug, PartialEq, Eq, Serialize)]
140enum ScopeMode {
141 DenyList(PackageSelection),
143 AllowList(Vec<String>),
145}
146
147impl Default for ScopeMode {
148 fn default() -> Self {
149 Self::DenyList(PackageSelection::default())
150 }
151}
152
153#[non_exhaustive]
154#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize)]
155pub struct PackageSelection {
156 selection: ScopeSelection,
157 excluded_packages: Vec<String>,
158}
159
160impl PackageSelection {
161 pub fn new(selection: ScopeSelection) -> Self {
162 Self {
163 selection,
164 excluded_packages: vec![],
165 }
166 }
167
168 pub fn set_excluded_packages(&mut self, packages: Vec<String>) -> &mut Self {
169 self.excluded_packages = packages;
170 self
171 }
172}
173
174#[non_exhaustive]
175#[derive(Default, Debug, PartialEq, Eq, Clone, Serialize)]
176pub enum ScopeSelection {
177 Workspace,
179 #[default]
181 DefaultMembers,
182}
183
184impl Scope {
185 fn selected_packages<'m>(
187 &self,
188 meta: &'m cargo_metadata::Metadata,
189 ) -> (
190 Vec<&'m cargo_metadata::Package>,
191 Vec<&'m cargo_metadata::Package>,
192 ) {
193 let workspace_members: HashSet<&PackageId> = meta.workspace_members.iter().collect();
194 let base_ids: HashSet<&PackageId> = match &self.mode {
195 ScopeMode::DenyList(PackageSelection {
196 selection,
197 excluded_packages,
198 }) => {
199 let packages = match selection {
200 ScopeSelection::Workspace => workspace_members,
201 ScopeSelection::DefaultMembers => {
202 let resolve = meta.resolve.as_ref().expect("no-deps is unsupported");
204 match &resolve.root {
205 Some(root) => {
206 let mut base_ids = HashSet::new();
207 base_ids.insert(root);
208 base_ids
209 }
210 None => workspace_members,
211 }
212 }
213 };
214
215 packages
216 .iter()
217 .filter(|p| !excluded_packages.contains(&meta[p].name))
218 .copied()
219 .collect()
220 }
221 ScopeMode::AllowList(patterns) => {
222 meta.packages
223 .iter()
224 .filter(|p| workspace_members.contains(&p.id) && patterns.contains(&p.name))
227 .map(|p| &p.id)
228 .collect()
229 }
230 };
231
232 meta.packages
233 .iter()
234 .filter(|&p| {
235 base_ids.contains(&p.id)
237 })
238 .partition(|&p| p.targets.iter().any(is_lib_like_checkable_target))
239 }
240}
241
242struct CrateToCheck<'a> {
243 overrides: OverrideStack,
244 current_crate_data: CrateDataForRustdoc<'a>,
245 baseline_crate_data: CrateDataForRustdoc<'a>,
246}
247
248#[expect(
253 clippy::unneeded_struct_pattern,
254 reason = "we don't want a breaking change if the target variants change from unit variants to a different kind"
255)]
256fn is_lib_like_checkable_target(target: &cargo_metadata::Target) -> bool {
257 target.is_lib()
258 || target.kind.iter().any(|kind| {
259 matches!(
260 kind,
261 cargo_metadata::TargetKind::RLib { .. }
262 | cargo_metadata::TargetKind::DyLib { .. }
263 | cargo_metadata::TargetKind::CDyLib { .. }
264 | cargo_metadata::TargetKind::StaticLib { .. }
265 )
266 })
267}
268
269impl Check {
270 pub fn new(current: Rustdoc) -> Self {
271 Self {
272 scope: Scope::default(),
273 current,
274 baseline: Rustdoc::from_registry_latest_crate_version(),
275 release_type: None,
276 current_feature_config: rustdoc_gen::FeatureConfig::default_for_current(),
277 baseline_feature_config: rustdoc_gen::FeatureConfig::default_for_baseline(),
278 build_target: None,
279 witness_generation: WitnessGeneration::default(),
280 }
281 }
282
283 pub fn set_package_selection(&mut self, selection: PackageSelection) -> &mut Self {
284 self.scope.mode = ScopeMode::DenyList(selection);
285 self
286 }
287
288 pub fn set_packages(&mut self, packages: Vec<String>) -> &mut Self {
289 self.scope.mode = ScopeMode::AllowList(packages);
290 self
291 }
292
293 pub fn set_baseline(&mut self, baseline: Rustdoc) -> &mut Self {
294 self.baseline = baseline;
295 self
296 }
297
298 pub fn set_release_type(&mut self, release_type: ReleaseType) -> &mut Self {
299 self.release_type = Some(release_type);
300 self
301 }
302
303 pub fn with_only_explicit_features(&mut self) -> &mut Self {
304 self.current_feature_config.features_group = rustdoc_gen::FeaturesGroup::None;
305 self.baseline_feature_config.features_group = rustdoc_gen::FeaturesGroup::None;
306 self
307 }
308
309 pub fn with_default_features(&mut self) -> &mut Self {
310 self.current_feature_config.features_group = rustdoc_gen::FeaturesGroup::Default;
311 self.baseline_feature_config.features_group = rustdoc_gen::FeaturesGroup::Default;
312 self
313 }
314
315 pub fn with_heuristically_included_features(&mut self) -> &mut Self {
316 self.current_feature_config.features_group = rustdoc_gen::FeaturesGroup::Heuristic;
317 self.baseline_feature_config.features_group = rustdoc_gen::FeaturesGroup::Heuristic;
318 self
319 }
320
321 pub fn with_all_features(&mut self) -> &mut Self {
322 self.current_feature_config.features_group = rustdoc_gen::FeaturesGroup::All;
323 self.baseline_feature_config.features_group = rustdoc_gen::FeaturesGroup::All;
324 self
325 }
326
327 pub fn set_extra_features(
328 &mut self,
329 extra_current_features: Vec<String>,
330 extra_baseline_features: Vec<String>,
331 ) -> &mut Self {
332 self.current_feature_config.extra_features = extra_current_features;
333 self.baseline_feature_config.extra_features = extra_baseline_features;
334 self
335 }
336
337 pub fn set_build_target(&mut self, build_target: String) -> &mut Self {
340 self.build_target = Some(build_target);
341 self
342 }
343
344 pub fn set_witness_generation(&mut self, witness_generation: WitnessGeneration) -> &mut Self {
346 self.witness_generation = witness_generation;
347 self
348 }
349
350 fn get_target_dir(&self, source: &RustdocSource) -> anyhow::Result<PathBuf> {
358 Ok(
359 if let Some(path) = get_target_dir_from_project_root(source)? {
360 path
361 } else if let Some(path) = get_target_dir_from_project_root(&self.current.source)? {
362 path
363 } else if let Some(path) = get_target_dir_from_project_root(&self.baseline.source)? {
364 path
365 } else {
366 get_cache_dir()?
367 },
368 )
369 }
370
371 fn get_rustdoc_generator(
372 &self,
373 config: &mut GlobalConfig,
374 source: &RustdocSource,
375 ) -> anyhow::Result<rustdoc_gen::RustdocGenerator> {
376 let target_dir = self.get_target_dir(source)?;
377 Ok(match source {
378 RustdocSource::Rustdoc(path) => {
379 rustdoc_gen::RustdocFromFile::new(path.to_owned()).into()
380 }
381 RustdocSource::Root(root) => {
382 rustdoc_gen::RustdocFromProjectRoot::new(root, &target_dir)?.into()
383 }
384 RustdocSource::Revision(root, rev) => {
385 let metadata = manifest_metadata_no_deps(root)?;
386 let source = metadata.workspace_root.as_std_path();
387 rustdoc_gen::RustdocFromGitRevision::with_rev(source, &target_dir, rev, config)?
388 .into()
389 }
390 RustdocSource::VersionFromRegistry(version) => {
391 let mut registry = rustdoc_gen::RustdocFromRegistry::new(&target_dir, config)?;
392 if let Some(ver) = version {
393 let semver = semver::Version::parse(ver)?;
394 registry.set_version(semver);
395 }
396 registry.into()
397 }
398 })
399 }
400
401 pub fn check_release(&self, config: &mut GlobalConfig) -> anyhow::Result<Report> {
402 let generation_settings = data_generation::GenerationSettings {
403 use_color: config.err_color_choice(),
404 deps: false,
405 pass_through_stderr: config.is_verbose(),
406 };
407
408 if !(matches!(self.current.source, RustdocSource::Rustdoc(_))
411 && matches!(self.baseline.source, RustdocSource::Rustdoc(_)))
412 {
413 let rustc_version_needed = config.minimum_rustc_version();
414 match rustc_version::version() {
415 Ok(rustc_version) => {
416 if rustc_version < *rustc_version_needed {
417 let help = "HELP: to use the latest rustc, run `rustup update stable && cargo +stable semver-checks <args>`";
418 anyhow::bail!(
419 "rustc version is not high enough: >={rustc_version_needed} needed, got {rustc_version}\n\n{help}"
420 );
421 }
422 }
423 Err(error) => {
424 let help = format!(
425 "HELP: to avoid errors please ensure rustc >={rustc_version_needed} is used"
426 );
427 config.shell_warn(format_args!(
428 "failed to determine the current rustc version: {error}\n\n{help}"
429 ))?;
430 }
431 };
432 }
433
434 let crates_to_check: Vec<CrateToCheck<'_>> = match &self.current.source {
435 RustdocSource::Rustdoc(_)
436 | RustdocSource::Revision(_, _)
437 | RustdocSource::VersionFromRegistry(_) => {
438 let names = match &self.scope.mode {
439 ScopeMode::DenyList(_) => match &self.current.source {
440 RustdocSource::Rustdoc(_) => {
441 vec!["<unknown>".to_string()]
445 }
446 _ => anyhow::bail!(
447 "couldn't deduce crate name, specify one through the package allow list"
448 ),
449 },
450 ScopeMode::AllowList(lst) => lst.clone(),
451 };
452 names
453 .into_iter()
454 .map(|name| {
455 let version = None;
456 CrateToCheck {
457 overrides: OverrideStack::new(),
458 current_crate_data: CrateDataForRustdoc {
459 crate_type: rustdoc_gen::CrateType::Current,
460 name: name.clone(),
461 feature_config: &self.current_feature_config,
462 build_target: self.build_target.as_deref(),
463 },
464 baseline_crate_data: CrateDataForRustdoc {
465 crate_type: rustdoc_gen::CrateType::Baseline {
466 highest_allowed_version: version,
467 },
468 name,
469 feature_config: &self.baseline_feature_config,
470 build_target: self.build_target.as_deref(),
471 },
472 }
473 })
474 .collect()
475 }
476 RustdocSource::Root(project_root) => {
477 let metadata = manifest_metadata(project_root)?;
478 let (selected, skipped) = self.scope.selected_packages(&metadata);
479 if selected.is_empty() {
480 let help = if skipped.is_empty() {
481 "".to_string()
482 } else {
483 let skipped = skipped.iter().map(|&p| &p.name).join(", ");
484 format!(
485 "
486note: only library targets contain an API surface that can be checked for semver
487note: skipped the following crates since they have no library target: {skipped}"
488 )
489 };
490 anyhow::bail!(
491 "no crates with library targets selected, nothing to semver-check{help}"
492 );
493 }
494
495 let workspace_overrides =
496 manifest::deserialize_lint_table(&metadata.workspace_metadata)
497 .context("[workspace.metadata.cargo-semver-checks] table is invalid")?
498 .map(|table| table.into_stack());
499
500 selected
501 .iter()
502 .map(|selected| {
503 let crate_name = &selected.name;
504 let version = &selected.version;
505
506 let is_implied = matches!(self.scope.mode, ScopeMode::DenyList(..))
511 && metadata.workspace_members.len() > 1
512 && selected.publish == Some(vec![]);
513 if is_implied {
514 config.log_verbose(|config| {
515 config.shell_status(
516 "Skipping",
517 format_args!("{crate_name} v{version} (current)"),
518 )
519 })?;
520 Ok(None)
521 } else {
522 let overrides = overrides_for_workspace_package(
523 selected,
524 workspace_overrides.as_deref(),
525 )?;
526
527 Ok(Some(CrateToCheck {
528 overrides,
529 current_crate_data: CrateDataForRustdoc {
530 crate_type: rustdoc_gen::CrateType::Current,
531 name: crate_name.to_string(),
532 feature_config: &self.current_feature_config,
533 build_target: self.build_target.as_deref(),
534 },
535 baseline_crate_data: CrateDataForRustdoc {
536 crate_type: rustdoc_gen::CrateType::Baseline {
537 highest_allowed_version: Some(version.clone()),
538 },
539 name: crate_name.to_string(),
540 feature_config: &self.baseline_feature_config,
541 build_target: self.build_target.as_deref(),
542 },
543 }))
544 }
545 })
546 .filter_map(|res| res.transpose())
547 .collect::<Result<Vec<_>, anyhow::Error>>()?
548 }
549 };
550
551 let current_loader = self.get_rustdoc_generator(config, &self.current.source)?;
552 let baseline_loader = self.get_rustdoc_generator(config, &self.baseline.source)?;
553
554 let all_outcomes: Vec<anyhow::Result<(String, CrateReport)>> = crates_to_check
557 .into_iter()
558 .map(|selected| {
559 let start = std::time::Instant::now();
560 let name = selected.current_crate_data.name.clone();
561
562 let current_loader = rustdoc_gen::StatefulRustdocGenerator::couple_data(
563 ¤t_loader,
564 config,
565 &selected.current_crate_data,
566 )
567 .map_err(|err| log_terminal_error(config, err))?;
568 let baseline_loader = rustdoc_gen::StatefulRustdocGenerator::couple_data(
569 &baseline_loader,
570 config,
571 &selected.baseline_crate_data,
572 )
573 .map_err(|err| log_terminal_error(config, err))?;
574
575 let current_loader = current_loader
576 .prepare_generator(config)
577 .map_err(|err| log_terminal_error(config, err))?;
578 let baseline_loader = baseline_loader
579 .prepare_generator(config)
580 .map_err(|err| log_terminal_error(config, err))?;
581
582 let witness_data = witness_gen::WitnessGenerationData::new(
583 baseline_loader.get_data_request(),
584 current_loader.get_data_request(),
585 current_loader.get_target_root(),
586 );
587
588 let data_storage = generate_crate_data(
589 config,
590 generation_settings,
591 ¤t_loader,
592 &baseline_loader,
593 )
594 .map_err(|err| log_terminal_error(config, err))?;
595
596 let report = run_check_release(
597 config,
598 &data_storage,
599 &name,
600 self.release_type,
601 &selected.overrides,
602 &self.witness_generation,
603 witness_data,
604 )?;
605 config.shell_status(
606 "Finished",
607 format_args!("[{:>8.3}s] {name}", start.elapsed().as_secs_f32()),
608 )?;
609 Ok((name, report))
610 })
611 .collect();
612 let crate_reports: BTreeMap<String, CrateReport> = {
613 let mut reports = BTreeMap::new();
614 for outcome in all_outcomes {
615 let (name, outcome) = outcome?;
616 reports.insert(name, outcome);
617 }
618 reports
619 };
620
621 Ok(Report { crate_reports })
622 }
623}
624
625fn overrides_for_workspace_package(
626 package: &cargo_metadata::Package,
627 workspace_overrides: Option<&[BTreeMap<String, QueryOverride>]>,
628) -> Result<OverrideStack, anyhow::Error> {
629 let lint_table = manifest::deserialize_lint_table(&package.metadata).with_context(|| {
630 format!(
631 "package `{}`'s [package.metadata.cargo-semver-checks] table is invalid (at {})",
632 package.name, package.manifest_path,
633 )
634 })?;
635 let selected_manifest =
636 manifest::Manifest::parse_standalone(package.manifest_path.clone().into_std_path_buf())?;
637
638 let use_workspace_lints = matches!(
640 selected_manifest.parsed.lints,
641 cargo_toml::Inheritable::Inherited
642 );
643 let metadata_workspace_key = lint_table.as_ref().is_some_and(|x| x.workspace);
644
645 let mut overrides = OverrideStack::new();
646 if (use_workspace_lints || metadata_workspace_key)
647 && let Some(workspace) = workspace_overrides
648 {
649 for level in workspace {
650 overrides.push(level);
651 }
652 }
653 if let Some(lint_table) = lint_table {
654 for level in lint_table.into_stack() {
655 overrides.push(&level);
656 }
657 }
658 Ok(overrides)
659}
660
661#[cold]
662fn log_terminal_error(config: &mut GlobalConfig, err: TerminalError) -> anyhow::Error {
663 match err {
664 TerminalError::WithAdvice(err, advice) => {
665 if let Err(err) = config.log_error(|config| {
666 writeln!(config.stderr(), "{advice}")?;
667 Ok(())
668 }) {
669 return err;
670 }
671 err
672 }
673 TerminalError::Other(err) => err,
674 }
675}
676
677#[derive(Debug)]
679struct Bumps {
680 major: u32,
681 minor: u32,
682}
683
684impl Bumps {
685 pub fn update_type(&self) -> Option<RequiredSemverUpdate> {
689 if self.major > 0 {
690 Some(RequiredSemverUpdate::Major)
691 } else if self.minor > 0 {
692 Some(RequiredSemverUpdate::Minor)
693 } else {
694 None
695 }
696 }
697}
698
699#[non_exhaustive]
701#[derive(Debug)]
702pub struct CrateReport {
703 detected_bump: ActualSemverUpdate,
705 required_bumps: Bumps,
707 suggested_bumps: Bumps,
709 lint_results: Vec<LintResult>,
711 checks_duration: Duration,
713 selected_checks: usize,
715 skipped_checks: usize,
717}
718
719impl CrateReport {
720 pub fn success(&self) -> bool {
723 match self.required_bumps.update_type().map(ReleaseType::from) {
724 None => true,
726 Some(required_bump) => {
728 match self.detected_bump {
731 ActualSemverUpdate::Major => {
734 panic!("detected_bump is major, while required_bump is {required_bump:?}")
735 }
736 ActualSemverUpdate::Minor => {
737 assert_eq!(required_bump, ReleaseType::Major);
738 }
739 ActualSemverUpdate::Patch | ActualSemverUpdate::NotChanged => {
740 assert!(matches!(
741 required_bump,
742 ReleaseType::Major | ReleaseType::Minor
743 ));
744 }
745 }
746 false
747 }
748 }
749 }
750
751 pub fn required_bump(&self) -> Option<ReleaseType> {
754 self.required_bumps.update_type().map(ReleaseType::from)
755 }
756
757 pub fn detected_bump(&self) -> ActualSemverUpdate {
759 self.detected_bump
760 }
761}
762
763#[non_exhaustive]
766#[derive(Debug)]
767pub struct Report {
768 crate_reports: BTreeMap<String, CrateReport>,
770}
771
772impl Report {
773 pub fn success(&self) -> bool {
775 self.crate_reports.values().all(|report| report.success())
776 }
777
778 pub fn crate_reports(&self) -> &BTreeMap<String, CrateReport> {
780 &self.crate_reports
781 }
782}
783
784#[non_exhaustive]
789#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
790pub struct WitnessGeneration {
791 pub show_hints: bool,
794 pub generate_witnesses: bool,
797}
798
799impl WitnessGeneration {
800 #[inline]
802 #[must_use]
803 pub const fn new() -> Self {
804 Self {
805 show_hints: false,
806 generate_witnesses: false,
807 }
808 }
809}
810
811fn generate_crate_data(
812 config: &mut GlobalConfig,
813 generation_settings: data_generation::GenerationSettings,
814 current_loader: &rustdoc_gen::StatefulRustdocGenerator<'_, rustdoc_gen::ReadyState<'_>>,
815 baseline_loader: &rustdoc_gen::StatefulRustdocGenerator<'_, rustdoc_gen::ReadyState<'_>>,
816) -> Result<DataStorage, TerminalError> {
817 let current_crate = current_loader.load_rustdoc(
818 config,
819 generation_settings,
820 data_generation::CacheSettings::ReadWrite(()),
821 )?;
822
823 let baseline_crate_name = &baseline_loader.get_crate_data().name;
824 let current_rustdoc_version = current_crate.version();
825
826 let baseline_crate = {
827 let mut baseline_crate = baseline_loader.load_rustdoc(
828 config,
829 generation_settings,
830 data_generation::CacheSettings::ReadWrite(()),
831 )?;
832
833 if baseline_crate.version() != current_rustdoc_version {
841 let crate_name = baseline_crate_name;
842 config
843 .shell_status(
844 "Removing",
845 format_args!("stale cached baseline rustdoc for {crate_name}"),
846 )
847 .into_terminal_result()?;
848
849 baseline_crate = baseline_loader.load_rustdoc(
850 config,
851 generation_settings,
852 data_generation::CacheSettings::WriteOnly(()),
853 )?;
854
855 assert_eq!(
856 baseline_crate.version(),
857 current_rustdoc_version,
858 "Deleting and regenerating the baseline JSON file did not resolve the rustdoc \
859 version mismatch."
860 );
861 }
862
863 baseline_crate
864 };
865
866 Ok(DataStorage::new(current_crate, baseline_crate))
867}
868
869fn manifest_path(project_root: &Path) -> anyhow::Result<PathBuf> {
870 if project_root.is_dir() {
871 let manifest_path = project_root.join("Cargo.toml");
872 if manifest_path.exists() {
876 Ok(manifest_path)
877 } else {
878 anyhow::bail!(
879 "couldn't find Cargo.toml in directory {}",
880 project_root.display()
881 )
882 }
883 } else if project_root.ends_with("Cargo.toml") {
884 Ok(project_root.to_path_buf())
888 } else {
889 anyhow::bail!(
890 "path {} is not a directory or a manifest",
891 project_root.display()
892 )
893 }
894}
895
896fn manifest_metadata(project_root: &Path) -> anyhow::Result<cargo_metadata::Metadata> {
897 let manifest_path = manifest_path(project_root)?;
898 let mut command = cargo_metadata::MetadataCommand::new();
899 let metadata = command.manifest_path(manifest_path).exec()?;
900 Ok(metadata)
901}
902
903fn manifest_metadata_no_deps(project_root: &Path) -> anyhow::Result<cargo_metadata::Metadata> {
904 let manifest_path = manifest_path(project_root)?;
905 let mut command = cargo_metadata::MetadataCommand::new();
906 let metadata = command.manifest_path(manifest_path).no_deps().exec()?;
907 Ok(metadata)
908}
909
910fn get_cache_dir() -> anyhow::Result<PathBuf> {
911 let project_dirs =
912 ProjectDirs::from("", "", "cargo-semver-checks").context("can't determine project dirs")?;
913 let cache_dir = project_dirs.cache_dir();
914 std::fs::create_dir_all(cache_dir).context("can't create cache dir")?;
915 Ok(cache_dir.to_path_buf())
916}
917
918fn get_target_dir_from_project_root(source: &RustdocSource) -> anyhow::Result<Option<PathBuf>> {
919 Ok(match source {
920 RustdocSource::Root(root) => {
921 let metadata = manifest_metadata_no_deps(root)?;
922 let target = metadata.target_directory.as_std_path().join(util::SCOPE);
923 Some(target)
924 }
925 RustdocSource::Revision(root, rev) => {
926 let metadata = manifest_metadata_no_deps(root)?;
927 let target = metadata.target_directory.as_std_path().join(util::SCOPE);
928 let target = target.join(format!("git-{}", util::slugify(rev)));
929 Some(target)
930 }
931 RustdocSource::Rustdoc(_path) => None,
932 RustdocSource::VersionFromRegistry(_version) => None,
933 })
934}