Skip to main content

cargo/ops/cargo_package/
mod.rs

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/// Message format for `cargo package`.
52///
53/// Currently only affect the output of the `--list` flag.
54#[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    /// Whether this packaging job is meant for a publishing dry-run.
94    ///
95    /// Packaging on its own has no side effects, so a dry-run doesn't
96    /// make sense from that point of view. But dry-run publishing needs
97    /// special packaging behavior, which this flag turns on.
98    ///
99    /// Specifically, we want dry-run packaging to work even if versions
100    /// have not yet been bumped. But then if you dry-run packaging in
101    /// a workspace with some declared versions that are already published,
102    /// the package verification step can fail with checksum mismatches.
103    /// So when dry-run is true, the verification step does some extra
104    /// checksum fudging in the lock file.
105    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    /// The relative path in the archive (not including the top-level package
113    /// name directory).
114    rel_path: PathBuf,
115    /// String variant of `rel_path`, for convenience.
116    rel_str: String,
117    /// The contents to add to the archive.
118    contents: FileContents,
119}
120
121enum FileContents {
122    /// Absolute path to the file on disk to add to the archive.
123    OnDisk(PathBuf),
124    /// Generates a file.
125    Generated(GeneratedFile),
126}
127
128enum GeneratedFile {
129    /// Generates `Cargo.toml` by rewriting the original.
130    ///
131    /// Associated path is the original manifest path.
132    Manifest(PathBuf),
133    /// Generates `Cargo.lock`.
134    ///
135    /// Associated path is the path to the original lock file, if existing.
136    Lockfile(Option<PathBuf>),
137    /// Adds a `.cargo_vcs_info.json` file if in a git repo.
138    VcsInfo(vcs::VcsInfo),
139}
140
141// Builds a tarball and places it in the output directory.
142#[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    // Check that the package dependencies are safe to deploy.
154    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    // Package up and test a temporary tarball and only move it to the final
173    // location if it actually passes all our tests. Any previously existing
174    // tarball can be assumed as corrupt or invalid, so we just blow it away if
175    // it exists.
176    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    // It doesn't really matter if this fails.
193    drop(gctx.shell().status("Packaged", message));
194
195    return Ok(dst);
196}
197
198/// Packages an entire workspace.
199///
200/// Returns the generated package files. If `opts.list` is true, skips
201/// generating package files and returns an empty list.
202pub fn package(ws: &Workspace<'_>, opts: &PackageOpts<'_>) -> CargoResult<Vec<FileLock>> {
203    let specs = &opts.to_package.to_package_id_specs(ws)?;
204    // If -p is used, we should check spec is matched with the members (See #13719)
205    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    // In `members_with_features_old`, it will add "current" package (determined by the cwd)
214    // So we need filter
215    pkgs.retain(|(pkg, _feats)| specs.iter().any(|spec| spec.matches(pkg.package_id())));
216
217    let packaged = do_package(ws, opts, pkgs)?;
218
219    // Uplifting artifacts
220    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
236/// Packages an entire workspace.
237///
238/// Returns the generated package files and the dependencies between them. If
239/// `opts.list` is true, skips generating package files and returns an empty
240/// list.
241pub(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        // Make sure the Cargo.lock is up-to-date and valid.
266        let dry_run = false;
267        let _ = ops::resolve_ws(ws, dry_run)?;
268        // If Cargo.lock does not exist, it will be generated by `build_lock`
269        // below, and will be validated during the verification step.
270    }
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    // The publish registry doesn't matter unless there are local dependencies that will be
276    // resolved,
277    // so only try to get one if we need it.
278    //
279    // If they explicitly passed a registry on the CLI, we check it no matter what to verify
280    // `package.publish`.
281    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    // Packages need to be created in dependency order, because dependencies must
294    // be added to our local overlay before we can create lockfiles that depend on them.
295    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                    // While this form is called "human",
309                    // it keeps the old file-per-line format for compatibility.
310                    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    // Verify all packages in the workspace. This can be done in any order, since the dependencies
346    // are already all in the local registry overlay.
347    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
357/// Determine which registry the packages are for.
358///
359/// The registry only affects the built packages if there are dependencies within the
360/// packages that we're packaging: if we're packaging foo-bin and foo-lib, and foo-bin
361/// depends on foo-lib, then the foo-lib entry in foo-bin's lockfile will depend on the
362/// registry that we're building packages for.
363fn 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    // Validate the registry against the packages' allow-lists.
374    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 (i.e. package.publish is false), we let it slide.
381                // This allows packaging unpublishable packages (although packaging might
382                // fail later if the unpublishable package is a dependency of something else).
383                if !allowed.is_empty() && !allowed.iter().any(|a| a == &reg_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/// Just the part of the dependency graph that's between the packages we're packaging.
398#[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
420/// Build just the part of the dependency graph that's between the given packages,
421/// ignoring dev dependencies.
422///
423/// We assume that the packages all belong to this workspace.
424fn 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    // Dependencies have source ids but not package ids. We draw an edge
430    // whenever a dependency's source id matches one of our packages. This is
431    // wrong in general because it doesn't require (e.g.) versions to match. But
432    // since we're working only with path dependencies here, it should be fine.
433    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            // We're only interested in local (i.e. living in this workspace) dependencies.
443            if !dep.source_id().is_path() {
444                continue;
445            }
446
447            // If local dev-dependencies don't have a version specified, they get stripped
448            // on publish so we should ignore them.
449            if dep.kind() == DepKind::Development && !dep.specified_req() {
450                continue;
451            };
452
453            // We don't care about cycles
454            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/// Performs pre-archiving checks and builds a list of files to archive.
468#[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    // Check (git) repository state, getting the current commit hash.
491    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/// Builds list of files to archive.
496#[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    // Ensure we normalize for case insensitive filesystems (like on Windows) by removing the
532    // existing entry, regardless of case, and adding in with the correct case
533    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            // The file exists somewhere outside of the package.
665            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
741/// Construct `Cargo.lock` for the package to be published.
742fn 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    // The local registry is an overlay used for simulating workspace packages
754    // that are supposed to be in the published registry, but that aren't there
755    // yet.
756    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
802// Checks that the package has some piece of metadata that a human can
803// use to tell what the package is about.
804fn 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        // Only warn if publishing to crates.io based on resolved registry
830        let should_warn = match reg_or_index {
831            Some(RegistryOrIndex::Registry(reg_name)) => reg_name == CRATES_IO_REGISTRY,
832            None => true,                             // Default is crates.io
833            Some(RegistryOrIndex::Index(_)) => false, // Custom index, not crates.io
834        };
835
836        if should_warn {
837            let mut things = missing[..missing.len() - 1].join(", ");
838            // `things` will be empty if and only if its length is 1 (i.e., the only case
839            // to have no `or`).
840            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
857/// Compresses and packages a list of [`ArchiveFile`]s and writes into the given file.
858///
859/// Returns the uncompressed size of the contents of the new archive file.
860fn 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    // Prepare the encoder and its header.
870    let filename = Path::new(filename);
871    let encoder = GzBuilder::new()
872        .filename(paths::path2bytes(filename)?)
873        .write(dst, Compression::best());
874
875    // Put all package files into a compressed archive.
876    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                // We need to have the same DETERMINISTIC_TIMESTAMP for generated files
927                // https://github.com/alexcrichton/tar-rs/blob/d0261f1f6cc959ba0758e7236b3fd81e90dd1dc6/src/header.rs#L18-L24
928                // Unfortunately tar-rs doesn't expose that so we hardcode the timestamp here.
929                // Hardcoded value be removed once alexcrichton/tar-rs#420 is merged and released.
930                // See also rust-lang/cargo#16237
931                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
945/// Generate warnings when packaging Cargo.lock, and the resolve have changed.
946fn 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    // Removed entries are ignored, this is used to quickly find hints for why
959    // an entry changed.
960    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            // Skip the package that is being created, since its SourceId
964            // (directory) changes.
965            continue;
966        }
967        // Check for candidates where the source has changed (such as [patch]
968        // or a dependency with multiple sources like path/version).
969        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                // This can happen if the original was out of date.
979                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                // This can happen for multi-sourced dependencies like
1002                // `{path="...", version="..."}` or `[patch]` replacement.
1003                // `[replace]` is not captured in Cargo.lock.
1004                format!(
1005                    ", was originally sourced from `{}`",
1006                    removed_candidates[0].source_id()
1007                )
1008            }
1009            _ => {
1010                // I don't know if there is a way to actually trigger this,
1011                // but handle it just in case.
1012                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    // Checking the yanked status involves taking a look at the registry and
1039    // maybe updating files, so be sure to lock it here.
1040    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
1085// It can often be the case that files of a particular name on one platform
1086// can't actually be created on another platform. For example files with colons
1087// in the name are allowed on Unix but not on Windows.
1088//
1089// To help out in situations like this, issue about weird filenames when
1090// packaging as a "heads up" that something may not work on other platforms.
1091fn 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
1120/// Manages a temporary local registry that we use to overlay our new packages on the
1121/// upstream registry. This way we can build lockfiles that depend on the new packages even
1122/// before they're published.
1123struct 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        // If there's an old temporary registry, delete it.
1143        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}