mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-24 10:36:46 -06:00
feat(installer): unify Windows setup and completion screens (INSTALLER-UI)
This commit is contained in:
+27
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: pre-alpha** · next: alpha, ~5% and ~110–180 h away (the alpha gate's three failing workflows)
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (readiness by audience with hours to ~95 %) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (shared Windows installer UI; existing folder/shortcut/launch options retained; install and uninstall completion) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
|
||||
SoundCraft is a clean-room, pure-Rust digital audio workstation that aims at full parity with
|
||||
Avid Pro Tools, and then beyond it: faster, open, scriptable and agent-controllable. This page is
|
||||
@@ -167,3 +167,29 @@ mutated and truncated movies. Field reports from the first public week are track
|
||||
| 2026-10-10 | major | Rewritten to the progress-docs standard: stage alpha (re-normalized the same day), two numbers (breadth ~70 %, ready ~40 %), dimension/feature/language tables, hours re-calibrated; area table moved to docs/target-app-parity.md, milestones to docs/roadmap.md |
|
||||
| 2026-10-07 | major | Menu parity 94 %, ~65 % estimated feature parity, alpha list |
|
||||
| 2026-10-06 | minor | Alpha and parity estimates |
|
||||
|
||||
## Packaging update — 2026-10-10
|
||||
|
||||
Windows setup now uses the shared `craft-installer` native UI source, with install/repair/remove
|
||||
completion, cancellation and failure screens. Existing MSI ownership and upgrade identities are
|
||||
preserved; unattended modes remain unattended. Source/generator checks pass; native Windows
|
||||
installer acceptance with real release payloads is still pending. See [setup validation](packaging/windows/README.md).
|
||||
|
||||
## Packaging update (2026-10-11)
|
||||
|
||||
Windows setup now uses the suite's shared `craft-installer` UI, preserving existing folder,
|
||||
shortcut and launch options and app-owned installation policy. Install, uninstall and repair
|
||||
have explicit completion screens; cancellation and failure have separate endings. Source/profile
|
||||
checks, WiX UI library compilation for x64/x86/ARM64, PowerShell parsing and package identity
|
||||
comparisons pass. Native fixture MSI linking/UI tables pass on all three architectures, and
|
||||
Windows full UI install/repair/uninstall, x86 install/remove, folder upgrades, desktop choices
|
||||
and launch checks pass with fixtures. Native evidence and real-release acceptance limits
|
||||
are linked from
|
||||
[Windows packaging](packaging/windows/README.md).
|
||||
|
||||
Windows Settings removal now opens the shared completion screen. Quiet install/removal
|
||||
remains unattended; Settings labels preserve compatibility with existing Launchers, and
|
||||
previous Modify policy is retained. Windows 11 fixture verification covers both installation
|
||||
scopes and x64/x86 removal, with full MSI validation on x64/x86/ARM64.
|
||||
|
||||
- **2026-10-15** · Recovery retains autosaves whose timestamps equal the saved project. New autosave folders are created atomically and retry occupied names, preserving existing recovery data when runs collide. Regression tests cover timestamp ties and occupied folders. No readiness percentage change.
|
||||
|
||||
@@ -179,10 +179,27 @@ impl Autosave {
|
||||
/// A new folder under `root` for the session called `name`.
|
||||
fn create(root: &Path, name: &str) -> Result<Autosave> {
|
||||
let stamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_millis();
|
||||
let dir = root.join(format!("{} {stamp}-{}", sanitize_name(name), std::process::id()));
|
||||
let lock = std::fs::create_dir_all(&dir).and_then(|()| std::fs::File::create(dir.join(LOCK))).map_err(|err| io_error(&dir, err))?;
|
||||
lock.try_lock().map_err(|err| EngineError::Io(format!("{}: {err}", dir.display())))?;
|
||||
Ok(Autosave { dir, lock: Some(lock), copies: BTreeMap::new(), next: 1, recording: false })
|
||||
Self::create_with_stamp(root, name, stamp)
|
||||
}
|
||||
|
||||
fn create_with_stamp(root: &Path, name: &str, stamp: u128) -> Result<Autosave> {
|
||||
std::fs::create_dir_all(root).map_err(|err| io_error(root, err))?;
|
||||
let prefix = format!("{} {stamp}-{}", sanitize_name(name), std::process::id());
|
||||
// Several sessions can close in one clock tick. Claim a fresh directory atomically;
|
||||
// reopening an existing one could overwrite work another session left for recovery.
|
||||
for suffix in 0..1024 {
|
||||
let dir = root.join(format!("{prefix}-{suffix}"));
|
||||
match std::fs::create_dir(&dir) {
|
||||
Ok(()) => {
|
||||
let lock = std::fs::File::create(dir.join(LOCK)).map_err(|err| io_error(&dir, err))?;
|
||||
lock.try_lock().map_err(|err| EngineError::Io(format!("{}: {err}", dir.display())))?;
|
||||
return Ok(Autosave { dir, lock: Some(lock), copies: BTreeMap::new(), next: 1, recording: false });
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
|
||||
Err(err) => return Err(io_error(&dir, err)),
|
||||
}
|
||||
}
|
||||
Err(EngineError::Io(format!("{}: could not allocate a new autosave folder", root.display())))
|
||||
}
|
||||
|
||||
/// Write `session`, opened from the session file `origin`, and the plan of the `recording`
|
||||
@@ -257,7 +274,9 @@ pub struct Leftover {
|
||||
}
|
||||
|
||||
/// Leftover autosaves under `root`, newest first: folders no open session holds, written after the
|
||||
/// session file they came from or holding a recording. Empty and outdated ones are deleted.
|
||||
/// session file they came from or holding a recording. Equal timestamps are ambiguous, so those
|
||||
/// copies are retained too: a filesystem clock tick can cover both writes. Empty and provably
|
||||
/// outdated ones are deleted.
|
||||
pub fn leftovers(root: &Path) -> Vec<Leftover> {
|
||||
let mut out = Vec::new();
|
||||
for dir in std::fs::read_dir(root).into_iter().flatten().flatten().map(|d| d.path()) {
|
||||
@@ -271,7 +290,7 @@ pub fn leftovers(root: &Path) -> Vec<Leftover> {
|
||||
let recording = dir.join(RECORDING).is_file();
|
||||
drop(lock);
|
||||
match saved {
|
||||
Ok(saved) if recording || origin_saved.is_none_or(|o| saved > o) => {
|
||||
Ok(saved) if recording || origin_saved.is_none_or(|o| saved >= o) => {
|
||||
let text = std::fs::read_to_string(dir.join(SESSION)).unwrap_or_default();
|
||||
let named = serde_json::from_str::<serde_json::Value>(&text).ok().and_then(|v| v.get("name")?.as_str().map(str::to_string));
|
||||
let name = named.unwrap_or_else(|| dir.file_name().unwrap_or_default().to_string_lossy().into_owned());
|
||||
@@ -423,6 +442,11 @@ mod tests {
|
||||
dir
|
||||
}
|
||||
|
||||
fn set_modified(path: &Path, time: SystemTime) {
|
||||
std::fs::OpenOptions::new().write(true).open(path).unwrap().set_modified(time).unwrap();
|
||||
assert_eq!(std::fs::metadata(path).unwrap().modified().unwrap(), time);
|
||||
}
|
||||
|
||||
fn record(e: &mut Engine, level: f32) {
|
||||
let before = e.session_arc();
|
||||
let s = e.session_mut();
|
||||
@@ -464,6 +488,82 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_timestamps_keep_unsaved_audio_for_recovery() {
|
||||
let dir = folder("autosave-equal-timestamps");
|
||||
let path = dir.join("Song/Song.scraft");
|
||||
let root = dir.join("Autosave");
|
||||
let mut e = Engine::default();
|
||||
record(&mut e, 0.5);
|
||||
crate::io::save_session(&mut e, &path.to_string_lossy()).unwrap();
|
||||
record(&mut e, -0.25);
|
||||
let mut a = Autosave::create(&root, "Song").unwrap();
|
||||
a.write(e.session(), e.path.as_deref(), None).unwrap();
|
||||
let saved = a.dir.join(SESSION);
|
||||
let same_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000);
|
||||
set_modified(&path, same_time);
|
||||
set_modified(&saved, same_time);
|
||||
assert!(leftovers(&root).is_empty(), "a live session still owns its recovery copy");
|
||||
drop(a);
|
||||
let left = leftovers(&root);
|
||||
assert_eq!(left.len(), 1, "equal timestamps cannot prove unsaved work obsolete");
|
||||
assert!(saved.is_file());
|
||||
let mut recovered = Engine::default();
|
||||
assert!(recover(&mut recovered, &left[0]).unwrap().is_empty());
|
||||
assert_eq!(levels(&recovered), [0.5, -0.25]);
|
||||
recovered.execute("session.save", &json!({})).unwrap();
|
||||
set_modified(&path, same_time + Duration::from_secs(1));
|
||||
assert!(leftovers(&root).is_empty());
|
||||
assert!(!left[0].dir.exists());
|
||||
let mut reopened = Engine::default();
|
||||
open_session(&mut reopened, &path.to_string_lossy()).unwrap();
|
||||
assert_eq!(levels(&reopened), [0.5, -0.25]);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_tick_autosaves_preserve_each_closed_session() {
|
||||
let root = folder("autosave-same-tick");
|
||||
let mut first = Engine::default();
|
||||
record(&mut first, 0.5);
|
||||
let mut a = Autosave::create_with_stamp(&root, "Untitled", 1_000_000).unwrap();
|
||||
a.write(first.session(), None, None).unwrap();
|
||||
let first_dir = a.dir.clone();
|
||||
let first_session = std::fs::read(first_dir.join(SESSION)).unwrap();
|
||||
let first_audio = std::fs::read_dir(first_dir.join("Audio Files")).unwrap().next().unwrap().unwrap().path();
|
||||
let first_samples = std::fs::read(&first_audio).unwrap();
|
||||
drop(a);
|
||||
let mut second = Engine::default();
|
||||
record(&mut second, -0.25);
|
||||
let mut b = Autosave::create_with_stamp(&root, "Untitled", 1_000_000).unwrap();
|
||||
b.write(second.session(), None, None).unwrap();
|
||||
let second_dir = b.dir.clone();
|
||||
drop(b);
|
||||
assert_ne!(first_dir, second_dir, "a new autosave must not reopen an earlier session's folder");
|
||||
assert_eq!(std::fs::read(first_dir.join(SESSION)).unwrap(), first_session);
|
||||
assert_eq!(std::fs::read(first_audio).unwrap(), first_samples);
|
||||
let left = leftovers(&root);
|
||||
assert_eq!(left.len(), 2);
|
||||
for (dir, expected) in [(first_dir, 0.5), (second_dir, -0.25)] {
|
||||
let copy = left.iter().find(|copy| copy.dir == dir).unwrap();
|
||||
let mut restored = Engine::default();
|
||||
assert!(recover(&mut restored, copy).unwrap().is_empty());
|
||||
assert_eq!(levels(&restored), [expected]);
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_tick_autosaves_do_not_reuse_a_live_folder() {
|
||||
let root = folder("autosave-live-same-tick");
|
||||
let first = Autosave::create_with_stamp(&root, "Untitled", 1_000_000).unwrap();
|
||||
let second = Autosave::create_with_stamp(&root, "Untitled", 1_000_000).unwrap();
|
||||
assert_ne!(first.dir, second.dir);
|
||||
assert!(first.lock.is_some() && second.lock.is_some());
|
||||
drop((first, second));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_take_recorded_after_undo_gets_its_own_copy() {
|
||||
let dir = folder("autosave-undo");
|
||||
@@ -557,13 +657,15 @@ mod tests {
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(path, bytes).unwrap();
|
||||
}
|
||||
let mut paths: Vec<PathBuf> = files.iter().map(|(p, _)| p.clone()).collect();
|
||||
let paths: Vec<PathBuf> = files.iter().map(|(p, _)| p.clone()).collect();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let paths = {
|
||||
let mut paths = paths;
|
||||
let link = takes.join("Vox_03.wav");
|
||||
std::os::unix::fs::symlink(&files[1].0, &link).unwrap();
|
||||
paths.push(link);
|
||||
}
|
||||
paths
|
||||
};
|
||||
let plan = Recording {
|
||||
start: 0,
|
||||
pass: None,
|
||||
@@ -603,6 +705,8 @@ mod tests {
|
||||
recover(&mut r, &left[0]).unwrap();
|
||||
assert!(r.is_dirty());
|
||||
r.execute("session.save", &json!({})).unwrap();
|
||||
let autosaved = std::fs::metadata(left[0].dir.join(SESSION)).unwrap().modified().unwrap();
|
||||
set_modified(Path::new(&path), autosaved + Duration::from_secs(1));
|
||||
assert!(leftovers(&root).is_empty());
|
||||
assert!(!left[0].dir.exists());
|
||||
let mut reopened = Engine::default();
|
||||
|
||||
@@ -398,7 +398,7 @@ mod tests {
|
||||
fn windows_ignores_case_and_refuses_devices() {
|
||||
let (base, roots) = folders("case");
|
||||
let upper = s(base.join("IN/a.scraft"));
|
||||
assert_eq!(roots.check(upper, Access::Read), Ok(()), "{upper}");
|
||||
assert_eq!(roots.check(&upper, Access::Read), Ok(()), "{upper}");
|
||||
assert_eq!(roots.check(s(base.join("In/Sub/New.scraft")), Access::Read), Ok(()));
|
||||
let e = roots.check(s(base.join("out/NUL")), Access::Write).unwrap_err();
|
||||
assert!(e.contains("reserved device names"), "{e}");
|
||||
|
||||
+17
-1
@@ -1,6 +1,6 @@
|
||||
# SoundCraft roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (alpha gate added; stage re-normalized to pre-alpha) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared Windows installer UI; existing folder/shortcut/launch options retained; install and uninstall completion) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
|
||||
Forward-looking plan. Status and numbers: [`../ROADMAP.md`](../ROADMAP.md) and
|
||||
[`target-app-parity.md`](target-app-parity.md); the ranked work list: [`gaps.md`](gaps.md).
|
||||
@@ -88,3 +88,19 @@ There is no `cargo xtask scorecard` in this repo; the measured number is `cargo
|
||||
|---|---|---|
|
||||
| 2026-10-10 | minor | Added the alpha gate (3 of 6 core workflows fail → pre-alpha) |
|
||||
| 2026-10-10 | major | Created from ROADMAP.md's milestones and alpha list; added M12–M17 and the beta plan with estimates |
|
||||
|
||||
## Packaging update — 2026-10-10
|
||||
|
||||
Windows setup now uses the shared `craft-installer` native UI source, with install/repair/remove
|
||||
completion, cancellation and failure screens. Existing MSI ownership and upgrade identities are
|
||||
preserved; unattended modes remain unattended. Source/generator checks pass; native Windows
|
||||
installer acceptance is still pending. See [setup validation](../packaging/windows/README.md).
|
||||
|
||||
## Packaging update (2026-10-11)
|
||||
|
||||
Windows setup now uses the suite's shared `craft-installer` UI, preserving existing folder,
|
||||
shortcut and launch options and app-owned installation policy. Install, uninstall and repair
|
||||
have explicit completion screens; cancellation and failure have separate endings. Source/profile
|
||||
checks, WiX UI library compilation for x64/x86/ARM64, PowerShell parsing and package identity
|
||||
comparisons pass. Full MSI linking and interactive Windows acceptance remain pending; see
|
||||
[Windows packaging](../packaging/windows/README.md).
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Windows installer UI
|
||||
|
||||
`installer-ui.wxs` is generated from the shared `craft-installer` crate in
|
||||
[craft-libs](https://github.com/storytold/craft-libs/tree/main/crates/installer).
|
||||
`package.ps1` compiles it with this app's package source. The common UI provides progress,
|
||||
repair/remove, and explicit install/uninstall/repair success, cancellation and failure screens.
|
||||
|
||||
Regenerate or check from a sibling craft-libs checkout:
|
||||
|
||||
```sh
|
||||
cargo run --locked -p craft-installer -- --write --app soundcraft ../soundcraft/packaging/windows/installer-ui.wxs
|
||||
cargo run --locked -p craft-installer -- --check --app soundcraft ../soundcraft/packaging/windows/installer-ui.wxs
|
||||
```
|
||||
|
||||
Edit the shared templates/profile and regenerate; avoid editing this snapshot by hand. The
|
||||
profile retains this app's existing folder, shortcut and launch options; defaults, persistence,
|
||||
components, associations and signing stay in the app-owned source. Release builds require no
|
||||
sibling checkout, published crate or additional runtime dependency. `/qn`, `/qb` and `/passive`
|
||||
remain unattended and do not launch the app.
|
||||
|
||||
Source/profile checks and native fixture MSI linking/UI table checks passed for x64, x86 and
|
||||
ARM64. All 14 profiles passed full UI install, missing-file repair and cached uninstall, and
|
||||
x86 silent install/removal. Folder upgrades, desktop choices and all six launch profiles also
|
||||
passed their Windows fixture checks. See the
|
||||
[Windows verification record](../../../craft-libs/docs/windows-installer-verification.md) for
|
||||
interactive results, scope validation and remaining acceptance checks with real release payloads.
|
||||
The shared crate includes reusable linking, full UI, option/upgrade/launch and x86 test runners.
|
||||
Windows Settings now uses a separate MSI-owned entry whose uninstall command selects full UI,
|
||||
including the completion screen. Its quiet command and direct `/qn`, `/qb`, `/passive` commands
|
||||
remain unattended. The main feature references `CraftInstallerSettings`; the shared snapshot
|
||||
owns this registration and hides the native MSI entry while retaining MSI repair/upgrade APIs.
|
||||
|
||||
The Settings display name appends `(Craft Apps)` for compatibility with older Launchers; the
|
||||
hidden native MSI name stays unchanged. Existing ARP Modify policy is retained explicitly.
|
||||
@@ -0,0 +1,229 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by craft-installer for soundcraft. Edit the template in craft-libs, then regenerate. -->
|
||||
<!-- Adapted from PdfCraft installer-ui.wxs (a6bc63a), GridCraft gridcraft.wxs (5b15827),
|
||||
and EffectCraft installer-ui.wxs (1b4d47f).
|
||||
Copyright (c) 2026 ArtCraft Team and the PdfCraft contributors.
|
||||
Copyright (c) 2026 ArtCraft Team and the GridCraft contributors.
|
||||
Copyright (c) 2026 ArtCraft Team and the EffectCraft contributors.
|
||||
Licensed under MIT OR Apache-2.0; see the repository's LICENSE-MIT and LICENSE-APACHE. -->
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
<Fragment>
|
||||
<!-- Windows Installer populates files-in-use lists at runtime. Keep their table even
|
||||
though these dialogs have no static ListItem rows (the GridCraft UI does the same). -->
|
||||
<EnsureTable Id="ListBox" />
|
||||
<!-- Reuse the existing package icon as a fallback; MSI supplies its standard message icon. -->
|
||||
<Binary Id="CraftInstallerErrorIcon" SourceFile="$(var.IconPath)" />
|
||||
|
||||
|
||||
<UI Id="CraftInstallerUI">
|
||||
<TextStyle Id="CraftInstallerNormal" FaceName="Segoe UI" Size="9" />
|
||||
<TextStyle Id="CraftInstallerTitle" FaceName="Segoe UI" Size="12" Bold="yes" />
|
||||
<Property Id="DefaultUIFont" Value="CraftInstallerNormal" />
|
||||
<Property Id="ErrorDialog" Value="CraftInstallerErrorDlg" />
|
||||
<Dialog Id="CraftInstallerErrorDlg" Width="320" Height="120" Title="[ProductName] Setup" ErrorDialog="yes" NoMinimize="yes">
|
||||
<Control Id="ErrorText" Type="Text" X="60" Y="15" Width="245" Height="65" TabSkip="no" />
|
||||
<Control Id="ErrorIcon" Type="Icon" X="15" Y="15" Width="32" Height="32" FixedSize="yes" IconSize="32" TabSkip="yes" Text="CraftInstallerErrorIcon" />
|
||||
<Control Id="A" Type="PushButton" X="15" Y="90" Width="65" Height="20" Text="Abort" TabSkip="yes">
|
||||
<Publish Event="EndDialog" Value="ErrorAbort" />
|
||||
</Control>
|
||||
<Control Id="C" Type="PushButton" X="15" Y="90" Width="65" Height="20" Text="Cancel" TabSkip="yes">
|
||||
<Publish Event="EndDialog" Value="ErrorCancel" />
|
||||
</Control>
|
||||
<Control Id="I" Type="PushButton" X="15" Y="90" Width="65" Height="20" Text="Ignore" TabSkip="yes">
|
||||
<Publish Event="EndDialog" Value="ErrorIgnore" />
|
||||
</Control>
|
||||
<Control Id="N" Type="PushButton" X="15" Y="90" Width="65" Height="20" Text="No" TabSkip="yes">
|
||||
<Publish Event="EndDialog" Value="ErrorNo" />
|
||||
</Control>
|
||||
<Control Id="O" Type="PushButton" X="15" Y="90" Width="65" Height="20" Text="OK" TabSkip="yes">
|
||||
<Publish Event="EndDialog" Value="ErrorOk" />
|
||||
</Control>
|
||||
<Control Id="R" Type="PushButton" X="15" Y="90" Width="65" Height="20" Text="Retry" TabSkip="yes">
|
||||
<Publish Event="EndDialog" Value="ErrorRetry" />
|
||||
</Control>
|
||||
<Control Id="Y" Type="PushButton" X="15" Y="90" Width="65" Height="20" Text="Yes" TabSkip="yes">
|
||||
<Publish Event="EndDialog" Value="ErrorYes" />
|
||||
</Control>
|
||||
</Dialog>
|
||||
<Dialog Id="FilesInUse" Width="320" Height="190" Title="[ProductName] Setup" NoMinimize="yes">
|
||||
<Control Id="Description" Type="Text" X="15" Y="15" Width="290" Height="35"
|
||||
Text="Close the applications listed below, then choose Retry. Ignore may require a restart." />
|
||||
<Control Id="Files" Type="ListBox" X="15" Y="55" Width="290" Height="85" Property="FileInUseProcess" />
|
||||
<Control Id="Retry" Type="PushButton" X="95" Y="155" Width="65" Height="20" Default="yes" Text="Retry">
|
||||
<Publish Event="EndDialog" Value="Retry" />
|
||||
</Control>
|
||||
<Control Id="Ignore" Type="PushButton" X="167" Y="155" Width="65" Height="20" Text="Ignore">
|
||||
<Publish Event="EndDialog" Value="Ignore" />
|
||||
</Control>
|
||||
<Control Id="Cancel" Type="PushButton" X="240" Y="155" Width="65" Height="20" Cancel="yes" Text="Cancel">
|
||||
<Publish Event="EndDialog" Value="Exit" />
|
||||
</Control>
|
||||
</Dialog>
|
||||
|
||||
|
||||
<Dialog Id="InstallWelcome" Width="370" Height="270" Title="[ProductName] Setup">
|
||||
<Control Id="Title" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\CraftInstallerTitle}Install [ProductName]" />
|
||||
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="40"
|
||||
Text="Setup will install [ProductName] for all users and add it to the Start menu. Click Install to continue." />
|
||||
|
||||
<Control Id="Install" Type="PushButton" X="195" Y="235" Width="75" Height="23" Default="yes" ElevationShield="yes" Text="&Install">
|
||||
|
||||
<Publish Event="EndDialog" Value="Return" Order="99" />
|
||||
</Control>
|
||||
<Control Id="Cancel" Type="PushButton" X="280" Y="235" Width="70" Height="23" Cancel="yes" Text="Cancel">
|
||||
<Publish Event="EndDialog" Value="Exit" />
|
||||
</Control>
|
||||
</Dialog>
|
||||
|
||||
<Dialog Id="InstallMaintenance" Width="370" Height="270" Title="[ProductName] Setup">
|
||||
<Control Id="Title" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\CraftInstallerTitle}Maintain [ProductName]" />
|
||||
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="70"
|
||||
Text="[ProductName] is already installed. Choose Repair to restore missing files and shortcuts, or Remove to uninstall it." />
|
||||
<Control Id="Repair" Type="PushButton" X="110" Y="235" Width="75" Height="23" Default="yes" ElevationShield="yes" Text="&Repair">
|
||||
<!-- Feature events do not retain REINSTALL/REMOVE in the client UI session. -->
|
||||
<Publish Property="CraftInstallerOperation" Value="repair" Order="1" />
|
||||
<Publish Event="ReinstallMode" Value="amus" Order="2" />
|
||||
<Publish Event="Reinstall" Value="ALL" Order="3" />
|
||||
<Publish Event="EndDialog" Value="Return" Order="4" />
|
||||
</Control>
|
||||
<Control Id="Remove" Type="PushButton" X="195" Y="235" Width="75" Height="23" ElevationShield="yes" Text="R&emove">
|
||||
<Publish Property="CraftInstallerOperation" Value="remove" Order="1" />
|
||||
<Publish Event="Remove" Value="ALL" Order="2" />
|
||||
<Publish Event="EndDialog" Value="Return" Order="3" />
|
||||
</Control>
|
||||
<Control Id="Cancel" Type="PushButton" X="280" Y="235" Width="70" Height="23" Cancel="yes" Text="Cancel">
|
||||
<Publish Event="EndDialog" Value="Exit" />
|
||||
</Control>
|
||||
</Dialog>
|
||||
|
||||
<Dialog Id="InstallProgress" Width="370" Height="270" Title="[ProductName] Setup" Modeless="yes">
|
||||
<Control Id="Title" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\CraftInstallerTitle}Setting up [ProductName]" />
|
||||
<!-- No ActionText table is authored: subscribing would blank this message (#305). -->
|
||||
<Control Id="Status" Type="Text" X="20" Y="60" Width="330" Height="40" Text="Please wait while setup completes." />
|
||||
<Control Id="Progress" Type="ProgressBar" X="20" Y="110" Width="330" Height="15" ProgressBlocks="yes">
|
||||
<Subscribe Event="SetProgress" Attribute="Progress" />
|
||||
</Control>
|
||||
<Control Id="Cancel" Type="PushButton" X="280" Y="235" Width="70" Height="23" Cancel="yes" Text="Cancel">
|
||||
<Publish Event="SpawnDialog" Value="ConfirmCancel" />
|
||||
</Control>
|
||||
</Dialog>
|
||||
|
||||
<Dialog Id="ConfirmCancel" Width="300" Height="110" Title="Cancel setup">
|
||||
<Control Id="Description" Type="Text" X="20" Y="20" Width="260" Height="35" Text="Cancel [ProductName] setup?" />
|
||||
<Control Id="Yes" Type="PushButton" X="130" Y="70" Width="70" Height="23" Text="&Yes">
|
||||
<Publish Event="EndDialog" Value="Exit" />
|
||||
</Control>
|
||||
<Control Id="No" Type="PushButton" X="210" Y="70" Width="70" Height="23" Default="yes" Cancel="yes" Text="&No">
|
||||
<Publish Event="EndDialog" Value="Return" />
|
||||
</Control>
|
||||
</Dialog>
|
||||
|
||||
<Dialog Id="InstallComplete" Width="370" Height="270" Title="[ProductName] Setup">
|
||||
<Control Id="Title" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\CraftInstallerTitle}Setup completed successfully" HideCondition="REMOVE = "ALL" OR CraftInstallerOperation = "remove"" />
|
||||
<Control Id="RemovedTitle" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\CraftInstallerTitle}Uninstall completed successfully" Hidden="yes" ShowCondition="REMOVE = "ALL" OR CraftInstallerOperation = "remove"" />
|
||||
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="70"
|
||||
Text="[ProductName] is installed. Open it from the Start menu. Click Finish to close setup." HideCondition="REMOVE = "ALL" OR REINSTALL OR CraftInstallerOperation OR ACTION = "ADMIN"" />
|
||||
<Control Id="Removed" Type="Text" X="20" Y="55" Width="330" Height="70" Hidden="yes"
|
||||
Text="[ProductName] was uninstalled. Your documents and settings were kept. Click Finish to close setup."
|
||||
ShowCondition="REMOVE = "ALL" OR CraftInstallerOperation = "remove"" />
|
||||
<Control Id="Repaired" Type="Text" X="20" Y="55" Width="330" Height="70" Hidden="yes"
|
||||
Text="[ProductName] was repaired. Open it from the Start menu. Click Finish to close setup."
|
||||
ShowCondition="(REINSTALL OR CraftInstallerOperation = "repair") AND NOT REMOVE AND CraftInstallerOperation <> "remove" AND ACTION <> "ADMIN"" />
|
||||
<Control Id="AdminComplete" Type="Text" X="20" Y="55" Width="330" Height="70" Hidden="yes"
|
||||
Text="The administrative image was created. Click Finish to close setup." ShowCondition="ACTION = "ADMIN"" />
|
||||
<Control Id="Finish" Type="PushButton" X="280" Y="235" Width="70" Height="23" Default="yes" Cancel="yes" Text="&Finish">
|
||||
|
||||
<Publish Event="EndDialog" Value="Return" Order="999" />
|
||||
</Control>
|
||||
|
||||
</Dialog>
|
||||
|
||||
<Dialog Id="InstallFailed" Width="370" Height="270" Title="[ProductName] Setup">
|
||||
<Control Id="Title" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\CraftInstallerTitle}Setup failed" />
|
||||
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="70"
|
||||
Text="[ProductName] setup could not be completed. Resolve the reported error and run setup again." />
|
||||
<Control Id="Finish" Type="PushButton" X="280" Y="235" Width="70" Height="23" Default="yes" Cancel="yes" Text="&Finish">
|
||||
<Publish Event="EndDialog" Value="Return" />
|
||||
</Control>
|
||||
</Dialog>
|
||||
|
||||
<Dialog Id="InstallCancelled" Width="370" Height="270" Title="[ProductName] Setup">
|
||||
<Control Id="Title" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\CraftInstallerTitle}Setup cancelled" />
|
||||
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="70"
|
||||
Text="[ProductName] setup was cancelled. Run setup again when you are ready." />
|
||||
<Control Id="Finish" Type="PushButton" X="280" Y="235" Width="70" Height="23" Default="yes" Cancel="yes" Text="&Finish">
|
||||
<Publish Event="EndDialog" Value="Return" />
|
||||
</Control>
|
||||
</Dialog>
|
||||
|
||||
<!-- Shown by Windows Installer (Restart Manager) when an upgrade or repair must replace files
|
||||
that a running app holds open. OK closes those programs (MSIRMSHUTDOWN=2) and
|
||||
continues; Cancel ends setup. -->
|
||||
<Dialog Id="MsiRMFilesInUse" Width="370" Height="200" Title="[ProductName] Setup" KeepModeless="yes">
|
||||
<Control Id="Title" Type="Text" X="20" Y="15" Width="330" Height="25" Text="{\CraftInstallerTitle}Files in use" />
|
||||
<Control Id="Description" Type="Text" X="20" Y="45" Width="330" Height="30"
|
||||
Text="These programs are using files that setup needs to update. Click OK to close them and continue, or Cancel to stop setup." />
|
||||
<Control Id="List" Type="ListBox" X="20" Y="80" Width="330" Height="55" Property="FileInUseProcess" Sunken="yes" TabSkip="yes" />
|
||||
<Control Id="OK" Type="PushButton" X="195" Y="165" Width="75" Height="23" Default="yes" Text="OK">
|
||||
<Publish Property="MSIRMSHUTDOWN" Value="2" Order="1" />
|
||||
<Publish Event="EndDialog" Value="Return" Order="2" />
|
||||
</Control>
|
||||
<Control Id="Cancel" Type="PushButton" X="280" Y="165" Width="70" Height="23" Cancel="yes" Text="Cancel">
|
||||
<Publish Event="EndDialog" Value="Exit" />
|
||||
</Control>
|
||||
</Dialog>
|
||||
|
||||
<InstallUISequence>
|
||||
<Show Dialog="InstallWelcome" Before="CostInitialize" Condition="UILevel = 5 AND NOT Installed AND NOT REMOVE AND NOT REINSTALL" />
|
||||
<!-- Reinstall/Remove control events require the selection manager initialized by costing. -->
|
||||
<Show Dialog="InstallMaintenance" After="CostFinalize" Condition="UILevel = 5 AND Installed AND NOT REMOVE AND NOT REINSTALL" />
|
||||
|
||||
<Show Dialog="InstallProgress" Before="ExecuteAction" Condition="UILevel = 5" />
|
||||
<Show Dialog="InstallComplete" OnExit="success" Condition="UILevel = 5" />
|
||||
<Show Dialog="InstallFailed" OnExit="error" Condition="UILevel = 5" />
|
||||
<Show Dialog="InstallCancelled" OnExit="cancel" Condition="UILevel = 5" />
|
||||
</InstallUISequence>
|
||||
<AdminUISequence>
|
||||
<Show Dialog="InstallProgress" Before="ExecuteAction" Condition="UILevel = 5" />
|
||||
<Show Dialog="InstallComplete" OnExit="success" Condition="UILevel = 5" />
|
||||
<Show Dialog="InstallFailed" OnExit="error" Condition="UILevel = 5" />
|
||||
<Show Dialog="InstallCancelled" OnExit="cancel" Condition="UILevel = 5" />
|
||||
</AdminUISequence>
|
||||
</UI>
|
||||
</Fragment>
|
||||
<Fragment>
|
||||
<!-- Keep MSI registration for repair, upgrades and managed removal, but let Settings
|
||||
invoke our explicit UI command instead of Windows Installer's basic-UI ARP path. -->
|
||||
<Property Id="ARPSYSTEMCOMPONENT" Value="1" />
|
||||
<?if $(sys.BUILDARCH) = x86 ?>
|
||||
<?define CraftMsiSystemFolder = SystemFolder ?>
|
||||
<?else ?>
|
||||
<?define CraftMsiSystemFolder = System64Folder ?>
|
||||
<?endif ?>
|
||||
<ComponentGroup Id="CraftInstallerSettings" Directory="INSTALLFOLDER">
|
||||
<Component Id="CraftInstallerSettingsEntry" Guid="*">
|
||||
<RegistryKey Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\Craft.soundcraft" ForceDeleteOnUninstall="yes">
|
||||
<!-- Legacy Launchers match the canonical MSI name exactly; keep this alias distinct. -->
|
||||
<RegistryValue Name="DisplayName" Type="string" Value="[ProductName] (Craft Apps)" KeyPath="yes" />
|
||||
<RegistryValue Name="DisplayVersion" Type="string" Value="[ProductVersion]" />
|
||||
<RegistryValue Name="Publisher" Type="string" Value="[Manufacturer]" />
|
||||
<RegistryValue Name="DisplayIcon" Type="string" Value=""[INSTALLFOLDER]soundcraft.exe",0" />
|
||||
<RegistryValue Name="InstallLocation" Type="string" Value="[INSTALLFOLDER]" />
|
||||
<RegistryValue Name="URLInfoAbout" Type="string" Value="[ARPURLINFOABOUT]" />
|
||||
<RegistryValue Name="URLUpdateInfo" Type="string" Value="[ARPURLUPDATEINFO]" />
|
||||
<RegistryValue Name="HelpLink" Type="string" Value="[ARPHELPLINK]" />
|
||||
<!-- Deliberately omit WindowsInstaller: Settings must honor UninstallString. -->
|
||||
<RegistryValue Name="UninstallString" Type="string" Value=""[$(var.CraftMsiSystemFolder)]msiexec.exe" /x [ProductCode] /qf /norestart" />
|
||||
<RegistryValue Name="QuietUninstallString" Type="string" Value=""[$(var.CraftMsiSystemFolder)]msiexec.exe" /x [ProductCode] /qn /norestart" />
|
||||
<RegistryValue Name="ModifyPath" Type="string" Value=""[$(var.CraftMsiSystemFolder)]msiexec.exe" /i [ProductCode] /qf /norestart" />
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
<!-- Preserve the app-owned ARPNOMODIFY policy instead of inventing a new default. -->
|
||||
<Component Id="CraftInstallerSettingsNoModify" Guid="*" Condition="ARPNOMODIFY = "1"">
|
||||
<RegistryValue Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\Craft.soundcraft"
|
||||
Name="NoModify" Type="integer" Value="1" KeyPath="yes" />
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
</Fragment>
|
||||
|
||||
</Wix>
|
||||
@@ -93,7 +93,7 @@ Copy-Item (Join-Path $Bin 'soundcraft.exe'), (Join-Path $Bin 'soundcraft-cli.exe
|
||||
# ---- MSI ---------------------------------------------------------------------------------------
|
||||
$Msi = Join-Path $Dist "soundcraft-$Version-windows-$Arch.msi"
|
||||
Invoke-Native 'wix build' {
|
||||
wix build (Join-Path $PSScriptRoot 'soundcraft.wxs') -arch $Arch `
|
||||
wix build (Join-Path $PSScriptRoot 'soundcraft.wxs') (Join-Path $PSScriptRoot 'installer-ui.wxs') -arch $Arch `
|
||||
-d "Version=$MsiVersion" -d "BinDir=$Stage" -d "IconPath=$(Join-Path $Root 'assets\app-icon\soundcraft.ico')" `
|
||||
-o $Msi
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<!--
|
||||
SoundCraft MSI (WiX Toolset v5: `dotnet tool install -g wix`). Built by package.ps1:
|
||||
|
||||
wix build soundcraft.wxs -arch x64 -d Version=1.2.3 -d BinDir=... -d IconPath=... -o out.msi
|
||||
wix build soundcraft.wxs installer-ui.wxs -arch x64 -d Version=1.2.3 -d BinDir=... -d IconPath=... -o out.msi
|
||||
|
||||
Per-machine install into Program Files, a Start Menu shortcut, `.scraft` sessions owned by
|
||||
SoundCraft, and SoundCraft offered under "Open with" for WAV/AIFF/FLAC/MP3 and MIDI files
|
||||
@@ -30,6 +30,8 @@
|
||||
<Property Id="ARPURLUPDATEINFO" Value="https://getartcraft.com/apps/soundcraft" />
|
||||
<Property Id="ARPHELPLINK" Value="https://discord.gg/artcraft" />
|
||||
|
||||
<!-- Shared native setup UI, including install/uninstall completion. -->
|
||||
|
||||
<StandardDirectory Id="ProgramFiles6432Folder">
|
||||
<Directory Id="INSTALLFOLDER" Name="SoundCraft" />
|
||||
</StandardDirectory>
|
||||
@@ -37,7 +39,10 @@
|
||||
|
||||
<Feature Id="Main" Title="SoundCraft" Level="1">
|
||||
<ComponentGroupRef Id="SoundcraftFiles" />
|
||||
<ComponentGroupRef Id="CraftInstallerSettings" />
|
||||
</Feature>
|
||||
<!-- Shared full UI; app installation policy remains below. -->
|
||||
<UIRef Id="CraftInstallerUI" />
|
||||
</Package>
|
||||
|
||||
<Fragment>
|
||||
|
||||
Reference in New Issue
Block a user