Skip to content

Modernize the Windows MSI: WiX v5, x64, service registration, upgrade from 5.1.1#701

Open
vharseko wants to merge 25 commits into
OpenIdentityPlatform:masterfrom
vharseko:features/windows-distribution-new
Open

Modernize the Windows MSI: WiX v5, x64, service registration, upgrade from 5.1.1#701
vharseko wants to merge 25 commits into
OpenIdentityPlatform:masterfrom
vharseko:features/windows-distribution-new

Conversation

@vharseko

@vharseko vharseko commented Jul 4, 2026

Copy link
Copy Markdown
Member

Modernizes the Windows MSI end to end. Builds on #664 (branched from it) and includes the Windows script quoting fixes from #671 (they merge cleanly once #671 lands).

Toolchain: WiX 3.11 + wine → WiX v5, Windows-native

  • WiX 3.11.1 (2017, EOL) → WiX v5.0.2 (.NET wix tool): heat/candle/light + wine + winetricks + dotnet40 collapse into a single wix build -arch x64. WiX v5 is the newest version without the Open Source Maintenance Fee (v7 refuses to run without accepting the OSMF EULA).
  • The MSI is built on Windows only. Verified empirically (wix 5.0.2/6.0.1/7.0.0 on macOS): non-Windows builds fail on WIX0389 for any Directory/@Name and, with no names at all, on the missing msi.dll — the MSI database is written by the Windows Installer library, so no cross-platform build exists. wine is removed from build.yml, deploy.yml and release.yml.
  • package.wxs rewritten to the v4+ schema: single <Package>, MediaTemplate CompressionLevel="high", InstallerVersion=500, <Files> harvest from Ant-staged payloads, empty instance directories shipped via CreateFolder components (the harvest ships files only).

Installer behavior

  • x64: installs into C:\Program Files\OpenDJ (was x86 / Program Files (x86)).
  • The MSI registers the OpenDJ Server Windows service (ServiceInstall on opendj_service.exe, not started during install — run setup first). A service registered by the legacy windows-service.bat is replaced automatically (its display name collides with the new one).
  • Upgrade over an existing install: the installer detects the location recorded in the registry by this package, or the legacy x86 default directory, and installs there — instance data (config, db, logs) is preserved; an explicit OPENDJ=... always wins. Verified in CI by upgrading from the released 5.1.1 MSI: "OpenDJ was successfully upgraded from 5.1.1 to 5.1.2".
  • A JRE (11+) is required at install: launch condition on %JAVA_HOME%\bin\java.exe or java-looking PATH entries, with a message pointing at https://adoptium.net. The MSI is not code-signed (no certificate) — SmartScreen note added to the docs.

Release / deploy

  • deploy.yml reuses the MSI already built by the triggering Build run (no rebuild).
  • release.yml: new release-msi job (windows, continue-on-error) installs the released server zip into the local repository and packages only :opendj-msi-standard, then attaches the MSI to the GitHub release.

CI

  • test-msi: asserts the installer fails without a JRE, then installs into the spaced x64 default directory, runs setup, starts/stops the MSI-registered service, ldapsearch, uninstalls.
  • test-msi-upgrade: released 5.1.1 x86 MSI → configure → legacy service → upgrade with the new x64 MSI: same directory kept, data intact, service replaced (sc getkeyname "OpenDJ Server"OpenDJ), upgrade.bat, start, search; plus a fresh-install scenario asserting the legacy default directory is auto-detected.

Windows script fixes surfaced by these tests (in #671)

Install paths with spaces/parentheses (C:\Program Files (x86)\OpenDJ — the old default!) broke the batch scripts in four places: unquoted java.io.tmpdir, setcp.bat argument parsing, "%VAR%" == "" checks with the now-quoted tmpdir, and the start-ds.bat tmp-cleanup block. All fixed and exercised by the MSI tests in this PR.

Deferred

  • Authenticode signing — no certificate.
  • A Burn bundle auto-installing the JRE — dropped in favor of the launch-condition message with the download link.

CI on the branch is still validating the two latest fixes (empty directories, service ImagePath).

vharseko added 22 commits June 26, 2026 20:37
The opendj-msi package was built and uploaded but never installed or
exercised in CI. Add a test-msi job (needs: build-maven) that, on a
windows-latest runner, installs the built .msi silently (msiexec /i),
runs setup, registers and starts/stops the OpenDJ Windows service with
an ldapsearch liveness check, then uninstalls (msiexec /x).
…E 25)

The MSI ships no JRE, so add a WiX launch condition that fails the install
early with a clear message when Java is not detected (it does not install
Java). JAVA_HOME is captured from the environment before LaunchConditions;
`Installed` keeps uninstall/repair working regardless of Java.

Fix the test-msi CI job (it failed with `net start` exit 2 "service already
started"):
- setup.bat is now invoked with --doNotStart, so the server is started only by
  `net start "OpenDJ Server"` (setup.bat no longer starts a standalone instance
  first).
- Bump actions/setup-java from 21 to 25 (latest LTS; smoke-tests the MSI under a
  fresh JRE). The runner has JAVA_HOME from setup-java, so the new launch
  condition is satisfied and the install proceeds.
The launch condition (Installed OR JAVA_HOME_ENV) false-blocked valid installs: a JRE does not always set JAVA_HOME (it may be only on PATH). Drop it - the MSI again only copies files and Java availability stays the admin's responsibility. The test-msi fix (--doNotStart, JRE 25) is unaffected.
The install guide covered only the .zip and native .deb/.rpm. Add Windows MSI sections to chap-install/chap-upgrade/chap-uninstall: GUI and silent msiexec install, Java as a runtime prerequisite the installer does not enforce, configure via setup.bat, optional Windows service registration via windows-service.bat, MSI upgrade (disable service, back up, install newer .msi, upgrade.bat, re-enable), and uninstall via Apps & features / msiexec /x.
Migrate the Windows MSI off the EOL WiX 3.11.1 (heat/candle/light run under
wine + winetricks dotnet40) to the cross-platform WiX v5 .NET tool, and
address the MSI validation findings:

- package.wxs rewritten to the WiX v4+ schema: single <Package>, x64
  (ProgramFiles64Folder), MediaTemplate CompressionLevel="high",
  InstallerVersion=500, <Files> directory harvest (replaces heat), and a
  <ServiceInstall>/<ServiceControl> on opendj_service.exe so the MSI itself
  registers the "OpenDJ Server" Windows service (Start=auto, not started during
  install - setup must configure the instance first). UpgradeCode preserved.
- opendj-msi-standard/pom.xml builds the MSI with `wix build -arch x64`; removed
  the external-dependency-plugin (wix3111 + winetricks), heat/candle/light and
  the wine/winetricks antrun steps.
- opendj-msi/pom.xml: wine-path profiles replaced by an unconditional module so
  the MSI builds natively on every OS where opendj-msi is included.
- build.yml: drop wine (keep rpm); add a "Setup WiX (.NET tool)" step; test-msi
  no longer calls windows-service.bat --enableService (the MSI registers it).
- install guide: x64 install path; the MSI registers the service.

Signing (OpenIdentityPlatform#4) deferred (no certificate). The Burn bundle that auto-installs a JRE
is a follow-up, to be added after this MSI build is validated in CI.
WiX 5 ships as a net6.0 .NET tool, but the runners only have a newer .NET runtime (no .NET 6), so 'wix' failed to launch (exit 131, missing_runtime) on arm64 macOS. Set DOTNET_ROLL_FORWARD=Major in the Setup WiX step (exported and written to GITHUB_ENV) so the tool rolls forward to the installed runtime, both for that step and the Maven build that invokes 'wix build'.
wix build failed with WIX0004 ('Files element contains an unexpected attribute Exclude'): in WiX 5.0.2 exclusion is a child <Exclude Path=.../> element (with <Include Path=.../>), not an inline attribute. Convert both <Files> harvests to the child-element form.

macOS (arm64) runners have no .NET runtime for the net6.0 wix apphost (DOTNET_ROOT unset, exit 131); add actions/setup-dotnet (8.0.x) on macOS so the tool finds a runtime and rolls forward.
…only Include)

WiX 5.0.2 requires Include as an attribute on <Files> and rejects an Exclude attribute as well as <Include>/<Exclude Path> child elements (WIX0004/0005/0010). Move all exclusions to the Ant staging step: stagingRoot = payload minus lib (and macOS/Unix bits), stagingLib = lib minus opendj_service.exe (registered via an explicit component sourced from the package dir). Each <Files> now uses only the Include attribute, and there is no duplicate lib directory.
On Linux/macOS the wix tool treats backslash as a literal, not a path separator, which may cascade into the WIX0389 'not a relative path' errors. Use '/**' (accepted on Windows too) to test whether the cross-platform wix build then succeeds.
The WiX Toolset can create MSI databases on Windows only: it warns "only
supports Windows" and, on Linux/macOS, fails with WIX0389 on every Directory
name and (with no name) on a missing msi.dll (the Windows Installer library).
Verified locally on macOS with WiX 5.0.2, 6.0.1 and 7.0.0 - none can build.

- opendj-packages/pom.xml: build opendj-msi only in the distribution-windows
  profile (removed from distribution-unix and distribution-mac).
- build.yml: run the WiX setup step only on Windows; drop macOS setup-dotnet.
- package.wxs: keep the Windows-proven backslash glob in <Files Include>.
- .gitattributes: force LF for *.wxs/*.wxi.
The MSI can only be built on Windows. Instead of rebuilding opendj-server-legacy
on a Windows runner:

- deploy.yml: reuse the MSI already built by the triggering Build run (download
  the windows-latest-11 artifact, re-upload as "OpenDJ MSI Package"); both steps
  continue-on-error; drop wine (rpm only).
- release.yml: release-maven uploads the released server zip as an artifact;
  new release-msi job (windows, continue-on-error so an MSI failure does not
  break the release) installs the zip into the local Maven repository and only
  packages :opendj-msi-standard (no -am), then attaches the MSI to the GitHub
  release; drop the .msi from the ubuntu release file list; drop wine.
…e legacy service

- package.wxs: refuse to install when no Java is detectable (JAVA_HOME\bin\java.exe
  or java-looking PATH entries; MSI cannot scan PATH, so substring heuristic) with
  a message pointing at https://adoptium.net; detect an existing install (InstallDir
  registry value written by this package, else the legacy x86 default directory)
  and apply it only when OPENDJ is not set explicitly; drop the legacy
  windows-service.bat "OpenDJ Server" service before InstallServices (its display
  name collides with the MSI ServiceInstall).
- build.yml: test-msi asserts the installer fails without a JRE before one is set
  up; new test-win-upgrade installs the released 5.1.1 x86 MSI, configures an
  instance with the legacy service, upgrades with the newly built x64 MSI without
  OPENDJ and asserts same directory, intact data, MSI-managed service, then runs
  upgrade.bat, starts the service and searches the pre-upgrade data.
- docs: Java is required by the installer (adoptium.net link); MSI upgrade
  autodetects the directory and replaces the service; uninstall removes the
  service automatically.
- _script-util.bat: quote -Djava.io.tmpdir="%OPENDJ_TMP_DIR%" - any install
  directory containing spaces (including the x64 default C:\Program Files\OpenDJ
  and the legacy Program Files (x86)) broke the CheckJVMVersion probe and every
  script with "The detected Java version could not be used".
- build.yml test-msi: the no-JRE negative test now hides Java at MACHINE scope
  (the Windows Installer service evaluates launch conditions with the machine
  environment) and restarts msiserver, restoring everything afterwards; the
  positive install now goes to the spaced x64 default directory end-to-end.
- build.yml test-msi-upgrade (renamed from test-win-upgrade): the released 5.1.1
  scripts cannot run from a spaced directory, so the old install lives in
  C:\opendj and the upgrade passes OPENDJ explicitly; added a fresh-install
  scenario asserting the legacy default directory is auto-detected when OPENDJ
  is not given, plus uninstall/service cleanup checks.
- ADNotificationRequestControl: escape && in the javadoc code sample
  (javadoc "invalid input: '&'" warnings).
…aces)

_script-util.bat appends -Djava.io.tmpdir=%OPENDJ_TMP_DIR% to OPENDJ_JAVA_ARGS
without quotes, so in any install directory containing spaces (for example
C:\Program Files (x86)\OpenDJ) the CheckJVMVersion probe - and with it setup
and every other command-line tool - fails with "The detected Java version
could not be used with the set of Java arguments".

Quote the value: -Djava.io.tmpdir="%OPENDJ_TMP_DIR%".
…cp.bat)

_script-util.bat passed unquoted paths to setcp.bat, and setcp.bat compared
arguments with if ""%1""=="""". The argument-joining hack survived spaces, but
a parenthesis in the path (C:\Program Files (x86)\OpenDJ - the default MSI
directory) breaks the cmd parser with "... was unexpected at this time", so
setup.bat and every tool exit with 255.

Quote the setcp.bat arguments at the three call sites and switch setcp.bat to
%~1 with quoted comparisons.
…e "%VAR%" == "" comparisons

After -Djava.io.tmpdir="%OPENDJ_TMP_DIR%" is appended, OPENDJ_JAVA_ARGS
contains embedded quotes, and the subsequent if "%OPENDJ_JAVA_ARGS%" == ""
checks blow up the cmd parser ('...\tmp"" was unexpected at this time', every
tool exits 255) regardless of whether the install path has spaces. Compare
with "if defined", which does not expand the value.
…h quoting

- The <Files> harvest ships files only (heat had -ke), so the empty instance
  directories (bak, changelogDb, classes, db, import-tmp, ldif, locks, logs,
  tmp and the template ones) were missing from the installed tree and the
  server could not create its lock/pid files - setup aborted with "error
  stopping server". Create them with explicit CreateFolder components.
- [OPENDJ] ends with a backslash which escaped the closing quote in the
  service ImagePath ('start "C:\opendj\"' -> broken argv), so the MSI-managed
  service failed to start (NET HELPMSG 2186). Append an extra backslash so
  \\" parses as backslash + closing quote.
Components whose KeyPath is a directory cannot use auto-generated GUIDs, so the empty-instance-directory components broke wix build with WIX0230. Assign fixed GUIDs.
The tmp-cleanup block expands %OPENDJ_TMP_DIR% unquoted inside a ( ) compound statement, so a parenthesis in the install path - C:\Program Files (x86)\OpenDJ - terminates the block at parse time and start-ds fails; the Windows service then dies with a service-specific error -1 while setup (which never runs this block) succeeds. Quote the three path expansions.
opendj_service.exe located its own service entry by comparing the raw
SCM ImagePath with the exact string it builds itself, so a service
registered by the MSI (exe unquoted, instance dir with a trailing
backslash) was never found and "net start" failed with error 2186.
getServiceName now compares the executable path, the subcommand and the
normalized instance dir token by token instead. The MSI ServiceInstall
argument becomes 'start "[OPENDJ]."' and the sc-config workaround is
not needed.
@vharseko vharseko added the packaging deb/rpm/MSI, distribution layout, config.ldif label Jul 6, 2026
@vharseko vharseko added the CI label Jul 6, 2026
vharseko added 2 commits July 6, 2026 18:55
Resolve build.yml conflict: keep master's build-docker-alpine benchmark
steps and append the test-msi and test-msi-upgrade CI jobs. The build-maven
matrix and WiX setup step merged automatically.
Replace opendj_service.exe, winlauncher.exe and launcher_administrator.exe
with the CI-built artifacts from the fixed service.c (tolerant ImagePath
matching, commit 262f98f). Verified by green test-msi and
test-msi-upgrade — the Windows service now starts (no more error 2186).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build CI enhancement packaging deb/rpm/MSI, distribution layout, config.ldif Windows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants