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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! WiX source text in, preprocessed text or diagnostics out: the behavior of//! the preprocessor, checked through the library's public interface.
const std = @import("std");const testing = std.testing;
const zixl = @import("zixl");
const Result = struct { arena: std.heap.ArenaAllocator, diags: zixl.Diagnostics, text: ?[]const u8,
fn deinit(r: *Result) void { r.arena.deinit(); testing.allocator.destroy(r); }
fn reported(r: *const Result, needle: []const u8) bool { var buf: [8192]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); r.diags.render(&w) catch return false; return std.mem.find(u8, w.buffered(), needle) != null; }};
const Env = struct { fn lookup(_: ?*const anyopaque, name: []const u8) ?[]const u8 { if (std.mem.eql(u8, name, "HOME_ISH")) return "/home/someone"; return null; }};
fn run(text: []const u8, defines: []const zixl.Define) !*Result { const r = try testing.allocator.create(Result); r.arena = .init(testing.allocator); r.diags = .init(r.arena.allocator()); const out = zixl.preprocess(r.arena.allocator(), &r.diags, .{ .path = "dir/product.wxs", .text = text }, .{ .defines = defines, .environment = .{ .lookupFn = Env.lookup }, .current_dir = "/build", .arch = .arm64, }) catch null; r.text = if (out) |o| o.text else null; return r;}
fn expectOutput(input: []const u8, defines: []const zixl.Define, expected: []const u8) !void { const r = try run(input, defines); defer r.deinit(); try testing.expectEqualStrings(expected, r.text.?);}
test "text without directives passes through untouched" { const text = "<?xml version=\"1.0\"?>\n<Wix>\n <!-- $(var.Nope) -->\n <![CDATA[$(var.Nope)]]>\n</Wix>\n"; try expectOutput(text, &.{}, text);}
test "define, then use" { try expectOutput( \\<?define A = "quoted" ?><?define B=unquoted?><?define C = "" ?><a x="$(var.A)" y="$(var.B)" z="[$(var.C)]"/> , &.{}, \\<a x="quoted" y="unquoted" z="[]"/> );}
test "a define's value can use other variables" { try expectOutput("<?define A = \"x\" ?><?define B = \"$(var.A)y\" ?>$(var.B)", &.{}, "xy");}
test "a value is escaped for where it lands" { try expectOutput( \\<?define V = "a & b < c > "d" 'e'" ?><a x="$(var.V)" y='$(var.V)'>$(var.V)</a> , &.{}, \\<a x="a & b < c > "d" 'e'" y='a & b < c > "d" 'e''>a & b < c > "d" 'e'</a> );}
test "undef forgets a variable, and a later define replaces it" { try expectOutput("<?define A = \"1\" ?>$(var.A)<?undef A?><?define A = \"2\" ?>$(var.A)", &.{}, "12");}
test "command-line defines are visible, and a later define replaces them" { try expectOutput("$(var.M)<?define M = \"source\" ?>$(var.M)", &.{.{ .name = "M", .value = "cmdline" }}, "cmdlinesource");}
test "environment variables" { try expectOutput("<a v=\"$(env.HOME_ISH)\"/>", &.{}, "<a v=\"/home/someone\"/>");}
test "system variables, the directories ending with a separator" { const sep = std.fs.path.sep_str; try expectOutput( "$(sys.CURRENTDIR)|$(sys.SOURCEFILEPATH)|$(sys.SOURCEFILEDIR)|$(sys.BUILDARCH)", &.{}, "/build" ++ sep ++ "|/build" ++ sep ++ "dir/product.wxs|/build" ++ sep ++ "dir" ++ sep ++ "|arm64", );}
test "$$( is a literal $(" { try expectOutput("<a v=\"$$(var.Name)\"/>", &.{}, "<a v=\"$(var.Name)\"/>");}
test "the XML declaration and other instructions pass through" { try expectOutput("<?xml version=\"1.0\"?><?other thing?>", &.{}, "<?xml version=\"1.0\"?><?other thing?>");}
// -- what is reported --------------------------------------------------------
test "an undefined variable is an error naming it and where it is" { const r = try run("<a>\n <b v=\"$(var.Missing)\"/>\n</a>", &.{}); defer r.deinit(); try testing.expect(r.text == null); try testing.expect(r.reported("dir/product.wxs:2:9: error: the variable $(var.Missing) is not defined"));}
test "an unset environment variable is an error" { const r = try run("$(env.NOT_SET)", &.{}); defer r.deinit(); try testing.expect(r.reported("1:1: error: the environment variable $(env.NOT_SET) is not set"));}
test "a reference with no kind" { const r = try run("$(Name)", &.{}); defer r.deinit(); try testing.expect(r.reported("$(Name) names no kind of variable"));}
test "an unknown system variable" { const r = try run("$(sys.PLATFORM)", &.{}); defer r.deinit(); try testing.expect(r.reported("$(sys.PLATFORM) is not a system variable"));}
test "an unclosed reference" { const r = try run("x $(var.A", &.{}); defer r.deinit(); try testing.expect(r.reported("1:3: error: a variable reference is not closed"));}
test "a define without a value defines it as empty" { try expectOutput("<?define A?>[$(var.A)]<?ifdef A?>defined<?endif?>", &.{}, "[]defined");}
test "a define of something that is not a name" { const r = try run("<?define 1x = \"v\" ?>", &.{}); defer r.deinit(); try testing.expect(r.reported("\"1x\" is not a variable name"));}
test "undef of something never defined" { const r = try run("<?undef Ghost?>", &.{}); defer r.deinit(); try testing.expect(r.reported("<?undef Ghost?>: the variable is not defined"));}
test "error stops the build where it is written" { const r = try run("<?define V = \"2\" ?>\n\n <?error Version $(var.V) is too old?>", &.{}); defer r.deinit(); try testing.expect(r.text == null); try testing.expect(r.reported("dir/product.wxs:3:3: error: Version 2 is too old"));}
test "warning is reported and the source goes on" { const r = try run("<?warning careful?>ok", &.{}); defer r.deinit(); try testing.expectEqualStrings("ok", r.text.?); try testing.expect(r.reported("dir/product.wxs:1:1: warning: careful"));}
test "an unclosed processing instruction" { const r = try run("<?define A = \"1\"", &.{}); defer r.deinit(); try testing.expect(r.reported("a processing instruction is not closed"));}
test "compiling reports at the WiX source's own lines, after preprocessing" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); var diags: zixl.Diagnostics = .init(arena.allocator()); var files: zixl.FileSource.Memory = .{ .files = &.{.{ .path = "p.wxs", .contents = \\<?xml version="1.0"?> \\<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"> \\ <?define Long = "a value much longer than the reference it replaces" ?> \\ <Product Id="{11111111-2222-3333-4444-555555555555}" Name="$(var.Long)" Language="1033" Version="1" Manufacturer="m" Bogus="1"> \\ <Package Id="{BBBBBBBB-CCCC-DDDD-EEEE-FFFFFFFFFFFF}" /> \\ </Product> \\</Wix> }} }; try testing.expectError(error.Invalid, zixl.buildDatabase(arena.allocator(), &diags, .{ .sources = &.{"p.wxs"}, .files = files.fileSource(), .timestamp = 0, })); var buf: [1024]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); try diags.render(&w); // `Bogus` is at column 120 of line 4 in the source as written, whatever // the substitution before it did to the preprocessed text. try testing.expect(std.mem.find(u8, w.buffered(), "p.wxs:4:120: error: attribute Bogus of <Product> is not supported") != null);}
// -- conditionals -----------------------------------------------------------
const ab = [_]zixl.Define{ .{ .name = "A", .value = "1" }, .{ .name = "B", .value = "good VAR" } };
test "if, elseif, else" { const t = "<?if $(var.A) = 2 ?>two<?elseif $(var.A) = 1 ?>one<?else?>other<?endif?>"; try expectOutput(t, &ab, "one"); try expectOutput(t, &.{.{ .name = "A", .value = "2" }}, "two"); try expectOutput(t, &.{.{ .name = "A", .value = "3" }}, "other");}
test "ifdef and ifndef" { try expectOutput("<?ifdef A?>yes<?else?>no<?endif?>|<?ifndef A?>yes<?else?>no<?endif?>", &ab, "yes|no"); try expectOutput("<?ifdef Z?>yes<?else?>no<?endif?>|<?ifndef Z?>yes<?else?>no<?endif?>", &ab, "no|yes");}
test "nested conditionals, including inside a branch not taken" { try expectOutput( "<?if $(var.A) = 1 ?>[<?if $(var.B) = x ?>inner-x<?else?>inner-else<?endif?>]<?else?><?if $(var.A) = 1 ?>never<?endif?><?endif?>", &ab, "[inner-else]", );}
test "the comparison operators, as WiX documents them" { const cases = [_]struct { []const u8, bool }{ .{ "$(var.A) = 1", true }, .{ "$(var.A) = \"1\"", true }, .{ "$(var.A) != 1", false }, .{ "$(var.B) = \"good VAR\"", true }, .{ "$(var.B) = \"GOOD var\"", false }, .{ "$(var.B) ~= \"GOOD var\"", true }, .{ "3 < $(var.A)", false }, .{ "$(var.A) < 3", true }, .{ "$(var.A) <= 1", true }, .{ "$(var.A) >= 2", false }, .{ "$(var.A) > 0", true }, .{ "not \"x\"= \"y\"", true }, .{ "$(var.A)", true }, .{ "$(var.Missing)", false }, .{ "$(var.A) and not $(var.Missing)", true }, .{ "$(var.A) Or ($(var.B) And $(var.A) >=3)", true }, .{ "$(var.Missing) or $(var.A) = 1 AND $(var.A) = 2", false }, .{ "NOT ($(var.A) = 1 or $(var.A) = 2)", false }, }; for (cases) |c| { var buf: [256]u8 = undefined; const text = try std.fmt.bufPrint(&buf, "<?if {s} ?>T<?else?>F<?endif?>", .{c[0]}); const r = try run(text, &ab); defer r.deinit(); std.testing.expectEqualStrings(if (c[1]) "T" else "F", r.text.?) catch |e| { std.debug.print("condition: {s}\n", .{c[0]}); return e; }; }}
test "comparing with an undefined variable is an error" { const r = try run("<?if $(var.Missing) = 1 ?>x<?endif?>", &ab); defer r.deinit(); try testing.expect(r.reported("1:1: error: the variable $(var.Missing) is not defined"));}
test "an inequality needs integers" { const r = try run("<?if $(var.B) < 3 ?>x<?endif?>", &ab); defer r.deinit(); try testing.expect(r.reported("< compares integers, and \"good VAR\" is not one"));}
test "a malformed condition" { const r = try run("<?if = 3 ?>x<?endif?>", &ab); defer r.deinit(); try testing.expect(r.reported("a condition cannot begin there"));}
test "an if that is never closed" { const r = try run("a\n<?if $(var.A) = 1 ?>x", &ab); defer r.deinit(); try testing.expect(r.text == null); try testing.expect(r.reported("2:1: error: <?if?> is never closed with <?endif?>"));}
test "endif, else and endforeach with nothing open" { for ([_][]const u8{ "<?endif?>", "<?else?>", "<?elseif 1 = 1?>", "<?endforeach?>" }) |t| { const r = try run(t, &ab); defer r.deinit(); try testing.expect(r.reported("has no <?")); }}
test "else must come last" { const r = try run("<?if $(var.A) = 1 ?>a<?else?>b<?elseif $(var.A) = 2 ?>c<?endif?>", &ab); defer r.deinit(); try testing.expect(r.reported("<?else?> must be the last branch"));}
test "directives in comments are not directives" { try expectOutput("<!-- <?if $(var.Nope) = 1 ?> -->ok", &.{}, "<!-- <?if $(var.Nope) = 1 ?> -->ok");}
// -- foreach -----------------------------------------------------------------
test "foreach over a list" { try expectOutput("<?foreach L in 1033;1041;1055?>[$(var.L)]<?endforeach?>", &.{}, "[1033][1041][1055]");}
test "foreach over a variable's list, nested, with nothing leaking out" { try expectOutput( "<?define List=a;b?><?foreach X in $(var.List)?><?foreach Y in 1;2?>$(var.X)$(var.Y) <?endforeach?><?define Inside = 1?><?endforeach?><?ifdef Inside?>leaked<?endif?><?ifdef X?>leaked<?endif?>", &.{}, "a1 a2 b1 b2 ", );}
test "a malformed foreach" { const r = try run("<?foreach L of 1;2?>x<?endforeach?>", &.{}); defer r.deinit(); try testing.expect(r.reported("<?foreach?> is written <?foreach NAME in A;B;C?>"));}
test "a foreach that is never closed" { const r = try run("<?foreach L in 1;2?>x", &.{}); defer r.deinit(); try testing.expect(r.reported("<?foreach?> is never closed with <?endforeach?>"));}
// -- includes ----------------------------------------------------------------
const Included = struct { arena: std.heap.ArenaAllocator, diags: zixl.Diagnostics, text: ?[]const u8,
fn reported(r: *const Included, needle: []const u8) bool { var buf: [8192]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); r.diags.render(&w) catch return false; return std.mem.find(u8, w.buffered(), needle) != null; }};
fn runWithFiles(r: *Included, main: []const u8, files: []const zixl.FileSource.Memory.File, include_dirs: []const []const u8) !void { r.arena = .init(testing.allocator); r.diags = .init(r.arena.allocator()); const all = try r.arena.allocator().alloc(zixl.FileSource.Memory.File, files.len); @memcpy(all, files); const memory = try r.arena.allocator().create(zixl.FileSource.Memory); memory.* = .{ .files = all }; const out = zixl.preprocess(r.arena.allocator(), &r.diags, .{ .path = "src/main.wxs", .text = main }, .{ .files = memory.fileSource(), .include_dirs = include_dirs, }) catch null; r.text = if (out) |o| o.text else null;}
const wxi_header = "<?xml version=\"1.0\"?>\n<!-- shared -->\n";
test "an include beside the including file, and its variables" { var r: Included = undefined; try runWithFiles(&r, "<?define Who = \"me\" ?><a><?include part.wxi ?></a>", &.{ .{ .path = "src/part.wxi", .contents = wxi_header ++ "<Include xmlns=\"http://schemas.microsoft.com/wix/2006/wi\"><b v=\"$(var.Who)\"/></Include>\n" }, }, &.{}); defer r.arena.deinit(); try testing.expectEqualStrings("<a><b v=\"me\"/></a>", r.text.?);}
test "an include found only through an include directory" { var r: Included = undefined; try runWithFiles(&r, "<a><?include common\\part.wxi?></a>", &.{ .{ .path = "shared/common/part.wxi", .contents = "<Include><b/></Include>" }, }, &.{ "elsewhere", "shared" }); defer r.arena.deinit(); try testing.expectEqualStrings("<a><b/></a>", r.text.?);}
test "an include found nowhere" { var r: Included = undefined; try runWithFiles(&r, "<a>\n <?include missing.wxi?></a>", &.{}, &.{"shared"}); defer r.arena.deinit(); try testing.expect(r.text == null); try testing.expect(r.reported("src/main.wxs:2:3: error: the included file \"missing.wxi\" is not beside src/main.wxs nor in any include directory"));}
test "an error two includes deep names the whole chain" { var r: Included = undefined; try runWithFiles(&r, "<a>\n\n <?include middle.wxi?></a>", &.{ .{ .path = "src/middle.wxi", .contents = "<Include>\n <?include inner.wxi?>\n</Include>" }, .{ .path = "src/inner.wxi", .contents = "<Include>\n\n\n <b v=\"$(var.Nope)\"/>\n</Include>" }, }, &.{}); defer r.arena.deinit(); try testing.expect(r.reported("src/inner.wxi:4:9: error: the variable $(var.Nope) is not defined (included from src/middle.wxi:2, included from src/main.wxs:3)"));}
test "a file that includes itself" { var r: Included = undefined; try runWithFiles(&r, "<a><?include loop.wxi?></a>", &.{ .{ .path = "src/loop.wxi", .contents = "<Include><?include loop.wxi?></Include>" }, }, &.{}); defer r.arena.deinit(); try testing.expect(r.reported("src/loop.wxi includes itself"));}
test "an included file must have Include at its root" { var r: Included = undefined; try runWithFiles(&r, "<a><?include bad.wxi?></a>", &.{ .{ .path = "src/bad.wxi", .contents = "<Fragment/>" }, }, &.{}); defer r.arena.deinit(); try testing.expect(r.reported("an included file's root element must be <Include>"));}
test "compile errors in an included file name the chain" { var arena: std.heap.ArenaAllocator = .init(testing.allocator); defer arena.deinit(); var diags: zixl.Diagnostics = .init(arena.allocator()); var files: zixl.FileSource.Memory = .{ .files = &.{ .{ .path = "p.wxs", .contents = \\<?xml version="1.0"?> \\<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"> \\ <Product Id="{11111111-2222-3333-4444-555555555555}" Name="n" Language="1033" Version="1" Manufacturer="m"> \\ <Package Id="{BBBBBBBB-CCCC-DDDD-EEEE-FFFFFFFFFFFF}" /> \\ <?include features.wxi?> \\ </Product> \\</Wix> }, .{ .path = "features.wxi", .contents = "<Include>\n <Feature Id=\"F\" Bogus=\"1\" />\n</Include>" }, } }; try testing.expectError(error.Invalid, zixl.buildDatabase(arena.allocator(), &diags, .{ .sources = &.{"p.wxs"}, .files = files.fileSource(), .timestamp = 0, })); var buf: [1024]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); try diags.render(&w); testing.expect(std.mem.find(u8, w.buffered(), "features.wxi:2:19: error: attribute Bogus of <Feature> is not supported (included from p.wxs:5)") != null) catch |e| { std.debug.print("{s}", .{w.buffered()}); return e; };}