mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
Conflict: apps/designcraft/src/profile.rs: recovery_dir takes the PR's root_with (config override, else portable folder) and keeps next's .map(own_run) and own_run Conflict: README.md: the PR's Portable mode section, then next's Graphics backend and processor section
This commit is contained in:
@@ -147,6 +147,13 @@ The file stops growing at 16 MiB. `--version` writes no file.
|
||||
|
||||
The logger is `apps/designcraft/src/logging.rs`; the web build logs to the browser console instead.
|
||||
|
||||
### Portable mode
|
||||
|
||||
A file named `portable.txt` next to the executable (on macOS, next to `DesignCraft.app`) makes the install portable: settings, logs and
|
||||
crash recovery are kept in a `DesignCraftData` folder beside it instead of the per-user folders
|
||||
above. The Windows portable zip ships with one; delete it to go back to `%APPDATA%\DesignCraft`.
|
||||
`DESIGNCRAFT_CONFIG_DIR` still takes precedence. The code is `apps/designcraft/src/profile.rs`.
|
||||
|
||||
### Graphics backend and processor
|
||||
|
||||
The canvas is rendered on the CPU; the GPU only composites the interface, through wgpu, so any
|
||||
|
||||
+109
-12
@@ -1,14 +1,19 @@
|
||||
//! Native settings and recovery paths, including isolated automation profiles.
|
||||
//! Native settings and recovery paths, including isolated automation profiles and portable installs.
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// A file with this name next to the executable makes the install portable: settings, logs and
|
||||
/// recovery data are kept in [`PORTABLE_DATA_DIR`] beside it. The Windows portable zip ships one.
|
||||
const PORTABLE_MARKER: &str = "portable.txt";
|
||||
const PORTABLE_DATA_DIR: &str = "DesignCraftData";
|
||||
|
||||
pub fn config_dir() -> Option<PathBuf> {
|
||||
config_dir_with(std::env::consts::OS, |key| std::env::var_os(key))
|
||||
config_dir_with(std::env::consts::OS, |key| std::env::var_os(key), executable_dir().as_deref())
|
||||
}
|
||||
|
||||
fn config_dir_with(os: &str, env: impl Fn(&str) -> Option<OsString>) -> Option<PathBuf> {
|
||||
if let Some(root) = env("DESIGNCRAFT_CONFIG_DIR") {
|
||||
fn config_dir_with(os: &str, env: impl Fn(&str) -> Option<OsString>, exe_dir: Option<&Path>) -> Option<PathBuf> {
|
||||
if let Some(root) = root_with(&env, exe_dir) {
|
||||
// An explicitly empty override must not fall back to the user's profile.
|
||||
return (!root.is_empty()).then(|| PathBuf::from(root));
|
||||
}
|
||||
@@ -21,8 +26,35 @@ fn config_dir_with(os: &str, env: impl Fn(&str) -> Option<OsString>) -> Option<P
|
||||
}
|
||||
}
|
||||
|
||||
/// The folder that holds settings and recovery data instead of the user's profile:
|
||||
/// `DESIGNCRAFT_CONFIG_DIR` (empty: none at all), else a portable install's data folder.
|
||||
fn root_with(env: impl Fn(&str) -> Option<OsString>, exe_dir: Option<&Path>) -> Option<OsString> {
|
||||
env("DESIGNCRAFT_CONFIG_DIR").or_else(|| exe_dir.and_then(portable_dir).map(PathBuf::into_os_string))
|
||||
}
|
||||
|
||||
/// `<dir>/DesignCraftData` when `dir` holds [`PORTABLE_MARKER`], where `dir` is the folder the
|
||||
/// user sees the app in: the executable's, or the one holding `DesignCraft.app` when the executable
|
||||
/// sits inside a macOS bundle (`DesignCraft.app/Contents/MacOS`, signed, never edited by hand).
|
||||
/// A folder named like the marker is no marker.
|
||||
fn portable_dir(exe_dir: &Path) -> Option<PathBuf> {
|
||||
let dir = app_bundle_parent(exe_dir).unwrap_or(exe_dir);
|
||||
dir.join(PORTABLE_MARKER).is_file().then(|| dir.join(PORTABLE_DATA_DIR))
|
||||
}
|
||||
|
||||
/// The folder holding `X.app` when `exe_dir` is `X.app/Contents/MacOS`.
|
||||
fn app_bundle_parent(exe_dir: &Path) -> Option<&Path> {
|
||||
let contents = exe_dir.parent().filter(|_| exe_dir.file_name() == Some("MacOS".as_ref()))?;
|
||||
let bundle = contents.parent().filter(|_| contents.file_name() == Some("Contents".as_ref()))?;
|
||||
bundle.extension().filter(|ext| *ext == "app").and(bundle.parent())
|
||||
}
|
||||
|
||||
fn executable_dir() -> Option<PathBuf> {
|
||||
std::env::current_exe().ok()?.parent().map(Path::to_path_buf)
|
||||
}
|
||||
|
||||
pub fn recovery_dir() -> Option<PathBuf> {
|
||||
recovery_dir_with(std::env::var_os("DESIGNCRAFT_CONFIG_DIR"), designcraft_engine::recovery::default_dir).map(own_run)
|
||||
recovery_dir_with(root_with(|key| std::env::var_os(key), executable_dir().as_deref()), designcraft_engine::recovery::default_dir)
|
||||
.map(own_run)
|
||||
}
|
||||
|
||||
/// This run's folder in the recovery folder `root`. A second running DesignCraft has its own, so
|
||||
@@ -57,7 +89,7 @@ mod tests {
|
||||
#[test]
|
||||
fn isolated_profile_overrides_every_platform_and_recovery() {
|
||||
for os in ["macos", "windows", "linux"] {
|
||||
let dir = config_dir_with(os, |key| if key == "DESIGNCRAFT_CONFIG_DIR" { Some("/isolated".into()) } else { environment(key) });
|
||||
let dir = config_dir_with(os, |key| if key == "DESIGNCRAFT_CONFIG_DIR" { Some("/isolated".into()) } else { environment(key) }, None);
|
||||
assert_eq!(dir, Some(PathBuf::from("/isolated")));
|
||||
}
|
||||
assert_eq!(recovery_dir_with(Some("/isolated".into()), || Some("/user/recovery".into())), Some(PathBuf::from("/isolated/Recovery")));
|
||||
@@ -65,16 +97,81 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn empty_override_never_reads_or_writes_the_default_profile() {
|
||||
assert_eq!(config_dir_with("macos", |key| { if key == "DESIGNCRAFT_CONFIG_DIR" { Some(OsString::new()) } else { environment(key) } }), None);
|
||||
assert_eq!(
|
||||
config_dir_with("macos", |key| { if key == "DESIGNCRAFT_CONFIG_DIR" { Some(OsString::new()) } else { environment(key) } }, None),
|
||||
None
|
||||
);
|
||||
assert_eq!(recovery_dir_with(Some(OsString::new()), || Some("/user/recovery".into())), None);
|
||||
}
|
||||
|
||||
/// A folder standing in for the executable's; `marker` puts `portable.txt` in it, or not.
|
||||
fn install(name: &str, marker: impl FnOnce(&Path)) -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!("designcraft-profile-{name}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
marker(&dir.join(PORTABLE_MARKER));
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portable_marker_keeps_settings_and_recovery_next_to_the_executable() {
|
||||
let exe = install("portable", |marker| std::fs::write(marker, "").unwrap());
|
||||
for os in ["macos", "windows", "linux"] {
|
||||
assert_eq!(config_dir_with(os, environment, Some(&exe)), Some(exe.join("DesignCraftData")));
|
||||
}
|
||||
assert_eq!(
|
||||
recovery_dir_with(root_with(environment, Some(&exe)), || Some("/user/recovery".into())),
|
||||
Some(exe.join("DesignCraftData/Recovery"))
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&exe);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inside_a_macos_bundle_the_marker_sits_next_to_the_app() {
|
||||
let folder = install("bundle", |marker| std::fs::write(marker, "").unwrap());
|
||||
let exe = folder.join("DesignCraft.app/Contents/MacOS");
|
||||
std::fs::create_dir_all(&exe).unwrap();
|
||||
assert_eq!(config_dir_with("macos", environment, Some(&exe)), Some(folder.join("DesignCraftData")));
|
||||
// a marker dropped inside the bundle is not looked at
|
||||
std::fs::remove_file(folder.join(PORTABLE_MARKER)).unwrap();
|
||||
std::fs::write(exe.join(PORTABLE_MARKER), "").unwrap();
|
||||
assert_eq!(config_dir_with("macos", environment, Some(&exe)), Some("/user/Library/Application Support/DesignCraft".into()));
|
||||
let _ = std::fs::remove_dir_all(&folder);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_a_marker_file_the_platform_paths_stay() {
|
||||
let plain = install("plain", |_| {});
|
||||
let folder = install("marker-folder", |marker| std::fs::create_dir(marker).unwrap());
|
||||
for exe in [&plain, &folder] {
|
||||
assert_eq!(config_dir_with("windows", environment, Some(exe)), Some("/roaming/DesignCraft".into()));
|
||||
assert_eq!(recovery_dir_with(root_with(environment, Some(exe)), || Some("/existing/recovery".into())), Some("/existing/recovery".into()));
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&plain);
|
||||
let _ = std::fs::remove_dir_all(&folder);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isolated_profile_wins_over_a_portable_install() {
|
||||
let exe = install("override", |marker| std::fs::write(marker, "").unwrap());
|
||||
let isolated = |key: &str| if key == "DESIGNCRAFT_CONFIG_DIR" { Some("/isolated".into()) } else { environment(key) };
|
||||
assert_eq!(config_dir_with("windows", isolated, Some(&exe)), Some("/isolated".into()));
|
||||
assert_eq!(recovery_dir_with(root_with(isolated, Some(&exe)), || None), Some("/isolated/Recovery".into()));
|
||||
let empty = |key: &str| if key == "DESIGNCRAFT_CONFIG_DIR" { Some(OsString::new()) } else { environment(key) };
|
||||
assert_eq!(config_dir_with("windows", empty, Some(&exe)), None);
|
||||
assert_eq!(recovery_dir_with(root_with(empty, Some(&exe)), || Some("/user/recovery".into())), None);
|
||||
let _ = std::fs::remove_dir_all(&exe);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unset_override_preserves_existing_platform_paths() {
|
||||
assert_eq!(config_dir_with("macos", environment), Some("/user/Library/Application Support/DesignCraft".into()));
|
||||
assert_eq!(config_dir_with("windows", environment), Some("/roaming/DesignCraft".into()));
|
||||
assert_eq!(config_dir_with("linux", environment), Some("/xdg-config/designcraft".into()));
|
||||
assert_eq!(config_dir_with("linux", |key| if key == "HOME" { environment(key) } else { None }), Some("/user/.config/designcraft".into()));
|
||||
assert_eq!(config_dir_with("macos", environment, None), Some("/user/Library/Application Support/DesignCraft".into()));
|
||||
assert_eq!(config_dir_with("windows", environment, None), Some("/roaming/DesignCraft".into()));
|
||||
assert_eq!(config_dir_with("linux", environment, None), Some("/xdg-config/designcraft".into()));
|
||||
assert_eq!(
|
||||
config_dir_with("linux", |key| if key == "HOME" { environment(key) } else { None }, None),
|
||||
Some("/user/.config/designcraft".into())
|
||||
);
|
||||
assert_eq!(recovery_dir_with(None, || Some("/existing/recovery".into())), Some("/existing/recovery".into()));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ Start the app with `--control <port>` (or `DESIGNCRAFT_CONTROL_PORT`). The serve
|
||||
|
||||
For an isolated native test profile, set `DESIGNCRAFT_CONFIG_DIR` to a dedicated directory before
|
||||
launch. UI/engine preferences and the `Recovery` directory then live under that root. An unset
|
||||
override preserves the usual platform paths; an explicitly empty override disables those paths.
|
||||
override preserves the usual platform paths (or `DesignCraftData` next to the executable when a
|
||||
`portable.txt` sits there); an explicitly empty override disables those paths.
|
||||
`DESIGNCRAFT_NO_PREFS` still disables preference reads/writes independently of recovery.
|
||||
|
||||
Control pointer and key input is queued across native frames. After a drag, wait for both the
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.DESCRIPTION
|
||||
Produces, in $env:DIST (default: dist/release):
|
||||
designcraft-<version>-windows-<arch>.msi per-machine installer (WiX v5)
|
||||
designcraft-<version>-windows-<arch>-portable.zip designcraft.exe + designcraft-cli.exe
|
||||
designcraft-<version>-windows-<arch>-portable.zip designcraft.exe + designcraft-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,12 @@ $Portable = Join-Path $TargetDir "windows-package\designcraft-$Version-windows-$
|
||||
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 DesignCraft keep its data in .\DesignCraftData next to the exe instead of
|
||||
# %APPDATA% (#574; see apps/designcraft/src/profile.rs).
|
||||
Set-Content -Encoding ascii -Path (Join-Path $Portable 'portable.txt') -Value @(
|
||||
'DesignCraft portable: preferences, logs and crash recovery are kept in the DesignCraftData folder',
|
||||
'next to designcraft.exe. Delete this file to use %APPDATA%\DesignCraft instead.'
|
||||
)
|
||||
foreach ($f in 'README.md', 'LICENSE', 'LICENSE-MIT', 'LICENSE-APACHE') {
|
||||
$p = Join-Path $Root $f
|
||||
if (Test-Path $p) { Copy-Item $p $Portable }
|
||||
|
||||
Reference in New Issue
Block a user