diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index b70f6b798..4cc867d72 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -19,7 +19,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "8792b00c7e9a12a247cbf6d4758d8583972f861010f4e4ed52949978e5a380e7" +sha256 = "e14c411ac69fd96279de51aaaa9589b8810e2970c3c212585bf47104f7c2cbd7" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9429421177790a0492b1513102823a9aa5dbd34c7a850a737792602d4d19b8d9" +sha256 = "27715eb6c29938938a39880f5ab6f3b0aa60ba6d0940609af1bfb96f1fd124de" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" @@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9a655daa584e3a5d049dbaaa30229ae7a931984b0f1c7f0177fde2ccdd6804ca" +sha256 = "3bc4e3c1d1a50c50d6be907f62e1fa2fd85ea719f1b65af21eeadcbbc7db5e44" [[asset]] path = "crates/ui-egui/src/i18n/pt-br.tsv" @@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "777a7f441950714a61d41e5a44405209aa85bfdd4b46e27090544eb9122d715f" +sha256 = "3a46800299b34558642676d0d67ff31619d8e9bbeb2ba83e713a108f544f2d82" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" @@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "cb3759b57445a3948a707aaf9423338053c7e7ad68f19f52583527dae3dfabee" +sha256 = "dad2013f25795cafc93d89c9ff4c0cc510ba844628b27469fe9b38c2fce5e0ea" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" @@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "b131025cb6c14135440968f08ab6334d70dd3b20271539471878d3825ecc9629" +sha256 = "c41079a024a360951e0f935b14b6d18a16c8478daac21bc7a1c57eb73b6b3590" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" @@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "c9879375eed52003bb16790e687808fe0d50dd9405390be383eab66cd513291c" +sha256 = "f42c77a513e83f4d48e1792bcba8dfe60b7453daf1b240d0a7ffb6d276ce98e3" [[asset]] path = "assets/icons/accessibility.svg" diff --git a/README.md b/README.md index a80469bcd..24db83c6f 100644 --- a/README.md +++ b/README.md @@ -420,6 +420,10 @@ The honest assessment by area, what's lacking and where we're going are in **[RO Installers and executables are code-signed. +The portable zip runs from any folder, a USB stick included. Its `portable.txt` keeps the settings, +logs and crash recovery in a `PdfCraftData` folder next to `pdfcraft.exe`, so nothing is written to +`%APPDATA%`; delete that file to use the normal per-user folders. + The MSI installs for all users and requires administrator privileges. For unattended deployment without a desktop shortcut, run from an elevated terminal: diff --git a/apps/pdfcraft/src/main.rs b/apps/pdfcraft/src/main.rs index 9bbf2f5ac..bf3b48833 100644 --- a/apps/pdfcraft/src/main.rs +++ b/apps/pdfcraft/src/main.rs @@ -39,16 +39,24 @@ const APP_ICON_PNG: &[u8] = include_bytes!("../../../assets/app-icon/hicolor/256 /// The app was called PrintCraft before; settings saved then are under this key. const LEGACY_STORAGE_KEY: &str = "printcraft"; -/// The settings folder: `app.ron` and the `logs` folder (docs/development.md). eframe would -/// otherwise derive it from the app id; keep it under "PdfCraft". +/// The settings folder: `app.ron` and the `logs` folder (docs/development.md). In portable mode +/// it is `PdfCraftData` beside the executable (#157). eframe would otherwise derive it from the +/// app id; keep it under "PdfCraft". fn settings_dir() -> Option { + if let Some(dir) = pdfcraft_ui_egui::portable::data_dir() { + return Some(dir.to_path_buf()); + } eframe::storage_dir("PdfCraft") } /// Move the settings and crash-recovery folders of the app's former name, PrintCraft, to the new /// name once, so an upgrade keeps recent files, preferences and unsaved work. Best effort: a -/// folder is left alone when the new one already exists or the move fails. +/// folder is left alone when the new one already exists or the move fails. A portable copy leaves +/// the per-user folders alone. fn migrate_legacy_folders() { + if pdfcraft_ui_egui::portable::data_dir().is_some() { + return; + } let mut moves = vec![(eframe::storage_dir("PrintCraft"), settings_dir())]; // Recovery lives in the settings folder except on Windows, where it is under %LOCALAPPDATA%. if cfg!(windows) { @@ -173,6 +181,13 @@ fn main() -> eframe::Result { if let Some(dir) = pdfcraft_ui_egui::RecoveryStore::default_dir() { app.enable_recovery(pdfcraft_ui_egui::RecoveryStore::new(dir)); } + // A portable marker whose data folder can't be written (#157): say where settings went. + if let Some(w) = &pdfcraft_ui_egui::portable::current().unwritable { + app.notify_fmt( + "Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead.", + &[("folder", &w.folder.display().to_string()), ("error", &w.error)], + ); + } if create_images { if let Err(e) = app.begin_image_import_paths(&files) { app.notify(e); diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 1f37708f3..704645f63 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -108,3 +108,4 @@ Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. Používá se, když PDF nepožaduje rozložení ani zvětšení. Plynulé posouvání mezi stránkami neskáče. Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. Vyberte obrázek PNG nebo JPEG (nejvýše 4 MiB). Průhlednost bude zachována. Couldn't import the signature image: {e} Obrázek podpisu nelze importovat: {e} + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. Přenosný režim je vypnutý: do složky {folder} nelze zapisovat ({error}). Nastavení se místo toho ukládá do vaší uživatelské složky. diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index f4a560c08..334c552a4 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1619,6 +1619,7 @@ Created a comment summary Se ha creado un resumen de comentarios Couldn't open {name}: {e} No se pudo abrir {name}: {e} Couldn't read {name}: {e} No se pudo leer {name}: {e} + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. El modo portátil está desactivado: no se puede escribir en {folder} ({error}). La configuración se guarda en tu carpeta de usuario. Couldn't save: {e} No se pudo guardar: {e} Downloading attachments on the web arrives with M3.10 ({n} bytes ready) La descarga de archivos adjuntos en la versión web llegará con M3.10 ({n} bytes listos) `{command}` {when} `{command}` {when} diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 28391f9ac..01ef070d1 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1560,6 +1560,7 @@ Created a comment summary Un résumé des commentaires a été créé Couldn't open {name}: {e} Impossible d'ouvrir {name} : {e} Couldn't read {name}: {e} Impossible de lire {name} : {e} + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. Le mode portable est désactivé : impossible d'écrire dans {folder} ({error}). Les réglages sont conservés dans votre dossier utilisateur. Couldn't save: {e} Impossible d'enregistrer : {e} Downloading attachments on the web arrives with M3.10 ({n} bytes ready) Le téléchargement des pièces jointes sur le web arrivera avec M3.10 ({n} octets prêts) `{command}` {when} `{command}` {when} diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 342955dc2..0e2fb0869 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1625,6 +1625,7 @@ Created a comment summary 注釈の一覧を作成しました Couldn't open {name}: {e} {name} を開けませんでした: {e} Couldn't read {name}: {e} {name} を読み取れませんでした: {e} + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. ポータブルモードは無効です: {folder} に書き込めません ({error})。設定は代わりにユーザーフォルダーに保存されます。 Couldn't save: {e} 保存できませんでした: {e} Downloading attachments on the web arrives with M3.10 ({n} bytes ready) Web 版の添付ファイルのダウンロードは M3.10 で対応予定です({n} バイト準備済み) `{command}` {when} `{command}`: {when} diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 0cbc9a887..7637345c1 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -62,3 +62,4 @@ Auto Automático Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. Escolha uma imagem PNG ou JPEG (até 4 MiB). A transparência é preservada. Couldn't import the signature image: {e} Não foi possível importar a imagem da assinatura: {e} + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. O modo portátil está desativado: não é possível gravar em {folder} ({error}). As configurações ficam na sua pasta de usuário. diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 583acd5f8..e7b99f878 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1576,6 +1576,7 @@ Created a comment summary 已创建注释摘要 Couldn't open {name}: {e} 无法打开{name}:{e} Couldn't read {name}: {e} 无法读取{name}:{e} + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. 便携模式已关闭:无法写入{folder}({error})。设置改为保存在你的用户文件夹中。 Couldn't save: {e} 无法保存:{e} Downloading attachments on the web arrives with M3.10 ({n} bytes ready) Web 端附件下载将于 M3.10 到来({n} 字节已就绪) `{command}` {when} `{command}` {when} diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index adea61bba..d1762eb78 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1534,6 +1534,7 @@ Created a comment summary 已建立註解摘要 Couldn't open {name}: {e} 無法開啟{name}:{e} Couldn't read {name}: {e} 無法讀取{name}:{e} + Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. 可攜模式已關閉:無法寫入{folder}({error})。設定改為儲存在你的使用者資料夾中。 Couldn't save: {e} 無法儲存:{e} Downloading attachments on the web arrives with M3.10 ({n} bytes ready) Web 端附件下載將於 M3.10 到來({n} 位元組已就緒) `{command}` {when} `{command}` {when} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index f1e857f5a..9e29b3c0c 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -84,6 +84,7 @@ pub mod i18n; /// The longest author name kept (Preferences ▸ Identity, restored settings). pub(crate) const MAX_AUTHOR_CHARS: usize = 200; +pub mod portable; mod protect; mod recovery; #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/ui-egui/src/portable.rs b/crates/ui-egui/src/portable.rs new file mode 100644 index 000000000..fefa268e0 --- /dev/null +++ b/crates/ui-egui/src/portable.rs @@ -0,0 +1,171 @@ +//! Portable mode (#157): a marker file beside the executable ([`MARKERS`]) keeps the settings, +//! log files, crash-recovery autosaves and new digital IDs in `/PdfCraftData`, so a +//! portable copy (on a USB stick, say) writes nothing to `%APPDATA%` or `%LOCALAPPDATA%`. The +//! Windows portable zip ships with `portable.txt`; the MSI doesn't. PhotoCraft uses the same +//! scheme (`photocraft/apps/photocraft/src/app_dirs.rs`). +//! +//! If that folder can't be written (read-only medium, Program Files), portable mode stays off, the +//! per-user folders are used and the app says why. + +use std::path::{Path, PathBuf}; +use std::sync::OnceLock; + +/// Files beside the executable that switch on portable mode (either one; contents are ignored). +pub const MARKERS: [&str; 2] = ["portable.txt", "PdfCraft.portable"]; +/// The data folder created beside the executable in portable mode. +pub const DATA_DIR: &str = "PdfCraftData"; + +/// The outcome of looking for a portable marker. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Portable { + /// `/PdfCraftData` when portable mode is on, `None` otherwise. + pub dir: Option, + /// Set when a marker was found but its data folder can't be written. + pub unwritable: Option, +} + +/// A marker was found, but the data folder beside it can't be created or written. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Unwritable { + pub marker: PathBuf, + pub folder: PathBuf, + /// The operating system's error text. + pub error: String, +} + +impl std::fmt::Display for Unwritable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "portable mode is off: {} was found, but {} can't be written ({}); settings are kept in the user folder instead", + self.marker.display(), + self.folder.display(), + self.error + ) + } +} + +/// Portable mode for this process, resolved once (the check probes the disk). +pub fn current() -> &'static Portable { + static CURRENT: OnceLock = OnceLock::new(); + CURRENT.get_or_init(|| { + let exe = std::env::current_exe().ok(); + let portable = resolve(exe.as_deref().and_then(Path::parent)); + if let Some(dir) = &portable.dir { + log::info!("portable mode: settings are kept in {}", dir.display()); + } + if let Some(unwritable) = &portable.unwritable { + log::warn!("{unwritable}"); + } + portable + }) +} + +/// The portable data folder, when portable mode is on. +pub fn data_dir() -> Option<&'static Path> { + current().dir.as_deref() +} + +/// Look for a marker in `exe_dir` and, when one is there, create its data folder beside it. +pub fn resolve(exe_dir: Option<&Path>) -> Portable { + let Some((exe_dir, marker)) = exe_dir.and_then(|d| find_marker(d).map(|m| (d, m))) else { + return Portable::default(); + }; + let folder = exe_dir.join(DATA_DIR); + match ensure_writable(&folder) { + Ok(()) => Portable { dir: Some(folder), unwritable: None }, + Err(e) => Portable { dir: None, unwritable: Some(Unwritable { marker, folder, error: e.to_string() }) }, + } +} + +/// The first marker file in `exe_dir`, if any. A folder with a marker's name is not a marker. +pub fn find_marker(exe_dir: &Path) -> Option { + MARKERS.iter().map(|m| exe_dir.join(m)).find(|p| p.is_file()) +} + +/// Create `dir` and prove a file can be written in it (a folder can exist yet be read-only). +fn ensure_writable(dir: &Path) -> std::io::Result<()> { + std::fs::create_dir_all(dir)?; + let probe = dir.join(".write-test"); + std::fs::write(&probe, b"")?; + // A leftover probe is harmless; don't fail portable mode over it. + let _ = std::fs::remove_file(&probe); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + + fn temp(tag: &str) -> PathBuf { + static N: AtomicUsize = AtomicUsize::new(0); + let d = std::env::temp_dir().join(format!("pdfcraft-portable-{tag}-{}-{}", std::process::id(), N.fetch_add(1, Ordering::Relaxed))); + let _ = std::fs::remove_dir_all(&d); + std::fs::create_dir_all(&d).unwrap(); + d + } + + #[test] + fn no_marker_leaves_portable_mode_off() { + let exe = temp("off"); + assert_eq!(resolve(Some(&exe)), Portable::default()); + assert!(!exe.join(DATA_DIR).exists(), "no data folder without a marker"); + let _ = std::fs::remove_dir_all(&exe); + } + + #[test] + fn either_marker_keeps_the_data_beside_the_exe() { + for marker in MARKERS { + let exe = temp("on"); + std::fs::write(exe.join(marker), b"").unwrap(); + let p = resolve(Some(&exe)); + assert_eq!(p.unwritable, None, "{marker}"); + let dir = p.dir.unwrap(); + assert_eq!(dir, exe.join(DATA_DIR)); + assert!(dir.is_dir(), "the data folder is created"); + assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 0, "the write probe is removed"); + let _ = std::fs::remove_dir_all(&exe); + } + } + + #[test] + fn an_existing_data_folder_is_reused() { + let exe = temp("reuse"); + std::fs::write(exe.join("portable.txt"), b"PdfCraft portable mode").unwrap(); + std::fs::create_dir_all(exe.join(DATA_DIR)).unwrap(); + std::fs::write(exe.join(DATA_DIR).join("app.ron"), b"()").unwrap(); + assert_eq!(resolve(Some(&exe)).dir, Some(exe.join(DATA_DIR))); + assert_eq!(std::fs::read(exe.join(DATA_DIR).join("app.ron")).unwrap(), b"()", "existing settings are kept"); + let _ = std::fs::remove_dir_all(&exe); + } + + #[test] + fn a_marker_folder_is_not_a_marker() { + let exe = temp("dir-marker"); + std::fs::create_dir_all(exe.join("portable.txt")).unwrap(); + assert_eq!(resolve(Some(&exe)), Portable::default()); + let _ = std::fs::remove_dir_all(&exe); + } + + #[test] + fn unwritable_data_folder_turns_portable_mode_off_and_says_why() { + let exe = temp("unwritable"); + std::fs::write(exe.join("portable.txt"), b"").unwrap(); + // A file where the data folder should go: it can't be created, on every platform. + std::fs::write(exe.join(DATA_DIR), b"in the way").unwrap(); + let p = resolve(Some(&exe)); + assert_eq!(p.dir, None); + let unwritable = p.unwritable.unwrap(); + assert_eq!(unwritable.marker, exe.join("portable.txt")); + assert_eq!(unwritable.folder, exe.join(DATA_DIR)); + let message = unwritable.to_string(); + assert!(message.contains("portable mode is off") && message.contains("can't be written"), "{message}"); + let _ = std::fs::remove_dir_all(&exe); + } + + #[test] + fn no_exe_dir_leaves_portable_mode_off() { + assert_eq!(resolve(None), Portable::default()); + } +} diff --git a/crates/ui-egui/src/recovery.rs b/crates/ui-egui/src/recovery.rs index 8c2217e7b..df6b8bddb 100644 --- a/crates/ui-egui/src/recovery.rs +++ b/crates/ui-egui/src/recovery.rs @@ -40,10 +40,14 @@ impl RecoveryStore { Self { dir: dir.into() } } - /// The platform's per-user data folder: `~/Library/Application Support/PdfCraft/Recovery` + /// `Recovery` in the portable data folder in portable mode ([`crate::portable`]), otherwise + /// the platform's per-user data folder: `~/Library/Application Support/PdfCraft/Recovery` /// (macOS), `%LOCALAPPDATA%\PdfCraft\Recovery` (Windows), or /// `$XDG_DATA_HOME/pdfcraft/recovery` / `~/.local/share/pdfcraft/recovery` (others). pub fn default_dir() -> Option { + if let Some(dir) = crate::portable::data_dir() { + return Some(dir.join("Recovery")); + } let env = |k: &str| std::env::var_os(k).filter(|v| !v.is_empty()).map(PathBuf::from); if cfg!(target_os = "macos") { env("HOME").map(|h| h.join("Library/Application Support/PdfCraft/Recovery")) diff --git a/crates/ui-egui/src/sign_ui.rs b/crates/ui-egui/src/sign_ui.rs index a43ff478a..a7dc7fb4c 100644 --- a/crates/ui-egui/src/sign_ui.rs +++ b/crates/ui-egui/src/sign_ui.rs @@ -157,7 +157,8 @@ pub(crate) fn page_input(ui: &egui::Ui, resp: &egui::Response, xf: &PageXform, p } } -/// Where new digital IDs are saved: next to the recovery folder (`…/PdfCraft/Digital IDs`). +/// Where new digital IDs are saved: next to the recovery folder (`…/PdfCraft/Digital IDs`, or +/// `PdfCraftData/Digital IDs` in portable mode). fn id_dir() -> Option { crate::recovery::RecoveryStore::default_dir().and_then(|d| d.parent().map(|p| p.join("Digital IDs"))) } diff --git a/docs/development.md b/docs/development.md index adcfd9732..aa46e0cd1 100644 --- a/docs/development.md +++ b/docs/development.md @@ -8,9 +8,12 @@ collects what the desktop app reads from its environment and where it writes its The desktop app writes its `log` records to standard error and to `logs/pdfcraft.log` in its settings folder, next to `app.ron`: Linux and FreeBSD `~/.local/share/pdfcraft/logs/` (or `$XDG_DATA_HOME/pdfcraft/logs/`), macOS `~/Library/Application Support/PdfCraft/logs/`, Windows -`%APPDATA%\PdfCraft\data\logs\`. A start from a desktop menu, Finder or the Start menu has no -terminal, so this file is what to attach to a bug report: a failed autosave, a page that would not -render, a render worker that could not start and the report of an internal error all land there. +`%APPDATA%\PdfCraft\data\logs\`. In portable mode (a `portable.txt` file next to the executable, as +the Windows portable zip ships) the settings folder is `PdfCraftData\` beside the executable, and +crash recovery and new digital IDs go there too. A start from a desktop menu, Finder or the Start +menu has no terminal, so this file is what to attach to a bug report: a failed autosave, a page that +would not render, a render worker that could not start and the report of an internal error all land +there. Each launch moves the previous log to `pdfcraft.1.log` (and that one to `pdfcraft.2.log`), so the log of a run that crashed survives the next start. The file stops growing at 16 MiB. `--version` writes no file. diff --git a/docs/releasing.md b/docs/releasing.md index 1b5616440..ec5e3b1d0 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -113,8 +113,10 @@ neither the MSI nor the portable zip needs the Visual C++ redistributable. child process before signing, so the MSI isn't held open when signtool runs. The ARM64 install smoke test checks that both all-users shortcuts point at the installed `pdfcraft.exe` and are removed on uninstall, and that `INSTALLDESKTOPSHORTCUT=0` skips the desktop one. -- The portable zip holds both executables, the README, the licences, and the OFL licence of each - embedded craft-fonts family. +- The portable zip holds both executables, the README, the licences, the OFL licence of each + embedded craft-fonts family, and `portable.txt`. That marker beside `pdfcraft.exe` keeps the + settings, logs, crash recovery and new digital IDs in `PdfCraftData\` next to the exe instead of + `%APPDATA%` and `%LOCALAPPDATA%` (`crates/ui-egui/src/portable.rs`). - **Signing:** `packaging/windows/sign.ps1` signs both executables and the MSI with `signtool` (SHA-256, RFC 3161 timestamp), from a `.pfx` (`WINDOWS_CERTIFICATE`) or Azure Trusted Signing (`AZURE_*`), whichever is configured. It is the one place to change when signing changes. diff --git a/packaging/windows/package.ps1 b/packaging/windows/package.ps1 index f6fc6a470..14588668d 100644 --- a/packaging/windows/package.ps1 +++ b/packaging/windows/package.ps1 @@ -5,7 +5,7 @@ .DESCRIPTION Produces, in $env:DIST (default: dist/release): pdfcraft--windows-.msi per-machine installer (WiX v5) - pdfcraft--windows--portable.zip pdfcraft.exe + pdfcraft-cli.exe + pdfcraft--windows--portable.zip pdfcraft.exe + pdfcraft-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 @@ -122,6 +122,10 @@ if ($env:CRAFT_FONTS_DIR) { if (Test-Path $ofl) { Copy-Item $ofl (Join-Path $Portable "OFL-$($_.Name).txt") } } } +# portable.txt beside pdfcraft.exe switches on portable mode: settings, logs, recovery files and new +# digital IDs go to PdfCraftData\ next to the exe instead of %APPDATA% (#157; see +# crates/ui-egui/src/portable.rs). +Copy-Item (Join-Path $PSScriptRoot 'portable.txt') $Portable $Zip = Join-Path $Dist "pdfcraft-$Version-windows-$Arch-portable.zip" Remove-Item -Force $Zip -ErrorAction SilentlyContinue Compress-Archive -Path $Portable -DestinationPath $Zip diff --git a/packaging/windows/portable.txt b/packaging/windows/portable.txt new file mode 100644 index 000000000..9586f9306 --- /dev/null +++ b/packaging/windows/portable.txt @@ -0,0 +1,9 @@ +PdfCraft portable mode + +While this file sits next to pdfcraft.exe, PdfCraft keeps its settings, log files, crash-recovery +autosaves and new digital IDs in the PdfCraftData folder beside it, and writes nothing to +%APPDATA% or %LOCALAPPDATA%. Copy the whole folder to move PdfCraft with its settings. + +Delete this file to use the normal per-user folders (%APPDATA%\PdfCraft and +%LOCALAPPDATA%\PdfCraft) instead. If PdfCraftData can't be written (for example on a read-only +drive), PdfCraft says so and uses the per-user folders instead.