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
32pub 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#[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 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 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 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 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 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 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 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 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 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 !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}