cargo/ops/
lockfile.rs

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