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