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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Linking: turning a WiX document into an installer database.
const std = @import("std");const Allocator = std.mem.Allocator;
const Database = @import("Database.zig");const Value = Database.Value;const Diagnostics = @import("Diagnostics.zig");const schema = @import("schema.zig");const pe = @import("pe.zig");const shortname = @import("shortname.zig");const wix = @import("wix.zig");
pub const Arch = enum { x86, x64, arm64,
/// The platform half of the summary information's template. pub fn platform(arch: Arch) []const u8 { return switch (arch) { .x86 => "Intel", .x64 => "x64", .arm64 => "Arm64", }; }
/// The oldest Windows Installer, times 100, that installs a package for /// the architecture: 2.0 for x64, 5.0 for arm64. pub fn minimumInstallerVersion(arch: Arch) i32 { return switch (arch) { .x86 => 100, .x64 => 200, .arm64 => 500, }; }};
pub const Options = struct { arch: Arch = .x64, /// When the installer package is made, in seconds since the Unix epoch. /// The caller decides, so that a build can be reproducible. timestamp: i64, /// Recorded as the creating application. creating_application: []const u8,};
pub const Error = error{ /// The WiX document cannot be linked; why is in the diagnostics. Invalid,} || Allocator.Error;
pub fn link(arena: Allocator, diags: *Diagnostics, document: wix.Document, options: Options) Error!Database { var db: Database = .init(arena); for (schema.standard) |t| _ = try db.addTable(t.name, t.columns);
const product = document.product; // Western European, unless the WiX source says otherwise. db.codepage = product.codepage orelse 1252;
try linkProperties(&db, product); try linkFeatures(&db, diags, product); try linkDirectories(&db, diags, product); try linkComponents(&db, diags, product, options); try linkUpgrades(&db, diags, product); try linkSequences(&db, product); try linkSummary(&db, diags, product, options);
if (diags.hasErrors()) return error.Invalid; return db;}
fn str(s: ?[]const u8) Value { return if (s) |v| .{ .string = v } else .null;}
fn int(i: ?i32) Value { return if (i) |v| .{ .int = v } else .null;}
fn linkProperties(db: *Database, product: wix.Product) Allocator.Error!void { const t = db.table("Property").?; const fixed = [_]struct { []const u8, ?[]const u8 }{ .{ "ProductCode", product.id }, .{ "ProductName", product.name }, .{ "ProductLanguage", product.language }, .{ "ProductVersion", product.version }, .{ "Manufacturer", product.manufacturer }, .{ "UpgradeCode", product.upgrade_code }, .{ "ALLUSERS", if (product.package.install_scope == .per_machine) "1" else null }, }; for (fixed) |p| { if (p[1]) |v| try t.insert(db.gpa, &.{ .{ .string = p[0] }, .{ .string = v } }); } // A property with no value is only a declaration: the Property table // cannot hold a null value, so it gets no row. for (product.properties) |p| { if (p.value) |v| try t.insert(db.gpa, &.{ .{ .string = p.id }, .{ .string = v } }); }}
fn linkFeatures(db: *Database, diags: *Diagnostics, product: wix.Product) Error!void { const t = db.table("Feature").?; const conditions = db.table("Condition").?; for (product.features) |f| { for (f.conditions) |cond| { try conditions.insert(db.gpa, &.{ .{ .string = f.id }, .{ .int = cond.level }, .{ .string = cond.condition } }); } if (f.configurable_directory) |dir| { // TARGETDIR always exists, whether or not it is written out. if (!std.mem.eql(u8, dir, "TARGETDIR") and findDirectory(product, dir) == null) { try diags.err(f.location, "ConfigurableDirectory names \"{s}\", but no directory of that name is defined", .{dir}); } } try t.insert(db.gpa, &.{ .{ .string = f.id }, str(f.parent), str(f.title), str(f.description), .{ .int = @backingInt(f.display) }, .{ .int = f.level }, str(f.configurable_directory), .{ .int = f.attributes }, }); }}
fn findDirectory(product: wix.Product, id: []const u8) ?wix.Directory { for (product.directories) |d| if (std.mem.eql(u8, d.id, id)) return d; return null;}
/// A name for the File or Directory table: `short|long` when the long name/// is not a valid short name itself.fn tableName(gpa: Allocator, long: []const u8, short: ?[]const u8) Allocator.Error![]const u8 { const s = short orelse return long; return std.fmt.allocPrint(gpa, "{s}|{s}", .{ s, long });}
/// The short name for each named directory and file, keyed by directory Id/// or file Id. Every name in one directory shares a namespace, files and/// subdirectories alike, so they are assigned together, in the order they/// were written.const ShortNames = struct { directories: std.StringHashMapUnmanaged(?[]const u8) = .empty, files: std.StringHashMapUnmanaged(?[]const u8) = .empty, shortcuts: std.StringHashMapUnmanaged(?[]const u8) = .empty, removals: std.StringHashMapUnmanaged(?[]const u8) = .empty,
const Owner = enum { directory, file, shortcut, removal };
fn assign(gpa: Allocator, product: wix.Product) Allocator.Error!ShortNames { var out: ShortNames = .{}; for (product.directories) |parent| { var names: std.ArrayList([]const u8) = .empty; var given: std.ArrayList(?[]const u8) = .empty; var owners: std.ArrayList(struct { Owner, []const u8 }) = .empty; for (product.directories) |d| { const p = d.parent orelse continue; if (!std.mem.eql(u8, p, parent.id)) continue; const name = d.name orelse continue; if (std.mem.eql(u8, name, ".")) continue; try names.append(gpa, name); try given.append(gpa, d.short_name); try owners.append(gpa, .{ .directory, d.id }); } for (product.components) |c| { if (std.mem.eql(u8, c.directory, parent.id)) { for (c.files) |f| { try names.append(gpa, f.name); try given.append(gpa, f.short_name); try owners.append(gpa, .{ .file, f.id }); } } for (c.shortcuts) |s| { if (!std.mem.eql(u8, s.directory, parent.id)) continue; try names.append(gpa, s.name); try given.append(gpa, s.short_name); try owners.append(gpa, .{ .shortcut, s.id }); } for (c.removals) |r| { const name = r.name orelse continue; if (!std.mem.eql(u8, r.directory, parent.id)) continue; // A wildcard is no short name's to hold. if (std.mem.findAny(u8, name, "*?") != null) continue; try names.append(gpa, name); try given.append(gpa, r.short_name); try owners.append(gpa, .{ .removal, r.id }); } } const shorts = try shortname.assign(gpa, names.items, given.items); for (owners.items, shorts) |owner, short| { const map = switch (owner[0]) { .directory => &out.directories, .file => &out.files, .shortcut => &out.shortcuts, .removal => &out.removals, }; try map.put(gpa, owner[1], short); } } return out; }};
fn linkDirectories(db: *Database, diags: *Diagnostics, product: wix.Product) Error!void { const t = db.table("Directory").?; const shorts = try ShortNames.assign(db.gpa, product); for (product.directories) |d| { if (d.parent == null and !std.mem.eql(u8, d.id, "TARGETDIR")) { try diags.err(d.location, "the root directory must be TARGETDIR, not \"{s}\"", .{d.id}); } // A directory with no name of its own, as the standard folders are // written, takes its parent's place: ".". const default_dir = if (d.name) |name| if (d.parent == null) name else try tableName(db.gpa, name, shorts.directories.get(d.id) orelse null) else "."; try t.insert(db.gpa, &.{ .{ .string = d.id }, str(d.parent), .{ .string = default_dir } }); }}
fn linkComponents(db: *Database, diags: *Diagnostics, product: wix.Product, options: Options) Error!void { const shorts = try ShortNames.assign(db.gpa, product); const components = db.table("Component").?; const files = db.table("File").?; const hashes = db.table("MsiFileHash").?; const media = db.table("Media").?; const feature_components = db.table("FeatureComponents").?;
// 64-bit components, in a package for a 64-bit platform, unless a // component says otherwise. const package_64bit = options.arch != .x86; const registry = db.table("Registry").?; const environment = db.table("Environment").?; const shortcuts = db.table("Shortcut").?; const create_folder = db.table("CreateFolder").?; const remove_file = db.table("RemoveFile").?; const icons = db.table("Icon").?; for (product.icons) |icon| { try icons.insert(db.gpa, &.{ .{ .string = icon.id }, .{ .stream = icon.contents } }); }
for (product.components) |c| { const is_64bit = switch (c.bitness) { .default => package_64bit, .always32 => false, .always64 => true, }; if (c.bitness == .always64 and !package_64bit) { try diags.err(c.location, "component \"{s}\" is always 64-bit, and an x86 package can hold only 32-bit components", .{c.id}); } // msidbComponentAttributes64bit, msidbComponentAttributesRegistryKeyPath const attributes = @as(i32, if (is_64bit) 256 else 0) | @as(i32, if (c.key_path_is_registry) 4 else 0); try components.insert(db.gpa, &.{ .{ .string = c.id }, str(c.guid), .{ .string = c.directory }, .{ .int = attributes }, str(c.condition), str(c.key_path), }); for (c.registry) |r| { if (registry.get(r.id, "Registry") != null) { try diags.err(r.location, "registry value \"{s}\" is defined twice", .{r.id}); continue; } try registry.insert(db.gpa, &.{ .{ .string = r.id }, .{ .int = @backingInt(r.root) }, .{ .string = r.key }, str(r.name), str(r.value), .{ .string = c.id }, }); } for (c.shortcuts) |sc| { try shortcuts.insert(db.gpa, &.{ .{ .string = sc.id }, .{ .string = sc.directory }, .{ .string = try tableName(db.gpa, sc.name, shorts.shortcuts.get(sc.id) orelse null) }, .{ .string = c.id }, .{ .string = sc.target }, str(sc.arguments), str(sc.description), int(sc.hotkey), str(sc.icon), int(sc.icon_index), int(sc.show), str(sc.working_directory), .null, .null, .null, .null, }); } for (c.shortcuts) |sc| for (sc.properties) |prop| { // Not one of the standard tables every package is given, since // only Windows Installer 5.0 knows it. const t = try db.addTable("MsiShortcutProperty", &shortcut_property_columns); if (t.get(prop.id, "MsiShortcutProperty") != null) { try diags.err(prop.location, "shortcut property \"{s}\" is defined twice", .{prop.id}); continue; } try t.insert(db.gpa, &.{ .{ .string = prop.id }, .{ .string = sc.id }, .{ .string = prop.key }, .{ .string = prop.value } }); const version = product.package.installer_version orelse options.arch.minimumInstallerVersion(); if (version < 500) { try diags.err(prop.location, "a shortcut property needs Windows Installer 5.0, so InstallerVersion must be at least 500", .{}); } }; for (c.create_folders) |dir| { try create_folder.insert(db.gpa, &.{ .{ .string = dir.id }, .{ .string = c.id } }); } for (c.removals) |r| { const name: ?[]const u8 = if (r.name) |n| try tableName(db.gpa, n, shorts.removals.get(r.id) orelse null) else null; try remove_file.insert(db.gpa, &.{ .{ .string = r.id }, .{ .string = c.id }, str(name), .{ .string = r.directory }, .{ .int = r.install_mode } }); } for (c.environment) |env| { if (environment.get(env.id, "Environment") != null) { try diags.err(env.location, "environment variable \"{s}\" is defined twice", .{env.id}); continue; } try environment.insert(db.gpa, &.{ .{ .string = env.id }, .{ .string = env.name }, str(env.value), .{ .string = c.id } }); } if (findDirectory(product, c.directory) == null) { try diags.err(c.location, "component \"{s}\" is in directory \"{s}\", which is not defined", .{ c.id, c.directory }); } }
// Files are numbered across every disk in turn, and on each disk in the // order they were written; each disk's cabinet holds its run of numbers. var sequence: i32 = 0; for (product.media) |m| { for (product.components) |c| { if (c.disk_id != m.disk_id) continue; for (c.files) |f| { sequence += 1; // A version read from the file wins over a default, as it // does in WiX: the default is for a file that has none. var version: ?[]const u8 = f.default_version; var language: ?[]const u8 = f.default_language; if (pe.fileVersion(f.contents)) |v| { version = try std.fmt.allocPrint(db.gpa, "{f}", .{v}); language = try db.gpa.dupe(u8, v.language()); } try files.insert(db.gpa, &.{ .{ .string = f.id }, .{ .string = c.id }, .{ .string = try tableName(db.gpa, f.name, shorts.files.get(f.id) orelse null) }, .{ .int = std.math.cast(i32, f.contents.len) orelse { try diags.err(f.location, "file \"{s}\" is too large for Windows Installer", .{f.id}); continue; } }, str(version), str(language), // msidbFileAttributesVital .{ .int = if (f.vital) 512 else 0 }, .{ .int = sequence }, }); // Windows Installer compares an unversioned file with the one // on disk by its hash, and a versioned one by its version // alone; ICE validation reports a hash for a versioned file. if (version == null) try hashes.insert(db.gpa, &hashRow(f)); try db.file_contents.put(db.gpa, f.id, f.contents); } } const cabinet: ?[]const u8 = if (m.cabinet) |name| if (m.embed) try std.fmt.allocPrint(db.gpa, "#{s}", .{name}) else name else null; try media.insert(db.gpa, &.{ .{ .int = m.disk_id }, .{ .int = sequence }, .null, str(cabinet), .null, .null }); try db.compression.put(db.gpa, m.disk_id, switch (m.compression) { inline else => |c| @field(Database.Compression, @tagName(c)), }); } for (product.components) |c| { if (c.files.len == 0) continue; for (product.media) |m| { if (m.disk_id == c.disk_id) break; } else { try diags.err(c.location, "component \"{s}\" has files on disk {d}, but no <Media> has that Id", .{ c.id, c.disk_id }); } }
for (product.features) |f| { for (f.components) |ref| { for (product.components) |c| { if (std.mem.eql(u8, c.id, ref.id)) break; } else { try diags.err(ref.location, "no component has the Id \"{s}\"", .{ref.id}); continue; } try feature_components.insert(db.gpa, &.{ .{ .string = f.id }, .{ .string = ref.id } }); } } for (product.components) |c| { for (product.features) |f| { for (f.components) |ref| { if (std.mem.eql(u8, ref.id, c.id)) break; } else continue; break; } else { try diags.warn(c.location, "component \"{s}\" belongs to no feature, so nothing installs it", .{c.id}); } }}
/// The MsiShortcutProperty table's columns, as the Windows Installer SDK/// documents them.const shortcut_property_columns = [_]Database.Column{ .parse("MsiShortcutProperty", "s72", true), .parse("Shortcut_", "s72", false), .parse("PropertyKey", "s0", false), .parse("PropVariantValue", "s0", false),};
/// An MsiFileHash row: the file's MD5, as four 32-bit integers.fn hashRow(f: wix.File) [6]Value { var digest: [std.crypto.hash.Md5.digest_length]u8 = undefined; std.crypto.hash.Md5.hash(f.contents, &digest, .{}); var row: [6]Value = .{ .{ .string = f.id }, .{ .int = 0 }, undefined, undefined, undefined, undefined }; for (0..4) |i| row[2 + i] = .{ .int = std.mem.readInt(i32, digest[4 * i ..][0..4], .little) }; return row;}
/// The properties a major upgrade sets when it finds an older version or a/// newer one, as WiX names them.const upgrade_detected = "WIX_UPGRADE_DETECTED";const downgrade_detected = "WIX_DOWNGRADE_DETECTED";
/// The Upgrade table's attribute bits, as the Windows Installer SDK defines/// them.const upgrade_migrate_features = 1;const upgrade_only_detect = 2;const upgrade_ignore_remove_failure = 4;const upgrade_version_max_inclusive = 512;
fn linkUpgrades(db: *Database, diags: *Diagnostics, product: wix.Product) Error!void { const launch = db.table("LaunchCondition").?; for (product.launch_conditions) |lc| { try launch.insert(db.gpa, &.{ .{ .string = lc.condition }, .{ .string = lc.message } }); }
const m = product.major_upgrade orelse return; const upgrade_code = product.upgrade_code orelse { try diags.err(m.location, "<MajorUpgrade> finds earlier versions by the product's UpgradeCode, and the <Product> has none", .{}); return; }; const upgrade = db.table("Upgrade").?; var secure: std.ArrayList([]const u8) = .empty; const language: Value = if (m.same_language) .{ .string = product.language } else .null;
if (m.disallow) { // Only find an older version, and refuse to go on when there is one. try upgrade.insert(db.gpa, &.{ .{ .string = upgrade_code }, .null, .{ .string = product.version }, language, .{ .int = upgrade_only_detect }, .null, .{ .string = upgrade_detected } }); try launch.insert(db.gpa, &.{ .{ .string = "NOT " ++ upgrade_detected }, .{ .string = m.disallow_message orelse "" } }); } else { // Every older version -- the same one too, when that is allowed -- // and every version at all when downgrades are. var attributes: i32 = 0; if (m.migrate_features) attributes |= upgrade_migrate_features; if (m.ignore_remove_failure) attributes |= upgrade_ignore_remove_failure; if (m.allow_same_version_upgrades) attributes |= upgrade_version_max_inclusive; try upgrade.insert(db.gpa, &.{ .{ .string = upgrade_code }, .null, if (m.allow_downgrades) .null else .{ .string = product.version }, language, .{ .int = attributes }, str(m.remove_features), .{ .string = upgrade_detected }, }); } try secure.append(db.gpa, upgrade_detected); if (!m.allow_downgrades) { // A newer version installed already: found, and refused. try upgrade.insert(db.gpa, &.{ .{ .string = upgrade_code }, .{ .string = product.version }, .null, language, .{ .int = upgrade_only_detect }, .null, .{ .string = downgrade_detected } }); try launch.insert(db.gpa, &.{ .{ .string = "NOT " ++ downgrade_detected }, .{ .string = m.downgrade_message orelse "" } }); try secure.append(db.gpa, downgrade_detected); }
// The properties Windows Installer passes from the client to the // server side of an installation, which the action properties must be. std.mem.sort([]const u8, secure.items, {}, struct { fn lessThan(_: void, a: []const u8, b: []const u8) bool { return std.mem.order(u8, a, b) == .lt; } }.lessThan); const properties = db.table("Property").?; const joined = try std.mem.join(db.gpa, ";", secure.items); if (properties.find(&.{.{ .string = "SecureCustomProperties" }})) |row| { row[1] = .{ .string = try std.fmt.allocPrint(db.gpa, "{s};{s}", .{ row[1].string, joined }) }; } else { try properties.insert(db.gpa, &.{ .{ .string = "SecureCustomProperties" }, .{ .string = joined } }); }}
const Action = struct { []const u8, i32 };
/// The standard actions every installer package is given, at the sequence/// numbers the Windows Installer SDK suggests for them.const sequences = [_]struct { []const u8, []const Action }{ .{ "AdminExecuteSequence", &.{ .{ "CostInitialize", 800 }, .{ "FileCost", 900 }, .{ "CostFinalize", 1000 }, .{ "InstallValidate", 1400 }, .{ "InstallInitialize", 1500 }, .{ "InstallAdminPackage", 3900 }, .{ "InstallFiles", 4000 }, .{ "InstallFinalize", 6600 }, } }, .{ "AdminUISequence", &.{ .{ "CostInitialize", 800 }, .{ "FileCost", 900 }, .{ "CostFinalize", 1000 }, .{ "ExecuteAction", 1300 }, } }, .{ "AdvtExecuteSequence", &.{ .{ "CostInitialize", 800 }, .{ "CostFinalize", 1000 }, .{ "InstallValidate", 1400 }, .{ "InstallInitialize", 1500 }, .{ "PublishFeatures", 6300 }, .{ "PublishProduct", 6400 }, .{ "InstallFinalize", 6600 }, } }, .{ "InstallExecuteSequence", &.{ .{ "ValidateProductID", 700 }, .{ "CostInitialize", 800 }, .{ "FileCost", 900 }, .{ "CostFinalize", 1000 }, .{ "InstallValidate", 1400 }, .{ "InstallInitialize", 1500 }, .{ "ProcessComponents", 1600 }, .{ "UnpublishFeatures", 1800 }, .{ "RegisterUser", 6000 }, .{ "RegisterProduct", 6100 }, .{ "PublishFeatures", 6300 }, .{ "PublishProduct", 6400 }, .{ "InstallFinalize", 6600 }, } }, .{ "InstallUISequence", &.{ .{ "ValidateProductID", 700 }, .{ "CostInitialize", 800 }, .{ "FileCost", 900 }, .{ "CostFinalize", 1000 }, .{ "ExecuteAction", 1300 }, } },};
/// Actions an installer package is given only when one of its tables has/// rows, since there is otherwise nothing for them to do.const conditional_actions = [_]struct { []const u8, []const u8, Action }{ .{ "File", "InstallExecuteSequence", .{ "RemoveFiles", 3500 } }, .{ "File", "InstallExecuteSequence", .{ "InstallFiles", 4000 } }, .{ "Registry", "InstallExecuteSequence", .{ "RemoveRegistryValues", 2600 } }, .{ "Registry", "InstallExecuteSequence", .{ "WriteRegistryValues", 5000 } }, .{ "Environment", "InstallExecuteSequence", .{ "RemoveEnvironmentStrings", 3300 } }, .{ "Environment", "InstallExecuteSequence", .{ "WriteEnvironmentStrings", 5200 } }, .{ "RemoveFile", "InstallExecuteSequence", .{ "RemoveFiles", 3500 } }, .{ "CreateFolder", "InstallExecuteSequence", .{ "RemoveFolders", 3600 } }, .{ "CreateFolder", "InstallExecuteSequence", .{ "CreateFolders", 3700 } }, .{ "Shortcut", "InstallExecuteSequence", .{ "RemoveShortcuts", 3200 } }, .{ "Shortcut", "InstallExecuteSequence", .{ "CreateShortcuts", 4500 } }, .{ "Shortcut", "AdvtExecuteSequence", .{ "CreateShortcuts", 4500 } }, .{ "LaunchCondition", "InstallExecuteSequence", .{ "LaunchConditions", 100 } }, .{ "LaunchCondition", "InstallUISequence", .{ "LaunchConditions", 100 } },};
fn linkSequences(db: *Database, product: wix.Product) Allocator.Error!void { for (sequences) |s| { const t = db.table(s[0]).?; for (s[1]) |action| { try t.insert(db.gpa, &.{ .{ .string = action[0] }, .null, .{ .int = action[1] } }); } } for (conditional_actions) |c| { if (db.table(c[0]).?.rows.items.len == 0) continue; try sequenceAction(db, c[1], c[2]); } if (product.major_upgrade) |m| { for ([_][]const u8{ "InstallExecuteSequence", "InstallUISequence" }) |s| { try sequenceAction(db, s, .{ "FindRelatedProducts", 25 }); if (!m.disallow) try sequenceAction(db, s, .{ "MigrateFeatureStates", 1200 }); } if (!m.disallow) try sequenceAction(db, "InstallExecuteSequence", .{ "RemoveExistingProducts", switch (m.schedule) { .after_install_validate => 1401, .after_install_initialize => 1501, .after_install_finalize => 6601, } }); }}
/// Adds `action` to the sequence table called `table`, unless it is there/// already: an action two tables call for is sequenced once.fn sequenceAction(db: *Database, table: []const u8, action: Action) Allocator.Error!void { const t = db.table(table).?; if (t.get(action[0], "Sequence") != null) return; try t.insert(db.gpa, &.{ .{ .string = action[0] }, .null, .{ .int = action[1] } });}
fn linkSummary(db: *Database, diags: *Diagnostics, product: wix.Product, options: Options) Allocator.Error!void { const package = product.package; const minimum = options.arch.minimumInstallerVersion(); if (package.installer_version) |v| if (v < minimum) { try diags.err(package.installer_version_location, "an {s} package needs Windows Installer {d}.{d} or later, so InstallerVersion must be at least {d}", .{ @tagName(options.arch), @divTrunc(minimum, 100), @mod(minimum, 100), minimum, }); }; const per_user = package.install_scope == .per_user; db.summary = .{ .codepage = package.summary_codepage orelse 1252, .title = "Installation Database", .subject = package.description orelse product.name, .author = package.author orelse product.manufacturer, .keywords = package.keywords orelse "Installer", .comments = package.comments orelse try std.fmt.allocPrint( db.gpa, "This installer database contains the logic and data required to install {s}.", .{product.name}, ), .template = try std.fmt.allocPrint(db.gpa, "{s};{s}", .{ options.arch.platform(), product.language }), .revision_number = package.id, .create_time = options.timestamp, .last_save_time = options.timestamp, .page_count = package.installer_version orelse minimum, // Files always go into cabinets, so the source is always marked // compressed; a per-user package needs no elevated privileges. .word_count = 2 | @as(i32, if (per_user) 8 else 0), .creating_application = options.creating_application, .security = 2, };}