diff --git a/crates/engine/src/autosave.rs b/crates/engine/src/autosave.rs index 5b87a958..45d462f0 100644 --- a/crates/engine/src/autosave.rs +++ b/crates/engine/src/autosave.rs @@ -470,7 +470,17 @@ impl Preferences { } } -/// The per-user data directory (`FILMCRAFT_DATA_DIR` overrides): +/// A file with this name next to the executable makes the install portable: its data directory is +/// `/data` instead of the per-user one (the Windows portable zip ships it). +pub const PORTABLE_MARKER: &str = "portable.txt"; + +/// `/data` when `exe_dir` holds [`PORTABLE_MARKER`], else `None`. +pub fn portable_data_dir(exe_dir: &Path) -> Option { + exe_dir.join(PORTABLE_MARKER).is_file().then(|| exe_dir.join("data")) +} + +/// The data directory. `FILMCRAFT_DATA_DIR` overrides; a portable install ([`PORTABLE_MARKER`] next +/// to the executable) uses `/data`; otherwise the per-user directory: /// macOS `~/Library/Application Support/FilmCraft`, Windows `%APPDATA%\FilmCraft`, /// elsewhere `$XDG_DATA_HOME/filmcraft` or `~/.local/share/filmcraft`. pub fn default_data_dir() -> Option { @@ -478,6 +488,9 @@ pub fn default_data_dir() -> Option { if let Some(d) = env("FILMCRAFT_DATA_DIR") { return Some(d); } + if let Some(d) = std::env::current_exe().ok().and_then(|exe| exe.parent().and_then(portable_data_dir)) { + return Some(d); + } if cfg!(target_os = "macos") { return env("HOME").map(|h| h.join("Library/Application Support/FilmCraft")); } diff --git a/crates/engine/src/autosave_tests.rs b/crates/engine/src/autosave_tests.rs index 0e113e41..5ac8281e 100644 --- a/crates/engine/src/autosave_tests.rs +++ b/crates/engine/src/autosave_tests.rs @@ -176,6 +176,19 @@ fn auto_save_ring_rotates_and_can_save_the_project() { let _ = std::fs::remove_dir_all(&d); } +#[test] +fn portable_marker_keeps_data_next_to_the_executable() { + // #139: a portable install must not write to %APPDATA%. + let d = temp_dir("portable"); + assert_eq!(autosave::portable_data_dir(&d), None); + std::fs::create_dir_all(d.join(autosave::PORTABLE_MARKER)).unwrap(); + assert_eq!(autosave::portable_data_dir(&d), None, "a directory is not the marker"); + std::fs::remove_dir_all(d.join(autosave::PORTABLE_MARKER)).unwrap(); + std::fs::write(d.join(autosave::PORTABLE_MARKER), "").unwrap(); + assert_eq!(autosave::portable_data_dir(&d), Some(d.join("data"))); + let _ = std::fs::remove_dir_all(&d); +} + #[test] fn journal_costs_the_ui_thread_only_a_handoff() { // Per change the UI thread pays an Arc clone and a channel send; encoding happens on the worker. diff --git a/docs/project-files.md b/docs/project-files.md index 68980cea..b7db090a 100644 --- a/docs/project-files.md +++ b/docs/project-files.md @@ -159,7 +159,8 @@ Desktop flags: `--recover` recovers the newest candidate without asking. `--no-r without asking; the changes stay available through File ▸ Recover Unsaved Changes…. `--data-dir ` (or `FILMCRAFT_DATA_DIR`) moves the data directory. The default is `~/Library/Application Support/FilmCraft` on macOS, `%APPDATA%\FilmCraft` on Windows, and `$XDG_DATA_HOME/filmcraft` or -`~/.local/share/filmcraft` elsewhere. +`~/.local/share/filmcraft` elsewhere. A `portable.txt` file next to the executable (the Windows +portable zip ships one) makes it `data` next to the executable instead. ### Cost diff --git a/packaging/windows/package.ps1 b/packaging/windows/package.ps1 index 04b31efb..faa4b863 100644 --- a/packaging/windows/package.ps1 +++ b/packaging/windows/package.ps1 @@ -5,7 +5,7 @@ .DESCRIPTION Produces, in $env:DIST (default: dist/release): filmcraft--windows-.msi per-machine installer (WiX v5) - filmcraft--windows--portable.zip filmcraft.exe + filmcraft-cli.exe + filmcraft--windows--portable.zip filmcraft.exe + filmcraft-cli.exe + portable.txt The binaries link the C runtime statically (+crt-static), so neither the MSI nor the portable zip needs the Visual C++ redistributable. Signing is delegated to sign.ps1 (skipped with a @@ -106,6 +106,11 @@ $Portable = Join-Path $TargetDir "windows-package\filmcraft-$Version-windows-$Ar Remove-Item -Recurse -Force $Portable -ErrorAction SilentlyContinue New-Item -ItemType Directory -Force -Path $Portable | Out-Null Copy-Item (Join-Path $Stage '*.exe') $Portable +# The marker that makes FilmCraft keep its data in .\data next to the exe instead of %APPDATA% (#139). +Set-Content -Encoding ascii -Path (Join-Path $Portable 'portable.txt') -Value @( + 'FilmCraft portable: preferences, presets, logs and crash recovery are kept in the data folder', + 'next to filmcraft.exe. Delete this file to use %APPDATA%\FilmCraft instead.' +) foreach ($f in 'README.md', 'LICENSE', 'LICENSE-MIT', 'LICENSE-APACHE') { $p = Join-Path $Root $f if (Test-Path $p) { Copy-Item $p $Portable }