#
cargo/ops/
lockfile.rs

    1use std::io::prelude::*;
    2
    3use crate::core::{Resolve, ResolveVersion, Workspace, resolver};
    4use crate::util::Filesystem;
    5use crate::util::errors::CargoResult;
    6
    7use anyhow::Context as _;
    8
    9pub const LOCKFILE_NAME: &str = "Cargo.lock";
    10
    11#[tracing::instrument(skip_all)]
    12pub fn load_pkg_lockfile(ws: &Workspace<'_>) -> CargoResult<Option<Resolve>> {
    13    let lock_root = ws.lock_root();
    14    if !lock_root.as_path_unlocked().join(LOCKFILE_NAME).exists() {
    15        return Ok(None);
    16    }
    17
    18    let mut f = lock_root.open_ro_shared(LOCKFILE_NAME, ws.gctx(), "Cargo.lock file")?;
    19
    20    let mut s = String::new();
    21    f.read_to_string(&mut s)
    22        .with_context(|| format!("failed to read file: {}", f.path().display()))?;
    23
    24    let resolve = (|| -> CargoResult<Option<Resolve>> {
    25        let v: resolver::EncodableResolve = toml::from_str(&s)?;
    26        Ok(Some(v.into_resolve(&s, ws)?))
    27    })()
    28    .with_context(|| format!("failed to parse lock file at: {}", f.path().display()))?;
    29    Ok(resolve)
    30}
    31
    32/// Generate a toml String of Cargo.lock from a Resolve.
    33pub fn resolve_to_string(ws: &Workspace<'_>, resolve: &Resolve) -> CargoResult<String> {
    34    let (_orig, out, _lock_root) = resolve_to_string_orig(ws, resolve);
    35    Ok(out)
    36}
    37
    38/// Ensure the resolve result is written to fisk
    39///
    40/// Returns `true` if the lockfile changed
    41#[tracing::instrument(skip_all)]
    42pub fn write_pkg_lockfile(ws: &Workspace<'_>, resolve: &mut Resolve) -> CargoResult<bool> {
    43    let (orig, mut out, lock_root) = resolve_to_string_orig(ws, resolve);
    44
    45    // If the lock file contents haven't changed so don't rewrite it. This is
    46    // helpful on read-only filesystems.
    47    if let Some(orig) = &orig {
    48        if are_equal_lockfiles(orig, &out, ws) {
    49            return Ok(false);
    50        }
    51    }
    52
    53    if let Some(locked_flag) = ws.gctx().locked_flag() {
    54        anyhow::bail!(
    55            "the lock file {} needs to be updated but {} was passed to prevent this\n\
    56             If you want to try to generate the lock file without accessing the network, \
    57             remove the {} flag and use --offline instead.",
    58            lock_root.as_path_unlocked().join(LOCKFILE_NAME).display(),
    59            locked_flag,
    60            locked_flag
    61        );
    62    }
    63
    64    // While we're updating the lock file anyway go ahead and update its
    65    // encoding to whatever the latest default is. That way we can slowly roll
    66    // out lock file updates as they're otherwise already updated, and changes
    67    // which don't touch dependencies won't seemingly spuriously update the lock
    68    // file.
    69    let default_version = ResolveVersion::with_rust_version(ws.lowest_rust_version());
    70    let current_version = resolve.version();
    71    let next_lockfile_bump = ws.gctx().cli_unstable().next_lockfile_bump;
    72    tracing::debug!("lockfile - current: {current_version:?}, default: {default_version:?}");
    73
    74    if current_version < default_version {
    75        resolve.set_version(default_version);
    76        out = serialize_resolve(resolve, orig.as_deref());
    77    } else if current_version > ResolveVersion::max_stable() && !next_lockfile_bump {
    78        // The next version hasn't yet stabilized.
    79        anyhow::bail!("lock file version `{current_version:?}` requires `-Znext-lockfile-bump`")
    80    }
    81
    82    if !lock_root.as_path_unlocked().exists() {
    83        lock_root.create_dir()?;
    84    }
    85
    86    // Ok, if that didn't work just write it out
    87    lock_root
    88        .open_rw_exclusive_create(LOCKFILE_NAME, ws.gctx(), "Cargo.lock file")
    89        .and_then(|mut f| {
    90            f.file().set_len(0)?;
    91            f.write_all(out.as_bytes())?;
    92            Ok(())
    93        })
    94        .with_context(|| {
    95            format!(
    96                "failed to write {}",
    97                lock_root.as_path_unlocked().join(LOCKFILE_NAME).display()
    98            )
    99        })?;
    100    Ok(true)
    101}
    102
    103fn resolve_to_string_orig(
    104    ws: &Workspace<'_>,
    105    resolve: &Resolve,
    106) -> (Option<String>, String, Filesystem) {
    107    // Load the original lock file if it exists.
    108    let lock_root = ws.lock_root();
    109    let orig = lock_root.open_ro_shared(LOCKFILE_NAME, ws.gctx(), "Cargo.lock file");
    110    let orig = orig.and_then(|mut f| {
    111        let mut s = String::new();
    112        f.read_to_string(&mut s)?;
    113        Ok(s)
    114    });
    115    let out = serialize_resolve(resolve, orig.as_deref().ok());
    116    (orig.ok(), out, lock_root)
    117}
    118
    119#[tracing::instrument(skip_all)]
    120fn serialize_resolve(resolve: &Resolve, orig: Option<&str>) -> String {
    121    let toml = toml::Table::try_from(resolve).unwrap();
    122
    123    let mut out = String::new();
    124
    125    // At the start of the file we notify the reader that the file is generated.
    126    // Specifically Phabricator ignores files containing "@generated", so we use that.
    127    let marker_line = "# This file is automatically @generated by Cargo.";
    128    let extra_line = "# It is not intended for manual editing.";
    129    out.push_str(marker_line);
    130    out.push('\n');
    131    out.push_str(extra_line);
    132    out.push('\n');
    133    // and preserve any other top comments
    134    if let Some(orig) = orig {
    135        let mut comments = orig.lines().take_while(|line| line.starts_with('#'));
    136        if let Some(first) = comments.next() {
    137            if first != marker_line {
    138                out.push_str(first);
    139                out.push('\n');
    140            }
    141            if let Some(second) = comments.next() {
    142                if second != extra_line {
    143                    out.push_str(second);
    144                    out.push('\n');
    145                }
    146                for line in comments {
    147                    out.push_str(line);
    148                    out.push('\n');
    149                }
    150            }
    151        }
    152    }
    153
    154    if let Some(version) = toml.get("version") {
    155        out.push_str(&format!("version = {}\n\n", version));
    156    }
    157
    158    let deps = toml["package"].as_array().unwrap();
    159    for dep in deps {
    160        let dep = dep.as_table().unwrap();
    161
    162        out.push_str("[[package]]\n");
    163        emit_package(dep, &mut out);
    164    }
    165
    166    if let Some(patch) = toml.get("patch") {
    167        let list = patch["unused"].as_array().unwrap();
    168        for entry in list {
    169            out.push_str("[[patch.unused]]\n");
    170            emit_package(entry.as_table().unwrap(), &mut out);
    171            out.push('\n');
    172        }
    173    }
    174
    175    if let Some(meta) = toml.get("metadata") {
    176        // 1. We need to ensure we print the entire tree, not just the direct members of `metadata`
    177        //    (which `toml_edit::Table::to_string` only shows)
    178        // 2. We need to ensure all children tables have `metadata.` prefix
    179        let meta_table = meta
    180            .as_table()
    181            .expect("validation ensures this is a table")
    182            .clone();
    183        let mut meta_doc = toml::Table::new();
    184        meta_doc.insert("metadata".to_owned(), toml::Value::Table(meta_table));
    185
    186        out.push_str(&meta_doc.to_string());
    187    }
    188
    189    // Historical versions of Cargo in the old format accidentally left trailing
    190    // blank newlines at the end of files, so we just leave that as-is. For all
    191    // encodings going forward, though, we want to be sure that our encoded lock
    192    // file doesn't contain any trailing newlines so trim out the extra if
    193    // necessary.
    194    if resolve.version() >= ResolveVersion::V2 {
    195        while out.ends_with("\n\n") {
    196            out.pop();
    197        }
    198    }
    199    out
    200}
    201
    202#[tracing::instrument(skip_all)]
    203fn are_equal_lockfiles(orig: &str, current: &str, ws: &Workspace<'_>) -> bool {
    204    // If we want to try and avoid updating the lock file, parse both and
    205    // compare them; since this is somewhat expensive, don't do it in the
    206    // common case where we can update lock files.
    207    if !ws.gctx().lock_update_allowed() {
    208        let res: CargoResult<bool> = (|| {
    209            let old: resolver::EncodableResolve = toml::from_str(orig)?;
    210            let new: resolver::EncodableResolve = toml::from_str(current)?;
    211            Ok(old.into_resolve(orig, ws)? == new.into_resolve(current, ws)?)
    212        })();
    213        if let Ok(true) = res {
    214            return true;
    215        }
    216    }
    217
    218    orig.lines().eq(current.lines())
    219}
    220
    221fn emit_package(dep: &toml::Table, out: &mut String) {
    222    out.push_str(&format!("name = {}\n", &dep["name"]));
    223    out.push_str(&format!("version = {}\n", &dep["version"]));
    224
    225    if dep.contains_key("source") {
    226        out.push_str(&format!("source = {}\n", &dep["source"]));
    227    }
    228    if dep.contains_key("checksum") {
    229        out.push_str(&format!("checksum = {}\n", &dep["checksum"]));
    230    }
    231
    232    if let Some(s) = dep.get("dependencies") {
    233        let slice = s.as_array().unwrap();
    234
    235        if !slice.is_empty() {
    236            out.push_str("dependencies = [\n");
    237
    238            for child in slice.iter() {
    239                out.push_str(&format!(" {},\n", child));
    240            }
    241
    242            out.push_str("]\n");
    243        }
    244        out.push('\n');
    245    } else if dep.contains_key("replace") {
    246        out.push_str(&format!("replace = {}\n\n", &dep["replace"]));
    247    }
    248}