diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68a416372..a0e3522e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,37 @@ jobs: - name: Attempt to install fuelup through fuelup-init.sh run: ./fuelup-init.sh + test-fuelup-nix: + needs: cancel-previous-runs + if: github.event_name != 'release' || github.event.action != 'published' + name: Test fuelup nix + strategy: + matrix: + job: + - os: ubuntu-latest + - os: macos-latest + runs-on: ${{ matrix.job.os }} + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + uses: dtolnay/rust-toolchain@stable + - name: Attempt to install fuelup through cargo + run: cargo install --path . + - uses: cachix/install-nix-action@v22 + with: + nix_path: nixpkgs=channel:nixos-unstable + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + - name: Copy nix.conf to /etc/nix/nix.conf + run: sudo cp -f /etc/nix/nix.conf ./nix.conf + - name: Test fuelup nix install + run: ~/.cargo/bin/fuelup nix install latest + - name: Test fuelup nix list + run: ~/.cargo/bin/fuelup nix list + - name: Test fuelup nix upgrade + run: ~/.cargo/bin/fuelup nix upgrade + - name: Test fuelup nix remove + run: ~/.cargo/bin/fuelup nix remove 0 + cargo-clippy: needs: cancel-previous-runs runs-on: ubuntu-latest @@ -59,7 +90,7 @@ jobs: - name: Install toolchain uses: dtolnay/rust-toolchain@stable - + - uses: Swatinem/rust-cache@v1 - name: Check Clippy Linter @@ -73,10 +104,10 @@ jobs: - name: Install toolchain uses: dtolnay/rust-toolchain@stable - + - name: Check Formatting run: cargo fmt --all -- --check - + cargo-test-workspace: needs: cancel-previous-runs runs-on: ubuntu-latest @@ -85,10 +116,10 @@ jobs: - name: Install toolchain uses: dtolnay/rust-toolchain@stable - + - name: Run tests run: cargo test --locked --workspace - + lint-toml-files: needs: cancel-previous-runs runs-on: ubuntu-latest @@ -99,7 +130,7 @@ jobs: - name: Install toolchain uses: dtolnay/rust-toolchain@stable - + - name: Install Cargo.toml linter uses: baptiste0928/cargo-install@v1 with: @@ -269,16 +300,16 @@ jobs: # Re-generate the channel TOML file - name: Rebuild channel with updated components run: | - mkdir -p ${{ env.LATEST_CHANNEL_DIR }} - CHANNEL_TOML="channel-fuel-latest.toml" + mkdir -p ${{ env.LATEST_CHANNEL_DIR }} + CHANNEL_TOML="channel-fuel-latest.toml" - FORC_VERSION=$(grep -A1 '\[pkg.forc\]' ./gh-pages/channel-fuel-latest.toml | cut -d "\"" -f2) - FUEL_CORE_VERSION=$(grep -A1 '\[pkg.fuel-core\]' ./gh-pages/channel-fuel-latest.toml | cut -d "\"" -f2) + FORC_VERSION=$(grep -A1 '\[pkg.forc\]' ./gh-pages/channel-fuel-latest.toml | cut -d "\"" -f2) + FUEL_CORE_VERSION=$(grep -A1 '\[pkg.fuel-core\]' ./gh-pages/channel-fuel-latest.toml | cut -d "\"" -f2) - PUBLISHED_DATE=$(date +'%Y-%m-%d') - build-channel $CHANNEL_TOML $PUBLISHED_DATE --github-run-id $GITHUB_RUN_ID forc=$FORC_VERSION fuel-core=$FUEL_CORE_VERSION + PUBLISHED_DATE=$(date +'%Y-%m-%d') + build-channel $CHANNEL_TOML $PUBLISHED_DATE --github-run-id $GITHUB_RUN_ID forc=$FORC_VERSION fuel-core=$FUEL_CORE_VERSION - cp $CHANNEL_TOML ${{ env.LATEST_CHANNEL_DIR }} + cp $CHANNEL_TOML ${{ env.LATEST_CHANNEL_DIR }} - name: Deploy latest channel if: ${{ env.LATEST_COMPATIBLE_FORC && env.LATEST_COMPATIBLE_FUEL_CORE }} @@ -288,8 +319,8 @@ jobs: publish_dir: ${{ env.LATEST_CHANNEL_DIR }} keep_files: true destination_dir: ./ - user_name: 'github-actions[bot]' - user_email: 'github-actions[bot]@users.noreply.github.com' + user_name: "github-actions[bot]" + user_email: "github-actions[bot]@users.noreply.github.com" post-release-checks: name: Do post-release checks diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..b48948abd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.showUnlinkedFileNotification": false +} diff --git a/fuelup-init.sh b/fuelup-init.sh index 4630b09be..23b83c73c 100755 --- a/fuelup-init.sh +++ b/fuelup-init.sh @@ -17,6 +17,13 @@ main() { check_cargo_bin forc-lsp check_cargo_bin fuel-core + local _found_nix=false + if check_cmd nix; then + _found_nix=true + else + run_fuel_nix_install_script + fi + get_architecture || return 1 local _arch="$RETVAL" assert_nz "$_arch" "arch" @@ -150,6 +157,10 @@ main() { add_path_message fi + if [ "$_found_nix" = true ]; then + found_nix_message + fi + return "$_retval" } @@ -158,6 +169,8 @@ preinstall_confirmation() { fuelup uses "$FUELUP_DIR" as its home directory to manage the Fuel toolchain, and will install binaries there. +Using the fuelup nix subcommands will manage installed binaries at /nix/store. + To use the toolchain, you will have to configure your PATH, which tells your machine where to locate fuelup and the Fuel toolchain. If permitted, fuelup-init will configure your PATH for you by running the following: @@ -183,6 +196,20 @@ fish_add_path ~/.fuelup/bin EOF } +found_nix_message() { + cat 1>&2 < Result<()> { match command { diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 643631f39..77359b444 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -3,6 +3,7 @@ pub mod completions; pub mod component; pub mod default; pub mod fuelup; +pub mod nix; pub mod show; pub mod toolchain; pub mod update; diff --git a/src/commands/nix/flake_utils.rs b/src/commands/nix/flake_utils.rs new file mode 100644 index 000000000..d02efef78 --- /dev/null +++ b/src/commands/nix/flake_utils.rs @@ -0,0 +1,317 @@ +//! Utility for translating user commands into flake links, +//! and flake links into display messages or internal debug info +//! for handling toolchain or component management for the user automatically. + +use super::{install::NixInstallCommand, list::UnlockedFlakeURL, FUEL_NIX_LINK}; +use crate::commands::fuelup::FuelupCommand; +use anyhow::{bail, Result}; + +/// Handles getting toolchain or component information to translate +/// into fuel.nix flake links and info presented to the user. +pub(crate) trait FlakeLinkInfo { + /// the name of a toolchain or component + fn name(&self) -> String; + fn get_toolchain(&self) -> Result { + FuelToolchain::from_str(self.name()) + } + fn get_component(&self) -> Result<(FuelComponent, FuelToolchain)> { + FuelComponent::from_str_with_toolchain(self.name()) + } + fn is_toolchain(&self) -> bool { + FuelToolchain::from_str(self.name()).is_ok() + } + fn is_component(&self) -> bool { + FuelComponent::from_str_with_toolchain(self.name()).is_ok() + } +} +/// Create toolchain and component links for the fuel.nix flake. +pub(crate) trait CachixLinkGenerator: FlakeLinkInfo { + fn flake_link_toolchain_suffix(&self) -> Result<&str> { + Ok(match self.get_toolchain()? { + FuelToolchain::Latest => "fuel", + FuelToolchain::Nightly => "fuel-nightly", + FuelToolchain::Beta1 => "fuel-beta-1", + FuelToolchain::Beta2 => "fuel-beta-2", + FuelToolchain::Beta3 => "fuel-beta-3", + FuelToolchain::Beta4rc => "fuel-beta-4-rc", + FuelToolchain::Unknown => { + let available_toolchains = DIST_TOOLCHAINS + .iter() + .map(|tc| tc.as_display_str()) + .collect::>() + .join("\n"); + bail!("available distributed toolchains:\n {available_toolchains}\n") + } + }) + } + fn flake_link_component_suffix(&self) -> Result<(&str, &str)> { + let (comp, tool) = self.get_component()?; + let comp = match comp { + FuelComponent::FuelCore => "fuel-core", + FuelComponent::FuelCoreClient => "fuel-core-client", + FuelComponent::FuelIndexer => "fuel-indexer", + FuelComponent::Forc => "forc", + FuelComponent::ForcClient => "forc-client", + FuelComponent::ForcDoc => "forc-doc", + FuelComponent::ForcExplore => "forc-explore", + FuelComponent::ForcFmt => "forc-fmt", + FuelComponent::ForcIndex => "forc-index", + FuelComponent::ForcLsp => "forc-lsp", + FuelComponent::ForcTx => "forc-tx", + FuelComponent::ForcWallet => "forc-wallet", + FuelComponent::SwayVim => "sway-vim", + }; + let tool = match tool { + FuelToolchain::Latest => "", + FuelToolchain::Nightly => "-nightly", + FuelToolchain::Beta1 => "-beta-1", + FuelToolchain::Beta2 => "-beta-2", + FuelToolchain::Beta3 => "-beta-3", + FuelToolchain::Beta4rc => "-beta-4-rc", + FuelToolchain::Unknown => { + let available_toolchains = DIST_TOOLCHAINS + .iter() + .map(|tc| tc.as_display_str()) + .collect::>() + .join("\n"); + bail!("available distributed toolchains:\n {available_toolchains}\n") + } + }; + Ok((comp, tool)) + } + fn flake_toolchain_link(&self) -> Result { + Ok(format!( + "{FUEL_NIX_LINK}#{}", + self.flake_link_toolchain_suffix()? + )) + } + fn flake_component_link(&self) -> Result { + let (comp, tool) = self.flake_link_component_suffix()?; + Ok(format!("{FUEL_NIX_LINK}#{}{}", comp, tool)) + } +} + +impl FlakeLinkInfo for NixInstallCommand { + fn name(&self) -> String { + self.name.clone() + } +} +impl FlakeLinkInfo for UnlockedFlakeURL { + fn name(&self) -> String { + let (comp, _) = split_at_toolchain(self.0.clone()) + .expect("failed to split whitespace of unlocked attribute path"); + if let Some(index) = comp.find(".fuel") { + let (_, comp) = comp.split_at(index); + if comp == ".fuel-" { + // return the full toolchain name + let comp = comp.replace('.', ""); + let comp = comp.replace('-', ""); + comp.to_string() + } else if comp == ".fuel" { + let comp = comp.replace('.', ""); + comp.to_string() + } else { + self.0.clone() + } + } else { + self.0.clone() + } + } +} +impl CachixLinkGenerator for NixInstallCommand {} + +pub(crate) const DIST_TOOLCHAINS: &[FuelToolchain; 6] = &[ + FuelToolchain::Latest, + FuelToolchain::Nightly, + FuelToolchain::Beta1, + FuelToolchain::Beta2, + FuelToolchain::Beta3, + FuelToolchain::Beta4rc, +]; + +#[derive(Eq, PartialEq, Debug, Hash)] +pub(crate) enum FuelToolchain { + Latest, + Nightly, + Beta1, + Beta2, + Beta3, + Beta4rc, + Unknown, +} + +impl FuelToolchain { + fn from_str(s: String) -> Result { + Ok(match s.to_lowercase().as_str() { + "latest" | "fuel" => Self::Latest, + "nightly" | "fuel-nightly" => Self::Nightly, + "beta-1" | "beta1" | "fuel-beta-1" => Self::Beta1, + "beta-2" | "beta2" | "fuel-beta-2" => Self::Beta2, + "beta-3" | "beta3" | "fuel-beta-3" => Self::Beta3, + "beta-4-rc" | "beta-4rc" | "beta4rc" | "fuel-beta-4-rc" => Self::Beta4rc, + _ => { + let available_toolchains = DIST_TOOLCHAINS + .iter() + .map(|tc| tc.as_display_str()) + .collect::>() + .join("\n"); + bail!("available distributed toolchains:\n {available_toolchains}\n") + } + }) + } + pub(crate) fn as_display_str(&self) -> &'static str { + match self { + FuelToolchain::Latest => "- latest", + FuelToolchain::Nightly => "- nightly", + FuelToolchain::Beta1 => "- beta-1", + FuelToolchain::Beta2 => "- beta-2", + FuelToolchain::Beta3 => "- beta-3", + FuelToolchain::Beta4rc => "- beta-4-rc", + FuelToolchain::Unknown => "unknown", + } + } + fn is_latest(&self) -> bool { + *self == FuelToolchain::Latest + } +} +impl From for FuelToolchain { + fn from(s: String) -> Self { + match s.to_lowercase().as_str() { + "latest" => Self::Latest, + "nightly" => Self::Nightly, + "beta-1" | "beta1" => Self::Beta1, + "beta-2" | "beta2" => Self::Beta2, + "beta-3" | "beta3" => Self::Beta3, + "beta-4-rc" | "beta-4rc" | "beta4rc" => Self::Beta4rc, + _ => Self::Unknown, + } + } +} +impl From for &str { + fn from(ft: FuelToolchain) -> &'static str { + match ft { + FuelToolchain::Latest => "latest", + FuelToolchain::Nightly => "nightly", + FuelToolchain::Beta1 => "beta-1", + FuelToolchain::Beta2 => "beta-2", + FuelToolchain::Beta3 => "beta-3", + FuelToolchain::Beta4rc => "beta-4-rc", + FuelToolchain::Unknown => "unknown", + } + } +} + +pub(crate) const DIST_COMPONENTS: &[FuelComponent; 13] = &[ + FuelComponent::FuelCore, + FuelComponent::FuelCoreClient, + FuelComponent::FuelIndexer, + FuelComponent::Forc, + FuelComponent::ForcClient, + FuelComponent::ForcDoc, + FuelComponent::ForcExplore, + FuelComponent::ForcFmt, + FuelComponent::ForcIndex, + FuelComponent::ForcLsp, + FuelComponent::ForcTx, + FuelComponent::ForcWallet, + FuelComponent::SwayVim, +]; + +#[derive(Debug)] +pub(crate) enum FuelComponent { + FuelCore, + FuelCoreClient, + FuelIndexer, + Forc, + ForcClient, + ForcDoc, + ForcExplore, + ForcFmt, + ForcIndex, + ForcLsp, + ForcTx, + ForcWallet, + SwayVim, +} +impl FuelComponent { + fn from_str_with_toolchain(s: String) -> Result<(Self, FuelToolchain)> { + let (raw_comp_str, tool) = split_at_toolchain(s.to_lowercase())?; + // remove the excess '-' between the comp and toolchain vers + let comp_str = if !tool.is_latest() { + let mut comp_str = raw_comp_str.chars(); + comp_str.next_back(); + comp_str.collect::() + } else { + raw_comp_str + }; + let comp = Self::from_str(comp_str)?; + Ok((comp, tool)) + } + + fn from_str(comp_str: String) -> Result { + match comp_str.as_str() { + "fuel-core" => Ok(Self::FuelCore), + "fuel-core-client" => Ok(Self::FuelCoreClient), + "fuel-indexer" => Ok(Self::FuelIndexer), + "forc" => Ok(Self::Forc), + "forc-client" => Ok(Self::ForcClient), + "forc-doc" => Ok(Self::ForcDoc), + "forc-explore" => Ok(Self::ForcExplore), + "forc-fmt" => Ok(Self::ForcFmt), + "forc-index" => Ok(Self::ForcIndex), + "forc-lsp" => Ok(Self::ForcLsp), + "forc-tx" => Ok(Self::ForcTx), + "forc-wallet" => Ok(Self::ForcWallet), + "sway-vim" => Ok(Self::SwayVim), + _ => { + let available_components = DIST_COMPONENTS + .iter() + .map(|comp| comp.as_display_str()) + .collect::>() + .join("\n"); + let available_toolchains = DIST_TOOLCHAINS + .iter() + .map(|tc| tc.as_display_str()) + .collect::>() + .join("\n"); + bail!( + "available distrubuted components:\n {available_components} + +available distributed toolchains:\n {available_toolchains} + +please form a valid component, like so: fuel-core-beta-3" + ) + } + } + } + + pub(crate) fn as_display_str(&self) -> &'static str { + match self { + FuelComponent::FuelCore => "- fuel-core", + FuelComponent::FuelCoreClient => "- fuel-core-client", + FuelComponent::FuelIndexer => "- fuel-indexer", + FuelComponent::Forc => "- forc", + FuelComponent::ForcClient => "- forc-client", + FuelComponent::ForcDoc => "- forc-doc", + FuelComponent::ForcExplore => "- forc-explore", + FuelComponent::ForcFmt => "- forc-fmt", + FuelComponent::ForcIndex => "- forc-index", + FuelComponent::ForcLsp => "- forc-lsp", + FuelComponent::ForcTx => "- forc-tx", + FuelComponent::ForcWallet => "- forc-wallet", + FuelComponent::SwayVim => "- sway-vim", + } + } +} +pub(crate) fn split_at_toolchain(s: String) -> Result<(String, FuelToolchain)> { + let (comp, tool) = if let Some(index) = s.find("beta") { + let (comp, tool) = s.split_at(index); + (comp.into(), FuelToolchain::from_str(tool.to_string())?) + } else if let Some(index) = s.find("nightly") { + let (comp, tool) = s.split_at(index); + (comp.into(), FuelToolchain::from_str(tool.to_string())?) + } else { + (s, FuelToolchain::Latest) + }; + Ok((comp, tool)) +} diff --git a/src/commands/nix/install.rs b/src/commands/nix/install.rs new file mode 100644 index 000000000..ceef2ba22 --- /dev/null +++ b/src/commands/nix/install.rs @@ -0,0 +1,223 @@ +use crate::commands::nix::{ + flake_utils::{CachixLinkGenerator, FlakeLinkInfo, DIST_COMPONENTS, DIST_TOOLCHAINS}, + NIX_CMD, PRIORITY_FLAG, PROFILE_INSTALL_ARGS, +}; +use anyhow::{anyhow, bail, Result}; +use clap::Parser; +use std::{ + fmt::Debug, + io::{BufRead, BufReader}, + process::{Command, Stdio}, + str::SplitWhitespace, + sync::mpsc, + thread, +}; +use tracing::info; + +const NIX_PKG_PRIORITY_MSG: &str = "The conflicting packages have a priority of"; +const NIXOS_PRIORITY_MSG: &str = "have the same priority"; + +#[derive(Debug, Parser)] +pub struct NixInstallCommand { + /// Toolchain or component + pub name: String, + pub verbose: Option, +} + +pub fn nix_install(command: NixInstallCommand) -> Result<()> { + let mut success = false; + let (priority_err, all_errs, link) = if command.is_toolchain() { + info!( + "downloading and installing fuel {} toolchain, this may take a while...", + command.name + ); + let link = command.flake_toolchain_link()?; + let mut priority_err = Vec::new(); + let mut all_errs = Vec::new(); + filter_command( + link.clone(), + command.name.clone(), + &mut priority_err, + &mut all_errs, + true, + ); + + (priority_err.concat(), all_errs.concat(), link) + } else if command.is_component() { + info!( + "downloading and installing component {}, this may take a while...", + command.name + ); + let link = command.flake_component_link()?; + let mut priority_err = Vec::new(); + let mut all_errs = Vec::new(); + filter_command( + link.clone(), + command.name.clone(), + &mut priority_err, + &mut all_errs, + false, + ); + + (priority_err.concat(), all_errs.concat(), link) + } else { + let available_components = DIST_COMPONENTS + .iter() + .map(|comp| comp.as_display_str()) + .collect::>() + .join("\n"); + let available_toolchains = DIST_TOOLCHAINS + .iter() + .map(|tc| tc.as_display_str()) + .collect::>() + .join("\n"); + bail!( + "available distrubuted components:\n {available_components} + +available distributed toolchains:\n {available_toolchains} + +please form a valid component or toolchain, like so: fuel-core-beta-3 or beta-3" + ) + }; + + // hacky way of getting the priority of the package automatically + if !priority_err.is_empty() { + // nix package manager + if let Some(index) = priority_err.find(NIX_PKG_PRIORITY_MSG) { + let (_, err) = priority_err.split_at(index); + let iter = err.split_whitespace(); + success = auto_prioritize_installed_package(iter, 7, link)?; + // nixos + } else if let Some(index) = priority_err.find(NIXOS_PRIORITY_MSG) { + let (_, err) = priority_err.split_at(index); + let iter = err.split_whitespace(); + success = auto_prioritize_installed_package(iter, 4, link)?; + } + } + if success || all_errs.is_empty() { + if command.is_toolchain() { + info!( + "successfully installed {:?} toolchain", + command.get_toolchain()? + ); + } else { + let (component, toolchain) = command.get_component()?; + info!("successfully installed {toolchain:?} component {component:?}"); + } + } else { + info!("{all_errs:?}"); + } + + Ok(()) +} + +/// Execute the `Command` and filter the priority errors so we can handle it for the user automatically. +fn filter_command( + link_clone: String, + command_name: String, + priority_err: &mut Vec, + all_errs: &mut Vec, + is_toolchain_cmd: bool, +) { + let (tx, rx) = mpsc::channel(); + if let Ok(mut child) = Command::new(NIX_CMD) + .args(PROFILE_INSTALL_ARGS) + .arg(link_clone) + .stderr(Stdio::piped()) + .spawn() + .map_err(|err| { + if is_toolchain_cmd { + anyhow!("failed to install fuel {} toolchain: {err}", command_name) + } else { + anyhow!("failed to install component {}: {err}", command_name) + } + }) + { + let handle = thread::spawn(move || { + while let Some(stderr) = child.stderr.take() { + let reader = BufReader::new(stderr); + let mut is_error = false; + for line in reader.lines().flatten() { + if line.contains("error:") || is_error { + is_error = true; + if line.contains(NIXOS_PRIORITY_MSG) || line.contains(NIX_PKG_PRIORITY_MSG) + { + // send to priority err and all err + tx.send((None, Some(line.clone()), Some(line))).unwrap(); + } else { + // send to all err + // useful if the build fails for reasons other than priority + tx.send((None, None, Some(line))).unwrap(); + } + } else { + tx.send((Some(line), None, None)).unwrap(); + } + } + } + }); + + while let Ok((line_opt, priority_err_opt, all_errs_opt)) = rx.recv() { + if let Some(line) = line_opt { + info!("hi: {line}"); + } + if let Some(err) = priority_err_opt { + priority_err.push(err); + } + if let Some(err) = all_errs_opt { + all_errs.push(err); + } + } + handle.join().unwrap(); + } +} + +/// Given an iterator over a priority error message, get the priority for the installed packages +/// and prioritize the newly installed package. +/// +/// This does not incur an overhead since nix will check if the package is already installed. +fn auto_prioritize_installed_package( + mut iter: SplitWhitespace, + msg_len: usize, + link: String, +) -> Result { + for _ in 0..msg_len { + iter.next(); + } + if let Some(given_priority) = iter.next() { + let chars = given_priority.chars(); + if let Ok(current_pkg_priority) = chars + .filter(|c| c.is_ascii_digit()) + .collect::() + .parse::() + { + try_prioritize(current_pkg_priority, link)? + } + } + + Ok(true) +} + +/// `nix profile install --priority` can be negative, so here we just continue to try +/// installing the package with decreasing priority number until the error goes away. +/// +/// There currently isn't a way to check the priority of packages other than the error +/// provided by nix when installing a package that it finds a conflict with. +fn try_prioritize(mut pkg_priority: i32, link: String) -> Result<()> { + pkg_priority -= 1; + let output = Command::new(NIX_CMD) + .args(PROFILE_INSTALL_ARGS) + .arg(PRIORITY_FLAG) + .arg(pkg_priority.to_string()) + .arg(link.clone()) + .output()?; + if !output.stderr.is_empty() { + let stderr_str = String::from_utf8_lossy(&output.stderr); + if stderr_str.contains(NIXOS_PRIORITY_MSG) || stderr_str.contains(NIX_PKG_PRIORITY_MSG) { + // recursively decriment the package priority until the + // newly installed package has the highest priority + try_prioritize(pkg_priority, link)? + } + } + + Ok(()) +} diff --git a/src/commands/nix/list.rs b/src/commands/nix/list.rs new file mode 100644 index 000000000..572bfadd3 --- /dev/null +++ b/src/commands/nix/list.rs @@ -0,0 +1,160 @@ +use super::{ + flake_utils::{split_at_toolchain, FlakeLinkInfo, FuelToolchain}, + NIX_CMD, PROFILE_LIST_ARGS, +}; +use crate::commands::nix::nix_info; +use anyhow::{bail, Result}; +use clap::Parser; +use std::{collections::HashMap, process::Command}; +use tracing::info; + +#[derive(Debug, Parser)] +pub struct NixListCommand; + +/// A binary package installed by the fuel.nix flake. +#[derive(Debug)] +pub(crate) struct NixBinaryInfo { + pub(crate) name: String, + pub(crate) index: u32, + pub(crate) flake_attribute: Option, + pub(crate) unlocked_flake_url: String, + pub(crate) locked_flake_url: String, + pub(crate) nix_store_path: String, +} +impl NixBinaryInfo { + fn new( + name: String, + index: &str, + flake_attribute: Option, + unlocked_flake_url: &str, + locked_flake_url: &str, + nix_store_path: &str, + ) -> Self { + Self { + name, + index: index.parse::().unwrap(), + flake_attribute, + unlocked_flake_url: unlocked_flake_url.into(), + locked_flake_url: unlocked_flake_url.into(), + nix_store_path: nix_store_path.into(), + } + } +} + +/// Used to get the information about a package since it has a +/// more reliable structure than either the flake attribute, which +/// isn't present on my machine (M1 macbook pro, nix (Nix) 2.15.0), +/// or the nix store path which contains a randomly produced hash. +pub(crate) struct UnlockedFlakeURL(pub(crate) String); +impl<'a> From<&'a str> for UnlockedFlakeURL { + fn from(s: &'a str) -> Self { + Self(s.to_string()) + } +} +impl UnlockedFlakeURL { + fn split_at_toolchain(&self) -> Result<(String, FuelToolchain)> { + if let Some(index) = self.0.find(".fuel") { + let (_, tool) = split_at_toolchain(self.0.split_at(index).1.split_at(1).1.to_string()) + .expect("failed to get toolchain from unlocked attribute path"); + Ok((self.name(), tool)) + } else { + bail!("could not get toolchain info from attribute path") + } + } + fn split_at_component(&self) -> Result<(String, FuelToolchain)> { + if let Some(index) = self.0.find(".forc") { + split_at_toolchain(self.0.split_at(index).1.split_at(1).1.to_string()) + } else { + bail!("could not get toolchain info from attribute path") + } + } +} + +#[derive(Debug)] +pub(crate) struct NixBinaryList(pub(crate) HashMap>); + +/// Currently this collects a static 4 values from the stdout string +/// produced by `nix profile list`, however, in some cases we may actually +/// get 5. The four present are the index, unlocked flake link, locked flake link +/// and nix store path. The fifth would come after the index and is the flake +/// attribute which for some reason doesn't show up presently but _could_ in the +/// future. +/// +/// To avoid breakage we could look to collect at every index, then to be sure +/// of each value we can perform checks to see what that data holds. eg, an index +/// can be parsed as an integer, the flake links will start with "github:fuellabs" and +/// the nix store path will start with "nix/store/". +impl From> for NixBinaryList { + fn from(v: Vec) -> Self { + let mut map: HashMap> = HashMap::new(); + let stdout = String::from_utf8_lossy(&v); + let mut stdout_iter = stdout.split_whitespace(); + let mut count = stdout_iter.clone().count(); + let mut outer_vec = Vec::new(); + while count != 0 { + let mut inner_vec = Vec::new(); + for _ in 0..4 { + if let Some(val) = stdout_iter.next() { + inner_vec.push(val); + } + } + outer_vec.push(inner_vec); + count -= 4; + } + for inner_vec in outer_vec.iter() { + let unlocked_attr_path = UnlockedFlakeURL::from(inner_vec[1]); + if unlocked_attr_path.is_toolchain() { + let (name, toolchain) = unlocked_attr_path + .split_at_toolchain() + .expect("failed to get pkg info from unlocked attribute path"); + let nix_bin = NixBinaryInfo::new( + name, + inner_vec[0], + None, /* see comment on this impl */ + inner_vec[1], + inner_vec[2], + inner_vec[3], + ); + + match map.get_mut(&toolchain) { + Some(nix_bins) => nix_bins.push(nix_bin), + None => { + map.insert(toolchain, vec![nix_bin]); + } + } + } else if unlocked_attr_path.is_component() { + let (name, toolchain) = unlocked_attr_path + .split_at_component() + .expect("failed to get pkg info from unlocked attribute path"); + let nix_bin = NixBinaryInfo::new( + name, + inner_vec[0], + None, /* see comment on this impl */ + inner_vec[1], + inner_vec[2], + inner_vec[3], + ); + + match map.get_mut(&toolchain) { + Some(nix_bins) => nix_bins.push(nix_bin), + None => { + map.insert(toolchain, vec![nix_bin]); + } + } + } + } + Self(map) + } +} + +pub fn nix_list(_command: NixListCommand) -> Result<()> { + match Command::new(NIX_CMD).args(PROFILE_LIST_ARGS).output() { + Ok(output) => { + let nix_bin_list = NixBinaryList::from(output.stdout); + dbg!(nix_bin_list); + // nix_info!(output); + Ok(()) + } + Err(err) => bail!("failed to show installed binaries for profile: {err}"), + } +} diff --git a/src/commands/nix/mod.rs b/src/commands/nix/mod.rs new file mode 100644 index 000000000..ef046a6c4 --- /dev/null +++ b/src/commands/nix/mod.rs @@ -0,0 +1,73 @@ +use self::{ + install::{nix_install, NixInstallCommand}, + list::{nix_list, NixListCommand}, + remove::{nix_remove, NixRemoveCommand}, + upgrade::{nix_upgrade, NixUpgradeCommand}, +}; +use anyhow::Result; +use clap::Parser; + +mod flake_utils; +mod install; +mod list; +mod remove; +mod upgrade; + +macro_rules! nix_info { + ($output:expr) => { + if !$output.stdout.is_empty() { + info!("{}", String::from_utf8_lossy(&$output.stdout)); + } + if !$output.stderr.is_empty() { + let err_str = String::from_utf8_lossy(&$output.stderr); + if err_str.contains("error") { + info!( +"fuelup nix encountered an problem, please open an issue at https://github.com/FuelLabs/fuelup/issues/new + +{}", err_str + ); + } else { + info!("{}", err_str); + } + } + }; +} +pub(crate) use nix_info; + +pub(crate) const NIX_CMD: &str = "nix"; +pub(crate) const PROFILE_ARG: &str = "profile"; +pub(crate) const UNLOCKED_FLAKE_REF: &str = ".*"; +pub(crate) const PROFILE_INSTALL_ARGS: &[&str; 2] = &[PROFILE_ARG, "install"]; +pub(crate) const PROFILE_LIST_ARGS: &[&str; 2] = &[PROFILE_ARG, "list"]; +pub(crate) const PROFILE_REMOVE_ARGS: &[&str; 2] = &[PROFILE_ARG, "remove"]; +pub(crate) const PROFILE_UPGRADE_ARGS: &[&str; 2] = &[PROFILE_ARG, "upgrade"]; +pub(crate) const PRIORITY_FLAG: &str = "--priority"; +pub(crate) const FUEL_NIX_LINK: &str = "github:fuellabs/fuel.nix"; + +#[derive(Debug, Parser)] +pub enum NixCommand { + /// Install a distributable toolchain or component. + Install(NixInstallCommand), + /// Uninstall a toolchain or component by providing its index, + /// unlocked attribute path or nix store path. + Remove(NixRemoveCommand), + /// Upgrade installed packages by index or unlocked attribute path + /// with the latest version of the fuel.nix flake. Upgrades all + /// installed packages if no index or path is provided. + Upgrade(NixUpgradeCommand), + /// Lists the installed packages by index, unlocked attribute path, + /// locked attribute path and nix store path, respectively. + List(NixListCommand), +} + +pub fn exec(command: NixCommand) -> Result<()> { + match command { + NixCommand::Install(command) => nix_install(command), + NixCommand::Remove(command) => nix_remove(command), + NixCommand::Upgrade(command) => nix_upgrade(command), + NixCommand::List(_command) => { + nix_list(_command)?; + Ok(()) + } + } +} diff --git a/src/commands/nix/remove.rs b/src/commands/nix/remove.rs new file mode 100644 index 000000000..805142e8a --- /dev/null +++ b/src/commands/nix/remove.rs @@ -0,0 +1,21 @@ +use crate::commands::nix::{nix_info, NIX_CMD, PROFILE_REMOVE_ARGS}; +use anyhow::Result; +use clap::Parser; +use std::process::Command; +use tracing::info; + +#[derive(Debug, Parser)] +pub struct NixRemoveCommand { + pub pkg: String, +} + +pub fn nix_remove(command: NixRemoveCommand) -> Result<()> { + let output = Command::new(NIX_CMD) + .args(PROFILE_REMOVE_ARGS) + .arg(command.pkg) + .output()?; + + nix_info!(output); + + Ok(()) +} diff --git a/src/commands/nix/upgrade.rs b/src/commands/nix/upgrade.rs new file mode 100644 index 000000000..2f46dc6dd --- /dev/null +++ b/src/commands/nix/upgrade.rs @@ -0,0 +1,46 @@ +use crate::commands::nix::{nix_info, NIX_CMD, PROFILE_UPGRADE_ARGS, UNLOCKED_FLAKE_REF}; +use anyhow::Result; +use clap::Parser; +use std::process::{Command, Stdio}; +use tracing::info; + +#[derive(Debug, Parser)] +pub struct NixUpgradeCommand { + pub pkg: Option, +} + +pub fn nix_upgrade(command: NixUpgradeCommand) -> Result<()> { + let output = if let Some(pkg) = command.pkg { + info!("upgrading package {pkg}, this may take a while..."); + Command::new(NIX_CMD) + .args(PROFILE_UPGRADE_ARGS) + .arg(pkg.clone()) + .stdout(Stdio::inherit()) + .spawn()? + .wait()?; + // capture output of the command + // because nix checks if the package is already up to date + // this doesn't incur any extra overhead but + // allows us to manage how errors are presented to users + Command::new(NIX_CMD) + .args(PROFILE_UPGRADE_ARGS) + .arg(pkg) + .output()? + } else { + info!("upgrading installed fuel.nix packages, this may take a while..."); + Command::new(NIX_CMD) + .args(PROFILE_UPGRADE_ARGS) + .arg(UNLOCKED_FLAKE_REF) + .stdout(Stdio::inherit()) + .spawn()? + .wait()?; + Command::new(NIX_CMD) + .args(PROFILE_UPGRADE_ARGS) + .arg(UNLOCKED_FLAKE_REF) + .output()? + }; + + nix_info!(output); + + Ok(()) +} diff --git a/src/commands/toolchain.rs b/src/commands/toolchain.rs index c40bea2cc..34d578d34 100644 --- a/src/commands/toolchain.rs +++ b/src/commands/toolchain.rs @@ -22,7 +22,7 @@ pub enum ToolchainCommand { #[derive(Debug, Parser)] pub struct InstallCommand { - /// Toolchain name [possible values: latest, beta-1, beta-2, beta-3, nightly] + /// Toolchain name [possible values: latest, beta-1, beta-2, beta-3, beta-4-rc, nightly] pub name: String, } diff --git a/src/fuelup_cli.rs b/src/fuelup_cli.rs index efa512698..b6f39f5c2 100644 --- a/src/fuelup_cli.rs +++ b/src/fuelup_cli.rs @@ -1,16 +1,16 @@ use anyhow::Result; use clap::Parser; -use crate::commands::show::ShowCommand; -use crate::commands::{check, completions, component, default, fuelup, show, toolchain, update}; - use crate::commands::check::CheckCommand; use crate::commands::completions::CompletionsCommand; use crate::commands::component::ComponentCommand; use crate::commands::default::DefaultCommand; use crate::commands::fuelup::FuelupCommand; +use crate::commands::nix::{self, NixCommand}; +use crate::commands::show::ShowCommand; use crate::commands::toolchain::ToolchainCommand; use crate::commands::update::UpdateCommand; +use crate::commands::{check, completions, component, default, fuelup, show, toolchain, update}; #[derive(Debug, Parser)] #[clap(name = "fuelup", about = "Fuel Toolchain Manager", version)] @@ -40,6 +40,10 @@ enum Commands { Show(ShowCommand), /// Updates the distributable toolchains, if already installed Update(UpdateCommand), + /// Use the fuel.nix flake to manage toolchains and components. + /// Refer to the fuel.nix book for more info: https://github.com/FuelLabs/fuel.nix/blob/master/book/src/packages.md#packages + #[clap(subcommand)] + Nix(NixCommand), } pub fn fuelup_cli() -> Result<()> { @@ -56,5 +60,6 @@ pub fn fuelup_cli() -> Result<()> { Commands::Show(_command) => show::exec(), Commands::Toolchain(command) => toolchain::exec(command), Commands::Update(_command) => update::exec(), + Commands::Nix(command) => nix::exec(command), } }