| // Copyright 2021 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use anyhow::{bail, Context}; |
| use argh::FromArgs; |
| use serde::Deserialize; |
| use std::{ |
| collections::BTreeSet, |
| env, iter, |
| path::{Path, PathBuf}, |
| str::FromStr, |
| }; |
| use toml_edit::{decorated, Document, Item, Value}; |
| |
| /// update outdated crates in the provided manifest |
| #[derive(Debug, FromArgs)] |
| #[argh(subcommand, name = "update")] |
| pub struct UpdateOptions { |
| /// path to the cargo config file to suppress when running cargo-outdated |
| #[argh(option)] |
| config_path: Option<PathBuf>, |
| |
| /// path to cargo binary |
| #[argh(option)] |
| cargo: PathBuf, |
| |
| /// path to the directory containing the cargo-outdated binary |
| #[argh(option)] |
| outdated_dir: PathBuf, |
| |
| /// tells cargo-outdated not to use the network, only resolving against local files |
| #[argh(switch)] |
| offline: bool, |
| } |
| |
| pub fn update_crates( |
| overrides: PathBuf, |
| manifest_path: PathBuf, |
| UpdateOptions { outdated_dir, cargo, config_path, offline }: UpdateOptions, |
| ) -> anyhow::Result<()> { |
| let mut cargo_toml = toml_edit::Document::from_str( |
| &std::fs::read_to_string(&manifest_path).context("reading cargo.toml")?, |
| ) |
| .context("parsing cargo.toml")?; |
| |
| let overrides = |
| toml::from_str(&std::fs::read_to_string(&overrides).context("reading overrides config")?) |
| .context("parsing overrides config")?; |
| |
| let updates = |
| crates_to_update(&cargo, &outdated_dir, config_path, &manifest_path, overrides, offline) |
| .context("getting list of crates to update")?; |
| |
| for to_update in updates { |
| eprintln!("Updating {:?}", to_update); |
| set_dep_version_to(to_update.get_dep_spec(&mut cargo_toml), &to_update.latest); |
| } |
| |
| let updated_contents = cargo_toml.to_string_in_original_order(); |
| std::fs::write(&manifest_path, updated_contents).context("writing updated contents")?; |
| |
| Ok(()) |
| } |
| |
| fn crates_to_update( |
| cargo: &Path, |
| outdated_dir: &Path, |
| config_path: Option<PathBuf>, |
| manifest_path: &Path, |
| OverrideConfig { skip_updating }: OverrideConfig, |
| offline: bool, |
| ) -> anyhow::Result<Vec<OutdatedCrate>> { |
| let outdated_output = run_cargo_outdated_raw( |
| cargo, |
| outdated_dir, |
| config_path, |
| manifest_path, |
| &skip_updating, |
| offline, |
| ) |
| .context("running cargo-outdated command for its stdout")?; |
| |
| let OutdatedOutput { dependencies: mut crates_to_update } = |
| serde_json::from_slice(&outdated_output).context("parsing cargo-outdated output")?; |
| crates_to_update.iter_mut().for_each(OutdatedCrate::fixup_name); |
| crates_to_update |
| .retain(|krate| krate.latest != "Removed" && !skip_updating.contains(&krate.name)); |
| |
| Ok(crates_to_update) |
| } |
| |
| fn run_cargo_outdated_raw( |
| cargo: &Path, |
| outdated_dir: &Path, |
| config_path: Option<PathBuf>, |
| manifest_path: &Path, |
| skip_updating: &BTreeSet<String>, |
| offline: bool, |
| ) -> anyhow::Result<Vec<u8>> { |
| // move the cargo config to a new spot, move it back when done |
| if let Some(config_path) = &config_path { |
| std::fs::rename(config_path, config_path.with_extension("ignore")) |
| .context("renaming cargo config")?; |
| } |
| scopeguard::defer! { |
| if let Some(config_path) = &config_path { |
| std::fs::rename(config_path.with_extension("ignore"), config_path) |
| .expect("couldn't restore cargo config, repo may be inconsistent"); |
| } |
| }; |
| |
| let mut excluded = String::new(); |
| for (i, krate) in skip_updating.iter().enumerate() { |
| if i > 0 { |
| excluded.push(','); |
| } |
| excluded.push_str(&krate); |
| } |
| |
| let path_with_outdated = env::join_paths( |
| iter::once(outdated_dir.to_path_buf()).chain(env::split_paths(&env::var("PATH").unwrap())), |
| ) |
| .unwrap(); |
| let mut cmd = std::process::Command::new(cargo); |
| cmd.arg("outdated") |
| // TODO(https://fxbug.dev/42160385) remove this flag when we start updating transitive deps |
| .arg("--root-deps-only") // only return things in Cargo.toml explicitly |
| .arg("--verbose") |
| .arg("--manifest-path") |
| .arg(manifest_path) |
| .arg("--format") |
| .arg("json") |
| .env("PATH", path_with_outdated) |
| .current_dir(manifest_path.parent().unwrap()); |
| |
| if !excluded.is_empty() { |
| cmd.arg("--exclude").arg(excluded); |
| } |
| |
| if offline { |
| cmd.arg("--offline"); |
| } |
| |
| let output = cmd.output().context("executing cargo outdated")?; |
| |
| if !output.status.success() { |
| bail!( |
| "cargo-outdated failed ({:?}): \n{}\n{}", |
| output.status, |
| String::from_utf8_lossy(&output.stdout), |
| String::from_utf8_lossy(&output.stderr), |
| ); |
| } |
| |
| Ok(output.stdout) |
| } |
| |
| /// serde type for override configuration |
| #[derive(Debug, Deserialize)] |
| struct OverrideConfig { |
| /// list of crates to skip |
| skip_updating: BTreeSet<String>, |
| } |
| |
| /// outer serde type for extracting what we want from cargo-outdated |
| #[derive(Debug, Deserialize)] |
| struct OutdatedOutput { |
| dependencies: Vec<OutdatedCrate>, |
| } |
| |
| #[derive(Debug, Deserialize)] |
| struct OutdatedCrate { |
| /// name of the crate |
| name: String, |
| /// the version we want to update to |
| latest: String, |
| /// platform cfg's, used for matching toml fields |
| platform: Option<String>, |
| // project: String, |
| // compat: String, |
| // kind: String, |
| } |
| |
| impl OutdatedCrate { |
| /// cargo-outdated gives us names in a format like `parent_crate->middle->actual_dep` where we |
| /// want `actual_dep` to be the name we use for looking everything up in all the metadata. |
| fn fixup_name(&mut self) { |
| self.name = self.name.split("->").last().unwrap().to_owned(); |
| } |
| |
| /// Finds the version string for `self` within `cargo_toml`. |
| fn get_dep_spec<'d>(&self, cargo_toml: &'d mut Document) -> &'d mut Value { |
| let dependencies = if let Some(platform) = &self.platform { |
| // cargo-outdated thinks this dep is in a table like `[target.'cfg(...)'.dependencies]` |
| if let Some(plat) = cargo_toml["target"][platform].as_table_mut() { |
| &mut plat["dependencies"] |
| } else { |
| panic!("cargo-outdated gave us a dep to update that's not in Cargo.toml"); |
| } |
| } else { |
| // cargo-outdated thinks this is in an unqualified block of `[dependencies]` |
| &mut cargo_toml["dependencies"] |
| } |
| .as_table_mut() |
| .expect("all deps from cargo-outdated must be present in Cargo.toml"); |
| |
| match &mut dependencies[&self.name] { |
| Item::Value(v) => v, // the version specifier is the only value for this key, return it |
| Item::Table(t) => { |
| // self is a table like `[dependencies.foo]` and has its own version key |
| t["version"].as_value_mut().expect("valid dep tables must have version keys") |
| } |
| Item::None => panic!("all deps from cargo-outdated must be present in Cargo.toml"), |
| Item::ArrayOfTables(_) => { |
| unreachable!("not valid to have array of tables for dep specs") |
| } |
| } |
| } |
| } |
| |
| fn set_dep_version_to(spec: &mut Value, to: &str) { |
| if let Some(table) = spec.as_inline_table_mut() { |
| let mut version = |
| table.get_mut("version").expect("dep spec tables must have a version key"); |
| // recursing here makes us more permissive than cargo because we'd be ok with |
| // foo = { { { version: "..." }, features: [...] }} |
| set_dep_version_to(&mut version, to); |
| } else { |
| assert!(spec.is_str(), "Dependency specs must be strings or tables."); |
| // just a plain swap is fine here, there are no features to preserve |
| *spec = decorated(Value::from(to.to_owned()), spec.decor().prefix(), spec.decor().suffix()); |
| } |
| } |