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