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