| use crate::ops; |
| use crate::sources::CRATES_IO_REGISTRY; |
| use crate::sources::RegistrySource; |
| use crate::sources::SourceConfigMap; |
| use crate::sources::path::PathSource; |
| use crate::util::cache_lock::CacheLockMode; |
| use crate::util::{CargoResult, GlobalContext, try_canonicalize}; |
| use crate::workspace::SourceId; |
| use crate::workspace::{GitReference, Package, Workspace}; |
| |
| use anyhow::{Context as _, bail}; |
| use cargo_util::{Sha256, paths}; |
| use cargo_util_schemas::core::SourceKind; |
| use cargo_util_schemas::manifest::TomlPackageBuild; |
| use cargo_util_terminal::Verbosity; |
| use serde::Serialize; |
| use walkdir::WalkDir; |
| |
| use crate::util::data_structures::{HashMap, HashSet}; |
| use std::collections::{BTreeMap, BTreeSet}; |
| use std::ffi::OsStr; |
| use std::fs::{self, File, OpenOptions}; |
| use std::io::{self, Read, Write}; |
| use std::path::{Path, PathBuf}; |
| |
| pub struct VendorOptions<'a> { |
| pub no_delete: bool, |
| pub versioned_dirs: bool, |
| pub destination: &'a Path, |
| pub extra: Vec<PathBuf>, |
| pub respect_source_config: bool, |
| } |
| |
| pub fn vendor(ws: &Workspace<'_>, opts: &VendorOptions<'_>) -> CargoResult<()> { |
| let gctx = ws.gctx(); |
| let mut extra_workspaces = Vec::new(); |
| for extra in opts.extra.iter() { |
| let extra = gctx.cwd().join(extra); |
| let ws = Workspace::new(&extra, gctx)?; |
| extra_workspaces.push(ws); |
| } |
| let workspaces = extra_workspaces.iter().chain(Some(ws)).collect::<Vec<_>>(); |
| let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?; |
| let vendor_config = sync(gctx, &workspaces, opts).context("failed to sync")?; |
| |
| if gctx.shell().verbosity() != Verbosity::Quiet { |
| if vendor_config.source.is_empty() { |
| crate::drop_eprintln!(gctx, "There is no dependency to vendor in this project."); |
| } else { |
| crate::drop_eprint!( |
| gctx, |
| "To use vendored sources, add this to your .cargo/config.toml for this project:\n\n" |
| ); |
| crate::drop_print!(gctx, "{}", &toml::to_string_pretty(&vendor_config).unwrap()); |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| #[derive(Serialize)] |
| struct VendorConfig { |
| source: BTreeMap<String, VendorSource>, |
| } |
| |
| #[derive(Serialize)] |
| #[serde(rename_all = "lowercase", untagged)] |
| enum VendorSource { |
| Directory { |
| directory: String, |
| }, |
| Registry { |
| registry: Option<String>, |
| #[serde(rename = "replace-with")] |
| replace_with: String, |
| }, |
| Git { |
| git: String, |
| branch: Option<String>, |
| tag: Option<String>, |
| rev: Option<String>, |
| #[serde(rename = "replace-with")] |
| replace_with: String, |
| }, |
| } |
| |
| /// Cache for mapping replaced sources to replacements. |
| struct SourceReplacementCache<'gctx> { |
| map: SourceConfigMap<'gctx>, |
| cache: HashMap<SourceId, SourceId>, |
| } |
| |
| impl SourceReplacementCache<'_> { |
| fn new( |
| gctx: &GlobalContext, |
| respect_source_config: bool, |
| ) -> CargoResult<SourceReplacementCache<'_>> { |
| Ok(SourceReplacementCache { |
| map: if respect_source_config { |
| SourceConfigMap::new(gctx) |
| } else { |
| SourceConfigMap::empty(gctx) |
| }?, |
| cache: Default::default(), |
| }) |
| } |
| |
| fn get(&mut self, id: SourceId) -> CargoResult<SourceId> { |
| use std::collections::hash_map::Entry; |
| match self.cache.entry(id) { |
| Entry::Occupied(e) => Ok(e.get().clone()), |
| Entry::Vacant(e) => { |
| let replaced = self.map.load(id)?.replaced_source_id(); |
| Ok(e.insert(replaced).clone()) |
| } |
| } |
| } |
| } |
| |
| fn sync( |
| gctx: &GlobalContext, |
| workspaces: &[&Workspace<'_>], |
| opts: &VendorOptions<'_>, |
| ) -> CargoResult<VendorConfig> { |
| let dry_run = false; |
| let vendor_dir = try_canonicalize(opts.destination); |
| let vendor_dir = vendor_dir.as_deref().unwrap_or(opts.destination); |
| let vendor_dir_already_exists = vendor_dir.exists(); |
| |
| paths::create_dir_all(&vendor_dir)?; |
| let mut to_remove = HashSet::default(); |
| if !opts.no_delete { |
| for entry in vendor_dir.read_dir()? { |
| let entry = entry?; |
| if !entry |
| .file_name() |
| .to_str() |
| .map_or(false, |s| s.starts_with('.')) |
| { |
| to_remove.insert(entry.path()); |
| } |
| } |
| } |
| |
| let mut source_replacement_cache = |
| SourceReplacementCache::new(gctx, opts.respect_source_config)?; |
| |
| let mut checksums = HashMap::default(); |
| let mut ids = BTreeMap::new(); |
| |
| // Let's download all crates and start storing internal tables about them. |
| for ws in workspaces { |
| let (packages, resolve) = ops::resolve_ws(ws, dry_run) |
| .with_context(|| format!("failed to load lockfile for {}", ws.root().display()))?; |
| |
| packages |
| .get_many(resolve.iter()) |
| .with_context(|| format!("failed to download packages for {}", ws.root().display()))?; |
| |
| for pkg in resolve.iter() { |
| let sid = source_replacement_cache.get(pkg.source_id())?; |
| |
| // Don't vendor path crates since they're already in the repository |
| if sid.is_path() { |
| // And don't delete actual source code! |
| if let Ok(path) = sid.url().to_file_path() { |
| if let Ok(path) = try_canonicalize(path) { |
| to_remove.remove(&path); |
| } |
| } |
| continue; |
| } |
| |
| ids.insert( |
| pkg, |
| packages |
| .get_one(pkg) |
| .context("failed to fetch package")? |
| .clone(), |
| ); |
| |
| checksums.insert(pkg, resolve.checksums().get(&pkg).cloned()); |
| } |
| } |
| |
| let mut versions = HashMap::default(); |
| for id in ids.keys() { |
| let map = versions.entry(id.name()).or_insert_with(BTreeMap::default); |
| if let Some(prev) = map.get(&id.version()) { |
| bail!( |
| "found duplicate version of package `{} v{}` \ |
| vendored from two sources:\n\ |
| \n\ |
| \tsource 1: {}\n\ |
| \tsource 2: {}", |
| id.name(), |
| id.version(), |
| prev, |
| id.source_id() |
| ); |
| } |
| map.insert(id.version(), id.source_id()); |
| } |
| |
| let mut sources = BTreeSet::new(); |
| let mut tmp_buf = [0; 64 * 1024]; |
| for (id, pkg) in ids.iter() { |
| // Next up, copy it to the vendor directory |
| let src = pkg.root(); |
| let max_version = *versions[&id.name()].iter().rev().next().unwrap().0; |
| let dir_has_version_suffix = opts.versioned_dirs || id.version() != max_version; |
| let dst_name = if dir_has_version_suffix { |
| // Eg vendor/futures-0.1.13 |
| format!("{}-{}", id.name(), id.version()) |
| } else { |
| // Eg vendor/futures |
| id.name().to_string() |
| }; |
| |
| sources.insert(id.source_id()); |
| let dst = vendor_dir.join(&dst_name); |
| to_remove.remove(&dst); |
| let cksum = dst.join(".cargo-checksum.json"); |
| // Registries are the only immutable sources, |
| // path and git dependencies' versions cannot be trusted to mean "no change" |
| if dir_has_version_suffix && id.source_id().is_registry() && cksum.exists() { |
| // Don't re-copy directory with version suffix in case it comes from a registry |
| continue; |
| } |
| |
| gctx.shell().status( |
| "Vendoring", |
| &format!("{} ({}) to {}", id, src.to_string_lossy(), dst.display()), |
| )?; |
| |
| let _ = fs::remove_dir_all(&dst); |
| |
| let mut file_cksums = BTreeMap::new(); |
| |
| // Need this mapping anyway because we will directly consult registry sources, |
| // otherwise builtin source replacement (sparse registry) won't be respected. |
| let sid = source_replacement_cache.get(id.source_id())?; |
| |
| if sid.is_registry() { |
| // To keep the unpacked source from registry in a pristine state, |
| // we'll do a direct extraction into the vendor directory. |
| let registry = match sid.kind() { |
| SourceKind::Registry | SourceKind::SparseRegistry => { |
| RegistrySource::remote(sid, gctx)? |
| } |
| SourceKind::LocalRegistry => { |
| let path = sid.url().to_file_path().expect("local path"); |
| RegistrySource::local(sid, &path, gctx) |
| } |
| _ => unreachable!("not registry source: {sid}"), |
| }; |
| |
| let walkdir = |root| { |
| WalkDir::new(root) |
| .into_iter() |
| // It is safe to skip errors, |
| // since we'll hit them during copying/reading later anyway. |
| .filter_map(|e| e.ok()) |
| // There should be no symlink in tarballs on crates.io, |
| // but might be wrong for local registries. |
| // Hence here be conservative and include symlinks. |
| .filter(|e| e.file_type().is_file() || e.file_type().is_symlink()) |
| }; |
| let mut compute_file_cksums = |root| { |
| for e in walkdir(root) { |
| let path = e.path(); |
| let relative = path.strip_prefix(&dst).unwrap(); |
| let cksum = Sha256::new() |
| .update_path(path) |
| .map(Sha256::finish_hex) |
| .with_context(|| format!("failed to checksum `{}`", path.display()))?; |
| file_cksums.insert(relative.to_str().unwrap().replace("\\", "/"), cksum); |
| } |
| Ok::<_, anyhow::Error>(()) |
| }; |
| if dir_has_version_suffix { |
| registry.unpack_package_in(id, &vendor_dir, &vendor_this)?; |
| compute_file_cksums(&dst)?; |
| } else { |
| // Due to the extra sanity check in registry unpack |
| // (ensure it contain only one top-level directory with name `pkg-version`), |
| // we can only unpack a directory with version suffix, |
| // and move it to the no suffix directory. |
| let staging_dir = tempfile::Builder::new() |
| .prefix(".vendor-staging") |
| .tempdir_in(vendor_dir)?; |
| let unpacked_src = |
| registry.unpack_package_in(id, staging_dir.path(), &vendor_this)?; |
| |
| let rename_result = if gctx |
| .get_env_os("__CARGO_TEST_VENDOR_FALLBACK_CP_SOURCES") |
| .is_some() |
| { |
| Err(io::Error::new( |
| io::ErrorKind::Other, |
| "simulated rename error for testing", |
| )) |
| } else { |
| fs::rename(&unpacked_src, &dst) |
| }; |
| |
| if let Err(e) = rename_result { |
| // This fallback is worked for sometimes `fs::rename` failed in a specific situation, such as: |
| // - In Windows 10 versions earlier than 1607, the destination of `fs::rename` can't be a directory in older versions. |
| // - `from` and `to` are on separate filesystems. |
| // - AntiVirus or our system indexer are doing stuff simultaneously. |
| // - Any other reasons documented in std::fs::rename. |
| tracing::warn!("failed to `mv {unpacked_src:?} {dst:?}`: {e}"); |
| let paths: Vec<_> = walkdir(&unpacked_src).map(|e| e.into_path()).collect(); |
| cp_sources( |
| pkg, |
| &unpacked_src, |
| &paths, |
| &dst, |
| &mut file_cksums, |
| &mut tmp_buf, |
| gctx, |
| ) |
| .with_context(|| format!("failed to copy vendored sources for {id}"))?; |
| } else { |
| compute_file_cksums(&dst)?; |
| } |
| } |
| } else { |
| let paths = PathSource::new(src, sid, gctx) |
| .list_files(pkg)? |
| .into_iter() |
| .map(|p| p.into_path_buf()) |
| .collect::<Vec<_>>(); |
| cp_sources(pkg, src, &paths, &dst, &mut file_cksums, &mut tmp_buf, gctx) |
| .with_context(|| format!("failed to copy vendored sources for {id}"))?; |
| } |
| |
| // Finally, emit the metadata about this package |
| let json = serde_json::json!({ |
| "$comment": "This file only protects against accidental modifications. \ |
| It is not a security mechanism and does not protect against malicious changes.", |
| "package": checksums.get(id), |
| "files": file_cksums, |
| }); |
| |
| paths::write(&cksum, json.to_string())?; |
| } |
| |
| for path in to_remove { |
| if path.is_dir() { |
| paths::remove_dir_all(&path)?; |
| } else { |
| paths::remove_file(&path)?; |
| } |
| } |
| |
| // add our vendored source |
| let mut config = BTreeMap::new(); |
| |
| let merged_source_name = "vendored-sources"; |
| |
| // replace original sources with vendor |
| for source_id in sources { |
| let name = if source_id.is_crates_io() { |
| CRATES_IO_REGISTRY.to_string() |
| } else { |
| // Remove `precise` since that makes the source name very long, |
| // and isn't needed to disambiguate multiple sources. |
| source_id.without_precise().as_url().to_string() |
| }; |
| |
| let source = if source_id.is_crates_io() { |
| VendorSource::Registry { |
| registry: None, |
| replace_with: merged_source_name.to_string(), |
| } |
| } else if source_id.is_remote_registry() { |
| let registry = source_id.url().to_string(); |
| VendorSource::Registry { |
| registry: Some(registry), |
| replace_with: merged_source_name.to_string(), |
| } |
| } else if source_id.is_git() { |
| let mut branch = None; |
| let mut tag = None; |
| let mut rev = None; |
| if let Some(reference) = source_id.git_reference() { |
| match *reference { |
| GitReference::Branch(ref b) => branch = Some(b.clone()), |
| GitReference::Tag(ref t) => tag = Some(t.clone()), |
| GitReference::Rev(ref r) => rev = Some(r.clone()), |
| GitReference::DefaultBranch => {} |
| } |
| } |
| VendorSource::Git { |
| git: source_id.url().to_string(), |
| branch, |
| tag, |
| rev, |
| replace_with: merged_source_name.to_string(), |
| } |
| } else { |
| panic!("Invalid source ID: {}", source_id) |
| }; |
| config.insert(name, source); |
| } |
| |
| if !config.is_empty() { |
| config.insert( |
| merged_source_name.to_string(), |
| VendorSource::Directory { |
| // Windows-flavour paths are valid here on Windows but Unix. |
| // This backslash normalization is for making output paths more |
| // cross-platform compatible. |
| directory: opts.destination.to_string_lossy().replace("\\", "/"), |
| }, |
| ); |
| } else if !vendor_dir_already_exists { |
| // Nothing to vendor. Remove the destination dir we've just created. |
| paths::remove_dir(vendor_dir)?; |
| } |
| |
| Ok(VendorConfig { source: config }) |
| } |
| |
| fn cp_sources( |
| pkg: &Package, |
| src: &Path, |
| paths: &[PathBuf], |
| dst: &Path, |
| cksums: &mut BTreeMap<String, String>, |
| tmp_buf: &mut [u8], |
| gctx: &GlobalContext, |
| ) -> CargoResult<()> { |
| for p in paths { |
| let relative = p.strip_prefix(&src).unwrap(); |
| |
| if !vendor_this(relative) { |
| continue; |
| } |
| |
| // Join pathname components individually to make sure that the joined |
| // path uses the correct directory separators everywhere, since |
| // `relative` may use Unix-style and `dst` may require Windows-style |
| // backslashes. |
| let dst = relative |
| .iter() |
| .fold(dst.to_owned(), |acc, component| acc.join(&component)); |
| |
| paths::create_dir_all(dst.parent().unwrap())?; |
| let mut dst_opts = OpenOptions::new(); |
| dst_opts.write(true).create(true).truncate(true); |
| // When vendoring git dependencies, the manifest has not been normalized like it would be |
| // when published. This causes issue when the manifest is using workspace inheritance. |
| // To get around this issue we use the "original" manifest after `{}.workspace = true` |
| // has been resolved for git dependencies. |
| let cksum = if dst.file_name() == Some(OsStr::new("Cargo.toml")) |
| && pkg.package_id().source_id().is_git() |
| { |
| let packaged_files = paths |
| .iter() |
| .map(|p| p.strip_prefix(src).unwrap().to_owned()) |
| .collect::<Vec<_>>(); |
| let vendored_pkg = prepare_for_vendor(pkg, &packaged_files, gctx)?; |
| let contents = vendored_pkg.manifest().to_normalized_contents()?; |
| copy_and_checksum( |
| &dst, |
| &mut dst_opts, |
| &mut contents.as_bytes(), |
| Path::new("Generated Cargo.toml"), |
| tmp_buf, |
| )? |
| } else { |
| let mut src = File::open(&p).with_context(|| format!("failed to open {:?}", &p))?; |
| #[cfg(unix)] |
| { |
| use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; |
| let src_metadata = src |
| .metadata() |
| .with_context(|| format!("failed to stat {:?}", p))?; |
| dst_opts.mode(src_metadata.mode()); |
| } |
| copy_and_checksum(&dst, &mut dst_opts, &mut src, &p, tmp_buf)? |
| }; |
| |
| cksums.insert(relative.to_str().unwrap().replace("\\", "/"), cksum); |
| } |
| Ok(()) |
| } |
| |
| /// HACK: Perform the bare minimum of `prepare_for_publish` needed for #14348. |
| /// |
| /// There are parts of `prepare_for_publish` that could be directly useful (e.g. stripping |
| /// `[workspace]`) while other parts that require other filesystem operations (moving the README |
| /// file) and ideally we'd reuse `cargo package` code to take care of all of this for us. |
| fn prepare_for_vendor( |
| me: &Package, |
| packaged_files: &[PathBuf], |
| gctx: &GlobalContext, |
| ) -> CargoResult<Package> { |
| let contents = me.manifest().contents(); |
| let document = me.manifest().document(); |
| let original_toml = prepare_toml_for_vendor( |
| me.manifest().normalized_toml().clone(), |
| packaged_files, |
| gctx, |
| )?; |
| let normalized_toml = original_toml.clone(); |
| let features = me.manifest().unstable_features().clone(); |
| let workspace_config = me.manifest().workspace_config().clone(); |
| let source_id = me.package_id().source_id(); |
| let mut warnings = Default::default(); |
| let mut errors = Default::default(); |
| let manifest = crate::workspace::parser::to_real_manifest( |
| contents.map(|c| c.to_owned()), |
| document.cloned(), |
| original_toml, |
| normalized_toml, |
| features, |
| workspace_config, |
| source_id, |
| me.manifest_path(), |
| me.manifest().is_embedded(), |
| gctx, |
| &mut warnings, |
| &mut errors, |
| )?; |
| let new_pkg = Package::new(manifest, me.manifest_path()); |
| Ok(new_pkg) |
| } |
| |
| fn prepare_toml_for_vendor( |
| mut me: cargo_util_schemas::manifest::TomlManifest, |
| packaged_files: &[PathBuf], |
| gctx: &GlobalContext, |
| ) -> CargoResult<cargo_util_schemas::manifest::TomlManifest> { |
| let package = me |
| .package |
| .as_mut() |
| .expect("venedored manifests must have packages"); |
| // Validates if build script file is included in package. If not, warn and ignore. |
| if let Some(custom_build_scripts) = package.normalized_build().expect("previously normalized") { |
| let mut included_scripts = Vec::new(); |
| for script in custom_build_scripts { |
| let path = paths::normalize_path(Path::new(script)); |
| let included = packaged_files.contains(&path); |
| if included { |
| let path = path |
| .into_os_string() |
| .into_string() |
| .map_err(|_err| anyhow::format_err!("non-UTF8 `package.build`"))?; |
| let path = crate::workspace::parser::normalize_path_string_sep(path); |
| included_scripts.push(path); |
| } else { |
| gctx.shell().warn(format!( |
| "ignoring `package.build` entry `{}` as it is not included in the published package", |
| path.display() |
| ))?; |
| } |
| } |
| package.build = Some(match included_scripts.len() { |
| 0 => TomlPackageBuild::Auto(false), |
| 1 => TomlPackageBuild::SingleScript(included_scripts[0].clone()), |
| _ => TomlPackageBuild::MultipleScript(included_scripts), |
| }); |
| } |
| |
| let lib = if let Some(target) = &me.lib { |
| crate::workspace::parser::prepare_target_for_publish( |
| target, |
| Some(packaged_files), |
| "library", |
| gctx, |
| )? |
| } else { |
| None |
| }; |
| let bin = crate::workspace::parser::prepare_targets_for_publish( |
| me.bin.as_ref(), |
| Some(packaged_files), |
| "binary", |
| gctx, |
| )?; |
| let example = crate::workspace::parser::prepare_targets_for_publish( |
| me.example.as_ref(), |
| Some(packaged_files), |
| "example", |
| gctx, |
| )?; |
| let test = crate::workspace::parser::prepare_targets_for_publish( |
| me.test.as_ref(), |
| Some(packaged_files), |
| "test", |
| gctx, |
| )?; |
| let bench = crate::workspace::parser::prepare_targets_for_publish( |
| me.bench.as_ref(), |
| Some(packaged_files), |
| "benchmark", |
| gctx, |
| )?; |
| |
| me.lib = lib; |
| me.bin = bin; |
| me.example = example; |
| me.test = test; |
| me.bench = bench; |
| |
| Ok(me) |
| } |
| |
| fn copy_and_checksum<T: Read>( |
| dst_path: &Path, |
| dst_opts: &mut OpenOptions, |
| contents: &mut T, |
| contents_path: &Path, |
| buf: &mut [u8], |
| ) -> CargoResult<String> { |
| let mut dst = dst_opts |
| .open(dst_path) |
| .with_context(|| format!("failed to create {:?}", dst_path))?; |
| // Not going to bother setting mode on pre-existing files, since there |
| // shouldn't be any under normal conditions. |
| let mut cksum = Sha256::new(); |
| loop { |
| let n = contents |
| .read(buf) |
| .with_context(|| format!("failed to read from {:?}", contents_path))?; |
| if n == 0 { |
| break Ok(cksum.finish_hex()); |
| } |
| let data = &buf[..n]; |
| cksum.update(data); |
| dst.write_all(data) |
| .with_context(|| format!("failed to write to {:?}", dst_path))?; |
| } |
| } |
| |
| /// Filters files we want to vendor. |
| /// |
| /// `relative` is a path relative to the package root. |
| |
| fn vendor_this(relative: &Path) -> bool { |
| // Skip git config files as they're not relevant to builds most of |
| // the time and if we respect them (e.g. in git) then it'll |
| // probably mess with the checksums when a vendor dir is checked |
| // into someone else's source control |
| for component in relative.components() { |
| if let Some(name) = component.as_os_str().to_str() { |
| if name == ".git" { |
| return false; |
| } |
| } |
| } |
| |
| if let Some(file_name) = relative.file_name().and_then(|s| s.to_str()) { |
| if matches!(file_name, ".gitattributes" | ".gitignore") { |
| return false; |
| } |
| } |
| |
| // Temporary Cargo files |
| match relative.to_str() { |
| Some(".cargo-ok") => false, |
| _ => true, |
| } |
| } |