// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // 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 /// , 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 `` 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"); }