| // Copyright (C) 2024 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| use std::{ |
| collections::HashMap, |
| fs::{copy, read_dir, read_link, read_to_string, remove_dir_all, rename, write}, |
| os::unix::fs::symlink, |
| path::PathBuf, |
| process::{Command, Output}, |
| str::from_utf8, |
| sync::LazyLock, |
| }; |
| |
| use anyhow::{anyhow, ensure, Context, Result}; |
| use glob::glob; |
| use google_metadata::GoogleMetadata; |
| use license_checker::find_licenses; |
| use name_and_version::NamedAndVersioned; |
| use rooted_path::RootedPath; |
| use semver::Version; |
| use test_mapping::TestMapping; |
| |
| use crate::{ |
| android_bp::run_cargo_embargo, |
| copy_dir, |
| crate_type::Crate, |
| ensure_exists_and_empty, |
| license::update_module_license_files, |
| patch::Patch, |
| pseudo_crate::{CargoVendorClean, PseudoCrate}, |
| SuccessOrError, |
| }; |
| |
| #[derive(Debug)] |
| pub struct ManagedCrate<State: ManagedCrateState> { |
| android_crate: Crate, |
| extra: State, |
| } |
| |
| #[derive(Debug)] |
| pub struct New {} |
| #[derive(Debug)] |
| pub struct Vendored { |
| vendored_crate: Crate, |
| } |
| #[derive(Debug)] |
| pub struct Staged { |
| vendored_crate: Crate, |
| patch_output: Vec<(String, Output)>, |
| cargo_embargo_output: Output, |
| android_bp_diff: Output, |
| } |
| pub trait ManagedCrateState {} |
| impl ManagedCrateState for New {} |
| impl ManagedCrateState for Vendored {} |
| impl ManagedCrateState for Staged {} |
| |
| static CUSTOMIZATIONS: &[&str] = &[ |
| "*.bp", |
| "*.bp.fragment", |
| "*.mk", |
| "android", |
| "cargo_embargo.json", |
| "patches", |
| "METADATA", |
| "TEST_MAPPING", |
| "MODULE_LICENSE_*", |
| ]; |
| |
| static SYMLINKS: &[&str] = &["LICENSE", "NOTICE"]; |
| |
| static DELETIONS: LazyLock<HashMap<&str, &[&str]>> = LazyLock::new(|| { |
| HashMap::from([ |
| ("libbpf-sys", ["elfutils", "zlib", "libbpf"].as_slice()), |
| ("libusb1-sys", ["libusb"].as_slice()), |
| ("libz-sys", ["src/zlib", "src/zlib-ng"].as_slice()), |
| ]) |
| }); |
| |
| impl<State: ManagedCrateState> ManagedCrate<State> { |
| pub fn name(&self) -> &str { |
| self.android_crate.name() |
| } |
| pub fn android_version(&self) -> &Version { |
| self.android_crate.version() |
| } |
| pub fn android_crate_path(&self) -> &RootedPath { |
| self.android_crate.path() |
| } |
| pub fn android_bp(&self) -> RootedPath { |
| self.android_crate_path().join("Android.bp").unwrap() |
| } |
| pub fn cargo_embargo_json(&self) -> RootedPath { |
| self.android_crate_path().join("cargo_embargo.json").unwrap() |
| } |
| pub fn staging_path(&self) -> RootedPath { |
| self.android_crate |
| .path() |
| .with_same_root("out/rust-crate-temporary-build") |
| .unwrap() |
| .join(self.name()) |
| .unwrap() |
| } |
| fn patch_dir(&self) -> RootedPath { |
| self.android_crate_path().join("patches").unwrap() |
| } |
| pub fn patches(&self) -> Result<Vec<PathBuf>> { |
| let mut patches = Vec::new(); |
| let patch_dir = self.patch_dir(); |
| if patch_dir.abs().exists() { |
| for entry in |
| read_dir(&patch_dir).context(format!("Failed to read_dir {}", patch_dir))? |
| { |
| let entry = entry?; |
| if entry.file_name() == "Android.bp.patch" |
| || entry.file_name() == "Android.bp.diff" |
| || entry.file_name() == "rules.mk.diff" |
| { |
| continue; |
| } |
| patches.push(entry.path()); |
| } |
| } |
| patches.sort(); |
| |
| Ok(patches) |
| } |
| pub fn recontextualize_patches(&self) -> Result<()> { |
| let output = Command::new("git") |
| .args(["status", "--porcelain", "."]) |
| .current_dir(self.android_crate_path()) |
| .output()? |
| .success_or_error()?; |
| if !output.stdout.is_empty() { |
| return Err(anyhow!( |
| "Crate directory {} has uncommitted changes", |
| self.android_crate_path() |
| )); |
| } |
| let mut new_patch_contents = Vec::new(); |
| for patch in self.patches()? { |
| println!("Recontextualizing {}", patch.display()); |
| // Patch files can be in many different formats, and patch is very |
| // forgiving. We might be able to use "git apply -R --directory=crates/foo" |
| // once we have everything in the same format. |
| Command::new("patch") |
| .args(["-R", "-p1", "-l", "--reject-file=-", "--no-backup-if-mismatch", "-i"]) |
| .arg(&patch) |
| .current_dir(self.android_crate_path()) |
| .spawn()? |
| .wait()? |
| .success_or_error()?; |
| Command::new("git") |
| .args(["add", "."]) |
| .current_dir(self.android_crate_path()) |
| .spawn()? |
| .wait()? |
| .success_or_error()?; |
| let output = Command::new("git") |
| .args([ |
| "diff", |
| format!("--relative=crates/{}", self.name()).as_str(), |
| "-p", |
| "--stat", |
| "-R", |
| "--staged", |
| ".", |
| ]) |
| .current_dir(self.android_crate_path()) |
| .output()? |
| .success_or_error()?; |
| Command::new("git") |
| .args(["restore", "--staged", "."]) |
| .current_dir(self.android_crate_path()) |
| .spawn()? |
| .wait()? |
| .success_or_error()?; |
| Command::new("git") |
| .args(["restore", "."]) |
| .current_dir(self.android_crate_path()) |
| .spawn()? |
| .wait()? |
| .success_or_error()?; |
| Command::new("git") |
| .args(["clean", "-f", "."]) |
| .current_dir(self.android_crate_path()) |
| .spawn()? |
| .wait()? |
| .success_or_error()?; |
| let patch_contents = read_to_string(&patch)?; |
| let parsed = Patch::parse(&patch_contents); |
| new_patch_contents.push((patch, parsed.reassemble(&output.stdout))); |
| } |
| for (path, contents) in new_patch_contents { |
| write(path, contents)?; |
| } |
| Ok(()) |
| } |
| pub fn fix_licenses(&self) -> Result<()> { |
| println!("{} = \"={}\"", self.name(), self.android_version()); |
| let state = |
| find_licenses(self.android_crate_path(), self.name(), self.android_crate.license())?; |
| if !state.unsatisfied.is_empty() { |
| println!("{:?}", state); |
| } else { |
| // For now, just update MODULE_LICENSE_* |
| update_module_license_files(self.android_crate_path(), &state)?; |
| } |
| Ok(()) |
| } |
| pub fn fix_metadata(&self) -> Result<()> { |
| println!("{} = \"={}\"", self.name(), self.android_version()); |
| let mut metadata = GoogleMetadata::try_from(self.android_crate_path().join("METADATA")?)?; |
| metadata.set_version_and_urls(self.name(), self.android_version().to_string())?; |
| metadata.migrate_archive(); |
| metadata.migrate_homepage(); |
| metadata.remove_deprecated_url(); |
| metadata.write()?; |
| Ok(()) |
| } |
| pub fn fix_test_mapping(&self) -> Result<()> { |
| let mut tm = TestMapping::read(self.android_crate_path().clone())?; |
| println!("{}", self.name()); |
| let mut changed = tm.fix_import_paths(); |
| changed |= tm.add_new_tests_to_postsubmit()?; |
| changed |= tm.remove_unknown_tests()?; |
| if changed { |
| tm.write()?; |
| } |
| Ok(()) |
| } |
| } |
| |
| impl ManagedCrate<New> { |
| pub fn new(android_crate: Crate) -> Self { |
| ManagedCrate { android_crate, extra: New {} } |
| } |
| pub fn into_legacy(self) -> ManagedCrate<Vendored> { |
| ManagedCrate { |
| android_crate: self.android_crate.clone(), |
| extra: Vendored { vendored_crate: self.android_crate }, |
| } |
| } |
| fn into_vendored( |
| self, |
| pseudo_crate: &PseudoCrate<CargoVendorClean>, |
| ) -> Result<ManagedCrate<Vendored>> { |
| let vendored_crate = |
| Crate::from(pseudo_crate.vendored_dir_for(self.android_crate.name())?.clone())?; |
| Ok(ManagedCrate { android_crate: self.android_crate, extra: Vendored { vendored_crate } }) |
| } |
| pub fn stage( |
| self, |
| pseudo_crate: &PseudoCrate<CargoVendorClean>, |
| ) -> Result<ManagedCrate<Staged>> { |
| self.into_vendored(pseudo_crate)?.stage() |
| } |
| pub fn regenerate( |
| self, |
| update_metadata: bool, |
| pseudo_crate: &PseudoCrate<CargoVendorClean>, |
| ) -> Result<ManagedCrate<Staged>> { |
| self.into_vendored(pseudo_crate)?.regenerate(update_metadata) |
| } |
| } |
| |
| impl ManagedCrate<Vendored> { |
| pub fn stage(self) -> Result<ManagedCrate<Staged>> { |
| self.copy_to_staging()?; |
| |
| // Workaround. When checking the health of a legacy crate, there is no separate vendored crate, |
| // so we just have self.android_crate and self.vendored_crate point to the same directory. |
| // In this case, there is no need to copy Android customizations into the clean vendored copy |
| // or apply the patches. |
| if self.android_crate.path() != self.extra.vendored_crate.path() { |
| self.copy_customizations()?; |
| } |
| |
| let patch_output = if self.android_crate.path() != self.extra.vendored_crate.path() { |
| self.apply_patches()? |
| } else { |
| Vec::new() |
| }; |
| |
| let cargo_embargo_output = run_cargo_embargo(&self.staging_path())?; |
| let android_bp_diff = self.diff_android_bp()?; |
| |
| Ok(ManagedCrate { |
| android_crate: self.android_crate, |
| extra: Staged { |
| vendored_crate: self.extra.vendored_crate, |
| patch_output, |
| cargo_embargo_output, |
| android_bp_diff, |
| }, |
| }) |
| } |
| fn copy_to_staging(&self) -> Result<()> { |
| let staging_path = self.staging_path(); |
| ensure_exists_and_empty(&staging_path)?; |
| remove_dir_all(&staging_path).context(format!("Failed to remove {}", staging_path))?; |
| copy_dir(self.extra.vendored_crate.path(), &staging_path).context(format!( |
| "Failed to copy {} to {}", |
| self.extra.vendored_crate.path(), |
| self.staging_path() |
| ))?; |
| if staging_path.join(".git")?.abs().is_dir() { |
| remove_dir_all(staging_path.join(".git")?) |
| .with_context(|| "Failed to remove .git".to_string())?; |
| } |
| Ok(()) |
| } |
| fn copy_customizations(&self) -> Result<()> { |
| let dest_dir = self.staging_path(); |
| for pattern in CUSTOMIZATIONS { |
| let full_pattern = self.android_crate.path().join(pattern)?; |
| for entry in glob( |
| full_pattern |
| .abs() |
| .to_str() |
| .ok_or(anyhow!("Failed to convert path {} to str", full_pattern))?, |
| )? { |
| let entry = entry?; |
| let filename = entry |
| .file_name() |
| .context(format!("Failed to get file name for {}", entry.display()))? |
| .to_os_string(); |
| if entry.is_dir() { |
| copy_dir(&entry, &dest_dir.join(filename)?).context(format!( |
| "Failed to copy {} to {}", |
| entry.display(), |
| dest_dir |
| ))?; |
| } else { |
| let dest_file = dest_dir.join(&filename)?; |
| if dest_file.abs().exists() { |
| return Err(anyhow!("Destination file {} exists", dest_file)); |
| } |
| copy(&entry, dest_dir.join(filename)?).context(format!( |
| "Failed to copy {} to {}", |
| entry.display(), |
| dest_dir |
| ))?; |
| } |
| } |
| } |
| for link in SYMLINKS { |
| let src_path = self.android_crate.path().join(link)?; |
| if src_path.abs().is_symlink() { |
| let dest = read_link(src_path)?; |
| if dest.exists() { |
| return Err(anyhow!( |
| "Can't symlink {} -> {} because destination exists", |
| link, |
| dest.display(), |
| )); |
| } |
| symlink(dest, dest_dir.join(link)?)?; |
| } |
| } |
| for deletion in *DELETIONS.get(self.name()).unwrap_or(&[].as_slice()) { |
| let dir = self.staging_path().join(deletion)?; |
| ensure!(dir.abs().is_dir(), "{dir} is not a directory"); |
| remove_dir_all(dir)?; |
| } |
| Ok(()) |
| } |
| fn apply_patches(&self) -> Result<Vec<(String, Output)>> { |
| let mut patch_output = Vec::new(); |
| for patch in self.patches()? { |
| let output = Command::new("patch") |
| .args(["-p1", "-l", "--no-backup-if-mismatch", "-i"]) |
| .arg(&patch) |
| .current_dir(self.staging_path()) |
| .output()?; |
| patch_output.push(( |
| String::from_utf8_lossy(patch.file_name().unwrap().as_encoded_bytes()).to_string(), |
| output, |
| )); |
| } |
| Ok(patch_output) |
| } |
| fn diff_android_bp(&self) -> Result<Output> { |
| Ok(Command::new("diff") |
| .args([ |
| "-u", |
| "-w", |
| "-B", |
| "-I", |
| r#"default_team: "trendy_team_android_rust""#, |
| "-I", |
| "// has rustc warnings", |
| "-I", |
| "This file is generated by", |
| "-I", |
| "cargo_pkg_version:", |
| ]) |
| .arg(self.android_bp().rel()) |
| .arg(self.staging_path().join("Android.bp")?.rel()) |
| .current_dir(self.android_crate.path().root()) |
| .output()?) |
| } |
| pub fn regenerate(self, update_metadata: bool) -> Result<ManagedCrate<Staged>> { |
| let staged = self.stage()?; |
| staged.check_staged()?; |
| if !staged.staging_path().abs().exists() { |
| return Err(anyhow!("Staged crate not found at {}", staged.staging_path())); |
| } |
| if update_metadata { |
| let mut metadata = |
| GoogleMetadata::try_from(staged.staging_path().join("METADATA").unwrap())?; |
| let mut writeback = false; |
| writeback |= metadata.migrate_homepage(); |
| writeback |= metadata.migrate_archive(); |
| writeback |= metadata.remove_deprecated_url(); |
| let vendored_version = staged.extra.vendored_crate.version(); |
| if staged.android_crate.version() != vendored_version { |
| metadata.set_date_to_today()?; |
| metadata.set_version_and_urls(staged.name(), vendored_version.to_string())?; |
| writeback |= true; |
| } |
| if writeback { |
| metadata.write()?; |
| } |
| } |
| |
| let android_crate_dir = staged.android_crate.path(); |
| remove_dir_all(android_crate_dir)?; |
| rename(staged.staging_path(), android_crate_dir)?; |
| staged.fix_test_mapping()?; |
| checksum::generate(android_crate_dir.abs())?; |
| |
| Ok(staged) |
| } |
| } |
| |
| impl ManagedCrate<Staged> { |
| pub fn vendored_version(&self) -> &Version { |
| self.extra.vendored_crate.version() |
| } |
| pub fn check_staged(&self) -> Result<()> { |
| if !self.patch_success() { |
| for (patch, output) in self.patch_output() { |
| if !output.status.success() { |
| return Err(anyhow!( |
| "Failed to patch {} with {}\nstdout:\n{}\nstderr:\n{}", |
| self.name(), |
| patch, |
| from_utf8(&output.stdout)?, |
| from_utf8(&output.stderr)? |
| )); |
| } |
| } |
| } |
| self.cargo_embargo_output() |
| .success_or_error() |
| .context(format!("cargo_embargo execution failed for {}", self.name()))?; |
| |
| Ok(()) |
| } |
| pub fn patch_success(&self) -> bool { |
| self.extra.patch_output.iter().all(|output| output.1.status.success()) |
| } |
| pub fn patch_output(&self) -> &Vec<(String, Output)> { |
| &self.extra.patch_output |
| } |
| pub fn android_bp_diff(&self) -> &Output { |
| &self.extra.android_bp_diff |
| } |
| pub fn cargo_embargo_output(&self) -> &Output { |
| &self.extra.cargo_embargo_output |
| } |
| pub fn cargo_embargo_success(&self) -> bool { |
| self.extra.cargo_embargo_output.status.success() |
| } |
| pub fn android_bp_unchanged(&self) -> bool { |
| self.extra.android_bp_diff.status.success() |
| } |
| } |