rust_playground_top_crates/
lib.rs

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/// The list of crates from crates.io
38#[derive(Debug, Deserialize)]
39struct TopCrates {
40    crates: Vec<Crate>,
41}
42
43/// The shared description of a crate
44#[derive(Debug, Deserialize)]
45struct Crate {
46    #[serde(rename = "id")]
47    name: InternedString,
48}
49
50/// A mapping of a crates name to its identifier used in source code
51#[derive(Debug, Serialize)]
52pub struct CrateInformation {
53    pub name: String,
54    pub version: Version,
55    pub id: String,
56}
57
58/// Hand-curated changes to the crate list
59#[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    /// List top 100 crates by number of downloads on crates.io.
122    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    /// Add crates that have been hand-picked
164    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
175/// Finds the features specified by the custom metadata of `pkg`.
176///
177/// Our custom metadata format looks like:
178///
179///     [package.metadata.playground]
180///     default-features = true
181///     features = ["std", "extra-traits"]
182///     all-features = false
183///
184/// All fields are optional.
185fn 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    // If `all-features` is set then we ignore `features`.
221    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    // Information about the playground's target platform.
236    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    // Registry of known packages.
246    let mut registry = PackageRegistry::new(config).expect("Unable to create package registry");
247    registry.lock_patches();
248
249    // Source for obtaining packages from the crates.io registry.
250    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    // Find the newest (non-prerelease, non-yanked) versions of all
292    // the interesting crates.
293    let mut package_ids = Vec::new();
294    for Crate { name } in top.crates {
295        if global.modifications.excluded(&name) {
296            continue;
297        }
298
299        // Query the registry for a summary of this crate.
300        // Usefully, this doesn't seem to include yanked versions
301        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        // Find the newest non-prelease version
312        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    // Add a direct dependency on each starting crate.
352    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    // Resolve transitive dependencies.
369    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    // Find transitive deps compatible with the playground's platform.
384    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    // Remove invalid and excluded packages that have been added due to resolution
407    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            // If enabled, all default features are already included in
426            // `features` by the resolver.
427            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    // Setup to interact with cargo.
440    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    // Sort all packages by name then version (descending), so that
463    // when we group them we know we get all the same crates together
464    // and the newest version first.
465    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            // We see the newest version first. Any subsequent
481            // versions will have their version appended so that they
482            // are uniquely named
483            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}