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
36pub 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#[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 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 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 anyhow::bail!("lock file version `{current_version:?}` requires `-Znext-lockfile-bump`")
88 }
89
90 if !lock_root.as_path_unlocked().exists() {
91 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 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 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 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 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 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 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 if orig.lines().eq(current.lines()) {
224 return true;
225 }
226
227 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}