Skip to main content

cargo/ops/
lockfile.rs

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