#![forbid(unsafe_code)]
mod callbacks;
mod check_release;
mod config;
mod data_generation;
mod manifest;
mod query;
mod rustdoc_gen;
mod templating;
mod util;
mod witness_gen;
use anyhow::Context;
use cargo_metadata::PackageId;
use clap::ValueEnum;
use data_generation::{DataStorage, IntoTerminalResult as _, TerminalError};
use directories::ProjectDirs;
use itertools::Itertools;
use serde::Serialize;
use std::collections::{BTreeMap, HashSet};
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::time::Duration;
use check_release::{LintResult, PendingCrateReport, run_check_release};
use rustdoc_gen::CrateDataForRustdoc;
pub use config::{FeatureFlag, GlobalConfig};
pub use query::{
ActualSemverUpdate, LintLevel, OverrideMap, OverrideStack, QueryOverride, RequiredSemverUpdate,
SemverQuery, Witness, WitnessPurpose,
};
#[non_exhaustive]
#[derive(Debug, PartialEq, Eq, Serialize)]
pub struct Check {
scope: Scope,
current: Rustdoc,
baseline: Rustdoc,
release_type: Option<ReleaseType>,
current_feature_config: rustdoc_gen::FeatureConfig,
baseline_feature_config: rustdoc_gen::FeatureConfig,
build_target: Option<String>,
witness_generation: WitnessGeneration,
}
#[non_exhaustive]
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum ReleaseType {
Major,
Minor,
Patch,
}
#[non_exhaustive]
#[derive(Debug, PartialEq, Eq, Serialize)]
pub struct Rustdoc {
source: RustdocSource,
}
impl Rustdoc {
pub fn from_path(rustdoc_path: impl Into<PathBuf>) -> Self {
Self {
source: RustdocSource::Rustdoc(rustdoc_path.into()),
}
}
pub fn from_root(project_root: impl Into<PathBuf>) -> Self {
Self {
source: RustdocSource::Root(project_root.into()),
}
}
pub fn from_git_revision(
project_root: impl Into<PathBuf>,
revision: impl Into<String>,
) -> Self {
Self {
source: RustdocSource::Revision(project_root.into(), revision.into()),
}
}
pub fn from_registry_latest_crate_version() -> Self {
Self {
source: RustdocSource::VersionFromRegistry(None),
}
}
pub fn from_registry(crate_version: impl Into<String>) -> Self {
Self {
source: RustdocSource::VersionFromRegistry(Some(crate_version.into())),
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize)]
enum RustdocSource {
Rustdoc(PathBuf),
Root(PathBuf),
Revision(PathBuf, String),
VersionFromRegistry(Option<String>),
}
#[derive(Default, Debug, PartialEq, Eq, Serialize)]
struct Scope {
mode: ScopeMode,
}
#[derive(Debug, PartialEq, Eq, Serialize)]
enum ScopeMode {
DenyList(PackageSelection),
AllowList(Vec<String>),
}
impl Default for ScopeMode {
fn default() -> Self {
Self::DenyList(PackageSelection::default())
}
}
#[non_exhaustive]
#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize)]
pub struct PackageSelection {
selection: ScopeSelection,
excluded_packages: Vec<String>,
}
impl PackageSelection {
pub fn new(selection: ScopeSelection) -> Self {
Self {
selection,
excluded_packages: vec![],
}
}
pub fn set_excluded_packages(&mut self, packages: Vec<String>) -> &mut Self {
self.excluded_packages = packages;
self
}
}
#[non_exhaustive]
#[derive(Default, Debug, PartialEq, Eq, Clone, Serialize)]
pub enum ScopeSelection {
Workspace,
#[default]
DefaultMembers,
}
impl Scope {
fn selected_packages<'m>(
&self,
meta: &'m cargo_metadata::Metadata,
) -> (
Vec<&'m cargo_metadata::Package>,
Vec<&'m cargo_metadata::Package>,
) {
let workspace_members: HashSet<&PackageId> = meta.workspace_members.iter().collect();
let base_ids: HashSet<&PackageId> = match &self.mode {
ScopeMode::DenyList(PackageSelection {
selection,
excluded_packages,
}) => {
let packages = match selection {
ScopeSelection::Workspace => workspace_members,
ScopeSelection::DefaultMembers => {
let resolve = meta.resolve.as_ref().expect("no-deps is unsupported");
match &resolve.root {
Some(root) => {
let mut base_ids = HashSet::new();
base_ids.insert(root);
base_ids
}
None => workspace_members,
}
}
};
packages
.iter()
.filter(|p| !excluded_packages.contains(&meta[p].name))
.copied()
.collect()
}
ScopeMode::AllowList(patterns) => {
meta.packages
.iter()
.filter(|p| workspace_members.contains(&p.id) && patterns.contains(&p.name))
.map(|p| &p.id)
.collect()
}
};
meta.packages
.iter()
.filter(|&p| {
base_ids.contains(&p.id)
})
.partition(|&p| p.targets.iter().any(is_lib_like_checkable_target))
}
}
struct CrateToCheck<'a> {
overrides: OverrideStack,
current_crate_data: CrateDataForRustdoc<'a>,
baseline_crate_data: CrateDataForRustdoc<'a>,
}
#[expect(
clippy::unneeded_struct_pattern,
reason = "we don't want a breaking change if the target variants change from unit variants to a different kind"
)]
fn is_lib_like_checkable_target(target: &cargo_metadata::Target) -> bool {
target.is_lib()
|| target.kind.iter().any(|kind| {
matches!(
kind,
cargo_metadata::TargetKind::RLib { .. }
| cargo_metadata::TargetKind::DyLib { .. }
| cargo_metadata::TargetKind::CDyLib { .. }
| cargo_metadata::TargetKind::StaticLib { .. }
)
})
}
impl Check {
pub fn new(current: Rustdoc) -> Self {
Self {
scope: Scope::default(),
current,
baseline: Rustdoc::from_registry_latest_crate_version(),
release_type: None,
current_feature_config: rustdoc_gen::FeatureConfig::default_for_current(),
baseline_feature_config: rustdoc_gen::FeatureConfig::default_for_baseline(),
build_target: None,
witness_generation: WitnessGeneration::default(),
}
}
pub fn set_package_selection(&mut self, selection: PackageSelection) -> &mut Self {
self.scope.mode = ScopeMode::DenyList(selection);
self
}
pub fn set_packages(&mut self, packages: Vec<String>) -> &mut Self {
self.scope.mode = ScopeMode::AllowList(packages);
self
}
pub fn set_baseline(&mut self, baseline: Rustdoc) -> &mut Self {
self.baseline = baseline;
self
}
pub fn set_release_type(&mut self, release_type: ReleaseType) -> &mut Self {
self.release_type = Some(release_type);
self
}
pub fn with_only_explicit_features(&mut self) -> &mut Self {
self.current_feature_config.features_group = rustdoc_gen::FeaturesGroup::None;
self.baseline_feature_config.features_group = rustdoc_gen::FeaturesGroup::None;
self
}
pub fn with_default_features(&mut self) -> &mut Self {
self.current_feature_config.features_group = rustdoc_gen::FeaturesGroup::Default;
self.baseline_feature_config.features_group = rustdoc_gen::FeaturesGroup::Default;
self
}
pub fn with_heuristically_included_features(&mut self) -> &mut Self {
self.current_feature_config.features_group = rustdoc_gen::FeaturesGroup::Heuristic;
self.baseline_feature_config.features_group = rustdoc_gen::FeaturesGroup::Heuristic;
self
}
pub fn with_all_features(&mut self) -> &mut Self {
self.current_feature_config.features_group = rustdoc_gen::FeaturesGroup::All;
self.baseline_feature_config.features_group = rustdoc_gen::FeaturesGroup::All;
self
}
pub fn set_extra_features(
&mut self,
extra_current_features: Vec<String>,
extra_baseline_features: Vec<String>,
) -> &mut Self {
self.current_feature_config.extra_features = extra_current_features;
self.baseline_feature_config.extra_features = extra_baseline_features;
self
}
pub fn set_build_target(&mut self, build_target: String) -> &mut Self {
self.build_target = Some(build_target);
self
}
pub fn set_witness_generation(&mut self, witness_generation: WitnessGeneration) -> &mut Self {
self.witness_generation = witness_generation;
self
}
fn get_target_dir(&self, source: &RustdocSource) -> anyhow::Result<PathBuf> {
Ok(
if let Some(path) = get_target_dir_from_project_root(source)? {
path
} else if let Some(path) = get_target_dir_from_project_root(&self.current.source)? {
path
} else if let Some(path) = get_target_dir_from_project_root(&self.baseline.source)? {
path
} else {
get_cache_dir()?
},
)
}
fn get_rustdoc_generator(
&self,
config: &mut GlobalConfig,
source: &RustdocSource,
) -> anyhow::Result<rustdoc_gen::RustdocGenerator> {
let target_dir = self.get_target_dir(source)?;
Ok(match source {
RustdocSource::Rustdoc(path) => {
rustdoc_gen::RustdocFromFile::new(path.to_owned()).into()
}
RustdocSource::Root(root) => {
rustdoc_gen::RustdocFromProjectRoot::new(root, &target_dir)?.into()
}
RustdocSource::Revision(root, rev) => {
let metadata = manifest_metadata_no_deps(root)?;
let source = metadata.workspace_root.as_std_path();
rustdoc_gen::RustdocFromGitRevision::with_rev(source, &target_dir, rev, config)?
.into()
}
RustdocSource::VersionFromRegistry(version) => {
let mut registry = rustdoc_gen::RustdocFromRegistry::new(&target_dir, config)?;
if let Some(ver) = version {
let semver = semver::Version::parse(ver)?;
registry.set_version(semver);
}
registry.into()
}
})
}
pub fn check_release(&self, config: &mut GlobalConfig) -> anyhow::Result<Report> {
let generation_settings = data_generation::GenerationSettings {
use_color: config.err_color_choice(),
pass_through_stderr: config.is_verbose(),
};
if !(matches!(self.current.source, RustdocSource::Rustdoc(_))
&& matches!(self.baseline.source, RustdocSource::Rustdoc(_)))
{
let rustc_version_needed = config.minimum_rustc_version();
match rustc_version::version() {
Ok(rustc_version) => {
if rustc_version < *rustc_version_needed {
let help = "HELP: to use the latest rustc, run `rustup update stable && cargo +stable semver-checks <args>`";
anyhow::bail!(
"rustc version is not high enough: >={rustc_version_needed} needed, got {rustc_version}\n\n{help}"
);
}
}
Err(error) => {
let help = format!(
"HELP: to avoid errors please ensure rustc >={rustc_version_needed} is used"
);
config.shell_warn(format_args!(
"failed to determine the current rustc version: {error}\n\n{help}"
))?;
}
};
}
let crates_to_check: Vec<CrateToCheck<'_>> = match &self.current.source {
RustdocSource::Rustdoc(_)
| RustdocSource::Revision(_, _)
| RustdocSource::VersionFromRegistry(_) => {
let names = match &self.scope.mode {
ScopeMode::DenyList(_) => match &self.current.source {
RustdocSource::Rustdoc(_) => {
vec!["<unknown>".to_string()]
}
_ => anyhow::bail!(
"couldn't deduce crate name, specify one through the package allow list"
),
},
ScopeMode::AllowList(lst) => lst.clone(),
};
names
.into_iter()
.map(|name| {
let version = None;
CrateToCheck {
overrides: OverrideStack::new(),
current_crate_data: CrateDataForRustdoc {
crate_type: rustdoc_gen::CrateType::Current,
name: name.clone(),
feature_config: &self.current_feature_config,
build_target: self.build_target.as_deref(),
},
baseline_crate_data: CrateDataForRustdoc {
crate_type: rustdoc_gen::CrateType::Baseline {
highest_allowed_version: version,
},
name,
feature_config: &self.baseline_feature_config,
build_target: self.build_target.as_deref(),
},
}
})
.collect()
}
RustdocSource::Root(project_root) => {
let metadata = manifest_metadata(project_root)?;
let (selected, skipped) = self.scope.selected_packages(&metadata);
if selected.is_empty() {
let help = if skipped.is_empty() {
"".to_string()
} else {
let skipped = skipped.iter().map(|&p| &p.name).join(", ");
format!(
"
note: only library targets contain an API surface that can be checked for semver
note: skipped the following crates since they have no library target: {skipped}"
)
};
anyhow::bail!(
"no crates with library targets selected, nothing to semver-check{help}"
);
}
let workspace_overrides =
manifest::deserialize_lint_table(&metadata.workspace_metadata)
.context("[workspace.metadata.cargo-semver-checks] table is invalid")?
.map(|table| table.into_stack());
selected
.iter()
.map(|selected| {
let crate_name = &selected.name;
let version = &selected.version;
let is_implied = matches!(self.scope.mode, ScopeMode::DenyList(..))
&& metadata.workspace_members.len() > 1
&& selected.publish == Some(vec![]);
if is_implied {
config.log_verbose(|config| {
config.shell_status(
"Skipping",
format_args!("{crate_name} v{version} (current)"),
)
})?;
Ok(None)
} else {
let overrides = overrides_for_workspace_package(
selected,
workspace_overrides.as_deref(),
)?;
Ok(Some(CrateToCheck {
overrides,
current_crate_data: CrateDataForRustdoc {
crate_type: rustdoc_gen::CrateType::Current,
name: crate_name.to_string(),
feature_config: &self.current_feature_config,
build_target: self.build_target.as_deref(),
},
baseline_crate_data: CrateDataForRustdoc {
crate_type: rustdoc_gen::CrateType::Baseline {
highest_allowed_version: Some(version.clone()),
},
name: crate_name.to_string(),
feature_config: &self.baseline_feature_config,
build_target: self.build_target.as_deref(),
},
}))
}
})
.filter_map(|res| res.transpose())
.collect::<Result<Vec<_>, anyhow::Error>>()?
}
};
let current_loader = self.get_rustdoc_generator(config, &self.current.source)?;
let baseline_loader = self.get_rustdoc_generator(config, &self.baseline.source)?;
let witness_target_dir = self.get_target_dir(&self.current.source)?;
let all_outcomes: Vec<anyhow::Result<(String, PendingCrateReport)>> = crates_to_check
.into_iter()
.map(|selected| {
let start = std::time::Instant::now();
let name = selected.current_crate_data.name.clone();
let current_loader = rustdoc_gen::StatefulRustdocGenerator::couple_data(
¤t_loader,
config,
&selected.current_crate_data,
)
.map_err(|err| log_terminal_error(config, err))?;
let baseline_loader = rustdoc_gen::StatefulRustdocGenerator::couple_data(
&baseline_loader,
config,
&selected.baseline_crate_data,
)
.map_err(|err| log_terminal_error(config, err))?;
let current_loader = current_loader
.prepare_generator(config)
.map_err(|err| log_terminal_error(config, err))?;
let baseline_loader = baseline_loader
.prepare_generator(config)
.map_err(|err| log_terminal_error(config, err))?;
let witness_data = witness_gen::WitnessGenerationData::new(
baseline_loader.get_data_request(),
current_loader.get_data_request(),
witness_target_dir.clone(),
);
let data_storage = generate_crate_data(
config,
generation_settings,
¤t_loader,
&baseline_loader,
)
.map_err(|err| log_terminal_error(config, err))?;
let report = run_check_release(
config,
&data_storage,
&name,
self.release_type,
&selected.overrides,
&self.witness_generation,
witness_data,
)?;
config.shell_status(
"Finished",
format_args!("[{:>8.3}s] {name}", start.elapsed().as_secs_f32()),
)?;
Ok((name, report))
})
.collect();
let crate_reports: BTreeMap<String, CrateReport> = {
let mut reports = BTreeMap::new();
let mut witness_run_reports = Vec::new();
for outcome in all_outcomes {
let (name, outcome) = outcome?;
witness_run_reports.push(outcome.witness_run_report);
reports.insert(name, outcome.report);
}
match witness_gen::finalize_retained_artifacts(config.run_id(), &witness_run_reports) {
Ok(retained_artifact_dirs) => {
if !retained_artifact_dirs.is_empty() {
if retained_artifact_dirs.len() == 1 {
config.shell_note(format_args!(
"retained witness artifacts in {}",
retained_artifact_dirs[0].display()
))?;
} else {
config.shell_note("retained witness artifacts in:")?;
for dir in retained_artifact_dirs {
writeln!(config.stderr(), "{:12}{}", "", dir.display())?;
}
}
}
}
Err(error) => {
config.shell_warn(format_args!(
"failed to retain witness artifacts: {error:#}"
))?;
}
}
reports
};
Ok(Report { crate_reports })
}
}
fn overrides_for_workspace_package(
package: &cargo_metadata::Package,
workspace_overrides: Option<&[BTreeMap<String, QueryOverride>]>,
) -> Result<OverrideStack, anyhow::Error> {
let lint_table = manifest::deserialize_lint_table(&package.metadata).with_context(|| {
format!(
"package `{}`'s [package.metadata.cargo-semver-checks] table is invalid (at {})",
package.name, package.manifest_path,
)
})?;
let selected_manifest =
manifest::Manifest::parse_standalone(package.manifest_path.clone().into_std_path_buf())?;
let use_workspace_lints = matches!(
selected_manifest.parsed.lints,
cargo_toml::Inheritable::Inherited
);
let metadata_workspace_key = lint_table.as_ref().is_some_and(|x| x.workspace);
let mut overrides = OverrideStack::new();
if (use_workspace_lints || metadata_workspace_key)
&& let Some(workspace) = workspace_overrides
{
for level in workspace {
overrides.push(level);
}
}
if let Some(lint_table) = lint_table {
for level in lint_table.into_stack() {
overrides.push(&level);
}
}
Ok(overrides)
}
#[cold]
fn log_terminal_error(config: &mut GlobalConfig, err: TerminalError) -> anyhow::Error {
match err {
TerminalError::WithAdvice(err, advice) => {
if let Err(err) = config.log_error(|config| {
writeln!(config.stderr(), "{advice}")?;
Ok(())
}) {
return err;
}
err
}
TerminalError::Other(err) => err,
}
}
#[derive(Debug)]
struct Bumps {
major: u32,
minor: u32,
}
impl Bumps {
pub fn update_type(&self) -> Option<RequiredSemverUpdate> {
if self.major > 0 {
Some(RequiredSemverUpdate::Major)
} else if self.minor > 0 {
Some(RequiredSemverUpdate::Minor)
} else {
None
}
}
}
#[non_exhaustive]
#[derive(Debug)]
pub struct CrateReport {
detected_bump: ActualSemverUpdate,
required_bumps: Bumps,
suggested_bumps: Bumps,
lint_results: Vec<LintResult>,
checks_duration: Duration,
selected_checks: usize,
skipped_checks: usize,
witness_statistics: Option<WitnessStatistics>,
}
impl CrateReport {
pub fn success(&self) -> bool {
match self.required_bumps.update_type().map(ReleaseType::from) {
None => true,
Some(required_bump) => {
match self.detected_bump {
ActualSemverUpdate::Major => {
panic!("detected_bump is major, while required_bump is {required_bump:?}")
}
ActualSemverUpdate::Minor => {
assert_eq!(required_bump, ReleaseType::Major);
}
ActualSemverUpdate::Patch | ActualSemverUpdate::NotChanged => {
assert!(matches!(
required_bump,
ReleaseType::Major | ReleaseType::Minor
));
}
}
false
}
}
}
pub fn has_required_witness_errors(&self) -> bool {
self.witness_statistics
.as_ref()
.is_some_and(|statistics| statistics.required_witness_errors() > 0)
}
pub fn required_bump(&self) -> Option<ReleaseType> {
self.required_bumps.update_type().map(ReleaseType::from)
}
pub fn detected_bump(&self) -> ActualSemverUpdate {
self.detected_bump
}
pub fn witness_statistics(&self) -> Option<&WitnessStatistics> {
self.witness_statistics.as_ref()
}
}
#[non_exhaustive]
#[derive(Debug)]
pub struct Report {
crate_reports: BTreeMap<String, CrateReport>,
}
impl Report {
pub fn success(&self) -> bool {
self.crate_reports.values().all(|report| report.success())
}
pub fn is_cli_success(&self) -> bool {
self.success() && !self.has_required_witness_errors()
}
pub fn has_required_witness_errors(&self) -> bool {
self.crate_reports
.values()
.any(|report| report.has_required_witness_errors())
}
pub fn crate_reports(&self) -> &BTreeMap<String, CrateReport> {
&self.crate_reports
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
pub struct WitnessGeneration {
pub show_hints: bool,
pub run_consistency_checks: bool,
}
impl WitnessGeneration {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
show_hints: false,
run_consistency_checks: false,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WitnessStatistics {
not_confirmed_by_witness: usize,
consistency_check_mismatches: usize,
consistency_check_errors: usize,
required_witness_errors: usize,
}
impl WitnessStatistics {
pub(crate) const fn new(
not_confirmed_by_witness: usize,
consistency_check_mismatches: usize,
consistency_check_errors: usize,
required_witness_errors: usize,
) -> Self {
Self {
not_confirmed_by_witness,
consistency_check_mismatches,
consistency_check_errors,
required_witness_errors,
}
}
pub(crate) fn is_empty(&self) -> bool {
self.not_confirmed_by_witness == 0
&& self.consistency_check_mismatches == 0
&& self.consistency_check_errors == 0
&& self.required_witness_errors == 0
}
pub fn not_confirmed_by_witness(&self) -> usize {
self.not_confirmed_by_witness
}
pub fn consistency_check_mismatches(&self) -> usize {
self.consistency_check_mismatches
}
pub fn consistency_check_errors(&self) -> usize {
self.consistency_check_errors
}
pub fn required_witness_errors(&self) -> usize {
self.required_witness_errors
}
}
fn generate_crate_data(
config: &mut GlobalConfig,
generation_settings: data_generation::GenerationSettings,
current_loader: &rustdoc_gen::StatefulRustdocGenerator<'_, rustdoc_gen::ReadyState<'_>>,
baseline_loader: &rustdoc_gen::StatefulRustdocGenerator<'_, rustdoc_gen::ReadyState<'_>>,
) -> Result<DataStorage, TerminalError> {
let current_crate = current_loader.load_rustdoc(
config,
generation_settings,
data_generation::CacheSettings::ReadWrite(()),
)?;
let baseline_crate_name = &baseline_loader.get_crate_data().name;
let current_rustdoc_version = current_crate.version();
let baseline_crate = {
let mut baseline_crate = baseline_loader.load_rustdoc(
config,
generation_settings,
data_generation::CacheSettings::ReadWrite(()),
)?;
if baseline_crate.version() != current_rustdoc_version {
let crate_name = baseline_crate_name;
config
.shell_status(
"Removing",
format_args!("stale cached baseline rustdoc for {crate_name}"),
)
.into_terminal_result()?;
baseline_crate = baseline_loader.load_rustdoc(
config,
generation_settings,
data_generation::CacheSettings::WriteOnly(()),
)?;
assert_eq!(
baseline_crate.version(),
current_rustdoc_version,
"Deleting and regenerating the baseline JSON file did not resolve the rustdoc \
version mismatch."
);
}
baseline_crate
};
Ok(DataStorage::new(current_crate, baseline_crate))
}
fn manifest_path(project_root: &Path) -> anyhow::Result<PathBuf> {
if project_root.is_dir() {
let manifest_path = project_root.join("Cargo.toml");
if manifest_path.exists() {
Ok(manifest_path)
} else {
anyhow::bail!(
"couldn't find Cargo.toml in directory {}",
project_root.display()
)
}
} else if project_root.ends_with("Cargo.toml") {
Ok(project_root.to_path_buf())
} else {
anyhow::bail!(
"path '{}' is not a directory or a manifest",
project_root.display()
)
}
}
fn manifest_metadata(project_root: &Path) -> anyhow::Result<cargo_metadata::Metadata> {
let manifest_path = manifest_path(project_root)?;
let mut command = cargo_metadata::MetadataCommand::new();
let metadata = command.manifest_path(manifest_path).exec()?;
Ok(metadata)
}
fn manifest_metadata_no_deps(project_root: &Path) -> anyhow::Result<cargo_metadata::Metadata> {
let manifest_path = manifest_path(project_root)?;
let mut command = cargo_metadata::MetadataCommand::new();
let metadata = command.manifest_path(manifest_path).no_deps().exec()?;
Ok(metadata)
}
fn get_cache_dir() -> anyhow::Result<PathBuf> {
let project_dirs =
ProjectDirs::from("", "", "cargo-semver-checks").context("can't determine project dirs")?;
let cache_dir = project_dirs.cache_dir();
std::fs::create_dir_all(cache_dir).context("can't create cache dir")?;
Ok(cache_dir.to_path_buf())
}
fn get_target_dir_from_project_root(source: &RustdocSource) -> anyhow::Result<Option<PathBuf>> {
Ok(match source {
RustdocSource::Root(root) => {
let metadata = manifest_metadata_no_deps(root)?;
let target = metadata.target_directory.as_std_path().join(util::SCOPE);
Some(target)
}
RustdocSource::Revision(root, rev) => {
let metadata = manifest_metadata_no_deps(root)?;
let target = metadata.target_directory.as_std_path().join(util::SCOPE);
let target = target.join(format!("git-{}", util::slugify(rev)));
Some(target)
}
RustdocSource::Rustdoc(_path) => None,
RustdocSource::VersionFromRegistry(_version) => None,
})
}