@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
|
@@ -13271,6 +13271,372 @@ int native_sdk_appkit_check_for_updates(native_sdk_appkit_host_t *host, int user
|
|
|
13271
13271
|
return 1;
|
|
13272
13272
|
}
|
|
13273
13273
|
|
|
13274
|
+
// ---------------------------------------------------------------------------
|
|
13275
|
+
// App-driven updates (`updates.mode = "app"`): the platform half of
|
|
13276
|
+
// src/updater/engine.zig, reached through src/updater/macos.zig. Unlike the
|
|
13277
|
+
// host flow above, nothing here shows UI; the engine calls these from its
|
|
13278
|
+
// worker threads and from the app loop. The release stages beside the app,
|
|
13279
|
+
// `<parent>/.<App.app>.native-update/`, so installing it is a rename on the
|
|
13280
|
+
// same volume, and a helper installs it once this process exits.
|
|
13281
|
+
|
|
13282
|
+
enum {
|
|
13283
|
+
NativeSdkUpdateOk = 0,
|
|
13284
|
+
NativeSdkUpdateNetwork = 1,
|
|
13285
|
+
NativeSdkUpdateHttpStatus = 2,
|
|
13286
|
+
NativeSdkUpdateTooLarge = 3,
|
|
13287
|
+
NativeSdkUpdateCancelled = 4,
|
|
13288
|
+
NativeSdkUpdateDisk = 5,
|
|
13289
|
+
};
|
|
13290
|
+
|
|
13291
|
+
static NSString *NativeSdkUpdateString(const char *bytes, size_t length) {
|
|
13292
|
+
if (!bytes || length == 0) return @"";
|
|
13293
|
+
return [[NSString alloc] initWithBytes:bytes length:length encoding:NSUTF8StringEncoding] ?: @"";
|
|
13294
|
+
}
|
|
13295
|
+
|
|
13296
|
+
static size_t NativeSdkUpdateCopy(NSString *value, char *output, size_t capacity) {
|
|
13297
|
+
const char *bytes = value.UTF8String;
|
|
13298
|
+
const size_t length = bytes ? strlen(bytes) : 0;
|
|
13299
|
+
if (length == 0 || length > capacity) return 0;
|
|
13300
|
+
memcpy(output, bytes, length);
|
|
13301
|
+
return length;
|
|
13302
|
+
}
|
|
13303
|
+
|
|
13304
|
+
static NSString *NativeSdkUpdateBundlePath(void) {
|
|
13305
|
+
return NSBundle.mainBundle.bundlePath.stringByStandardizingPath;
|
|
13306
|
+
}
|
|
13307
|
+
|
|
13308
|
+
static NSString *NativeSdkUpdateSiblingPath(NSString *suffix) {
|
|
13309
|
+
NSString *bundlePath = NativeSdkUpdateBundlePath();
|
|
13310
|
+
return [bundlePath.stringByDeletingLastPathComponent stringByAppendingPathComponent:[NSString stringWithFormat:@".%@.%@", bundlePath.lastPathComponent, suffix]];
|
|
13311
|
+
}
|
|
13312
|
+
|
|
13313
|
+
static NSString *NativeSdkUpdateResultPath(NSString *bundleIdentifier) {
|
|
13314
|
+
NSURL *support = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask].firstObject;
|
|
13315
|
+
if (!support || bundleIdentifier.length == 0) return nil;
|
|
13316
|
+
return [[support.path stringByAppendingPathComponent:bundleIdentifier] stringByAppendingPathComponent:@".native-update-result"];
|
|
13317
|
+
}
|
|
13318
|
+
|
|
13319
|
+
static BOOL NativeSdkUpdateURLAllowed(NSURL *url, BOOL allowLoopbackHttp) {
|
|
13320
|
+
NSString *scheme = url.scheme.lowercaseString;
|
|
13321
|
+
if ([scheme isEqualToString:@"https"]) return url.host.length > 0;
|
|
13322
|
+
if (!allowLoopbackHttp || ![scheme isEqualToString:@"http"]) return NO;
|
|
13323
|
+
NSString *host = url.host.lowercaseString;
|
|
13324
|
+
return [host isEqualToString:@"127.0.0.1"] || [host isEqualToString:@"::1"] || [host isEqualToString:@"localhost"];
|
|
13325
|
+
}
|
|
13326
|
+
|
|
13327
|
+
/// The one app bundle a staged release unpacked to, when it is this app at
|
|
13328
|
+
/// `version`; Info.plist is read directly because NSBundle caches bundles
|
|
13329
|
+
/// by path and the staging path is reused.
|
|
13330
|
+
static NSString *NativeSdkUpdateStagedBundle(NSString *stagePath, NSString *bundleIdentifier, NSString *version) {
|
|
13331
|
+
NSFileManager *manager = [NSFileManager defaultManager];
|
|
13332
|
+
NSArray<NSString *> *entries = [manager contentsOfDirectoryAtPath:stagePath error:nil];
|
|
13333
|
+
NSString *bundleName = nil;
|
|
13334
|
+
for (NSString *entry in entries ?: @[]) {
|
|
13335
|
+
if ([entry isEqualToString:@".native-staged"]) continue;
|
|
13336
|
+
if (bundleName || ![[entry pathExtension].lowercaseString isEqualToString:@"app"]) return nil;
|
|
13337
|
+
bundleName = entry;
|
|
13338
|
+
}
|
|
13339
|
+
if (!bundleName) return nil;
|
|
13340
|
+
NSString *candidate = [stagePath stringByAppendingPathComponent:bundleName];
|
|
13341
|
+
NSNumber *isDirectory = nil;
|
|
13342
|
+
NSNumber *isSymlink = nil;
|
|
13343
|
+
[[NSURL fileURLWithPath:candidate] getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil];
|
|
13344
|
+
[[NSURL fileURLWithPath:candidate] getResourceValue:&isSymlink forKey:NSURLIsSymbolicLinkKey error:nil];
|
|
13345
|
+
if (!isDirectory.boolValue || isSymlink.boolValue) return nil;
|
|
13346
|
+
NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:[candidate stringByAppendingPathComponent:@"Contents/Info.plist"]];
|
|
13347
|
+
id identifier = info[@"CFBundleIdentifier"];
|
|
13348
|
+
id bundleVersion = info[@"CFBundleVersion"];
|
|
13349
|
+
if (![identifier isKindOfClass:[NSString class]] || ![identifier isEqualToString:bundleIdentifier]) return nil;
|
|
13350
|
+
if (![bundleVersion isKindOfClass:[NSString class]] || ![bundleVersion isEqualToString:version]) return nil;
|
|
13351
|
+
return candidate;
|
|
13352
|
+
}
|
|
13353
|
+
|
|
13354
|
+
/// A Developer ID app accepts only a release signed by the same team, as
|
|
13355
|
+
/// the host flow does; unsigned and ad-hoc installs rely on the feed's
|
|
13356
|
+
/// Ed25519 signature alone.
|
|
13357
|
+
static BOOL NativeSdkUpdateSignatureMatches(NSString *candidate, NSString *bundleIdentifier) {
|
|
13358
|
+
NSString *teamIdentifier = NativeSdkSigningTeamIdentifier(NativeSdkUpdateBundlePath());
|
|
13359
|
+
if (teamIdentifier.length == 0) return YES;
|
|
13360
|
+
if ([teamIdentifier rangeOfCharacterFromSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]].location != NSNotFound ||
|
|
13361
|
+
[bundleIdentifier rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"\\\""]].location != NSNotFound) return NO;
|
|
13362
|
+
NSString *requirement = [NSString stringWithFormat:@"anchor apple generic and certificate leaf[subject.OU] = \"%@\" and identifier \"%@\"", teamIdentifier, bundleIdentifier];
|
|
13363
|
+
return NativeSdkRunTask(@"/usr/bin/codesign", @[ @"--verify", @"--deep", @"--strict", [@"-R=" stringByAppendingString:requirement], candidate ], NULL);
|
|
13364
|
+
}
|
|
13365
|
+
|
|
13366
|
+
typedef void (*NativeSdkUpdateProgressFn)(void *context, uint64_t received);
|
|
13367
|
+
typedef int (*NativeSdkUpdateCancelledFn)(void *context);
|
|
13368
|
+
|
|
13369
|
+
@interface NativeSdkUpdateTransfer : NSObject <NSURLSessionDataDelegate>
|
|
13370
|
+
@property(nonatomic) BOOL allowLoopbackHttp;
|
|
13371
|
+
@property(nonatomic) uint64_t limit;
|
|
13372
|
+
@property(nonatomic) uint64_t received;
|
|
13373
|
+
@property(atomic) int result;
|
|
13374
|
+
@property(nonatomic, strong) NSMutableData *data;
|
|
13375
|
+
@property(nonatomic, strong) NSFileHandle *file;
|
|
13376
|
+
@property(nonatomic) void *context;
|
|
13377
|
+
@property(nonatomic) NativeSdkUpdateProgressFn progress;
|
|
13378
|
+
@property(nonatomic, strong) dispatch_semaphore_t done;
|
|
13379
|
+
@end
|
|
13380
|
+
|
|
13381
|
+
@implementation NativeSdkUpdateTransfer
|
|
13382
|
+
|
|
13383
|
+
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler {
|
|
13384
|
+
(void)session;
|
|
13385
|
+
(void)response;
|
|
13386
|
+
if (NativeSdkUpdateURLAllowed(request.URL, self.allowLoopbackHttp)) {
|
|
13387
|
+
completionHandler(request);
|
|
13388
|
+
return;
|
|
13389
|
+
}
|
|
13390
|
+
self.result = NativeSdkUpdateNetwork;
|
|
13391
|
+
completionHandler(nil);
|
|
13392
|
+
[task cancel];
|
|
13393
|
+
}
|
|
13394
|
+
|
|
13395
|
+
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
|
|
13396
|
+
(void)session;
|
|
13397
|
+
(void)dataTask;
|
|
13398
|
+
NSHTTPURLResponse *http = [response isKindOfClass:[NSHTTPURLResponse class]] ? (NSHTTPURLResponse *)response : nil;
|
|
13399
|
+
if (!http || !NativeSdkUpdateURLAllowed(response.URL, self.allowLoopbackHttp)) {
|
|
13400
|
+
self.result = NativeSdkUpdateNetwork;
|
|
13401
|
+
} else if (http.statusCode < 200 || http.statusCode >= 300) {
|
|
13402
|
+
self.result = NativeSdkUpdateHttpStatus;
|
|
13403
|
+
} else if (response.expectedContentLength > 0 && (uint64_t)response.expectedContentLength > self.limit) {
|
|
13404
|
+
self.result = NativeSdkUpdateTooLarge;
|
|
13405
|
+
}
|
|
13406
|
+
completionHandler(self.result == NativeSdkUpdateOk ? NSURLSessionResponseAllow : NSURLSessionResponseCancel);
|
|
13407
|
+
}
|
|
13408
|
+
|
|
13409
|
+
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
|
|
13410
|
+
(void)session;
|
|
13411
|
+
if (self.result != NativeSdkUpdateOk) return;
|
|
13412
|
+
if (data.length > self.limit - self.received) {
|
|
13413
|
+
self.result = NativeSdkUpdateTooLarge;
|
|
13414
|
+
[dataTask cancel];
|
|
13415
|
+
return;
|
|
13416
|
+
}
|
|
13417
|
+
if (self.file) {
|
|
13418
|
+
NSError *error = nil;
|
|
13419
|
+
if (![self.file writeData:data error:&error]) {
|
|
13420
|
+
self.result = NativeSdkUpdateDisk;
|
|
13421
|
+
[dataTask cancel];
|
|
13422
|
+
return;
|
|
13423
|
+
}
|
|
13424
|
+
} else {
|
|
13425
|
+
[self.data appendData:data];
|
|
13426
|
+
}
|
|
13427
|
+
self.received += data.length;
|
|
13428
|
+
if (self.progress) self.progress(self.context, self.received);
|
|
13429
|
+
}
|
|
13430
|
+
|
|
13431
|
+
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
|
|
13432
|
+
(void)session;
|
|
13433
|
+
(void)task;
|
|
13434
|
+
if (error && self.result == NativeSdkUpdateOk) self.result = NativeSdkUpdateNetwork;
|
|
13435
|
+
dispatch_semaphore_signal(self.done);
|
|
13436
|
+
}
|
|
13437
|
+
|
|
13438
|
+
@end
|
|
13439
|
+
|
|
13440
|
+
/// GETs `url` through the user's network settings, waiting on the calling
|
|
13441
|
+
/// (worker) thread and honoring `cancelled` while the transfer stalls.
|
|
13442
|
+
static int NativeSdkUpdateTransferRun(NSURL *url, NativeSdkUpdateTransfer *transfer, void *context, NativeSdkUpdateCancelledFn cancelled) {
|
|
13443
|
+
NSURLSessionConfiguration *configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration;
|
|
13444
|
+
configuration.timeoutIntervalForRequest = 30;
|
|
13445
|
+
configuration.timeoutIntervalForResource = 60 * 60;
|
|
13446
|
+
configuration.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
|
13447
|
+
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
|
|
13448
|
+
queue.maxConcurrentOperationCount = 1;
|
|
13449
|
+
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:transfer delegateQueue:queue];
|
|
13450
|
+
transfer.done = dispatch_semaphore_create(0);
|
|
13451
|
+
NSURLSessionDataTask *task = [session dataTaskWithURL:url];
|
|
13452
|
+
[task resume];
|
|
13453
|
+
while (dispatch_semaphore_wait(transfer.done, dispatch_time(DISPATCH_TIME_NOW, 200 * NSEC_PER_MSEC)) != 0) {
|
|
13454
|
+
if (cancelled && cancelled(context) && transfer.result == NativeSdkUpdateOk) {
|
|
13455
|
+
transfer.result = NativeSdkUpdateCancelled;
|
|
13456
|
+
[task cancel];
|
|
13457
|
+
}
|
|
13458
|
+
}
|
|
13459
|
+
[session finishTasksAndInvalidate];
|
|
13460
|
+
return transfer.result;
|
|
13461
|
+
}
|
|
13462
|
+
|
|
13463
|
+
int native_sdk_macos_updates_eligibility(void) {
|
|
13464
|
+
@autoreleasepool {
|
|
13465
|
+
NSString *bundlePath = NativeSdkUpdateBundlePath();
|
|
13466
|
+
if (![bundlePath.pathExtension.lowercaseString isEqualToString:@"app"]) return 1;
|
|
13467
|
+
// A quarantined app Gatekeeper runs from a randomized read-only
|
|
13468
|
+
// mount cannot replace itself until the user moves it.
|
|
13469
|
+
if ([bundlePath rangeOfString:@"/AppTranslocation/"].location != NSNotFound) return 2;
|
|
13470
|
+
if (![[NSFileManager defaultManager] isWritableFileAtPath:bundlePath.stringByDeletingLastPathComponent]) return 2;
|
|
13471
|
+
return 0;
|
|
13472
|
+
}
|
|
13473
|
+
}
|
|
13474
|
+
|
|
13475
|
+
size_t native_sdk_macos_updates_staged_version(const char *bundle_id, size_t bundle_id_len, char *output, size_t capacity) {
|
|
13476
|
+
@autoreleasepool {
|
|
13477
|
+
NSString *stagePath = NativeSdkUpdateSiblingPath(@"native-update");
|
|
13478
|
+
NSString *version = [NSString stringWithContentsOfFile:[stagePath stringByAppendingPathComponent:@".native-staged"] encoding:NSUTF8StringEncoding error:nil];
|
|
13479
|
+
if (version.length == 0 || !NativeSdkUpdateStagedBundle(stagePath, NativeSdkUpdateString(bundle_id, bundle_id_len), version)) return 0;
|
|
13480
|
+
return NativeSdkUpdateCopy(version, output, capacity);
|
|
13481
|
+
}
|
|
13482
|
+
}
|
|
13483
|
+
|
|
13484
|
+
size_t native_sdk_macos_updates_take_last(const char *bundle_id, size_t bundle_id_len, char *output, size_t capacity) {
|
|
13485
|
+
@autoreleasepool {
|
|
13486
|
+
NSString *resultPath = NativeSdkUpdateResultPath(NativeSdkUpdateString(bundle_id, bundle_id_len));
|
|
13487
|
+
if (!resultPath) return 0;
|
|
13488
|
+
NSString *record = [NSString stringWithContentsOfFile:resultPath encoding:NSUTF8StringEncoding error:nil];
|
|
13489
|
+
if (!record) return 0;
|
|
13490
|
+
[[NSFileManager defaultManager] removeItemAtPath:resultPath error:nil];
|
|
13491
|
+
NSString *line = [record stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
|
13492
|
+
if (![line hasPrefix:@"updated "]) return 0;
|
|
13493
|
+
return NativeSdkUpdateCopy([line substringFromIndex:8], output, capacity);
|
|
13494
|
+
}
|
|
13495
|
+
}
|
|
13496
|
+
|
|
13497
|
+
int native_sdk_macos_updates_fetch(const char *url, size_t url_len, int allow_loopback_http, uint8_t *output, size_t capacity, size_t *output_len) {
|
|
13498
|
+
@autoreleasepool {
|
|
13499
|
+
NSURL *target = [NSURL URLWithString:NativeSdkUpdateString(url, url_len)];
|
|
13500
|
+
if (!target || !NativeSdkUpdateURLAllowed(target, allow_loopback_http != 0)) return NativeSdkUpdateNetwork;
|
|
13501
|
+
NativeSdkUpdateTransfer *transfer = [[NativeSdkUpdateTransfer alloc] init];
|
|
13502
|
+
transfer.allowLoopbackHttp = allow_loopback_http != 0;
|
|
13503
|
+
transfer.limit = capacity;
|
|
13504
|
+
transfer.data = [NSMutableData data];
|
|
13505
|
+
const int result = NativeSdkUpdateTransferRun(target, transfer, NULL, NULL);
|
|
13506
|
+
if (result != NativeSdkUpdateOk) return result;
|
|
13507
|
+
memcpy(output, transfer.data.bytes, transfer.data.length);
|
|
13508
|
+
*output_len = transfer.data.length;
|
|
13509
|
+
return NativeSdkUpdateOk;
|
|
13510
|
+
}
|
|
13511
|
+
}
|
|
13512
|
+
|
|
13513
|
+
int native_sdk_macos_updates_download(const char *url, size_t url_len, int allow_loopback_http, uint64_t limit, char *path_output, size_t path_capacity, size_t *path_len, void *context, NativeSdkUpdateProgressFn progress, NativeSdkUpdateCancelledFn cancelled) {
|
|
13514
|
+
@autoreleasepool {
|
|
13515
|
+
NSURL *target = [NSURL URLWithString:NativeSdkUpdateString(url, url_len)];
|
|
13516
|
+
if (!target || !NativeSdkUpdateURLAllowed(target, allow_loopback_http != 0)) return NativeSdkUpdateNetwork;
|
|
13517
|
+
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"native-update-%@.zip", NSUUID.UUID.UUIDString]];
|
|
13518
|
+
NSFileManager *manager = [NSFileManager defaultManager];
|
|
13519
|
+
if (![manager createFileAtPath:path contents:nil attributes:@{ NSFilePosixPermissions: @0600 }]) return NativeSdkUpdateDisk;
|
|
13520
|
+
NSFileHandle *file = [NSFileHandle fileHandleForWritingAtPath:path];
|
|
13521
|
+
if (!file) {
|
|
13522
|
+
[manager removeItemAtPath:path error:nil];
|
|
13523
|
+
return NativeSdkUpdateDisk;
|
|
13524
|
+
}
|
|
13525
|
+
NativeSdkUpdateTransfer *transfer = [[NativeSdkUpdateTransfer alloc] init];
|
|
13526
|
+
transfer.allowLoopbackHttp = allow_loopback_http != 0;
|
|
13527
|
+
transfer.limit = limit;
|
|
13528
|
+
transfer.file = file;
|
|
13529
|
+
transfer.context = context;
|
|
13530
|
+
transfer.progress = progress;
|
|
13531
|
+
int result = NativeSdkUpdateTransferRun(target, transfer, context, cancelled);
|
|
13532
|
+
if (![file closeAndReturnError:nil] && result == NativeSdkUpdateOk) result = NativeSdkUpdateDisk;
|
|
13533
|
+
const size_t length = NativeSdkUpdateCopy(path, path_output, path_capacity);
|
|
13534
|
+
if (result == NativeSdkUpdateOk && length == 0) result = NativeSdkUpdateDisk;
|
|
13535
|
+
if (result != NativeSdkUpdateOk) {
|
|
13536
|
+
[manager removeItemAtPath:path error:nil];
|
|
13537
|
+
return result;
|
|
13538
|
+
}
|
|
13539
|
+
*path_len = length;
|
|
13540
|
+
return NativeSdkUpdateOk;
|
|
13541
|
+
}
|
|
13542
|
+
}
|
|
13543
|
+
|
|
13544
|
+
int native_sdk_macos_updates_stage(const char *archive_path, size_t archive_path_len, const char *bundle_id, size_t bundle_id_len, const char *version, size_t version_len) {
|
|
13545
|
+
@autoreleasepool {
|
|
13546
|
+
NSFileManager *manager = [NSFileManager defaultManager];
|
|
13547
|
+
NSString *bundleIdentifier = NativeSdkUpdateString(bundle_id, bundle_id_len);
|
|
13548
|
+
NSString *expectedVersion = NativeSdkUpdateString(version, version_len);
|
|
13549
|
+
NSString *stagePath = NativeSdkUpdateSiblingPath(@"native-update");
|
|
13550
|
+
[manager removeItemAtPath:stagePath error:nil];
|
|
13551
|
+
if (![manager createDirectoryAtPath:stagePath withIntermediateDirectories:NO attributes:nil error:nil]) return 2;
|
|
13552
|
+
NSString *candidate = nil;
|
|
13553
|
+
if (NativeSdkRunTask(@"/usr/bin/ditto", @[ @"-x", @"-k", NativeSdkUpdateString(archive_path, archive_path_len), stagePath ], NULL)) {
|
|
13554
|
+
candidate = NativeSdkUpdateStagedBundle(stagePath, bundleIdentifier, expectedVersion);
|
|
13555
|
+
}
|
|
13556
|
+
if (!candidate || !NativeSdkUpdateSignatureMatches(candidate, bundleIdentifier)) {
|
|
13557
|
+
[manager removeItemAtPath:stagePath error:nil];
|
|
13558
|
+
return 1;
|
|
13559
|
+
}
|
|
13560
|
+
if (![expectedVersion writeToFile:[stagePath stringByAppendingPathComponent:@".native-staged"] atomically:YES encoding:NSUTF8StringEncoding error:nil]) {
|
|
13561
|
+
[manager removeItemAtPath:stagePath error:nil];
|
|
13562
|
+
return 2;
|
|
13563
|
+
}
|
|
13564
|
+
return 0;
|
|
13565
|
+
}
|
|
13566
|
+
}
|
|
13567
|
+
|
|
13568
|
+
void native_sdk_macos_updates_discard(void) {
|
|
13569
|
+
@autoreleasepool {
|
|
13570
|
+
[[NSFileManager defaultManager] removeItemAtPath:NativeSdkUpdateSiblingPath(@"native-update") error:nil];
|
|
13571
|
+
}
|
|
13572
|
+
}
|
|
13573
|
+
|
|
13574
|
+
/// Starts the helper that waits for this process to exit, replaces the app
|
|
13575
|
+
/// with the staged release (the old one returns if that fails), records
|
|
13576
|
+
/// the switch for the new version to report, and with `relaunch` opens it.
|
|
13577
|
+
int native_sdk_macos_updates_install(const char *bundle_id, size_t bundle_id_len, const char *from_version, size_t from_version_len, int relaunch) {
|
|
13578
|
+
@autoreleasepool {
|
|
13579
|
+
NSFileManager *manager = [NSFileManager defaultManager];
|
|
13580
|
+
NSString *bundleIdentifier = NativeSdkUpdateString(bundle_id, bundle_id_len);
|
|
13581
|
+
NSString *stagePath = NativeSdkUpdateSiblingPath(@"native-update");
|
|
13582
|
+
NSString *version = [NSString stringWithContentsOfFile:[stagePath stringByAppendingPathComponent:@".native-staged"] encoding:NSUTF8StringEncoding error:nil];
|
|
13583
|
+
NSString *candidate = version.length > 0 ? NativeSdkUpdateStagedBundle(stagePath, bundleIdentifier, version) : nil;
|
|
13584
|
+
if (!candidate) return 0;
|
|
13585
|
+
NSString *scriptPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"native-update-install-%@.sh", NSUUID.UUID.UUIDString]];
|
|
13586
|
+
NSString *script = @"#!/bin/sh\n"
|
|
13587
|
+
"set -u\n"
|
|
13588
|
+
"pid=\"$1\"; current=\"$2\"; candidate=\"$3\"; backup=\"$4\"; stage=\"$5\"; relaunch=\"$6\"; result=\"$7\"; from=\"$8\"; to=\"$9\"\n"
|
|
13589
|
+
"count=0\n"
|
|
13590
|
+
"while /bin/kill -0 \"$pid\" 2>/dev/null; do\n"
|
|
13591
|
+
" count=$((count + 1))\n"
|
|
13592
|
+
" if [ \"$count\" -ge 600 ]; then /bin/rm -f \"$0\"; exit 1; fi\n"
|
|
13593
|
+
" /bin/sleep 0.1\n"
|
|
13594
|
+
"done\n"
|
|
13595
|
+
"/bin/rm -rf \"$backup\"\n"
|
|
13596
|
+
"if /bin/mv \"$current\" \"$backup\"; then\n"
|
|
13597
|
+
" if /bin/mv \"$candidate\" \"$current\"; then\n"
|
|
13598
|
+
" if [ -n \"$result\" ]; then /bin/mkdir -p \"${result%/*}\" && /usr/bin/printf 'updated %s %s\\n' \"$from\" \"$to\" > \"$result\"; fi\n"
|
|
13599
|
+
" if [ \"$relaunch\" != 1 ] || /usr/bin/open \"$current\"; then\n"
|
|
13600
|
+
" /bin/rm -rf \"$backup\" \"$stage\"\n"
|
|
13601
|
+
" /bin/rm -f \"$0\"\n"
|
|
13602
|
+
" exit 0\n"
|
|
13603
|
+
" fi\n"
|
|
13604
|
+
" if [ -n \"$result\" ]; then /bin/rm -f \"$result\"; fi\n"
|
|
13605
|
+
" /bin/rm -rf \"$current\"\n"
|
|
13606
|
+
" fi\n"
|
|
13607
|
+
" /bin/mv \"$backup\" \"$current\"\n"
|
|
13608
|
+
"fi\n"
|
|
13609
|
+
"/bin/rm -rf \"$stage\"\n"
|
|
13610
|
+
"if [ \"$relaunch\" = 1 ]; then /usr/bin/open \"$current\" >/dev/null 2>&1 || true; fi\n"
|
|
13611
|
+
"/bin/rm -f \"$0\"\n"
|
|
13612
|
+
"exit 1\n";
|
|
13613
|
+
if (![script writeToFile:scriptPath atomically:YES encoding:NSUTF8StringEncoding error:nil] ||
|
|
13614
|
+
![manager setAttributes:@{ NSFilePosixPermissions: @0700 } ofItemAtPath:scriptPath error:nil]) {
|
|
13615
|
+
[manager removeItemAtPath:scriptPath error:nil];
|
|
13616
|
+
return 0;
|
|
13617
|
+
}
|
|
13618
|
+
NSTask *installer = [[NSTask alloc] init];
|
|
13619
|
+
installer.executableURL = [NSURL fileURLWithPath:@"/bin/sh"];
|
|
13620
|
+
installer.arguments = @[
|
|
13621
|
+
scriptPath,
|
|
13622
|
+
[NSString stringWithFormat:@"%d", getpid()],
|
|
13623
|
+
NativeSdkUpdateBundlePath(),
|
|
13624
|
+
candidate,
|
|
13625
|
+
NativeSdkUpdateSiblingPath(@"native-update-backup"),
|
|
13626
|
+
stagePath,
|
|
13627
|
+
relaunch ? @"1" : @"0",
|
|
13628
|
+
NativeSdkUpdateResultPath(bundleIdentifier) ?: @"",
|
|
13629
|
+
NativeSdkUpdateString(from_version, from_version_len),
|
|
13630
|
+
version,
|
|
13631
|
+
];
|
|
13632
|
+
if (![installer launchAndReturnError:nil]) {
|
|
13633
|
+
[manager removeItemAtPath:scriptPath error:nil];
|
|
13634
|
+
return 0;
|
|
13635
|
+
}
|
|
13636
|
+
return 1;
|
|
13637
|
+
}
|
|
13638
|
+
}
|
|
13639
|
+
|
|
13274
13640
|
void native_sdk_appkit_run(native_sdk_appkit_host_t *host, native_sdk_appkit_event_callback_t callback, void *context) {
|
|
13275
13641
|
NativeSdkAppKitHost *object = (__bridge NativeSdkAppKitHost *)host;
|
|
13276
13642
|
[object runWithCallback:callback context:context];
|
|
@@ -9,6 +9,7 @@ const canvas = @import("canvas");
|
|
|
9
9
|
// macOS mask/inset render so the Dock tile matches `native package`.
|
|
10
10
|
const app_icon = canvas.app_icon;
|
|
11
11
|
const updater_c_api = @import("../../updater/c_api.zig");
|
|
12
|
+
const update_feed = @import("../../updater/feed.zig");
|
|
12
13
|
|
|
13
14
|
comptime {
|
|
14
15
|
_ = updater_c_api.native_sdk_update_verify_feed;
|
|
@@ -685,12 +686,10 @@ pub const MacPlatform = struct {
|
|
|
685
686
|
const dock_icon = planDockIcon(app_info.icon_path);
|
|
686
687
|
const icon_path = if (dock_icon == .host_file) app_info.icon_path else "";
|
|
687
688
|
const host = native_sdk_appkit_create(app_info.app_name.ptr, app_info.app_name.len, display_name.ptr, display_name.len, app_info.version.ptr, app_info.version.len, app_info.description.ptr, app_info.description.len, if (app_info.has_web_content) 1 else 0, if (app_info.dock_visible) 1 else 0, window_title.ptr, window_title.len, app_info.bundle_id.ptr, app_info.bundle_id.len, icon_path.ptr, icon_path.len, window_options.label.ptr, window_options.label.len, frame.x, frame.y, frame.width, frame.height, if (window_options.restore_state) 1 else 0, initialPlacementInt(window_options.initial_placement), restorePolicyInt(window_options.restore_policy), if (window_options.resizable) 1 else 0, titlebarStyleInt(window_options.titlebar), showModeInt(window_options.show), windowFlags(window_options)) orelse return error.CreateFailed;
|
|
688
|
-
const update_target = comptime
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
};
|
|
693
|
-
native_sdk_appkit_configure_updates(host, app_info.update_feed_url.ptr, app_info.update_feed_url.len, app_info.update_public_key.ptr, app_info.update_public_key.len, if (app_info.update_check_on_start) 1 else 0, update_target.ptr, update_target.len);
|
|
689
|
+
const update_target = comptime update_feed.currentTarget() orelse "macos-unsupported";
|
|
690
|
+
var update_feed_url_buffer: [update_feed.max_feed_url_bytes]u8 = undefined;
|
|
691
|
+
const update_feed_url = expandedUpdateFeedUrl(&update_feed_url_buffer, app_info.update_feed_url, update_target, app_info.version);
|
|
692
|
+
native_sdk_appkit_configure_updates(host, update_feed_url.ptr, update_feed_url.len, app_info.update_public_key.ptr, app_info.update_public_key.len, if (app_info.update_check_on_start) 1 else 0, update_target.ptr, update_target.len);
|
|
694
693
|
switch (dock_icon) {
|
|
695
694
|
.host_file => {},
|
|
696
695
|
.masked_render => spawnDevDockIconRender(host, app_info.icon_path),
|
|
@@ -2390,6 +2389,18 @@ fn checkForUpdates(context: ?*anyopaque, user_initiated: bool) anyerror!void {
|
|
|
2390
2389
|
if (native_sdk_appkit_check_for_updates(self.host, if (user_initiated) 1 else 0) == 0) return error.UnsupportedService;
|
|
2391
2390
|
}
|
|
2392
2391
|
|
|
2392
|
+
/// The feed URL the host fetches: the manifest's, with `{target}` and
|
|
2393
|
+
/// `{current_version}` expanded for this install. A URL this build cannot
|
|
2394
|
+
/// expand leaves updates unconfigured rather than fetching some other
|
|
2395
|
+
/// target's feed.
|
|
2396
|
+
fn expandedUpdateFeedUrl(buffer: []u8, template: []const u8, target: []const u8, version: []const u8) []const u8 {
|
|
2397
|
+
if (template.len == 0) return "";
|
|
2398
|
+
return update_feed.expandFeedUrl(buffer, template, target, version) catch |err| {
|
|
2399
|
+
std.log.warn("updates disabled: feed_url cannot be expanded for {s} {s} ({s})", .{ target, version, @errorName(err) });
|
|
2400
|
+
return "";
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2393
2404
|
fn configureSecurityPolicy(context: ?*anyopaque, policy: security.Policy) anyerror!void {
|
|
2394
2405
|
const self: *MacPlatform = @ptrCast(@alignCast(context.?));
|
|
2395
2406
|
var origins_buffer: [4096]u8 = undefined;
|
|
@@ -3103,6 +3114,18 @@ test "mac updater hashes archives off the main thread and treats user cancellati
|
|
|
3103
3114
|
try std.testing.expect(std.mem.indexOf(u8, download_body, "strongSelf.updateCheckRunning = NO;") != null);
|
|
3104
3115
|
}
|
|
3105
3116
|
|
|
3117
|
+
test "mac updater fetches the feed URL expanded for this install" {
|
|
3118
|
+
var buffer: [update_feed.max_feed_url_bytes]u8 = undefined;
|
|
3119
|
+
try std.testing.expectEqualStrings(
|
|
3120
|
+
"https://example.com/releases/macos-aarch64/native-update.json?from=1.2.3",
|
|
3121
|
+
expandedUpdateFeedUrl(&buffer, "https://example.com/releases/{target}/native-update.json?from={current_version}", "macos-aarch64", "1.2.3"),
|
|
3122
|
+
);
|
|
3123
|
+
try std.testing.expectEqualStrings("", expandedUpdateFeedUrl(&buffer, "", "macos-aarch64", "1.2.3"));
|
|
3124
|
+
// A build with no release format fetches nothing rather than another
|
|
3125
|
+
// target's feed.
|
|
3126
|
+
try std.testing.expectEqualStrings("", expandedUpdateFeedUrl(&buffer, "https://example.com/{target}.json", "macos-unsupported", "1.2.3"));
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3106
3129
|
test "mac updater binds packaged releases to the installed bundle identity" {
|
|
3107
3130
|
const host_source = @embedFile("appkit_host.m");
|
|
3108
3131
|
try std.testing.expect(std.mem.indexOf(u8, host_source, "static NSString *NativeSdkPackagedBundleIdentifier(void)") != null);
|
|
@@ -448,6 +448,11 @@ pub const NullPlatform = struct {
|
|
|
448
448
|
/// real hosts' close delegates, hiding instead of closing under
|
|
449
449
|
/// `.hide`.
|
|
450
450
|
window_close_policy: [max_windows]types.WindowClosePolicy = [_]types.WindowClosePolicy{.quit} ** max_windows,
|
|
451
|
+
/// Close requests (`request_close_window_fn`) not yet handled, oldest
|
|
452
|
+
/// first. The Windows host posts WM_CLOSE, so a requested close
|
|
453
|
+
/// resolves on a later loop turn; `takeQueuedCloseRequest` models it.
|
|
454
|
+
queued_close_requests: [max_windows]WindowId = [_]WindowId{0} ** max_windows,
|
|
455
|
+
queued_close_request_count: usize = 0,
|
|
451
456
|
/// Test seam: refuse one OS create without retaining a window identity.
|
|
452
457
|
fail_next_create_window: bool = false,
|
|
453
458
|
/// Test seam: make the NEXT `close_window_fn` call fail with
|
|
@@ -918,6 +923,9 @@ pub const NullPlatform = struct {
|
|
|
918
923
|
.focus_window_fn = focusWindow,
|
|
919
924
|
.close_window_fn = closeWindow,
|
|
920
925
|
.minimize_window_fn = minimizeWindow,
|
|
926
|
+
.maximize_window_fn = maximizeWindow,
|
|
927
|
+
.unmaximize_window_fn = unmaximizeWindow,
|
|
928
|
+
.request_close_window_fn = requestCloseWindow,
|
|
921
929
|
.hide_window_fn = hideWindow,
|
|
922
930
|
.show_window_fn = showWindow,
|
|
923
931
|
.set_dock_presence_fn = setDockPresence,
|
|
@@ -1319,6 +1327,36 @@ pub const NullPlatform = struct {
|
|
|
1319
1327
|
self.window_occluded[index] = true;
|
|
1320
1328
|
}
|
|
1321
1329
|
|
|
1330
|
+
/// The modeled maximize: the refusals the Windows host makes, then the
|
|
1331
|
+
/// state flip its WM_SIZE handler reports. `windowFrameEvent` hands a
|
|
1332
|
+
/// test the frame event that report carries.
|
|
1333
|
+
fn maximizeWindow(context: ?*anyopaque, window_id: WindowId) anyerror!void {
|
|
1334
|
+
const self: *NullPlatform = @ptrCast(@alignCast(context.?));
|
|
1335
|
+
const index = self.findWindowIndex(window_id) orelse return error.WindowNotFound;
|
|
1336
|
+
if (!self.windows[index].open) return error.WindowNotFound;
|
|
1337
|
+
if (!self.window_resizable[index]) return error.WindowNotResizable;
|
|
1338
|
+
if (self.windows[index].hidden or !self.window_visible[index]) return error.WindowHidden;
|
|
1339
|
+
self.windows[index].maximized = true;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
fn unmaximizeWindow(context: ?*anyopaque, window_id: WindowId) anyerror!void {
|
|
1343
|
+
const self: *NullPlatform = @ptrCast(@alignCast(context.?));
|
|
1344
|
+
const index = self.findWindowIndex(window_id) orelse return error.WindowNotFound;
|
|
1345
|
+
if (!self.windows[index].open) return error.WindowNotFound;
|
|
1346
|
+
if (!self.windows[index].maximized) return;
|
|
1347
|
+
if (self.windows[index].hidden or !self.window_visible[index]) return error.WindowHidden;
|
|
1348
|
+
self.windows[index].maximized = false;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
fn requestCloseWindow(context: ?*anyopaque, window_id: WindowId) anyerror!void {
|
|
1352
|
+
const self: *NullPlatform = @ptrCast(@alignCast(context.?));
|
|
1353
|
+
const index = self.findWindowIndex(window_id) orelse return error.WindowNotFound;
|
|
1354
|
+
if (!self.windows[index].open) return error.WindowNotFound;
|
|
1355
|
+
if (self.queued_close_request_count >= self.queued_close_requests.len) return error.WindowLimitReached;
|
|
1356
|
+
self.queued_close_requests[self.queued_close_request_count] = window_id;
|
|
1357
|
+
self.queued_close_request_count += 1;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1322
1360
|
fn showWindow(context: ?*anyopaque, window_id: WindowId) anyerror!void {
|
|
1323
1361
|
const self: *NullPlatform = @ptrCast(@alignCast(context.?));
|
|
1324
1362
|
if (self.fail_next_show_window) {
|
|
@@ -2928,6 +2966,7 @@ pub const NullPlatform = struct {
|
|
|
2928
2966
|
.open = true,
|
|
2929
2967
|
.focused = false,
|
|
2930
2968
|
.hidden = true,
|
|
2969
|
+
.maximized = info.maximized,
|
|
2931
2970
|
} };
|
|
2932
2971
|
}
|
|
2933
2972
|
const info = self.windows[index];
|
|
@@ -2945,6 +2984,33 @@ pub const NullPlatform = struct {
|
|
|
2945
2984
|
} };
|
|
2946
2985
|
}
|
|
2947
2986
|
|
|
2987
|
+
/// Test seam: drain the oldest queued close request, the modeled loop
|
|
2988
|
+
/// turn on which the host handles its posted WM_CLOSE. It resolves
|
|
2989
|
+
/// exactly like `userCloseWindow` (close_policy decides); dispatch the
|
|
2990
|
+
/// returned event through the runtime. Null while nothing is queued,
|
|
2991
|
+
/// or when the window closed before its turn (a WM_CLOSE posted to a
|
|
2992
|
+
/// destroyed window is never delivered).
|
|
2993
|
+
pub fn takeQueuedCloseRequest(self: *NullPlatform) ?Event {
|
|
2994
|
+
if (self.queued_close_request_count == 0) return null;
|
|
2995
|
+
const window_id = self.queued_close_requests[0];
|
|
2996
|
+
var cursor: usize = 0;
|
|
2997
|
+
while (cursor + 1 < self.queued_close_request_count) : (cursor += 1) {
|
|
2998
|
+
self.queued_close_requests[cursor] = self.queued_close_requests[cursor + 1];
|
|
2999
|
+
}
|
|
3000
|
+
self.queued_close_request_count -= 1;
|
|
3001
|
+
const index = self.findWindowIndex(window_id) orelse return null;
|
|
3002
|
+
if (!self.windows[index].open) return null;
|
|
3003
|
+
return self.userCloseWindow(window_id);
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
/// Test seam: the frame event for a window's current modeled state,
|
|
3007
|
+
/// `maximized` included: what the Windows host emits when a maximize
|
|
3008
|
+
/// or restore lands. Null for an unknown window.
|
|
3009
|
+
pub fn windowFrameEvent(self: *const NullPlatform, window_id: WindowId) ?Event {
|
|
3010
|
+
const index = self.findWindowIndex(window_id) orelse return null;
|
|
3011
|
+
return .{ .window_frame_changed = self.windows[index].state() };
|
|
3012
|
+
}
|
|
3013
|
+
|
|
2948
3014
|
/// Test seam: the user asked the app to come back with no window on
|
|
2949
3015
|
/// the glass (the macOS Dock-icon reopen). Models the host's
|
|
2950
3016
|
/// `applicationShouldHandleReopen:` — every window hidden by its
|
package/src/platform/root.zig
CHANGED
|
@@ -119,6 +119,7 @@ pub const ViewPatch = types.ViewPatch;
|
|
|
119
119
|
pub const Cursor = types.Cursor;
|
|
120
120
|
pub const ViewInfo = types.ViewInfo;
|
|
121
121
|
pub const AppInfo = types.AppInfo;
|
|
122
|
+
pub const UpdateMode = types.UpdateMode;
|
|
122
123
|
pub const Surface = types.Surface;
|
|
123
124
|
pub const BridgeMessage = types.BridgeMessage;
|
|
124
125
|
pub const ViewFocusEvent = types.ViewFocusEvent;
|
package/src/platform/types.zig
CHANGED
|
@@ -718,6 +718,10 @@ pub const WindowInfo = struct {
|
|
|
718
718
|
focused: bool = false,
|
|
719
719
|
/// Alive but policy-hidden — see `WindowState.hidden`.
|
|
720
720
|
hidden: bool = false,
|
|
721
|
+
/// Maximized as last reported by the host's frame event, whichever
|
|
722
|
+
/// path changed it (app call, caption double-click, system menu,
|
|
723
|
+
/// keyboard shortcut). Hosts that do not report it leave it false.
|
|
724
|
+
maximized: bool = false,
|
|
721
725
|
|
|
722
726
|
pub fn state(self: WindowInfo) WindowState {
|
|
723
727
|
return .{
|
|
@@ -729,6 +733,7 @@ pub const WindowInfo = struct {
|
|
|
729
733
|
.open = self.open,
|
|
730
734
|
.focused = self.focused,
|
|
731
735
|
.hidden = self.hidden,
|
|
736
|
+
.maximized = self.maximized,
|
|
732
737
|
};
|
|
733
738
|
}
|
|
734
739
|
};
|
|
@@ -1284,6 +1289,9 @@ pub const ViewInfo = struct {
|
|
|
1284
1289
|
}
|
|
1285
1290
|
};
|
|
1286
1291
|
|
|
1292
|
+
/// Who drives updates on macOS (the manifest's `updates.mode`).
|
|
1293
|
+
pub const UpdateMode = enum { host, app };
|
|
1294
|
+
|
|
1287
1295
|
pub const AppInfo = struct {
|
|
1288
1296
|
app_name: []const u8 = "native-sdk",
|
|
1289
1297
|
/// The human-facing application name (app.zon `display_name`) the
|
|
@@ -1318,9 +1326,15 @@ pub const AppInfo = struct {
|
|
|
1318
1326
|
window_title: []const u8 = "",
|
|
1319
1327
|
bundle_id: []const u8 = "dev.native_sdk.app",
|
|
1320
1328
|
icon_path: []const u8 = "",
|
|
1329
|
+
/// The manifest's feed URL, before the host expands `{target}` and
|
|
1330
|
+
/// `{current_version}` for this install.
|
|
1321
1331
|
update_feed_url: []const u8 = "",
|
|
1332
|
+
/// The trusted update keys: base64 Ed25519 public keys, comma-joined.
|
|
1322
1333
|
update_public_key: []const u8 = "",
|
|
1323
1334
|
update_check_on_start: bool = false,
|
|
1335
|
+
/// `.app`: the app drives updates through `native-sdk.updates` and the
|
|
1336
|
+
/// macOS host shows no update UI of its own.
|
|
1337
|
+
update_mode: UpdateMode = .host,
|
|
1324
1338
|
main_window: WindowOptions = .{},
|
|
1325
1339
|
windows: []const WindowOptions = &.{},
|
|
1326
1340
|
|
|
@@ -2708,6 +2722,23 @@ pub const PlatformServices = struct {
|
|
|
2708
2722
|
/// controls — chromeless windows have no system button to click.
|
|
2709
2723
|
/// Platforms without the concept leave this null.
|
|
2710
2724
|
minimize_window_fn: ?*const fn (context: ?*anyopaque, window_id: WindowId) anyerror!void = null,
|
|
2725
|
+
/// The OS maximize and restore-from-maximized verbs for app-drawn
|
|
2726
|
+
/// window controls (Windows `SW_MAXIMIZE`/`SW_RESTORE`). Hosts refuse
|
|
2727
|
+
/// a non-resizable window (`error.WindowNotResizable`, the system has
|
|
2728
|
+
/// no maximize affordance for it) and a window that is not on the
|
|
2729
|
+
/// glass (`error.WindowHidden`). Unmaximizing a window that is not
|
|
2730
|
+
/// maximized succeeds without effect. The resulting state arrives on
|
|
2731
|
+
/// the window's frame event (`WindowState.maximized`). Platforms
|
|
2732
|
+
/// without the verbs leave these null.
|
|
2733
|
+
maximize_window_fn: ?*const fn (context: ?*anyopaque, window_id: WindowId) anyerror!void = null,
|
|
2734
|
+
unmaximize_window_fn: ?*const fn (context: ?*anyopaque, window_id: WindowId) anyerror!void = null,
|
|
2735
|
+
/// Ask the window to close the way the user's close button does
|
|
2736
|
+
/// (Windows posts `WM_CLOSE`), so `close_policy` decides between
|
|
2737
|
+
/// hiding and closing. Unlike `close_window_fn` this only queues the
|
|
2738
|
+
/// request: the outcome arrives later as the host's ordinary frame
|
|
2739
|
+
/// events, exactly like a user close. Platforms without the verb
|
|
2740
|
+
/// leave this null.
|
|
2741
|
+
request_close_window_fn: ?*const fn (context: ?*anyopaque, window_id: WindowId) anyerror!void = null,
|
|
2711
2742
|
/// Keep a live window and all of its views, but order it off the
|
|
2712
2743
|
/// glass. The runtime marks `WindowInfo.hidden` while it is out;
|
|
2713
2744
|
/// `show_window_fn` is the inverse. Platforms without a reliable
|
|
@@ -3167,6 +3198,21 @@ pub const PlatformServices = struct {
|
|
|
3167
3198
|
return minimize_fn(self.context, window_id);
|
|
3168
3199
|
}
|
|
3169
3200
|
|
|
3201
|
+
pub fn maximizeWindow(self: PlatformServices, window_id: WindowId) anyerror!void {
|
|
3202
|
+
const maximize_fn = self.maximize_window_fn orelse return error.UnsupportedService;
|
|
3203
|
+
return maximize_fn(self.context, window_id);
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
pub fn unmaximizeWindow(self: PlatformServices, window_id: WindowId) anyerror!void {
|
|
3207
|
+
const unmaximize_fn = self.unmaximize_window_fn orelse return error.UnsupportedService;
|
|
3208
|
+
return unmaximize_fn(self.context, window_id);
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
pub fn requestCloseWindow(self: PlatformServices, window_id: WindowId) anyerror!void {
|
|
3212
|
+
const request_fn = self.request_close_window_fn orelse return error.UnsupportedService;
|
|
3213
|
+
return request_fn(self.context, window_id);
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3170
3216
|
pub fn hideWindow(self: PlatformServices, window_id: WindowId) anyerror!void {
|
|
3171
3217
|
const hide_fn = self.hide_window_fn orelse return error.UnsupportedService;
|
|
3172
3218
|
return hide_fn(self.context, window_id);
|