From ab974121fdd70a93bb54b0e341b014923f98717a Mon Sep 17 00:00:00 2001 From: 4evy Date: Sat, 11 Jul 2026 10:00:00 +0300 Subject: [PATCH] itsycal: build from source --- ...001-Allow-running-from-the-Nix-store.patch | 29 +++ ...-self-update-controls-for-Nix-builds.patch | 124 ++++++++++ ...modern-ServiceManagement-login-items.patch | 116 +++++++++ ...ilable-on-the-Nixpkgs-macOS-baseline.patch | 90 +++++++ pkgs/by-name/it/itsycal/package.nix | 224 +++++++++++++++++- pkgs/by-name/it/itsycal/update.sh | 20 -- 6 files changed, 570 insertions(+), 33 deletions(-) create mode 100644 pkgs/by-name/it/itsycal/0001-Allow-running-from-the-Nix-store.patch create mode 100644 pkgs/by-name/it/itsycal/0002-Remove-self-update-controls-for-Nix-builds.patch create mode 100644 pkgs/by-name/it/itsycal/0003-Use-modern-ServiceManagement-login-items.patch create mode 100644 pkgs/by-name/it/itsycal/0004-Use-APIs-available-on-the-Nixpkgs-macOS-baseline.patch delete mode 100755 pkgs/by-name/it/itsycal/update.sh diff --git a/pkgs/by-name/it/itsycal/0001-Allow-running-from-the-Nix-store.patch b/pkgs/by-name/it/itsycal/0001-Allow-running-from-the-Nix-store.patch new file mode 100644 index 000000000000..dc11f42f160d --- /dev/null +++ b/pkgs/by-name/it/itsycal/0001-Allow-running-from-the-Nix-store.patch @@ -0,0 +1,29 @@ +From 9ce5e368c00ed887f6d2628d4e8eff664bff5522 Mon Sep 17 00:00:00 2001 +From: 4evy +Date: Sat, 11 Jul 2026 01:12:36 +0300 +Subject: [PATCH 1/4] Allow running from the Nix store + +Upstream normally requires the bundle to live in /Applications because its +Sparkle updater needs a stable, writable installation path. Nix applications +instead live in the immutable Nix store and are updated by Nix. + +Use upstream's existing kAllowOutsideApplicationsFolder escape hatch as the +registered default. This preserves upstream's check and keeps the downstream +change to one explicit policy setting. +--- + Itsycal/AppDelegate.m | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/Itsycal/AppDelegate.m b/Itsycal/AppDelegate.m +index 5cc187a..ee256d9 100644 +--- a/Itsycal/AppDelegate.m ++++ b/Itsycal/AppDelegate.m +@@ -42,6 +42,8 @@ + kThemePreference: @0, // System=0, Light=1, Dark=2 + kHideIcon: @(NO), + kShowLocation: @(NO), ++ // Nix installs the app in the immutable Nix store, not /Applications. ++ kAllowOutsideApplicationsFolder: @(YES), + kEnableTahoeMenuIcons: @(NO), + kDoNotDrawOutlineAroundCurrentMonth: @(NO) + }]; diff --git a/pkgs/by-name/it/itsycal/0002-Remove-self-update-controls-for-Nix-builds.patch b/pkgs/by-name/it/itsycal/0002-Remove-self-update-controls-for-Nix-builds.patch new file mode 100644 index 000000000000..de244f092ff6 --- /dev/null +++ b/pkgs/by-name/it/itsycal/0002-Remove-self-update-controls-for-Nix-builds.patch @@ -0,0 +1,124 @@ +From f1474c9b296a3fbef30ca577dbd9a92224290b97 Mon Sep 17 00:00:00 2001 +From: 4evy +Date: Sat, 11 Jul 2026 01:12:47 +0300 +Subject: [PATCH 2/4] Remove self-update controls for Nix builds + +Sparkle cannot replace an application inside the immutable Nix store. Leaving +its menu item and preference checkbox backed by a no-op stub is misleading. + +Remove the updater object, imports, controls, action, and matching Tahoe menu +symbol so the Nix build has no Sparkle runtime dependency or non-functional +update UI. +--- + Itsycal/Base.lproj/MainMenu.xib | 1 - + Itsycal/PrefsGeneralVC.m | 11 ++--------- + Itsycal/ViewController.m | 9 --------- + 3 files changed, 2 insertions(+), 19 deletions(-) + +diff --git a/Itsycal/Base.lproj/MainMenu.xib b/Itsycal/Base.lproj/MainMenu.xib +index aa2067e..1a25048 100644 +--- a/Itsycal/Base.lproj/MainMenu.xib ++++ b/Itsycal/Base.lproj/MainMenu.xib +@@ -89,6 +89,5 @@ + + + +- + + +diff --git a/Itsycal/PrefsGeneralVC.m b/Itsycal/PrefsGeneralVC.m +index 7c19942..86f6be0 100644 +--- a/Itsycal/PrefsGeneralVC.m ++++ b/Itsycal/PrefsGeneralVC.m +@@ -9,7 +9,6 @@ + #import "MoVFLHelper.h" + #import "EventCenter.h" + #import "MASShortcut/Shortcut.h" +-#import "Sparkle/SUUpdater.h" + + static NSString * const kSourceCellId = @"SourceCell"; + static NSString * const kCalendarCellId = @"CalendarCell"; +@@ -33,7 +32,6 @@ static NSString * const kCalendarCellId = @"CalendarCell"; + { + NSTextField *_title; + NSButton *_login; +- NSButton *_checkUpdates; + NSButton *_beepBeep; + NSPopUpButton *_firstDayPopup; + NSTableView *_calendarsTV; +@@ -66,7 +64,6 @@ static NSString * const kCalendarCellId = @"CalendarCell"; + // Checkboxes + _login = chkbx(NSLocalizedString(@"Launch at login", @"")); + _login.action = @selector(launchAtLogin:); +- _checkUpdates = chkbx(NSLocalizedString(@"Automatically check for updates", @"")); + _beepBeep = chkbx(NSLocalizedString(@"Beep beep on the hour", @"")); + + // First day of week label +@@ -130,10 +127,9 @@ static NSString * const kCalendarCellId = @"CalendarCell"; + NSLocalizedString(@"31 days", @"")]]; + [v addSubview:_agendaDaysPopup]; + +- MoVFLHelper *vfl = [[MoVFLHelper alloc] initWithSuperview:v metrics:@{@"m": @20} views:NSDictionaryOfVariableBindings(_login, _checkUpdates, _beepBeep, firstDayLabel, _firstDayPopup, shortcutLabel, shortcutView, tvContainer, agendaDaysLabel, _agendaDaysPopup)]; +- [vfl :@"V:|-m-[_login]-[_checkUpdates]-[_beepBeep]-20-[_firstDayPopup]-20-[shortcutLabel]-3-[shortcutView(25)]-20-[tvContainer(170)]-[_agendaDaysPopup]-m-|"]; ++ MoVFLHelper *vfl = [[MoVFLHelper alloc] initWithSuperview:v metrics:@{@"m": @20} views:NSDictionaryOfVariableBindings(_login, _beepBeep, firstDayLabel, _firstDayPopup, shortcutLabel, shortcutView, tvContainer, agendaDaysLabel, _agendaDaysPopup)]; ++ [vfl :@"V:|-m-[_login]-[_beepBeep]-20-[_firstDayPopup]-20-[shortcutLabel]-3-[shortcutView(25)]-20-[tvContainer(170)]-[_agendaDaysPopup]-m-|"]; + [vfl :@"H:|-m-[_login]-(>=m)-|"]; +- [vfl :@"H:|-m-[_checkUpdates]-(>=m)-|"]; + [vfl :@"H:|-m-[_beepBeep]-(>=m)-|"]; + [vfl :@"H:|-m-[firstDayLabel]-[_firstDayPopup]-(>=m)-|" :NSLayoutFormatAlignAllFirstBaseline]; + [vfl :@"H:|-(>=m)-[shortcutLabel]-(>=m)-|"]; +@@ -144,9 +140,6 @@ static NSString * const kCalendarCellId = @"CalendarCell"; + // Center shortcutLabel + [v addConstraint:[NSLayoutConstraint constraintWithItem:shortcutLabel attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:v attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]]; + +- // Binding for Sparkle automatic update checks +- [_checkUpdates bind:@"value" toObject:[SUUpdater sharedUpdater] withKeyPath:@"automaticallyChecksForUpdates" options:@{NSContinuouslyUpdatesValueBindingOption: @(YES)}]; +- + // Binding for hourly beep + [_beepBeep bind:@"value" toObject:[NSUserDefaultsController sharedUserDefaultsController] withKeyPath:[@"values." stringByAppendingString:kBeepBeepOnTheHour] options:@{NSContinuouslyUpdatesValueBindingOption: @(YES)}]; + +diff --git a/Itsycal/ViewController.m b/Itsycal/ViewController.m +index 1d55c4f..0bd303b 100644 +--- a/Itsycal/ViewController.m ++++ b/Itsycal/ViewController.m +@@ -21,7 +21,6 @@ + #import "MoButton.h" + #import "MoVFLHelper.h" + #import "MoUtils.h" +-#import "Sparkle/SUUpdater.h" + + @implementation ViewController + { +@@ -378,7 +377,6 @@ + NSInteger i = 0; + + [optMenu insertItemWithTitle:NSLocalizedString(@"About Itsycal", @"") action:@selector(showAbout:) keyEquivalent:@"" atIndex:i++]; +- [optMenu insertItemWithTitle:NSLocalizedString(@"Check for Updates…", @"") action:@selector(checkForUpdates:) keyEquivalent:@"" atIndex:i++]; + [optMenu insertItem:[NSMenuItem separatorItem] atIndex:i++]; + [optMenu insertItemWithTitle:NSLocalizedString(@"Go to Date…", @"") action:@selector(showDatePickerPopover:) keyEquivalent:@"T" atIndex:i++]; + [optMenu insertItem:[NSMenuItem separatorItem] atIndex:i++]; +@@ -394,7 +392,6 @@ + NSInteger index = 0; + NSArray *symbolNames = @[ + @"info.circle", +- @"arrow.trianglehead.2.clockwise", + @"21.calendar", + @"gear", + @"calendar.badge.clock", +@@ -462,11 +459,6 @@ + [self.prefsWC showWindow:self]; + } + +-- (void)checkForUpdates:(id)sender +-{ +- [[SUUpdater sharedUpdater] checkForUpdates:self]; +-} +- + - (void)openDateAndTimePrefs:(id)sender + { + NSURL *url = nil; +@@ -1514,4 +1506,3 @@ + } + + @end +- diff --git a/pkgs/by-name/it/itsycal/0003-Use-modern-ServiceManagement-login-items.patch b/pkgs/by-name/it/itsycal/0003-Use-modern-ServiceManagement-login-items.patch new file mode 100644 index 000000000000..44a67ce1d791 --- /dev/null +++ b/pkgs/by-name/it/itsycal/0003-Use-modern-ServiceManagement-login-items.patch @@ -0,0 +1,116 @@ +From d3d2dd9ce8b566496503c38158199432a5ae87bb Mon Sep 17 00:00:00 2001 +From: 4evy +Date: Sat, 11 Jul 2026 01:12:47 +0300 +Subject: [PATCH 3/4] Use modern ServiceManagement login items + +LSSharedFileList was deprecated in macOS 10.11 and its status lookup can fail +when disconnected network volumes are present. Nixpkgs now targets macOS 14, +so no legacy fallback is needed. + +Use SMAppService.mainAppService, available since macOS 13. Treat +RequiresApproval as registered so users can still turn the pending login item +off. The package signs the complete app bundle after fixup, as required by +ServiceManagement. +--- + Itsycal/MoLoginItem.m | 58 +++++++--------------------------------- + Itsycal/PrefsGeneralVC.m | 12 +-------- + 2 files changed, 11 insertions(+), 59 deletions(-) + +diff --git a/Itsycal/MoLoginItem.m b/Itsycal/MoLoginItem.m +index 7efa602..0dc4960 100644 +--- a/Itsycal/MoLoginItem.m ++++ b/Itsycal/MoLoginItem.m +@@ -4,60 +4,22 @@ + // + + #import "MoLoginItem.h" +- +-// LSSharedFileList API was deprecated in macOS 10.11 +-#pragma clang diagnostic push +-#pragma clang diagnostic ignored "-Wdeprecated-declarations" ++#import + + BOOL MOIsLoginItemEnabled(void) + { +- BOOL isEnabled = NO; +- NSString *appPath = [[NSBundle mainBundle] bundlePath]; +- LSSharedFileListRef loginItemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL); +- +- if (loginItemsRef) { +- UInt32 seedValue; +- NSArray *loginItems = CFBridgingRelease(LSSharedFileListCopySnapshot(loginItemsRef, &seedValue)); +- for (id item in loginItems) { +- LSSharedFileListItemRef itemRef = (__bridge LSSharedFileListItemRef)item; +- NSURL *pathURL = CFBridgingRelease(LSSharedFileListItemCopyResolvedURL(itemRef, 0, NULL)); +- if (pathURL && [pathURL.path hasPrefix:appPath]) { +- isEnabled = YES; +- break; +- } +- } +- CFRelease(loginItemsRef); +- } +- return isEnabled; ++ SMAppServiceStatus status = SMAppService.mainAppService.status; ++ return status == SMAppServiceStatusEnabled || status == SMAppServiceStatusRequiresApproval; + } + + void MOEnableLoginItem(BOOL enable) + { +- NSString *appPath = [[NSBundle mainBundle] bundlePath]; +- LSSharedFileListRef loginItemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL); +- +- if (loginItemsRef) { +- if (enable) { +- // We call LSSharedFileListInsertItemURL to insert the item at the bottom of Login Items list. +- CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:appPath]; +- LSSharedFileListItemRef item = LSSharedFileListInsertItemURL(loginItemsRef, kLSSharedFileListItemLast, NULL, NULL, url, NULL, NULL); +- if (item != NULL) CFRelease(item); +- } +- else { +- // Grab the contents of the shared file list (LSSharedFileListItemRef objects) +- // and pop it in an array so we can iterate through it to find our item. +- UInt32 seedValue; +- NSArray *loginItems = CFBridgingRelease(LSSharedFileListCopySnapshot(loginItemsRef, &seedValue)); +- for (id item in loginItems) { +- LSSharedFileListItemRef itemRef = (__bridge LSSharedFileListItemRef)item; +- NSURL *pathURL = CFBridgingRelease(LSSharedFileListItemCopyResolvedURL(itemRef, 0, NULL)); +- if (pathURL && [pathURL.path hasPrefix:appPath]) { +- LSSharedFileListItemRemove(loginItemsRef, itemRef); // Deleting the item +- } +- } +- } +- CFRelease(loginItemsRef); ++ SMAppService *service = SMAppService.mainAppService; ++ NSError *error = nil; ++ BOOL succeeded = enable ++ ? [service registerAndReturnError:&error] ++ : [service unregisterAndReturnError:&error]; ++ if (!succeeded) { ++ NSLog(@"Unable to %@ launch at login: %@", enable ? @"enable" : @"disable", error); + } + } +- +-#pragma clang diagnostic pop +diff --git a/Itsycal/PrefsGeneralVC.m b/Itsycal/PrefsGeneralVC.m +index 86f6be0..ecfd30d 100644 +--- a/Itsycal/PrefsGeneralVC.m ++++ b/Itsycal/PrefsGeneralVC.m +@@ -158,17 +158,7 @@ static NSString * const kCalendarCellId = @"CalendarCell"; + + _sourcesAndCalendars = [self.ec sourcesAndCalendars]; + +- // The API used to check the login item's state (LSSharedFileList) causes +- // errors for users who have network drives but are not connected to their +- // network (github.com/sfsam/Itsycal/issues/15). Give them an option to +- // disable this check. +- if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DoNotCheckLoginItemStatus"] == NO) { +- _login.hidden = NO; +- _login.state = MOIsLoginItemEnabled() ? NSControlStateValueOn : NSControlStateValueOff; +- } +- else { +- _login.hidden = YES; +- } ++ _login.state = MOIsLoginItemEnabled() ? NSControlStateValueOn : NSControlStateValueOff; + + _calendarsTV.enabled = self.ec.calendarAccessGranted; + _agendaDaysPopup.enabled = self.ec.calendarAccessGranted; diff --git a/pkgs/by-name/it/itsycal/0004-Use-APIs-available-on-the-Nixpkgs-macOS-baseline.patch b/pkgs/by-name/it/itsycal/0004-Use-APIs-available-on-the-Nixpkgs-macOS-baseline.patch new file mode 100644 index 000000000000..21cd84f9f0e2 --- /dev/null +++ b/pkgs/by-name/it/itsycal/0004-Use-APIs-available-on-the-Nixpkgs-macOS-baseline.patch @@ -0,0 +1,90 @@ +From 6fb6ac13a8deee492449442e1ab3d7429aa9fff8 Mon Sep 17 00:00:00 2001 +From: 4evy +Date: Sat, 11 Jul 2026 01:12:55 +0300 +Subject: [PATCH 4/4] Use APIs available on the Nixpkgs macOS baseline + +The package metadata and Mach-O deployment target are both macOS 14. Remove +compatibility branches for older systems and use the corresponding current +APIs: + +* request EventKit full access directly; +* discover the calendar handler through NSWorkspace and + UniformTypeIdentifiers; +* use the renamed CoreAudio main-element constant. +--- + Itsycal/EventCenter.m | 20 ++------------------ + Itsycal/ViewController.m | 9 ++++----- + 2 files changed, 6 insertions(+), 23 deletions(-) + +diff --git a/Itsycal/EventCenter.m b/Itsycal/EventCenter.m +index 148c631..3862630 100644 +--- a/Itsycal/EventCenter.m ++++ b/Itsycal/EventCenter.m +@@ -59,15 +59,7 @@ static NSString *kSelectedCalendars = @"SelectedCalendars"; + _queueIsol = dispatch_queue_create("com.mowglii.Itsycal.queueIsol", DISPATCH_QUEUE_SERIAL); + _queueIsol2 = dispatch_queue_create("com.mowglii.Itsycal.queueIsol2", DISPATCH_QUEUE_SERIAL); + _store = [EKEventStore new]; +-#if MAC_OS_X_VERSION_MAX_ALLOWED >= 140000 +- if (@available(macOS 14.0, *)) { +- [_store requestFullAccessToEventsWithCompletion:requestCompletionHandler]; +- } else { +- [_store requestAccessToEntityType:EKEntityTypeEvent completion:requestCompletionHandler]; +- } +-#else +- [_store requestAccessToEntityType:EKEntityTypeEvent completion:requestCompletionHandler]; +-#endif ++ [_store requestFullAccessToEventsWithCompletion:requestCompletionHandler]; + + // Refetch everything when the event store has changed. + __weak __typeof(self) weakSelf = self; +@@ -85,15 +77,7 @@ static NSString *kSelectedCalendars = @"SelectedCalendars"; + #pragma mark - Public (main thread) + + - (BOOL)calendarAccessGranted { +-#if MAC_OS_X_VERSION_MAX_ALLOWED >= 140000 +- if (@available(macOS 14.0, *)) { +- return [EKEventStore authorizationStatusForEntityType:EKEntityTypeEvent] == EKAuthorizationStatusFullAccess; +- } else { +- return [EKEventStore authorizationStatusForEntityType:EKEntityTypeEvent] == EKAuthorizationStatusAuthorized; +- } +-#else +- return [EKEventStore authorizationStatusForEntityType:EKEntityTypeEvent] == EKAuthorizationStatusAuthorized; +-#endif ++ return [EKEventStore authorizationStatusForEntityType:EKEntityTypeEvent] == EKAuthorizationStatusFullAccess; + } + + - (NSString *)defaultCalendarIdentifier { +diff --git a/Itsycal/ViewController.m b/Itsycal/ViewController.m +index 0bd303b..a887682 100644 +--- a/Itsycal/ViewController.m ++++ b/Itsycal/ViewController.m +@@ -21,6 +21,7 @@ + #import "MoButton.h" + #import "MoVFLHelper.h" + #import "MoUtils.h" ++#import + + @implementation ViewController + { +@@ -319,10 +320,8 @@ + // Determine the default calendar app. + // See: support.busymac.com/help/21535-busycal-url-handler + +- CFStringRef strRef = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, CFSTR("ics"), kUTTypeData); +- CFStringRef bundleID = LSCopyDefaultRoleHandlerForContentType(strRef, kLSRolesEditor); +- CFRelease(strRef); +- NSString *defaultCalendarAppBundleID = CFBridgingRelease(bundleID); ++ NSURL *defaultCalendarAppURL = [NSWorkspace.sharedWorkspace URLForApplicationToOpenContentType:UTTypeCalendarEvent]; ++ NSString *defaultCalendarAppBundleID = [NSBundle bundleWithURL:defaultCalendarAppURL].bundleIdentifier; + + // Use URL scheme to open BusyCal or Fantastical2 on the + // date selected in our calendar. +@@ -1263,7 +1262,7 @@ + AudioObjectPropertyAddress propertyAddress; + propertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; + propertyAddress.mScope = kAudioObjectPropertyScopeGlobal; +- propertyAddress.mElement = kAudioObjectPropertyElementMaster; ++ propertyAddress.mElement = kAudioObjectPropertyElementMain; + + OSStatus result = AudioObjectGetPropertyData(kAudioObjectSystemObject, &propertyAddress, 0, NULL, &dataSize, &deviceID); + diff --git a/pkgs/by-name/it/itsycal/package.nix b/pkgs/by-name/it/itsycal/package.nix index 18542c37f049..78b7fcb4bd51 100644 --- a/pkgs/by-name/it/itsycal/package.nix +++ b/pkgs/by-name/it/itsycal/package.nix @@ -1,42 +1,240 @@ { lib, - fetchzip, - stdenvNoCC, + stdenv, + fetchFromGitHub, + actool, + ibtool, + lld, + makeWrapper, + rcodesign, + re-plistbuddy, + nix-update-script, }: -stdenvNoCC.mkDerivation (finalAttrs: { +let + # Upstream ships MASShortcut only as a prebuilt framework. Build the + # maintained Rectangle fork instead; it adds F20 and macOS Tahoe fixes while + # retaining the API and bundle identity used by Itsycal. + masShortcutSrc = fetchFromGitHub { + owner = "rxhanson"; + repo = "MASShortcut"; + rev = "2f9fbb3f959b7a683c6faaf9638d22afad37a235"; + hash = "sha256-EZLt7ph24L1wwFEMlltuPutId09RBug/y9OtDhixIig="; + }; + + masShortcutSources = [ + "Model/MASShortcut.m" + "Model/MASShortcutValidator.m" + "Monitoring/MASHotKey.m" + "Monitoring/MASShortcutMonitor.m" + "UI/MASLocalization.m" + "UI/MASShortcutView.m" + "UI/MASShortcutView+Bindings.m" + "UI/MASShortcutViewButtonCell.m" + "User Defaults Storage/MASDictionaryTransformer.m" + "User Defaults Storage/MASShortcutBinder.m" + ]; + + masShortcutInfoPlist = lib.generators.toPlist { escape = true; } { + CFBundleDevelopmentRegion = "English"; + CFBundleExecutable = "MASShortcut"; + CFBundleIdentifier = "com.github.shpakovski.MASShortcut"; + CFBundleInfoDictionaryVersion = "6.0"; + CFBundleName = "MASShortcut"; + CFBundlePackageType = "FMWK"; + CFBundleShortVersionString = "2.4.0"; + CFBundleVersion = "2.4.0"; + NSHumanReadableCopyright = "Copyright © Vadim Shpakovski. All rights reserved."; + }; +in +stdenv.mkDerivation (finalAttrs: { pname = "itsycal"; version = "0.15.12"; - src = fetchzip { - url = "https://itsycal.s3.amazonaws.com/Itsycal-${finalAttrs.version}.zip"; - hash = "sha256-2Xu1ZQnNl0o2/AYOIjxZPDnc0TxMXrqKej7CCZEVV9I="; - stripRoot = false; + src = fetchFromGitHub { + owner = "sfsam"; + repo = "Itsycal"; + tag = finalAttrs.version; + hash = "sha256-K25oG8d+OauNHLkatLLskfcWCqOmM2WEw2Ygd3v1uqE="; }; + patches = [ + # Use upstream's hidden escape hatch for installations outside /Applications. + ./0001-Allow-running-from-the-Nix-store.patch + # Nix owns updates, so remove the otherwise non-functional Sparkle UI. + ./0002-Remove-self-update-controls-for-Nix-builds.patch + # Replace the deprecated LSSharedFileList implementation with SMAppService. + ./0003-Use-modern-ServiceManagement-login-items.patch + # The Nixpkgs Darwin baseline is macOS 14; use its non-deprecated APIs. + ./0004-Use-APIs-available-on-the-Nixpkgs-macOS-baseline.patch + ]; + + strictDeps = true; + + nativeBuildInputs = [ + actool + ibtool + lld + makeWrapper + rcodesign + re-plistbuddy + ]; + + # The classic open-source ld64 crashes while linking MASShortcut on arm64. + # Keep lld until the cctools linker can link this framework reliably. + env.NIX_CFLAGS_LINK = "-fuse-ld=lld"; + + dontConfigure = true; + + postPatch = '' + # Both upstream frameworks contain native code. MASShortcut is rebuilt + # below and Sparkle is removed by patch 0002, so make accidental reuse fail. + rm -rf Itsycal/_frameworks + ''; + + buildPhase = '' + runHook preBuild + + buildDir="$PWD/build" + mkdir -p "$buildDir/module-cache" + + # actool accepts one catalog, while upstream keeps images and colors in + # separate catalogs. Merge them without changing their contents. + cp -R Itsycal/Images.xcassets "$buildDir/Assets.xcassets" + cp -R Itsycal/Colors.xcassets/*.colorset "$buildDir/Assets.xcassets/" + + # Compile .m -> .o; flags before --, sources after. Sets _objects array + compile_objc() { + local prefix="$1"; shift + local -a flags=() + while [[ "$1" != "--" ]]; do flags+=("$1"); shift; done; shift + _objects=() + for src in "$@"; do + local obj="$buildDir/''${prefix}_$(basename "$src" .m).o" + clang -fobjc-arc "''${flags[@]}" -c "$src" -o "$obj" + _objects+=("$obj") + done + } + + link_dylib() { + local name="$1"; shift + clang -dynamiclib -fobjc-arc \ + -install_name "@rpath/$name.framework/$name" "$@" + } + + # Build MASShortcut as a real framework. The explicit source list avoids + # pulling its demo or test targets into the application. + masHeaders="$buildDir/MAS_headers/MASShortcut" + mkdir -p "$masHeaders" + cp ${masShortcutSrc}/Framework/include/*.h "$masHeaders/" + + masSources=( ${ + lib.escapeShellArgs (map (src: "${masShortcutSrc}/Framework/${src}") masShortcutSources) + } ) + compile_objc mas -fmodules -fmodules-cache-path="$buildDir/module-cache" \ + -I"$masHeaders" -include AppKit/AppKit.h -- "''${masSources[@]}" + link_dylib MASShortcut -framework AppKit -framework Carbon \ + -compatibility_version 1 -current_version 2.4.0 \ + -o "$buildDir/MASShortcut.dylib" "''${_objects[@]}" + + # Itsycal + compile_objc app -fmodules -fmodules-cache-path="$buildDir/module-cache" \ + -I"$buildDir/MAS_headers" -IItsycal -- Itsycal/*.m + clang -fobjc-arc \ + -framework AppKit -framework EventKit -framework ScriptingBridge \ + -framework Carbon -framework ServiceManagement -framework UniformTypeIdentifiers \ + -Wl,-rpath,@executable_path/../Frameworks \ + "$buildDir/MASShortcut.dylib" \ + -o "$buildDir/Itsycal" "''${_objects[@]}" + + runHook postBuild + ''; + installPhase = '' runHook preInstall - mkdir -p "$out/Applications" - cp -R Itsycal.app "$out/Applications/" + appDir="$out/Applications/Itsycal.app/Contents" + mkdir -p "$appDir"/{MacOS,Frameworks,Resources} + + cp "$buildDir/Itsycal" "$appDir/MacOS/Itsycal" + + # Assemble the framework bundle metadata normally generated by Xcode. + mkdir -p "$appDir/Frameworks/MASShortcut.framework/Resources" + cp "$buildDir/MASShortcut.dylib" "$appDir/Frameworks/MASShortcut.framework/MASShortcut" + cp -r ${masShortcutSrc}/Framework/Resources/*.lproj "$appDir/Frameworks/MASShortcut.framework/Resources/" + printf '%s' ${lib.escapeShellArg masShortcutInfoPlist} \ + > "$appDir/Frameworks/MASShortcut.framework/Resources/Info.plist" + + # Resources + ibtool --compile "$appDir/Resources/MainMenu.nib" Itsycal/Base.lproj/MainMenu.xib + actool --compile "$appDir/Resources" \ + --platform macosx \ + --minimum-deployment-target ${stdenv.hostPlatform.darwinMinVersion} \ + --app-icon AppIcon \ + --output-partial-info-plist "$buildDir/asset-info.plist" \ + "$buildDir/Assets.xcassets" + cp Itsycal/beep.mp3 "$appDir/Resources/" + mkdir -p "$appDir/Resources/Fonts" + cp Itsycal/_fonts/Mow.otf "$appDir/Resources/Fonts/" + for lproj in Itsycal/*.lproj; do + mkdir -p "$appDir/Resources/$(basename "$lproj")" + cp "$lproj"/*.strings "$appDir/Resources/$(basename "$lproj")/" 2>/dev/null || true + done + + # Resolve the Xcode variables in upstream's plist. Keeping that plist as + # the source of truth means new privacy and bundle metadata is preserved. + bundleVersion=$(sed -n 's/.*CURRENT_PROJECT_VERSION = \([0-9]*\);/\1/p' \ + Itsycal.xcodeproj/project.pbxproj | head -1) + cp Itsycal/Info.plist "$appDir/Info.plist" + substituteInPlace "$appDir/Info.plist" \ + --replace-fail '$(EXECUTABLE_NAME)' 'Itsycal' \ + --replace-fail '$(PRODUCT_BUNDLE_IDENTIFIER)' 'com.mowglii.ItsycalApp' \ + --replace-fail '$(PRODUCT_NAME)' 'Itsycal' \ + --replace-fail '$(MARKETING_VERSION)' '${finalAttrs.version}' \ + --replace-fail '$(CURRENT_PROJECT_VERSION)' "$bundleVersion" \ + --replace-fail '$(MACOSX_DEPLOYMENT_TARGET)' '${stdenv.hostPlatform.darwinMinVersion}' + + # Merge actool's icon metadata and remove the Sparkle configuration that + # no longer has a consumer in this immutable Nix build. + PlistBuddy -c 'Set :CFBundleIconFile AppIcon' \ + -c 'Add :CFBundleIconName string AppIcon' \ + -c 'Delete :SUAllowsAutomaticUpdates' \ + -c 'Delete :SUEnableAutomaticChecks' \ + -c 'Delete :SUFeedURL' \ + -c 'Delete :SUPublicEDKey' \ + -c 'Delete :SUScheduledCheckInterval' \ + "$appDir/Info.plist" + + makeWrapper "$out/Applications/Itsycal.app/Contents/MacOS/Itsycal" "$out/bin/itsycal" runHook postInstall ''; - passthru = { - updateScript = ./update.sh; + # Signing only Mach-O files leaves app resources unsealed. Sign the complete + # bundle after fixup so ServiceManagement accepts it and nested code verifies. + postFixup = '' + rcodesign sign "$out/Applications/Itsycal.app" + ''; + + passthru.updateScript = nix-update-script { + # Ignore upstream's non-version "help" tag. + extraArgs = [ "--version-regex=^([0-9]+\\.[0-9]+\\.[0-9]+)$" ]; }; meta = { changelog = "https://www.mowglii.com/itsycal/versionhistory.html"; description = "Tiny menu bar calendar"; homepage = "https://www.mowglii.com/itsycal/"; - license = lib.licenses.mit; + license = with lib.licenses; [ + bsd2 + mit + ]; + mainProgram = "itsycal"; maintainers = with lib.maintainers; [ eclairevoyant _4evy ]; platforms = lib.platforms.darwin; - sourceProvenance = with lib.sourceTypes; [ binaryNativeCode ]; + sourceProvenance = with lib.sourceTypes; [ fromSource ]; }; }) diff --git a/pkgs/by-name/it/itsycal/update.sh b/pkgs/by-name/it/itsycal/update.sh deleted file mode 100755 index cd176cc46860..000000000000 --- a/pkgs/by-name/it/itsycal/update.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env nix-shell -#!nix-shell -i bash -p curl xq-xml common-updater-scripts - -set -eu - -ROOT="$(dirname "$(readlink -f "$0")")" -NIX_DRV="$ROOT/package.nix" -if [ ! -f "$NIX_DRV" ]; then - echo "ERROR: cannot find package.nix in $ROOT" - exit 1 -fi - -LATEST_VERSION="$(curl -Ls https://www.mowglii.com/itsycal/versionhistory.html | xq -m -q 'h4' -a 'id' | head -n1)" - -if [ -z "$LATEST_VERSION" ]; then - echo "ERROR: Failed to scrape the latest version." - exit 1 -fi - -update-source-version itsycal "$LATEST_VERSION" --file="$NIX_DRV"