Understanding the WiX candle.exe Exit Code 14 Error

When packaging desktop applications using JDK 21's jpackage—frequently via Gradle and frameworks like JetBrains Compose Multiplatform—generating a Windows EXE installer (--type exe) relies heavily on the WiX Toolset (v3.11). Under the hood, jpackage first builds an MSI installer and then wraps it inside an EXE bootstrapper using WiX's Burn engine.

During this bootstrap phase, jpackage generates a WiX file named bundle.wxf and compiles it using candle.exe. If this command suddenly fails with:

java.io.IOException: Command [candle.exe, -nologo, ...\bundle.wxf, -ext, WixUtilExtension, -arch, x64, -out, ...\bundle.wixobj] exited with 14 code

It means candle.exe encountered a fatal XML parsing or schema validation error. Unlike minor compiler warnings or reference errors (which usually throw exit codes 1 or 2), exit code 14 indicates that the generated XML file could not be parsed into a valid DOM or syntax tree.

Root Causes of Exit Code 14 in bundle.wxf

Because bundle.wxf is generated dynamically by jpackage, several subtle environment and metadata issues can corrupt the file syntax:

  • Unescaped XML Characters in Application Metadata: If your appName, vendor, copyright, or description attributes contain reserved XML entities like ampersands (&), quotation marks ("), angle brackets (<, >), or apostrophes ('), jpackage may write them directly without escaping, creating invalid XML.
  • Invalid or Malformed GUID (Upgrade UUID): WiX Burn bundles strictly enforce the registry-style GUID format (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX). If the upgradeUuid in your Gradle script is lowercase, lacks hyphens, or is improperly formatted, bundle.wxf validation will crash candle.exe.
  • Missing .NET Framework 3.5 Runtime: WiX Toolset 3.11 Burn bootstrapper components rely on .NET Framework 3.5/2.0. If Windows 10 does not have the .NET 3.5 feature enabled, certain custom bootstrapper actions or extension hooks will crash prematurely.
  • File Locking and Path Collisions: Antivirus engines or aggressive indexing services on Windows frequently lock temporary files during the rapid creation and compilation of bundle.wxf, leading to partial writes (which explains the observed XML unexpected-end-of-file condition).

How to Diagnose and Resolve the Issue

1. Preserve jpackage Intermediate Files

By default, jpackage deletes the temporary build folder when the task finishes or fails. To capture the full bundle.wxf file and the raw WiX compiler diagnostic output, pass the --temp and --verbose arguments through your build configuration.

In Compose Multiplatform (build.gradle.kts), pass additional arguments to jpackage:

compose.desktop {\n    application {\n        mainClass = "com.drivesweep.MainKt"\n        nativeDistributions {\n            targetFormats(\n                org.jetbrains.compose.desktop.application.dsl.TargetFormat.Exe,\n                org.jetbrains.compose.desktop.application.dsl.TargetFormat.Msi\n            )\n            windows {\n                // Specify an explicit directory to inspect generated WiX sources\n                freeCompilerArgs.addAll(\n                    "--verbose",\n                    "--temp", layout.buildDirectory.dir("tmp/jpackage").get().asFile.absolutePath\n                )\n            }\n        }\n    }\n}

Once the task fails, navigate to build/tmp/jpackage/config/bundle.wxf and open it in an editor to inspect line-by-line syntax errors or identify where the XML structure broke.

2. Sanitize Metadata and Verify UUIDs

Ensure that all application properties configured in your Gradle script are strictly alphanumeric and do not contain special symbols:

compose.desktop {\n    application {\n        nativeDistributions {\n            packageName = "DriveSweep"\n            packageVersion = "1.0.0"\n            description = "DriveSweep Disk Cleanup Utility" // Avoid &, <, >, \", etc.\n            vendor = "DriveSweep Team"\n            \n            windows {\n                menuGroup = "DriveSweep"\n                // Ensure valid, uppercase GUID syntax with hyphens\n                upgradeUuid = "9B1B8A44-245C-4F75-9C49-2A2F1D0A358B"\n            }\n        }\n    }\n}

3. Enable .NET Framework 3.5 on Windows 10

WiX 3.11's bootstrapper extensions call underlying Windows components that require .NET 3.5. Ensure this Windows Feature is enabled:

  1. Press Win + R, type optionalfeatures, and hit Enter.
  2. Check the box for .NET Framework 3.5 (includes .NET 2.0 and 3.0).
  3. Click OK and let Windows download the required binaries.

4. Alternative Workaround: Build an MSI Directly

WiX Burn (which creates .exe files) wraps an underlying .msi. In enterprise and professional deployment environments, deploying via MSI is standard and completely avoids the bundle.wxf compilation step.

To test if the core application packaging succeeds without the EXE wrapper, run:

.\gradlew.bat :composeApp:packageMsi

If packageMsi succeeds, the issue is strictly isolated to the Burn bootstrapper configuration in bundle.wxf.