1use std::collections::BTreeMap;
2use std::collections::BTreeSet;
3use std::collections::HashMap;
4use std::fs::File;
5use std::io::SeekFrom;
6use std::io::prelude::*;
7use std::path::{Path, PathBuf};
8
9use crate::core::Dependency;
10use crate::core::PackageIdSpecQuery;
11use crate::core::Workspace;
12use crate::core::dependency::DepKind;
13use crate::core::manifest::Target;
14use crate::core::resolver::CliFeatures;
15use crate::core::resolver::HasDevUnits;
16use crate::core::{Package, PackageId, PackageSet, Resolve, SourceId};
17use crate::ops::lockfile::LOCKFILE_NAME;
18use crate::ops::registry::{RegistryOrIndex, infer_registry};
19use crate::sources::path::PathEntry;
20use crate::sources::source::QueryKind;
21use crate::sources::{CRATES_IO_REGISTRY, PathSource};
22use crate::util::FileLock;
23use crate::util::Filesystem;
24use crate::util::GlobalContext;
25use crate::util::Graph;
26use crate::util::HumanBytes;
27use crate::util::OptVersionReq;
28use crate::util::cache_lock::CacheLockMode;
29use crate::util::context::JobsConfig;
30use crate::util::errors::CargoResult;
31use crate::util::errors::ManifestError;
32use crate::util::restricted_names;
33use crate::util::toml::prepare_for_publish;
34use crate::{drop_println, ops};
35use anyhow::{Context as _, bail};
36use cargo_util::paths;
37use cargo_util_schemas::index::{IndexPackage, RegistryDependency};
38use cargo_util_schemas::messages;
39use cargo_util_terminal::report::Level;
40use cargo_util_terminal::{Shell, Verbosity};
41use flate2::{Compression, GzBuilder};
42use futures::TryStreamExt;
43use futures::stream::FuturesUnordered;
44use tar::{Builder, EntryType, Header, HeaderMode};
45use tracing::debug;
46use unicase::Ascii as UncasedAscii;
47
48mod vcs;
49mod verify;
50
51#[derive(Debug, Clone)]
55pub enum PackageMessageFormat {
56 Human,
57 Json,
58}
59
60impl PackageMessageFormat {
61 pub const POSSIBLE_VALUES: [&str; 2] = ["human", "json"];
62
63 pub const DEFAULT: &str = "human";
64}
65
66impl std::str::FromStr for PackageMessageFormat {
67 type Err = anyhow::Error;
68
69 fn from_str(s: &str) -> Result<PackageMessageFormat, anyhow::Error> {
70 match s {
71 "human" => Ok(PackageMessageFormat::Human),
72 "json" => Ok(PackageMessageFormat::Json),
73 f => bail!("unknown message format `{f}`"),
74 }
75 }
76}
77
78#[derive(Clone)]
79pub struct PackageOpts<'gctx> {
80 pub gctx: &'gctx GlobalContext,
81 pub list: bool,
82 pub fmt: PackageMessageFormat,
83 pub check_metadata: bool,
84 pub allow_dirty: bool,
85 pub include_lockfile: bool,
86 pub verify: bool,
87 pub jobs: Option<JobsConfig>,
88 pub keep_going: bool,
89 pub to_package: ops::Packages,
90 pub targets: Vec<String>,
91 pub cli_features: CliFeatures,
92 pub reg_or_index: Option<ops::RegistryOrIndex>,
93 pub dry_run: bool,
106}
107
108const ORIGINAL_MANIFEST_FILE: &str = "Cargo.toml.orig";
109const VCS_INFO_FILE: &str = ".cargo_vcs_info.json";
110
111struct ArchiveFile {
112 rel_path: PathBuf,
115 rel_str: String,
117 contents: FileContents,
119}
120
121enum FileContents {
122 OnDisk(PathBuf),
124 Generated(GeneratedFile),
126}
127
128enum GeneratedFile {
129 Manifest(PathBuf),
133 Lockfile(Option<PathBuf>),
137 VcsInfo(vcs::VcsInfo),
139}
140
141#[tracing::instrument(skip_all)]
143fn create_package(
144 ws: &Workspace<'_>,
145 opts: &PackageOpts<'_>,
146 pkg: &Package,
147 ar_files: Vec<ArchiveFile>,
148 local_reg: Option<&TmpRegistry<'_>>,
149) -> CargoResult<FileLock> {
150 let gctx = ws.gctx();
151 let filecount = ar_files.len();
152
153 for dep in pkg.dependencies() {
155 super::check_dep_has_version(dep, false).map_err(|err| {
156 ManifestError::new(
157 err.context(format!(
158 "failed to verify manifest at `{}`",
159 pkg.manifest_path().display()
160 )),
161 pkg.manifest_path().into(),
162 )
163 })?;
164 }
165
166 let filename = pkg.package_id().tarball_name();
167 let build_dir = ws.build_dir();
168 paths::create_dir_all_excluded_from_backups_atomic(build_dir.as_path_unlocked())?;
169 let dir = build_dir.join("package").join("tmp-crate");
170 let dst = dir.open_rw_exclusive_create(&filename, gctx, "package scratch space")?;
171
172 gctx.shell()
177 .status("Packaging", pkg.package_id().to_string())?;
178 dst.file().set_len(0)?;
179 let uncompressed_size = tar(ws, opts, pkg, local_reg, ar_files, dst.file(), &filename)
180 .context("failed to prepare local package for uploading")?;
181
182 let dst_metadata = dst
183 .file()
184 .metadata()
185 .with_context(|| format!("could not learn metadata for: `{}`", dst.path().display()))?;
186 let compressed_size = dst_metadata.len();
187
188 let uncompressed = HumanBytes(uncompressed_size);
189 let compressed = HumanBytes(compressed_size);
190
191 let message = format!("{filecount} files, {uncompressed:.1} ({compressed:.1} compressed)");
192 drop(gctx.shell().status("Packaged", message));
194
195 return Ok(dst);
196}
197
198pub fn package(ws: &Workspace<'_>, opts: &PackageOpts<'_>) -> CargoResult<Vec<FileLock>> {
203 let specs = &opts.to_package.to_package_id_specs(ws)?;
204 if let ops::Packages::Packages(_) = opts.to_package {
206 for spec in specs.iter() {
207 let member_ids = ws.members().map(|p| p.package_id());
208 spec.query(member_ids)?;
209 }
210 }
211 let mut pkgs = ws.members_with_features(specs, &opts.cli_features)?;
212
213 pkgs.retain(|(pkg, _feats)| specs.iter().any(|spec| spec.matches(pkg.package_id())));
216
217 let packaged = do_package(ws, opts, pkgs)?;
218
219 let mut result = Vec::new();
221 let target_dir = ws.target_dir();
222 paths::create_dir_all_excluded_from_backups_atomic(target_dir.as_path_unlocked())?;
223 let artifact_dir = target_dir.join("package");
224 for (pkg, _, src) in packaged {
225 let filename = pkg.package_id().tarball_name();
226 let dst = artifact_dir.open_rw_exclusive_create(filename, ws.gctx(), "uplifted package")?;
227 dst.file().set_len(0)?;
228 src.file().seek(SeekFrom::Start(0))?;
229 std::io::copy(&mut src.file(), &mut dst.file())?;
230 result.push(dst);
231 }
232
233 Ok(result)
234}
235
236pub(crate) fn package_with_dep_graph(
242 ws: &Workspace<'_>,
243 opts: &PackageOpts<'_>,
244 pkgs: Vec<(&Package, CliFeatures)>,
245) -> CargoResult<LocalDependencies<(CliFeatures, FileLock)>> {
246 let output = do_package(ws, opts, pkgs)?;
247
248 Ok(local_deps(output.into_iter().map(
249 |(pkg, opts, tarball)| (pkg, (opts.cli_features, tarball)),
250 )))
251}
252
253fn do_package<'a>(
254 ws: &Workspace<'_>,
255 opts: &PackageOpts<'a>,
256 pkgs: Vec<(&Package, CliFeatures)>,
257) -> CargoResult<Vec<(Package, PackageOpts<'a>, FileLock)>> {
258 if ws
259 .lock_root()
260 .as_path_unlocked()
261 .join(LOCKFILE_NAME)
262 .exists()
263 && opts.include_lockfile
264 {
265 let dry_run = false;
267 let _ = ops::resolve_ws(ws, dry_run)?;
268 }
271
272 let deps = local_deps(pkgs.iter().map(|(p, f)| ((*p).clone(), f.clone())));
273 let just_pkgs: Vec<_> = pkgs.iter().map(|p| p.0).collect();
274
275 let needs_local_reg = deps.has_dependencies() && (opts.include_lockfile || opts.verify);
282 let verify_registry_allow_list = opts.reg_or_index.is_some();
283 let mut local_reg = if !opts.list && (needs_local_reg || verify_registry_allow_list) {
284 let sid = get_registry(ws.gctx(), &just_pkgs, opts.reg_or_index.clone())?;
285 debug!("packaging for registry {}", sid);
286 let reg_dir = ws.build_dir().join("package").join("tmp-registry");
287 let local_reg = TmpRegistry::new(ws.gctx(), reg_dir, sid)?;
288 Some(local_reg)
289 } else {
290 None
291 };
292
293 let sorted_pkgs = deps.sort();
296 let mut outputs: Vec<(Package, PackageOpts<'_>, FileLock)> = Vec::new();
297 for (pkg, cli_features) in sorted_pkgs {
298 let opts = PackageOpts {
299 cli_features: cli_features.clone(),
300 to_package: ops::Packages::Default,
301 ..opts.clone()
302 };
303 let ar_files = prepare_archive(ws, &pkg, &opts)?;
304
305 if opts.list {
306 match opts.fmt {
307 PackageMessageFormat::Human => {
308 for ar_file in &ar_files {
311 drop_println!(ws.gctx(), "{}", ar_file.rel_str);
312 }
313 }
314 PackageMessageFormat::Json => {
315 let message = messages::PackageList {
316 id: pkg.package_id().to_spec(),
317 files: BTreeMap::from_iter(ar_files.into_iter().map(|f| {
318 let file = match f.contents {
319 FileContents::OnDisk(path) => messages::PackageFile::Copy { path },
320 FileContents::Generated(
321 GeneratedFile::Manifest(path)
322 | GeneratedFile::Lockfile(Some(path)),
323 ) => messages::PackageFile::Generate { path: Some(path) },
324 FileContents::Generated(
325 GeneratedFile::VcsInfo(_) | GeneratedFile::Lockfile(None),
326 ) => messages::PackageFile::Generate { path: None },
327 };
328 (f.rel_path, file)
329 })),
330 };
331 let _ = ws.gctx().shell().print_json(&message);
332 }
333 }
334 } else {
335 let tarball = create_package(ws, &opts, &pkg, ar_files, local_reg.as_ref())?;
336 if let Some(local_reg) = local_reg.as_mut() {
337 if pkg.publish() != &Some(Vec::new()) {
338 local_reg.add_package(ws, &pkg, &tarball)?;
339 }
340 }
341 outputs.push((pkg, opts, tarball));
342 }
343 }
344
345 if opts.verify {
348 for (pkg, opts, tarball) in &outputs {
349 verify::run_verify(ws, pkg, tarball, local_reg.as_ref(), opts)
350 .context("failed to verify package tarball")?
351 }
352 }
353
354 Ok(outputs)
355}
356
357fn get_registry(
364 gctx: &GlobalContext,
365 pkgs: &[&Package],
366 reg_or_index: Option<RegistryOrIndex>,
367) -> CargoResult<SourceId> {
368 let reg_or_index = match reg_or_index.clone() {
369 Some(r) => Some(r),
370 None => infer_registry(pkgs)?,
371 };
372
373 let reg = reg_or_index
375 .clone()
376 .unwrap_or_else(|| RegistryOrIndex::Registry(CRATES_IO_REGISTRY.to_owned()));
377 if let RegistryOrIndex::Registry(reg_name) = reg {
378 for pkg in pkgs {
379 if let Some(allowed) = pkg.publish().as_ref() {
380 if !allowed.is_empty() && !allowed.iter().any(|a| a == ®_name) {
384 bail!(
385 "`{}` cannot be packaged.\n\
386 The registry `{}` is not listed in the `package.publish` value in Cargo.toml.",
387 pkg.name(),
388 reg_name
389 );
390 }
391 }
392 }
393 }
394 Ok(ops::registry::get_source_id(gctx, reg_or_index.as_ref())?.replacement)
395}
396
397#[derive(Clone, Debug, Default)]
399pub(crate) struct LocalDependencies<T> {
400 pub packages: HashMap<PackageId, (Package, T)>,
401 pub graph: Graph<PackageId, ()>,
402}
403
404impl<T: Clone> LocalDependencies<T> {
405 pub fn sort(&self) -> Vec<(Package, T)> {
406 self.graph
407 .sort()
408 .into_iter()
409 .map(|name| self.packages[&name].clone())
410 .collect()
411 }
412
413 pub fn has_dependencies(&self) -> bool {
414 self.graph
415 .iter()
416 .any(|node| self.graph.edges(node).next().is_some())
417 }
418}
419
420fn local_deps<T>(packages: impl Iterator<Item = (Package, T)>) -> LocalDependencies<T> {
425 let packages: HashMap<PackageId, (Package, T)> = packages
426 .map(|(pkg, payload)| (pkg.package_id(), (pkg, payload)))
427 .collect();
428
429 let source_to_pkg: HashMap<_, _> = packages
434 .keys()
435 .map(|pkg_id| (pkg_id.source_id(), *pkg_id))
436 .collect();
437
438 let mut graph = Graph::new();
439 for (pkg, _payload) in packages.values() {
440 graph.add(pkg.package_id());
441 for dep in pkg.dependencies() {
442 if !dep.source_id().is_path() {
444 continue;
445 }
446
447 if dep.kind() == DepKind::Development && !dep.specified_req() {
450 continue;
451 };
452
453 if dep.source_id() == pkg.package_id().source_id() {
455 continue;
456 }
457
458 if let Some(dep_pkg) = source_to_pkg.get(&dep.source_id()) {
459 graph.link(pkg.package_id(), *dep_pkg);
460 }
461 }
462 }
463
464 LocalDependencies { packages, graph }
465}
466
467#[tracing::instrument(skip_all)]
469fn prepare_archive(
470 ws: &Workspace<'_>,
471 pkg: &Package,
472 opts: &PackageOpts<'_>,
473) -> CargoResult<Vec<ArchiveFile>> {
474 let gctx = ws.gctx();
475 let src = PathSource::new(pkg.root(), pkg.package_id().source_id(), gctx);
476 src.load()?;
477
478 if opts.check_metadata {
479 check_metadata(pkg, opts.reg_or_index.as_ref(), gctx)?;
480 }
481
482 if !pkg.manifest().exclude().is_empty() && !pkg.manifest().include().is_empty() {
483 gctx.shell().warn(
484 "both package.include and package.exclude are specified; \
485 the exclude list will be ignored",
486 )?;
487 }
488 let src_files = src.list_files(pkg)?;
489
490 let vcs_info = vcs::check_repo_state(pkg, &src_files, ws, &opts)?;
492 build_ar_list(ws, pkg, src_files, vcs_info, opts.include_lockfile)
493}
494
495#[tracing::instrument(skip_all)]
497fn build_ar_list(
498 ws: &Workspace<'_>,
499 pkg: &Package,
500 src_files: Vec<PathEntry>,
501 vcs_info: Option<vcs::VcsInfo>,
502 include_lockfile: bool,
503) -> CargoResult<Vec<ArchiveFile>> {
504 let mut result = HashMap::new();
505 let root = pkg.root();
506 for src_file in &src_files {
507 let rel_path = src_file.strip_prefix(&root)?;
508 check_filename(rel_path, &mut ws.gctx().shell())?;
509 let rel_str = rel_path.to_str().ok_or_else(|| {
510 anyhow::format_err!("non-utf8 path in source directory: {}", rel_path.display())
511 })?;
512 match rel_str {
513 "Cargo.lock" => continue,
514 VCS_INFO_FILE | ORIGINAL_MANIFEST_FILE => anyhow::bail!(
515 "invalid inclusion of reserved file name {} in package source",
516 rel_str
517 ),
518 _ => {
519 result
520 .entry(UncasedAscii::new(rel_str))
521 .or_insert_with(Vec::new)
522 .push(ArchiveFile {
523 rel_path: rel_path.to_owned(),
524 rel_str: rel_str.to_owned(),
525 contents: FileContents::OnDisk(src_file.to_path_buf()),
526 });
527 }
528 }
529 }
530
531 if result.remove(&UncasedAscii::new("Cargo.toml")).is_some() {
534 result
535 .entry(UncasedAscii::new(ORIGINAL_MANIFEST_FILE))
536 .or_insert_with(Vec::new)
537 .push(ArchiveFile {
538 rel_path: PathBuf::from(ORIGINAL_MANIFEST_FILE),
539 rel_str: ORIGINAL_MANIFEST_FILE.to_string(),
540 contents: FileContents::OnDisk(pkg.manifest_path().to_owned()),
541 });
542 result
543 .entry(UncasedAscii::new("Cargo.toml"))
544 .or_insert_with(Vec::new)
545 .push(ArchiveFile {
546 rel_path: PathBuf::from("Cargo.toml"),
547 rel_str: "Cargo.toml".to_string(),
548 contents: FileContents::Generated(GeneratedFile::Manifest(
549 pkg.manifest_path().to_owned(),
550 )),
551 });
552 } else {
553 ws.gctx().shell().warn(&format!(
554 "no `Cargo.toml` file found when packaging `{}` (note the case of the file name).",
555 pkg.name()
556 ))?;
557 }
558
559 if include_lockfile {
560 let lockfile_path = ws.lock_root().as_path_unlocked().join(LOCKFILE_NAME);
561 let lockfile_path = lockfile_path.exists().then_some(lockfile_path);
562 let rel_str = "Cargo.lock";
563 result
564 .entry(UncasedAscii::new(rel_str))
565 .or_insert_with(Vec::new)
566 .push(ArchiveFile {
567 rel_path: PathBuf::from(rel_str),
568 rel_str: rel_str.to_string(),
569 contents: FileContents::Generated(GeneratedFile::Lockfile(lockfile_path)),
570 });
571 }
572
573 if let Some(vcs_info) = vcs_info {
574 let rel_str = VCS_INFO_FILE;
575 result
576 .entry(UncasedAscii::new(rel_str))
577 .or_insert_with(Vec::new)
578 .push(ArchiveFile {
579 rel_path: PathBuf::from(rel_str),
580 rel_str: rel_str.to_string(),
581 contents: FileContents::Generated(GeneratedFile::VcsInfo(vcs_info)),
582 });
583 }
584
585 let mut invalid_manifest_field: Vec<String> = vec![];
586
587 let mut result = result.into_values().flatten().collect();
588 if let Some(license_file) = &pkg.manifest().metadata().license_file {
589 let license_path = Path::new(license_file);
590 let abs_file_path = paths::normalize_path(&pkg.root().join(license_path));
591 if abs_file_path.is_file() {
592 check_for_file_and_add(
593 "license-file",
594 license_path,
595 abs_file_path,
596 pkg,
597 &mut result,
598 ws,
599 )?;
600 } else {
601 error_on_nonexistent_file(
602 &pkg,
603 &license_path,
604 "license-file",
605 &mut invalid_manifest_field,
606 );
607 }
608 }
609 if let Some(readme) = &pkg.manifest().metadata().readme {
610 let readme_path = Path::new(readme);
611 let abs_file_path = paths::normalize_path(&pkg.root().join(readme_path));
612 if abs_file_path.is_file() {
613 check_for_file_and_add("readme", readme_path, abs_file_path, pkg, &mut result, ws)?;
614 } else {
615 error_on_nonexistent_file(&pkg, &readme_path, "readme", &mut invalid_manifest_field);
616 }
617 }
618
619 if !invalid_manifest_field.is_empty() {
620 return Err(anyhow::anyhow!(invalid_manifest_field.join("\n")));
621 }
622
623 for t in pkg
624 .manifest()
625 .targets()
626 .iter()
627 .filter(|t| t.is_custom_build())
628 {
629 if let Some(custom_build_path) = t.src_path().path() {
630 let abs_custom_build_path = paths::normalize_path(&pkg.root().join(custom_build_path));
631 if !abs_custom_build_path.is_file() || !abs_custom_build_path.starts_with(pkg.root()) {
632 error_custom_build_file_not_in_package(pkg, &abs_custom_build_path, t)?;
633 }
634 }
635 }
636
637 result.sort_unstable_by(|a, b| a.rel_path.cmp(&b.rel_path));
638
639 Ok(result)
640}
641
642fn check_for_file_and_add(
643 label: &str,
644 file_path: &Path,
645 abs_file_path: PathBuf,
646 pkg: &Package,
647 result: &mut Vec<ArchiveFile>,
648 ws: &Workspace<'_>,
649) -> CargoResult<()> {
650 match abs_file_path.strip_prefix(&pkg.root()) {
651 Ok(rel_file_path) => {
652 if !result.iter().any(|ar| ar.rel_path == rel_file_path) {
653 result.push(ArchiveFile {
654 rel_path: rel_file_path.to_path_buf(),
655 rel_str: rel_file_path
656 .to_str()
657 .expect("everything was utf8")
658 .to_string(),
659 contents: FileContents::OnDisk(abs_file_path),
660 })
661 }
662 }
663 Err(_) => {
664 let file_name = file_path.file_name().unwrap();
666 if result.iter().any(|ar| ar.rel_path == file_name) {
667 ws.gctx().shell().warn(&format!(
668 "{} `{}` appears to be a path outside of the package, \
669 but there is already a file named `{}` in the root of the package. \
670 The archived crate will contain the copy in the root of the package. \
671 Update the {} to point to the path relative \
672 to the root of the package to remove this warning.",
673 label,
674 file_path.display(),
675 file_name.to_str().unwrap(),
676 label,
677 ))?;
678 } else {
679 result.push(ArchiveFile {
680 rel_path: PathBuf::from(file_name),
681 rel_str: file_name.to_str().unwrap().to_string(),
682 contents: FileContents::OnDisk(abs_file_path),
683 })
684 }
685 }
686 }
687 Ok(())
688}
689
690fn error_on_nonexistent_file(
691 pkg: &Package,
692 path: &Path,
693 manifest_key_name: &'static str,
694 invalid: &mut Vec<String>,
695) {
696 let rel_msg = if path.is_absolute() {
697 "".to_string()
698 } else {
699 format!(" (relative to `{}`)", pkg.root().display())
700 };
701
702 let msg = format!(
703 "{manifest_key_name} `{}` does not appear to exist{}.\n\
704 Please update the {manifest_key_name} setting in the manifest at `{}`.",
705 path.display(),
706 rel_msg,
707 pkg.manifest_path().display()
708 );
709
710 invalid.push(msg);
711}
712
713fn error_custom_build_file_not_in_package(
714 pkg: &Package,
715 path: &Path,
716 target: &Target,
717) -> CargoResult<Vec<ArchiveFile>> {
718 let tip = {
719 let description_name = target.description_named();
720 if path.is_file() {
721 format!(
722 "the source file of {description_name} doesn't appear to be a path inside of the package.\n\
723 It is at `{}`, whereas the root the package is `{}`.\n",
724 path.display(),
725 pkg.root().display()
726 )
727 } else {
728 format!("the source file of {description_name} doesn't appear to exist.\n",)
729 }
730 };
731 let msg = format!(
732 "{}\
733 This may cause issue during packaging, as modules resolution and resources included via macros are often relative to the path of source files.\n\
734 Please update the `build` setting in the manifest at `{}` and point to a path inside the root of the package.",
735 tip,
736 pkg.manifest_path().display()
737 );
738 anyhow::bail!(msg)
739}
740
741fn build_lock(
743 ws: &Workspace<'_>,
744 opts: &PackageOpts<'_>,
745 publish_pkg: &Package,
746 local_reg: Option<&TmpRegistry<'_>>,
747) -> CargoResult<String> {
748 let gctx = ws.gctx();
749 let mut orig_resolve = ops::load_pkg_lockfile(ws)?;
750
751 let mut tmp_ws = Workspace::ephemeral(publish_pkg.clone(), ws.gctx(), None, true)?;
752
753 if let Some(local_reg) = local_reg {
757 tmp_ws.add_local_overlay(
758 local_reg.upstream,
759 local_reg.root.as_path_unlocked().to_owned(),
760 );
761 if opts.dry_run {
762 if let Some(orig_resolve) = orig_resolve.as_mut() {
763 let upstream_in_lock = if local_reg.upstream.is_crates_io() {
764 SourceId::crates_io(gctx)?
765 } else {
766 local_reg.upstream
767 };
768 for (p, s) in local_reg.checksums() {
769 orig_resolve.set_checksum(p.with_source_id(upstream_in_lock), s.to_owned());
770 }
771 }
772 }
773 }
774 let mut tmp_reg = tmp_ws.package_registry()?;
775
776 let mut new_resolve = ops::resolve_with_previous(
777 &mut tmp_reg,
778 &tmp_ws,
779 &CliFeatures::new_all(true),
780 HasDevUnits::Yes,
781 orig_resolve.as_ref(),
782 None,
783 &[],
784 true,
785 )?;
786
787 let pkg_set = ops::get_resolved_packages(&new_resolve, tmp_reg)?;
788
789 if let Some(orig_resolve) = orig_resolve {
790 compare_resolve(gctx, tmp_ws.current()?, &orig_resolve, &new_resolve)?;
791 }
792 check_yanked(
793 gctx,
794 &pkg_set,
795 &new_resolve,
796 "consider updating to a version that is not yanked",
797 )?;
798
799 ops::resolve_to_string(&tmp_ws, &mut new_resolve)
800}
801
802fn check_metadata(
805 pkg: &Package,
806 reg_or_index: Option<&RegistryOrIndex>,
807 gctx: &GlobalContext,
808) -> CargoResult<()> {
809 let md = pkg.manifest().metadata();
810
811 let mut missing = vec![];
812
813 macro_rules! lacking {
814 ($( $($field: ident)||* ),*) => {{
815 $(
816 if $(md.$field.as_ref().map_or(true, |s| s.is_empty()))&&* {
817 $(missing.push(stringify!($field).replace("_", "-"));)*
818 }
819 )*
820 }}
821 }
822 lacking!(
823 description,
824 license || license_file,
825 documentation || homepage || repository
826 );
827
828 if !missing.is_empty() {
829 let should_warn = match reg_or_index {
831 Some(RegistryOrIndex::Registry(reg_name)) => reg_name == CRATES_IO_REGISTRY,
832 None => true, Some(RegistryOrIndex::Index(_)) => false, };
835
836 if should_warn {
837 let mut things = missing[..missing.len() - 1].join(", ");
838 if !things.is_empty() {
841 things.push_str(" or ");
842 }
843 things.push_str(missing.last().unwrap());
844
845 gctx.shell().print_report(&[
846 Level::WARNING.secondary_title(format!("manifest has no {things}"))
847 .element(Level::NOTE.message("see https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info"))
848 ],
849 false
850 )?
851 }
852 }
853
854 Ok(())
855}
856
857fn tar(
861 ws: &Workspace<'_>,
862 opts: &PackageOpts<'_>,
863 pkg: &Package,
864 local_reg: Option<&TmpRegistry<'_>>,
865 ar_files: Vec<ArchiveFile>,
866 dst: &File,
867 filename: &str,
868) -> CargoResult<u64> {
869 let filename = Path::new(filename);
871 let encoder = GzBuilder::new()
872 .filename(paths::path2bytes(filename)?)
873 .write(dst, Compression::best());
874
875 let mut ar = Builder::new(encoder);
877 ar.sparse(false);
878 let gctx = ws.gctx();
879
880 let base_name = format!("{}-{}", pkg.name(), pkg.version());
881 let base_path = Path::new(&base_name);
882 let included = ar_files
883 .iter()
884 .map(|ar_file| ar_file.rel_path.clone())
885 .collect::<Vec<_>>();
886 let publish_pkg = prepare_for_publish(pkg, ws, Some(&included))?;
887
888 let mut uncompressed_size = 0;
889 for ar_file in ar_files {
890 let ArchiveFile {
891 rel_path,
892 rel_str,
893 contents,
894 } = ar_file;
895 let ar_path = base_path.join(&rel_path);
896 gctx.shell()
897 .verbose(|shell| shell.status("Archiving", &rel_str))?;
898 let mut header = Header::new_gnu();
899 match contents {
900 FileContents::OnDisk(disk_path) => {
901 let mut file = File::open(&disk_path).with_context(|| {
902 format!("failed to open for archiving: `{}`", disk_path.display())
903 })?;
904 let metadata = file.metadata().with_context(|| {
905 format!("could not learn metadata for: `{}`", disk_path.display())
906 })?;
907 header.set_metadata_in_mode(&metadata, HeaderMode::Deterministic);
908 header.set_cksum();
909 ar.append_data(&mut header, &ar_path, &mut file)
910 .with_context(|| {
911 format!("could not archive source file `{}`", disk_path.display())
912 })?;
913 uncompressed_size += metadata.len() as u64;
914 }
915 FileContents::Generated(generated_kind) => {
916 let contents = match generated_kind {
917 GeneratedFile::Manifest(_) => {
918 publish_pkg.manifest().to_normalized_contents()?
919 }
920 GeneratedFile::Lockfile(_) => build_lock(ws, opts, &publish_pkg, local_reg)?,
921 GeneratedFile::VcsInfo(ref s) => serde_json::to_string_pretty(s)?,
922 };
923 header.set_entry_type(EntryType::file());
924 header.set_mode(0o644);
925 header.set_size(contents.len() as u64);
926 header.set_mtime(1153704088);
932 header.set_cksum();
933 ar.append_data(&mut header, &ar_path, contents.as_bytes())
934 .with_context(|| format!("could not archive source file `{}`", rel_str))?;
935 uncompressed_size += contents.len() as u64;
936 }
937 }
938 }
939
940 let encoder = ar.into_inner()?;
941 encoder.finish()?;
942 Ok(uncompressed_size)
943}
944
945fn compare_resolve(
947 gctx: &GlobalContext,
948 current_pkg: &Package,
949 orig_resolve: &Resolve,
950 new_resolve: &Resolve,
951) -> CargoResult<()> {
952 if gctx.shell().verbosity() != Verbosity::Verbose {
953 return Ok(());
954 }
955 let new_set: BTreeSet<PackageId> = new_resolve.iter().collect();
956 let orig_set: BTreeSet<PackageId> = orig_resolve.iter().collect();
957 let added = new_set.difference(&orig_set);
958 let removed: Vec<&PackageId> = orig_set.difference(&new_set).collect();
961 for pkg_id in added {
962 if pkg_id.name() == current_pkg.name() && pkg_id.version() == current_pkg.version() {
963 continue;
966 }
967 let removed_candidates: Vec<&PackageId> = removed
970 .iter()
971 .filter(|orig_pkg_id| {
972 orig_pkg_id.name() == pkg_id.name() && orig_pkg_id.version() == pkg_id.version()
973 })
974 .cloned()
975 .collect();
976 let extra = match removed_candidates.len() {
977 0 => {
978 let previous_versions: Vec<&PackageId> = removed
980 .iter()
981 .filter(|orig_pkg_id| orig_pkg_id.name() == pkg_id.name())
982 .cloned()
983 .collect();
984 match previous_versions.len() {
985 0 => String::new(),
986 1 => format!(
987 ", previous version was `{}`",
988 previous_versions[0].version()
989 ),
990 _ => format!(
991 ", previous versions were: {}",
992 previous_versions
993 .iter()
994 .map(|pkg_id| format!("`{}`", pkg_id.version()))
995 .collect::<Vec<_>>()
996 .join(", ")
997 ),
998 }
999 }
1000 1 => {
1001 format!(
1005 ", was originally sourced from `{}`",
1006 removed_candidates[0].source_id()
1007 )
1008 }
1009 _ => {
1010 let comma_list = removed_candidates
1013 .iter()
1014 .map(|pkg_id| format!("`{}`", pkg_id.source_id()))
1015 .collect::<Vec<_>>()
1016 .join(", ");
1017 format!(
1018 ", was originally sourced from one of these sources: {}",
1019 comma_list
1020 )
1021 }
1022 };
1023 let msg = format!(
1024 "package `{}` added to the packaged Cargo.lock file{}",
1025 pkg_id, extra
1026 );
1027 gctx.shell().note(msg)?;
1028 }
1029 Ok(())
1030}
1031
1032pub fn check_yanked(
1033 gctx: &GlobalContext,
1034 pkg_set: &PackageSet<'_>,
1035 resolve: &Resolve,
1036 hint: &str,
1037) -> CargoResult<()> {
1038 let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
1041
1042 for (_id, source) in pkg_set.sources().iter() {
1043 source.invalidate_cache();
1044 }
1045
1046 let sources = &pkg_set.sources();
1047 let mut futures = resolve
1048 .iter()
1049 .map(|pkg_id| async move {
1050 let Some(source) = sources.get(pkg_id.source_id()) else {
1051 return CargoResult::Ok(());
1052 };
1053
1054 let mut dep = Dependency::new_override(pkg_id.name(), pkg_id.source_id());
1055 dep.set_version_req(OptVersionReq::lock_to_exact(pkg_id.version()));
1056 let mut yanked = false;
1057 source
1058 .query(&dep, QueryKind::Exact, &mut |s| {
1059 if s.is_yanked() {
1060 yanked = true;
1061 }
1062 })
1063 .await?;
1064
1065 if yanked {
1066 gctx.shell().print_report(
1067 &[Level::WARNING
1068 .secondary_title(format!(
1069 "package `{pkg_id}` in Cargo.lock is yanked in registry `{}`",
1070 pkg_id.source_id().display_registry_name(),
1071 ))
1072 .element(Level::HELP.message(hint))],
1073 false,
1074 )?;
1075 }
1076 CargoResult::Ok(())
1077 })
1078 .collect::<FuturesUnordered<_>>();
1079 crate::util::block_on(async {
1080 while futures.try_next().await?.is_some() {}
1081 CargoResult::Ok(())
1082 })
1083}
1084
1085fn check_filename(file: &Path, shell: &mut Shell) -> CargoResult<()> {
1092 let Some(name) = file.file_name() else {
1093 return Ok(());
1094 };
1095 let Some(name) = name.to_str() else {
1096 anyhow::bail!(
1097 "path does not have a unicode filename which may not unpack \
1098 on all platforms: {}",
1099 file.display()
1100 )
1101 };
1102 let bad_chars = ['/', '\\', '<', '>', ':', '"', '|', '?', '*'];
1103 if let Some(c) = bad_chars.iter().find(|c| name.contains(**c)) {
1104 anyhow::bail!(
1105 "cannot package a filename with a special character `{}`: {}",
1106 c,
1107 file.display()
1108 )
1109 }
1110 if restricted_names::is_windows_reserved_path(file) {
1111 shell.warn(format!(
1112 "file {} is a reserved Windows filename, \
1113 it will not work on Windows platforms",
1114 file.display()
1115 ))?;
1116 }
1117 Ok(())
1118}
1119
1120struct TmpRegistry<'a> {
1124 gctx: &'a GlobalContext,
1125 upstream: SourceId,
1126 root: Filesystem,
1127 checksums: HashMap<PackageId, String>,
1128 _lock: FileLock,
1129}
1130
1131impl<'a> TmpRegistry<'a> {
1132 fn new(gctx: &'a GlobalContext, root: Filesystem, upstream: SourceId) -> CargoResult<Self> {
1133 root.create_dir()?;
1134 let _lock = root.open_rw_exclusive_create(".cargo-lock", gctx, "temporary registry")?;
1135 let slf = Self {
1136 gctx,
1137 root,
1138 upstream,
1139 checksums: HashMap::new(),
1140 _lock,
1141 };
1142 let index_path = slf.index_path().into_path_unlocked();
1144 if index_path.exists() {
1145 paths::remove_dir_all(index_path)?;
1146 }
1147 slf.index_path().create_dir()?;
1148 Ok(slf)
1149 }
1150
1151 fn index_path(&self) -> Filesystem {
1152 self.root.join("index")
1153 }
1154
1155 fn add_package(
1156 &mut self,
1157 ws: &Workspace<'_>,
1158 package: &Package,
1159 tar: &FileLock,
1160 ) -> CargoResult<()> {
1161 debug!(
1162 "adding package {}@{} to local overlay at {}",
1163 package.name(),
1164 package.version(),
1165 self.root.as_path_unlocked().display()
1166 );
1167 {
1168 let mut tar_copy = self.root.open_rw_exclusive_create(
1169 package.package_id().tarball_name(),
1170 self.gctx,
1171 "temporary package registry",
1172 )?;
1173 tar_copy.file().set_len(0)?;
1174 tar.file().seek(SeekFrom::Start(0))?;
1175 std::io::copy(&mut tar.file(), &mut tar_copy)?;
1176 tar_copy.flush()?;
1177 }
1178
1179 let new_crate = super::registry::prepare_transmit(self.gctx, ws, package, self.upstream)?;
1180
1181 tar.file().seek(SeekFrom::Start(0))?;
1182 let cksum = cargo_util::Sha256::new()
1183 .update_file(tar.file())?
1184 .finish_hex();
1185
1186 self.checksums.insert(package.package_id(), cksum.clone());
1187
1188 let deps: Vec<_> = new_crate
1189 .deps
1190 .into_iter()
1191 .map(|dep| {
1192 let name = dep
1193 .explicit_name_in_toml
1194 .clone()
1195 .unwrap_or_else(|| dep.name.clone())
1196 .into();
1197 let package = dep
1198 .explicit_name_in_toml
1199 .as_ref()
1200 .map(|_| dep.name.clone().into());
1201 RegistryDependency {
1202 name: name,
1203 req: dep.version_req.into(),
1204 features: dep.features.into_iter().map(|x| x.into()).collect(),
1205 optional: dep.optional,
1206 default_features: dep.default_features,
1207 target: dep.target.map(|x| x.into()),
1208 kind: Some(dep.kind.into()),
1209 registry: dep.registry.map(|x| x.into()),
1210 package: package,
1211 public: None,
1212 artifact: dep
1213 .artifact
1214 .map(|xs| xs.into_iter().map(|x| x.into()).collect()),
1215 bindep_target: dep.bindep_target.map(|x| x.into()),
1216 lib: dep.lib,
1217 }
1218 })
1219 .collect();
1220
1221 let index_line = serde_json::to_string(&IndexPackage {
1222 name: new_crate.name.into(),
1223 vers: package.version().clone(),
1224 deps,
1225 features: new_crate
1226 .features
1227 .into_iter()
1228 .map(|(k, v)| (k.into(), v.into_iter().map(|x| x.into()).collect()))
1229 .collect(),
1230 features2: None,
1231 cksum,
1232 yanked: None,
1233 links: new_crate.links.map(|x| x.into()),
1234 rust_version: None,
1235 pubtime: None,
1236 v: Some(2),
1237 })?;
1238
1239 let file =
1240 cargo_util::registry::make_dep_path(&package.name().as_str().to_lowercase(), false);
1241 let mut dst = self.index_path().open_rw_exclusive_create(
1242 file,
1243 self.gctx,
1244 "temporary package registry",
1245 )?;
1246 dst.file().set_len(0)?;
1247 dst.write_all(index_line.as_bytes())?;
1248 Ok(())
1249 }
1250
1251 fn checksums(&self) -> impl Iterator<Item = (PackageId, &str)> {
1252 self.checksums.iter().map(|(p, s)| (*p, s.as_str()))
1253 }
1254}