From a11837a58c397b5b095f8887b47da75edfb44046 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 23 Mar 2024 15:45:45 +0100 Subject: [PATCH] Initial implementation of github source, add subcommand --- Cargo.lock | 298 +++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 + lib/lib.rs | 1 + lib/sources/artifact.rs | 28 ++++ lib/sources/github.rs | 244 ++++++++++++++++++++++++++++++++ lib/sources/mod.rs | 5 + lib/tool/id.rs | 7 +- src/cli/add.rs | 28 +++- src/main.rs | 29 +++- src/util/id_or_spec.rs | 9 ++ 10 files changed, 637 insertions(+), 15 deletions(-) create mode 100644 lib/sources/artifact.rs create mode 100644 lib/sources/github.rs create mode 100644 lib/sources/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 48e0b01..64b1b42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,7 +42,9 @@ dependencies = [ "dirs", "futures", "itertools", + "octocrab", "reqwest", + "secrecy", "semver", "serde", "serde_json", @@ -55,6 +57,7 @@ dependencies = [ "toml_edit", "tracing", "tracing-subscriber", + "url", "winreg 0.10.1", "zip", ] @@ -137,6 +140,12 @@ version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "async-io" version = "2.3.2" @@ -194,6 +203,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "async-trait" +version = "0.1.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -227,6 +247,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + [[package]] name = "base64ct" version = "1.6.0" @@ -360,7 +386,7 @@ version = "4.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn", @@ -436,6 +462,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -781,8 +817,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -822,6 +860,12 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -919,13 +963,28 @@ dependencies = [ "http", "hyper", "hyper-util", + "log", "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", ] +[[package]] +name = "hyper-timeout" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.3" @@ -1022,6 +1081,16 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "iri-string" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21859b667d66a4c1dacd9df0863b3efb65785474255face87f5bca39dd8407c0" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itertools" version = "0.12.1" @@ -1055,6 +1124,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1173,12 +1257,32 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.18" @@ -1207,12 +1311,57 @@ dependencies = [ "memchr", ] +[[package]] +name = "octocrab" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71940dbb2db7c9884d27c5f14894d14468c92c889f848e2feb4419b4dda1c13d" +dependencies = [ + "arc-swap", + "async-trait", + "base64 0.22.0", + "bytes", + "cfg-if", + "chrono", + "either", + "futures", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "jsonwebtoken", + "once_cell", + "percent-encoding", + "pin-project", + "secrecy", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "snafu", + "tokio", + "tower", + "tower-http", + "tracing", + "url", +] + [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "option-ext" version = "0.2.0" @@ -1277,6 +1426,16 @@ dependencies = [ "sha2", ] +[[package]] +name = "pem" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" +dependencies = [ + "base64 0.21.7", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1435,7 +1594,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58b48d98d932f4ee75e541614d32a7f44c889b72bd9c2e04d95edd135989df88" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "futures-core", "futures-util", @@ -1454,7 +1613,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls", - "rustls-pemfile", + "rustls-pemfile 1.0.4", "rustls-pki-types", "serde", "serde_json", @@ -1519,13 +1678,36 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.1", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab" +dependencies = [ + "base64 0.21.7", + "rustls-pki-types", ] [[package]] @@ -1551,12 +1733,53 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.22" @@ -1597,6 +1820,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.5" @@ -1624,7 +1857,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" dependencies = [ - "base64", + "base64 0.21.7", "chrono", "hex", "indexmap 1.9.3", @@ -1704,6 +1937,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -1719,6 +1964,27 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "snafu" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75976f4748ab44f6e5332102be424e7c2dc18daeaf7e725f2040c3ebb133512e" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b19911debfb8c2fb1107bc6cb2d61868aaf53a988449213959bb1b5b1ed95f" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "socket2" version = "0.5.6" @@ -1958,6 +2224,27 @@ dependencies = [ "pin-project", "pin-project-lite", "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.5.0", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "iri-string", + "pin-project-lite", + "tower", "tower-layer", "tower-service", "tracing", @@ -2097,6 +2384,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 703cc09..8e892a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,10 +19,12 @@ dialoguer = "0.11" dirs = "5.0" futures = "0.3" itertools = "0.12" +octocrab = "0.36" reqwest = { version = "0.12", default-features = false, features = [ "rustls-tls", "http2", ] } +secrecy = "0.8" semver = { version = "1.0", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -34,6 +36,7 @@ toml = "0.8" toml_edit = "0.22" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +url = "2.5" zip = "0.6" [target.'cfg(windows)'.dependencies] diff --git a/lib/lib.rs b/lib/lib.rs index 2695184..d778f7e 100644 --- a/lib/lib.rs +++ b/lib/lib.rs @@ -1,4 +1,5 @@ pub mod description; +pub mod sources; pub mod storage; pub mod system; pub mod tool; diff --git a/lib/sources/artifact.rs b/lib/sources/artifact.rs new file mode 100644 index 0000000..370e739 --- /dev/null +++ b/lib/sources/artifact.rs @@ -0,0 +1,28 @@ +use octocrab::models::repos::Asset; +use url::Url; + +use crate::tool::ToolSpec; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ArtifactProvider { + GitHub, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Artifact { + pub provider: ArtifactProvider, + pub tool_spec: ToolSpec, + pub source_url: Url, + pub download_url: Url, +} + +impl Artifact { + pub(crate) fn from_github_release_asset(asset: &Asset, spec: &ToolSpec) -> Self { + Self { + provider: ArtifactProvider::GitHub, + tool_spec: spec.clone(), + source_url: asset.url.clone(), + download_url: asset.browser_download_url.clone(), + } + } +} diff --git a/lib/sources/github.rs b/lib/sources/github.rs new file mode 100644 index 0000000..17aa026 --- /dev/null +++ b/lib/sources/github.rs @@ -0,0 +1,244 @@ +use std::{ + backtrace::Backtrace, + io::{stdout, IsTerminal}, + time::Duration, +}; + +use octocrab::{models::repos::Release, Error, Octocrab, OctocrabBuilder, Result}; +use reqwest::header::ACCEPT; +use secrecy::{ExposeSecret, SecretString}; +use semver::Version; +use tokio::time::sleep; +use tracing::{info, instrument, trace}; + +use crate::{ + description::Description, + tool::{ToolId, ToolSpec}, +}; + +use super::Artifact; + +const BASE_URI: &str = "https://api.github.com"; + +const ERR_AUTH_UNRECOGNIZED: &str = + "Unrecognized access token format - must begin with `ghp_` or `gho_`."; +const ERR_AUTH_DEVICE_INTERACTIVE: &str = + "Device authentication flow may only be used in an interactive terminal."; + +// NOTE: Users typically install somewhat recent tools, and fetching +// a smaller number of releases here lets us install tools much faster. +const RESULTS_PER_PAGE: u8 = 8; + +pub struct GitHubSource { + client: Octocrab, +} + +impl GitHubSource { + /** + Creates a new GitHub source instance. + + This instance is unauthenticated and may be rate limited and/or unable to access + private resources. To authenticate using an access token, use `new_authenticated`. + */ + pub fn new() -> Result { + let client = crab_builder().build()?; + Ok(Self { client }) + } + + /** + Creates a new authorized GitHub source instance with a personal access token. + + May be used with either personal access tokens or tokens generated using the GitHub device flow. + */ + pub fn new_authenticated(pat: impl AsRef) -> Result { + let pat: String = pat.as_ref().trim().to_string(); + // https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/ + if pat.starts_with("ghp_") { + Ok(Self { + client: crab_builder().personal_token(pat).build()?, + }) + } else if pat.starts_with("gho_") { + Ok(Self { + client: crab_builder().user_access_token(pat).build()?, + }) + } else { + Err(Error::Other { + source: ERR_AUTH_UNRECOGNIZED.into(), + backtrace: Backtrace::capture(), + }) + } + } + + /** + Authenticates with GitHub using the device flow. + + Note that this will emit messages using `info` to guide the + user through the authentication process, and requires user interaction. + If the user does not interact, this will keep polling the GitHub API for a + maximum of 15 minutes (900 seconds) before timing out and returning an error. + + Returns the access token if authentication is successful, but *does not* store it. + A new client instance must be created using `new_authenticated` to use it. + */ + pub async fn auth_with_device(&self, client_id: C, scope: I) -> Result + where + C: Into, + I: IntoIterator, + S: AsRef, + { + if !stdout().is_terminal() { + return Err(Error::Other { + source: ERR_AUTH_DEVICE_INTERACTIVE.into(), + backtrace: Backtrace::capture(), + }); + } + + let client_id = client_id.into(); + let codes = self + .client + .authenticate_as_device(&client_id, scope) + .await?; + + info!( + "Authentication is awaiting your approval.\ + \nPlease visit the authentication page: {}\ + \nAnd enter the verification code: {}", + codes.verification_uri, codes.user_code + ); + + let oauth = loop { + sleep(Duration::from_secs(codes.interval)).await; + let status = codes.poll_once(&self.client, &client_id).await?; + if status.is_left() { + break status.unwrap_left(); + } + }; + + info!("Authentication successful!"); + let token = oauth.access_token.expose_secret().clone(); + + Ok(token) + } + + /** + Fetches a page of releases for a given tool. + */ + #[instrument(skip(self), fields(%tool_id), level = "trace")] + pub async fn get_releases(&self, tool_id: &ToolId, page: u32) -> Result> { + trace!("fetching releases for tool"); + + let repository = self.client.repos(tool_id.author(), tool_id.name()); + let releases = repository + .releases() + .list() + .per_page(RESULTS_PER_PAGE) + .page(page) + .send() + .await?; + + Ok(releases.items) + } + + /** + Fetches a specific release for a given tool. + */ + #[instrument(skip(self), fields(%tool_spec), level = "trace")] + pub async fn find_release(&self, tool_spec: &ToolSpec) -> Result> { + trace!("fetching release for tool"); + + let repository = self.client.repos(tool_spec.author(), tool_spec.name()); + let releases = repository.releases(); + + let tag_with_prefix = format!("v{}", tool_spec.version()); + let tag_without_prefix = tool_spec.version().to_string(); + let (response_with_prefix, response_without_prefix) = tokio::join!( + releases.get_by_tag(&tag_with_prefix), + releases.get_by_tag(&tag_without_prefix), + ); + + if response_with_prefix.is_err() && response_without_prefix.is_err() { + #[allow(clippy::unnecessary_unwrap)] + return Err(response_with_prefix.unwrap_err()); + } + + let opt_with_prefix = response_with_prefix.ok(); + let opt_without_prefix = response_without_prefix.ok(); + Ok(opt_with_prefix.or(opt_without_prefix)) + } + + /** + Finds the latest version of a tool, optionally allowing prereleases. + + If no releases are found, or no non-prerelease releases are found, this will return `None`. + */ + #[instrument(skip(self), fields(%tool_id), level = "trace")] + pub async fn find_latest_version( + &self, + tool_id: &ToolId, + allow_prereleases: bool, + ) -> Result> { + trace!("fetching latest version for tool"); + + let releases = self.get_releases(tool_id, 1).await?; + Ok(releases.into_iter().find_map(|release| { + if allow_prereleases || !release.prerelease { + let version = release.tag_name.trim_start_matches('v'); + Version::parse(version).ok() + } else { + None + } + })) + } + + /** + Finds compatible release artifacts for the given release and description. + + The resulting list of artifacts will be sorted by preferred compatibility. + + See [`Description::is_compatible_with`] and + [`Description::sort_by_preferred_compat`] for more information. + */ + pub fn find_compatible_artifacts( + &self, + tool_spec: &ToolSpec, + release: &Release, + description: &Description, + ) -> Vec { + let mut compatible_artifacts = release + .assets + .iter() + .filter_map(|asset| { + if let Some(asset_desc) = Description::detect(asset.name.as_str()) { + if description.is_compatible_with(&asset_desc) { + let artifact = Artifact::from_github_release_asset(asset, tool_spec); + Some((asset_desc, artifact)) + } else { + None + } + } else { + None + } + }) + .collect::>(); + + compatible_artifacts.sort_by(|(desc_a, _), (desc_b, _)| { + description.sort_by_preferred_compat(desc_a, desc_b) + }); + + compatible_artifacts + .into_iter() + .map(|(_, artifact)| artifact) + .collect() + } +} + +// So generic, such wow + +use octocrab::{DefaultOctocrabBuilderConfig, NoAuth, NoSvc, NotLayerReady}; + +fn crab_builder() -> OctocrabBuilder { + OctocrabBuilder::new() + .base_uri(BASE_URI) + .unwrap() + .add_header(ACCEPT, String::from("application/json")) +} diff --git a/lib/sources/mod.rs b/lib/sources/mod.rs new file mode 100644 index 0000000..aa58578 --- /dev/null +++ b/lib/sources/mod.rs @@ -0,0 +1,5 @@ +mod artifact; +mod github; + +pub use self::artifact::{Artifact, ArtifactProvider}; +pub use self::github::GitHubSource; diff --git a/lib/tool/id.rs b/lib/tool/id.rs index c9e2189..9526964 100644 --- a/lib/tool/id.rs +++ b/lib/tool/id.rs @@ -1,9 +1,10 @@ use std::{fmt, str::FromStr}; +use semver::Version; use serde_with::{DeserializeFromStr, SerializeDisplay}; use thiserror::Error; -use super::util::is_invalid_identifier; +use super::{util::is_invalid_identifier, ToolSpec}; /** Error type representing the possible errors that can occur when parsing a ToolId. @@ -41,6 +42,10 @@ impl ToolId { pub fn name(&self) -> &str { &self.name } + + pub fn into_spec(self, version: Version) -> ToolSpec { + ToolSpec::from((self, version)) + } } impl FromStr for ToolId { diff --git a/src/cli/add.rs b/src/cli/add.rs index d8b1e01..15e43c9 100644 --- a/src/cli/add.rs +++ b/src/cli/add.rs @@ -1,7 +1,8 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use clap::Parser; -use aftman::{storage::Home, tool::ToolAlias}; +use aftman::{sources::GitHubSource, storage::Home, tool::ToolAlias}; +use semver::Version; use crate::util::ToolIdOrSpec; @@ -23,7 +24,28 @@ pub struct AddSubcommand { impl AddSubcommand { pub async fn run(&self, home: &Home) -> Result<()> { - // TODO: Implement the add subcommand + let source = GitHubSource::new()?; + + // If we only got an id without a specified version, we + // will fetch the latest no-prerelease release and use that + let spec = match self.tool_spec.clone() { + ToolIdOrSpec::Spec(spec) => spec, + ToolIdOrSpec::Id(id) => { + let version = source + .find_latest_version(&id, false) + .await? + .with_context(|| format!("No non-prerelease releases were found for {id}"))?; + id.into_spec(version) + } + }; + + // Fetch the release for the tool + let _release = source + .find_release(&spec) + .await? + .with_context(|| format!("No release was found for {spec}"))?; + + // TODO: Add the tool spec to the desired manifest file, and install it Ok(()) } diff --git a/src/main.rs b/src/main.rs index 0bf74a6..237d449 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,12 @@ mod cli; mod util; use cli::Cli; +#[cfg(debug_assertions)] +const FMT_PRETTY: bool = true; + +#[cfg(not(debug_assertions))] +const FMT_PRETTY: bool = false; + #[tokio::main] async fn main() { let tracing_env_filter = EnvFilter::builder() @@ -15,22 +21,33 @@ async fn main() { .from_env_lossy() // Adding the below extra directives will let us debug // aftman easier using RUST_LOG=debug or RUST_LOG=trace + .add_directive("octocrab=info".parse().unwrap()) .add_directive("reqwest=info".parse().unwrap()) .add_directive("rustls=info".parse().unwrap()) + .add_directive("tower=info".parse().unwrap()) .add_directive("hyper=info".parse().unwrap()) .add_directive("h2=info".parse().unwrap()); - tracing_subscriber::fmt() - .with_env_filter(tracing_env_filter) - .with_target(false) - .without_time() - .init(); + // Use the excessively verbose and pretty tracing-subscriber during + // development, and a more concise and less pretty output in production. + if FMT_PRETTY { + tracing_subscriber::fmt() + .with_env_filter(tracing_env_filter) + .pretty() + .init(); + } else { + tracing_subscriber::fmt() + .with_env_filter(tracing_env_filter) + .with_target(false) + .without_time() + .init(); + } if let Err(e) = Cli::parse().run().await { // NOTE: We use tracing for errors here for consistent // output between returned errors, and errors that // may be logged while the program is running. - error!("{e}"); + error!("{e:?}"); exit(1); } } diff --git a/src/util/id_or_spec.rs b/src/util/id_or_spec.rs index e632e0d..03fff23 100644 --- a/src/util/id_or_spec.rs +++ b/src/util/id_or_spec.rs @@ -38,3 +38,12 @@ impl From for ToolIdOrSpec { Self::Spec(spec) } } + +impl From for ToolId { + fn from(id_or_spec: ToolIdOrSpec) -> Self { + match id_or_spec { + ToolIdOrSpec::Id(id) => id, + ToolIdOrSpec::Spec(spec) => spec.into(), + } + } +}