diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000..2676ab14f7
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,3 @@
+# WiX sources must stay LF so the WiX toolset parses them consistently across runners.
+*.wxs text eol=lf
+*.wxi text eol=lf
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 041c6b6ebf..db6499f4b9 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -35,19 +35,12 @@ jobs:
- { os: 'windows-latest', java: '26' }
fail-fast: false
steps:
- - name: Install wine+rpm for distribution
+ - name: Install rpm for distribution
if: runner.os == 'Linux'
shell: bash
run: |
- sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list
- sudo dpkg --add-architecture i386
- sudo mkdir -pm755 /etc/apt/keyrings && sudo wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key
- sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/$(lsb_release -c -s)/winehq-$(lsb_release -c -s).sources
sudo apt-get update
- sudo apt install --install-recommends winehq-stable || sudo apt install --install-recommends winehq-staging
- wine --version
- version="9.4.0"; sudo wget "https://dl.winehq.org/wine/wine-mono/$version/wine-mono-$version-x86.msi" -O /tmp/wine-mono.msi
- wine msiexec /i /tmp/wine-mono.msi
+ sudo apt-get install -y rpm
- uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -86,6 +79,19 @@ jobs:
run: |
echo "MAVEN_PROFILE_FLAG=-P precommit" >> $GITHUB_OUTPUT
+ - name: Setup WiX (.NET tool) for MSI
+ if: runner.os == 'Windows'
+ shell: bash
+ run: |
+ # The MSI builds on Windows only (WiX cannot author MSIs on Linux/macOS). WiX 5 ships as a
+ # net6.0 tool; allow it to run on the newer .NET runtime present on the runner.
+ echo "DOTNET_ROLL_FORWARD=Major" >> "$GITHUB_ENV"
+ export DOTNET_ROLL_FORWARD=Major
+ dotnet tool install --global wix --version 5.0.2 || dotnet tool update --global wix --version 5.0.2
+ echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH"
+ export PATH="$HOME/.dotnet/tools:$PATH"
+ wix --version
+ wix extension add -g WixToolset.UI.wixext/5.0.2 || true
- name: Build with Maven
timeout-minutes: 180
env:
@@ -506,3 +512,169 @@ jobs:
*.jmeter.out
if-no-files-found: warn
retention-days: 90
+
+ test-msi:
+ needs: build-maven
+ runs-on: 'windows-latest'
+ steps:
+ - name: Download artifacts
+ uses: actions/download-artifact@v8
+ with:
+ name: windows-latest-11
+ - name: Install must fail without a JRE
+ shell: pwsh
+ run: |
+ # Simulate a machine with no Java. The launch condition is evaluated by the Windows
+ # Installer service, which sees the MACHINE environment - so hide Java there too and
+ # restart msiserver to make it pick the change up; restore everything afterwards.
+ Stop-Service msiserver -Force -ErrorAction SilentlyContinue
+ $oldJH = [Environment]::GetEnvironmentVariable('JAVA_HOME','Machine')
+ $oldPM = [Environment]::GetEnvironmentVariable('Path','Machine')
+ [Environment]::SetEnvironmentVariable('JAVA_HOME',$null,'Machine')
+ [Environment]::SetEnvironmentVariable('Path',((($oldPM -split ';') | Where-Object { $_ -notmatch 'java|jdk|jre|zulu|javapath' }) -join ';'),'Machine')
+ $env:JAVA_HOME = ""
+ $env:PATH = (($env:PATH -split ';') | Where-Object { $_ -notmatch 'java|jdk|jre|zulu|javapath' }) -join ';'
+ try {
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ if (-not $msi) { throw "MSI not found in the windows-latest-11 artifact" }
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart /l*v install-nojre.log"
+ if ($p.ExitCode -eq 0) {
+ Get-Content install-nojre.log -Tail 60
+ throw "installer succeeded without a JRE, but the Java launch condition must fail it"
+ }
+ Write-Host "Installer refused to install without a JRE as expected (exit $($p.ExitCode))"
+ Select-String -Path install-nojre.log -Pattern "requires Java" | Select-Object -First 1
+ } finally {
+ [Environment]::SetEnvironmentVariable('JAVA_HOME',$oldJH,'Machine')
+ [Environment]::SetEnvironmentVariable('Path',$oldPM,'Machine')
+ Stop-Service msiserver -Force -ErrorAction SilentlyContinue
+ }
+ - name: Set up Java
+ uses: actions/setup-java@v5
+ with:
+ java-version: '25'
+ distribution: 'zulu'
+ - name: Install MSI (silent)
+ shell: pwsh
+ run: |
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ if (-not $msi) { throw "MSI not found in the windows-latest-11 artifact" }
+ Write-Host "MSI: $msi"
+ # No OPENDJ property: exercise the x64 default C:\Program Files\OpenDJ (a path with
+ # spaces, which the server scripts must handle).
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart /l*v install.log"
+ if ($p.ExitCode -ne 0) { Get-Content install.log -Tail 80; throw "msiexec /i failed: $($p.ExitCode)" }
+ $root = "C:\Program Files\OpenDJ"
+ if (-not (Test-Path "$root\setup.bat")) { Get-Content install.log -Tail 80; throw "OpenDJ not installed into the x64 default $root" }
+ Write-Host "Installed to $root"
+ "OPENDJ_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Append
+ - name: Setup and start/stop the Windows service
+ shell: pwsh
+ run: |
+ $root = $env:OPENDJ_ROOT
+ $env:OPENDJ_JAVA_ARGS = "-server -Xmx512m"
+ & "$root\setup.bat" -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com --addBaseEntry --cli --acceptLicense --no-prompt --doNotStart
+ if ($LASTEXITCODE -ne 0) { throw "setup.bat failed: $LASTEXITCODE" }
+ # The service is already registered by the MSI (WiX ServiceInstall); just start it.
+ net start "OpenDJ Server"
+ if ($LASTEXITCODE -ne 0) { throw "net start failed: $LASTEXITCODE" }
+ for ($i=0; $i -lt 12; $i++) { try { $c = New-Object System.Net.Sockets.TcpClient('localhost', 1636); $c.Close(); break } catch { Start-Sleep -Seconds 5 } }
+ & "$root\bat\ldapsearch.bat" --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1
+ if ($LASTEXITCODE -ne 0) { throw "ldapsearch failed: $LASTEXITCODE" }
+ net stop "OpenDJ Server"
+ if ($LASTEXITCODE -ne 0) { throw "net stop failed: $LASTEXITCODE" }
+ - name: Uninstall MSI
+ shell: pwsh
+ run: |
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x `"$msi`" /quiet /qn /norestart /l*v uninstall.log"
+ if ($p.ExitCode -ne 0) { Get-Content uninstall.log -Tail 80; throw "msiexec /x failed: $($p.ExitCode)" }
+ Write-Host "Uninstalled OK"
+
+ # Upgrade path: released 5.1.1 x86 MSI (wine-built, WiX3) -> this build's x64 MSI.
+ # Verifies the new installer detects the legacy Program Files (x86) install, keeps the
+ # instance data in place, replaces the windows-service.bat service with the MSI-managed
+ # one, and the upgraded server starts with the old data.
+ test-msi-upgrade:
+ needs: build-maven
+ runs-on: 'windows-latest'
+ steps:
+ - name: Download artifacts
+ uses: actions/download-artifact@v8
+ with:
+ name: windows-latest-11
+ - name: Set up Java
+ uses: actions/setup-java@v5
+ with:
+ java-version: '25'
+ distribution: 'zulu'
+ - name: Install released 5.1.1 MSI and configure an instance
+ shell: pwsh
+ run: |
+ Invoke-WebRequest -Uri "https://github.com/OpenIdentityPlatform/OpenDJ/releases/download/5.1.1/opendj-5.1.1.msi" -OutFile opendj-5.1.1.msi
+ # The released 5.1.1 scripts cannot run from a directory with spaces (unquoted
+ # java.io.tmpdir), so put the old install into C:\opendj.
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i opendj-5.1.1.msi /quiet /qn /norestart OPENDJ=C:\opendj /l*v install-old.log"
+ if ($p.ExitCode -ne 0) { Get-Content install-old.log -Tail 80; throw "msiexec /i (5.1.1) failed: $($p.ExitCode)" }
+ $root = "C:\opendj"
+ if (-not (Test-Path "$root\setup.bat")) { Get-Content install-old.log -Tail 80; throw "5.1.1 install root not found at $root" }
+ $env:OPENDJ_JAVA_ARGS = "-server -Xmx512m"
+ & "$root\setup.bat" -h localhost -p 1389 --ldapsPort 1636 --adminConnectorPort 4444 --enableStartTLS --generateSelfSignedCertificate --rootUserDN "cn=Directory Manager" --rootUserPassword password --baseDN dc=example,dc=com --addBaseEntry --cli --acceptLicense --no-prompt --doNotStart
+ if ($LASTEXITCODE -ne 0) { throw "setup.bat (5.1.1) failed: $LASTEXITCODE" }
+ # Register the LEGACY service the pre-MSI way and prove it works, then stop it.
+ & "$root\bat\windows-service.bat" --enableService
+ if ($LASTEXITCODE -ne 0) { throw "windows-service --enableService failed: $LASTEXITCODE" }
+ net start "OpenDJ Server"
+ if ($LASTEXITCODE -ne 0) { throw "net start (5.1.1) failed: $LASTEXITCODE" }
+ net stop "OpenDJ Server"
+ if ($LASTEXITCODE -ne 0) { throw "net stop (5.1.1) failed: $LASTEXITCODE" }
+ - name: Upgrade with the newly built MSI (same directory passed explicitly)
+ shell: pwsh
+ run: |
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ if (-not $msi) { throw "MSI not found in the windows-latest-11 artifact" }
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart OPENDJ=C:\opendj /l*v upgrade.log"
+ if ($p.ExitCode -ne 0) { Get-Content upgrade.log -Tail 120; throw "msiexec /i (upgrade) failed: $($p.ExitCode)" }
+ $root = "C:\opendj"
+ # New package files landed in the old directory, not the x64 default
+ if (-not (Test-Path "$root\setup.bat")) { throw "upgrade did not keep the old install dir" }
+ if (Test-Path "C:\Program Files\OpenDJ") { throw "upgrade unexpectedly installed into the x64 default dir" }
+ # Instance data survived
+ if (-not (Test-Path "$root\config\config.ldif")) { throw "instance data (config\config.ldif) lost by the upgrade" }
+ # Legacy service replaced by the MSI-managed one: display name maps to key 'OpenDJ'
+ $key = (sc.exe getkeyname "OpenDJ Server" | Select-String -Pattern "Name = (.+)").Matches[0].Groups[1].Value.Trim()
+ if ($key -ne "OpenDJ") { sc.exe query; throw "expected MSI-managed service key 'OpenDJ', got '$key'" }
+ sc.exe qc OpenDJ
+ "OPENDJ_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Append
+ - name: Run upgrade.bat and start the upgraded server
+ shell: pwsh
+ run: |
+ $root = $env:OPENDJ_ROOT
+ $env:OPENDJ_JAVA_ARGS = "-server -Xmx512m"
+ & "$root\upgrade.bat" --no-prompt --acceptLicense --force
+ if ($LASTEXITCODE -ne 0) { throw "upgrade.bat failed: $LASTEXITCODE" }
+ net start "OpenDJ Server"
+ if ($LASTEXITCODE -ne 0) { throw "net start (upgraded) failed: $LASTEXITCODE" }
+ for ($i=0; $i -lt 12; $i++) { try { $c = New-Object System.Net.Sockets.TcpClient('localhost', 1636); $c.Close(); break } catch { Start-Sleep -Seconds 5 } }
+ # The pre-upgrade data must still be served
+ & "$root\bat\ldapsearch.bat" --hostname localhost --port 1636 --bindDN "cn=Directory Manager" --bindPassword password --useSsl --trustAll --baseDN "dc=example,dc=com" --searchScope base "(objectClass=*)" 1.1
+ if ($LASTEXITCODE -ne 0) { throw "ldapsearch after upgrade failed: $LASTEXITCODE" }
+ net stop "OpenDJ Server"
+ if ($LASTEXITCODE -ne 0) { throw "net stop (upgraded) failed: $LASTEXITCODE" }
+ - name: Auto-detect the legacy default directory on a fresh install
+ shell: pwsh
+ run: |
+ $msi = (Get-ChildItem -Recurse -Filter *.msi -Path opendj-packages/opendj-msi | Select-Object -First 1).FullName
+ # Clean up the previous scenario first.
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x `"$msi`" /quiet /qn /norestart /l*v uninstall1.log"
+ if ($p.ExitCode -ne 0) { Get-Content uninstall1.log -Tail 80; throw "msiexec /x failed: $($p.ExitCode)" }
+ if (Get-Service OpenDJ -ErrorAction SilentlyContinue) { throw "service not removed by uninstall" }
+ # An existing legacy default directory must be picked up when OPENDJ is not given.
+ New-Item -ItemType Directory -Force "C:\Program Files (x86)\OpenDJ" | Out-Null
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/i `"$msi`" /quiet /qn /norestart /l*v install-autodetect.log"
+ if ($p.ExitCode -ne 0) { Get-Content install-autodetect.log -Tail 80; throw "msiexec /i (autodetect) failed: $($p.ExitCode)" }
+ if (-not (Test-Path "C:\Program Files (x86)\OpenDJ\setup.bat")) { Get-Content install-autodetect.log -Tail 80; throw "installer did not auto-detect the legacy default dir" }
+ if (Test-Path "C:\Program Files\OpenDJ") { throw "installer used the x64 default dir despite an existing legacy dir" }
+ Write-Host "Legacy default directory auto-detected OK"
+ $p = Start-Process msiexec -Wait -PassThru -ArgumentList "/x `"$msi`" /quiet /qn /norestart /l*v uninstall2.log"
+ if ($p.ExitCode -ne 0) { Get-Content uninstall2.log -Tail 80; throw "msiexec /x (cleanup) failed: $($p.ExitCode)" }
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 8b56d45faa..723c8606be 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -14,19 +14,12 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJSON(github) }}
run: echo "$GITHUB_CONTEXT"
- - name: Install wine+rpm for distribution
+ - name: Install rpm for distribution
if: runner.os == 'Linux'
shell: bash
run: |
- sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list
- sudo dpkg --add-architecture i386
- sudo mkdir -pm755 /etc/apt/keyrings && sudo wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key
- sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/$(lsb_release -c -s)/winehq-$(lsb_release -c -s).sources
sudo apt-get update
- sudo apt install --install-recommends winehq-stable || sudo apt install --install-recommends winehq-staging
- wine --version
- version="9.4.0"; sudo wget "https://dl.winehq.org/wine/wine-mono/$version/wine-mono-$version-x86.msi" -O /tmp/wine-mono.msi
- wine msiexec /i /tmp/wine-mono.msi
+ sudo apt-get install -y rpm
- uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -90,11 +83,22 @@ jobs:
with:
name: OpenDJ RPM Package
path: opendj-packages/opendj-rpm/opendj-rpm-standard/target/rpm/opendj/RPMS/noarch/*.rpm
+ # The MSI can only be built on Windows; reuse the one already built by the triggering
+ # Build run (windows-latest-11 artifact) instead of rebuilding it here.
+ - name: Download Windows build artifact (contains the MSI)
+ continue-on-error: true
+ uses: actions/download-artifact@v8
+ with:
+ name: windows-latest-11
+ run-id: ${{ github.event.workflow_run.id }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ path: windows-build
- name: Upload artifacts OpenDJ MSI Package
+ continue-on-error: true
uses: actions/upload-artifact@v7
with:
name: OpenDJ MSI Package
- path: opendj-packages/opendj-msi/opendj-msi-standard/target/*.msi
+ path: windows-build/opendj-packages/opendj-msi/opendj-msi-standard/target/*.msi
- name: Upload artifacts OpenDJ Docker Packages
uses: actions/upload-artifact@v7
with:
@@ -169,3 +173,4 @@ jobs:
git commit -a -m "upload ${{github.event.repository.name}} docs after deploy ${{ github.sha }}"
git push --force https://github.com/OpenIdentityPlatform/doc.openidentityplatform.org.git
fi
+
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1309c07ba7..0ca4f0c670 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -19,18 +19,11 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJSON(github) }}
run: echo "$GITHUB_CONTEXT"
- - name: Install wine+rpm for distribution
+ - name: Install rpm for distribution
shell: bash
run: |
- sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list
- sudo dpkg --add-architecture i386
- sudo mkdir -pm755 /etc/apt/keyrings && sudo wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key
- sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/$(lsb_release -c -s)/winehq-$(lsb_release -c -s).sources
sudo apt-get update
- sudo apt install --install-recommends winehq-stable || sudo apt install --install-recommends winehq-staging
- wine --version
- version="9.4.0"; sudo wget "https://dl.winehq.org/wine/wine-mono/$version/wine-mono-$version-x86.msi" -O /tmp/wine-mono.msi
- wine msiexec /i /tmp/wine-mono.msi
+ sudo apt-get install -y rpm
- uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -70,6 +63,15 @@ jobs:
MAVEN_OPTS: -Dhttps.protocols=TLSv1.2 -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 -Dmaven.wagon.http.retryHandler.requestSentEnabled=true -Dmaven.wagon.http.retryHandler.count=10
if: ${{ env.MAVEN_USERNAME!='' && env.MAVEN_PASSWORD!='' }}
run: mvn --batch-mode -Darguments="-Dgpg.passphrase=${{ secrets.GPG_PASSPHRASE }}" -DsignTag=true -DtagNameFormat="${{ github.event.inputs.releaseVersion }}" -DreleaseVersion=${{ github.event.inputs.releaseVersion }} -DdevelopmentVersion=${{ github.event.inputs.developmentVersion }} release:prepare release:perform --file pom.xml
+ # Hand the just-released server zip to the release-msi job (the MSI can only be
+ # built on Windows), so it does not have to rebuild opendj-server-legacy.
+ - name: Upload the server zip for the MSI job
+ continue-on-error: true
+ uses: actions/upload-artifact@v7
+ with:
+ name: release-server-zip
+ retention-days: 1
+ path: target/checkout/opendj-server-legacy/target/package/*.zip
- name: Release on GitHub
uses: softprops/action-gh-release@v2
with:
@@ -84,7 +86,6 @@ jobs:
target/checkout/opendj-ldap-toolkit/target/*.zip
target/checkout/opendj-packages/opendj-deb/opendj-deb-standard/target/*.deb
target/checkout/opendj-packages/opendj-rpm/opendj-rpm-standard/target/rpm/opendj/RPMS/noarch/*.rpm
- target/checkout/opendj-packages/opendj-msi/opendj-msi-standard/target/*.msi
target/checkout/opendj-packages/opendj-docker/target/Dockerfile.zip
target/checkout/opendj-packages/opendj-openshift-template/*.yaml
target/checkout/opendj-doc-generated-ref/target/*.zip
@@ -130,6 +131,65 @@ jobs:
git tag -f ${TAG_NAME}
git push --quiet --force origin ${TAG_NAME}
+ # The MSI can only be built on Windows. Reuses the server zip built by release-maven
+ # (installed into the local repo), so only the opendj-msi-standard module is built here.
+ # continue-on-error: an MSI failure must not break the release.
+ release-msi:
+ name: Windows MSI release
+ runs-on: 'windows-latest'
+ continue-on-error: true
+ needs:
+ - release-maven
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ ref: ${{ github.event.inputs.releaseVersion }}
+ submodules: recursive
+ - name: Set up Java
+ uses: actions/setup-java@v5
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+ - name: Cache Maven packages
+ uses: actions/cache@v5
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-m2-repository-${{ hashFiles('**/pom.xml') }}
+ restore-keys: ${{ runner.os }}-m2-repository
+ - name: Setup WiX (.NET tool)
+ shell: bash
+ run: |
+ echo "DOTNET_ROLL_FORWARD=Major" >> "$GITHUB_ENV"
+ export DOTNET_ROLL_FORWARD=Major
+ dotnet tool install --global wix --version 5.0.2 || dotnet tool update --global wix --version 5.0.2
+ echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH"
+ export PATH="$HOME/.dotnet/tools:$PATH"
+ wix --version
+ wix extension add -g WixToolset.UI.wixext/5.0.2 || true
+ - name: Download the server zip built by release-maven
+ uses: actions/download-artifact@v8
+ with:
+ name: release-server-zip
+ path: server-zip
+ - name: Install the server zip into the local Maven repository
+ shell: bash
+ run: |
+ ZIP=$(ls server-zip/*.zip | head -1)
+ echo "Installing $ZIP as opendj-server-legacy:${{ github.event.inputs.releaseVersion }}:zip"
+ mvn --batch-mode install:install-file -Dfile="$ZIP" \
+ -DgroupId=org.openidentityplatform.opendj -DartifactId=opendj-server-legacy \
+ -Dversion=${{ github.event.inputs.releaseVersion }} -Dpackaging=zip
+ - name: Build the MSI (packaging only, no rebuild)
+ env:
+ MAVEN_OPTS: -Dhttps.protocols=TLSv1.2 -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 -Dmaven.wagon.http.retryHandler.requestSentEnabled=true -Dmaven.wagon.http.retryHandler.count=10
+ run: mvn --batch-mode --errors -DskipTests package -pl :opendj-msi-standard --file pom.xml
+ - name: Attach the MSI to the GitHub release
+ uses: softprops/action-gh-release@v2
+ with:
+ tag_name: ${{ github.event.inputs.releaseVersion }}
+ fail_on_unmatched_files: true
+ files: opendj-packages/opendj-msi/opendj-msi-standard/target/*.msi
+
release-docker:
name: Docker release
runs-on: 'ubuntu-latest'
diff --git a/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ADNotificationRequestControl.java b/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ADNotificationRequestControl.java
index c2c6ea3669..631f5e1e05 100644
--- a/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ADNotificationRequestControl.java
+++ b/opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/ADNotificationRequestControl.java
@@ -12,6 +12,7 @@
* information: "Portions Copyright [year] [name of copyright owner]".
*
* Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC
*/
package org.forgerock.opendj.ldap.controls;
@@ -39,12 +40,12 @@
* SearchResultEntry entry = reader.readEntry(); // Entry that changed
*
* Boolean isDeleted = entry.parseAttribute("isDeleted").asBoolean();
- * if (isDeleted != null && isDeleted) {
+ * if (isDeleted != null && isDeleted) {
* // Handle entry deletion
* }
* String whenCreated = entry.parseAttribute("whenCreated").asString();
* String whenChanged = entry.parseAttribute("whenChanged").asString();
- * if (whenCreated != null && whenChanged != null) {
+ * if (whenCreated != null && whenChanged != null) {
* if (whenCreated.equals(whenChanged)) {
* //Handle entry addition
* } else {
diff --git a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc
index 92be08f08a..5770bfc24d 100644
--- a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc
+++ b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc
@@ -40,6 +40,8 @@ This chapter covers installation of OpenDJ server software and includes the foll
* xref:#install-rpm["To Install From the RPM Package"]
+* xref:#install-msi["To Install With the Windows Installer (MSI)"]
+
* xref:#install-properties-file["To Install OpenDJ Directory Server With a Properties File"]
* xref:#pdb-to-je["To Move Data from a PDB Backend to a JE Backend"]
@@ -635,6 +637,49 @@ opendj 0:off 1:off 2:on 3:on 4:on 5:on 6:off
====
+[#install-msi]
+.To Install With the Windows Installer (MSI)
+====
+On Windows you can install OpenDJ directory server from the `.msi` package. The installer copies the server files to disk and registers the `OpenDJ Server` Windows service, but it does not configure or start a server (run `setup` first) and it does not install a Java runtime.
+
+. Make sure a supported Java runtime is available, as described in xref:#before-you-install["To Prepare For Installation"].
++
+The installer refuses to install when it cannot detect a Java installation: install a JRE (for example link:https://adoptium.net[Eclipse Temurin, window=\_blank]), set `JAVA_HOME` to your Java installation, or make sure the `java` executable is on the `PATH`, before running it. If your default Java environment is not the one OpenDJ should use, additionally set `OPENDJ_JAVA_HOME` to the correct Java installation (or `OPENDJ_JAVA_BIN` to the absolute path of the `java` command) before you run `setup` or start the server.
+
+. Install the package, either with the GUI or silently:
++
+The package is not code-signed, so Windows SmartScreen or User Account Control may warn about an unrecognized publisher; choose to run the installer anyway.
++
+* GUI: double-click `opendj-{opendj-version}.msi` and follow the wizard.
++
+* Silent: run the following command (optionally set the installation directory with the `OPENDJ` property):
++
+
+[source, console, subs="attributes"]
+----
+C:\> msiexec /i opendj-{opendj-version}.msi /quiet OPENDJ="C:\opendj"
+----
++
+By default the x64 package installs under `C:\Program Files\OpenDJ`.
+
+. Configure OpenDJ directory server by running the `setup` command, described in xref:../reference/admin-tools-ref.adoc#setup-1[setup(1)] in the __Reference__, from the installation directory. Use `setup.bat` for the GUI wizard or `setup.bat --cli` for the command-line:
++
+
+[source, console]
+----
+C:\path\to\opendj> setup.bat --cli
+----
+
+. Start the OpenDJ Windows service. The installer already registered it as `OpenDJ Server`; after configuring with `setup`, start it (it is not started automatically during installation):
++
+
+[source, console]
+----
+C:\> net start "OpenDJ Server"
+----
+
+====
+
[#install-properties-file]
.To Install OpenDJ Directory Server With a Properties File
====
diff --git a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-uninstall.adoc b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-uninstall.adoc
index 19cc3a3875..3adc45d381 100644
--- a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-uninstall.adoc
+++ b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-uninstall.adoc
@@ -12,7 +12,7 @@
information: "Portions copyright [year] [name of copyright owner]".
Copyright 2017 ForgeRock AS.
- Portions Copyright 2024 3A Systems LLC.
+ Portions Copyright 2024-2026 3A Systems LLC.
////
:figure-caption!:
@@ -33,6 +33,8 @@ This chapter includes the following procedures:
* xref:#uninstall-rpm["To Uninstall the RPM Package"]
+* xref:#uninstall-msi["To Uninstall the Windows MSI Package"]
+
[#uninstall-gui]
.To Remove OpenDJ With the GUI Uninstaller
@@ -157,3 +159,20 @@ Removing the package does not remove your data or configuration. You must remove
====
+[#uninstall-msi]
+.To Uninstall the Windows MSI Package
+====
+Remove OpenDJ directory server installed from the `.msi` package like any other Windows program. The uninstaller stops and removes the `OpenDJ Server` Windows service that the package registered.
+
+* Uninstall the package, either through __Settings > Apps__ (or __Control Panel > Programs and Features__) by selecting OpenDJ and choosing Uninstall, or from the command-line:
++
+
+[source, console, subs="attributes"]
+----
+C:\> msiexec /x opendj-{opendj-version}.msi /quiet
+----
++
+Uninstalling removes the files installed by the package. Your configured instance data under the installation directory (for example `config`, `db`, and `logs`) is not removed; delete the installation directory manually to remove all files.
+
+====
+
diff --git a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-upgrade.adoc b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-upgrade.adoc
index cf0268dec0..d91b763711 100644
--- a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-upgrade.adoc
+++ b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-upgrade.adoc
@@ -45,6 +45,8 @@ This chapter includes the following procedures and examples:
* xref:#upgrade-zip-example["Upgrading to OpenDJ {opendj-version-short}"]
+* xref:#upgrade-msi["To Upgrade the Windows MSI Installation"]
+
* xref:#upgrade-repl["To Upgrade Replicated Servers"]
* xref:#new-repl-mixed-topology["To Add a New Replica to an Existing Topology"]
@@ -243,6 +245,43 @@ $
----
====
+[#upgrade-msi]
+.To Upgrade the Windows MSI Installation
+====
+Before starting this procedure, follow the steps in xref:#before-you-upgrade["Before You Upgrade"]. Installing the newer `.msi` performs a major upgrade that replaces the installed program files, so make a full file-system backup of the current installation first.
+
+. Stop the current OpenDJ server (if it runs as a Windows service, `net stop "OpenDJ Server"`).
+
+. Back up the file-system directory where OpenDJ is installed.
+
+. Install the newer package (GUI or silent). The installer detects the existing installation — the location recorded in the registry by a previous x64 package, or the default directory of the older 32-bit package (`C:\Program Files (x86)\OpenDJ`) — and installs into the same directory, so your configured instance data (`config`, `db`, `logs`) is kept and only the program files are replaced. If the older server was installed in a custom directory the installer did not detect, select that directory in the GUI or pass it explicitly:
++
+
+[source, console, subs="attributes"]
+----
+C:\> msiexec /i opendj-{opendj-version}.msi /quiet OPENDJ="C:\path\to\opendj"
+----
++
+The installer replaces a service registered by the older `windows-service.bat` command with the MSI-managed `OpenDJ Server` service automatically; no manual `--disableService`/`--enableService` is needed.
+
+. Run the `upgrade` command, described in xref:../reference/admin-tools-ref.adoc#upgrade-1[upgrade(1)] in the __Reference__, to bring the configuration and application data up to date with the new binary and script files:
++
+
+[source, console]
+----
+C:\path\to\opendj> upgrade.bat --no-prompt --acceptLicense
+----
+
+. Start the upgraded OpenDJ server:
++
+
+[source, console]
+----
+C:\> net start "OpenDJ Server"
+----
+
+====
+
[#upgrade-repl]
.To Upgrade Replicated Servers
====
diff --git a/opendj-packages/opendj-msi/opendj-msi-standard/pom.xml b/opendj-packages/opendj-msi/opendj-msi-standard/pom.xml
index 9b4bf07b7e..f7505785c1 100644
--- a/opendj-packages/opendj-msi/opendj-msi-standard/pom.xml
+++ b/opendj-packages/opendj-msi/opendj-msi-standard/pom.xml
@@ -13,7 +13,7 @@
information: "Portions Copyright [year] [name of copyright owner]".
Copyright 2015-2016 ForgeRock AS.
- Portions Copyright 2018 Open Identity Platform Community
+ Portions Copyright 2018-2026 3A Systems, LLC
-->
4.0.0
@@ -29,265 +29,130 @@
OpenDJ MSI Standard Package
- This module generates an OpenDJ MSI package.
+ This module generates an OpenDJ MSI package using the WiX Toolset v5 .NET tool
+ (cross-platform; no wine). The `wix` tool must be on the PATH (CI installs it via
+ `dotnet tool install --global wix`).
${basedir}/resources/msi
${project.build.directory}/${product.name.lowercase}
+
+ ${project.build.directory}/msi-staging
+ ${project.build.directory}/msi-staging-lib
+ ${package.dir}/lib/opendj_service.exe
+ ${project.build.directory}/${product.name.lowercase}-${project.version}.msi
-
-
-
+
+
${project.groupId}.${project.artifactId}
+
org.codehaus.mojo
build-helper-maven-plugin
-
-
-
- org.openidentityplatform.commons
- maven-external-dependency-plugin
- false
-
-
- ${project.build.directory}/dependencies/
-
- false
- true
- false
-
-
- openidentityplatform.org
- wixtoolset
- 3.11.1
- zip
-
- https://github.com/wixtoolset/wix3/releases/download/wix3111rtm/wix311-binaries.zip
-
- false
-
-
- openidentityplatform.org
- winetricks
- LAST
- sh
-
- https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks
-
- false
-
-
-
-
-
- clean-external-dependencies
- clean
-
- clean-external
-
-
-
- resolve-install-external-dependencies
- process-resources
-
- resolve-external
- install-external
-
-
-
- deploy-external-dependencies
- deploy
-
- deploy-external
-
-
-
-
-
- org.apache.maven.plugins
- maven-dependency-plugin
- unpack-wix
+ attach-msi-and-bundle
package
- unpack
+ attach-artifact
-
-
- openidentityplatform.org
- wixtoolset
- 3.11.1
- zip
-
-
-
- ${project.build.directory}/wix
-
-
-
-
- unpack-winetricks
- package
-
- copy
-
-
-
-
- openidentityplatform.org
- winetricks
- LAST
- sh
-
-
-
- ${project.build.directory}/winetricks
-
+
+ ${msi.file}msi
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+
org.apache.maven.plugins
maven-antrun-plugin
- build-msi-package-prepare
+ stage-msi-payload
package
run
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ------------------- ${exec.heat} ${param.heat} -------------------
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ------------------- ${exec.candle} ${param.candle} -------------------
-
-
-
-
-
-
-
-
-
-
-
-
- ------------------- ${exec.light} ${param.light} -------------------
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+
+ wix-build-msi
+ package
+
+ exec
+
+
+ wix
+ ${project.build.directory}
+
+ build
+ ${msi.resources}/package.wxs
+ -archx64
+ -extWixToolset.UI.wixext
+ -bindpath${msi.resources}
+ -dname=${product.name}
+ -dmajor=${parsedVersion.majorVersion}
+ -dminor=${parsedVersion.minorVersion}
+ -dpoint=${parsedVersion.incrementalVersion}
+ -dstagingRoot=${staging.root}
+ -dstagingLib=${staging.lib}
+ -dserviceExe=${service.exe}
+ -o${msi.file}
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/opendj-packages/opendj-msi/opendj-msi-standard/resources/msi/package.wxs b/opendj-packages/opendj-msi/opendj-msi-standard/resources/msi/package.wxs
index 23f4dd4471..cfb0d85b79 100644
--- a/opendj-packages/opendj-msi/opendj-msi-standard/resources/msi/package.wxs
+++ b/opendj-packages/opendj-msi/opendj-msi-standard/resources/msi/package.wxs
@@ -13,47 +13,157 @@
! information: "Portions Copyright [year] [name of copyright owner]".
!
! Copyright 2013-2016 ForgeRock AS.
- ! Portion Copyright 2018 Open Identity Platform Community
+ ! Portions Copyright 2018-2026 3A Systems, LLC
! -->
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
-
-
-
-
-
-
- NOT Installed
- 1
-
-
+
+
+
diff --git a/opendj-packages/opendj-msi/pom.xml b/opendj-packages/opendj-msi/pom.xml
index f652541ac6..3a697e73f4 100644
--- a/opendj-packages/opendj-msi/pom.xml
+++ b/opendj-packages/opendj-msi/pom.xml
@@ -32,63 +32,10 @@
This module contains configuration and generic plugin call to build OpenDJ MSI packages.
-
-
- /usr/bin/wine
-
- unix
- /usr/bin/wine
-
-
- opendj-msi-standard
-
-
- /usr/bin/wine${project.build.directory}/wix/heat.exe
- /usr/bin/wine${project.build.directory}/wix/candle.exe
- /usr/bin/wine${project.build.directory}/wix/light.exe
-
-
-
- /usr/local/bin/wine
-
- unix
- /usr/local/bin/wine
-
-
- opendj-msi-standard
-
-
- /usr/local/bin/wine${project.build.directory}/wix/heat.exe
- /usr/local/bin/wine${project.build.directory}/wix/candle.exe
- /usr/local/bin/wine${project.build.directory}/wix/light.exe
-
-
-
- /opt/local/bin/wine
-
- unix
- /opt/local/bin/wine
-
-
- opendj-msi-standard
-
-
- /opt/local/bin/wine${project.build.directory}/wix/heat.exe
- /opt/local/bin/wine${project.build.directory}/wix/candle.exe
- /opt/local/bin/wine${project.build.directory}/wix/light.exe
-
-
-
- windows
- windows
-
- opendj-msi-standard
-
-
- ${project.build.directory}\wix\heat.exe
- ${project.build.directory}\wix\candle.exe
- ${project.build.directory}\wix\light.exe
-
-
-
+
+
+ opendj-msi-standard
+
diff --git a/opendj-packages/pom.xml b/opendj-packages/pom.xml
index 739e77779f..83d43d7ec0 100644
--- a/opendj-packages/pom.xml
+++ b/opendj-packages/pom.xml
@@ -55,7 +55,6 @@
opendj-deb
opendj-rpm
opendj-svr4
- opendj-msi
opendj-docker
@@ -69,7 +68,6 @@
opendj-svr4
- opendj-msi
opendj-docker
@@ -79,6 +77,8 @@
windows
+
opendj-msi
opendj-docker
diff --git a/opendj-server-legacy/lib/launcher_administrator.exe b/opendj-server-legacy/lib/launcher_administrator.exe
index 50b885811d..bfb8ae4fdf 100644
Binary files a/opendj-server-legacy/lib/launcher_administrator.exe and b/opendj-server-legacy/lib/launcher_administrator.exe differ
diff --git a/opendj-server-legacy/lib/opendj_service.exe b/opendj-server-legacy/lib/opendj_service.exe
index 0b458d8f72..4e20ac7941 100644
Binary files a/opendj-server-legacy/lib/opendj_service.exe and b/opendj-server-legacy/lib/opendj_service.exe differ
diff --git a/opendj-server-legacy/lib/winlauncher.exe b/opendj-server-legacy/lib/winlauncher.exe
index 04f210cb64..d1e3eaef75 100644
Binary files a/opendj-server-legacy/lib/winlauncher.exe and b/opendj-server-legacy/lib/winlauncher.exe differ
diff --git a/opendj-server-legacy/resource/bin/_script-util.bat b/opendj-server-legacy/resource/bin/_script-util.bat
index f6bacbb9d1..bea162b7cd 100644
--- a/opendj-server-legacy/resource/bin/_script-util.bat
+++ b/opendj-server-legacy/resource/bin/_script-util.bat
@@ -13,7 +13,7 @@ rem information: "Portions Copyright [year] [name of copyright owner]".
rem
rem Copyright 2008-2010 Sun Microsystems, Inc.
rem Portions Copyright 2011-2016 ForgeRock AS.
-rem Portions Copyright 2020-2025 3A Systems, LLC.
+rem Portions Copyright 2020-2026 3A Systems, LLC.
set SET_JAVA_HOME_AND_ARGS_DONE=false
set SET_ENVIRONMENT_VARS_DONE=false
@@ -56,10 +56,10 @@ rem get the absolute paths before building the classpath
rem it also helps comparing the two paths
FOR /F "delims=" %%i IN ("%INSTALL_ROOT%") DO set INSTALL_ROOT=%%~dpnxi
FOR /F "delims=" %%i IN ("%INSTANCE_ROOT%") DO set INSTANCE_ROOT=%%~dpnxi
-call "%INSTALL_ROOT%\lib\setcp.bat" %INSTALL_ROOT%\lib\bootstrap-client.jar
+call "%INSTALL_ROOT%\lib\setcp.bat" "%INSTALL_ROOT%\lib\bootstrap-client.jar"
set CLASSPATH=%INSTANCE_ROOT%\classes;%CLASSPATH%
if "%INSTALL_ROOT%" == "%INSTANCE_ROOT%" goto setClassPathDone
-FOR %%x in ("%INSTANCE_ROOT%\lib\*.jar") DO call "%INSTANCE_ROOT%\lib\setcp.bat" %%x
+FOR %%x in ("%INSTANCE_ROOT%\lib\*.jar") DO call "%INSTANCE_ROOT%\lib\setcp.bat" "%%x"
:setClassPathDone
set SET_CLASSPATH_DONE=true
goto scriptBegin
@@ -71,7 +71,7 @@ rem get the absolute paths before building the classpath
rem it also helps comparing the two paths
FOR /F "delims=" %%i IN ("%INSTALL_ROOT%") DO set INSTALL_ROOT=%%~dpnxi
FOR /F "delims=" %%i IN ("%INSTANCE_ROOT%") DO set INSTANCE_ROOT=%%~dpnxi
-call "%INSTALL_ROOT%\lib\setcp.bat" %INSTALL_ROOT%\lib\*
+call "%INSTALL_ROOT%\lib\setcp.bat" "%INSTALL_ROOT%\lib\*"
set CLASSPATH=%INSTANCE_ROOT%\classes;%CLASSPATH%
set SET_CLASSPATH_DONE=true
goto scriptBegin
@@ -100,7 +100,8 @@ rem if not "%OPENDJ_JAVA_ARGS%" == "" goto checkEnvJavaHome
set SCRIPT_JAVA_ARGS_PROPERTY=%SCRIPT_NAME%.java-args
call:readProperty %SCRIPT_JAVA_ARGS_PROPERTY%
set OPENDJ_JAVA_ARGS=%OPENDJ_JAVA_ARGS% %PROPERTY_VALUE%
-if not "%OPENDJ_JAVA_ARGS%" == "" goto checkEnvJavaHome
+rem "if defined" comparisons: the value may contain quotes (java.io.tmpdir), which break "%VAR%" == "" checks.
+if defined OPENDJ_JAVA_ARGS goto checkEnvJavaHome
call:readProperty default.java-args
set OPENDJ_JAVA_ARGS=%OPENDJ_JAVA_ARGS% %PROPERTY_VALUE
if "%OPENDJ_JAVA_BIN%" == "" goto checkEnvJavaHome
@@ -186,12 +187,12 @@ goto scriptBegin
if %SET_TEMP_DIR_DONE% == "true" goto end
set OPENDJ_TMP_DIR=%INSTANCE_ROOT%\tmp
if not exist "%OPENDJ_TMP_DIR%" mkdir "%OPENDJ_TMP_DIR%"
-set OPENDJ_JAVA_ARGS=%OPENDJ_JAVA_ARGS% -Djava.io.tmpdir=%OPENDJ_TMP_DIR%
+set OPENDJ_JAVA_ARGS=%OPENDJ_JAVA_ARGS% -Djava.io.tmpdir="%OPENDJ_TMP_DIR%"
set SET_TEMP_DIR_DONE=true
goto scriptBegin
:testJava
-if "%OPENDJ_JAVA_ARGS%" == "" goto checkLegacyArgs
+if not defined OPENDJ_JAVA_ARGS goto checkLegacyArgs
:continueTestJava
"%OPENDJ_JAVA_BIN%" %OPENDJ_JAVA_ARGS% org.opends.server.tools.CheckJVMVersion > NUL 2>&1
set RESULT_CODE=%errorlevel%
@@ -200,12 +201,12 @@ if not %RESULT_CODE% == 0 goto noValidJavaHome
goto end
:checkLegacyArgs
-if "%OPENDS_JAVA_ARGS%" == "" goto continueTestJava
+if not defined OPENDS_JAVA_ARGS goto continueTestJava
set OPENDJ_JAVA_ARGS=%OPENDS_JAVA_ARGS%
goto continueTestJava
:noValidJavaHome
-if NOT "%OPENDJ_JAVA_ARGS%" == "" goto noValidHomeWithArgs
+if defined OPENDJ_JAVA_ARGS goto noValidHomeWithArgs
echo ERROR: The detected Java version could not be used. The detected
echo Java binary is:
echo %OPENDJ_JAVA_BIN%
diff --git a/opendj-server-legacy/resource/bin/setcp.bat b/opendj-server-legacy/resource/bin/setcp.bat
index a47d4e6ac5..eca179c082 100644
--- a/opendj-server-legacy/resource/bin/setcp.bat
+++ b/opendj-server-legacy/resource/bin/setcp.bat
@@ -12,14 +12,17 @@ rem Header, with the fields enclosed by brackets [] replaced by your own identif
rem information: "Portions Copyright [year] [name of copyright owner]".
rem
rem Copyright 2006-2008 Sun Microsystems, Inc.
+rem Portions Copyright 2026 3A Systems, LLC
-set CLASSPATHCOMPONENT=%1
-if ""%1""=="""" goto gotAllArgs
+rem Use %~1 and quoted comparisons so paths containing spaces and parentheses
+rem (for example C:\Program Files (x86)\OpenDJ) do not break the parser.
+set CLASSPATHCOMPONENT=%~1
+if "%~1"=="" goto gotAllArgs
shift
:argCheck
-if ""%1""=="""" goto gotAllArgs
-set CLASSPATHCOMPONENT=%CLASSPATHCOMPONENT% %1
+if "%~1"=="" goto gotAllArgs
+set CLASSPATHCOMPONENT=%CLASSPATHCOMPONENT% %~1
shift
goto argCheck
diff --git a/opendj-server-legacy/resource/bin/start-ds.bat b/opendj-server-legacy/resource/bin/start-ds.bat
index 7f7be60d89..f591502ba2 100644
--- a/opendj-server-legacy/resource/bin/start-ds.bat
+++ b/opendj-server-legacy/resource/bin/start-ds.bat
@@ -14,7 +14,7 @@ rem information: "Portions Copyright [year] [name of copyright owner]".
rem
rem Copyright 2006-2010 Sun Microsystems, Inc.
rem Portions Copyright 2011-2014 ForgeRock AS.
-rem Portions Copyright 2025 3A Systems LLC.
+rem Portions Copyright 2025-2026 3A Systems LLC.
setlocal
set DIR_HOME=%~dp0..
@@ -61,10 +61,12 @@ echo %SCRIPT%: PATH=%PATH% >> %LOG%
rem cleanup the tmp directory
set CUR_DIR=%CD%
set OPENDJ_TMP_DIR=%INSTANCE_ROOT%\tmp
-dir /b /s /a %OPENDJ_TMP_DIR% | findstr .>nul && (
- cd /d %OPENDJ_TMP_DIR%
+rem The paths must be quoted: an unquoted parenthesis (e.g. from
+rem "C:\Program Files (x86)") terminates the ( ) block at parse time.
+dir /b /s /a "%OPENDJ_TMP_DIR%" | findstr .>nul && (
+ cd /d "%OPENDJ_TMP_DIR%"
for /F "delims=" %%i in ('dir /b') do (rmdir "%%i" /s/q>NUL 2>&1 || del "%%i" /s/q>NUL 2>&1)
- cd /d %CUR_DIR%
+ cd /d "%CUR_DIR%"
)
"%OPENDJ_JAVA_BIN%" -client %SCRIPT_NAME_ARG% org.opends.server.core.DirectoryServer --configFile "%INSTANCE_ROOT%\config\config.ldif" --checkStartability %*
diff --git a/opendj-server-legacy/src/build-tools/windows/service.c b/opendj-server-legacy/src/build-tools/windows/service.c
index 1fd7e373d0..29fbb87381 100644
--- a/opendj-server-legacy/src/build-tools/windows/service.c
+++ b/opendj-server-legacy/src/build-tools/windows/service.c
@@ -826,6 +826,118 @@ ServiceReturnCode createServiceBinPath(char* serviceBinPath)
return returnValue;
} // createServiceBinPath
+// ----------------------------------------------------
+// Reads the next command line token starting at *p into out (at most
+// outSize - 1 characters). A token is either a run of non-blank
+// characters or a double-quoted string; quotes are treated as plain
+// delimiters without escape processing, so a trailing backslash-quote
+// sequence (as written by msiexec) leaves trailing backslashes on the
+// token, which normalizeInstanceDir strips. Returns the position right
+// after the token.
+// ----------------------------------------------------
+
+static const char* nextCmdToken(const char* p, char* out, int outSize)
+{
+ int i = 0;
+ while ((*p == ' ') || (*p == '\t'))
+ {
+ p++;
+ }
+ if (*p == '"')
+ {
+ p++;
+ while ((*p != '\0') && (*p != '"'))
+ {
+ if (i < (outSize - 1))
+ {
+ out[i++] = *p;
+ }
+ p++;
+ }
+ if (*p == '"')
+ {
+ p++;
+ }
+ }
+ else
+ {
+ while ((*p != '\0') && (*p != ' ') && (*p != '\t'))
+ {
+ if (i < (outSize - 1))
+ {
+ out[i++] = *p;
+ }
+ p++;
+ }
+ }
+ out[i] = '\0';
+ return p;
+} // nextCmdToken
+
+// ----------------------------------------------------
+// Strips trailing backslashes and "\." path segments from an instance
+// dir so that "C:\opendj", "C:\opendj\" and "C:\opendj\." all compare
+// equal.
+// ----------------------------------------------------
+
+static void normalizeInstanceDir(char* dir)
+{
+ size_t len = strlen(dir);
+ BOOL changed = TRUE;
+ while (changed && (len > 0))
+ {
+ changed = FALSE;
+ if (dir[len - 1] == '\\')
+ {
+ dir[--len] = '\0';
+ changed = TRUE;
+ }
+ else if ((len > 1) && (dir[len - 1] == '.') && (dir[len - 2] == '\\'))
+ {
+ dir[--len] = '\0';
+ changed = TRUE;
+ }
+ }
+} // normalizeInstanceDir
+
+// ----------------------------------------------------
+// Tells whether two service command lines refer to the same server
+// instance. The strings cannot be compared verbatim because every
+// writer quotes differently: this executable and the java tools write
+// '"\lib\opendj_service.exe" start ""' while the MSI
+// ServiceInstall writes the executable unquoted (when the path has no
+// spaces) and the instance dir with a trailing backslash. Instead the
+// executable path, the subcommand and the normalized instance dir are
+// compared token by token, case-insensitively.
+// ----------------------------------------------------
+
+static BOOL serviceCmdsMatch(const char* cmd1, const char* cmd2)
+{
+ char exe1[COMMAND_SIZE];
+ char exe2[COMMAND_SIZE];
+ char sub1[COMMAND_SIZE];
+ char sub2[COMMAND_SIZE];
+ char dir1[COMMAND_SIZE];
+ char dir2[COMMAND_SIZE];
+ const char* p1 = cmd1;
+ const char* p2 = cmd2;
+
+ p1 = nextCmdToken(p1, exe1, COMMAND_SIZE);
+ p1 = nextCmdToken(p1, sub1, COMMAND_SIZE);
+ nextCmdToken(p1, dir1, COMMAND_SIZE);
+
+ p2 = nextCmdToken(p2, exe2, COMMAND_SIZE);
+ p2 = nextCmdToken(p2, sub2, COMMAND_SIZE);
+ nextCmdToken(p2, dir2, COMMAND_SIZE);
+
+ normalizeInstanceDir(dir1);
+ normalizeInstanceDir(dir2);
+
+ return (_stricmp(exe1, exe2) == 0)
+ && (_stricmp(sub1, sub2) == 0)
+ && (_stricmp(dir1, dir2) == 0);
+} // serviceCmdsMatch
+
// ----------------------------------------------------
// Returns the service name that maps the command used to start the
// product. All commands are supposed to be unique because they have
@@ -866,7 +978,7 @@ ServiceReturnCode getServiceName(char* cmdToRun, char* serviceName)
ServiceDescriptor curService = serviceList[i];
if (curService.cmdToRun != NULL)
{
- if (_stricmp(cmdToRun, curService.cmdToRun) == 0)
+ if (serviceCmdsMatch(cmdToRun, curService.cmdToRun))
{
if (strlen(curService.serviceName) < MAX_SERVICE_NAME)
{