1 // Copyright (C) 2023 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 std::{
16 fs::{read_dir, remove_dir_all},
17 path::{Path, PathBuf},
18 process::{Command, Output},
19 str::from_utf8,
20 };
21
22 use anyhow::{anyhow, Context, Result};
23 use cargo::{
24 core::{Manifest, SourceId},
25 util::toml::read_manifest,
26 Config,
27 };
28 use semver::Version;
29
30 use crate::{
31 copy_dir, ensure_exists_and_empty, name_and_version::IsUpgradableTo, CrateError,
32 NameAndVersionRef, NamedAndVersioned, RepoPath,
33 };
34
35 #[derive(Debug)]
36 pub struct Crate {
37 manifest: Manifest,
38
39 path: RepoPath,
40
41 patch_output: Vec<Output>,
42 generate_android_bp_output: Option<Output>,
43 android_bp_diff: Option<Output>,
44 }
45
46 impl NamedAndVersioned for Crate {
name(&self) -> &str47 fn name(&self) -> &str {
48 self.manifest.name().as_str()
49 }
version(&self) -> &Version50 fn version(&self) -> &Version {
51 self.manifest.version()
52 }
key<'k>(&'k self) -> NameAndVersionRef<'k>53 fn key<'k>(&'k self) -> NameAndVersionRef<'k> {
54 NameAndVersionRef::new(self.name(), self.version())
55 }
56 }
57
58 impl IsUpgradableTo for Crate {}
59
60 impl Crate {
new<P: Into<PathBuf>, Q: Into<PathBuf>>( manifest: Manifest, root: P, relpath: Q, ) -> Crate61 pub fn new<P: Into<PathBuf>, Q: Into<PathBuf>>(
62 manifest: Manifest,
63 root: P,
64 relpath: Q,
65 ) -> Crate {
66 let root: PathBuf = root.into();
67 Crate {
68 manifest,
69 path: RepoPath::new(root.clone(), relpath),
70 patch_output: Vec::new(),
71 generate_android_bp_output: None,
72 android_bp_diff: None,
73 }
74 }
from<P: Into<PathBuf>>(cargo_toml: &impl AsRef<Path>, root: P) -> Result<Crate>75 pub fn from<P: Into<PathBuf>>(cargo_toml: &impl AsRef<Path>, root: P) -> Result<Crate> {
76 let root: PathBuf = root.into();
77 let manifest_dir = cargo_toml.as_ref().parent().ok_or(anyhow!(
78 "Failed to get parent directory of manifest at {}",
79 cargo_toml.as_ref().display()
80 ))?;
81 let relpath = manifest_dir.strip_prefix(&root)?.to_path_buf();
82 let source_id = SourceId::for_path(manifest_dir)?;
83 let (manifest, _nested) =
84 read_manifest(cargo_toml.as_ref(), source_id, &Config::default()?)?;
85 match manifest {
86 cargo::core::EitherManifest::Real(r) => Ok(Crate::new(r, root, relpath)),
87 cargo::core::EitherManifest::Virtual(_) => {
88 Err(anyhow!(CrateError::VirtualCrate(cargo_toml.as_ref().to_path_buf())))
89 }
90 }
91 }
92
path(&self) -> &RepoPath93 pub fn path(&self) -> &RepoPath {
94 &self.path
95 }
android_bp(&self) -> RepoPath96 pub fn android_bp(&self) -> RepoPath {
97 self.path.join(&"Android.bp")
98 }
cargo_embargo_json(&self) -> RepoPath99 pub fn cargo_embargo_json(&self) -> RepoPath {
100 self.path.join(&"cargo_embargo.json")
101 }
staging_path(&self) -> RepoPath102 pub fn staging_path(&self) -> RepoPath {
103 self.path.with_same_root(
104 Path::new("out/rust-crate-temporary-build").join(self.staging_dir_name()),
105 )
106 }
patch_dir(&self) -> RepoPath107 pub fn patch_dir(&self) -> RepoPath {
108 self.staging_path().join(&"patches")
109 }
staging_dir_name(&self) -> String110 pub fn staging_dir_name(&self) -> String {
111 if let Some(dirname) = self.path.rel().file_name().and_then(|x| x.to_str()) {
112 if dirname == self.name() {
113 return dirname.to_string();
114 }
115 }
116 format!("{}-{}", self.name(), self.version().to_string())
117 }
118
aosp_url(&self) -> Option<String>119 pub fn aosp_url(&self) -> Option<String> {
120 if self.path.rel().starts_with("external/rust/crates") {
121 if self.path.rel().ends_with(self.name()) {
122 Some(format!(
123 "https://android.googlesource.com/platform/{}/+/refs/heads/main",
124 self.path()
125 ))
126 } else if self.path.rel().parent()?.ends_with(self.name()) {
127 Some(format!(
128 "https://android.googlesource.com/platform/{}/+/refs/heads/main/{}",
129 self.path().rel().parent()?.display(),
130 self.path().rel().file_name()?.to_str()?
131 ))
132 } else {
133 None
134 }
135 } else {
136 None
137 }
138 }
crates_io_url(&self) -> String139 pub fn crates_io_url(&self) -> String {
140 format!("https://crates.io/crates/{}", self.name())
141 }
142
is_crates_io(&self) -> bool143 pub fn is_crates_io(&self) -> bool {
144 const NOT_CRATES_IO: &'static [&'static str] = &[
145 "external/rust/beto-rust/", // Google crates
146 "external/rust/pica/", // Google crate
147 "external/rust/crates/webpki/third-party/", // Internal/example code
148 "external/rust/cxx/third-party/", // Internal/example code
149 "external/rust/cxx/demo/", // Internal/example code
150 ];
151 !NOT_CRATES_IO.iter().any(|prefix| self.path().rel().starts_with(prefix))
152 }
is_migration_denied(&self) -> bool153 pub fn is_migration_denied(&self) -> bool {
154 const MIGRATION_DENYLIST: &'static [&'static str] = &[
155 "external/rust/crates/openssl/", // It's complicated.
156 "external/rust/cxx/", // It's REALLY complicated.
157 ];
158 MIGRATION_DENYLIST.iter().any(|prefix| self.path().rel().starts_with(prefix))
159 }
is_android_bp_healthy(&self) -> bool160 pub fn is_android_bp_healthy(&self) -> bool {
161 !self.is_migration_denied()
162 && self.android_bp().abs().exists()
163 && self.cargo_embargo_json().abs().exists()
164 && self.generate_android_bp_success()
165 && self.android_bp_unchanged()
166 }
patch_success(&self) -> bool167 pub fn patch_success(&self) -> bool {
168 self.patch_output.iter().all(|output| output.status.success())
169 }
generate_android_bp_success(&self) -> bool170 pub fn generate_android_bp_success(&self) -> bool {
171 self.generate_android_bp_output.as_ref().is_some_and(|output| output.status.success())
172 }
android_bp_unchanged(&self) -> bool173 pub fn android_bp_unchanged(&self) -> bool {
174 self.android_bp_diff.as_ref().is_some_and(|output| output.status.success())
175 }
176
print(&self) -> Result<()>177 pub fn print(&self) -> Result<()> {
178 println!("{} {} {}", self.name(), self.version(), self.path());
179 if let Some(output) = &self.generate_android_bp_output {
180 println!("generate Android.bp exit status: {}", output.status);
181 println!("{}", from_utf8(&output.stdout)?);
182 println!("{}", from_utf8(&output.stderr)?);
183 }
184 if let Some(output) = &self.android_bp_diff {
185 println!("diff exit status: {}", output.status);
186 println!("{}", from_utf8(&output.stdout)?);
187 println!("{}", from_utf8(&output.stderr)?);
188 }
189 Ok(())
190 }
191
192 // Make a clean copy of the crate in out/
stage_crate(&self) -> Result<()>193 pub fn stage_crate(&self) -> Result<()> {
194 let staging_path_absolute = self.staging_path().abs();
195 ensure_exists_and_empty(&staging_path_absolute)?;
196 remove_dir_all(&staging_path_absolute)
197 .context(format!("Failed to remove {}", staging_path_absolute.display()))?;
198 copy_dir(&self.path().abs(), &staging_path_absolute).context(format!(
199 "Failed to copy {} to {}",
200 self.path(),
201 self.staging_path()
202 ))?;
203 if staging_path_absolute.join(".git").is_dir() {
204 remove_dir_all(staging_path_absolute.join(".git"))
205 .with_context(|| "Failed to remove .git".to_string())?;
206 }
207 Ok(())
208 }
209
diff_android_bp(&mut self) -> Result<()>210 pub fn diff_android_bp(&mut self) -> Result<()> {
211 self.set_diff_output(
212 diff_android_bp(
213 &self.android_bp().rel(),
214 &self.staging_path().join(&"Android.bp").rel(),
215 &self.path.root(),
216 )
217 .context("Failed to diff Android.bp".to_string())?,
218 );
219 Ok(())
220 }
221
apply_patches(&mut self) -> Result<()>222 pub fn apply_patches(&mut self) -> Result<()> {
223 let patch_dir_absolute = self.patch_dir().abs();
224 if patch_dir_absolute.exists() {
225 for entry in read_dir(&patch_dir_absolute)
226 .context(format!("Failed to read_dir {}", patch_dir_absolute.display()))?
227 {
228 let entry = entry?;
229 if entry.file_name() == "Android.bp.patch"
230 || entry.file_name() == "Android.bp.diff"
231 || entry.file_name() == "rules.mk.diff"
232 {
233 continue;
234 }
235 let entry_path = entry.path();
236 let output = Command::new("patch")
237 .args(["-p1", "-l", "--no-backup-if-mismatch", "-i"])
238 .arg(&entry_path)
239 .current_dir(self.staging_path().abs())
240 .output()?;
241 if !output.status.success() {
242 println!(
243 "Failed to apply {}\nstdout:\n{}\nstderr:\n:{}",
244 entry_path.display(),
245 from_utf8(&output.stdout)?,
246 from_utf8(&output.stderr)?
247 );
248 }
249 self.patch_output.push(output);
250 }
251 }
252 Ok(())
253 }
254
android_bp_diff(&self) -> Option<&Output>255 pub fn android_bp_diff(&self) -> Option<&Output> {
256 self.android_bp_diff.as_ref()
257 }
generate_android_bp_output(&self) -> Option<&Output>258 pub fn generate_android_bp_output(&self) -> Option<&Output> {
259 self.generate_android_bp_output.as_ref()
260 }
set_generate_android_bp_output(&mut self, c2a_output: Output)261 pub fn set_generate_android_bp_output(&mut self, c2a_output: Output) {
262 self.generate_android_bp_output.replace(c2a_output);
263 }
set_diff_output(&mut self, diff_output: Output)264 pub fn set_diff_output(&mut self, diff_output: Output) {
265 self.android_bp_diff.replace(diff_output);
266 }
set_patch_output(&mut self, patch_output: Vec<Output>)267 pub fn set_patch_output(&mut self, patch_output: Vec<Output>) {
268 self.patch_output = patch_output;
269 }
270 }
271
272 pub trait Migratable {
is_migration_eligible(&self) -> bool273 fn is_migration_eligible(&self) -> bool;
is_migratable(&self) -> bool274 fn is_migratable(&self) -> bool;
275 }
276
277 impl Migratable for Crate {
is_migration_eligible(&self) -> bool278 fn is_migration_eligible(&self) -> bool {
279 self.is_crates_io()
280 && !self.is_migration_denied()
281 && self.android_bp().abs().exists()
282 && self.cargo_embargo_json().abs().exists()
283 }
is_migratable(&self) -> bool284 fn is_migratable(&self) -> bool {
285 self.patch_success() && self.generate_android_bp_success() && self.android_bp_unchanged()
286 }
287 }
288
289 #[cfg(test)]
290 mod tests {
291 use std::fs::{create_dir, write};
292
293 use super::*;
294 use anyhow::anyhow;
295 use tempfile::tempdir;
296
write_test_manifest(temp_crate_dir: &Path, name: &str, version: &str) -> Result<PathBuf>297 fn write_test_manifest(temp_crate_dir: &Path, name: &str, version: &str) -> Result<PathBuf> {
298 let cargo_toml: PathBuf = [temp_crate_dir, &Path::new("Cargo.toml")].iter().collect();
299 write(
300 cargo_toml.as_path(),
301 format!("[package]\nname = \"{}\"\nversion = \"{}\"\n", name, version),
302 )?;
303 let lib_rs: PathBuf = [temp_crate_dir, &Path::new("src/lib.rs")].iter().collect();
304 create_dir(lib_rs.parent().ok_or(anyhow!("Failed to get parent"))?)?;
305 write(lib_rs.as_path(), "// foo")?;
306 Ok(cargo_toml)
307 }
308
309 #[test]
test_from_and_properties() -> Result<()>310 fn test_from_and_properties() -> Result<()> {
311 let temp_crate_dir = tempdir()?;
312 let cargo_toml = write_test_manifest(temp_crate_dir.path(), "foo", "1.2.0")?;
313 let krate = Crate::from(&cargo_toml, &"/")?;
314 assert_eq!(krate.name(), "foo");
315 assert_eq!(krate.version().to_string(), "1.2.0");
316 assert!(krate.is_crates_io());
317 assert_eq!(krate.android_bp().abs(), temp_crate_dir.path().join("Android.bp"));
318 assert_eq!(
319 krate.cargo_embargo_json().abs(),
320 temp_crate_dir.path().join("cargo_embargo.json")
321 );
322 Ok(())
323 }
324
325 #[test]
test_from_error() -> Result<()>326 fn test_from_error() -> Result<()> {
327 let temp_crate_dir = tempdir()?;
328 let cargo_toml = write_test_manifest(temp_crate_dir.path(), "foo", "1.2.0")?;
329 assert!(Crate::from(&cargo_toml, &"/blah").is_err());
330 Ok(())
331 }
332 }
333
diff_android_bp( a: &impl AsRef<Path>, b: &impl AsRef<Path>, root: &impl AsRef<Path>, ) -> Result<Output>334 pub fn diff_android_bp(
335 a: &impl AsRef<Path>,
336 b: &impl AsRef<Path>,
337 root: &impl AsRef<Path>,
338 ) -> Result<Output> {
339 Ok(Command::new("diff")
340 .args([
341 "-u",
342 "-w",
343 "-B",
344 "-I",
345 "// has rustc warnings",
346 "-I",
347 "This file is generated by",
348 "-I",
349 "cargo_pkg_version:",
350 ])
351 .arg(a.as_ref())
352 .arg(b.as_ref())
353 .current_dir(root)
354 .output()?)
355 }
356