use anyhow::Context;
use cargo_metadata::{
Package,
camino::{Utf8Path, Utf8PathBuf},
};
use cargo_utils::{CARGO_TOML, get_manifest_metadata};
use tracing::{debug, info};
use crate::{cargo::run_cargo, fs_utils};
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
io::{self, Read},
path::Path,
};
pub fn are_packages_equal(
local_package: &Utf8Path,
registry_package: &Utf8Path,
) -> anyhow::Result<bool> {
debug!(
"compare local package {:?} with registry package {:?}",
local_package, registry_package
);
if !are_cargo_toml_equal(local_package, registry_package) {
debug!("Cargo.toml is different");
return Ok(false);
}
rename(
registry_package.join("Cargo.toml.orig"),
registry_package.join("Cargo.toml.orig.orig"),
)?;
let local_package_files = get_cargo_package_files(local_package).with_context(|| {
format!("cannot determine packaged files of local package {local_package:?}")
})?;
let registry_package_files = get_cargo_package_files(registry_package).with_context(|| {
format!("cannot determine packaged files of registry package {registry_package:?}")
})?;
rename(
registry_package.join("Cargo.toml.orig.orig"),
registry_package.join("Cargo.toml.orig"),
)?;
let local_files = local_package_files
.iter()
.filter(|file| *file != "Cargo.toml.orig" && *file != ".cargo_vcs_info.json");
let registry_files = registry_package_files.iter().filter(|file| {
*file != "Cargo.toml.orig"
&& *file != "Cargo.toml.orig.orig"
&& *file != ".cargo_vcs_info.json"
});
if !local_files.clone().eq(registry_files) {
debug!("cargo package list is different");
return Ok(false);
}
let local_files = local_files
.map(|file| local_package.join(file))
.filter(|file| {
!(file.is_symlink()
|| !file.exists()
|| file.file_name() == Some("Cargo.lock")
|| file.file_name() == Some(CARGO_TOML)
|| file.file_name() == Some("Cargo.toml.orig"))
});
for local_path in local_files {
let relative_path = local_path
.strip_prefix(local_package)
.with_context(|| format!("can't find {local_package:?} prefix in {local_path:?}"))?;
let registry_path = registry_package.join(relative_path);
if !are_files_equal(&local_path, ®istry_path).context("files are not equal")? {
return Ok(false);
}
}
Ok(true)
}
fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> anyhow::Result<()> {
let from = from.as_ref();
let to = to.as_ref();
fs_err::rename(from, to).with_context(|| format!("cannot rename {from:?} to {to:?}"))
}
pub fn get_cargo_package_files(package: &Utf8Path) -> anyhow::Result<Vec<Utf8PathBuf>> {
info!("Getting packaged files for crate at {}", package);
if is_cargo_packaged_dir(package)
&& (package.join("Cargo.toml.orig").exists()
|| package.join("Cargo.toml.orig.orig").exists())
{
let list =
list_packaged_files(package).context("cannot list packaged files from directory")?;
debug!("Packaged files: {:?}", list);
Ok(list)
} else {
let list = get_cargo_package_list(package)
.context("cannot get packaged files from cargo package list")?;
debug!("Cargo Packaged files: {:?}", list);
Ok(list)
}
}
fn get_cargo_package_list(package: &Utf8Path) -> Result<Vec<Utf8PathBuf>, anyhow::Error> {
let args = ["package", "--list", "--quiet", "--allow-dirty"];
let output = run_cargo(package, &args).context("cannot run `cargo package`")?;
anyhow::ensure!(
output.status.success(),
"error while running `cargo package`: {}",
output.stderr
);
let files = output.stdout.lines().map(Utf8PathBuf::from).collect();
Ok(files)
}
fn is_cargo_packaged_dir(package: &Utf8Path) -> bool {
package.ancestors().any(|ancestor| {
ancestor.file_name() == Some("package")
&& ancestor.parent().and_then(|parent| parent.file_name()) == Some("target")
})
}
fn list_packaged_files(package: &Utf8Path) -> anyhow::Result<Vec<Utf8PathBuf>> {
let mut files = Vec::new();
let mut dirs = vec![package.to_path_buf()];
while let Some(dir) = dirs.pop() {
for entry in fs_err::read_dir(&dir).with_context(|| format!("cannot read dir {dir:?}"))? {
let entry = entry.with_context(|| format!("cannot read dir entry in {dir:?}"))?;
let path = Utf8PathBuf::from_path_buf(entry.path())
.map_err(|path| anyhow::anyhow!("non-utf8 path in package: {path:?}"))?;
let file_type = entry
.file_type()
.with_context(|| format!("cannot read file type for {path:?}"))?;
if file_type.is_dir() {
dirs.push(path);
} else {
let rel_path = path
.strip_prefix(package)
.with_context(|| format!("can't find {package:?} prefix in {path:?}"))?;
files.push(rel_path.to_path_buf());
}
}
}
files.sort_by(|a, b| a.as_str().cmp(b.as_str()));
Ok(files)
}
fn are_cargo_toml_equal(local_package: &Utf8Path, registry_package: &Utf8Path) -> bool {
let cargo_orig = format!("{CARGO_TOML}.orig");
are_files_equal(
&local_package.join(CARGO_TOML),
®istry_package.join(cargo_orig),
)
.unwrap_or(false)
}
pub fn is_readme_updated(
package_name: &str,
local_package_path: &Utf8Path,
registry_package_path: &Utf8Path,
) -> anyhow::Result<bool> {
let package = match read_package_metadata(package_name, local_package_path) {
Ok(package) => package,
Err(e) => {
tracing::warn!(
"cannot read package metadata of {package_name} in {local_package_path}: {e:?}"
);
return Ok(false);
}
};
let local_package_readme_path = local_readme_override(&package, local_package_path);
let are_readmes_equal = match local_package_readme_path? {
Some(local_package_readme_path) => {
let registry_package_readme_path = registry_package_path.join("README.md");
if !registry_package_readme_path.exists() {
return Ok(true);
}
match are_files_equal(&local_package_readme_path, ®istry_package_readme_path) {
Ok(are_readmes_equal) => are_readmes_equal,
Err(e) => {
tracing::warn!("cannot compare README files: {e}");
true
}
}
}
None => true,
};
Ok(!are_readmes_equal)
}
pub fn local_readme_override(
package: &Package,
local_package_path: &Utf8Path,
) -> anyhow::Result<Option<Utf8PathBuf>> {
package
.readme
.as_ref()
.and_then(|readme| {
let readme_path = local_package_path.join(readme);
if !readme_path.exists() {
tracing::warn!(
"README path '{}' doesn't exist for package '{}'. Hint: ensure the path set in Cargo.toml points to a file that exists and is included in the crate.",
readme_path,
package.name
);
return None;
}
Some(fs_utils::canonicalize_utf8(&readme_path))
})
.transpose()
}
fn are_files_equal(first: &Utf8Path, second: &Utf8Path) -> anyhow::Result<bool> {
let hash1 = file_hash(first).with_context(|| format!("cannot determine hash of {first:?}"))?;
let hash2 =
file_hash(second).with_context(|| format!("cannot determine hash of {second:?}"))?;
Ok(hash1 == hash2)
}
fn file_hash(file: &Utf8Path) -> io::Result<u64> {
let buffer = &mut vec![];
fs_err::File::open(file)?.read_to_end(buffer)?;
let mut hasher = DefaultHasher::new();
buffer.hash(&mut hasher);
let hash = hasher.finish();
Ok(hash)
}
fn read_package_metadata(
package_name: &str,
local_package_path: &Utf8Path,
) -> anyhow::Result<Package> {
let package = get_manifest_metadata(&local_package_path.join(CARGO_TOML))
.context("cannot read Cargo.toml")?
.workspace_packages()
.into_iter()
.find(|&p| *p.name == package_name)
.cloned()
.context("cannot find package in Cargo.toml")?;
Ok(package)
}