diff --git a/Cargo.lock b/Cargo.lock index f908564fb..c93056fee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -395,6 +395,19 @@ dependencies = [ "typenum", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.3", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "deranged" version = "0.3.11" @@ -405,6 +418,12 @@ dependencies = [ "serde", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -485,6 +504,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "forc-tracing" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9504fde5c9179bf6bf6f4ca6902ad1733aa64ad5679946b72d648b133d24d24" +dependencies = [ + "ansi_term", + "tracing", + "tracing-subscriber", +] + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -507,10 +537,13 @@ dependencies = [ "component", "dirs", "flate2", + "forc-tracing", "indicatif", + "pretty_assertions", "semver", "serde", "serde_json", + "serial_test", "sha2", "strip-ansi-escapes", "tar", @@ -524,6 +557,83 @@ dependencies = [ "ureq", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -551,6 +661,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "heck" version = "0.4.1" @@ -606,7 +722,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", ] [[package]] @@ -693,6 +809,16 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.20" @@ -766,6 +892,29 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -778,6 +927,12 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "portable-atomic" version = "1.6.0" @@ -790,6 +945,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -951,6 +1116,12 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "sct" version = "0.7.1" @@ -1001,6 +1172,31 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ad9342b3aaca7cb43c45c097dd008d4907070394bd0751a0aa8817e5a018d" +dependencies = [ + "dashmap", + "futures", + "lazy_static", + "log", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93fb4adc70021ac1b47f7d45e8cc4169baaa7ea58483bc5b721d19a26202212" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "sha2" version = "0.10.8" @@ -1021,6 +1217,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.13.1" @@ -1645,3 +1850,9 @@ dependencies = [ "linux-raw-sys", "rustix", ] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/Cargo.toml b/Cargo.toml index 90a530e6a..4c07fb9ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ clap_complete = "4.2" component = { path = "component" } dirs = "4" flate2 = "1" +forc-tracing = "0.47.0" indicatif = "0.17.7" semver = { version = "1", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } @@ -46,4 +47,6 @@ members = ["component", "ci/build-channel", "ci/compare-versions"] [dev-dependencies] chrono = "0.4.33" +pretty_assertions = "1.4.0" +serial_test = "3.0.0" strip-ansi-escapes = "0.2.0" diff --git a/src/commands/toolchain.rs b/src/commands/toolchain.rs index f5e18b178..d0764c6f4 100644 --- a/src/commands/toolchain.rs +++ b/src/commands/toolchain.rs @@ -1,12 +1,18 @@ use anyhow::{bail, Result}; use clap::Parser; +use serde::Deserialize; +use std::io::{stdin, BufReader}; +use std::str::FromStr; +use crate::constants::VALID_CHANNEL_STR; +use crate::ops::fuelup_toolchain::export::export; use crate::ops::fuelup_toolchain::install::install; use crate::ops::fuelup_toolchain::list_revisions::list_revisions; use crate::ops::fuelup_toolchain::new::new; use crate::ops::fuelup_toolchain::uninstall::uninstall; use crate::target_triple::TargetTriple; use crate::toolchain::RESERVED_TOOLCHAIN_NAMES; +use crate::toolchain_override; #[derive(Debug, Parser)] pub enum ToolchainCommand { @@ -18,6 +24,8 @@ pub enum ToolchainCommand { Uninstall(UninstallCommand), /// Fetch the list of published `latest` toolchains, starting from the most recent ListRevisions(ListRevisionsCommand), + /// Export the toolchain info into fuel-toolchain.toml + Export(ExportCommand), } #[derive(Debug, Parser)] @@ -42,6 +50,20 @@ pub struct UninstallCommand { #[derive(Debug, Parser)] pub struct ListRevisionsCommand {} +#[derive(Debug, Deserialize, Parser)] +pub struct ExportCommand { + /// Toolchain to export, [possible values: latest, beta-1, beta-2, beta-3, beta-4, nightly]. + /// The default toolchain will be exported if name isn't specified + pub name: Option, + /// Channel to export, [possible values: latest-YYYY-MM-DD, nightly-YYYY-MM-DD, beta-1, beta-2, beta-3, beta-4]. + #[clap(value_parser = channel_allowed)] + #[arg(short, long)] + pub channel: Option, + /// Forces exporting the toolchain, replacing any existing toolchain override file + #[arg(short, long)] + pub force: bool, +} + fn name_allowed(s: &str) -> Result { let name = match s.split_once('-') { Some((prefix, target_triple)) => { @@ -64,12 +86,27 @@ fn name_allowed(s: &str) -> Result { } } +fn channel_allowed(channel: &str) -> Result { + if channel.is_empty() { + return Ok(channel.to_string()); + } + if toolchain_override::Channel::from_str(channel).is_err() { + bail!( + "Invalid channel '{}', expected one of {}.", + channel, + VALID_CHANNEL_STR, + ); + } + Ok(channel.to_string()) +} + pub fn exec(command: ToolchainCommand) -> Result<()> { match command { ToolchainCommand::Install(command) => install(command)?, ToolchainCommand::New(command) => new(command)?, ToolchainCommand::Uninstall(command) => uninstall(command)?, ToolchainCommand::ListRevisions(command) => list_revisions(command)?, + ToolchainCommand::Export(command) => export(command, BufReader::new(stdin()))?, }; Ok(()) diff --git a/src/constants.rs b/src/constants.rs index 278bc193d..dfdd77869 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -16,3 +16,5 @@ pub const CHANNEL_BETA_5_FILE_NAME: &str = "channel-fuel-beta-5.toml"; pub const DATE_FORMAT: &[FormatItem] = format_description!("[year]-[month]-[day]"); pub const DATE_FORMAT_URL_FRIENDLY: &[FormatItem] = format_description!("[year]/[month]/[day]"); +pub const VALID_CHANNEL_STR: &str = + ""; diff --git a/src/lib.rs b/src/lib.rs index a00756d8c..cf5e0b494 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,3 +16,4 @@ pub mod store; pub mod target_triple; pub mod toolchain; pub mod toolchain_override; +pub mod util; diff --git a/src/ops/fuelup_toolchain/export.rs b/src/ops/fuelup_toolchain/export.rs new file mode 100644 index 000000000..d30684e55 --- /dev/null +++ b/src/ops/fuelup_toolchain/export.rs @@ -0,0 +1,390 @@ +use crate::{ + commands::toolchain::ExportCommand, + constants::{FUEL_TOOLCHAIN_TOML_FILE, VALID_CHANNEL_STR}, + path::get_fuel_toolchain_toml, + toolchain::{DistToolchainDescription, Toolchain}, + toolchain_override::{self, OverrideCfg, ToolchainCfg, ToolchainOverride}, + util::version::exec_version, +}; +use anyhow::{bail, Result}; +use component::{self, Components}; +use forc_tracing::println_warning; +use semver::Version; +use std::{collections::HashMap, path::PathBuf}; +use std::{fs, io::BufRead, str::FromStr}; +use tracing::info; + +pub fn export(command: ExportCommand, mut reader: impl BufRead) -> Result<()> { + let ExportCommand { + name, + channel, + force, + } = command; + let mut toolchain_info_path = PathBuf::from("./").join(FUEL_TOOLCHAIN_TOML_FILE); + if let Some(path) = get_fuel_toolchain_toml() { + toolchain_info_path = path; + match force { + true => { + println_warning(&format!( + "Because the `--force` argument was supplied, this file will be overwritten: {}", + &toolchain_info_path.display(), + )); + if fs::remove_file(&toolchain_info_path).is_err() { + bail!("Failed to remove file {}", &toolchain_info_path.display()); + } + } + false => { + println_warning(&format!( + "There is an existing toolchain override file at {}. \ + Do you wish to replace it with a new one? (y/N) ", + &toolchain_info_path.display(), + )); + let mut need_replace = String::new(); + if reader.read_line(&mut need_replace).is_err() { + bail!("Failed to read user input"); + } + if need_replace.trim() == "y" { + if fs::remove_file(&toolchain_info_path).is_err() { + bail!("Failed to remove file {}", &toolchain_info_path.display()); + }; + } else { + println_warning("Cancelled toolchain export"); + return Ok(()); + } + } + } + } + let toolchain_name = name.unwrap_or(Toolchain::from_settings()?.name); + + let export_toolchain = match DistToolchainDescription::from_str(&toolchain_name) { + Ok(desc) => Toolchain::from_path(&desc.to_string()), + Err(_) => Toolchain::from_path(&toolchain_name), + }; + + let mut version_map: HashMap = HashMap::new(); + for component in Components::collect_exclude_plugins()? { + let _ = exec_version(&export_toolchain.bin_path.join(&component.name)) + .map(|version| version_map.insert(component.name.clone(), version)); + + if component.name == component::FORC { + let forc_executables = component.executables; + for plugin in Components::collect_plugins()? { + if !forc_executables.contains(&plugin.name) { + plugin.executables.iter().for_each(|executable| { + let _ = exec_version(&export_toolchain.bin_path.join(executable)) + .map(|version| version_map.insert(executable.clone(), version)); + }); + } + } + } + } + let mut export_channel = channel.unwrap_or(export_toolchain.name); + if toolchain_override::Channel::from_str(&export_channel).is_err() { + println_warning(&format!( + "Invalid channel '{}', expected one of {}. \ + \nPlease input a valid channel: ", + export_channel, VALID_CHANNEL_STR, + )); + let mut input_channel_name = String::new(); + if reader.read_line(&mut input_channel_name).is_err() { + bail!("Failed to read user input"); + } + input_channel_name = String::from(input_channel_name.trim()); + if toolchain_override::Channel::from_str(&input_channel_name).is_err() { + bail!( + "Invalid channel '{}', expected one of {}.", + input_channel_name, + VALID_CHANNEL_STR, + ); + } else { + export_channel = input_channel_name; + } + } + + let toolchain_override = ToolchainOverride { + cfg: OverrideCfg::new( + ToolchainCfg { + // here shouldn't be err as we has checked above + channel: toolchain_override::Channel::from_str(&export_channel)?, + }, + Some(version_map), + ), + path: toolchain_info_path.clone(), + }; + let document = toolchain_override.to_toml(); + if std::fs::write(toolchain_override.path, document.to_string()).is_err() { + bail!("Failed to write {}", toolchain_info_path.display()); + } + + info!( + "Exported toolchain \"{}\" to file: \"{}\"", + toolchain_name, + toolchain_info_path.display() + ); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + channel, path::settings_file, toolchain::DistToolchainName, + toolchain_override::ToolchainOverride, + }; + use pretty_assertions::assert_eq; + use serial_test::serial; + use std::fs; + + // Simulate user input + const INPUT_NOP: &[u8; 1] = b"\n"; + const INPUT_YES: &[u8; 2] = b"y\r"; + const INPUT_NO: &[u8; 4] = b"n \r\n"; + const INPUT_INVALID_CHANNEL: &[u8; 11] = b"my-channel\n"; + const INVALID_CHANNEL: &str = "my-channel"; + + // Mock file contents + const NO_COMPONENTS_FILE_CONTENTS: &str = r#" +[toolchain] +channel = "beta-3" +"#; + + const BETA_3_FILE_CONTENTS: &str = r#" +[toolchain] +channel = "beta-3" + +[components] +forc = "0.37.3" +forc-deploy = "0.37.3" +forc-explore = "0.28.1" +forc-run = "0.37.3" +forc-tx = "0.37.3" +forc-wallet = "0.2.2" +fuel-core = "0.17.11" +"#; + + fn remove_toolchain_info() { + let toolchain_path = match get_fuel_toolchain_toml() { + Some(path) => path, + None => PathBuf::from(FUEL_TOOLCHAIN_TOML_FILE), + }; + + if toolchain_path.exists() { + fs::remove_file(&toolchain_path) + .unwrap_or_else(|_| panic!("remove file {:?}", toolchain_path)); + } + } + fn create_toolchain_info() { + let toolchain_path = match get_fuel_toolchain_toml() { + Some(path) => path, + None => PathBuf::from(FUEL_TOOLCHAIN_TOML_FILE), + }; + + fs::create_dir_all(toolchain_path.parent().unwrap()).expect("create parent directory"); + + std::fs::write(toolchain_path, NO_COMPONENTS_FILE_CONTENTS).expect("write toolchain file"); + } + + fn assert_toolchain_info(expected_toml: &str) { + let toolchain_path = get_fuel_toolchain_toml().expect("toolchain toml exists"); + assert!(toolchain_path.exists()); + let actual_toml = fs::read_to_string(&toolchain_path).unwrap(); + assert_eq!(expected_toml.to_string(), actual_toml); + } + + // Creates a default `settings.toml` + fn create_settings_file() { + let setting_file_path = settings_file(); + if !setting_file_path.exists() { + fs::create_dir_all(setting_file_path.parent().unwrap()) + .expect("create parent directory"); + std::fs::write(setting_file_path, b"default_toolchain = \"latest\"") + .expect("write settings file"); + } + } + + fn assert_channel_name(expected: String) { + let toolchain_path = get_fuel_toolchain_toml().expect("toolchain toml exists"); + let toolchain_override = + ToolchainOverride::from_path(toolchain_path).expect("toolchain override"); + let actual = toolchain_override.cfg.toolchain.channel.to_string(); + assert_eq!(expected, actual); + } + + #[test] + #[serial] + fn test_export_toolchain_with_existing_toolchain_info_then_cancel() { + create_settings_file(); + create_toolchain_info(); + + export( + ExportCommand { + name: Some(DistToolchainName::Beta3.to_string()), + channel: None, + force: false, + }, + &INPUT_NO[..], + ) + .expect("should succeed"); + assert_toolchain_info(NO_COMPONENTS_FILE_CONTENTS); + } + + #[test] + #[serial] + fn test_export_toolchain_with_forced_overwrite() { + create_settings_file(); + create_toolchain_info(); + + let channel = channel::BETA_3.to_string(); + export( + ExportCommand { + name: None, + channel: Some(channel.clone()), + force: true, + }, + &INPUT_NOP[..], + ) + .expect("should succeed"); + assert_channel_name(channel); + assert_toolchain_info(BETA_3_FILE_CONTENTS); + } + + #[test] + #[serial] + fn test_export_toolchain_with_forced_overwrite_and_valid_input() { + create_settings_file(); + create_toolchain_info(); + + let channel = channel::BETA_3.to_string(); + let channel_input = format!("{}\n", channel); + export( + ExportCommand { + name: None, + channel: None, + force: true, + }, + channel_input.as_bytes(), + ) + .expect("should succeed"); + assert_channel_name(channel); + assert_toolchain_info(BETA_3_FILE_CONTENTS); + } + + #[test] + #[serial] + fn test_export_toolchain_with_forced_overwrite_and_valid_provide() { + create_settings_file(); + create_toolchain_info(); + + let channel = channel::BETA_3.to_string(); + let channel_input = format!("{}\n", channel); + export( + ExportCommand { + name: None, + channel: Some(INVALID_CHANNEL.to_string()), + force: true, + }, + channel_input.as_bytes(), + ) + .expect("should succeed"); + assert_channel_name(channel); + assert_toolchain_info(BETA_3_FILE_CONTENTS); + } + + #[test] + #[serial] + fn test_export_toolchain_without_forced_overwrite() { + create_settings_file(); + create_toolchain_info(); + + let channel = channel::BETA_3.to_string(); + export( + ExportCommand { + name: None, + channel: Some(channel.clone()), + force: false, + }, + &INPUT_YES[..], + ) + .expect("should succeed"); + assert_channel_name(channel); + assert_toolchain_info(BETA_3_FILE_CONTENTS); + } + + #[test] + #[serial] + fn test_export_toolchain_without_exists_toolchain_info_with_valid_channel() { + create_settings_file(); + // case: path not exist with valid channel + remove_toolchain_info(); + let channel = channel::BETA_3.to_string(); + export( + ExportCommand { + name: None, + channel: Some(channel.clone()), + force: false, + }, + &INPUT_NOP[..], + ) + .expect("should succeed"); + assert_channel_name(channel); + assert_toolchain_info(BETA_3_FILE_CONTENTS); + } + + #[test] + #[serial] + fn test_export_toolchain_without_exists_toolchain_info_with_invalid_channel() { + create_settings_file(); + remove_toolchain_info(); + let channel = channel::BETA_3.to_string(); + let channel_input = format!("{}\n", channel); + export( + ExportCommand { + name: None, + channel: None, + force: false, + }, + channel_input.as_bytes(), + ) + .expect("should succeed"); + assert_channel_name(channel); + assert_toolchain_info(BETA_3_FILE_CONTENTS); + } + + #[test] + #[should_panic( + expected = "Invalid channel 'my-channel', expected one of ." + )] + #[serial] + fn test_export_toolchain_with_invalid_channel() { + create_settings_file(); + remove_toolchain_info(); + export( + ExportCommand { + name: Some(DistToolchainName::Beta3.to_string()), + channel: Some(INVALID_CHANNEL.to_string()), + force: false, + }, + &INPUT_INVALID_CHANNEL[..], + ) + .unwrap(); + } + + #[test] + #[should_panic( + expected = "Invalid channel 'my-channel', expected one of ." + )] + #[serial] + fn test_export_toolchain_without_channel() { + create_settings_file(); + remove_toolchain_info(); + export( + ExportCommand { + name: Some(INVALID_CHANNEL.to_string()), + channel: None, + force: false, + }, + &INPUT_INVALID_CHANNEL[..], + ) + .unwrap(); + } +} diff --git a/src/ops/fuelup_toolchain/mod.rs b/src/ops/fuelup_toolchain/mod.rs index 468cfc7a8..7c98d9d91 100644 --- a/src/ops/fuelup_toolchain/mod.rs +++ b/src/ops/fuelup_toolchain/mod.rs @@ -1,3 +1,4 @@ +pub mod export; pub mod install; pub mod list_revisions; pub mod new; diff --git a/src/toolchain_override.rs b/src/toolchain_override.rs index d1cdadb50..f8ef7d70d 100644 --- a/src/toolchain_override.rs +++ b/src/toolchain_override.rs @@ -1,3 +1,9 @@ +use crate::channel::{is_beta_toolchain, LATEST, NIGHTLY}; +use crate::constants::{DATE_FORMAT, FUEL_TOOLCHAIN_TOML_FILE, VALID_CHANNEL_STR}; +use crate::toolchain::{DistToolchainDescription, DistToolchainName, Toolchain}; +use crate::{ + download::DownloadCfg, file, path::get_fuel_toolchain_toml, target_triple::TargetTriple, +}; use anyhow::{bail, Result}; use semver::Version; use serde::de::Error; @@ -7,16 +13,9 @@ use std::fmt; use std::str::FromStr; use std::{collections::HashMap, path::PathBuf}; use time::Date; -use toml_edit::{de, ser, value, Document}; +use toml_edit::{de, ser, Document, Key, Table, Value}; use tracing::{info, warn}; -use crate::channel::{is_beta_toolchain, LATEST, NIGHTLY}; -use crate::constants::{DATE_FORMAT, FUEL_TOOLCHAIN_TOML_FILE}; -use crate::toolchain::{DistToolchainDescription, Toolchain}; -use crate::{ - download::DownloadCfg, file, path::get_fuel_toolchain_toml, target_triple::TargetTriple, -}; - // 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 @@ -69,7 +68,7 @@ where |_| { Err(Error::invalid_value( serde::de::Unexpected::Str(&channel_str), - &"one of ", + &format!("one of {}", VALID_CHANNEL_STR).as_str(), )) }, Result::Ok, @@ -97,8 +96,8 @@ impl FromStr for Channel { if let Some((name, d)) = s.split_once('-') { Ok(Self { - name: name.to_string(), - date: Date::parse(d, DATE_FORMAT).ok(), + name: DistToolchainName::from_str(name)?.to_string(), + date: Some(Date::parse(d, DATE_FORMAT)?), }) } else { if s == LATEST || s == NIGHTLY { @@ -121,11 +120,20 @@ impl ToolchainOverride { pub fn to_toml(&self) -> Document { let mut document = toml_edit::Document::new(); - document["toolchain"]["channel"] = value(self.cfg.toolchain.channel.to_string()); + let mut toolchain_table = Table::from_iter(vec![( + Key::from("channel"), + Value::from(self.cfg.toolchain.channel.to_string()), + )]); + toolchain_table.sort_values(); + document.insert("toolchain", toml_edit::Item::Table(toolchain_table)); if let Some(components) = &self.cfg.components { - for (k, v) in components.iter() { - document["components"][k] = value(v.to_string()); - } + let mut components_table = Table::from_iter( + components + .iter() + .map(|(k, v)| (Key::from(k), Value::from(v.to_string()))), + ); + components_table.sort_values(); + document.insert("components", toml_edit::Item::Table(components_table)); } document } @@ -258,17 +266,25 @@ channel = "nightly" let result = OverrideCfg::from_toml(LATEST); assert!(result.is_err()); let e = result.unwrap_err(); - assert_eq!(e - .to_string(), - "invalid value: string \"latest\", expected one of for key `toolchain.channel`".to_string()); + assert_eq!( + e.to_string(), + format!( + "invalid value: string \"latest\", expected one of {} for key `toolchain.channel`", + VALID_CHANNEL_STR + ) + ); let result = OverrideCfg::from_toml(NIGHTLY); assert!(result.is_err()); let e = result.unwrap_err(); - assert_eq!(e - .to_string(), - "invalid value: string \"nightly\", expected one of for key `toolchain.channel`".to_string()); + assert_eq!( + e.to_string(), + format!( + "invalid value: string \"nightly\", expected one of {} for key `toolchain.channel`", + VALID_CHANNEL_STR + ) + ); } #[test] diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 000000000..a6db76ad4 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1 @@ +pub mod version; diff --git a/src/util/version.rs b/src/util/version.rs new file mode 100644 index 000000000..4c60d7c2f --- /dev/null +++ b/src/util/version.rs @@ -0,0 +1,30 @@ +use anyhow::{bail, Result}; +use semver::Version; +use std::path::Path; + +pub fn exec_version(component_executable: &Path) -> Result { + 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().last() { + Some(v) => { + let version = Version::parse(v)?; + Ok(version) + } + None => { + bail!("Error getting version string"); + } + } + } + Err(e) => { + if component_executable.exists() { + bail!("execution error - {}", e); + } else { + bail!("not found"); + } + } + } +}