#
cargo/ops/
lockfile.rs

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