Skip to main content

cargo/ops/
lockfile.rs

1use std::io::prelude::*;
2
3use crate::core::{resolver, Resolve, ResolveVersion, Workspace};
4use crate::util::errors::{CargoResult, CargoResultExt};
5use crate::util::toml as cargo_toml;
6use crate::util::Filesystem;
7
8pub fn load_pkg_lockfile(ws: &Workspace<'_>) -> CargoResult<Option<Resolve>> {
9    if !ws.root().join("Cargo.lock").exists() {
10        return Ok(None);
11    }
12
13    let root = Filesystem::new(ws.root().to_path_buf());
14    let mut f = root.open_ro("Cargo.lock", ws.config(), "Cargo.lock file")?;
15
16    let mut s = String::new();
17    f.read_to_string(&mut s)
18        .chain_err(|| format!("failed to read file: {}", f.path().display()))?;
19
20    let resolve = (|| -> CargoResult<Option<Resolve>> {
21        let resolve: toml::Value = cargo_toml::parse(&s, f.path(), ws.config())?;
22        let v: resolver::EncodableResolve = resolve.try_into()?;
23        Ok(Some(v.into_resolve(&s, ws)?))
24    })()
25    .chain_err(|| format!("failed to parse lock file at: {}", f.path().display()))?;
26    Ok(resolve)
27}
28
29/// Generate a toml String of Cargo.lock from a Resolve.
30pub fn resolve_to_string(ws: &Workspace<'_>, resolve: &Resolve) -> CargoResult<String> {
31    let (_orig, out, _ws_root) = resolve_to_string_orig(ws, resolve)?;
32    Ok(out)
33}
34
35pub fn write_pkg_lockfile(ws: &Workspace<'_>, resolve: &Resolve) -> CargoResult<()> {
36    let (orig, out, ws_root) = resolve_to_string_orig(ws, resolve)?;
37
38    // If the lock file contents haven't changed so don't rewrite it. This is
39    // helpful on read-only filesystems.
40    if let Some(orig) = orig {
41        if are_equal_lockfiles(orig, &out, ws) {
42            return Ok(());
43        }
44    }
45
46    if !ws.config().lock_update_allowed() {
47        if ws.config().offline() {
48            anyhow::bail!("can't update in the offline mode");
49        }
50
51        let flag = if ws.config().network_allowed() {
52            "--locked"
53        } else {
54            "--frozen"
55        };
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             use the --offline flag.",
60            ws.root().to_path_buf().join("Cargo.lock").display(),
61            flag
62        );
63    }
64
65    // Ok, if that didn't work just write it out
66    ws_root
67        .open_rw("Cargo.lock", ws.config(), "Cargo.lock file")
68        .and_then(|mut f| {
69            f.file().set_len(0)?;
70            f.write_all(out.as_bytes())?;
71            Ok(())
72        })
73        .chain_err(|| format!("failed to write {}", ws.root().join("Cargo.lock").display()))?;
74    Ok(())
75}
76
77fn resolve_to_string_orig(
78    ws: &Workspace<'_>,
79    resolve: &Resolve,
80) -> CargoResult<(Option<String>, String, Filesystem)> {
81    // Load the original lock file if it exists.
82    let ws_root = Filesystem::new(ws.root().to_path_buf());
83    let orig = ws_root.open_ro("Cargo.lock", ws.config(), "Cargo.lock file");
84    let orig = orig.and_then(|mut f| {
85        let mut s = String::new();
86        f.read_to_string(&mut s)?;
87        Ok(s)
88    });
89
90    let toml = toml::Value::try_from(resolve).unwrap();
91
92    let mut out = String::new();
93
94    // At the start of the file we notify the reader that the file is generated.
95    // Specifically Phabricator ignores files containing "@generated", so we use that.
96    let marker_line = "# This file is automatically @generated by Cargo.";
97    let extra_line = "# It is not intended for manual editing.";
98    out.push_str(marker_line);
99    out.push('\n');
100    out.push_str(extra_line);
101    out.push('\n');
102    // and preserve any other top comments
103    if let Ok(orig) = &orig {
104        let mut comments = orig.lines().take_while(|line| line.starts_with('#'));
105        if let Some(first) = comments.next() {
106            if first != marker_line {
107                out.push_str(first);
108                out.push('\n');
109            }
110            if let Some(second) = comments.next() {
111                if second != extra_line {
112                    out.push_str(second);
113                    out.push('\n');
114                }
115                for line in comments {
116                    out.push_str(line);
117                    out.push('\n');
118                }
119            }
120        }
121    }
122
123    let deps = toml["package"].as_array().unwrap();
124    for dep in deps {
125        let dep = dep.as_table().unwrap();
126
127        out.push_str("[[package]]\n");
128        emit_package(dep, &mut out);
129    }
130
131    if let Some(patch) = toml.get("patch") {
132        let list = patch["unused"].as_array().unwrap();
133        for entry in list {
134            out.push_str("[[patch.unused]]\n");
135            emit_package(entry.as_table().unwrap(), &mut out);
136            out.push_str("\n");
137        }
138    }
139
140    if let Some(meta) = toml.get("metadata") {
141        out.push_str("[metadata]\n");
142        out.push_str(&meta.to_string());
143    }
144
145    // Historical versions of Cargo in the old format accidentally left trailing
146    // blank newlines at the end of files, so we just leave that as-is. For all
147    // encodings going forward, though, we want to be sure that our encoded lock
148    // file doesn't contain any trailing newlines so trim out the extra if
149    // necessary.
150    match resolve.version() {
151        ResolveVersion::V1 => {}
152        _ => {
153            while out.ends_with("\n\n") {
154                out.pop();
155            }
156        }
157    }
158
159    Ok((orig.ok(), out, ws_root))
160}
161
162fn are_equal_lockfiles(mut orig: String, current: &str, ws: &Workspace<'_>) -> bool {
163    if has_crlf_line_endings(&orig) {
164        orig = orig.replace("\r\n", "\n");
165    }
166
167    // If we want to try and avoid updating the lock file, parse both and
168    // compare them; since this is somewhat expensive, don't do it in the
169    // common case where we can update lock files.
170    if !ws.config().lock_update_allowed() {
171        let res: CargoResult<bool> = (|| {
172            let old: resolver::EncodableResolve = toml::from_str(&orig)?;
173            let new: resolver::EncodableResolve = toml::from_str(current)?;
174            Ok(old.into_resolve(&orig, ws)? == new.into_resolve(current, ws)?)
175        })();
176        if let Ok(true) = res {
177            return true;
178        }
179    }
180
181    current == orig
182}
183
184fn has_crlf_line_endings(s: &str) -> bool {
185    // Only check the first line.
186    if let Some(lf) = s.find('\n') {
187        s[..lf].ends_with('\r')
188    } else {
189        false
190    }
191}
192
193fn emit_package(dep: &toml::value::Table, out: &mut String) {
194    out.push_str(&format!("name = {}\n", &dep["name"]));
195    out.push_str(&format!("version = {}\n", &dep["version"]));
196
197    if dep.contains_key("source") {
198        out.push_str(&format!("source = {}\n", &dep["source"]));
199    }
200    if dep.contains_key("checksum") {
201        out.push_str(&format!("checksum = {}\n", &dep["checksum"]));
202    }
203
204    if let Some(s) = dep.get("dependencies") {
205        let slice = s.as_array().unwrap();
206
207        if !slice.is_empty() {
208            out.push_str("dependencies = [\n");
209
210            for child in slice.iter() {
211                out.push_str(&format!(" {},\n", child));
212            }
213
214            out.push_str("]\n");
215        }
216        out.push_str("\n");
217    } else if dep.contains_key("replace") {
218        out.push_str(&format!("replace = {}\n\n", &dep["replace"]));
219    }
220}