blob: 5ca1dfef3a5bda147e72d9480037bae4a476f5a2 [file] [log] [blame]
// 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::{BTreeMap, BTreeSet},
env,
fs::{create_dir, create_dir_all, read_dir, remove_file, write},
os::unix::fs::symlink,
path::Path,
process::Command,
str::from_utf8,
};
use anyhow::{anyhow, Context, Result};
use crates_index::DependencyKind;
use glob::glob;
use google_metadata::GoogleMetadata;
use itertools::Itertools;
use license_checker::find_licenses;
use name_and_version::{NameAndVersionMap, NameAndVersionRef, NamedAndVersioned};
use rooted_path::RootedPath;
use semver::Version;
use spdx::Licensee;
use crate::{
android_bp::cargo_embargo_autoconfig,
copy_dir,
crate_collection::CrateCollection,
crate_type::Crate,
crates_io::{AndroidDependencies, CratesIoIndex, DependencyChanges, SafeVersions},
license::{most_restrictive_type, update_module_license_files},
managed_crate::ManagedCrate,
pseudo_crate::{CargoVendorDirty, PseudoCrate},
upgradable::{IsUpgradableTo, MatchesRelaxed},
SuccessOrError,
};
pub struct ManagedRepo {
path: RootedPath,
crates_io: CratesIoIndex,
}
impl ManagedRepo {
pub fn new(path: RootedPath, offline: bool) -> Result<ManagedRepo> {
Ok(ManagedRepo {
path,
crates_io: if offline { CratesIoIndex::new_offline()? } else { CratesIoIndex::new()? },
})
}
fn pseudo_crate(&self) -> PseudoCrate<CargoVendorDirty> {
PseudoCrate::new(self.path.join("pseudo_crate").unwrap())
}
fn contains(&self, crate_name: &str) -> bool {
self.managed_dir_for(crate_name).abs().exists()
}
fn managed_dir(&self) -> RootedPath {
self.path.join("crates").unwrap()
}
fn managed_dir_for(&self, crate_name: &str) -> RootedPath {
self.managed_dir().join(crate_name).unwrap()
}
fn legacy_dir_for(&self, crate_name: &str, version: Option<&Version>) -> Result<RootedPath> {
match version {
Some(v) => {
let cc = self.legacy_crates_for(crate_name)?;
let nv = NameAndVersionRef::new(crate_name, v);
Ok(cc
.map_field()
.get(&nv as &dyn NamedAndVersioned)
.ok_or(anyhow!("Failed to find crate {} v{}", crate_name, v))?
.path()
.clone())
}
None => {
Ok(self.path.with_same_root("external/rust/crates").unwrap().join(crate_name)?)
}
}
}
fn legacy_crates_for(&self, crate_name: &str) -> Result<CrateCollection> {
let mut cc = self.new_cc();
cc.add_from(format!("external/rust/crates/{}", crate_name))?;
Ok(cc)
}
fn legacy_crates(&self) -> Result<CrateCollection> {
let mut cc = self.new_cc();
cc.add_from("external/rust/crates")?;
Ok(cc)
}
fn new_cc(&self) -> CrateCollection {
CrateCollection::new(self.path.root())
}
fn managed_crate_for(
&self,
crate_name: &str,
) -> Result<ManagedCrate<crate::managed_crate::New>> {
Ok(ManagedCrate::new(Crate::from(self.managed_dir_for(crate_name))?))
}
pub fn all_crate_names(&self) -> Result<BTreeSet<String>> {
let mut managed_dirs = BTreeSet::new();
if self.managed_dir().abs().exists() {
for entry in read_dir(self.managed_dir())? {
let entry = entry?;
if entry.path().is_dir() {
managed_dirs.insert(entry.file_name().into_string().map_err(|e| {
anyhow!("Failed to convert {} to string", e.to_string_lossy())
})?);
}
}
}
Ok(managed_dirs)
}
pub fn migration_health(
&self,
crate_name: &str,
verbose: bool,
unpinned: bool,
version: Option<&Version>,
) -> Result<Version> {
if self.contains(crate_name) {
return Err(anyhow!("Crate {} already exists in {}/crates", crate_name, self.path));
}
let cc = self.legacy_crates_for(crate_name)?;
let krate = match version {
Some(v) => {
let nv = NameAndVersionRef::new(crate_name, v);
match cc.map_field().get(&nv as &dyn NamedAndVersioned) {
Some(k) => k,
None => {
return Err(anyhow!("Did not find crate {} v{}", crate_name, v));
}
}
}
None => {
if cc.map_field().len() != 1 {
return Err(anyhow!(
"Expected a single crate version for {}, but found {}. Specify a version with --versions={}=<version>",
crate_name,
cc.get_versions(crate_name).map(|(nv, _)| nv.version()).join(", "),
crate_name
));
}
cc.map_field().values().next().unwrap()
}
};
println!("Found {} v{} in {}", krate.name(), krate.version(), krate.path());
let mut healthy_self_contained = true;
if krate.is_migration_denied() {
println!("This crate is on the migration denylist");
healthy_self_contained = false;
}
let mc = ManagedCrate::new(Crate::from(krate.path().clone())?).into_legacy();
if !mc.android_bp().abs().exists() {
println!("There is no Android.bp file in {}", krate.path());
healthy_self_contained = false;
}
if !mc.cargo_embargo_json().abs().exists() {
println!("There is no cargo_embargo.json file in {}", krate.path());
healthy_self_contained = false;
}
if healthy_self_contained {
let mc = mc.stage()?;
if !mc.cargo_embargo_success() {
println!("cargo_embargo execution did not succeed for {}", mc.staging_path(),);
if verbose {
println!(
"stdout:\n{}\nstderr:\n{}",
from_utf8(&mc.cargo_embargo_output().stdout)?,
from_utf8(&mc.cargo_embargo_output().stderr)?,
);
}
healthy_self_contained = false;
} else if !mc.android_bp_unchanged() {
println!(
"Running cargo_embargo on {} produced changes to the Android.bp file",
mc.staging_path()
);
if verbose {
println!("{}", from_utf8(&mc.android_bp_diff().stdout)?);
}
healthy_self_contained = false;
}
}
if !healthy_self_contained {
println!("Crate {} is UNHEALTHY", crate_name);
return Err(anyhow!("Crate {} is unhealthy", crate_name));
}
let pseudo_crate = self.pseudo_crate();
if unpinned {
pseudo_crate.cargo_add_unpinned(krate)
} else {
pseudo_crate.cargo_add(krate)
}
.inspect_err(|_e| {
let _ = pseudo_crate.remove(krate.name());
})?;
let pseudo_crate = pseudo_crate.vendor()?;
let mc = ManagedCrate::new(Crate::from(krate.path().clone())?).stage(&pseudo_crate)?;
pseudo_crate.remove(krate.name())?;
let version = mc.vendored_version().clone();
if mc.android_version() != mc.vendored_version() {
println!(
"Source and destination versions are different: {} -> {}",
mc.android_version(),
mc.vendored_version()
);
}
if !mc.patch_success() {
println!("Patches did not apply successfully to the migrated crate");
if verbose {
for output in mc.patch_output() {
if !output.1.status.success() {
println!(
"Failed to apply {}\nstdout:\n{}\nstderr:\n:{}",
output.0,
from_utf8(&output.1.stdout)?,
from_utf8(&output.1.stderr)?
);
}
}
}
}
if !mc.cargo_embargo_success() {
println!("cargo_embargo execution did not succeed for the migrated crate");
if verbose {
println!(
"stdout:\n{}\nstderr:\n{}",
from_utf8(&mc.cargo_embargo_output().stdout)?,
from_utf8(&mc.cargo_embargo_output().stderr)?,
);
}
} else if !mc.android_bp_unchanged() {
println!("Running cargo_embargo for the migrated crate produced changes to the Android.bp file");
if verbose {
println!("{}", from_utf8(&mc.android_bp_diff().stdout)?);
}
}
let mut diff_cmd = Command::new("diff");
diff_cmd.args(["-u", "-r", "-w", "--no-dereference"]);
if !verbose {
diff_cmd.arg("-q");
}
let diff_status = diff_cmd
.args(IGNORED_FILES.iter().map(|ignored| format!("--exclude={}", ignored)))
.args(["-I", r#"default_team: "trendy_team_android_rust""#])
.arg(mc.android_crate_path().rel())
.arg(mc.staging_path().rel())
.current_dir(self.path.root())
.spawn()?
.wait()?;
if !diff_status.success() {
println!(
"Found differences between {} and {}",
mc.android_crate_path(),
mc.staging_path()
);
}
if verbose {
println!("All diffs:");
Command::new("diff")
.args(["-u", "-r", "-w", "-q", "--no-dereference"])
.arg(mc.android_crate_path().rel())
.arg(mc.staging_path().rel())
.current_dir(self.path.root())
.spawn()?
.wait()?;
}
// Patching and running cargo_embargo *must* succeed. But if we are migrating with a version change,
// there could be some changes to the Android.bp.
if !mc.patch_success()
|| !mc.cargo_embargo_success()
|| (!mc.android_bp_unchanged() && !unpinned)
{
println!("Crate {} is UNHEALTHY", crate_name);
return Err(anyhow!("Crate {} is unhealthy", crate_name));
}
if diff_status.success() && mc.android_bp_unchanged() {
println!("Crate {} is healthy", crate_name);
return Ok(version);
}
if unpinned {
println!("The crate was added with an unpinned version, and diffs were found which must be inspected manually");
return Ok(version);
}
println!("Crate {} is UNHEALTHY", crate_name);
Err(anyhow!("Crate {} is unhealthy", crate_name))
}
pub fn migrate<T: AsRef<str>>(
&self,
crates: Vec<T>,
verbose: bool,
unpinned: &BTreeSet<String>,
versions: &BTreeMap<String, Version>,
) -> Result<()> {
let pseudo_crate = self.pseudo_crate();
for crate_name in &crates {
let crate_name = crate_name.as_ref();
let version = self.migration_health(
crate_name,
verbose,
unpinned.contains(crate_name),
versions.get(crate_name),
)?;
let src_dir = self.legacy_dir_for(
crate_name,
if unpinned.contains(crate_name) { None } else { Some(&version) },
)?;
let monorepo_crate_dir = self.managed_dir();
if !monorepo_crate_dir.abs().exists() {
create_dir(monorepo_crate_dir)?;
}
copy_dir(src_dir, self.managed_dir_for(crate_name))?;
if unpinned.contains(crate_name) {
pseudo_crate.cargo_add_unpinned(&NameAndVersionRef::new(crate_name, &version))?;
} else {
pseudo_crate.cargo_add(&NameAndVersionRef::new(crate_name, &version))?;
}
}
self.regenerate(crates.iter(), false)?;
for crate_name in &crates {
let crate_name = crate_name.as_ref();
let src_dir = self.legacy_dir_for(
crate_name,
if unpinned.contains(crate_name) { None } else { versions.get(crate_name) },
)?;
for entry in glob(
src_dir
.abs()
.join("*.bp")
.to_str()
.ok_or(anyhow!("Failed to convert path *.bp to str"))?,
)? {
remove_file(entry?)?;
}
remove_file(src_dir.join("cargo_embargo.json")?)?;
let test_mapping = src_dir.join("TEST_MAPPING")?;
if test_mapping.abs().exists() {
remove_file(test_mapping)?;
}
write(
src_dir.join("Android.bp")?,
format!("// This crate has been migrated to {}.\n", self.path),
)?;
}
Ok(())
}
pub fn analyze_import(&self, crate_name: &str) -> Result<()> {
if self.contains(crate_name) {
println!("Crate already imported at {}", self.managed_dir_for(crate_name));
return Ok(());
}
let legacy_dir = self.legacy_dir_for(crate_name, None)?;
if legacy_dir.abs().exists() {
println!("Legacy crate already imported at {}", legacy_dir);
return Ok(());
}
let mut managed_crates = self.new_cc();
managed_crates.add_from(self.managed_dir().rel())?;
let legacy_crates = self.legacy_crates()?;
let cio_crate = self.crates_io.get_crate(crate_name)?;
for version in cio_crate.versions() {
println!("Version {}", version.version());
let mut found_problems = false;
for (dep, req) in version.android_deps_with_version_reqs() {
println!("Found dep {}", dep.crate_name());
let cc = if managed_crates.contains_name(dep.crate_name()) {
&managed_crates
} else {
&legacy_crates
};
if !cc.contains_name(dep.crate_name()) {
found_problems = true;
println!(
" Dep {} {} has not been imported to Android",
dep.crate_name(),
dep.requirement()
);
// This is a no-op because our dependency code only considers normal deps anyway.
// TODO: Fix the deps code.
if matches!(dep.kind(), DependencyKind::Dev) {
println!(" But this is a dev dependency, probably only needed if you want to run the tests");
}
if dep.is_optional() {
println!(" But this is an optional dependency, used by the following features: {}", dep.features().join(", "));
}
continue;
}
let versions = cc.get_versions(dep.crate_name()).collect::<Vec<_>>();
let has_matching_version =
versions.iter().any(|(nv, _)| req.matches_relaxed(nv.version()));
if !has_matching_version {
found_problems = true;
}
if !has_matching_version || versions.len() > 1 {
if has_matching_version {
println!(" Dep {} has multiple versions available. You may need to override the default choice in cargo_embargo.json", dep.crate_name());
}
for (_, dep_crate) in versions {
println!(
" Dep {} {} is {}satisfied by v{} at {}",
dep.crate_name(),
dep.requirement(),
if req.matches_relaxed(dep_crate.version()) { "" } else { "not " },
dep_crate.version(),
dep_crate.path()
);
}
}
}
if !found_problems {
println!(" No problems found with this version.")
}
}
Ok(())
}
pub fn import(&self, crate_name: &str, version: &str, autoconfig: bool) -> Result<()> {
if self.contains(crate_name) {
return Err(anyhow!("Crate already imported at {}", self.managed_dir_for(crate_name)));
}
let legacy_dir = self.legacy_dir_for(crate_name, None)?;
if legacy_dir.abs().exists() {
return Err(anyhow!("Legacy crate already imported at {}", legacy_dir));
}
let pseudo_crate = self.pseudo_crate();
let version = Version::parse(version)?;
let nv = NameAndVersionRef::new(crate_name, &version);
pseudo_crate.cargo_add(&nv)?;
let pseudo_crate = pseudo_crate.vendor()?;
let vendored_dir = pseudo_crate.vendored_dir_for(crate_name)?;
let managed_dir = self.managed_dir_for(crate_name);
println!("Creating {} from vendored crate", managed_dir);
copy_dir(vendored_dir, &managed_dir)?;
println!("Sprinkling Android glitter on {}:", crate_name);
let krate = Crate::from(managed_dir.clone())?;
println!(" Finding license files");
let licenses = find_licenses(krate.path().abs(), krate.name(), krate.license())?;
if !licenses.unsatisfied.is_empty() && licenses.satisfied.is_empty() {
let mut satisfied = false;
// Sometimes multiple crates live in a single GitHub repo. A common case
// is a crate with an associated proc_macro crate. In such cases, the individual
// crates are in subdirectories with license files at root of the repo, and
// the license files don't get distributed with the crates.
// So, if we didn't find a license file, try to guess the URL of the appropriate
// license file and download it. This is incredibly hacky, and only supports
// the most common case, which is LICENSE-APACHE.
if licenses.unsatisfied.len() == 1 {
let req = licenses.unsatisfied.first().unwrap();
if let Some(repository) = krate.repository() {
if *req == Licensee::parse("Apache-2.0").unwrap().into_req() {
let url = format!("{}/master/LICENSE-APACHE", repository);
let body = reqwest::blocking::get(
url.replace("github.com", "raw.githubusercontent.com"),
)?
.text()?;
write(krate.path().abs().join("LICENSE"), body)?;
let patch_dir = krate.path().abs().join("patches");
create_dir(&patch_dir)?;
let output = Command::new("diff")
.args(["-u", "/dev/null", "LICENSE"])
.current_dir(krate.path().abs())
.output()?;
write(patch_dir.join("LICENSE.patch"), output.stdout)?;
satisfied = true;
}
}
}
if !satisfied {
return Err(anyhow!(
"Could not find license files for all licenses. Missing {}",
licenses.unsatisfied.iter().join(", ")
));
}
}
// If there's a single applicable license file, symlink it to LICENSE.
if licenses.satisfied.len() == 1 && licenses.unsatisfied.is_empty() {
let license_file = krate.path().join("LICENSE")?;
if !license_file.abs().exists() {
symlink(
licenses.satisfied.iter().next().unwrap().1.file_name().unwrap(),
license_file,
)?;
}
}
update_module_license_files(&krate.path().abs(), &licenses)?;
println!(" Creating METADATA");
let metadata = GoogleMetadata::init(
krate.path().join("METADATA")?,
krate.name(),
krate.version().to_string(),
krate.description(),
most_restrictive_type(&licenses),
)?;
metadata.write()?;
println!(" Creating cargo_embargo.json and Android.bp");
if autoconfig {
// TODO: Copy to a temp dir, because otherwise we might run cargo and create/modify Cargo.lock.
cargo_embargo_autoconfig(&managed_dir)?
.success_or_error()
.context("Failed to generate cargo_embargo.json with 'cargo_embargo autoconfig'")?;
} else {
write(krate.path().abs().join("cargo_embargo.json"), "{}")?;
}
// Workaround. Our logic for crate health assumes the crate isn't healthy if there's
// no Android.bp. So create an empty one.
write(krate.path().abs().join("Android.bp"), "")?;
self.regenerate([&crate_name].iter(), true)?;
println!("Please edit {} and run 'regenerate' for this crate", managed_dir);
// TODO: Create TEST_MAPPING
Ok(())
}
pub fn regenerate<T: AsRef<str>>(
&self,
crates: impl Iterator<Item = T>,
update_metadata: bool,
) -> Result<()> {
let pseudo_crate = self.pseudo_crate().vendor()?;
for crate_name in crates {
println!("Regenerating {}", crate_name.as_ref());
let mc = self.managed_crate_for(crate_name.as_ref())?;
// TODO: Don't give up if there's a failure.
mc.regenerate(update_metadata, &pseudo_crate)?;
}
pseudo_crate.regenerate_crate_list()?;
Ok(())
}
pub fn stage<T: AsRef<str>>(&self, crates: impl Iterator<Item = T>) -> Result<()> {
let pseudo_crate = self.pseudo_crate().vendor()?;
for crate_name in crates {
let mc = self.managed_crate_for(crate_name.as_ref())?.stage(&pseudo_crate)?;
// TODO: Don't give up if there's a failure.
mc.check_staged()?;
}
Ok(())
}
pub fn preupload_check(&self, files: &[String]) -> Result<()> {
let pseudo_crate = self.pseudo_crate().vendor()?;
let deps = pseudo_crate.deps().keys().cloned().collect::<BTreeSet<_>>();
let managed_dirs = self.all_crate_names()?;
if deps != managed_dirs {
return Err(anyhow!("Deps in pseudo_crate/Cargo.toml don't match directories in {}\nDirectories not in Cargo.toml: {}\nCargo.toml deps with no directory: {}",
self.managed_dir(), managed_dirs.difference(&deps).join(", "), deps.difference(&managed_dirs).join(", ")));
}
let crate_list = pseudo_crate.read_crate_list()?;
if deps.iter().ne(crate_list.iter()) {
return Err(anyhow!("Deps in pseudo_crate/Cargo.toml don't match deps in crate-list.txt\nCargo.toml: {}\ncrate-list.txt: {}",
deps.iter().join(", "), crate_list.iter().join(", ")));
}
// Per https://android.googlesource.com/platform/tools/repohooks/,
// the REPO_PATH environment variable is the path of the git repo relative to the
// root of the Android source tree.
let prefix = self.path.rel().strip_prefix(env::var("REPO_PATH")?)?;
let changed_android_crates = files
.iter()
.filter_map(|file| Path::new(file).strip_prefix(prefix).ok())
.filter_map(|path| {
let components = path.components().collect::<Vec<_>>();
if path.starts_with("crates/") && components.len() > 2 {
Some(components[1].as_os_str().to_string_lossy().to_string())
} else {
None
}
})
.collect::<BTreeSet<_>>();
for crate_name in changed_android_crates {
println!("Verifying checksums for {}", crate_name);
checksum::verify(self.managed_dir_for(&crate_name).abs())?;
}
Ok(())
}
pub fn fix_licenses<T: AsRef<str>>(&self, crates: impl Iterator<Item = T>) -> Result<()> {
for crate_name in crates {
self.managed_crate_for(crate_name.as_ref())?.fix_licenses()?;
}
Ok(())
}
pub fn fix_metadata<T: AsRef<str>>(&self, crates: impl Iterator<Item = T>) -> Result<()> {
for crate_name in crates {
self.managed_crate_for(crate_name.as_ref())?.fix_metadata()?;
}
Ok(())
}
pub fn recontextualize_patches<T: AsRef<str>>(
&self,
crates: impl Iterator<Item = T>,
) -> Result<()> {
for crate_name in crates {
let mc = self.managed_crate_for(crate_name.as_ref())?;
mc.recontextualize_patches()?;
}
Ok(())
}
pub fn updatable_crates(&self) -> Result<()> {
let mut cc = self.new_cc();
cc.add_from(self.managed_dir().rel())?;
for krate in cc.map_field().values() {
let cio_crate = self.crates_io.get_crate(krate.name())?;
let upgrades =
cio_crate.versions_gt(krate.version()).map(|v| v.version()).collect::<Vec<_>>();
if !upgrades.is_empty() {
println!(
"{} v{}:\n {}",
krate.name(),
krate.version(),
upgrades
.iter()
.chunks(10)
.into_iter()
.map(|mut c| { c.join(", ") })
.join(",\n ")
);
}
}
Ok(())
}
pub fn analyze_updates(&self, crate_name: impl AsRef<str>) -> Result<()> {
let mut managed_crates = self.new_cc();
managed_crates.add_from(self.managed_dir().rel())?;
let legacy_crates = self.legacy_crates()?;
let krate = self.managed_crate_for(crate_name.as_ref())?;
println!("Analyzing updates to {} v{}", krate.name(), krate.android_version());
let patches = krate.patches()?;
if !patches.is_empty() {
println!("This crate has patches, so expect a fun time trying to update it:");
for patch in patches {
println!(
" {}",
Path::new(patch.file_name().ok_or(anyhow!("No file name"))?).display()
);
}
}
let cio_crate = self.crates_io.get_crate(crate_name)?;
let base_version = cio_crate.get_version(krate.android_version()).ok_or(anyhow!(
"{} v{} not found in crates.io",
krate.name(),
krate.android_version()
))?;
let base_deps = base_version.android_version_reqs_by_name();
let mut newer_versions = cio_crate.versions_gt(krate.android_version()).peekable();
if newer_versions.peek().is_none() {
println!("There are no newer versions of this crate.");
}
for version in newer_versions {
println!("Version {}", version.version());
let mut found_problems = false;
let parsed_version = semver::Version::parse(version.version())?;
if !krate.android_version().is_upgradable_to(&parsed_version) {
found_problems = true;
if !krate.android_version().is_upgradable_to_relaxed(&parsed_version) {
println!(" Not semver-compatible, even by relaxed standards");
} else {
println!(" Semver-compatible, but only by relaxed standards since major version is 0");
}
}
// Check to see if the update has any missing dependencies.
// We try to be a little clever about this in the following ways:
// * Only consider deps that are likely to be relevant to Android. For example, ignore Windows-only deps.
// * If a dep is missing, but the same dep exists for the current version of the crate, it's probably not actually necessary.
// * Use relaxed version requirements, treating 0.x and 0.y as compatible, even though they aren't according to semver rules.
for (dep, req) in version.android_deps_with_version_reqs() {
let cc = if managed_crates.contains_name(dep.crate_name()) {
&managed_crates
} else {
&legacy_crates
};
if !cc.contains_name(dep.crate_name()) {
found_problems = true;
println!(
" Dep {} {} has not been imported to Android",
dep.crate_name(),
dep.requirement()
);
if !dep.is_new_dep(&base_deps) {
println!(" But the current version has the same dependency, and it seems to work");
} else {
continue;
}
}
for (_, dep_crate) in cc.get_versions(dep.crate_name()) {
if !req.matches_relaxed(dep_crate.version()) {
found_problems = true;
println!(
" Dep {} {} is not satisfied by v{} at {}",
dep.crate_name(),
dep.requirement(),
dep_crate.version(),
dep_crate.path()
);
if !dep.is_changed_dep(&base_deps) {
println!(" But the current version has the same dependency and it seems to work.")
}
}
}
}
if !found_problems {
println!(" No problems found with this version.")
}
}
Ok(())
}
pub fn suggest_updates(&self, consider_patched_crates: bool) -> Result<Vec<(String, String)>> {
let mut suggestions = Vec::new();
let mut managed_crates = self.new_cc();
managed_crates.add_from(self.managed_dir().rel())?;
let legacy_crates = self.legacy_crates()?;
for krate in managed_crates.map_field().values() {
let cio_crate = self.crates_io.get_crate(krate.name())?;
let base_version = cio_crate.get_version(krate.version());
if base_version.is_none() {
println!(
"Skipping crate {} v{} because it was not found in crates.io",
krate.name(),
krate.version()
);
continue;
}
let base_version = base_version.unwrap();
let base_deps = base_version.android_version_reqs_by_name();
let patch_dir = krate.path().join("patches").unwrap();
if patch_dir.abs().exists() && !consider_patched_crates {
println!(
"Skipping crate {} v{} because it has patches",
krate.name(),
krate.version()
);
continue;
}
for version in cio_crate.versions_gt(krate.version()).rev() {
let parsed_version = semver::Version::parse(version.version())?;
if !krate.version().is_upgradable_to_relaxed(&parsed_version) {
continue;
}
if !version.android_deps_with_version_reqs().any(|(dep, req)| {
if !dep.is_changed_dep(&base_deps) {
return false;
}
let cc = if managed_crates.contains_name(dep.crate_name()) {
&managed_crates
} else {
&legacy_crates
};
for (_, dep_crate) in cc.get_versions(dep.crate_name()) {
if req.matches_relaxed(dep_crate.version()) {
return false;
}
}
true
}) {
println!(
"Upgrade crate {} v{} to {}",
krate.name(),
krate.version(),
version.version()
);
suggestions.push((krate.name().to_string(), version.version().to_string()));
break;
}
}
}
Ok(suggestions)
}
pub fn update(&self, crate_name: impl AsRef<str>, version: impl AsRef<str>) -> Result<()> {
let pseudo_crate = self.pseudo_crate();
let version = Version::parse(version.as_ref())?;
let nv = NameAndVersionRef::new(crate_name.as_ref(), &version);
pseudo_crate.remove(&crate_name)?;
pseudo_crate.cargo_add(&nv)?;
self.regenerate([&crate_name].iter(), true)?;
Ok(())
}
pub fn try_updates(&self) -> Result<()> {
let output = Command::new("git")
.args(["status", "--porcelain", "."])
.current_dir(&self.path)
.output()?
.success_or_error()?;
if !output.stdout.is_empty() {
return Err(anyhow!("Crate repo {} has uncommitted changes", self.path));
}
for (crate_name, version) in self.suggest_updates(true)? {
println!("Trying to update {} to {}", crate_name, version);
Command::new("git")
.args(["restore", "."])
.current_dir(&self.path)
.output()?
.success_or_error()?;
Command::new("git")
.args(["clean", "-f", "."])
.current_dir(&self.path)
.output()?
.success_or_error()?;
if let Err(e) = self.update(&crate_name, &version) {
println!("Updating {} to {} failed: {}", crate_name, version, e);
continue;
}
let build_result = Command::new("/usr/bin/bash")
.args(["-c", "source build/envsetup.sh && lunch aosp_husky-trunk_staging-eng && cd external/rust && mm"])
.env_remove("OUT_DIR")
.current_dir(self.path.root())
.spawn().context("Failed to spawn mm")?
.wait().context("Failed to wait on mm")?
.success_or_error();
if let Err(e) = build_result {
println!("Faild to build {} {}: {}", crate_name, version, e);
continue;
}
println!("Update {} to {} succeeded", crate_name, version);
}
Ok(())
}
pub fn init(&self) -> Result<()> {
if self.path.abs().exists() {
return Err(anyhow!("{} already exists", self.path));
}
create_dir_all(&self.path).context(format!("Failed to create {}", self.path))?;
let crates_dir = self.path.join("crates")?;
create_dir_all(&crates_dir).context(format!("Failed to create {}", crates_dir))?;
self.pseudo_crate().init()?;
Ok(())
}
pub fn fix_test_mapping<T: AsRef<str>>(&self, crates: impl Iterator<Item = T>) -> Result<()> {
for crate_name in crates {
let mc = self.managed_crate_for(crate_name.as_ref())?;
mc.fix_test_mapping()?;
}
Ok(())
}
pub fn verify_checksums<T: AsRef<str>>(&self, crates: impl Iterator<Item = T>) -> Result<()> {
for krate in crates {
println!("Verifying checksums for {}", krate.as_ref());
checksum::verify(self.managed_dir_for(krate.as_ref()).abs())?;
}
Ok(())
}
}
// Files that are ignored when migrating a crate to the monorepo.
static IGNORED_FILES: &[&str] = &[
".appveyor.yml",
".bazelci",
".bazelignore",
".bazelrc",
".bazelversion",
".buildkite",
".cargo",
".cargo-checksum.json",
".cargo_vcs_info.json",
".circleci",
".cirrus.yml",
".clang-format",
".clang-tidy",
".clippy.toml",
".clog.toml",
".clog.toml",
".codecov.yaml",
".codecov.yml",
".editorconfig",
".envrc",
".gcloudignore",
".gdbinit",
".git",
".git-blame-ignore-revs",
".git-ignore-revs",
".gitallowed",
".gitattributes",
".github",
".gitignore",
".idea",
".ignore",
".istanbul.yml",
".mailmap",
".md-inc.toml",
".mdl-style.rb",
".mdlrc",
".pylintrc",
".pylintrc-examples",
".pylintrc-tests",
".reuse",
".rspec",
".rustfmt.toml",
".shellcheckrc",
".standard-version",
".tarpaulin.toml",
".tokeignore",
".travis.yml",
".versionrc",
".vim",
".vscode",
".yapfignore",
".yardopts",
"BUILD",
"Cargo.lock",
"Cargo.lock.saved",
"Cargo.toml.orig",
"OWNERS",
// Deprecated config file for rules.mk.
"cargo2rulesmk.json",
// cargo_embargo intermediates.
"Android.bp.orig",
"cargo.metadata",
"cargo.out",
"target.tmp",
];