Repository: FuelLabs/fuelup (rust)

Issue:
refactor fuelup to use channels
With the latest channel now [live on gh-pages](https://github.com/FuelLabs/fuelup/blob/gh-pages/channel-fuel-latest.toml) thanks to #75, `fuelup` can use the TOML file to source download links to the binaries instead of manually building the download links within itself. 

This would mean that the parts of the code that directly queries the GitHub API to get the latest tags and builds the download URLs manually can serve as a fallback instead of being the main method of downloading the toolchain/components. The main method should instead be to download the TOML file linked above, parse the TOML and get the appropriate download link for your desired host system.

This refactor would also serve as a basis to support the other toolchains `nightly` and `stable` - `fuelup` would not and should not contain the logic to decide what makes a `nightly` or `stable` toolchain, and rather would just be told by this TOML file what the correct versions are to download.

Pull request:
use channel over github api
Closes #87 

_Note: we might want to wait for #95 to be approved and merged prior to getting this merged_

Now that we have channels we should move to using them instead of directly forming URLs within the code itself. This also gets rid of the flakiness issue of getting versions from GitHub API, which fails sometimes as reported [here](https://github.com/FuelLabs/fuelup/pull/103#issuecomment-1182953720) as well.

- new `channel.rs` to handle all things `Channel` related. Eventually we should also support `nightly` and `stable` here. There's also a simple test included here to verify correctness of parsing a sample channel (provided within `tests` dir)

- Under this new file there is also the `Package` construct which represent a downloadable component and its different distribution info, represented by a `HashedBinary` which contains the respective `url` and `hash` based on the target architecture. Currently we do not check the hash when we download the file but this can be done in a future PR.

- Introduced new way of creating a `DownloadCfg` from a `Package`. This allows `fuelup` to use the download URL directly without having to manually build and format the URL.

- code changes for existing code has to do with commands `fuelup check` and `fuelup toolchain install latest`. `check` has been updated to parse the versions from the channel toml, while `toolchain install latest` has been updated to use `DownloadCfg::from_package` to directly download from the `url` within the correct `HashedBinary` representation (depending on your machine) 

- There is also some refactoring of existing code (eg. `get_latest_tag()`), mostly to do with `download.rs` to make things smoother when handling both channel usage and github api usage.

- We do not yet deprecate GitHub API support completely for at least a few versions to ensure stability, so we keep them as a fallback for now.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 7540ad94..3087706a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -542,6 +542,9 @@ name = "semver"
 version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "serde"
diff --git a/Cargo.toml b/Cargo.toml
index da768cba..26b75bf5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,7 +22,7 @@ anyhow = "1"
 clap = { version = "3.1", features = ["cargo", "derive", "env"] }
 dirs = "4"
 flate2 = "1"
-semver = "1"
+semver = { version = "1", features = ["serde"] }
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 tar = "0.4"
diff --git a/src/channel.rs b/src/channel.rs
new file mode 100644
index 00000000..0f35daad
--- /dev/null
+++ b/src/channel.rs
@@ -0,0 +1,134 @@
+use crate::{
+    constants::{CHANNEL_LATEST_FILE_NAME, FUELUP_GH_PAGES},
+    download::{download_file, DownloadCfg},
+    file::read_file,
+    path::fuelup_dir,
+    toolchain::DistToolchainName,
+};
+use anyhow::{bail, Result};
+use semver::Version;
+use serde::Deserialize;
+use std::collections::HashMap;
+use tempfile::tempdir_in;
+use toml_edit::de;
+
+#[derive(Debug, Deserialize)]
+pub struct HashedBinary {
+    pub url: String,
+    pub hash: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Channel {
+    pub pkg: HashMap<String, Package>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Package {
+    pub target: HashMap<String, HashedBinary>,
+    pub version: Version,
+}
+
+impl Channel {
+    pub fn from_dist_channel(name: &DistToolchainName) -> Result<Self> {
+        let channel_url = match name {
+            DistToolchainName::Latest => FUELUP_GH_PAGES.to_owned() + CHANNEL_LATEST_FILE_NAME,
+        };
+        let fuelup_dir = fuelup_dir();
+        let tmp_dir = tempdir_in(&fuelup_dir)?;
+        let tmp_dir_path = tmp_dir.path();
+        let toml = match download_file(&channel_url, &tmp_dir_path.join(CHANNEL_LATEST_FILE_NAME)) {
+            Ok(_) => {
+                let toml_path = tmp_dir_path.join(CHANNEL_LATEST_FILE_NAME);
+                read_file(CHANNEL_LATEST_FILE_NAME, &toml_path)?
+            }
+            Err(_) => bail!(
+                "Could not download {} to {}",
+                &channel_url,
+                tmp_dir_path.display()
+            ),
+        };
+
+        Self::from_toml(&toml)
+    }
+
+    pub fn from_toml(toml: &str) -> Result<Self> {
+        let channel: Channel = de::from_str(toml)?;
+        Ok(channel)
+    }
+
+    pub fn build_download_configs(self) -> Vec<DownloadCfg> {
+        let mut cfgs = self
+            .pkg
+            .into_iter()
+            .map(|(name, package)| {
+                DownloadCfg::from_package(&name, package)
+                    .expect("Could not create DownloadCfg from a package parsed in latest channel")
+            })
+            .collect::<Vec<DownloadCfg>>();
+        cfgs.sort_by(|a, b| a.name.cmp(&b.name));
+        cfgs
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{download::DownloadCfg, file::read_file};
+
+    #[test]
+    fn channel_from_toml() {
+        let channel_path = std::env::current_dir()
+            .unwrap()
+            .join("tests/channel-fuel-latest-example.toml");
+        let channel_file = read_file("channel-fuel-latest-example", &channel_path).unwrap();
+        let channel = Channel::from_toml(&channel_file).unwrap();
+
+        assert_eq!(channel.pkg.keys().len(), 2);
+        assert!(channel.pkg.contains_key("forc"));
+        assert_eq!(channel.pkg["forc"].version, Version::new(0, 17, 0));
+        assert!(channel.pkg.contains_key("fuel-core"));
+        assert_eq!(channel.pkg["fuel-core"].version, Version::new(0, 9, 4));
+
+        let targets = &channel.pkg["forc"].target;
+        assert_eq!(targets.len(), 4);
+
+        for target in targets.keys() {
+            assert!(!targets[target].url.is_empty());
+            assert!(!targets[target].hash.is_empty());
+        }
+        assert!(targets.contains_key("darwin_amd64"));
+        assert!(targets.contains_key("darwin_arm64"));
+        assert!(targets.contains_key("linux_amd64"));
+        assert!(targets.contains_key("linux_arm64"));
+
+        let targets = &channel.pkg["fuel-core"].target;
+        assert_eq!(targets.len(), 4);
+
+        for target in targets.keys() {
+            assert!(!targets[target].url.is_empty());
+            assert!(!targets[target].hash.is_empty());
+        }
+        assert!(targets.contains_key("aarch64-apple-darwin"));
+        assert!(targets.contains_key("aarch64-unknown-linux-gnu"));
+        assert!(targets.contains_key("x86_64-apple-darwin"));
+        assert!(targets.contains_key("x86_64-unknown-linux-gnu"));
+    }
+
+    #[test]
+    fn download_cfgs_from_channel() {
+        let channel_path = std::env::current_dir()
+            .unwrap()
+            .join("tests/channel-fuel-latest-example.toml");
+        let channel_file = read_file("channel-fuel-latest-example", &channel_path).unwrap();
+        let channel = Channel::from_toml(&channel_file).unwrap();
+
+        let cfgs: Vec<DownloadCfg> = channel.build_download_configs();
+
+        assert_eq!(cfgs.len(), 2);
+        assert_eq!(cfgs[0].name, "forc");
+        assert_eq!(cfgs[0].version, Version::new(0, 17, 0));
+        assert_eq!(cfgs[1].name, "fuel-core");
+        assert_eq!(cfgs[1].version, Version::new(0, 9, 4));
+    }
+}
diff --git a/src/config.rs b/src/config.rs
index e9f6c90a..b6f5e209 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -4,6 +4,7 @@ use std::path::PathBuf;
 use anyhow::Result;
 use std::io;
 
+use crate::ops::fuelup_toolchain::toolchain;
 use crate::path::toolchain_dir;
 
 pub struct Config {
@@ -17,11 +18,15 @@ impl Config {
         })
     }
 
-    pub(crate) fn list_toolchains(&self) -> Result<Vec<String>> {
+    pub(crate) fn list_official_toolchains(&self) -> Result<Vec<String>> {
         if self.toolchains_dir.is_dir() {
             let toolchains: Vec<String> = fs::read_dir(&self.toolchains_dir)?
                 .filter_map(io::Result::ok)
-                .filter(|e| e.file_type().map(|f| f.is_dir()).unwrap_or(false))
+                .filter(|e| {
+                    e.file_type().map(|f| f.is_dir()).unwrap_or(false)
+                        // TODO: match nightly/stable when channels are available
+                        && e.file_name().to_string_lossy().starts_with(toolchain::LATEST)
+                })
                 .map(|e| e.file_name().into_string().ok().unwrap_or_default())
                 .collect();
             Ok(toolchains)
diff --git a/src/constants.rs b/src/constants.rs
index 0a458c70..95d6e5a9 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -11,3 +11,6 @@ pub const FUELUP_RELEASE_DOWNLOAD_URL: &str =
     "https://github.com/FuelLabs/fuelup/releases/download";
 pub const FUEL_CORE_RELEASE_DOWNLOAD_URL: &str =
     "https://github.com/FuelLabs/fuel-core/releases/download";
+pub const FUELUP_GH_PAGES: &str = "https://raw.githubusercontent.com/FuelLabs/fuelup/gh-pages/";
+
+pub const CHANNEL_LATEST_FILE_NAME: &str = "channel-fuel-latest.toml";
diff --git a/src/download.rs b/src/download.rs
index 4228a0e8..78b56ea9 100644
--- a/src/download.rs
+++ b/src/download.rs
@@ -1,4 +1,4 @@
-use anyhow::{bail, Result};
+use anyhow::{anyhow, bail, Result};
 use flate2::read::GzDecoder;
 use semver::Version;
 use serde::{Deserialize, Serialize};
@@ -10,6 +10,7 @@ use std::{fs, thread};
 use tar::Archive;
 use tracing::{error, info};
 
+use crate::channel::Package;
 use crate::component;
 use crate::constants::{
     FUELUP_RELEASE_DOWNLOAD_URL, FUELUP_REPO, FUEL_CORE_RELEASE_DOWNLOAD_URL, FUEL_CORE_REPO,
@@ -28,51 +29,57 @@ struct LatestReleaseApiResponse {
 #[derive(Debug, PartialEq, Eq)]
 pub struct DownloadCfg {
     pub name: String,
+    pub target: String,
     pub version: Version,
-    release_url: String,
+    tarball_name: String,
+    tarball_url: String,
 }
 
 impl DownloadCfg {
-    pub fn new(name: &str, version: Option<Version>) -> Result<DownloadCfg> {
+    pub fn new(name: &str, target: Option<String>, version: Option<Version>) -> Result<Self> {
+        let version = match version {
+            Some(version) => version,
+            None => get_latest_tag(name).map_err(|e| {
+                anyhow!("Error getting latest tag for component: {:?}: {}", name, e)
+            })?,
+        };
+        let target = match target {
+            Some(target) => target,
+            None => target_from_name(name)?,
+        };
+
+        let release_url = match name {
+            component::FORC => SWAY_RELEASE_DOWNLOAD_URL.to_string(),
+            component::FUEL_CORE => FUEL_CORE_RELEASE_DOWNLOAD_URL.to_string(),
+            component::FUELUP => FUELUP_RELEASE_DOWNLOAD_URL.to_string(),
+            _ => bail!("Unrecognized component: {}", name),
+        };
+        let tarball_name = tarball_name(name, &version, &target)?;
+        let tarball_url = format!("{}/v{}/{}", &release_url, &version, &tarball_name);
+
+        Ok(Self {
+            name: name.to_string(),
+            target,
+            version,
+            tarball_name,
+            tarball_url,
+        })
+    }
+
+    pub fn from_package(name: &str, package: Package) -> Result<Self> {
+        let target = target_from_name(name)?;
         Ok(Self {
             name: name.to_string(),
-            version: match version {
-                Some(version) => version,
-                None => {
-                    let latest_tag_url = match name {
-                        component::FORC => format!(
-                            "{}{}/{}",
-                            GITHUB_API_REPOS_BASE_URL, SWAY_REPO, RELEASES_LATEST
-                        ),
-                        component::FUEL_CORE => format!(
-                            "{}{}/{}",
-                            GITHUB_API_REPOS_BASE_URL, FUEL_CORE_REPO, RELEASES_LATEST
-                        ),
-                        component::FUELUP => format!(
-                            "{}{}/{}",
-                            GITHUB_API_REPOS_BASE_URL, FUELUP_REPO, RELEASES_LATEST
-                        ),
-                        _ => bail!("Unrecognized component: {}", name),
-                    };
-                    if let Ok(result) = get_latest_tag(&latest_tag_url) {
-                        result
-                    } else {
-                        bail!("Error getting latest tag for component: {}", name);
-                    }
-                }
-            },
-            release_url: match name {
-                component::FORC => SWAY_RELEASE_DOWNLOAD_URL.to_string(),
-                component::FUEL_CORE => FUEL_CORE_RELEASE_DOWNLOAD_URL.to_string(),
-                component::FUELUP => FUELUP_RELEASE_DOWNLOAD_URL.to_string(),
-                _ => bail!("Unrecognized component: {}", name),
-            },
+            target: target.clone(),
+            version: package.version.clone(),
+            tarball_name: tarball_name(name, &package.version, &target)?,
+            tarball_url: package.target[&target].url.clone(),
         })
     }
 }
 
-pub fn tarball_name(download_cfg: &DownloadCfg) -> Result<String> {
-    match download_cfg.name.as_ref() {
+pub fn target_from_name(name: &str) -> Result<String> {
+    match name {
         component::FORC => {
             let os = match std::env::consts::OS {
                 "macos" => "darwin",
@@ -85,7 +92,7 @@ pub fn tarball_name(download_cfg: &DownloadCfg) -> Result<String> {
                 unsupported_arch => bail!("Unsupported architecture: {}", unsupported_arch),
             };
 
-            Ok(format!("forc-binaries-{}_{}.tar.gz", os, architecture))
+            Ok(format!("{}_{}", os, architecture))
         }
 
         component::FUEL_CORE => {
@@ -105,13 +112,7 @@ pub fn tarball_name(download_cfg: &DownloadCfg) -> Result<String> {
                 unsupported_os => bail!("Unsupported os: {}", unsupported_os),
             };
 
-            Ok(format!(
-                "fuel-core-{}-{}-{}-{}.tar.gz",
-                &download_cfg.version.to_string(),
-                architecture,
-                vendor,
-                os
-            ))
+            Ok(format!("{}-{}-{}", architecture, vendor, os))
         }
         component::FUELUP => {
             let architecture = match std::env::consts::ARCH {
@@ -130,21 +131,39 @@ pub fn tarball_name(download_cfg: &DownloadCfg) -> Result<String> {
                 unsupported_os => bail!("Unsupported os: {}", unsupported_os),
             };
 
-            Ok(format!(
-                "fuelup-{}-{}-{}-{}.tar.gz",
-                &download_cfg.version.to_string(),
-                architecture,
-                vendor,
-                os
-            ))
+            Ok(format!("{}-{}-{}", architecture, vendor, os))
         }
-        _ => bail!("Unrecognized component: {}", download_cfg.name),
+        _ => bail!("Unrecognized component: {}", name),
     }
 }
 
-pub fn get_latest_tag(github_api_url: &str) -> Result<Version> {
+pub fn tarball_name(name: &str, version: &Version, target: &str) -> Result<String> {
+    match name {
+        component::FORC => Ok(format!("forc-binaries-{}.tar.gz", target)),
+        component::FUEL_CORE => Ok(format!("fuel-core-{}-{}.tar.gz", version, target)),
+        component::FUELUP => Ok(format!("fuelup-{}-{}.tar.gz", version, target)),
+        _ => bail!("Unrecognized component: {}", name),
+    }
+}
+
+pub fn get_latest_tag(name: &str) -> Result<Version> {
+    let latest_tag_url = match name {
+        component::FORC => format!(
+            "{}{}/{}",
+            GITHUB_API_REPOS_BASE_URL, SWAY_REPO, RELEASES_LATEST
+        ),
+        component::FUEL_CORE => format!(
+            "{}{}/{}",
+            GITHUB_API_REPOS_BASE_URL, FUEL_CORE_REPO, RELEASES_LATEST
+        ),
+        component::FUELUP => format!(
+            "{}{}/{}",
+            GITHUB_API_REPOS_BASE_URL, FUELUP_REPO, RELEASES_LATEST
+        ),
+        _ => bail!("Unrecognized component: {}", name),
+    };
     let handle = ureq::builder().user_agent("fuelup").build();
-    let resp = handle.get(github_api_url).call()?;
+    let resp = handle.get(&latest_tag_url).call()?;
 
     let mut data = Vec::new();
     resp.into_reader().read_to_end(&mut data)?;
@@ -215,20 +234,14 @@ pub fn download_file(url: &str, path: &PathBuf) -> Result<()> {
 }
 
 pub fn download_file_and_unpack(download_cfg: &DownloadCfg, dst_dir_path: &Path) -> Result<()> {
-    let tarball_name = tarball_name(download_cfg)?;
-    let tarball_url = format!(
-        "{}/v{}/{}",
-        &download_cfg.release_url, &download_cfg.version, &tarball_name
-    );
-
-    info!("Fetching binary from {}", &tarball_url);
+    info!("Fetching binary from {}", &download_cfg.tarball_url);
 
-    let tarball_path = dst_dir_path.join(&tarball_name);
+    let tarball_path = dst_dir_path.join(&download_cfg.tarball_name);
 
-    if download_file(&tarball_url, &tarball_path).is_err() {
+    if download_file(&download_cfg.tarball_url, &tarball_path).is_err() {
         bail!(
             "Failed to download {} - the release might not be ready yet.",
-            &tarball_name
+            &download_cfg.tarball_name
         );
     };
 
diff --git a/src/lib.rs b/src/lib.rs
index 26e39184..4fe30988 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,4 @@
+pub mod channel;
 pub mod commands;
 pub mod component;
 pub mod config;
diff --git a/src/ops/fuelup_check.rs b/src/ops/fuelup_check.rs
index 12071036..7f6cf79b 100644
--- a/src/ops/fuelup_check.rs
+++ b/src/ops/fuelup_check.rs
@@ -1,12 +1,16 @@
 use std::collections::HashMap;
 use std::io::Write;
+use std::str::FromStr;
+use tracing::error;
 
 use crate::{
+    channel::Channel,
     commands::check::CheckCommand,
     component::SUPPORTED_PLUGINS,
     config::Config,
+    download::target_from_name,
     fmt::{bold, colored_bold},
-    toolchain::Toolchain,
+    toolchain::{DistToolchainName, Toolchain},
 };
 use anyhow::Result;
 use semver::Version;
@@ -14,6 +18,15 @@ use termcolor::Color;
 
 use crate::{component, download::DownloadCfg};
 
+fn collect_versions(channel: Channel) -> HashMap<String, Version> {
+    let mut latest_versions: HashMap<String, Version> = HashMap::new();
+    for (name, package) in channel.pkg.into_iter() {
+        latest_versions.insert(name, package.version);
+    }
+
+    latest_versions
+}
+
 fn check_plugin(toolchain: &Toolchain, plugin: &str, latest_version: &Version) -> Result<()> {
     let plugin_executable = toolchain.path.join(&plugin);
     match std::process::Command::new(&plugin_executable)
@@ -21,22 +34,25 @@ fn check_plugin(toolchain: &Toolchain, plugin: &str, latest_version: &Version) -
         .output()
     {
         Ok(o) => {
-            let version = Version::parse(
-                String::from_utf8_lossy(&o.stdout)
-                    .split_whitespace()
-                    .nth(1)
-                    .expect("expected version"),
-            )?;
-            print!("    - ");
-            bold(|s| write!(s, "{}", plugin));
-            print!(" - ");
-            if &version == latest_version {
-                colored_bold(Color::Green, |s| write!(s, "Up to date"));
-                println!(" : {}", version);
-            } else {
-                colored_bold(Color::Yellow, |s| write!(s, "Update available"));
-                println!(" : {} -> {}", version, latest_version);
-            }
+            let output = String::from_utf8_lossy(&o.stdout).into_owned();
+            match output.split_whitespace().nth(1) {
+                Some(v) => {
+                    let version = Version::parse(v)?;
+                    print!("    - ");
+                    bold(|s| write!(s, "{}", plugin));
+                    print!(" - ");
+                    if &version == latest_version {
+                        colored_bold(Color::Green, |s| write!(s, "Up to date"));
+                        println!(" : {}", version);
+                    } else {
+                        colored_bold(Color::Yellow, |s| write!(s, "Update available"));
+                        println!(" : {} -> {}", version, latest_version);
+                    }
+                }
+                None => {
+                    eprintln!("    - {} - Error getting version string", plugin);
+                }
+            };
         }
         Err(e) => {
             print!("    - ");
@@ -52,79 +68,109 @@ fn check_plugin(toolchain: &Toolchain, plugin: &str, latest_version: &Version) -
     Ok(())
 }
 
-pub fn check(command: CheckCommand) -> Result<()> {
-    let CheckCommand { verbose } = command;
-
-    let cfg = Config::from_env()?;
-    let toolchains = cfg.list_toolchains()?;
-    let mut latest_versions: HashMap<String, Version> = HashMap::new();
-    let components = [component::FORC, component::FUEL_CORE, component::FUELUP];
+fn check_fuelup() -> Result<()> {
+    const FUELUP_VERSION: &str = clap::crate_version!();
 
-    for &component in &components {
-        let download_cfg: DownloadCfg = DownloadCfg::new(component, None)?;
-        latest_versions.insert(component.to_string(), download_cfg.version);
+    if let Ok(fuelup_download_cfg) = DownloadCfg::new(
+        component::FUELUP,
+        target_from_name(component::FUELUP).ok(),
+        None,
+    ) {
+        bold(|s| write!(s, "{} - ", component::FUELUP));
+        if FUELUP_VERSION == fuelup_download_cfg.version.to_string() {
+            colored_bold(Color::Green, |s| write!(s, "Up to date"));
+            println!(" : {}", FUELUP_VERSION);
+        } else {
+            colored_bold(Color::Yellow, |s| write!(s, "Update available"));
+            println!(" : {} -> {}", FUELUP_VERSION, fuelup_download_cfg.version);
+        };
+    } else {
+        error!("Failed to create DownloadCfg for component 'fuelup'; skipping check for 'fuelup'");
     }
+    Ok(())
+}
 
-    for toolchain in toolchains {
-        let toolchain = Toolchain::from(&toolchain);
-        bold(|s| writeln!(s, "{}", &toolchain.name));
-        for component in [component::FORC, component::FUEL_CORE] {
-            let component_executable = toolchain.path.join(component);
+fn check_toolchain(toolchain: &str, verbose: bool) -> Result<()> {
+    let latest_versions = match Channel::from_dist_channel(&DistToolchainName::from_str(toolchain)?)
+    {
+        Ok(c) => collect_versions(c),
+        Err(e) => {
+            error!(
+                "Failed to get latest channel {} - fetching versions using GitHub API",
+                e
+            );
+            [component::FORC, component::FUEL_CORE, component::FUELUP]
+                .iter()
+                .map(|&c| {
+                    (
+                        c.to_owned(),
+                        DownloadCfg::new(c, target_from_name(c).ok(), None)
+                            .unwrap()
+                            .version,
+                    )
+                })
+                .collect()
+        }
+    };
 
-            match std::process::Command::new(&component_executable)
-                .arg("--version")
-                .output()
-            {
-                Ok(o) => {
-                    let version = Version::parse(
-                        String::from_utf8_lossy(&o.stdout)
-                            .split_whitespace()
-                            .nth(1)
-                            .expect("expected version"),
-                    )?;
+    let toolchain = Toolchain::from(toolchain)?;
+    bold(|s| writeln!(s, "{}", &toolchain.name));
+    for component in [component::FORC, component::FUEL_CORE] {
+        let component_executable = toolchain.path.join(component);
 
-                    bold(|s| write!(s, "  {} - ", &component));
-                    if version == latest_versions[component] {
-                        colored_bold(Color::Green, |s| write!(s, "Up to date"));
-                        println!(" : {}", version);
-                    } else {
-                        colored_bold(Color::Yellow, |s| write!(s, "Update available"));
-                        println!(" : {} -> {}", version, latest_versions[component]);
+        match std::process::Command::new(&component_executable)
+            .arg("--version")
+            .output()
+        {
+            Ok(o) => {
+                let output = String::from_utf8_lossy(&o.stdout).into_owned();
+                match output.split_whitespace().nth(1) {
+                    Some(v) => {
+                        let version = Version::parse(v)?;
+                        bold(|s| write!(s, "  {} - ", &component));
+                        if version == latest_versions[component] {
+                            colored_bold(Color::Green, |s| write!(s, "Up to date"));
+                            println!(" : {}", version);
+                        } else {
+                            colored_bold(Color::Yellow, |s| write!(s, "Update available"));
+                            println!(" : {} -> {}", version, latest_versions[component]);
+                        }
                     }
-                }
-                Err(e) => {
-                    print!("  ");
-                    bold(|s| write!(s, "{}", &component));
-                    print!(" - ");
-                    if component_executable.exists() {
-                        println!("execution error - {}", e);
-                    } else {
-                        println!("not found");
+                    None => {
+                        eprintln!("  {} - Error getting version string", component);
                     }
+                };
+            }
+            Err(e) => {
+                print!("  ");
+                bold(|s| write!(s, "{}", &component));
+                print!(" - ");
+                if component_executable.exists() {
+                    println!("execution error - {}", e);
+                } else {
+                    println!("not found");
                 }
-            };
+            }
+        };
 
-            if verbose && component == component::FORC {
-                for plugin in SUPPORTED_PLUGINS {
-                    check_plugin(&toolchain, plugin, &latest_versions[component::FORC])?;
-                }
+        if verbose && component == component::FORC {
+            for plugin in SUPPORTED_PLUGINS {
+                check_plugin(&toolchain, plugin, &latest_versions[component::FORC])?;
             }
         }
     }
+    Ok(())
+}
 
-    const FUELUP_VERSION: &str = clap::crate_version!();
+pub fn check(command: CheckCommand) -> Result<()> {
+    let CheckCommand { verbose } = command;
 
-    bold(|s| write!(s, "{} - ", component::FUELUP));
-    if FUELUP_VERSION == latest_versions[component::FUELUP].to_string() {
-        colored_bold(Color::Green, |s| write!(s, "Up to date"));
-        println!(" : {}", FUELUP_VERSION);
-    } else {
-        colored_bold(Color::Yellow, |s| write!(s, "Update available"));
-        println!(
-            " : {} -> {}",
-            FUELUP_VERSION,
-            latest_versions[component::FUELUP]
-        );
-    };
+    let cfg = Config::from_env()?;
+
+    for toolchain in cfg.list_official_toolchains()? {
+        check_toolchain(&toolchain, verbose)?;
+    }
+
+    check_fuelup()?;
     Ok(())
 }
diff --git a/src/ops/fuelup_self.rs b/src/ops/fuelup_self.rs
index b6d9fb8a..88e342d8 100644
--- a/src/ops/fuelup_self.rs
+++ b/src/ops/fuelup_self.rs
@@ -5,7 +5,7 @@ use tracing::{error, info};
 
 use crate::{
     component,
-    download::{download_file_and_unpack, unpack_bins, DownloadCfg},
+    download::{download_file_and_unpack, target_from_name, unpack_bins, DownloadCfg},
     path::{fuelup_bin, fuelup_bin_dir},
 };
 
@@ -17,7 +17,11 @@ pub fn attempt_install_self(download_cfg: DownloadCfg, dst: &Path) -> Result<()>
 }
 
 pub fn self_update() -> Result<()> {
-    let download_cfg = DownloadCfg::new(component::FUELUP, None)?;
+    let download_cfg = DownloadCfg::new(
+        component::FUELUP,
+        target_from_name(component::FUELUP).ok(),
+        None,
+    )?;
 
     let fuelup_bin = fuelup_bin();
     let fuelup_bin_dir = fuelup_bin_dir();
diff --git a/src/ops/fuelup_toolchain.rs b/src/ops/fuelup_toolchain.rs
index 52389429..9b219624 100644
--- a/src/ops/fuelup_toolchain.rs
+++ b/src/ops/fuelup_toolchain.rs
@@ -1,9 +1,9 @@
-use crate::commands::toolchain::InstallCommand;
 use crate::component;
-use crate::download::DownloadCfg;
+use crate::download::{target_from_name, DownloadCfg};
 use crate::path::settings_file;
 use crate::settings::SettingsFile;
-use crate::toolchain::Toolchain;
+use crate::toolchain::{DistToolchainName, Toolchain};
+use crate::{channel::Channel, commands::toolchain::InstallCommand};
 use anyhow::Result;
 use std::fmt::Write;
 use tracing::{error, info};
@@ -25,17 +25,30 @@ pub fn install(command: InstallCommand) -> Result<()> {
 
     let mut errored_bins = String::new();
     let mut installed_bins = String::new();
-    let mut download_msg = String::new();
 
-    let mut cfgs: Vec<DownloadCfg> = Vec::new();
-
-    for component in [component::FORC, component::FUEL_CORE].iter() {
-        write!(download_msg, "{} ", component)?;
-        let download_cfg: DownloadCfg = DownloadCfg::new(component, None)?;
-        cfgs.push(download_cfg);
-    }
+    let cfgs: Vec<DownloadCfg> = match Channel::from_dist_channel(&DistToolchainName::Latest) {
+        Ok(c) => c.build_download_configs(),
+        Err(e) => {
+            error!(
+                "Failed to get latest channel {} - fetching versions using GitHub API",
+                e
+            );
+            [component::FORC, component::FUEL_CORE, component::FORC_LSP]
+                .iter()
+                .map(|c| {
+                    DownloadCfg::new(c, target_from_name(c).ok(), None)
+                        .expect("Failed to create DownloadCfg from component")
+                })
+                .collect()
+        }
+    };
 
-    info!("Downloading: {}", download_msg);
+    info!(
+        "Downloading: {}",
+        cfgs.iter()
+            .map(|c| c.name.clone() + " ")
+            .collect::<String>()
+    );
     for cfg in cfgs {
         match toolchain.add_component(cfg) {
             Ok(cfg) => writeln!(installed_bins, "- {} {}", cfg.name, cfg.version)?,
diff --git a/src/proxy_cli.rs b/src/proxy_cli.rs
index 30c39bf6..756ff68c 100644
--- a/src/proxy_cli.rs
+++ b/src/proxy_cli.rs
@@ -14,7 +14,7 @@ pub fn proxy_run(arg0: &str) -> Result<ExitCode> {
     let cmd_args: Vec<_> = env::args_os().skip(1).collect();
     let settings_file = SettingsFile::new(settings_file());
     let toolchain =
-        settings_file.with(|s| Toolchain::from_settings(&s.default_toolchain.clone().unwrap()))?;
+        settings_file.with(|s| Toolchain::from(&s.default_toolchain.clone().unwrap()))?;
 
     if !cmd_args.is_empty()
         && component::SUPPORTED_PLUGINS
diff --git a/src/toolchain.rs b/src/toolchain.rs
index d7007183..a6781051 100644
--- a/src/toolchain.rs
+++ b/src/toolchain.rs
@@ -1,6 +1,5 @@
 use anyhow::{bail, Result};
 use std::fmt;
-use std::path::Path;
 use std::path::PathBuf;
 use std::str::FromStr;
 use tracing::info;
@@ -9,14 +8,16 @@ use crate::download::{download_file_and_unpack, link_to_fuelup, unpack_bins, Dow
 use crate::ops::fuelup_self::self_update;
 use crate::path::{ensure_dir_exists, fuelup_bin, fuelup_bin_dir, toolchain_bin_dir};
 
-pub enum ToolchainName {
+pub enum DistToolchainName {
     Latest,
 }
 
-impl FromStr for ToolchainName {
+impl FromStr for DistToolchainName {
     type Err = anyhow::Error;
     fn from_str(s: &str) -> Result<Self> {
-        match s {
+        let split = s.split_once('-').unwrap();
+        let name = split.0;
+        match name {
             "latest" => Ok(Self::Latest),
             _ => bail!("Unknown name for toolchain: {}", s),
         }
@@ -70,42 +71,19 @@ impl Toolchain {
             Some(t) => TargetTriple(t),
             None => TargetTriple::from_host()?,
         };
-        let name = match ToolchainName::from_str(name)? {
-            ToolchainName::Latest => format!("{}-{}", name, target),
-        };
-        let path = toolchain_bin_dir(&name);
-        Ok(Self { name, path })
-    }
-
-    pub fn from(name: &str) -> Self {
-        let path = toolchain_bin_dir(name);
-        Self {
-            name: name.to_string(),
+        let toolchain = format!("{}-{}", name, target);
+        let path = toolchain_bin_dir(&toolchain);
+        Ok(Self {
+            name: toolchain,
             path,
-        }
-    }
-
-    pub fn from_settings(toolchain: &str) -> Result<Self> {
-        let split = toolchain.split_once('-').unwrap();
-        let name = split.0.to_string();
-        let target = TargetTriple(split.1.to_string());
-        let path = match ToolchainName::from_str(&name)? {
-            ToolchainName::Latest => toolchain_bin_dir(&format!("{}-{}", name, target)),
-        };
-        Ok(Self { name, path })
+        })
     }
 
-    pub fn from_path(&self, path: &Path) -> Result<Self> {
-        let name = path.file_name().unwrap();
-
-        // Minimally check that there's a /bin directory
-        if !path.join("bin").is_dir() {
-            bail!("Invalid toolchain path");
-        }
-
+    pub fn from(toolchain: &str) -> Result<Self> {
+        let path = toolchain_bin_dir(toolchain);
         Ok(Self {
-            name: name.to_string_lossy().to_string(),
-            path: path.to_path_buf(),
+            name: toolchain.to_string(),
+            path,
         })
     }
 


Test patch:
diff --git a/tests/channel-fuel-latest-example.toml b/tests/channel-fuel-latest-example.toml
new file mode 100644
index 00000000..ac318a3f
--- /dev/null
+++ b/tests/channel-fuel-latest-example.toml
@@ -0,0 +1,37 @@
+[pkg.forc]
+version = "0.17.0"
+
+[pkg.forc.target.darwin_amd64]
+url = "https://github.com/FuelLabs/sway/releases/download/v0.17.0/forc-binaries-darwin_amd64.tar.gz"
+hash = "a5a2bedd4cf64e372dae28c435a7902160924424cbc50a6f4b582b5a50134485"
+
+[pkg.forc.target.darwin_arm64]
+url = "https://github.com/FuelLabs/sway/releases/download/v0.17.0/forc-binaries-darwin_arm64.tar.gz"
+hash = "dadc6c0e04fd79c71806848747ca7edd4ba86b14016a93a3af42fe0da9afbc14"
+
+[pkg.forc.target.linux_amd64]
+url = "https://github.com/FuelLabs/sway/releases/download/v0.17.0/forc-binaries-linux_amd64.tar.gz"
+hash = "83f010f8d1185629bd6506139945e3a21f7e927ad470b674da367bafb698b5ce"
+
+[pkg.forc.target.linux_arm64]
+url = "https://github.com/FuelLabs/sway/releases/download/v0.17.0/forc-binaries-linux_arm64.tar.gz"
+hash = "6008e2421cfd6f40c3dad73466d105f462e1ada56a5c21b34a4bd4a719a35b21"
+
+[pkg.fuel-core]
+version = "0.9.4"
+
+[pkg.fuel-core.target.aarch64-apple-darwin]
+url = "https://github.com/FuelLabs/fuel-core/releases/download/v0.9.4/fuel-core-0.9.4-aarch64-apple-darwin.tar.gz"
+hash = "17e255b3f9a293b5f6b991092d43ac19560de9091fcf2913add6958549018b0f"
+
+[pkg.fuel-core.target.aarch64-unknown-linux-gnu]
+url = "https://github.com/FuelLabs/fuel-core/releases/download/v0.9.4/fuel-core-0.9.4-aarch64-unknown-linux-gnu.tar.gz"
+hash = "28176b355f4e8b92ad006c89cbd620255a93a7736468dfd5f81a0c1a1f356164"
+
+[pkg.fuel-core.target.x86_64-apple-darwin]
+url = "https://github.com/FuelLabs/fuel-core/releases/download/v0.9.4/fuel-core-0.9.4-x86_64-apple-darwin.tar.gz"
+hash = "053cf33056434e2a6fcf1c445ed048df424d83b7bb7dd5326b1a4f3d2d3dec53"
+
+[pkg.fuel-core.target.x86_64-unknown-linux-gnu]
+url = "https://github.com/FuelLabs/fuel-core/releases/download/v0.9.4/fuel-core-0.9.4-x86_64-unknown-linux-gnu.tar.gz"
+hash = "ec2909cd20741ba8a1b7ef54b7026b0d33dd1d3d9cf42d1564bd7f56307bc16e"