@zogien/native-sdk 0.11.0-fork.3 → 0.11.0-fork.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/build/app.zig +3 -0
  3. package/build/cli.zig +22 -0
  4. package/package.json +10 -10
  5. package/packages/core/package-lock.json +2 -2
  6. package/packages/core/package.json +1 -1
  7. package/schemas/app.schema.json +9 -1
  8. package/skill-data/ts-core/SKILL.md +2 -0
  9. package/src/app_runner/app_assets.zig +85 -1
  10. package/src/app_runner/root.zig +63 -3
  11. package/src/app_runner/ts_core_main.zig +8 -0
  12. package/src/platform/macos/appkit_host.h +9 -0
  13. package/src/platform/macos/appkit_host.m +366 -0
  14. package/src/platform/macos/root.zig +29 -6
  15. package/src/platform/root.zig +1 -0
  16. package/src/platform/types.zig +9 -0
  17. package/src/platform/windows/root.zig +37 -0
  18. package/src/platform/windows/webview2_host.cpp +371 -26
  19. package/src/primitives/app_manifest/root.zig +1 -0
  20. package/src/primitives/app_manifest/tests.zig +35 -0
  21. package/src/primitives/app_manifest/types.zig +12 -0
  22. package/src/primitives/app_manifest/validation.zig +44 -6
  23. package/src/root.zig +1 -0
  24. package/src/runtime/channel_sources.zig +49 -1
  25. package/src/runtime/effects.zig +12 -1
  26. package/src/runtime/flow.zig +6 -0
  27. package/src/runtime/flow_reference.zig +6 -0
  28. package/src/runtime/root.zig +4 -0
  29. package/src/runtime/single_instance.zig +893 -0
  30. package/src/tooling/manifest.zig +111 -3
  31. package/src/tooling/package.zig +98 -8
  32. package/src/tooling/raw_manifest.zig +7 -0
  33. package/src/tooling/root.zig +2 -0
  34. package/src/tooling/templates.zig +44 -2
  35. package/src/tooling/update.zig +228 -10
  36. package/src/tooling/windows_installer.zig +245 -0
  37. package/src/tooling/zip_writer.zig +211 -0
  38. package/src/updater/engine.zig +936 -0
  39. package/src/updater/feed.zig +261 -12
  40. package/src/updater/macos.zig +187 -0
  41. package/src/updater/root.zig +41 -0
  42. package/src/updater/windows.zig +470 -0
  43. package/src/windows_install/config.zig +247 -0
  44. package/src/windows_install/main.zig +690 -0
  45. package/src/windows_install/payload.zig +146 -0
  46. package/src/windows_install/switch.zig +184 -0
  47. package/src/windows_install/tests.zig +11 -0
  48. package/src/windows_install/win32.zig +248 -0
  49. package/tools/native-sdk/main.zig +10 -9
@@ -0,0 +1,146 @@
1
+ //! The setup program's payload: `native package --installer` appends the
2
+ //! install configuration and the install image (a ZIP) to the setup
3
+ //! program, followed by a fixed trailer that locates both and carries
4
+ //! their SHA-256. Signing setup.exe with Authenticode appends the
5
+ //! certificate table after the trailer (8-byte aligned), so the reader
6
+ //! looks for the trailer just before that table when the PE header names
7
+ //! one, and at the end of the file otherwise.
8
+
9
+ const std = @import("std");
10
+
11
+ pub const magic = "NSDK-SETUP-V1\x00\x00\x00";
12
+ pub const trailer_len: usize = magic.len + 4 * @sizeOf(u64) + Sha256.digest_length;
13
+
14
+ const Sha256 = std.crypto.hash.sha2.Sha256;
15
+
16
+ pub const Trailer = struct {
17
+ config_offset: u64,
18
+ config_size: u64,
19
+ zip_offset: u64,
20
+ zip_size: u64,
21
+ sha256: [Sha256.digest_length]u8,
22
+
23
+ /// The setup program itself: everything before the configuration.
24
+ pub fn programSize(self: Trailer) u64 {
25
+ return self.config_offset;
26
+ }
27
+ };
28
+
29
+ pub fn encode(trailer: Trailer) [trailer_len]u8 {
30
+ var bytes: [trailer_len]u8 = undefined;
31
+ @memcpy(bytes[0..magic.len], magic);
32
+ std.mem.writeInt(u64, bytes[16..24], trailer.config_offset, .little);
33
+ std.mem.writeInt(u64, bytes[24..32], trailer.config_size, .little);
34
+ std.mem.writeInt(u64, bytes[32..40], trailer.zip_offset, .little);
35
+ std.mem.writeInt(u64, bytes[40..48], trailer.zip_size, .little);
36
+ @memcpy(bytes[48..80], &trailer.sha256);
37
+ return bytes;
38
+ }
39
+
40
+ /// Decodes a trailer and checks that the regions it names sit in order
41
+ /// before `trailer_offset`.
42
+ pub fn decode(bytes: *const [trailer_len]u8, trailer_offset: u64) ?Trailer {
43
+ if (!std.mem.eql(u8, bytes[0..magic.len], magic)) return null;
44
+ const trailer: Trailer = .{
45
+ .config_offset = std.mem.readInt(u64, bytes[16..24], .little),
46
+ .config_size = std.mem.readInt(u64, bytes[24..32], .little),
47
+ .zip_offset = std.mem.readInt(u64, bytes[32..40], .little),
48
+ .zip_size = std.mem.readInt(u64, bytes[40..48], .little),
49
+ .sha256 = bytes[48..80].*,
50
+ };
51
+ if (trailer.config_offset == 0 or trailer.config_size == 0 or trailer.zip_size == 0) return null;
52
+ const config_end = std.math.add(u64, trailer.config_offset, trailer.config_size) catch return null;
53
+ if (trailer.zip_offset != config_end) return null;
54
+ const zip_end = std.math.add(u64, trailer.zip_offset, trailer.zip_size) catch return null;
55
+ if (zip_end != trailer_offset) return null;
56
+ return trailer;
57
+ }
58
+
59
+ /// Where the setup data ends: the start of the Authenticode certificate
60
+ /// table when the PE header names one that ends the file, else the file
61
+ /// size. `headers` holds the file's first bytes (at least the PE headers).
62
+ pub fn dataEnd(headers: []const u8, file_size: u64) u64 {
63
+ if (headers.len < 0x40 or !std.mem.eql(u8, headers[0..2], "MZ")) return file_size;
64
+ const pe_offset: usize = std.mem.readInt(u32, headers[0x3c..0x40], .little);
65
+ if (pe_offset > headers.len -| 24 or !std.mem.eql(u8, headers[pe_offset..][0..4], "PE\x00\x00")) return file_size;
66
+ const optional_offset = pe_offset + 24;
67
+ if (optional_offset + 2 > headers.len) return file_size;
68
+ const directories_offset: usize = switch (std.mem.readInt(u16, headers[optional_offset..][0..2], .little)) {
69
+ 0x10b => optional_offset + 96,
70
+ 0x20b => optional_offset + 112,
71
+ else => return file_size,
72
+ };
73
+ const security_offset = directories_offset + 4 * 8;
74
+ if (security_offset + 8 > headers.len) return file_size;
75
+ const table_offset: u64 = std.mem.readInt(u32, headers[security_offset..][0..4], .little);
76
+ const table_size: u64 = std.mem.readInt(u32, headers[security_offset + 4 ..][0..4], .little);
77
+ if (table_offset == 0 or table_size == 0 or table_offset + table_size != file_size) return file_size;
78
+ return table_offset;
79
+ }
80
+
81
+ /// Finds the trailer in the bytes that end at `end` (`tail` holds those
82
+ /// bytes, at least `trailer_len + 7` of them when available), allowing
83
+ /// the up to seven zero bytes of padding signing adds before the
84
+ /// certificate table.
85
+ pub fn find(tail: []const u8, end: u64) ?Trailer {
86
+ var padding: usize = 0;
87
+ while (padding <= 7 and padding + trailer_len <= tail.len) : (padding += 1) {
88
+ const at = tail.len - padding - trailer_len;
89
+ for (tail[tail.len - padding ..]) |byte| {
90
+ if (byte != 0) return null;
91
+ }
92
+ if (decode(tail[at..][0..trailer_len], end - padding - trailer_len)) |trailer| return trailer;
93
+ }
94
+ return null;
95
+ }
96
+
97
+ pub fn digest(config_bytes: []const u8, zip_bytes: []const u8) [Sha256.digest_length]u8 {
98
+ var hasher = Sha256.init(.{});
99
+ hasher.update(config_bytes);
100
+ hasher.update(zip_bytes);
101
+ var out: [Sha256.digest_length]u8 = undefined;
102
+ hasher.final(&out);
103
+ return out;
104
+ }
105
+
106
+ test "a trailer locates the configuration and image before it" {
107
+ const trailer: Trailer = .{ .config_offset = 1000, .config_size = 200, .zip_offset = 1200, .zip_size = 5000, .sha256 = [_]u8{7} ** 32 };
108
+ const bytes = encode(trailer);
109
+ const decoded = decode(&bytes, 6200).?;
110
+ try std.testing.expectEqual(@as(u64, 1200), decoded.zip_offset);
111
+ try std.testing.expectEqual(@as(u64, 1000), decoded.programSize());
112
+ try std.testing.expect(decode(&bytes, 6300) == null);
113
+ var corrupt = bytes;
114
+ corrupt[0] = 'X';
115
+ try std.testing.expect(decode(&corrupt, 6200) == null);
116
+ }
117
+
118
+ test "the trailer is found before signing padding" {
119
+ const trailer: Trailer = .{ .config_offset = 10, .config_size = 5, .zip_offset = 15, .zip_size = 20, .sha256 = [_]u8{1} ** 32 };
120
+ var tail: [trailer_len + 7]u8 = undefined;
121
+ @memcpy(tail[0..trailer_len], &encode(trailer));
122
+ @memset(tail[trailer_len..], 0);
123
+ // Payload ends at 35, the trailer takes 80 bytes, then 7 bytes of
124
+ // padding precede the certificate table at 122.
125
+ try std.testing.expectEqual(@as(u64, 15), find(&tail, 35 + trailer_len + 7).?.zip_offset);
126
+ try std.testing.expectEqual(@as(u64, 15), find(tail[0..trailer_len], 35 + trailer_len).?.zip_offset);
127
+ tail[trailer_len + 3] = 1;
128
+ try std.testing.expect(find(&tail, 35 + trailer_len + 7) == null);
129
+ }
130
+
131
+ test "data ends at a certificate table that ends the file" {
132
+ var headers = [_]u8{0} ** 0x200;
133
+ headers[0] = 'M';
134
+ headers[1] = 'Z';
135
+ std.mem.writeInt(u32, headers[0x3c..0x40], 0x80, .little);
136
+ @memcpy(headers[0x80..0x84], "PE\x00\x00");
137
+ std.mem.writeInt(u16, headers[0x98..0x9a], 0x20b, .little);
138
+ const security = 0x98 + 112 + 32;
139
+ std.mem.writeInt(u32, headers[security..][0..4], 4096, .little);
140
+ std.mem.writeInt(u32, headers[security + 4 ..][0..4], 512, .little);
141
+ try std.testing.expectEqual(@as(u64, 4096), dataEnd(&headers, 4608));
142
+ try std.testing.expectEqual(@as(u64, 9000), dataEnd(&headers, 9000));
143
+ std.mem.writeInt(u32, headers[security..][0..4], 0, .little);
144
+ try std.testing.expectEqual(@as(u64, 4608), dataEnd(&headers, 4608));
145
+ try std.testing.expectEqual(@as(u64, 77), dataEnd("not a pe file", 77));
146
+ }
@@ -0,0 +1,184 @@
1
+ //! Moving a staged app version into place inside an install root:
2
+ //!
3
+ //! <root>\<name>.exe the launcher, at a stable path
4
+ //! <root>\app\ the current version
5
+ //! <root>\app.next\ a staged version, complete once it holds .native-staged
6
+ //! <root>\app.prev\ the version before the last switch
7
+ //!
8
+ //! The executable inside app\ keeps one path across versions, so the
9
+ //! settings Windows keys to that path (firewall rules, GPU preference,
10
+ //! compatibility flags) survive updates. The switch renames whole
11
+ //! directories. Windows renames a directory even while an executable in
12
+ //! it runs, so callers first make sure no process runs from app\; a
13
+ //! rename that still fails (another program holding a file open) leaves
14
+ //! the install as it was. A journal file marks the window between the two
15
+ //! renames, and `recover` finishes or undoes an interrupted switch on the
16
+ //! next launch.
17
+
18
+ const std = @import("std");
19
+
20
+ pub const current = "app";
21
+ pub const staged = "app.next";
22
+ pub const previous = "app.prev";
23
+ pub const journal = ".native-switch";
24
+ /// Written into app.next last, after every file of the staged version.
25
+ pub const staged_marker = ".native-staged";
26
+ /// `updated <from> <to>`, written by the launcher after it switched to a
27
+ /// staged version, and read once by that version's updater.
28
+ pub const update_result = ".native-update-result";
29
+
30
+ pub const Outcome = enum {
31
+ /// No complete staged version.
32
+ nothing_staged,
33
+ switched,
34
+ /// A file of the current version is held open; nothing changed.
35
+ busy,
36
+ };
37
+
38
+ /// Finishes an interrupted switch, then moves a complete staged version
39
+ /// into place. The caller has checked that no process runs from app\.
40
+ /// The steps:
41
+ /// 1. remove an old app.prev,
42
+ /// 2. write the journal,
43
+ /// 3. rename app -> app.prev,
44
+ /// 4. rename app.next -> app,
45
+ /// 5. remove the journal.
46
+ /// A failed step 1 or 3 leaves the install as it was.
47
+ pub fn applyStaged(root: std.Io.Dir, io: std.Io) !Outcome {
48
+ try recover(root, io);
49
+ if (!exists(root, io, staged ++ "/" ++ staged_marker)) return .nothing_staged;
50
+
51
+ if (exists(root, io, previous)) {
52
+ root.deleteTree(io, previous) catch return .busy;
53
+ }
54
+ const had_current = exists(root, io, current);
55
+ try root.writeFile(io, .{ .sub_path = journal, .data = "switching\n" });
56
+ if (had_current) {
57
+ root.renamePreserve(current, root, previous, io) catch {
58
+ root.deleteFile(io, journal) catch {};
59
+ return .busy;
60
+ };
61
+ }
62
+ try root.renamePreserve(staged, root, current, io);
63
+ root.deleteFile(io, current ++ "/" ++ staged_marker) catch {};
64
+ try root.deleteFile(io, journal);
65
+ return .switched;
66
+ }
67
+
68
+ /// Completes or undoes a switch a crash interrupted. Only acts while the
69
+ /// journal exists.
70
+ pub fn recover(root: std.Io.Dir, io: std.Io) !void {
71
+ if (!exists(root, io, journal)) return;
72
+ if (!exists(root, io, current)) {
73
+ if (exists(root, io, staged)) {
74
+ try root.renamePreserve(staged, root, current, io);
75
+ root.deleteFile(io, current ++ "/" ++ staged_marker) catch {};
76
+ } else if (exists(root, io, previous)) {
77
+ try root.renamePreserve(previous, root, current, io);
78
+ }
79
+ }
80
+ try root.deleteFile(io, journal);
81
+ }
82
+
83
+ /// Prepares app.next for a new staged copy, removing an earlier one.
84
+ pub fn clearStaged(root: std.Io.Dir, io: std.Io) !void {
85
+ if (exists(root, io, staged)) try root.deleteTree(io, staged);
86
+ }
87
+
88
+ /// Marks app.next complete. Call after every file is in place.
89
+ pub fn markStaged(root: std.Io.Dir, io: std.Io) !void {
90
+ try root.writeFile(io, .{ .sub_path = staged ++ "/" ++ staged_marker, .data = "staged\n" });
91
+ }
92
+
93
+ fn exists(root: std.Io.Dir, io: std.Io, sub_path: []const u8) bool {
94
+ root.access(io, sub_path, .{}) catch return false;
95
+ return true;
96
+ }
97
+
98
+ fn writeVersion(root: std.Io.Dir, io: std.Io, comptime dir: []const u8, version: []const u8) !void {
99
+ try root.createDirPath(io, dir ++ "/bin");
100
+ const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/bin/version.txt", .{dir});
101
+ defer std.testing.allocator.free(path);
102
+ try root.writeFile(io, .{ .sub_path = path, .data = version });
103
+ }
104
+
105
+ fn readVersion(root: std.Io.Dir, io: std.Io, dir: []const u8) ![]u8 {
106
+ const path = try std.fmt.allocPrint(std.testing.allocator, "{s}/bin/version.txt", .{dir});
107
+ defer std.testing.allocator.free(path);
108
+ return root.readFileAlloc(io, path, std.testing.allocator, .limited(64));
109
+ }
110
+
111
+ test "a first install moves the staged version into place" {
112
+ var tmp = std.testing.tmpDir(.{});
113
+ defer tmp.cleanup();
114
+ const io = std.testing.io;
115
+ try writeVersion(tmp.dir, io, staged, "1.0.0");
116
+ try std.testing.expectEqual(Outcome.nothing_staged, try applyStaged(tmp.dir, io));
117
+ try markStaged(tmp.dir, io);
118
+ try std.testing.expectEqual(Outcome.switched, try applyStaged(tmp.dir, io));
119
+ const version = try readVersion(tmp.dir, io, current);
120
+ defer std.testing.allocator.free(version);
121
+ try std.testing.expectEqualStrings("1.0.0", version);
122
+ try std.testing.expect(!exists(tmp.dir, io, staged));
123
+ try std.testing.expect(!exists(tmp.dir, io, journal));
124
+ try std.testing.expect(!exists(tmp.dir, io, current ++ "/" ++ staged_marker));
125
+ }
126
+
127
+ test "a switch keeps the previous version and replaces an older one" {
128
+ var tmp = std.testing.tmpDir(.{});
129
+ defer tmp.cleanup();
130
+ const io = std.testing.io;
131
+ try writeVersion(tmp.dir, io, previous, "0.9.0");
132
+ try writeVersion(tmp.dir, io, current, "1.0.0");
133
+ try writeVersion(tmp.dir, io, staged, "1.1.0");
134
+ try markStaged(tmp.dir, io);
135
+ try std.testing.expectEqual(Outcome.switched, try applyStaged(tmp.dir, io));
136
+ const now = try readVersion(tmp.dir, io, current);
137
+ defer std.testing.allocator.free(now);
138
+ const before = try readVersion(tmp.dir, io, previous);
139
+ defer std.testing.allocator.free(before);
140
+ try std.testing.expectEqualStrings("1.1.0", now);
141
+ try std.testing.expectEqualStrings("1.0.0", before);
142
+ }
143
+
144
+ test "recovery finishes a switch interrupted between its renames" {
145
+ var tmp = std.testing.tmpDir(.{});
146
+ defer tmp.cleanup();
147
+ const io = std.testing.io;
148
+ // A crash after app -> app.prev and before app.next -> app.
149
+ try writeVersion(tmp.dir, io, previous, "1.0.0");
150
+ try writeVersion(tmp.dir, io, staged, "1.1.0");
151
+ try markStaged(tmp.dir, io);
152
+ try tmp.dir.writeFile(io, .{ .sub_path = journal, .data = "switching\n" });
153
+ try recover(tmp.dir, io);
154
+ const version = try readVersion(tmp.dir, io, current);
155
+ defer std.testing.allocator.free(version);
156
+ try std.testing.expectEqualStrings("1.1.0", version);
157
+ try std.testing.expect(!exists(tmp.dir, io, journal));
158
+ }
159
+
160
+ test "recovery restores the previous version when the staged copy is gone" {
161
+ var tmp = std.testing.tmpDir(.{});
162
+ defer tmp.cleanup();
163
+ const io = std.testing.io;
164
+ try writeVersion(tmp.dir, io, previous, "1.0.0");
165
+ try tmp.dir.writeFile(io, .{ .sub_path = journal, .data = "switching\n" });
166
+ try recover(tmp.dir, io);
167
+ const version = try readVersion(tmp.dir, io, current);
168
+ defer std.testing.allocator.free(version);
169
+ try std.testing.expectEqualStrings("1.0.0", version);
170
+ }
171
+
172
+ test "an incomplete staged copy is never switched in" {
173
+ var tmp = std.testing.tmpDir(.{});
174
+ defer tmp.cleanup();
175
+ const io = std.testing.io;
176
+ try writeVersion(tmp.dir, io, current, "1.0.0");
177
+ try writeVersion(tmp.dir, io, staged, "1.1.0");
178
+ try std.testing.expectEqual(Outcome.nothing_staged, try applyStaged(tmp.dir, io));
179
+ const version = try readVersion(tmp.dir, io, current);
180
+ defer std.testing.allocator.free(version);
181
+ try std.testing.expectEqualStrings("1.0.0", version);
182
+ try clearStaged(tmp.dir, io);
183
+ try std.testing.expect(!exists(tmp.dir, io, staged));
184
+ }
@@ -0,0 +1,11 @@
1
+ //! Test root for the Windows setup program, launcher and uninstaller.
2
+ //! The modules other than win32/main run on every host; the Windows-only
3
+ //! program entry is never analyzed here.
4
+
5
+ test {
6
+ _ = @import("config.zig");
7
+ _ = @import("payload.zig");
8
+ _ = @import("switch.zig");
9
+ _ = @import("win32.zig");
10
+ _ = @import("main.zig");
11
+ }
@@ -0,0 +1,248 @@
1
+ //! The Win32 surface the setup program, launcher and uninstaller use,
2
+ //! declared here so the program links nothing beyond the system DLLs.
3
+
4
+ const std = @import("std");
5
+
6
+ pub const HANDLE = std.os.windows.HANDLE;
7
+ pub const HKEY = *opaque {};
8
+ pub const HWND = *opaque {};
9
+ pub const DWORD = u32;
10
+ pub const BOOL = i32;
11
+ pub const HRESULT = i32;
12
+ pub const WCHAR = u16;
13
+
14
+ pub const GUID = extern struct {
15
+ data1: u32,
16
+ data2: u16,
17
+ data3: u16,
18
+ data4: [8]u8,
19
+
20
+ pub fn parse(comptime text: []const u8) GUID {
21
+ @setEvalBranchQuota(10_000);
22
+ var guid: GUID = undefined;
23
+ guid.data1 = std.fmt.parseInt(u32, text[0..8], 16) catch unreachable;
24
+ guid.data2 = std.fmt.parseInt(u16, text[9..13], 16) catch unreachable;
25
+ guid.data3 = std.fmt.parseInt(u16, text[14..18], 16) catch unreachable;
26
+ guid.data4[0] = std.fmt.parseInt(u8, text[19..21], 16) catch unreachable;
27
+ guid.data4[1] = std.fmt.parseInt(u8, text[21..23], 16) catch unreachable;
28
+ for (0..6) |index| {
29
+ guid.data4[2 + index] = std.fmt.parseInt(u8, text[24 + 2 * index ..][0..2], 16) catch unreachable;
30
+ }
31
+ return guid;
32
+ }
33
+ };
34
+
35
+ pub const STARTUPINFOW = extern struct {
36
+ cb: DWORD = @sizeOf(STARTUPINFOW),
37
+ lpReserved: ?[*:0]u16 = null,
38
+ lpDesktop: ?[*:0]u16 = null,
39
+ lpTitle: ?[*:0]u16 = null,
40
+ dwX: DWORD = 0,
41
+ dwY: DWORD = 0,
42
+ dwXSize: DWORD = 0,
43
+ dwYSize: DWORD = 0,
44
+ dwXCountChars: DWORD = 0,
45
+ dwYCountChars: DWORD = 0,
46
+ dwFillAttribute: DWORD = 0,
47
+ dwFlags: DWORD = 0,
48
+ wShowWindow: u16 = 0,
49
+ cbReserved2: u16 = 0,
50
+ lpReserved2: ?*u8 = null,
51
+ hStdInput: ?HANDLE = null,
52
+ hStdOutput: ?HANDLE = null,
53
+ hStdError: ?HANDLE = null,
54
+ };
55
+
56
+ pub const PROCESS_INFORMATION = extern struct {
57
+ hProcess: HANDLE,
58
+ hThread: HANDLE,
59
+ dwProcessId: DWORD,
60
+ dwThreadId: DWORD,
61
+ };
62
+
63
+ pub const PROPERTYKEY = extern struct {
64
+ fmtid: GUID,
65
+ pid: DWORD,
66
+ };
67
+
68
+ /// PROPVARIANT, 16 bytes on 32-bit and 24 bytes on 64-bit Windows; only
69
+ /// the VT_LPWSTR form is used here.
70
+ pub const PROPVARIANT = extern struct {
71
+ vt: u16,
72
+ reserved1: u16 = 0,
73
+ reserved2: u16 = 0,
74
+ reserved3: u16 = 0,
75
+ value: [2]usize = .{ 0, 0 },
76
+ };
77
+
78
+ /// The longest path the wide-character file APIs accept.
79
+ pub const MAX_PATH_WIDE: usize = 32768;
80
+
81
+ pub const HKEY_CURRENT_USER: HKEY = @ptrFromInt(0x80000001);
82
+ pub const KEY_READ: DWORD = 0x20019;
83
+ pub const KEY_WRITE: DWORD = 0x20006;
84
+ pub const REG_SZ: DWORD = 1;
85
+ pub const REG_DWORD: DWORD = 4;
86
+ pub const ERROR_SUCCESS: i32 = 0;
87
+ pub const ERROR_FILE_NOT_FOUND: i32 = 2;
88
+
89
+ pub const PROCESS_QUERY_LIMITED_INFORMATION: DWORD = 0x1000;
90
+ pub const SYNCHRONIZE: DWORD = 0x0010_0000;
91
+ pub const WAIT_OBJECT_0: DWORD = 0;
92
+ pub const WAIT_ABANDONED: DWORD = 0x80;
93
+ pub const CREATE_UNICODE_ENVIRONMENT: DWORD = 0x400;
94
+ pub const CREATE_NO_WINDOW: DWORD = 0x0800_0000;
95
+
96
+ pub const MB_OK: u32 = 0x0;
97
+ pub const MB_RETRYCANCEL: u32 = 0x5;
98
+ pub const MB_YESNO: u32 = 0x4;
99
+ pub const MB_ICONERROR: u32 = 0x10;
100
+ pub const MB_ICONQUESTION: u32 = 0x20;
101
+ pub const MB_SETFOREGROUND: u32 = 0x10000;
102
+ pub const IDYES: i32 = 6;
103
+ pub const IDRETRY: i32 = 4;
104
+
105
+ pub const COINIT_APARTMENTTHREADED: DWORD = 0x2;
106
+ pub const CLSCTX_INPROC_SERVER: DWORD = 0x1;
107
+ pub const SHCNE_ASSOCCHANGED: i32 = 0x0800_0000;
108
+ pub const SHCNF_IDLIST: u32 = 0;
109
+ pub const VT_LPWSTR: u16 = 31;
110
+ pub const MOVEFILE_REPLACE_EXISTING: DWORD = 0x1;
111
+ pub const MOVEFILE_WRITE_THROUGH: DWORD = 0x8;
112
+
113
+ pub const FOLDERID_LocalAppData = GUID.parse("F1B32785-6FBA-4FCF-9D55-7B8E7F157091");
114
+ /// The current user's Start menu Programs folder.
115
+ pub const FOLDERID_Programs = GUID.parse("A77F5D77-2E2B-44C3-A6A2-ABA601054A51");
116
+ pub const CLSID_ShellLink = GUID.parse("00021401-0000-0000-C000-000000000046");
117
+ pub const IID_IShellLinkW = GUID.parse("000214F9-0000-0000-C000-000000000046");
118
+ pub const IID_IPersistFile = GUID.parse("0000010B-0000-0000-C000-000000000046");
119
+ pub const IID_IPropertyStore = GUID.parse("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99");
120
+ pub const PKEY_AppUserModel_ID: PROPERTYKEY = .{ .fmtid = GUID.parse("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"), .pid = 5 };
121
+
122
+ pub const IUnknownVtbl = extern struct {
123
+ QueryInterface: *const fn (*anyopaque, *const GUID, *?*anyopaque) callconv(.winapi) HRESULT,
124
+ AddRef: *const fn (*anyopaque) callconv(.winapi) u32,
125
+ Release: *const fn (*anyopaque) callconv(.winapi) u32,
126
+ };
127
+
128
+ pub const IShellLinkW = extern struct {
129
+ vtable: *const Vtbl,
130
+ pub const Vtbl = extern struct {
131
+ unknown: IUnknownVtbl,
132
+ GetPath: *const anyopaque,
133
+ GetIDList: *const anyopaque,
134
+ SetIDList: *const anyopaque,
135
+ GetDescription: *const anyopaque,
136
+ SetDescription: *const fn (*IShellLinkW, [*:0]const u16) callconv(.winapi) HRESULT,
137
+ GetWorkingDirectory: *const anyopaque,
138
+ SetWorkingDirectory: *const fn (*IShellLinkW, [*:0]const u16) callconv(.winapi) HRESULT,
139
+ GetArguments: *const anyopaque,
140
+ SetArguments: *const fn (*IShellLinkW, [*:0]const u16) callconv(.winapi) HRESULT,
141
+ GetHotkey: *const anyopaque,
142
+ SetHotkey: *const anyopaque,
143
+ GetShowCmd: *const anyopaque,
144
+ SetShowCmd: *const anyopaque,
145
+ GetIconLocation: *const anyopaque,
146
+ SetIconLocation: *const fn (*IShellLinkW, [*:0]const u16, i32) callconv(.winapi) HRESULT,
147
+ SetRelativePath: *const anyopaque,
148
+ Resolve: *const anyopaque,
149
+ SetPath: *const fn (*IShellLinkW, [*:0]const u16) callconv(.winapi) HRESULT,
150
+ };
151
+ };
152
+
153
+ pub const IPersistFile = extern struct {
154
+ vtable: *const Vtbl,
155
+ pub const Vtbl = extern struct {
156
+ unknown: IUnknownVtbl,
157
+ GetClassID: *const anyopaque,
158
+ IsDirty: *const anyopaque,
159
+ Load: *const anyopaque,
160
+ Save: *const fn (*IPersistFile, [*:0]const u16, BOOL) callconv(.winapi) HRESULT,
161
+ SaveCompleted: *const anyopaque,
162
+ GetCurFile: *const anyopaque,
163
+ };
164
+ };
165
+
166
+ pub const IPropertyStore = extern struct {
167
+ vtable: *const Vtbl,
168
+ pub const Vtbl = extern struct {
169
+ unknown: IUnknownVtbl,
170
+ GetCount: *const anyopaque,
171
+ GetAt: *const anyopaque,
172
+ GetValue: *const anyopaque,
173
+ SetValue: *const fn (*IPropertyStore, *const PROPERTYKEY, *const PROPVARIANT) callconv(.winapi) HRESULT,
174
+ Commit: *const fn (*IPropertyStore) callconv(.winapi) HRESULT,
175
+ };
176
+ };
177
+
178
+ pub extern "kernel32" fn GetLastError() callconv(.winapi) DWORD;
179
+ pub extern "kernel32" fn GetModuleFileNameW(hModule: ?HANDLE, lpFilename: [*]u16, nSize: DWORD) callconv(.winapi) DWORD;
180
+ pub extern "kernel32" fn GetCommandLineW() callconv(.winapi) [*:0]u16;
181
+ pub extern "kernel32" fn CreateProcessW(
182
+ lpApplicationName: ?[*:0]const u16,
183
+ lpCommandLine: ?[*:0]u16,
184
+ lpProcessAttributes: ?*anyopaque,
185
+ lpThreadAttributes: ?*anyopaque,
186
+ bInheritHandles: BOOL,
187
+ dwCreationFlags: DWORD,
188
+ lpEnvironment: ?*anyopaque,
189
+ lpCurrentDirectory: ?[*:0]const u16,
190
+ lpStartupInfo: *STARTUPINFOW,
191
+ lpProcessInformation: *PROCESS_INFORMATION,
192
+ ) callconv(.winapi) BOOL;
193
+ pub extern "kernel32" fn CloseHandle(hObject: HANDLE) callconv(.winapi) BOOL;
194
+ pub extern "kernel32" fn WaitForSingleObject(hHandle: HANDLE, dwMilliseconds: DWORD) callconv(.winapi) DWORD;
195
+ pub extern "kernel32" fn CreateMutexW(lpMutexAttributes: ?*anyopaque, bInitialOwner: BOOL, lpName: ?[*:0]const u16) callconv(.winapi) ?HANDLE;
196
+ pub extern "kernel32" fn ReleaseMutex(hMutex: HANDLE) callconv(.winapi) BOOL;
197
+ pub extern "kernel32" fn OpenProcess(dwDesiredAccess: DWORD, bInheritHandle: BOOL, dwProcessId: DWORD) callconv(.winapi) ?HANDLE;
198
+ pub extern "kernel32" fn GetCurrentProcessId() callconv(.winapi) DWORD;
199
+ pub extern "kernel32" fn K32EnumProcesses(lpidProcess: [*]DWORD, cb: DWORD, lpcbNeeded: *DWORD) callconv(.winapi) BOOL;
200
+ pub extern "kernel32" fn QueryFullProcessImageNameW(hProcess: HANDLE, dwFlags: DWORD, lpExeName: [*]u16, lpdwSize: *DWORD) callconv(.winapi) BOOL;
201
+ pub extern "kernel32" fn CopyFileW(lpExistingFileName: [*:0]const u16, lpNewFileName: [*:0]const u16, bFailIfExists: BOOL) callconv(.winapi) BOOL;
202
+ pub extern "kernel32" fn MoveFileExW(lpExistingFileName: [*:0]const u16, lpNewFileName: ?[*:0]const u16, dwFlags: DWORD) callconv(.winapi) BOOL;
203
+ pub extern "kernel32" fn DeleteFileW(lpFileName: [*:0]const u16) callconv(.winapi) BOOL;
204
+ pub extern "kernel32" fn GetTempPathW(nBufferLength: DWORD, lpBuffer: [*]u16) callconv(.winapi) DWORD;
205
+ pub extern "kernel32" fn GetSystemDirectoryW(lpBuffer: [*]u16, uSize: u32) callconv(.winapi) u32;
206
+ pub extern "kernel32" fn Sleep(dwMilliseconds: DWORD) callconv(.winapi) void;
207
+
208
+ pub extern "user32" fn MessageBoxW(hWnd: ?HWND, lpText: [*:0]const u16, lpCaption: [*:0]const u16, uType: u32) callconv(.winapi) i32;
209
+ pub extern "user32" fn AllowSetForegroundWindow(dwProcessId: DWORD) callconv(.winapi) BOOL;
210
+
211
+ pub extern "shell32" fn SHGetKnownFolderPath(rfid: *const GUID, dwFlags: DWORD, hToken: ?HANDLE, ppszPath: *?[*:0]u16) callconv(.winapi) HRESULT;
212
+ pub extern "shell32" fn SHChangeNotify(wEventId: i32, uFlags: u32, dwItem1: ?*const anyopaque, dwItem2: ?*const anyopaque) callconv(.winapi) void;
213
+
214
+ pub extern "ole32" fn CoInitializeEx(pvReserved: ?*anyopaque, dwCoInit: DWORD) callconv(.winapi) HRESULT;
215
+ pub extern "ole32" fn CoUninitialize() callconv(.winapi) void;
216
+ pub extern "ole32" fn CoCreateInstance(rclsid: *const GUID, pUnkOuter: ?*anyopaque, dwClsContext: DWORD, riid: *const GUID, ppv: *?*anyopaque) callconv(.winapi) HRESULT;
217
+ pub extern "ole32" fn CoTaskMemFree(pv: ?*anyopaque) callconv(.winapi) void;
218
+
219
+ pub extern "advapi32" fn RegCreateKeyExW(hKey: HKEY, lpSubKey: [*:0]const u16, Reserved: DWORD, lpClass: ?[*:0]const u16, dwOptions: DWORD, samDesired: DWORD, lpSecurityAttributes: ?*anyopaque, phkResult: *HKEY, lpdwDisposition: ?*DWORD) callconv(.winapi) i32;
220
+ pub extern "advapi32" fn RegOpenKeyExW(hKey: HKEY, lpSubKey: [*:0]const u16, ulOptions: DWORD, samDesired: DWORD, phkResult: *HKEY) callconv(.winapi) i32;
221
+ pub extern "advapi32" fn RegSetValueExW(hKey: HKEY, lpValueName: ?[*:0]const u16, Reserved: DWORD, dwType: DWORD, lpData: [*]const u8, cbData: DWORD) callconv(.winapi) i32;
222
+ pub extern "advapi32" fn RegQueryValueExW(hKey: HKEY, lpValueName: ?[*:0]const u16, lpReserved: ?*DWORD, lpType: ?*DWORD, lpData: ?[*]u8, lpcbData: ?*DWORD) callconv(.winapi) i32;
223
+ pub extern "advapi32" fn RegDeleteValueW(hKey: HKEY, lpValueName: ?[*:0]const u16) callconv(.winapi) i32;
224
+ pub extern "advapi32" fn RegDeleteTreeW(hKey: HKEY, lpSubKey: ?[*:0]const u16) callconv(.winapi) i32;
225
+ pub extern "advapi32" fn RegDeleteKeyW(hKey: HKEY, lpSubKey: [*:0]const u16) callconv(.winapi) i32;
226
+ pub extern "advapi32" fn RegQueryInfoKeyW(
227
+ hKey: HKEY,
228
+ lpClass: ?[*]u16,
229
+ lpcchClass: ?*DWORD,
230
+ lpReserved: ?*DWORD,
231
+ lpcSubKeys: ?*DWORD,
232
+ lpcbMaxSubKeyLen: ?*DWORD,
233
+ lpcbMaxClassLen: ?*DWORD,
234
+ lpcValues: ?*DWORD,
235
+ lpcbMaxValueNameLen: ?*DWORD,
236
+ lpcbMaxValueLen: ?*DWORD,
237
+ lpcbSecurityDescriptor: ?*DWORD,
238
+ lpftLastWriteTime: ?*anyopaque,
239
+ ) callconv(.winapi) i32;
240
+ pub extern "advapi32" fn RegCloseKey(hKey: HKEY) callconv(.winapi) i32;
241
+
242
+ test "GUIDs parse from their registry form" {
243
+ const guid = GUID.parse("00021401-0000-0000-C000-000000000046");
244
+ try std.testing.expectEqual(@as(u32, 0x00021401), guid.data1);
245
+ try std.testing.expectEqual(@as(u8, 0xC0), guid.data4[0]);
246
+ try std.testing.expectEqual(@as(u8, 0x46), guid.data4[7]);
247
+ try std.testing.expectEqual(@as(usize, if (@sizeOf(usize) == 8) 24 else 16), @sizeOf(PROPVARIANT));
248
+ }
@@ -6,7 +6,7 @@ const tooling = @import("tooling");
6
6
  const automation_protocol = @import("automation_protocol");
7
7
  const cli_build_info = @import("cli_build_info");
8
8
 
9
- const version = "0.11.0-fork.3";
9
+ const version = "0.11.0-fork.4";
10
10
 
11
11
  pub fn main(init: std.process.Init) !void {
12
12
  const allocator = init.arena.allocator();
@@ -105,15 +105,15 @@ pub fn main(init: std.process.Init) !void {
105
105
  };
106
106
  } else if (std.mem.eql(u8, command, "update")) {
107
107
  tooling.update.run(allocator, init.io, args[2..]) catch |err| switch (err) {
108
- error.InvalidArguments => fail("usage: native update keygen [--private-key path] | sign --archive app.zip --url https://example.com/app.zip --target macos-aarch64|macos-x86_64 [--manifest app.json] [--private-key path] [--notes text] [--output native-update.json]"),
108
+ error.InvalidArguments => fail("usage: native update keygen [--private-key path] | sign --archive app.zip --url https://example.com/app.zip --target macos-aarch64|macos-x86_64|windows-x86_64|windows-aarch64 [--manifest app.json] [--private-key path] [--notes text] [--output native-update.json] [--allow-loopback-http]"),
109
109
  error.KeyAlreadyExists => fail("the private update key already exists; choose another --private-key path or move the existing key explicitly"),
110
- error.InvalidArchiveUrl => fail("update archive URLs must use HTTPS and be at most 4096 bytes"),
111
- error.InvalidTarget => fail("update target must be macos-aarch64 or macos-x86_64"),
110
+ error.InvalidArchiveUrl => fail("update archive URLs must use HTTPS and be at most 4096 bytes (--allow-loopback-http also admits http://127.0.0.1, http://[::1] and http://localhost for local testing)"),
111
+ error.InvalidTarget => fail("update target must be macos-aarch64, macos-x86_64, windows-x86_64 or windows-aarch64"),
112
112
  error.UpdateArchitectureMismatch => fail("the update archive executable does not contain the architecture named by --target"),
113
- error.InvalidArchive => fail("the update archive must be a non-empty .zip containing the packaged .app bundle"),
113
+ error.InvalidArchive => fail("the update archive must be the .zip `native package --update-archive` writes: the packaged .app bundle for macOS, the installed app image for Windows"),
114
114
  error.UpdatesNotConfigured => fail("the app manifest has no updates block; add feed_url and public_key before signing releases"),
115
115
  error.InvalidManifestVersion => fail("the app manifest version must use canonical X.Y.Z numeric components without leading zeroes"),
116
- error.InvalidUpdatePublicKey => fail("the app manifest update public_key is not a valid base64 Ed25519 public key"),
116
+ error.InvalidUpdatePublicKey => fail("the app manifest update public_key or a public_keys entry is not a valid base64 Ed25519 public key"),
117
117
  error.UpdateKeyMismatch => fail("the private update key does not match the public_key embedded in the app manifest"),
118
118
  error.GeneratedFeedInvalid => fail("the generated update feed failed its own verification; no feed was written"),
119
119
  else => return err,
@@ -204,9 +204,9 @@ pub fn main(init: std.process.Init) !void {
204
204
  std.debug.print("bundled {d} assets into {s}\n", .{ stats.asset_count, output_dir });
205
205
  } else if (std.mem.eql(u8, command, "package")) {
206
206
  checkVerbFlags("package", args[2..], .{
207
- .usage = "package [--target macos] [--output path] [--binary path] [--service-binary path] [--assets path] [--web-engine system|chromium] [--web-layer auto|include|exclude] [--cef-dir path] [--cef-auto-install] [--signing none|adhoc|identity] [--identity name] [--entitlements path] [--notarize --notary-profile name] [--archive] [--update-archive]",
207
+ .usage = "package [--target macos] [--output path] [--binary path] [--service-binary path] [--assets path] [--web-engine system|chromium] [--web-layer auto|include|exclude] [--cef-dir path] [--cef-auto-install] [--signing none|adhoc|identity] [--identity name] [--entitlements path] [--notarize --notary-profile name] [--archive] [--update-archive] [--installer]",
208
208
  .value_flags = &.{ "--manifest", "--target", "--output", "--binary", "--service-binary", "--assets", "--web-engine", "--web-layer", "--cef-dir", "--signing", "--identity", "--entitlements", "--notary-profile", "--optimize" },
209
- .bool_flags = &.{ "--cef-auto-install", "--notarize", "--archive", "--update-archive" },
209
+ .bool_flags = &.{ "--cef-auto-install", "--notarize", "--archive", "--update-archive", "--installer" },
210
210
  });
211
211
  const manifest_path = try flagValue(args, "--manifest") orelse tooling.manifest.defaultPath(init.io) orelse "app.json";
212
212
  const metadata = tooling.manifest.readMetadata(allocator, init.io, manifest_path) catch |err| switch (err) {
@@ -283,6 +283,7 @@ pub fn main(init: std.process.Init) !void {
283
283
  .archive = archive,
284
284
  .notarize = flagBool(args, "--notarize"),
285
285
  .update_archive = flagBool(args, "--update-archive"),
286
+ .installer = flagBool(args, "--installer"),
286
287
  .env_map = init.environ_map,
287
288
  });
288
289
  tooling.package.printDiagnostic(stats);
@@ -485,7 +486,7 @@ fn usage() void {
485
486
  \\ doctor [--strict] [--manifest app.json] [--web-engine system|chromium] [--cef-dir path] [--cef-auto-install]
486
487
  \\ validate [app.json|app.zon]
487
488
  \\ bundle-assets [app.json|app.zon] [assets] [output]
488
- \\ package [--target macos|windows|linux|ios|android] [--output path] [--binary path] [--service-binary path] [--assets path] [--web-engine system|chromium] [--web-layer auto|include|exclude] [--cef-dir path] [--cef-auto-install] [--signing none|adhoc|identity] [--identity name] [--entitlements path] [--notarize --notary-profile name] [--archive] [--update-archive]
489
+ \\ package [--target macos|windows|linux|ios|android] [--output path] [--binary path] [--service-binary path] [--assets path] [--web-engine system|chromium] [--web-layer auto|include|exclude] [--cef-dir path] [--cef-auto-install] [--signing none|adhoc|identity] [--identity name] [--entitlements path] [--notarize --notary-profile name] [--archive] [--update-archive] [--installer]
489
490
  \\ dev [--manifest app.json] --binary path [--url http://127.0.0.1:5173/] [--command "npm run dev"] [--timeout-ms 30000]
490
491
  \\ package-windows [--output path] [--binary path] [--service-binary path]
491
492
  \\ package-linux [--output path] [--binary path] [--service-binary path]