Compare commits

...

5 Commits
master ... rust

Author SHA1 Message Date
Thomas Forgione 1d3c6c83e4 Working on UI 2021-03-06 16:01:28 +01:00
Thomas Forgione 89838ae594 Getting shit working 2020-12-07 20:00:07 +01:00
Thomas Forgione 153d28170b Progress 2020-11-25 14:28:07 +01:00
Thomas Forgione 470c723fcf Preparing stuff 2020-11-22 18:58:20 +01:00
Thomas Forgione 7e9d69cd4c Initial commit 2020-11-22 18:25:11 +01:00
4 changed files with 332 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
target
Cargo.lock

10
Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "tforgione-installer"
version = "0.1.0"
authors = ["Thomas Forgione <thomas@forgione.fr>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dialoguer = "0.7.1"

314
src/lib.rs Normal file
View File

@ -0,0 +1,314 @@
use std::ffi::OsStr;
use std::fmt;
use std::fs::{metadata, symlink_metadata};
use std::path::PathBuf;
use std::process::Command;
use dialoguer::{theme::SimpleTheme, Confirm, MultiSelect};
macro_rules! unwrap_or_false {
($expr: expr) => {
match $expr {
Ok(v) => v,
Err(_) => return false,
}
};
}
/// Returns the home directory.
pub fn home() -> PathBuf {
PathBuf::from(std::env::var("HOME").unwrap())
}
/// A path from the user's home.
pub fn from_home(path: &str) -> PathBuf {
home().join(path)
}
/// Tests that a command exists.
pub fn test_command(cmd: &str) -> bool {
unwrap_or_false!(command("sh", &["-c", &format!("command -v {}", cmd)]).output())
.status
.success()
}
/// Helper to create commands in one line.
pub fn command<I, S>(program: &str, args: I) -> Command
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut command = Command::new(program);
command.args(args);
command
}
/// Helper to create commands for cloning a github repository.
pub fn github_clone<T: AsRef<OsStr>>(user: &str, repo: &str, dest: T) -> Command {
command(
"git",
&[
&"clone".as_ref(),
&format!("https://github.com/{}/{}", user, repo).as_ref(),
dest.as_ref(),
],
)
}
/// Helper to create commands for cloning a gitea repository.
pub fn gitea_clone<T: AsRef<OsStr>>(user: &str, repo: &str, dest: T) -> Command {
command(
"git",
&[
&"clone".as_ref(),
&format!("https://gitea.tforgione.fr/{}/{}", user, repo).as_ref(),
dest.as_ref(),
],
)
}
/// The supported package manager.
#[derive(Copy, Clone)]
pub enum PackageManager {
/// The apt manager, availble on Debian, Ubuntu and others.
Apt,
/// The pacman manager, available on ArchLinux, Manjaro and others.
Pacman,
}
impl PackageManager {
/// Returns the command to install a page using the right package manager.
pub fn install<'a>(&self, package: &'a str) -> Command {
match self {
PackageManager::Apt => {
command("sh", &["-c", &format!("sudo apt install -y {}", package)])
}
PackageManager::Pacman => command("sudo", &["pacman", "-S", package]),
}
}
}
/// All the installable components.
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum Component {
/// Whether the user is allowed to run sudo commands.
Sudo,
/// Git enables to clone repository, which is necessary for many components.
Git,
/// The configuration uses the zsh shell.
Zsh,
/// The configuration files.
Dotfiles,
/// The zshrc config file
Zshrc,
/// The oh my zsh repository.
OhMyZsh,
}
impl Component {
/// List all components.
pub fn all() -> Vec<Component> {
vec![
Component::Git,
Component::Zsh,
Component::Dotfiles,
Component::OhMyZsh,
Component::Zshrc,
]
}
/// Returns the command that installs a component.
pub fn install_command(self, package_manager: PackageManager) -> Command {
match self {
Component::Sudo => panic!("you can't install sudo"),
Component::Git => package_manager.install("git"),
Component::Zsh => package_manager.install("zsh"),
Component::Dotfiles => {
gitea_clone("tforgione", "dotfiles", from_home(".config/dotfiles"))
}
Component::OhMyZsh => github_clone("ohmyzsh", "ohmyzsh", from_home(".config/ohmyzsh")),
Component::Zshrc => command(
"ln",
&[
AsRef::<OsStr>::as_ref("-s"),
&from_home(".config/dotfiles/zshrc").as_ref(),
&from_home(".zshrc").as_ref(),
],
),
}
}
/// Installs a component.
pub fn install(self) -> Result<()> {
self.install_command(PackageManager::Apt).output().unwrap();
Ok(())
}
/// Returns a command that tests if a component is installed.
pub fn is_installed(self) -> bool {
match self {
Component::Sudo => true,
Component::Git => test_command("git"),
Component::Zsh => test_command("zsh"),
Component::Dotfiles => {
unwrap_or_false!(metadata(from_home(".config/dotfiles"))).is_dir()
}
Component::OhMyZsh => unwrap_or_false!(metadata(from_home(".config/ohmyzsh"))).is_dir(),
Component::Zshrc => unwrap_or_false!(symlink_metadata(from_home(".zshrc")))
.file_type()
.is_symlink(),
}
}
/// Returns the dependencies of each component.
pub fn dependencies(self) -> &'static [Component] {
match self {
Component::Sudo => &[],
Component::Git => &[Component::Sudo],
Component::Zsh => &[Component::Sudo],
Component::Dotfiles => &[Component::Git],
Component::Zshrc => &[Component::Zsh, Component::Dotfiles, Component::OhMyZsh],
Component::OhMyZsh => &[Component::Git],
}
}
/// Returns the str representation of the command.
pub fn to_str(self) -> &'static str {
match self {
Component::Sudo => "sudo",
Component::Git => "git",
Component::Zsh => "zsh",
Component::Dotfiles => "dotfiles",
Component::Zshrc => "zshrc",
Component::OhMyZsh => "ohmyzsh",
}
}
}
impl fmt::Display for Component {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(
fmt,
"{} (requires {}{})",
self.to_str(),
self.dependencies()
.into_iter()
.map(|x| x.to_str())
.collect::<Vec<_>>()
.join(", "),
if self.is_installed() {
", already installed"
} else {
""
}
)
}
}
/// The struct that holds the information about the install.
pub struct Installer {
/// All the components to install, and whether the users want to install them.
components: Vec<(Component, bool)>,
}
impl Installer {
/// Creates a new installer.
pub fn new() -> Installer {
Installer {
components: Component::all().into_iter().map(|x| (x, false)).collect(),
}
}
/// Asks interactively to the user which components to install.
pub fn interact(&mut self) -> bool {
let multiselected = self
.components
.iter()
.map(|(component, _)| format!("{}", component))
.collect::<Vec<_>>();
let defaults = self
.components
.iter()
.map(|(_, active)| *active)
.collect::<Vec<_>>();
let selections = MultiSelect::with_theme(&SimpleTheme)
.items(&multiselected[..])
.defaults(&defaults[..])
.interact()
.unwrap();
for i in selections {
self.components[i].1 = true;
}
println!("The following components will be installed:");
for (comp, to_install) in &self.components {
if *to_install {
println!(" - {}", comp.to_str());
}
}
let message = "Is this correct?";
Confirm::with_theme(&SimpleTheme)
.with_prompt(message)
.interact()
.unwrap()
}
/// Runs the installer.
pub fn start(&self) -> Result<()> {
for (component, to_install) in &self.components {
if *to_install {
self.install_component(*component)?;
}
}
Ok(())
}
/// Installs a component.
pub fn install_component(&self, component: Component) -> Result<()> {
if component.is_installed() || component == Component::Sudo {
return Ok(());
}
println!("Install {}", component.to_str());
for dep in component.dependencies() {
self.install_component(*dep)?;
}
if !component.is_installed() {
component.install()?;
}
Ok(())
}
}
/// The error type of this library.
pub enum Error {}
impl fmt::Display for Error {
fn fmt(&self, _: &mut fmt::Formatter) -> fmt::Result {
Ok(())
}
}
/// The result type of this library.
pub type Result<T> = std::result::Result<T, Error>;
/// Runs the installer
pub fn main() -> Result<()> {
let mut installer = Installer::new();
if installer.interact() {
installer.start()?;
}
Ok(())
}

6
src/main.rs Normal file
View File

@ -0,0 +1,6 @@
fn main() {
if let Err(e) = tforgione_installer::main() {
eprintln!("installer crashed: {}", e);
std::process::exit(1);
}
}