feat(rustup): add rustup backend
This commit adds support for rustup and was provided by @InnocentZero. The main change under the hood is that rustup packages require up to three path elements, either 'toolchain/<VERSION>' or 'component/<VERSION>/<component>', where <VERSION> can be stable, nightly, or any explicit rust version. This change was facilitated by making Package::repo crate-public. The README still needs to be updated. Related to #54. Merges #55.
This commit is contained in:
@@ -5,3 +5,4 @@ pub mod debian;
|
||||
pub mod flatpak;
|
||||
pub mod python;
|
||||
pub mod rust;
|
||||
pub mod rustup;
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
use crate::backend::backend_trait::{Backend, Switches, Text};
|
||||
use crate::backend::macros::impl_backend_constants;
|
||||
use crate::{Group, Package};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::collections::HashSet;
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
use std::process::{Command, ExitStatus};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Rustup {
|
||||
pub(crate) packages: HashSet<Package>,
|
||||
}
|
||||
|
||||
enum Repotype {
|
||||
Toolchain,
|
||||
Component,
|
||||
}
|
||||
|
||||
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<HashSet<Package>> {
|
||||
let mut toolchains_vec = self
|
||||
.run_toolchain_command(get_info_switches(Repotype::Toolchain))
|
||||
.context("Getting installed toolchains")?;
|
||||
|
||||
let mut toolchains: HashSet<Package> = toolchains_vec
|
||||
.iter()
|
||||
.map(|name| ["toolchain", name].join("/").into())
|
||||
.collect();
|
||||
|
||||
let packages: HashSet<Package> = self
|
||||
.run_component_command(get_info_switches(Repotype::Component), &mut toolchains_vec)
|
||||
.context("Getting installed components")?
|
||||
.iter()
|
||||
.map(|name| ["component", name].join("/").into())
|
||||
.collect();
|
||||
|
||||
toolchains.extend(packages);
|
||||
Ok(toolchains)
|
||||
}
|
||||
|
||||
fn get_explicitly_installed_packages(&self) -> Result<HashSet<Package>> {
|
||||
self.get_all_installed_packages()
|
||||
.context("Getting all installed packages")
|
||||
}
|
||||
|
||||
fn make_dependency(&self, _: &[Package]) -> Result<ExitStatus> {
|
||||
bail!("Not supported by {}", self.get_binary())
|
||||
}
|
||||
|
||||
fn install_packages(&self, packages: &[Package], _: bool) -> Result<ExitStatus> {
|
||||
for p in packages {
|
||||
let repo = match p.repo.as_ref() {
|
||||
Some(name) => name,
|
||||
None => bail!("Not specified whether it is a toolchain or a component"),
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
match repo.as_str() {
|
||||
"toolchain" => {
|
||||
cmd.args(get_install_switches(Repotype::Toolchain));
|
||||
cmd.arg(&p.name);
|
||||
}
|
||||
"component" => {
|
||||
cmd.args(get_install_switches(Repotype::Component));
|
||||
|
||||
let mut iter = p.name.split('/');
|
||||
|
||||
let toolchain = match iter.next() {
|
||||
Some(name) => name,
|
||||
None => bail!("Toolchain not specified!"),
|
||||
};
|
||||
cmd.arg(toolchain);
|
||||
|
||||
let component = match iter.next() {
|
||||
Some(name) => name,
|
||||
None => bail!("Component not specified!"),
|
||||
};
|
||||
cmd.arg(component);
|
||||
}
|
||||
_ => bail!("No such type is managed by rustup!"),
|
||||
}
|
||||
|
||||
let result = cmd.status().context("Installing toolchain {p}");
|
||||
if !result.as_ref().is_ok_and(|exit| exit.success()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Ok(ExitStatus::from_raw(0))
|
||||
}
|
||||
|
||||
fn remove_packages(&self, packages: &[Package], _: bool) -> Result<ExitStatus> {
|
||||
let mut toolchains_rem = Vec::new();
|
||||
|
||||
for p in packages {
|
||||
let repo = match p.repo.as_ref() {
|
||||
Some(reponame) => reponame,
|
||||
None => bail!("Not specified whether it is a toolchain or a component"),
|
||||
};
|
||||
|
||||
if repo == "toolchain" {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
cmd.args(get_remove_switches(Repotype::Toolchain))
|
||||
.arg(&p.name);
|
||||
toolchains_rem.push(p.name.as_str());
|
||||
|
||||
let result = cmd.status().context("Removing toolchain {p}");
|
||||
if !result.as_ref().is_ok_and(|exit| exit.success()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for p in packages {
|
||||
let repo = match p.repo.as_ref() {
|
||||
Some(reponame) => reponame,
|
||||
None => bail!("Not specified whether it is a toolchain or a component"),
|
||||
};
|
||||
|
||||
let mut iter = p.name.split('/').peekable();
|
||||
let toolchain = match iter.peek() {
|
||||
Some(name) => name,
|
||||
None => bail!("No toolchain name provided for the given component!"),
|
||||
};
|
||||
|
||||
if repo == "component" && !toolchains_rem.contains(toolchain) {
|
||||
let mut cmd = Command::new(self.get_binary());
|
||||
|
||||
cmd.args(get_remove_switches(Repotype::Component))
|
||||
.arg(toolchain);
|
||||
iter.next();
|
||||
let component = match iter.next() {
|
||||
Some(name) => name,
|
||||
None => bail!("No component name provided for {}", p.name),
|
||||
};
|
||||
|
||||
cmd.arg(component);
|
||||
|
||||
let result = cmd.status().context("Removing toolchain {p}");
|
||||
if !result.as_ref().is_ok_and(|exit| exit.success()) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ExitStatus::from_raw(0))
|
||||
}
|
||||
}
|
||||
|
||||
impl Rustup {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
packages: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_component_command(
|
||||
&self,
|
||||
args: &[&str],
|
||||
toolchains: &mut Vec<String>,
|
||||
) -> Result<Vec<String>> {
|
||||
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<Vec<String>> {
|
||||
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 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<String>) {
|
||||
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("/"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,4 +19,5 @@ pub enum Backends {
|
||||
Flatpak,
|
||||
Python,
|
||||
Rust,
|
||||
Rustup,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::hash::Hash;
|
||||
#[derive(Debug, Eq, PartialOrd, Ord, Clone)]
|
||||
pub struct Package {
|
||||
pub(crate) name: String,
|
||||
repo: Option<String>,
|
||||
pub(crate) repo: Option<String>,
|
||||
}
|
||||
|
||||
fn remove_comment_and_trim_whitespace(s: &str) -> &str {
|
||||
@@ -41,10 +41,11 @@ impl Package {
|
||||
///
|
||||
/// Panics if `string` is empty.
|
||||
fn split_into_name_and_repo(string: &str) -> (String, Option<String>) {
|
||||
let mut iter = string.split('/').rev();
|
||||
let name = iter.next().expect("we checked that earlier").to_string();
|
||||
let repo = iter.next().map(|s| s.to_string());
|
||||
(name, repo)
|
||||
if let Some((before, after)) = string.split_once('/') {
|
||||
(after.to_string(), Some(before.to_string()))
|
||||
} else {
|
||||
(string.to_string(), None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to parse a string (from a line in a group file) and return a package.
|
||||
|
||||
Reference in New Issue
Block a user