@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.
- package/README.md +1 -1
- package/build/app.zig +3 -0
- package/build/cli.zig +22 -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/root.zig +1 -0
- package/src/platform/types.zig +9 -0
- package/src/platform/windows/root.zig +37 -0
- package/src/platform/windows/webview2_host.cpp +371 -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/channel_sources.zig +49 -1
- 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/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 +44 -2
- 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);
|
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
|
@@ -1289,6 +1289,9 @@ pub const ViewInfo = struct {
|
|
|
1289
1289
|
}
|
|
1290
1290
|
};
|
|
1291
1291
|
|
|
1292
|
+
/// Who drives updates on macOS (the manifest's `updates.mode`).
|
|
1293
|
+
pub const UpdateMode = enum { host, app };
|
|
1294
|
+
|
|
1292
1295
|
pub const AppInfo = struct {
|
|
1293
1296
|
app_name: []const u8 = "native-sdk",
|
|
1294
1297
|
/// The human-facing application name (app.zon `display_name`) the
|
|
@@ -1323,9 +1326,15 @@ pub const AppInfo = struct {
|
|
|
1323
1326
|
window_title: []const u8 = "",
|
|
1324
1327
|
bundle_id: []const u8 = "dev.native_sdk.app",
|
|
1325
1328
|
icon_path: []const u8 = "",
|
|
1329
|
+
/// The manifest's feed URL, before the host expands `{target}` and
|
|
1330
|
+
/// `{current_version}` for this install.
|
|
1326
1331
|
update_feed_url: []const u8 = "",
|
|
1332
|
+
/// The trusted update keys: base64 Ed25519 public keys, comma-joined.
|
|
1327
1333
|
update_public_key: []const u8 = "",
|
|
1328
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,
|
|
1329
1338
|
main_window: WindowOptions = .{},
|
|
1330
1339
|
windows: []const WindowOptions = &.{},
|
|
1331
1340
|
|
|
@@ -2727,6 +2727,43 @@ test "windows webview focus reports the focused child label" {
|
|
|
2727
2727
|
try std.testing.expect(std.mem.indexOf(u8, host_source, "event.view_label = focused->second.label.c_str();") != null);
|
|
2728
2728
|
}
|
|
2729
2729
|
|
|
2730
|
+
test "windows webview2 keeps its user data with the app's per-user data" {
|
|
2731
|
+
// WebView2's default user data folder sits beside the executable,
|
|
2732
|
+
// which is read-only under Program Files and is swapped away with
|
|
2733
|
+
// the program files on update. Every environment the host creates
|
|
2734
|
+
// names the folder beside the app_dirs roots instead, and an earlier
|
|
2735
|
+
// "<exe>.WebView2" folder moves there once.
|
|
2736
|
+
const host_source = @embedFile("webview2_host.cpp");
|
|
2737
|
+
try std.testing.expect(std.mem.indexOf(u8, host_source, "factory(nullptr, webViewUserDataFolder(host), nullptr,") != null);
|
|
2738
|
+
try std.testing.expect(std.mem.indexOf(u8, host_source, "factory(nullptr, nullptr, nullptr,") == null);
|
|
2739
|
+
const resolve_at = std.mem.indexOf(u8, host_source, "static const wchar_t *webViewUserDataFolder(Host *host) {") orelse return error.TestExpectedEqual;
|
|
2740
|
+
const resolve = host_source[resolve_at..];
|
|
2741
|
+
try std.testing.expect(std.mem.indexOf(u8, resolve, "GetEnvironmentVariableW(L\"LOCALAPPDATA\"") != null);
|
|
2742
|
+
try std.testing.expect(std.mem.indexOf(u8, resolve, "const std::wstring folder = app_root + L\"\\\\WebView2\";") != null);
|
|
2743
|
+
try std.testing.expect(std.mem.indexOf(u8, resolve, "MoveFileExW(legacy.c_str(), folder.c_str(), 0)") != null);
|
|
2744
|
+
try std.testing.expect(std.mem.indexOf(u8, host_source, "return path + L\".WebView2\";") != null);
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
test "windows webview assets resolve against the packaged resources beside bin" {
|
|
2748
|
+
// `native package` stages bin\<exe> and the app-relative asset tree
|
|
2749
|
+
// in resources\; a relative asset root names that tree when the app
|
|
2750
|
+
// runs from a package and the working directory otherwise.
|
|
2751
|
+
const host_source = @embedFile("webview2_host.cpp");
|
|
2752
|
+
try std.testing.expect(std.mem.indexOf(u8, host_source, "std::string path = assetRootPath(webview.asset_root);") != null);
|
|
2753
|
+
const resources_at = std.mem.indexOf(u8, host_source, "static const std::wstring &packagedResourcesDirectory() {") orelse return error.TestExpectedEqual;
|
|
2754
|
+
const resources = host_source[resources_at..];
|
|
2755
|
+
try std.testing.expect(std.mem.indexOf(u8, resources, "CompareStringOrdinal(bin.c_str() + bin_slash + 1, -1, L\"bin\", -1, TRUE)") != null);
|
|
2756
|
+
try std.testing.expect(std.mem.indexOf(u8, resources, "return bin.substr(0, bin_slash) + L\"\\\\resources\";") != null);
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
test "windows names the process by its bundle id for the taskbar" {
|
|
2760
|
+
// The installer's Start menu shortcut carries the bundle id as its
|
|
2761
|
+
// AppUserModelID; the process must claim the same id.
|
|
2762
|
+
const host_source = @embedFile("webview2_host.cpp");
|
|
2763
|
+
const create_at = std.mem.indexOf(u8, host_source, "host->bundle_id = slice(bundle_id, bundle_id_len);") orelse return error.TestExpectedEqual;
|
|
2764
|
+
try std.testing.expect(std.mem.indexOf(u8, host_source[create_at..], "SetCurrentProcessExplicitAppUserModelID(app_user_model_id.c_str());") != null);
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2730
2767
|
test "windows audio event maps kinds and payload" {
|
|
2731
2768
|
var event = std.mem.zeroes(WindowsEvent);
|
|
2732
2769
|
event.audio_kind = 1;
|