diff --git a/Installer/AppDelegate.swift b/Installer/AppDelegate.swift index e18516fd..73d55031 100644 --- a/Installer/AppDelegate.swift +++ b/Installer/AppDelegate.swift @@ -12,41 +12,41 @@ import Cocoa import IMKUtils import InputMethodKit -private let kTargetBin = "vChewing" -private let kTargetType = "app" -private let kTargetBundle = "vChewing.app" -private let kTargetBundleWithComponents = "Library/Input%20Methods/vChewing.app" +public let kTargetBin = "vChewing" +public let kTargetType = "app" +public let kTargetBundle = "vChewing.app" +public let kTargetBundleWithComponents = "Library/Input%20Methods/vChewing.app" -private let realHomeDir = URL( +public let realHomeDir = URL( fileURLWithFileSystemRepresentation: getpwuid(getuid()).pointee.pw_dir, isDirectory: true, relativeTo: nil ) -private let urlDestinationPartial = realHomeDir.appendingPathComponent("Library/Input Methods") -private let urlTargetPartial = realHomeDir.appendingPathComponent(kTargetBundleWithComponents) -private let urlTargetFullBinPartial = urlTargetPartial.appendingPathComponent("Contents/MacOS") +public let urlDestinationPartial = realHomeDir.appendingPathComponent("Library/Input Methods") +public let urlTargetPartial = realHomeDir.appendingPathComponent(kTargetBundleWithComponents) +public let urlTargetFullBinPartial = urlTargetPartial.appendingPathComponent("Contents/MacOS") .appendingPathComponent(kTargetBin) -private let kDestinationPartial = urlDestinationPartial.path -private let kTargetPartialPath = urlTargetPartial.path -private let kTargetFullBinPartialPath = urlTargetFullBinPartial.path +public let kDestinationPartial = urlDestinationPartial.path +public let kTargetPartialPath = urlTargetPartial.path +public let kTargetFullBinPartialPath = urlTargetFullBinPartial.path -private let kTranslocationRemovalTickInterval: TimeInterval = 0.5 -private let kTranslocationRemovalDeadline: TimeInterval = 60.0 +public let kTranslocationRemovalTickInterval: TimeInterval = 0.5 +public let kTranslocationRemovalDeadline: TimeInterval = 60.0 @NSApplicationMain @objc(AppDelegate) class AppDelegate: NSWindowController, NSApplicationDelegate { - @IBOutlet private var installButton: NSButton! - @IBOutlet private var cancelButton: NSButton! - @IBOutlet private var progressSheet: NSWindow! - @IBOutlet private var progressIndicator: NSProgressIndicator! - @IBOutlet private var appVersionLabel: NSTextField! - @IBOutlet private var appCopyrightLabel: NSTextField! - @IBOutlet private var appEULAContent: NSTextView! + @IBOutlet var installButton: NSButton! + @IBOutlet var cancelButton: NSButton! + @IBOutlet var progressSheet: NSWindow! + @IBOutlet var progressIndicator: NSProgressIndicator! + @IBOutlet var appVersionLabel: NSTextField! + @IBOutlet var appCopyrightLabel: NSTextField! + @IBOutlet var appEULAContent: NSTextView! - private var installingVersion = "" - private var upgrading = false - private var translocationRemovalStartTime: Date? - private var currentVersionNumber: Int = 0 + var installingVersion = "" + var upgrading = false + var translocationRemovalStartTime: Date? + var currentVersionNumber: Int = 0 let imeURLInstalled = realHomeDir.appendingPathComponent("Library/Input Methods/vChewing.app") @@ -138,208 +138,13 @@ class AppDelegate: NSWindowController, NSApplicationDelegate { if elapsed >= kTranslocationRemovalDeadline { timer.invalidate() window.endSheet(progressSheet, returnCode: .cancel) - } else if isAppBundleTranslocated(atPath: kTargetPartialPath) == false { + } else if Reloc.isAppBundleTranslocated(atPath: kTargetPartialPath) == false { progressIndicator.doubleValue = 1.0 timer.invalidate() window.endSheet(progressSheet, returnCode: .continue) } } - func removeThenInstallInputMethod() { - // if !FileManager.default.fileExists(atPath: kTargetPartialPath) { - // installInputMethod( - // previousExists: false, previousVersionNotFullyDeactivatedWarning: false - // ) - // return - // } - - guard let window = window else { return } - - let shouldWaitForTranslocationRemoval = - isAppBundleTranslocated(atPath: kTargetPartialPath) - && window.responds(to: #selector(NSWindow.beginSheet(_:completionHandler:))) - - // 將既存輸入法扔到垃圾桶內 - do { - let sourceDir = kDestinationPartial - let fileManager = FileManager.default - let fileURLString = sourceDir + "/" + kTargetBundle - let fileURL = URL(fileURLWithPath: fileURLString) - - // 檢查檔案是否存在 - if fileManager.fileExists(atPath: fileURLString) { - // 塞入垃圾桶 - try fileManager.trashItem(at: fileURL, resultingItemURL: nil) - } else { - NSLog("File does not exist") - } - - } catch let error as NSError { - NSLog("An error took place: \(error)") - } - - let killTask = Process() - killTask.launchPath = "/usr/bin/killall" - killTask.arguments = ["-9", kTargetBin] - killTask.launch() - killTask.waitUntilExit() - - if shouldWaitForTranslocationRemoval { - progressIndicator.startAnimation(self) - window.beginSheet(progressSheet) { returnCode in - DispatchQueue.main.async { - if returnCode == .continue { - self.installInputMethod( - previousExists: true, - previousVersionNotFullyDeactivatedWarning: false - ) - } else { - self.installInputMethod( - previousExists: true, - previousVersionNotFullyDeactivatedWarning: true - ) - } - } - } - - translocationRemovalStartTime = Date() - Timer.scheduledTimer( - timeInterval: kTranslocationRemovalTickInterval, target: self, - selector: #selector(timerTick(_:)), userInfo: nil, repeats: true - ) - } else { - installInputMethod( - previousExists: false, previousVersionNotFullyDeactivatedWarning: false - ) - } - } - - func installInputMethod( - previousExists _: Bool, previousVersionNotFullyDeactivatedWarning warning: Bool - ) { - guard - let targetBundle = Bundle.main.path(forResource: kTargetBin, ofType: kTargetType) - else { - return - } - let cpTask = Process() - cpTask.launchPath = "/bin/cp" - print(kDestinationPartial) - cpTask.arguments = [ - "-R", targetBundle, kDestinationPartial, - ] - cpTask.launch() - cpTask.waitUntilExit() - - if cpTask.terminationStatus != 0 { - runAlertPanel( - title: NSLocalizedString("Install Failed", comment: ""), - message: NSLocalizedString("Cannot copy the file to the destination.", comment: ""), - buttonTitle: NSLocalizedString("Cancel", comment: "") - ) - endAppWithDelay() - } - - _ = try? shell("/usr/bin/xattr -drs com.apple.quarantine \(kTargetPartialPath)") - - guard let theBundle = Bundle(url: imeURLInstalled), - let imeIdentifier = theBundle.bundleIdentifier - else { - endAppWithDelay() - return - } - - let imeBundleURL = theBundle.bundleURL - - if allRegisteredInstancesOfThisInputMethod.isEmpty { - NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString).") - let status = (TISRegisterInputSource(imeBundleURL as CFURL) == noErr) - if !status { - let message = String( - format: NSLocalizedString( - "Cannot find input source %@ after registration.", comment: "" - ), - imeIdentifier - ) - runAlertPanel( - title: NSLocalizedString("Fatal Error", comment: ""), message: message, - buttonTitle: NSLocalizedString("Abort", comment: "") - ) - endAppWithDelay() - return - } - - if allRegisteredInstancesOfThisInputMethod.isEmpty { - let message = String( - format: NSLocalizedString( - "Cannot find input source %@ after registration.", comment: "" - ), - imeIdentifier - ) - runAlertPanel( - title: NSLocalizedString("Fatal Error", comment: ""), message: message, - buttonTitle: NSLocalizedString("Abort", comment: "") - ) - } - } - - var isMacOS12OrAbove = false - if #available(macOS 12.0, *) { - NSLog("macOS 12 or later detected.") - isMacOS12OrAbove = true - } else { - NSLog("Installer runs with the pre-macOS 12 flow.") - } - - // Unconditionally enable the IME on macOS 12.0+, - // as the kTISPropertyInputSourceIsEnabled can still be true even if the IME is *not* - // enabled in the user's current set of IMEs (which means the IME does not show up in - // the user's input menu). - - var mainInputSourceEnabled = false - - allRegisteredInstancesOfThisInputMethod.forEach { - if $0.activate() { - NSLog("Input method enabled: \(imeIdentifier)") - } else { - NSLog("Failed to enable input method: \(imeIdentifier)") - } - mainInputSourceEnabled = $0.isActivated - } - - // Alert Panel - let ntfPostInstall = NSAlert() - if warning { - ntfPostInstall.messageText = NSLocalizedString("Attention", comment: "") - ntfPostInstall.informativeText = NSLocalizedString( - "vChewing is upgraded, but please log out or reboot for the new version to be fully functional.", - comment: "" - ) - ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: "")) - } else { - if !mainInputSourceEnabled, !isMacOS12OrAbove { - ntfPostInstall.messageText = NSLocalizedString("Warning", comment: "") - ntfPostInstall.informativeText = NSLocalizedString( - "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.", - comment: "" - ) - ntfPostInstall.addButton(withTitle: NSLocalizedString("Continue", comment: "")) - } else { - ntfPostInstall.messageText = NSLocalizedString( - "Installation Successful", comment: "" - ) - ntfPostInstall.informativeText = NSLocalizedString( - "vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account.", - comment: "" - ) - ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: "")) - } - } - ntfPostInstall.beginSheetModal(for: window!) { _ in - self.endAppWithDelay() - } - } - func endAppWithDelay() { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) { NSApp.terminate(self) @@ -354,7 +159,7 @@ class AppDelegate: NSWindowController, NSApplicationDelegate { NSApp.terminate(self) } - private func shell(_ command: String) throws -> String { + func shell(_ command: String) throws -> String { let task = Process() let pipe = Pipe() @@ -377,26 +182,4 @@ class AppDelegate: NSWindowController, NSApplicationDelegate { return output } - - // Determines if an app is translocated by Gatekeeper to a randomized path. - // See https://weblog.rogueamoeba.com/2016/06/29/sierra-and-gatekeeper-path-randomization/ - // Originally written by Zonble Yang in Objective-C (MIT License). - // Swiftified by: Rob Mayoff. Ref: https://forums.swift.org/t/58719/5 - func isAppBundleTranslocated(atPath bundlePath: String) -> Bool { - var entryCount = getfsstat(nil, 0, 0) - var entries: [statfs] = .init(repeating: .init(), count: Int(entryCount)) - let absPath = bundlePath.cString(using: .utf8) - entryCount = getfsstat(&entries, entryCount * Int32(MemoryLayout.stride), MNT_NOWAIT) - for entry in entries.prefix(Int(entryCount)) { - let isMatch = withUnsafeBytes(of: entry.f_mntfromname) { mntFromName in - strcmp(absPath, mntFromName.baseAddress) == 0 - } - if isMatch { - var stat = statfs() - let rc = statfs(absPath, &stat) - return rc == 0 - } - } - return false - } } diff --git a/Installer/AppDelegate_Extension.swift b/Installer/AppDelegate_Extension.swift new file mode 100644 index 00000000..dcce917a --- /dev/null +++ b/Installer/AppDelegate_Extension.swift @@ -0,0 +1,209 @@ +// (c) 2011 and onwards The OpenVanilla Project (MIT License). +// All possible vChewing-specific modifications are of: +// (c) 2021 and onwards The vChewing Project (MIT-NTL License). +// ==================== +// This code is released under the MIT license (SPDX-License-Identifier: MIT) +// ... with NTL restriction stating that: +// No trademark license is granted to use the trade names, trademarks, service +// marks, or product names of Contributor, except as required to fulfill notice +// requirements defined in MIT License. + +import Cocoa +import InputMethodKit + +extension AppDelegate { + func removeThenInstallInputMethod() { + // if !FileManager.default.fileExists(atPath: kTargetPartialPath) { + // installInputMethod( + // previousExists: false, previousVersionNotFullyDeactivatedWarning: false + // ) + // return + // } + + guard let window = window else { return } + + let shouldWaitForTranslocationRemoval = + Reloc.isAppBundleTranslocated(atPath: kTargetPartialPath) + && window.responds(to: #selector(NSWindow.beginSheet(_:completionHandler:))) + + // 將既存輸入法扔到垃圾桶內 + do { + let sourceDir = kDestinationPartial + let fileManager = FileManager.default + let fileURLString = sourceDir + "/" + kTargetBundle + let fileURL = URL(fileURLWithPath: fileURLString) + + // 檢查檔案是否存在 + if fileManager.fileExists(atPath: fileURLString) { + // 塞入垃圾桶 + try fileManager.trashItem(at: fileURL, resultingItemURL: nil) + } else { + NSLog("File does not exist") + } + + } catch let error as NSError { + NSLog("An error took place: \(error)") + } + + let killTask = Process() + killTask.launchPath = "/usr/bin/killall" + killTask.arguments = ["-9", kTargetBin] + killTask.launch() + killTask.waitUntilExit() + + if shouldWaitForTranslocationRemoval { + progressIndicator.startAnimation(self) + window.beginSheet(progressSheet) { returnCode in + DispatchQueue.main.async { + if returnCode == .continue { + self.installInputMethod( + previousExists: true, + previousVersionNotFullyDeactivatedWarning: false + ) + } else { + self.installInputMethod( + previousExists: true, + previousVersionNotFullyDeactivatedWarning: true + ) + } + } + } + + translocationRemovalStartTime = Date() + Timer.scheduledTimer( + timeInterval: kTranslocationRemovalTickInterval, target: self, + selector: #selector(timerTick(_:)), userInfo: nil, repeats: true + ) + } else { + installInputMethod( + previousExists: false, previousVersionNotFullyDeactivatedWarning: false + ) + } + } + + func installInputMethod( + previousExists _: Bool, previousVersionNotFullyDeactivatedWarning warning: Bool + ) { + guard + let targetBundle = Bundle.main.path(forResource: kTargetBin, ofType: kTargetType) + else { + return + } + let cpTask = Process() + cpTask.launchPath = "/bin/cp" + print(kDestinationPartial) + cpTask.arguments = [ + "-R", targetBundle, kDestinationPartial, + ] + cpTask.launch() + cpTask.waitUntilExit() + + if cpTask.terminationStatus != 0 { + runAlertPanel( + title: NSLocalizedString("Install Failed", comment: ""), + message: NSLocalizedString("Cannot copy the file to the destination.", comment: ""), + buttonTitle: NSLocalizedString("Cancel", comment: "") + ) + endAppWithDelay() + } + + _ = try? shell("/usr/bin/xattr -drs com.apple.quarantine \(kTargetPartialPath)") + + guard let theBundle = Bundle(url: imeURLInstalled), + let imeIdentifier = theBundle.bundleIdentifier + else { + endAppWithDelay() + return + } + + let imeBundleURL = theBundle.bundleURL + + if allRegisteredInstancesOfThisInputMethod.isEmpty { + NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString).") + let status = (TISRegisterInputSource(imeBundleURL as CFURL) == noErr) + if !status { + let message = String( + format: NSLocalizedString( + "Cannot find input source %@ after registration.", comment: "" + ), + imeIdentifier + ) + runAlertPanel( + title: NSLocalizedString("Fatal Error", comment: ""), message: message, + buttonTitle: NSLocalizedString("Abort", comment: "") + ) + endAppWithDelay() + return + } + + if allRegisteredInstancesOfThisInputMethod.isEmpty { + let message = String( + format: NSLocalizedString( + "Cannot find input source %@ after registration.", comment: "" + ), + imeIdentifier + ) + runAlertPanel( + title: NSLocalizedString("Fatal Error", comment: ""), message: message, + buttonTitle: NSLocalizedString("Abort", comment: "") + ) + } + } + + var isMacOS12OrAbove = false + if #available(macOS 12.0, *) { + NSLog("macOS 12 or later detected.") + isMacOS12OrAbove = true + } else { + NSLog("Installer runs with the pre-macOS 12 flow.") + } + + // Unconditionally enable the IME on macOS 12.0+, + // as the kTISPropertyInputSourceIsEnabled can still be true even if the IME is *not* + // enabled in the user's current set of IMEs (which means the IME does not show up in + // the user's input menu). + + var mainInputSourceEnabled = false + + allRegisteredInstancesOfThisInputMethod.forEach { + if $0.activate() { + NSLog("Input method enabled: \(imeIdentifier)") + } else { + NSLog("Failed to enable input method: \(imeIdentifier)") + } + mainInputSourceEnabled = $0.isActivated + } + + // Alert Panel + let ntfPostInstall = NSAlert() + if warning { + ntfPostInstall.messageText = NSLocalizedString("Attention", comment: "") + ntfPostInstall.informativeText = NSLocalizedString( + "vChewing is upgraded, but please log out or reboot for the new version to be fully functional.", + comment: "" + ) + ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: "")) + } else { + if !mainInputSourceEnabled, !isMacOS12OrAbove { + ntfPostInstall.messageText = NSLocalizedString("Warning", comment: "") + ntfPostInstall.informativeText = NSLocalizedString( + "Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.", + comment: "" + ) + ntfPostInstall.addButton(withTitle: NSLocalizedString("Continue", comment: "")) + } else { + ntfPostInstall.messageText = NSLocalizedString( + "Installation Successful", comment: "" + ) + ntfPostInstall.informativeText = NSLocalizedString( + "vChewing is ready to use. \n\nPlease relogin if this is the first time you install it in this user account.", + comment: "" + ) + ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: "")) + } + } + ntfPostInstall.beginSheetModal(for: window!) { _ in + self.endAppWithDelay() + } + } +} diff --git a/Installer/RelocationDetector.swift b/Installer/RelocationDetector.swift new file mode 100644 index 00000000..b237ed7e --- /dev/null +++ b/Installer/RelocationDetector.swift @@ -0,0 +1,35 @@ +// (c) 2011 and onwards The OpenVanilla Project (MIT License). +// All possible vChewing-specific modifications are of: +// (c) 2021 and onwards The vChewing Project (MIT-NTL License). +// ==================== +// This code is released under the MIT license (SPDX-License-Identifier: MIT) +// ... with NTL restriction stating that: +// No trademark license is granted to use the trade names, trademarks, service +// marks, or product names of Contributor, except as required to fulfill notice +// requirements defined in MIT License. + +import Foundation + +public enum Reloc { + // Determines if an app is translocated by Gatekeeper to a randomized path. + // See https://weblog.rogueamoeba.com/2016/06/29/sierra-and-gatekeeper-path-randomization/ + // Originally written by Zonble Yang in Objective-C (MIT License). + // Swiftified by: Rob Mayoff. Ref: https://forums.swift.org/t/58719/5 + public static func isAppBundleTranslocated(atPath bundlePath: String) -> Bool { + var entryCount = getfsstat(nil, 0, 0) + var entries: [statfs] = .init(repeating: .init(), count: Int(entryCount)) + let absPath = bundlePath.cString(using: .utf8) + entryCount = getfsstat(&entries, entryCount * Int32(MemoryLayout.stride), MNT_NOWAIT) + for entry in entries.prefix(Int(entryCount)) { + let isMatch = withUnsafeBytes(of: entry.f_mntfromname) { mntFromName in + strcmp(absPath, mntFromName.baseAddress) == 0 + } + if isMatch { + var stat = statfs() + let rc = statfs(absPath, &stat) + return rc == 0 + } + } + return false + } +} diff --git a/vChewing.xcodeproj/project.pbxproj b/vChewing.xcodeproj/project.pbxproj index 0d8f17c7..298654d1 100644 --- a/vChewing.xcodeproj/project.pbxproj +++ b/vChewing.xcodeproj/project.pbxproj @@ -54,6 +54,8 @@ 5BBC2D9F28F51C0400C986F6 /* LICENSE-CHT.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5B18BA7427C7BD8C0056EB19 /* LICENSE-CHT.txt */; }; 5BBC2DA028F51C0400C986F6 /* LICENSE-JPN.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5B18BA7327C7BD8C0056EB19 /* LICENSE-JPN.txt */; }; 5BBC2DA128F51C0400C986F6 /* LICENSE-CHS.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5B18BA6F27C7BD8B0056EB19 /* LICENSE-CHS.txt */; }; + 5BBC2DA328F5212100C986F6 /* RelocationDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBC2DA228F5212100C986F6 /* RelocationDetector.swift */; }; + 5BBC2DA528F521C200C986F6 /* AppDelegate_Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBC2DA428F521C200C986F6 /* AppDelegate_Extension.swift */; }; 5BC2652227E04B7E00700291 /* uninstall.sh in Resources */ = {isa = PBXBuildFile; fileRef = 5BC2652127E04B7B00700291 /* uninstall.sh */; }; 5BC4479D2865686500EDC323 /* data-chs.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5BEDB71C283B4AEA0078EB25 /* data-chs.plist */; }; 5BC4479E2865686500EDC323 /* data-cht.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5BEDB720283B4AEA0078EB25 /* data-cht.plist */; }; @@ -243,6 +245,8 @@ 5BBBB77127AED70B0023B93A /* MenuIcon-SCVIM.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuIcon-SCVIM.png"; sourceTree = ""; }; 5BBBB77227AED70B0023B93A /* MenuIcon-TCVIM.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "MenuIcon-TCVIM.png"; sourceTree = ""; }; 5BBBB77727AEDB290023B93A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MainMenu.strings; sourceTree = ""; }; + 5BBC2DA228F5212100C986F6 /* RelocationDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelocationDetector.swift; sourceTree = ""; }; + 5BBC2DA428F521C200C986F6 /* AppDelegate_Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate_Extension.swift; sourceTree = ""; }; 5BBD627827B6C4D900271480 /* Update-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Update-Info.plist"; sourceTree = ""; }; 5BC0AAC927F58472002D33E9 /* pkgPreInstall.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = pkgPreInstall.sh; sourceTree = ""; }; 5BC0AACA27F58472002D33E9 /* pkgPostInstall.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = pkgPostInstall.sh; sourceTree = ""; }; @@ -695,6 +699,8 @@ 5B44B97C28D2F283004508BF /* PKGRoot */, 5BBBB77827AEDB330023B93A /* Resources */, D4F0BBE0279AF8B30071253C /* AppDelegate.swift */, + 5BBC2DA428F521C200C986F6 /* AppDelegate_Extension.swift */, + 5BBC2DA228F5212100C986F6 /* RelocationDetector.swift */, 6ACA41F215FC1D9000935EF6 /* Installer-Info.plist */, 5BC0AACA27F58472002D33E9 /* pkgPostInstall.sh */, 5BC0AAC927F58472002D33E9 /* pkgPreInstall.sh */, @@ -1094,6 +1100,8 @@ buildActionMask = 2147483647; files = ( D4F0BBE1279AF8B30071253C /* AppDelegate.swift in Sources */, + 5BBC2DA528F521C200C986F6 /* AppDelegate_Extension.swift in Sources */, + 5BBC2DA328F5212100C986F6 /* RelocationDetector.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };