Builds Windows Installer packages from WiX sources on Linux or Windows, in pure Zig; a clean-room reimplementation of msitools' wixl.
  • Zig 92.7%
  • Shell 5%
  • Nix 2.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeffrey C. Ollie f78e5ee916
All checks were successful
test / test (push) Successful in 11m43s
test / docs (push) Successful in 8m47s
test / e2e (push) Successful in 18m13s
Keep build-machine paths out of the product and package codes
The product and package codes were hashed from the preprocessed source
text, which holds every File/@Source and Icon/@SourceFile as written. A
path passed in as a define points into the build directory, which Nix
makes afresh for every build, so two builds of the same sources gave two
packages (reported from harrier, issue #14 there).

A path now contributes only its file name, which a File with no Name
installs under; the file's contents stand for the rest. Icons' contents
are hashed too, since their path no longer carries a change to them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HAVLVrFPvxBP829696uqnJ
2026-10-16 13:50:57 -05:00
.forgejo/workflows Fuzz 10K inputs in CI rather than 50K 2026-10-14 06:56:43 -05:00
docs Keep build-machine paths out of the product and package codes 2026-10-16 13:50:57 -05:00
LICENSES Start zixl: a clean-room Zig reimplementation of msitools' wixl 2026-10-14 00:52:02 -05:00
src Keep build-machine paths out of the product and package codes 2026-10-16 13:50:57 -05:00
tests Keep build-machine paths out of the product and package codes 2026-10-16 13:50:57 -05:00
tools Start zixl: a clean-room Zig reimplementation of msitools' wixl 2026-10-14 00:52:02 -05:00
.gitignore Start zixl: a clean-room Zig reimplementation of msitools' wixl 2026-10-14 00:52:02 -05:00
AGENTS.md WiX v4 registry, environment, shortcuts, icons and removals (#27) 2026-10-14 12:40:42 -05:00
build.zig Read each file's version and language from its version resource 2026-10-14 20:26:23 -05:00
build.zig.zon Generate GUIDs and keep output reproducible (#9) 2026-10-14 03:54:02 -05:00
build.zig.zon.nix Generate GUIDs and keep output reproducible (#9) 2026-10-14 03:54:02 -05:00
CONTRIBUTING.md Read each file's version and language from its version resource 2026-10-14 20:26:23 -05:00
flake.lock Build an installer package end to end (the tracer bullet, #2) 2026-10-14 02:44:05 -05:00
flake.nix WiX v4 registry, environment, shortcuts, icons and removals (#27) 2026-10-14 12:40:42 -05:00
GLOSSARY.md Build a WiX v4 <Package> (#24) 2026-10-14 12:05:28 -05:00
package.nix Build an installer package end to end (the tracer bullet, #2) 2026-10-14 02:44:05 -05:00
README.md Read each file's version and language from its version resource 2026-10-14 20:26:23 -05:00
REUSE.toml Report a <File> with no Source rather than read it 2026-10-14 12:49:50 -05:00

zixl

Windows Installer packages from WiX sources, in Zig.

zixl turns WiX source documents (.wxs) into .msi installer packages, as GNOME msitools' wixl [1] does for WiX v3, and for the WiX v4 language that every WiX since v4 reads. It is written in Zig 0.17 with no C libraries beneath it, so it runs on Linux and on Windows, and a Windows binary cross-compiles from Linux with nothing else installed.

Note

zixl is young. It builds real installer packages, which Windows Installer (under Wine, in its tests) installs, upgrades and uninstalls, but it implements only the parts of WiX listed below, and refuses everything else by name rather than leaving it out.

The API documentation, generated from the doc comments in the source, is at https://jeff.jcollie.page/zixl/.

A reimplementation, not a translation

zixl does what wixl does, but none of its code comes from wixl. wixl and the libmsi library under it are LGPL-2.1-or-later. zixl is written from the public specifications for Windows Installer, compound files, cabinets and the WiX schema, and wixl is only ever run to compare zixl's output against. For the one layer Microsoft never documented, the layout of the database inside its compound file, zixl follows rust-msi [3], which is MIT-licensed. That is what lets zixl be MIT-licensed and used as a dependency of any build.zig.

docs/adr/ records this and the other decisions that shape the project, and GLOSSARY.md defines the terms used throughout.

What it does

zixl builds a per-machine or per-user installer package from one or more WiX v3 sources:

  • The whole WiX preprocessor: define, undef, if, ifdef, ifndef, elseif, else, endif, include (searched beside the including file, then in each -I directory), foreach, warning and error, and the var., env. and sys. variables.
  • Products, packages, properties, features (with conditions), directories, components, component groups, files, fragments and the references that join them across sources, icons, shortcuts and their shell properties (ShortcutProperty, such as an AppUserModel ID), registry keys and values, environment variables, folders created and removed, major upgrades and launch conditions.
  • x86, x64 and arm64 packages, with x64 as the default.
  • Cabinets compressed with MSZIP or stored as they are, embedded in the package or kept beside it, on as many disks as the Media elements say.
  • Each program's and library's version and languages in the File table, read from its version resource, which is how Windows Installer decides whether an upgrade replaces the copy already installed; DefaultVersion and DefaultLanguage for a file that has none, and an MD5 hash for every file left without a version.
  • Strings stored in the code page the source chooses (1252 unless it says otherwise), with deterministic 8.3 short names for long file names.
  • Reproducible output: the same sources and files always give the same bytes. GUIDs left for zixl to generate are derived from the inputs rather than made up, and timestamps come from SOURCE_DATE_EPOCH.
  • The same options as wixl, wherever zixl implements the feature behind them.
  • A step for build.zig, so that a Zig project can build its program and the installer package holding it in one zig build.
  • Reading installer packages, zixl's own or any other tool's, back into their tables, rows and summary information. zixl-info lists a package's tables and streams, prints its summary information, exports a table as text or SQL and extracts a stream, as msitools' msiinfo does; and zixl-dump writes a whole package out as files, as msidump does.

Where wixl departs from what WiX and Windows Installer document -- it ignores Product/@Codepage, leaves a program's version out of the File table, leaves - off an environment variable that should be removed on uninstall, and treats a defined variable on its own as false in a condition, among others -- zixl follows the documentation. The issues and the .wixl-errata files in tests/e2e/fixtures record each one.

Tables are exported in the text archive (.idt) format as Microsoft documents it, which differs from what msitools writes in a few places: text in the database's code page, control characters translated, binary cells naming .ibd files. docs/adr/0006 lists them.

It builds WiX v4 sources too, in the language every WiX since v4 reads, which the reference implementation does not. A source's namespace says which language it is in. For WiX v4, zixl does what the current WiX documents:

  • <Package> with v4's defaults, <SummaryInformation>, <Launch> conditions, and features with <Level> conditions.
  • <StandardDirectory>, with ProgramFiles6432Folder following the package's bitness, and Subdirectory wherever a directory is named.
  • Components with Bitness, Condition and Feature; <Media> and <MediaTemplate>.
  • The registry, environment variables, shortcuts, icons, created folders and removals, as in WiX v3.
  • The defaults WiX v5 and later supply: a feature holding every component when none is written, an INSTALLFOLDER when one is used but not defined, and a component for a <File> written on its own.
  • !(bind.Property.NAME), and the preprocessor's sys.BUILDARCHSHORT and sys.WIXMAJORVERSION.

So a package can be as small as WiX's own template:

<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
  <Package Name="Example" Manufacturer="Example" Version="1.0.0" UpgradeCode="PUT-GUID-HERE">
    <MajorUpgrade DowngradeErrorMessage="A newer version is already installed." />
    <File Source="example.exe" />
  </Package>
</Wix>

What the documentation leaves too vague to reproduce is refused, with the reason: Package/@Id, <Files> and the default major upgrade, among others. docs/adr/0007 lists those cases, along with the few defaults zixl has to infer.

Not planned for now: the WixUI dialog sets, advertised shortcuts, custom actions, services, merge modules, patches, transforms and wixl-heat.

Building

The development shell comes from the flake, and has Zig 0.17.0 in it along with msitools and Wine for testing:

$ nix develop
$ zig build
$ ./zig-out/bin/zixl -o product.msi product.wxs
$ zig build -Dtarget=x86_64-windows

zixl --help lists the options. zixl-info --help lists what it can say about an installer package:

$ ./zig-out/bin/zixl-info tables product.msi
$ ./zig-out/bin/zixl-info suminfo product.msi
$ ./zig-out/bin/zixl-info export product.msi File
$ ./zig-out/bin/zixl-info extract product.msi product.cab > product.cab
$ ./zig-out/bin/zixl-dump -t -s -d product product.msi

zixl-dump writes each table as TABLE.idt, a binary cell's contents as TABLE/KEY.ibd, and, with -s, every stream into _Streams.

Times are printed in UTC. When -o is left out, the package is written beside its WiX source. SOURCE_DATE_EPOCH, when it is set, is the time recorded in the package (note that nix develop sets it to 1980).

zig build test runs the library's tests, zig build docs builds the API documentation into zig-out/docs, and zig build docs-serve serves it at http://127.0.0.1:8000/.

From a build.zig

A Zig project can make an installer package as part of its own build. Add zixl as a dependency:

$ zig fetch --save git+https://git.jcollie.dev/jeff/zixl.git

and call addInstallerPackage from build.zig. A file the build makes reaches the package through a preprocessor variable, used as a Source in the WiX source:

const zixl = @import("zixl");

pub fn build(b: *std.Build) void {
    // ...
    const installer = zixl.addInstallerPackage(b, .{
        .sources = &.{b.path("installer/product.wxs")},
        .name = "product.msi",
        .arch = .x64,
        .defines = &.{
            .{ .name = "ProgramExe", .value = .{ .file = exe.getEmittedBin() } },
            .{ .name = "Version", .value = .{ .text = "1.2.3" } },
        },
        .include_dirs = &.{b.path("installer/include")},
    });
    b.getInstallStep().dependOn(&b.addInstallDirectory(.{
        .source_dir = installer.directory,
        .install_dir = .prefix,
        .install_subdir = "installer",
    }).step);
}
<File Id="ProgramExe" Name="program.exe" Source="$(var.ProgramExe)" KeyPath="yes" />

installer.package is the .msi itself, and installer.directory the directory holding it along with any cabinets kept beside it. The step builds zixl for the machine running the build and runs it, so it works the same whatever the project is being built for. The package is made again when a WiX source, an included file, a file that a source path names, or a define changes, and not otherwise. Variables read from the environment through $(env.NAME) are not watched. Set .timestamp to make the package reproducible. tests/build is a complete example.

Testing

Three kinds of test:

  • zig build test drives the library from WiX source text to preprocessed text and to an installer database, checking the text, the tables, rows and summary information, and the diagnostics that come out. It writes every fixture's installer database and reads it back, and checks that what is not an installer package is refused with a reason.
  • zig build fuzz --fuzz fuzzes the preprocessor, the whole of building a package and reading one, from the fixtures as a seed corpus; whatever the input, each must produce its result or report what is wrong, never crash or hang.
  • nix flake check builds every WiX source in tests/e2e/fixtures with zixl and with wixl, dumps both installer packages with zixl-dump and compares them table by table, then has Windows Installer, running under Wine, install and uninstall zixl's package, checking that every file lands where it should with the right contents and is gone afterwards. An arm64 package, which wixl cannot build, must have the tables of the same source built for x64, but for the platform. A WiX v4 source, which wixl cannot read, has a twin written in WiX v3, and zixl must build the two into the same tables, apart from the differences WiX documents, each listed with its source. zixl's reader is itself checked against msitools: zixl-info and zixl-dump must read both tools' packages as msiinfo and msidump do, apart from the differences docs/adr/0006 records. The checks also cover what the command line refuses; that zixl -E preprocesses each source in tests/e2e/preprocess to what wixl -E does, compared in XML's canonical form; and that a project with zixl as a dependency (tests/build) makes its installer package with the build.zig step, and makes it again exactly when it should. Every fixture is built for x86, x64 and arm64, twice, and must come out byte for byte the same each time; and the Windows build of zixl, run under Wine, must write exactly the bytes the Linux build does. Wine runs headless inside the Nix build sandbox, so none of this needs a virtual machine.

docs/msi-layout.md describes how an installer database is laid out inside its compound file, which Microsoft never documented.

Where this lives

The code is at https://git.jcollie.dev/jeff/zixl:

git clone https://git.jcollie.dev/jeff/zixl.git

It is mirrored on Tangled at https://tangled.org/jcollie.dev/zixl, which is the place to report issues and send pull requests.

License

MIT. See LICENSES/MIT.txt. The project follows the REUSE specification.

References cited

  1. Lureau, M.-A. msitools (version 0.106). GNOME, 2025. https://gitlab.gnome.org/GNOME/msitools
  2. Lureau, M.-A. wixl, 2013. https://github.com/eliasp/wixl
  3. Steele, M. D. rust-msi (version 0.10.0), 2026. https://github.com/mdsteele/rust-msi
  4. Mensching, R. "Inside the MSI file format", 2003. https://robmensching.com/blog/posts/2003/11/25/inside-the-msi-file-format/
  5. Mensching, R. "Inside the MSI file format, again", 2004. https://robmensching.com/blog/posts/2004/2/10/inside-the-msi-file-format-again/
  6. Microsoft Corporation. [MS-CFB]: Compound File Binary File Format. https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/
  7. Microsoft Corporation. [MS-OLEPS]: Object Linking and Embedding (OLE) Property Set Data Structures. https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-oleps/
  8. Microsoft Corporation. "Windows Installer". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/msi/windows-installer-portal
  9. Microsoft Corporation. "Archive File Format". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/msi/archive-file-format
  10. Microsoft Corporation. "ASCII Data in Text Archive Files". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/msi/ascii-data-in-text-archive-files
  11. Microsoft Corporation. "SQL Syntax". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/msi/sql-syntax
  12. Microsoft Corporation. "MsiShortcutProperty Table". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/msi/msishortcutproperty-table
  13. FireGiant. WiX Toolset documentation: the WiX v4 schema reference, release notes and improvement proposals. https://docs.firegiant.com/wix/
  14. Microsoft Corporation. "PE Format". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
  15. Microsoft Corporation. "VS_VERSIONINFO structure". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo
  16. Microsoft Corporation. "VS_FIXEDFILEINFO structure (verrsrc.h)". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
  17. Microsoft Corporation. "VarFileInfo structure". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/menurc/varfileinfo
  18. Microsoft Corporation. "Var structure". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/menurc/var-str
  19. Microsoft Corporation. "File Table". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/msi/file-table
  20. Microsoft Corporation. "MsiFileHash Table". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/msi/msifilehash-table
  21. Microsoft Corporation. "File Versioning Rules". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/msi/file-versioning-rules
  22. FireGiant. "File element". FireGiant Documentation. https://docs.firegiant.com/wix/schema/wxs/file/