diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..fed4212 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +name: Release + +on: + workflow_dispatch + +env: + CARGO_TERM_COLOR: always + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Build + run: cargo build --release + - name: Move generated files + run: mv target/release/build/rhino-config-*/out/* target/release/ + - uses: actions/upload-artifact@v3 + with: + name: rhino-config + path: | + target/release/rhino-config + target/release/_rhino-config.ps1 + target/release/rhino-config.1 + target/release/rhino-config.bash + target/release/rhino-config.elv + target/release/rhino-config.fish diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c5bc9e3..d90575c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,13 +2,13 @@ name: Rust on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] workflow_dispatch: inputs: debug_enabled: - description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + description: "Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)" required: false default: false @@ -17,21 +17,20 @@ env: jobs: build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose - - name: Setup tmate session (Debug) - uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} - - uses: actions/upload-artifact@v3 - with: - name: rhino-config - path: | + - uses: actions/checkout@v3 + - name: Build + run: cargo build --verbose + - uses: actions/upload-artifact@v3 + with: + name: rhino-config + path: | target/debug/rhino-config target/debug/build/rhino-config-*/out/* + - name: Run tests + run: cargo test --verbose + - name: Setup tmate session (Debug) + uses: mxschmitt/action-tmate@v3 + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} + \ No newline at end of file diff --git a/.gitignore b/.gitignore index e014e2d..78d22d2 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ replit.nix .replit # Gitpod -gitpod.yml \ No newline at end of file +gitpod.yml +setup.sh \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9503e03..01c6d9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,6 +31,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "clap" version = "3.1.12" @@ -89,6 +95,15 @@ dependencies = [ "roff", ] +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + [[package]] name = "hashbrown" version = "0.11.2" @@ -129,6 +144,15 @@ dependencies = [ "unindent", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -189,6 +213,24 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "rhino-config" version = "0.1.0" @@ -198,6 +240,8 @@ dependencies = [ "clap_complete", "clap_mangen", "indoc", + "rstest", + "tempfile", ] [[package]] @@ -206,6 +250,34 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" +[[package]] +name = "rstest" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d912f35156a3f99a66ee3e11ac2e0b3f34ac85a07e05263d05a7e2c8810d616f" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65bd28f48be7196d222d95b9243287f48d27aca604e08497513019ff0502cc4" + [[package]] name = "strsim" version = "0.10.0" @@ -223,6 +295,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "termcolor" version = "1.1.3" diff --git a/Cargo.toml b/Cargo.toml index dff386d..cf8e78c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,7 @@ indoc = "1.0.4" clap = { version = "~3.1.10", features = ["derive"] } clap_complete = "3.1.2" clap_mangen = "0.1.6" + +[dev-dependencies] +rstest = "0.12.0" +tempfile = "3.3.0" diff --git a/src/commands/disable.rs b/src/commands/disable.rs index ec62f64..1d37ecd 100644 --- a/src/commands/disable.rs +++ b/src/commands/disable.rs @@ -2,7 +2,7 @@ use std::fs; use std::path::Path; use std::process::Command; -use anyhow::{Context, Result}; +use anyhow::{ensure, Context, Result}; pub fn mainline(config_path: &Path) -> Result<()> { fs::remove_file(&config_path).context("Unable to remove mainline config file!")?; @@ -10,6 +10,34 @@ pub fn mainline(config_path: &Path) -> Result<()> { Ok(()) } +pub fn pacstall(config_path: &Path) -> Result<()> { + fs::remove_file(&config_path).context("Unable to remove pacstall config file!")?; + println!("Pacstall has been disabled."); + println!("Removing pacstall..."); + + // Get the uninstall script from `curl` or `wget` depending upon which is + // installed on the system. Capture the output also. + let uninstall_script = if Command::new("curl").output()?.status.success() { + String::from_utf8( + Command::new("curl") + .args(["-fsSL", "https://git.io/JEZbi"]) + .output()? + .stdout, + )? + } else { + String::from_utf8( + Command::new("wget") + .args(["-q", "https://git.io/JEZbi", "-O", "-"]) + .output()? + .stdout, + )? + }; + ensure!(Command::new("bash") + .args(["-c", &uninstall_script]) + .status()? + .success()); + Ok(()) +} pub fn snapdpurge(config_path: &Path) -> Result<()> { fs::remove_file(&config_path).context("Unable to remove snapdpurge config file!")?; @@ -17,7 +45,7 @@ pub fn snapdpurge(config_path: &Path) -> Result<()> { println!("Reinstalling Snapcraft..."); - Command::new("sudo") + ensure!(Command::new("sudo") .args([ "apt", "install", @@ -25,19 +53,96 @@ pub fn snapdpurge(config_path: &Path) -> Result<()> { "gnome-software-plugin-snap", "-y", ]) - .spawn() - .context("Unable to reinstall snapd!")?; + .status() + .context("Unable to reinstall snapd!")? + .success()); - Command::new("sudo") + ensure!(Command::new("sudo") .args(["apt-mark", "unhold", "snapd"]) - .spawn() - .context("Unable to unhold snapd!")?; + .status() + .context("Unable to unhold snapd!")? + .success()); Ok(()) } -pub fn pacstall(config_path: &Path) -> Result<()> { - fs::remove_file(&config_path).context("Unable to remove pacstall config file!")?; - println!("Pacstall has been disabled."); - Ok(()) +#[cfg(test)] +mod tests { + use std::env::var; + use std::error::Error; + use std::fs::File; + use std::process::Command; + + use rstest::*; + use tempfile::{tempdir, TempDir}; + + #[fixture] + fn temp_dir() -> TempDir { tempdir().unwrap() } + + #[rstest] + fn test_mainline(temp_dir: TempDir) -> Result<(), Box> { + let config_path = temp_dir.path().join("mainline"); + File::create(&config_path)?; + + super::mainline(&config_path)?; + // Test that the config file is deleted + assert!(!config_path.exists()); + + Ok(()) + } + + #[rstest] + fn test_pacstall(temp_dir: TempDir) -> Result<(), Box> { + let config_path = temp_dir.path().join("pacstall"); + File::create(&config_path)?; + + super::pacstall(&config_path)?; + // Test that the config file is deleted + assert!(!config_path.exists()); + + Ok(()) + } + + #[rstest] + fn test_snapdpurge(temp_dir: TempDir) -> Result<(), Box> { + let config_path = temp_dir.path().join("snapdpurge"); + File::create(&config_path)?; + let snapd_previously_installed = Command::new("dpkg") + .args(["--status", "snapd"]) + .status()? + .success(); + + super::snapdpurge(&config_path)?; + // Test that the config file is deleted + assert!(!config_path.exists()); + // Test that `snapd` and `gnome-software-plugin-snap` have been installed + assert!(Command::new("dpkg") + .args(["--status", "snapd", "gnome-software-plugin-snap"]) + .status()? + .success()); + // Test that `snapd` is unholded, i.e, it doesn't appear on `apt-mark showhold` + assert!(!String::from_utf8( + Command::new("sh") + .arg("apt-mark") + .arg("showhold") + .output()? + .stdout + )? + .contains("snapd")); + + // Purge `snapd` and `gnome-software-plugin-snap` if previously not installed + // before test Don't run if the test is being run on a CI + if !snapd_previously_installed && var("CI").is_err() { + Command::new("sudo") + .args([ + "apt", + "autopurge", + "snapd", + "gnome-software-plugin-snap", + "--assume-yes", + ]) + .status()?; + } + Ok(()) + } } diff --git a/src/commands/enable.rs b/src/commands/enable.rs index 087b944..585ce7d 100644 --- a/src/commands/enable.rs +++ b/src/commands/enable.rs @@ -2,7 +2,7 @@ use std::fs::{self, File}; use std::path::Path; use std::process::Command; -use anyhow::{Context, Result}; +use anyhow::{ensure, Context, Result}; use indoc::indoc; pub fn mainline(config_path: &Path) -> Result<()> { @@ -26,15 +26,16 @@ pub fn pacstall(config_path: &Path) -> Result<()> { Ok(()) } -pub fn snapdpurge(config_path: &Path, home_dir: &str) -> Result<()> { +pub fn snapdpurge(config_path: &Path, snap_path: &Path) -> Result<()> { File::create(config_path).context("Failed to create the snapdpurge config!")?; - Command::new("sudo") + ensure!(Command::new("sudo") .args(["rm", "-rf", "/var/cache/snapd/"]) - .spawn() - .context("Failed to remove snapd cache!")?; + .status() + .context("Failed to remove snapd cache!")? + .success()); - Command::new("sudo") + ensure!(Command::new("sudo") .args([ "apt", "autopurge", @@ -42,13 +43,13 @@ pub fn snapdpurge(config_path: &Path, home_dir: &str) -> Result<()> { "gnome-software-plugin-snap", "-y", ]) - .spawn() - .context("Failed to remove snapd cache!")?; + .status() + .context("Failed to remove snapd cache!")? + .success()); - fs::remove_dir_all(Path::new(&format!("{}/snap", home_dir))) - .context("Failed to remove snap directory!")?; + fs::remove_dir_all(&snap_path).context("Failed to remove snap directory!")?; - Command::new("sudo") + ensure!(Command::new("sudo") .args([ "apt", "install", @@ -56,19 +57,84 @@ pub fn snapdpurge(config_path: &Path, home_dir: &str) -> Result<()> { "gnome-software-plugin-flatpak", "-y", ]) - .spawn() - .context("Failed to install flatpak!")?; + .status() + .context("Failed to install flatpak!")? + .success()); - Command::new("flatpak") + ensure!(Command::new("sudo") .args([ + "flatpak", "remote-add", "--if-not-exists", "flathub", "https://flathub.org/repo/flathub.flatpakrepo", ]) - .spawn() - .context("Failed to add flathub repository!")?; + .status() + .context("Failed to add flathub repository!")? + .success()); println!("Configuration updated, snapd has been removed from the system."); Ok(()) } + +#[cfg(test)] +mod tests { + use std::error::Error; + use std::process::Command; + + use rstest::*; + use tempfile::{tempdir, TempDir}; + + #[fixture] + fn temp_dir() -> TempDir { tempdir().unwrap() } + + #[rstest] + fn test_mainline(temp_dir: TempDir) -> Result<(), Box> { + let config_path = temp_dir.path().join("mainline"); + + super::mainline(&config_path)?; + // Test that the config file is created + assert!(config_path.exists()); + + Ok(()) + } + + #[rstest] + fn test_pacstall(temp_dir: TempDir) -> Result<(), Box> { + let config_path = temp_dir.path().join("pacstall"); + + super::pacstall(&config_path)?; + // Test that the config file is created + assert!(config_path.exists()); + + Ok(()) + } + + #[rstest] + fn test_snapdpurge(temp_dir: TempDir) -> Result<(), Box> { + let config_path = temp_dir.path().join("snapdpurge"); + let snap_dir = tempdir().unwrap(); + let snap_path = snap_dir.path(); + + super::snapdpurge(&config_path, snap_path)?; + // Test that the config file is created + assert!(config_path.exists()); + // Test that the snap_path has been deleted + assert!(!snap_path.exists()); + // Test that `snapd` and `gnome-software-plugin-snap` have been uninstalled + assert!(!Command::new("dpkg") + .args(["--status", "snapd", "gnome-software-plugin-snap"]) + .status()? + .success()); + // Test that `flatpak` has been installed + assert!(!Command::new("dpkg") + .args(["--status", "flatpak", "gnome-software-plugin-flatpak"]) + .status()? + .success()); + // Test that the flathub repository has been added to flatpak + let output = Command::new("flatpak").args(["remotes"]).output()?.stdout; + assert!(String::from_utf8(output)?.contains("flathub")); + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 94bb47a..ab93c15 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,7 +27,7 @@ use crate::commands::{disable, enable}; /// ask("Do you want to continue?"); /// ``` fn ask(message: &str) -> bool { - print!("{} [Y/n] ", message,); + print!("{} [Y/n] ", message); io::stdout().flush().unwrap(); let mut reply = String::new(); @@ -45,14 +45,16 @@ fn main() -> Result<()> { let cli = Cli::parse(); let home_dir = var("HOME").context("Unable to find HOME environment variable!")?; + let home_path = Path::new(&home_dir); - let config_dir = format!("{}/.rhino/config/", home_dir); - let config_path = Path::new(&config_dir); - fs::create_dir_all(config_path).context("Failed to create config directory!")?; + let config_path = home_path.join(".rhino/config/"); + fs::create_dir_all(&config_path).context("Failed to create config directory!")?; let pacstall_config_path = config_path.join("pacstall"); let mainline_config_path = config_path.join("mainline"); + let snapdpurge_config_path = config_path.join("snapdpurge"); + let snapdpurge_snap_path = home_path.join("snap/"); match &cli.command { Commands::Enable(flag) => { @@ -70,7 +72,7 @@ fn main() -> Result<()> { if !snapdpurge_config_path.exists() { if ask("Do you wish to remove Snapcraft (snapd) and replace it with Flatpak?") { - enable::snapdpurge(&snapdpurge_config_path, &home_dir)?; + enable::snapdpurge(&snapdpurge_config_path, &snapdpurge_snap_path)?; } else { println!( "No changes were made to the Rhino configuration, snapd has not been \ @@ -105,9 +107,9 @@ fn main() -> Result<()> { if flag.snapdpurge { ensure!( !snapdpurge_config_path.exists(), - "Mainline kernel is already enabled!" + "Snapdpurge is already enabled!" ); - enable::snapdpurge(&snapdpurge_config_path, &home_dir)?; + enable::snapdpurge(&snapdpurge_config_path, &snapdpurge_snap_path)?; } if flag.pacstall {