From 56c547793a926f2b34e3d1000f2b13bf6f19ea24 Mon Sep 17 00:00:00 2001 From: steven-omaha <35634100+steven-omaha@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:40:22 +0200 Subject: [PATCH] refact(rustup): introduce modules --- .../pacdef_core/src/backend/actual/rustup.rs | 399 ------------------ .../src/backend/actual/rustup/helpers.rs | 96 +++++ .../src/backend/actual/rustup/mod.rs | 248 +++++++++++ .../src/backend/actual/rustup/types.rs | 88 ++++ 4 files changed, 432 insertions(+), 399 deletions(-) delete mode 100644 crates/pacdef_core/src/backend/actual/rustup.rs create mode 100644 crates/pacdef_core/src/backend/actual/rustup/helpers.rs create mode 100644 crates/pacdef_core/src/backend/actual/rustup/mod.rs create mode 100644 crates/pacdef_core/src/backend/actual/rustup/types.rs diff --git a/crates/pacdef_core/src/backend/actual/rustup.rs b/crates/pacdef_core/src/backend/actual/rustup.rs deleted file mode 100644 index 492496b..0000000 --- a/crates/pacdef_core/src/backend/actual/rustup.rs +++ /dev/null @@ -1,399 +0,0 @@ -use crate::backend::backend_trait::{Backend, Switches, Text}; -use crate::backend::macros::impl_backend_constants; -use crate::cmd::run_external_command; -use crate::{Group, Package}; -use anyhow::{bail, Context, Result}; -use std::collections::HashSet; -use std::os::unix::process::ExitStatusExt; -use std::process::{Command, ExitStatus}; - -const BINARY: Text = "rustup"; -const SECTION: Text = "rustup"; - -const SWITCHES_INSTALL: Switches = &["component", "add"]; -const SWITCHES_INFO: Switches = &["component", "list", "--installed"]; -const SWITCHES_MAKE_DEPENDENCY: Switches = &[]; -const SWITCHES_NOCONFIRM: Switches = &[]; -const SWITCHES_REMOVE: Switches = &["component", "remove"]; - -const SUPPORTS_AS_DEPENDENCY: bool = false; - -#[derive(Debug, Clone)] -pub struct Rustup { - pub(crate) packages: HashSet, -} - -#[derive(Debug)] -enum Repotype { - Toolchain, - Component, -} - -impl Repotype { - fn try_from(value: T) -> Result - where - T: AsRef, - { - let value = value.as_ref(); - let result = match value { - "toolchain" => Self::Toolchain, - "component" => Self::Component, - _ => bail!("{} is neither toolchain nor component", value), - }; - Ok(result) - } -} - -/// A package as used exclusively in the rustup backend. Contrary to other packages, this does not -/// have an (optional) repository and a name, but is either a component or a toolchain, has a -/// toolchain version, and if it is a toolchain also a name. -#[derive(Debug)] -struct RustupPackage { - /// Whether it is a toolchain or a component. - pub repotype: Repotype, - /// The name of the toolchain this belongs to (stable, nightly, a pinned version) - pub toolchain: String, - /// If it is a toolchain, it will not have a component name. - /// If it is a component, this will be its name. - pub component: Option, -} - -impl RustupPackage { - /// Creates a new [`RustupPackage`]. - /// - /// # Panics - /// - /// Panics if - /// - repotype is Toolchain and component is Some, or - /// - repotype is Component and component is None. - fn new(repotype: Repotype, toolchain: String, component: Option) -> Self { - match repotype { - Repotype::Toolchain => assert!(component.is_none()), - Repotype::Component => assert!(component.is_some()), - }; - - Self { - repotype, - toolchain, - component, - } - } -} - -impl TryFrom<&Package> for RustupPackage { - type Error = anyhow::Error; - - fn try_from(package: &Package) -> Result { - let repo = package.repo.as_ref().context("getting repo from package")?; - let repotype = Repotype::try_from(repo).context("getting repotype")?; - - let (toolchain, component) = match repotype { - Repotype::Toolchain => (package.name.to_string(), None), - Repotype::Component => { - let (toolchain, component) = package - .name - .split_once('/') - .context("splitting package into toolchain and component")?; - (toolchain.to_string(), Some(component.into())) - } - }; - - Ok(Self::new(repotype, toolchain, component)) - } -} - -impl Backend for Rustup { - impl_backend_constants!(); - - fn get_all_installed_packages(&self) -> Result> { - let toolchains_vec = self - .run_toolchain_command(get_info_switches(Repotype::Toolchain)) - .context("Getting installed toolchains")?; - - let toolchains: HashSet = toolchains_vec - .iter() - .map(|name| ["toolchain", name].join("/").into()) - .collect(); - - let components: HashSet = self - .run_component_command(get_info_switches(Repotype::Component), &toolchains_vec) - .context("Getting installed components")? - .iter() - .map(|name| ["component", name].join("/").into()) - .collect(); - - let mut packages = HashSet::new(); - - packages.extend(toolchains); - packages.extend(components); - - Ok(packages) - } - - fn get_explicitly_installed_packages(&self) -> Result> { - self.get_all_installed_packages() - .context("Getting all installed packages") - } - - fn make_dependency(&self, _: &[Package]) -> Result { - panic!("Not supported by {}", self.get_binary()) - } - - fn install_packages(&self, packages: &[Package], _: bool) -> Result { - let packages = convert_all_packages_to_rustup_packages(packages)?; - - let (toolchains, components) = sort_packages_into_toolchains_and_components(packages); - - self.install_toolchains(toolchains)?; - self.install_components(components)?; - - Ok(ExitStatus::from_raw(0)) - } - - fn remove_packages(&self, packages: &[Package], _: bool) -> Result { - let rustup_packages = convert_all_packages_to_rustup_packages(packages)?; - - let (toolchains, components) = - sort_packages_into_toolchains_and_components(rustup_packages); - - let mut removed_toolchains = vec![]; - - if !toolchains.is_empty() { - let mut cmd = Command::new(self.get_binary()); - cmd.args(get_remove_switches(Repotype::Toolchain)); - - for toolchain_package in &toolchains { - let name = toolchain_package.toolchain.as_str(); - cmd.arg(name); - removed_toolchains.push(name); - } - - run_external_command(cmd) - .with_context(|| format!("removing toolchains [{toolchains:?}]"))?; - } - - for component_package in components { - let mut cmd = Command::new(self.get_binary()); - cmd.args(get_remove_switches(Repotype::Component)); - - if toolchain_of_component_was_already_removed(&removed_toolchains, &component_package) { - continue; - } - - cmd.arg(&component_package.toolchain); - cmd.arg( - component_package - .component - .as_ref() - .expect("the constructor ensures this cannot be None"), - ); - - run_external_command(cmd) - .with_context(|| format!("removing component {component_package:?}"))?; - } - Ok(ExitStatus::from_raw(0)) - } -} - -fn convert_all_packages_to_rustup_packages(packages: &[Package]) -> Result> { - let mut result = vec![]; - - for package in packages { - let rustup_package = RustupPackage::try_from(package).with_context(|| { - format!( - "converting pacdef package {} to rustup package", - package.name - ) - })?; - result.push(rustup_package); - } - - Ok(result) -} - -fn toolchain_of_component_was_already_removed( - removed_toolchains: &[&str], - component: &RustupPackage, -) -> bool { - removed_toolchains.contains(&component.toolchain.as_ref()) -} - -fn sort_packages_into_toolchains_and_components( - packages: Vec, -) -> (Vec, Vec) { - let mut toolchains = vec![]; - let mut components = vec![]; - - for package in packages { - match package.repotype { - Repotype::Toolchain => toolchains.push(package), - Repotype::Component => components.push(package), - } - } - - (toolchains, components) -} - -impl Rustup { - pub(crate) fn new() -> Self { - Self { - packages: HashSet::new(), - } - } - - fn run_component_command(&self, args: &[&str], toolchains: &[String]) -> Result> { - let mut val = Vec::new(); - - for toolchain in toolchains { - let mut cmd = Command::new(self.get_binary()); - cmd.args(args).arg(toolchain); - - let output = String::from_utf8(cmd.output()?.stdout)?; - - for component in output.lines() { - install_components(component, toolchain, &mut val); - } - } - - Ok(val) - } - - fn run_toolchain_command(&self, args: &[&str]) -> Result> { - let mut cmd = Command::new(self.get_binary()); - cmd.args(args); - - let output = String::from_utf8(cmd.output()?.stdout)?; - - let mut val = Vec::new(); - - for line in output.lines() { - let toolchain = line.split('-').next(); - match toolchain { - Some(name) => val.push(name.to_string()), - None => bail!("Toolchain name not provided!"), - } - } - - Ok(val) - } - - fn install_toolchains(&self, toolchains: Vec) -> Result<()> { - if toolchains.is_empty() { - return Ok(()); - } - let mut cmd = Command::new(self.get_binary()); - cmd.args(get_install_switches(Repotype::Toolchain)); - - for toolchain in toolchains { - cmd.arg(&toolchain.toolchain); - } - - run_external_command(cmd).context("installing toolchains")?; - - Ok(()) - } - - fn install_components(&self, components: Vec) -> Result<()> { - if components.is_empty() { - return Ok(()); - } - - let components_by_toolchain = group_components_by_toolchains(components); - - for components_for_one_toolchain in components_by_toolchain { - let mut cmd = Command::new(self.get_binary()); - cmd.args(get_install_switches(Repotype::Component)); - - let the_toolchain = &components_for_one_toolchain - .first() - .expect("will have at least one element") - .toolchain; - - cmd.arg(the_toolchain); - - for component_package in &components_for_one_toolchain { - let actual_component = component_package - .component - .as_ref() - .expect("constructor makes sure this is Some"); - - cmd.arg(actual_component); - } - - run_external_command(cmd) - .with_context(|| format!("installing [{components_for_one_toolchain:?}]"))?; - } - - Ok(()) - } -} - -fn get_install_switches(repotype: Repotype) -> Switches { - match repotype { - Repotype::Toolchain => &["toolchain", "install"], - Repotype::Component => &["component", "add", "--toolchain"], - } -} - -fn get_remove_switches(repotype: Repotype) -> Switches { - match repotype { - Repotype::Toolchain => &["toolchain", "uninstall"], - Repotype::Component => &["component", "remove", "--toolchain"], - } -} - -fn get_info_switches(repotype: Repotype) -> Switches { - match repotype { - Repotype::Toolchain => &["toolchain", "list"], - Repotype::Component => &["component", "list", "--installed", "--toolchain"], - } -} - -fn install_components(line: &str, toolchain: &str, val: &mut Vec) { - let mut chunks = line.splitn(3, '-'); - let component = chunks.next().expect("Component name is empty!"); - match component { - // these are the only components that have a single word name - "cargo" | "rustfmt" | "clippy" | "miri" | "rls" | "rustc" => { - val.push([toolchain, component].join("/")); - } - // all the others have two words hyphenated as component names - _ => { - let component = [ - component, - chunks - .next() - .expect("No such component is managed by rustup"), - ] - .join("-"); - val.push([toolchain, component.as_str()].join("/")); - } - } -} - -fn group_components_by_toolchains(components: Vec) -> Vec> { - let mut result = vec![]; - - let mut toolchains: Vec = vec![]; - - for component in components { - let index = toolchains - .iter() - .enumerate() - .find(|(_, toolchain)| toolchain == &&component.toolchain) - .map(|(idx, _)| idx) - .unwrap_or_else(|| { - toolchains.push(component.toolchain.clone()); - result.push(vec![]); - toolchains.len() - 1 - }); - result - .get_mut(index) - .expect( - "either the index already existed or we just pushed the element with that index", - ) - .push(component); - } - - result -} diff --git a/crates/pacdef_core/src/backend/actual/rustup/helpers.rs b/crates/pacdef_core/src/backend/actual/rustup/helpers.rs new file mode 100644 index 0000000..d8e56b5 --- /dev/null +++ b/crates/pacdef_core/src/backend/actual/rustup/helpers.rs @@ -0,0 +1,96 @@ +use crate::backend::backend_trait::Switches; + +use super::types::{Repotype, RustupPackage}; + +pub fn toolchain_of_component_was_already_removed( + removed_toolchains: &[String], + component: &RustupPackage, +) -> bool { + removed_toolchains.contains(&component.toolchain) +} + +pub fn sort_packages_into_toolchains_and_components( + packages: Vec, +) -> (Vec, Vec) { + let mut toolchains = vec![]; + let mut components = vec![]; + + for package in packages { + match package.repotype { + Repotype::Toolchain => toolchains.push(package), + Repotype::Component => components.push(package), + } + } + + (toolchains, components) +} + +pub fn get_install_switches(repotype: Repotype) -> Switches { + match repotype { + Repotype::Toolchain => &["toolchain", "install"], + Repotype::Component => &["component", "add", "--toolchain"], + } +} + +pub fn get_remove_switches(repotype: Repotype) -> Switches { + match repotype { + Repotype::Toolchain => &["toolchain", "uninstall"], + Repotype::Component => &["component", "remove", "--toolchain"], + } +} + +pub fn get_info_switches(repotype: Repotype) -> Switches { + match repotype { + Repotype::Toolchain => &["toolchain", "list"], + Repotype::Component => &["component", "list", "--installed", "--toolchain"], + } +} + +pub fn install_components(line: &str, toolchain: &str, val: &mut Vec) { + let mut chunks = line.splitn(3, '-'); + let component = chunks.next().expect("Component name is empty!"); + match component { + // these are the only components that have a single word name + "cargo" | "rustfmt" | "clippy" | "miri" | "rls" | "rustc" => { + val.push([toolchain, component].join("/")); + } + // all the others have two words hyphenated as component names + _ => { + let component = [ + component, + chunks + .next() + .expect("No such component is managed by rustup"), + ] + .join("-"); + val.push([toolchain, component.as_str()].join("/")); + } + } +} + +pub fn group_components_by_toolchains(components: Vec) -> Vec> { + let mut result = vec![]; + + let mut toolchains: Vec = vec![]; + + for component in components { + let index = toolchains + .iter() + .enumerate() + .find(|(_, toolchain)| toolchain == &&component.toolchain) + .map(|(idx, _)| idx) + .unwrap_or_else(|| { + toolchains.push(component.toolchain.clone()); + result.push(vec![]); + toolchains.len() - 1 + }); + result + .get_mut(index) + .expect( + "either the index already existed or we just pushed the element with that index", + ) + .push(component); + } + + result +} diff --git a/crates/pacdef_core/src/backend/actual/rustup/mod.rs b/crates/pacdef_core/src/backend/actual/rustup/mod.rs new file mode 100644 index 0000000..c0f2b77 --- /dev/null +++ b/crates/pacdef_core/src/backend/actual/rustup/mod.rs @@ -0,0 +1,248 @@ +mod helpers; +mod types; + +use crate::backend::backend_trait::{Backend, Switches, Text}; +use crate::backend::macros::impl_backend_constants; +use crate::cmd::run_external_command; +use crate::{Group, Package}; +use anyhow::{bail, Context, Result}; +use std::collections::HashSet; +use std::os::unix::process::ExitStatusExt; +use std::process::{Command, ExitStatus}; + +pub use self::types::Rustup; +use self::types::{Repotype, RustupPackage}; + +const BINARY: Text = "rustup"; +const SECTION: Text = "rustup"; + +const SWITCHES_INSTALL: Switches = &["component", "add"]; +const SWITCHES_INFO: Switches = &["component", "list", "--installed"]; +const SWITCHES_MAKE_DEPENDENCY: Switches = &[]; +const SWITCHES_NOCONFIRM: Switches = &[]; +const SWITCHES_REMOVE: Switches = &["component", "remove"]; + +const SUPPORTS_AS_DEPENDENCY: bool = false; + +impl Backend for Rustup { + impl_backend_constants!(); + + fn get_all_installed_packages(&self) -> Result> { + let toolchains_vec = self + .run_toolchain_command(helpers::get_info_switches(Repotype::Toolchain)) + .context("Getting installed toolchains")?; + + let toolchains: HashSet = toolchains_vec + .iter() + .map(|name| ["toolchain", name].join("/").into()) + .collect(); + + let components: HashSet = self + .run_component_command( + helpers::get_info_switches(Repotype::Component), + &toolchains_vec, + ) + .context("Getting installed components")? + .iter() + .map(|name| ["component", name].join("/").into()) + .collect(); + + let mut packages = HashSet::new(); + + packages.extend(toolchains); + packages.extend(components); + + Ok(packages) + } + + fn get_explicitly_installed_packages(&self) -> Result> { + self.get_all_installed_packages() + .context("Getting all installed packages") + } + + fn make_dependency(&self, _: &[Package]) -> Result { + panic!("Not supported by {}", self.get_binary()) + } + + fn install_packages(&self, packages: &[Package], _: bool) -> Result { + let packages = convert_all_packages_to_rustup_packages(packages)?; + + let (toolchains, components) = + helpers::sort_packages_into_toolchains_and_components(packages); + + self.install_toolchains(toolchains)?; + self.install_components(components)?; + + Ok(ExitStatus::from_raw(0)) + } + + fn remove_packages(&self, packages: &[Package], _: bool) -> Result { + let rustup_packages = convert_all_packages_to_rustup_packages(packages)?; + + let (toolchains, components) = + helpers::sort_packages_into_toolchains_and_components(rustup_packages); + + let removed_toolchains = self.remove_toolchains(toolchains)?; + + self.remove_components(components, removed_toolchains)?; + Ok(ExitStatus::from_raw(0)) + } +} + +fn convert_all_packages_to_rustup_packages(packages: &[Package]) -> Result> { + let mut result = vec![]; + + for package in packages { + let rustup_package = RustupPackage::try_from(package).with_context(|| { + format!( + "converting pacdef package {} to rustup package", + package.name + ) + })?; + result.push(rustup_package); + } + + Ok(result) +} + +impl Rustup { + pub(crate) fn new() -> Self { + Self { + packages: HashSet::new(), + } + } + + fn run_component_command(&self, args: &[&str], toolchains: &[String]) -> Result> { + let mut val = Vec::new(); + + for toolchain in toolchains { + let mut cmd = Command::new(self.get_binary()); + cmd.args(args).arg(toolchain); + + let output = String::from_utf8(cmd.output()?.stdout)?; + + for component in output.lines() { + helpers::install_components(component, toolchain, &mut val); + } + } + + Ok(val) + } + + fn run_toolchain_command(&self, args: &[&str]) -> Result> { + let mut cmd = Command::new(self.get_binary()); + cmd.args(args); + + let output = String::from_utf8(cmd.output()?.stdout)?; + + let mut val = Vec::new(); + + for line in output.lines() { + let toolchain = line.split('-').next(); + match toolchain { + Some(name) => val.push(name.to_string()), + None => bail!("Toolchain name not provided!"), + } + } + + Ok(val) + } + + fn install_toolchains(&self, toolchains: Vec) -> Result<()> { + if toolchains.is_empty() { + return Ok(()); + } + let mut cmd = Command::new(self.get_binary()); + cmd.args(helpers::get_install_switches(Repotype::Toolchain)); + + for toolchain in toolchains { + cmd.arg(&toolchain.toolchain); + } + + run_external_command(cmd).context("installing toolchains")?; + + Ok(()) + } + + fn install_components(&self, components: Vec) -> Result<()> { + if components.is_empty() { + return Ok(()); + } + + let components_by_toolchain = helpers::group_components_by_toolchains(components); + + for components_for_one_toolchain in components_by_toolchain { + let mut cmd = Command::new(self.get_binary()); + cmd.args(helpers::get_install_switches(Repotype::Component)); + + let the_toolchain = &components_for_one_toolchain + .first() + .expect("will have at least one element") + .toolchain; + + cmd.arg(the_toolchain); + + for component_package in &components_for_one_toolchain { + let actual_component = component_package + .component + .as_ref() + .expect("constructor makes sure this is Some"); + + cmd.arg(actual_component); + } + + run_external_command(cmd) + .with_context(|| format!("installing [{components_for_one_toolchain:?}]"))?; + } + + Ok(()) + } + + fn remove_toolchains(&self, toolchains: Vec) -> Result> { + let mut removed_toolchains = vec![]; + if !toolchains.is_empty() { + let mut cmd = Command::new(self.get_binary()); + cmd.args(helpers::get_remove_switches(Repotype::Toolchain)); + + for toolchain_package in &toolchains { + let name = toolchain_package.toolchain.as_str(); + cmd.arg(name); + removed_toolchains.push(name.to_string()); + } + + run_external_command(cmd) + .with_context(|| format!("removing toolchains [{toolchains:?}]"))?; + } + Ok(removed_toolchains) + } + + fn remove_components( + &self, + components: Vec, + removed_toolchains: Vec, + ) -> Result<()> { + for component_package in components { + let mut cmd = Command::new(self.get_binary()); + cmd.args(helpers::get_remove_switches(Repotype::Component)); + + if helpers::toolchain_of_component_was_already_removed( + &removed_toolchains, + &component_package, + ) { + continue; + } + + cmd.arg(&component_package.toolchain); + cmd.arg( + component_package + .component + .as_ref() + .expect("the constructor ensures this cannot be None"), + ); + + run_external_command(cmd) + .with_context(|| format!("removing component {component_package:?}"))?; + } + Ok(()) + } +} diff --git a/crates/pacdef_core/src/backend/actual/rustup/types.rs b/crates/pacdef_core/src/backend/actual/rustup/types.rs new file mode 100644 index 0000000..820120c --- /dev/null +++ b/crates/pacdef_core/src/backend/actual/rustup/types.rs @@ -0,0 +1,88 @@ +use anyhow::{bail, Context, Result}; +use std::collections::HashSet; + +use crate::Package; + +#[derive(Debug, Clone)] +pub struct Rustup { + pub(crate) packages: HashSet, +} + +#[derive(Debug)] +pub enum Repotype { + Toolchain, + Component, +} + +impl Repotype { + fn try_from(value: T) -> Result + where + T: AsRef, + { + let value = value.as_ref(); + let result = match value { + "toolchain" => Self::Toolchain, + "component" => Self::Component, + _ => bail!("{} is neither toolchain nor component", value), + }; + Ok(result) + } +} + +/// A package as used exclusively in the rustup backend. Contrary to other packages, this does not +/// have an (optional) repository and a name, but is either a component or a toolchain, has a +/// toolchain version, and if it is a toolchain also a name. +#[derive(Debug)] +pub struct RustupPackage { + /// Whether it is a toolchain or a component. + pub repotype: Repotype, + /// The name of the toolchain this belongs to (stable, nightly, a pinned version) + pub toolchain: String, + /// If it is a toolchain, it will not have a component name. + /// If it is a component, this will be its name. + pub component: Option, +} + +impl RustupPackage { + /// Creates a new [`RustupPackage`]. + /// + /// # Panics + /// + /// Panics if + /// - repotype is Toolchain and component is Some, or + /// - repotype is Component and component is None. + fn new(repotype: Repotype, toolchain: String, component: Option) -> Self { + match repotype { + Repotype::Toolchain => assert!(component.is_none()), + Repotype::Component => assert!(component.is_some()), + }; + + Self { + repotype, + toolchain, + component, + } + } +} + +impl TryFrom<&Package> for RustupPackage { + type Error = anyhow::Error; + + fn try_from(package: &Package) -> Result { + let repo = package.repo.as_ref().context("getting repo from package")?; + let repotype = Repotype::try_from(repo).context("getting repotype")?; + + let (toolchain, component) = match repotype { + Repotype::Toolchain => (package.name.to_string(), None), + Repotype::Component => { + let (toolchain, component) = package + .name + .split_once('/') + .context("splitting package into toolchain and component")?; + (toolchain.to_string(), Some(component.into())) + } + }; + + Ok(Self::new(repotype, toolchain, component)) + } +}