diff --git a/McBopomofo.xcodeproj/project.pbxproj b/McBopomofo.xcodeproj/project.pbxproj index c6f93399..f154e946 100644 --- a/McBopomofo.xcodeproj/project.pbxproj +++ b/McBopomofo.xcodeproj/project.pbxproj @@ -29,6 +29,8 @@ 6A0D4F5715FC0EF900ABF4B3 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6A0D4F4815FC0EE100ABF4B3 /* InfoPlist.strings */; }; 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 */; }; 6A38BC1515FC117A00A8A51F /* data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A38BBF615FC117A00A8A51F /* data.txt */; }; 6A38BC1D15FC11C700A8A51F /* UpdateNotificationController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6A38BC1F15FC11C700A8A51F /* UpdateNotificationController.xib */; }; 6A38BC2815FC158A00A8A51F /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A38BC2715FC158A00A8A51F /* InputMethodKit.framework */; }; @@ -156,6 +158,9 @@ 6A15B32621A51F2300B92CD3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/UpdateNotificationController.xib; sourceTree = ""; }; 6A15B32721A51F2300B92CD3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Source/Base.lproj/preferences.xib; sourceTree = ""; }; 6A187E2916004C7300466B2E /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = "zh-Hant"; path = "zh-Hant.lproj/MainMenu.xib"; sourceTree = ""; }; + 6A225A1E23679F2600F685C6 /* NotarizedArchives */ = {isa = PBXFileReference; lastKnownFileType = folder; path = NotarizedArchives; sourceTree = ""; }; + 6A225A212367A1D700F685C6 /* ArchiveUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ArchiveUtil.h; sourceTree = ""; }; + 6A225A222367A1D700F685C6 /* ArchiveUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ArchiveUtil.m; sourceTree = ""; }; 6A38BBDE15FC117A00A8A51F /* 4_in_5.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = 4_in_5.txt; sourceTree = ""; }; 6A38BBDF15FC117A00A8A51F /* 4_in_6.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = 4_in_6.txt; sourceTree = ""; }; 6A38BBE015FC117A00A8A51F /* 5_in_6.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = 5_in_6.txt; sourceTree = ""; }; @@ -471,6 +476,9 @@ 6ACA41E715FC1D9000935EF6 /* Installer */ = { isa = PBXGroup; children = ( + 6A225A212367A1D700F685C6 /* ArchiveUtil.h */, + 6A225A222367A1D700F685C6 /* ArchiveUtil.m */, + 6A225A1E23679F2600F685C6 /* NotarizedArchives */, 6ACA41E815FC1D9000935EF6 /* AppDelegate.h */, 6ACA41E915FC1D9000935EF6 /* AppDelegate.m */, 6ACA41EA15FC1D9000935EF6 /* InfoPlist.strings */, @@ -529,6 +537,7 @@ 6ACA41C715FC1D7500935EF6 /* Sources */, 6ACA41C815FC1D7500935EF6 /* Frameworks */, 6ACA41C915FC1D7500935EF6 /* Resources */, + 6A225A2023679F5F00F685C6 /* ShellScript */, ); buildRules = ( ); @@ -553,6 +562,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, "zh-Hant", Base, @@ -598,6 +608,7 @@ files = ( 6ACA420215FC1E5200935EF6 /* McBopomofo.app in Resources */, 6ACA41FA15FC1D9000935EF6 /* InfoPlist.strings in Resources */, + 6A225A1F23679F2600F685C6 /* NotarizedArchives in Resources */, 6ACA41FB15FC1D9000935EF6 /* License.rtf in Resources */, 6ACA41FC15FC1D9000935EF6 /* Localizable.strings in Resources */, 6ACA41FD15FC1D9000935EF6 /* MainMenu.xib in Resources */, @@ -607,6 +618,26 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 6A225A2023679F5F00F685C6 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Remove the README.md in the NotarizedArchives directory\nrm -f \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/NotarizedArchives/README.md\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 6A0D4E9E15FC0D2D00ABF4B3 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -634,6 +665,7 @@ buildActionMask = 2147483647; files = ( 6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */, + 6A225A232367A1D700F685C6 /* ArchiveUtil.m in Sources */, 6ACA41FF15FC1D9000935EF6 /* main.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Source/Installer/AppDelegate.h b/Source/Installer/AppDelegate.h index 8f95771b..ebcf8b69 100644 --- a/Source/Installer/AppDelegate.h +++ b/Source/Installer/AppDelegate.h @@ -26,10 +26,12 @@ // #import +#import "ArchiveUtil.h" @interface AppDelegate : NSWindowController { @protected + ArchiveUtil *_archiveUtil; NSString *_installingVersion; BOOL _upgrading; NSButton *_installButton; diff --git a/Source/Installer/AppDelegate.m b/Source/Installer/AppDelegate.m index af70dc04..f8e01341 100644 --- a/Source/Installer/AppDelegate.m +++ b/Source/Installer/AppDelegate.m @@ -58,6 +58,7 @@ void RunAlertPanel(NSString *title, NSString *message, NSString *buttonTitle) { - (void)dealloc { + [_archiveUtil release]; [_installingVersion release]; [_translocationRemovalStartTime release]; [super dealloc]; @@ -65,6 +66,12 @@ void RunAlertPanel(NSString *title, NSString *message, NSString *buttonTitle) { - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + _installingVersion = [[[[NSBundle mainBundle] infoDictionary] objectForKey:(id)kCFBundleVersionKey] retain]; + 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]]; @@ -75,11 +82,7 @@ void RunAlertPanel(NSString *title, NSString *message, NSString *buttonTitle) { [mutableAttrStr addAttribute:NSForegroundColorAttributeName value:[NSColor controlTextColor] range:NSMakeRange(0, [mutableAttrStr length])]; [[self.textView textStorage] setAttributedString:mutableAttrStr]; [self.textView setSelectedRange:NSMakeRange(0, 0)]; - - NSBundle *installingBundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:kTargetBin ofType:kTargetType]]; - _installingVersion = [[[installingBundle infoDictionary] objectForKey:(id)kCFBundleVersionKey] retain]; - NSString *versionString = [[installingBundle infoDictionary] objectForKey:@"CFBundleShortVersionString"]; - + [[self window] setTitle:[NSString stringWithFormat:NSLocalizedString(@"%@ (for version %@, r%@)", nil), [[self window] title], versionString, _installingVersion]]; if ([[NSFileManager defaultManager] fileExistsAtPath:[kTargetPartialPath stringByExpandingTildeInPath]]) { @@ -170,7 +173,13 @@ void RunAlertPanel(NSString *title, NSString *message, NSString *buttonTitle) { - (void)installInputMethodWithWarning:(BOOL)warning { - NSTask *cpTask = [NSTask launchedTaskWithLaunchPath:@"/bin/cp" arguments:[NSArray arrayWithObjects:@"-R", [[NSBundle mainBundle] pathForResource:kTargetBin ofType:kTargetType], [kDestinationPartial stringByExpandingTildeInPath], nil]]; + // 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)); diff --git a/Source/Installer/ArchiveUtil.h b/Source/Installer/ArchiveUtil.h new file mode 100644 index 00000000..aab19c61 --- /dev/null +++ b/Source/Installer/ArchiveUtil.h @@ -0,0 +1,39 @@ +// Copyright (c) 2011-2019 The McBopomofo Project. +// +// 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 + +@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 diff --git a/Source/Installer/ArchiveUtil.m b/Source/Installer/ArchiveUtil.m new file mode 100644 index 00000000..2cd15003 --- /dev/null +++ b/Source/Installer/ArchiveUtil.m @@ -0,0 +1,136 @@ +// Copyright (c) 2011-2019 The McBopomofo Project. +// +// 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 retain]; + _targetAppBundleName = [targetAppBundleName retain]; + } + return self; +} + +- (void)delloc { + [_appName release]; + [_targetAppBundleName release]; + [super dealloc]; +} + +- (BOOL)validateIfNotarizedArchiveExists { + NSString *resourePath = [[NSBundle mainBundle] resourcePath]; + NSString *devModeAppBundlePath = + [resourePath stringByAppendingPathComponent:_targetAppBundleName]; + + NSArray *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]; + [alert autorelease]; + + [[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]; + [alert autorelease]; + + [[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 diff --git a/Source/Installer/Installer-Info.plist b/Source/Installer/Installer-Info.plist index f13f486f..11569f7e 100644 --- a/Source/Installer/Installer-Info.plist +++ b/Source/Installer/Installer-Info.plist @@ -22,6 +22,8 @@ MBIN CFBundleVersion 805 + LSApplicationCategoryType + public.app-category.utilities LSHasLocalizedDisplayName LSMinimumSystemVersion diff --git a/Source/Installer/NotarizedArchives/README.md b/Source/Installer/NotarizedArchives/README.md new file mode 100644 index 00000000..a2f530c8 --- /dev/null +++ b/Source/Installer/NotarizedArchives/README.md @@ -0,0 +1 @@ +Place the notarized archive here for producing the release installer.