@zogien/native-sdk 0.11.0-fork.2 → 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.
- package/README.md +1 -1
- package/build/app.zig +47 -23
- package/build/cli.zig +22 -0
- package/native-sdk.d.ts +2 -0
- package/package.json +10 -10
- package/packages/core/package-lock.json +2 -2
- package/packages/core/package.json +1 -1
- package/schemas/app.schema.json +9 -1
- package/skill-data/ts-core/SKILL.md +2 -0
- package/src/app_runner/app_assets.zig +85 -1
- package/src/app_runner/root.zig +63 -3
- package/src/app_runner/ts_core_main.zig +8 -0
- package/src/platform/macos/appkit_host.h +9 -0
- package/src/platform/macos/appkit_host.m +366 -0
- package/src/platform/macos/root.zig +29 -6
- package/src/platform/null_platform.zig +66 -0
- package/src/platform/root.zig +1 -0
- package/src/platform/types.zig +46 -0
- package/src/platform/windows/root.zig +174 -0
- package/src/platform/windows/scriptc_mingw_compat.c +248 -0
- package/src/platform/windows/webview2_host.cpp +500 -26
- package/src/primitives/app_manifest/root.zig +1 -0
- package/src/primitives/app_manifest/tests.zig +35 -0
- package/src/primitives/app_manifest/types.zig +12 -0
- package/src/primitives/app_manifest/validation.zig +44 -6
- package/src/root.zig +1 -0
- package/src/runtime/bridge_responses.zig +2 -0
- package/src/runtime/channel_sources.zig +49 -1
- package/src/runtime/core.zig +5 -0
- package/src/runtime/effects.zig +12 -1
- package/src/runtime/flow.zig +6 -0
- package/src/runtime/flow_reference.zig +6 -0
- package/src/runtime/root.zig +4 -0
- package/src/runtime/single_instance.zig +893 -0
- package/src/runtime/tests.zig +1 -0
- package/src/runtime/window_controls.zig +73 -0
- package/src/runtime/window_controls_tests.zig +192 -0
- package/src/runtime/window_storage.zig +3 -0
- package/src/tooling/manifest.zig +111 -3
- package/src/tooling/package.zig +98 -8
- package/src/tooling/raw_manifest.zig +7 -0
- package/src/tooling/root.zig +2 -0
- package/src/tooling/templates.zig +53 -3
- package/src/tooling/update.zig +228 -10
- package/src/tooling/windows_installer.zig +245 -0
- package/src/tooling/zip_writer.zig +211 -0
- package/src/updater/engine.zig +936 -0
- package/src/updater/feed.zig +261 -12
- package/src/updater/macos.zig +187 -0
- package/src/updater/root.zig +41 -0
- package/src/updater/windows.zig +470 -0
- package/src/windows_install/config.zig +247 -0
- package/src/windows_install/main.zig +690 -0
- package/src/windows_install/payload.zig +146 -0
- package/src/windows_install/switch.zig +184 -0
- package/src/windows_install/tests.zig +11 -0
- package/src/windows_install/win32.zig +248 -0
- package/tools/native-sdk/main.zig +10 -9
package/src/runtime/tests.zig
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//! Window controls for app-drawn title bars, beyond minimize: maximize,
|
|
2
|
+
//! restore from maximized, and the user-close request. Each is the real
|
|
3
|
+
//! OS verb for a tracked, open window. Like `minimizeWindow`, no runtime
|
|
4
|
+
//! bookkeeping moves here: the host's own frame events report what the
|
|
5
|
+
//! OS did, the same way they report the caption buttons, the system menu,
|
|
6
|
+
//! and keyboard shortcuts.
|
|
7
|
+
const lifecycle = @import("window_lifecycle_policy.zig");
|
|
8
|
+
const std = @import("std");
|
|
9
|
+
const runtime_window_views = @import("window_views.zig");
|
|
10
|
+
const platform = @import("../platform/root.zig");
|
|
11
|
+
|
|
12
|
+
/// The page event a window's WebViews receive when its maximized state
|
|
13
|
+
/// changes, whoever changed it, so a page-drawn maximize/restore button
|
|
14
|
+
/// can follow the caption double-click, the system menu, and keyboard
|
|
15
|
+
/// shortcuts: `window.zero.on("window:state", ({ maximized }) => ...)`.
|
|
16
|
+
pub const window_state_event = "window:state";
|
|
17
|
+
|
|
18
|
+
/// Called by the runtime's window-state update when `maximized` flips.
|
|
19
|
+
/// Best effort, like the other page notifications: the detail is always
|
|
20
|
+
/// valid JSON, so the only failure is a host without page events
|
|
21
|
+
/// (`error.UnsupportedService`), where no page exists to tell.
|
|
22
|
+
pub fn announceMaximized(runtime: anytype, window_id: platform.WindowId, maximized: bool) void {
|
|
23
|
+
var detail_buffer: [64]u8 = undefined;
|
|
24
|
+
const detail = std.fmt.bufPrint(&detail_buffer, "{{\"windowId\":{d},\"maximized\":{s}}}", .{ window_id, if (maximized) "true" else "false" }) catch return;
|
|
25
|
+
runtime.options.platform.services.emitWindowEvent(window_id, window_state_event, detail) catch {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub fn RuntimeWindowControls(comptime Runtime: type) type {
|
|
29
|
+
const WindowViewMethods = runtime_window_views.RuntimeWindowViews(Runtime);
|
|
30
|
+
|
|
31
|
+
return struct {
|
|
32
|
+
/// Maximize a window. Refused for a non-resizable window
|
|
33
|
+
/// (`error.WindowNotResizable`) and for one that is not on the
|
|
34
|
+
/// glass (`error.WindowHidden`). `WindowInfo.maximized` follows
|
|
35
|
+
/// the host's frame event.
|
|
36
|
+
pub fn maximizeWindow(self: *Runtime, window_id: platform.WindowId) anyerror!void {
|
|
37
|
+
try requireOpenWindow(self, window_id);
|
|
38
|
+
try self.options.platform.services.maximizeWindow(window_id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Restore a maximized window to its normal frame. A window that
|
|
42
|
+
/// is not maximized is left as it is.
|
|
43
|
+
pub fn unmaximizeWindow(self: *Runtime, window_id: platform.WindowId) anyerror!void {
|
|
44
|
+
try requireOpenWindow(self, window_id);
|
|
45
|
+
try self.options.platform.services.unmaximizeWindow(window_id);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Ask a window to close as if the user pressed its close button,
|
|
49
|
+
/// so its `close_policy` applies: `.hide` hides it and keeps its
|
|
50
|
+
/// views, `.quit` closes it and dispatches `window_closed`.
|
|
51
|
+
/// `closeWindow` remains the app's own unconditional close. The
|
|
52
|
+
/// request is queued; its outcome arrives as the host's frame
|
|
53
|
+
/// events, so success here means the request was delivered.
|
|
54
|
+
pub fn requestCloseWindow(self: *Runtime, window_id: platform.WindowId) anyerror!void {
|
|
55
|
+
try requireOpenWindow(self, window_id);
|
|
56
|
+
try self.options.platform.services.requestCloseWindow(window_id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// The liveness gate every window verb shares: a closed window
|
|
60
|
+
/// keeps its table slot, so a resolvable id is not enough.
|
|
61
|
+
fn requireOpenWindow(self: *Runtime, window_id: platform.WindowId) !void {
|
|
62
|
+
if (self.window_lifecycle_policy) |policy| {
|
|
63
|
+
// The compiled policy knows close, hide, show, and focus,
|
|
64
|
+
// which move runtime flags, and minimize, which moves
|
|
65
|
+
// none. These controls move none either, so they take
|
|
66
|
+
// minimize's admission: the window exists and is open.
|
|
67
|
+
return lifecycle.plan(policy, self, .begin, .{ .id = window_id, .variant = @intFromEnum(lifecycle.Verb.minimize) }).admitted();
|
|
68
|
+
}
|
|
69
|
+
const index = WindowViewMethods.findWindowIndexById(self, window_id) orelse return error.WindowNotFound;
|
|
70
|
+
if (!self.windows[index].info.open) return error.WindowNotFound;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
const support = @import("test_support.zig");
|
|
2
|
+
const std = support.std;
|
|
3
|
+
const platform = support.platform;
|
|
4
|
+
const App = support.App;
|
|
5
|
+
const Runtime = support.Runtime;
|
|
6
|
+
const Event = support.Event;
|
|
7
|
+
const TestHarness = support.TestHarness;
|
|
8
|
+
const window_controls = @import("window_controls.zig");
|
|
9
|
+
const bridge_responses = @import("bridge_responses.zig");
|
|
10
|
+
|
|
11
|
+
const ControlsApp = struct {
|
|
12
|
+
window_closed_count: u32 = 0,
|
|
13
|
+
|
|
14
|
+
fn app(self: *@This()) App {
|
|
15
|
+
return .{ .context = self, .name = "window-controls", .source = platform.WebViewSource.html("<p>Controls</p>"), .event_fn = event };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fn event(context: *anyopaque, runtime: *Runtime, incoming: Event) !void {
|
|
19
|
+
_ = runtime;
|
|
20
|
+
const self: *@This() = @ptrCast(@alignCast(context));
|
|
21
|
+
if (incoming == .window_closed) self.window_closed_count += 1;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
fn windowInfo(runtime: *const Runtime, window_id: platform.WindowId) ?platform.WindowInfo {
|
|
26
|
+
for (runtime.windows[0..runtime.window_count]) |window| {
|
|
27
|
+
if (window.info.id == window_id) return window.info;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn expectEveryControlRefused(runtime: *Runtime, window_id: platform.WindowId, expected: anyerror) !void {
|
|
33
|
+
try std.testing.expectError(expected, runtime.maximizeWindow(window_id));
|
|
34
|
+
try std.testing.expectError(expected, runtime.unmaximizeWindow(window_id));
|
|
35
|
+
try std.testing.expectError(expected, runtime.requestCloseWindow(window_id));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test "maximize and unmaximize reach the host and maximized follows its frame events" {
|
|
39
|
+
const harness = try TestHarness().create(std.testing.allocator, .{});
|
|
40
|
+
defer harness.destroy(std.testing.allocator);
|
|
41
|
+
var app_state: ControlsApp = .{};
|
|
42
|
+
try harness.start(app_state.app());
|
|
43
|
+
|
|
44
|
+
const tools = try harness.runtime.createWindow(.{ .label = "tools", .title = "Tools" });
|
|
45
|
+
try harness.runtime.maximizeWindow(tools.id);
|
|
46
|
+
// The verb moves no runtime state; the host's frame event reports it.
|
|
47
|
+
try std.testing.expect(!windowInfo(&harness.runtime, tools.id).?.maximized);
|
|
48
|
+
try harness.runtime.dispatchPlatformEvent(app_state.app(), harness.null_platform.windowFrameEvent(tools.id).?);
|
|
49
|
+
try std.testing.expect(windowInfo(&harness.runtime, tools.id).?.maximized);
|
|
50
|
+
try std.testing.expect(windowInfo(&harness.runtime, tools.id).?.state().maximized);
|
|
51
|
+
var output: [platform.max_windows]platform.WindowInfo = undefined;
|
|
52
|
+
for (harness.runtime.listWindows(&output)) |listed| {
|
|
53
|
+
if (listed.id == tools.id) try std.testing.expect(listed.maximized);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try harness.runtime.unmaximizeWindow(tools.id);
|
|
57
|
+
try harness.runtime.dispatchPlatformEvent(app_state.app(), harness.null_platform.windowFrameEvent(tools.id).?);
|
|
58
|
+
try std.testing.expect(!windowInfo(&harness.runtime, tools.id).?.maximized);
|
|
59
|
+
// Restoring a window that is not maximized leaves it as it is.
|
|
60
|
+
try harness.runtime.unmaximizeWindow(tools.id);
|
|
61
|
+
try std.testing.expect(!harness.null_platform.windows[1].maximized);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
test "a maximized change reaches the window's pages and its window JSON" {
|
|
65
|
+
const harness = try TestHarness().create(std.testing.allocator, .{});
|
|
66
|
+
defer harness.destroy(std.testing.allocator);
|
|
67
|
+
var app_state: ControlsApp = .{};
|
|
68
|
+
try harness.start(app_state.app());
|
|
69
|
+
|
|
70
|
+
const tools = try harness.runtime.createWindow(.{ .label = "tools" });
|
|
71
|
+
try harness.runtime.maximizeWindow(tools.id);
|
|
72
|
+
const before = harness.null_platform.windowEventCount();
|
|
73
|
+
try harness.runtime.dispatchPlatformEvent(app_state.app(), harness.null_platform.windowFrameEvent(tools.id).?);
|
|
74
|
+
try std.testing.expectEqual(before + 1, harness.null_platform.windowEventCount());
|
|
75
|
+
try std.testing.expectEqual(tools.id, harness.null_platform.lastWindowEventWindowId());
|
|
76
|
+
try std.testing.expectEqualStrings(window_controls.window_state_event, harness.null_platform.lastWindowEventName());
|
|
77
|
+
try std.testing.expectEqualStrings("{\"windowId\":2,\"maximized\":true}", harness.null_platform.lastWindowEventDetail());
|
|
78
|
+
|
|
79
|
+
// A page loading now reads the same state from the window list.
|
|
80
|
+
var json_buffer: [512]u8 = undefined;
|
|
81
|
+
var writer = std.Io.Writer.fixed(&json_buffer);
|
|
82
|
+
try bridge_responses.writeWindowJsonToWriter(windowInfo(&harness.runtime, tools.id).?, &writer);
|
|
83
|
+
try std.testing.expect(std.mem.endsWith(u8, writer.buffered(), ",\"maximized\":true}"));
|
|
84
|
+
|
|
85
|
+
// A frame event that leaves the state alone (a move) tells nobody.
|
|
86
|
+
try harness.runtime.dispatchPlatformEvent(app_state.app(), harness.null_platform.windowFrameEvent(tools.id).?);
|
|
87
|
+
try std.testing.expectEqual(before + 1, harness.null_platform.windowEventCount());
|
|
88
|
+
|
|
89
|
+
try harness.runtime.unmaximizeWindow(tools.id);
|
|
90
|
+
try harness.runtime.dispatchPlatformEvent(app_state.app(), harness.null_platform.windowFrameEvent(tools.id).?);
|
|
91
|
+
try std.testing.expectEqual(before + 2, harness.null_platform.windowEventCount());
|
|
92
|
+
try std.testing.expectEqualStrings("{\"windowId\":2,\"maximized\":false}", harness.null_platform.lastWindowEventDetail());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
test "a maximize the app did not request still updates maximized" {
|
|
96
|
+
const harness = try TestHarness().create(std.testing.allocator, .{});
|
|
97
|
+
defer harness.destroy(std.testing.allocator);
|
|
98
|
+
var app_state: ControlsApp = .{};
|
|
99
|
+
try harness.start(app_state.app());
|
|
100
|
+
|
|
101
|
+
// A caption double-click, the system menu, or Win+Up: the host's
|
|
102
|
+
// frame event is the only signal, and it is enough.
|
|
103
|
+
var state = windowInfo(&harness.runtime, 1).?.state();
|
|
104
|
+
state.maximized = true;
|
|
105
|
+
try harness.runtime.dispatchPlatformEvent(app_state.app(), .{ .window_frame_changed = state });
|
|
106
|
+
try std.testing.expect(windowInfo(&harness.runtime, 1).?.maximized);
|
|
107
|
+
state.maximized = false;
|
|
108
|
+
try harness.runtime.dispatchPlatformEvent(app_state.app(), .{ .window_frame_changed = state });
|
|
109
|
+
try std.testing.expect(!windowInfo(&harness.runtime, 1).?.maximized);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
test "window controls refuse unknown, closed, hidden, and non-resizable windows" {
|
|
113
|
+
const harness = try TestHarness().create(std.testing.allocator, .{});
|
|
114
|
+
defer harness.destroy(std.testing.allocator);
|
|
115
|
+
var app_state: ControlsApp = .{};
|
|
116
|
+
try harness.start(app_state.app());
|
|
117
|
+
|
|
118
|
+
try expectEveryControlRefused(&harness.runtime, 99, error.WindowNotFound);
|
|
119
|
+
|
|
120
|
+
const fixed = try harness.runtime.createWindow(.{ .label = "fixed", .resizable = false });
|
|
121
|
+
try std.testing.expectError(error.WindowNotResizable, harness.runtime.maximizeWindow(fixed.id));
|
|
122
|
+
|
|
123
|
+
const tools = try harness.runtime.createWindow(.{ .label = "tools" });
|
|
124
|
+
try harness.runtime.hideWindow(tools.id);
|
|
125
|
+
try std.testing.expectError(error.WindowHidden, harness.runtime.maximizeWindow(tools.id));
|
|
126
|
+
|
|
127
|
+
// A closed window keeps its runtime slot; resolving its id is not
|
|
128
|
+
// liveness, and no verb reaches the host for it.
|
|
129
|
+
try harness.runtime.closeWindow(tools.id);
|
|
130
|
+
try expectEveryControlRefused(&harness.runtime, tools.id, error.WindowNotFound);
|
|
131
|
+
try std.testing.expect(harness.null_platform.takeQueuedCloseRequest() == null);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
test "requestCloseWindow closes a quit-policy window the way the user's close button does" {
|
|
135
|
+
const harness = try TestHarness().create(std.testing.allocator, .{});
|
|
136
|
+
defer harness.destroy(std.testing.allocator);
|
|
137
|
+
var app_state: ControlsApp = .{};
|
|
138
|
+
try harness.start(app_state.app());
|
|
139
|
+
|
|
140
|
+
const tools = try harness.runtime.createWindow(.{ .label = "tools" });
|
|
141
|
+
try harness.runtime.requestCloseWindow(tools.id);
|
|
142
|
+
// Only a request: the window is untouched until the host handles it.
|
|
143
|
+
try std.testing.expect(windowInfo(&harness.runtime, tools.id).?.open);
|
|
144
|
+
try std.testing.expectEqual(@as(u32, 0), app_state.window_closed_count);
|
|
145
|
+
|
|
146
|
+
try harness.runtime.dispatchPlatformEvent(app_state.app(), harness.null_platform.takeQueuedCloseRequest().?);
|
|
147
|
+
try std.testing.expect(!windowInfo(&harness.runtime, tools.id).?.open);
|
|
148
|
+
try std.testing.expectEqual(@as(u32, 1), app_state.window_closed_count);
|
|
149
|
+
try std.testing.expect(harness.null_platform.takeQueuedCloseRequest() == null);
|
|
150
|
+
|
|
151
|
+
// closeWindow keeps its meaning: the app's own close, which never
|
|
152
|
+
// reports back as a user close.
|
|
153
|
+
const other = try harness.runtime.createWindow(.{ .label = "other" });
|
|
154
|
+
try harness.runtime.closeWindow(other.id);
|
|
155
|
+
try std.testing.expectEqual(@as(u32, 1), app_state.window_closed_count);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
test "requestCloseWindow hides a hide-policy window and keeps it alive" {
|
|
159
|
+
const harness = try TestHarness().create(std.testing.allocator, .{});
|
|
160
|
+
defer harness.destroy(std.testing.allocator);
|
|
161
|
+
var app_state: ControlsApp = .{};
|
|
162
|
+
try harness.start(app_state.app());
|
|
163
|
+
|
|
164
|
+
const palette = try harness.runtime.createWindow(.{ .label = "palette", .close_policy = .hide });
|
|
165
|
+
_ = try harness.runtime.createView(.{ .window_id = palette.id, .label = "action", .kind = .button, .frame = .init(10, 10, 80, 30) });
|
|
166
|
+
const view_count = harness.runtime.view_count;
|
|
167
|
+
|
|
168
|
+
try harness.runtime.requestCloseWindow(palette.id);
|
|
169
|
+
try harness.runtime.dispatchPlatformEvent(app_state.app(), harness.null_platform.takeQueuedCloseRequest().?);
|
|
170
|
+
const info = windowInfo(&harness.runtime, palette.id).?;
|
|
171
|
+
try std.testing.expect(info.open);
|
|
172
|
+
try std.testing.expect(info.hidden);
|
|
173
|
+
try std.testing.expectEqual(view_count, harness.runtime.view_count);
|
|
174
|
+
try std.testing.expectEqual(@as(u32, 0), app_state.window_closed_count);
|
|
175
|
+
|
|
176
|
+
// The hidden window comes back with its views, as after a user close.
|
|
177
|
+
try harness.runtime.showWindow(palette.id);
|
|
178
|
+
try std.testing.expect(!windowInfo(&harness.runtime, palette.id).?.hidden);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
test "a close request for a window the app closed first is dropped" {
|
|
182
|
+
const harness = try TestHarness().create(std.testing.allocator, .{});
|
|
183
|
+
defer harness.destroy(std.testing.allocator);
|
|
184
|
+
var app_state: ControlsApp = .{};
|
|
185
|
+
try harness.start(app_state.app());
|
|
186
|
+
|
|
187
|
+
const tools = try harness.runtime.createWindow(.{ .label = "tools" });
|
|
188
|
+
try harness.runtime.requestCloseWindow(tools.id);
|
|
189
|
+
try harness.runtime.closeWindow(tools.id);
|
|
190
|
+
try std.testing.expect(harness.null_platform.takeQueuedCloseRequest() == null);
|
|
191
|
+
try std.testing.expectEqual(@as(u32, 0), app_state.window_closed_count);
|
|
192
|
+
}
|
|
@@ -220,6 +220,9 @@ pub fn RuntimeWindowStorage(comptime Runtime: type) type {
|
|
|
220
220
|
const index = existing_index orelse try Self.reserveWindow(self, state.id, state.label, state.title, null, true, .allow_source_less);
|
|
221
221
|
self.windows[index].info.frame = state.frame;
|
|
222
222
|
self.windows[index].info.scale_factor = state.scale_factor;
|
|
223
|
+
const was_maximized = self.windows[index].info.maximized;
|
|
224
|
+
self.windows[index].info.maximized = state.maximized;
|
|
225
|
+
if (was_maximized != state.maximized) @import("window_controls.zig").announceMaximized(self, state.id, state.maximized);
|
|
223
226
|
if (self.window_lifecycle_policy == null) {
|
|
224
227
|
self.windows[index].info.open = state.open;
|
|
225
228
|
self.windows[index].info.hidden = state.hidden;
|
package/src/tooling/manifest.zig
CHANGED
|
@@ -212,6 +212,9 @@ pub const Metadata = struct {
|
|
|
212
212
|
if (self.url_schemes.len > 0) allocator.free(self.url_schemes);
|
|
213
213
|
if (self.updates.feed_url) |value| allocator.free(value);
|
|
214
214
|
if (self.updates.public_key) |value| allocator.free(value);
|
|
215
|
+
for (self.updates.public_keys) |value| allocator.free(value);
|
|
216
|
+
if (self.updates.public_keys.len > 0) allocator.free(self.updates.public_keys);
|
|
217
|
+
if (self.updates.mode) |value| allocator.free(value);
|
|
215
218
|
if (self.dmg.volume_name) |value| allocator.free(value);
|
|
216
219
|
if (self.dmg.background) |value| allocator.free(value);
|
|
217
220
|
for (self.dmg.items) |item| {
|
|
@@ -425,11 +428,50 @@ pub const DmgMetadata = struct {
|
|
|
425
428
|
pub const UpdateMetadata = struct {
|
|
426
429
|
feed_url: ?[]const u8 = null,
|
|
427
430
|
public_key: ?[]const u8 = null,
|
|
431
|
+
public_keys: []const []const u8 = &.{},
|
|
428
432
|
check_on_start: bool = false,
|
|
433
|
+
/// `host` or `app`; null leaves the host default.
|
|
434
|
+
mode: ?[]const u8 = null,
|
|
429
435
|
|
|
430
436
|
pub fn enabled(self: UpdateMetadata) bool {
|
|
431
437
|
return self.feed_url != null;
|
|
432
438
|
}
|
|
439
|
+
|
|
440
|
+
/// Whether `public_key` is set and it and every `public_keys` entry
|
|
441
|
+
/// decode as base64 Ed25519 public keys.
|
|
442
|
+
pub fn publicKeysAreValid(self: UpdateMetadata) bool {
|
|
443
|
+
const primary = self.public_key orelse return false;
|
|
444
|
+
if (decodeUpdatePublicKey(primary) == null) return false;
|
|
445
|
+
for (self.public_keys) |key| {
|
|
446
|
+
if (decodeUpdatePublicKey(key) == null) return false;
|
|
447
|
+
}
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/// Whether `public_key` or one of `public_keys` decodes to the same key
|
|
452
|
+
/// as this base64 public key.
|
|
453
|
+
pub fn trustsPublicKey(self: UpdateMetadata, public_key: []const u8) bool {
|
|
454
|
+
const wanted = decodeUpdatePublicKey(public_key) orelse return false;
|
|
455
|
+
if (self.public_key) |primary| {
|
|
456
|
+
if (decodeUpdatePublicKey(primary)) |bytes| {
|
|
457
|
+
if (std.mem.eql(u8, &bytes, &wanted)) return true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
for (self.public_keys) |key| {
|
|
461
|
+
if (decodeUpdatePublicKey(key)) |bytes| {
|
|
462
|
+
if (std.mem.eql(u8, &bytes, &wanted)) return true;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
fn decodeUpdatePublicKey(value: []const u8) ?[32]u8 {
|
|
469
|
+
var bytes: [32]u8 = undefined;
|
|
470
|
+
const size = std.base64.standard.Decoder.calcSizeForSlice(value) catch return null;
|
|
471
|
+
if (size != bytes.len) return null;
|
|
472
|
+
std.base64.standard.Decoder.decode(&bytes, value) catch return null;
|
|
473
|
+
return bytes;
|
|
474
|
+
}
|
|
433
475
|
};
|
|
434
476
|
|
|
435
477
|
pub const FrontendDevMetadata = struct {
|
|
@@ -580,7 +622,9 @@ pub fn validateFile(allocator: std.mem.Allocator, io: std.Io, path: []const u8)
|
|
|
580
622
|
defer allocator.free(file_associations);
|
|
581
623
|
const url_schemes = parseUrlSchemes(allocator, metadata.url_schemes) catch return .{ .ok = false, .message = "app manifest URL schemes are invalid" };
|
|
582
624
|
defer allocator.free(url_schemes);
|
|
583
|
-
const
|
|
625
|
+
const updates_message = "app manifest updates are invalid - updates require an HTTPS feed_url and a base64 Ed25519 public_key; public_keys adds at most 3 further distinct keys; feed_url may name {target} and {current_version} in its path or query; check_on_start requires both; mode is \"host\" or \"app\"";
|
|
626
|
+
const updates = convertUpdates(metadata.updates) catch return .{ .ok = false, .message = updates_message };
|
|
627
|
+
app_manifest.validateUpdates(updates) catch return .{ .ok = false, .message = updates_message };
|
|
584
628
|
// General manifest validation owns only values the app explicitly
|
|
585
629
|
// declared. Archive-time fallbacks (notably display_name as the volume
|
|
586
630
|
// name) are validated when a macOS archive is actually requested, so an
|
|
@@ -751,17 +795,20 @@ fn duplicateRawUpdates(allocator: std.mem.Allocator, raw: RawUpdates) !UpdateMet
|
|
|
751
795
|
return .{
|
|
752
796
|
.feed_url = try duplicateOptionalString(allocator, raw.feed_url),
|
|
753
797
|
.public_key = try duplicateOptionalString(allocator, raw.public_key),
|
|
798
|
+
.public_keys = try duplicateStringList(allocator, raw.public_keys),
|
|
754
799
|
.check_on_start = raw.check_on_start,
|
|
800
|
+
.mode = try duplicateOptionalString(allocator, raw.mode),
|
|
755
801
|
};
|
|
756
802
|
}
|
|
757
803
|
|
|
758
804
|
fn convertUpdates(updates: UpdateMetadata) !app_manifest.UpdateConfig {
|
|
759
|
-
if (updates.feed_url == null and updates.public_key == null and !updates.check_on_start) return .{};
|
|
805
|
+
if (updates.feed_url == null and updates.public_key == null and updates.public_keys.len == 0 and !updates.check_on_start and updates.mode == null) return .{};
|
|
760
806
|
const feed_url = updates.feed_url orelse return error.InvalidUpdates;
|
|
761
807
|
const public_key = updates.public_key orelse return error.InvalidUpdates;
|
|
762
808
|
if (!std.mem.startsWith(u8, feed_url, "https://")) return error.InvalidUpdates;
|
|
763
809
|
if (public_key.len == 0) return error.InvalidUpdates;
|
|
764
|
-
|
|
810
|
+
const mode: app_manifest.UpdateMode = if (updates.mode) |value| std.meta.stringToEnum(app_manifest.UpdateMode, value) orelse return error.InvalidUpdates else .host;
|
|
811
|
+
return .{ .feed_url = feed_url, .public_key = public_key, .public_keys = updates.public_keys, .check_on_start = updates.check_on_start, .mode = mode };
|
|
765
812
|
}
|
|
766
813
|
|
|
767
814
|
fn duplicateRawServicePackages(allocator: std.mem.Allocator, packages: []const raw_manifest.RawServicePackage) ![]const ServicePackageMetadata {
|
|
@@ -2555,6 +2602,67 @@ test "JSON manifest parser carries native update configuration" {
|
|
|
2555
2602
|
try std.testing.expectEqualStrings("https://example.com/native-update.json", metadata.updates.feed_url.?);
|
|
2556
2603
|
}
|
|
2557
2604
|
|
|
2605
|
+
test "JSON manifest parser carries per-target feeds and rotated update keys" {
|
|
2606
|
+
const metadata = try parseJsonText(std.testing.allocator,
|
|
2607
|
+
\\{
|
|
2608
|
+
\\ "id": "com.example.updates",
|
|
2609
|
+
\\ "name": "updates",
|
|
2610
|
+
\\ "version": "1.2.3",
|
|
2611
|
+
\\ "updates": {
|
|
2612
|
+
\\ "feed_url": "https://example.com/releases/{target}/native-update.json",
|
|
2613
|
+
\\ "public_key": "wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo=",
|
|
2614
|
+
\\ "public_keys": ["11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo="],
|
|
2615
|
+
\\ "mode": "app"
|
|
2616
|
+
\\ }
|
|
2617
|
+
\\}
|
|
2618
|
+
);
|
|
2619
|
+
defer metadata.deinit(std.testing.allocator);
|
|
2620
|
+
try std.testing.expectEqualStrings("https://example.com/releases/{target}/native-update.json", metadata.updates.feed_url.?);
|
|
2621
|
+
try std.testing.expectEqualStrings("app", metadata.updates.mode.?);
|
|
2622
|
+
try std.testing.expectEqual(app_manifest.UpdateMode.app, (try convertUpdates(metadata.updates)).mode);
|
|
2623
|
+
try std.testing.expect(metadata.updates.trustsPublicKey("wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo="));
|
|
2624
|
+
try std.testing.expect(metadata.updates.trustsPublicKey("11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo="));
|
|
2625
|
+
try std.testing.expect(!metadata.updates.trustsPublicKey("IBLLkMpg6OXY2vZuInLSIz4EhtVX6MZhQe2JIBd9frc="));
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
test "manifest validation names the update rules" {
|
|
2629
|
+
var cwd = std.Io.Dir.cwd();
|
|
2630
|
+
const root = ".zig-cache/test-validate-updates";
|
|
2631
|
+
try cwd.deleteTree(std.testing.io, root);
|
|
2632
|
+
defer cwd.deleteTree(std.testing.io, root) catch {};
|
|
2633
|
+
try cwd.createDirPath(std.testing.io, root);
|
|
2634
|
+
try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/app.json", .data =
|
|
2635
|
+
\\{
|
|
2636
|
+
\\ "id": "com.example.updates",
|
|
2637
|
+
\\ "name": "updates",
|
|
2638
|
+
\\ "version": "1.2.3",
|
|
2639
|
+
\\ "updates": {
|
|
2640
|
+
\\ "feed_url": "https://example.com/{arch}/native-update.json",
|
|
2641
|
+
\\ "public_key": "wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo="
|
|
2642
|
+
\\ }
|
|
2643
|
+
\\}
|
|
2644
|
+
});
|
|
2645
|
+
const result = try validateFile(std.testing.allocator, std.testing.io, root ++ "/app.json");
|
|
2646
|
+
try std.testing.expect(!result.ok);
|
|
2647
|
+
try std.testing.expect(std.mem.indexOf(u8, result.message, "{target} and {current_version}") != null);
|
|
2648
|
+
|
|
2649
|
+
try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/app.json", .data =
|
|
2650
|
+
\\{
|
|
2651
|
+
\\ "id": "com.example.updates",
|
|
2652
|
+
\\ "name": "updates",
|
|
2653
|
+
\\ "version": "1.2.3",
|
|
2654
|
+
\\ "updates": {
|
|
2655
|
+
\\ "feed_url": "https://example.com/{target}/native-update.json",
|
|
2656
|
+
\\ "public_key": "wFDFY3pE+oYp//PMzOIwDLNipj2Z2V/FQUUmb0MyRFo=",
|
|
2657
|
+
\\ "mode": "automatic"
|
|
2658
|
+
\\ }
|
|
2659
|
+
\\}
|
|
2660
|
+
});
|
|
2661
|
+
const mode_result = try validateFile(std.testing.allocator, std.testing.io, root ++ "/app.json");
|
|
2662
|
+
try std.testing.expect(!mode_result.ok);
|
|
2663
|
+
try std.testing.expect(std.mem.indexOf(u8, mode_result.message, "mode is \"host\" or \"app\"") != null);
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2558
2666
|
test "JSON file validation rejects null with a teaching diagnostic regardless of extension case" {
|
|
2559
2667
|
var cwd = std.Io.Dir.cwd();
|
|
2560
2668
|
const root = ".zig-cache/test-validate-json-null";
|
package/src/tooling/package.zig
CHANGED
|
@@ -9,6 +9,7 @@ const diagnostics = @import("diagnostics");
|
|
|
9
9
|
const ios_tool = @import("ios.zig");
|
|
10
10
|
const manifest_tool = @import("manifest.zig");
|
|
11
11
|
const web_engine_tool = @import("web_engine.zig");
|
|
12
|
+
const windows_installer = @import("windows_installer.zig");
|
|
12
13
|
const xcodeproj_tool = @import("xcodeproj.zig");
|
|
13
14
|
|
|
14
15
|
/// The SDK's default app icon (kept in sync by `zig build generate-icon`):
|
|
@@ -86,9 +87,14 @@ pub const PackageOptions = struct {
|
|
|
86
87
|
/// then staple and validate its ticket. Requires identity signing and a
|
|
87
88
|
/// notarytool Keychain profile in `signing.profile`.
|
|
88
89
|
notarize: bool = false,
|
|
89
|
-
/// Emit the ZIP consumed by the native updater. macOS
|
|
90
|
-
///
|
|
90
|
+
/// Emit the ZIP consumed by the native updater. On macOS it contains
|
|
91
|
+
/// exactly the packaged .app (unlike the user-facing DMG); on Windows
|
|
92
|
+
/// it is the install image a setup program carries
|
|
93
|
+
/// (windows_installer.zig).
|
|
91
94
|
update_archive: bool = false,
|
|
95
|
+
/// Emit a per-user setup program beside a Windows artifact
|
|
96
|
+
/// (windows_installer.zig). Windows only.
|
|
97
|
+
installer: bool = false,
|
|
92
98
|
/// The process environment, when the caller has one (the CLI). The
|
|
93
99
|
/// Android artifact probes it for the SDK/NDK toolchain to assemble
|
|
94
100
|
/// the debug APK; without it the generated project is still complete
|
|
@@ -161,6 +167,7 @@ pub const PackageStats = struct {
|
|
|
161
167
|
web_layer: ?manifest_tool.WebLayer = null,
|
|
162
168
|
archive_path: ?[]const u8 = null,
|
|
163
169
|
update_archive_path: ?[]const u8 = null,
|
|
170
|
+
installer_path: ?[]const u8 = null,
|
|
164
171
|
/// The Windows subsystem verdict: `console` when the package
|
|
165
172
|
/// wrapped a CONSOLE-subsystem exe (the app will flash a terminal
|
|
166
173
|
/// window behind itself on every launch), `gui` for a GUI-subsystem
|
|
@@ -278,13 +285,51 @@ pub fn createPackage(allocator: std.mem.Allocator, io: std.Io, options: PackageO
|
|
|
278
285
|
stats.notarized = true;
|
|
279
286
|
}
|
|
280
287
|
if (options.update_archive) {
|
|
281
|
-
if (options.target != .macos) return error.UnsupportedUpdateTarget;
|
|
282
288
|
if (!options.metadata.updates.enabled()) return error.UpdatesNotConfigured;
|
|
283
|
-
stats.update_archive_path =
|
|
289
|
+
stats.update_archive_path = switch (options.target) {
|
|
290
|
+
.macos => try createUpdateArchive(allocator, io, options),
|
|
291
|
+
.windows => try createWindowsImageFile(allocator, io, options, .update_archive),
|
|
292
|
+
else => return error.UnsupportedUpdateTarget,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if (options.installer) {
|
|
296
|
+
if (options.target != .windows) return error.UnsupportedInstallerTarget;
|
|
297
|
+
stats.installer_path = try createWindowsImageFile(allocator, io, options, .setup);
|
|
284
298
|
}
|
|
285
299
|
return stats;
|
|
286
300
|
}
|
|
287
301
|
|
|
302
|
+
/// The per-user setup program or the update archive, beside the artifact
|
|
303
|
+
/// directory, for the architecture of the packaged executable.
|
|
304
|
+
fn createWindowsImageFile(allocator: std.mem.Allocator, io: std.Io, options: PackageOptions, kind: enum { setup, update_archive }) ![]const u8 {
|
|
305
|
+
const binary_path = options.binary_path orelse {
|
|
306
|
+
std.debug.print("error: --installer and --update-archive need the app executable; pass --binary\n", .{});
|
|
307
|
+
return error.MissingBinary;
|
|
308
|
+
};
|
|
309
|
+
const arch: windows_installer.Arch = if (try peExecutableIsArm64(io, binary_path)) .aarch64 else .x86_64;
|
|
310
|
+
const executable = try std.fmt.allocPrint(allocator, "{s}.exe", .{options.metadata.name});
|
|
311
|
+
defer allocator.free(executable);
|
|
312
|
+
const file_name = switch (kind) {
|
|
313
|
+
.setup => try windows_installer.setupFileName(allocator, options.metadata, arch),
|
|
314
|
+
.update_archive => try windows_installer.updateArchiveFileName(allocator, options.metadata, arch),
|
|
315
|
+
};
|
|
316
|
+
defer allocator.free(file_name);
|
|
317
|
+
const path = try std.fs.path.join(allocator, &.{ std.fs.path.dirname(options.output_path) orelse ".", file_name });
|
|
318
|
+
errdefer allocator.free(path);
|
|
319
|
+
const image_options: windows_installer.Options = .{
|
|
320
|
+
.artifact_dir = options.output_path,
|
|
321
|
+
.metadata = options.metadata,
|
|
322
|
+
.executable = executable,
|
|
323
|
+
.arch = arch,
|
|
324
|
+
.output_path = path,
|
|
325
|
+
};
|
|
326
|
+
switch (kind) {
|
|
327
|
+
.setup => try windows_installer.createSetup(allocator, io, image_options),
|
|
328
|
+
.update_archive => try windows_installer.createUpdateArchive(allocator, io, image_options),
|
|
329
|
+
}
|
|
330
|
+
return path;
|
|
331
|
+
}
|
|
332
|
+
|
|
288
333
|
fn metadataHasCapability(metadata: manifest_tool.Metadata, name: []const u8) bool {
|
|
289
334
|
for (metadata.capabilities) |capability| {
|
|
290
335
|
if (std.mem.eql(u8, capability, name)) return true;
|
|
@@ -332,6 +377,9 @@ pub fn printDiagnostic(stats: PackageStats) void {
|
|
|
332
377
|
if (stats.update_archive_path) |archive| {
|
|
333
378
|
std.debug.print(" update archive: {s}\n", .{archive});
|
|
334
379
|
}
|
|
380
|
+
if (stats.installer_path) |installer| {
|
|
381
|
+
std.debug.print(" installer: {s}\n", .{installer});
|
|
382
|
+
}
|
|
335
383
|
}
|
|
336
384
|
|
|
337
385
|
fn createUpdateArchive(allocator: std.mem.Allocator, io: std.Io, options: PackageOptions) ![]const u8 {
|
|
@@ -500,7 +548,7 @@ fn createDesktopArtifact(allocator: std.mem.Allocator, io: std.Io, options: Pack
|
|
|
500
548
|
try makeExecutable(dir, io, service_subpath);
|
|
501
549
|
}
|
|
502
550
|
|
|
503
|
-
const assets_output = try
|
|
551
|
+
const assets_output = try desktopAssetOutputPath(allocator, options);
|
|
504
552
|
defer allocator.free(assets_output);
|
|
505
553
|
const bundle_stats = try assets_tool.bundle(allocator, io, options.assets_dir, assets_output);
|
|
506
554
|
try writeFile(dir, io, "README.txt", artifactReadme(options.target));
|
|
@@ -787,6 +835,20 @@ fn macosAssetOutputPath(allocator: std.mem.Allocator, options: PackageOptions) !
|
|
|
787
835
|
return std.fs.path.join(allocator, &.{ options.output_path, "Contents/Resources" });
|
|
788
836
|
}
|
|
789
837
|
|
|
838
|
+
/// Where a Windows or Linux artifact carries the app's assets: the macOS
|
|
839
|
+
/// bundle's layout (macosAssetOutputPath) under resources/. The packaged
|
|
840
|
+
/// runner and Windows host resolve relative asset paths against the
|
|
841
|
+
/// resources/ directory beside the executable's bin/, so a relative path
|
|
842
|
+
/// the app reads ("assets/fonts/mono.ttf") names the same file it names
|
|
843
|
+
/// in a dev run.
|
|
844
|
+
fn desktopAssetOutputPath(allocator: std.mem.Allocator, options: PackageOptions) ![]const u8 {
|
|
845
|
+
if (options.frontend != null) return assetOutputPath(allocator, options.output_path, "resources", options);
|
|
846
|
+
if (appRelativeAssetSubpath(options.assets_dir)) |subpath| {
|
|
847
|
+
return std.fs.path.join(allocator, &.{ options.output_path, "resources", subpath });
|
|
848
|
+
}
|
|
849
|
+
return std.fs.path.join(allocator, &.{ options.output_path, "resources" });
|
|
850
|
+
}
|
|
851
|
+
|
|
790
852
|
/// The asset directory as an app-relative bundle subpath, or null when
|
|
791
853
|
/// it cannot honestly be one (empty, ".", absolute, or escaping the app
|
|
792
854
|
/// root through a ".." segment).
|
|
@@ -1465,7 +1527,7 @@ fn elfStringAt(strtab: []const u8, offset: u64) ?[]const u8 {
|
|
|
1465
1527
|
/// Whether a PE executable targets arm64, read from the COFF machine
|
|
1466
1528
|
/// field. Anything unrecognized falls back to x64, the default Windows
|
|
1467
1529
|
/// build target.
|
|
1468
|
-
fn peExecutableIsArm64(io: std.Io, path: []const u8) !bool {
|
|
1530
|
+
pub fn peExecutableIsArm64(io: std.Io, path: []const u8) !bool {
|
|
1469
1531
|
var file = try std.Io.Dir.cwd().openFile(io, path, .{});
|
|
1470
1532
|
defer file.close(io);
|
|
1471
1533
|
var read_buffer: [4096]u8 = undefined;
|
|
@@ -1798,7 +1860,7 @@ fn appendFmt(allocator: std.mem.Allocator, out: *std.ArrayList(u8), comptime for
|
|
|
1798
1860
|
try out.appendSlice(allocator, text);
|
|
1799
1861
|
}
|
|
1800
1862
|
|
|
1801
|
-
fn trimExtensionDot(extension: []const u8) []const u8 {
|
|
1863
|
+
pub fn trimExtensionDot(extension: []const u8) []const u8 {
|
|
1802
1864
|
if (extension.len > 0 and extension[0] == '.') return extension[1..];
|
|
1803
1865
|
return extension;
|
|
1804
1866
|
}
|
|
@@ -2109,7 +2171,7 @@ fn windowsRegistrationScript(allocator: std.mem.Allocator, metadata: manifest_to
|
|
|
2109
2171
|
return out.toOwnedSlice(allocator);
|
|
2110
2172
|
}
|
|
2111
2173
|
|
|
2112
|
-
fn windowsProgId(allocator: std.mem.Allocator, metadata: manifest_tool.Metadata, association: manifest_tool.FileAssociationMetadata) ![]const u8 {
|
|
2174
|
+
pub fn windowsProgId(allocator: std.mem.Allocator, metadata: manifest_tool.Metadata, association: manifest_tool.FileAssociationMetadata) ![]const u8 {
|
|
2113
2175
|
const app = try windowsIdentifierComponentAlloc(allocator, metadata.id);
|
|
2114
2176
|
defer allocator.free(app);
|
|
2115
2177
|
const name = try windowsIdentifierComponentAlloc(allocator, association.name);
|
|
@@ -3432,6 +3494,34 @@ test "macOS package mirrors app assets at their app-relative path" {
|
|
|
3432
3494
|
try std.testing.expectEqualStrings("mp3-bytes", bundled);
|
|
3433
3495
|
}
|
|
3434
3496
|
|
|
3497
|
+
test "windows and linux packages mirror app assets at their app-relative path" {
|
|
3498
|
+
// The packaged runner resolves "assets/music/track.mp3" against the
|
|
3499
|
+
// resources/ directory beside bin/, so the artifact keeps the asset
|
|
3500
|
+
// tree at the relative path a dev run reads.
|
|
3501
|
+
var cwd = std.Io.Dir.cwd();
|
|
3502
|
+
const root = ".zig-cache/test-package-desktop-asset-layout";
|
|
3503
|
+
try cwd.deleteTree(std.testing.io, root);
|
|
3504
|
+
defer cwd.deleteTree(std.testing.io, root) catch {};
|
|
3505
|
+
try cwd.createDirPath(std.testing.io, root ++ "/assets/music");
|
|
3506
|
+
try cwd.writeFile(std.testing.io, .{ .sub_path = root ++ "/assets/music/track.mp3", .data = "mp3-bytes" });
|
|
3507
|
+
|
|
3508
|
+
const metadata: manifest_tool.Metadata = .{ .id = "dev.example.app", .name = "demo", .version = "1.2.3" };
|
|
3509
|
+
inline for (.{ PackageTarget.windows, PackageTarget.linux }) |target| {
|
|
3510
|
+
const output = root ++ "/demo-" ++ @tagName(target);
|
|
3511
|
+
const stats = try createPackage(std.testing.allocator, std.testing.io, .{
|
|
3512
|
+
.metadata = metadata,
|
|
3513
|
+
.target = target,
|
|
3514
|
+
.output_path = output,
|
|
3515
|
+
.assets_dir = root ++ "/assets",
|
|
3516
|
+
});
|
|
3517
|
+
try std.testing.expectEqual(@as(usize, 1), stats.asset_count);
|
|
3518
|
+
const packaged = try readPath(std.testing.allocator, std.testing.io, output ++ "/resources/" ++ root ++ "/assets/music/track.mp3");
|
|
3519
|
+
defer std.testing.allocator.free(packaged);
|
|
3520
|
+
try std.testing.expectEqualStrings("mp3-bytes", packaged);
|
|
3521
|
+
try std.testing.expectError(error.FileNotFound, readPath(std.testing.allocator, std.testing.io, output ++ "/resources/music/track.mp3"));
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3435
3525
|
test "app-relative asset subpaths accept plain trees and reject escapes" {
|
|
3436
3526
|
try std.testing.expectEqualStrings("assets", appRelativeAssetSubpath("assets").?);
|
|
3437
3527
|
try std.testing.expectEqualStrings("data/sounds", appRelativeAssetSubpath("data/sounds").?);
|
|
@@ -19,6 +19,11 @@ pub const RawManifest = struct {
|
|
|
19
19
|
/// startup window is created; the runtime dock-presence command may
|
|
20
20
|
/// still promote or demote the app later.
|
|
21
21
|
dock_visible: bool = true,
|
|
22
|
+
/// Whether one process per bundle id, user, and login session owns
|
|
23
|
+
/// the app. A later launch hands its working directory and arguments
|
|
24
|
+
/// to the owner over the `native-sdk.app.launches` channel source and
|
|
25
|
+
/// exits before creating windows (Windows today).
|
|
26
|
+
single_instance: bool = false,
|
|
22
27
|
persist: ?RawPersist = null,
|
|
23
28
|
images: RawImages = .{},
|
|
24
29
|
service_packages: []const RawServicePackage = &.{},
|
|
@@ -55,7 +60,9 @@ pub const RawManifest = struct {
|
|
|
55
60
|
pub const RawUpdates = struct {
|
|
56
61
|
feed_url: ?[]const u8 = null,
|
|
57
62
|
public_key: ?[]const u8 = null,
|
|
63
|
+
public_keys: []const []const u8 = &.{},
|
|
58
64
|
check_on_start: bool = false,
|
|
65
|
+
mode: ?[]const u8 = null,
|
|
59
66
|
};
|
|
60
67
|
|
|
61
68
|
pub const RawImages = struct {
|