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 std::{ 16 cell::OnceCell, 17 collections::BTreeSet, 18 fs::{copy, read_dir, read_to_string, remove_dir_all, remove_file, rename, write}, 19 os::unix::fs::symlink, 20 path::{Path, PathBuf}, 21 process::Command, 22 }; 23 24 use anyhow::{anyhow, bail, Context, Result}; 25 use crate_config::CrateConfig; 26 use glob::glob; 27 use google_metadata::GoogleMetadata; 28 use itertools::Itertools; 29 use license_checker::{find_licenses, LicenseState}; 30 use name_and_version::NamedAndVersioned; 31 use rooted_path::RootedPath; 32 use semver::Version; 33 use test_mapping::TestMapping; 34 35 use crate::{ 36 android_bp::run_cargo_embargo, 37 copy_dir, 38 crate_type::Crate, 39 ensure_exists_and_empty, 40 license::{most_restrictive_type, update_module_license_files}, 41 patch::Patch, 42 pseudo_crate::{CargoVendorClean, PseudoCrate}, 43 SuccessOrError, 44 }; 45 46 #[derive(Debug)] 47 pub struct ManagedCrate<State: ManagedCrateState> { 48 /// The crate with Android customizations, a subdirectory of crates/. 49 android_crate: Crate, 50 config: OnceCell<CrateConfig>, 51 extra: State, 52 } 53 54 #[derive(Debug)] 55 pub struct New {} 56 57 /// Crate state indicating we have run `cargo vendor` on the pseudo-crate. 58 #[derive(Debug)] 59 pub struct Vendored { 60 /// The vendored copy of the crate, from running `cargo vendor` 61 vendored_crate: Crate, 62 } 63 64 /// Crate state indicating we have copied the vendored code to a temporary build 65 /// directory, copied over Android customizations, and applied patches. 66 #[derive(Debug)] 67 pub struct CopiedAndPatched { 68 /// The vendored copy of the crate, from running `cargo vendor` 69 vendored_crate: Crate, 70 /// The license terms and associated license files. 71 licenses: LicenseState, 72 } 73 pub trait ManagedCrateState {} 74 impl ManagedCrateState for New {} 75 impl ManagedCrateState for Vendored {} 76 impl ManagedCrateState for CopiedAndPatched {} 77 78 static CUSTOMIZATIONS: &[&str] = &[ 79 "*.bp", 80 "*.bp.fragment", 81 "*.mk", 82 "android_config.toml", 83 "android", 84 "cargo_embargo.json", 85 "patches", 86 "METADATA", 87 "TEST_MAPPING", 88 ]; 89 90 impl<State: ManagedCrateState> ManagedCrate<State> { name(&self) -> &str91 pub fn name(&self) -> &str { 92 self.android_crate.name() 93 } android_version(&self) -> &Version94 pub fn android_version(&self) -> &Version { 95 self.android_crate.version() 96 } android_crate_path(&self) -> &RootedPath97 fn android_crate_path(&self) -> &RootedPath { 98 self.android_crate.path() 99 } config(&self) -> &CrateConfig100 pub fn config(&self) -> &CrateConfig { 101 self.config.get_or_init(|| { 102 CrateConfig::read(self.android_crate_path().abs()).unwrap_or_else(|e| { 103 panic!( 104 "Failed to read crate config {}/{}: {}", 105 self.android_crate_path(), 106 crate_config::CONFIG_FILE_NAME, 107 e 108 ) 109 }) 110 }) 111 } patch_dir(&self) -> RootedPath112 fn patch_dir(&self) -> RootedPath { 113 self.android_crate_path().join("patches").unwrap() 114 } temporary_build_directory(&self) -> RootedPath115 fn temporary_build_directory(&self) -> RootedPath { 116 self.android_crate 117 .path() 118 .with_same_root("out/rust-crate-temporary-build") 119 .unwrap() 120 .join(self.name()) 121 .unwrap() 122 } patches(&self) -> Result<Vec<PathBuf>>123 pub fn patches(&self) -> Result<Vec<PathBuf>> { 124 let mut patches = Vec::new(); 125 let patch_dir = self.patch_dir(); 126 if patch_dir.abs().exists() { 127 for entry in 128 read_dir(&patch_dir).context(format!("Failed to read_dir {}", patch_dir))? 129 { 130 let entry = entry?; 131 if entry.file_name() == "Android.bp.patch" 132 || entry.file_name() == "Android.bp.diff" 133 || entry.file_name() == "rules.mk.diff" 134 { 135 continue; 136 } 137 patches.push(entry.path()); 138 } 139 } 140 patches.sort(); 141 142 Ok(patches) 143 } recontextualize_patches(&self) -> Result<()>144 pub fn recontextualize_patches(&self) -> Result<()> { 145 let output = Command::new("git") 146 .args(["status", "--porcelain", "."]) 147 .current_dir(self.android_crate_path()) 148 .output()? 149 .success_or_error()?; 150 if !output.stdout.is_empty() { 151 return Err(anyhow!( 152 "Crate directory {} has uncommitted changes", 153 self.android_crate_path() 154 )); 155 } 156 let mut new_patch_contents = Vec::new(); 157 for patch in self.patches()? { 158 println!("Recontextualizing {}", patch.display()); 159 // Patch files can be in many different formats, and patch is very 160 // forgiving. We might be able to use "git apply -R --directory=crates/foo" 161 // once we have everything in the same format. 162 Command::new("patch") 163 .args(["-R", "-p1", "-l", "--reject-file=-", "--no-backup-if-mismatch", "-i"]) 164 .arg(&patch) 165 .current_dir(self.android_crate_path()) 166 .spawn()? 167 .wait()? 168 .success_or_error()?; 169 Command::new("git") 170 .args(["add", "."]) 171 .current_dir(self.android_crate_path()) 172 .spawn()? 173 .wait()? 174 .success_or_error()?; 175 let output = Command::new("git") 176 .args([ 177 "diff", 178 format!("--relative=crates/{}", self.name()).as_str(), 179 "-p", 180 "--stat", 181 "-R", 182 "--staged", 183 ".", 184 ]) 185 .current_dir(self.android_crate_path()) 186 .output()? 187 .success_or_error()?; 188 Command::new("git") 189 .args(["restore", "--staged", "."]) 190 .current_dir(self.android_crate_path()) 191 .spawn()? 192 .wait()? 193 .success_or_error()?; 194 Command::new("git") 195 .args(["restore", "."]) 196 .current_dir(self.android_crate_path()) 197 .spawn()? 198 .wait()? 199 .success_or_error()?; 200 Command::new("git") 201 .args(["clean", "-f", "."]) 202 .current_dir(self.android_crate_path()) 203 .spawn()? 204 .wait()? 205 .success_or_error()?; 206 let patch_contents = read_to_string(&patch)?; 207 let parsed = Patch::parse(&patch_contents); 208 new_patch_contents.push((patch, parsed.reassemble(&output.stdout))); 209 } 210 for (path, contents) in new_patch_contents { 211 write(path, contents)?; 212 } 213 Ok(()) 214 } 215 } 216 217 impl ManagedCrate<New> { new(android_crate: Crate) -> Self218 pub fn new(android_crate: Crate) -> Self { 219 ManagedCrate { android_crate, config: OnceCell::new(), extra: New {} } 220 } into_vendored( self, pseudo_crate: &PseudoCrate<CargoVendorClean>, ) -> Result<ManagedCrate<Vendored>>221 fn into_vendored( 222 self, 223 pseudo_crate: &PseudoCrate<CargoVendorClean>, 224 ) -> Result<ManagedCrate<Vendored>> { 225 let vendored_crate = 226 Crate::from(pseudo_crate.vendored_dir_for(self.android_crate.name())?.clone())?; 227 Ok(ManagedCrate { 228 android_crate: self.android_crate, 229 config: self.config, 230 extra: Vendored { vendored_crate }, 231 }) 232 } regenerate( self, pseudo_crate: &PseudoCrate<CargoVendorClean>, run_cargo_embargo: bool, ) -> Result<ManagedCrate<CopiedAndPatched>>233 pub fn regenerate( 234 self, 235 pseudo_crate: &PseudoCrate<CargoVendorClean>, 236 run_cargo_embargo: bool, 237 ) -> Result<ManagedCrate<CopiedAndPatched>> { 238 let regenerated = self.into_vendored(pseudo_crate)?.regenerate(run_cargo_embargo)?; 239 Ok(regenerated) 240 } 241 } 242 243 impl ManagedCrate<Vendored> { into_copied_and_patched( self, licenses: LicenseState, ) -> Result<ManagedCrate<CopiedAndPatched>>244 fn into_copied_and_patched( 245 self, 246 licenses: LicenseState, 247 ) -> Result<ManagedCrate<CopiedAndPatched>> { 248 Ok(ManagedCrate { 249 android_crate: self.android_crate, 250 config: self.config, 251 extra: CopiedAndPatched { vendored_crate: self.extra.vendored_crate, licenses }, 252 }) 253 } 254 /// Makes a clean copy of the vendored crates in a temporary build directory. copy_to_temporary_build_directory(&self) -> Result<()>255 fn copy_to_temporary_build_directory(&self) -> Result<()> { 256 let build_dir = self.temporary_build_directory(); 257 ensure_exists_and_empty(&build_dir)?; 258 remove_dir_all(&build_dir).context(format!("Failed to remove {}", build_dir))?; 259 copy_dir(self.extra.vendored_crate.path(), &build_dir).context(format!( 260 "Failed to copy {} to {}", 261 self.extra.vendored_crate.path(), 262 self.temporary_build_directory() 263 ))?; 264 Ok(()) 265 } 266 /// Copies Android-specific customizations, such as Android.bp, METADATA, etc. from 267 /// the managed crate directory to the temporary build directory. These are things that 268 /// we have added and know need to be preserved. copy_customizations(&self) -> Result<()>269 fn copy_customizations(&self) -> Result<()> { 270 let dest_dir = self.temporary_build_directory(); 271 for pattern in CUSTOMIZATIONS { 272 let full_pattern = self.android_crate.path().join(pattern)?; 273 for entry in glob( 274 full_pattern 275 .abs() 276 .to_str() 277 .ok_or(anyhow!("Failed to convert path {} to str", full_pattern))?, 278 )? { 279 let entry = entry?; 280 let filename = entry 281 .file_name() 282 .context(format!("Failed to get file name for {}", entry.display()))? 283 .to_os_string(); 284 if entry.is_dir() { 285 copy_dir(&entry, &dest_dir.join(filename)?).context(format!( 286 "Failed to copy {} to {}", 287 entry.display(), 288 dest_dir 289 ))?; 290 } else { 291 let dest_file = dest_dir.join(&filename)?; 292 if dest_file.abs().exists() { 293 return Err(anyhow!("Destination file {} exists", dest_file)); 294 } 295 copy(&entry, dest_dir.join(filename)?).context(format!( 296 "Failed to copy {} to {}", 297 entry.display(), 298 dest_dir 299 ))?; 300 } 301 } 302 } 303 Ok(()) 304 } 305 /// Applies patches from the patches/ directory in a deterministic order. 306 /// 307 /// Patches to the Android.bp file are excluded as they are applied 308 /// later, by cargo_embargo apply_patches(&self) -> Result<()>309 fn apply_patches(&self) -> Result<()> { 310 for patch in self.patches()? { 311 Command::new("patch") 312 .args(["-p1", "-l", "--no-backup-if-mismatch", "-i"]) 313 .arg(&patch) 314 .current_dir(self.temporary_build_directory()) 315 .output()? 316 .success_or_error() 317 .context(format!( 318 "Failed to apply patch file {}", 319 patch.file_name().unwrap_or_default().to_string_lossy() 320 ))?; 321 } 322 Ok(()) 323 } regenerate(self, run_cargo_embargo: bool) -> Result<ManagedCrate<CopiedAndPatched>>324 pub fn regenerate(self, run_cargo_embargo: bool) -> Result<ManagedCrate<CopiedAndPatched>> { 325 self.copy_to_temporary_build_directory()?; 326 self.copy_customizations()?; 327 328 // Delete stuff that we don't want to keep around, as specified in the 329 // android_config.toml 330 for deletion in self.config().deletions() { 331 let dir = self.temporary_build_directory().join(deletion)?; 332 if dir.abs().is_dir() { 333 remove_dir_all(dir)?; 334 } else { 335 remove_file(dir)?; 336 } 337 } 338 339 self.apply_patches()?; 340 341 let licenses = find_licenses( 342 self.temporary_build_directory(), 343 self.name(), 344 self.android_crate.license(), 345 )?; 346 let regenerated = self.into_copied_and_patched(licenses)?; 347 regenerated.regenerate(run_cargo_embargo)?; 348 Ok(regenerated) 349 } 350 } 351 352 impl ManagedCrate<CopiedAndPatched> { regenerate(&self, run_cargo_embargo: bool) -> Result<()>353 pub fn regenerate(&self, run_cargo_embargo: bool) -> Result<()> { 354 // License logic must happen AFTER applying patches, because we use patches 355 // to add missing license files. It must also happen BEFORE cargo_embargo, 356 // because cargo_embargo needs to put license information in the Android.bp. 357 self.update_license_files()?; 358 359 // If the crate has an out/ directory, that means we cannot skip running cargo_embargo. 360 // The out/ directory is used by cargo_embargo to preserve files generated 361 // by a build script. Soong can't run build scripts, so we rely on cargo_embargo 362 // to run `cargo build` and save the intermediates. 363 if run_cargo_embargo || self.android_crate_path().abs().join("out").exists() { 364 self.run_cargo_embargo()?; 365 } 366 367 self.update_metadata()?; 368 self.fix_test_mapping()?; 369 // Fails on dangling symlinks, which happens when we run on the log crate. 370 checksum::generate(self.temporary_build_directory())?; 371 372 let android_crate_dir = self.android_crate.path(); 373 remove_dir_all(android_crate_dir)?; 374 rename(self.temporary_build_directory(), android_crate_dir)?; 375 376 Ok(()) 377 } update_license_files(&self) -> Result<()>378 fn update_license_files(&self) -> Result<()> { 379 // For every chosen license, we must be able to find an associated 380 // license file. 381 if !self.extra.licenses.unsatisfied.is_empty() { 382 bail!( 383 "Could not find license files for some licenses: {:?}", 384 self.extra.licenses.unsatisfied 385 ); 386 } 387 388 // SOME license must apply to the code. If none apply, that's an error. 389 if self.extra.licenses.satisfied.is_empty() { 390 bail!("No license terms were found for this crate"); 391 } 392 393 // There should be no license files for terms not mentioned in Cargo.toml 394 // license expression. For example, if Cargo.toml says "Apache-2.0" and we 395 // find a file named "LICENSE-MIT", that suggests that something suspicious might 396 // be going on. 397 if !self.extra.licenses.unexpected.is_empty() { 398 bail!( 399 "Found unexpected license files that don't correspond to any terms of {}: {:?}", 400 self.android_crate.license().unwrap_or("<license terms not found in Cargo.toml>"), 401 self.extra.licenses.unexpected 402 ); 403 } 404 405 // Per go/thirdparty/licenses#multiple: 406 // "Delete any LICENSE files or license texts that were not selected and are not in use." 407 for unneeded_license_file in self.extra.licenses.unneeded.values() { 408 remove_file(self.temporary_build_directory().abs().join(unneeded_license_file))?; 409 } 410 411 // Per http://go/thirdpartyreviewers#license: 412 // "There must be a file called LICENSE containing an allowed third party license." 413 414 let license_files = self 415 .extra 416 .licenses 417 .satisfied 418 .values() 419 .map(|path| path.as_path()) 420 .collect::<BTreeSet<_>>(); 421 let canonical_license_file_name = Path::new("LICENSE"); 422 let canonical_license_path = self.temporary_build_directory().abs().join("LICENSE"); 423 if license_files.len() == 1 { 424 // If there's a single applicable license file, it must either 425 // be called LICENSE, or else we need to symlink LICENSE to it. 426 let license_file = license_files.first().unwrap(); 427 if *license_file != canonical_license_file_name { 428 if canonical_license_path.exists() { 429 // TODO: Maybe just blindly delete LICENSE and replace it with a symlink. 430 // Currently, we have to use a deletions config in android_config.toml 431 // to remove it. 432 bail!("Found a single license file {}, but we can't create a symlink to it because a file named LICENSE already exists", license_file.display()); 433 } else { 434 symlink(license_file, canonical_license_path)?; 435 } 436 } 437 } else { 438 // We found multiple license files. Per go/thirdparty/licenses#multiple they should 439 // be concatenated into a single LICENSE file, separated by dashed line dividers. 440 let mut license_contents = Vec::new(); 441 for file in license_files { 442 license_contents 443 .push(read_to_string(self.temporary_build_directory().abs().join(file))?); 444 } 445 // TODO: Maybe warn if LICENSE file exists. But there are cases where we can't just delete it 446 // because it contains *some* of the necessary license texts. 447 write( 448 canonical_license_path, 449 license_contents.iter().join("\n\n------------------\n\n"), 450 )?; 451 } 452 453 update_module_license_files(&self.temporary_build_directory(), &self.extra.licenses)?; 454 Ok(()) 455 } 456 /// Runs cargo_embargo on the crate in the temporary build directory. 457 /// 458 /// Because cargo_embargo can modify Cargo.lock files, we save them, if present. run_cargo_embargo(&self) -> Result<()>459 fn run_cargo_embargo(&self) -> Result<()> { 460 let temporary_build_path = self.temporary_build_directory(); 461 462 let cargo_lock = temporary_build_path.join("Cargo.lock")?; 463 let saved_cargo_lock = temporary_build_path.join("Cargo.lock.saved")?; 464 if cargo_lock.abs().exists() { 465 rename(&cargo_lock, &saved_cargo_lock)?; 466 } 467 468 run_cargo_embargo(&temporary_build_path)? 469 .success_or_error() 470 .context(format!("cargo_embargo execution failed for {}", self.name()))?; 471 472 if cargo_lock.abs().exists() { 473 remove_file(&cargo_lock)?; 474 } 475 if saved_cargo_lock.abs().exists() { 476 rename(saved_cargo_lock, cargo_lock)?; 477 } 478 479 Ok(()) 480 } 481 /// Updates the METADATA file in the temporary build directory. update_metadata(&self) -> Result<()>482 fn update_metadata(&self) -> Result<()> { 483 let mut metadata = 484 GoogleMetadata::try_from(self.temporary_build_directory().join("METADATA").unwrap())?; 485 metadata.update( 486 self.name(), 487 self.extra.vendored_crate.version().to_string(), 488 self.extra.vendored_crate.description(), 489 most_restrictive_type(&self.extra.licenses), 490 ); 491 metadata.write()?; 492 493 Ok(()) 494 } 495 /// Updates the TEST_MAPPING file in the temporary build directory, by 496 /// removing deleted tests and adding new tests as post-submits. fix_test_mapping(&self) -> Result<()>497 fn fix_test_mapping(&self) -> Result<()> { 498 let mut tm = TestMapping::read(self.temporary_build_directory())?; 499 let mut changed = tm.fix_import_paths(); 500 changed |= tm.add_new_tests_to_postsubmit()?; 501 changed |= tm.remove_unknown_tests()?; 502 // TODO: Add an option to fix up the reverse dependencies. 503 if changed { 504 println!("Updating TEST_MAPPING for {}", self.name()); 505 tm.write()?; 506 } 507 Ok(()) 508 } 509 } 510