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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Where zixl gets the files it reads: WiX sources, included files, and the//! files named by source paths.//!//! zixl never opens a file itself. The command line supplies `Disk`, and//! tests supply `Memory`, so that each decides where files come from and the//! library does not have to know. The command line wraps its `Disk` in a//! `Recording`, which is how the `build.zig` step learns what a package was//! made from.
const std = @import("std");const Allocator = std.mem.Allocator;const Io = std.Io;
const FileSource = @This();
ptr: *anyopaque,vtable: *const VTable,
pub const Error = error{ FileNotFound, /// Anything else that stopped the file being read. ReadFailed,} || Allocator.Error;
pub const VTable = struct { /// The whole contents of the file at `path`, allocated with `gpa`. read: *const fn (ptr: *anyopaque, gpa: Allocator, path: []const u8) Error![]u8,};
pub fn read(fs: FileSource, gpa: Allocator, path: []const u8) Error![]u8 { return fs.vtable.read(fs.ptr, gpa, path);}
/// Files held in memory, keyed by path exactly as it is asked for.pub const Memory = struct { files: []const File,
pub const File = struct { path: []const u8, contents: []const u8, };
pub fn fileSource(m: *Memory) FileSource { return .{ .ptr = m, .vtable = &.{ .read = readMemory } }; }
fn readMemory(ptr: *anyopaque, gpa: Allocator, path: []const u8) Error![]u8 { const m: *Memory = @ptrCast(@alignCast(ptr)); for (m.files) |f| { if (std.mem.eql(u8, f.path, path)) return gpa.dupe(u8, f.contents); } return error.FileNotFound; }};
/// Files on disk, with relative paths taken from the current directory.pub const Disk = struct { io: Io,
pub fn fileSource(d: *Disk) FileSource { return .{ .ptr = d, .vtable = &.{ .read = readDisk } }; }
fn readDisk(ptr: *anyopaque, gpa: Allocator, path: []const u8) Error![]u8 { const d: *Disk = @ptrCast(@alignCast(ptr)); return Io.Dir.cwd().readFileAlloc(d.io, path, gpa, .unlimited) catch |e| switch (e) { error.FileNotFound => error.FileNotFound, error.OutOfMemory => error.OutOfMemory, else => error.ReadFailed, }; }};
/// Another file source, noting the path of every file it hands over, so that/// whatever builds a package can say afterwards what the package was made/// from. Files asked for and not found are not noted.pub const Recording = struct { inner: FileSource, gpa: Allocator, paths: std.ArrayList([]const u8) = .empty,
pub fn fileSource(r: *Recording) FileSource { return .{ .ptr = r, .vtable = &.{ .read = readRecording } }; }
pub fn deinit(r: *Recording) void { for (r.paths.items) |p| r.gpa.free(p); r.paths.deinit(r.gpa); }
fn readRecording(ptr: *anyopaque, gpa: Allocator, path: []const u8) Error![]u8 { const r: *Recording = @ptrCast(@alignCast(ptr)); const contents = try r.inner.read(gpa, path); errdefer gpa.free(contents); for (r.paths.items) |p| { if (std.mem.eql(u8, p, path)) return contents; } const copy = try r.gpa.dupe(u8, path); errdefer r.gpa.free(copy); try r.paths.append(r.gpa, copy); return contents; }};
test "memory" { var m: Memory = .{ .files = &.{.{ .path = "a.wxs", .contents = "<Wix/>" }} }; const fs = m.fileSource(); const got = try fs.read(std.testing.allocator, "a.wxs"); defer std.testing.allocator.free(got); try std.testing.expectEqualStrings("<Wix/>", got); try std.testing.expectError(error.FileNotFound, fs.read(std.testing.allocator, "b.wxs"));}
test "recording" { const gpa = std.testing.allocator; var m: Memory = .{ .files = &.{ .{ .path = "a.wxs", .contents = "<Wix/>" }, .{ .path = "b.wxi", .contents = "<Include/>" }, } }; var r: Recording = .{ .inner = m.fileSource(), .gpa = gpa }; defer r.deinit(); const fs = r.fileSource(); for ([_][]const u8{ "a.wxs", "b.wxi", "a.wxs" }) |path| gpa.free(try fs.read(gpa, path)); try std.testing.expectError(error.FileNotFound, fs.read(gpa, "c.wxs")); try std.testing.expectEqual(2, r.paths.items.len); try std.testing.expectEqualStrings("a.wxs", r.paths.items[0]); try std.testing.expectEqualStrings("b.wxi", r.paths.items[1]);}