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