From 5fad8c8cf0b486c5414323f1bc94115325914554 Mon Sep 17 00:00:00 2001 From: FlameFlag Date: Wed, 1 Apr 2026 19:38:38 +0300 Subject: [PATCH] rectangle: build from source --- pkgs/by-name/re/rectangle/package.nix | 295 +++++++++++++++++- .../re/rectangle/stubs/MASShortcut.modulemap | 4 + .../re/rectangle/stubs/SparkleStub.swift | 16 + 3 files changed, 303 insertions(+), 12 deletions(-) create mode 100644 pkgs/by-name/re/rectangle/stubs/MASShortcut.modulemap create mode 100644 pkgs/by-name/re/rectangle/stubs/SparkleStub.swift diff --git a/pkgs/by-name/re/rectangle/package.nix b/pkgs/by-name/re/rectangle/package.nix index 2ecb994d634b..8db468fde490 100644 --- a/pkgs/by-name/re/rectangle/package.nix +++ b/pkgs/by-name/re/rectangle/package.nix @@ -1,29 +1,300 @@ { lib, - stdenvNoCC, - fetchurl, - undmg, + swiftPackages, + fetchFromGitHub, + darwin, + actool, + ibtool, + makeWrapper, nix-update-script, }: -stdenvNoCC.mkDerivation (finalAttrs: { +let + inherit (swiftPackages) stdenv swift; + + 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" + ]; + + # Derive object file name from source path: "UI/MASShortcutView.m" -> "MASShortcutView" + objName = src: lib.removeSuffix ".m" (baseNameOf src); + + toPlist = lib.generators.toPlist { escape = true; }; + + # Standard macOS app plist fields. Pass app-specific attrs to override/extend. + mkAppPlist = + attrs: + toPlist ( + { + CFBundleInfoDictionaryVersion = "6.0"; + CFBundlePackageType = "APPL"; + LSMinimumSystemVersion = "10.15"; + NSMainStoryboardFile = "Main"; + NSPrincipalClass = "NSApplication"; + } + // attrs + ); + + mkFrameworkPlist = + name: + toPlist { + CFBundleExecutable = name; + CFBundleIdentifier = "com.knollsoft.Rectangle.${name}"; + CFBundleInfoDictionaryVersion = "6.0"; + CFBundleName = name; + CFBundlePackageType = "FMWK"; + CFBundleVersion = "1"; + }; + + # Generate shell to assemble a .app bundle: directory structure, binary, + # frameworks (with plists and optional localizations), auto-discovered IB + # resources, localizations, and Info.plist + # Requires compileIB and installLocalizations shell functions in scope + mkAppBundle = + { + name, + binary, + sourceDir, + plist, + destDir, + frameworks ? [ ], + }: + let + app = "${destDir}/${name}.app"; + in + '' + nixLog "assembling ${name}.app" + mkdir -p "${app}/Contents/"{MacOS,Resources} + cp "${binary}" "${app}/Contents/MacOS/${name}" + + ${lib.concatMapStrings (fw: '' + nixLog "installing framework: ${fw.name}" + mkdir -p "${app}/Contents/Frameworks/${fw.name}.framework/Resources" + cp "${fw.dylib}" "${app}/Contents/Frameworks/${fw.name}.framework/${fw.name}" + printf '%s' ${lib.escapeShellArg (mkFrameworkPlist fw.name)} \ + > "${app}/Contents/Frameworks/${fw.name}.framework/Resources/Info.plist" + ${lib.optionalString (fw ? localizations) '' + find "${fw.localizations}" -name '*.lproj' -type d \ + -exec cp -r {} "${app}/Contents/Frameworks/${fw.name}.framework/Resources/" \; + ''} + '') frameworks} + + while IFS= read -r -d "" f; do + compileIB "$f" "${app}/Contents/Resources" + done < <(find ${sourceDir} \( -name '*.storyboard' -o -name '*.xib' \) -print0) + + installLocalizations ${sourceDir} "${app}/Contents/Resources" + printf '%s' ${lib.escapeShellArg plist} > "${app}/Contents/Info.plist" + ''; + + mainInfoPlist = + version: + mkAppPlist { + CFBundleDevelopmentRegion = "en"; + CFBundleExecutable = "Rectangle"; + CFBundleIdentifier = "com.knollsoft.Rectangle"; + CFBundleName = "Rectangle"; + CFBundleShortVersionString = version; + CFBundleVersion = "100"; + CFBundleURLTypes = [ { CFBundleURLSchemes = [ "rectangle" ]; } ]; + CFBundleIconFile = "AppIcon"; + CFBundleIconName = "AppIcon"; + LSApplicationCategoryType = "public.app-category.productivity"; + LSUIElement = true; + NSHumanReadableCopyright = "Copyright © 2019-2026 Ryan Hanson. All rights reserved."; + SUFeedURL = "https://rectangleapp.com/downloads/updates.xml"; + SUPublicEDKey = "lpt9M3PhocbZ3MZiLH+crEqRfU11kfoNzGxSqiEIdvM="; + SUScheduledCheckInterval = 172800; + }; + + launcherInfoPlist = mkAppPlist { + CFBundleExecutable = "RectangleLauncher"; + CFBundleIdentifier = "com.knollsoft.RectangleLauncher"; + CFBundleName = "RectangleLauncher"; + CFBundleShortVersionString = "1.0"; + CFBundleVersion = "1"; + LSBackgroundOnly = true; + }; +in +stdenv.mkDerivation (finalAttrs: { pname = "rectangle"; version = "0.95"; - src = fetchurl { - url = "https://github.com/rxhanson/Rectangle/releases/download/v${finalAttrs.version}/Rectangle${finalAttrs.version}.dmg"; - hash = "sha256-fykSgXAGxouBHAlcW4rcKthApQVkYfRiuw5GnI6hIAA="; + src = fetchFromGitHub { + owner = "rxhanson"; + repo = "Rectangle"; + tag = "v${finalAttrs.version}"; + hash = "sha256-M/qZo2dWsFQxiBD5ypKh0M7AdHdLkY/rx4Lx01OBSlc="; }; - sourceRoot = "."; + nativeBuildInputs = [ + swift + actool + ibtool + darwin.autoSignDarwinBinariesHook + makeWrapper + ]; - nativeBuildInputs = [ undmg ]; + dontConfigure = true; + + buildPhase = '' + runHook preBuild + buildDir="$PWD/build" + mkdir -p "$buildDir" + + commonSwiftFlags=( + -O -disable-bridging-pch + -Xlinker -platform_version -Xlinker macos -Xlinker 14.0 -Xlinker 26.0 + ) + + nixLog "building Sparkle stub framework" + swiftc "''${commonSwiftFlags[@]}" -emit-module -emit-library \ + -module-name Sparkle -module-link-name Sparkle \ + -emit-module-path "$buildDir/Sparkle.swiftmodule" \ + -Xlinker -install_name -Xlinker "@rpath/Sparkle.framework/Sparkle" \ + ${./stubs/SparkleStub.swift} -o "$buildDir/libSparkle.dylib" + + nixLog "building MASShortcut framework" + masDir="${masShortcutSrc}/Framework" + mkdir -p "$buildDir/MAS_headers/MASShortcut" + for h in "$masDir"/include/*.h; do + ln -sf "$h" "$buildDir/MAS_headers/MASShortcut/$(basename "$h")" + done + cp ${./stubs/MASShortcut.modulemap} "$buildDir/MAS_headers/MASShortcut/module.modulemap" + + masObjFiles=() + ${lib.concatMapStrings ( + src: + let + name = objName src; + in + '' + nixLog "compiling ${name}" + clang -fobjc-arc -O2 -I "$masDir/include" -include Foundation/Foundation.h \ + -c "$masDir/${src}" -o "$buildDir/MAS_${name}.o" + masObjFiles+=("$buildDir/MAS_${name}.o") + '' + ) masShortcutSources} + + nixLog "linking MASShortcut dylib" + clang -dynamiclib "''${masObjFiles[@]}" \ + -framework AppKit -framework Carbon -framework Foundation \ + -install_name "@rpath/MASShortcut.framework/MASShortcut" \ + -o "$buildDir/libMASShortcut.dylib" + + rectSwiftFiles=() + while IFS= read -r -d "" f; do + rectSwiftFiles+=("$f") + done < <(find Rectangle -name '*.swift' -print0) + + nixLog "compiling Rectangle (''${#rectSwiftFiles[@]} swift files)" + swiftc "''${commonSwiftFlags[@]}" -emit-executable \ + -module-name Rectangle \ + -import-objc-header Rectangle/Rectangle-Bridging-Header.h \ + -I "$buildDir" -L "$buildDir" \ + -Xcc -fmodule-map-file="$buildDir/MAS_headers/MASShortcut/module.modulemap" \ + -Xcc -I"$buildDir/MAS_headers" \ + -lSparkle -lMASShortcut \ + -framework AppKit -framework Cocoa -framework Carbon \ + -framework ServiceManagement -framework IOKit \ + -F /System/Library/PrivateFrameworks \ + -Xlinker -rpath -Xlinker "@executable_path/../Frameworks" \ + "''${rectSwiftFiles[@]}" -o "$buildDir/Rectangle" + + nixLog "compiling RectangleLauncher" + swiftc "''${commonSwiftFlags[@]}" -emit-executable \ + -module-name RectangleLauncher \ + -parse-as-library \ + -framework AppKit \ + RectangleLauncher/AppDelegate.swift \ + -o "$buildDir/RectangleLauncher" + + runHook postBuild + ''; installPhase = '' runHook preInstall - mkdir -p "$out/Applications" - mv Rectangle.app "$out/Applications" + # Compile a .storyboard or .xib into a target resource directory + # Preserves .lproj subdirectory structure automatically + compileIB() { + local src="$1" destDir="$2" + local filename="$(basename "$src")" + local name="''${filename%.*}" ext="''${filename##*.}" + local outExt; if [[ "$ext" == "storyboard" ]]; then outExt="storyboardc"; else outExt="nib"; fi + + local parentDir="$(basename "$(dirname "$src")")" + nixLog "compiling IB resource: $src" + if [[ "$parentDir" == *.lproj ]]; then + mkdir -p "$destDir/$parentDir" + ibtool --compile "$destDir/$parentDir/$name.$outExt" "$src" + else + ibtool --compile "$destDir/$name.$outExt" "$src" + fi + } + + # Copy .lproj dirs (for .strings localization files) and strip source IB files + installLocalizations() { + local srcDir="$1" destDir="$2" + nixLog "installing localizations from $srcDir" + find "$srcDir" -name '*.lproj' -type d -exec cp -r {} "$destDir/" \; + find "$destDir" \( -name '*.storyboard' -o -name '*.xib' \) -delete + } + + # Rectangle.app + app="$out/Applications/Rectangle.app" + ${mkAppBundle { + name = "Rectangle"; + binary = "$buildDir/Rectangle"; + sourceDir = "Rectangle"; + plist = mainInfoPlist finalAttrs.version; + destDir = "$out/Applications"; + frameworks = [ + { + name = "Sparkle"; + dylib = "$buildDir/libSparkle.dylib"; + } + { + name = "MASShortcut"; + dylib = "$buildDir/libMASShortcut.dylib"; + localizations = "${masShortcutSrc}/Framework/Resources"; + } + ]; + }} + + # Asset catalog + nixLog "compiling asset catalog" + actool --compile "$app/Contents/Resources" \ + --platform macosx --minimum-deployment-target 14.0 \ + --app-icon AppIcon --output-partial-info-plist /dev/null \ + Rectangle/Assets.xcassets + + # RectangleLauncher.app + ${mkAppBundle { + name = "RectangleLauncher"; + binary = "$buildDir/RectangleLauncher"; + sourceDir = "RectangleLauncher"; + plist = launcherInfoPlist; + destDir = "$app/Contents/Library/LoginItems"; + }} + + makeWrapper "$app/Contents/MacOS/Rectangle" "$out/bin/rectangle" runHook postInstall ''; @@ -35,10 +306,10 @@ stdenvNoCC.mkDerivation (finalAttrs: { homepage = "https://rectangleapp.com/"; license = lib.licenses.mit; maintainers = with lib.maintainers; [ + FlameFlag Intuinewin wegank ]; platforms = lib.platforms.darwin; - sourceProvenance = with lib.sourceTypes; [ binaryNativeCode ]; }; }) diff --git a/pkgs/by-name/re/rectangle/stubs/MASShortcut.modulemap b/pkgs/by-name/re/rectangle/stubs/MASShortcut.modulemap new file mode 100644 index 000000000000..e28ae99fa753 --- /dev/null +++ b/pkgs/by-name/re/rectangle/stubs/MASShortcut.modulemap @@ -0,0 +1,4 @@ +module MASShortcut { + header "Shortcut.h" + export * +} diff --git a/pkgs/by-name/re/rectangle/stubs/SparkleStub.swift b/pkgs/by-name/re/rectangle/stubs/SparkleStub.swift new file mode 100644 index 000000000000..e37228ee3902 --- /dev/null +++ b/pkgs/by-name/re/rectangle/stubs/SparkleStub.swift @@ -0,0 +1,16 @@ +import Foundation +import AppKit + +public protocol SPUStandardUserDriverDelegate: AnyObject {} + +public class SUAppcastItem: NSObject {} + +public class SPUUpdater: NSObject { + @objc dynamic public var automaticallyChecksForUpdates: Bool = false +} + +public class SPUStandardUpdaterController: NSObject { + public let updater = SPUUpdater() + public init(updaterDelegate: Any?, userDriverDelegate: Any?) {} + @objc public func checkForUpdates(_ sender: Any?) {} +}