Update Installer to support app notarization

Soon notarization will be required for Developer ID apps. This change allows
the Installer to run in two modes. The "dev mode" still builds the IME as
the prerequisite of the Installer and places the IME app bundle inside the
Installer's resources folder. That has been so since the beginning of this
project, and this continues to allow IME developers to test the input method.
On the other hand, if "McBopomofo-r$rev.zip" is placed in the NotarizedArchives
folder and McBopomofo is not built as a dependency of the Installer and the
app bundle is not copied to the resources folder, the Installer then can be
built as a notarizable app (otherwise Xcode wouldn't even let you submit it
for notarization).

To build the distributable Installer, notarize the IME app first, then zip the
app as McBopomofo-r$rev.zip and place that to the NotarizedArchives folder
under Source/Installer. Then build and submit the Installer for notarization.
This is in line with Apple's guideline in
https://developer.apple.com/documentation/xcode/notarizing_your_app_before_distribution/customizing_the_notarization_workflow
("If you distribute your software via a custom third-party installer, you need
two rounds of notarization.")

We don't expect that we make new Installers often enough, and therefore we
don't intend to automate this process via scripting.
This commit is contained in:
Lukhnos Liu 2019-10-28 15:57:57 -07:00
parent 7978a9efba
commit 4f6d1acc43
7 changed files with 227 additions and 6 deletions

View File

@ -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 = "<group>"; };
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>"; };
6A38BBDE15FC117A00A8A51F /* 4_in_5.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = 4_in_5.txt; sourceTree = "<group>"; };
6A38BBDF15FC117A00A8A51F /* 4_in_6.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = 4_in_6.txt; sourceTree = "<group>"; };
6A38BBE015FC117A00A8A51F /* 5_in_6.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = 5_in_6.txt; sourceTree = "<group>"; };
@ -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;

View File

@ -26,10 +26,12 @@
//
#import <Cocoa/Cocoa.h>
#import "ArchiveUtil.h"
@interface AppDelegate : NSWindowController <NSApplicationDelegate>
{
@protected
ArchiveUtil *_archiveUtil;
NSString *_installingVersion;
BOOL _upgrading;
NSButton *_installButton;

View File

@ -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));

View File

@ -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 <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

@ -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<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];
[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

View File

@ -22,6 +22,8 @@
<string>MBIN</string>
<key>CFBundleVersion</key>
<string>805</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSHasLocalizedDisplayName</key>
<true/>
<key>LSMinimumSystemVersion</key>

View File

@ -0,0 +1 @@
Place the notarized archive here for producing the release installer.