From 733dfb3d33733fb52aea8c7f918ffdc8bb2af1ed Mon Sep 17 00:00:00 2001 From: Sertac Ozercan Date: Wed, 15 Jul 2026 22:07:24 -0700 Subject: [PATCH 1/4] refactor: migrate app to Swift Port Finder automation, VS Code launching, and path handling to Swift 6. Rewrite the focused tests and remove the generated Finder Objective-C bridge and legacy build settings. Signed-off-by: Sertac Ozercan --- AGENTS.md | 21 ++- Open in Code.xcodeproj/project.pbxproj | 55 ++------ Open in Code_Prefix.pch | 3 - OpenInCodeCore.h | 7 - OpenInCodeCore.m | 58 -------- OpenInCodeCore.swift | 66 +++++++++ Tests/OpenInCodeCoreTests.m | 85 ------------ Tests/OpenInCodeCoreTests.swift | 151 ++++++++++++++++++++ main.m | 182 ------------------------- main.swift | 150 ++++++++++++++++++++ scripts/test.sh | 19 +-- 11 files changed, 398 insertions(+), 399 deletions(-) delete mode 100755 Open in Code_Prefix.pch delete mode 100644 OpenInCodeCore.h delete mode 100644 OpenInCodeCore.m create mode 100644 OpenInCodeCore.swift delete mode 100644 Tests/OpenInCodeCoreTests.m create mode 100644 Tests/OpenInCodeCoreTests.swift delete mode 100755 main.m create mode 100644 main.swift diff --git a/AGENTS.md b/AGENTS.md index d335f34..022db7f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,22 +2,22 @@ ## Project at a glance -OpenInCode is a small macOS Finder toolbar utility written in Objective-C. It reads the selected Finder item, or the front Finder window when nothing is selected, and opens the corresponding folder in Visual Studio Code. +OpenInCode is a small macOS Finder toolbar utility written in Swift. It reads the selected Finder item, or the front Finder window when nothing is selected, and opens the corresponding folder in Visual Studio Code. - Deployment target: macOS 12 or newer. - Build system: `Open in Code.xcodeproj`; Xcode 26 or newer is required for the current Icon Composer asset. -- Memory model: manual reference counting; ARC is disabled. +- Swift language mode: Swift 6. - Dependencies: Apple frameworks only; there is no package manager or dependency-install step. ## Repository map -- `main.m`: Finder automation, user-facing errors, application lookup, and launch flow. -- `OpenInCodeCore.h` / `OpenInCodeCore.m`: testable editor-priority and Finder-path logic. -- `Tests/OpenInCodeCoreTests.m`: standalone Foundation test executable used by the test script. +- `main.swift`: Finder automation, user-facing errors, application lookup, and launch flow. +- `OpenInCodeCore.swift`: testable editor-priority and Finder-path logic. +- `Tests/OpenInCodeCoreTests.swift`: standalone Foundation test executable used by the test script. - `scripts/test.sh`: canonical focused test command; also validates Homebrew cask rendering. - `scripts/render-homebrew-cask.sh`: release cask template generator. - `Info.plist` and `Open in Code.entitlements`: app metadata and Finder Apple Events permission. -- `Open in Code.xcodeproj`: target membership, build settings, signing, and the build rule that generates `Finder.h` from Finder's scripting definition. +- `Open in Code.xcodeproj`: target membership, Swift build settings, and signing. - `.github/workflows/release.yml`: tag-driven universal build, signing, notarization, GitHub release, and Homebrew cask update. ## Working rules @@ -25,9 +25,8 @@ OpenInCode is a small macOS Finder toolbar utility written in Objective-C. It re 1. Check `git status --short` before editing and preserve unrelated user changes. 2. Inspect only the files relevant to the task; do not search generated `build/` or `DerivedData/` content. 3. Keep changes focused. Avoid adding dependencies, new abstractions, or project files unless the task requires them. -4. Put deterministic, UI-independent behavior in `OpenInCodeCore.*` and cover it in `Tests/OpenInCodeCoreTests.m`. +4. Put deterministic, UI-independent behavior in `OpenInCodeCore.swift` and cover it in `Tests/OpenInCodeCoreTests.swift`. 5. When adding or removing source or resource files, keep the Xcode project references and target membership in sync. -6. Do not add a generated `Finder.h` to the repository; Xcode creates it during the build. ## Behavioral invariants @@ -40,10 +39,10 @@ Preserve these unless the requested change explicitly replaces them: - Present actionable errors for Finder permission failures and missing or failed VS Code launches. - Keep compatibility with macOS 12; guard any newer API before use. -## Objective-C conventions +## Swift conventions -- Match the existing Objective-C style and naming (`OIC` prefix for shared symbols). -- ARC is disabled. Balance ownership with `retain`, `release`, `autorelease`, or `copy`, and keep `dealloc` implementations correct. +- Match the existing Swift style and naming (`OIC` prefix for shared symbols). +- Keep the application compatible with Swift 6 language mode and macOS 12. - Treat compiler warnings as errors in testable core code. - Prefer small functions with explicit failure handling over silent fallback behavior. diff --git a/Open in Code.xcodeproj/project.pbxproj b/Open in Code.xcodeproj/project.pbxproj index 58cd4f8..692d98d 100755 --- a/Open in Code.xcodeproj/project.pbxproj +++ b/Open in Code.xcodeproj/project.pbxproj @@ -7,32 +7,13 @@ objects = { /* Begin PBXBuildFile section */ - 69C8AA89106EE02F00C4DE0D /* Finder.app in Sources */ = {isa = PBXBuildFile; fileRef = 69C8AA70106E8F5800C4DE0D /* Finder.app */; }; 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; - 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; + 8D11072D0486CEB800E47090 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.swift */; }; 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; - C82F4C8C1D31B01E0084C3F5 /* ScriptingBridge.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69C8A98D106E818900C4DE0D /* ScriptingBridge.framework */; }; C89E34F12FBA29F900F208FE /* vscode.icon in Resources */ = {isa = PBXBuildFile; fileRef = C89E34F02FBA29F900F208FE /* vscode.icon */; }; - C89E34F62FBA29F900F208FE /* OpenInCodeCore.m in Sources */ = {isa = PBXBuildFile; fileRef = C89E34F32FBA29F900F208FE /* OpenInCodeCore.m */; }; + C89E34F62FBA29F900F208FE /* OpenInCodeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89E34F32FBA29F900F208FE /* OpenInCodeCore.swift */; }; /* End PBXBuildFile section */ -/* Begin PBXBuildRule section */ - 69C8AA6F106E8F0C00C4DE0D /* PBXBuildRule */ = { - isa = PBXBuildRule; - compilerSpec = com.apple.compilers.proxy.script; - filePatterns = "*.app"; - fileType = pattern.proxy; - inputFiles = ( - ); - isEditable = 1; - outputFiles = ( - "$(DERIVED_FILES_DIR)/$(INPUT_FILE_BASE).h", - ); - runOncePerArchitecture = 0; - script = "sdef \"$INPUT_FILE_PATH\" | sdp -fh -o \"$DERIVED_FILES_DIR\" --basename \"$INPUT_FILE_BASE\" --bundleid `defaults read \"$INPUT_FILE_PATH/Contents/Info\" CFBundleIdentifier`"; - }; -/* End PBXBuildRule section */ - /* Begin PBXCopyFilesBuildPhase section */ 694495D00B869BB500A19631 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; @@ -49,17 +30,13 @@ 089C165DFE840E0CC02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = ""; }; 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; - 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 29B97316FDCFA39411CA2CEA /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; - 32CA4F630368D1EE00C91783 /* Open in Code_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Open in Code_Prefix.pch"; sourceTree = ""; }; - 69C8A98D106E818900C4DE0D /* ScriptingBridge.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ScriptingBridge.framework; path = /System/Library/Frameworks/ScriptingBridge.framework; sourceTree = ""; }; - 69C8AA70106E8F5800C4DE0D /* Finder.app */ = {isa = PBXFileReference; lastKnownFileType = wrapper.application; name = Finder.app; path = /System/Library/CoreServices/Finder.app; sourceTree = ""; }; 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 8D1107320486CEB800E47090 /* Open in Code.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Open in Code.app"; sourceTree = BUILT_PRODUCTS_DIR; }; C89E34F02FBA29F900F208FE /* vscode.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = vscode.icon; sourceTree = ""; }; - C89E34F22FBA29F900F208FE /* OpenInCodeCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OpenInCodeCore.h; sourceTree = ""; }; - C89E34F32FBA29F900F208FE /* OpenInCodeCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OpenInCodeCore.m; sourceTree = ""; }; + C89E34F32FBA29F900F208FE /* OpenInCodeCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInCodeCore.swift; sourceTree = ""; }; C89E34F52FBA29F900F208FE /* Open in Code.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Open in Code.entitlements"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -69,7 +46,6 @@ buildActionMask = 2147483647; files = ( 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */, - C82F4C8C1D31B01E0084C3F5 /* ScriptingBridge.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -80,7 +56,6 @@ isa = PBXGroup; children = ( 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */, - 69C8A98D106E818900C4DE0D /* ScriptingBridge.framework */, ); name = "Linked Frameworks"; sourceTree = ""; @@ -106,7 +81,6 @@ 29B97314FDCFA39411CA2CEA /* Open In Code */ = { isa = PBXGroup; children = ( - 69C8AA70106E8F5800C4DE0D /* Finder.app */, 29B97315FDCFA39411CA2CEA /* Other Sources */, 29B97317FDCFA39411CA2CEA /* Resources */, 29B97323FDCFA39411CA2CEA /* Frameworks */, @@ -118,10 +92,8 @@ 29B97315FDCFA39411CA2CEA /* Other Sources */ = { isa = PBXGroup; children = ( - 32CA4F630368D1EE00C91783 /* Open in Code_Prefix.pch */, - 29B97316FDCFA39411CA2CEA /* main.m */, - C89E34F22FBA29F900F208FE /* OpenInCodeCore.h */, - C89E34F32FBA29F900F208FE /* OpenInCodeCore.m */, + 29B97316FDCFA39411CA2CEA /* main.swift */, + C89E34F32FBA29F900F208FE /* OpenInCodeCore.swift */, C89E34F52FBA29F900F208FE /* Open in Code.entitlements */, ); name = "Other Sources"; @@ -158,9 +130,6 @@ 8D11072E0486CEB800E47090 /* Frameworks */, 694495D00B869BB500A19631 /* CopyFiles */, ); - buildRules = ( - 69C8AA6F106E8F0C00C4DE0D /* PBXBuildRule */, - ); dependencies = ( ); name = "Open in Code"; @@ -175,6 +144,7 @@ 29B97313FDCFA39411CA2CEA /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 2700; LastUpgradeCheck = 2700; }; buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "Open in Code" */; @@ -211,9 +181,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 69C8AA89106EE02F00C4DE0D /* Finder.app in Sources */, - 8D11072D0486CEB800E47090 /* main.m in Sources */, - C89E34F62FBA29F900F208FE /* OpenInCodeCore.m in Sources */, + 8D11072D0486CEB800E47090 /* main.swift in Sources */, + C89E34F62FBA29F900F208FE /* OpenInCodeCore.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -235,7 +204,6 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = vscode; - CLANG_ENABLE_OBJC_ARC = NO; CODE_SIGN_ENTITLEMENTS = "Open in Code.entitlements"; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Manual; @@ -244,13 +212,13 @@ "COPY_PHASE_STRIP[sdk=*]" = NO; ENABLE_HARDENED_RUNTIME = YES; GCC_DYNAMIC_NO_PIC = NO; - GCC_ENABLE_OBJC_EXCEPTIONS = YES; GCC_OPTIMIZATION_LEVEL = 0; INFOPLIST_FILE = Info.plist; INSTALL_PATH = /Applications; MACOSX_DEPLOYMENT_TARGET = 12.0; PRODUCT_BUNDLE_IDENTIFIER = com.sertacozercan.openincode; PRODUCT_NAME = "Open in Code"; + SWIFT_VERSION = 6.0; WRAPPER_EXTENSION = app; }; name = Debug; @@ -259,7 +227,6 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = vscode; - CLANG_ENABLE_OBJC_ARC = NO; CODE_SIGN_ENTITLEMENTS = "Open in Code.entitlements"; CODE_SIGN_IDENTITY = "Developer ID Application"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; @@ -268,13 +235,13 @@ DEVELOPMENT_TEAM = VURRGRYW45; "DEVELOPMENT_TEAM[sdk=macosx*]" = 57QNR9B89Q; ENABLE_HARDENED_RUNTIME = YES; - GCC_ENABLE_OBJC_EXCEPTIONS = YES; GCC_GENERATE_DEBUGGING_SYMBOLS = NO; INFOPLIST_FILE = Info.plist; INSTALL_PATH = /Applications; MACOSX_DEPLOYMENT_TARGET = 12.0; PRODUCT_BUNDLE_IDENTIFIER = com.sertacozercan.openincode; PRODUCT_NAME = "Open in Code"; + SWIFT_VERSION = 6.0; WRAPPER_EXTENSION = app; }; name = Release; diff --git a/Open in Code_Prefix.pch b/Open in Code_Prefix.pch deleted file mode 100755 index aabef47..0000000 --- a/Open in Code_Prefix.pch +++ /dev/null @@ -1,3 +0,0 @@ -#ifdef __OBJC__ - #import -#endif diff --git a/OpenInCodeCore.h b/OpenInCodeCore.h deleted file mode 100644 index f3489e4..0000000 --- a/OpenInCodeCore.h +++ /dev/null @@ -1,7 +0,0 @@ -#import - -extern NSString * const OICVSCodeBundleIdentifier; -extern NSString * const OICVSCodeInsidersBundleIdentifier; - -NSArray *OICPreferredVSCodeBundleIdentifiers(void); -NSString *OICPathForFinderURL(NSURL *url); diff --git a/OpenInCodeCore.m b/OpenInCodeCore.m deleted file mode 100644 index 1fcf838..0000000 --- a/OpenInCodeCore.m +++ /dev/null @@ -1,58 +0,0 @@ -#import "OpenInCodeCore.h" - -NSString * const OICVSCodeBundleIdentifier = @"com.microsoft.VSCode"; -NSString * const OICVSCodeInsidersBundleIdentifier = @"com.microsoft.VSCodeInsiders"; - -NSArray *OICPreferredVSCodeBundleIdentifiers(void) -{ - return @[OICVSCodeBundleIdentifier, OICVSCodeInsidersBundleIdentifier]; -} - -NSString *OICPathForFinderURL(NSURL *url) -{ - if (url == nil || ![url isFileURL]) { - return nil; - } - - NSNumber *isAliasFile = nil; - NSNumber *isSymbolicLink = nil; - [url getResourceValue:&isAliasFile forKey:NSURLIsAliasFileKey error:nil]; - [url getResourceValue:&isSymbolicLink forKey:NSURLIsSymbolicLinkKey error:nil]; - if ([isAliasFile boolValue] && ![isSymbolicLink boolValue]) { - NSData *bookmark = [NSURL bookmarkDataWithContentsOfURL:url error:nil]; - if (bookmark == nil) { - return nil; - } - - NSURL *resolvedURL = [NSURL URLByResolvingBookmarkData:bookmark - options:NSURLBookmarkResolutionWithoutUI - relativeToURL:nil - bookmarkDataIsStale:nil - error:nil]; - if (resolvedURL == nil) { - return nil; - } - url = resolvedURL; - } - - NSString *path = [[url path] stringByExpandingTildeInPath]; - if ([path length] == 0) { - return nil; - } - - BOOL isDirectory = NO; - if (![[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDirectory]) { - return nil; - } - - NSURL *packageCheckURL = [url URLByResolvingSymlinksInPath]; - NSNumber *isPackageValue = nil; - [packageCheckURL getResourceValue:&isPackageValue forKey:NSURLIsPackageKey error:nil]; - BOOL isPackage = [isPackageValue boolValue]; - - if (!isDirectory || isPackage) { - path = [path stringByDeletingLastPathComponent]; - } - - return [path length] > 0 ? path : nil; -} diff --git a/OpenInCodeCore.swift b/OpenInCodeCore.swift new file mode 100644 index 0000000..980debb --- /dev/null +++ b/OpenInCodeCore.swift @@ -0,0 +1,66 @@ +import Foundation + +let OICVSCodeBundleIdentifier = "com.microsoft.VSCode" +let OICVSCodeInsidersBundleIdentifier = "com.microsoft.VSCodeInsiders" + +func OICPreferredVSCodeBundleIdentifiers() -> [String] { + [OICVSCodeBundleIdentifier, OICVSCodeInsidersBundleIdentifier] +} + +func OICFileSystemPathForURL(_ url: URL) -> String { + var path: String + if #available(macOS 13.0, *) { + path = url.path(percentEncoded: false) + } else { + path = url.path + } + + while path.count > 1 && path.last == "/" { + path.removeLast() + } + return path +} + +func OICPathForFinderURL(_ finderURL: URL?) -> String? { + guard var url = finderURL, url.isFileURL else { + return nil + } + + let resourceValues = try? url.resourceValues(forKeys: [.isAliasFileKey, .isSymbolicLinkKey]) + if resourceValues?.isAliasFile == true && resourceValues?.isSymbolicLink != true { + guard let bookmarkData = try? URL.bookmarkData(withContentsOf: url) else { + return nil + } + + var isStale = false + guard let resolvedURL = try? URL( + resolvingBookmarkData: bookmarkData, + options: .withoutUI, + relativeTo: nil, + bookmarkDataIsStale: &isStale + ) else { + return nil + } + url = resolvedURL + } + + var path = (OICFileSystemPathForURL(url) as NSString).expandingTildeInPath + guard !path.isEmpty else { + return nil + } + + var isDirectory: ObjCBool = false + guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) else { + return nil + } + + let packageCheckURL = url.resolvingSymlinksInPath() + let packageValues = try? packageCheckURL.resourceValues(forKeys: [.isPackageKey]) + let isPackage = packageValues?.isPackage == true + + if !isDirectory.boolValue || isPackage { + path = (path as NSString).deletingLastPathComponent + } + + return path.isEmpty ? nil : path +} diff --git a/Tests/OpenInCodeCoreTests.m b/Tests/OpenInCodeCoreTests.m deleted file mode 100644 index 4ec5161..0000000 --- a/Tests/OpenInCodeCoreTests.m +++ /dev/null @@ -1,85 +0,0 @@ -#import -#include -#import "OpenInCodeCore.h" - -static void assertTrue(BOOL condition, NSString *message) -{ - if (!condition) { - NSLog(@"FAIL: %@", message); - exit(1); - } -} - -int main(void) -{ - @autoreleasepool { - NSArray *bundleIdentifiers = OICPreferredVSCodeBundleIdentifiers(); - assertTrue([bundleIdentifiers count] == 2, @"expected two VS Code channels"); - assertTrue([bundleIdentifiers[0] isEqualToString:OICVSCodeBundleIdentifier], @"stable VS Code must be preferred"); - assertTrue([bundleIdentifiers[1] isEqualToString:OICVSCodeInsidersBundleIdentifier], @"Insiders must be the fallback"); - - assertTrue(OICPathForFinderURL(nil) == nil, @"nil URL must return nil"); - assertTrue(OICPathForFinderURL([NSURL URLWithString:@"https://example.com"]) == nil, @"non-file URL must return nil"); - - NSString *temporaryRoot = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]]; - NSString *folderPath = [temporaryRoot stringByAppendingPathComponent:@"Project"]; - NSString *filePath = [folderPath stringByAppendingPathComponent:@"README.md"]; - NSString *packagePath = [temporaryRoot stringByAppendingPathComponent:@"Example.app"]; - NSString *directorySymlinkPath = [temporaryRoot stringByAppendingPathComponent:@"LinkedProject"]; - NSURL *folderAliasURL = [NSURL fileURLWithPath:[temporaryRoot stringByAppendingPathComponent:@"Project Folder.alias"]]; - NSURL *fileAliasURL = [NSURL fileURLWithPath:[temporaryRoot stringByAppendingPathComponent:@"Project File.alias"]]; - NSString *deletedTargetPath = [temporaryRoot stringByAppendingPathComponent:@"Deleted Target.txt"]; - NSURL *brokenAliasURL = [NSURL fileURLWithPath:[temporaryRoot stringByAppendingPathComponent:@"Broken Target.alias"]]; - - NSFileManager *fileManager = [NSFileManager defaultManager]; - assertTrue([fileManager createDirectoryAtPath:folderPath withIntermediateDirectories:YES attributes:nil error:nil], @"create test folder"); - assertTrue([@"test" writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil], @"create test file"); - assertTrue([@"delete me" writeToFile:deletedTargetPath atomically:YES encoding:NSUTF8StringEncoding error:nil], @"create deleted alias target"); - assertTrue([fileManager createDirectoryAtPath:packagePath withIntermediateDirectories:YES attributes:nil error:nil], @"create test package"); - assertTrue([fileManager createSymbolicLinkAtPath:directorySymlinkPath withDestinationPath:folderPath error:nil], @"create directory symlink"); - - NSData *folderBookmark = [[NSURL fileURLWithPath:folderPath isDirectory:YES] - bookmarkDataWithOptions:NSURLBookmarkCreationSuitableForBookmarkFile - includingResourceValuesForKeys:nil - relativeToURL:nil - error:nil]; - NSData *fileBookmark = [[NSURL fileURLWithPath:filePath] - bookmarkDataWithOptions:NSURLBookmarkCreationSuitableForBookmarkFile - includingResourceValuesForKeys:nil - relativeToURL:nil - error:nil]; - NSData *deletedTargetBookmark = [[NSURL fileURLWithPath:deletedTargetPath] - bookmarkDataWithOptions:NSURLBookmarkCreationSuitableForBookmarkFile - includingResourceValuesForKeys:nil - relativeToURL:nil - error:nil]; - assertTrue(folderBookmark != nil, @"create folder bookmark data"); - assertTrue(fileBookmark != nil, @"create file bookmark data"); - assertTrue(deletedTargetBookmark != nil, @"create deleted-target bookmark data"); - assertTrue([NSURL writeBookmarkData:folderBookmark toURL:folderAliasURL options:0 error:nil], @"create folder alias"); - assertTrue([NSURL writeBookmarkData:fileBookmark toURL:fileAliasURL options:0 error:nil], @"create file alias"); - assertTrue([NSURL writeBookmarkData:deletedTargetBookmark toURL:brokenAliasURL options:0 error:nil], @"create broken alias"); - assertTrue([fileManager removeItemAtPath:deletedTargetPath error:nil], @"delete alias target"); - - NSURL *resolvedFolderAliasURL = [NSURL URLByResolvingAliasFileAtURL:folderAliasURL options:NSURLBookmarkResolutionWithoutUI error:nil]; - NSURL *resolvedFileAliasURL = [NSURL URLByResolvingAliasFileAtURL:fileAliasURL options:NSURLBookmarkResolutionWithoutUI error:nil]; - assertTrue(resolvedFolderAliasURL != nil, @"resolve folder alias"); - assertTrue(resolvedFileAliasURL != nil, @"resolve file alias"); - NSString *resolvedFolderAliasPath = [resolvedFolderAliasURL path]; - NSString *resolvedFileAliasParentPath = [[resolvedFileAliasURL path] stringByDeletingLastPathComponent]; - - assertTrue([OICPathForFinderURL([NSURL fileURLWithPath:folderPath]) isEqualToString:folderPath], @"folder should open itself"); - assertTrue([OICPathForFinderURL([NSURL fileURLWithPath:filePath]) isEqualToString:folderPath], @"file should open its parent"); - assertTrue([OICPathForFinderURL([NSURL fileURLWithPath:packagePath]) isEqualToString:temporaryRoot], @"Finder package should open its parent"); - assertTrue([OICPathForFinderURL([NSURL fileURLWithPath:directorySymlinkPath]) isEqualToString:directorySymlinkPath], @"directory symlink should open the linked directory"); - assertTrue([OICPathForFinderURL(folderAliasURL) isEqualToString:resolvedFolderAliasPath], @"folder alias should resolve to its target"); - assertTrue([OICPathForFinderURL(fileAliasURL) isEqualToString:resolvedFileAliasParentPath], @"file alias should resolve to its target parent"); - assertTrue(OICPathForFinderURL(brokenAliasURL) == nil, @"broken alias must return nil"); - assertTrue(OICPathForFinderURL([NSURL fileURLWithPath:[temporaryRoot stringByAppendingPathComponent:@"missing"]]) == nil, @"missing item must return nil"); - - assertTrue([fileManager removeItemAtPath:temporaryRoot error:nil], @"remove test files"); - NSLog(@"OpenInCodeCoreTests: PASS"); - } - - return 0; -} diff --git a/Tests/OpenInCodeCoreTests.swift b/Tests/OpenInCodeCoreTests.swift new file mode 100644 index 0000000..0f288a1 --- /dev/null +++ b/Tests/OpenInCodeCoreTests.swift @@ -0,0 +1,151 @@ +import Darwin +import Foundation + +private func fail(_ message: String) -> Never { + let output = "FAIL: \(message)\n" + FileHandle.standardError.write(Data(output.utf8)) + exit(1) +} + +private func assertTrue(_ condition: @autoclosure () -> Bool, _ message: String) { + if !condition() { + fail(message) + } +} + +@main +private struct OpenInCodeCoreTests { + static func main() { + let bundleIdentifiers = OICPreferredVSCodeBundleIdentifiers() + assertTrue(bundleIdentifiers.count == 2, "expected two VS Code channels") + assertTrue(bundleIdentifiers[0] == OICVSCodeBundleIdentifier, "stable VS Code must be preferred") + assertTrue(bundleIdentifiers[1] == OICVSCodeInsidersBundleIdentifier, "Insiders must be the fallback") + + assertTrue(OICPathForFinderURL(nil) == nil, "nil URL must return nil") + assertTrue(OICPathForFinderURL(URL(string: "https://example.com")) == nil, "non-file URL must return nil") + + let temporaryRoot = (NSTemporaryDirectory() as NSString) + .appendingPathComponent(UUID().uuidString) + let folderPath = (temporaryRoot as NSString).appendingPathComponent("Project") + let filePath = (folderPath as NSString).appendingPathComponent("README.md") + let packagePath = (temporaryRoot as NSString).appendingPathComponent("Example.app") + let directorySymlinkPath = (temporaryRoot as NSString).appendingPathComponent("LinkedProject") + let folderAliasURL = URL( + fileURLWithPath: (temporaryRoot as NSString).appendingPathComponent("Project Folder.alias") + ) + let fileAliasURL = URL( + fileURLWithPath: (temporaryRoot as NSString).appendingPathComponent("Project File.alias") + ) + let deletedTargetPath = (temporaryRoot as NSString).appendingPathComponent("Deleted Target.txt") + let brokenAliasURL = URL( + fileURLWithPath: (temporaryRoot as NSString).appendingPathComponent("Broken Target.alias") + ) + + let fileManager = FileManager.default + do { + try fileManager.createDirectory( + atPath: folderPath, + withIntermediateDirectories: true, + attributes: nil + ) + try "test".write(toFile: filePath, atomically: true, encoding: .utf8) + try "delete me".write(toFile: deletedTargetPath, atomically: true, encoding: .utf8) + try fileManager.createDirectory( + atPath: packagePath, + withIntermediateDirectories: true, + attributes: nil + ) + try fileManager.createSymbolicLink( + atPath: directorySymlinkPath, + withDestinationPath: folderPath + ) + } catch { + fail("create test files: \(error)") + } + + let folderBookmark: Data + let fileBookmark: Data + let deletedTargetBookmark: Data + do { + folderBookmark = try URL(fileURLWithPath: folderPath, isDirectory: true).bookmarkData( + options: .suitableForBookmarkFile, + includingResourceValuesForKeys: nil, + relativeTo: nil + ) + fileBookmark = try URL(fileURLWithPath: filePath).bookmarkData( + options: .suitableForBookmarkFile, + includingResourceValuesForKeys: nil, + relativeTo: nil + ) + deletedTargetBookmark = try URL(fileURLWithPath: deletedTargetPath).bookmarkData( + options: .suitableForBookmarkFile, + includingResourceValuesForKeys: nil, + relativeTo: nil + ) + try URL.writeBookmarkData(folderBookmark, to: folderAliasURL) + try URL.writeBookmarkData(fileBookmark, to: fileAliasURL) + try URL.writeBookmarkData(deletedTargetBookmark, to: brokenAliasURL) + try fileManager.removeItem(atPath: deletedTargetPath) + } catch { + fail("create test aliases: \(error)") + } + + let resolvedFolderAliasURL: URL + let resolvedFileAliasURL: URL + do { + resolvedFolderAliasURL = try URL( + resolvingAliasFileAt: folderAliasURL, + options: .withoutUI + ) + resolvedFileAliasURL = try URL( + resolvingAliasFileAt: fileAliasURL, + options: .withoutUI + ) + } catch { + fail("resolve test aliases: \(error)") + } + + let resolvedFolderAliasPath = OICFileSystemPathForURL(resolvedFolderAliasURL) + let resolvedFileAliasParentPath = (OICFileSystemPathForURL(resolvedFileAliasURL) as NSString).deletingLastPathComponent + + assertTrue( + OICPathForFinderURL(URL(fileURLWithPath: folderPath)) == folderPath, + "folder should open itself" + ) + assertTrue( + OICPathForFinderURL(URL(fileURLWithPath: filePath)) == folderPath, + "file should open its parent" + ) + assertTrue( + OICPathForFinderURL(URL(fileURLWithPath: packagePath)) == temporaryRoot, + "Finder package should open its parent" + ) + assertTrue( + OICPathForFinderURL(URL(fileURLWithPath: directorySymlinkPath)) == directorySymlinkPath, + "directory symlink should open the linked directory" + ) + assertTrue( + OICPathForFinderURL(folderAliasURL) == resolvedFolderAliasPath, + "folder alias should resolve to its target" + ) + assertTrue( + OICPathForFinderURL(fileAliasURL) == resolvedFileAliasParentPath, + "file alias should resolve to its target parent" + ) + assertTrue(OICPathForFinderURL(brokenAliasURL) == nil, "broken alias must return nil") + assertTrue( + OICPathForFinderURL( + URL(fileURLWithPath: (temporaryRoot as NSString).appendingPathComponent("missing")) + ) == nil, + "missing item must return nil" + ) + + do { + try fileManager.removeItem(atPath: temporaryRoot) + } catch { + fail("remove test files: \(error)") + } + + print("OpenInCodeCoreTests: PASS") + } +} diff --git a/main.m b/main.m deleted file mode 100755 index 8f7da68..0000000 --- a/main.m +++ /dev/null @@ -1,182 +0,0 @@ -// -// main.m -// Open in Code -// -// Created by Sertac Ozercan on 7/9/2016. -// Copyright Sertac Ozercan 2016. All rights reserved. -// - -#import -#import -#import "Finder.h" -#import "OpenInCodeCore.h" - -static NSString * const FinderAutomationPermissionMessage = @"Open the Automation privacy settings and allow Open in Code to control Finder, then try again."; - -@interface OICScriptingBridgeErrorHandler : NSObject { - NSError *_lastError; -} - -@property(nonatomic, retain) NSError *lastError; - -@end - -@implementation OICScriptingBridgeErrorHandler - -@synthesize lastError = _lastError; - -- (id)eventDidFail:(const AppleEvent *)event withError:(NSError *)error -{ - (void)event; - [self setLastError:error]; - return nil; -} - -- (void)dealloc -{ - [_lastError release]; - [super dealloc]; -} - -@end - -static void showErrorAlert(NSString *message, NSString *informativeText) -{ - [NSApplication sharedApplication]; - [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]; - [NSApp activateIgnoringOtherApps:YES]; - - NSAlert *alert = [[[NSAlert alloc] init] autorelease]; - [alert setAlertStyle:NSAlertStyleCritical]; - [alert setMessageText:message]; - [alert setInformativeText:informativeText]; - [alert addButtonWithTitle:@"OK"]; - [alert runModal]; -} - -static NSString *pathToFrontFinderLocation(NSString **errorMessage) -{ - @try { - FinderApplication *finder = [SBApplication applicationWithBundleIdentifier:@"com.apple.Finder"]; - OICScriptingBridgeErrorHandler *errorHandler = [[[OICScriptingBridgeErrorHandler alloc] init] autorelease]; - [finder setDelegate:errorHandler]; - - FinderItem *target = [(NSArray *)[[finder selection] get] firstObject]; - - if (target == nil) { - target = [[[[finder FinderWindows] firstObject] target] get]; - } - - NSString *targetURLString = [target URL]; - if ([targetURLString length] == 0) { - if (errorMessage != NULL) { - if ([errorHandler lastError] != nil) { - *errorMessage = FinderAutomationPermissionMessage; - } else { - *errorMessage = @"Open a Finder window for a local folder and try again."; - } - } - return nil; - } - - NSString *path = OICPathForFinderURL([NSURL URLWithString:targetURLString]); - if ([path length] == 0 && errorMessage != NULL) { - *errorMessage = @"The selected Finder item does not have an accessible local path."; - } - - return path; - } - @catch (NSException *exception) { - if (errorMessage != NULL) { - *errorMessage = FinderAutomationPermissionMessage; - } - return nil; - } -} - -static BOOL openPathInPreferredVSCode(NSString *path, NSString **errorMessage) -{ - if ([path length] == 0) { - if (errorMessage != NULL) { - *errorMessage = @"No folder path was available to open."; - } - return NO; - } - - NSURL *folderURL = [NSURL fileURLWithPath:path isDirectory:YES]; - NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; - for (NSString *bundleIdentifier in OICPreferredVSCodeBundleIdentifiers()) { - NSURL *applicationURL = [workspace URLForApplicationWithBundleIdentifier:bundleIdentifier]; - if (applicationURL == nil) { - continue; - } - - NSWorkspaceOpenConfiguration *configuration = [NSWorkspaceOpenConfiguration configuration]; - [configuration setActivates:YES]; - [configuration setPromptsUserIfNeeded:YES]; - [configuration setAllowsRunningApplicationSubstitution:NO]; - - dispatch_semaphore_t completion = dispatch_semaphore_create(0); - __block BOOL opened = NO; - __block NSString *attemptError = nil; - - [workspace openURLs:@[folderURL] - withApplicationAtURL:applicationURL - configuration:configuration - completionHandler:^(NSRunningApplication *application, NSError *error) { - opened = application != nil && error == nil; - if (error != nil) { - attemptError = [[error localizedDescription] copy]; - } - dispatch_semaphore_signal(completion); - }]; - - dispatch_semaphore_wait(completion, DISPATCH_TIME_FOREVER); -#if !OS_OBJECT_USE_OBJC - dispatch_release(completion); -#endif - - if (opened) { - [attemptError release]; - return YES; - } - - if (errorMessage != NULL) { - if ([attemptError length] > 0) { - *errorMessage = [attemptError autorelease]; - attemptError = nil; - } else { - *errorMessage = @"Visual Studio Code could not open the selected folder."; - } - } - - [attemptError release]; - return NO; - } - - if (errorMessage != NULL) { - *errorMessage = @"Install Visual Studio Code or Visual Studio Code Insiders, then try again."; - } - return NO; -} - -int main(int argc, char *argv[]) -{ - @autoreleasepool { - NSString *finderError = nil; - NSString *path = pathToFrontFinderLocation(&finderError); - - if ([path length] == 0) { - showErrorAlert(@"Couldn’t read the Finder location", finderError ?: @"Open a Finder window and try again."); - return 1; - } - - NSString *launchError = nil; - if (!openPathInPreferredVSCode(path, &launchError)) { - showErrorAlert(@"Couldn’t open Visual Studio Code", launchError ?: @"The application could not be launched."); - return 2; - } - - return 0; - } -} diff --git a/main.swift b/main.swift new file mode 100644 index 0000000..c5fe688 --- /dev/null +++ b/main.swift @@ -0,0 +1,150 @@ +import AppKit +import Darwin +import Dispatch +import Foundation + +private let finderAutomationPermissionMessage = "Open the Automation privacy settings and allow Open in Code to control Finder, then try again." + +private let finderLocationScript = """ +tell application "Finder" + if (count of selection) > 0 then + set targetItem to item 1 of selection + else if (count of Finder windows) > 0 then + set targetItem to target of front Finder window + else + return "" + end if + + return URL of targetItem +end tell +""" + +@MainActor +private func showErrorAlert(message: String, informativeText: String) { + _ = NSApplication.shared + NSApp.setActivationPolicy(.accessory) + NSApp.activate(ignoringOtherApps: true) + + let alert = NSAlert() + alert.alertStyle = .critical + alert.messageText = message + alert.informativeText = informativeText + alert.addButton(withTitle: "OK") + alert.runModal() +} + +@MainActor +private func pathToFrontFinderLocation() -> (path: String?, errorMessage: String?) { + guard let script = NSAppleScript(source: finderLocationScript) else { + return (nil, finderAutomationPermissionMessage) + } + + var errorInfo: NSDictionary? + let result = script.executeAndReturnError(&errorInfo) + if errorInfo != nil { + return (nil, finderAutomationPermissionMessage) + } + + guard let targetURLString = result.stringValue, !targetURLString.isEmpty else { + return (nil, "Open a Finder window for a local folder and try again.") + } + + guard let targetURL = URL(string: targetURLString), + let path = OICPathForFinderURL(targetURL) else { + return (nil, "The selected Finder item does not have an accessible local path.") + } + + return (path, nil) +} + +private final class OICOpenAttempt: @unchecked Sendable { + private let lock = NSLock() + private var opened = false + private var errorMessage: String? + + func complete(application: NSRunningApplication?, error: Error?) { + lock.lock() + opened = application != nil && error == nil + errorMessage = error?.localizedDescription + lock.unlock() + } + + func result() -> (opened: Bool, errorMessage: String?) { + lock.lock() + defer { lock.unlock() } + return (opened, errorMessage) + } +} + +private func openPathInPreferredVSCode(_ path: String) -> (opened: Bool, errorMessage: String?) { + guard !path.isEmpty else { + return (false, "No folder path was available to open.") + } + + let folderURL = URL(fileURLWithPath: path, isDirectory: true) + let workspace = NSWorkspace.shared + + for bundleIdentifier in OICPreferredVSCodeBundleIdentifiers() { + guard let applicationURL = workspace.urlForApplication(withBundleIdentifier: bundleIdentifier) else { + continue + } + + let configuration = NSWorkspace.OpenConfiguration() + configuration.activates = true + configuration.promptsUserIfNeeded = true + configuration.allowsRunningApplicationSubstitution = false + + let completion = DispatchSemaphore(value: 0) + let attempt = OICOpenAttempt() + + workspace.open( + [folderURL], + withApplicationAt: applicationURL, + configuration: configuration + ) { application, error in + attempt.complete(application: application, error: error) + completion.signal() + } + + completion.wait() + + let result = attempt.result() + if result.opened { + return (true, nil) + } + + return ( + false, + result.errorMessage?.isEmpty == false + ? result.errorMessage + : "Visual Studio Code could not open the selected folder." + ) + } + + return (false, "Install Visual Studio Code or Visual Studio Code Insiders, then try again.") +} + +@MainActor +private func run() -> Int32 { + let finderResult = pathToFrontFinderLocation() + guard let path = finderResult.path else { + showErrorAlert( + message: "Couldn’t read the Finder location", + informativeText: finderResult.errorMessage ?? "Open a Finder window and try again." + ) + return 1 + } + + let launchResult = openPathInPreferredVSCode(path) + guard launchResult.opened else { + showErrorAlert( + message: "Couldn’t open Visual Studio Code", + informativeText: launchResult.errorMessage ?? "The application could not be launched." + ) + return 2 + } + + return 0 +} + +exit(run()) diff --git a/scripts/test.sh b/scripts/test.sh index 37d3e1c..3c42be0 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -5,15 +5,16 @@ repo_root="$(cd "$(dirname "$0")/.." && pwd)" build_dir="$(mktemp -d "${TMPDIR:-/tmp}/OpenInCodeTests.XXXXXX")" trap 'rm -rf "$build_dir"' EXIT -xcrun clang \ - -fno-objc-arc \ - -Wall \ - -Wextra \ - -Werror \ - -framework Foundation \ - -I"$repo_root" \ - "$repo_root/OpenInCodeCore.m" \ - "$repo_root/Tests/OpenInCodeCoreTests.m" \ +sdk_path="$(xcrun --sdk macosx --show-sdk-path)" +architecture="$(uname -m)" + +xcrun swiftc \ + -swift-version 6 \ + -warnings-as-errors \ + -target "${architecture}-apple-macos12.0" \ + -sdk "$sdk_path" \ + "$repo_root/OpenInCodeCore.swift" \ + "$repo_root/Tests/OpenInCodeCoreTests.swift" \ -o "$build_dir/OpenInCodeCoreTests" cask_path="$build_dir/open-in-code.rb" From 5950a9fe00397385661e473e897f20afaef6f6bc Mon Sep 17 00:00:00 2001 From: Sertac Ozercan Date: Wed, 15 Jul 2026 22:34:01 -0700 Subject: [PATCH 2/4] build: migrate project to SwiftPM Replace the Xcode project with a SwiftPM executable and XCTest target. Add a universal app packaging script for Icon Composer assets, bundle metadata, signing, and release workflow integration. Signed-off-by: Sertac Ozercan --- .github/workflows/release.yml | 29 +- .gitignore | 1 + AGENTS.md | 30 +- Info.plist | 4 +- Open in Code.xcodeproj/project.pbxproj | 325 ------------------ .../contents.xcworkspacedata | 7 - .../xcschemes/Open in Code.xcscheme | 80 ----- Package.swift | 32 ++ README.md | 28 +- .../OpenInCode/OpenInCodeCore.swift | 0 main.swift => Sources/OpenInCode/main.swift | 0 Tests/OpenInCodeCoreTests.swift | 151 -------- .../OpenInCodeTests/OpenInCodeCoreTests.swift | 108 ++++++ scripts/build-app.sh | 165 +++++++++ scripts/test.sh | 17 +- 15 files changed, 350 insertions(+), 627 deletions(-) delete mode 100755 Open in Code.xcodeproj/project.pbxproj delete mode 100755 Open in Code.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 Open in Code.xcodeproj/xcshareddata/xcschemes/Open in Code.xcscheme create mode 100644 Package.swift rename OpenInCodeCore.swift => Sources/OpenInCode/OpenInCodeCore.swift (100%) rename main.swift => Sources/OpenInCode/main.swift (100%) delete mode 100644 Tests/OpenInCodeCoreTests.swift create mode 100644 Tests/OpenInCodeTests/OpenInCodeCoreTests.swift create mode 100755 scripts/build-app.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9cb6d16..8e66862 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -177,33 +177,26 @@ jobs: - name: Build and sign release app id: app env: - VERSION: ${{ steps.version.outputs.version }} - BUNDLE_VERSION: ${{ steps.version.outputs.bundle_version }} + MARKETING_VERSION: ${{ steps.version.outputs.bundle_version }} + BUILD_NUMBER: ${{ github.run_number }} + ARCHES: "arm64 x86_64" + OPEN_IN_CODE_SIGNING: unsigned SIGNING_IDENTITY: ${{ steps.signing.outputs.identity }} SIGNING_MODE: ${{ steps.signing.outputs.mode }} run: | set -euo pipefail - DERIVED_DATA="$RUNNER_TEMP/OpenInCodeDerivedData" - xcodebuild \ - -project "Open in Code.xcodeproj" \ - -scheme "Open in Code" \ - -configuration Release \ - -destination "generic/platform=macOS" \ - -derivedDataPath "$DERIVED_DATA" \ - ARCHS="arm64 x86_64" \ - ONLY_ACTIVE_ARCH=NO \ - CODE_SIGNING_ALLOWED=NO \ - CODE_SIGNING_REQUIRED=NO \ - clean build - - APP_PATH="$DERIVED_DATA/Build/Products/Release/Open in Code.app" + ./scripts/build-app.sh release + + APP_PATH="$PWD/.build/app/Open in Code.app" PLIST_PATH="$APP_PATH/Contents/Info.plist" EXECUTABLE_PATH="$APP_PATH/Contents/MacOS/Open in Code" test -d "$APP_PATH" - /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $BUNDLE_VERSION" "$PLIST_PATH" - /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $GITHUB_RUN_NUMBER" "$PLIST_PATH" + test -f "$APP_PATH/Contents/Resources/Assets.car" + test -f "$APP_PATH/Contents/Resources/vscode.icns" + test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$PLIST_PATH")" = "$MARKETING_VERSION" + test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$PLIST_PATH")" = "$BUILD_NUMBER" ARCHITECTURES=$(lipo -archs "$EXECUTABLE_PATH") echo "Architectures: $ARCHITECTURES" diff --git a/.gitignore b/.gitignore index 6cf1eee..3454df8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ ## Build generated build/ +.build/ DerivedData/ ## Various settings diff --git a/AGENTS.md b/AGENTS.md index 022db7f..8ef41fc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,28 +5,29 @@ OpenInCode is a small macOS Finder toolbar utility written in Swift. It reads the selected Finder item, or the front Finder window when nothing is selected, and opens the corresponding folder in Visual Studio Code. - Deployment target: macOS 12 or newer. -- Build system: `Open in Code.xcodeproj`; Xcode 26 or newer is required for the current Icon Composer asset. +- Build system: Swift Package Manager via `Package.swift`; Xcode 26 or newer is required only for `actool` to compile the current Icon Composer asset when packaging the app. - Swift language mode: Swift 6. -- Dependencies: Apple frameworks only; there is no package manager or dependency-install step. +- Dependencies: Apple frameworks only; SwiftPM resolves no external packages. ## Repository map -- `main.swift`: Finder automation, user-facing errors, application lookup, and launch flow. -- `OpenInCodeCore.swift`: testable editor-priority and Finder-path logic. -- `Tests/OpenInCodeCoreTests.swift`: standalone Foundation test executable used by the test script. -- `scripts/test.sh`: canonical focused test command; also validates Homebrew cask rendering. +- `Package.swift`: SwiftPM executable and test target definitions. +- `Sources/OpenInCode/main.swift`: Finder automation, user-facing errors, application lookup, and launch flow. +- `Sources/OpenInCode/OpenInCodeCore.swift`: testable editor-priority and Finder-path logic. +- `Tests/OpenInCodeTests/OpenInCodeCoreTests.swift`: XCTest coverage for editor priority and Finder paths. +- `scripts/test.sh`: canonical focused test command; runs `swift test` and validates Homebrew cask rendering. +- `scripts/build-app.sh`: builds thin SwiftPM executables, creates a universal binary when requested, compiles the icon, assembles the app bundle, and optionally ad-hoc signs it. - `scripts/render-homebrew-cask.sh`: release cask template generator. - `Info.plist` and `Open in Code.entitlements`: app metadata and Finder Apple Events permission. -- `Open in Code.xcodeproj`: target membership, Swift build settings, and signing. - `.github/workflows/release.yml`: tag-driven universal build, signing, notarization, GitHub release, and Homebrew cask update. ## Working rules 1. Check `git status --short` before editing and preserve unrelated user changes. -2. Inspect only the files relevant to the task; do not search generated `build/` or `DerivedData/` content. +2. Inspect only the files relevant to the task; do not search generated `.build/`, `build/`, or `DerivedData/` content. 3. Keep changes focused. Avoid adding dependencies, new abstractions, or project files unless the task requires them. -4. Put deterministic, UI-independent behavior in `OpenInCodeCore.swift` and cover it in `Tests/OpenInCodeCoreTests.swift`. -5. When adding or removing source or resource files, keep the Xcode project references and target membership in sync. +4. Put deterministic, UI-independent behavior in `Sources/OpenInCode/OpenInCodeCore.swift` and cover it in `Tests/OpenInCodeTests/OpenInCodeCoreTests.swift`. +5. Keep SwiftPM target definitions, source layout, and packaging inputs in sync when adding or removing source or resource files. ## Behavioral invariants @@ -57,15 +58,10 @@ Run the smallest relevant checks: ./scripts/test.sh ``` -- Application code, plist, entitlements, icon, or Xcode project changes: run the focused tests, then an unsigned Release build: +- Application code, package manifest, plist, entitlements, icon, or packaging changes: run the focused tests, then assemble an unsigned Release app: ```sh - xcodebuild \ - -project "Open in Code.xcodeproj" \ - -scheme "Open in Code" \ - -configuration Release \ - CODE_SIGNING_ALLOWED=NO \ - clean build + OPEN_IN_CODE_SIGNING=unsigned ./scripts/build-app.sh release ``` Do not create tags, publish releases, notarize artifacts, update the Homebrew tap, or alter signing identities, team IDs, bundle identifiers, or release secrets unless explicitly requested. diff --git a/Info.plist b/Info.plist index 1ad0840..b941174 100755 --- a/Info.plist +++ b/Info.plist @@ -9,7 +9,7 @@ CFBundleIconFile vscode CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) + com.sertacozercan.openincode CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -32,5 +32,7 @@ Open in Code uses Finder to determine which folder or selected item to open in Visual Studio Code. NSPrincipalClass NSApplication + CFBundleIconName + vscode diff --git a/Open in Code.xcodeproj/project.pbxproj b/Open in Code.xcodeproj/project.pbxproj deleted file mode 100755 index 692d98d..0000000 --- a/Open in Code.xcodeproj/project.pbxproj +++ /dev/null @@ -1,325 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; - 8D11072D0486CEB800E47090 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.swift */; }; - 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; - C89E34F12FBA29F900F208FE /* vscode.icon in Resources */ = {isa = PBXBuildFile; fileRef = C89E34F02FBA29F900F208FE /* vscode.icon */; }; - C89E34F62FBA29F900F208FE /* OpenInCodeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89E34F32FBA29F900F208FE /* OpenInCodeCore.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 694495D00B869BB500A19631 /* CopyFiles */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 7; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 089C165DFE840E0CC02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = ""; }; - 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; - 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; - 29B97316FDCFA39411CA2CEA /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; - 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; - 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; - 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; - 8D1107320486CEB800E47090 /* Open in Code.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Open in Code.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - C89E34F02FBA29F900F208FE /* vscode.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = vscode.icon; sourceTree = ""; }; - C89E34F32FBA29F900F208FE /* OpenInCodeCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInCodeCore.swift; sourceTree = ""; }; - C89E34F52FBA29F900F208FE /* Open in Code.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Open in Code.entitlements"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 8D11072E0486CEB800E47090 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = { - isa = PBXGroup; - children = ( - 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */, - ); - name = "Linked Frameworks"; - sourceTree = ""; - }; - 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */ = { - isa = PBXGroup; - children = ( - 29B97324FDCFA39411CA2CEA /* AppKit.framework */, - 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */, - 29B97325FDCFA39411CA2CEA /* Foundation.framework */, - ); - name = "Other Frameworks"; - sourceTree = ""; - }; - 19C28FACFE9D520D11CA2CBB /* Products */ = { - isa = PBXGroup; - children = ( - 8D1107320486CEB800E47090 /* Open in Code.app */, - ); - name = Products; - sourceTree = ""; - }; - 29B97314FDCFA39411CA2CEA /* Open In Code */ = { - isa = PBXGroup; - children = ( - 29B97315FDCFA39411CA2CEA /* Other Sources */, - 29B97317FDCFA39411CA2CEA /* Resources */, - 29B97323FDCFA39411CA2CEA /* Frameworks */, - 19C28FACFE9D520D11CA2CBB /* Products */, - ); - name = "Open In Code"; - sourceTree = ""; - }; - 29B97315FDCFA39411CA2CEA /* Other Sources */ = { - isa = PBXGroup; - children = ( - 29B97316FDCFA39411CA2CEA /* main.swift */, - C89E34F32FBA29F900F208FE /* OpenInCodeCore.swift */, - C89E34F52FBA29F900F208FE /* Open in Code.entitlements */, - ); - name = "Other Sources"; - sourceTree = ""; - }; - 29B97317FDCFA39411CA2CEA /* Resources */ = { - isa = PBXGroup; - children = ( - 8D1107310486CEB800E47090 /* Info.plist */, - 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, - C89E34F02FBA29F900F208FE /* vscode.icon */, - ); - name = Resources; - sourceTree = ""; - }; - 29B97323FDCFA39411CA2CEA /* Frameworks */ = { - isa = PBXGroup; - children = ( - 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */, - 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 8D1107260486CEB800E47090 /* Open in Code */ = { - isa = PBXNativeTarget; - buildConfigurationList = C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "Open in Code" */; - buildPhases = ( - 8D1107290486CEB800E47090 /* Resources */, - 8D11072C0486CEB800E47090 /* Sources */, - 8D11072E0486CEB800E47090 /* Frameworks */, - 694495D00B869BB500A19631 /* CopyFiles */, - ); - dependencies = ( - ); - name = "Open in Code"; - productInstallPath = /Applications; - productName = "Open In Code"; - productReference = 8D1107320486CEB800E47090 /* Open in Code.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 29B97313FDCFA39411CA2CEA /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 2700; - LastUpgradeCheck = 2700; - }; - buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "Open in Code" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; - hasScannedForEncodings = 1; - knownRegions = ( - English, - en, - ); - mainGroup = 29B97314FDCFA39411CA2CEA /* Open In Code */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 8D1107260486CEB800E47090 /* Open in Code */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 8D1107290486CEB800E47090 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */, - C89E34F12FBA29F900F208FE /* vscode.icon in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 8D11072C0486CEB800E47090 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 8D11072D0486CEB800E47090 /* main.swift in Sources */, - C89E34F62FBA29F900F208FE /* OpenInCodeCore.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - 089C165DFE840E0CC02AAC07 /* English */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - C01FCF4B08A954540054247B /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = vscode; - CODE_SIGN_ENTITLEMENTS = "Open in Code.entitlements"; - CODE_SIGN_IDENTITY = "-"; - CODE_SIGN_STYLE = Manual; - COMBINE_HIDPI_IMAGES = YES; - COPY_PHASE_STRIP = NO; - "COPY_PHASE_STRIP[sdk=*]" = NO; - ENABLE_HARDENED_RUNTIME = YES; - GCC_DYNAMIC_NO_PIC = NO; - GCC_OPTIMIZATION_LEVEL = 0; - INFOPLIST_FILE = Info.plist; - INSTALL_PATH = /Applications; - MACOSX_DEPLOYMENT_TARGET = 12.0; - PRODUCT_BUNDLE_IDENTIFIER = com.sertacozercan.openincode; - PRODUCT_NAME = "Open in Code"; - SWIFT_VERSION = 6.0; - WRAPPER_EXTENSION = app; - }; - name = Debug; - }; - C01FCF4C08A954540054247B /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = vscode; - CODE_SIGN_ENTITLEMENTS = "Open in Code.entitlements"; - CODE_SIGN_IDENTITY = "Developer ID Application"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; - CODE_SIGN_STYLE = Manual; - COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = VURRGRYW45; - "DEVELOPMENT_TEAM[sdk=macosx*]" = 57QNR9B89Q; - ENABLE_HARDENED_RUNTIME = YES; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; - INFOPLIST_FILE = Info.plist; - INSTALL_PATH = /Applications; - MACOSX_DEPLOYMENT_TARGET = 12.0; - PRODUCT_BUNDLE_IDENTIFIER = com.sertacozercan.openincode; - PRODUCT_NAME = "Open in Code"; - SWIFT_VERSION = 6.0; - WRAPPER_EXTENSION = app; - }; - name = Release; - }; - C01FCF4F08A954540054247B /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - INSTALL_PATH = ""; - MACOSX_DEPLOYMENT_TARGET = 12.0; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - }; - name = Debug; - }; - C01FCF5008A954540054247B /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - INSTALL_PATH = ""; - MACOSX_DEPLOYMENT_TARGET = 12.0; - SDKROOT = macosx; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "Open in Code" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - C01FCF4B08A954540054247B /* Debug */, - C01FCF4C08A954540054247B /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - C01FCF4E08A954540054247B /* Build configuration list for PBXProject "Open in Code" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - C01FCF4F08A954540054247B /* Debug */, - C01FCF5008A954540054247B /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 29B97313FDCFA39411CA2CEA /* Project object */; -} diff --git a/Open in Code.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Open in Code.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100755 index 919434a..0000000 --- a/Open in Code.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/Open in Code.xcodeproj/xcshareddata/xcschemes/Open in Code.xcscheme b/Open in Code.xcodeproj/xcshareddata/xcschemes/Open in Code.xcscheme deleted file mode 100644 index cfed17d..0000000 --- a/Open in Code.xcodeproj/xcshareddata/xcschemes/Open in Code.xcscheme +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..f54fe6e --- /dev/null +++ b/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version: 6.2 + +import PackageDescription + +let package = Package( + name: "OpenInCode", + platforms: [ + .macOS(.v12), + ], + products: [ + .executable( + name: "OpenInCode", + targets: ["OpenInCode"] + ), + ], + targets: [ + .executableTarget( + name: "OpenInCode", + swiftSettings: [ + .swiftLanguageMode(.v6), + ] + ), + .testTarget( + name: "OpenInCodeTests", + dependencies: ["OpenInCode"], + swiftSettings: [ + .swiftLanguageMode(.v6), + ] + ), + ], + swiftLanguageModes: [.v6] +) diff --git a/README.md b/README.md index c759834..ed78288 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ OpenInCode prefers stable Visual Studio Code and falls back to Visual Studio Cod - macOS 12 or newer - Visual Studio Code or Visual Studio Code Insiders -- Xcode 26 or newer to build the current Icon Composer asset +- Swift 6.2 or newer +- Xcode 26 or newer for the `actool` step that compiles the current Icon Composer asset ## Install @@ -23,38 +24,35 @@ Release archives are also available from the [GitHub Releases](https://github.co To build from source: 1. Clone this repository. -2. Open `Open in Code.xcodeproj` in Xcode. -3. Build the Debug configuration for local use, or select a Developer ID Application certificate before archiving Release. -4. Copy `Open in Code.app` to `/Applications`. -5. Hold Command and drag the app from `/Applications` to a Finder toolbar. -6. Click the toolbar icon while viewing a folder or selecting a file. +2. Run `./scripts/build-app.sh release`. The script uses SwiftPM and creates `.build/app/Open in Code.app`. +3. Copy `Open in Code.app` to `/Applications`. +4. Hold Command and drag the app from `/Applications` to a Finder toolbar. +5. Click the toolbar icon while viewing a folder or selecting a file. The first use asks for permission to control Finder. If permission was denied, enable **Open in Code → Finder** here: - macOS 13 or newer: **System Settings → Privacy & Security → Automation** - macOS 12: **System Preferences → Security & Privacy → Privacy → Automation** -Release archives are configured for hardened runtime and the project publisher team. Fork maintainers should override `DEVELOPMENT_TEAM` with their own team and provide a Developer ID Application certificate. Notarize public release artifacts before distribution. +The local packaging script applies an ad-hoc signature by default so Finder automation entitlements are available during development. Public release artifacts should be signed with a Developer ID Application certificate and notarized before distribution. ## Development -Run the focused path and editor-selection tests: +Build the SwiftPM executable and run the focused path and editor-selection tests: ```sh +swift build ./scripts/test.sh ``` -Run a clean unsigned compiler verification: +Assemble an unsigned Release app bundle for verification: ```sh -xcodebuild \ - -project "Open in Code.xcodeproj" \ - -scheme "Open in Code" \ - -configuration Release \ - CODE_SIGNING_ALLOWED=NO \ - clean build +OPEN_IN_CODE_SIGNING=unsigned ./scripts/build-app.sh release ``` +Set `ARCHES="arm64 x86_64"` when a universal app is required. + ## Publishing a release Pushing a `v*` tag runs `.github/workflows/release.yml`. The workflow builds a universal app, signs it, creates a GitHub release, and updates `Casks/open-in-code.rb` in [`sozercan/homebrew-repo`](https://github.com/sozercan/homebrew-repo). diff --git a/OpenInCodeCore.swift b/Sources/OpenInCode/OpenInCodeCore.swift similarity index 100% rename from OpenInCodeCore.swift rename to Sources/OpenInCode/OpenInCodeCore.swift diff --git a/main.swift b/Sources/OpenInCode/main.swift similarity index 100% rename from main.swift rename to Sources/OpenInCode/main.swift diff --git a/Tests/OpenInCodeCoreTests.swift b/Tests/OpenInCodeCoreTests.swift deleted file mode 100644 index 0f288a1..0000000 --- a/Tests/OpenInCodeCoreTests.swift +++ /dev/null @@ -1,151 +0,0 @@ -import Darwin -import Foundation - -private func fail(_ message: String) -> Never { - let output = "FAIL: \(message)\n" - FileHandle.standardError.write(Data(output.utf8)) - exit(1) -} - -private func assertTrue(_ condition: @autoclosure () -> Bool, _ message: String) { - if !condition() { - fail(message) - } -} - -@main -private struct OpenInCodeCoreTests { - static func main() { - let bundleIdentifiers = OICPreferredVSCodeBundleIdentifiers() - assertTrue(bundleIdentifiers.count == 2, "expected two VS Code channels") - assertTrue(bundleIdentifiers[0] == OICVSCodeBundleIdentifier, "stable VS Code must be preferred") - assertTrue(bundleIdentifiers[1] == OICVSCodeInsidersBundleIdentifier, "Insiders must be the fallback") - - assertTrue(OICPathForFinderURL(nil) == nil, "nil URL must return nil") - assertTrue(OICPathForFinderURL(URL(string: "https://example.com")) == nil, "non-file URL must return nil") - - let temporaryRoot = (NSTemporaryDirectory() as NSString) - .appendingPathComponent(UUID().uuidString) - let folderPath = (temporaryRoot as NSString).appendingPathComponent("Project") - let filePath = (folderPath as NSString).appendingPathComponent("README.md") - let packagePath = (temporaryRoot as NSString).appendingPathComponent("Example.app") - let directorySymlinkPath = (temporaryRoot as NSString).appendingPathComponent("LinkedProject") - let folderAliasURL = URL( - fileURLWithPath: (temporaryRoot as NSString).appendingPathComponent("Project Folder.alias") - ) - let fileAliasURL = URL( - fileURLWithPath: (temporaryRoot as NSString).appendingPathComponent("Project File.alias") - ) - let deletedTargetPath = (temporaryRoot as NSString).appendingPathComponent("Deleted Target.txt") - let brokenAliasURL = URL( - fileURLWithPath: (temporaryRoot as NSString).appendingPathComponent("Broken Target.alias") - ) - - let fileManager = FileManager.default - do { - try fileManager.createDirectory( - atPath: folderPath, - withIntermediateDirectories: true, - attributes: nil - ) - try "test".write(toFile: filePath, atomically: true, encoding: .utf8) - try "delete me".write(toFile: deletedTargetPath, atomically: true, encoding: .utf8) - try fileManager.createDirectory( - atPath: packagePath, - withIntermediateDirectories: true, - attributes: nil - ) - try fileManager.createSymbolicLink( - atPath: directorySymlinkPath, - withDestinationPath: folderPath - ) - } catch { - fail("create test files: \(error)") - } - - let folderBookmark: Data - let fileBookmark: Data - let deletedTargetBookmark: Data - do { - folderBookmark = try URL(fileURLWithPath: folderPath, isDirectory: true).bookmarkData( - options: .suitableForBookmarkFile, - includingResourceValuesForKeys: nil, - relativeTo: nil - ) - fileBookmark = try URL(fileURLWithPath: filePath).bookmarkData( - options: .suitableForBookmarkFile, - includingResourceValuesForKeys: nil, - relativeTo: nil - ) - deletedTargetBookmark = try URL(fileURLWithPath: deletedTargetPath).bookmarkData( - options: .suitableForBookmarkFile, - includingResourceValuesForKeys: nil, - relativeTo: nil - ) - try URL.writeBookmarkData(folderBookmark, to: folderAliasURL) - try URL.writeBookmarkData(fileBookmark, to: fileAliasURL) - try URL.writeBookmarkData(deletedTargetBookmark, to: brokenAliasURL) - try fileManager.removeItem(atPath: deletedTargetPath) - } catch { - fail("create test aliases: \(error)") - } - - let resolvedFolderAliasURL: URL - let resolvedFileAliasURL: URL - do { - resolvedFolderAliasURL = try URL( - resolvingAliasFileAt: folderAliasURL, - options: .withoutUI - ) - resolvedFileAliasURL = try URL( - resolvingAliasFileAt: fileAliasURL, - options: .withoutUI - ) - } catch { - fail("resolve test aliases: \(error)") - } - - let resolvedFolderAliasPath = OICFileSystemPathForURL(resolvedFolderAliasURL) - let resolvedFileAliasParentPath = (OICFileSystemPathForURL(resolvedFileAliasURL) as NSString).deletingLastPathComponent - - assertTrue( - OICPathForFinderURL(URL(fileURLWithPath: folderPath)) == folderPath, - "folder should open itself" - ) - assertTrue( - OICPathForFinderURL(URL(fileURLWithPath: filePath)) == folderPath, - "file should open its parent" - ) - assertTrue( - OICPathForFinderURL(URL(fileURLWithPath: packagePath)) == temporaryRoot, - "Finder package should open its parent" - ) - assertTrue( - OICPathForFinderURL(URL(fileURLWithPath: directorySymlinkPath)) == directorySymlinkPath, - "directory symlink should open the linked directory" - ) - assertTrue( - OICPathForFinderURL(folderAliasURL) == resolvedFolderAliasPath, - "folder alias should resolve to its target" - ) - assertTrue( - OICPathForFinderURL(fileAliasURL) == resolvedFileAliasParentPath, - "file alias should resolve to its target parent" - ) - assertTrue(OICPathForFinderURL(brokenAliasURL) == nil, "broken alias must return nil") - assertTrue( - OICPathForFinderURL( - URL(fileURLWithPath: (temporaryRoot as NSString).appendingPathComponent("missing")) - ) == nil, - "missing item must return nil" - ) - - do { - try fileManager.removeItem(atPath: temporaryRoot) - } catch { - fail("remove test files: \(error)") - } - - print("OpenInCodeCoreTests: PASS") - } -} diff --git a/Tests/OpenInCodeTests/OpenInCodeCoreTests.swift b/Tests/OpenInCodeTests/OpenInCodeCoreTests.swift new file mode 100644 index 0000000..edbc966 --- /dev/null +++ b/Tests/OpenInCodeTests/OpenInCodeCoreTests.swift @@ -0,0 +1,108 @@ +import Foundation +import XCTest +@testable import OpenInCode + +final class OpenInCodeCoreTests: XCTestCase { + func testPreferredVSCodeBundleIdentifiers() { + let bundleIdentifiers = OICPreferredVSCodeBundleIdentifiers() + + XCTAssertEqual(bundleIdentifiers.count, 2) + XCTAssertEqual(bundleIdentifiers[0], OICVSCodeBundleIdentifier) + XCTAssertEqual(bundleIdentifiers[1], OICVSCodeInsidersBundleIdentifier) + } + + func testFinderPathRejectsUnavailableURLs() { + XCTAssertNil(OICPathForFinderURL(nil)) + XCTAssertNil(OICPathForFinderURL(URL(string: "https://example.com"))) + } + + func testFinderPathResolution() throws { + let temporaryRoot = (NSTemporaryDirectory() as NSString) + .appendingPathComponent(UUID().uuidString) + let folderPath = (temporaryRoot as NSString).appendingPathComponent("Project") + let filePath = (folderPath as NSString).appendingPathComponent("README.md") + let packagePath = (temporaryRoot as NSString).appendingPathComponent("Example.app") + let directorySymlinkPath = (temporaryRoot as NSString).appendingPathComponent("LinkedProject") + let folderAliasURL = URL( + fileURLWithPath: (temporaryRoot as NSString).appendingPathComponent("Project Folder.alias") + ) + let fileAliasURL = URL( + fileURLWithPath: (temporaryRoot as NSString).appendingPathComponent("Project File.alias") + ) + let deletedTargetPath = (temporaryRoot as NSString).appendingPathComponent("Deleted Target.txt") + let brokenAliasURL = URL( + fileURLWithPath: (temporaryRoot as NSString).appendingPathComponent("Broken Target.alias") + ) + + let fileManager = FileManager.default + defer { + try? fileManager.removeItem(atPath: temporaryRoot) + } + + try fileManager.createDirectory( + atPath: folderPath, + withIntermediateDirectories: true, + attributes: nil + ) + try "test".write(toFile: filePath, atomically: true, encoding: .utf8) + try "delete me".write(toFile: deletedTargetPath, atomically: true, encoding: .utf8) + try fileManager.createDirectory( + atPath: packagePath, + withIntermediateDirectories: true, + attributes: nil + ) + try fileManager.createSymbolicLink( + atPath: directorySymlinkPath, + withDestinationPath: folderPath + ) + + let folderBookmark = try URL(fileURLWithPath: folderPath, isDirectory: true).bookmarkData( + options: .suitableForBookmarkFile, + includingResourceValuesForKeys: nil, + relativeTo: nil + ) + let fileBookmark = try URL(fileURLWithPath: filePath).bookmarkData( + options: .suitableForBookmarkFile, + includingResourceValuesForKeys: nil, + relativeTo: nil + ) + let deletedTargetBookmark = try URL(fileURLWithPath: deletedTargetPath).bookmarkData( + options: .suitableForBookmarkFile, + includingResourceValuesForKeys: nil, + relativeTo: nil + ) + try URL.writeBookmarkData(folderBookmark, to: folderAliasURL) + try URL.writeBookmarkData(fileBookmark, to: fileAliasURL) + try URL.writeBookmarkData(deletedTargetBookmark, to: brokenAliasURL) + try fileManager.removeItem(atPath: deletedTargetPath) + + let resolvedFolderAliasURL = try URL( + resolvingAliasFileAt: folderAliasURL, + options: .withoutUI + ) + let resolvedFileAliasURL = try URL( + resolvingAliasFileAt: fileAliasURL, + options: .withoutUI + ) + let resolvedFolderAliasPath = OICFileSystemPathForURL(resolvedFolderAliasURL) + let resolvedFileAliasParentPath = ( + OICFileSystemPathForURL(resolvedFileAliasURL) as NSString + ).deletingLastPathComponent + + XCTAssertEqual(OICPathForFinderURL(URL(fileURLWithPath: folderPath)), folderPath) + XCTAssertEqual(OICPathForFinderURL(URL(fileURLWithPath: filePath)), folderPath) + XCTAssertEqual(OICPathForFinderURL(URL(fileURLWithPath: packagePath)), temporaryRoot) + XCTAssertEqual( + OICPathForFinderURL(URL(fileURLWithPath: directorySymlinkPath)), + directorySymlinkPath + ) + XCTAssertEqual(OICPathForFinderURL(folderAliasURL), resolvedFolderAliasPath) + XCTAssertEqual(OICPathForFinderURL(fileAliasURL), resolvedFileAliasParentPath) + XCTAssertNil(OICPathForFinderURL(brokenAliasURL)) + XCTAssertNil( + OICPathForFinderURL( + URL(fileURLWithPath: (temporaryRoot as NSString).appendingPathComponent("missing")) + ) + ) + } +} diff --git a/scripts/build-app.sh b/scripts/build-app.sh new file mode 100755 index 0000000..73fdcb0 --- /dev/null +++ b/scripts/build-app.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +configuration="${1:-release}" +case "$configuration" in + debug|release) ;; + *) + echo "Usage: $0 [debug|release]" >&2 + exit 2 + ;; +esac + +app_name="Open in Code" +product_name="OpenInCode" +bundle_id="com.sertacozercan.openincode" +build_root="$repo_root/.build/app" +app_bundle="$build_root/$app_name.app" +signing_mode="${OPEN_IN_CODE_SIGNING:-adhoc}" +marketing_version="${MARKETING_VERSION:-$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$repo_root/Info.plist")}" +build_number="${BUILD_NUMBER:-$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$repo_root/Info.plist")}" + +architectures=() +if [[ -n "${ARCHES:-}" ]]; then + IFS=' ' read -r -a architectures <<< "$ARCHES" +else + architectures=("$(uname -m)") +fi + +verify_architectures() { + local binary="$1" + local actual + actual="$(lipo -archs "$binary")" + + for architecture in "${architectures[@]}"; do + if [[ "$actual" != *"$architecture"* ]]; then + echo "ERROR: $binary is missing $architecture (contains: $actual)" >&2 + exit 1 + fi + done +} + +build_architecture() { + local architecture="$1" + local scratch_path="$build_root/swiftpm/$architecture" + local binary_directory + local source_binary + local staged_directory="$build_root/arch-products/$architecture" + + echo " → Building $product_name for $architecture" + swift build \ + --package-path "$repo_root" \ + --scratch-path "$scratch_path" \ + --configuration "$configuration" \ + --arch "$architecture" \ + --product "$product_name" + + binary_directory="$(swift build \ + --package-path "$repo_root" \ + --scratch-path "$scratch_path" \ + --configuration "$configuration" \ + --arch "$architecture" \ + --show-bin-path)" + source_binary="$binary_directory/$product_name" + + if [[ ! -f "$source_binary" ]]; then + echo "ERROR: SwiftPM did not produce $source_binary" >&2 + exit 1 + fi + + mkdir -p "$staged_directory" + cp "$source_binary" "$staged_directory/$product_name" + chmod +x "$staged_directory/$product_name" +} + +install_executable() { + local destination="$1" + local binaries=() + local architecture + + for architecture in "${architectures[@]}"; do + binaries+=("$build_root/arch-products/$architecture/$product_name") + done + + if [[ ${#binaries[@]} -eq 1 ]]; then + cp "${binaries[0]}" "$destination" + else + lipo -create "${binaries[@]}" -output "$destination" + fi + chmod +x "$destination" + verify_architectures "$destination" +} + +compile_app_icon() { + local resources_directory="$1" + local partial_info_plist="$build_root/AppIconPartialInfo.plist" + + xcrun actool "$repo_root/vscode.icon" \ + --compile "$resources_directory" \ + --notices --warnings --errors \ + --output-partial-info-plist "$partial_info_plist" \ + --app-icon vscode \ + --enable-on-demand-resources NO \ + --development-region English \ + --target-device mac \ + --minimum-deployment-target 12.0 \ + --platform macosx + + [[ -f "$resources_directory/Assets.car" ]] || { + echo "ERROR: actool did not produce Assets.car" >&2 + exit 1 + } + [[ -f "$resources_directory/vscode.icns" ]] || { + echo "ERROR: actool did not produce vscode.icns" >&2 + exit 1 + } +} + +echo "Building $app_name ($configuration) for ${architectures[*]}" +rm -rf "$build_root" +mkdir -p "$build_root" + +for architecture in "${architectures[@]}"; do + build_architecture "$architecture" +done + +mkdir -p \ + "$app_bundle/Contents/MacOS" \ + "$app_bundle/Contents/Resources/English.lproj" + +install_executable "$app_bundle/Contents/MacOS/$app_name" +compile_app_icon "$app_bundle/Contents/Resources" +cp "$repo_root/English.lproj/InfoPlist.strings" \ + "$app_bundle/Contents/Resources/English.lproj/InfoPlist.strings" +cp "$repo_root/Info.plist" "$app_bundle/Contents/Info.plist" +echo -n "APPL????" > "$app_bundle/Contents/PkgInfo" + +/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $bundle_id" "$app_bundle/Contents/Info.plist" +/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $marketing_version" "$app_bundle/Contents/Info.plist" +/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $build_number" "$app_bundle/Contents/Info.plist" +plutil -lint "$app_bundle/Contents/Info.plist" >/dev/null + +xattr -cr "$app_bundle" 2>/dev/null || true +find "$app_bundle" -name '._*' -delete 2>/dev/null || true + +case "$signing_mode" in + unsigned|none) + echo "Leaving app unsigned" + ;; + adhoc) + echo "Applying ad-hoc signature" + codesign --force --options runtime --timestamp=none \ + --entitlements "$repo_root/Open in Code.entitlements" \ + --sign - "$app_bundle" + codesign --verify --deep --strict --verbose=2 "$app_bundle" + ;; + *) + echo "ERROR: OPEN_IN_CODE_SIGNING must be 'adhoc' or 'unsigned'" >&2 + exit 2 + ;; +esac + +echo "Built $app_bundle" +echo "Architectures: $(lipo -archs "$app_bundle/Contents/MacOS/$app_name")" +echo "Version: $marketing_version ($build_number)" diff --git a/scripts/test.sh b/scripts/test.sh index 3c42be0..e888342 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -5,17 +5,10 @@ repo_root="$(cd "$(dirname "$0")/.." && pwd)" build_dir="$(mktemp -d "${TMPDIR:-/tmp}/OpenInCodeTests.XXXXXX")" trap 'rm -rf "$build_dir"' EXIT -sdk_path="$(xcrun --sdk macosx --show-sdk-path)" -architecture="$(uname -m)" - -xcrun swiftc \ - -swift-version 6 \ - -warnings-as-errors \ - -target "${architecture}-apple-macos12.0" \ - -sdk "$sdk_path" \ - "$repo_root/OpenInCodeCore.swift" \ - "$repo_root/Tests/OpenInCodeCoreTests.swift" \ - -o "$build_dir/OpenInCodeCoreTests" +swift test \ + --package-path "$repo_root" \ + --scratch-path "$build_dir/swiftpm" \ + -Xswiftc -warnings-as-errors cask_path="$build_dir/open-in-code.rb" "$repo_root/scripts/render-homebrew-cask.sh" \ @@ -25,5 +18,3 @@ cask_path="$build_dir/open-in-code.rb" ruby -c "$cask_path" >/dev/null grep -q 'version "1.2.3"' "$cask_path" grep -q 'app "Open in Code.app"' "$cask_path" - -"$build_dir/OpenInCodeCoreTests" From aea2847927b31a2e8d35df279ac2e5481aea849a Mon Sep 17 00:00:00 2001 From: Sertac Ozercan Date: Wed, 15 Jul 2026 22:52:20 -0700 Subject: [PATCH 3/4] fix: address Swift migration review feedback Resolve Finder file-reference URLs before path extraction, bridge NSWorkspace launching through async/await, and document every accepted packaging signing mode. Signed-off-by: Sertac Ozercan --- AGENTS.md | 2 +- ...main.swift => OpenInCodeApplication.swift} | 61 ++++++++----------- Sources/OpenInCode/OpenInCodeCore.swift | 19 +++++- scripts/build-app.sh | 2 +- 4 files changed, 43 insertions(+), 41 deletions(-) rename Sources/OpenInCode/{main.swift => OpenInCodeApplication.swift} (74%) diff --git a/AGENTS.md b/AGENTS.md index 8ef41fc..14f2567 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ OpenInCode is a small macOS Finder toolbar utility written in Swift. It reads th ## Repository map - `Package.swift`: SwiftPM executable and test target definitions. -- `Sources/OpenInCode/main.swift`: Finder automation, user-facing errors, application lookup, and launch flow. +- `Sources/OpenInCode/OpenInCodeApplication.swift`: Finder automation, user-facing errors, application lookup, and launch flow. - `Sources/OpenInCode/OpenInCodeCore.swift`: testable editor-priority and Finder-path logic. - `Tests/OpenInCodeTests/OpenInCodeCoreTests.swift`: XCTest coverage for editor priority and Finder paths. - `scripts/test.sh`: canonical focused test command; runs `swift test` and validates Homebrew cask rendering. diff --git a/Sources/OpenInCode/main.swift b/Sources/OpenInCode/OpenInCodeApplication.swift similarity index 74% rename from Sources/OpenInCode/main.swift rename to Sources/OpenInCode/OpenInCodeApplication.swift index c5fe688..a830bfb 100644 --- a/Sources/OpenInCode/main.swift +++ b/Sources/OpenInCode/OpenInCodeApplication.swift @@ -1,6 +1,5 @@ import AppKit import Darwin -import Dispatch import Foundation private let finderAutomationPermissionMessage = "Open the Automation privacy settings and allow Open in Code to control Finder, then try again." @@ -57,26 +56,8 @@ private func pathToFrontFinderLocation() -> (path: String?, errorMessage: String return (path, nil) } -private final class OICOpenAttempt: @unchecked Sendable { - private let lock = NSLock() - private var opened = false - private var errorMessage: String? - - func complete(application: NSRunningApplication?, error: Error?) { - lock.lock() - opened = application != nil && error == nil - errorMessage = error?.localizedDescription - lock.unlock() - } - - func result() -> (opened: Bool, errorMessage: String?) { - lock.lock() - defer { lock.unlock() } - return (opened, errorMessage) - } -} - -private func openPathInPreferredVSCode(_ path: String) -> (opened: Bool, errorMessage: String?) { +@MainActor +private func openPathInPreferredVSCode(_ path: String) async -> (opened: Bool, errorMessage: String?) { guard !path.isEmpty else { return (false, "No folder path was available to open.") } @@ -94,21 +75,21 @@ private func openPathInPreferredVSCode(_ path: String) -> (opened: Bool, errorMe configuration.promptsUserIfNeeded = true configuration.allowsRunningApplicationSubstitution = false - let completion = DispatchSemaphore(value: 0) - let attempt = OICOpenAttempt() - - workspace.open( - [folderURL], - withApplicationAt: applicationURL, - configuration: configuration - ) { application, error in - attempt.complete(application: application, error: error) - completion.signal() + let result: (opened: Bool, errorMessage: String?) = await withCheckedContinuation { continuation in + workspace.open( + [folderURL], + withApplicationAt: applicationURL, + configuration: configuration + ) { application, error in + continuation.resume( + returning: ( + application != nil && error == nil, + error?.localizedDescription + ) + ) + } } - completion.wait() - - let result = attempt.result() if result.opened { return (true, nil) } @@ -125,7 +106,7 @@ private func openPathInPreferredVSCode(_ path: String) -> (opened: Bool, errorMe } @MainActor -private func run() -> Int32 { +private func run() async -> Int32 { let finderResult = pathToFrontFinderLocation() guard let path = finderResult.path else { showErrorAlert( @@ -135,7 +116,7 @@ private func run() -> Int32 { return 1 } - let launchResult = openPathInPreferredVSCode(path) + let launchResult = await openPathInPreferredVSCode(path) guard launchResult.opened else { showErrorAlert( message: "Couldn’t open Visual Studio Code", @@ -147,4 +128,10 @@ private func run() -> Int32 { return 0 } -exit(run()) +@main +private enum OpenInCodeApplication { + @MainActor + static func main() async { + exit(await run()) + } +} diff --git a/Sources/OpenInCode/OpenInCodeCore.swift b/Sources/OpenInCode/OpenInCodeCore.swift index 980debb..94cf85e 100644 --- a/Sources/OpenInCode/OpenInCodeCore.swift +++ b/Sources/OpenInCode/OpenInCodeCore.swift @@ -7,6 +7,18 @@ func OICPreferredVSCodeBundleIdentifiers() -> [String] { [OICVSCodeBundleIdentifier, OICVSCodeInsidersBundleIdentifier] } +func OICFilePathURLForURL(_ url: URL) -> URL? { + guard url.isFileURL else { + return nil + } + + let foundationURL = url as NSURL + if foundationURL.isFileReferenceURL() { + return foundationURL.filePathURL + } + return url +} + func OICFileSystemPathForURL(_ url: URL) -> String { var path: String if #available(macOS 13.0, *) { @@ -22,7 +34,7 @@ func OICFileSystemPathForURL(_ url: URL) -> String { } func OICPathForFinderURL(_ finderURL: URL?) -> String? { - guard var url = finderURL, url.isFileURL else { + guard let finderURL, var url = OICFilePathURLForURL(finderURL) else { return nil } @@ -41,7 +53,10 @@ func OICPathForFinderURL(_ finderURL: URL?) -> String? { ) else { return nil } - url = resolvedURL + guard let filePathURL = OICFilePathURLForURL(resolvedURL) else { + return nil + } + url = filePathURL } var path = (OICFileSystemPathForURL(url) as NSString).expandingTildeInPath diff --git a/scripts/build-app.sh b/scripts/build-app.sh index 73fdcb0..38b52be 100755 --- a/scripts/build-app.sh +++ b/scripts/build-app.sh @@ -155,7 +155,7 @@ case "$signing_mode" in codesign --verify --deep --strict --verbose=2 "$app_bundle" ;; *) - echo "ERROR: OPEN_IN_CODE_SIGNING must be 'adhoc' or 'unsigned'" >&2 + echo "ERROR: OPEN_IN_CODE_SIGNING must be 'adhoc', 'unsigned', or 'none'" >&2 exit 2 ;; esac From 6e1a9311bd2d2f99bcc151bc78ee7b45b53f0d9c Mon Sep 17 00:00:00 2001 From: Sertac Ozercan Date: Wed, 15 Jul 2026 22:55:23 -0700 Subject: [PATCH 4/4] ci: validate SwiftPM app builds Run focused tests and assemble a signed universal app for pull requests and master pushes. Signed-off-by: Sertac Ozercan --- .github/workflows/ci.yml | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..af5e99f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI + +on: + pull_request: + push: + branches: + - master + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-and-package: + runs-on: macos-26 + timeout-minutes: 30 + + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer + + - name: Run focused tests + run: ./scripts/test.sh + + - name: Build universal app + env: + ARCHES: "arm64 x86_64" + run: ./scripts/build-app.sh release + + - name: Verify packaged app + run: | + set -euo pipefail + + APP_PATH="$PWD/.build/app/Open in Code.app" + EXECUTABLE_PATH="$APP_PATH/Contents/MacOS/Open in Code" + + test -d "$APP_PATH" + test -f "$APP_PATH/Contents/Resources/Assets.car" + test -f "$APP_PATH/Contents/Resources/vscode.icns" + plutil -lint "$APP_PATH/Contents/Info.plist" + + ARCHITECTURES=$(lipo -archs "$EXECUTABLE_PATH") + echo "Architectures: $ARCHITECTURES" + [[ "$ARCHITECTURES" == *arm64* ]] + [[ "$ARCHITECTURES" == *x86_64* ]] + + codesign --verify --deep --strict --verbose=2 "$APP_PATH" + ENTITLEMENTS=$(codesign -d --entitlements :- "$APP_PATH" 2>/dev/null) + grep -F 'com.apple.security.automation.apple-events' <<< "$ENTITLEMENTS" >/dev/null