#
cargo/ops/cargo_package/
verify.rs

    1//! Helpers to verify a packaged `.crate` file.
    2
    3use std::collections::HashMap;
    4use std::fs;
    5use std::fs::File;
    6use std::io::prelude::*;
    7use std::io::SeekFrom;
    8use std::path::Path;
    9use std::path::PathBuf;
    10use std::sync::Arc;
    11
    12use anyhow::Context as _;
    13use cargo_util::paths;
    14use flate2::read::GzDecoder;
    15use tar::Archive;
    16
    17use crate::core::compiler::BuildConfig;
    18use crate::core::compiler::DefaultExecutor;
    19use crate::core::compiler::Executor;
    20use crate::core::compiler::UserIntent;
    21use crate::core::Feature;
    22use crate::core::Package;
    23use crate::core::SourceId;
    24use crate::core::Workspace;
    25use crate::ops;
    26use crate::sources::PathSource;
    27use crate::util;
    28use crate::util::FileLock;
    29use crate::CargoResult;
    30
    31use super::PackageOpts;
    32use super::TmpRegistry;
    33
    34/// Verifies whether a `.crate` file is able to compile.
    35pub fn run_verify(
    36    ws: &Workspace<'_>,
    37    pkg: &Package,
    38    tar: &FileLock,
    39    local_reg: Option<&TmpRegistry<'_>>,
    40    opts: &PackageOpts<'_>,
    41) -> CargoResult<()> {
    42    let gctx = ws.gctx();
    43
    44    gctx.shell().status("Verifying", pkg)?;
    45
    46    tar.file().seek(SeekFrom::Start(0))?;
    47    let f = GzDecoder::new(tar.file());
    48    let dst = ws.build_dir().as_path_unlocked().join(&format!(
    49        "package/{}-{}",
    50        pkg.name(),
    51        pkg.version()
    52    ));
    53    if dst.exists() {
    54        paths::remove_dir_all(&dst)?;
    55    }
    56    let mut archive = Archive::new(f);
    57    // We don't need to set the Modified Time, as it's not relevant to verification
    58    // and it errors on filesystems that don't support setting a modified timestamp
    59    archive.set_preserve_mtime(false);
    60    archive.unpack(dst.parent().unwrap())?;
    61
    62    // Manufacture an ephemeral workspace to ensure that even if the top-level
    63    // package has a workspace we can still build our new crate.
    64    let id = SourceId::for_path(&dst)?;
    65    let mut src = PathSource::new(&dst, id, ws.gctx());
    66    let new_pkg = src.root_package()?;
    67    let pkg_fingerprint = hash_all(&dst)?;
    68
    69    let target_dir = if gctx.cli_unstable().build_dir {
    70        Some(ws.build_dir())
    71    } else {
    72        None
    73    };
    74
    75    let mut ws = Workspace::ephemeral(new_pkg, gctx, target_dir, true)?;
    76    if let Some(local_reg) = local_reg {
    77        ws.add_local_overlay(
    78            local_reg.upstream,
    79            local_reg.root.as_path_unlocked().to_owned(),
    80        );
    81    }
    82
    83    let rustc_args = if pkg
    84        .manifest()
    85        .unstable_features()
    86        .require(Feature::public_dependency())
    87        .is_ok()
    88        || ws.gctx().cli_unstable().public_dependency
    89    {
    90        // FIXME: Turn this on at some point in the future
    91        //Some(vec!["-D exported_private_dependencies".to_string()])
    92        Some(vec![])
    93    } else {
    94        None
    95    };
    96
    97    let exec: Arc<dyn Executor> = Arc::new(DefaultExecutor);
    98    ops::compile_with_exec(
    99        &ws,
    100        &ops::CompileOptions {
    101            build_config: BuildConfig::new(
    102                gctx,
    103                opts.jobs.clone(),
    104                opts.keep_going,
    105                &opts.targets,
    106                UserIntent::Build,
    107            )?,
    108            cli_features: opts.cli_features.clone(),
    109            spec: ops::Packages::Packages(Vec::new()),
    110            filter: ops::CompileFilter::Default {
    111                required_features_filterable: true,
    112            },
    113            target_rustdoc_args: None,
    114            target_rustc_args: rustc_args,
    115            target_rustc_crate_types: None,
    116            rustdoc_document_private_items: false,
    117            honor_rust_version: None,
    118        },
    119        &exec,
    120    )?;
    121
    122    // Check that `build.rs` didn't modify any files in the `src` directory.
    123    let ws_fingerprint = hash_all(&dst)?;
    124    if pkg_fingerprint != ws_fingerprint {
    125        let changes = report_hash_difference(&pkg_fingerprint, &ws_fingerprint);
    126        anyhow::bail!(
    127            "Source directory was modified by build.rs during cargo publish. \
    128             Build scripts should not modify anything outside of OUT_DIR.\n\
    129             {}\n\n\
    130             To proceed despite this, pass the `--no-verify` flag.",
    131            changes
    132        )
    133    }
    134
    135    Ok(())
    136}
    137
    138/// Hashes everything under a given directory.
    139///
    140/// This is for checking if any source file inside a `.crate` file has changed
    141/// durint the compilation. It is usually caused by bad build scripts or proc
    142/// macros trying to modify source files. Cargo disallows that.
    143fn hash_all(path: &Path) -> CargoResult<HashMap<PathBuf, u64>> {
    144    fn wrap(path: &Path) -> CargoResult<HashMap<PathBuf, u64>> {
    145        let mut result = HashMap::new();
    146        let walker = walkdir::WalkDir::new(path).into_iter();
    147        for entry in walker.filter_entry(|e| !(e.depth() == 1 && e.file_name() == "target")) {
    148            let entry = entry?;
    149            let file_type = entry.file_type();
    150            if file_type.is_file() {
    151                let file = File::open(entry.path())?;
    152                let hash = util::hex::hash_u64_file(&file)?;
    153                result.insert(entry.path().to_path_buf(), hash);
    154            } else if file_type.is_symlink() {
    155                let hash = util::hex::hash_u64(&fs::read_link(entry.path())?);
    156                result.insert(entry.path().to_path_buf(), hash);
    157            } else if file_type.is_dir() {
    158                let hash = util::hex::hash_u64(&());
    159                result.insert(entry.path().to_path_buf(), hash);
    160            }
    161        }
    162        Ok(result)
    163    }
    164    let result = wrap(path).with_context(|| format!("failed to verify output at {:?}", path))?;
    165    Ok(result)
    166}
    167
    168/// Reports the hash difference before and after the compilation computed by  [`hash_all`].
    169fn report_hash_difference(orig: &HashMap<PathBuf, u64>, after: &HashMap<PathBuf, u64>) -> String {
    170    let mut changed = Vec::new();
    171    let mut removed = Vec::new();
    172    for (key, value) in orig {
    173        match after.get(key) {
    174            Some(after_value) => {
    175                if value != after_value {
    176                    changed.push(key.to_string_lossy());
    177                }
    178            }
    179            None => removed.push(key.to_string_lossy()),
    180        }
    181    }
    182    let mut added: Vec<_> = after
    183        .keys()
    184        .filter(|key| !orig.contains_key(*key))
    185        .map(|key| key.to_string_lossy())
    186        .collect();
    187    let mut result = Vec::new();
    188    if !changed.is_empty() {
    189        changed.sort_unstable();
    190        result.push(format!("Changed: {}", changed.join("\n\t")));
    191    }
    192    if !added.is_empty() {
    193        added.sort_unstable();
    194        result.push(format!("Added: {}", added.join("\n\t")));
    195    }
    196    if !removed.is_empty() {
    197        removed.sort_unstable();
    198        result.push(format!("Removed: {}", removed.join("\n\t")));
    199    }
    200    assert!(!result.is_empty(), "unexpected empty change detection");
    201    result.join("\n")
    202}