Skip to content

Commit 8bcf7fe

Browse files
committed
Stabilize Windows MSI recovery packaging
1 parent 87ea70e commit 8bcf7fe

11 files changed

Lines changed: 424 additions & 37 deletions

File tree

.github/workflows/windows-packaging.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,13 @@ jobs:
3333
3434
build-msi:
3535
needs: resolve-version
36-
uses: danjboyd/gnustep-packager/.github/workflows/package-gnustep-app.yml@bca864ff163e129100881145e017429fed155bf7
36+
uses: danjboyd/gnustep-packager/.github/workflows/package-gnustep-app.yml@4fc362a68b3e55191942c01a92cf2f8da82031bb
3737
with:
3838
manifest-path: packaging/manifests/windows-msi.manifest.json
3939
backend: msi
4040
package-version: ${{ needs.resolve-version.outputs.package_version }}
4141
packager-repository: danjboyd/gnustep-packager
42-
packager-ref: bca864ff163e129100881145e017429fed155bf7
43-
msys2-packages: >-
44-
mingw-w64-clang-x86_64-cmark
42+
packager-ref: 4fc362a68b3e55191942c01a92cf2f8da82031bb
4543
run-validation: true
4644
run-smoke: true
4745
upload-artifacts: true

ObjcMarkdownViewer/OMDAppDelegate.m

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3532,14 +3532,8 @@ - (void)drawRect:(NSRect)dirtyRect
35323532

35333533
static void OMDAddPreferencesPopupOverlay(NSView *container, NSPopUpButton *popup)
35343534
{
3535-
if (container == nil || popup == nil) {
3536-
return;
3537-
}
3538-
OMDPreferencesPopupOverlayView *overlay = [[[OMDPreferencesPopupOverlayView alloc] initWithFrame:[popup frame]
3539-
popupButton:popup] autorelease];
3540-
[container addSubview:overlay
3541-
positioned:NSWindowAbove
3542-
relativeTo:popup];
3535+
(void)container;
3536+
(void)popup;
35433537
}
35443538

35453539
@interface OMDRoundedCardView : OMDFlippedFillView

ObjcMarkdownViewer/main.m

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,16 @@ static void OMDStartupTrace(NSString *message)
7171
{
7272
#if defined(_WIN32)
7373
NSString *bundleRoot = OMDWindowsInstallRoot();
74-
NSString *defaultsPath = [[bundleRoot stringByAppendingPathComponent:@"clang64"]
75-
stringByAppendingPathComponent:@"bin\\defaults.exe"];
76-
77-
if ([[NSFileManager defaultManager] isExecutableFileAtPath:defaultsPath]) {
78-
return defaultsPath;
74+
NSArray *relativePaths = [NSArray arrayWithObjects:
75+
@"runtime\\bin\\defaults.exe",
76+
@"clang64\\bin\\defaults.exe",
77+
nil];
78+
79+
for (NSString *relativePath in relativePaths) {
80+
NSString *defaultsPath = [bundleRoot stringByAppendingPathComponent:relativePath];
81+
if ([[NSFileManager defaultManager] isExecutableFileAtPath:defaultsPath]) {
82+
return defaultsPath;
83+
}
7984
}
8085
#endif
8186
return @"defaults.exe";
@@ -121,7 +126,7 @@ static BOOL OMDWindowsBundledExecutableExists(NSString *relativePath)
121126
{
122127
#if defined(_WIN32)
123128
NSString *bundleRoot = OMDWindowsInstallRoot();
124-
NSString *bundledThemesRoot = nil;
129+
NSMutableArray *bundledThemeRoots = [NSMutableArray array];
125130
NSString *userThemesRoot = [[[NSHomeDirectory() stringByAppendingPathComponent:@"GNUstep"]
126131
stringByAppendingPathComponent:@"Library"] stringByAppendingPathComponent:@"Themes"];
127132
NSString *systemThemesRoot = [[[@"C:\\clang64" stringByAppendingPathComponent:@"lib"]
@@ -130,12 +135,17 @@ static BOOL OMDWindowsBundledExecutableExists(NSString *relativePath)
130135
NSString *themeName = nil;
131136

132137
if (bundleRoot != nil && [bundleRoot length] > 0) {
133-
bundledThemesRoot = [[[[bundleRoot stringByAppendingPathComponent:@"clang64"]
138+
[bundledThemeRoots addObject:[[[[bundleRoot stringByAppendingPathComponent:@"runtime"]
139+
stringByAppendingPathComponent:@"lib"] stringByAppendingPathComponent:@"GNUstep"]
140+
stringByAppendingPathComponent:@"Themes"]];
141+
[bundledThemeRoots addObject:[[[[bundleRoot stringByAppendingPathComponent:@"clang64"]
134142
stringByAppendingPathComponent:@"lib"] stringByAppendingPathComponent:@"GNUstep"]
135-
stringByAppendingPathComponent:@"Themes"];
143+
stringByAppendingPathComponent:@"Themes"]];
136144
for (themeName in preferredThemes) {
137-
if (OMDWindowsThemeBundleExistsInDirectory(bundledThemesRoot, themeName)) {
138-
return themeName;
145+
for (NSString *bundledThemesRoot in bundledThemeRoots) {
146+
if (OMDWindowsThemeBundleExistsInDirectory(bundledThemesRoot, themeName)) {
147+
return themeName;
148+
}
139149
}
140150
}
141151
}

OpenIssues.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,27 @@
118118
- The remote Windows build reached the `ObjcMarkdown` compile and failed on missing `cmark.h`.
119119
- The immediate local fix was to update the ad hoc remote build script to install `mingw-w64-clang-x86_64-cmark` before invoking the packaging pipeline.
120120
- The long-term fix should be to declare `cmark` in the manifest under `hostDependencies.windows.msys2Packages` and then repin/use the updated `gnustep-packager` commit so host dependency realization happens systematically.
121+
- **Phase 10A-C Recovery Findings (2026-04-30)**:
122+
- Connected to the known-good libvirt Windows VM at `172.17.2.148` as `Administrator`; hostname `OTVM-WIN-05SPHS`.
123+
- Captured environment and payload evidence in `dist/phase10-msi/` and summarized it in [docs/windows-phase10-msi-recovery.md](/home/danboyd/git/ObjcMarkdown/docs/windows-phase10-msi-recovery.md).
124+
- The known-good VM has `ObjcMarkdown` at `87ea70ed2df2bac8c634b6b82471339ce5dc8e6d` and `plugins-themes-WinUITheme` at `48d21f0a2ae97ca70a03197d93a305144635517f`.
125+
- The known-good runtime is not an existing `dist/packaging/windows/stage`; it is a source-tree app build using `C:\\Users\\Administrator\\AppData\\Local\\gnustep-cli`.
126+
- A temporary imported-payload stage produced an MSI through `gnustep-packager`; clean Windows install succeeded, then validation failed because the imported payload did not include TinyTeX.
127+
- The normal source stage includes TinyTeX but differs materially from the imported known-good payload, especially project DLL placement and overall runtime size.
128+
- Normal source staging on the gnustep-cli runtime still logs `/etc/profile` and `/c/...` path warnings, confirming a `gnustep-cli-new` mount-convention gap.
129+
- **Phase 10D-G Recovery Work (2026-04-30)**:
130+
- [packaging/manifests/windows-msi.manifest.json](/home/danboyd/git/ObjcMarkdown/packaging/manifests/windows-msi.manifest.json) now declares `mingw-w64-clang-x86_64-cmark` under `hostDependencies.windows.msys2Packages`, declares required Windows theme inputs, sets packaged default theme `WinUITheme`, and ignores confirmed Windows system DLLs `DNSAPI.dll` and `IPHLPAPI.DLL` during MSI runtime closure checks.
131+
- [.github/workflows/windows-packaging.yml](/home/danboyd/git/ObjcMarkdown/.github/workflows/windows-packaging.yml) is repinned to `gnustep-packager` commit `4fc362a68b3e55191942c01a92cf2f8da82031bb`, which includes manifest-driven host dependency provisioning.
132+
- [scripts/windows/build-from-powershell.ps1](/home/danboyd/git/ObjcMarkdown/scripts/windows/build-from-powershell.ps1) and [packaging/scripts/ensure-windows-theme-inputs.ps1](/home/danboyd/git/ObjcMarkdown/packaging/scripts/ensure-windows-theme-inputs.ps1) now detect whether the managed MSYS runtime exposes Windows drives as `/c` or `/cygdrive/c` and only source `/etc/profile` when it exists.
133+
- The known-good VM validates the fixed build script against `C:\\Users\\Administrator\\AppData\\Local\\gnustep-cli`: `-Task command -Command pwd` resolves to `/cygdrive/c/Users/Administrator/git/ObjcMarkdown`, and `-Task stage -StageDir dist/phase10-source-stage-fixed/windows/stage` completes cleanly.
134+
- The temporary imported-payload MSI path is quarantined as evidence only under ignored `dist/phase10-msi/`; it is not a release path because it omits TinyTeX and imports the full mutable `gnustep-cli` runtime.
135+
- **Manual MSI Validation Findings (2026-04-30)**:
136+
- Candidate `dist/packaging/windows/packages/phase10-manual/ObjcMarkdown-0.1.1-rc2-win64.msi` installed and launched on clean validation VM `lease-20260430203645-pbs16d` at `172.17.2.177`.
137+
- The installed payload includes `WinUITheme.dll` under `C:\\Users\\Administrator\\AppData\\Local\\ObjcMarkdown\\runtime\\lib\\GNUstep\\Themes\\WinUITheme.theme`, but the app launched with the GNUstep theme active.
138+
- Runtime inspection showed `GSTheme=WinUITheme` in the app defaults domain, while `NSGlobalDomain` had no `GSTheme`; app startup was still searching the old `clang64` packaged layout instead of the MSI's `runtime` layout for `defaults.exe` and bundled themes.
139+
- Local fix: [ObjcMarkdownViewer/main.m](/home/danboyd/git/ObjcMarkdown/ObjcMarkdownViewer/main.m) now searches `runtime\\bin\\defaults.exe` before `clang64\\bin\\defaults.exe` and checks `runtime\\lib\\GNUstep\\Themes` before the legacy packaged theme path.
140+
- Preferences validation found that clicking the `GNUstep Theme` popup opened the neighboring `Layout Mode` popup; Linux validation also showed broken hover tracking because Preferences used a custom popup overlay and manually drawn popup panel instead of native `NSPopUpButton` menu tracking.
141+
- Local fix: [ObjcMarkdownViewer/OMDAppDelegate.m](/home/danboyd/git/ObjcMarkdown/ObjcMarkdownViewer/OMDAppDelegate.m) now disables the custom Preferences popup overlay path so the theme and layout controls use native `NSPopUpButton` popup behavior.
121142
- **Next Step**:
122-
- Update `ObjcMarkdown`’s Windows packaging manifest to declare `mingw-w64-clang-x86_64-cmark` under `hostDependencies.windows.msys2Packages`.
123-
- Repin the repo to the new `gnustep-packager` commit that includes host dependency provisioning.
124-
- Rebuild the MSI through the normal packaging path instead of the increasingly patched ad hoc OCI remote bundle.
125-
- Once a fresh MSI is produced, push the `.msi` and portable `.zip` to a Windows validation VM and re-run manual/UAT verification.
143+
- Rebuild the MSI through the normal source-built packaging path with the app-side runtime-layout and native-popup fixes.
144+
- Once a fresh MSI is produced, push the `.msi` and portable `.zip` to a clean Windows validation VM and re-run manual/UAT verification.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ Release flow:
167167
- Ensure the target commit has already passed the separate Linux CI workflow if you want a GNUstep/Linux gate before release tagging.
168168
- Push an annotated tag like `v0.1.0`.
169169
- GitHub Actions runs `linux-appimage` as a thin caller to the reusable `gnustep-packager` workflow pinned to `bca864ff163e129100881145e017429fed155bf7`, using this repo's Linux manifest, stage script, and self-hosted GNUstep preflight.
170-
- GitHub Actions runs `windows-packaging` as a thin caller to the same pinned reusable workflow, using this repo's Windows MSI manifest and normalized Windows stage script. The staged Windows payload includes the GNUstep runtime, bundled Windows themes, and TinyTeX runtime for external LaTeX rendering. Windows releases are expected to bundle `WinUITheme` and use it as the default packaged theme.
170+
- GitHub Actions runs `windows-packaging` as a thin caller to the reusable `gnustep-packager` workflow pinned to `4fc362a68b3e55191942c01a92cf2f8da82031bb`, using this repo's Windows MSI manifest and normalized Windows stage script. The Windows manifest owns app-specific host dependencies such as `mingw-w64-clang-x86_64-cmark`. The staged Windows payload includes the GNUstep runtime, bundled Windows themes, and TinyTeX runtime for external LaTeX rendering. Windows releases are expected to bundle `WinUITheme` and use it as the default packaged theme.
171171
- Each tagged packaging workflow then downloads its `-packages` artifact and attaches the release files to the matching GitHub Release page. Linux publishes the `.AppImage` and `.zsync`; Windows publishes the `.msi` and portable ZIP, along with generated sidecars such as `.update-feed.json`.
172172
- Clean-machine Windows validation is documented in [docs/windows-otvm-msi-validation.md](docs/windows-otvm-msi-validation.md). Going forward, the supported Debian and Windows VM path is libvirt-backed `OracleTestVMs` leases. The older direct-OCI helper has been retired; [docs/windows-oci-msi-validation.md](docs/windows-oci-msi-validation.md) is kept only as a retirement note.
173173

Roadmap.md

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ Scope:
143143
- configure the Windows caller to use:
144144
- `windows-latest`
145145
- the packager MSI baseline
146-
- `msys2-packages` for app-specific dependencies such as `mingw-w64-clang-x86_64-cmark`
146+
- manifest-declared host dependencies for app-specific packages such as `mingw-w64-clang-x86_64-cmark`
147147
- keep CI validation enabled for both backends and preserve any extra app-specific smoke we still need beyond the shared packager validation
148148

149149
### Phase 8E: External-Only Backend Packaging Cleanup
@@ -245,6 +245,108 @@ Acceptance criteria:
245245
- `Phase 9D`: separate Windows build and clean-test VMs can be launched from `OracleTestVMs`, the clean-test VM contains sample Markdown files, and the MSI can be validated there without relying on build-machine state
246246
- `Phase 9E`: the operator can follow repo-documented instructions to connect to the Linux VM and the Windows test VM and perform manual validation of AppImage and MSI behavior
247247

248+
## Phase 10: Golden Windows MSI Reproduction Through gnustep-packager
249+
250+
Goal:
251+
- use the known-good Windows build VM as a controlled reference point so `gnustep-packager` and `gnustep-cli-new` can reproduce a working MSI without relying on session memory, mutable machine state, or ad hoc repair scripts
252+
253+
### Phase 10A: Snapshot The Known-Good Windows Build VM
254+
255+
Scope:
256+
- connect to the known-good Windows VM through `OracleTestVMs`/libvirt SSH and record its exact build identity:
257+
- `ObjcMarkdown` commit
258+
- `plugins-themes-winuitheme` commit
259+
- `plugins-themes-win11theme` commit
260+
- `gnustep-packager` commit if present
261+
- export the Windows build environment facts needed for reproduction:
262+
- MSYS2 package list
263+
- `C:\\msys64\\clang64` GNUstep layout
264+
- `GNUSTEP_*`, `PATH`, `MSYSTEM`, and `MSYS2_LOCATION`
265+
- successful build, stage, and install/runtime tree locations
266+
- save the result as a committed diagnostic note or generated evidence bundle referenced from repo documentation
267+
268+
### Phase 10B: Prove MSI Generation From The Known-Good Payload
269+
270+
Scope:
271+
- add a temporary, explicitly marked payload-import workflow for recovery testing only
272+
- copy the VM's known-good app/runtime/theme payload into `dist/packaging/windows/stage`
273+
- run only the `gnustep-packager` MSI backend against that imported stage
274+
- validate the resulting MSI on a clean `OracleTestVMs` Windows VM
275+
- use the result to separate installer-generation defects from source-build/staging defects
276+
277+
### Phase 10C: Diff Known-Good, Staged, And Installed Payloads
278+
279+
Scope:
280+
- generate relative path, size, hash, and DLL dependency reports for:
281+
- the known-good VM payload
282+
- `packaging/scripts/stage-windows.ps1` output
283+
- the tree installed by the MSI
284+
- identify meaningful differences rather than accepting broad directory drift
285+
- make validation fail for missing or incorrect release-critical content:
286+
- `WinUITheme.theme/WinUITheme.dll`
287+
- packaged default `GSTheme=WinUITheme`
288+
- `cmark` runtime dependency coverage
289+
- GNUstep backend bundle
290+
- TinyTeX binaries
291+
- unresolved non-system DLLs
292+
293+
### Phase 10D: Move VM Facts Into Declarative Packager Inputs
294+
295+
Scope:
296+
- declare app-specific Windows host dependencies directly in `packaging/manifests/windows-msi.manifest.json`
297+
- keep `mingw-w64-clang-x86_64-cmark` in the manifest rather than relying only on workflow inputs
298+
- add any additional MSYS2 packages proven necessary by the known-good VM snapshot
299+
- keep Windows theme requirements explicit through packaging inputs and validation rather than hand-prepared machine state
300+
301+
### Phase 10E: Repin To The Required gnustep-packager Baseline
302+
303+
Scope:
304+
- repin the Windows packaging workflow from the older reusable-workflow commit to the `gnustep-packager` commit that contains:
305+
- host dependency provisioning
306+
- first-class GNUstep theme input handling and validation
307+
- packaged default-theme support
308+
- Windows executable smoke validation
309+
- update packaging docs and release notes with the exact packager commit used for the next MSI recovery build
310+
- verify the repinned workflow still produces the expected MSI, portable ZIP, update feed, metadata, and diagnostics artifacts
311+
312+
### Phase 10F: Harden gnustep-cli-new Against The Golden VM
313+
314+
Scope:
315+
- treat the known-good VM's `C:\\msys64` state as the acceptance target for `gnustep-cli-new`
316+
- make `gnustep-cli-new` install or verify the same MSYS2/GNUstep prerequisites required by this app
317+
- add or update a `doctor`/probe output that reports the key differences between a fresh toolchain and the known-good build VM
318+
- remove recovery-only bootstrap steps once `gnustep-cli-new` can create an equivalent packaging-capable environment
319+
320+
### Phase 10G: Retire The Temporary Payload Import Path
321+
322+
Scope:
323+
- remove or quarantine the payload-import workflow after source-built staging matches the known-good payload and clean-machine MSI validation passes
324+
- keep the payload comparison and environment probe scripts as regression tools
325+
- document the final normal path as:
326+
- `gnustep-cli-new` provisions or verifies the toolchain
327+
- repo scripts build and stage the app
328+
- `gnustep-packager` creates the MSI and portable ZIP
329+
- `OracleTestVMs` validates the installer on a clean Windows VM
330+
331+
Immediate next steps:
332+
- `Phase 10A`: completed on `2026-04-30` by exporting known-good VM evidence from `Administrator@172.17.2.148` on `OTVM-WIN-05SPHS`
333+
- `Phase 10B`: completed on `2026-04-30` enough to prove the MSI backend can package the imported known-good payload; clean install succeeded, but release validation failed because the imported payload lacks TinyTeX
334+
- `Phase 10C`: completed on `2026-04-30` with a payload comparison between the imported known-good stage, normal source stage, and MSI-installed tree
335+
- `Phase 10D`: completed on `2026-04-30` by moving `mingw-w64-clang-x86_64-cmark`, Windows theme inputs, packaged default theme, and confirmed system-DLL ignores into the Windows manifest
336+
- `Phase 10E`: completed on `2026-04-30` by repinning `.github/workflows/windows-packaging.yml` to `gnustep-packager` commit `4fc362a68b3e55191942c01a92cf2f8da82031bb`
337+
- `Phase 10F`: completed on `2026-04-30` for this repo's Windows build/theme scripts by resolving the active MSYS drive mount convention and making `/etc/profile` optional under `gnustep-cli-new`
338+
- `Phase 10G`: import-lane retirement started on `2026-04-30`; the imported MSI is retained only as ignored evidence under `dist/phase10-msi/`, and the release path is the normal source-built stage plus `gnustep-packager` MSI and clean `OracleTestVMs` validation
339+
- next verification gate: rebuild the MSI through the normal source-built path and validate that release artifact on a clean Windows VM
340+
341+
Acceptance criteria:
342+
- `Phase 10A`: the known-good VM's build inputs, package inventory, environment, and payload locations are captured in a durable repo-referenced artifact
343+
- `Phase 10B`: `gnustep-packager` can create an MSI from the imported known-good payload and that MSI passes clean Windows validation
344+
- `Phase 10C`: validation reports clearly explain payload differences between the known-good VM, source-built stage, and MSI-installed tree
345+
- `Phase 10D`: app-specific Windows host dependencies and theme requirements are declared in manifests or packaging inputs, not hidden in VM state
346+
- `Phase 10E`: the Windows packaging workflow is pinned to a `gnustep-packager` baseline that includes the required MSI recovery features
347+
- `Phase 10F`: `gnustep-cli-new` can provision or verify a fresh Windows build environment equivalent to the known-good VM for this app's packaging needs
348+
- `Phase 10G`: the normal source-built `gnustep-packager` MSI path passes clean-machine validation without the temporary payload-import workflow
349+
248350
## Deferred Work
249351

250352
These are interesting, but they are not the current release gate:

0 commit comments

Comments
 (0)