Converts the installer to Swift.

This commit is contained in:
zonble 2022-01-22 00:01:21 +08:00
parent 5fe1c28ae7
commit bfb66cb080
12 changed files with 457 additions and 556 deletions

View File

@ -17,7 +17,6 @@
6A0D4F5815FC0EF900ABF4B3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6A0D4F4A15FC0EE100ABF4B3 /* Localizable.strings */; };
6A187E2616004C5900466B2E /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6A187E2816004C5900466B2E /* MainMenu.xib */; };
6A225A1F23679F2600F685C6 /* NotarizedArchives in Resources */ = {isa = PBXBuildFile; fileRef = 6A225A1E23679F2600F685C6 /* NotarizedArchives */; };
6A225A232367A1D700F685C6 /* ArchiveUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A225A222367A1D700F685C6 /* ArchiveUtil.m */; };
6A2E40F6253A69DA00D1AE1D /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A2E40F5253A69DA00D1AE1D /* Images.xcassets */; };
6A2E40F9253A6AA000D1AE1D /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A2E40F5253A69DA00D1AE1D /* Images.xcassets */; };
6A38BC1515FC117A00A8A51F /* data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A38BBF615FC117A00A8A51F /* data.txt */; };
@ -26,9 +25,7 @@
6A6ED16C2797650A0012872E /* template-data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A6ED1652797650A0012872E /* template-data.txt */; };
6A6ED16D2797650A0012872E /* template-exclude-phrases-plain-bpmf.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A6ED1672797650A0012872E /* template-exclude-phrases-plain-bpmf.txt */; };
6A6ED16E2797650A0012872E /* template-exclude-phrases.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A6ED1692797650A0012872E /* template-exclude-phrases.txt */; };
6A93050E2798780000D370DA /* InstallerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A93050D2798780000D370DA /* InstallerUtil.swift */; };
6ACA41CD15FC1D7500935EF6 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A0D4EA615FC0D2D00ABF4B3 /* Cocoa.framework */; };
6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6ACA41E915FC1D9000935EF6 /* AppDelegate.m */; };
6ACA41FA15FC1D9000935EF6 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41EA15FC1D9000935EF6 /* InfoPlist.strings */; };
6ACA41FB15FC1D9000935EF6 /* License.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41EC15FC1D9000935EF6 /* License.rtf */; };
6ACA41FC15FC1D9000935EF6 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41EE15FC1D9000935EF6 /* Localizable.strings */; };
@ -62,6 +59,9 @@
D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; };
D485D3B92796A8A000657FF3 /* PreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3B82796A8A000657FF3 /* PreferencesTests.swift */; };
D485D3C02796CE3200657FF3 /* VersionUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */; };
D4F0BBDF279AF1AF0071253C /* ArchiveUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F0BBDE279AF1AF0071253C /* ArchiveUtil.swift */; };
D4F0BBE1279AF8B30071253C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4F0BBE0279AF8B30071253C /* AppDelegate.swift */; };
D4F0BBE4279B08900071253C /* BundleTranslocate.m in Sources */ = {isa = PBXBuildFile; fileRef = D4F0BBE3279B08900071253C /* BundleTranslocate.m */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -154,8 +154,6 @@
6A15B32721A51F2300B92CD3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Source/Base.lproj/preferences.xib; sourceTree = "<group>"; };
6A187E2916004C7300466B2E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = "zh-Hant"; path = "zh-Hant.lproj/MainMenu.xib"; sourceTree = "<group>"; };
6A225A1E23679F2600F685C6 /* NotarizedArchives */ = {isa = PBXFileReference; lastKnownFileType = folder; path = NotarizedArchives; sourceTree = "<group>"; };
6A225A212367A1D700F685C6 /* ArchiveUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ArchiveUtil.h; sourceTree = "<group>"; };
6A225A222367A1D700F685C6 /* ArchiveUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ArchiveUtil.m; sourceTree = "<group>"; };
6A2E40F5253A69DA00D1AE1D /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
6A38BBF615FC117A00A8A51F /* data.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = data.txt; sourceTree = "<group>"; };
6A38BC2715FC158A00A8A51F /* InputMethodKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputMethodKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.8.sdk/System/Library/Frameworks/InputMethodKit.framework; sourceTree = DEVELOPER_DIR; };
@ -168,10 +166,7 @@
6A6ED171279765170012872E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text; name = "zh-Hant"; path = "zh-Hant.lproj/template-exclude-phrases.txt"; sourceTree = "<group>"; };
6A6ED1722797651A0012872E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text; name = "zh-Hant"; path = "zh-Hant.lproj/template-phrases-replacement.txt"; sourceTree = "<group>"; };
6A93050C279877FF00D370DA /* McBopomofoInstaller-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "McBopomofoInstaller-Bridging-Header.h"; sourceTree = "<group>"; };
6A93050D2798780000D370DA /* InstallerUtil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InstallerUtil.swift; path = InstallerUtil.swift; sourceTree = "<group>"; };
6ACA41CB15FC1D7500935EF6 /* McBopomofoInstaller.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = McBopomofoInstaller.app; sourceTree = BUILT_PRODUCTS_DIR; };
6ACA41E815FC1D9000935EF6 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Source/Installer/AppDelegate.h; sourceTree = SOURCE_ROOT; };
6ACA41E915FC1D9000935EF6 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = Source/Installer/AppDelegate.m; sourceTree = SOURCE_ROOT; };
6ACA41EB15FC1D9000935EF6 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
6ACA41ED15FC1D9000935EF6 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/License.rtf; sourceTree = "<group>"; };
6ACA41EF15FC1D9000935EF6 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -218,6 +213,10 @@
D485D3B62796A8A000657FF3 /* McBopomofoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = McBopomofoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D485D3B82796A8A000657FF3 /* PreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesTests.swift; sourceTree = "<group>"; };
D485D3BF2796CE3200657FF3 /* VersionUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionUpdateTests.swift; sourceTree = "<group>"; };
D4F0BBDE279AF1AF0071253C /* ArchiveUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveUtil.swift; sourceTree = "<group>"; };
D4F0BBE0279AF8B30071253C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
D4F0BBE2279B08900071253C /* BundleTranslocate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BundleTranslocate.h; sourceTree = "<group>"; };
D4F0BBE3279B08900071253C /* BundleTranslocate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BundleTranslocate.m; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -450,12 +449,9 @@
6ACA41E715FC1D9000935EF6 /* Installer */ = {
isa = PBXGroup;
children = (
6A93050D2798780000D370DA /* InstallerUtil.swift */,
6A225A212367A1D700F685C6 /* ArchiveUtil.h */,
6A225A222367A1D700F685C6 /* ArchiveUtil.m */,
D4F0BBE0279AF8B30071253C /* AppDelegate.swift */,
D4F0BBDE279AF1AF0071253C /* ArchiveUtil.swift */,
6A225A1E23679F2600F685C6 /* NotarizedArchives */,
6ACA41E815FC1D9000935EF6 /* AppDelegate.h */,
6ACA41E915FC1D9000935EF6 /* AppDelegate.m */,
6ACA41EA15FC1D9000935EF6 /* InfoPlist.strings */,
6ACA41EC15FC1D9000935EF6 /* License.rtf */,
6ACA41EE15FC1D9000935EF6 /* Localizable.strings */,
@ -464,6 +460,8 @@
6ACA41F315FC1D9000935EF6 /* Installer-Prefix.pch */,
6ACA41F415FC1D9000935EF6 /* main.m */,
6A93050C279877FF00D370DA /* McBopomofoInstaller-Bridging-Header.h */,
D4F0BBE2279B08900071253C /* BundleTranslocate.h */,
D4F0BBE3279B08900071253C /* BundleTranslocate.m */,
);
path = Installer;
sourceTree = "<group>";
@ -716,10 +714,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */,
6A225A232367A1D700F685C6 /* ArchiveUtil.m in Sources */,
6A93050E2798780000D370DA /* InstallerUtil.swift in Sources */,
D4F0BBE1279AF8B30071253C /* AppDelegate.swift in Sources */,
6ACA41FF15FC1D9000935EF6 /* main.m in Sources */,
D4F0BBE4279B08900071253C /* BundleTranslocate.m in Sources */,
D4F0BBDF279AF1AF0071253C /* ArchiveUtil.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -1,49 +0,0 @@
// Copyright (c) 2012 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
#import <Cocoa/Cocoa.h>
#import "ArchiveUtil.h"
@interface AppDelegate : NSWindowController <NSApplicationDelegate>
{
@protected
ArchiveUtil *_archiveUtil;
NSString *_installingVersion;
BOOL _upgrading;
NSButton *__weak _installButton;
NSButton *__weak _cancelButton;
NSTextView *__unsafe_unretained _textView;
NSWindow *__weak _progressSheet;
NSProgressIndicator *__weak _progressIndicator;
NSDate *_translocationRemovalStartTime;
NSInteger _currentVersionNumber;
}
- (IBAction)agreeAndInstallAction:(id)sender;
- (IBAction)cancelAction:(id)sender;
@property (weak) IBOutlet NSButton *installButton;
@property (weak) IBOutlet NSButton *cancelButton;
@property (unsafe_unretained) IBOutlet NSTextView *textView;
@property (weak) IBOutlet NSWindow *progressSheet;
@property (weak) IBOutlet NSProgressIndicator *progressIndicator;
@end

View File

@ -1,289 +0,0 @@
// Copyright (c) 2012 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
#import "AppDelegate.h"
#import <sys/mount.h>
//#import "McBopomofoInstaller-Swift.h"
@import InputSourceHelper;
static NSString *const kTargetBin = @"McBopomofo";
static NSString *const kTargetType = @"app";
static NSString *const kTargetBundle = @"McBopomofo.app";
static NSString *const kDestinationPartial = @"~/Library/Input Methods/";
static NSString *const kTargetPartialPath = @"~/Library/Input Methods/McBopomofo.app";
static NSString *const kTargetFullBinPartialPath = @"~/Library/Input Methods/McBopomofo.app/Contents/MacOS/McBopomofo";
static const NSTimeInterval kTranslocationRemovalTickInterval = 0.5;
static const NSTimeInterval kTranslocationRemovalDeadline = 60.0;
/// A simple replacement for the deprecated NSRunAlertPanel.
void RunAlertPanel(NSString *title, NSString *message, NSString *buttonTitle) {
NSAlert *alert = [[NSAlert alloc] init];
[alert setAlertStyle:NSAlertStyleInformational];
[alert setMessageText:title];
[alert setInformativeText:message];
[alert addButtonWithTitle:buttonTitle];
[alert runModal];
}
@implementation AppDelegate
@synthesize installButton = _installButton;
@synthesize cancelButton = _cancelButton;
@synthesize textView = _textView;
@synthesize progressSheet = _progressSheet;
@synthesize progressIndicator = _progressIndicator;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
_installingVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:(id)kCFBundleVersionKey];
NSString *versionString = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
_archiveUtil = [[ArchiveUtil alloc] initWithAppName:kTargetBin targetAppBundleName:kTargetBundle];
[_archiveUtil validateIfNotarizedArchiveExists];
[self.cancelButton setNextKeyView:self.installButton];
[self.installButton setNextKeyView:self.cancelButton];
[[self window] setDefaultButtonCell:[self.installButton cell]];
NSAttributedString *attrStr = [[NSAttributedString alloc] initWithRTF:[NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"License" ofType:@"rtf"]] documentAttributes:NULL];
NSMutableAttributedString *mutableAttrStr = [attrStr mutableCopy];
[mutableAttrStr addAttribute:NSForegroundColorAttributeName value:[NSColor controlTextColor] range:NSMakeRange(0, [mutableAttrStr length])];
[[self.textView textStorage] setAttributedString:mutableAttrStr];
[self.textView setSelectedRange:NSMakeRange(0, 0)];
[[self window] setTitle:[NSString stringWithFormat:NSLocalizedString(@"%@ (for version %@, r%@)", nil), [[self window] title], versionString, _installingVersion]];
if ([[NSFileManager defaultManager] fileExistsAtPath:[kTargetPartialPath stringByExpandingTildeInPath]]) {
NSBundle *currentBundle = [NSBundle bundleWithPath:[kTargetPartialPath stringByExpandingTildeInPath]];
NSString *shortVersion = [[currentBundle infoDictionary] objectForKey:@"CFBundleShortVersionString"];
NSString *currentVersion = [[currentBundle infoDictionary] objectForKey:(id)kCFBundleVersionKey];
_currentVersionNumber = [currentVersion integerValue];
if (shortVersion && currentVersion && [currentVersion compare:_installingVersion options:NSNumericSearch] == NSOrderedAscending) {
_upgrading = YES;
}
}
if (_upgrading) {
[_installButton setTitle:NSLocalizedString(@"Agree and Upgrade", nil)];
}
[[self window] center];
[[self window] orderFront:self];
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
}
- (IBAction)agreeAndInstallAction:(id)sender
{
[_cancelButton setEnabled:NO];
[_installButton setEnabled:NO];
[self removeThenInstallInputMethod];
}
- (void)removeThenInstallInputMethod
{
if ([[NSFileManager defaultManager] fileExistsAtPath:[kTargetPartialPath stringByExpandingTildeInPath]]) {
BOOL shouldWaitForTranslocationRemoval =
[self appBundleTranslocatedToARandomizedPath:kTargetPartialPath] &&
[self.window respondsToSelector:@selector(beginSheet:completionHandler:)];
// http://www.cocoadev.com/index.pl?MoveToTrash
NSString *sourceDir = [kDestinationPartial stringByExpandingTildeInPath];
NSString *trashDir = [NSHomeDirectory() stringByAppendingPathComponent:@".Trash"];
NSInteger tag;
[[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation source:sourceDir destination:trashDir files:[NSArray arrayWithObject:kTargetBundle] tag:&tag];
(void)tag;
NSTask *killTask = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/killall" arguments:[NSArray arrayWithObjects: @"-9", kTargetBin, nil]];
[killTask waitUntilExit];
if (shouldWaitForTranslocationRemoval) {
[self.progressIndicator startAnimation:self];
[self.window beginSheet:self.progressSheet completionHandler:^(NSModalResponse returnCode) {
// Schedule the install action in runloop so that the sheet gets a change to dismiss itself.
dispatch_async(dispatch_get_main_queue(), ^{
if (returnCode == NSModalResponseContinue) {
[self installInputMethodWithPreviousExists:YES previousVersionNotFullyDeactivatedWarning:NO];
} else {
[self installInputMethodWithPreviousExists:YES previousVersionNotFullyDeactivatedWarning:YES];
}
});
}];
_translocationRemovalStartTime = [NSDate date];
[NSTimer scheduledTimerWithTimeInterval:kTranslocationRemovalTickInterval target:self selector:@selector(timerTick:) userInfo:nil repeats:YES];
return;
}
}
[self installInputMethodWithPreviousExists:NO previousVersionNotFullyDeactivatedWarning:NO];
}
- (void)timerTick:(NSTimer *)timer
{
NSTimeInterval elapsed = [[NSDate date] timeIntervalSinceDate:_translocationRemovalStartTime];
[self.progressIndicator setDoubleValue:MIN(elapsed / kTranslocationRemovalDeadline, 1.0)];
if (elapsed >= kTranslocationRemovalDeadline) {
[timer invalidate];
[self.window endSheet:self.progressSheet returnCode:NSModalResponseCancel];
} else if (![self appBundleTranslocatedToARandomizedPath:kTargetPartialPath]) {
[self.progressIndicator setDoubleValue:1.0];
[timer invalidate];
[self.window endSheet:self.progressSheet returnCode:NSModalResponseContinue];
}
}
- (void)installInputMethodWithPreviousExists:(BOOL)previousVersionExists previousVersionNotFullyDeactivatedWarning:(BOOL)warning
{
// If the unzipped archive does not exist, this must be a dev-mode installer.
NSString *targetBundle = [_archiveUtil unzipNotarizedArchive];
if (!targetBundle) {
targetBundle = [[NSBundle mainBundle] pathForResource:kTargetBin ofType:kTargetType];
}
NSTask *cpTask = [NSTask launchedTaskWithLaunchPath:@"/bin/cp" arguments:[NSArray arrayWithObjects:@"-R", targetBundle, [kDestinationPartial stringByExpandingTildeInPath], nil]];
[cpTask waitUntilExit];
if ([cpTask terminationStatus] != 0) {
RunAlertPanel(NSLocalizedString(@"Install Failed", nil), NSLocalizedString(@"Cannot copy the file to the destination.", nil), NSLocalizedString(@"Cancel", nil));
[NSApp terminate:self];
}
NSBundle *imeBundle = [NSBundle bundleWithPath:[kTargetPartialPath stringByExpandingTildeInPath]];
NSCAssert(imeBundle != nil, @"Target bundle must exists");
NSURL *imeBundleURL = imeBundle.bundleURL;
NSString *imeIdentifier = imeBundle.bundleIdentifier;
TISInputSourceRef inputSource = [InputSourceHelper inputSourceForInputSourceID:imeIdentifier];
// if this IME name is not found in the list of available IMEs
if (!inputSource) {
NSLog(@"Registering input source %@ at %@.", imeIdentifier, imeBundleURL.absoluteString);
// then register
BOOL status = [InputSourceHelper registerInputSource:imeBundleURL];
if (!status) {
NSString *message = [NSString stringWithFormat:NSLocalizedString(@"Cannot register input source %@ at %@.", nil), imeIdentifier, imeBundleURL.absoluteString];
RunAlertPanel(NSLocalizedString(@"Fatal Error", nil), message, NSLocalizedString(@"Abort", nil));
[self endAppWithDelay];
return;
}
inputSource = [InputSourceHelper inputSourceForInputSourceID:imeIdentifier];
// if it still doesn't register successfully, bail.
if (!inputSource) {
NSString *message = [NSString stringWithFormat:NSLocalizedString(@"Cannot find input source %@ after registration.", nil), imeIdentifier];
RunAlertPanel(NSLocalizedString(@"Fatal Error", nil), message, NSLocalizedString(@"Abort", nil));
[self endAppWithDelay];
return;
}
}
BOOL isMacOS12OrAbove = NO;
if (@available(macOS 12.0, *)) {
NSLog(@"macOS 12 or later detected.");
isMacOS12OrAbove = YES;
} else {
NSLog(@"Installer runs with the pre-macOS 12 flow.");
}
// If the IME is not enabled, enable it. Also, unconditionally enable it 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).
BOOL mainInputSourceEnabled = [InputSourceHelper inputSourceEnabled:inputSource];
if (!mainInputSourceEnabled || isMacOS12OrAbove) {
mainInputSourceEnabled = [InputSourceHelper enableInputSource:inputSource];
if (mainInputSourceEnabled) {
NSLog(@"Input method enabled: %@", imeIdentifier);
} else {
NSLog(@"Failed to enable input method: %@", imeIdentifier);
}
}
if (warning) {
RunAlertPanel(NSLocalizedString(@"Attention", nil), NSLocalizedString(@"McBopomofo is upgraded, but please log out or reboot for the new version to be fully functional.", nil), NSLocalizedString(@"OK", nil));
} else {
// Only prompt a warning if pre-macOS 12. The flag is not indicative of anything meaningful due to the need of user intervention in Prefernces.app on macOS 12.
if (!mainInputSourceEnabled && !isMacOS12OrAbove) {
RunAlertPanel(NSLocalizedString(@"Warning", nil), NSLocalizedString(@"Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.", nil), NSLocalizedString(@"Continue", nil));
} else {
RunAlertPanel(NSLocalizedString(@"Installation Successful", nil), NSLocalizedString(@"McBopomofo is ready to use.", nil), NSLocalizedString(@"OK", nil));
}
}
[self endAppWithDelay];
}
- (void)endAppWithDelay
{
[[NSApplication sharedApplication] performSelector:@selector(terminate:) withObject:self afterDelay:0.1];
}
- (IBAction)cancelAction:(id)sender
{
[NSApp terminate:self];
}
- (void)windowWillClose:(NSNotification *)notification
{
[NSApp terminate:self];
}
// 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/
- (BOOL)appBundleTranslocatedToARandomizedPath:(NSString *)bundle
{
const char *bundleAbsPath = [[bundle stringByExpandingTildeInPath] UTF8String];
int entryCount = getfsstat(NULL, 0, 0);
int entrySize = sizeof(struct statfs);
struct statfs *bufs = (struct statfs *)calloc(entryCount, entrySize);
entryCount = getfsstat(bufs, entryCount * entrySize, MNT_NOWAIT);
for (int i = 0; i < entryCount; i++) {
if (!strcmp(bundleAbsPath, bufs[i].f_mntfromname)) {
free(bufs);
// getfsstat() may return us a cached result, and so we need to get the stat of the mounted fs.
// If statfs() returns an error, the mounted fs is already gone.
struct statfs stat;
int checkResult = statfs(bundleAbsPath, &stat);
if (checkResult != 0) {
// Meaning the app's bundle is not mounted, that is it's not translocated.
// It also means that the app is not loaded.
return NO;
}
return YES;
}
}
free(bufs);
return NO;
}
@end

View File

@ -0,0 +1,258 @@
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
import Cocoa
import InputSourceHelper
private let kTargetBin = "McBopomofo"
private let kTargetType = "app"
private let kTargetBundle = "McBopomofo.app"
private let kDestinationPartial = "~/Library/Input Methods/"
private let kTargetPartialPath = "~/Library/Input Methods/McBopomofo.app"
private let kTargetFullBinPartialPath = "~/Library/Input Methods/McBopomofo.app/Contents/MacOS/McBopomofo"
private let kTranslocationRemovalTickInterval: TimeInterval = 0.5
private let kTranslocationRemovalDeadline: TimeInterval = 60.0
@objc (AppDelegate)
class AppDelegate: NSWindowController, NSApplicationDelegate {
@IBOutlet weak private var installButton: NSButton!
@IBOutlet weak private var cancelButton: NSButton!
@IBOutlet private var textView: NSTextView!
@IBOutlet weak private var progressSheet: NSWindow!
@IBOutlet weak private var progressIndicator: NSProgressIndicator!
private var archiveUtil: ArchiveUtil?
private var installingVersion = ""
private var upgrading = false
private var translocationRemovalStartTime: Date?
private var currentVersionNumber: Int = 0
func runAlertPanel(title: String, message: String, buttonTitle: String) {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = title
alert.informativeText = message
alert.addButton(withTitle: buttonTitle)
alert.runModal()
}
func applicationDidFinishLaunching(_ notification: Notification) {
guard let installingVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String,
let versionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else {
return
}
self.installingVersion = installingVersion
self.archiveUtil = ArchiveUtil(appName: kTargetBin, targetAppBundleName: kTargetBundle)
_ = archiveUtil?.validateIfNotarizedArchiveExists()
cancelButton.nextKeyView = installButton
installButton.nextKeyView = cancelButton
if let cell = installButton.cell as? NSButtonCell {
window?.defaultButtonCell = cell
}
var attrStr = NSAttributedString()
if let rtfPath = Bundle.main.url(forResource: "License", withExtension: "rtf"),
let rtfData = try? Data(contentsOf: rtfPath),
let rtf = NSAttributedString(rtf: rtfData, documentAttributes: nil) {
attrStr = rtf
}
let mutableAttrStr = NSMutableAttributedString(attributedString: attrStr)
mutableAttrStr.addAttribute(.foregroundColor, value: NSColor.controlTextColor, range: NSMakeRange(0, mutableAttrStr.length))
textView.textStorage?.setAttributedString(mutableAttrStr)
textView.setSelectedRange(NSMakeRange(0, 0))
window?.title = String(format: NSLocalizedString("%@ (for version %@, r%@)", comment: ""), window?.title ?? "", versionString, installingVersion)
if FileManager.default.fileExists(atPath: (kTargetPartialPath as NSString).expandingTildeInPath) {
let currentBundle = Bundle(path: (kTargetPartialPath as NSString).expandingTildeInPath)
let shortVersion = currentBundle?.infoDictionary?["CFBundleShortVersionString"] as? String
let currentVersion = currentBundle?.infoDictionary?[kCFBundleVersionKey as String] as? String
currentVersionNumber = (currentVersion as NSString?)?.integerValue ?? 0
if shortVersion != nil, let currentVersion = currentVersion, currentVersion.compare(installingVersion, options: .numeric) == .orderedAscending {
upgrading = true
}
}
if upgrading {
installButton.title = NSLocalizedString("Agree and Upgrade", comment: "")
}
window?.center()
window?.orderFront(self)
NSApp.activate(ignoringOtherApps: true)
}
@IBAction func agreeAndInstallAction(_ sender: AnyObject) {
cancelButton.isEnabled = false
installButton.isEnabled = false
removeThenInstallInputMethod()
}
@objc func timerTick(_ timer: Timer) {
let elapsed = Date().timeIntervalSince(translocationRemovalStartTime ?? Date())
if elapsed >= kTranslocationRemovalDeadline {
timer.invalidate()
window?.endSheet(progressSheet, returnCode: .cancel)
} else if appBundleTranslocatedToARandomizedPath(kTargetPartialPath) == false {
progressIndicator.doubleValue = 1.0
timer.invalidate()
window?.endSheet(progressSheet, returnCode: .continue)
}
}
func removeThenInstallInputMethod() {
if FileManager.default.fileExists(atPath: (kTargetPartialPath as NSString).expandingTildeInPath) == false {
self.installInputMethod(previousExists: false, previousVersionNotFullyDeactivatedWarning: false)
return
}
let shouldWaitForTranslocationRemoval = appBundleTranslocatedToARandomizedPath(kTargetPartialPath) && (window?.responds(to: #selector(NSWindow.beginSheet(_:completionHandler:))) ?? false)
// http://www.cocoadev.com/index.pl?MoveToTrash
let sourceDir = (kDestinationPartial as NSString).expandingTildeInPath
let trashDir = (NSHomeDirectory() as NSString).appendingPathComponent(".Trash")
var tag = 0
NSWorkspace.shared.performFileOperation(.recycleOperation, source: sourceDir, destination: trashDir, files: [kTargetBundle], tag: &tag)
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)
}
}
func installInputMethod(previousExists: Bool, previousVersionNotFullyDeactivatedWarning warning: Bool) {
guard let targetBundle = archiveUtil?.unzipNotarizedArchive() ?? Bundle.main.path(forResource: kTargetBin, ofType: kTargetType) else {
return
}
let cpTask = Process()
cpTask.launchPath = "/bin/cp"
cpTask.arguments = ["-R", targetBundle, (kDestinationPartial as NSString).expandingTildeInPath]
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()
}
guard let imeBundle = Bundle(path: (kTargetPartialPath as NSString).expandingTildeInPath),
let imeIdentifier = imeBundle.bundleIdentifier
else {
endAppWithDelay()
return
}
let imeBundleURL = imeBundle.bundleURL
var inputSource = InputSourceHelper.inputSource(for: imeIdentifier)
if inputSource == nil {
NSLog("Registering input source \(imeIdentifier) at \(imeBundleURL.absoluteString).");
let status = InputSourceHelper.registerTnputSource(at: imeBundleURL)
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
}
inputSource = InputSourceHelper.inputSource(for: imeIdentifier)
if inputSource == nil {
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.");
}
// If the IME is not enabled, enable it. Also, unconditionally enable it 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 = InputSourceHelper.inputSourceEnabled(for: inputSource!)
if !mainInputSourceEnabled || isMacOS12OrAbove {
mainInputSourceEnabled = InputSourceHelper.enable(inputSource: inputSource!)
if (mainInputSourceEnabled) {
NSLog("Input method enabled: \(imeIdentifier)");
} else {
NSLog("Failed to enable input method: \(imeIdentifier)");
}
}
if warning {
runAlertPanel(title: NSLocalizedString("Attention", comment: ""), message: NSLocalizedString("McBopomofo is upgraded, but please log out or reboot for the new version to be fully functional.", comment: ""), buttonTitle: NSLocalizedString("OK", comment: ""))
} else {
if !mainInputSourceEnabled && !isMacOS12OrAbove {
runAlertPanel(title: NSLocalizedString("Warning", comment: ""), message: NSLocalizedString("Input method may not be fully enabled. Please enable it through System Preferences > Keyboard > Input Sources.", comment: ""), buttonTitle: NSLocalizedString("Continue", comment: ""))
} else {
runAlertPanel(title: NSLocalizedString("Installation Successful", comment: ""), message: NSLocalizedString("McBopomofo is ready to use.", comment: ""), buttonTitle: NSLocalizedString("OK", comment: ""))
}
}
endAppWithDelay()
}
func endAppWithDelay() {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
NSApp.terminate(self)
}
}
@IBAction func cancelAction(_ sender: AnyObject) {
NSApp.terminate(self)
}
func windowWillClose(_ Notification: Notification) {
NSApp.terminate(self)
}
}

View File

@ -1,39 +0,0 @@
// Copyright (c) 2019 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
#import <Foundation/Foundation.h>
@interface ArchiveUtil : NSObject {
NSString *_appName;
NSString *_targetAppBundleName;
}
- (instancetype _Nonnull)initWithAppName:(NSString *_Nonnull)name
targetAppBundleName:(NSString *_Nonnull)invalidAppBundleName;
// Returns YES if (1) a zip file under
// Resources/NotarizedArchives/$_appName-$bundleVersion.zip exists, and (2) if
// Resources/$_invalidAppBundleName does not exist.
- (BOOL)validateIfNotarizedArchiveExists;
- (NSString *_Nullable)unzipNotarizedArchive;
@end

View File

@ -1,132 +0,0 @@
// Copyright (c) 2012 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
#import "ArchiveUtil.h"
@implementation ArchiveUtil
- (instancetype)initWithAppName:(NSString *)name
targetAppBundleName:(NSString *)targetAppBundleName {
self = [super init];
if (self) {
_appName = name;
_targetAppBundleName = targetAppBundleName;
}
return self;
}
- (void)delloc {
_appName = nil;
_targetAppBundleName = nil;
}
- (BOOL)validateIfNotarizedArchiveExists {
NSString *resourePath = [[NSBundle mainBundle] resourcePath];
NSString *devModeAppBundlePath =
[resourePath stringByAppendingPathComponent:_targetAppBundleName];
NSArray<NSString *> *notarizedArchivesContent =
[[NSFileManager defaultManager] subpathsAtPath:[self notarizedArchivesPath]];
NSInteger count = [notarizedArchivesContent count];
BOOL notarizedArchiveExists =
[[NSFileManager defaultManager] fileExistsAtPath:[self notarizedArchive]];
BOOL devModeAppBundleExists =
[[NSFileManager defaultManager] fileExistsAtPath:devModeAppBundlePath];
if (count > 0) {
// Not a valid distribution package.
if (count != 1 || !notarizedArchiveExists || devModeAppBundleExists) {
NSAlert *alert = [[NSAlert alloc] init];
[alert setAlertStyle:NSAlertStyleInformational];
[alert setMessageText:@"Internal Error"];
[alert
setInformativeText:
[NSString stringWithFormat:@"devMode installer, expected archive name: %@, "
@"archive exists: %d, devMode app bundle exists: %d",
[self notarizedArchive], notarizedArchiveExists,
devModeAppBundleExists]];
[alert addButtonWithTitle:@"Terminate"];
[alert runModal];
[[NSApplication sharedApplication] terminate:nil];
} else {
return YES;
}
}
if (!devModeAppBundleExists) {
NSAlert *alert = [[NSAlert alloc] init];
[alert setAlertStyle:NSAlertStyleInformational];
[alert setMessageText:@"Internal Error"];
[alert
setInformativeText:[NSString stringWithFormat:@"Dev target bundle does not exist: %@",
devModeAppBundlePath]];
[alert addButtonWithTitle:@"Terminate"];
[alert runModal];
[[NSApplication sharedApplication] terminate:nil];
}
// Notarized archive does not exist, but it's ok.
return NO;
}
- (NSString *)unzipNotarizedArchive {
if (![self validateIfNotarizedArchiveExists]) {
return nil;
}
NSString *tempFilePath =
[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]];
NSArray *arguments = @[ [self notarizedArchive], @"-d", tempFilePath ];
NSTask *unzipTask = [[NSTask alloc] init];
[unzipTask setLaunchPath:@"/usr/bin/unzip"];
[unzipTask setCurrentDirectoryPath:[[NSBundle mainBundle] resourcePath]];
[unzipTask setArguments:arguments];
[unzipTask launch];
[unzipTask waitUntilExit];
NSAssert(unzipTask.terminationStatus == 0, @"Must successfully unzipped");
NSString *result = [tempFilePath stringByAppendingPathComponent:_targetAppBundleName];
NSAssert([[NSFileManager defaultManager] fileExistsAtPath:result],
@"App bundle must be unzipped at %@", result);
return result;
}
- (NSString *)notarizedArchivesPath {
NSString *resourePath = [[NSBundle mainBundle] resourcePath];
NSString *notarizedArchivesPath =
[resourePath stringByAppendingPathComponent:@"NotarizedArchives"];
return notarizedArchivesPath;
}
- (NSString *)notarizedArchive {
NSString *bundleVersion =
[[[NSBundle mainBundle] infoDictionary] objectForKey:(id)kCFBundleVersionKey];
NSString *notarizedArchiveBasename =
[NSString stringWithFormat:@"%@-r%@.zip", _appName, bundleVersion];
NSString *notarizedArchive =
[[self notarizedArchivesPath] stringByAppendingPathComponent:notarizedArchiveBasename];
return notarizedArchive;
}
@end

View File

@ -0,0 +1,121 @@
// Copyright (c) 2022 and onwards The McBopomofo Authors.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
import Cocoa
struct ArchiveUtil {
var appName: String
var targetAppBundleName: String
init(appName: String, targetAppBundleName: String) {
self.appName = appName
self.targetAppBundleName = targetAppBundleName
}
// Returns YES if (1) a zip file under
// Resources/NotarizedArchives/$_appName-$bundleVersion.zip exists, and (2) if
// Resources/$_invalidAppBundleName does not exist.
func validateIfNotarizedArchiveExists() -> Bool {
guard let resourePath = Bundle.main.resourcePath,
let notarizedArchivesPath = notarizedArchivesPath,
let notarizedArchive = notarizedArchive,
let notarizedArchivesContent: [String] = try? FileManager.default.subpathsOfDirectory(atPath: notarizedArchivesPath)
else {
return false
}
let devModeAppBundlePath = (resourePath as NSString).appendingPathComponent(targetAppBundleName)
let count = notarizedArchivesContent.count
let notarizedArchiveExists = FileManager.default.fileExists(atPath: notarizedArchive)
let devModeAppBundleExists = FileManager.default.fileExists(atPath: devModeAppBundlePath)
if count > 0 {
if count != 1 || !notarizedArchiveExists || devModeAppBundleExists {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = "Internal Error"
alert.informativeText = "devMode installer, expected archive name: \(notarizedArchive), " +
"archive exists: \(notarizedArchiveExists), devMode app bundle exists: \(devModeAppBundleExists)"
alert.addButton(withTitle: "Terminate")
alert.runModal()
NSApp.terminate(nil)
} else {
return true
}
}
if !devModeAppBundleExists {
let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = "Internal Error"
alert.informativeText = "Dev target bundle does not exist: \(devModeAppBundlePath)"
alert.addButton(withTitle: "Terminate")
alert.runModal()
NSApp.terminate(nil)
}
return false
}
func unzipNotarizedArchive() -> String? {
if !self.validateIfNotarizedArchiveExists() {
return nil
}
guard let notarizedArchive = notarizedArchive,
let resourcePath = Bundle.main.resourcePath else {
return nil
}
let tempFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent(UUID().uuidString)
let arguments: [String] = [notarizedArchive, "-d", tempFilePath]
let unzipTask = Process()
unzipTask.launchPath = "/usr/bin/unzip"
unzipTask.currentDirectoryPath = resourcePath
unzipTask.arguments = arguments
unzipTask.launch()
unzipTask.waitUntilExit()
assert(unzipTask.terminationStatus == 0, "Must successfully unzipped")
guard let result = (tempFilePath as NSString).appendingPathExtension(targetAppBundleName) else {
return nil
}
assert(FileManager.default.fileExists(atPath: result), "App bundle must be unzipped at \(resourcePath).")
return result
}
private var notarizedArchivesPath: String? {
let resourePath = Bundle.main.resourcePath
let notarizedArchivesPath = resourePath?.appending("NotarizedArchives")
return notarizedArchivesPath
}
private var notarizedArchive: String? {
guard let notarizedArchivesPath = notarizedArchivesPath,
let bundleVersion = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String else {
return nil
}
let notarizedArchiveBasename = "\(appName)-r\(bundleVersion).zip"
let notarizedArchive = notarizedArchivesPath.appending(notarizedArchiveBasename)
return notarizedArchive
}
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -81,17 +81,18 @@
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
</items>
<point key="canvasLocation" x="140" y="114"/>
</menu>
<window title="McBopomofo Installer" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="371">
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="335" y="390" width="640" height="360"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="878"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1055"/>
<view key="contentView" id="372">
<rect key="frame" x="0.0" y="0.0" width="640" height="360"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<scrollView horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" id="536">
<scrollView fixedFrame="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="536">
<rect key="frame" x="20" y="60" width="600" height="251"/>
<autoresizingMask key="autoresizingMask"/>
<clipView key="contentView" drawsBackground="NO" id="vR5-yR-zjT">
@ -118,7 +119,7 @@
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<button verticalHuggingPriority="750" imageHugsTitle="YES" id="575">
<button verticalHuggingPriority="750" fixedFrame="YES" imageHugsTitle="YES" translatesAutoresizingMaskIntoConstraints="NO" id="575">
<rect key="frame" x="460" y="12" width="166" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Agree and Install" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="576">
@ -129,7 +130,7 @@
<action selector="agreeAndInstallAction:" target="494" id="708"/>
</connections>
</button>
<button verticalHuggingPriority="750" imageHugsTitle="YES" id="592">
<button verticalHuggingPriority="750" fixedFrame="YES" imageHugsTitle="YES" translatesAutoresizingMaskIntoConstraints="NO" id="592">
<rect key="frame" x="330" y="12" width="130" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="593">
@ -143,7 +144,7 @@ Gw
<action selector="cancelAction:" target="494" id="707"/>
</connections>
</button>
<textField verticalHuggingPriority="750" id="623">
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="623">
<rect key="frame" x="17" y="323" width="559" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Welcome to the McBopomofo Installer! Please read the following Software Licence:" id="624">
@ -152,7 +153,7 @@ Gw
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" id="688">
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="688">
<rect key="frame" x="17" y="19" width="337" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="By installing the software I agree to the terms above." id="689">
@ -181,16 +182,16 @@ Gw
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="gHl-Hx-eQn">
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="283" y="305" width="480" height="180"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1055"/>
<view key="contentView" id="wAe-c8-Vh9">
<rect key="frame" x="0.0" y="0.0" width="480" height="180"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<progressIndicator wantsLayer="YES" maxValue="1" style="bar" id="deb-uT-yNv">
<progressIndicator wantsLayer="YES" fixedFrame="YES" maxValue="1" style="bar" translatesAutoresizingMaskIntoConstraints="NO" id="deb-uT-yNv">
<rect key="frame" x="20" y="67" width="440" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMinY="YES"/>
</progressIndicator>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" id="VDL-Yq-heb">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="VDL-Yq-heb">
<rect key="frame" x="18" y="94" width="444" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Stopping the old version. This may take up to one minute…" id="nTo-dx-qfZ">

View File

@ -21,12 +21,12 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
import Cocoa
import Carbon
@import Cocoa;
public class InstallerUtil: NSObject {
@available(*, unavailable)
public override init() {
super.init()
}
}
NS_ASSUME_NONNULL_BEGIN
// 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/
BOOL appBundleTranslocatedToARandomizedPath(NSString *bundle);
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,30 @@
#import "BundleTranslocate.h"
#import <sys/mount.h>
BOOL appBundleTranslocatedToARandomizedPath(NSString *bundle)
{
const char *bundleAbsPath = [[bundle stringByExpandingTildeInPath] UTF8String];
int entryCount = getfsstat(NULL, 0, 0);
int entrySize = sizeof(struct statfs);
struct statfs *bufs = (struct statfs *)calloc(entryCount, entrySize);
entryCount = getfsstat(bufs, entryCount * entrySize, MNT_NOWAIT);
for (int i = 0; i < entryCount; i++) {
if (!strcmp(bundleAbsPath, bufs[i].f_mntfromname)) {
free(bufs);
// getfsstat() may return us a cached result, and so we need to get the stat of the mounted fs.
// If statfs() returns an error, the mounted fs is already gone.
struct statfs stat;
int checkResult = statfs(bundleAbsPath, &stat);
if (checkResult != 0) {
// Meaning the app's bundle is not mounted, that is it's not translocated.
// It also means that the app is not loaded.
return NO;
}
return YES;
}
}
free(bufs);
return NO;
}

View File

@ -2,3 +2,4 @@
// Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "BundleTranslocate.h"

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="19529" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="19529"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -81,17 +81,18 @@
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
</items>
<point key="canvasLocation" x="141" y="100"/>
</menu>
<window title="小麥注音輸入法安裝程式" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="371">
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="335" y="390" width="640" height="360"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="878"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1055"/>
<view key="contentView" id="372">
<rect key="frame" x="0.0" y="0.0" width="640" height="360"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<scrollView horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" id="536">
<scrollView fixedFrame="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="536">
<rect key="frame" x="20" y="60" width="600" height="251"/>
<autoresizingMask key="autoresizingMask"/>
<clipView key="contentView" drawsBackground="NO" id="c84-Fy-j39">
@ -118,7 +119,7 @@
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<button verticalHuggingPriority="750" imageHugsTitle="YES" id="575">
<button verticalHuggingPriority="750" fixedFrame="YES" imageHugsTitle="YES" translatesAutoresizingMaskIntoConstraints="NO" id="575">
<rect key="frame" x="460" y="12" width="166" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="同意安裝" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="576">
@ -129,7 +130,7 @@
<action selector="agreeAndInstallAction:" target="494" id="708"/>
</connections>
</button>
<button verticalHuggingPriority="750" imageHugsTitle="YES" id="592">
<button verticalHuggingPriority="750" fixedFrame="YES" imageHugsTitle="YES" translatesAutoresizingMaskIntoConstraints="NO" id="592">
<rect key="frame" x="330" y="12" width="130" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="取消" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="593">
@ -143,7 +144,7 @@ Gw
<action selector="cancelAction:" target="494" id="707"/>
</connections>
</button>
<textField verticalHuggingPriority="750" id="623">
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="623">
<rect key="frame" x="17" y="323" width="559" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="歡迎您安裝「小麥注音輸入法」!在安裝之前,請您閱讀本軟體的使用授權:" id="624">
@ -152,7 +153,7 @@ Gw
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" id="688">
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="688">
<rect key="frame" x="17" y="19" width="337" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="點選「同意安裝」,表示您同意本軟體的使用授權。" id="689">
@ -181,20 +182,20 @@ Gw
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="Pxv-O2-I1W">
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="283" y="305" width="480" height="180"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1055"/>
<view key="contentView" id="Qab-7x-ryB">
<rect key="frame" x="0.0" y="0.0" width="480" height="180"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<progressIndicator wantsLayer="YES" maxValue="1" style="bar" id="4v9-yU-zek">
<progressIndicator wantsLayer="YES" fixedFrame="YES" maxValue="1" style="bar" translatesAutoresizingMaskIntoConstraints="NO" id="4v9-yU-zek">
<rect key="frame" x="20" y="67" width="440" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMinY="YES"/>
</progressIndicator>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" id="6mo-HZ-Dbl">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6mo-HZ-Dbl">
<rect key="frame" x="18" y="94" width="444" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="等待舊版完全停用,大約需要一分鐘…" id="T73-Ph-Unt">
<font key="font" size="13" name=".PingFangTC-Regular"/>
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>