xref: /aosp_15_r20/development/tools/external_crates/crate_tool/src/crates_io.rs (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1 // Copyright (C) 2024 The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 use anyhow::{anyhow, Result};
16 use cfg_expr::{
17     targets::{Arch, Family, Os},
18     Predicate, TargetPredicate,
19 };
20 use crates_index::{http, Crate, Dependency, DependencyKind, SparseIndex, Version};
21 use reqwest::blocking::Client;
22 use semver::VersionReq;
23 use std::{
24     cell::RefCell,
25     collections::{HashMap, HashSet},
26 };
27 
28 pub struct CratesIoIndex {
29     fetcher: Box<dyn CratesIoFetcher>,
30 }
31 
32 impl CratesIoIndex {
new() -> Result<CratesIoIndex>33     pub fn new() -> Result<CratesIoIndex> {
34         Ok(CratesIoIndex {
35             fetcher: Box::new(OnlineFetcher {
36                 index: crates_index::SparseIndex::new_cargo_default()?,
37                 client: reqwest::blocking::ClientBuilder::new().gzip(true).build()?,
38                 fetched: RefCell::new(HashSet::new()),
39             }),
40         })
41     }
new_offline() -> Result<CratesIoIndex>42     pub fn new_offline() -> Result<CratesIoIndex> {
43         Ok(CratesIoIndex {
44             fetcher: Box::new(OfflineFetcher {
45                 index: crates_index::SparseIndex::new_cargo_default()?,
46             }),
47         })
48     }
get_crate(&self, crate_name: impl AsRef<str>) -> Result<Crate>49     pub fn get_crate(&self, crate_name: impl AsRef<str>) -> Result<Crate> {
50         self.fetcher.fetch(crate_name.as_ref())
51     }
52 }
53 
54 pub trait CratesIoFetcher {
fetch(&self, crate_name: &str) -> Result<Crate>55     fn fetch(&self, crate_name: &str) -> Result<Crate>;
56 }
57 
58 pub struct OnlineFetcher {
59     index: SparseIndex,
60     client: Client,
61     // Keep track of crates we have fetched, to avoid fetching them multiple times.
62     fetched: RefCell<HashSet<String>>,
63 }
64 
65 pub struct OfflineFetcher {
66     index: SparseIndex,
67 }
68 
69 impl CratesIoFetcher for OnlineFetcher {
fetch(&self, crate_name: &str) -> Result<Crate>70     fn fetch(&self, crate_name: &str) -> Result<Crate> {
71         // Adapted from https://github.com/frewsxcv/rust-crates-index/blob/master/examples/sparse_http_reqwest.rs
72 
73         let mut fetched = self.fetched.borrow_mut();
74         if fetched.contains(crate_name) {
75             return Ok(self.index.crate_from_cache(crate_name.as_ref())?);
76         }
77         let req = self.index.make_cache_request(crate_name)?.body(())?;
78         let req = http::Request::from_parts(req.into_parts().0, vec![]);
79         let req: reqwest::blocking::Request = req.try_into()?;
80         let res = self.client.execute(req)?;
81         let mut builder = http::Response::builder().status(res.status()).version(res.version());
82         builder
83             .headers_mut()
84             .ok_or(anyhow!("Failed to get headers"))?
85             .extend(res.headers().iter().map(|(k, v)| (k.clone(), v.clone())));
86         let body = res.bytes()?;
87         let res = builder.body(body.to_vec())?;
88         let res = self
89             .index
90             .parse_cache_response(crate_name, res, true)?
91             .ok_or(anyhow!("Crate not found"))?;
92         fetched.insert(crate_name.to_string());
93         Ok(res)
94     }
95 }
96 impl CratesIoFetcher for OfflineFetcher {
fetch(&self, crate_name: &str) -> Result<Crate>97     fn fetch(&self, crate_name: &str) -> Result<Crate> {
98         Ok(self.index.crate_from_cache(crate_name.as_ref())?)
99     }
100 }
101 
102 /// Filter versions by those that are "safe", meaning not yanked or pre-release.
103 pub trait SafeVersions {
104     // Versions of the crate that aren't yanked or pre-release.
safe_versions(&self) -> impl DoubleEndedIterator<Item = &Version>105     fn safe_versions(&self) -> impl DoubleEndedIterator<Item = &Version>;
106     // Versions of the crate greater than 'version'.
versions_gt(&self, version: &semver::Version) -> impl DoubleEndedIterator<Item = &Version>107     fn versions_gt(&self, version: &semver::Version) -> impl DoubleEndedIterator<Item = &Version> {
108         self.safe_versions().filter(|v| {
109             semver::Version::parse(v.version()).map_or(false, |parsed| parsed.gt(version))
110         })
111     }
112     // Get a specific version of a crate.
get_version(&self, version: &semver::Version) -> Option<&Version>113     fn get_version(&self, version: &semver::Version) -> Option<&Version> {
114         self.safe_versions().find(|v| {
115             semver::Version::parse(v.version()).map_or(false, |parsed| parsed.eq(version))
116         })
117     }
118 }
119 impl SafeVersions for Crate {
safe_versions(&self) -> impl DoubleEndedIterator<Item = &Version>120     fn safe_versions(&self) -> impl DoubleEndedIterator<Item = &Version> {
121         self.versions().iter().filter(|v| {
122             !v.is_yanked()
123                 && semver::Version::parse(v.version()).map_or(false, |parsed| parsed.pre.is_empty())
124         })
125     }
126 }
127 
128 /// Filter dependencies for those likely to be relevant to Android.
129 pub trait AndroidDependencies {
android_deps(&self) -> impl DoubleEndedIterator<Item = &Dependency>130     fn android_deps(&self) -> impl DoubleEndedIterator<Item = &Dependency>;
android_version_reqs_by_name(&self) -> HashMap<&str, &str>131     fn android_version_reqs_by_name(&self) -> HashMap<&str, &str> {
132         self.android_deps().map(|dep| (dep.crate_name(), dep.requirement())).collect()
133     }
android_deps_with_version_reqs( &self, ) -> impl DoubleEndedIterator<Item = (&Dependency, VersionReq)>134     fn android_deps_with_version_reqs(
135         &self,
136     ) -> impl DoubleEndedIterator<Item = (&Dependency, VersionReq)> {
137         self.android_deps().filter_map(|dep| {
138             VersionReq::parse(dep.requirement()).map_or(None, |req| Some((dep, req)))
139         })
140     }
141 }
142 impl AndroidDependencies for Version {
android_deps(&self) -> impl DoubleEndedIterator<Item = &Dependency>143     fn android_deps(&self) -> impl DoubleEndedIterator<Item = &Dependency> {
144         self.dependencies().iter().filter(|dep| {
145             dep.kind() == DependencyKind::Normal && !dep.is_optional() && dep.is_android()
146         })
147     }
148 }
149 
150 /// Dependencies that are likely to be relevant to Android.
151 /// Unconditional dependencies (without a target cfg string) are always relevant.
152 /// Conditional dependencies are relevant if they are for Unix, Android, or Linux, and for an architecture we care about (Arm, RISC-V, or X86)
153 pub trait IsAndroid {
154     /// Returns true if this dependency is likely to be relevant to Android.
is_android(&self) -> bool155     fn is_android(&self) -> bool;
156 }
157 impl IsAndroid for Dependency {
is_android(&self) -> bool158     fn is_android(&self) -> bool {
159         self.target().map_or(true, is_android)
160     }
161 }
is_android(target: &str) -> bool162 fn is_android(target: &str) -> bool {
163     let expr = cfg_expr::Expression::parse(target);
164     if expr.is_err() {
165         return false;
166     }
167     let expr = expr.unwrap();
168     expr.eval(|pred| match pred {
169         Predicate::Target(target_predicate) => match target_predicate {
170             TargetPredicate::Family(family) => *family == Family::unix,
171             TargetPredicate::Os(os) => *os == Os::android || *os == Os::linux,
172             TargetPredicate::Arch(arch) => {
173                 [Arch::arm, Arch::aarch64, Arch::riscv32, Arch::riscv64, Arch::x86, Arch::x86_64]
174                     .contains(arch)
175             }
176             _ => true,
177         },
178         _ => true,
179     })
180 }
181 
182 pub trait DependencyChanges {
is_new_dep(&self, base_deps: &HashMap<&str, &str>) -> bool183     fn is_new_dep(&self, base_deps: &HashMap<&str, &str>) -> bool;
is_changed_dep(&self, base_deps: &HashMap<&str, &str>) -> bool184     fn is_changed_dep(&self, base_deps: &HashMap<&str, &str>) -> bool;
185 }
186 
187 impl DependencyChanges for Dependency {
is_new_dep(&self, base_deps: &HashMap<&str, &str>) -> bool188     fn is_new_dep(&self, base_deps: &HashMap<&str, &str>) -> bool {
189         !base_deps.contains_key(self.crate_name())
190     }
191 
is_changed_dep(&self, base_deps: &HashMap<&str, &str>) -> bool192     fn is_changed_dep(&self, base_deps: &HashMap<&str, &str>) -> bool {
193         let base_dep = base_deps.get(self.crate_name());
194         base_dep.is_none() || base_dep.is_some_and(|base_req| *base_req != self.requirement())
195     }
196 }
197 
198 #[cfg(test)]
199 mod tests {
200     use super::*;
201     #[test]
test_android_cfgs()202     fn test_android_cfgs() {
203         assert!(!is_android("asmjs-unknown-emscripten"), "Parse error");
204         assert!(!is_android("cfg(windows)"));
205         assert!(is_android("cfg(unix)"));
206         assert!(!is_android(r#"cfg(target_os = "redox")"#));
207         assert!(!is_android(r#"cfg(target_arch = "wasm32")"#));
208         assert!(is_android(r#"cfg(any(target_os = "linux", target_os = "android"))"#));
209         assert!(is_android(
210             r#"cfg(any(all(target_arch = "arm", target_pointer_width = "32"), target_arch = "mips", target_arch = "powerpc"))"#
211         ));
212         assert!(!is_android(
213             r#"cfg(all(target_arch = "wasm32", target_vendor = "unknown", target_os = "unknown"))"#
214         ));
215         assert!(is_android("cfg(tracing_unstable)"));
216         assert!(is_android(r#"cfg(any(unix, target_os = "wasi"))"#));
217         assert!(is_android(r#"cfg(not(all(target_arch = "arm", target_os = "none")))"#))
218     }
219 }
220