use crate::{
error::warn,
toml::{Context, DetailedTomlDependency},
};
use anyhow::{anyhow, bail, ensure, Result};
use cargo::{
core::{source::MaybePackage, Dependency, Features, Package, PackageId, Source, SourceId},
util::Config,
};
use cargo_metadata::{Error, Metadata, MetadataCommand};
use dylint_internal::rustup::SanitizeEnvironment;
use glob::glob;
use if_chain::if_chain;
use serde::Deserialize;
use serde_json::{Map, Value};
use std::{
path::{Path, PathBuf},
process::Stdio,
};
#[derive(Debug, Deserialize)]
struct Library {
pattern: Option<String>,
#[serde(flatten)]
details: DetailedTomlDependency,
}
pub fn workspace_metadata_paths(opts: &crate::Dylint) -> Result<Vec<(PathBuf, bool)>> {
if opts.no_metadata {
return Ok(vec![]);
}
let mut command = MetadataCommand::new();
if let Some(path) = &opts.manifest_path {
command.manifest_path(path);
}
match command.exec() {
Ok(metadata) => {
if let Value::Object(object) = &metadata.workspace_metadata {
let paths = dylint_metadata_paths(opts, &metadata, object)?;
Ok(paths
.into_iter()
.map(|path| (path, !opts.no_build))
.collect())
} else {
Ok(vec![])
}
}
Err(err) => {
if opts.manifest_path.is_none() {
if_chain! {
if let Error::CargoMetadata { stderr } = err;
if let Some(line) = stderr.lines().next();
if !line.starts_with("error: could not find `Cargo.toml`");
then {
warn(opts, line.strip_prefix("error: ").unwrap_or(line));
}
}
Ok(vec![])
} else {
Err(err.into())
}
}
}
}
fn dylint_metadata_paths(
opts: &crate::Dylint,
metadata: &Metadata,
object: &Map<String, Value>,
) -> Result<Vec<PathBuf>> {
if let Some(value) = object.get("dylint") {
if let Value::Object(object) = value {
let libraries = object
.iter()
.map(|entry| {
if entry.0 == "libraries" {
let libraries = serde_json::from_value::<Vec<Library>>(entry.1.clone())?;
maybe_build_libraries(opts, metadata, &libraries)
} else {
bail!("Unknown key `{}`", entry.0)
}
})
.collect::<Result<Vec<_>>>()?;
Ok(libraries.into_iter().flatten().collect())
} else {
bail!("`dylint` value must be a map")
}
} else {
Ok(vec![])
}
}
fn maybe_build_libraries(
opts: &crate::Dylint,
metadata: &Metadata,
libraries: &[Library],
) -> Result<Vec<PathBuf>> {
let config = Config::default()?;
let paths = libraries
.iter()
.map(|library| maybe_build_packages(opts, metadata, &config, library))
.collect::<Result<Vec<_>>>()?;
Ok(paths.into_iter().flatten().collect())
}
#[allow(clippy::option_if_let_else)]
fn maybe_build_packages(
opts: &crate::Dylint,
metadata: &Metadata,
config: &Config,
library: &Library,
) -> Result<Vec<PathBuf>> {
let dep = dependency(opts, metadata, config, library)?;
let dependency_root = dependency_root(config, &dep)?;
let pattern = if let Some(pattern) = &library.pattern {
dependency_root.join(Path::new(pattern))
} else {
dependency_root
};
let entries = glob(&pattern.to_string_lossy())?;
let paths = entries.collect::<std::result::Result<Vec<_>, _>>()?;
let package_root_ids = paths
.into_iter()
.filter_map(|path| {
if path.is_dir() {
Some(package_id(dep.source_id(), &path).map(|package_id| (path, package_id)))
} else {
None
}
})
.collect::<Result<Vec<_>>>()?;
package_root_ids
.into_iter()
.map(|(package_root, package_id)| {
package_library_path(opts, metadata, &package_root, package_id)
})
.collect()
}
fn dependency(
opts: &crate::Dylint,
metadata: &Metadata,
config: &Config,
library: &Library,
) -> Result<Dependency> {
let name_in_toml = "library";
let mut deps = vec![];
let root = PathBuf::from(&metadata.workspace_root);
let source_id = SourceId::for_path(&root)?;
let mut nested_paths = vec![];
let mut warnings = vec![];
let features = Features::new(&[], config, &mut warnings, source_id.is_path())?;
let mut cx = Context::new(
&mut deps,
source_id,
&mut nested_paths,
config,
&mut warnings,
None,
&root,
&features,
);
let kind = None;
let dependency = library.details.to_dependency(name_in_toml, &mut cx, kind)?;
if !warnings.is_empty() {
warn(opts, &warnings.join("\n"));
}
Ok(dependency)
}
fn dependency_root(config: &Config, dep: &Dependency) -> Result<PathBuf> {
let source_id = dep.source_id();
if source_id.is_path() {
if let Some(path) = source_id.local_path() {
Ok(path)
} else {
bail!("Path source should have a local path: {}", source_id)
}
} else if source_id.is_git() {
git_dependency_root(config, dep)
} else {
bail!("Only git and path entries are supported: {}", source_id)
}
}
#[allow(clippy::default_trait_access)]
fn git_dependency_root(config: &Config, dep: &Dependency) -> Result<PathBuf> {
let _lock = config.acquire_package_cache_lock();
let mut source = dep.source_id().load(config, &Default::default())?;
source.update()?;
let package_id = sample_package_id(dep, &mut *source)?;
if let MaybePackage::Ready(package) = source.download(package_id)? {
git_dependency_root_from_package(config, &*source, &package)
} else {
bail!(format!("`{}` is not ready", package_id.name()))
}
}
fn sample_package_id(dep: &Dependency, source: &mut dyn Source) -> Result<PackageId> {
let mut package_id: Option<PackageId> = None;
source.fuzzy_query(dep, &mut |summary| {
if package_id.is_none() {
package_id = Some(summary.package_id());
}
})?;
package_id.ok_or_else(|| anyhow!("Found no packages in `{}`", dep.source_id()))
}
fn git_dependency_root_from_package<'a>(
config: &'a Config,
source: &(dyn Source + 'a),
package: &Package,
) -> Result<PathBuf> {
let package_root = package.root();
if source.source_id().is_git() {
let git_path = config.git_path();
let git_path = config.assert_package_cache_locked(&git_path);
ensure!(
package_root.starts_with(git_path.join("checkouts")),
"Unexpected path: {}",
package_root.to_string_lossy()
);
let n = git_path.components().count() + 3;
Ok(package_root.components().take(n).collect())
} else if source.source_id().is_path() {
unreachable!()
} else {
unimplemented!()
}
}
#[allow(clippy::unwrap_used)]
fn package_id(source_id: SourceId, package_root: &Path) -> Result<PackageId> {
let metadata = MetadataCommand::new()
.current_dir(package_root)
.no_deps()
.exec()?;
ensure!(
metadata.packages.len() <= 1,
"Library is not its own workspace: {}",
package_root.to_string_lossy()
);
let package = metadata
.packages
.first()
.ok_or_else(|| anyhow!("Found no packages in `{}`", package_root.to_string_lossy()))?;
assert!(metadata.workspace_root == package.manifest_path.parent().unwrap());
PackageId::new(&package.name, &package.version.to_string(), source_id)
}
fn package_library_path(
opts: &crate::Dylint,
metadata: &Metadata,
package_root: &Path,
package_id: PackageId,
) -> Result<PathBuf> {
let target_dir = target_dir(metadata, package_root, package_id)?;
if !opts.no_build {
let mut command = dylint_internal::build();
command
.sanitize_environment()
.current_dir(package_root)
.args(&["--release", "--target-dir", &target_dir.to_string_lossy()]);
if opts.quiet {
command.stderr(Stdio::null());
}
command.success()?;
}
Ok(target_dir.join("release"))
}
fn target_dir(metadata: &Metadata, package_root: &Path, _package_id: PackageId) -> Result<PathBuf> {
let toolchain = dylint_internal::rustup::active_toolchain(package_root)?;
Ok(metadata
.target_directory
.join("dylint")
.join(toolchain)
.into())
}
#[cfg(any())]
mod disabled {
fn pkg_dir(package_root: &Path, pkg_id: PackageId) -> String {
let name = pkg_id.name();
format!("{}-{}", name, target_short_hash(package_root, pkg_id))
}
const METADATA_VERSION: u8 = 2;
fn target_short_hash(package_root: &Path, pkg_id: PackageId) -> String {
let hashable = pkg_id.stable_hash(package_root);
cargo::util::short_hash(&(METADATA_VERSION, hashable))
}
}