diff --git a/.cargo/config.toml b/.cargo/config.toml index d6815e28..2ce16a9f 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,9 @@ [net] git-fetch-with-cli = true +[alias] +xtask = "run --package xtask --" + [build] rustflags = ["--cfg", "tokio_unstable"] diff --git a/Cargo.lock b/Cargo.lock index 6f9458dd..c5e52b96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -483,11 +483,11 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.12" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5" +checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -715,7 +715,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -1092,7 +1092,7 @@ dependencies = [ "pretty_assertions", "reqwest", "schemars", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", "sled", @@ -1354,6 +1354,20 @@ dependencies = [ "uuid", ] +[[package]] +name = "drift" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43eb40edecda6106744f5e4f3d4dc78b3adf19d3cfb2d81cc4faa007da91e527" +dependencies = [ + "anyhow", + "indexmap", + "openapiv3", + "regex", + "serde", + "serde_json", +] + [[package]] name = "dropshot" version = "0.16.4" @@ -1384,7 +1398,7 @@ dependencies = [ "rustls-pemfile", "schemars", "scopeguard", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", "serde_path_to_error", @@ -1405,6 +1419,49 @@ dependencies = [ "waitgroup", ] +[[package]] +name = "dropshot-api-manager" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f448e29400392b55ed2c0133c79841e1bc1bc771e6e20841cb1a5c70a77ef65" +dependencies = [ + "anyhow", + "atomicwrites", + "camino", + "clap", + "debug-ignore", + "drift", + "dropshot", + "dropshot-api-manager-types", + "fs-err", + "hex", + "indent_write", + "newtype_derive", + "openapiv3", + "owo-colors", + "paste", + "semver 1.0.27", + "serde_json", + "sha2", + "similar", + "supports-color", + "textwrap", + "thiserror 2.0.16", +] + +[[package]] +name = "dropshot-api-manager-types" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b913840b90fcccce6afbdb146acf39aa3243b8510e255a70572e53a964fc96a" +dependencies = [ + "anyhow", + "camino", + "paste", + "semver 1.0.27", + "serde_json", +] + [[package]] name = "dropshot_endpoint" version = "0.16.4" @@ -1414,7 +1471,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_tokenstream", "syn 2.0.106", @@ -1645,6 +1702,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f150ffc8782f35521cec2b23727707cb4045706ba3c854e86bef66b3a8cdbd" +dependencies = [ + "autocfg", +] + [[package]] name = "fs2" version = "0.4.3" @@ -2252,7 +2318,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.0", "system-configuration", "tokio", "tower-service", @@ -2491,13 +2557,14 @@ checksum = "0cfe9645a18782869361d9c8732246be7b410ad4e919d3609ebabdac00ba12c3" [[package]] name = "indexmap" -version = "2.11.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", "hashbrown", "serde", + "serde_core", ] [[package]] @@ -2653,6 +2720,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -2851,7 +2924,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.53.3", ] [[package]] @@ -3017,6 +3090,20 @@ dependencies = [ "serde", ] +[[package]] +name = "maghemite-dropshot-apis" +version = "0.1.0" +dependencies = [ + "anyhow", + "camino", + "clap", + "ddm-api", + "dropshot-api-manager", + "dropshot-api-manager-types", + "mg-api", + "semver 1.0.27", +] + [[package]] name = "managed" version = "0.8.0" @@ -3245,7 +3332,6 @@ dependencies = [ "rand 0.8.5", "rdb", "schemars", - "semver 1.0.26", "serde", "slog", "slog-async", @@ -3447,7 +3533,7 @@ dependencies = [ "parse-display", "regex", "schemars", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", "serde_with", @@ -3695,7 +3781,7 @@ dependencies = [ "regress", "reqwest", "schemars", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_human_bytes", "serde_json", @@ -3759,7 +3845,7 @@ dependencies = [ "futures-util", "hex", "reqwest", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_derive", "serde_json", @@ -4820,7 +4906,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls 0.23.31", - "socket2 0.5.10", + "socket2 0.6.0", "thiserror 2.0.16", "tokio", "tracing", @@ -4857,7 +4943,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.0", "tracing", "windows-sys 0.60.2", ] @@ -5200,7 +5286,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.26", + "semver 1.0.27", ] [[package]] @@ -5362,7 +5448,7 @@ dependencies = [ "chrono", "dyn-clone", "schemars_derive", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", "url", @@ -5430,19 +5516,21 @@ checksum = "d4f410fedcf71af0345d7607d246e7ad15faaadd49d240ee3b24e5dc21a820ac" [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", + "serde_core", ] [[package]] name = "serde" -version = "1.0.219" +version = "1.0.227" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "80ece43fc6fbed4eb5392ab50c07334d3e577cbf40997ee896fe7af40bba4245" dependencies = [ + "serde_core", "serde_derive", ] @@ -5455,11 +5543,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_core" +version = "1.0.227" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a576275b607a2c86ea29e410193df32bc680303c82f31e275bbfcafe8b33be5" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.227" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "51e694923b8824cf0e9b382adf0f60d4e05f348f357b38833a3fa5ed7c2ede04" dependencies = [ "proc-macro2", "quote", @@ -5488,14 +5585,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -5671,6 +5769,9 @@ name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +dependencies = [ + "bstr", +] [[package]] name = "slab" @@ -5921,7 +6022,7 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.106", @@ -6071,6 +6172,15 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + [[package]] name = "swrite" version = "0.1.0" @@ -6836,7 +6946,7 @@ dependencies = [ "hex", "proptest", "schemars", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_human_bytes", "strum 0.26.3", @@ -6907,7 +7017,7 @@ dependencies = [ "quote", "regress", "schemars", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", "syn 2.0.106", @@ -6927,7 +7037,7 @@ dependencies = [ "quote", "regress", "schemars", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", "syn 2.0.106", @@ -6944,7 +7054,7 @@ dependencies = [ "proc-macro2", "quote", "schemars", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", "serde_tokenstream", @@ -6961,7 +7071,7 @@ dependencies = [ "proc-macro2", "quote", "schemars", - "semver 1.0.26", + "semver 1.0.27", "serde", "serde_json", "serde_tokenstream", @@ -7852,6 +7962,16 @@ dependencies = [ "rustix 1.0.8", ] +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "anyhow", + "camino", + "clap", + "tokio", +] + [[package]] name = "xz2" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index 946fc257..0854a11f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ default-members = [ "ddm-admin-client", "ddm-api", "ddm-types", + "dropshot-apis", "bfd", "util", "mg-ddm-verify", @@ -20,6 +21,7 @@ default-members = [ "mgd", "mg-lower", "mg-common", + "xtask", ] members = [ @@ -30,6 +32,7 @@ members = [ "ddm-admin-client", "ddm-api", "ddm-types", + "dropshot-apis", "bfd", "package", "util", @@ -43,6 +46,7 @@ members = [ "mgd", "mg-lower", "mg-common", + "xtask", ] [workspace.dependencies] @@ -57,6 +61,8 @@ serde = { version = "1.0.219", features = ["derive"] } hostname = "0.3" thiserror = "1.0" dropshot = { version = "0.16.4", features = [ "usdt-probes" ] } +dropshot-api-manager = "0.2.1" +dropshot-api-manager-types = "0.2.1" schemars = { version = "0.8", features = [ "uuid1", "chrono" ] } tokio = { version = "1.37", features = ["full"] } serde_repr = "0.1" @@ -81,6 +87,7 @@ pretty-hex = "0.4" pretty_assertions = "1.4" lazy_static = "1.4" sled = "0.34" +camino = "1.2.0" ciborium = "0.2.2" http = "1.3.1" http-body-util = "0.1" diff --git a/ddm/src/bin/ddm-apigen.rs b/ddm/src/bin/ddm-apigen.rs deleted file mode 100644 index 806b9fe8..00000000 --- a/ddm/src/bin/ddm-apigen.rs +++ /dev/null @@ -1,26 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -use anyhow::Result; -use anyhow::anyhow; -use semver::{BuildMetadata, Prerelease, Version}; -use std::fs::File; - -fn main() -> Result<()> { - let api = ddm_api::ddm_admin_api_mod::stub_api_description() - .map_err(|e| anyhow!("{}", e))?; - let openapi = api.openapi( - "DDM Admin", - Version { - major: 0, - minor: 1, - patch: 0, - pre: Prerelease::EMPTY, - build: BuildMetadata::EMPTY, - }, - ); - let mut out = File::create("ddm-admin.json")?; - openapi.write(&mut out)?; - Ok(()) -} diff --git a/dropshot-apis/Cargo.toml b/dropshot-apis/Cargo.toml new file mode 100644 index 00000000..92bdda80 --- /dev/null +++ b/dropshot-apis/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "maghemite-dropshot-apis" +version = "0.1.0" +edition = "2024" +license = "MPL-2.0" + +[dependencies] +anyhow.workspace = true +camino.workspace = true +clap.workspace = true +ddm-api.workspace = true +dropshot-api-manager-types.workspace = true +dropshot-api-manager.workspace = true +mg-api.workspace = true +semver.workspace = true diff --git a/dropshot-apis/src/main.rs b/dropshot-apis/src/main.rs new file mode 100644 index 00000000..4fab35f6 --- /dev/null +++ b/dropshot-apis/src/main.rs @@ -0,0 +1,94 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::process::ExitCode; + +use anyhow::Context; +use camino::Utf8PathBuf; +use clap::Parser; +use ddm_api::*; +use dropshot_api_manager::{Environment, ManagedApiConfig, ManagedApis}; +use dropshot_api_manager_types::{ManagedApiMetadata, Versions}; +use mg_api::*; + +pub fn environment() -> anyhow::Result { + // The workspace root is one level up from this crate's directory. + let workspace_root = Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .to_path_buf(); + let env = Environment::new( + // This is the command used to run the OpenAPI manager. + "cargo xtask openapi".to_owned(), + workspace_root, + // This is the location within the workspace root where the OpenAPI + // documents are stored. + "openapi", + )?; + Ok(env) +} + +/// The list of APIs managed by the OpenAPI manager. +pub fn all_apis() -> anyhow::Result { + let apis = vec![ + ManagedApiConfig { + ident: "ddm-admin", + versions: Versions::Lockstep { + version: semver::Version::new(0, 1, 0), + }, + title: "DDM Admin", + metadata: ManagedApiMetadata { + contact_url: Some("https://oxide.computer"), + contact_email: Some("api@oxide.computer"), + ..Default::default() + }, + api_description: ddm_admin_api_mod::stub_api_description, + extra_validation: None, + }, + ManagedApiConfig { + ident: "mg-admin", + versions: Versions::Lockstep { + version: semver::Version::new(0, 1, 0), + }, + title: "Maghemite Admin", + metadata: ManagedApiMetadata { + contact_url: Some("https://oxide.computer"), + contact_email: Some("api@oxide.computer"), + ..Default::default() + }, + api_description: mg_admin_api_mod::stub_api_description, + extra_validation: None, + }, + ]; + + let apis = ManagedApis::new(apis).context("error creating ManagedApis")?; + Ok(apis) +} + +fn main() -> anyhow::Result { + let app = dropshot_api_manager::App::parse(); + let env = environment()?; + let apis = all_apis()?; + + Ok(app.exec(&env, &apis)) +} + +#[cfg(test)] +mod test { + use dropshot_api_manager::test_util::check_apis_up_to_date; + + use super::*; + + // Also recommended: a test which ensures documents are up-to-date. The + // OpenAPI manager comes with a helper function for this, called + // `check_apis_up_to_date`. + #[test] + fn test_apis_up_to_date() -> anyhow::Result { + let env = environment()?; + let apis = all_apis()?; + + let result = check_apis_up_to_date(&env, &apis)?; + Ok(result.to_exit_code()) + } +} diff --git a/mgd/Cargo.toml b/mgd/Cargo.toml index e5a9c5e5..f8bd9d87 100644 --- a/mgd/Cargo.toml +++ b/mgd/Cargo.toml @@ -31,7 +31,6 @@ omicron-common.workspace = true hostname.workspace = true uuid.workspace = true smf.workspace = true -semver.workspace = true [features] default = ["mg-lower"] diff --git a/mgd/src/admin.rs b/mgd/src/admin.rs index a0af765a..d77d06cf 100644 --- a/mgd/src/admin.rs +++ b/mgd/src/admin.rs @@ -14,11 +14,9 @@ use dropshot::{ use mg_api::*; use mg_common::stats::MgLowerStats; use rdb::{BfdPeerConfig, Db, Prefix}; -use semver::{BuildMetadata, Prerelease, Version}; use slog::o; use slog::{Logger, error, info, warn}; use std::collections::HashMap; -use std::fs::File; use std::net::{IpAddr, Ipv6Addr, SocketAddr}; use std::sync::{Arc, Mutex}; use tokio::task::JoinHandle; @@ -328,19 +326,3 @@ impl MgAdminApi for MgAdminApiImpl { pub fn api_description() -> ApiDescription> { mg_admin_api_mod::api_description::().unwrap() } - -pub fn apigen() { - let api = mg_admin_api_mod::stub_api_description().unwrap(); - let openapi = api.openapi( - "Maghemite Admin", - Version { - major: 0, - minor: 1, - patch: 0, - pre: Prerelease::EMPTY, - build: BuildMetadata::EMPTY, - }, - ); - let mut out = File::create("mg-admin.json").expect("create json api file"); - openapi.write(&mut out).expect("write json api file"); -} diff --git a/mgd/src/main.rs b/mgd/src/main.rs index 9357a307..fc4da1fa 100644 --- a/mgd/src/main.rs +++ b/mgd/src/main.rs @@ -41,8 +41,6 @@ struct Cli { enum Commands { /// Run the mgd routing daemon. Run(RunArgs), - /// Generate the OpenAPI spec for this router. - Apigen, } #[derive(Parser, Debug)] @@ -88,7 +86,6 @@ fn main() { let args = Cli::parse(); match args.command { Commands::Run(run_args) => oxide_tokio_rt::run(run(run_args)), - Commands::Apigen => admin::apigen(), } } diff --git a/openapi/ddm-admin.json b/openapi/ddm-admin.json index 1378bd62..1ce267ae 100644 --- a/openapi/ddm-admin.json +++ b/openapi/ddm-admin.json @@ -2,6 +2,10 @@ "openapi": "3.0.3", "info": { "title": "DDM Admin", + "contact": { + "url": "https://oxide.computer", + "email": "api@oxide.computer" + }, "version": "0.1.0" }, "paths": { diff --git a/openapi/mg-admin.json b/openapi/mg-admin.json index 0ff89cd6..048b680e 100644 --- a/openapi/mg-admin.json +++ b/openapi/mg-admin.json @@ -2,6 +2,10 @@ "openapi": "3.0.3", "info": { "title": "Maghemite Admin", + "contact": { + "url": "https://oxide.computer", + "email": "api@oxide.computer" + }, "version": "0.1.0" }, "paths": { diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 00000000..93ee21fe --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow.workspace = true +camino.workspace = true +clap.workspace = true +tokio.workspace = true diff --git a/xtask/src/external.rs b/xtask/src/external.rs new file mode 100644 index 00000000..f8a5c9de --- /dev/null +++ b/xtask/src/external.rs @@ -0,0 +1,131 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! External xtasks. (extasks?) + +use std::ffi::OsString; +use std::os::unix::process::CommandExt; +use std::process::Command; + +use anyhow::{Context, Result}; +use clap::Parser; + +/// Argument parser for external xtasks. +/// +/// In general we want all developer tasks to be discoverable simply by running +/// `cargo xtask`, but some development tools end up with a particularly +/// large dependency tree. It's not ideal to have to pay the cost of building +/// our release engineering tooling if all the user wants to do is check for +/// workspace dependency issues. +/// +/// `External` provides a pattern for creating xtasks that live in other crates. +/// An external xtask is defined on `crate::Cmds` as a tuple variant containing +/// `External`, which captures all arguments and options (even `--help`) as +/// a `Vec`. The main function then calls `External::exec` with the +/// appropriate bin target name and any additional Cargo arguments. +#[derive(Debug, Parser)] +#[clap( + disable_help_flag(true), + disable_help_subcommand(true), + disable_version_flag(true) +)] +pub struct External { + #[clap(trailing_var_arg(true), allow_hyphen_values(true))] + args: Vec, + + // This stores an in-progress Command builder. `cargo_args` appends args + // to it, and `exec` consumes it. Clap does not treat this as a command + // (`skip`), but fills in this field by calling `new_command`. + #[clap(skip = new_command())] + command: Command, +} + +impl External { + pub fn exec_bin( + self, + package: impl AsRef, + bin_target: impl AsRef, + ) -> Result<()> { + self.exec_common(&[ + "--package", + package.as_ref(), + "--bin", + bin_target.as_ref(), + ]) + } + + fn exec_common(mut self, args: &[&str]) -> Result<()> { + let error = self.command.args(args).arg("--").args(self.args).exec(); + Err(error).context("failed to exec `cargo run`") + } +} + +fn new_command() -> Command { + let mut command = cargo_command(CargoLocation::FromEnv); + command.arg("run"); + command +} + +/// Creates and prepares a `std::process::Command` for the `cargo` executable. +pub fn cargo_command(location: CargoLocation) -> Command { + let mut command = location.resolve(); + + for (key, _) in std::env::vars_os() { + let Some(key) = key.to_str() else { continue }; + if SANITIZED_ENV_VARS.matches(key) { + command.env_remove(key); + } + } + + command +} + +/// How to determine the location of the `cargo` executable. +#[derive(Clone, Copy, Debug)] +pub enum CargoLocation { + /// Use the `CARGO` environment variable, and fall back to `"cargo"` if it + /// is not set. + FromEnv, +} + +impl CargoLocation { + fn resolve(self) -> Command { + match self { + CargoLocation::FromEnv => { + let cargo = std::env::var_os("CARGO") + .unwrap_or_else(|| OsString::from("cargo")); + Command::new(&cargo) + } + } + } +} + +#[derive(Debug)] +struct SanitizedEnvVars { + // At the moment we only ban some prefixes, but we may also want to ban env + // vars by exact name in the future. + prefixes: &'static [&'static str], +} + +impl SanitizedEnvVars { + const fn new() -> Self { + // Remove many of the environment variables set in + // https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts. + // This is done to avoid recompilation with crates like ring between + // `cargo clippy` and `cargo xtask clippy`. (This is really a bug in + // both ring's build script and in Cargo.) + // + // The current list is informed by looking at ring's build script, so + // it's not guaranteed to be exhaustive and it may need to grow over + // time. + let prefixes = &["CARGO_PKG_", "CARGO_MANIFEST_", "CARGO_CFG_"]; + Self { prefixes } + } + + fn matches(&self, key: &str) -> bool { + self.prefixes.iter().any(|prefix| key.starts_with(prefix)) + } +} + +static SANITIZED_ENV_VARS: SanitizedEnvVars = SanitizedEnvVars::new(); diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 00000000..66ca436e --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,40 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/ +// +// Copyright 2025 Oxide Computer Company + +use clap::{Parser, Subcommand}; + +mod external; + +#[derive(Debug, Parser)] +struct Xtasks { + #[command(subcommand)] + subcommand: XtaskCommands, +} + +/// Maghemite xtask support +#[derive(Debug, Subcommand)] +#[clap(name = "xtask")] +enum XtaskCommands { + /// Manage OpenAPI documents + Openapi(external::External), +} + +#[expect( + clippy::disallowed_macros, + reason = "using `#[tokio::main]` in xtasks is fine, as they are not \ + deployed in production" +)] +#[tokio::main] +async fn main() { + let task = Xtasks::parse(); + if let Err(e) = match task.subcommand { + XtaskCommands::Openapi(external) => external + .exec_bin("maghemite-dropshot-apis", "maghemite-dropshot-apis"), + } { + eprintln!("failed: {e}"); + std::process::exit(-1); + } +}