#
cargo/ops/cargo_package/
mod.rs

    1use std::collections::BTreeMap;
    2use std::collections::BTreeSet;
    3use std::collections::HashMap;
    4use std::fs::{self, File};
    5use std::io::prelude::*;
    6use std::io::SeekFrom;
    7use std::path::{Path, PathBuf};
    8use std::task::Poll;
    9
    10use crate::core::dependency::DepKind;
    11use crate::core::manifest::Target;
    12use crate::core::resolver::CliFeatures;
    13use crate::core::resolver::HasDevUnits;
    14use crate::core::PackageIdSpecQuery;
    15use crate::core::Shell;
    16use crate::core::Verbosity;
    17use crate::core::Workspace;
    18use crate::core::{Package, PackageId, PackageSet, Resolve, SourceId};
    19use crate::ops::lockfile::LOCKFILE_NAME;
    20use crate::ops::registry::{infer_registry, RegistryOrIndex};
    21use crate::sources::path::PathEntry;
    22use crate::sources::registry::index::{IndexPackage, RegistryDependency};
    23use crate::sources::{PathSource, CRATES_IO_REGISTRY};
    24use crate::util::cache_lock::CacheLockMode;
    25use crate::util::context::JobsConfig;
    26use crate::util::errors::CargoResult;
    27use crate::util::restricted_names;
    28use crate::util::toml::prepare_for_publish;
    29use crate::util::FileLock;
    30use crate::util::Filesystem;
    31use crate::util::GlobalContext;
    32use crate::util::Graph;
    33use crate::util::HumanBytes;
    34use crate::{drop_println, ops};
    35use anyhow::{bail, Context as _};
    36use cargo_util::paths;
    37use cargo_util_schemas::messages;
    38use flate2::{Compression, GzBuilder};
    39use tar::{Builder, EntryType, Header, HeaderMode};
    40use tracing::debug;
    41use unicase::Ascii as UncasedAscii;
    42
    43mod vcs;
    44mod verify;
    45
    46/// Message format for `cargo package`.
    47///
    48/// Currently only affect the output of the `--list` flag.
    49#[derive(Debug, Clone)]
    50pub enum PackageMessageFormat {
    51    Human,
    52    Json,
    53}
    54
    55impl PackageMessageFormat {
    56    pub const POSSIBLE_VALUES: [&str; 2] = ["human", "json"];
    57
    58    pub const DEFAULT: &str = "human";
    59}
    60
    61impl std::str::FromStr for PackageMessageFormat {
    62    type Err = anyhow::Error;
    63
    64    fn from_str(s: &str) -> Result<PackageMessageFormat, anyhow::Error> {
    65        match s {
    66            "human" => Ok(PackageMessageFormat::Human),
    67            "json" => Ok(PackageMessageFormat::Json),
    68            f => bail!("unknown message format `{f}`"),
    69        }
    70    }
    71}
    72
    73#[derive(Clone)]
    74pub struct PackageOpts<'gctx> {
    75    pub gctx: &'gctx GlobalContext,
    76    pub list: bool,
    77    pub fmt: PackageMessageFormat,
    78    pub check_metadata: bool,
    79    pub allow_dirty: bool,
    80    pub include_lockfile: bool,
    81    pub verify: bool,
    82    pub jobs: Option<JobsConfig>,
    83    pub keep_going: bool,
    84    pub to_package: ops::Packages,
    85    pub targets: Vec<String>,
    86    pub cli_features: CliFeatures,
    87    pub reg_or_index: Option<ops::RegistryOrIndex>,
    88}
    89
    90const ORIGINAL_MANIFEST_FILE: &str = "Cargo.toml.orig";
    91const VCS_INFO_FILE: &str = ".cargo_vcs_info.json";
    92
    93struct ArchiveFile {
    94    /// The relative path in the archive (not including the top-level package
    95    /// name directory).
    96    rel_path: PathBuf,
    97    /// String variant of `rel_path`, for convenience.
    98    rel_str: String,
    99    /// The contents to add to the archive.
    100    contents: FileContents,
    101}
    102
    103enum FileContents {
    104    /// Absolute path to the file on disk to add to the archive.
    105    OnDisk(PathBuf),
    106    /// Generates a file.
    107    Generated(GeneratedFile),
    108}
    109
    110enum GeneratedFile {
    111    /// Generates `Cargo.toml` by rewriting the original.
    112    ///
    113    /// Associated path is the original manifest path.
    114    Manifest(PathBuf),
    115    /// Generates `Cargo.lock`.
    116    ///
    117    /// Associated path is the path to the original lock file, if existing.
    118    Lockfile(Option<PathBuf>),
    119    /// Adds a `.cargo_vcs_info.json` file if in a git repo.
    120    VcsInfo(vcs::VcsInfo),
    121}
    122
    123// Builds a tarball and places it in the output directory.
    124#[tracing::instrument(skip_all)]
    125fn create_package(
    126    ws: &Workspace<'_>,
    127    pkg: &Package,
    128    ar_files: Vec<ArchiveFile>,
    129    local_reg: Option<&TmpRegistry<'_>>,
    130) -> CargoResult<FileLock> {
    131    let gctx = ws.gctx();
    132    let filecount = ar_files.len();
    133
    134    // Check that the package dependencies are safe to deploy.
    135    for dep in pkg.dependencies() {
    136        super::check_dep_has_version(dep, false)?;
    137    }
    138
    139    let filename = pkg.package_id().tarball_name();
    140    let dir = ws.target_dir().join("package");
    141    let mut dst = {
    142        let tmp = format!(".{}", filename);
    143        dir.open_rw_exclusive_create(&tmp, gctx, "package scratch space")?
    144    };
    145
    146    // Package up and test a temporary tarball and only move it to the final
    147    // location if it actually passes all our tests. Any previously existing
    148    // tarball can be assumed as corrupt or invalid, so we just blow it away if
    149    // it exists.
    150    gctx.shell()
    151        .status("Packaging", pkg.package_id().to_string())?;
    152    dst.file().set_len(0)?;
    153    let uncompressed_size = tar(ws, pkg, local_reg, ar_files, dst.file(), &filename)
    154        .context("failed to prepare local package for uploading")?;
    155
    156    dst.seek(SeekFrom::Start(0))?;
    157    let src_path = dst.path();
    158    let dst_path = dst.parent().join(&filename);
    159    fs::rename(&src_path, &dst_path)
    160        .context("failed to move temporary tarball into final location")?;
    161
    162    let dst_metadata = dst
    163        .file()
    164        .metadata()
    165        .with_context(|| format!("could not learn metadata for: `{}`", dst_path.display()))?;
    166    let compressed_size = dst_metadata.len();
    167
    168    let uncompressed = HumanBytes(uncompressed_size);
    169    let compressed = HumanBytes(compressed_size);
    170
    171    let message = format!("{filecount} files, {uncompressed:.1} ({compressed:.1} compressed)");
    172    // It doesn't really matter if this fails.
    173    drop(gctx.shell().status("Packaged", message));
    174
    175    return Ok(dst);
    176}
    177
    178/// Packages an entire workspace.
    179///
    180/// Returns the generated package files. If `opts.list` is true, skips
    181/// generating package files and returns an empty list.
    182pub fn package(ws: &Workspace<'_>, opts: &PackageOpts<'_>) -> CargoResult<Vec<FileLock>> {
    183    let specs = &opts.to_package.to_package_id_specs(ws)?;
    184    // If -p is used, we should check spec is matched with the members (See #13719)
    185    if let ops::Packages::Packages(_) = opts.to_package {
    186        for spec in specs.iter() {
    187            let member_ids = ws.members().map(|p| p.package_id());
    188            spec.query(member_ids)?;
    189        }
    190    }
    191    let mut pkgs = ws.members_with_features(specs, &opts.cli_features)?;
    192
    193    // In `members_with_features_old`, it will add "current" package (determined by the cwd)
    194    // So we need filter
    195    pkgs.retain(|(pkg, _feats)| specs.iter().any(|spec| spec.matches(pkg.package_id())));
    196
    197    Ok(do_package(ws, opts, pkgs)?
    198        .into_iter()
    199        .map(|x| x.2)
    200        .collect())
    201}
    202
    203/// Packages an entire workspace.
    204///
    205/// Returns the generated package files and the dependencies between them. If
    206/// `opts.list` is true, skips generating package files and returns an empty
    207/// list.
    208pub(crate) fn package_with_dep_graph(
    209    ws: &Workspace<'_>,
    210    opts: &PackageOpts<'_>,
    211    pkgs: Vec<(&Package, CliFeatures)>,
    212) -> CargoResult<LocalDependencies<(CliFeatures, FileLock)>> {
    213    let output = do_package(ws, opts, pkgs)?;
    214
    215    Ok(local_deps(output.into_iter().map(
    216        |(pkg, opts, tarball)| (pkg, (opts.cli_features, tarball)),
    217    )))
    218}
    219
    220fn do_package<'a>(
    221    ws: &Workspace<'_>,
    222    opts: &PackageOpts<'a>,
    223    pkgs: Vec<(&Package, CliFeatures)>,
    224) -> CargoResult<Vec<(Package, PackageOpts<'a>, FileLock)>> {
    225    if ws
    226        .lock_root()
    227        .as_path_unlocked()
    228        .join(LOCKFILE_NAME)
    229        .exists()
    230        && opts.include_lockfile
    231    {
    232        // Make sure the Cargo.lock is up-to-date and valid.
    233        let dry_run = false;
    234        let _ = ops::resolve_ws(ws, dry_run)?;
    235        // If Cargo.lock does not exist, it will be generated by `build_lock`
    236        // below, and will be validated during the verification step.
    237    }
    238
    239    let deps = local_deps(pkgs.iter().map(|(p, f)| ((*p).clone(), f.clone())));
    240    let just_pkgs: Vec<_> = pkgs.iter().map(|p| p.0).collect();
    241
    242    let mut local_reg = if ws.gctx().cli_unstable().package_workspace {
    243        // The publish registry doesn't matter unless there are local dependencies that will be
    244        // resolved,
    245        // so only try to get one if we need it. If they explicitly passed a
    246        // registry on the CLI, we check it no matter what.
    247        let sid = if (deps.has_dependencies() && (opts.include_lockfile || opts.verify))
    248            || opts.reg_or_index.is_some()
    249        {
    250            let sid = get_registry(ws.gctx(), &just_pkgs, opts.reg_or_index.clone())?;
    251            debug!("packaging for registry {}", sid);
    252            Some(sid)
    253        } else {
    254            None
    255        };
    256        let reg_dir = ws.build_dir().join("package").join("tmp-registry");
    257        sid.map(|sid| TmpRegistry::new(ws.gctx(), reg_dir, sid))
    258            .transpose()?
    259    } else {
    260        None
    261    };
    262
    263    // Packages need to be created in dependency order, because dependencies must
    264    // be added to our local overlay before we can create lockfiles that depend on them.
    265    let sorted_pkgs = deps.sort();
    266    let mut outputs: Vec<(Package, PackageOpts<'_>, FileLock)> = Vec::new();
    267    for (pkg, cli_features) in sorted_pkgs {
    268        let opts = PackageOpts {
    269            cli_features: cli_features.clone(),
    270            to_package: ops::Packages::Default,
    271            ..opts.clone()
    272        };
    273        let ar_files = prepare_archive(ws, &pkg, &opts)?;
    274
    275        if opts.list {
    276            match opts.fmt {
    277                PackageMessageFormat::Human => {
    278                    // While this form is called "human",
    279                    // it keeps the old file-per-line format for compatibility.
    280                    for ar_file in &ar_files {
    281                        drop_println!(ws.gctx(), "{}", ar_file.rel_str);
    282                    }
    283                }
    284                PackageMessageFormat::Json => {
    285                    let message = messages::PackageList {
    286                        id: pkg.package_id().to_spec(),
    287                        files: BTreeMap::from_iter(ar_files.into_iter().map(|f| {
    288                            let file = match f.contents {
    289                                FileContents::OnDisk(path) => messages::PackageFile::Copy { path },
    290                                FileContents::Generated(
    291                                    GeneratedFile::Manifest(path)
    292                                    | GeneratedFile::Lockfile(Some(path)),
    293                                ) => messages::PackageFile::Generate { path: Some(path) },
    294                                FileContents::Generated(
    295                                    GeneratedFile::VcsInfo(_) | GeneratedFile::Lockfile(None),
    296                                ) => messages::PackageFile::Generate { path: None },
    297                            };
    298                            (f.rel_path, file)
    299                        })),
    300                    };
    301                    let _ = ws.gctx().shell().print_json(&message);
    302                }
    303            }
    304        } else {
    305            let tarball = create_package(ws, &pkg, ar_files, local_reg.as_ref())?;
    306            if let Some(local_reg) = local_reg.as_mut() {
    307                if pkg.publish() != &Some(Vec::new()) {
    308                    local_reg.add_package(ws, &pkg, &tarball)?;
    309                }
    310            }
    311            outputs.push((pkg, opts, tarball));
    312        }
    313    }
    314
    315    // Verify all packages in the workspace. This can be done in any order, since the dependencies
    316    // are already all in the local registry overlay.
    317    if opts.verify {
    318        for (pkg, opts, tarball) in &outputs {
    319            verify::run_verify(ws, pkg, tarball, local_reg.as_ref(), opts)
    320                .context("failed to verify package tarball")?
    321        }
    322    }
    323
    324    Ok(outputs)
    325}
    326
    327/// Determine which registry the packages are for.
    328///
    329/// The registry only affects the built packages if there are dependencies within the
    330/// packages that we're packaging: if we're packaging foo-bin and foo-lib, and foo-bin
    331/// depends on foo-lib, then the foo-lib entry in foo-bin's lockfile will depend on the
    332/// registry that we're building packages for.
    333fn get_registry(
    334    gctx: &GlobalContext,
    335    pkgs: &[&Package],
    336    reg_or_index: Option<RegistryOrIndex>,
    337) -> CargoResult<SourceId> {
    338    let reg_or_index = match reg_or_index.clone() {
    339        Some(r) => Some(r),
    340        None => infer_registry(pkgs)?,
    341    };
    342
    343    // Validate the registry against the packages' allow-lists.
    344    let reg = reg_or_index
    345        .clone()
    346        .unwrap_or_else(|| RegistryOrIndex::Registry(CRATES_IO_REGISTRY.to_owned()));
    347    if let RegistryOrIndex::Registry(reg_name) = reg {
    348        for pkg in pkgs {
    349            if let Some(allowed) = pkg.publish().as_ref() {
    350                // If allowed is empty (i.e. package.publish is false), we let it slide.
    351                // This allows packaging unpublishable packages (although packaging might
    352                // fail later if the unpublishable package is a dependency of something else).
    353                if !allowed.is_empty() && !allowed.iter().any(|a| a == &reg_name) {
    354                    bail!(
    355                        "`{}` cannot be packaged.\n\
    356                         The registry `{}` is not listed in the `package.publish` value in Cargo.toml.",
    357                        pkg.name(),
    358                        reg_name
    359                    );
    360                }
    361            }
    362        }
    363    }
    364    Ok(ops::registry::get_source_id(gctx, reg_or_index.as_ref())?.replacement)
    365}
    366
    367/// Just the part of the dependency graph that's between the packages we're packaging.
    368#[derive(Clone, Debug, Default)]
    369pub(crate) struct LocalDependencies<T> {
    370    pub packages: HashMap<PackageId, (Package, T)>,
    371    pub graph: Graph<PackageId, ()>,
    372}
    373
    374impl<T: Clone> LocalDependencies<T> {
    375    pub fn sort(&self) -> Vec<(Package, T)> {
    376        self.graph
    377            .sort()
    378            .into_iter()
    379            .map(|name| self.packages[&name].clone())
    380            .collect()
    381    }
    382
    383    pub fn has_dependencies(&self) -> bool {
    384        self.graph
    385            .iter()
    386            .any(|node| self.graph.edges(node).next().is_some())
    387    }
    388}
    389
    390/// Build just the part of the dependency graph that's between the given packages,
    391/// ignoring dev dependencies.
    392///
    393/// We assume that the packages all belong to this workspace.
    394fn local_deps<T>(packages: impl Iterator<Item = (Package, T)>) -> LocalDependencies<T> {
    395    let packages: HashMap<PackageId, (Package, T)> = packages
    396        .map(|(pkg, payload)| (pkg.package_id(), (pkg, payload)))
    397        .collect();
    398
    399    // Dependencies have source ids but not package ids. We draw an edge
    400    // whenever a dependency's source id matches one of our packages. This is
    401    // wrong in general because it doesn't require (e.g.) versions to match. But
    402    // since we're working only with path dependencies here, it should be fine.
    403    let source_to_pkg: HashMap<_, _> = packages
    404        .keys()
    405        .map(|pkg_id| (pkg_id.source_id(), *pkg_id))
    406        .collect();
    407
    408    let mut graph = Graph::new();
    409    for (pkg, _payload) in packages.values() {
    410        graph.add(pkg.package_id());
    411        for dep in pkg.dependencies() {
    412            // We're only interested in local (i.e. living in this workspace) dependencies.
    413            if !dep.source_id().is_path() {
    414                continue;
    415            }
    416
    417            // If local dev-dependencies don't have a version specified, they get stripped
    418            // on publish so we should ignore them.
    419            if dep.kind() == DepKind::Development && !dep.specified_req() {
    420                continue;
    421            };
    422
    423            // We don't care about cycles
    424            if dep.source_id() == pkg.package_id().source_id() {
    425                continue;
    426            }
    427
    428            if let Some(dep_pkg) = source_to_pkg.get(&dep.source_id()) {
    429                graph.link(pkg.package_id(), *dep_pkg);
    430            }
    431        }
    432    }
    433
    434    LocalDependencies { packages, graph }
    435}
    436
    437/// Performs pre-archiving checks and builds a list of files to archive.
    438#[tracing::instrument(skip_all)]
    439fn prepare_archive(
    440    ws: &Workspace<'_>,
    441    pkg: &Package,
    442    opts: &PackageOpts<'_>,
    443) -> CargoResult<Vec<ArchiveFile>> {
    444    let gctx = ws.gctx();
    445    let mut src = PathSource::new(pkg.root(), pkg.package_id().source_id(), gctx);
    446    src.load()?;
    447
    448    if opts.check_metadata {
    449        check_metadata(pkg, gctx)?;
    450    }
    451
    452    if !pkg.manifest().exclude().is_empty() && !pkg.manifest().include().is_empty() {
    453        gctx.shell().warn(
    454            "both package.include and package.exclude are specified; \
    455             the exclude list will be ignored",
    456        )?;
    457    }
    458    let src_files = src.list_files(pkg)?;
    459
    460    // Check (git) repository state, getting the current commit hash.
    461    let vcs_info = vcs::check_repo_state(pkg, &src_files, ws, &opts)?;
    462
    463    build_ar_list(ws, pkg, src_files, vcs_info, opts.include_lockfile)
    464}
    465
    466/// Builds list of files to archive.
    467#[tracing::instrument(skip_all)]
    468fn build_ar_list(
    469    ws: &Workspace<'_>,
    470    pkg: &Package,
    471    src_files: Vec<PathEntry>,
    472    vcs_info: Option<vcs::VcsInfo>,
    473    include_lockfile: bool,
    474) -> CargoResult<Vec<ArchiveFile>> {
    475    let mut result = HashMap::new();
    476    let root = pkg.root();
    477    for src_file in &src_files {
    478        let rel_path = src_file.strip_prefix(&root)?;
    479        check_filename(rel_path, &mut ws.gctx().shell())?;
    480        let rel_str = rel_path.to_str().ok_or_else(|| {
    481            anyhow::format_err!("non-utf8 path in source directory: {}", rel_path.display())
    482        })?;
    483        match rel_str {
    484            "Cargo.lock" => continue,
    485            VCS_INFO_FILE | ORIGINAL_MANIFEST_FILE => anyhow::bail!(
    486                "invalid inclusion of reserved file name {} in package source",
    487                rel_str
    488            ),
    489            _ => {
    490                result
    491                    .entry(UncasedAscii::new(rel_str))
    492                    .or_insert_with(Vec::new)
    493                    .push(ArchiveFile {
    494                        rel_path: rel_path.to_owned(),
    495                        rel_str: rel_str.to_owned(),
    496                        contents: FileContents::OnDisk(src_file.to_path_buf()),
    497                    });
    498            }
    499        }
    500    }
    501
    502    // Ensure we normalize for case insensitive filesystems (like on Windows) by removing the
    503    // existing entry, regardless of case, and adding in with the correct case
    504    if result.remove(&UncasedAscii::new("Cargo.toml")).is_some() {
    505        result
    506            .entry(UncasedAscii::new(ORIGINAL_MANIFEST_FILE))
    507            .or_insert_with(Vec::new)
    508            .push(ArchiveFile {
    509                rel_path: PathBuf::from(ORIGINAL_MANIFEST_FILE),
    510                rel_str: ORIGINAL_MANIFEST_FILE.to_string(),
    511                contents: FileContents::OnDisk(pkg.manifest_path().to_owned()),
    512            });
    513        result
    514            .entry(UncasedAscii::new("Cargo.toml"))
    515            .or_insert_with(Vec::new)
    516            .push(ArchiveFile {
    517                rel_path: PathBuf::from("Cargo.toml"),
    518                rel_str: "Cargo.toml".to_string(),
    519                contents: FileContents::Generated(GeneratedFile::Manifest(
    520                    pkg.manifest_path().to_owned(),
    521                )),
    522            });
    523    } else {
    524        ws.gctx().shell().warn(&format!(
    525            "no `Cargo.toml` file found when packaging `{}` (note the case of the file name).",
    526            pkg.name()
    527        ))?;
    528    }
    529
    530    if include_lockfile {
    531        let lockfile_path = ws.lock_root().as_path_unlocked().join(LOCKFILE_NAME);
    532        let lockfile_path = lockfile_path.exists().then_some(lockfile_path);
    533        let rel_str = "Cargo.lock";
    534        result
    535            .entry(UncasedAscii::new(rel_str))
    536            .or_insert_with(Vec::new)
    537            .push(ArchiveFile {
    538                rel_path: PathBuf::from(rel_str),
    539                rel_str: rel_str.to_string(),
    540                contents: FileContents::Generated(GeneratedFile::Lockfile(lockfile_path)),
    541            });
    542    }
    543
    544    if let Some(vcs_info) = vcs_info {
    545        let rel_str = VCS_INFO_FILE;
    546        result
    547            .entry(UncasedAscii::new(rel_str))
    548            .or_insert_with(Vec::new)
    549            .push(ArchiveFile {
    550                rel_path: PathBuf::from(rel_str),
    551                rel_str: rel_str.to_string(),
    552                contents: FileContents::Generated(GeneratedFile::VcsInfo(vcs_info)),
    553            });
    554    }
    555
    556    let mut invalid_manifest_field: Vec<String> = vec![];
    557
    558    let mut result = result.into_values().flatten().collect();
    559    if let Some(license_file) = &pkg.manifest().metadata().license_file {
    560        let license_path = Path::new(license_file);
    561        let abs_file_path = paths::normalize_path(&pkg.root().join(license_path));
    562        if abs_file_path.is_file() {
    563            check_for_file_and_add(
    564                "license-file",
    565                license_path,
    566                abs_file_path,
    567                pkg,
    568                &mut result,
    569                ws,
    570            )?;
    571        } else {
    572            error_on_nonexistent_file(
    573                &pkg,
    574                &license_path,
    575                "license-file",
    576                &mut invalid_manifest_field,
    577            );
    578        }
    579    }
    580    if let Some(readme) = &pkg.manifest().metadata().readme {
    581        let readme_path = Path::new(readme);
    582        let abs_file_path = paths::normalize_path(&pkg.root().join(readme_path));
    583        if abs_file_path.is_file() {
    584            check_for_file_and_add("readme", readme_path, abs_file_path, pkg, &mut result, ws)?;
    585        } else {
    586            error_on_nonexistent_file(&pkg, &readme_path, "readme", &mut invalid_manifest_field);
    587        }
    588    }
    589
    590    if !invalid_manifest_field.is_empty() {
    591        return Err(anyhow::anyhow!(invalid_manifest_field.join("\n")));
    592    }
    593
    594    for t in pkg
    595        .manifest()
    596        .targets()
    597        .iter()
    598        .filter(|t| t.is_custom_build())
    599    {
    600        if let Some(custome_build_path) = t.src_path().path() {
    601            let abs_custome_build_path =
    602                paths::normalize_path(&pkg.root().join(custome_build_path));
    603            if !abs_custome_build_path.is_file() || !abs_custome_build_path.starts_with(pkg.root())
    604            {
    605                error_custom_build_file_not_in_package(pkg, &abs_custome_build_path, t)?;
    606            }
    607        }
    608    }
    609
    610    result.sort_unstable_by(|a, b| a.rel_path.cmp(&b.rel_path));
    611
    612    Ok(result)
    613}
    614
    615fn check_for_file_and_add(
    616    label: &str,
    617    file_path: &Path,
    618    abs_file_path: PathBuf,
    619    pkg: &Package,
    620    result: &mut Vec<ArchiveFile>,
    621    ws: &Workspace<'_>,
    622) -> CargoResult<()> {
    623    match abs_file_path.strip_prefix(&pkg.root()) {
    624        Ok(rel_file_path) => {
    625            if !result.iter().any(|ar| ar.rel_path == rel_file_path) {
    626                result.push(ArchiveFile {
    627                    rel_path: rel_file_path.to_path_buf(),
    628                    rel_str: rel_file_path
    629                        .to_str()
    630                        .expect("everything was utf8")
    631                        .to_string(),
    632                    contents: FileContents::OnDisk(abs_file_path),
    633                })
    634            }
    635        }
    636        Err(_) => {
    637            // The file exists somewhere outside of the package.
    638            let file_name = file_path.file_name().unwrap();
    639            if result.iter().any(|ar| ar.rel_path == file_name) {
    640                ws.gctx().shell().warn(&format!(
    641                    "{} `{}` appears to be a path outside of the package, \
    642                            but there is already a file named `{}` in the root of the package. \
    643                            The archived crate will contain the copy in the root of the package. \
    644                            Update the {} to point to the path relative \
    645                            to the root of the package to remove this warning.",
    646                    label,
    647                    file_path.display(),
    648                    file_name.to_str().unwrap(),
    649                    label,
    650                ))?;
    651            } else {
    652                result.push(ArchiveFile {
    653                    rel_path: PathBuf::from(file_name),
    654                    rel_str: file_name.to_str().unwrap().to_string(),
    655                    contents: FileContents::OnDisk(abs_file_path),
    656                })
    657            }
    658        }
    659    }
    660    Ok(())
    661}
    662
    663fn error_on_nonexistent_file(
    664    pkg: &Package,
    665    path: &Path,
    666    manifest_key_name: &'static str,
    667    invalid: &mut Vec<String>,
    668) {
    669    let rel_msg = if path.is_absolute() {
    670        "".to_string()
    671    } else {
    672        format!(" (relative to `{}`)", pkg.root().display())
    673    };
    674
    675    let msg = format!(
    676        "{manifest_key_name} `{}` does not appear to exist{}.\n\
    677                Please update the {manifest_key_name} setting in the manifest at `{}`.",
    678        path.display(),
    679        rel_msg,
    680        pkg.manifest_path().display()
    681    );
    682
    683    invalid.push(msg);
    684}
    685
    686fn error_custom_build_file_not_in_package(
    687    pkg: &Package,
    688    path: &Path,
    689    target: &Target,
    690) -> CargoResult<Vec<ArchiveFile>> {
    691    let tip = {
    692        let description_name = target.description_named();
    693        if path.is_file() {
    694            format!("the source file of {description_name} doesn't appear to be a path inside of the package.\n\
    695            It is at `{}`, whereas the root the package is `{}`.\n",
    696            path.display(), pkg.root().display()
    697            )
    698        } else {
    699            format!("the source file of {description_name} doesn't appear to exist.\n",)
    700        }
    701    };
    702    let msg = format!(
    703        "{}\
    704        This may cause issue during packaging, as modules resolution and resources included via macros are often relative to the path of source files.\n\
    705        Please update the `build` setting in the manifest at `{}` and point to a path inside the root of the package.",
    706        tip,  pkg.manifest_path().display()
    707    );
    708    anyhow::bail!(msg)
    709}
    710
    711/// Construct `Cargo.lock` for the package to be published.
    712fn build_lock(
    713    ws: &Workspace<'_>,
    714    publish_pkg: &Package,
    715    local_reg: Option<&TmpRegistry<'_>>,
    716) -> CargoResult<String> {
    717    let gctx = ws.gctx();
    718    let orig_resolve = ops::load_pkg_lockfile(ws)?;
    719
    720    let mut tmp_ws = Workspace::ephemeral(publish_pkg.clone(), ws.gctx(), None, true)?;
    721
    722    // The local registry is an overlay used for simulating workspace packages
    723    // that are supposed to be in the published registry, but that aren't there
    724    // yet.
    725    if let Some(local_reg) = local_reg {
    726        tmp_ws.add_local_overlay(
    727            local_reg.upstream,
    728            local_reg.root.as_path_unlocked().to_owned(),
    729        );
    730    }
    731    let mut tmp_reg = tmp_ws.package_registry()?;
    732
    733    let mut new_resolve = ops::resolve_with_previous(
    734        &mut tmp_reg,
    735        &tmp_ws,
    736        &CliFeatures::new_all(true),
    737        HasDevUnits::Yes,
    738        orig_resolve.as_ref(),
    739        None,
    740        &[],
    741        true,
    742    )?;
    743
    744    let pkg_set = ops::get_resolved_packages(&new_resolve, tmp_reg)?;
    745
    746    if let Some(orig_resolve) = orig_resolve {
    747        compare_resolve(gctx, tmp_ws.current()?, &orig_resolve, &new_resolve)?;
    748    }
    749    check_yanked(
    750        gctx,
    751        &pkg_set,
    752        &new_resolve,
    753        "consider updating to a version that is not yanked",
    754    )?;
    755
    756    ops::resolve_to_string(&tmp_ws, &mut new_resolve)
    757}
    758
    759// Checks that the package has some piece of metadata that a human can
    760// use to tell what the package is about.
    761fn check_metadata(pkg: &Package, gctx: &GlobalContext) -> CargoResult<()> {
    762    let md = pkg.manifest().metadata();
    763
    764    let mut missing = vec![];
    765
    766    macro_rules! lacking {
    767        ($( $($field: ident)||* ),*) => {{
    768            $(
    769                if $(md.$field.as_ref().map_or(true, |s| s.is_empty()))&&* {
    770                    $(missing.push(stringify!($field).replace("_", "-"));)*
    771                }
    772            )*
    773        }}
    774    }
    775    lacking!(
    776        description,
    777        license || license_file,
    778        documentation || homepage || repository
    779    );
    780
    781    if !missing.is_empty() {
    782        let mut things = missing[..missing.len() - 1].join(", ");
    783        // `things` will be empty if and only if its length is 1 (i.e., the only case
    784        // to have no `or`).
    785        if !things.is_empty() {
    786            things.push_str(" or ");
    787        }
    788        things.push_str(missing.last().unwrap());
    789
    790        gctx.shell().warn(&format!(
    791            "manifest has no {things}.\n\
    792             See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.",
    793            things = things
    794        ))?
    795    }
    796
    797    Ok(())
    798}
    799
    800/// Compresses and packages a list of [`ArchiveFile`]s and writes into the given file.
    801///
    802/// Returns the uncompressed size of the contents of the new archive file.
    803fn tar(
    804    ws: &Workspace<'_>,
    805    pkg: &Package,
    806    local_reg: Option<&TmpRegistry<'_>>,
    807    ar_files: Vec<ArchiveFile>,
    808    dst: &File,
    809    filename: &str,
    810) -> CargoResult<u64> {
    811    // Prepare the encoder and its header.
    812    let filename = Path::new(filename);
    813    let encoder = GzBuilder::new()
    814        .filename(paths::path2bytes(filename)?)
    815        .write(dst, Compression::best());
    816
    817    // Put all package files into a compressed archive.
    818    let mut ar = Builder::new(encoder);
    819    ar.sparse(false);
    820    let gctx = ws.gctx();
    821
    822    let base_name = format!("{}-{}", pkg.name(), pkg.version());
    823    let base_path = Path::new(&base_name);
    824    let included = ar_files
    825        .iter()
    826        .map(|ar_file| ar_file.rel_path.clone())
    827        .collect::<Vec<_>>();
    828    let publish_pkg = prepare_for_publish(pkg, ws, Some(&included))?;
    829
    830    let mut uncompressed_size = 0;
    831    for ar_file in ar_files {
    832        let ArchiveFile {
    833            rel_path,
    834            rel_str,
    835            contents,
    836        } = ar_file;
    837        let ar_path = base_path.join(&rel_path);
    838        gctx.shell()
    839            .verbose(|shell| shell.status("Archiving", &rel_str))?;
    840        let mut header = Header::new_gnu();
    841        match contents {
    842            FileContents::OnDisk(disk_path) => {
    843                let mut file = File::open(&disk_path).with_context(|| {
    844                    format!("failed to open for archiving: `{}`", disk_path.display())
    845                })?;
    846                let metadata = file.metadata().with_context(|| {
    847                    format!("could not learn metadata for: `{}`", disk_path.display())
    848                })?;
    849                header.set_metadata_in_mode(&metadata, HeaderMode::Deterministic);
    850                header.set_cksum();
    851                ar.append_data(&mut header, &ar_path, &mut file)
    852                    .with_context(|| {
    853                        format!("could not archive source file `{}`", disk_path.display())
    854                    })?;
    855                uncompressed_size += metadata.len() as u64;
    856            }
    857            FileContents::Generated(generated_kind) => {
    858                let contents = match generated_kind {
    859                    GeneratedFile::Manifest(_) => {
    860                        publish_pkg.manifest().to_normalized_contents()?
    861                    }
    862                    GeneratedFile::Lockfile(_) => build_lock(ws, &publish_pkg, local_reg)?,
    863                    GeneratedFile::VcsInfo(ref s) => serde_json::to_string_pretty(s)?,
    864                };
    865                header.set_entry_type(EntryType::file());
    866                header.set_mode(0o644);
    867                header.set_size(contents.len() as u64);
    868                // use something nonzero to avoid rust-lang/cargo#9512
    869                header.set_mtime(1);
    870                header.set_cksum();
    871                ar.append_data(&mut header, &ar_path, contents.as_bytes())
    872                    .with_context(|| format!("could not archive source file `{}`", rel_str))?;
    873                uncompressed_size += contents.len() as u64;
    874            }
    875        }
    876    }
    877
    878    let encoder = ar.into_inner()?;
    879    encoder.finish()?;
    880    Ok(uncompressed_size)
    881}
    882
    883/// Generate warnings when packaging Cargo.lock, and the resolve have changed.
    884fn compare_resolve(
    885    gctx: &GlobalContext,
    886    current_pkg: &Package,
    887    orig_resolve: &Resolve,
    888    new_resolve: &Resolve,
    889) -> CargoResult<()> {
    890    if gctx.shell().verbosity() != Verbosity::Verbose {
    891        return Ok(());
    892    }
    893    let new_set: BTreeSet<PackageId> = new_resolve.iter().collect();
    894    let orig_set: BTreeSet<PackageId> = orig_resolve.iter().collect();
    895    let added = new_set.difference(&orig_set);
    896    // Removed entries are ignored, this is used to quickly find hints for why
    897    // an entry changed.
    898    let removed: Vec<&PackageId> = orig_set.difference(&new_set).collect();
    899    for pkg_id in added {
    900        if pkg_id.name() == current_pkg.name() && pkg_id.version() == current_pkg.version() {
    901            // Skip the package that is being created, since its SourceId
    902            // (directory) changes.
    903            continue;
    904        }
    905        // Check for candidates where the source has changed (such as [patch]
    906        // or a dependency with multiple sources like path/version).
    907        let removed_candidates: Vec<&PackageId> = removed
    908            .iter()
    909            .filter(|orig_pkg_id| {
    910                orig_pkg_id.name() == pkg_id.name() && orig_pkg_id.version() == pkg_id.version()
    911            })
    912            .cloned()
    913            .collect();
    914        let extra = match removed_candidates.len() {
    915            0 => {
    916                // This can happen if the original was out of date.
    917                let previous_versions: Vec<&PackageId> = removed
    918                    .iter()
    919                    .filter(|orig_pkg_id| orig_pkg_id.name() == pkg_id.name())
    920                    .cloned()
    921                    .collect();
    922                match previous_versions.len() {
    923                    0 => String::new(),
    924                    1 => format!(
    925                        ", previous version was `{}`",
    926                        previous_versions[0].version()
    927                    ),
    928                    _ => format!(
    929                        ", previous versions were: {}",
    930                        previous_versions
    931                            .iter()
    932                            .map(|pkg_id| format!("`{}`", pkg_id.version()))
    933                            .collect::<Vec<_>>()
    934                            .join(", ")
    935                    ),
    936                }
    937            }
    938            1 => {
    939                // This can happen for multi-sourced dependencies like
    940                // `{path="...", version="..."}` or `[patch]` replacement.
    941                // `[replace]` is not captured in Cargo.lock.
    942                format!(
    943                    ", was originally sourced from `{}`",
    944                    removed_candidates[0].source_id()
    945                )
    946            }
    947            _ => {
    948                // I don't know if there is a way to actually trigger this,
    949                // but handle it just in case.
    950                let comma_list = removed_candidates
    951                    .iter()
    952                    .map(|pkg_id| format!("`{}`", pkg_id.source_id()))
    953                    .collect::<Vec<_>>()
    954                    .join(", ");
    955                format!(
    956                    ", was originally sourced from one of these sources: {}",
    957                    comma_list
    958                )
    959            }
    960        };
    961        let msg = format!(
    962            "package `{}` added to the packaged Cargo.lock file{}",
    963            pkg_id, extra
    964        );
    965        gctx.shell().note(msg)?;
    966    }
    967    Ok(())
    968}
    969
    970pub fn check_yanked(
    971    gctx: &GlobalContext,
    972    pkg_set: &PackageSet<'_>,
    973    resolve: &Resolve,
    974    hint: &str,
    975) -> CargoResult<()> {
    976    // Checking the yanked status involves taking a look at the registry and
    977    // maybe updating files, so be sure to lock it here.
    978    let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?;
    979
    980    let mut sources = pkg_set.sources_mut();
    981    let mut pending: Vec<PackageId> = resolve.iter().collect();
    982    let mut results = Vec::new();
    983    for (_id, source) in sources.sources_mut() {
    984        source.invalidate_cache();
    985    }
    986    while !pending.is_empty() {
    987        pending.retain(|pkg_id| {
    988            if let Some(source) = sources.get_mut(pkg_id.source_id()) {
    989                match source.is_yanked(*pkg_id) {
    990                    Poll::Ready(result) => results.push((*pkg_id, result)),
    991                    Poll::Pending => return true,
    992                }
    993            }
    994            false
    995        });
    996        for (_id, source) in sources.sources_mut() {
    997            source.block_until_ready()?;
    998        }
    999    }
    1000
    1001    for (pkg_id, is_yanked) in results {
    1002        if is_yanked? {
    1003            gctx.shell().warn(format!(
    1004                "package `{}` in Cargo.lock is yanked in registry `{}`, {}",
    1005                pkg_id,
    1006                pkg_id.source_id().display_registry_name(),
    1007                hint
    1008            ))?;
    1009        }
    1010    }
    1011    Ok(())
    1012}
    1013
    1014// It can often be the case that files of a particular name on one platform
    1015// can't actually be created on another platform. For example files with colons
    1016// in the name are allowed on Unix but not on Windows.
    1017//
    1018// To help out in situations like this, issue about weird filenames when
    1019// packaging as a "heads up" that something may not work on other platforms.
    1020fn check_filename(file: &Path, shell: &mut Shell) -> CargoResult<()> {
    1021    let Some(name) = file.file_name() else {
    1022        return Ok(());
    1023    };
    1024    let Some(name) = name.to_str() else {
    1025        anyhow::bail!(
    1026            "path does not have a unicode filename which may not unpack \
    1027             on all platforms: {}",
    1028            file.display()
    1029        )
    1030    };
    1031    let bad_chars = ['/', '\\', '<', '>', ':', '"', '|', '?', '*'];
    1032    if let Some(c) = bad_chars.iter().find(|c| name.contains(**c)) {
    1033        anyhow::bail!(
    1034            "cannot package a filename with a special character `{}`: {}",
    1035            c,
    1036            file.display()
    1037        )
    1038    }
    1039    if restricted_names::is_windows_reserved_path(file) {
    1040        shell.warn(format!(
    1041            "file {} is a reserved Windows filename, \
    1042                it will not work on Windows platforms",
    1043            file.display()
    1044        ))?;
    1045    }
    1046    Ok(())
    1047}
    1048
    1049/// Manages a temporary local registry that we use to overlay our new packages on the
    1050/// upstream registry. This way we can build lockfiles that depend on the new packages even
    1051/// before they're published.
    1052struct TmpRegistry<'a> {
    1053    gctx: &'a GlobalContext,
    1054    upstream: SourceId,
    1055    root: Filesystem,
    1056    _lock: FileLock,
    1057}
    1058
    1059impl<'a> TmpRegistry<'a> {
    1060    fn new(gctx: &'a GlobalContext, root: Filesystem, upstream: SourceId) -> CargoResult<Self> {
    1061        root.create_dir()?;
    1062        let _lock = root.open_rw_exclusive_create(".cargo-lock", gctx, "temporary registry")?;
    1063        let slf = Self {
    1064            gctx,
    1065            root,
    1066            upstream,
    1067            _lock,
    1068        };
    1069        // If there's an old temporary registry, delete it.
    1070        let index_path = slf.index_path().into_path_unlocked();
    1071        if index_path.exists() {
    1072            paths::remove_dir_all(index_path)?;
    1073        }
    1074        slf.index_path().create_dir()?;
    1075        Ok(slf)
    1076    }
    1077
    1078    fn index_path(&self) -> Filesystem {
    1079        self.root.join("index")
    1080    }
    1081
    1082    fn add_package(
    1083        &mut self,
    1084        ws: &Workspace<'_>,
    1085        package: &Package,
    1086        tar: &FileLock,
    1087    ) -> CargoResult<()> {
    1088        debug!(
    1089            "adding package {}@{} to local overlay at {}",
    1090            package.name(),
    1091            package.version(),
    1092            self.root.as_path_unlocked().display()
    1093        );
    1094        {
    1095            let mut tar_copy = self.root.open_rw_exclusive_create(
    1096                package.package_id().tarball_name(),
    1097                self.gctx,
    1098                "temporary package registry",
    1099            )?;
    1100            tar.file().seek(SeekFrom::Start(0))?;
    1101            std::io::copy(&mut tar.file(), &mut tar_copy)?;
    1102            tar_copy.flush()?;
    1103        }
    1104
    1105        let new_crate = super::registry::prepare_transmit(self.gctx, ws, package, self.upstream)?;
    1106
    1107        tar.file().seek(SeekFrom::Start(0))?;
    1108        let cksum = cargo_util::Sha256::new()
    1109            .update_file(tar.file())?
    1110            .finish_hex();
    1111
    1112        let deps: Vec<_> = new_crate
    1113            .deps
    1114            .into_iter()
    1115            .map(|dep| {
    1116                let name = dep
    1117                    .explicit_name_in_toml
    1118                    .clone()
    1119                    .unwrap_or_else(|| dep.name.clone())
    1120                    .into();
    1121                let package = dep
    1122                    .explicit_name_in_toml
    1123                    .as_ref()
    1124                    .map(|_| dep.name.clone().into());
    1125                RegistryDependency {
    1126                    name: name,
    1127                    req: dep.version_req.into(),
    1128                    features: dep.features.into_iter().map(|x| x.into()).collect(),
    1129                    optional: dep.optional,
    1130                    default_features: dep.default_features,
    1131                    target: dep.target.map(|x| x.into()),
    1132                    kind: Some(dep.kind.into()),
    1133                    registry: dep.registry.map(|x| x.into()),
    1134                    package: package,
    1135                    public: None,
    1136                    artifact: dep
    1137                        .artifact
    1138                        .map(|xs| xs.into_iter().map(|x| x.into()).collect()),
    1139                    bindep_target: dep.bindep_target.map(|x| x.into()),
    1140                    lib: dep.lib,
    1141                }
    1142            })
    1143            .collect();
    1144
    1145        let index_line = serde_json::to_string(&IndexPackage {
    1146            name: new_crate.name.into(),
    1147            vers: package.version().clone(),
    1148            deps,
    1149            features: new_crate
    1150                .features
    1151                .into_iter()
    1152                .map(|(k, v)| (k.into(), v.into_iter().map(|x| x.into()).collect()))
    1153                .collect(),
    1154            features2: None,
    1155            cksum,
    1156            yanked: None,
    1157            links: new_crate.links.map(|x| x.into()),
    1158            rust_version: None,
    1159            v: Some(2),
    1160        })?;
    1161
    1162        let file =
    1163            cargo_util::registry::make_dep_path(&package.name().as_str().to_lowercase(), false);
    1164        let mut dst = self.index_path().open_rw_exclusive_create(
    1165            file,
    1166            self.gctx,
    1167            "temporary package registry",
    1168        )?;
    1169        dst.write_all(index_line.as_bytes())?;
    1170        Ok(())
    1171    }
    1172}