1 use crate::core::registry::PackageRegistry;
2 use crate::core::resolver::features::{CliFeatures, HasDevUnits};
3 use crate::core::{PackageId, PackageIdSpec};
4 use crate::core::{Resolve, SourceId, Workspace};
5 use crate::ops;
6 use crate::util::config::Config;
7 use crate::util::CargoResult;
8 use anyhow::Context;
9 use log::debug;
10 use std::collections::{BTreeMap, HashSet};
11 use termcolor::Color::{self, Cyan, Green, Red};
12 
13 pub struct UpdateOptions<'a> {
14     pub config: &'a Config,
15     pub to_update: Vec<String>,
16     pub precise: Option<&'a str>,
17     pub aggressive: bool,
18     pub dry_run: bool,
19     pub workspace: bool,
20 }
21 
generate_lockfile(ws: &Workspace<'_>) -> CargoResult<()>22 pub fn generate_lockfile(ws: &Workspace<'_>) -> CargoResult<()> {
23     let mut registry = PackageRegistry::new(ws.config())?;
24     let mut resolve = ops::resolve_with_previous(
25         &mut registry,
26         ws,
27         &CliFeatures::new_all(true),
28         HasDevUnits::Yes,
29         None,
30         None,
31         &[],
32         true,
33     )?;
34     ops::write_pkg_lockfile(ws, &mut resolve)?;
35     Ok(())
36 }
37 
update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoResult<()>38 pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoResult<()> {
39     if opts.aggressive && opts.precise.is_some() {
40         anyhow::bail!("cannot specify both aggressive and precise simultaneously")
41     }
42 
43     if ws.members().count() == 0 {
44         anyhow::bail!("you can't generate a lockfile for an empty workspace.")
45     }
46 
47     // Updates often require a lot of modifications to the registry, so ensure
48     // that we're synchronized against other Cargos.
49     let _lock = ws.config().acquire_package_cache_lock()?;
50 
51     let previous_resolve = match ops::load_pkg_lockfile(ws)? {
52         Some(resolve) => resolve,
53         None => {
54             match opts.precise {
55                 None => return generate_lockfile(ws),
56 
57                 // Precise option specified, so calculate a previous_resolve required
58                 // by precise package update later.
59                 Some(_) => {
60                     let mut registry = PackageRegistry::new(opts.config)?;
61                     ops::resolve_with_previous(
62                         &mut registry,
63                         ws,
64                         &CliFeatures::new_all(true),
65                         HasDevUnits::Yes,
66                         None,
67                         None,
68                         &[],
69                         true,
70                     )?
71                 }
72             }
73         }
74     };
75     let mut registry = PackageRegistry::new(opts.config)?;
76     let mut to_avoid = HashSet::new();
77 
78     if opts.to_update.is_empty() {
79         if !opts.workspace {
80             to_avoid.extend(previous_resolve.iter());
81             to_avoid.extend(previous_resolve.unused_patches());
82         }
83     } else {
84         let mut sources = Vec::new();
85         for name in opts.to_update.iter() {
86             let dep = previous_resolve.query(name)?;
87             if opts.aggressive {
88                 fill_with_deps(&previous_resolve, dep, &mut to_avoid, &mut HashSet::new());
89             } else {
90                 to_avoid.insert(dep);
91                 sources.push(match opts.precise {
92                     Some(precise) => {
93                         // TODO: see comment in `resolve.rs` as well, but this
94                         //       seems like a pretty hokey reason to single out
95                         //       the registry as well.
96                         let precise = if dep.source_id().is_registry() {
97                             semver::Version::parse(precise).with_context(|| {
98                                 format!("invalid version format for precise version `{}`", precise)
99                             })?;
100                             format!("{}={}->{}", dep.name(), dep.version(), precise)
101                         } else {
102                             precise.to_string()
103                         };
104                         dep.source_id().with_precise(Some(precise))
105                     }
106                     None => dep.source_id().with_precise(None),
107                 });
108             }
109             if let Ok(unused_id) =
110                 PackageIdSpec::query_str(name, previous_resolve.unused_patches().iter().cloned())
111             {
112                 to_avoid.insert(unused_id);
113             }
114         }
115 
116         registry.add_sources(sources)?;
117     }
118 
119     let mut resolve = ops::resolve_with_previous(
120         &mut registry,
121         ws,
122         &CliFeatures::new_all(true),
123         HasDevUnits::Yes,
124         Some(&previous_resolve),
125         Some(&to_avoid),
126         &[],
127         true,
128     )?;
129 
130     // Summarize what is changing for the user.
131     let print_change = |status: &str, msg: String, color: Color| {
132         opts.config.shell().status_with_color(status, msg, color)
133     };
134     for (removed, added) in compare_dependency_graphs(&previous_resolve, &resolve) {
135         if removed.len() == 1 && added.len() == 1 {
136             let msg = if removed[0].source_id().is_git() {
137                 format!(
138                     "{} -> #{}",
139                     removed[0],
140                     &added[0].source_id().precise().unwrap()[..8]
141                 )
142             } else {
143                 format!("{} -> v{}", removed[0], added[0].version())
144             };
145             print_change("Updating", msg, Green)?;
146         } else {
147             for package in removed.iter() {
148                 print_change("Removing", format!("{}", package), Red)?;
149             }
150             for package in added.iter() {
151                 print_change("Adding", format!("{}", package), Cyan)?;
152             }
153         }
154     }
155     if opts.dry_run {
156         opts.config
157             .shell()
158             .warn("not updating lockfile due to dry run")?;
159     } else {
160         ops::write_pkg_lockfile(ws, &mut resolve)?;
161     }
162     return Ok(());
163 
164     fn fill_with_deps<'a>(
165         resolve: &'a Resolve,
166         dep: PackageId,
167         set: &mut HashSet<PackageId>,
168         visited: &mut HashSet<PackageId>,
169     ) {
170         if !visited.insert(dep) {
171             return;
172         }
173         set.insert(dep);
174         for (dep, _) in resolve.deps_not_replaced(dep) {
175             fill_with_deps(resolve, dep, set, visited);
176         }
177     }
178 
179     fn compare_dependency_graphs(
180         previous_resolve: &Resolve,
181         resolve: &Resolve,
182     ) -> Vec<(Vec<PackageId>, Vec<PackageId>)> {
183         fn key(dep: PackageId) -> (&'static str, SourceId) {
184             (dep.name().as_str(), dep.source_id())
185         }
186 
187         // Removes all package IDs in `b` from `a`. Note that this is somewhat
188         // more complicated because the equality for source IDs does not take
189         // precise versions into account (e.g., git shas), but we want to take
190         // that into account here.
191         fn vec_subtract(a: &[PackageId], b: &[PackageId]) -> Vec<PackageId> {
192             a.iter()
193                 .filter(|a| {
194                     // If this package ID is not found in `b`, then it's definitely
195                     // in the subtracted set.
196                     let i = match b.binary_search(a) {
197                         Ok(i) => i,
198                         Err(..) => return true,
199                     };
200 
201                     // If we've found `a` in `b`, then we iterate over all instances
202                     // (we know `b` is sorted) and see if they all have different
203                     // precise versions. If so, then `a` isn't actually in `b` so
204                     // we'll let it through.
205                     //
206                     // Note that we only check this for non-registry sources,
207                     // however, as registries contain enough version information in
208                     // the package ID to disambiguate.
209                     if a.source_id().is_registry() {
210                         return false;
211                     }
212                     b[i..]
213                         .iter()
214                         .take_while(|b| a == b)
215                         .all(|b| a.source_id().precise() != b.source_id().precise())
216                 })
217                 .cloned()
218                 .collect()
219         }
220 
221         // Map `(package name, package source)` to `(removed versions, added versions)`.
222         let mut changes = BTreeMap::new();
223         let empty = (Vec::new(), Vec::new());
224         for dep in previous_resolve.iter() {
225             changes
226                 .entry(key(dep))
227                 .or_insert_with(|| empty.clone())
228                 .0
229                 .push(dep);
230         }
231         for dep in resolve.iter() {
232             changes
233                 .entry(key(dep))
234                 .or_insert_with(|| empty.clone())
235                 .1
236                 .push(dep);
237         }
238 
239         for v in changes.values_mut() {
240             let (ref mut old, ref mut new) = *v;
241             old.sort();
242             new.sort();
243             let removed = vec_subtract(old, new);
244             let added = vec_subtract(new, old);
245             *old = removed;
246             *new = added;
247         }
248         debug!("{:#?}", changes);
249 
250         changes.into_iter().map(|(_, v)| v).collect()
251     }
252 }
253