use std::thread::available_parallelism; use clap::{CommandFactory, Parser}; use feddy::*; use ini::Ini; fn main() { configure_logger(); let cli = Cli::parse(); let mut cmd = Cli::command(); if let Some(generator) = cli.generator { log::info!("Generating completion file for {generator:?}..."); print_completions(generator, &mut cmd); return; } if let Some(command) = cli.command { match command { Commands::ConfigureDnf => { if let Err(e) = configure_dnf() { log::error!("Error configuring dnf: {}", e); } } Commands::AddRepo(repo_command) => { if let Err(e) = add_repo(repo_command) { log::error!("Error adding repository: {}", e); } } Commands::InstallRpmFusion => { if let Err(e) = install_rpm_fusion() { log::error!("Error installing RPM Fusion: {}", e); } } Commands::ConfigureGroups(configure_groups_command) => { if let Err(e) = configure_groups(configure_groups_command) { log::error!("Error configuring groups: {}", e); } } Commands::Package(package_command) => { if let Err(e) = manage_package(package_command) { log::error!("Error executing package command: {}", e); } } } } else { cmd.print_help().unwrap(); } success!("Bye :)"); } fn configure_groups(configure_groups_command: ConfigureGroupsCommand) -> Result<()> { let groups = include_str!("../data/user_groups.txt").to_string(); if configure_groups_command.print { log::info!("Printing groups..."); println!("{}", groups); return Ok(()); } let groups = groups .lines() .map(|line| line.trim()) .filter(|line| !line.is_empty()) .filter(|group| { let group_exists = std::process::Command::new("getent") .arg("group") .arg(group) .output() .map(|output| output.status.success()) .unwrap_or(false); if !group_exists { log::warn!("Group {} does not exist.", group); } group_exists }) .collect::>(); if groups.is_empty() { log::warn!("No valid groups found to add user to."); return Ok(()); } let groups = groups.join(","); let user = configure_groups_command.user.unwrap_or_else(|| { log::info!("No user specified, using current user"); whoami::username() }); log::info!("Adding user {} to groups: {}", user, groups); let mut cmd = std::process::Command::new("usermod"); cmd.arg("-aG"); cmd.arg(groups); cmd.arg(&user); log::debug!("Executing command: {:?}", cmd); cmd.status()?; Ok(()) } fn add_repo(repo_command: AddRepoCommand) -> Result<()> { match repo_command.command { AddRepoSubCommand::Vscode => { add_repo_common( "vscode", "Visual Studio Code", "https://packages.microsoft.com/keys/microsoft.asc", "https://packages.microsoft.com/yumrepos/vscode", "/etc/yum.repos.d/vscode.repo", )?; } AddRepoSubCommand::Mullvad => { add_repo_common( "mullvad-stable", "Mullvad VPN", "https://repository.mullvad.net/rpm/mullvad-keyring.asc", "https://repository.mullvad.net/rpm/stable/$basearch", "/etc/yum.repos.d/mullvad.repo", )?; } AddRepoSubCommand::Vivaldi => { add_repo_common( "vivaldi", "Vivaldi", "https://repo.vivaldi.com/archive/linux_signing_key.pub", "https://repo.vivaldi.com/archive/rpm/x86_64", "/etc/yum.repos.d/vivaldi.repo", )?; } } Ok(()) } fn add_repo_common( name: &str, display_name: &str, gpg_key_url: &str, baseurl: &str, repo_file_path: &str, ) -> Result<()> { log::info!("Importing {} GPG key...", display_name); std::process::Command::new("rpm") .arg("--import") .arg(gpg_key_url) .status()?; let mut conf = Ini::new(); conf.with_section(Some(name)) .set("name", display_name) .set("enabled", "1") .set("gpgcheck", "1") .set("autorefresh", "1") .set("baseurl", baseurl) .set("gpgkey", gpg_key_url); log::info!("Adding {} repository...", display_name); conf.write_to_file(repo_file_path)?; Ok(()) } fn install_rpm_fusion() -> Result<()> { let fedora_version_vec = std::process::Command::new("rpm") .arg("-E") .arg("%fedora") .output()? .stdout; let fedora_version = String::from_utf8_lossy(&fedora_version_vec); let fedora_version = fedora_version.trim(); log::info!("Installing RPM Fusion for Fedora {}", fedora_version); log::info!("Enabling the openh264 library..."); std::process::Command::new("dnf") .arg("config-manager") .arg("setopt") .arg("fedora-cisco-openh264.enabled=1") .status()?; log::info!("Installing RPM Fusion free and non-free repositories..."); std::process::Command::new("dnf") .arg("install") .arg(format!( "https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-{}.noarch.rpm", fedora_version )) .arg(format!( "https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-{}.noarch.rpm", fedora_version )) .status()?; log::info!("Installing RPM Fusion additional packages..."); std::process::Command::new("dnf") .arg("install") .arg("rpmfusion-free-appstream-data") .arg("rpmfusion-nonfree-appstream-data") .arg("rpmfusion-free-release-tainted") .arg("rpmfusion-nonfree-release-tainted") .status()?; Ok(()) } fn manage_package(package_command: PackageCommand) -> Result<()> { let list = match &package_command.command { PackageSubCommand::CommonList => include_str!("../data/common_list.txt").to_string(), PackageSubCommand::AmdList => include_str!("../data/amd_list.txt").to_string(), PackageSubCommand::IntelList => include_str!("../data/intel_list.txt").to_string(), PackageSubCommand::GnomeExtraList => { include_str!("../data/gnome_extra_list.txt").to_string() } PackageSubCommand::FirmwareList => include_str!("../data/firmware_list.txt").to_string(), PackageSubCommand::CustomList(custom_list_command) => { log::info!("Using custom list from {}", custom_list_command.file); read_file!(&custom_list_command.file)? } }; if package_command.print { log::info!("Printing package list..."); println!("{}", list); return Ok(()); } manage_list(list, package_command.remove)?; Ok(()) } fn manage_list(list: String, remove: bool) -> Result<()> { process_packages(&list, remove, '+')?; process_packages(&list, remove, '-')?; Ok(()) } fn process_packages(list: &String, remove: bool, prefix: char) -> Result<()> { let packages = list .lines() .map(|line| line.trim()) .filter(|line| !line.is_empty() && line.starts_with(prefix)) .map(|line| line.trim_start_matches(prefix).trim()) .collect::>(); if packages.is_empty() { return Ok(()); } let mut dnf_cmd = std::process::Command::new("dnf"); if remove { if prefix == '+' { log::info!("Removing wanted list's packages..."); dnf_cmd.arg("remove"); } else { log::info!("Re-installing unwanted list's packages..."); dnf_cmd.arg("install").arg("--allowerasing"); } } else { if prefix == '+' { log::info!("Installing wanted list's packages..."); dnf_cmd.arg("install").arg("--allowerasing"); } else { log::info!("Removing unwanted list's packages..."); dnf_cmd.arg("remove"); } } packages.iter().for_each(|package| { dnf_cmd.arg(package); }); dnf_cmd.status()?; Ok(()) } fn configure_dnf() -> Result<()> { log::info!("Tweaking dnf configuration..."); let mut conf = Ini::load_from_file("/etc/dnf/dnf.conf")?; let max_parallel_downloads = std::cmp::min(20, std::cmp::max(available_parallelism()?.get() / 2, 3)); log::info!( "Setting max_parallel_downloads to {}", max_parallel_downloads ); log::info!("Setting defaultyes to True"); conf.with_section(Some("main")) .set("defaultyes", "True") .set("max_parallel_downloads", max_parallel_downloads.to_string()); // Write the changes back to the file log::info!("Writing changes to /etc/dnf/dnf.conf"); conf.write_to_file("/etc/dnf/dnf.conf")?; Ok(()) }