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..91390ab0 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, @@ -474,6 +527,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-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/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/toolchain.rs b/crates/huak-package-manager/src/ops/toolchain.rs new file mode 100644 index 00000000..9ade195c --- /dev/null +++ b/crates/huak-package-manager/src/ops/toolchain.rs @@ -0,0 +1,51 @@ +use crate::{Config, HuakResult}; +use huak_toolchain::{Channel, Tool}; +use std::path::PathBuf; + +pub fn add_to_toolchain(tool: Tool, channel: Option, config: &Config) -> HuakResult<()> { + todo!() +} + +pub fn display_toolchain_info(channel: Option, config: &Config) -> HuakResult<()> { + todo!() +} + +pub fn install_toolchain( + channel: Option, + target: Option, + config: &Config, +) -> HuakResult<()> { + todo!() +} + +pub fn list_toolchains(config: &Config) -> HuakResult<()> { + todo!() +} + +pub fn remove_from_toolchain( + tool: Tool, + channel: Option, + config: &Config, +) -> HuakResult<()> { + todo!() +} + +pub fn run_from_toolchain(tool: Tool, channel: Option, config: &Config) -> HuakResult<()> { + todo!() +} + +pub fn uninstall_toolchain(channel: Option, config: &Config) -> HuakResult<()> { + todo!() +} + +pub fn update_toolchain( + tool: Option, + channel: Option, + config: &Config, +) -> HuakResult<()> { + todo!() +} + +pub fn use_toolchain(channel: Channel, config: &Config) -> HuakResult<()> { + todo!() +} 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..9a1269f1 --- /dev/null +++ b/crates/huak-toolchain/src/channel.rs @@ -0,0 +1,81 @@ +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 { + huak: Option, + 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 `DiscriptorParts` data. + let mut parts = Vec::with_capacity(6); + + if let Some(huak) = &self.huak { + parts.push(huak.to_string()); + } + + 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..eb3bb7f1 --- /dev/null +++ b/crates/huak-toolchain/src/error.rs @@ -0,0 +1,7 @@ +use thiserror::Error as ThisError; + +#[derive(ThisError, Debug)] +pub enum Error { + #[error("a problem occurred attempting to parse a channel: {0}")] + ParseChannelError(String), +} diff --git a/crates/huak-toolchain/src/lib.rs b/crates/huak-toolchain/src/lib.rs new file mode 100644 index 00000000..4ce56cdb --- /dev/null +++ b/crates/huak-toolchain/src/lib.rs @@ -0,0 +1,85 @@ +//! # 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; +use error::Error; +use std::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, path: PathBuf) -> Self { + Toolchain { + inner: ToolchainInner { + channel, + path, + tools: Tools::new(), + }, + } + } + + pub fn channel(&self) -> &Channel { + &self.inner.channel + } + + pub fn path(&self) -> &PathBuf { + &self.inner.path + } +} + +impl From for Toolchain { + fn from(value: PathBuf) -> Self { + todo!() + } +} + +struct ToolchainInner { + channel: Channel, + path: PathBuf, + tools: Tools, +} 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!() + } +}