use std::{
collections::btree_map::{self, BTreeMap},
env, fmt, fs, io,
path::{Path, PathBuf},
process::Command,
sync::Mutex,
time::SystemTime,
};
use toml_edit::{DocumentMut, Item, TableLike, TomlError};
pub enum Error {
NotFound(PathBuf),
CargoManifestDirNotSet,
FailedGettingWorkspaceManifestPath,
CouldNotRead { path: PathBuf, source: io::Error },
InvalidToml { source: TomlError },
CrateNotFound { crate_name: String, path: PathBuf },
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::CouldNotRead { source, .. } => Some(source),
Error::InvalidToml { source } => Some(source),
_ => None,
}
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::NotFound(path) =>
write!(f, "Could not find `Cargo.toml` in manifest dir: `{}`.", path.display()),
Error::CargoManifestDirNotSet =>
f.write_str("`CARGO_MANIFEST_DIR` env variable not set."),
Error::CouldNotRead { path, .. } => write!(f, "Could not read `{}`.", path.display()),
Error::InvalidToml { .. } => f.write_str("Invalid toml file."),
Error::CrateNotFound { crate_name, path } => write!(
f,
"Could not find `{}` in `dependencies` or `dev-dependencies` in `{}`!",
crate_name,
path.display(),
),
Error::FailedGettingWorkspaceManifestPath =>
f.write_str("Failed to get the path of the workspace manifest path."),
}
}
}
#[derive(Debug, PartialEq, Clone, Eq)]
pub enum FoundCrate {
Itself,
Name(String),
}
type Cache = BTreeMap<String, CacheEntry>;
struct CacheEntry {
manifest_ts: SystemTime,
workspace_manifest_ts: SystemTime,
workspace_manifest_path: PathBuf,
crate_names: CrateNames,
}
type CrateNames = BTreeMap<String, FoundCrate>;
pub fn crate_name(orig_name: &str) -> Result<FoundCrate, Error> {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| Error::CargoManifestDirNotSet)?;
let manifest_path = Path::new(&manifest_dir).join("Cargo.toml");
let manifest_ts = cargo_toml_timestamp(&manifest_path)?;
static CACHE: Mutex<Cache> = Mutex::new(BTreeMap::new());
let mut cache = CACHE.lock().unwrap();
let crate_names = match cache.entry(manifest_dir) {
btree_map::Entry::Occupied(entry) => {
let cache_entry = entry.into_mut();
let workspace_manifest_path = cache_entry.workspace_manifest_path.as_path();
let workspace_manifest_ts = cargo_toml_timestamp(&workspace_manifest_path)?;
if manifest_ts != cache_entry.manifest_ts ||
workspace_manifest_ts != cache_entry.workspace_manifest_ts
{
*cache_entry = read_cargo_toml(
&manifest_path,
&workspace_manifest_path,
manifest_ts,
workspace_manifest_ts,
)?;
}
&cache_entry.crate_names
},
btree_map::Entry::Vacant(entry) => {
let workspace_manifest_path =
workspace_manifest_path(&manifest_path)?.unwrap_or_else(|| manifest_path.clone());
let workspace_manifest_ts = cargo_toml_timestamp(&workspace_manifest_path)?;
let cache_entry = entry.insert(read_cargo_toml(
&manifest_path,
&workspace_manifest_path,
manifest_ts,
workspace_manifest_ts,
)?);
&cache_entry.crate_names
},
};
Ok(crate_names
.get(orig_name)
.ok_or_else(|| Error::CrateNotFound {
crate_name: orig_name.to_owned(),
path: manifest_path,
})?
.clone())
}
fn workspace_manifest_path(cargo_toml_manifest: &Path) -> Result<Option<PathBuf>, Error> {
let Ok(cargo) = env::var("CARGO") else {
return Ok(None);
};
let stdout = Command::new(cargo)
.arg("locate-project")
.args(&["--workspace", "--message-format=plain"])
.arg(format!("--manifest-path={}", cargo_toml_manifest.display()))
.output()
.map_err(|_| Error::FailedGettingWorkspaceManifestPath)?
.stdout;
String::from_utf8(stdout)
.map_err(|_| Error::FailedGettingWorkspaceManifestPath)
.map(|s| {
let path = s.trim();
if path.is_empty() {
None
} else {
Some(path.into())
}
})
}
fn cargo_toml_timestamp(manifest_path: &Path) -> Result<SystemTime, Error> {
fs::metadata(manifest_path).and_then(|meta| meta.modified()).map_err(|source| {
if source.kind() == io::ErrorKind::NotFound {
Error::NotFound(manifest_path.to_owned())
} else {
Error::CouldNotRead { path: manifest_path.to_owned(), source }
}
})
}
fn read_cargo_toml(
manifest_path: &Path,
workspace_manifest_path: &Path,
manifest_ts: SystemTime,
workspace_manifest_ts: SystemTime,
) -> Result<CacheEntry, Error> {
let manifest = open_cargo_toml(manifest_path)?;
let workspace_dependencies = if manifest_path != workspace_manifest_path {
let workspace_manifest = open_cargo_toml(workspace_manifest_path)?;
extract_workspace_dependencies(&workspace_manifest)?
} else {
extract_workspace_dependencies(&manifest)?
};
let crate_names = extract_crate_names(&manifest, workspace_dependencies)?;
Ok(CacheEntry {
manifest_ts,
workspace_manifest_ts,
crate_names,
workspace_manifest_path: workspace_manifest_path.to_path_buf(),
})
}
fn extract_workspace_dependencies(
workspace_toml: &DocumentMut,
) -> Result<BTreeMap<String, String>, Error> {
Ok(workspace_dep_tables(&workspace_toml)
.into_iter()
.map(|t| t.iter())
.flatten()
.map(move |(dep_name, dep_value)| {
let pkg_name = dep_value.get("package").and_then(|i| i.as_str()).unwrap_or(dep_name);
(dep_name.to_owned(), pkg_name.to_owned())
})
.collect())
}
fn workspace_dep_tables(cargo_toml: &DocumentMut) -> Option<&dyn TableLike> {
cargo_toml
.get("workspace")
.and_then(|w| w.as_table_like()?.get("dependencies")?.as_table_like())
}
fn sanitize_crate_name<S: AsRef<str>>(name: S) -> String {
name.as_ref().replace('-', "_")
}
fn open_cargo_toml(path: &Path) -> Result<DocumentMut, Error> {
let content = fs::read_to_string(path)
.map_err(|e| Error::CouldNotRead { source: e, path: path.into() })?;
content.parse::<DocumentMut>().map_err(|e| Error::InvalidToml { source: e })
}
fn extract_crate_names(
cargo_toml: &DocumentMut,
workspace_dependencies: BTreeMap<String, String>,
) -> Result<CrateNames, Error> {
let package_name = extract_package_name(cargo_toml);
let root_pkg = package_name.as_ref().map(|name| {
let cr = match env::var_os("CARGO_TARGET_TMPDIR") {
None => FoundCrate::Itself,
Some(_) => FoundCrate::Name(sanitize_crate_name(name)),
};
(name.to_string(), cr)
});
let dep_tables = dep_tables(cargo_toml.as_table()).chain(target_dep_tables(cargo_toml));
let dep_pkgs =
dep_tables.map(|t| t.iter()).flatten().filter_map(move |(dep_name, dep_value)| {
let pkg_name = dep_value.get("package").and_then(|i| i.as_str()).unwrap_or(dep_name);
if package_name.as_ref().map_or(false, |n| *n == pkg_name) {
return None
}
let workspace =
dep_value.get("workspace").and_then(|w| w.as_bool()).unwrap_or_default();
let pkg_name = workspace
.then(|| workspace_dependencies.get(pkg_name).map(|p| p.as_ref()))
.flatten()
.unwrap_or(pkg_name);
let cr = FoundCrate::Name(sanitize_crate_name(dep_name));
Some((pkg_name.to_owned(), cr))
});
Ok(root_pkg.into_iter().chain(dep_pkgs).collect())
}
fn extract_package_name(cargo_toml: &DocumentMut) -> Option<&str> {
cargo_toml.get("package")?.get("name")?.as_str()
}
fn target_dep_tables(cargo_toml: &DocumentMut) -> impl Iterator<Item = &dyn TableLike> {
cargo_toml
.get("target")
.into_iter()
.filter_map(Item::as_table_like)
.flat_map(|t| {
t.iter()
.map(|(_, value)| value)
.filter_map(Item::as_table_like)
.flat_map(dep_tables)
})
}
fn dep_tables(table: &dyn TableLike) -> impl Iterator<Item = &dyn TableLike> {
table
.get("dependencies")
.into_iter()
.chain(table.get("dev-dependencies"))
.filter_map(Item::as_table_like)
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! create_test {
(
$name:ident,
$cargo_toml:expr,
$workspace_toml:expr,
$( $result:tt )*
) => {
#[test]
fn $name() {
let cargo_toml = $cargo_toml.parse::<DocumentMut>()
.expect("Parses `Cargo.toml`");
let workspace_cargo_toml = $workspace_toml.parse::<DocumentMut>()
.expect("Parses workspace `Cargo.toml`");
let workspace_deps = extract_workspace_dependencies(&workspace_cargo_toml)
.expect("Extracts workspace dependencies");
match extract_crate_names(&cargo_toml, workspace_deps)
.map(|mut map| map.remove("my_crate"))
{
$( $result )* => (),
o => panic!("Invalid result: {:?}", o),
}
}
};
}
create_test! {
deps_with_crate,
r#"
[dependencies]
my_crate = "0.1"
"#,
"",
Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
}
create_test! {
deps_with_crate_inline_table,
r#"
dependencies = { my_crate = "0.1" }
"#,
"",
Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
}
create_test! {
dev_deps_with_crate,
r#"
[dev-dependencies]
my_crate = "0.1"
"#,
"",
Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
}
create_test! {
deps_with_crate_renamed,
r#"
[dependencies]
cool = { package = "my_crate", version = "0.1" }
"#,
"",
Ok(Some(FoundCrate::Name(name))) if name == "cool"
}
create_test! {
deps_with_crate_renamed_second,
r#"
[dependencies.cool]
package = "my_crate"
version = "0.1"
"#,
"",
Ok(Some(FoundCrate::Name(name))) if name == "cool"
}
create_test! {
deps_empty,
r#"
[dependencies]
"#,
"",
Ok(None)
}
create_test! {
crate_not_found,
r#"
[dependencies]
serde = "1.0"
"#,
"",
Ok(None)
}
create_test! {
target_dependency,
r#"
[target.'cfg(target_os="android")'.dependencies]
my_crate = "0.1"
"#,
"",
Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
}
create_test! {
target_dependency2,
r#"
[target.x86_64-pc-windows-gnu.dependencies]
my_crate = "0.1"
"#,
"",
Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
}
create_test! {
own_crate,
r#"
[package]
name = "my_crate"
"#,
"",
Ok(Some(FoundCrate::Itself))
}
create_test! {
own_crate_and_in_deps,
r#"
[package]
name = "my_crate"
[dev-dependencies]
my_crate = "0.1"
"#,
"",
Ok(Some(FoundCrate::Itself))
}
create_test! {
multiple_times,
r#"
[dependencies]
my_crate = { version = "0.5" }
my-crate-old = { package = "my_crate", version = "0.1" }
"#,
"",
Ok(Some(FoundCrate::Name(name))) if name == "my_crate_old"
}
create_test! {
workspace_deps,
r#"
[dependencies]
my_crate_cool = { workspace = true }
"#,
r#"
[workspace.dependencies]
my_crate_cool = { package = "my_crate" }
"#,
Ok(Some(FoundCrate::Name(name))) if name == "my_crate_cool"
}
}