1use std::io::prelude::*;
2
3use crate::core::resolver::encode::into_resolve;
4use crate::core::{Resolve, ResolveVersion, Workspace};
5use crate::util::Filesystem;
6use crate::util::errors::CargoResult;
7
8use anyhow::Context as _;
9use cargo_util_schemas::lockfile::TomlLockfile;
10
11pub const LOCKFILE_NAME: &str = "Cargo.lock";
12
13#[tracing::instrument(skip_all)]
14pub fn load_pkg_lockfile(ws: &Workspace<'_>) -> CargoResult<Option<Resolve>> {
15 let lock_root = ws.lock_root();
16 if !lock_root.as_path_unlocked().join(LOCKFILE_NAME).exists() {
17 return Ok(None);
18 }
19
20 let mut f = lock_root.open_ro_shared(LOCKFILE_NAME, ws.gctx(), "Cargo.lock file")?;
21
22 let mut s = String::new();
23 f.read_to_string(&mut s)
24 .with_context(|| format!("failed to read file: {}", f.path().display()))?;
25
26 let resolve = (|| -> CargoResult<Option<Resolve>> {
27 let v: TomlLockfile = toml::from_str(&s)?;
28 Ok(Some(into_resolve(v, &s, ws)?))
29 })()
30 .with_context(|| format!("failed to parse lock file at: {}", f.path().display()))?;
31 Ok(resolve)
32}
33
34pub fn resolve_to_string(ws: &Workspace<'_>, resolve: &Resolve) -> CargoResult<String> {
36 let (_orig, out, _lock_root) = resolve_to_string_orig(ws, resolve);
37 Ok(out)
38}
39
40#[tracing::instrument(skip_all)]
44pub fn write_pkg_lockfile(ws: &Workspace<'_>, resolve: &mut Resolve) -> CargoResult<bool> {
45 let (orig, mut out, lock_root) = resolve_to_string_orig(ws, resolve);
46
47 if let Some(orig) = &orig {
50 if are_equal_lockfiles(orig, &out, ws) {
51 return Ok(false);
52 }
53 }
54
55 if let Some(locked_flag) = ws.gctx().locked_flag() {
56 let lockfile_path = lock_root.as_path_unlocked().join(LOCKFILE_NAME);
57 let action = if lockfile_path.exists() {
58 "update"
59 } else {
60 "create"
61 };
62 let lockfile_path = lockfile_path.display();
63 anyhow::bail!(
64 "cannot {action} the lock file {lockfile_path} because {locked_flag} was passed to prevent this\n\
65 help: to generate the lock file without accessing the network, \
66 remove the {locked_flag} flag and use --offline instead."
67 );
68 }
69
70 let default_version = ResolveVersion::with_rust_version(ws.lowest_rust_version());
76 let current_version = resolve.version();
77 let next_lockfile_bump = ws.gctx().cli_unstable().next_lockfile_bump;
78 tracing::debug!("lockfile - current: {current_version:?}, default: {default_version:?}");
79
80 if current_version < default_version {
81 resolve.set_version(default_version);
82 out = serialize_resolve(resolve, orig.as_deref());
83 } else if current_version > ResolveVersion::max_stable() && !next_lockfile_bump {
84 anyhow::bail!("lock file version `{current_version:?}` requires `-Znext-lockfile-bump`")
86 }
87
88 if !lock_root.as_path_unlocked().exists() {
89 lock_root.create_dir()?;
90 }
91
92 lock_root
94 .open_rw_exclusive_create(LOCKFILE_NAME, ws.gctx(), "Cargo.lock file")
95 .and_then(|mut f| {
96 f.file().set_len(0)?;
97 f.write_all(out.as_bytes())?;
98 Ok(())
99 })
100 .with_context(|| {
101 format!(
102 "failed to write {}",
103 lock_root.as_path_unlocked().join(LOCKFILE_NAME).display()
104 )
105 })?;
106 Ok(true)
107}
108
109fn resolve_to_string_orig(
110 ws: &Workspace<'_>,
111 resolve: &Resolve,
112) -> (Option<String>, String, Filesystem) {
113 let lock_root = ws.lock_root();
115 let orig = lock_root.open_ro_shared(LOCKFILE_NAME, ws.gctx(), "Cargo.lock file");
116 let orig = orig.and_then(|mut f| {
117 let mut s = String::new();
118 f.read_to_string(&mut s)?;
119 Ok(s)
120 });
121 let out = serialize_resolve(resolve, orig.as_deref().ok());
122 (orig.ok(), out, lock_root)
123}
124
125#[tracing::instrument(skip_all)]
126fn serialize_resolve(resolve: &Resolve, orig: Option<&str>) -> String {
127 let toml = toml::Table::try_from(resolve).unwrap();
128
129 let mut out = String::new();
130
131 let marker_line = "# This file is automatically @generated by Cargo.";
134 let extra_line = "# It is not intended for manual editing.";
135 out.push_str(marker_line);
136 out.push('\n');
137 out.push_str(extra_line);
138 out.push('\n');
139 if let Some(orig) = orig {
141 let mut comments = orig.lines().take_while(|line| line.starts_with('#'));
142 if let Some(first) = comments.next() {
143 if first != marker_line {
144 out.push_str(first);
145 out.push('\n');
146 }
147 if let Some(second) = comments.next() {
148 if second != extra_line {
149 out.push_str(second);
150 out.push('\n');
151 }
152 for line in comments {
153 out.push_str(line);
154 out.push('\n');
155 }
156 }
157 }
158 }
159
160 if let Some(version) = toml.get("version") {
161 out.push_str(&format!("version = {}\n\n", version));
162 }
163
164 let deps = toml["package"].as_array().unwrap();
165 for dep in deps {
166 let dep = dep.as_table().unwrap();
167
168 out.push_str("[[package]]\n");
169 emit_package(dep, &mut out);
170 }
171
172 if let Some(patch) = toml.get("patch") {
173 let list = patch["unused"].as_array().unwrap();
174 for entry in list {
175 out.push_str("[[patch.unused]]\n");
176 emit_package(entry.as_table().unwrap(), &mut out);
177 out.push('\n');
178 }
179 }
180
181 if let Some(meta) = toml.get("metadata") {
182 let meta_table = meta
186 .as_table()
187 .expect("validation ensures this is a table")
188 .clone();
189 let mut meta_doc = toml::Table::new();
190 meta_doc.insert("metadata".to_owned(), toml::Value::Table(meta_table));
191
192 out.push_str(&meta_doc.to_string());
193 }
194
195 if resolve.version() >= ResolveVersion::V2 {
201 while out.ends_with("\n\n") {
202 out.pop();
203 }
204 }
205 out
206}
207
208#[tracing::instrument(skip_all)]
209fn are_equal_lockfiles(orig: &str, current: &str, ws: &Workspace<'_>) -> bool {
210 if !ws.gctx().lock_update_allowed() {
214 let res: CargoResult<bool> = (|| {
215 let old: TomlLockfile = toml::from_str(orig)?;
216 let new: TomlLockfile = toml::from_str(current)?;
217 Ok(into_resolve(old, orig, ws)? == into_resolve(new, current, ws)?)
218 })();
219 if let Ok(true) = res {
220 return true;
221 }
222 }
223
224 orig.lines().eq(current.lines())
225}
226
227fn emit_package(dep: &toml::Table, out: &mut String) {
228 out.push_str(&format!("name = {}\n", &dep["name"]));
229 out.push_str(&format!("version = {}\n", &dep["version"]));
230
231 if dep.contains_key("source") {
232 out.push_str(&format!("source = {}\n", &dep["source"]));
233 }
234 if dep.contains_key("checksum") {
235 out.push_str(&format!("checksum = {}\n", &dep["checksum"]));
236 }
237
238 if let Some(s) = dep.get("dependencies") {
239 let slice = s.as_array().unwrap();
240
241 if !slice.is_empty() {
242 out.push_str("dependencies = [\n");
243
244 for child in slice.iter() {
245 out.push_str(&format!(" {},\n", child));
246 }
247
248 out.push_str("]\n");
249 }
250 out.push('\n');
251 } else if dep.contains_key("replace") {
252 out.push_str(&format!("replace = {}\n\n", &dep["replace"]));
253 }
254}