Repository: FuelLabs/fuelup (rust)

Issue:
Support project specific toolchains with `fuel-toolchain.toml`
Following some conversations on changes in forc from 0.14.5 -> 0.15.1 causing SwaySwap to break, I think there's space for fuelup to support usage of project-specific toolchains (which is basically rustup's override mechanic, described [here](https://rust-lang.github.io/rustup/overrides.html)). 

Will expand on this when I have more thoughts regarding this but my preliminary idea is that, instead of having a `rust-toolchain.toml`, we can have an entry within `Forc.toml` (would this create issues with parsing the toml?) stating which version of forc is built with the project. `fuelup` can detect this and decide which version of `forc` to run for that project. 

I think this idea is also mentioned [here](https://github.com/FuelLabs/fuelup/issues/1#issue-1242131332) in the original issue created by @adlerjohn. 

We would need to support the toolchain pattern #63 for this.

cc: @mitchmindtree @Braqzen for comments and ideas

Pull request:
feat: allow usage of fuel-toolchain.toml in projects
closes #65 

## Summary

This should be good to go for review. Basically this PR implements
- the store,
- basic overriding with `channel` and `components`.
- displaying the overridden toolchain with `fuelup default`.

### How does this work?
Whenever we execute binaries installed via fuelup, we always check in `proxy_cli.rs` the existence of an override file in the project root. From here, `fuelup` will decide which toolchain or component to use, and decide to install components (if not installed).

To test this PR:
1. checkout this PR, `cargo build`
2. link the built fuelup and a chosen fuelup binary of your choice to test. For me I am using `forc` and have this script to quickly iterate/test things:

```sh
rm -f ~/.fuelup/bin/forc
ln target/debug/fuelup ~/.fuelup/bin/forc
```

3. create a `fuel-toolchain.toml` at your current working directory, edit it to your wish to test things. You can see the [tests](https://github.com/FuelLabs/fuelup/blob/10e2807e5ef2b7470932a35e109002e2009da87d/src/toolchain_override.rs#L144) for a few examples.


This PR does **NOT** yet support:
- specifying the dates (eg. `nightly-2022-01-09` won't work, and by extension the dated latest versions) https://github.com/FuelLabs/fuelup/issues/331
- installing to store via `fuelup component add`. Ultimately the goal is to move all downloads to the store and link components from there. https://github.com/FuelLabs/fuelup/issues/329
- `fuelup show` - currently it behaves like before, but we want to show if something is overriding. https://github.com/FuelLabs/fuelup/issues/330

The reason the above few features aren't supported in this PR is for reviewability. Keeping the diff small would make reviews easier. The above 3 features will be implemented in the next few PRs.


~Opening a draft PR to track the deliverables below:~

## Todos

- [x] fuelup should adhere to both the naming conventions of distributed toolchains and custom toolchains
- [x] fuelup should auto download the toolchain, or any missing components, when any `fuelup` managed binaries are invoked with this toml file in the project root
- [x] ~`fuelup show` should show the override as the currently active toolchain, instead of what is shown in `fuelup default`~ deferred to separate PR for reviewability
- [x] `fuelup default` should show current override as well
- [x] document `fuel-toolchain.toml` in the fuelup book
- [x] add some tests with `fuel-toolchain.toml`

Additional todos, following updates [here](https://github.com/FuelLabs/fuelup/issues/320):
- [x] download missing components to a new directory, `/.fuelup/store`, instead of in the toolchain context
- [x] execute components from the store directly instead


Patch:
diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
index 07c9e4ac..ca8f0812 100644
--- a/docs/src/SUMMARY.md
+++ b/docs/src/SUMMARY.md
@@ -14,6 +14,7 @@
   - [Components](concepts/components.md)
   - [Proxies](concepts/proxies.md)
 - [Basic usage](basics.md)
+- [Overrides](overrides.md)
 - [Configuration](configuration.md)
 - [Examples](examples.md)
 - [Contributing to fuelup](./contributing_to_fuelup.md)
diff --git a/docs/src/overrides.md b/docs/src/overrides.md
new file mode 100644
index 00000000..a441f64d
--- /dev/null
+++ b/docs/src/overrides.md
@@ -0,0 +1,55 @@
+# Overrides
+
+`fuelup` automatically determines which [toolchain] to use when one of the installed commands like
+`forc` is executed.
+
+You can override the installed default toolchain using a `fuel-toolchain.toml` file.
+
+## The toolchain file
+
+The fuel-toolchain.toml file allows projects to "pin" to a specific set of fuel toolchain component versions.
+This greatly improves the reproducibility of a project, as the fuel-toolchain.toml contains the set of known,
+working versions for each tool used to build it.
+
+When this file is present, `fuelup` will override the default toolchain with the specified toolchain when executing binaries
+in the toolchain.
+
+In these cases, the toolchain can be specified in a file called `fuel-toolchain.toml`. These toolchains can only be
+the [distributed toolchains] at this point in time.
+
+Here's what a sample project might look like:
+
+```console
+$ tree -L 1 # 'tree -L 1' shows the project structure up to the depth of 1
+.
+├── Cargo.toml
+├── Forc.toml
+├── fuel-toolchain.toml
+├── project
+├── README.md
+└── SPECIFICATION.md
+```
+
+### Examples
+
+An application using the [`beta-2`] toolchain:
+
+```toml
+[toolchain]
+channel = "beta-2"
+```
+
+Let's say we have a project on the `beta-2` testnet, and we want to try using the
+latest (at time of writing) forc to develop on it:
+
+```toml
+[toolchain]
+channel = "beta-2"
+
+[components]
+forc = "0.33.0" # in beta-2, forc is pinned to v0.31.1
+```
+
+[toolchain]: concepts/toolchains.md
+[distributed toolchains]: concepts/toolchains.md#toolchains
+[`beta-2`]: concepts/channels/beta-2.md
diff --git a/src/constants.rs b/src/constants.rs
index 3e7c0393..8181e902 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -1,6 +1,7 @@
 use time::{format_description::FormatItem, macros::format_description};
 
 pub const FUELUP_GH_PAGES: &str = "https://raw.githubusercontent.com/FuelLabs/fuelup/gh-pages/";
+pub const FUEL_TOOLCHAIN_TOML_FILE: &str = "fuel-toolchain.toml";
 
 pub const CHANNEL_LATEST_URL: &str =
     "https://raw.githubusercontent.com/FuelLabs/fuelup/gh-pages/channel-fuel-latest.toml";
diff --git a/src/lib.rs b/src/lib.rs
index 40fda88a..2e234bdc 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -10,5 +10,7 @@ pub mod ops;
 pub mod path;
 pub mod proxy_cli;
 pub mod settings;
+pub mod store;
 pub mod target_triple;
 pub mod toolchain;
+pub mod toolchain_override;
diff --git a/src/ops/fuelup_default.rs b/src/ops/fuelup_default.rs
index 859ecb90..9513207a 100644
--- a/src/ops/fuelup_default.rs
+++ b/src/ops/fuelup_default.rs
@@ -6,6 +6,7 @@ use crate::{
     path::settings_file,
     settings::SettingsFile,
     toolchain::{DistToolchainDescription, Toolchain},
+    toolchain_override::ToolchainOverride,
 };
 
 pub fn default(toolchain: Option<String>) -> Result<()> {
@@ -14,7 +15,17 @@ pub fn default(toolchain: Option<String>) -> Result<()> {
     let toolchain = match toolchain {
         Some(toolchain) => toolchain,
         None => {
-            info!("{} (default)", current_toolchain.name);
+            let mut result = String::new();
+            if let Some(to) = ToolchainOverride::from_project_root() {
+                let name = match DistToolchainDescription::from_str(&to.cfg.toolchain.channel) {
+                    Ok(desc) => desc.to_string(),
+                    Err(_) => to.cfg.toolchain.channel,
+                };
+                result.push_str(&format!("{} (override), ", name))
+            }
+            result.push_str(&format!("{} (default)", current_toolchain.name));
+
+            info!("{}", result);
             return Ok(());
         }
     };
diff --git a/src/ops/fuelup_show.rs b/src/ops/fuelup_show.rs
index 980968d8..1d6722bb 100644
--- a/src/ops/fuelup_show.rs
+++ b/src/ops/fuelup_show.rs
@@ -1,6 +1,7 @@
 use anyhow::Result;
 use component::{self, Components};
 use semver::Version;
+use std::str::FromStr;
 use std::{io::Write, path::Path};
 use tracing::{error, info};
 
@@ -9,7 +10,8 @@ use crate::{
     fmt::{bold, print_header},
     path::fuelup_dir,
     target_triple::TargetTriple,
-    toolchain::Toolchain,
+    toolchain::{DistToolchainDescription, Toolchain},
+    toolchain_override::ToolchainOverride,
 };
 
 fn exec_show_version(component_executable: &Path) -> Result<()> {
@@ -51,19 +53,51 @@ pub fn show() -> Result<()> {
 
     print_header("installed toolchains");
     let cfg = Config::from_env()?;
-    let active_toolchain = Toolchain::from_settings()?;
+    let mut active_toolchain = Toolchain::from_settings()?;
+
+    let toolchain_override = ToolchainOverride::from_project_root();
+
+    let override_name = if let Some(toolchain_override) = toolchain_override.as_ref() {
+        match DistToolchainDescription::from_str(&toolchain_override.cfg.toolchain.channel) {
+            Ok(desc) => Some(desc.to_string()),
+            Err(_) => Some(toolchain_override.cfg.toolchain.channel.clone()),
+        }
+    } else {
+        None
+    };
 
     for toolchain in cfg.list_toolchains()? {
+        let mut message = toolchain.clone();
         if toolchain == active_toolchain.name {
-            info!("{} (default)", toolchain);
-        } else {
-            info!("{}", toolchain);
+            message.push_str(" (default)")
+        }
+
+        if Some(toolchain) == override_name {
+            message.push_str(" (override)");
         }
+        info!("{}", message)
     }
 
-    print_header("\nactive toolchain");
+    let mut active_toolchain_message = String::new();
+    if let Some(toolchain_override) = toolchain_override {
+        // We know that the override exists, but we want to show the target triple as well.
+        let override_name = override_name.as_ref().unwrap();
+        if &active_toolchain.name == override_name {
+            active_toolchain_message.push_str(" (default)");
+        }
 
-    info!("{} (default)", active_toolchain.name);
+        active_toolchain = Toolchain::from_path(override_name);
+        active_toolchain_message.push_str(&active_toolchain.name);
+        active_toolchain_message.push_str(&format!(
+            " (override), path: {}",
+            toolchain_override.path.display()
+        ));
+    } else {
+        active_toolchain_message.push_str(&format!("{} (default)", active_toolchain.name));
+    };
+
+    print_header("\nactive toolchain");
+    info!("{}", active_toolchain_message);
 
     for component in Components::collect_exclude_plugins()? {
         bold(|s| write!(s, "  {}", &component.name));
diff --git a/src/path.rs b/src/path.rs
index ea617e25..d2ea6f60 100644
--- a/src/path.rs
+++ b/src/path.rs
@@ -9,6 +9,8 @@ use tracing::warn;
 
 use dirs;
 
+use crate::constants::FUEL_TOOLCHAIN_TOML_FILE;
+
 pub const FUELUP_DIR: &str = ".fuelup";
 
 pub fn fuelup_dir() -> PathBuf {
@@ -35,6 +37,10 @@ pub fn toolchains_dir() -> PathBuf {
     fuelup_dir().join("toolchains")
 }
 
+pub fn store_dir() -> PathBuf {
+    fuelup_dir().join("store")
+}
+
 pub fn fuelup_tmp_dir() -> PathBuf {
     fuelup_dir().join("tmp")
 }
@@ -120,6 +126,28 @@ pub fn warn_existing_fuel_executables() -> Result<()> {
     Ok(())
 }
 
+fn find_parent_dir_with_file(starter_path: &Path, file_name: &str) -> Option<PathBuf> {
+    let mut path = std::fs::canonicalize(starter_path).ok()?;
+    let empty_path = PathBuf::from("/");
+    while path != empty_path {
+        path.push(file_name);
+        if path.exists() {
+            path.pop();
+            return Some(path);
+        } else {
+            path.pop();
+            path.pop();
+        }
+    }
+    None
+}
+
+pub fn get_fuel_toolchain_toml() -> Option<PathBuf> {
+    let parent_dir =
+        find_parent_dir_with_file(&std::env::current_dir().unwrap(), FUEL_TOOLCHAIN_TOML_FILE);
+    parent_dir.map(|p| p.join(FUEL_TOOLCHAIN_TOML_FILE))
+}
+
 #[cfg(unix)]
 pub fn is_executable(path: &Path) -> bool {
     use std::os::unix::prelude::*;
diff --git a/src/proxy_cli.rs b/src/proxy_cli.rs
index 08b5f10d..7e9bee98 100644
--- a/src/proxy_cli.rs
+++ b/src/proxy_cli.rs
@@ -3,9 +3,12 @@ use std::ffi::OsString;
 use std::io::{Error, ErrorKind};
 use std::os::unix::prelude::CommandExt;
 use std::process::{Command, ExitCode, Stdio};
+use std::str::FromStr;
 use std::{env, io};
 
-use crate::toolchain::Toolchain;
+use crate::store::Store;
+use crate::toolchain::{DistToolchainDescription, Toolchain};
+use crate::toolchain_override::ToolchainOverride;
 use component::Components;
 
 /// Runs forc or fuel-core in proxy mode
@@ -24,23 +27,58 @@ pub fn proxy_run(arg0: &str) -> Result<ExitCode> {
     Ok(ExitCode::SUCCESS)
 }
 
-fn direct_proxy(proc_name: &str, args: &[OsString], toolchain: &Toolchain) -> io::Result<ExitCode> {
-    let bin_path = toolchain.bin_path.join(proc_name);
+fn direct_proxy(proc_name: &str, args: &[OsString], toolchain: &Toolchain) -> Result<ExitCode> {
+    let toolchain_override: Option<ToolchainOverride> = ToolchainOverride::from_project_root();
+
+    let (bin_path, toolchain_name) = match toolchain_override {
+        Some(to) => {
+            // unwrap() is safe here since we try DistToolchainDescription::from_str()
+            // when deserializing from the toml.
+            let description =
+                DistToolchainDescription::from_str(&to.cfg.toolchain.channel).unwrap();
+            let toolchain = Toolchain::from_path(&description.to_string());
+
+            // Install the entire toolchain declared in [toolchain] if it does not exist.
+            toolchain.install_if_nonexistent(&description)?;
+
+            // Install components within [components] that are declared but missing from the store.
+            if let Some(version) = to.get_component_version(proc_name) {
+                let store = Store::from_env().unwrap();
+                if store.has_component(proc_name, version)
+                    && store.install_component(proc_name, &to).is_ok()
+                {
+                    (
+                        store.component_dir_path(proc_name, version).join(proc_name),
+                        description.to_string(),
+                    )
+                } else {
+                    (toolchain.bin_path.join(proc_name), description.to_string())
+                };
+            }
+
+            (toolchain.bin_path.join(proc_name), description.to_string())
+        }
+        None => (
+            toolchain.bin_path.join(proc_name),
+            toolchain.name.to_owned(),
+        ),
+    };
+
     let mut cmd = Command::new(bin_path);
 
     cmd.args(args);
     cmd.stdin(Stdio::inherit());
 
-    return exec(&mut cmd, proc_name, toolchain);
+    return exec(&mut cmd, proc_name, &toolchain_name).map_err(anyhow::Error::from);
 
-    fn exec(cmd: &mut Command, proc_name: &str, toolchain: &Toolchain) -> io::Result<ExitCode> {
+    fn exec(cmd: &mut Command, proc_name: &str, toolchain_name: &str) -> io::Result<ExitCode> {
         let error = cmd.exec();
         match error.kind() {
             ErrorKind::NotFound => Err(Error::new(
                 ErrorKind::NotFound,
                 format!(
                     "component '{}' not found in currently active toolchain '{}'",
-                    proc_name, toolchain.name
+                    proc_name, toolchain_name
                 ),
             )),
             _ => Err(error),
diff --git a/src/store.rs b/src/store.rs
new file mode 100644
index 00000000..5c7041f8
--- /dev/null
+++ b/src/store.rs
@@ -0,0 +1,67 @@
+use std::path::{Path, PathBuf};
+
+use anyhow::Result;
+use semver::Version;
+
+use crate::{
+    download::{download_file_and_unpack, unpack_bins, DownloadCfg},
+    path::{ensure_dir_exists, store_dir},
+    target_triple::TargetTriple,
+    toolchain_override::ToolchainOverride,
+};
+
+fn component_dirname(component_name: &str, version: &Version) -> String {
+    format!("{component_name}-{version}")
+}
+
+pub struct Store {
+    path: PathBuf,
+}
+
+impl Store {
+    pub(crate) fn from_env() -> Result<Self> {
+        let path = store_dir();
+        ensure_dir_exists(&path)?;
+        Ok(Self { path })
+    }
+
+    pub(crate) fn path(&self) -> &Path {
+        &self.path
+    }
+
+    pub(crate) fn has_component(&self, component_name: &str, version: &Version) -> bool {
+        let dirname = component_dirname(component_name, version);
+        self.path().join(dirname).exists()
+    }
+
+    pub(crate) fn component_dir_path(&self, component_name: &str, version: &Version) -> PathBuf {
+        self.path.join(component_dirname(component_name, version))
+    }
+
+    // This function installs a component into a directory within '/.fuelup/store'.
+    // The directory is named '<component_name>-<version>', eg. 'fuel-core-0.15.1'.
+    pub(crate) fn install_component(
+        &self,
+        component_name: &str,
+        toolchain_override: &ToolchainOverride,
+    ) -> Result<()> {
+        if let Some(components) = toolchain_override.cfg.components.as_ref() {
+            if let Some(version) = components.get(component_name) {
+                let download_cfg = DownloadCfg::new(
+                    component_name,
+                    TargetTriple::from_component(component_name)?,
+                    Some(version.clone()),
+                )?;
+
+                let component_dir = self.component_dir_path(component_name, version);
+
+                ensure_dir_exists(&component_dir)?;
+                download_file_and_unpack(&download_cfg, &component_dir)?;
+                // We ensure that component_dir exists above, so its parent must exist here.
+                unpack_bins(&component_dir, component_dir.parent().unwrap())?;
+            }
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/toolchain.rs b/src/toolchain.rs
index 267b955c..1286ddc5 100644
--- a/src/toolchain.rs
+++ b/src/toolchain.rs
@@ -8,7 +8,8 @@ use std::str::FromStr;
 use time::Date;
 use tracing::{error, info};
 
-use crate::channel::{self, is_beta_toolchain};
+use crate::channel::{self, is_beta_toolchain, Channel};
+use crate::config::Config;
 use crate::constants::DATE_FORMAT;
 use crate::download::{download_file_and_unpack, link_to_fuelup, unpack_bins, DownloadCfg};
 use crate::ops::fuelup_self::self_update;
@@ -276,6 +277,21 @@ impl Toolchain {
         Ok(download_cfg)
     }
 
+    pub fn install_if_nonexistent(&self, description: &DistToolchainDescription) -> Result<()> {
+        if !self.exists() {
+            if let Ok((channel, hash)) = Channel::from_dist_channel(description) {
+                let config = Config::from_env()?;
+                if let Ok(true) = config.hash_matches(description, &hash) {
+                    info!("'{}' is already installed and up to date", self.name);
+                };
+                for cfg in channel.build_download_configs() {
+                    self.add_component(cfg)?;
+                }
+            }
+        };
+
+        Ok(())
+    }
     fn remove_executables(&self, component: &str) -> Result<()> {
         let executables = &Components::collect().unwrap().component[component].executables;
         for executable in executables {
diff --git a/src/toolchain_override.rs b/src/toolchain_override.rs
new file mode 100644
index 00000000..f46157b5
--- /dev/null
+++ b/src/toolchain_override.rs
@@ -0,0 +1,202 @@
+use anyhow::{bail, Result};
+use semver::Version;
+use serde::{Deserialize, Serialize};
+use std::str::FromStr;
+use std::{collections::HashMap, path::PathBuf};
+use toml_edit::{de, ser, value, Document};
+use tracing::{info, warn};
+
+use crate::{
+    constants::FUEL_TOOLCHAIN_TOML_FILE,
+    download::DownloadCfg,
+    file,
+    path::get_fuel_toolchain_toml,
+    target_triple::TargetTriple,
+    toolchain::{DistToolchainName, Toolchain},
+};
+
+// For composability with other functionality of fuelup, we want to add
+// additional info to OverrideCfg (representation of 'fuel-toolchain.toml').
+// In this case, we want the path to the toml file. More info might be
+// needed in future.
+#[derive(Debug, Deserialize, Serialize)]
+pub struct ToolchainOverride {
+    pub cfg: OverrideCfg,
+    pub path: PathBuf,
+}
+
+// Representation of the entire 'fuel-toolchain.toml'.
+#[derive(Debug, Deserialize, Serialize)]
+pub struct OverrideCfg {
+    pub toolchain: ToolchainCfg,
+    pub components: Option<HashMap<String, Version>>,
+}
+
+// Represents the [toolchain] table in 'fuel-toolchain.toml'.
+#[derive(Debug, Deserialize, Serialize)]
+pub struct ToolchainCfg {
+    pub channel: String,
+}
+
+impl ToolchainOverride {
+    // Creates a representation of a 'fuel-toolchain.toml' from a file path.
+    // This representation is an OverrideCfg and the file path.
+    pub(crate) fn from_path(path: PathBuf) -> Result<Self> {
+        let f = file::read_file(FUEL_TOOLCHAIN_TOML_FILE, path.as_path())?;
+        let cfg: OverrideCfg = OverrideCfg::from_toml(&f)?;
+        Ok(Self { cfg, path })
+    }
+
+    pub fn to_toml(&self) -> Document {
+        let mut document = toml_edit::Document::new();
+
+        document["toolchain"]["channel"] = value(self.cfg.toolchain.channel.to_string());
+        if let Some(components) = &self.cfg.components {
+            for (k, v) in components.iter() {
+                document["components"][k] = value(v.to_string());
+            }
+        }
+        document
+    }
+
+    pub fn from_project_root() -> Option<ToolchainOverride> {
+        if let Some(fuel_toolchain_toml_file) = get_fuel_toolchain_toml() {
+            match ToolchainOverride::from_path(fuel_toolchain_toml_file) {
+                Ok(to) => Some(to),
+                Err(e) => {
+                    warn!("warning: invalid 'fuel-toolchain.toml' in project root: {e}");
+                    None
+                }
+            }
+        } else {
+            None
+        }
+    }
+
+    pub fn get_component_version(&self, component: &str) -> Option<&Version> {
+        if let Some(components) = &self.cfg.components {
+            components.get(component)
+        } else {
+            None
+        }
+    }
+
+    pub fn install_missing_components(&self, toolchain: &Toolchain, called: &str) -> Result<()> {
+        match &self.cfg.components {
+            None => warn!(
+                "warning: overriding toolchain '{}' in {} does not have any components listed",
+                &self.cfg.toolchain.channel, FUEL_TOOLCHAIN_TOML_FILE
+            ),
+            Some(components) => {
+                for component in components.keys() {
+                    if !toolchain.has_component(component) {
+                        let target_triple = TargetTriple::from_component(component)?;
+
+                        if let Ok(download_cfg) = DownloadCfg::new(called, target_triple, None) {
+                            info!(
+                                "installing missing component '{}' specified in {}",
+                                component, FUEL_TOOLCHAIN_TOML_FILE
+                            );
+                            toolchain.add_component(download_cfg)?;
+                        };
+                    }
+                }
+            }
+        };
+        Ok(())
+    }
+}
+
+impl OverrideCfg {
+    pub fn new(toolchain: ToolchainCfg, components: Option<HashMap<String, Version>>) -> Self {
+        Self {
+            toolchain,
+            components,
+        }
+    }
+
+    // Creates a representation of a 'fuel-toolchain.toml' from a toml string.
+    // This is used in the implementation of ToolchainOverride, which is just
+    // an OverrideCfg with its file path.
+    pub(crate) fn from_toml(toml: &str) -> Result<Self> {
+        let cfg: OverrideCfg = de::from_str(toml)?;
+        if DistToolchainName::from_str(&cfg.toolchain.channel).is_err() {
+            bail!("Invalid channel '{}'", &cfg.toolchain.channel)
+        }
+
+        if let Some(components) = cfg.components.as_ref() {
+            if components.is_empty() {
+                bail!("'[components]' table is declared with no components")
+            }
+        }
+
+        Ok(cfg)
+    }
+
+    pub fn to_string_pretty(self) -> Result<String, ser::Error> {
+        ser::to_string_pretty(&self)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn parse_toolchain_override_channel_only() {
+        const TOML: &str = r#"[toolchain]
+channel = "latest"
+"#;
+
+        let cfg = OverrideCfg::from_toml(TOML).unwrap();
+
+        assert_eq!(cfg.toolchain.channel, "latest");
+        assert!(cfg.components.is_none());
+        assert_eq!(TOML, cfg.to_string_pretty().unwrap());
+    }
+
+    #[test]
+    fn parse_toolchain_override_components() {
+        const TOML: &str = r#"[toolchain]
+channel = "latest"
+
+[components]
+fuel-core = "0.15.1"
+"#;
+
+        let cfg = OverrideCfg::from_toml(TOML).unwrap();
+
+        assert_eq!(cfg.toolchain.channel, "latest");
+        assert_eq!(cfg.components.as_ref().unwrap().keys().len(), 1);
+        assert_eq!(
+            cfg.components.as_ref().unwrap().get("fuel-core").unwrap(),
+            &Version::new(0, 15, 1)
+        );
+        assert_eq!(TOML, cfg.to_string_pretty().unwrap());
+    }
+
+    #[test]
+    fn parse_toolchain_override_invalid_tomls() {
+        const EMPTY_STR: &str = "";
+        const EMPTY_TOOLCHAIN: &str = r#"[toolchain]
+"#;
+        const INVALID_CHANNEL: &str = r#"[toolchain]
+channel = "invalid-channel"
+"#;
+
+        const EMPTY_COMPONENTS: &str = r#"[toolchain]
+channel = "beta-2"
+
+[components]
+"#;
+
+        for toml in [
+            EMPTY_STR,
+            EMPTY_TOOLCHAIN,
+            INVALID_CHANNEL,
+            EMPTY_COMPONENTS,
+        ] {
+            assert!(OverrideCfg::from_toml(toml).is_err());
+        }
+    }
+}


Test patch:
diff --git a/tests/commands.rs b/tests/commands.rs
index 0155c379..89b393f2 100644
--- a/tests/commands.rs
+++ b/tests/commands.rs
@@ -1,5 +1,8 @@
 use anyhow::Result;
-use fuelup::{channel, fmt::format_toolchain_with_target, target_triple::TargetTriple};
+use fuelup::{
+    channel, constants::FUEL_TOOLCHAIN_TOML_FILE, fmt::format_toolchain_with_target,
+    target_triple::TargetTriple,
+};
 use std::{env, path::Path};
 
 mod testcfg;
@@ -318,6 +321,49 @@ my_toolchain (default)
     Ok(())
 }
 
+#[test]
+fn fuelup_show_override() -> Result<()> {
+    testcfg::setup(FuelupState::LatestAndNightlyWithBetaOverride, &|cfg| {
+        let stdout = cfg.fuelup(&["show"]).stdout;
+
+        let mut lines = stdout.lines();
+        assert_eq!(
+            lines.next().unwrap(),
+            &format!("Default host: {}", TargetTriple::from_host().unwrap())
+        );
+        assert!(lines.next().unwrap().contains("fuelup home: "));
+
+        let target = TargetTriple::from_host().unwrap();
+        let expected_stdout = &format!(
+            r#"
+installed toolchains
+--------------------
+latest-{target} (default)
+nightly-{target}
+
+active toolchain
+-----------------
+beta-1-{target} (override), path: {}
+  forc - not found
+    - forc-client
+      - forc-deploy - not found
+      - forc-run - not found
+    - forc-doc - not found
+    - forc-explore - not found
+    - forc-fmt - not found
+    - forc-index - not found
+    - forc-lsp - not found
+    - forc-wallet - not found
+  fuel-core - not found
+  fuel-indexer - not found
+"#,
+            cfg.home.join(FUEL_TOOLCHAIN_TOML_FILE).display()
+        );
+        assert!(stdout.contains(expected_stdout));
+    })?;
+    Ok(())
+}
+
 #[test]
 fn fuelup_self_update() -> Result<()> {
     testcfg::setup(FuelupState::LatestToolchainInstalled, &|cfg| {
@@ -428,6 +474,20 @@ fn fuelup_default_nightly_and_nightly_date() -> Result<()> {
     Ok(())
 }
 
+#[test]
+fn fuelup_default_override() -> Result<()> {
+    testcfg::setup(FuelupState::LatestAndNightlyWithBetaOverride, &|cfg| {
+        let output = cfg.fuelup(&["default"]);
+        let triple = TargetTriple::from_host().unwrap();
+
+        let expected_stdout = format!("beta-1-{triple} (override), latest-{triple} (default)\n");
+
+        assert_eq!(output.stdout, expected_stdout);
+    })?;
+
+    Ok(())
+}
+
 #[test]
 fn fuelup_toolchain_new() -> Result<()> {
     testcfg::setup(FuelupState::Empty, &|cfg| {
diff --git a/tests/testcfg.rs b/tests/testcfg.rs
index 3f6a0ee0..959d228c 100644
--- a/tests/testcfg.rs
+++ b/tests/testcfg.rs
@@ -1,6 +1,8 @@
 use anyhow::Result;
+use fuelup::constants::FUEL_TOOLCHAIN_TOML_FILE;
 use fuelup::settings::SettingsFile;
 use fuelup::target_triple::TargetTriple;
+use fuelup::toolchain_override::{OverrideCfg, ToolchainCfg, ToolchainOverride};
 use std::os::unix::fs::OpenOptionsExt;
 use std::{
     env, fs,
@@ -20,8 +22,10 @@ pub enum FuelupState {
     LatestAndNightlyInstalled,
     NightlyAndNightlyDateInstalled,
     Beta1Installed,
+    LatestAndNightlyWithBetaOverride,
 }
 
+#[derive(Debug)]
 pub struct TestCfg {
     pub fuelup_path: PathBuf,
     pub root: PathBuf,
@@ -82,6 +86,7 @@ impl TestCfg {
     pub fn fuelup(&mut self, args: &[&str]) -> TestOutput {
         let output = Command::new(&self.fuelup_path)
             .args(args)
+            .current_dir(&self.home)
             .env("HOME", &self.home)
             .env("CARGO_HOME", self.home.join(".cargo").to_str().unwrap())
             .env(
@@ -145,6 +150,15 @@ fn setup_settings_file(settings_dir: &Path, default_toolchain: &str) -> Result<(
     Ok(())
 }
 
+fn setup_override_file(toolchain_override: ToolchainOverride) -> Result<()> {
+    let document = toolchain_override.to_toml();
+
+    fs::write(toolchain_override.path, document.to_string())
+        .unwrap_or_else(|_| panic!("Failed to write {}", FUEL_TOOLCHAIN_TOML_FILE));
+
+    Ok(())
+}
+
 pub fn setup(state: FuelupState, f: &dyn Fn(&mut TestCfg)) -> Result<()> {
     let root = env::current_exe()
         .unwrap()
@@ -242,6 +256,20 @@ pub fn setup(state: FuelupState, f: &dyn Fn(&mut TestCfg)) -> Result<()> {
             )?;
             setup_settings_file(&tmp_fuelup_root_path, &beta_1)?;
         }
+        FuelupState::LatestAndNightlyWithBetaOverride => {
+            setup_toolchain(&tmp_fuelup_root_path, &latest)?;
+            setup_toolchain(&tmp_fuelup_root_path, &nightly)?;
+            setup_settings_file(&tmp_fuelup_root_path, &latest)?;
+            setup_override_file(ToolchainOverride {
+                cfg: OverrideCfg::new(
+                    ToolchainCfg {
+                        channel: "beta-1".to_string(),
+                    },
+                    None,
+                ),
+                path: tmp_home.join(FUEL_TOOLCHAIN_TOML_FILE),
+            })?;
+        }
     }
 
     f(&mut TestCfg::new(