Skip to main content

cargo/ops/cargo_package/
mod.rs

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