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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/build/app.zig +3 -0
  3. package/build/cli.zig +22 -0
  4. package/package.json +10 -10
  5. package/packages/core/package-lock.json +2 -2
  6. package/packages/core/package.json +1 -1
  7. package/schemas/app.schema.json +9 -1
  8. package/skill-data/ts-core/SKILL.md +2 -0
  9. package/src/app_runner/app_assets.zig +85 -1
  10. package/src/app_runner/root.zig +63 -3
  11. package/src/app_runner/ts_core_main.zig +8 -0
  12. package/src/platform/macos/appkit_host.h +9 -0
  13. package/src/platform/macos/appkit_host.m +366 -0
  14. package/src/platform/macos/root.zig +29 -6
  15. package/src/platform/root.zig +1 -0
  16. package/src/platform/types.zig +9 -0
  17. package/src/platform/windows/root.zig +37 -0
  18. package/src/platform/windows/webview2_host.cpp +371 -26
  19. package/src/primitives/app_manifest/root.zig +1 -0
  20. package/src/primitives/app_manifest/tests.zig +35 -0
  21. package/src/primitives/app_manifest/types.zig +12 -0
  22. package/src/primitives/app_manifest/validation.zig +44 -6
  23. package/src/root.zig +1 -0
  24. package/src/runtime/channel_sources.zig +49 -1
  25. package/src/runtime/effects.zig +12 -1
  26. package/src/runtime/flow.zig +6 -0
  27. package/src/runtime/flow_reference.zig +6 -0
  28. package/src/runtime/root.zig +4 -0
  29. package/src/runtime/single_instance.zig +893 -0
  30. package/src/tooling/manifest.zig +111 -3
  31. package/src/tooling/package.zig +98 -8
  32. package/src/tooling/raw_manifest.zig +7 -0
  33. package/src/tooling/root.zig +2 -0
  34. package/src/tooling/templates.zig +44 -2
  35. package/src/tooling/update.zig +228 -10
  36. package/src/tooling/windows_installer.zig +245 -0
  37. package/src/tooling/zip_writer.zig +211 -0
  38. package/src/updater/engine.zig +936 -0
  39. package/src/updater/feed.zig +261 -12
  40. package/src/updater/macos.zig +187 -0
  41. package/src/updater/root.zig +41 -0
  42. package/src/updater/windows.zig +470 -0
  43. package/src/windows_install/config.zig +247 -0
  44. package/src/windows_install/main.zig +690 -0
  45. package/src/windows_install/payload.zig +146 -0
  46. package/src/windows_install/switch.zig +184 -0
  47. package/src/windows_install/tests.zig +11 -0
  48. package/src/windows_install/win32.zig +248 -0
  49. package/tools/native-sdk/main.zig +10 -9
@@ -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 switch (builtin.cpu.arch) {
689
- .aarch64 => "macos-aarch64",
690
- .x86_64 => "macos-x86_64",
691
- else => "macos-unsupported",
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);
@@ -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;
@@ -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;