Files
Sparkle/sparkle-cli/main.m
Zorg 5343305e3f Report friendly error if installation fails with no write permission (#2157)
On macOS 13 (Ventura) and later, installation could fail if Gatekeeper blocks app modifications. Note this is only applicable for external updaters like sparkle-cli where the updater and target bundle to update have different team identifiers.
2022-06-12 18:50:40 -07:00

201 lines
12 KiB
Objective-C

//
// main.m
// sparkle-cli
//
// Created by Mayur Pawashe on 4/10/16.
// Copyright © 2016 Sparkle Project. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <Sparkle/Sparkle.h>
#import "SPUCommandLineDriver.h"
#include <getopt.h>
#define APPLICATION_FLAG "application"
#define DEFER_FLAG "defer-install"
#define VERBOSE_FLAG "verbose"
#define CHECK_NOW_FLAG "check-immediately"
#define GRANT_AUTOMATIC_CHECKING_FLAG "grant-automatic-checks"
#define SEND_PROFILE_FLAG "send-profile"
#define PROBE_FLAG "probe"
#define INTERACTIVE_FLAG "interactive"
#define FEED_URL_FLAG "feed-url"
#define CHANNELS_FLAG "channels"
#define ALLOW_MAJOR_UPGRADES_FLAG "allow-major-upgrades"
#define USER_AGENT_NAME "user-agent-name"
static void printUsage(char **argv)
{
fprintf(stderr, "Usage: %s bundle [--%s app-path] [--%s] [--%s] [--%s chan1,chan2,…] [--%s feed-url] [--%s display-name] [--%s] [--%s] [--%s] [--%s] [--%s] [--%s]\n", argv[0], APPLICATION_FLAG, CHECK_NOW_FLAG, PROBE_FLAG, CHANNELS_FLAG, FEED_URL_FLAG, USER_AGENT_NAME, GRANT_AUTOMATIC_CHECKING_FLAG, SEND_PROFILE_FLAG, DEFER_FLAG, INTERACTIVE_FLAG, ALLOW_MAJOR_UPGRADES_FLAG, VERBOSE_FLAG);
fprintf(stderr, "Description:\n");
fprintf(stderr, " Check if any new updates for a Sparkle supported bundle need to be installed.\n\n");
fprintf(stderr, " If any new updates need to be installed, the user application\n is terminated and the update is installed immediately unless --%s\n is specified. If the application was alive, then it will be relaunched after.\n\n", DEFER_FLAG);
fprintf(stderr, " To check if an update is available without installing, use --%s.\n\n", PROBE_FLAG);
fprintf(stderr, " if no updates are available now, or if the last update check was recently\n (unless --%s is specified) then nothing is done.\n\n", CHECK_NOW_FLAG);
fprintf(stderr, " If update permission is requested and --%s is not\n specified, then checking for updates is aborted.\n\n", GRANT_AUTOMATIC_CHECKING_FLAG);
fprintf(stderr, " Unless --%s is specified, this tool will not request for escalated\n authorization. Alternatively, this tool can be run as root under an active user login\n session, which will not require (and disallow) interaction.\n\n", INTERACTIVE_FLAG);
fprintf(stderr, " If --%s is specified, this tool will exit leaving a spawned process\n for finishing the installation after the target application terminates.\n\n", DEFER_FLAG);
fprintf(stderr, " If update installation fails due to not having permission (e.g. from Gatekeeper) to replace the old bundle, an exit status of 8 is returned.\n");
fprintf(stderr, " Please specify --%s if you intend to use this tool in an automated way.\n", USER_AGENT_NAME);
fprintf(stderr, "Options:\n");
fprintf(stderr, " --%s\n Path to the application to watch for termination and to relaunch.\n If not provided, this is assumed to be the same as the bundle.\n", APPLICATION_FLAG);
fprintf(stderr, " --%s\n Immediately checks for updates to install.\n Without this, updates are checked only when needed on a scheduled basis.\n", CHECK_NOW_FLAG);
fprintf(stderr, " --%s\n Probe for updates. Check if any updates are available but do not install.\n An exit status of 0 is returned if a new update is available.\n", PROBE_FLAG);
fprintf(stderr, " --%s\n Allows probing and installing major upgrades. Without passing this, an exit\n status of 2 is returned if a major upgrade is found.\n", ALLOW_MAJOR_UPGRADES_FLAG);
fprintf(stderr, " --%s\n List of allowed Sparkle channels to look for updates in. By default,\n only the default channel is used.\n", CHANNELS_FLAG);
fprintf(stderr, " --%s\n URL for appcast feed. This URL will be used for the feed instead of the one\n in the bundle's Info.plist or in the bundle's user defaults.\n", FEED_URL_FLAG);
fprintf(stderr, " --%s\n Display name that will be included as a part of the User-Agent string.\n We encourage setting this so developers know what is querying their feed.\n Otherwise, this value may be set and inferred automatically.\n", USER_AGENT_NAME);
fprintf(stderr, " --%s\n Allows prompting the user for an authorization dialog prompt if the\n installer needs elevated privileges, or allows performing an interactive\n installer package. Without passing this, an exit status of 3 is returned\n if an update requires user interaction. An exit status of 5 is returned\n if the user cancels the authorization prompt.\n", INTERACTIVE_FLAG);
fprintf(stderr, " --%s\n If update permission is requested, this enables automatic update checks.\n Note that this behavior may overwrite the user's defaults for the bundle.\n This option has no effect if --%s is passed, or if the\n user has replied to this request already, or if the developer configured\n to skip it. Without passing this, an exit status of 6 is returned\n if permission is needed.\n", GRANT_AUTOMATIC_CHECKING_FLAG, CHECK_NOW_FLAG);
fprintf(stderr, " --%s\n Choose to send system profile information if update permission is requested.\n This option can only take effect if --%s is passed.\n", SEND_PROFILE_FLAG, GRANT_AUTOMATIC_CHECKING_FLAG);
fprintf(stderr, " --%s\n Defer installation until after the application terminates on its own. The\n application will not be relaunched unless the installation is resumed later.\n", DEFER_FLAG);
fprintf(stderr, " --%s\n Enable verbose logging.\n", VERBOSE_FLAG);
}
int main(int argc, char **argv)
{
@autoreleasepool
{
struct option longOptions[] = {
{APPLICATION_FLAG, required_argument, NULL, 0},
{CHANNELS_FLAG, required_argument, NULL, 0},
{FEED_URL_FLAG, required_argument, NULL, 0},
{USER_AGENT_NAME, required_argument, NULL, 0},
{DEFER_FLAG, no_argument, NULL, 0},
{VERBOSE_FLAG, no_argument, NULL, 0},
{CHECK_NOW_FLAG, no_argument, NULL, 0},
{GRANT_AUTOMATIC_CHECKING_FLAG, no_argument, NULL, 0},
{SEND_PROFILE_FLAG, no_argument, NULL, 0},
{PROBE_FLAG, no_argument, NULL, 0},
{INTERACTIVE_FLAG, no_argument, NULL, 0},
{ALLOW_MAJOR_UPGRADES_FLAG, no_argument, NULL, 0},
{0, 0, 0, 0}
};
NSString *applicationPath = nil;
NSString *feedURL = nil;
NSString *userAgentName = nil;
NSSet<NSString *> *channels = [NSSet set];
BOOL deferInstall = NO;
BOOL verbose = NO;
BOOL checkForUpdatesNow = NO;
BOOL grantAutomaticChecking = NO;
BOOL sendProfile = NO;
BOOL probeForUpdates = NO;
BOOL interactive = NO;
BOOL allowMajorUpgrades = NO;
while (YES) {
int optionIndex = 0;
int choice = getopt_long(argc, argv, "", longOptions, &optionIndex);
if (choice == -1) {
break;
}
switch (choice) {
case 0:
if (strcmp(APPLICATION_FLAG, longOptions[optionIndex].name) == 0) {
assert(optarg != NULL);
applicationPath = [[NSString alloc] initWithUTF8String:optarg];
if (applicationPath == nil) {
printUsage(argv);
return EXIT_FAILURE;
}
} else if (strcmp(FEED_URL_FLAG, longOptions[optionIndex].name) == 0) {
assert(optarg != NULL);
feedURL = [[NSString alloc] initWithUTF8String:optarg];
if (feedURL == nil) {
printUsage(argv);
return EXIT_FAILURE;
}
} else if (strcmp(USER_AGENT_NAME, longOptions[optionIndex].name) == 0) {
assert(optarg != NULL);
userAgentName = [[NSString alloc] initWithUTF8String:optarg];
if (userAgentName == nil) {
printUsage(argv);
return EXIT_FAILURE;
}
} else if (strcmp(CHANNELS_FLAG, longOptions[optionIndex].name) == 0) {
assert(optarg != NULL);
NSString *channelsString = [[NSString alloc] initWithUTF8String:optarg];
if (channelsString == nil) {
printUsage(argv);
return EXIT_FAILURE;
}
if (channelsString.length > 0) {
channels = [NSSet setWithArray:[channelsString componentsSeparatedByString:@","]];
}
} else if (strcmp(DEFER_FLAG, longOptions[optionIndex].name) == 0) {
deferInstall = YES;
} else if (strcmp(VERBOSE_FLAG, longOptions[optionIndex].name) == 0) {
verbose = YES;
} else if (strcmp(CHECK_NOW_FLAG, longOptions[optionIndex].name) == 0) {
checkForUpdatesNow = YES;
} else if (strcmp(GRANT_AUTOMATIC_CHECKING_FLAG, longOptions[optionIndex].name) == 0) {
grantAutomaticChecking = YES;
} else if (strcmp(SEND_PROFILE_FLAG, longOptions[optionIndex].name) == 0) {
sendProfile = YES;
} else if (strcmp(PROBE_FLAG, longOptions[optionIndex].name) == 0) {
probeForUpdates = YES;
} else if (strcmp(INTERACTIVE_FLAG, longOptions[optionIndex].name) == 0) {
interactive = YES;
} else if (strcmp(ALLOW_MAJOR_UPGRADES_FLAG, longOptions[optionIndex].name) == 0) {
allowMajorUpgrades = YES;
}
case ':':
break;
case '?':
printUsage(argv);
return EXIT_FAILURE;
default:
abort();
}
}
if (optind >= argc) {
printUsage(argv);
return EXIT_FAILURE;
}
NSString *updatePath = [[NSString alloc] initWithUTF8String:argv[optind]];
if (updatePath == nil) {
printUsage(argv);
return EXIT_FAILURE;
}
if (probeForUpdates && (applicationPath != nil || deferInstall || checkForUpdatesNow || interactive)) {
fprintf(stderr, "Error: --%s does not work together with --%s, --%s, --%s, --%s\n", PROBE_FLAG, APPLICATION_FLAG, DEFER_FLAG, CHECK_NOW_FLAG, INTERACTIVE_FLAG);
return EXIT_FAILURE;
}
if (interactive && geteuid() == 0) {
fprintf(stderr, "Error: --%s is not supported when running as root\n", INTERACTIVE_FLAG);
return EXIT_FAILURE;
}
SUUpdatePermissionResponse *updatePermissionResponse = nil;
if (grantAutomaticChecking) {
updatePermissionResponse = [[SUUpdatePermissionResponse alloc] initWithAutomaticUpdateChecks:YES sendSystemProfile:sendProfile];
}
SPUCommandLineDriver *driver = [[SPUCommandLineDriver alloc] initWithUpdateBundlePath:updatePath applicationBundlePath:applicationPath allowedChannels:channels customFeedURL:feedURL userAgentName:userAgentName updatePermissionResponse:updatePermissionResponse deferInstallation:deferInstall interactiveInstallation:interactive allowMajorUpgrades:allowMajorUpgrades verbose:verbose];
if (driver == nil) {
fprintf(stderr, "Error: Failed to initialize updater. Are the bundle paths provided valid?\n");
return EXIT_FAILURE;
}
if (probeForUpdates) {
[driver probeForUpdates];
} else {
[driver runAndCheckForUpdatesNow:checkForUpdatesNow];
}
[[NSRunLoop currentRunLoop] run];
}
return EXIT_SUCCESS;
}