Something went wrong. Try again.
Builds Windows Installer packages from WiX sources on Linux or Windows, in pure Zig; a clean-room reimplementation of msitools' wixl.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Windows Installer packages from WiX sources, in Zig.//!//! zixl does what GNOME msitools' `wixl` does — it turns WiX v3 source//! documents into `.msi` installer packages — as a clean-room implementation//! written from the public specifications, with no C libraries beneath it.//!//! A WiX source goes through four stages on its way to an installer package://!//! 1. **Preprocessing** expands variables, conditionals and includes, leaving//! plain XML: `preprocess`.//! 2. **Compiling** turns that XML into a WiX document, with every reference//! bound and every value checked: `compile`.//! 3. **Linking** turns the WiX document into an installer database: tables//! of rows plus summary information, with no bytes yet: `link`.//! 4. **Writing** lays the installer database and the files it names out as//! the bytes of an installer package: `write`.//!//! `build` runs all four, and `read` goes the other way, from an installer//! package's bytes back to its installer database. Every stage reports what is wrong with its input//! as `Diagnostics`, and reads files only through a `FileSource`.
const std = @import("std");const Allocator = std.mem.Allocator;const Io = std.Io;
pub const Database = @import("Database.zig");pub const Diagnostics = @import("Diagnostics.zig");pub const FileSource = @import("FileSource.zig");pub const wix = @import("wix.zig");pub const schema = @import("schema.zig");
pub const preprocess = @import("preprocess.zig").preprocess;pub const PreprocessOptions = @import("preprocess.zig").Options;pub const PreprocessOutput = @import("preprocess.zig").Output;pub const Define = @import("preprocess.zig").Define;pub const Environment = @import("preprocess.zig").Environment;pub const SourceMap = @import("preprocess.zig").SourceMap;pub const compile = @import("compile.zig").compile;pub const Source = @import("compile.zig").Source;pub const link = @import("link.zig").link;pub const Arch = @import("link.zig").Arch;pub const LinkOptions = @import("link.zig").Options;pub const write = @import("write.zig").write;pub const writeWithCabinets = @import("write.zig").writeWithCabinets;pub const CabinetWriter = @import("write.zig").CabinetWriter;pub const read = @import("read.zig").read;pub const Package = @import("read.zig").Package;pub const Stream = @import("read.zig").Stream;pub const archive = @import("archive.zig");pub const pe = @import("pe.zig");
/// The version of zixl, from `build.zig.zon`.pub const version: []const u8 = @import("build_options").version;
pub const BuildOptions = struct { /// The WiX sources to build into one installer package: one holds the /// <Product>, and the others fragments it refers to. sources: []const []const u8, files: FileSource, arch: Arch = .x64, /// Variables defined before preprocessing, as `-D` defines them. defines: []const Define = &.{}, environment: Environment = .empty, /// The directory the build runs in, for `$(sys.CURRENTDIR)`. current_dir: []const u8 = "", /// Where `<?include?>` looks for a file not beside the one including it. include_dirs: []const []const u8 = &.{}, /// When the installer package is made, in seconds since the Unix epoch. timestamp: i64,};
pub const BuildError = error{ /// The build failed; why is in the diagnostics. Invalid,} || Allocator.Error || Io.Writer.Error;
/// Reads the WiX sources through `options.files`, and writes the installer/// package it describes to `w`. Everything allocated is put in `arena`.pub fn build(arena: Allocator, diags: *Diagnostics, options: BuildOptions, w: *Io.Writer) BuildError!void { const db = try buildDatabase(arena, diags, options); try write(arena, diags, &db, w);}
/// Runs every stage but writing, returning the installer database.pub fn buildDatabase(arena: Allocator, diags: *Diagnostics, options: BuildOptions) error{Invalid}!Database { return buildDatabaseOrOom(arena, diags, options) catch |e| switch (e) { error.Invalid => error.Invalid, error.OutOfMemory => { diags.err(null, "out of memory", .{}) catch {}; return error.Invalid; }, };}
fn buildDatabaseOrOom(arena: Allocator, diags: *Diagnostics, options: BuildOptions) (error{Invalid} || Allocator.Error)!Database { // Each source is preprocessed on its own, with the same definitions to // start from, as WiX does. const sources = try arena.alloc(Source, options.sources.len); var failed = false; for (options.sources, sources) |path, *source| { const text = options.files.read(arena, path) catch |e| switch (e) { error.OutOfMemory => return error.OutOfMemory, error.FileNotFound => { try diags.err(null, "{s}: no such file", .{path}); failed = true; continue; }, error.ReadFailed => { try diags.err(null, "{s}: cannot be read", .{path}); failed = true; continue; }, }; const pre = preprocess(arena, diags, .{ .path = path, .text = text }, .{ .defines = options.defines, .environment = options.environment, .current_dir = options.current_dir, .arch = options.arch, .files = options.files, .include_dirs = options.include_dirs, }) catch |e| switch (e) { error.OutOfMemory => return error.OutOfMemory, error.Invalid => { failed = true; continue; }, }; const map = try arena.create(SourceMap); map.* = pre.map; source.* = .{ .path = path, .text = pre.text, .map = map }; } if (failed) return error.Invalid; const document = try compile(arena, diags, options.files, sources, options.arch); return link(arena, diags, document, .{ .arch = options.arch, .timestamp = options.timestamp, .creating_application = "zixl " ++ version, });}
test { std.testing.refAllDecls(@This()); _ = @import("write/cab.zig"); _ = @import("write/deflate.zig"); _ = @import("write/cfb.zig"); _ = @import("shortname.zig"); _ = @import("codepage.zig"); _ = @import("names.zig"); _ = @import("read/cfb.zig"); _ = @import("read/summary.zig"); _ = @import("read.zig"); _ = @import("archive.zig"); _ = @import("pe.zig"); _ = @import("write/summary.zig");}