use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{bail, Context as AnyhowContext, Result};
use camino::Utf8PathBuf;
use cargo_lock::Lockfile;
use clap::Parser;
use crate::config::Config;
use crate::context::Context;
use crate::lockfile::{lock_context, write_lockfile};
use crate::metadata::{load_metadata, Annotations, Cargo, SourceAnnotation};
use crate::rendering::{write_outputs, Renderer};
use crate::splicing::SplicingManifest;
use crate::utils::normalize_cargo_file_paths;
use crate::utils::starlark::Label;
#[derive(Parser, Debug)]
#[clap(about = "Command line options for the `generate` subcommand", version)]
pub struct GenerateOptions {
#[clap(long, env = "CARGO")]
pub cargo: Option<PathBuf>,
#[clap(long, env = "RUSTC")]
pub rustc: Option<PathBuf>,
#[clap(long)]
pub config: PathBuf,
#[clap(long)]
pub splicing_manifest: PathBuf,
#[clap(long)]
pub lockfile: Option<PathBuf>,
#[clap(long)]
pub cargo_lockfile: PathBuf,
#[clap(long)]
pub repository_dir: PathBuf,
#[clap(long)]
pub cargo_config: Option<PathBuf>,
#[clap(long)]
pub repin: bool,
#[clap(long)]
pub metadata: Option<PathBuf>,
#[clap(long)]
pub dry_run: bool,
#[clap(long)]
pub nonhermetic_root_bazel_workspace_dir: Utf8PathBuf,
#[clap(long)]
pub paths_to_track: PathBuf,
#[clap(long)]
pub(crate) generator: Option<Label>,
#[clap(long)]
pub warnings_output_path: PathBuf,
#[clap(long)]
pub skip_cargo_lockfile_overwrite: bool,
#[clap(long)]
pub strip_internal_dependencies_from_cargo_lockfile: bool,
}
pub fn generate(opt: GenerateOptions) -> Result<()> {
let config = Config::try_from_path(&opt.config)?;
if !opt.repin {
if let Some(lockfile) = &opt.lockfile {
let context = Context::try_from_path(lockfile)?;
let outputs = Renderer::new(
Arc::new(config.rendering),
Arc::new(config.supported_platform_triples),
)
.render(&context, opt.generator)?;
let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.repository_dir);
write_outputs(normalized_outputs, opt.dry_run)?;
let splicing_manifest = SplicingManifest::try_from_path(&opt.splicing_manifest)?;
write_paths_to_track(
&opt.paths_to_track,
&opt.warnings_output_path,
splicing_manifest.manifests.keys().cloned(),
context
.crates
.values()
.filter_map(|crate_context| crate_context.repository.as_ref()),
context.unused_patches.iter(),
&opt.nonhermetic_root_bazel_workspace_dir,
)?;
return Ok(());
}
}
let rustc_bin = match &opt.rustc {
Some(bin) => bin,
None => bail!("The `--rustc` argument is required when generating unpinned content"),
};
let cargo_bin = Cargo::new(
match opt.cargo {
Some(bin) => bin,
None => bail!("The `--cargo` argument is required when generating unpinned content"),
},
rustc_bin.clone(),
);
let metadata_path = match &opt.metadata {
Some(path) => path,
None => bail!("The `--metadata` argument is required when generating unpinned content"),
};
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 (cargo_metadata, cargo_lockfile) = load_metadata(metadata_path, &lockfile_path)?;
let annotations = Annotations::new(
cargo_metadata,
&Some(lockfile_path),
cargo_lockfile.clone(),
config.clone(),
&opt.nonhermetic_root_bazel_workspace_dir,
)?;
let splicing_manifest = SplicingManifest::try_from_path(&opt.splicing_manifest)?;
write_paths_to_track(
&opt.paths_to_track,
&opt.warnings_output_path,
splicing_manifest.manifests.keys().cloned(),
annotations.lockfile.crates.values(),
cargo_lockfile.patch.unused.iter(),
&opt.nonhermetic_root_bazel_workspace_dir,
)?;
let context = Context::new(annotations, config.rendering.are_sources_present())?;
let outputs = Renderer::new(
Arc::new(config.rendering.clone()),
Arc::new(config.supported_platform_triples.clone()),
)
.render(&context, opt.generator)?;
let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.repository_dir);
write_outputs(normalized_outputs, opt.dry_run)?;
if let Some(lockfile) = opt.lockfile {
let lock_content =
lock_context(context, &config, &splicing_manifest, &cargo_bin, rustc_bin)?;
write_lockfile(lock_content, &lockfile, opt.dry_run)?;
}
if !opt.skip_cargo_lockfile_overwrite {
let cargo_lockfile_to_write = if opt.strip_internal_dependencies_from_cargo_lockfile {
remove_internal_dependencies_from_cargo_lockfile(cargo_lockfile)
} else {
cargo_lockfile
};
update_cargo_lockfile(&opt.cargo_lockfile, cargo_lockfile_to_write)?;
}
Ok(())
}
fn remove_internal_dependencies_from_cargo_lockfile(cargo_lockfile: Lockfile) -> Lockfile {
let filtered_packages: Vec<_> = cargo_lockfile
.packages
.into_iter()
.filter(|pkg| pkg.source.is_some())
.collect();
Lockfile {
packages: filtered_packages,
..cargo_lockfile
}
}
fn update_cargo_lockfile(path: &Path, cargo_lockfile: Lockfile) -> Result<()> {
let old_contents = fs::read_to_string(path).ok();
let new_contents = cargo_lockfile.to_string();
if old_contents.as_ref() == Some(&new_contents) {
return Ok(());
}
fs::write(path, new_contents)
.context("Failed to write Cargo.lock file back to the workspace.")?;
Ok(())
}
fn write_paths_to_track<
'a,
SourceAnnotations: Iterator<Item = &'a SourceAnnotation>,
Paths: Iterator<Item = Utf8PathBuf>,
UnusedPatches: Iterator<Item = &'a cargo_lock::Dependency>,
>(
output_file: &Path,
warnings_output_path: &Path,
manifests: Paths,
source_annotations: SourceAnnotations,
unused_patches: UnusedPatches,
nonhermetic_root_bazel_workspace_dir: &Utf8PathBuf,
) -> Result<()> {
let source_annotation_manifests: BTreeSet<_> = source_annotations
.filter_map(|v| {
if let SourceAnnotation::Path { path } = v {
Some(path.join("Cargo.toml"))
} else {
None
}
})
.collect();
let paths_to_track: BTreeSet<_> = source_annotation_manifests
.iter()
.cloned()
.chain(manifests)
.filter(|p| p.starts_with(nonhermetic_root_bazel_workspace_dir))
.collect();
std::fs::write(
output_file,
serde_json::to_string(&paths_to_track).context("Failed to serialize paths to track")?,
)
.context("Failed to write paths to track")?;
let mut warnings = Vec::new();
for source_annotation_manifest in &source_annotation_manifests {
warnings.push(format!("Build is not hermetic - path dependency pulling in crate at {source_annotation_manifest} is being used."));
}
for unused_patch in unused_patches {
warnings.push(format!("You have a [patch] Cargo.toml entry that is being ignored by cargo. Unused patch: {} {}{}", unused_patch.name, unused_patch.version, if let Some(source) = unused_patch.source.as_ref() { format!(" ({})", source) } else { String::new() }));
}
std::fs::write(
warnings_output_path,
serde_json::to_string(&warnings).context("Failed to serialize warnings to track")?,
)
.context("Failed to write warnings file")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test;
#[test]
fn test_remove_internal_dependencies_from_cargo_lockfile_workspace_build_scripts_deps_should_remove_internal_dependencies(
) {
let original_lockfile = test::lockfile::workspace_build_scripts_deps();
let filtered_lockfile =
remove_internal_dependencies_from_cargo_lockfile(original_lockfile.clone());
assert!(filtered_lockfile.packages.len() < original_lockfile.packages.len());
assert!(original_lockfile
.packages
.iter()
.any(|pkg| pkg.name.as_str() == "child"));
assert!(!filtered_lockfile
.packages
.iter()
.any(|pkg| pkg.name.as_str() == "child"));
assert!(filtered_lockfile
.packages
.iter()
.any(|pkg| pkg.name.as_str() == "anyhow"));
}
}