diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cdef224..ddeee25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,6 +70,14 @@ jobs: target: aarch64-apple-darwin artifact_name: stop-nagging asset_name: stop-nagging-aarch64-apple-darwin.tar.gz + - os: windows-latest + target: x86_64-pc-windows-msvc + artifact_name: stop-nagging.exe + asset_name: stop-nagging-x86_64-pc-windows-msvc.zip + - os: windows-latest + target: aarch64-pc-windows-msvc + artifact_name: stop-nagging.exe + asset_name: stop-nagging-aarch64-pc-windows-msvc.zip steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@stable @@ -84,7 +92,11 @@ jobs: staging="stop-nagging-${{ matrix.target }}" mkdir -p "$staging" cp "target/${{ matrix.target }}/release/${{ matrix.artifact_name }}" "$staging/" - tar czf "${{ matrix.asset_name }}" "$staging" + if [[ "${{ matrix.asset_name }}" == *.zip ]]; then + 7z a "${{ matrix.asset_name }}" "$staging" + else + tar czf "${{ matrix.asset_name }}" "$staging" + fi - name: Upload artifact uses: actions/upload-artifact@v3 with: @@ -112,6 +124,7 @@ jobs: if: steps.semantic.outputs.new_release_published == 'true' run: | mv artifacts/*/*.tar.gz ./ + mv artifacts/*/*.zip ./ - name: Update Release with Artifacts if: steps.semantic.outputs.new_release_published == 'true' uses: softprops/action-gh-release@v1 @@ -119,3 +132,4 @@ jobs: tag_name: v${{ steps.semantic.outputs.new_release_version }} files: | *.tar.gz + *.zip diff --git a/.github/workflows/installation_test.yml b/.github/workflows/installation_test.yml new file mode 100644 index 0000000..d035ee3 --- /dev/null +++ b/.github/workflows/installation_test.yml @@ -0,0 +1,41 @@ +name: Installation Test + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + test-linux-install: + name: Test Linux Installation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Test installation script + run: | + curl -fsSL https://raw.githubusercontent.com/${{ github.repository }}/${{ github.ref_name }}/scripts/install_stop_nagging.sh | bash + + - name: Verify installation + run: | + stop-nagging --version + # Test basic functionality + stop-nagging check + + test-windows-install: + name: Test Windows Installation + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + + - name: Test installation script + shell: powershell + run: | + iwr https://raw.githubusercontent.com/${{ github.repository }}/${{ github.ref_name }}/scripts/install_stop_nagging.ps1 -useb | iex + + - name: Verify installation + shell: powershell + run: | + stop-nagging --version + # Test basic functionality + stop-nagging check diff --git a/README.md b/README.md index 5146db5..b91fb04 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Head over to [`tools.yaml`](tools.yaml) to see the list of supported tools. ## Installation -### Quick Install +### Quick Install (Linux/macOS) ```bash curl -s https://raw.githubusercontent.com/bodo-run/stop-nagging/main/scripts/install_stop_nagging.sh | bash @@ -22,6 +22,15 @@ curl -s https://raw.githubusercontent.com/bodo-run/stop-nagging/main/scripts/ins Then add `~/.local/bin` to your PATH if not already. +### Quick Install (Windows) + +1. Download and run the PowerShell installer script: + ```powershell + # Example in PowerShell + iwr https://raw.githubusercontent.com/bodo-run/stop-nagging/main/scripts/install_stop_nagging.ps1 -UseBasicParsing | iex + ``` +2. If needed, add the installation directory (default: `$HOME\.local\bin`) to your PATH. + ### From Source 1. Ensure Rust is installed diff --git a/scripts/install_stop_nagging.ps1 b/scripts/install_stop_nagging.ps1 new file mode 100644 index 0000000..ca3b4c7 --- /dev/null +++ b/scripts/install_stop_nagging.ps1 @@ -0,0 +1,90 @@ +# install_stop_nagging.ps1 +# Install Stop-Nagging on Windows via PowerShell +param( + [string]$InstallDir = "$HOME\.local\bin" +) + +# Exit on error +$ErrorActionPreference = "Stop" + +Write-Host "Stop-Nagging Windows Installer" + +if (!(Test-Path -Path $InstallDir)) { + New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null +} + +Write-Host "Selected install directory: $InstallDir" + +# Detect architecture +$arch = $ENV:PROCESSOR_ARCHITECTURE +switch ($arch) { + "AMD64" { $target = "x86_64-pc-windows-msvc" } + "ARM64" { $target = "aarch64-pc-windows-msvc" } + default { + Write-Host "Unsupported or unknown architecture: $arch" + Write-Host "Please build from source or check for a compatible artifact." + exit 1 + } +} + +$repoOwner = "bodo-run" +$repoName = "stop-nagging" +$assetName = "stop-nagging-$target.zip" + +Write-Host "OS/ARCH => Windows / $arch" +Write-Host "Asset name => $assetName" + +Write-Host "Fetching latest release info from GitHub..." +$releasesUrl = "https://api.github.com/repos/$repoOwner/$repoName/releases/latest" +try { + $releaseData = Invoke-RestMethod -Uri $releasesUrl +} catch { + Write-Host "Failed to fetch release info from GitHub." + Write-Host "Please build from source or check back later." + exit 0 +} + +# Find the asset download URL +$asset = $releaseData.assets | Where-Object { $_.name -eq $assetName } +if (!$asset) { + Write-Host "Failed to find an asset named $assetName in the latest release." + Write-Host "Check that your OS/ARCH is built or consider building from source." + exit 0 +} + +$downloadUrl = $asset.browser_download_url +Write-Host "Downloading from: $downloadUrl" + +$zipPath = Join-Path $env:TEMP $assetName +Invoke-WebRequest -Uri $downloadUrl -OutFile $zipPath -UseBasicParsing + +Write-Host "Extracting archive..." +$extractDir = Join-Path $env:TEMP "stop-nagging-$($arch)" +if (Test-Path $extractDir) { + Remove-Item -Recurse -Force $extractDir +} +Expand-Archive -Path $zipPath -DestinationPath $extractDir + +Write-Host "Moving binary to $InstallDir..." +$binaryPath = Join-Path $extractDir "stop-nagging-$target" "stop-nagging.exe" +if (!(Test-Path $binaryPath)) { + Write-Host "stop-nagging.exe not found in the extracted folder." + exit 1 +} +Move-Item -Force $binaryPath $InstallDir + +Write-Host "Cleanup temporary files..." +Remove-Item -Force $zipPath +Remove-Item -Recurse -Force $extractDir + +Write-Host "Installation complete!" + +# Check if $InstallDir is in PATH +$pathDirs = $ENV:PATH -split ";" +if ($pathDirs -notcontains (Resolve-Path $InstallDir)) { + Write-Host "NOTE: $InstallDir is not in your PATH. Add it by running something like:" + Write-Host "`$env:Path += `";$(Resolve-Path $InstallDir)`"" + Write-Host "Or update your system's environment variables to persist this." +} + +Write-Host "Now you can run: stop-nagging --help" \ No newline at end of file diff --git a/tests/installer_test.rs b/tests/installer_test.rs new file mode 100644 index 0000000..ce40e09 --- /dev/null +++ b/tests/installer_test.rs @@ -0,0 +1,245 @@ +use std::env; +use std::fs; +#[cfg(target_family = "unix")] +use std::os::unix::fs::PermissionsExt; +use std::path::PathBuf; +use std::process::Command; +use tempfile::TempDir; + +/// Tests the Unix installer using a locally built binary +#[cfg(target_family = "unix")] +#[test] +fn test_unix_installer_with_local_binary() { + let temp_dir = TempDir::new().unwrap(); + let install_dir = temp_dir.path().join("bin"); + fs::create_dir_all(&install_dir).unwrap(); + + let binary_path = get_debug_binary_path("stop-nagging"); + let temp_binary = temp_dir.path().join("stop-nagging"); + fs::copy(&binary_path, &temp_binary).unwrap(); + + let installer_script = temp_dir.path().join("install.sh"); + let script_content = format!( + r#"#!/bin/bash +set -e +INSTALL_DIR="{}" +mkdir -p "$INSTALL_DIR" +cp "{}" "$INSTALL_DIR/stop-nagging" +chmod +x "$INSTALL_DIR/stop-nagging" +"#, + install_dir.to_str().unwrap(), + temp_binary.to_str().unwrap() + ); + + fs::write(&installer_script, script_content).unwrap(); + fs::set_permissions(&installer_script, fs::Permissions::from_mode(0o755)).unwrap(); + + let status = Command::new("bash") + .arg(&installer_script) + .status() + .unwrap(); + assert!(status.success()); + + let installed_binary = install_dir.join("stop-nagging"); + assert!(installed_binary.exists()); + verify_binary_works(&installed_binary); +} + +/// Tests the Windows installer using a locally built binary +#[cfg(target_family = "windows")] +#[test] +fn test_windows_installer_with_local_binary() { + let temp_dir = TempDir::new().unwrap(); + let install_dir = temp_dir.path().join("bin"); + fs::create_dir_all(&install_dir).unwrap(); + + let binary_path = get_debug_binary_path("stop-nagging.exe"); + let temp_binary = temp_dir.path().join("stop-nagging.exe"); + fs::copy(&binary_path, &temp_binary).unwrap(); + + let installer_script = temp_dir.path().join("install.ps1"); + let original_script = fs::read_to_string( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("scripts") + .join("install_stop_nagging.ps1"), + ) + .unwrap(); + + let modified_script = modify_windows_script(&original_script, &temp_binary, &install_dir); + + fs::write(&installer_script, modified_script).unwrap(); + + // Skip the test if PowerShell is not available + let powershell_check = Command::new("powershell") + .arg("-Command") + .arg("$PSVersionTable.PSVersion") + .status(); + if powershell_check.is_err() { + println!("Skipping Windows installer test - PowerShell not available"); + return; + } + + let output = Command::new("powershell") + .arg("-ExecutionPolicy") + .arg("Bypass") + .arg("-File") + .arg(&installer_script) + .output() + .unwrap(); + + // Print output for debugging + println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); + println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); + + let installed_binary = install_dir.join("stop-nagging.exe"); + assert!( + installed_binary.exists(), + "Binary was not installed to the expected location" + ); + verify_binary_works(&installed_binary); +} + +/// Tests the Unix installer by downloading from GitHub releases +#[test] +#[ignore] +fn test_unix_installer_download() { + if cfg!(not(target_family = "unix")) { + return; + } + + let temp_dir = TempDir::new().unwrap(); + let install_dir = temp_dir.path().join("bin"); + fs::create_dir_all(&install_dir).unwrap(); + + let installer_script = temp_dir.path().join("install.sh"); + let original_script = fs::read_to_string( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("scripts") + .join("install_stop_nagging.sh"), + ) + .unwrap(); + + let modified_script = original_script.replace( + "INSTALL_DIR=\"$HOME/.local/bin\"", + &format!("INSTALL_DIR=\"{}\"", install_dir.to_str().unwrap()), + ); + + fs::write(&installer_script, modified_script).unwrap(); + + let status = Command::new("bash") + .arg(&installer_script) + .status() + .unwrap(); + assert!(status.success()); + + let installed_binary = install_dir.join("stop-nagging"); + assert!(installed_binary.exists()); + verify_binary_works(&installed_binary); +} + +/// Tests the Windows installer by downloading from GitHub releases +#[test] +#[ignore] +fn test_windows_installer_download() { + if cfg!(not(target_family = "windows")) { + return; + } + + let temp_dir = TempDir::new().unwrap(); + let install_dir = temp_dir.path().join("bin"); + fs::create_dir_all(&install_dir).unwrap(); + + let installer_script = temp_dir.path().join("install.ps1"); + let original_script = fs::read_to_string( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("scripts") + .join("install_stop_nagging.ps1"), + ) + .unwrap(); + + let modified_script = original_script.replace( + "$InstallDir = \"$HOME\\.local\\bin\"", + &format!( + "$InstallDir = \"{}\"", + install_dir.to_str().unwrap().replace('\\', "\\\\") + ), + ); + + fs::write(&installer_script, modified_script).unwrap(); + + let status = Command::new("powershell") + .arg("-ExecutionPolicy") + .arg("Bypass") + .arg("-File") + .arg(&installer_script) + .status() + .unwrap(); + assert!(status.success()); + + let installed_binary = install_dir.join("stop-nagging.exe"); + assert!(installed_binary.exists()); + verify_binary_works(&installed_binary); +} + +// Helper functions + +fn get_debug_binary_path(binary_name: &str) -> PathBuf { + let cargo_target_dir = env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join(cargo_target_dir) + .join("debug") + .join(binary_name) +} + +fn verify_binary_works(binary_path: &PathBuf) { + let output = Command::new(binary_path).arg("--help").output().unwrap(); + assert!(output.status.success()); +} + +#[cfg(target_family = "windows")] +fn modify_windows_script( + original_script: &str, + temp_binary: &PathBuf, + install_dir: &PathBuf, +) -> String { + let script = original_script.replace( + "$InstallDir = \"$HOME\\.local\\bin\"", + &format!( + "$InstallDir = \"{}\"", + install_dir.to_str().unwrap().replace('\\', "\\\\") + ), + ); + + // Simplify the script for local binary installation + let mut modified_lines = Vec::new(); + let mut skip_block = false; + for line in script.lines() { + if line.contains("$repoOwner = ") + || line.contains("$repoName = ") + || line.contains("$assetName = ") + { + continue; + } + if line.contains("Fetching latest release") { + skip_block = true; + modified_lines.push(format!( + "Copy-Item -Path \"{}\" -Destination \"$InstallDir\\stop-nagging.exe\" -Force", + temp_binary.to_str().unwrap().replace('\\', "\\\\") + )); + continue; + } + if skip_block { + if line.contains("Installation complete") { + skip_block = false; + } + continue; + } + if !line.contains("$downloadUrl") + && !line.contains("$zipPath") + && !line.contains("$extractDir") + { + modified_lines.push(line.to_string()); + } + } + modified_lines.join("\n") +}