diff --git a/Cargo.lock b/Cargo.lock index 7a871d6f..82b4f69f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -548,6 +548,7 @@ dependencies = [ "huak-home", "huak-package-manager", "huak-python-manager", + "huak-toolchain", "human-panic", "insta-cmd", "openssl", @@ -574,6 +575,7 @@ dependencies = [ "huak-dev", "huak-home", "huak-python-manager", + "huak-toolchain", "indexmap 2.0.2", "pep440_rs", "pep508_rs", @@ -604,6 +606,15 @@ dependencies = [ "zstd", ] +[[package]] +name = "huak-toolchain" +version = "0.0.0" +dependencies = [ + "huak-python-manager", + "pep440_rs", + "thiserror", +] + [[package]] name = "human-panic" version = "1.2.1" diff --git a/crates/huak-cli/Cargo.toml b/crates/huak-cli/Cargo.toml index 22d289fd..d0208190 100644 --- a/crates/huak-cli/Cargo.toml +++ b/crates/huak-cli/Cargo.toml @@ -20,6 +20,7 @@ colored.workspace = true huak-home = { path = "../huak-home" } huak-package-manager = { path = "../huak-package-manager"} huak-python-manager = { path = "../huak-python-manager" } +huak-toolchain = { path = "../huak-toolchain" } human-panic.workspace = true # included to build PyPi Wheels (see .github/workflow/README.md) openssl = { version = "0.10.57", features = ["vendored"], optional = true } diff --git a/crates/huak-cli/src/cli.rs b/crates/huak-cli/src/cli.rs index d251e9af..ebbaee39 100644 --- a/crates/huak-cli/src/cli.rs +++ b/crates/huak-cli/src/cli.rs @@ -11,6 +11,7 @@ use huak_package_manager::{ Verbosity, WorkspaceOptions, }; use huak_python_manager::RequestedVersion; +use huak_toolchain::{Channel, Tool}; use std::{env::current_dir, path::PathBuf, process::ExitCode, str::FromStr}; use termcolor::ColorChoice; @@ -156,6 +157,12 @@ enum Commands { #[arg(last = true)] trailing: Option>, }, + /// Manage toolchains. + #[clap(alias = "tc")] + Toolchain { + #[command(subcommand)] + command: Toolchain, + }, /// Update the project's dependencies. Update { #[arg(num_args = 0..)] @@ -188,21 +195,66 @@ enum Python { #[derive(Subcommand)] enum Toolchain { - /// List available toolchains. - List, - /// Use an available toolchain. - Use { - /// The version of Python to use. - #[arg(required = true)] - version: RequestedVersion, + /// Add a tool to a toolchain. + Add { + /// A tool to add. + tool: Tool, + /// Add a tool to a specific channel. + #[arg(long, required = false)] + channel: Option, + }, + /// Display information about a toolchain. + Info { + /// The toolchain channel to display information for. + #[arg(required = false)] + channel: Option, }, /// Install a toolchain. Install { - /// The version of Python to install. - #[arg(required = true)] - version: RequestedVersion, + /// The toolchain channel to install. + #[arg(required = false)] + channel: Option, /// The path to install a toolchain to. - target: PathBuf, + #[arg(required = false)] + target: Option, // TODO(cnpryer): Could default to home dir toolchains dir. + }, + /// List available toolchains. + List, + /// Remove a tool from a toolchain. + Remove { + /// A tool to add. + tool: Tool, + /// Remove a tool from a specific channel. + #[arg(long, required = false)] + channel: Option, + }, + /// Run a tool installed to a toolchain. + Run { + /// The tool to run. + tool: Tool, + /// The toolchain channel to run a tool from. + #[arg(long, required = false)] + channel: Option, + }, + /// Uninstall a toolchain. + Uninstall { + /// The toolchain channel to uninstall. + #[arg(required = false)] + channel: Option, + }, + /// Update the current toolchain. + Update { + /// A tool to update. + #[arg(required = false)] + tool: Option, // TODO(cnpryer): Either include @version or add version arg. + /// The toolchain channel to update. + #[arg(long, required = false)] + channel: Option, + }, + /// Use an available toolchain. + Use { + /// The toolchain channel to use. + channel: Channel, }, } @@ -345,6 +397,7 @@ fn exec_command(cmd: Commands, config: &mut Config) -> HuakResult<()> { }; test(config, &options) } + Commands::Toolchain { command } => toolchain(command, config), Commands::Update { dependencies, trailing, @@ -375,6 +428,7 @@ fn get_config(cwd: PathBuf, cli: &Cli) -> Config { workspace_root, cwd, terminal_options, + ..Default::default() }; if cli.no_color { config.terminal_options = TerminalOptions { @@ -474,6 +528,20 @@ fn test(config: &Config, options: &TestOptions) -> HuakResult<()> { ops::test_project(config, options) } +fn toolchain(command: Toolchain, config: &Config) -> HuakResult<()> { + match command { + Toolchain::Add { tool, channel } => ops::add_to_toolchain(tool, channel, config), + Toolchain::Info { channel } => ops::display_toolchain_info(channel, config), + Toolchain::Install { channel, target } => ops::install_toolchain(channel, target, config), + Toolchain::List => ops::list_toolchains(config), + Toolchain::Remove { tool, channel } => ops::remove_from_toolchain(tool, channel, config), + Toolchain::Run { tool, channel } => ops::run_from_toolchain(tool, channel, config), + Toolchain::Uninstall { channel } => ops::uninstall_toolchain(channel, config), + Toolchain::Update { tool, channel } => ops::update_toolchain(tool, channel, config), + Toolchain::Use { channel } => ops::use_toolchain(channel, config), + } +} + fn update( dependencies: Option>, config: &Config, diff --git a/crates/huak-cli/src/main.rs b/crates/huak-cli/src/main.rs index a6d2d2ef..038c8c25 100644 --- a/crates/huak-cli/src/main.rs +++ b/crates/huak-cli/src/main.rs @@ -15,6 +15,7 @@ mod error; #[must_use] pub fn main() -> ExitCode { setup_panic!(); + setup_home(); match Cli::parse().run() { Ok(0) => ExitCode::SUCCESS, @@ -33,3 +34,8 @@ pub fn main() -> ExitCode { } } } + +// TODO(cnpryer): On install +fn setup_home() { + todo!() +} diff --git a/crates/huak-package-manager/Cargo.toml b/crates/huak-package-manager/Cargo.toml index 3a4f06c8..e8aef213 100644 --- a/crates/huak-package-manager/Cargo.toml +++ b/crates/huak-package-manager/Cargo.toml @@ -28,6 +28,7 @@ toml_edit = "0.19.4" regex = "1.9.5" huak-python-manager = { path = "../huak-python-manager" } huak-home = { path = "../huak-home" } +huak-toolchain = { path = "../huak-toolchain" } [dev-dependencies] huak-dev = { path = "../huak-dev" } diff --git a/crates/huak-package-manager/src/config.rs b/crates/huak-package-manager/src/config.rs index 93cb4b6e..794f9c01 100644 --- a/crates/huak-package-manager/src/config.rs +++ b/crates/huak-package-manager/src/config.rs @@ -1,5 +1,7 @@ use std::path::PathBuf; +use huak_home::huak_home_dir; + use crate::{sys::Terminal, workspace::Workspace, TerminalOptions}; /// The main `Config` for Huak. @@ -21,7 +23,7 @@ use crate::{sys::Terminal, workspace::Workspace, TerminalOptions}; /// /// let workspace = config.workspace(); /// ``` -#[derive(Clone, Default)] +#[derive(Clone)] pub struct Config { /// The configured `Workspace` root path. pub workspace_root: PathBuf, @@ -29,6 +31,8 @@ pub struct Config { pub cwd: PathBuf, /// `Terminal` options to use. pub terminal_options: TerminalOptions, + /// Huak's home directory. + pub home: Option, } impl Config { @@ -51,6 +55,18 @@ impl Config { workspace_root: self.workspace_root, cwd: self.cwd, terminal_options, + ..Default::default() + } + } +} + +impl Default for Config { + fn default() -> Self { + Self { + workspace_root: Default::default(), + cwd: Default::default(), + terminal_options: Default::default(), + home: huak_home_dir(), } } } diff --git a/crates/huak-package-manager/src/error.rs b/crates/huak-package-manager/src/error.rs index 43d259c6..cd41cef5 100644 --- a/crates/huak-package-manager/src/error.rs +++ b/crates/huak-package-manager/src/error.rs @@ -4,6 +4,8 @@ use thiserror::Error as ThisError; pub type HuakResult = Result; +// TODO(cnpryer): If errors are given "a problem..." prompts there could be redundancy in messages. +// These prompts feel more like application experience than library needs. #[derive(ThisError, Debug)] pub enum Error { #[error("a problem with argument parsing occurred: {0}")] @@ -22,6 +24,12 @@ pub enum Error { HuakConfigurationError(String), #[error("a problem occurred resolving huak's home directory")] HuakHomeNotFound, + #[error("a toolchain cannot be found")] + HuakToolchainNotFound, + #[error("{0}")] // See TODO note above. + HuakToolchainError(#[from] huak_toolchain::Error), + #[error("a toolchain already exists: {0}")] + HuakToolchainExistsError(PathBuf), #[error("a problem with huak's internals occurred: {0}")] InternalError(String), #[error("a version number could not be parsed: {0}")] @@ -58,6 +66,8 @@ pub enum Error { TOMLDeserializationError(#[from] toml::de::Error), #[error("a problem with toml serialization occurred {0}")] TOMLSerializationError(#[from] toml::ser::Error), + #[error("{0}")] + TOMLEditError(#[from] toml_edit::TomlError), #[error("a problem with toml deserialization occurred: {0}")] TOMLEditDeserializationError(#[from] toml_edit::de::Error), #[error("a problem with toml serialization occurred {0}")] diff --git a/crates/huak-package-manager/src/fs.rs b/crates/huak-package-manager/src/fs.rs index 3b38ba57..b481ccf4 100644 --- a/crates/huak-package-manager/src/fs.rs +++ b/crates/huak-package-manager/src/fs.rs @@ -111,6 +111,11 @@ pub fn last_path_component>(path: T) -> HuakResult { Ok(path) } +/// A helper for determining if an &str is a valid path. +pub(crate) fn is_path(value: &str) -> bool { + PathBuf::from(value).exists() +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/huak-package-manager/src/lib.rs b/crates/huak-package-manager/src/lib.rs index dbd7cff0..2f3eb5d1 100644 --- a/crates/huak-package-manager/src/lib.rs +++ b/crates/huak-package-manager/src/lib.rs @@ -56,7 +56,9 @@ mod metadata; pub mod ops; mod package; mod python_environment; +mod settings; mod sys; +mod toolchain; mod version; mod workspace; diff --git a/crates/huak-package-manager/src/metadata.rs b/crates/huak-package-manager/src/metadata.rs index ad26b1e7..c02953c2 100644 --- a/crates/huak-package-manager/src/metadata.rs +++ b/crates/huak-package-manager/src/metadata.rs @@ -248,6 +248,10 @@ impl Metadata { .entry(name.to_string()) .or_insert(entrypoint.to_string()); } + + pub fn tool(&self) -> Option<&Table> { + self.tool.as_ref() + } } impl Default for Metadata { diff --git a/crates/huak-package-manager/src/ops/add.rs b/crates/huak-package-manager/src/ops/add.rs index 086dd8a4..c3230013 100644 --- a/crates/huak-package-manager/src/ops/add.rs +++ b/crates/huak-package-manager/src/ops/add.rs @@ -126,6 +126,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let ws = config.workspace(); let venv = ws.resolve_python_environment().unwrap(); @@ -162,6 +163,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let ws = config.workspace(); initialize_venv(ws.root().join(".venv"), &ws.environment()).unwrap(); diff --git a/crates/huak-package-manager/src/ops/build.rs b/crates/huak-package-manager/src/ops/build.rs index 7c86b73e..c778aca8 100644 --- a/crates/huak-package-manager/src/ops/build.rs +++ b/crates/huak-package-manager/src/ops/build.rs @@ -75,6 +75,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let ws = config.workspace(); initialize_venv(ws.root().join(".venv"), &ws.environment()).unwrap(); diff --git a/crates/huak-package-manager/src/ops/clean.rs b/crates/huak-package-manager/src/ops/clean.rs index 519866f5..4275f772 100644 --- a/crates/huak-package-manager/src/ops/clean.rs +++ b/crates/huak-package-manager/src/ops/clean.rs @@ -73,6 +73,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let options = CleanOptions { include_pycache: true, diff --git a/crates/huak-package-manager/src/ops/format.rs b/crates/huak-package-manager/src/ops/format.rs index e05927d3..c57a6e85 100644 --- a/crates/huak-package-manager/src/ops/format.rs +++ b/crates/huak-package-manager/src/ops/format.rs @@ -99,6 +99,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let ws = config.workspace(); initialize_venv(ws.root().join(".venv"), &ws.environment()).unwrap(); diff --git a/crates/huak-package-manager/src/ops/init.rs b/crates/huak-package-manager/src/ops/init.rs index 2f78d005..ba29773e 100644 --- a/crates/huak-package-manager/src/ops/init.rs +++ b/crates/huak-package-manager/src/ops/init.rs @@ -57,6 +57,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let options = WorkspaceOptions { uses_git: false }; init_lib_project(&config, &options).unwrap(); @@ -84,6 +85,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let options = WorkspaceOptions { uses_git: false }; diff --git a/crates/huak-package-manager/src/ops/install.rs b/crates/huak-package-manager/src/ops/install.rs index fa6541e1..20bba174 100644 --- a/crates/huak-package-manager/src/ops/install.rs +++ b/crates/huak-package-manager/src/ops/install.rs @@ -85,6 +85,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let ws = config.workspace(); initialize_venv(ws.root().join(".venv"), &ws.environment()).unwrap(); @@ -118,6 +119,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let ws = config.workspace(); initialize_venv(ws.root().join(".venv"), &ws.environment()).unwrap(); diff --git a/crates/huak-package-manager/src/ops/lint.rs b/crates/huak-package-manager/src/ops/lint.rs index 63353e39..f3daef77 100644 --- a/crates/huak-package-manager/src/ops/lint.rs +++ b/crates/huak-package-manager/src/ops/lint.rs @@ -105,6 +105,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let options = LintOptions { values: None, @@ -134,6 +135,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let ws = config.workspace(); initialize_venv(ws.root().join(".venv"), &ws.environment()).unwrap(); diff --git a/crates/huak-package-manager/src/ops/mod.rs b/crates/huak-package-manager/src/ops/mod.rs index 7a6ca1d1..869e2f90 100644 --- a/crates/huak-package-manager/src/ops/mod.rs +++ b/crates/huak-package-manager/src/ops/mod.rs @@ -12,6 +12,7 @@ mod python; mod remove; mod run; mod test; +mod toolchain; mod update; mod version; @@ -33,6 +34,11 @@ pub use remove::{remove_project_dependencies, RemoveOptions}; pub use run::run_command_str; use std::{path::PathBuf, process::Command}; pub use test::{test_project, TestOptions}; +pub use toolchain::{ + add_to_toolchain, display_toolchain_info, install_toolchain, list_toolchains, + remove_from_toolchain, run_from_toolchain, uninstall_toolchain, update_toolchain, + use_toolchain, +}; pub use update::{update_project_dependencies, UpdateOptions}; pub use version::display_project_version; diff --git a/crates/huak-package-manager/src/ops/new.rs b/crates/huak-package-manager/src/ops/new.rs index 9e80393d..a6f7c191 100644 --- a/crates/huak-package-manager/src/ops/new.rs +++ b/crates/huak-package-manager/src/ops/new.rs @@ -83,6 +83,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let options = WorkspaceOptions { uses_git: false }; @@ -125,6 +126,7 @@ def test_version(): workspace_root, cwd, terminal_options, + ..Default::default() }; let options = WorkspaceOptions { uses_git: false }; diff --git a/crates/huak-package-manager/src/ops/python.rs b/crates/huak-package-manager/src/ops/python.rs index a081c61e..bf4916f0 100644 --- a/crates/huak-package-manager/src/ops/python.rs +++ b/crates/huak-package-manager/src/ops/python.rs @@ -107,6 +107,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; use_python(&version.to_string(), &config).unwrap(); diff --git a/crates/huak-package-manager/src/ops/remove.rs b/crates/huak-package-manager/src/ops/remove.rs index f158338b..7b70b1c8 100644 --- a/crates/huak-package-manager/src/ops/remove.rs +++ b/crates/huak-package-manager/src/ops/remove.rs @@ -77,6 +77,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let options = RemoveOptions { install_options: InstallOptions { values: None }, @@ -124,6 +125,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let options = RemoveOptions { install_options: InstallOptions { values: None }, diff --git a/crates/huak-package-manager/src/ops/run.rs b/crates/huak-package-manager/src/ops/run.rs index 9a05a196..24e81e5e 100644 --- a/crates/huak-package-manager/src/ops/run.rs +++ b/crates/huak-package-manager/src/ops/run.rs @@ -42,6 +42,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let ws = config.workspace(); // For some reason this test fails with multiple threads used. Workspace.resolve_python_environment() diff --git a/crates/huak-package-manager/src/ops/test.rs b/crates/huak-package-manager/src/ops/test.rs index 5375018e..248adbde 100644 --- a/crates/huak-package-manager/src/ops/test.rs +++ b/crates/huak-package-manager/src/ops/test.rs @@ -81,6 +81,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let ws = config.workspace(); initialize_venv(ws.root().join(".venv"), &ws.environment()).unwrap(); diff --git a/crates/huak-package-manager/src/ops/toolchain.rs b/crates/huak-package-manager/src/ops/toolchain.rs new file mode 100644 index 00000000..c2e58b50 --- /dev/null +++ b/crates/huak-package-manager/src/ops/toolchain.rs @@ -0,0 +1,183 @@ +use crate::{Config, Error, HuakResult}; +use huak_home::huak_home_dir; +use huak_toolchain::{ + install_toolchain_with_target, toolchain_from_channel, Channel, Tool, Toolchain, +}; +use std::path::PathBuf; +use termcolor::Color; + +/// Resolve the target toolchain if a user provides one, otherwise get the current toolchain +/// for the current workspace. If no toolchain is found then emit "error: no toolchain found". +/// Add the user-provided tool to the toolchain. If the tool is +/// already installed to the toolchain, and a version is provided that's different from the +/// installed tool, then replace the installed tool with the desired version. +pub fn add_to_toolchain(tool: Tool, channel: Option, config: &Config) -> HuakResult<()> { + todo!() +} + +/// Resolve the target toolchain if a user provides one, otherwise get the current toolchain +/// for the current workspace. If no toolchain is found then emit "error: no toolchain found". +/// +/// Display the toolchain's information: +/// +/// Toolchain: +/// Path: +/// Channel: +/// Tools: +/// python () +/// ruff () +/// mypy () +/// pytest () +pub fn display_toolchain_info(channel: Option, config: &Config) -> HuakResult<()> { + let Some(info) = resolve_toolchain(channel, config)?.map(|it| it.info()) else { + return Ok(()); + }; + + config + .terminal() + .print_custom("", info, Color::White, false) +} + +/// Resolve the target toolchain by using the user-provided `Channel` if it's present, +/// otherwise attempt to resolve a current toolchain from the current workspace. +/// If no toolchain can be resolved from either user input or the current workspace, +/// and no toolchain can be found in Huak's home directory, then install the current +/// default toolchain available. +pub fn install_toolchain( + channel: Option, + target: Option, + config: &Config, +) -> HuakResult<()> { + // If a toolchain cannot be resolved with a channel or the current config data then the default + // will be installed if it doesn't already exist. + let toolchain = resolve_toolchain(channel, config)?.unwrap_or_default(); + + if toolchain.exists() { + let path = toolchain.path().expect("toolchain path"); + return Err(Error::HuakToolchainExistsError(path.clone())); + } + + // If no target path is provided we always install to Huak's toolchain directory + let Some(path) = target.or(huak_home_dir().map(|it| it.join("toolchains"))) else { + return Err(Error::InternalError( + "target path is invalid or missing".to_string(), + )); + }; + + let mut terminal = config.terminal(); + terminal.print_custom( + "", + format!("Installing {}", toolchain.name()), + Color::White, + false, + )?; + + install_toolchain_with_target(toolchain, &path)?; + + terminal.print_custom( + "", + format!("Successfully installed to {}", path.display()), + Color::White, + false, + ) +} + +/// Resolve available toolchains and display their names as a list. Display the following with +/// +/// Current toolchain: +/// +/// Installed toolchains: +/// 1: +/// 2: +/// 3: +pub fn list_toolchains(config: &Config) -> HuakResult<()> { + let Some(toolchains) = resolve_installed_toolchains(config)? else { + return Ok(()); // TODO(cnpryer): Silent or emit notice + }; + let current_toolchain = resolve_toolchain(None, config)?; + let mut terminal = config.terminal(); + + terminal.print_custom( + "Current toolchain", + current_toolchain.map_or(String::from("None"), |it| it.name()), + Color::Blue, + false, + )?; + terminal.print_custom("", "", Color::White, false)?; + terminal.print_custom("Installed toolchains", "", Color::Blue, false)?; + toolchains.iter().enumerate().for_each(|(i, toolchain)| { + config + .terminal() + .print_custom(i + 1, toolchain.name(), Color::Blue, false) + .ok(); + }); + + Ok(()) +} + +/// Resolve the target toolchain but don't perform and installs if none can be found. If a toolchain +/// can be resolved (located) then remove the tool. If the tool is not installed to the toolchain then +/// exit silently. +pub fn remove_from_toolchain( + tool: Tool, + channel: Option, + config: &Config, +) -> HuakResult<()> { + todo!() +} + +/// Resolve the target toolchain but don't perform and installs if none can be found. If a toolchain +/// can be resolved (located) then run the tool. If the tool is not installed to the toolchain then +/// emit "error: a problem occurred running a tool: {tool} is not installed" +pub fn run_from_toolchain(tool: Tool, channel: Option, config: &Config) -> HuakResult<()> { + todo!() +} + +/// Resolve the target toolchain but don't perform and installs if none can be found. If a toolchain +/// can be resolved (located) then uninstall it. +pub fn uninstall_toolchain(channel: Option, config: &Config) -> HuakResult<()> { + let Some(toolchain) = resolve_toolchain(channel, config)? else { + return Ok(()); + }; + + config.terminal().print_custom( + "", + format!("Uninstalling {}...", toolchain.name()), + Color::White, + false, + ) +} + +/// Resolve the target toolchain but don't perform and installs if none can be found. If a toolchain +/// can be resolved (located) then attempt to update its tools according to its channel. If the channel +/// is version-defined without a patch number then install the latest released Python for that channel. +/// Update the rest of the tools in the toolchain. +pub fn update_toolchain( + tool: Option, + channel: Option, + config: &Config, +) -> HuakResult<()> { + todo!() +} + +pub fn use_toolchain(channel: Channel, config: &Config) -> HuakResult<()> { + // Resolve the target toolchain if a user provides one, otherwise get the current toolchain + // for the current workspace. If none can be found then install and use the default toolchain. + todo!() +} + +// TODO(cnpryer): These helpers might be better decoupled from `Workspace` since it'd be useful +// to have Huak install toolchains from outside a workspace. +fn resolve_installed_toolchains(config: &Config) -> HuakResult>> { + todo!() +} + +fn resolve_toolchain(channel: Option, config: &Config) -> HuakResult> { + let maybe_toolchain = if let Some(it) = channel.as_ref() { + toolchain_from_channel(it) + } else { + config.workspace().current_toolchain()? + }; + + Ok(maybe_toolchain) +} diff --git a/crates/huak-package-manager/src/ops/update.rs b/crates/huak-package-manager/src/ops/update.rs index b67cc98b..174b1fd3 100644 --- a/crates/huak-package-manager/src/ops/update.rs +++ b/crates/huak-package-manager/src/ops/update.rs @@ -104,6 +104,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let ws = config.workspace(); initialize_venv(ws.root().join(".venv"), &ws.environment()).unwrap(); @@ -133,6 +134,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let ws = config.workspace(); initialize_venv(ws.root().join(".venv"), &ws.environment()).unwrap(); diff --git a/crates/huak-package-manager/src/python_environment.rs b/crates/huak-package-manager/src/python_environment.rs index 4da8509e..eb61fa84 100644 --- a/crates/huak-package-manager/src/python_environment.rs +++ b/crates/huak-package-manager/src/python_environment.rs @@ -615,6 +615,7 @@ mod tests { workspace_root, cwd, terminal_options, + ..Default::default() }; let ws = config.workspace(); let venv = ws.resolve_python_environment().unwrap(); diff --git a/crates/huak-package-manager/src/settings.rs b/crates/huak-package-manager/src/settings.rs new file mode 100644 index 00000000..d87ae606 --- /dev/null +++ b/crates/huak-package-manager/src/settings.rs @@ -0,0 +1,11 @@ +//! This module implements read and write functionality for Huak's persisted application data. +use crate::HuakResult; +use std::path::Path; +use toml_edit::Document; + +/// A helper for reading the contents of a settings.toml file. +pub(crate) fn read_settings_file>(path: T) -> HuakResult { + let doc = std::str::from_utf8(std::fs::read(path)?.as_slice())?.parse::()?; + + Ok(doc) +} diff --git a/crates/huak-package-manager/src/toolchain.rs b/crates/huak-package-manager/src/toolchain.rs new file mode 100644 index 00000000..d0c9486a --- /dev/null +++ b/crates/huak-package-manager/src/toolchain.rs @@ -0,0 +1,57 @@ +use crate::{fs::is_path, HuakResult, LocalMetadata}; +use huak_toolchain::{Channel, Toolchain}; +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; +use toml_edit::Document; + +pub(crate) fn toolchain_from_environment_variable() -> HuakResult> { + let Ok(value) = std::env::var("HUAK_TOOLCHAIN") else { + return Ok(None); + }; + + toolchain_from_path_or_channel(&value) +} + +pub(crate) fn toolchain_from_local_metadata( + metadata: LocalMetadata, +) -> HuakResult> { + let Some(value) = metadata + .metadata() + .tool() + .and_then(|it| it.get("toolchain")) + .map(|s| s.to_string()) + else { + return Ok(None); + }; + + toolchain_from_path_or_channel(&value) +} + +pub(crate) fn toolchain_from_settings>( + settings_file: Document, + scope_path: T, +) -> HuakResult> { + // TODO(cnpryer): Keying paths (is lossy correct?) + let Some(value) = settings_file + .get("toolchains") + .and_then(|it| { + it.get(scope_path.as_ref().to_string_lossy().as_ref()) + .or(it.get("default")) + }) + .map(ToString::to_string) + else { + return Ok(None); + }; + + toolchain_from_path_or_channel(&value) +} + +fn toolchain_from_path_or_channel(value: &str) -> HuakResult> { + if is_path(&value) { + Ok(Some(Toolchain::from_path(PathBuf::from(value))?)) + } else { + Ok(Some(Toolchain::new(Channel::from_str(&value)?))) + } +} diff --git a/crates/huak-package-manager/src/workspace.rs b/crates/huak-package-manager/src/workspace.rs index 6be043a3..2710c55e 100644 --- a/crates/huak-package-manager/src/workspace.rs +++ b/crates/huak-package-manager/src/workspace.rs @@ -1,4 +1,8 @@ use crate::package::Package; +use crate::settings::read_settings_file; +use crate::toolchain::{ + toolchain_from_environment_variable, toolchain_from_local_metadata, toolchain_from_settings, +}; use crate::{ environment::Environment, fs, @@ -6,6 +10,7 @@ use crate::{ python_environment::{default_venv_name, venv_config_file_name}, Config, Error, HuakResult, PythonEnvironment, }; +use huak_toolchain::Toolchain; use std::{path::PathBuf, process::Command}; /// The `Workspace` is a struct for resolving things like the current `Package` @@ -118,6 +123,31 @@ impl Workspace { Ok(python_env) } + + /// Get the current toolchain. The current toolchain is found by: + /// 1. HUAK_TOOLCHAIN environment variable + /// 2. [tool.huak.toolchain] pyproject.toml configuration + /// 3. ~/.huak/settings.toml configuration + pub fn current_toolchain(&self) -> HuakResult> { + // Parse `Toolchain` from `HUAK_TOOLCHAIN` environment variable. The environment variable + // might be either a toolchain name, channel, or a path. We can parse it as either a `Channel` + // or a `PathBuf` and then resolve a `Toolchain from that`. + let maybe_toolchain = toolchain_from_environment_variable()?.or( + toolchain_from_local_metadata(self.current_local_metadata()?)?, + ); + + if maybe_toolchain.is_none() { + let Some(settings_path) = self.config.home.as_ref().map(|it| it.join("settings.toml")) + else { + return Ok(None); + }; + let maybe_toolchain = + toolchain_from_settings(read_settings_file(settings_path)?, &self.root)?; + Ok(maybe_toolchain) + } else { + Ok(maybe_toolchain) + } + } } /// A struct used to configure options for `Workspace`s. diff --git a/crates/huak-toolchain/Cargo.toml b/crates/huak-toolchain/Cargo.toml new file mode 100644 index 00000000..9c9c896d --- /dev/null +++ b/crates/huak-toolchain/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "huak-toolchain" +version = "0.0.0" +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +huak-python-manager = { path = "../huak-python-manager" } +pep440_rs.workspace = true +thiserror.workspace = true + +[lints] +workspace = true diff --git a/crates/huak-toolchain/README.md b/crates/huak-toolchain/README.md new file mode 100644 index 00000000..42c8fdd7 --- /dev/null +++ b/crates/huak-toolchain/README.md @@ -0,0 +1,3 @@ +# Toolchain + +The toolchain implementation for Huak. \ No newline at end of file diff --git a/crates/huak-toolchain/src/channel.rs b/crates/huak-toolchain/src/channel.rs new file mode 100644 index 00000000..acc48bb9 --- /dev/null +++ b/crates/huak-toolchain/src/channel.rs @@ -0,0 +1,76 @@ +use std::{fmt::Display, str::FromStr}; + +#[derive(Default, Clone, Debug)] +pub enum Channel { + #[default] + Default, + Version(Version), + Descriptor(DescriptorParts), +} + +/// Parse `Channel` from strings. This is useful for parsing channel inputs for applications implementing CLI. +impl FromStr for Channel { + type Err = crate::Error; + + fn from_str(s: &str) -> Result { + todo!() + } +} + +// Right now this is just a dynamic struct of `Release` data. +#[derive(Clone, Debug)] +pub struct DescriptorParts { + kind: Option, + version: Option, + os: Option, + architecture: Option, + build_configuration: Option, +} + +impl Display for DescriptorParts { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Only allocate enough for `DescriptorParts` data. + let mut parts = Vec::with_capacity(5); + + if let Some(kind) = &self.kind { + parts.push(kind.to_string()); + } + + if let Some(version) = &self.version { + parts.push(format!("{}", version)); + } + + if let Some(os) = &self.os { + parts.push(os.to_string()); + } + + if let Some(architecture) = &self.architecture { + parts.push(architecture.to_string()); + } + + if let Some(build_config) = &self.build_configuration { + parts.push(build_config.to_string()); + } + + write!(f, "{}", parts.join("-")) + } +} + +#[derive(Clone, Debug)] +pub struct Version { + major: u8, + minor: u8, + patch: Option, +} + +impl Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}", self.major, self.minor)?; + + if let Some(patch) = self.patch { + write!(f, ".{patch}")?; + } + + Ok(()) + } +} diff --git a/crates/huak-toolchain/src/error.rs b/crates/huak-toolchain/src/error.rs new file mode 100644 index 00000000..97dd2248 --- /dev/null +++ b/crates/huak-toolchain/src/error.rs @@ -0,0 +1,17 @@ +use std::path::PathBuf; + +use thiserror::Error as ThisError; + +#[derive(ThisError, Debug)] +pub enum Error { + #[error("invalid directory: {0}")] + InvalidDirectory(PathBuf), + #[error("a problem occurred due to an invalid toolchain: {0}")] + InvalidToolchain(String), + #[error("{0}")] + IoError(#[from] std::io::Error), + #[error("a problem occurred attempting to parse a channel: {0}")] + ParseChannelError(String), + #[error("a toolchain already exists: {0}")] + ToolchainExistsError(String), +} diff --git a/crates/huak-toolchain/src/lib.rs b/crates/huak-toolchain/src/lib.rs new file mode 100644 index 00000000..72814a96 --- /dev/null +++ b/crates/huak-toolchain/src/lib.rs @@ -0,0 +1,146 @@ +//! # The toolchain implementation for Huak. +//! +//! ## Toolchain +//! +//! - Channel +//! - Path +//! - Tools +//! +//! ## Channels +//! +//! Channels are used to identify toolchains. +//! +//! - major.minor of a Python interpreter +//! - major.minor.patch of a Python interpreter +//! - Complete Python interpreter identifying chains (for example, 'cpython-3.12.0-apple-aarch64-pgo+lto') +//! - Etc. +//! +//! ## Path +//! +//! A unique toolchain is identifiable by the path it's installed to. A directory contains the entire toolchain. +//! +//! ## Tools +//! +//! Toolchains are composed of installed tools. The default tools installed are: +//! +//! - python (and Python installation management system) +//! - ruff +//! - mypy (TODO(cnpryer): May be replaced) +//! - pytest (TODO(cnpryer): May be replaced) +//! +//! ## Other +//! +//! Tools are centralized around a common Python inerpreter installed to the toolchain. The toolchain utilizes +//! a virtual environment shared by the tools in the toolchain. A bin directory contains the symlinked tools. +//! If a platform doesn't support symlinks hardlinks are used. +//! +//! ## `huak-toolchain` +//! +//! This crate implements Huak's toolchain via `Channel`, `Toolchain`, and `Tool`. + +pub use channel::Channel; +pub use error::Error; +use std::{ + fs, + path::{Path, PathBuf}, +}; +pub use tools::Tool; +use tools::Tools; + +mod channel; +mod error; +mod tools; + +pub struct Toolchain { + inner: ToolchainInner, +} + +impl Toolchain { + pub fn new(channel: Channel) -> Self { + Toolchain { + inner: ToolchainInner { + channel, + path: None, + tools: Tools::new(), + }, + } + } + + pub fn from_path>(path: T) -> Result { + todo!() + } + + // TODO(cnpryer): Perf + pub fn name(&self) -> String { + todo!() + } + + pub fn channel(&self) -> &Channel { + &self.inner.channel + } + + pub fn path(&self) -> Option<&PathBuf> { + self.inner.path.as_ref() + } + + /// Check if the toolchain exists as a local toolchain. + // TODO(cnpryer): `LocalToolchain` + pub fn exists(&self) -> bool { + self.path().map_or(false, |it| it.is_dir()) + } + + /// String used for `Toolchain` information. This string is formatted as: + /// + /// Toolchain: + /// Path: + /// Channel: + /// Tools: + /// python () + /// ruff () + /// mypy () + /// pytest () + pub fn info(&self) -> String { + todo!() + } +} + +// The default toolchain for Huak is cpython-{latest major.minor - 1.x}-... +impl Default for Toolchain { + fn default() -> Self { + Self { inner: todo!() } + } +} + +struct ToolchainInner { + channel: Channel, + path: Option, // TODO(cnpryer): `LocalToolchain` + tools: Tools, +} + +pub fn toolchain_from_channel(channel: &Channel) -> Option { + todo!() +} + +pub fn install_toolchain_with_target(toolchain: Toolchain, target: &PathBuf) -> Result<(), Error> { + if toolchain.exists() { + return Err(Error::ToolchainExistsError(toolchain.name())); + } + + let Some(path) = toolchain.path() else { + return Err(Error::InvalidToolchain( + "no path could be resolved".to_string(), + )); + }; + + if path.is_dir() { + fs::create_dir(path)?; + } else { + return Err(Error::InvalidDirectory(path.to_path_buf())); + } + + setup_toolchain(toolchain, target) +} + +fn setup_toolchain(toolchain: Toolchain, target: &PathBuf) -> Result<(), Error> { + todo!() +} diff --git a/crates/huak-toolchain/src/tools.rs b/crates/huak-toolchain/src/tools.rs new file mode 100644 index 00000000..0e3d2f63 --- /dev/null +++ b/crates/huak-toolchain/src/tools.rs @@ -0,0 +1,39 @@ +use std::str::FromStr; + +const DEFAULT_TOOLS: [&str; 4] = ["python", "ruff", "mypy", "pytest"]; + +pub(crate) struct Tools { + tools: Vec, +} + +impl Tools { + pub(crate) fn new() -> Self { + Tools { + tools: Vec::with_capacity(DEFAULT_TOOLS.len()), + } + } + + fn add(&mut self, tool: Tool) { + todo!() + } + + fn remove(&mut self, tool: Tool) { + todo!() + } +} + +#[derive(Clone, Debug)] +pub struct Tool { + name: String, + version: pep440_rs::Version, // TODO(cnpryer): Wrap PEP440 Version for non Python package tools. +} + +/// Parse `Tool` from strings. This is useful for parsing channel inputs for applications implementing CLI. +// TODO(cnpryer): May be better to have somethink like `ToolId` provide this instead. +impl FromStr for Tool { + type Err = crate::Error; + + fn from_str(s: &str) -> Result { + todo!() + } +} diff --git a/dev-resources/planning.md b/dev-resources/planning.md index adbe2eae..1bcf237a 100644 --- a/dev-resources/planning.md +++ b/dev-resources/planning.md @@ -73,7 +73,7 @@ Without any other arguments `install` will: - Install the latest toolchain available if Huak doesn't already have one set up. - Install the toolchain associated with the project's Python environment if it isn't already installed. - - If a `[huak.toolchain]` table is used in the project's pyproject.toml file it will install it if it isn't already installed. + - If a `[tool.huak.toolchain]` table is used in the project's pyproject.toml file it will install it if it isn't already installed. - It will attempt to figure out which toolchain to install by checking for a related virtual environment. - If no virtual environment is associated with the project it will install a toolchain compatible with the latest version of Python available on the system. - If no Python is found on the system it will install the latest available version. @@ -121,14 +121,14 @@ In order to resolve a toolchain this behavior will follow the same logic defined As mentioned in *"Installing toolchains"*, toolchains have *versions*. Versions are paired with Python release versions. Channels can be further differentiated by information such as the release source or build type, but for now channels available to Huak users will remain the default matching a CPython release. -To use a channel for a pyproject.toml-managed project add the `[huak.toolchain]` table: +To use a channel for a pyproject.toml-managed project add the `[tool.huak.toolchain]` table: ```toml -[huak.toolchain] +[tool.huak.toolchain] channel = 3.12 ``` -See *"Pyproject.toml `[huak.toolchain]`"* for more. +See *"Pyproject.toml `[tool.huak.toolchain]`"* for more. Eventually channels won't be limited to version identifiers. @@ -175,7 +175,7 @@ Huak will utilize tools from a toolchain without the user having to manage that Toolchains can be used without Huak by activating their virtual environments. If `huak toolchain install 3.12.0 --target ~/desktop` is used a directory containing an installed Python interpreter is available for use. In order to utilize these tools users can activate the virtual environment by running: ``` -. ./desktop/huak-cpython-3.12.0-apple-aarch64/.venv/bin/activate +. ./desktop/cpython-3.12.0-apple-aarch64/.venv/bin/activate ``` #### Home directory @@ -183,8 +183,8 @@ Toolchains can be used without Huak by activating their virtual environments. If - Env file: Centralized method for updating PATH and environment for Huak usage. Includes adding bin directory to PATH. - Bin directory: Contains executable programs often used as proxies for other programs for Huak. For the toolchain usage only Huak is added to this directory. - Settings file: Settings data for Huak is stored in settings.toml. Stores defaults and project-specific configuration including toolchains and eventually Python environments. -- Toolchains directory: Contains Huak's toolchains with the following naming convention huak-{interpreter kind}-{version}-{os}-{architecture}. +- Toolchains directory: Contains Huak's toolchains with the following naming convention {interpreter kind}-{version}-{os}-{architecture}. -#### Pyproject.toml `[huak.toolchain]` +#### Pyproject.toml `[tool.huak.toolchain]` This table is used to configure the toolchain for a project. The channel field is used to indicate the requested version of the toolchain, but eventually channels can include markers unlike version requests.