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:
aiongg authored and GitHub committed 2026-10-15 23:51:07 -04:00
commit a7f3239cfb
12 files changed
+358 -8

No files matched your search

Generated
+1
View File
@@ -997,6 +997,7 @@ dependencies = [
"log",
"quick-xml",
"regex",
"same-file",
"serde",
"serde_json",
"tempfile",
+1
View File
@@ -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"
+7
View File
@@ -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.
+3
View File
@@ -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 }
+2 -2
View File
@@ -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);
+68 -3
View File
@@ -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]
+1 -1
View File
@@ -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));
+5
View File
@@ -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.
+34
View File
@@ -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.
+6 -1
View File
@@ -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>
+229
View File
@@ -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="&amp;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="&amp;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&amp;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="&amp;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="&amp;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 = &quot;ALL&quot; OR CraftInstallerOperation = &quot;remove&quot;" />
<Control Id="RemovedTitle" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\CraftInstallerTitle}Uninstall completed successfully" Hidden="yes" ShowCondition="REMOVE = &quot;ALL&quot; OR CraftInstallerOperation = &quot;remove&quot;" />
<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 = &quot;ALL&quot; OR REINSTALL OR CraftInstallerOperation OR ACTION = &quot;ADMIN&quot;" />
<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 = &quot;ALL&quot; OR CraftInstallerOperation = &quot;remove&quot;" />
<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 = &quot;repair&quot;) AND NOT REMOVE AND CraftInstallerOperation &lt;&gt; &quot;remove&quot; AND ACTION &lt;&gt; &quot;ADMIN&quot;" />
<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 = &quot;ADMIN&quot;" />
<Control Id="Finish" Type="PushButton" X="280" Y="235" Width="70" Height="23" Default="yes" Cancel="yes" Text="&amp;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="&amp;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="&amp;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="&quot;[INSTALLFOLDER]designcraft.exe&quot;,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="&quot;[$(var.CraftMsiSystemFolder)]msiexec.exe&quot; /x [ProductCode] /qf /norestart" />
<RegistryValue Name="QuietUninstallString" Type="string" Value="&quot;[$(var.CraftMsiSystemFolder)]msiexec.exe&quot; /x [ProductCode] /qn /norestart" />
<RegistryValue Name="ModifyPath" Type="string" Value="&quot;[$(var.CraftMsiSystemFolder)]msiexec.exe&quot; /i [ProductCode] /qf /norestart" />
</RegistryKey>
</Component>
<!-- Preserve the app-owned ARPNOMODIFY policy instead of inventing a new default. -->
<Component Id="CraftInstallerSettingsNoModify" Guid="*" Condition="ARPNOMODIFY = &quot;1&quot;">
<RegistryValue Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\Craft.designcraft"
Name="NoModify" Type="integer" Value="1" KeyPath="yes" />
</Component>
</ComponentGroup>
</Fragment>
</Wix>
+1 -1
View File
@@ -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
}