mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-20 18:28:50 +00:00
Merge pull request #1103 from skylartaylor/codex/windows-installer-latest-20261014-01
feat(installer): unify Windows setup and completion screens
This commit is contained in:
12 files changed
+358
-8
No files matched your search
Generated
+1
@@ -997,6 +997,7 @@ dependencies = [
|
||||
"log",
|
||||
"quick-xml",
|
||||
"regex",
|
||||
"same-file",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
|
||||
@@ -49,6 +49,7 @@ unicode-segmentation = "1"
|
||||
caseless = "0.2"
|
||||
regex = { version = "1", default-features = false, features = ["std", "unicode", "perf"] }
|
||||
tempfile = "3"
|
||||
same-file = "1.0.6"
|
||||
|
||||
# text / rendering / formats
|
||||
skrifa = "0.47"
|
||||
|
||||
@@ -494,3 +494,10 @@ Newest first.
|
||||
| 2026-10-10 | major | Restructured to the progress-docs standard; full re-measure; the "Working today" inventory moved to docs/target-app-parity.md, milestones to docs/roadmap.md, the CLI/MCP section left to docs/agents.md (which already covered it) |
|
||||
| 2026-10-04 | major | Parity estimate: breadth 99%, depth ~83%, ~170 h |
|
||||
| 2026-10-01 | major | First roadmap: status, milestones |
|
||||
|
||||
|
||||
### Windows installer unification — release preparation, 2026-10-12
|
||||
|
||||
The shared craft-installer UI preserves this revision's install options and provides persistent installation, repair and Windows Settings removal completion screens. Silent modes stay unattended. Earlier Windows acceptance is recorded in packaging/windows/README.md; normal CI and production-profile upgrade qualification against pinned current releases are in progress. No release or PR has been published.
|
||||
|
||||
- **2026-10-15** · Windows recovery folders validate the identity of the live locked file without reading it through a second handle. Missing or replaced locks disable writes and cleanup; regression tests cover retained exclusive locking and replacement-file protection. No readiness percentage change.
|
||||
@@ -36,6 +36,9 @@ unicode-segmentation = { workspace = true }
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
same-file = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
designcraft-fonts = { workspace = true, features = ["testing"] }
|
||||
zip = { workspace = true }
|
||||
|
||||
@@ -34,8 +34,8 @@ fn a_commit_failure_reports_the_completed_prefix_and_cleans_staging() {
|
||||
assert!(error.contains("synthetic disk full"), "{error}");
|
||||
assert!(error.contains(second.to_str().unwrap()), "{error}");
|
||||
let completed = error.split("completed chapter paths:").nth(1).unwrap();
|
||||
assert!(completed.contains(first.to_str().unwrap()), "{error}");
|
||||
assert!(!completed.contains(second.to_str().unwrap()) && !completed.contains(third.to_str().unwrap()), "{error}");
|
||||
// The completed list uses debug formatting, which escapes Windows path separators.
|
||||
assert_eq!(completed.trim(), format!("{:?}", [first.to_str().unwrap()]), "{error}");
|
||||
assert_eq!(designcraft_format::load(&std::fs::read(first).unwrap()).unwrap().sections.first().unwrap().start_number, Some(9));
|
||||
assert_eq!(std::fs::read(second).unwrap(), old_bytes);
|
||||
assert_eq!(std::fs::read(third).unwrap(), old_bytes);
|
||||
|
||||
@@ -133,8 +133,9 @@ fn new_lock(dir: &Path) -> std::io::Result<std::fs::File> {
|
||||
new.mode(0o600);
|
||||
}
|
||||
let mut file = new.open(dir.join(LOCK))?;
|
||||
// A marker distinguishes the held handle from a replacement at the same pathname on
|
||||
// platforms without portable file-identity metadata. Empty older-version locks remain valid.
|
||||
// A marker supplements Unix inode validation and identifies locks on other platforms.
|
||||
// Windows uses live handle identity because its exclusive lock prevents separate-handle reads.
|
||||
// Empty older-version locks remain valid.
|
||||
static NEXT_LOCK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_err(std::io::Error::other)?.as_nanos();
|
||||
let marker = format!("{}:{}:{now}", std::process::id(), NEXT_LOCK.fetch_add(1, std::sync::atomic::Ordering::Relaxed));
|
||||
@@ -165,6 +166,18 @@ fn validate_lock(dir: &Path, lock: &std::fs::File) -> Result<()> {
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
let _ = (held, current);
|
||||
// Windows locks prevent reads through separately opened handles, even in this process.
|
||||
// Compare the held and named file identities without releasing the recovery lock.
|
||||
#[cfg(windows)]
|
||||
let held_identity = {
|
||||
let held = same_file::Handle::from_file(lock.try_clone().map_err(|_| fail())?).map_err(|_| fail())?;
|
||||
let current = same_file::Handle::from_file(named.try_clone().map_err(|_| fail())?).map_err(|_| fail())?;
|
||||
if held != current {
|
||||
return Err(fail());
|
||||
}
|
||||
held
|
||||
};
|
||||
#[cfg(not(windows))]
|
||||
if lock_marker(lock).map_err(|_| fail())? != lock_marker(&named).map_err(|_| fail())? {
|
||||
return Err(fail());
|
||||
}
|
||||
@@ -179,10 +192,18 @@ fn validate_lock(dir: &Path, lock: &std::fs::File) -> Result<()> {
|
||||
return Err(fail());
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let after_named = std::fs::File::open(&path).map_err(|_| fail())?;
|
||||
let after_identity = same_file::Handle::from_file(after_named).map_err(|_| fail())?;
|
||||
if held_identity != after_identity {
|
||||
return Err(fail());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
|
||||
fn lock_marker(file: &std::fs::File) -> std::io::Result<Vec<u8>> {
|
||||
use std::io::{Read, Seek};
|
||||
let mut file = file.try_clone()?;
|
||||
@@ -1324,6 +1345,50 @@ mod tests {
|
||||
assert!(list(&own).is_empty(), "another owner's folder must not receive our recovery data");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn an_exclusively_locked_windows_recovery_file_validates() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let lock = new_lock(root.path()).unwrap();
|
||||
lock.try_lock().unwrap();
|
||||
validate_lock(root.path(), &lock).unwrap();
|
||||
let named = std::fs::File::open(root.path().join(LOCK)).unwrap();
|
||||
assert!(matches!(named.try_lock(), Err(std::fs::TryLockError::WouldBlock)), "validation must retain the exclusive lock");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn windows_recovery_rejects_a_replacement_with_the_same_marker() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let lock = new_lock(root.path()).unwrap();
|
||||
let marker = std::fs::read(root.path().join(LOCK)).unwrap();
|
||||
lock.try_lock().unwrap();
|
||||
let moved = root.path().join("original-lock");
|
||||
std::fs::rename(root.path().join(LOCK), &moved).unwrap();
|
||||
std::fs::write(root.path().join(LOCK), marker).unwrap();
|
||||
assert!(validate_lock(root.path(), &lock).is_err(), "matching marker bytes do not make another file our lock");
|
||||
std::fs::remove_file(root.path().join(LOCK)).unwrap();
|
||||
assert!(validate_lock(root.path(), &lock).is_err(), "a missing lock must not validate");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn windows_recovery_does_not_write_or_discard_a_replacement_runs_entries() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let own = run_dir(root.path()).unwrap();
|
||||
std::fs::rename(own.join(LOCK), own.join("original-lock")).unwrap();
|
||||
let replacement = new_lock(&own).unwrap();
|
||||
replacement.try_lock().unwrap();
|
||||
let session = dirty_session();
|
||||
let (doc, meta) = files(&own, session.docs[0].uid);
|
||||
std::fs::write(&doc, b"another owner's document").unwrap();
|
||||
std::fs::write(&meta, b"{}").unwrap();
|
||||
assert!(save(&session, &own).is_err());
|
||||
discard(&own, session.docs[0].uid);
|
||||
assert_eq!(std::fs::read(&doc).unwrap(), b"another owner's document");
|
||||
assert_eq!(std::fs::read(&meta).unwrap(), b"{}");
|
||||
}
|
||||
|
||||
/// A blocked attempt to recreate a run's lock must stop the save before it writes documents.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
|
||||
@@ -58,7 +58,7 @@ fn save_a_copy_rebases_its_data_source_without_changing_the_open_document() {
|
||||
session.execute("file.saveACopy", &json!({"path": destination})).unwrap();
|
||||
|
||||
let copy = designcraft_format::load(&std::fs::read(destination).unwrap()).unwrap();
|
||||
assert_eq!(copy.data_merge.sources[0].relative_path.as_deref(), Some("../../source/contacts.csv"));
|
||||
assert_eq!(copy.data_merge.sources[0].relative_path.as_deref().map(Path::new), Some(Path::new("../../source/contacts.csv")));
|
||||
let after = session.doc().unwrap();
|
||||
assert!(Arc::ptr_eq(&before.doc, &after.doc));
|
||||
assert!(Arc::ptr_eq(&before.saved_doc, &after.saved_doc));
|
||||
|
||||
@@ -113,3 +113,8 @@ output intent · EPS place") is done except Books depth and EPS (preview only).
|
||||
| 2026-10-10 | trivial | Ready figure 42% → 47% (method aligned with the standard); beta distance 28 points |
|
||||
| 2026-10-10 | minor | Alpha gate table added (6 core workflows); gate fails on manual threading, stage pre-alpha; A1 milestone and focus item 0 |
|
||||
| 2026-10-10 | major | Created: milestones moved from ROADMAP.md and re-assessed; Current focus and the beta plan from the full re-measure |
|
||||
|
||||
|
||||
### Windows installer unification — release preparation, 2026-10-12
|
||||
|
||||
The shared craft-installer UI preserves this revision's install options and provides persistent installation, repair and Windows Settings removal completion screens. Silent modes stay unattended. Earlier Windows acceptance is recorded in packaging/windows/README.md; normal CI and production-profile upgrade qualification against pinned current releases are in progress. No release or PR has been published.
|
||||
@@ -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 designcraft ../designcraft/packaging/windows/installer-ui.wxs
|
||||
cargo run --locked -p craft-installer -- --check --app designcraft ../designcraft/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.
|
||||
@@ -2,7 +2,7 @@
|
||||
<!--
|
||||
DesignCraft MSI (WiX Toolset v5: `dotnet tool install -g wix`). Built by package.ps1:
|
||||
|
||||
wix build designcraft.wxs -arch x64 -d Version=1.2.3 -d BinDir=... -d IconPath=... -o out.msi
|
||||
wix build designcraft.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, `.pcraft` owned by DesignCraft,
|
||||
and DesignCraft offered under "Open with" for PSD/PSB and the image formats it reads (without
|
||||
@@ -30,6 +30,8 @@
|
||||
<Property Id="ARPURLUPDATEINFO" Value="https://getartcraft.com/apps/designcraft" />
|
||||
<Property Id="ARPHELPLINK" Value="https://discord.gg/artcraft" />
|
||||
|
||||
<!-- Shared native setup UI, including install/uninstall completion. -->
|
||||
|
||||
<StandardDirectory Id="ProgramFiles6432Folder">
|
||||
<Directory Id="INSTALLFOLDER" Name="DesignCraft" />
|
||||
</StandardDirectory>
|
||||
@@ -37,7 +39,10 @@
|
||||
|
||||
<Feature Id="Main" Title="DesignCraft" Level="1">
|
||||
<ComponentGroupRef Id="DesigncraftFiles" />
|
||||
<ComponentGroupRef Id="CraftInstallerSettings" />
|
||||
</Feature>
|
||||
<!-- Shared full UI; app installation policy remains below. -->
|
||||
<UIRef Id="CraftInstallerUI" />
|
||||
</Package>
|
||||
|
||||
<Fragment>
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generated by craft-installer for designcraft. 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.designcraft" 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]designcraft.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.designcraft"
|
||||
Name="NoModify" Type="integer" Value="1" KeyPath="yes" />
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
</Fragment>
|
||||
|
||||
</Wix>
|
||||
@@ -93,7 +93,7 @@ Copy-Item (Join-Path $Bin 'designcraft.exe'), (Join-Path $Bin 'designcraft-cli.e
|
||||
# ---- MSI ---------------------------------------------------------------------------------------
|
||||
$Msi = Join-Path $Dist "designcraft-$Version-windows-$Arch.msi"
|
||||
Invoke-Native 'wix build' {
|
||||
wix build (Join-Path $PSScriptRoot 'designcraft.wxs') -arch $Arch `
|
||||
wix build (Join-Path $PSScriptRoot 'designcraft.wxs') (Join-Path $PSScriptRoot 'installer-ui.wxs') -arch $Arch `
|
||||
-d "Version=$MsiVersion" -d "BinDir=$Stage" -d "IconPath=$(Join-Path $Root 'assets\app-icon\designcraft.ico')" `
|
||||
-o $Msi
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user