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