blob: a2e6a662b0daa9094802cb2a8c2f185f4b0505b4 [file] [log] [blame]
//! Tools for gathering various kinds of metadata (Cargo.lock, Cargo metadata, Crate Index info).
mod cargo_bin;
mod cargo_tree_resolver;
mod dependency;
mod metadata_annotation;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::{bail, Context, Result};
use cargo_lock::Lockfile as CargoLockfile;
use cargo_metadata::Metadata as CargoMetadata;
use tracing::debug;
pub(crate) use self::cargo_bin::*;
pub(crate) use self::cargo_tree_resolver::*;
pub(crate) use self::dependency::*;
pub(crate) use self::metadata_annotation::*;
// TODO: This should also return a set of [crate-index::IndexConfig]s for packages in metadata.packages
/// A Trait for generating metadata (`cargo metadata` output and a lock file) from a Cargo manifest.
pub(crate) trait MetadataGenerator {
fn generate<T: AsRef<Path>>(&self, manifest_path: T) -> Result<(CargoMetadata, CargoLockfile)>;
}
/// Generates Cargo metadata and a lockfile from a provided manifest.
pub(crate) struct Generator {
/// The path to a `cargo` binary
cargo_bin: Cargo,
/// The path to a `rustc` binary
rustc_bin: PathBuf,
}
impl Generator {
pub(crate) fn new() -> Self {
let rustc_bin = PathBuf::from(env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string()));
Generator {
cargo_bin: Cargo::new(
PathBuf::from(env::var("CARGO").unwrap_or_else(|_| "cargo".to_string())),
rustc_bin.clone(),
),
rustc_bin,
}
}
pub(crate) fn with_cargo(mut self, cargo_bin: Cargo) -> Self {
self.cargo_bin = cargo_bin;
self
}
pub(crate) fn with_rustc(mut self, rustc_bin: PathBuf) -> Self {
self.rustc_bin = rustc_bin;
self
}
}
impl MetadataGenerator for Generator {
fn generate<T: AsRef<Path>>(&self, manifest_path: T) -> Result<(CargoMetadata, CargoLockfile)> {
let manifest_dir = manifest_path
.as_ref()
.parent()
.expect("The manifest should have a parent directory");
let lockfile = {
let lock_path = manifest_dir.join("Cargo.lock");
if !lock_path.exists() {
bail!("No `Cargo.lock` file was found with the given manifest")
}
cargo_lock::Lockfile::load(lock_path)?
};
let metadata = self
.cargo_bin
.metadata_command_with_options(manifest_path.as_ref(), vec!["--locked".to_owned()])?
.exec()?;
Ok((metadata, lockfile))
}
}
/// A configuration describing how to invoke [cargo update](https://doc.rust-lang.org/cargo/commands/cargo-update.html).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CargoUpdateRequest {
/// Translates to an unrestricted `cargo update` command
Eager,
/// Translates to `cargo update --workspace`
Workspace,
/// Translates to `cargo update --package foo` with an optional `--precise` argument.
Package {
/// The name of the crate used with `--package`.
name: String,
/// If set, the `--precise` value that pairs with `--package`.
version: Option<String>,
},
}
impl FromStr for CargoUpdateRequest {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let lower = s.to_lowercase();
if ["eager", "full", "all"].contains(&lower.as_str()) {
return Ok(Self::Eager);
}
if ["1", "yes", "true", "on", "workspace", "minimal"].contains(&lower.as_str()) {
return Ok(Self::Workspace);
}
let mut split = s.splitn(2, '=');
Ok(Self::Package {
name: split.next().map(|s| s.to_owned()).unwrap(),
version: split.next().map(|s| s.to_owned()),
})
}
}
impl CargoUpdateRequest {
/// Determine what arguments to pass to the `cargo update` command.
fn get_update_args(&self) -> Vec<String> {
match self {
CargoUpdateRequest::Eager => Vec::new(),
CargoUpdateRequest::Workspace => vec!["--workspace".to_owned()],
CargoUpdateRequest::Package { name, version } => {
let mut update_args = vec!["--package".to_owned(), name.clone()];
if let Some(version) = version {
update_args.push("--precise".to_owned());
update_args.push(version.clone());
}
update_args
}
}
}
/// Calls `cargo update` with arguments specific to the state of the current variant.
pub(crate) fn update(&self, manifest: &Path, cargo_bin: &Cargo) -> Result<()> {
let manifest_dir = manifest.parent().unwrap();
// Simply invoke `cargo update`
let output = cargo_bin
.command()?
// Cargo detects config files based on `pwd` when running so
// to ensure user provided Cargo config files are used, it's
// critical to set the working directory to the manifest dir.
.current_dir(manifest_dir)
.arg("update")
.arg("--manifest-path")
.arg(manifest)
.args(self.get_update_args())
.output()
.with_context(|| {
format!(
"Error running cargo to update packages for manifest '{}'",
manifest.display()
)
})?;
if !output.status.success() {
eprintln!("{}", String::from_utf8_lossy(&output.stdout));
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
bail!(format!("Failed to update lockfile: {}", output.status))
}
Ok(())
}
}
pub(crate) struct LockGenerator {
/// Interface to cargo.
cargo_bin: Cargo,
}
impl LockGenerator {
pub(crate) fn new(cargo_bin: Cargo) -> Self {
Self { cargo_bin }
}
#[tracing::instrument(name = "LockGenerator::generate", skip_all)]
pub(crate) fn generate(
&self,
manifest_path: &Path,
existing_lock: &Option<PathBuf>,
update_request: &Option<CargoUpdateRequest>,
) -> Result<cargo_lock::Lockfile> {
debug!("Generating Cargo Lockfile for {}", manifest_path.display());
let manifest_dir = manifest_path.parent().unwrap();
let generated_lockfile_path = manifest_dir.join("Cargo.lock");
if let Some(lock) = existing_lock {
debug!("Using existing lock {}", lock.display());
if !lock.exists() {
bail!(
"An existing lockfile path was provided but a file at '{}' does not exist",
lock.display()
)
}
// Install the file into the target location
if generated_lockfile_path.exists() {
fs::remove_file(&generated_lockfile_path)?;
}
fs::copy(lock, &generated_lockfile_path)?;
if let Some(request) = update_request {
request.update(manifest_path, &self.cargo_bin)?;
}
// Ensure the Cargo cache is up to date to simulate the behavior
// of having just generated a new one
let output = self
.cargo_bin
.command()?
// Cargo detects config files based on `pwd` when running so
// to ensure user provided Cargo config files are used, it's
// critical to set the working directory to the manifest dir.
.current_dir(manifest_dir)
.arg("fetch")
.arg("--manifest-path")
.arg(manifest_path)
.output()
.context(format!(
"Error running cargo to fetch crates '{}'",
manifest_path.display()
))?;
if !output.status.success() {
eprintln!("{}", String::from_utf8_lossy(&output.stdout));
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
bail!(format!(
"Failed to fetch crates for lockfile: {}",
output.status
))
}
} else {
debug!("Generating new lockfile");
// Simply invoke `cargo generate-lockfile`
let output = self
.cargo_bin
.command()?
// Cargo detects config files based on `pwd` when running so
// to ensure user provided Cargo config files are used, it's
// critical to set the working directory to the manifest dir.
.current_dir(manifest_dir)
.arg("generate-lockfile")
.arg("--manifest-path")
.arg(manifest_path)
.output()
.context(format!(
"Error running cargo to generate lockfile '{}'",
manifest_path.display()
))?;
if !output.status.success() {
eprintln!("{}", String::from_utf8_lossy(&output.stdout));
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
bail!(format!("Failed to generate lockfile: {}", output.status))
}
}
cargo_lock::Lockfile::load(&generated_lockfile_path).context(format!(
"Failed to load lockfile: {}",
generated_lockfile_path.display()
))
}
}
/// A generator which runs `cargo vendor` on a given manifest
pub(crate) struct VendorGenerator {
/// The path to a `cargo` binary
cargo_bin: Cargo,
/// The path to a `rustc` binary
rustc_bin: PathBuf,
}
impl VendorGenerator {
pub(crate) fn new(cargo_bin: Cargo, rustc_bin: PathBuf) -> Self {
Self {
cargo_bin,
rustc_bin,
}
}
#[tracing::instrument(name = "VendorGenerator::generate", skip_all)]
pub(crate) fn generate(&self, manifest_path: &Path, output_dir: &Path) -> Result<()> {
debug!(
"Vendoring {} to {}",
manifest_path.display(),
output_dir.display()
);
let manifest_dir = manifest_path.parent().unwrap();
// Simply invoke `cargo generate-lockfile`
let output = self
.cargo_bin
.command()?
// Cargo detects config files based on `pwd` when running so
// to ensure user provided Cargo config files are used, it's
// critical to set the working directory to the manifest dir.
.current_dir(manifest_dir)
.arg("vendor")
.arg("--manifest-path")
.arg(manifest_path)
.arg("--locked")
.arg("--versioned-dirs")
.arg(output_dir)
.env("RUSTC", &self.rustc_bin)
.output()
.with_context(|| {
format!(
"Error running cargo to vendor sources for manifest '{}'",
manifest_path.display()
)
})?;
if !output.status.success() {
eprintln!("{}", String::from_utf8_lossy(&output.stdout));
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
bail!(format!("Failed to vendor sources with: {}", output.status))
}
debug!("Done");
Ok(())
}
}
/// A helper function for writing Cargo metadata to a file.
pub(crate) fn write_metadata(path: &Path, metadata: &cargo_metadata::Metadata) -> Result<()> {
let content =
serde_json::to_string_pretty(metadata).context("Failed to serialize Cargo Metadata")?;
fs::write(path, content).context("Failed to write metadata to disk")
}
/// A helper function for deserializing Cargo metadata and lockfiles
pub(crate) fn load_metadata(
metadata_path: &Path,
) -> Result<(cargo_metadata::Metadata, cargo_lock::Lockfile)> {
// Locate the Cargo.lock file related to the metadata file.
let lockfile_path = metadata_path
.parent()
.expect("metadata files should always have parents")
.join("Cargo.lock");
if !lockfile_path.exists() {
bail!(
"The metadata file at {} is not next to a `Cargo.lock` file.",
metadata_path.display()
)
}
let content = fs::read_to_string(metadata_path)
.with_context(|| format!("Failed to load Cargo Metadata: {}", metadata_path.display()))?;
let metadata =
serde_json::from_str(&content).context("Unable to deserialize Cargo metadata")?;
let lockfile = cargo_lock::Lockfile::load(&lockfile_path)
.with_context(|| format!("Failed to load lockfile: {}", lockfile_path.display()))?;
Ok((metadata, lockfile))
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn deserialize_cargo_update_request_for_eager() {
for value in ["all", "full", "eager"] {
let request = CargoUpdateRequest::from_str(value).unwrap();
assert_eq!(request, CargoUpdateRequest::Eager);
}
}
#[test]
fn deserialize_cargo_update_request_for_workspace() {
for value in ["1", "true", "yes", "on", "workspace", "minimal"] {
let request = CargoUpdateRequest::from_str(value).unwrap();
assert_eq!(request, CargoUpdateRequest::Workspace);
}
}
#[test]
fn deserialize_cargo_update_request_for_package() {
let request = CargoUpdateRequest::from_str("cargo-bazel").unwrap();
assert_eq!(
request,
CargoUpdateRequest::Package {
name: "cargo-bazel".to_owned(),
version: None
}
);
}
#[test]
fn deserialize_cargo_update_request_for_precise() {
let request = CargoUpdateRequest::from_str("cargo-bazel@1.2.3").unwrap();
assert_eq!(
request,
CargoUpdateRequest::Package {
name: "cargo-bazel@1.2.3".to_owned(),
version: None
}
);
}
#[test]
fn deserialize_cargo_update_request_for_precise_pin() {
let request = CargoUpdateRequest::from_str("cargo-bazel@1.2.3=4.5.6").unwrap();
assert_eq!(
request,
CargoUpdateRequest::Package {
name: "cargo-bazel@1.2.3".to_owned(),
version: Some("4.5.6".to_owned()),
}
);
}
}