| // Copyright (C) 2023 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::{ |
| cell::OnceCell, |
| collections::{BTreeMap, BTreeSet}, |
| env, |
| fs::{read, write}, |
| io::BufRead, |
| process::Command, |
| str::from_utf8, |
| }; |
| |
| use anyhow::{anyhow, bail, Context, Result}; |
| use itertools::Itertools; |
| use name_and_version::{NameAndVersionMap, NamedAndVersioned}; |
| use rooted_path::RootedPath; |
| use semver::Version; |
| |
| use crate::{crate_collection::CrateCollection, ensure_exists_and_empty, RunQuiet}; |
| |
| pub struct PseudoCrate<State: PseudoCrateState> { |
| path: RootedPath, |
| extra: State, |
| } |
| |
| #[derive(Debug)] |
| pub struct CargoVendorClean { |
| crates: OnceCell<CrateCollection>, |
| deps: OnceCell<BTreeMap<String, Version>>, |
| } |
| #[derive(Debug)] |
| pub struct CargoVendorDirty {} |
| |
| pub trait PseudoCrateState {} |
| impl PseudoCrateState for CargoVendorDirty {} |
| impl PseudoCrateState for CargoVendorClean {} |
| |
| impl<State: PseudoCrateState> PseudoCrate<State> { |
| pub fn get_path(&self) -> &RootedPath { |
| &self.path |
| } |
| pub fn read_crate_list(&self, filename: &str) -> Result<BTreeSet<String>> { |
| let mut lines = BTreeSet::new(); |
| for line in read(self.path.join(filename)?)?.lines() { |
| let line = line?; |
| if !lines.insert(line.clone()) { |
| bail!("Duplicate entry {line} in crate list {filename}"); |
| } |
| } |
| Ok(lines) |
| } |
| } |
| |
| impl PseudoCrate<CargoVendorClean> { |
| pub fn regenerate_crate_list(&self) -> Result<()> { |
| let old_crate_list = self.read_crate_list("crate-list.txt")?; |
| let current_crates = self.deps().keys().cloned().collect::<BTreeSet<_>>(); |
| write( |
| self.path.join("crate-list.txt")?, |
| format!("{}\n", old_crate_list.union(¤t_crates).join("\n")), |
| )?; |
| write( |
| self.path.join("deleted-crates.txt")?, |
| format!("{}\n", old_crate_list.difference(¤t_crates).join("\n")), |
| )?; |
| Ok(()) |
| } |
| fn version_of(&self, crate_name: &str) -> Result<Version> { |
| self.deps() |
| .get(crate_name) |
| .cloned() |
| .ok_or(anyhow!("Crate {} not found in Cargo.toml", crate_name)) |
| } |
| pub fn deps(&self) -> &BTreeMap<String, Version> { |
| self.extra.deps.get_or_init(|| { |
| let output = Command::new("cargo") |
| .args(["tree", "--depth=1", "--prefix=none"]) |
| .current_dir(&self.path) |
| .run_quiet_and_expect_success() |
| .unwrap(); |
| let mut deps = BTreeMap::new(); |
| for line in from_utf8(&output.stdout).unwrap().lines().skip(1) { |
| let words = line.split(' ').collect::<Vec<_>>(); |
| if words.len() < 2 { |
| panic!("Failed to parse crate name and version from cargo tree: {}", line); |
| } |
| let version = words[1] |
| .strip_prefix('v') |
| .ok_or(anyhow!("Failed to parse version: {}", words[1])) |
| .unwrap(); |
| deps.insert(words[0].to_string(), Version::parse(version).unwrap()); |
| } |
| deps |
| }) |
| } |
| pub fn vendored_dir_for(&self, crate_name: &str) -> Result<&RootedPath> { |
| let version = self.version_of(crate_name)?; |
| for (nv, krate) in self.crates().get_versions(crate_name) { |
| if *nv.version() == version { |
| return Ok(krate.path()); |
| } |
| } |
| Err(anyhow!("Couldn't find vendored directory for {} v{}", crate_name, version.to_string())) |
| } |
| fn crates(&self) -> &CrateCollection { |
| self.extra.crates.get_or_init(|| { |
| let out_dir = env::var("OUT_DIR").unwrap_or("out".to_string()); |
| let vendor_dir = self |
| .get_path() |
| .with_same_root(format!("{out_dir}/rust-vendored-crates")) |
| .unwrap() |
| .join(self.get_path().rel()) |
| .unwrap(); |
| Command::new("cargo") |
| .args(["vendor", "--versioned-dirs"]) |
| .arg(vendor_dir.abs()) |
| .current_dir(&self.path) |
| .run_quiet_and_expect_success() |
| .unwrap(); |
| let mut crates = CrateCollection::new(self.path.root()); |
| crates.add_from(vendor_dir).unwrap(); |
| crates |
| }) |
| } |
| fn mark_dirty(self) -> PseudoCrate<CargoVendorDirty> { |
| PseudoCrate { path: self.path, extra: CargoVendorDirty {} } |
| } |
| #[allow(dead_code)] |
| pub fn cargo_add( |
| self, |
| krate: &impl NamedAndVersioned, |
| ) -> Result<PseudoCrate<CargoVendorDirty>> { |
| let dirty = self.mark_dirty(); |
| dirty.cargo_add(krate)?; |
| Ok(dirty) |
| } |
| #[allow(dead_code)] |
| pub fn cargo_add_unpinned( |
| self, |
| krate: &impl NamedAndVersioned, |
| ) -> Result<PseudoCrate<CargoVendorDirty>> { |
| let dirty: PseudoCrate<CargoVendorDirty> = self.mark_dirty(); |
| dirty.cargo_add_unpinned(krate)?; |
| Ok(dirty) |
| } |
| #[allow(dead_code)] |
| pub fn cargo_add_unversioned(self, crate_name: &str) -> Result<PseudoCrate<CargoVendorDirty>> { |
| let dirty: PseudoCrate<CargoVendorDirty> = self.mark_dirty(); |
| dirty.cargo_add_unversioned(crate_name)?; |
| Ok(dirty) |
| } |
| pub fn remove(self, crate_name: &str) -> Result<PseudoCrate<CargoVendorDirty>> { |
| let dirty: PseudoCrate<CargoVendorDirty> = self.mark_dirty(); |
| dirty.remove(crate_name)?; |
| Ok(dirty) |
| } |
| } |
| |
| impl PseudoCrate<CargoVendorDirty> { |
| pub fn new(path: RootedPath) -> Self { |
| PseudoCrate { path, extra: CargoVendorDirty {} } |
| } |
| pub fn init(&self) -> Result<()> { |
| if self.path.abs().exists() { |
| return Err(anyhow!("Can't init pseudo-crate because {} already exists", self.path)); |
| } |
| ensure_exists_and_empty(&self.path)?; |
| |
| write( |
| self.path.join("Cargo.toml")?, |
| r#"[package] |
| name = "android-pseudo-crate" |
| version = "0.1.0" |
| edition = "2021" |
| publish = false |
| license = "Apache-2.0" |
| |
| [dependencies] |
| "#, |
| )?; |
| write(self.path.join("crate-list.txt")?, "")?; |
| write(self.path.join("deleted-crates.txt")?, "")?; |
| write(self.path.join(".gitignore")?, "target/\nvendor/\n")?; |
| |
| ensure_exists_and_empty(&self.path.join("src")?)?; |
| write(self.path.join("src/lib.rs")?, "// Nothing") |
| .context("Failed to create src/lib.rs")?; |
| |
| Ok(()) |
| } |
| fn add_internal(&self, crate_and_version_str: &str, crate_name: &str) -> Result<()> { |
| if let Err(e) = Command::new("cargo") |
| .args(["add", crate_and_version_str]) |
| .current_dir(&self.path) |
| .run_quiet_and_expect_success() |
| { |
| self.remove(crate_name).with_context(|| { |
| format!("Failed to remove {} after failing to add it: {}", crate_name, e) |
| })?; |
| return Err(e); |
| } |
| Ok(()) |
| } |
| pub fn cargo_add(&self, krate: &impl NamedAndVersioned) -> Result<()> { |
| self.add_internal(format!("{}@={}", krate.name(), krate.version()).as_str(), krate.name()) |
| } |
| pub fn cargo_add_unpinned(&self, krate: &impl NamedAndVersioned) -> Result<()> { |
| self.add_internal(format!("{}@{}", krate.name(), krate.version()).as_str(), krate.name()) |
| } |
| pub fn cargo_add_unversioned(&self, crate_name: &str) -> Result<()> { |
| self.add_internal(crate_name, crate_name) |
| } |
| pub fn remove(&self, crate_name: impl AsRef<str>) -> Result<()> { |
| Command::new("cargo") |
| .args(["remove", crate_name.as_ref()]) |
| .current_dir(&self.path) |
| .run_quiet_and_expect_success()?; |
| Ok(()) |
| } |
| // Mark the crate clean. Ironically, we don't actually need to run "cargo vendor" |
| // immediately thanks to LazyCell. |
| pub fn vendor(self) -> Result<PseudoCrate<CargoVendorClean>> { |
| Ok(PseudoCrate { |
| path: self.path, |
| extra: CargoVendorClean { crates: OnceCell::new(), deps: OnceCell::new() }, |
| }) |
| } |
| } |