1#![deny(rust_2018_idioms)]
2
3use cargo::{
4 core::{
5 compiler::{CompileKind, CompileTarget, TargetInfo},
6 package::PackageSet,
7 registry::PackageRegistry,
8 resolver::{self, features::RequestedFeatures, ResolveOpts, VersionPreferences},
9 source::SourceMap,
10 Dependency, Package, PackageId, QueryKind, Source, SourceId, Summary, Target,
11 },
12 sources::RegistrySource,
13 util::{interning::InternedString, Config, VersionExt},
14};
15use itertools::Itertools;
16use semver::Version;
17use serde::{Deserialize, Serialize};
18use std::{
19 collections::{BTreeMap, BTreeSet, HashSet},
20 io::Read,
21 mem,
22 rc::Rc,
23 task::Poll,
24};
25
26const PLAYGROUND_TARGET_PLATFORM: &str = "x86_64-unknown-linux-gnu";
27
28struct GlobalState<'cfg> {
29 config: &'cfg Config,
30 target_info: TargetInfo,
31 registry: PackageRegistry<'cfg>,
32 crates_io: SourceId,
33 source: RegistrySource<'cfg>,
34 modifications: &'cfg Modifications,
35}
36
37#[derive(Debug, Deserialize)]
39struct TopCrates {
40 crates: Vec<Crate>,
41}
42
43#[derive(Debug, Deserialize)]
45struct Crate {
46 #[serde(rename = "id")]
47 name: InternedString,
48}
49
50#[derive(Debug, Serialize)]
52pub struct CrateInformation {
53 pub name: String,
54 pub version: Version,
55 pub id: String,
56}
57
58#[derive(Debug, Default, Deserialize)]
60pub struct Modifications {
61 #[serde(default)]
62 pub exclusions: Vec<InternedString>,
63 #[serde(default)]
64 pub additions: BTreeSet<InternedString>,
65}
66
67#[derive(Debug, Serialize, Clone)]
68#[serde(rename_all = "kebab-case")]
69pub struct DependencySpec {
70 #[serde(skip_serializing_if = "String::is_empty")]
71 pub package: String,
72 #[serde(serialize_with = "exact_version")]
73 pub version: Version,
74 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
75 pub features: BTreeSet<InternedString>,
76 #[serde(skip_serializing_if = "is_true")]
77 pub default_features: bool,
78}
79
80#[derive(Debug)]
81struct ResolvedDep {
82 summary: Summary,
83 lib_target: Target,
84 features: BTreeSet<InternedString>,
85 uses_default_features: bool,
86}
87
88fn exact_version<S>(version: &Version, serializer: S) -> Result<S::Ok, S::Error>
89where
90 S: serde::Serializer,
91{
92 semver::Comparator {
93 op: semver::Op::Exact,
94 major: version.major,
95 minor: Some(version.minor),
96 patch: Some(version.patch),
97 pre: version.pre.clone(),
98 }
99 .serialize(serializer)
100}
101
102fn is_true(b: &bool) -> bool {
103 *b
104}
105
106impl Modifications {
107 fn excluded(&self, name: &str) -> bool {
108 self.exclusions.iter().any(|n| n == name)
109 }
110}
111
112fn simple_get(url: &str) -> reqwest::Result<reqwest::blocking::Response> {
113 reqwest::blocking::ClientBuilder::new()
114 .user_agent("Rust Playground - Top Crates Utility")
115 .build()?
116 .get(url)
117 .send()
118}
119
120impl TopCrates {
121 fn download() -> TopCrates {
123 let resp = simple_get("https://crates.io/api/v1/crates?page=1&per_page=100&sort=downloads")
124 .expect("Could not fetch top crates");
125 assert!(
126 resp.status().is_success(),
127 "Could not download top crates; HTTP status was {}",
128 resp.status(),
129 );
130
131 serde_json::from_reader(resp).expect("Invalid JSON")
132 }
133
134 fn add_rust_cookbook_crates(&mut self) {
135 let mut resp = simple_get(
136 "https://raw.githubusercontent.com/rust-lang-nursery/rust-cookbook/master/Cargo.toml",
137 )
138 .expect("Could not fetch cookbook manifest");
139 assert!(
140 resp.status().is_success(),
141 "Could not download cookbook; HTTP status was {}",
142 resp.status(),
143 );
144
145 let mut content = String::new();
146 resp.read_to_string(&mut content)
147 .expect("could not read cookbook manifest");
148
149 let manifest = content
150 .parse::<toml::Value>()
151 .expect("could not parse cookbook manifest");
152
153 let dependencies = manifest["dependencies"]
154 .as_table()
155 .expect("no dependencies found for cookbook manifest");
156 self.crates.extend({
157 dependencies.iter().map(|(name, _)| Crate {
158 name: InternedString::new(name),
159 })
160 })
161 }
162
163 fn add_curated_crates(&mut self, modifications: &Modifications) {
165 self.crates.extend({
166 modifications
167 .additions
168 .iter()
169 .copied()
170 .map(|name| Crate { name })
171 });
172 }
173}
174
175fn playground_metadata_features(pkg: &Package) -> Option<(BTreeSet<InternedString>, bool)> {
186 let custom_metadata = pkg.manifest().custom_metadata()?;
187 let playground_metadata = custom_metadata.get("playground")?;
188
189 #[derive(Deserialize)]
190 #[serde(default, rename_all = "kebab-case")]
191 struct Metadata {
192 features: BTreeSet<InternedString>,
193 default_features: bool,
194 all_features: bool,
195 }
196
197 impl Default for Metadata {
198 fn default() -> Self {
199 Metadata {
200 features: BTreeSet::new(),
201 default_features: true,
202 all_features: false,
203 }
204 }
205 }
206
207 let metadata = match playground_metadata.clone().try_into::<Metadata>() {
208 Ok(metadata) => metadata,
209 Err(err) => {
210 eprintln!(
211 "Failed to parse custom metadata for {} {}: {}",
212 pkg.name(),
213 pkg.version(),
214 err
215 );
216 return None;
217 }
218 };
219
220 let summary = pkg.summary();
222 let enabled_features: BTreeSet<InternedString> = if metadata.all_features {
223 summary.features().keys().copied().collect()
224 } else {
225 metadata.features
226 };
227
228 Some((enabled_features, metadata.default_features))
229}
230
231fn make_global_state<'cfg>(
232 config: &'cfg Config,
233 modifications: &'cfg Modifications,
234) -> GlobalState<'cfg> {
235 let compile_target =
237 CompileTarget::new(PLAYGROUND_TARGET_PLATFORM).expect("Unable to create a CompileTarget");
238 let compile_kind = CompileKind::Target(compile_target);
239 let rustc = config
240 .load_global_rustc(None)
241 .expect("Unable to load the global rustc");
242 let target_info = TargetInfo::new(config, &[compile_kind], &rustc, compile_kind)
243 .expect("Unable to create a TargetInfo");
244
245 let mut registry = PackageRegistry::new(config).expect("Unable to create package registry");
247 registry.lock_patches();
248
249 let crates_io = SourceId::crates_io(config).expect("Unable to create crates.io source ID");
251 let yanked_whitelist = HashSet::new();
252 let mut source = RegistrySource::remote(crates_io, &yanked_whitelist, config)
253 .expect("Unable to create registry source");
254 source.invalidate_cache();
255 source
256 .block_until_ready()
257 .expect("Unable to wait for registry to be ready");
258
259 GlobalState {
260 config,
261 target_info,
262 registry,
263 crates_io,
264 source,
265 modifications,
266 }
267}
268
269fn bulk_download(global: &mut GlobalState<'_>, package_ids: &[PackageId]) -> Vec<Package> {
270 let mut sources = SourceMap::new();
271 sources.insert(Box::new(&mut global.source));
272
273 let package_set = PackageSet::new(package_ids, sources, global.config)
274 .expect("Unable to create a PackageSet");
275
276 package_set
277 .get_many(package_set.package_ids())
278 .expect("Unable to download packages")
279 .into_iter()
280 .map(Package::clone)
281 .collect()
282}
283
284fn populate_initial_direct_dependencies(
285 global: &mut GlobalState<'_>,
286) -> BTreeMap<PackageId, ResolvedDep> {
287 let mut top = TopCrates::download();
288 top.add_rust_cookbook_crates();
289 top.add_curated_crates(global.modifications);
290
291 let mut package_ids = Vec::new();
294 for Crate { name } in top.crates {
295 if global.modifications.excluded(&name) {
296 continue;
297 }
298
299 let version = None;
302 let dep = Dependency::parse(name, version, global.crates_io)
303 .unwrap_or_else(|e| panic!("Unable to parse dependency for {}: {}", name, e));
304
305 let matches = match global.source.query_vec(&dep, QueryKind::Exact) {
306 Poll::Ready(Ok(v)) => v,
307 Poll::Ready(Err(e)) => panic!("Unable to query registry for {}: {}", name, e),
308 Poll::Pending => panic!("Registry not ready to query"),
309 };
310
311 let summary = matches
313 .into_iter()
314 .filter(|summary| !summary.version().is_prerelease())
315 .max_by_key(|summary| summary.version().clone())
316 .unwrap_or_else(|| panic!("Registry has no viable versions of {}", name));
317
318 let package_id = PackageId::pure(name, summary.version().clone(), global.crates_io);
319 package_ids.push(package_id);
320 }
321
322 let packages = bulk_download(global, &package_ids);
323
324 let mut initial_direct_dependencies = BTreeMap::new();
325 for download in packages {
326 let id = download.package_id();
327 let lib_target = download
328 .library()
329 .unwrap_or_else(|| panic!("{} did not have a library", id))
330 .clone();
331 let mut dep = ResolvedDep {
332 summary: download.summary().clone(),
333 lib_target,
334 features: BTreeSet::new(),
335 uses_default_features: true,
336 };
337 if let Some((features, default_features)) = playground_metadata_features(&download) {
338 dep.features = features;
339 dep.uses_default_features = default_features;
340 }
341 initial_direct_dependencies.insert(id, dep);
342 }
343
344 initial_direct_dependencies
345}
346
347fn extend_direct_dependencies(
348 global: &mut GlobalState<'_>,
349 crates: &mut BTreeMap<PackageId, ResolvedDep>,
350) {
351 let mut summaries = Vec::new();
353 let mut valid_for_our_platform = BTreeSet::new();
354 for dep in mem::take(crates).into_values() {
355 valid_for_our_platform.insert(dep.summary.package_id());
356 summaries.push((
357 dep.summary,
358 ResolveOpts {
359 dev_deps: false,
360 features: RequestedFeatures::DepFeatures {
361 features: Rc::new(dep.features),
362 uses_default_features: dep.uses_default_features,
363 },
364 },
365 ));
366 }
367
368 let replacements = [];
370 let version_prefs = VersionPreferences::default();
371 let warnings = None;
372 let check_public_visible_dependencies = true;
373 let resolve = resolver::resolve(
374 &summaries,
375 &replacements,
376 &mut global.registry,
377 &version_prefs,
378 warnings,
379 check_public_visible_dependencies,
380 )
381 .expect("Unable to resolve dependencies");
382
383 let mut to_visit = valid_for_our_platform.clone();
385 while !to_visit.is_empty() {
386 let mut visit_next = BTreeSet::new();
387
388 for package_id in to_visit {
389 for (dep_pkg, deps) in resolve.deps(package_id) {
390 let for_this_platform = deps.iter().any(|dep| {
391 dep.platform().map_or(true, |platform| {
392 platform.matches(PLAYGROUND_TARGET_PLATFORM, global.target_info.cfg())
393 })
394 });
395
396 if for_this_platform {
397 valid_for_our_platform.insert(dep_pkg);
398 visit_next.insert(dep_pkg);
399 }
400 }
401 }
402
403 to_visit = visit_next;
404 }
405
406 let package_ids = resolve
408 .iter()
409 .filter(|pkg| valid_for_our_platform.contains(pkg))
410 .filter(|pkg| !global.modifications.excluded(pkg.name().as_str()))
411 .collect_vec();
412
413 let packages = bulk_download(global, &package_ids);
414
415 for download in packages {
416 let id = download.package_id();
417 let lib_target = download
418 .library()
419 .unwrap_or_else(|| panic!("{} did not have a library", id))
420 .clone();
421 let mut dep = ResolvedDep {
422 summary: download.summary().clone(),
423 lib_target,
424 features: resolve.features(id).iter().copied().collect(),
425 uses_default_features: false,
428 };
429 if let Some((features, _default_features)) = playground_metadata_features(&download) {
430 dep.features.extend(features);
431 }
432 crates.insert(id, dep);
433 }
434}
435
436pub fn generate_info(
437 modifications: &Modifications,
438) -> (BTreeMap<String, DependencySpec>, Vec<CrateInformation>) {
439 let config = Config::default().expect("Unable to create default Cargo config");
441 let _lock = config.acquire_package_cache_lock();
442 let mut global = make_global_state(&config, modifications);
443
444 let mut resolved_crates = populate_initial_direct_dependencies(&mut global);
445
446 loop {
447 let num_crates_before = resolved_crates.len();
448 extend_direct_dependencies(&mut global, &mut resolved_crates);
449 if num_crates_before == resolved_crates.len() {
450 break;
451 }
452 }
453
454 let dependencies = generate_dependency_specs(&resolved_crates);
455 let infos = generate_crate_information(&dependencies);
456 (dependencies, infos)
457}
458
459fn generate_dependency_specs(
460 crates: &BTreeMap<PackageId, ResolvedDep>,
461) -> BTreeMap<String, DependencySpec> {
462 let mut crates = crates.values().collect_vec();
466 crates.sort_by(|a, b| {
467 let name_cmp = a.summary.name().as_str().cmp(b.summary.name().as_str());
468 let version_cmp = a.summary.version().cmp(b.summary.version());
469 name_cmp.then(version_cmp.reverse())
470 });
471
472 let mut dependencies = BTreeMap::new();
473 for (name, pkgs) in &crates.iter().group_by(|dep| dep.summary.name()) {
474 let mut first = true;
475
476 for dep in pkgs {
477 let summary = &dep.summary;
478 let version = summary.version();
479
480 let crate_name = dep.lib_target.crate_name();
484 let exposed_name = if first {
485 crate_name
486 } else {
487 format!(
488 "{}_{}_{}_{}",
489 crate_name, version.major, version.minor, version.patch
490 )
491 };
492
493 let mut features = dep.features.clone();
494 let mut default_features = dep.uses_default_features;
495 if features.contains("default") || summary.features().get("default").is_none() {
496 features.remove("default");
497 default_features = true;
498 }
499
500 dependencies.insert(
501 exposed_name,
502 DependencySpec {
503 package: name.to_string(),
504 version: version.clone(),
505 features,
506 default_features,
507 },
508 );
509
510 first = false;
511 }
512 }
513
514 dependencies
515}
516
517fn generate_crate_information(
518 dependencies: &BTreeMap<String, DependencySpec>,
519) -> Vec<CrateInformation> {
520 let mut infos = Vec::new();
521
522 for (exposed_name, dependency_spec) in dependencies {
523 infos.push(CrateInformation {
524 name: dependency_spec.package.clone(),
525 version: dependency_spec.version.clone(),
526 id: exposed_name.clone(),
527 });
528 }
529
530 infos
531}