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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The errors and warnings a stage reports about its input.//!//! A stage that finds something wrong with a WiX source adds a diagnostic//! here, naming where it is, and carries on when it can, so that one run//! reports as much as it can find. It returns `error.Invalid` once it cannot//! go on, or at the end when any error was added. Nothing is printed: the//! caller decides how diagnostics reach a person, with `render` as the//! conventional `file:line:column: severity: message` form.
const std = @import("std");const Allocator = std.mem.Allocator;
const Diagnostics = @This();
/// Where every message and copied path is allocated. An arena is the natural/// choice, since diagnostics live as long as the build that produced them.gpa: Allocator,list: std.ArrayList(Diagnostic) = .empty,
pub const Severity = enum { @"error", warning,};
/// A position in a WiX source, counted from 1. The column counts bytes.pub const Location = struct { path: []const u8, line: u32, column: u32, /// For a position in an included file, the `<?include?>` that brought /// it in, which may itself be in an included file. included_from: ?*const Location = null,
/// The location of byte `offset` in `text`, which was read from `path`. pub fn at(path: []const u8, text: []const u8, offset: usize) Location { var line: u32 = 1; var line_start: usize = 0; const end = @min(offset, text.len); for (text[0..end], 0..) |c, i| { if (c == '\n') { line += 1; line_start = i + 1; } } return .{ .path = path, .line = line, .column = @intCast(end - line_start + 1) }; }};
pub const Diagnostic = struct { severity: Severity, /// Absent for a problem that belongs to no particular place, such as an /// option given on the command line. location: ?Location, message: []const u8,};
pub fn init(gpa: Allocator) Diagnostics { return .{ .gpa = gpa };}
pub fn add( d: *Diagnostics, severity: Severity, location: ?Location, comptime fmt: []const u8, args: anytype,) Allocator.Error!void { try d.list.append(d.gpa, .{ .severity = severity, .location = location, .message = try std.fmt.allocPrint(d.gpa, fmt, args), });}
pub fn err(d: *Diagnostics, location: ?Location, comptime fmt: []const u8, args: anytype) Allocator.Error!void { try d.add(.@"error", location, fmt, args);}
pub fn warn(d: *Diagnostics, location: ?Location, comptime fmt: []const u8, args: anytype) Allocator.Error!void { try d.add(.warning, location, fmt, args);}
pub fn hasErrors(d: *const Diagnostics) bool { for (d.list.items) |item| { if (item.severity == .@"error") return true; } return false;}
/// Writes each diagnostic on a line of its own, as/// `path:line:column: error: message`.pub fn render(d: *const Diagnostics, w: *std.Io.Writer) std.Io.Writer.Error!void { for (d.list.items) |item| { if (item.location) |loc| { try w.print("{s}:{d}:{d}: ", .{ loc.path, loc.line, loc.column }); } else { try w.writeAll("zixl: "); } try w.print("{s}: {s}", .{ @tagName(item.severity), item.message }); if (item.location) |loc| { var from = loc.included_from; var first = true; while (from) |f| : (from = f.included_from) { try w.print("{s}included from {s}:{d}", .{ if (first) " (" else ", ", f.path, f.line }); first = false; } if (!first) try w.writeAll(")"); } try w.writeAll("\n"); }}
test "a location is counted from 1, in lines and bytes" { const text = "<a>\n <b/>\n</a>\n"; const loc: Location = .at("x.wxs", text, std.mem.find(u8, text, "<b").?); try std.testing.expectEqual(@as(u32, 2), loc.line); try std.testing.expectEqual(@as(u32, 3), loc.column);}
test "an included file's location names what included it" { var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); defer arena.deinit(); var d: Diagnostics = .init(arena.allocator()); const main: Location = .{ .path = "main.wxs", .line = 12, .column = 3 }; const middle: Location = .{ .path = "middle.wxi", .line = 4, .column = 5, .included_from = &main }; try d.err(.{ .path = "inner.wxi", .line = 7, .column = 9, .included_from = &middle }, "bad", .{}); var buf: [256]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); try d.render(&w); try std.testing.expectEqualStrings( "inner.wxi:7:9: error: bad (included from middle.wxi:4, included from main.wxs:12)\n", w.buffered(), );}
test "render" { var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); defer arena.deinit(); var d: Diagnostics = .init(arena.allocator()); try d.err(.{ .path = "a.wxs", .line = 3, .column = 5 }, "unknown element '{s}'", .{"Frob"}); try d.warn(null, "nothing to do", .{}); var buf: [256]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); try d.render(&w); try std.testing.expectEqualStrings( "a.wxs:3:5: error: unknown element 'Frob'\nzixl: warning: nothing to do\n", w.buffered(), ); try std.testing.expect(d.hasErrors());}