- Zig 92.7%
- Shell 5%
- Nix 2.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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 |
||
| .forgejo/workflows | ||
| docs | ||
| LICENSES | ||
| src | ||
| tests | ||
| tools | ||
| .gitignore | ||
| AGENTS.md | ||
| build.zig | ||
| build.zig.zon | ||
| build.zig.zon.nix | ||
| CONTRIBUTING.md | ||
| flake.lock | ||
| flake.nix | ||
| GLOSSARY.md | ||
| package.nix | ||
| README.md | ||
| REUSE.toml | ||
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-Idirectory),foreach,warninganderror, and thevar.,env.andsys.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
Mediaelements 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;
DefaultVersionandDefaultLanguagefor 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 onezig build. - Reading installer packages, zixl's own or any other tool's, back into
their tables, rows and summary information.
zixl-infolists a package's tables and streams, prints its summary information, exports a table as text or SQL and extracts a stream, as msitools'msiinfodoes; andzixl-dumpwrites a whole package out as files, asmsidumpdoes.
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>, withProgramFiles6432Folderfollowing the package's bitness, andSubdirectorywherever a directory is named.- Components with
Bitness,ConditionandFeature;<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
INSTALLFOLDERwhen one is used but not defined, and a component for a<File>written on its own. !(bind.Property.NAME), and the preprocessor'ssys.BUILDARCHSHORTandsys.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 testdrives 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 --fuzzfuzzes 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 checkbuilds every WiX source intests/e2e/fixtureswith zixl and with wixl, dumps both installer packages withzixl-dumpand 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-infoandzixl-dumpmust read both tools' packages asmsiinfoandmsidumpdo, apart from the differencesdocs/adr/0006records. The checks also cover what the command line refuses; thatzixl -Epreprocesses each source intests/e2e/preprocessto whatwixl -Edoes, compared in XML's canonical form; and that a project with zixl as a dependency (tests/build) makes its installer package with thebuild.zigstep, 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
- Lureau, M.-A. msitools (version 0.106). GNOME, 2025. https://gitlab.gnome.org/GNOME/msitools
- Lureau, M.-A. wixl, 2013. https://github.com/eliasp/wixl
- Steele, M. D. rust-msi (version 0.10.0), 2026. https://github.com/mdsteele/rust-msi
- Mensching, R. "Inside the MSI file format", 2003. https://robmensching.com/blog/posts/2003/11/25/inside-the-msi-file-format/
- Mensching, R. "Inside the MSI file format, again", 2004. https://robmensching.com/blog/posts/2004/2/10/inside-the-msi-file-format-again/
- Microsoft Corporation. [MS-CFB]: Compound File Binary File Format. https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/
- Microsoft Corporation. [MS-OLEPS]: Object Linking and Embedding (OLE) Property Set Data Structures. https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-oleps/
- Microsoft Corporation. "Windows Installer". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/msi/windows-installer-portal
- Microsoft Corporation. "Archive File Format". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/msi/archive-file-format
- 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
- Microsoft Corporation. "SQL Syntax". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/msi/sql-syntax
- Microsoft Corporation. "MsiShortcutProperty Table". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/msi/msishortcutproperty-table
- FireGiant. WiX Toolset documentation: the WiX v4 schema reference, release notes and improvement proposals. https://docs.firegiant.com/wix/
- Microsoft Corporation. "PE Format". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
- Microsoft Corporation. "VS_VERSIONINFO structure". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo
- Microsoft Corporation. "VS_FIXEDFILEINFO structure (verrsrc.h)". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
- Microsoft Corporation. "VarFileInfo structure". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/menurc/varfileinfo
- Microsoft Corporation. "Var structure". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/menurc/var-str
- Microsoft Corporation. "File Table". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/msi/file-table
- Microsoft Corporation. "MsiFileHash Table". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/msi/msifilehash-table
- Microsoft Corporation. "File Versioning Rules". Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/msi/file-versioning-rules
- FireGiant. "File element". FireGiant Documentation. https://docs.firegiant.com/wix/schema/wxs/file/