diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e7a650..7b473d1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -225,6 +225,44 @@ jobs: prerelease: false args: ${{ matrix.args }} + - name: Create portable Windows ZIP + if: matrix.platform == 'windows-latest' + shell: bash + env: + TAG: ${{ github.ref_name }} + run: | + VERSION="${TAG#v}" + PORTABLE_DIR="Donut-Portable" + mkdir -p "$PORTABLE_DIR" + + # Copy main executable + cp "src-tauri/target/${{ matrix.target }}/release/Donut.exe" "$PORTABLE_DIR/" + + # Copy sidecar binaries + cp "src-tauri/target/${{ matrix.target }}/release/donut-proxy.exe" "$PORTABLE_DIR/" + cp "src-tauri/target/${{ matrix.target }}/release/donut-daemon.exe" "$PORTABLE_DIR/" + + # Copy WebView2Loader if present + if [ -f "src-tauri/target/${{ matrix.target }}/release/WebView2Loader.dll" ]; then + cp "src-tauri/target/${{ matrix.target }}/release/WebView2Loader.dll" "$PORTABLE_DIR/" + fi + + # Create .portable marker + touch "$PORTABLE_DIR/.portable" + + # Create ZIP + 7z a "Donut_${VERSION}_x64-portable.zip" "$PORTABLE_DIR" + + - name: Upload portable ZIP to release + if: matrix.platform == 'windows-latest' + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + run: | + VERSION="${TAG#v}" + gh release upload "$TAG" "Donut_${VERSION}_x64-portable.zip" --clobber + - name: Clean up Apple certificate if: matrix.platform == 'macos-latest' && always() run: | @@ -346,7 +384,7 @@ jobs: ### Windows - [Download Windows Installer (x64)](${BASE}/Donut_${VERSION}_x64-setup.exe) + [Download Windows Installer (x64)](${BASE}/Donut_${VERSION}_x64-setup.exe) ยท [Portable (x64)](${BASE}/Donut_${VERSION}_x64-portable.zip) ### Linux diff --git a/.github/workflows/rolling-release.yml b/.github/workflows/rolling-release.yml index db552d6..b9fe8cc 100644 --- a/.github/workflows/rolling-release.yml +++ b/.github/workflows/rolling-release.yml @@ -235,6 +235,34 @@ jobs: prerelease: true args: ${{ matrix.args }} + - name: Create portable Windows ZIP + if: matrix.platform == 'windows-latest' + shell: bash + run: | + PORTABLE_DIR="Donut-Portable" + mkdir -p "$PORTABLE_DIR" + + cp "src-tauri/target/${{ matrix.target }}/release/Donut.exe" "$PORTABLE_DIR/" + cp "src-tauri/target/${{ matrix.target }}/release/donut-proxy.exe" "$PORTABLE_DIR/" + cp "src-tauri/target/${{ matrix.target }}/release/donut-daemon.exe" "$PORTABLE_DIR/" + + if [ -f "src-tauri/target/${{ matrix.target }}/release/WebView2Loader.dll" ]; then + cp "src-tauri/target/${{ matrix.target }}/release/WebView2Loader.dll" "$PORTABLE_DIR/" + fi + + touch "$PORTABLE_DIR/.portable" + + 7z a "Donut_x64-portable.zip" "$PORTABLE_DIR" + + - name: Upload portable ZIP to release + if: matrix.platform == 'windows-latest' + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NIGHTLY_TAG: "nightly-${{ steps.timestamp.outputs.timestamp }}" + run: | + gh release upload "$NIGHTLY_TAG" "Donut_x64-portable.zip" --clobber + - name: Clean up Apple certificate if: matrix.platform == 'macos-latest' && always() run: | diff --git a/src-tauri/src/app_auto_updater.rs b/src-tauri/src/app_auto_updater.rs index c7b0472..392b75e 100644 --- a/src-tauri/src/app_auto_updater.rs +++ b/src-tauri/src/app_auto_updater.rs @@ -1604,6 +1604,10 @@ rm "{}" #[tauri::command] pub async fn check_for_app_updates() -> Result, String> { + if crate::app_dirs::is_portable() { + log::info!("App auto-updates disabled in portable mode"); + return Ok(None); + } // The disable_auto_updates setting controls app self-updates only let disabled = crate::settings_manager::SettingsManager::instance() .load_settings() diff --git a/src-tauri/src/app_dirs.rs b/src-tauri/src/app_dirs.rs index 83c4876..7763204 100644 --- a/src-tauri/src/app_dirs.rs +++ b/src-tauri/src/app_dirs.rs @@ -3,11 +3,29 @@ use std::path::PathBuf; use std::sync::OnceLock; static BASE_DIRS: OnceLock = OnceLock::new(); +static PORTABLE_DIR: OnceLock> = OnceLock::new(); fn base_dirs() -> &'static BaseDirs { BASE_DIRS.get_or_init(|| BaseDirs::new().expect("Failed to get base directories")) } +/// Returns the portable base directory if a `.portable` marker exists next to the executable. +fn portable_dir() -> Option<&'static PathBuf> { + PORTABLE_DIR + .get_or_init(|| { + std::env::current_exe() + .ok() + .and_then(|exe| exe.parent().map(|p| p.to_path_buf())) + .filter(|dir| dir.join(".portable").exists()) + }) + .as_ref() +} + +/// Returns true if the app is running in portable mode. +pub fn is_portable() -> bool { + portable_dir().is_some() +} + pub fn app_name() -> &'static str { if cfg!(debug_assertions) { "DonutBrowserDev" @@ -28,6 +46,10 @@ pub fn data_dir() -> PathBuf { return PathBuf::from(dir); } + if let Some(dir) = portable_dir() { + return dir.join("data"); + } + base_dirs().data_local_dir().join(app_name()) } @@ -43,6 +65,10 @@ pub fn cache_dir() -> PathBuf { return PathBuf::from(dir); } + if let Some(dir) = portable_dir() { + return dir.join("cache"); + } + base_dirs().cache_dir().join(app_name()) } diff --git a/src-tauri/src/daemon/autostart.rs b/src-tauri/src/daemon/autostart.rs index 5973f82..9727f12 100644 --- a/src-tauri/src/daemon/autostart.rs +++ b/src-tauri/src/daemon/autostart.rs @@ -340,6 +340,9 @@ pub fn is_autostart_enabled() -> bool { } pub fn get_data_dir() -> Option { + if crate::app_dirs::is_portable() { + return Some(crate::app_dirs::data_dir()); + } if let Some(proj_dirs) = ProjectDirs::from("com", "donutbrowser", "Donut Browser") { Some(proj_dirs.data_dir().to_path_buf()) } else { diff --git a/src-tauri/src/settings_manager.rs b/src-tauri/src/settings_manager.rs index 459ea43..b239703 100644 --- a/src-tauri/src/settings_manager.rs +++ b/src-tauri/src/settings_manager.rs @@ -950,6 +950,7 @@ pub struct SystemInfo { pub app_version: String, pub os: String, pub arch: String, + pub portable: bool, } #[tauri::command] @@ -976,6 +977,7 @@ pub fn get_system_info() -> SystemInfo { app_version: crate::app_auto_updater::AppAutoUpdater::get_current_version(), os: os.to_string(), arch: arch.to_string(), + portable: crate::app_dirs::is_portable(), } } diff --git a/src/components/settings-dialog.tsx b/src/components/settings-dialog.tsx index 8281304..a30eb79 100644 --- a/src/components/settings-dialog.tsx +++ b/src/components/settings-dialog.tsx @@ -128,6 +128,7 @@ export function SettingsDialog({ app_version: string; os: string; arch: string; + portable: boolean; } | null>(null); const { t } = useTranslation(); @@ -237,6 +238,7 @@ export function SettingsDialog({ app_version: string; os: string; arch: string; + portable: boolean; }>("get_system_info"); setSystemInfo(info); } catch { @@ -1109,7 +1111,7 @@ export function SettingsDialog({ {systemInfo && (

- {`Donut Browser ${systemInfo.app_version}\n${systemInfo.os} ${systemInfo.arch}`} + {`Donut Browser ${systemInfo.app_version}\n${systemInfo.os} ${systemInfo.arch}${systemInfo.portable ? " (portable)" : ""}`}

)}