Skip to content

Instantly share code, notes, and snippets.

@Alexandro1112
Created January 22, 2023 13:02
Show Gist options
  • Save Alexandro1112/6ab23e2ca49fc30480479d493477c264 to your computer and use it in GitHub Desktop.
Save Alexandro1112/6ab23e2ca49fc30480479d493477c264 to your computer and use it in GitHub Desktop.
Updated version of lib
This gist exceeds the recommended number of files (~10). To access all files, please clone this gist.
[submodule "blueutil"]
path = blueutil
url = https://github.com/toy/blueutil
[submodule "brightness"]
path = brightness
url = https://github.com/nriley/brightness
[submodule "brew"]
path = brew
url = https://github.com/Homebrew/brew
from .info import *
from.CONSTANTS import *
# ---------------------- #
from sys import platform
if platform == 'linux':
from .linux_terminal import *
else:
from .mac_terminal import *
# -------------------------- #
---
Language: ObjC
BasedOnStyle: google
AlignAfterOpenBracket: DontAlign
AlignOperands: false
AllowAllArgumentsOnNextLine: false
AllowShortFunctionsOnASingleLine: Inline
BinPackArguments: false
BreakStringLiterals: false
ColumnLimit: 120
ConstructorInitializerIndentWidth: 2
ContinuationIndentWidth: 2
NamespaceIndentation: All
ObjCSpaceAfterProperty: true
SpacesInContainerLiterals: false
TabWidth: 2
...
/blueutil
/pkg/
build/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspective
*.perspectivev3
project.xcworkspace/
xcuserdata/
// blueutil
//
// CLI for bluetooth on OSX: power, discoverable state, list, inquire devices, connect, info, …
// Uses private API from IOBluetooth framework (i.e. IOBluetoothPreference*()).
// https://github.com/toy/blueutil
//
// Originally written by Frederik Seiffert <[email protected]> http://www.frederikseiffert.de/blueutil/
//
// Copyright (c) 2011-2023 Ivan Kuchin. See <LICENSE.txt> for details.
#define VERSION "2.9.1"
#import <IOBluetooth/IOBluetooth.h>
#include <getopt.h>
#include <regex.h>
#include <sysexits.h>
// https://stackoverflow.com/a/12648993/96823
#define STRINGIFY_(x) #x
#define STRINGIFY(x) STRINGIFY_(x)
#define eprintf(...) fprintf(stderr, ##__VA_ARGS__)
void *assert_alloc(void *pointer) {
if (pointer == NULL) {
eprintf("%s\n", strerror(errno));
exit(EX_OSERR);
}
return pointer;
}
int assert_reg(int errcode, const regex_t *restrict preg, char *reason) {
if (errcode == 0 || errcode == REG_NOMATCH) return errcode;
size_t errbuf_size = regerror(errcode, preg, NULL, 0);
char *restrict errbuf = assert_alloc(malloc(errbuf_size));
regerror(errcode, preg, errbuf, errbuf_size);
eprintf("%s: %s\n", reason, errbuf);
exit(EX_SOFTWARE);
}
// private methods
int IOBluetoothPreferencesAvailable();
int IOBluetoothPreferenceGetControllerPowerState();
void IOBluetoothPreferenceSetControllerPowerState(int state);
int IOBluetoothPreferenceGetDiscoverableState();
void IOBluetoothPreferenceSetDiscoverableState(int state);
// short names
typedef int (*GetterFunc)();
typedef bool (*SetterFunc)(int);
enum state {
toggle = -1,
off = 0,
on = 1,
};
bool BTSetParamState(enum state state, GetterFunc getter, void (*setter)(int), const char *name) {
if (state == toggle) state = !getter();
if (state == getter()) return true;
setter(state);
for (int i = 0; i <= 100; i++) {
if (i) usleep(100000);
if (state == getter()) return true;
}
eprintf("Failed to switch bluetooth %s %s in 10 seconds\n", name, state ? "on" : "off");
return false;
}
#define BTAvaliable IOBluetoothPreferencesAvailable
#define BTPowerState IOBluetoothPreferenceGetControllerPowerState
bool BTSetPowerState(enum state state) {
return BTSetParamState(state, BTPowerState, IOBluetoothPreferenceSetControllerPowerState, "power");
}
#define BTDiscoverableState IOBluetoothPreferenceGetDiscoverableState
bool BTSetDiscoverableState(enum state state) {
return BTSetParamState(state, BTDiscoverableState, IOBluetoothPreferenceSetDiscoverableState, "discoverable");
}
void check_power_on_for(const char *command) {
if (BTPowerState()) return;
eprintf("Power is required to be on for %s command\n", command);
}
void usage(FILE *io) {
static const char *lines[] = {
"blueutil v" VERSION,
"",
"Usage:",
" blueutil [options]",
"",
"Without options outputs current state",
"",
" -p, --power output power state as 1 or 0",
" -p, --power STATE set power state",
" -d, --discoverable output discoverable state as 1 or 0",
" -d, --discoverable STATE set discoverable state",
"",
" --favourites, --favorites",
" list favourite devices",
" --inquiry [T] inquiry devices in range, 10 seconds duration by default excluding time for name updates",
" --paired list paired devices",
" --recent [N] list recently used devices, 10 by default, 0 to list all",
" --connected list connected devices",
"",
" --info ID show information about device",
" --is-connected ID connected state of device as 1 or 0",
" --connect ID create a connection to device",
" --disconnect ID close the connection to device",
" --pair ID [PIN] pair with device, optional PIN of up to 16 characters will be used instead of interactive input if requested in specific pair mode",
" --unpair ID EXPERIMENTAL unpair the device",
" --add-favourite ID, --add-favorite ID",
" add to favourites",
" --remove-favourite ID, --remove-favorite ID",
" remove from favourites",
"",
" --format FORMAT change output format of info and all listing commands",
"",
" --wait-connect ID [TIMEOUT]",
" EXPERIMENTAL wait for device to connect",
" --wait-disconnect ID [TIMEOUT]",
" EXPERIMENTAL wait for device to disconnect",
" --wait-rssi ID OP VALUE [PERIOD [TIMEOUT]]",
" EXPERIMENTAL wait for device RSSI value which is 0 for golden range, -129 if it cannot be read (e.g. device is disconnected)",
"",
" -h, --help this help",
" -v, --version show version",
"",
"STATE can be one of: 1, on, 0, off, toggle",
"ID can be either address in form xxxxxxxxxxxx, xx-xx-xx-xx-xx-xx or xx:xx:xx:xx:xx:xx, or name of device to search in used devices",
"OP can be one of: >, >=, <, <=, =, !=; or equivalents: gt, ge, lt, le, eq, ne",
"PERIOD is in seconds, defaults to 1",
"TIMEOUT is in seconds, default value 0 doesn't add timeout",
"FORMAT can be one of:",
" default - human readable text output not intended for consumption by scripts",
" new-default - human readable comma separated key-value pairs (EXPERIMENTAL, THE BEHAVIOUR MAY CHANGE)",
" json - compact JSON",
" json-pretty - pretty printed JSON",
"",
"Due to possible problems, blueutil will refuse to run as root user (see https://github.com/toy/blueutil/issues/41).",
"Use environment variable BLUEUTIL_ALLOW_ROOT=1 to override (sudo BLUEUTIL_ALLOW_ROOT=1 blueutil …).",
"",
"Exit codes:",
" " STRINGIFY(EXIT_SUCCESS) " Success",
" " STRINGIFY(EXIT_FAILURE) " General failure",
" " STRINGIFY(EX_USAGE) " Wrong usage like missing or unexpected arguments, wrong parameters",
" " STRINGIFY(EX_UNAVAILABLE) " Bluetooth or interface not available",
" " STRINGIFY(EX_SOFTWARE) " Internal error",
" " STRINGIFY(EX_OSERR) " System error like shortage of memory",
" " STRINGIFY(EX_TEMPFAIL) " Timeout error",
};
for (size_t i = 0, _i = sizeof(lines) / sizeof(lines[0]); i < _i; i++) {
fprintf(io, "%s\n", lines[i]);
}
}
char *next_arg(int argc, char *argv[], bool required) {
if (optind < argc && NULL != argv[optind] && (required || '-' != argv[optind][0])) {
return argv[optind++];
} else {
return NULL;
}
}
char *next_reqarg(int argc, char *argv[]) {
return next_arg(argc, argv, true);
}
char *next_optarg(int argc, char *argv[]) {
return next_arg(argc, argv, false);
}
// getopt_long doesn't consume optional argument separated by space
// https://stackoverflow.com/a/32575314
void extend_optarg(int argc, char *argv[]) {
if (!optarg) optarg = next_optarg(argc, argv);
}
bool parse_state_arg(char *arg, enum state *state) {
if (0 == strcmp(arg, "1") || 0 == strcasecmp(arg, "on")) {
*state = on;
return true;
}
if (0 == strcmp(arg, "0") || 0 == strcasecmp(arg, "off")) {
*state = off;
return true;
}
if (0 == strcasecmp(arg, "toggle")) {
*state = toggle;
return true;
}
return false;
}
bool check_device_address_arg(char *arg) {
regex_t regex;
int result;
result = regcomp(&regex,
"^[0-9a-f]{2}([0-9a-f]{10}|(-[0-9a-f]{2}){5}|(:[0-9a-f]{2}){5})$",
REG_EXTENDED | REG_ICASE | REG_NOSUB);
assert_reg(result, &regex, "Compiling device address regex");
result = regexec(&regex, arg, 0, NULL, 0);
assert_reg(result, &regex, "Matching device address regex");
regfree(&regex);
return result == 0;
}
bool parse_unsigned_long_arg(char *arg, unsigned long *number) {
regex_t regex;
int result;
result = regcomp(&regex, "^[[:digit:]]+$", REG_EXTENDED | REG_NOSUB);
assert_reg(result, &regex, "Compiling number regex");
result = regexec(&regex, arg, 0, NULL, 0);
assert_reg(result, &regex, "Matching number regex");
regfree(&regex);
if (result == 0) {
*number = strtoul(arg, NULL, 10);
return true;
} else {
return false;
}
}
bool parse_signed_long_arg(char *arg, long *number) {
regex_t regex;
int result;
result = regcomp(&regex, "^-?[[:digit:]]+$", REG_EXTENDED | REG_NOSUB);
assert_reg(result, &regex, "Compiling number regex");
result = regexec(&regex, arg, 0, NULL, 0);
assert_reg(result, &regex, "Matching number regex");
regfree(&regex);
if (result == 0) {
*number = strtol(arg, NULL, 10);
return true;
} else {
return false;
}
}
IOBluetoothDevice *get_device(char *id) {
NSString *nsId = [NSString stringWithCString:id encoding:[NSString defaultCStringEncoding]];
IOBluetoothDevice *device = nil;
if (check_device_address_arg(id)) {
device = [IOBluetoothDevice deviceWithAddressString:nsId];
if (!device) {
eprintf("Device not found by address: %s\n", id);
exit(EXIT_FAILURE);
}
} else {
NSArray *recentDevices = [IOBluetoothDevice recentDevices:0];
if (!recentDevices) {
eprintf("No recent devices to search for: %s\n", id);
exit(EXIT_FAILURE);
}
NSArray *byName = [recentDevices filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"name == %@", nsId]];
if (byName.count > 0) {
device = byName.firstObject;
}
if (!device) {
eprintf("Device not found by name: %s\n", id);
exit(EXIT_FAILURE);
}
}
return device;
}
void list_devices_default(NSArray *devices, bool first_only) {
for (IOBluetoothDevice *device in devices) {
printf("address: %s", [[device addressString] UTF8String]);
if ([device isConnected]) {
printf(", connected (%s, %d dBm)", [device isIncoming] ? "slave" : "master", [device rawRSSI]);
} else {
printf(", not connected");
}
printf(", %s", [device isFavorite] ? "favourite" : "not favourite");
printf(", %s", [device isPaired] ? "paired" : "not paired");
printf(", name: \"%s\"", [device name] ? [[device name] UTF8String] : "-");
printf(", recent access date: %s",
[device recentAccessDate] ? [[[device recentAccessDate] description] UTF8String] : "-");
printf("\n");
if (first_only) break;
}
}
void list_devices_new_default(NSArray *devices, bool first_only) {
const char *separator = first_only ? "\n" : ", ";
for (IOBluetoothDevice *device in devices) {
printf("address: %s%s", [[device addressString] UTF8String], separator);
printf("recent access: %s%s",
[device recentAccessDate] ? [[[device recentAccessDate] description] UTF8String] : "-",
separator);
printf("favourite: %s%s", [device isFavorite] ? "yes" : "no", separator);
printf("paired: %s%s", [device isPaired] ? "yes" : "no", separator);
printf("connected: %s%s", [device isConnected] ? ([device isIncoming] ? "slave" : "master") : "no", separator);
printf("rssi: %s%s",
[device isConnected] ? [[NSString stringWithFormat:@"%d", [device RSSI]] UTF8String] : "-",
separator);
printf("raw rssi: %s%s",
[device isConnected] ? [[NSString stringWithFormat:@"%d", [device rawRSSI]] UTF8String] : "-",
separator);
printf("name: %s\n", [device name] ? [[device name] UTF8String] : "-");
if (first_only) break;
}
}
void list_devices_json(NSArray *devices, bool first_only, bool pretty) {
NSMutableArray *descriptions = [NSMutableArray arrayWithCapacity:[devices count]];
@autoreleasepool {
// https://stackoverflow.com/a/16254918/96823
NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
[dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
[dateFormatter setCalendar:[NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian]];
[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZZZ"];
for (IOBluetoothDevice *device in devices) {
NSMutableDictionary *description = [NSMutableDictionary dictionaryWithDictionary:@{
@"address": [device addressString],
@"name": [device name] ? [device name] : [NSNull null],
@"recentAccessDate": [device recentAccessDate] ? [dateFormatter stringFromDate:[device recentAccessDate]]
: [NSNull null],
@"favourite": [device isFavorite] ? @(YES) : @(NO),
@"paired": [device isPaired] ? @(YES) : @(NO),
@"connected": [device isConnected] ? @(YES) : @(NO),
}];
if ([device isConnected]) {
description[@"slave"] = [device isIncoming] ? @(YES) : @(NO);
description[@"RSSI"] = [NSNumber numberWithChar:[device RSSI]];
description[@"rawRSSI"] = [NSNumber numberWithChar:[device rawRSSI]];
}
[descriptions addObject:description];
}
}
NSOutputStream *stdout = [NSOutputStream outputStreamToFileAtPath:@"/dev/stdout" append:NO];
[stdout open];
id object = first_only ? [descriptions firstObject] : descriptions;
NSJSONWritingOptions options = pretty ? NSJSONWritingPrettyPrinted : 0;
[NSJSONSerialization writeJSONObject:object toStream:stdout options:options error:NULL];
if (pretty) {
[stdout write:(const uint8_t *)"\n" maxLength:1];
}
[stdout close];
}
void list_devices_json_default(NSArray *devices, bool first_only) {
list_devices_json(devices, first_only, false);
}
void list_devices_json_pretty(NSArray *devices, bool first_only) {
list_devices_json(devices, first_only, true);
}
typedef void (*FormatterFunc)(NSArray *, bool);
bool parse_output_formatter(char *arg, FormatterFunc *formatter) {
if (0 == strcasecmp(arg, "default")) {
*formatter = list_devices_default;
return true;
}
if (0 == strcasecmp(arg, "new-default")) {
*formatter = list_devices_new_default;
return true;
}
if (0 == strcasecmp(arg, "json")) {
*formatter = list_devices_json_default;
return true;
}
if (0 == strcasecmp(arg, "json-pretty")) {
*formatter = list_devices_json_pretty;
return true;
}
return false;
}
@interface DeviceInquiryRunLoopStopper : NSObject <IOBluetoothDeviceInquiryDelegate>
@end
@implementation DeviceInquiryRunLoopStopper
- (void)deviceInquiryComplete:(__unused IOBluetoothDeviceInquiry *)sender
error:(__unused IOReturn)error
aborted:(__unused BOOL)aborted {
CFRunLoopStop(CFRunLoopGetCurrent());
}
@end
static inline bool is_caseabbr(const char *name, const char *str) {
size_t length = strlen(str);
if (length < 1) length = 1;
return strncasecmp(name, str, length) == 0;
}
const char *hci_error_descriptions[] = {
[0x01] = "Unknown HCI Command",
[0x02] = "No Connection",
[0x03] = "Hardware Failure",
[0x04] = "Page Timeout",
[0x05] = "Authentication Failure",
[0x06] = "Key Missing",
[0x07] = "Memory Full",
[0x08] = "Connection Timeout",
[0x09] = "Max Number of Connections",
[0x0a] = "Max Number of SCO Connections to a Device",
[0x0b] = "ACL Connection Already Exists",
[0x0c] = "Command Disallowed",
[0x0d] = "Host Rejected Limited Resources",
[0x0e] = "Host Rejected Security Reasons",
[0x0f] = "Host Rejected Remote Device Is Personal / Host Rejected Unacceptable Device Address (2.0+)",
[0x10] = "Host Timeout",
[0x11] = "Unsupported Feature or Parameter Value",
[0x12] = "Invalid HCI Command Parameters",
[0x13] = "Other End Terminated Connection User Ended",
[0x14] = "Other End Terminated Connection Low Resources",
[0x15] = "Other End Terminated Connection About to Power Off",
[0x16] = "Connection Terminated by Local Host",
[0x17] = "Repeated Attempts",
[0x18] = "Pairing Not Allowed",
[0x19] = "Unknown LMP PDU",
[0x1a] = "Unsupported Remote Feature",
[0x1b] = "SCO Offset Rejected",
[0x1c] = "SCO Interval Rejected",
[0x1d] = "SCO Air Mode Rejected",
[0x1e] = "Invalid LMP Parameters",
[0x1f] = "Unspecified Error",
[0x20] = "Unsupported LMP Parameter Value",
[0x21] = "Role Change Not Allowed",
[0x22] = "LMP Response Timeout",
[0x23] = "LMP Error Transaction Collision",
[0x24] = "LMP PDU Not Allowed",
[0x25] = "Encryption Mode Not Acceptable",
[0x26] = "Unit Key Used",
[0x27] = "QoS Not Supported",
[0x28] = "Instant Passed",
[0x29] = "Pairing With Unit Key Not Supported",
[0x2a] = "Different Transaction Collision",
[0x2c] = "QoS Unacceptable Parameter",
[0x2d] = "QoS Rejected",
[0x2e] = "Channel Classification Not Supported",
[0x2f] = "Insufficient Security",
[0x30] = "Parameter Out of Mandatory Range",
[0x31] = "Role Switch Pending",
[0x34] = "Reserved Slot Violation",
[0x35] = "Role Switch Failed",
[0x36] = "Extended Inquiry Response Too Large",
[0x37] = "Secure Simple Pairing Not Supported by Host",
[0x38] = "Host Busy Pairing",
[0x39] = "Connection Rejected Due to No Suitable Channel Found",
[0x3a] = "Controller Busy",
[0x3b] = "Unacceptable Connection Interval",
[0x3c] = "Directed Advertising Timeout",
[0x3d] = "Connection Terminated Due to MIC Failure",
[0x3e] = "Connection Failed to Be Established",
[0x3f] = "MAC Connection Failed",
[0x40] = "Coarse Clock Adjustment Rejected",
};
@interface DevicePairDelegate : NSObject <IOBluetoothDevicePairDelegate>
@property (readonly) IOReturn errorCode;
@property char *requestedPin;
@end
@implementation DevicePairDelegate
- (const char *)errorDescription {
if (_errorCode >= 0 && (unsigned)_errorCode < sizeof(hci_error_descriptions) / sizeof(hci_error_descriptions[0]) &&
hci_error_descriptions[_errorCode]) {
return hci_error_descriptions[_errorCode];
} else {
return "UNKNOWN ERROR";
}
}
- (void)devicePairingFinished:(__unused id)sender error:(IOReturn)error {
_errorCode = error;
CFRunLoopStop(CFRunLoopGetCurrent());
}
- (void)devicePairingPINCodeRequest:(id)sender {
BluetoothPINCode pinCode;
ByteCount pinCodeSize;
if (_requestedPin) {
eprintf("Input pin %.16s on \"%s\" (%s)\n",
_requestedPin,
[[[sender device] name] UTF8String],
[[[sender device] addressString] UTF8String]);
pinCodeSize = strlen(_requestedPin);
if (pinCodeSize > 16) pinCodeSize = 16;
strncpy((char *)pinCode.data, _requestedPin, pinCodeSize);
} else {
eprintf("Type pin code (up to 16 characters) for \"%s\" (%s) and press Enter: ",
[[[sender device] name] UTF8String],
[[[sender device] addressString] UTF8String]);
uint input_size = 16 + 2;
char input[input_size];
fgets(input, input_size, stdin);
input[strcspn(input, "\n")] = 0;
pinCodeSize = strlen(input);
strncpy((char *)pinCode.data, input, pinCodeSize);
}
[sender replyPINCode:pinCodeSize PINCode:&pinCode];
}
- (void)devicePairingUserConfirmationRequest:(id)sender numericValue:(BluetoothNumericValue)numericValue {
eprintf("Does \"%s\" (%s) display number %06u (yes/no)? ",
[[[sender device] name] UTF8String],
[[[sender device] addressString] UTF8String],
numericValue);
uint input_size = 3 + 2;
char input[input_size];
fgets(input, input_size, stdin);
input[strcspn(input, "\n")] = 0;
if (is_caseabbr("yes", input)) {
[sender replyUserConfirmation:YES];
return;
}
if (is_caseabbr("no", input)) {
[sender replyUserConfirmation:NO];
return;
}
}
- (void)devicePairingUserPasskeyNotification:(id)sender passkey:(BluetoothPasskey)passkey {
eprintf("Input passkey %06u on \"%s\" (%s)\n",
passkey,
[[[sender device] name] UTF8String],
[[[sender device] addressString] UTF8String]);
}
@end
#define OP_FUNC(name, operator) \
bool op_##name(const long a, const long b) { return a operator b; }
OP_FUNC(gt, >);
OP_FUNC(ge, >=);
OP_FUNC(lt, <);
OP_FUNC(le, <=);
OP_FUNC(eq, ==);
OP_FUNC(ne, !=);
typedef bool (*OpFunc)(const long a, const long b);
#define PARSE_OP_ARG_MATCHER(name, operator) \
if (0 == strcmp(arg, #name) || 0 == strcmp(arg, #operator)) { \
*op = op_##name; \
*op_name = #operator; \
return true; \
}
bool parse_op_arg(const char *arg, OpFunc *op, const char **op_name) {
PARSE_OP_ARG_MATCHER(gt, >);
PARSE_OP_ARG_MATCHER(ge, >=);
PARSE_OP_ARG_MATCHER(lt, <);
PARSE_OP_ARG_MATCHER(le, <=);
PARSE_OP_ARG_MATCHER(eq, =);
PARSE_OP_ARG_MATCHER(ne, !=);
return false;
}
@interface DeviceNotificationRunLoopStopper : NSObject
@end
@implementation DeviceNotificationRunLoopStopper {
IOBluetoothDevice *expectedDevice;
}
- (id)initWithExpectedDevice:(IOBluetoothDevice *)device {
expectedDevice = device;
return self;
}
- (void)notification:(IOBluetoothUserNotification *)notification fromDevice:(IOBluetoothDevice *)device {
if ([expectedDevice isEqual:device]) {
[notification unregister];
CFRunLoopStop(CFRunLoopGetCurrent());
}
}
@end
struct args_state_get {
GetterFunc func;
};
struct args_state_set {
SetterFunc func;
enum state state;
};
struct args_inquiry {
unsigned long duration;
};
struct args_recent {
unsigned long max;
};
struct args_device_id {
char *device_id;
};
struct args_pair {
char *device_id;
char *pin_code;
};
struct args_wait_connection_change {
char *device_id;
bool wait_connect;
unsigned long timeout;
};
struct args_wait_rssi {
char *device_id;
OpFunc op;
const char *op_name;
long value;
unsigned long period;
unsigned long timeout;
};
typedef int (^cmd)(void *args);
struct cmd_with_args {
cmd cmd;
void *args;
};
#define CMD_CHUNK 8
struct cmd_with_args *cmds = NULL;
size_t cmd_n = 0, cmd_reserved = 0;
#define ALLOC_ARGS(type) struct args_##type *args = assert_alloc(malloc(sizeof(struct args_##type)))
void add_cmd(void *args, cmd cmd) {
if (cmd_n >= cmd_reserved) {
cmd_reserved += CMD_CHUNK;
cmds = assert_alloc(reallocf(cmds, sizeof(struct cmd_with_args) * cmd_reserved));
}
cmds[cmd_n++] = (struct cmd_with_args){.cmd = cmd, .args = args};
}
FormatterFunc list_devices = list_devices_default;
int main(int argc, char *argv[]) {
if (geteuid() == 0) {
char *allow_root = getenv("BLUEUTIL_ALLOW_ROOT");
if (NULL == allow_root || 0 != strcmp(allow_root, "1")) {
eprintf("Error: Not running as root user without environment variable BLUEUTIL_ALLOW_ROOT=1\n");
return EXIT_FAILURE;
}
}
if (!BTAvaliable()) {
eprintf("Error: Bluetooth not available!\n");
return EX_UNAVAILABLE;
}
if (argc == 1) {
printf("Power: %d\nDiscoverable: %d\n", BTPowerState(), BTDiscoverableState());
return EXIT_SUCCESS;
}
enum {
arg_power = 'p',
arg_discoverable = 'd',
arg_help = 'h',
arg_version = 'v',
arg_favourites = 256,
arg_inquiry,
arg_paired,
arg_recent,
arg_connected,
arg_info,
arg_is_connected,
arg_connect,
arg_disconnect,
arg_pair,
arg_unpair,
arg_add_favourite,
arg_remove_favourite,
arg_format,
arg_wait_connect,
arg_wait_disconnect,
arg_wait_rssi,
};
const char *optstring = "p::d::hv";
// clang-format off
static struct option long_options[] = {
{"power", optional_argument, NULL, arg_power},
{"discoverable", optional_argument, NULL, arg_discoverable},
{"favourites", no_argument, NULL, arg_favourites},
{"favorites", no_argument, NULL, arg_favourites},
{"inquiry", optional_argument, NULL, arg_inquiry},
{"paired", no_argument, NULL, arg_paired},
{"recent", optional_argument, NULL, arg_recent},
{"connected", no_argument, NULL, arg_connected},
{"info", required_argument, NULL, arg_info},
{"is-connected", required_argument, NULL, arg_is_connected},
{"connect", required_argument, NULL, arg_connect},
{"disconnect", required_argument, NULL, arg_disconnect},
{"pair", required_argument, NULL, arg_pair},
{"unpair", required_argument, NULL, arg_unpair},
{"add-favourite", required_argument, NULL, arg_add_favourite},
{"add-favorite", required_argument, NULL, arg_add_favourite},
{"remove-favourite", required_argument, NULL, arg_remove_favourite},
{"remove-favorite", required_argument, NULL, arg_remove_favourite},
{"format", required_argument, NULL, arg_format},
{"wait-connect", required_argument, NULL, arg_wait_connect},
{"wait-disconnect", required_argument, NULL, arg_wait_disconnect},
{"wait-rssi", required_argument, NULL, arg_wait_rssi},
{"help", no_argument, NULL, arg_help},
{"version", no_argument, NULL, arg_version},
{NULL, 0, NULL, 0}
};
// clang-format on
int arg;
while ((arg = getopt_long(argc, argv, optstring, long_options, NULL)) != -1) {
switch (arg) {
case arg_power:
case arg_discoverable: {
extend_optarg(argc, argv);
if (optarg) {
ALLOC_ARGS(state_set);
args->func = arg == arg_power ? BTSetPowerState : BTSetDiscoverableState;
if (!parse_state_arg(optarg, &args->state)) {
eprintf("Unexpected value: %s\n", optarg);
return EX_USAGE;
}
add_cmd(args, ^int(void *_args) {
struct args_state_set *args = (struct args_state_set *)_args;
return args->func(args->state) ? EXIT_SUCCESS : EX_TEMPFAIL;
});
} else {
ALLOC_ARGS(state_get);
args->func = arg == arg_power ? BTPowerState : BTDiscoverableState;
add_cmd(args, ^int(void *_args) {
struct args_state_get *args = (struct args_state_get *)_args;
printf("%d\n", args->func());
return EXIT_SUCCESS;
});
}
} break;
case arg_favourites: {
add_cmd(NULL, ^int(__unused void *_args) {
list_devices([IOBluetoothDevice favoriteDevices], false);
return EXIT_SUCCESS;
});
} break;
case arg_paired: {
add_cmd(NULL, ^int(__unused void *_args) {
list_devices([IOBluetoothDevice pairedDevices], false);
return EXIT_SUCCESS;
});
} break;
case arg_inquiry: {
ALLOC_ARGS(inquiry);
extend_optarg(argc, argv);
args->duration = 10;
if (optarg) {
if (!parse_unsigned_long_arg(optarg, &args->duration)) {
eprintf("Expected numeric duration, got: %s\n", optarg);
return EX_USAGE;
}
}
add_cmd(args, ^int(void *_args) {
struct args_inquiry *args = (struct args_inquiry *)_args;
check_power_on_for("inquiry");
@autoreleasepool {
DeviceInquiryRunLoopStopper *stopper = [[[DeviceInquiryRunLoopStopper alloc] init] autorelease];
IOBluetoothDeviceInquiry *inquirer = [IOBluetoothDeviceInquiry inquiryWithDelegate:stopper];
[inquirer setInquiryLength:args->duration];
[inquirer start];
CFRunLoopRun();
[inquirer stop];
list_devices([inquirer foundDevices], false);
}
return EXIT_SUCCESS;
});
} break;
case arg_recent: {
ALLOC_ARGS(recent);
extend_optarg(argc, argv);
args->max = 10;
if (optarg) {
if (!parse_unsigned_long_arg(optarg, &args->max)) {
eprintf("Expected numeric count, got: %s\n", optarg);
return EX_USAGE;
}
}
add_cmd(args, ^int(void *_args) {
struct args_recent *args = (struct args_recent *)_args;
list_devices([IOBluetoothDevice recentDevices:args->max], false);
return EXIT_SUCCESS;
});
} break;
case arg_connected: {
add_cmd(NULL, ^int(__unused void *_args) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"isConnected == YES"];
list_devices([[IOBluetoothDevice pairedDevices] filteredArrayUsingPredicate:predicate], false);
return EXIT_SUCCESS;
});
} break;
case arg_info: {
ALLOC_ARGS(device_id);
args->device_id = optarg;
add_cmd(args, ^int(void *_args) {
struct args_device_id *args = (struct args_device_id *)_args;
list_devices(@[get_device(args->device_id)], true);
return EXIT_SUCCESS;
});
} break;
case arg_is_connected: {
ALLOC_ARGS(device_id);
args->device_id = optarg;
add_cmd(args, ^int(void *_args) {
struct args_device_id *args = (struct args_device_id *)_args;
printf("%d\n", [get_device(args->device_id) isConnected] ? 1 : 0);
return EXIT_SUCCESS;
});
} break;
case arg_connect: {
ALLOC_ARGS(device_id);
args->device_id = optarg;
add_cmd(args, ^int(void *_args) {
struct args_device_id *args = (struct args_device_id *)_args;
check_power_on_for("connect");
if ([get_device(args->device_id) openConnection] != kIOReturnSuccess) {
eprintf("Failed to connect \"%s\"\n", args->device_id);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
});
} break;
case arg_disconnect: {
ALLOC_ARGS(device_id);
args->device_id = optarg;
add_cmd(args, ^int(void *_args) {
struct args_device_id *args = (struct args_device_id *)_args;
check_power_on_for("disconnect");
@autoreleasepool {
IOBluetoothDevice *device = get_device(args->device_id);
DeviceNotificationRunLoopStopper *stopper =
[[[DeviceNotificationRunLoopStopper alloc] initWithExpectedDevice:device] autorelease];
[device registerForDisconnectNotification:stopper selector:@selector(notification:fromDevice:)];
if ([device closeConnection] != kIOReturnSuccess) {
eprintf("Failed to disconnect \"%s\"\n", args->device_id);
return EXIT_FAILURE;
}
CFRunLoopRun();
}
return EXIT_SUCCESS;
});
} break;
case arg_add_favourite: {
ALLOC_ARGS(device_id);
args->device_id = optarg;
add_cmd(args, ^int(void *_args) {
struct args_device_id *args = (struct args_device_id *)_args;
if ([get_device(args->device_id) addToFavorites] != kIOReturnSuccess) {
eprintf("Failed to add \"%s\" to favourites\n", args->device_id);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
});
} break;
case arg_remove_favourite: {
ALLOC_ARGS(device_id);
args->device_id = optarg;
add_cmd(args, ^int(void *_args) {
struct args_device_id *args = (struct args_device_id *)_args;
if ([get_device(args->device_id) removeFromFavorites] != kIOReturnSuccess) {
eprintf("Failed to remove \"%s\" from favourites\n", args->device_id);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
});
} break;
case arg_pair: {
ALLOC_ARGS(pair);
args->device_id = optarg;
args->pin_code = next_optarg(argc, argv);
if (args->pin_code && strlen(args->pin_code) > 16) {
eprintf("Pairing pin can't be longer than 16 characters, got %lu (%s)\n",
strlen(args->pin_code),
args->pin_code);
return EX_USAGE;
}
add_cmd(args, ^int(void *_args) {
struct args_pair *args = (struct args_pair *)_args;
check_power_on_for("pair");
@autoreleasepool {
IOBluetoothDevice *device = get_device(args->device_id);
DevicePairDelegate *delegate = [[[DevicePairDelegate alloc] init] autorelease];
IOBluetoothDevicePair *pairer = [IOBluetoothDevicePair pairWithDevice:device];
pairer.delegate = delegate;
delegate.requestedPin = args->pin_code;
if ([pairer start] != kIOReturnSuccess) {
eprintf("Failed to start pairing with \"%s\"\n", args->device_id);
return EXIT_FAILURE;
}
CFRunLoopRun();
[pairer stop];
if (![device isPaired]) {
eprintf("Failed to pair \"%s\" with error 0x%02x (%s)\n",
args->device_id,
[delegate errorCode],
[delegate errorDescription]);
return EXIT_FAILURE;
}
}
return EXIT_SUCCESS;
});
} break;
case arg_unpair: {
ALLOC_ARGS(device_id);
args->device_id = optarg;
add_cmd(args, ^int(void *_args) {
struct args_device_id *args = (struct args_device_id *)_args;
IOBluetoothDevice *device = get_device(args->device_id);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if ([device respondsToSelector:@selector(remove)]) {
[device performSelector:@selector(remove)];
#pragma clang diagnostic pop
return EXIT_SUCCESS;
} else {
return EX_UNAVAILABLE;
}
});
} break;
case arg_format: {
if (!parse_output_formatter(optarg, &list_devices)) {
eprintf("Unexpected format: %s\n", optarg);
return EX_USAGE;
}
} break;
case arg_wait_connect:
case arg_wait_disconnect: {
ALLOC_ARGS(wait_connection_change);
args->wait_connect = arg == arg_wait_connect;
args->device_id = optarg;
char *timeout_arg = next_optarg(argc, argv);
args->timeout = 0;
if (timeout_arg && !parse_unsigned_long_arg(timeout_arg, &args->timeout)) {
eprintf("Expected numeric timeout, got: %s\n", timeout_arg);
return EX_USAGE;
}
add_cmd(args, ^int(void *_args) {
struct args_wait_connection_change *args = (struct args_wait_connection_change *)_args;
@autoreleasepool {
IOBluetoothDevice *device = get_device(args->device_id);
DeviceNotificationRunLoopStopper *stopper =
[[[DeviceNotificationRunLoopStopper alloc] initWithExpectedDevice:device] autorelease];
CFRunLoopTimerRef timer =
CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, 0, 0, 0, 0, ^(__unused CFRunLoopTimerRef timer) {
if (args->wait_connect) {
if ([device isConnected]) {
CFRunLoopStop(CFRunLoopGetCurrent());
} else {
[IOBluetoothDevice registerForConnectNotifications:stopper
selector:@selector(notification:fromDevice:)];
}
} else {
if ([device isConnected]) {
[device registerForDisconnectNotification:stopper selector:@selector(notification:fromDevice:)];
} else {
CFRunLoopStop(CFRunLoopGetCurrent());
}
}
});
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode);
if (args->timeout > 0) {
if (kCFRunLoopRunTimedOut == CFRunLoopRunInMode(kCFRunLoopDefaultMode, args->timeout, false)) {
eprintf("Timed out waiting for \"%s\" to %s\n", optarg, args->wait_connect ? "connect" : "disconnect");
return EX_TEMPFAIL;
}
} else {
CFRunLoopRun();
}
CFRunLoopRemoveTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode);
CFRelease(timer);
}
return EXIT_SUCCESS;
});
} break;
case arg_wait_rssi: {
ALLOC_ARGS(wait_rssi);
args->device_id = optarg;
char *op_arg = next_reqarg(argc, argv);
if (!op_arg) {
eprintf("%s: option `%s' requires 2nd argument\n", argv[0], argv[optind - 2]);
usage(stderr);
return EX_USAGE;
} else if (!parse_op_arg(op_arg, &args->op, &args->op_name)) {
eprintf("Expected operator, got: %s\n", op_arg);
return EX_USAGE;
}
char *value_arg = next_reqarg(argc, argv);
if (!value_arg) {
eprintf("%s: option `%s' requires 3rd argument\n", argv[0], argv[optind - 3]);
usage(stderr);
return EX_USAGE;
} else if (!parse_signed_long_arg(value_arg, &args->value)) {
eprintf("Expected numeric value, got: %s\n", value_arg);
return EX_USAGE;
}
char *period_arg = next_optarg(argc, argv);
args->period = 1;
if (period_arg) {
if (!parse_unsigned_long_arg(period_arg, &args->period)) {
eprintf("Expected numeric period, got: %s\n", period_arg);
return EX_USAGE;
} else if (args->period < 1) {
eprintf("Expected period to be at least 1, got: %ld\n", args->period);
return EX_USAGE;
}
}
char *timeout_arg = next_optarg(argc, argv);
args->timeout = 0;
if (timeout_arg && !parse_unsigned_long_arg(timeout_arg, &args->timeout)) {
eprintf("Expected numeric timeout, got: %s\n", timeout_arg);
return EX_USAGE;
}
add_cmd(args, ^int(void *_args) {
struct args_wait_rssi *args = (struct args_wait_rssi *)_args;
IOBluetoothDevice *device = get_device(args->device_id);
CFRunLoopTimerRef timer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault,
0,
args->period,
0,
0,
^(__unused CFRunLoopTimerRef timer) {
long rssi = [device RSSI];
if (rssi == 127) rssi = -129;
if (args->op(rssi, args->value)) {
CFRunLoopStop(CFRunLoopGetCurrent());
}
});
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode);
if (args->timeout > 0) {
if (kCFRunLoopRunTimedOut == CFRunLoopRunInMode(kCFRunLoopDefaultMode, args->timeout, false)) {
eprintf("Timed out waiting for rssi of \"%s\" to be %s %ld\n",
args->device_id,
args->op_name,
args->value);
return EX_TEMPFAIL;
}
} else {
CFRunLoopRun();
}
CFRunLoopRemoveTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode);
CFRelease(timer);
return EXIT_SUCCESS;
});
} break;
case arg_version: {
printf(VERSION "\n");
return EXIT_SUCCESS;
}
case arg_help: {
usage(stdout);
return EXIT_SUCCESS;
}
default: {
usage(stderr);
return EX_USAGE;
}
}
}
if (optind < argc) {
eprintf("Unexpected arguments: %s", argv[optind++]);
while (optind < argc) {
eprintf(", %s", argv[optind++]);
}
eprintf("\n");
return EX_USAGE;
}
for (size_t i = 0; i < cmd_n; i++) {
int status = cmds[i].cmd(cmds[i].args);
if (status != EXIT_SUCCESS) return status;
free(cmds[i].args);
}
free(cmds);
return EXIT_SUCCESS;
}
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
8DD76F9A0486AA7600D96B5E /* blueutil.m in Sources */ = {isa = PBXBuildFile; fileRef = 08FB7796FE84155DC02AAC07 /* blueutil.m */; settings = {ATTRIBUTES = (); }; };
B2A6DE1312F4624400C5007F /* IOBluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2A6DE1212F4624400C5007F /* IOBluetooth.framework */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
8DD76F9E0486AA7600D96B5E /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 8;
dstPath = /usr/share/man/man1/;
dstSubfolderSpec = 0;
files = (
);
runOnlyForDeploymentPostprocessing = 1;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
08FB7796FE84155DC02AAC07 /* blueutil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = blueutil.m; sourceTree = "<group>"; };
32A70AAB03705E1F00C91783 /* blueutil_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = blueutil_Prefix.pch; sourceTree = "<group>"; };
8DD76FA10486AA7600D96B5E /* blueutil */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = blueutil; sourceTree = BUILT_PRODUCTS_DIR; };
B2A6DE1212F4624400C5007F /* IOBluetooth.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOBluetooth.framework; path = System/Library/Frameworks/IOBluetooth.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
8DD76F9B0486AA7600D96B5E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B2A6DE1312F4624400C5007F /* IOBluetooth.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
08FB7794FE84155DC02AAC07 /* blueutil */ = {
isa = PBXGroup;
children = (
08FB7795FE84155DC02AAC07 /* Source */,
08FB779DFE84155DC02AAC07 /* External Frameworks and Libraries */,
1AB674ADFE9D54B511CA2CBB /* Products */,
);
name = blueutil;
sourceTree = "<group>";
};
08FB7795FE84155DC02AAC07 /* Source */ = {
isa = PBXGroup;
children = (
32A70AAB03705E1F00C91783 /* blueutil_Prefix.pch */,
08FB7796FE84155DC02AAC07 /* blueutil.m */,
);
name = Source;
sourceTree = "<group>";
};
08FB779DFE84155DC02AAC07 /* External Frameworks and Libraries */ = {
isa = PBXGroup;
children = (
B2A6DE1212F4624400C5007F /* IOBluetooth.framework */,
);
name = "External Frameworks and Libraries";
sourceTree = "<group>";
};
1AB674ADFE9D54B511CA2CBB /* Products */ = {
isa = PBXGroup;
children = (
8DD76FA10486AA7600D96B5E /* blueutil */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
8DD76F960486AA7600D96B5E /* blueutil */ = {
isa = PBXNativeTarget;
buildConfigurationList = 1DEB927408733DD40010E9CD /* Build configuration list for PBXNativeTarget "blueutil" */;
buildPhases = (
8DD76F990486AA7600D96B5E /* Sources */,
8DD76F9B0486AA7600D96B5E /* Frameworks */,
8DD76F9E0486AA7600D96B5E /* CopyFiles */,
);
buildRules = (
);
dependencies = (
);
name = blueutil;
productInstallPath = "$(HOME)/bin";
productName = blueutil;
productReference = 8DD76FA10486AA7600D96B5E /* blueutil */;
productType = "com.apple.product-type.tool";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
08FB7793FE84155DC02AAC07 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0830;
};
buildConfigurationList = 1DEB927808733DD40010E9CD /* Build configuration list for PBXProject "blueutil" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 1;
knownRegions = (
en,
);
mainGroup = 08FB7794FE84155DC02AAC07 /* blueutil */;
projectDirPath = "";
projectRoot = "";
targets = (
8DD76F960486AA7600D96B5E /* blueutil */,
);
};
/* End PBXProject section */
/* Begin PBXSourcesBuildPhase section */
8DD76F990486AA7600D96B5E /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8DD76F9A0486AA7600D96B5E /* blueutil.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
1DEB927508733DD40010E9CD /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
COPY_PHASE_STRIP = NO;
GCC_DYNAMIC_NO_PIC = NO;
GCC_ENABLE_FIX_AND_CONTINUE = YES;
GCC_MODEL_TUNING = G5;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = blueutil_Prefix.pch;
INSTALL_PATH = /usr/local/bin;
PRODUCT_NAME = blueutil;
};
name = Debug;
};
1DEB927608733DD40010E9CD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
GCC_MODEL_TUNING = G5;
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = blueutil_Prefix.pch;
INSTALL_PATH = /usr/local/bin;
PRODUCT_NAME = blueutil;
};
name = Release;
};
1DEB927908733DD40010E9CD /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.9;
ONLY_ACTIVE_ARCH = YES;
PREBINDING = NO;
};
name = Debug;
};
1DEB927A08733DD40010E9CD /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.9;
PREBINDING = NO;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
1DEB927408733DD40010E9CD /* Build configuration list for PBXNativeTarget "blueutil" */ = {
isa = XCConfigurationList;
buildConfigurations = (
1DEB927508733DD40010E9CD /* Debug */,
1DEB927608733DD40010E9CD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
1DEB927808733DD40010E9CD /* Build configuration list for PBXProject "blueutil" */ = {
isa = XCConfigurationList;
buildConfigurations = (
1DEB927908733DD40010E9CD /* Debug */,
1DEB927A08733DD40010E9CD /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 08FB7793FE84155DC02AAC07 /* Project object */;
}
//
// Prefix header for all source files of the 'blueutil' target in the 'blueutil' project.
//
#ifdef __OBJC__
#import <IOBluetooth/IOBluetooth.h>
#endif

ChangeLog

unreleased

v2.9.1 (2023-01-14)

  • When disconnecting, explicitly wait for device disconnection #70 @toy

v2.9.0 (2021-05-23)

  • Add unpairing/removing pairing functionality #27 #31 #32 #46 #53 @toy
  • Remove experimental mark from different failure exit codes @toy

v2.8.0 (2021-03-21)

  • Print a warning when no power when running connect, disconnect, inquiry and pair commands #50 @toy
  • Fix check for RSSI equality when waiting for device @toy
  • Support US spelling favorite for switches including favourite #49 @toy
  • List connected devices #47 @toy

v2.7.0 (2020-11-17)

  • Refuse to run as root user to prevent possible issues like discoverability getting stuck in some state #41 @toy

v2.6.0 (2020-03-25)

  • Show underlying regex error messages in output, use default out of memory message @toy
  • Experimental use different failure exit codes from sysexits @toy
  • Add changelog @toy
  • Internal change to use blocks instead of going two times through options @toy
  • Mention in usage that requesting 0 recent devices will list all of them @toy
  • Introduce clang-format (required converting tabs to spaces) @toy
  • Experimental functionality to wait for device to connect, disconnect or for its RSSI to match expectation @toy
  • Fix probable leaks by using autoreleasepool in few places @toy
  • Add ability to add/remove favourites #29 @toy
  • Add instructions to update/uninstall #28 @toy

v2.5.1 (2019-08-27)

  • Use last specified format for all output commands #25 @toy
  • Handle null for name and recent access date to fix an error for json output and ugly output in other formatters #24 @toy

v2.5.0 (2019-08-21)

  • Allow switching default formatter to json, json-pretty and new-default (comma separated key-value pairs) #17 @toy
  • Add instructions to install from MacPorts @toy
  • Specify 10.9 as the minimum version explicitly #16 @toy

v2.4.0 (2019-01-25)

v2.3.0 (2019-01-14)

v2.2.0 (2018-10-11)

  • Add ability to connect, disconnect, get information about, and check connected state of device by address or name from the list of recent devices mentioned in #9 @toy
  • Add inquiring devices in range and listing favourite, paired and recent devices #9 @toy
  • Fix missing newline after message about unexpected state value @toy
  • Set deployment target to 10.6 @toy

v2.1.0 (2018-04-19)

  • Add ability to toggle power and discoverability state #8 @toy
  • Add note about effect of opening bluetooth preference pane on discoverability suggested in #3 @toy
  • Update xcode project to compatibility version 3.2 missing part of #7 @toy

v2.0.0 (2018-02-18)

  • Change arguments specification to Unix/POSIX style #7 @toy
  • Don’t show the WARNING when piping yes to the test script @toy
  • Make error message for discoverable consistent @toy
  • Run make install/uninstall commands instead of only printing them @toy

v1.1.2 (2017-02-04)

  • Add a warning and confirmation to the test script for users of wireless input devices #6 @toy
  • Add proper make targets: build (default), test, clean, install and uninstall @toy
  • Fix wrong handling of length in is_abbr_arg #6 @toy

v1.1.0 (2017-02-01)

  • Add basic makefile as an alternative to using xcode @toy
  • Add simple test script for getting/setting power/discoverability @toy
  • Allow abbreviating help, version and status commands @toy
  • Add version command @toy
  • Restore waiting for state to change after setting it, check every 0.1 second for 10 seconds @toy
  • Add help command @toy
  • Restore original style arguments: status, on, off #4 @toy
  • Allow abbreviating power and discoverable arguments @toy

v1.0.0 (2012-02-26)

  • Switch to unconditionally waiting 1 second after setting value as waiting for result to change was not working @toy
  • Allow getting and setting discoverable state alongside power state, use 1/0 instead of on/off @toy
  • Import original code by Frederik Seiffert @triplef from http://frederikseiffert.de/blueutil
Originally written by Frederik Seiffert <[email protected]>
Copyright (c) 2011-2023 Ivan Kuchin
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.
CFLAGS = -Wall -Wextra -Werror -mmacosx-version-min=10.9 -framework Foundation -framework IOBluetooth
DESTDIR =
prefix = /usr/local
bindir = $(prefix)/bin
INSTALL = install
INSTALL_PROGRAM = $(INSTALL) -m 755
all: build
build: blueutil
format:
clang-format -i *.m
update_usage: blueutil
./update_usage
touch update_usage
test: build
./test
clean:
$(RM) blueutil
install: build
$(INSTALL_PROGRAM) blueutil $(DESTDIR)$(bindir)/blueutil
uninstall:
$(RM) $(DESTDIR)$(bindir)/blueutil
.PHONY: all build format test clean install uninstall

blueutil

CLI for bluetooth on OSX: power, discoverable state, list, inquire devices, connect, info, …

Usage

Usage:
  blueutil [options]

Without options outputs current state

    -p, --power               output power state as 1 or 0
    -p, --power STATE         set power state
    -d, --discoverable        output discoverable state as 1 or 0
    -d, --discoverable STATE  set discoverable state

        --favourites, --favorites
                              list favourite devices
        --inquiry [T]         inquiry devices in range, 10 seconds duration by default excluding time for name updates
        --paired              list paired devices
        --recent [N]          list recently used devices, 10 by default, 0 to list all
        --connected           list connected devices

        --info ID             show information about device
        --is-connected ID     connected state of device as 1 or 0
        --connect ID          create a connection to device
        --disconnect ID       close the connection to device
        --pair ID [PIN]       pair with device, optional PIN of up to 16 characters will be used instead of interactive input if requested in specific pair mode
        --unpair ID           EXPERIMENTAL unpair the device
        --add-favourite ID, --add-favorite ID
                              add to favourites
        --remove-favourite ID, --remove-favorite ID
                              remove from favourites

        --format FORMAT       change output format of info and all listing commands

        --wait-connect ID [TIMEOUT]
                              EXPERIMENTAL wait for device to connect
        --wait-disconnect ID [TIMEOUT]
                              EXPERIMENTAL wait for device to disconnect
        --wait-rssi ID OP VALUE [PERIOD [TIMEOUT]]
                              EXPERIMENTAL wait for device RSSI value which is 0 for golden range, -129 if it cannot be read (e.g. device is disconnected)

    -h, --help                this help
    -v, --version             show version

STATE can be one of: 1, on, 0, off, toggle
ID can be either address in form xxxxxxxxxxxx, xx-xx-xx-xx-xx-xx or xx:xx:xx:xx:xx:xx, or name of device to search in used devices
OP can be one of: >, >=, <, <=, =, !=; or equivalents: gt, ge, lt, le, eq, ne
PERIOD is in seconds, defaults to 1
TIMEOUT is in seconds, default value 0 doesn't add timeout
FORMAT can be one of:
  default - human readable text output not intended for consumption by scripts
  new-default - human readable comma separated key-value pairs (EXPERIMENTAL, THE BEHAVIOUR MAY CHANGE)
  json - compact JSON
  json-pretty - pretty printed JSON

Due to possible problems, blueutil will refuse to run as root user (see https://github.com/toy/blueutil/issues/41).
Use environment variable BLUEUTIL_ALLOW_ROOT=1 to override (sudo BLUEUTIL_ALLOW_ROOT=1 blueutil …).

Exit codes:
   0 Success
   1 General failure
  64 Wrong usage like missing or unexpected arguments, wrong parameters
  69 Bluetooth or interface not available
  70 Internal error
  71 System error like shortage of memory
  75 Timeout error

Install/update/uninstall

Homebrew

Using package manager Homebrew:

# install
brew install blueutil

# update
brew update
brew upgrade blueutil

# uninstall
brew remove blueutil

MacPorts

Using package manager MacPorts:

# install
port install blueutil

# update
port selfupdate
port upgrade blueutil

# uninstall
port uninstall blueutil

You will probably need to prefix all commands with sudo.

From source

git clone https://github.com/toy/blueutil.git
cd blueutil

# build
make

# install/update
git pull
make install

# uninstall
make uninstall

You may need to prefix install/update and uninstall make commands with sudo.

Notes

Uses private API from IOBluetooth framework (i.e. IOBluetoothPreference*()).

Opening Bluetooth preference pane always turns on discoverability if bluetooth power is on or if it is switched on when preference pane is open, this change of state is not reported by the function used by blueutil.

Development

To build and update usage:

make build update_usage

To apply clang-format:

make format

To test:

make test

To release new version:

./release major|minor|patch

To create release on github:

./verify_release

If there are no validation errors, copy generated markdown to description of new release:

open "https://github.com/toy/blueutil/releases/new?tag=$(git describe --tags --abbrev=0)"

Copyright

Originally written by Frederik Seiffert [email protected] http://www.frederikseiffert.de/blueutil/

Copyright (c) 2011-2023 Ivan Kuchin. See LICENSE.txt for details.

#!/usr/bin/env ruby
require 'pathname'
require 'date'
MAIN_FILE = 'blueutil.m'
DEFINE_VERSION_REGEXP = /(?<=#define VERSION ")\d+(?:\.\d+)+(?=")/
version_parts = File.read(MAIN_FILE)[DEFINE_VERSION_REGEXP].split('.').map(&:to_i)
new_version = case ARGV
when %w[major]
"#{version_parts[0] + 1}.0.0"
when %w[minor]
"#{version_parts[0]}.#{version_parts[1] + 1}.0"
when %w[patch]
"#{version_parts[0]}.#{version_parts[1]}.#{version_parts[2] + 1}"
else
abort 'Expected major, minor or patch as the only argument'
end
def clean_workind_directory?
`git status --porcelain`.empty?
end
clean_workind_directory? or abort('Working directory not clean')
system './update_usage'
clean_workind_directory? or abort('Usage in README is not up to date')
paths = Pathname.glob('*').select do |path|
next unless path.file?
next if path.executable?
original = path.read
changed = original.gsub(/(?<before>Copyright \(c\) )(?<year>\d+)(?:-\d+)?(?<after> \w+ \w+)/) do
m = Regexp.last_match
"#{m[:before]}#{[m[:year].to_i, Time.now.year].uniq.join('-')}#{m[:after]}"
end
case path.to_s
when MAIN_FILE
changed = changed.sub(DEFINE_VERSION_REGEXP, new_version)
when 'CHANGELOG.md'
lines = changed.lines
{
2 => "## unreleased\n",
3 => "\n",
}.each do |n, expected|
abort "Expected #{expected} on line #{n}, got #{lines[n]}" unless lines[n] == expected
end
lines.insert(3, "\n", "## v#{new_version} (#{Date.today.strftime('%Y-%m-%d')})\n")
changed = lines.join
end
next if original == changed
path.open('w'){ |f| f.write(changed) }
end
Pathname('blueutil').unlink
system(*%w[make blueutil]) or abort('failed to build')
`./blueutil -h`[new_version] or abort('did not find new version in help output')
system *%w[git add] + paths.map(&:to_s)
system *%w[git diff --cached]
puts %q{Type "yes" to continue}
abort unless $stdin.gets.strip == 'yes'
system *%W[git commit -m v#{new_version}]
system *%W[git tag v#{new_version}]
system *%w[git push]
system *%w[git push --tags]
#!/usr/bin/env bash
if ! read -t 0.001; then
cat <<-EOF
WARNING! This test script will turn the bluetooth power and discoverability on
and off several times. While it will try to restore the original state at the
end of the test, it is recommended to have wired keyboard and pointing device at
hand. You can skip the confirmation by piping yes to the script.
EOF
read -p "Please type y or Y followed by [ENTER] to proceed: "
fi
[[ $REPLY =~ ^[Yy]$ ]] || exit 1
blueutil=./blueutil
errors=
status=$($blueutil)
trap "{
$blueutil -p$($blueutil -p) -d$($blueutil -d)
echo
if [[ -n \$errors ]]; then
echo -n \"\$errors\"
exit 1
fi
}" EXIT
ANSI_BOLD=$(tput bold 2> /dev/null)
ANSI_RED=$(tput setaf 1 2> /dev/null)
ANSI_GREEN=$(tput setaf 2 2> /dev/null)
ANSI_RESET=$(tput sgr0 2> /dev/null)
success() {
printf "$ANSI_GREEN"."$ANSI_RESET"
}
failure() {
printf "$ANSI_RED"F"$ANSI_RESET"
errors+="$@"$'\n'
}
assert_eq() {
[[ $1 == $2 ]] && success || failure "$ANSI_BOLD""$BASH_LINENO""$ANSI_RESET: Expected \"$1\" to eq \"$2\""
}
assert_match() {
[[ $1 =~ $2 ]] && success || failure "$ANSI_BOLD""$BASH_LINENO""$ANSI_RESET: Expected \"$1\" to match \"$2\""
}
# Power
$blueutil -p1
assert_eq "$($blueutil)" *'Power: 1'*
$blueutil -p 0
assert_eq "$($blueutil)" *'Power: 0'*
$blueutil --power=1
assert_eq "$($blueutil)" *'Power: 1'*
assert_eq "$($blueutil -p)" '1'
assert_eq "$($blueutil --pow)" '1'
assert_eq "$($blueutil --power)" '1'
$blueutil --pow 0
assert_eq "$($blueutil)" *'Power: 0'*
assert_eq "$($blueutil -p)" '0'
assert_eq "$($blueutil --pow)" '0'
assert_eq "$($blueutil --power)" '0'
# Discoverable
$blueutil -d1
assert_eq "$($blueutil)" *'Discoverable: 1'*
$blueutil -d 0
assert_eq "$($blueutil)" *'Discoverable: 0'*
$blueutil --discoverable=1
assert_eq "$($blueutil)" *'Discoverable: 1'*
assert_eq "$($blueutil -d)" '1'
assert_eq "$($blueutil --discov)" '1'
assert_eq "$($blueutil --discoverable)" '1'
$blueutil --discov 0
assert_eq "$($blueutil)" *'Discoverable: 0'*
assert_eq "$($blueutil -d)" '0'
assert_eq "$($blueutil --discov)" '0'
assert_eq "$($blueutil --discoverable)" '0'
# Combined
$blueutil -p1 -d1
assert_eq "$($blueutil)" $'Power: 1\nDiscoverable: 1'
$blueutil -p0 -d1
assert_eq "$($blueutil)" $'Power: 0\nDiscoverable: 1'
$blueutil -pon -dOff
assert_eq "$($blueutil)" $'Power: 1\nDiscoverable: 0'
$blueutil -pOFF -doFF
assert_eq "$($blueutil)" $'Power: 0\nDiscoverable: 0'
# Help
assert_eq "$($blueutil --help)" *'this help'*
assert_eq "$($blueutil -h)" *'this help'*
# Version
assert_match "$($blueutil --version)" '^[0-9]+\.[0-9]+\.[0-9]+$'
assert_match "$($blueutil -v)" '^[0-9]+\.[0-9]+\.[0-9]+$'
#!/usr/bin/env ruby
require 'pathname'
USAGE_REGEXP = /
(?<open>#{Regexp.escape '<!--USAGE[-->'})
.*
(?<close>#{Regexp.escape '<!--]USAGE-->'})
/mx
path = Pathname('README.md')
original = path.read
changed = original.sub(USAGE_REGEXP) do
m = Regexp.last_match
system 'make -s blueutil'
usage = `./blueutil --help`.sub(/\Ablueutil v.*/, '').lstrip
"#{m[:open]}\n```\n#{usage}```\n#{m[:close]}"
end
path.open('w'){ |f| f.write(changed) } unless original == changed
#!/usr/bin/env ruby
gem 'rugged', '~> 1.1'
gem 'rubyzip', '~> 2.3'
require 'rubygems/package'
require 'rugged'
require 'zip'
class Release
class Base
attr_reader :tag, :name
def initialize(tag)
@tag = tag
@name = tag.sub(/^v/, 'blueutil-')
end
end
class Repo < Base
def files
@files ||= begin
repo = Rugged::Repository.new('.')
ref = repo.tags[tag]
abort "No such tag #{tag}" unless ref
ref.target.tree.walk(:postorder).map do |root, entry|
next unless entry[:type] == :blob
["#{name}/#{root}#{entry[:name]}", repo.lookup(entry[:oid]).read_raw.data]
end.compact.to_h
end
end
end
class Archive < Base
def basename
"#{name}#{extname}"
end
def url
"https://github.com/toy/blueutil/archive/refs/tags/#{tag}#{extname}"
end
def data
@data ||= IO.popen(%W[curl -sL #{url}], &:read)
end
end
class Gzip < Archive
def extname
'.tar.gz'
end
def files
@files ||= Gem::Package::TarReader.new(Zlib::GzipReader.new(StringIO.new(data))).map do |entry|
next if entry.directory?
next if entry.full_name == 'pax_global_header'
[entry.full_name, entry.read]
end.compact.to_h
end
end
class Zip < Archive
def extname
'.zip'
end
def files
@files ||= ::Zip::File.open_buffer(StringIO.new(data)).entries.map do |entry|
next if entry.directory?
[entry.name, entry.get_input_stream.read]
end.compact.to_h
end
end
attr_reader :tag
def initialize(tag)
@tag = tag
end
def repo
@repo ||= Repo.new(tag)
end
def archives
@archives ||= [Gzip.new(tag), Zip.new(tag)]
end
def check!
archives.each do |archive|
next if repo.files == archive.files
$stderr.puts "#{archive.basename} didn't match:"
(repo.files.keys | archive.files.keys).sort.each do |path|
$stderr.puts "#{path} #{repo.files[path] == archive.files[path] ? 'equal' : 'not equal'}"
end
abort
end
end
def print
puts '````'
archives.each do |archive|
puts archive.basename
puts " sha1 #{Digest::SHA1.hexdigest(archive.data)}"
puts " sha256 #{Digest::SHA256.hexdigest(archive.data)}"
end
puts '````'
end
end
(ARGV.empty? ? [`git describe --tags --abbrev=0`.strip] : ARGV).each do |tag|
release = Release.new(tag)
release.check!
release.print
end
{
"name": "Homebrew/brew",
"image": "ghcr.io/homebrew/brew:latest",
"workspaceFolder": "/home/linuxbrew/.linuxbrew/Homebrew",
"workspaceMount": "source=${localWorkspaceFolder},target=/home/linuxbrew/.linuxbrew/Homebrew,type=bind,consistency=cached",
"onCreateCommand": ".devcontainer/on-create-command.sh",
"remoteEnv": {
"HOMEBREW_GITHUB_API_TOKEN": "${localEnv:GITHUB_TOKEN}"
}
}
#!/bin/bash
set -e
# fix permissions so Homebrew and Bundler don't complain
sudo chmod -R g-w,o-w /home/linuxbrew
# everything below is too slow to do unless prebuilding so skip it
CODESPACES_ACTION_NAME="$(jq --raw-output '.ACTION_NAME' /workspaces/.codespaces/shared/environment-variables.json)"
if [[ "${CODESPACES_ACTION_NAME}" != "createPrebuildTemplate" ]]
then
echo "Skipping slow items, not prebuilding."
exit 0
fi
# install Homebrew's development gems
brew install-bundler-gems --groups=sorbet
# install Homebrew formulae we might need
brew install shellcheck shfmt gh gnu-tar
# cleanup any mess
brew cleanup
# install some useful development things
sudo apt-get update
apt_get_install() {
sudo apt-get install -y \
-o Dpkg::Options::=--force-confdef \
-o Dpkg::Options::=--force-confnew \
"$@"
}
apt_get_install \
openssh-server \
zsh
# Ubuntu 18.04 doesn't include zsh-autosuggestions
if ! grep -q "Ubuntu 18.04" /etc/issue &>/dev/null
then
apt_get_install zsh-autosuggestions
fi
# Start the SSH server so that `gh cs ssh` works.
sudo service ssh start
{
"name": "Homebrew/brew-ubuntu18.04",
"image": "ghcr.io/homebrew/ubuntu18.04:latest",
"workspaceFolder": "/home/linuxbrew/.linuxbrew/Homebrew",
"workspaceMount": "source=${localWorkspaceFolder},target=/home/linuxbrew/.linuxbrew/Homebrew,type=bind,consistency=cached",
"onCreateCommand": ".devcontainer/on-create-command.sh",
"remoteEnv": {
"HOMEBREW_GITHUB_API_TOKEN": "${localEnv:GITHUB_TOKEN}"
}
}
Library/Homebrew/vendor/portable-ruby
Library/Taps
# https://editorconfig.org/
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
[{Library/Homebrew/**.rb,.simplecov}]
trim_trailing_whitespace = true
[Library/Taps/**.rb]
# trailing whitespace is crucial for patches
trim_trailing_whitespace = false
[**.drawio.svg]
indent_size = unset
indent_style = unset
insert_final_newline = false
[**.md]
trim_trailing_whitespace = true
x-soft-wrap-text = true
coverage:
round: nearest
status:
project:
default:
threshold: 0.05%
patch:
default:
informational: true
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
# The actions in triage-issues.yml are updated in the Homebrew/.github repo
ignore:
- dependency-name: actions/stale
- dependency-name: dessant/lock-threads
- package-ecosystem: bundler
directory: /Library/Homebrew
schedule:
interval: daily
allow:
- dependency-type: all
versioning-strategy: lockfile-only
name: New issue for Reproducible Bug
description: "If you're sure it's reproducible and not just your machine: submit an issue so we can investigate."
labels: [bug]
body:
- type: markdown
attributes:
value: Please note we will close your issue without comment if you do not correctly fill out the issue checklist below and provide ALL the requested information. If you repeatedly fail to use the issue template, we will block you from ever submitting issues to Homebrew again.
- type: textarea
attributes:
render: shell
label: "`brew doctor` output"
validations:
required: true
- type: checkboxes
attributes:
label: Verification
description: Please verify that you've followed these steps. If you cannot truthfully check these boxes, open a discussion at https://github.com/orgs/Homebrew/discussions instead.
options:
- label: My "`brew doctor` output" above says `Your system is ready to brew.` and am still able to reproduce my issue.
required: true
- label: I ran `brew update` twice and am still able to reproduce my issue.
required: true
- label: This issue's title and/or description do not reference a single formula e.g. `brew install wget`. If they do, open an issue at https://github.com/Homebrew/homebrew-core/issues/new/choose instead.
required: true
- type: textarea
attributes:
render: shell
label: "`brew config` output"
validations:
required: true
- type: textarea
attributes:
label: What were you trying to do (and why)?
validations:
required: true
- type: textarea
attributes:
label: What happened (include all command output)?
validations:
required: true
- type: textarea
attributes:
label: What did you expect to happen?
validations:
required: true
- type: textarea
attributes:
render: shell
label: Step-by-step reproduction instructions (by running `brew` commands)
validations:
required: true
blank_issues_enabled: false
contact_links:
- name: Get help in GitHub Discussions
url: https://github.com/orgs/Homebrew/discussions
about: Have a question? Not sure if your issue affects everyone reproducibly? The quickest way to get help is on Homebrew's GitHub Discussions!
- name: New issue on Homebrew/homebrew-core
url: https://github.com/Homebrew/homebrew-core/issues/new/choose
about: Having a `brew` problem with a `brew install` or `brew upgrade` of a single formula/package? Report it to Homebrew/homebrew-core (the core tap/repository).
- name: New issue on Homebrew/homebrew-cask
url: https://github.com/Homebrew/homebrew-cask/issues/new/choose
about: Having a `brew --cask` problem? Report it to Homebrew/homebrew-cask (the cask tap/repository).
- name: New issue on Homebrew/install
url: https://github.com/Homebrew/install/issues/new/choose
about: Having a problem installing Homebrew? Report it to Homebrew/install (the installer repository).
- name: Get help from Homebrew mirror maintainers
url: https://github.com/orgs/Homebrew/discussions/1917
about: Slow download speed? Homebrew mirror not working as expected? Please take a look at the mirror list and contact respective mirror maintainers.
name: New issue for Feature Suggestion
description: Request our thoughts on your suggestion for a new feature for Homebrew.
labels: features
body:
- type: markdown
attributes:
value: Please note we will close your issue without comment if you do not fill out the issue checklist below and provide ALL the requested information. If you repeatedly fail to use the issue template, we will block you from ever submitting issues to Homebrew again.
- type: checkboxes
attributes:
label: Verification
description: Please verify that you've followed these steps.
options:
- label: This issue's title and/or description do not reference a single formula e.g. `brew install wget`. If they do, open an issue at https://github.com/Homebrew/homebrew-core/issues/new/choose instead.
required: true
- type: textarea
attributes:
label: Provide a detailed description of the proposed feature
validations:
required: true
- type: textarea
attributes:
label: What is the motivation for the feature?
validations:
required: true
- type: textarea
attributes:
label: How will the feature be relevant to at least 90% of Homebrew users?
validations:
required: true
- type: textarea
attributes:
label: What alternatives to the feature have been considered?
validations:
required: true
- type: markdown
attributes:
value: We will close this issue or ask you to create a pull-request if it's something the maintainers are not actively planning to work on.
  • Have you followed the guidelines in our Contributing document?
  • Have you checked to ensure there aren't other open Pull Requests for the same change?
  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your changes? Here's an example.
  • Have you successfully run brew style with your changes locally?
  • Have you successfully run brew typecheck with your changes locally?
  • Have you successfully run brew tests with your changes locally?

changelog:
exclude:
authors:
- dependabot
- BrewTestBot
name: Build Homebrew package
on:
push:
paths:
- .github/workflows/build-pkg.yml
- package/scripts
release:
types:
- published
jobs:
build:
runs-on: macos-12
env:
IDENTIFIER: sh.brew.Homebrew
TMP_PATH: /tmp/brew
MIN_OS: '11.0'
steps:
- uses: actions/checkout@v3
with:
path: brew
fetch-depth: 0
- name: Version name
id: print-version
run: |
echo "version=$(git -C brew describe --tags --always)" > $GITHUB_OUTPUT
- name: Build package
run: |
pkgbuild --root brew \
--scripts brew/package/scripts \
--install-location "$TMP_PATH" \
--identifier "$IDENTIFIER" \
--min-os-version "$MIN_OS" \
--filter .DS_Store \
--version ${{ steps.print-version.outputs.version }} \
Homebrew-${{ steps.print-version.outputs.version }}.pkg
- uses: actions/upload-artifact@v3
with:
name: Homebrew ${{ steps.print-version.outputs.version }}
path: Homebrew-${{ steps.print-version.outputs.version }}.pkg
name: "CodeQL"
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
analyze:
name: Analyze
runs-on: ubuntu-22.04
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ruby
config-file: ./.github/codeql/codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
name: Docker
on:
push:
paths:
- .github/workflows/docker.yml
- Dockerfile
branches-ignore:
- master
release:
types:
- published
permissions:
contents: read
jobs:
ubuntu:
if: startsWith(github.repository, 'Homebrew/')
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
version: ["16.04", "18.04", "20.04", "22.04"]
steps:
- name: Checkout
uses: actions/checkout@main
with:
fetch-depth: 0
persist-credentials: false
- name: Fetch origin/master from Git
run: git fetch origin master
- name: Build Docker image
run: |
brew_version="$(git describe --tags --dirty --abbrev=7)"
echo "Building for Homebrew ${brew_version}"
docker build -t brew \
--build-arg=version=${{matrix.version}} \
--label org.opencontainers.image.created="$(date --rfc-3339=seconds --utc)" \
--label org.opencontainers.image.url="https://brew.sh" \
--label org.opencontainers.image.documentation="https://docs.brew.sh" \
--label org.opencontainers.image.source="https://github.com/${GITHUB_REPOSITORY}" \
--label org.opencontainers.image.version="${brew_version}" \
--label org.opencontainers.image.revision="${GITHUB_SHA}" \
--label org.opencontainers.image.vendor="${GITHUB_REPOSITORY_OWNER}" \
--label org.opencontainers.image.licenses="BSD-2-Clause" \
.
- name: Run brew test-bot --only-setup
run: docker run --rm brew brew test-bot --only-setup
- name: Deploy the tagged Docker image to GitHub Packages
if: startsWith(github.ref, 'refs/tags/')
run: |
brew_version="${GITHUB_REF:10}"
echo "brew_version=${brew_version}" >> "${GITHUB_ENV}"
echo ${{secrets.HOMEBREW_BREW_GITHUB_PACKAGES_TOKEN}} | docker login ghcr.io -u BrewTestBot --password-stdin
docker tag brew "ghcr.io/homebrew/ubuntu${{matrix.version}}:${brew_version}"
docker push "ghcr.io/homebrew/ubuntu${{matrix.version}}:${brew_version}"
docker tag brew "ghcr.io/homebrew/ubuntu${{matrix.version}}:latest"
docker push "ghcr.io/homebrew/ubuntu${{matrix.version}}:latest"
- name: Deploy the tagged Docker image to Docker Hub
if: startsWith(github.ref, 'refs/tags/')
run: |
echo ${{secrets.HOMEBREW_BREW_DOCKER_TOKEN}} | docker login -u brewtestbot --password-stdin
docker tag brew "homebrew/ubuntu${{matrix.version}}:${brew_version}"
docker push "homebrew/ubuntu${{matrix.version}}:${brew_version}"
docker tag brew "homebrew/ubuntu${{matrix.version}}:latest"
docker push "homebrew/ubuntu${{matrix.version}}:latest"
- name: Deploy the homebrew/brew Docker image to GitHub Packages and Docker Hub
if: startsWith(github.ref, 'refs/tags/') && matrix.version == '22.04'
run: |
docker tag brew "ghcr.io/homebrew/brew:${brew_version}"
docker push "ghcr.io/homebrew/brew:${brew_version}"
docker tag brew "ghcr.io/homebrew/brew:latest"
docker push "ghcr.io/homebrew/brew:latest"
docker tag brew "homebrew/brew:${brew_version}"
docker push "homebrew/brew:${brew_version}"
docker tag brew "homebrew/brew:latest"
docker push "homebrew/brew:latest"
name: Documentation CI
on:
push:
branches:
- master
pull_request:
permissions:
contents: read
jobs:
linting:
if: github.repository == 'Homebrew/brew'
runs-on: ubuntu-22.04
defaults:
run:
working-directory: docs
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Install vale
run: brew install vale
- name: Run vale for docs linting
run: vale .
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "2.7"
bundler-cache: true
working-directory: docs
- name: Check Markdown syntax
run: bundle exec rake lint
- name: Build docs site
run: bundle exec rake build
rubydoc:
if: github.repository == 'Homebrew/brew'
runs-on: ubuntu-22.04
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/rubydoc/Gemfile
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Checkout Homebrew/rubydoc.brew.sh
uses: actions/checkout@main
with:
repository: Homebrew/rubydoc.brew.sh
path: rubydoc
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "2.7"
bundler-cache: true
- name: Process rubydoc comments
working-directory: Library/Homebrew
run: bundle exec yard doc --plugin sorbet --no-output --fail-on-warning
name: brew doctor
on:
pull_request:
paths:
- .github/workflows/doctor.yml
- Library/Homebrew/cmd/doctor.rb
- Library/Homebrew/diagnostic.rb
- Library/Homebrew/extend/os/diagnostic.rb
- Library/Homebrew/extend/os/mac/diagnostic.rb
- Library/Homebrew/os/mac/xcode.rb
permissions:
contents: read
env:
HOMEBREW_DEVELOPER: 1
HOMEBREW_NO_AUTO_UPDATE: 1
jobs:
tests:
strategy:
matrix:
runner:
- "13-arm64-${{github.run_id}}-${{github.run_attempt}}"
- "12-arm64"
- "12-${{github.run_id}}-${{github.run_attempt}}"
- "11-arm64"
- "11-${{github.run_id}}-${{github.run_attempt}}"
- "10.15-${{github.run_id}}-${{github.run_attempt}}"
fail-fast: false
runs-on: ${{ matrix.runner }}
env:
PATH: "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
defaults:
run:
working-directory: /tmp
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- run: brew test-bot --only-cleanup-before
if: !contains(matrix.runner, github.run_id)
- run: brew doctor
- run: brew test-bot --only-cleanup-after
if: always() && !contains(matrix.runner, github.run_id)
name: Update Sorbet files
on:
push:
paths:
- .github/workflows/sorbet.yml
branches-ignore:
- master
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
tapioca:
if: github.repository == 'Homebrew/brew'
runs-on: macos-12
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Configure Git user
uses: Homebrew/actions/git-user-config@master
with:
username: BrewTestBot
- name: Set up commit signing
uses: Homebrew/actions/setup-commit-signing@master
with:
signing_key: ${{ secrets.BREWTESTBOT_GPG_SIGNING_SUBKEY }}
- name: Update RBI files
id: update
env:
GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_PUBLIC_REPO_TOKEN }}
HOMEBREW_GPG_PASSPHRASE: ${{ secrets.BREWTESTBOT_GPG_SIGNING_SUBKEY_PASSPHRASE }}
run: |
git fetch origin
BRANCH="sorbet-files-update"
echo "branch=${BRANCH}" >> $GITHUB_OUTPUT
if git ls-remote --exit-code --heads origin "${BRANCH}"
then
git checkout "${BRANCH}"
git checkout "${GITHUB_WORKSPACE}/Library/Homebrew/sorbet"
else
git checkout --no-track -B "${BRANCH}" origin/master
fi
brew typecheck --update
if ! git diff --stat --exit-code "${GITHUB_WORKSPACE}/Library/Homebrew/sorbet"
then
git add "${GITHUB_WORKSPACE}/Library/Homebrew/sorbet"
git commit -m "sorbet: Update RBI files." \
-m "Autogenerated by the [sorbet](https://github.com/Homebrew/brew/blob/master/.github/workflows/sorbet.yml) workflow."
echo "committed=true" >> $GITHUB_OUTPUT
PULL_REQUEST_STATE="$(gh pr view --json=state | jq -r ".state")"
if [[ "${PULL_REQUEST_STATE}" != "OPEN" ]]
then
echo "pull_request=true" >> $GITHUB_OUTPUT
fi
fi
- name: Push commits
if: steps.update.outputs.committed == 'true'
uses: Homebrew/actions/git-try-push@master
with:
token: ${{ secrets.HOMEBREW_GITHUB_PUBLIC_REPO_TOKEN }}
branch: ${{ steps.update.outputs.branch }}
force: true
origin_branch: "master"
- name: Open a pull request
if: steps.update.outputs.pull_request == 'true'
run: hub pull-request --no-edit
env:
GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_PUBLIC_REPO_TOKEN }}
name: Update SPDX license data
on:
push:
paths:
- .github/workflows/spdx.yml
branches-ignore:
- master
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
spdx:
if: github.repository == 'Homebrew/brew'
runs-on: ubuntu-22.04
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Configure Git user
uses: Homebrew/actions/git-user-config@master
with:
username: BrewTestBot
- name: Set up commit signing
uses: Homebrew/actions/setup-commit-signing@master
with:
signing_key: ${{ secrets.BREWTESTBOT_GPG_SIGNING_SUBKEY }}
- name: Update SPDX license data
id: update
env:
GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_PUBLIC_REPO_TOKEN }}
HOMEBREW_GPG_PASSPHRASE: ${{ secrets.BREWTESTBOT_GPG_SIGNING_SUBKEY_PASSPHRASE }}
run: |
git fetch origin
BRANCH="spdx-update"
echo "branch=${BRANCH}" >> $GITHUB_OUTPUT
if git ls-remote --exit-code --heads origin "${BRANCH}"
then
git checkout "${BRANCH}"
git checkout "${GITHUB_WORKSPACE}/Library/Homebrew/data/spdx"
else
git checkout --no-track -B "${BRANCH}" origin/master
fi
if brew update-license-data
then
git add "${GITHUB_WORKSPACE}/Library/Homebrew/data/spdx"
git commit -m "spdx: update license data." -m "Autogenerated by [a scheduled GitHub Action](https://github.com/Homebrew/brew/blob/master/.github/workflows/spdx.yml)."
echo "committed=true" >> $GITHUB_OUTPUT
PULL_REQUEST_STATE="$(gh pr view --json=state | jq -r ".state")"
if [[ "${PULL_REQUEST_STATE}" != "OPEN" ]]
then
echo "pull_request=true" >> $GITHUB_OUTPUT
fi
fi
- name: Push commits
if: steps.update.outputs.committed == 'true'
uses: Homebrew/actions/git-try-push@master
with:
token: ${{ secrets.HOMEBREW_GITHUB_PUBLIC_REPO_TOKEN }}
branch: ${{ steps.update.outputs.branch }}
force: true
origin_branch: "master"
- name: Open a pull request
if: steps.update.outputs.pull_request == 'true'
run: hub pull-request --no-edit
env:
GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_PUBLIC_REPO_TOKEN }}
name: Update sponsors, maintainers, manpage and completions
on:
push:
paths:
- .github/workflows/sponsors-maintainers-man-completions.yml
- README.md
- Library/Homebrew/cmd/**
- Library/Homebrew/dev-cmd/**
- Library/Homebrew/completions/**
- Library/Homebrew/manpages/**
- Library/Homebrew/cli/parser.rb
- Library/Homebrew/completions.rb
- Library/Homebrew/env_config.rb
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
updates:
runs-on: ubuntu-22.04
if: github.repository == 'Homebrew/brew'
steps:
- name: Setup Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Configure Git user
uses: Homebrew/actions/git-user-config@master
with:
username: BrewTestBot
- name: Set up commit signing
uses: Homebrew/actions/setup-commit-signing@master
with:
signing_key: ${{ secrets.BREWTESTBOT_GPG_SIGNING_SUBKEY }}
- name: Cache Bundler RubyGems
uses: actions/cache@v1
with:
path: ${{ steps.set-up-homebrew.outputs.gems-path }}
key: ${{ runner.os }}-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }}
restore-keys: ${{ runner.os }}-rubygems-
- name: Update sponsors, maintainers, manpage and completions
id: update
run: |
git fetch origin
if [[ -n "$GITHUB_REF_NAME" && "$GITHUB_REF_NAME" != "master" ]]
then
BRANCH="$GITHUB_REF_NAME"
else
BRANCH=sponsors-maintainers-man-completions
fi
echo "branch=${BRANCH}" >> $GITHUB_OUTPUT
if git ls-remote --exit-code --heads origin "${BRANCH}"
then
git checkout "${BRANCH}"
git checkout "${GITHUB_WORKSPACE}/README.md" \
"${GITHUB_WORKSPACE}/docs/Manpage.md" \
"${GITHUB_WORKSPACE}/manpages/brew.1" \
"${GITHUB_WORKSPACE}/completions"
else
git checkout --no-track -B "${BRANCH}" origin/master
fi
if brew update-sponsors
then
git add "${GITHUB_WORKSPACE}/README.md"
git commit -m "Update sponsors." \
-m "Autogenerated by the [sponsors-maintainers-man-completions](https://github.com/Homebrew/brew/blob/HEAD/.github/workflows/sponsors-maintainers-man-completions.yml) workflow."
COMMITTED=true
fi
if brew update-maintainers
then
git add "${GITHUB_WORKSPACE}/README.md" \
"${GITHUB_WORKSPACE}/docs/Manpage.md" \
"${GITHUB_WORKSPACE}/manpages/brew.1"
git commit -m "Update maintainers." \
-m "Autogenerated by the [sponsors-maintainers-man-completions](https://github.com/Homebrew/brew/blob/HEAD/.github/workflows/sponsors-maintainers-man-completions.yml) workflow."
COMMITTED=true
fi
if brew generate-man-completions
then
git add "${GITHUB_WORKSPACE}/README.md" \
"${GITHUB_WORKSPACE}/docs/Manpage.md" \
"${GITHUB_WORKSPACE}/manpages/brew.1" \
"${GITHUB_WORKSPACE}/completions"
git commit -m "Update manpage and completions." \
-m "Autogenerated by the [sponsors-maintainers-man-completions](https://github.com/Homebrew/brew/blob/HEAD/.github/workflows/sponsors-maintainers-man-completions.yml) workflow."
COMMITTED=true
fi
if [[ -n "$COMMITTED" ]]
then
echo "committed=true" >> $GITHUB_OUTPUT
PULL_REQUEST_STATE="$(gh pr view --json=state | jq -r ".state")"
if [[ "${PULL_REQUEST_STATE}" != "OPEN" ]]
then
echo "pull_request=true" >> $GITHUB_OUTPUT
fi
fi
env:
GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_PUBLIC_REPO_TOKEN }}
HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.HOMEBREW_BREW_UPDATE_SPONSORS_MAINTAINERS_TOKEN }}
HOMEBREW_GPG_PASSPHRASE: ${{ secrets.BREWTESTBOT_GPG_SIGNING_SUBKEY_PASSPHRASE }}
- name: Push commits
if: steps.update.outputs.committed == 'true'
uses: Homebrew/actions/git-try-push@master
with:
token: ${{ secrets.HOMEBREW_GITHUB_PUBLIC_REPO_TOKEN }}
branch: ${{ steps.update.outputs.branch }}
force: true
origin_branch: "master"
- name: Open a pull request
if: steps.update.outputs.pull_request == 'true'
run: hub pull-request --no-edit
env:
GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_PUBLIC_REPO_TOKEN }}
name: CI
on:
push:
branches:
- master
pull_request:
permissions:
contents: read
env:
HOMEBREW_DEVELOPER: 1
HOMEBREW_NO_AUTO_UPDATE: 1
HOMEBREW_NO_ENV_HINTS: 1
concurrency:
group: "${{ github.ref }}"
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
syntax:
if: github.repository == 'Homebrew/brew'
runs-on: ubuntu-22.04
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Cache Bundler RubyGems
uses: actions/cache@v1
with:
path: ${{ steps.set-up-homebrew.outputs.gems-path }}
key: ${{ runner.os }}-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }}
restore-keys: ${{ runner.os }}-rubygems-
- name: Install Bundler RubyGems
run: brew install-bundler-gems --groups=sorbet
- name: Install shellcheck and shfmt
run: brew install shellcheck shfmt
- run: brew style --display-cop-names
- run: brew typecheck
tap-syntax:
name: tap syntax
needs: syntax
if: startsWith(github.repository, 'Homebrew/')
runs-on: ubuntu-22.04
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- run: brew test-bot --only-cleanup-before
- run: brew config
- name: Cache Bundler RubyGems
uses: actions/cache@v1
with:
path: ${{ steps.set-up-homebrew.outputs.gems-path }}
key: ${{ runner.os }}-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }}
restore-keys: ${{ runner.os }}-rubygems-
- name: Install Bundler RubyGems
run: brew install-bundler-gems --groups=sorbet
- run: brew doctor
- name: Run brew update-tests
if: github.event_name == 'pull_request'
run: |
brew update-test
brew update-test --to-tag
brew update-test --commit=HEAD
- name: Run brew readall on all taps
run: brew readall --eval-all --aliases
- name: Run brew style on homebrew-core
run: brew style --display-cop-names homebrew/core
- name: Run brew audit --skip-style on all taps
run: brew audit --eval-all --skip-style --except=version --display-failures-only
- name: Set up all Homebrew taps
run: |
brew tap homebrew/aliases
brew tap homebrew/autoupdate
brew tap homebrew/bundle
brew tap homebrew/cask
brew tap homebrew/cask-drivers
brew tap homebrew/cask-fonts
brew tap homebrew/cask-versions
brew tap homebrew/command-not-found
brew tap homebrew/formula-analytics
brew tap homebrew/portable-ruby
brew tap homebrew/services
brew update-reset Library/Taps/homebrew/homebrew-bundle
# brew style doesn't like world writable directories
sudo chmod -R g-w,o-w "$(brew --repo)/Library/Taps"
- name: Run brew audit --skip-style on homebrew-core for macOS
run: brew audit --skip-style --except=version --tap=homebrew/core
env:
HOMEBREW_SIMULATE_MACOS_ON_LINUX: 1
- name: Run brew style on official taps
run: |
brew style --display-cop-names homebrew/bundle \
homebrew/services \
homebrew/test-bot
brew style --display-cop-names homebrew/aliases\
homebrew/autoupdate\
homebrew/command-not-found \
homebrew/formula-analytics \
homebrew/portable-ruby
- name: Run brew style on cask taps
run: |
brew style --display-cop-names homebrew/cask \
homebrew/cask-drivers \
homebrew/cask-fonts \
homebrew/cask-versions
vendored-gems:
name: vendored gems
runs-on: ubuntu-22.04
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Configure Git user
uses: Homebrew/actions/git-user-config@master
with:
username: BrewTestBot
# Can't cache this because we need to check that it doesn't fail the
# "uncommitted RubyGems" step with a cold cache.
- name: Install Bundler RubyGems
run: brew install-bundler-gems --groups=sorbet
- name: Check for uncommitted RubyGems
run: git diff --stat --exit-code Library/Homebrew/vendor/bundle/ruby
docker:
needs: syntax
runs-on: ubuntu-22.04
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Build Docker image
run: |
docker build -t brew --build-arg=version=22.04 \
--label org.opencontainers.image.created="$(date --rfc-3339=seconds --utc)" \
--label org.opencontainers.image.url="https://brew.sh" \
--label org.opencontainers.image.documentation="https://docs.brew.sh" \
--label org.opencontainers.image.source="https://github.com/${GITHUB_REPOSITORY}" \
--label org.opencontainers.image.revision="${GITHUB_SHA}" \
--label org.opencontainers.image.vendor="${GITHUB_REPOSITORY_OWNER}" \
--label org.opencontainers.image.licenses="BSD-2-Clause" \
.
- name: Deploy the Docker image to GitHub Packages and Docker Hub
if: github.ref == 'refs/heads/master'
run: |
echo ${{secrets.HOMEBREW_BREW_GITHUB_PACKAGES_TOKEN}} |
docker login ghcr.io -u BrewTestBot --password-stdin
docker tag brew "ghcr.io/homebrew/ubuntu22.04:master"
docker push "ghcr.io/homebrew/ubuntu22.04:master"
echo ${{secrets.HOMEBREW_BREW_DOCKER_TOKEN}} |
docker login -u brewtestbot --password-stdin
docker tag brew "homebrew/ubuntu22.04:master"
docker push "homebrew/ubuntu22.04:master"
- name: Build deprecated 16.04 Docker image
run: |
echo "homebrew/ubuntu16.04:master is deprecated and will soon be retired. Use homebrew/ubuntu22.04:master or homebrew/ubuntu16.04 or homebrew/brew. For CI, homebrew/ubuntu22.04:master is recommended." > .docker-deprecate
docker build -t brew-deprecated --build-arg=version=16.04 \
--label org.opencontainers.image.created="$(date --rfc-3339=seconds --utc)" \
--label org.opencontainers.image.url="https://brew.sh" \
--label org.opencontainers.image.documentation="https://docs.brew.sh" \
--label org.opencontainers.image.source="https://github.com/${GITHUB_REPOSITORY}" \
--label org.opencontainers.image.revision="${GITHUB_SHA}" \
--label org.opencontainers.image.vendor="${GITHUB_REPOSITORY_OWNER}" \
--label org.opencontainers.image.licenses="BSD-2-Clause" \
--label org.opencontainers.image.support.end-of-support="2022-09-07T00:00:00Z" \
.
- name: Deploy the deprecated 16.04 Docker image to GitHub Packages and Docker Hub
if: github.ref == 'refs/heads/master'
run: |
docker tag brew-deprecated "ghcr.io/homebrew/ubuntu16.04:master"
docker push "ghcr.io/homebrew/ubuntu16.04:master"
docker tag brew-deprecated "homebrew/ubuntu16.04:master"
docker push "homebrew/ubuntu16.04:master"
tests:
name: ${{ matrix.name }}
needs: syntax
runs-on: ${{ matrix.runs-on }}
strategy:
matrix:
include:
- name: tests (no-compatibility mode)
test-flags: --no-compat --coverage
runs-on: ubuntu-22.04
- name: tests (generic OS)
test-flags: --generic --coverage
runs-on: ubuntu-22.04
- name: tests (Ubuntu 22.04)
test-flags: --online --coverage
runs-on: ubuntu-22.04
- name: tests (Ubuntu 18.04)
test-flags: --coverage
runs-on: ubuntu-18.04
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Cache Bundler RubyGems
uses: actions/cache@v1
with:
path: ${{ steps.set-up-homebrew.outputs.gems-path }}
key: ${{ runner.os }}-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }}
restore-keys: ${{ runner.os }}-rubygems-
- name: Install Bundler RubyGems
run: brew install-bundler-gems --groups=sorbet
- name: Create parallel test log directory
run: mkdir tests
- name: Cache parallel tests log
uses: actions/cache@v1
with:
path: tests
key: ${{ runner.os }}-${{ matrix.test-flags }}-parallel_runtime_rspec-${{ github.sha }}
restore-keys: ${{ runner.os }}-${{ matrix.test-flags }}-parallel_runtime_rspec-
- name: Install brew tests dependencies
run: brew install curl
- name: Run brew tests
run: |
# brew tests doesn't like world writable directories
sudo chmod -R g-w,o-w /home/linuxbrew/.linuxbrew/Homebrew
brew tests ${{ matrix.test-flags }}
env:
HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70
with:
files: Library/Homebrew/test/coverage/coverage.xml
test-default-formula-linux:
name: ${{ matrix.name }}
if: startsWith(github.repository, 'Homebrew/')
runs-on: ${{ matrix.runs-on }}
env:
HOMEBREW_BOOTSNAP: 1
strategy:
matrix:
include:
- name: test default formula (Ubuntu 22.04)
test-flags: --online --coverage
runs-on: ubuntu-22.04
- name: test default formula (Ubuntu 18.04)
test-flags: --online --coverage
runs-on: ubuntu-18.04
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- run: brew test-bot --only-cleanup-before
- run: brew test-bot --only-formulae --only-json-tab --test-default-formula
test-everything:
name: test everything (macOS)
needs: syntax
if: startsWith(github.repository, 'Homebrew/')
runs-on: macos-12
env:
HOMEBREW_BOOTSNAP: 1
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- run: brew test-bot --only-cleanup-before
- run: brew config
# Can't cache this because we need to check that it doesn't fail the
# "uncommitted RubyGems" step with a cold cache.
- name: Install Bundler RubyGems
run: brew install-bundler-gems --groups=sorbet
- name: Check for uncommitted RubyGems
run: git diff --stat --exit-code Library/Homebrew/vendor/bundle/ruby
- run: brew doctor
- name: Run brew update-tests
if: github.event_name == 'pull_request'
run: |
brew update-test
brew update-test --to-tag
brew update-test --commit=HEAD
- name: Set up all Homebrew taps
run: |
brew tap homebrew/cask
brew tap homebrew/cask-drivers
brew tap homebrew/cask-fonts
brew tap homebrew/cask-versions
brew update-reset Library/Taps/homebrew/homebrew-bundle \
Library/Taps/homebrew/homebrew-cask \
Library/Taps/homebrew/homebrew-cask-versions \
Library/Taps/homebrew/homebrew-services
- name: Run brew readall on all taps
run: brew readall --eval-all --aliases
- name: Install brew tests dependencies
run: brew install subversion curl
- name: Create parallel test log directory
run: mkdir tests
- name: Cache parallel tests log
uses: actions/cache@v1
with:
path: tests
key: ${{ runner.os }}-parallel_runtime_rspec-${{ github.sha }}
restore-keys: ${{ runner.os }}-parallel_runtime_rspec-
- name: Run brew tests
run: |
# Retry multiple times when using BuildPulse to detect and submit
# flakiness (because rspec-retry is disabled).
if [[ -n "${HOMEBREW_BUILDPULSE_ACCESS_KEY_ID}" ]]
then
brew tests --online --coverage ||
brew tests --online --coverage ||
brew tests --online --coverage
else
brew tests --online --coverage
fi
env:
HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# These cannot be queried at the macOS level on GitHub Actions.
HOMEBREW_LANGUAGES: en-GB
HOMEBREW_BUILDPULSE_ACCESS_KEY_ID: ${{ secrets.BUILDPULSE_ACCESS_KEY_ID }}
HOMEBREW_BUILDPULSE_SECRET_ACCESS_KEY: ${{ secrets.BUILDPULSE_SECRET_ACCESS_KEY }}
HOMEBREW_BUILDPULSE_ACCOUNT_ID: 1503512
HOMEBREW_BUILDPULSE_REPOSITORY_ID: 53238813
- run: brew test-bot --only-formulae --test-default-formula
- uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70
with:
files: Library/Homebrew/test/coverage/coverage.xml
# This file is synced from the `.github` repository, do not modify it directly.
name: Triage issues
on:
push:
paths:
- .github/workflows/triage-issues.yml
branches-ignore:
- dependabot/**
schedule:
# Once every day at midnight UTC
- cron: "0 0 * * *"
issue_comment:
permissions:
issues: write
pull-requests: write
concurrency:
group: triage-issues
cancel-in-progress: ${{ github.event_name != 'issue_comment' }}
jobs:
stale:
if: >
startsWith(github.repository, 'Homebrew/') && (
github.event_name != 'issue_comment' || (
contains(github.event.issue.labels.*.name, 'stale') ||
contains(github.event.pull_request.labels.*.name, 'stale')
)
)
runs-on: ubuntu-latest
steps:
- name: Mark/Close Stale Issues and Pull Requests
uses: actions/stale@v7
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 21
days-before-close: 7
stale-issue-message: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs.
stale-pr-message: >
This pull request has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs.
exempt-issue-labels: "gsoc-outreachy,help wanted,in progress"
exempt-pr-labels: "gsoc-outreachy,help wanted,in progress"
bump-pr-stale:
if: >
startsWith(github.repository, 'Homebrew/') && (
github.event_name != 'issue_comment' || (
contains(github.event.issue.labels.*.name, 'stale') ||
contains(github.event.pull_request.labels.*.name, 'stale')
)
)
runs-on: ubuntu-latest
steps:
- name: Mark/Close Stale `bump-formula-pr` and `bump-cask-pr` Pull Requests
uses: actions/stale@v7
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 2
days-before-close: 1
stale-pr-message: >
This pull request has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. To keep this
pull request open, add a `help wanted` or `in progress` label.
exempt-pr-labels: "help wanted,in progress"
any-of-labels: "bump-formula-pr,bump-cask-pr"
lock-threads:
if: startsWith(github.repository, 'Homebrew/') && github.event_name != 'issue_comment'
runs-on: ubuntu-latest
steps:
- name: Lock Outdated Threads
uses: dessant/lock-threads@c1b35aecc5cdb1a34539d14196df55838bb2f836
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
issue-inactive-days: 30
add-issue-labels: outdated
pr-inactive-days: 30
add-pr-labels: outdated
name: Triage
on:
pull_request_target:
types:
- opened
- synchronize
- reopened
- closed
- labeled
- unlabeled
schedule:
- cron: "0 */3 * * *" # every 3 hours
permissions: {}
concurrency: triage-${{ github.head_ref }}
jobs:
review:
runs-on: ubuntu-22.04
if: startsWith(github.repository, 'Homebrew/')
steps:
- name: Re-run this workflow
if: github.event_name == 'schedule' || github.event.action == 'closed'
uses: reitermarkus/rerun-workflow@1a15891377b6667377207cdd85b628b79ebd6f81
with:
token: ${{ secrets.HOMEBREW_GITHUB_PUBLIC_REPO_TOKEN }}
continuous-label: waiting for feedback
trigger-labels: critical
workflow: triage.yml
- name: Review pull request
if: >
(github.event_name == 'pull_request' || github.event_name == 'pull_request_target') &&
github.event.action != 'closed' && github.event.pull_request.state != 'closed'
uses: actions/github-script@v6
with:
github-token: ${{ secrets.HOMEBREW_BREW_TRIAGE_PULL_REQUESTS_TOKEN }}
script: |
async function approvePullRequest(pullRequestNumber) {
const reviews = await approvalsByAuthenticatedUser(pullRequestNumber)
if (reviews.length > 0) {
return
}
await github.rest.pulls.createReview({
...context.repo,
pull_number: pullRequestNumber,
event: 'APPROVE',
})
}
async function findComment(pullRequestNumber, id) {
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequestNumber,
})
const regex = new RegExp(`<!--\\s*#${id}\\s*-->`)
return comments.filter(comment => comment.body.match(regex))[0]
}
async function createOrUpdateComment(pullRequestNumber, id, message) {
const beginComment = await findComment(pullRequestNumber, id)
const body = `<!-- #${id} -->\n\n${message}`
if (beginComment) {
await github.rest.issues.updateComment({
...context.repo,
comment_id: beginComment.id,
body,
})
} else {
await github.rest.issues.createComment({
...context.repo,
issue_number: pullRequestNumber,
body,
})
}
}
async function approvalsByAuthenticatedUser(pullRequestNumber) {
const { data: user } = await github.rest.users.getAuthenticated()
const { data: reviews } = await github.rest.pulls.listReviews({
...context.repo,
pull_number: pullRequestNumber,
})
const approvals = reviews.filter(review => review.state == 'APPROVED')
return approvals.filter(review => review.user.login == user.login)
}
async function dismissApprovals(pullRequestNumber, message) {
const reviews = await approvalsByAuthenticatedUser(pullRequestNumber)
for (const review of reviews) {
await github.rest.pulls.dismissReview({
...context.repo,
pull_number: pullRequestNumber,
review_id: review.id,
message: message
});
}
}
function offsetDate(start, offsetHours, skippedDays) {
let end = new Date(start)
end.setUTCHours(end.getUTCHours() + (offsetHours % 24))
while (skippedDays.includes(end.getUTCDay()) || offsetHours >= 24) {
if (!skippedDays.includes(end.getUTCDay())) {
offsetHours -= 24
}
end.setUTCDate(end.getUTCDate() + 1)
}
if (skippedDays.includes(start.getUTCDay())) {
end.setUTCHours(offsetHours, 0, 0)
}
return end
}
function formatDate(date) {
return date.toISOString().replace(/\.\d+Z$/, ' UTC').replace('T', ' at ')
}
async function reviewPullRequest(pullRequestNumber) {
const { data: pullRequest } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pullRequestNumber,
})
const { data: user } = await github.rest.users.getAuthenticated()
if (pullRequest.user.login == user.login) {
core.warning('Pull request author is a bot.')
return
}
if (pullRequest.author_association != 'MEMBER') {
core.warning('Pull request author is not a member.')
return
}
const reviewLabel = 'waiting for feedback'
const criticalLabel = 'critical'
const labels = pullRequest.labels.map(label => label.name)
const hasReviewLabel = labels.includes(reviewLabel)
const hasCriticalLabel = labels.includes(criticalLabel)
const offsetHours = 24
const skippedDays = [
6, // Saturday
0, // Sunday
]
const currentDate = new Date()
const reviewStartDate = new Date(pullRequest.created_at)
const reviewEndDate = offsetDate(reviewStartDate, offsetHours, skippedDays)
const reviewEnded = currentDate > reviewEndDate
if (reviewEnded || hasCriticalLabel) {
let message
if (hasCriticalLabel && !reviewEnded) {
message = `Review period skipped due to \`${criticalLabel}\` label.`
} else {
message = 'Review period ended.'
}
if (hasReviewLabel) {
await github.rest.issues.removeLabel({
...context.repo,
issue_number: pullRequestNumber,
name: reviewLabel,
})
}
core.info(message)
await createOrUpdateComment(pullRequestNumber, 'review-period-end', message)
await approvePullRequest(pullRequestNumber)
} else {
const message = `Review period will end on ${formatDate(reviewEndDate)}.`
core.info(message)
await dismissApprovals(pullRequestNumber, 'Review period has not ended yet.')
await createOrUpdateComment(pullRequestNumber, 'review-period-begin', message)
const endComment = await findComment(pullRequestNumber, 'review-period-end')
if (endComment) {
await github.rest.issues.deleteComment({
...context.repo,
comment_id: endComment.id,
})
}
await github.rest.issues.addLabels({
...context.repo,
issue_number: pullRequestNumber,
labels: [reviewLabel],
})
core.setFailed('Review period has not ended yet.')
}
}
await reviewPullRequest(context.issue.number)
name: Vendor Gems
on:
pull_request_target:
workflow_dispatch:
inputs:
pull_request:
description: Pull request number
required: true
permissions:
contents: read
pull-requests: read
jobs:
vendor-gems:
if: >
startsWith(github.repository, 'Homebrew/') && (
github.event_name == 'workflow_dispatch' || (
github.event.pull_request.user.login == 'dependabot[bot]' &&
contains(github.event.pull_request.title, '/Library/Homebrew')
)
)
runs-on: macos-12
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Configure Git user
uses: Homebrew/actions/git-user-config@master
with:
username: BrewTestBot
- name: Set up commit signing
uses: Homebrew/actions/setup-commit-signing@master
with:
signing_key: ${{ secrets.BREWTESTBOT_GPG_SIGNING_SUBKEY }}
- name: Check out pull request
id: checkout
run: |
gh pr checkout '${{ github.event.pull_request.number || github.event.inputs.pull_request }}'
branch="$(git branch --show-current)"
echo "branch=${branch}" >> $GITHUB_OUTPUT
gem_name="$(echo "${branch}" | sed -E 's|.*/||;s|(.*)-.*$|\1|')"
echo "gem_name=${gem_name}" >> $GITHUB_OUTPUT
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Vendor Gems
env:
HOMEBREW_GPG_PASSPHRASE: ${{ secrets.BREWTESTBOT_GPG_SIGNING_SUBKEY_PASSPHRASE }}
run: brew vendor-gems
- name: Update RBI files
env:
GEM_NAME: ${{ steps.checkout.outputs.gem_name }}
HOMEBREW_GPG_PASSPHRASE: ${{ secrets.BREWTESTBOT_GPG_SIGNING_SUBKEY_PASSPHRASE }}
run: |
brew typecheck --update
if ! git diff --stat --exit-code "${GITHUB_WORKSPACE}/Library/Homebrew/sorbet"
then
git add "${GITHUB_WORKSPACE}/Library/Homebrew/sorbet"
git commit -m "Update RBI files for ${GEM_NAME}." \
-m "Autogenerated by the [vendor-gems](https://github.com/Homebrew/brew/blob/HEAD/.github/workflows/vendor-gems.yml) workflow."
fi
- name: Push to pull request
uses: Homebrew/actions/git-try-push@master
with:
token: ${{ secrets.HOMEBREW_GITHUB_PUBLIC_REPO_TOKEN }}
branch: ${{ steps.checkout.outputs.branch }}
force: true
# First, ignore everything.
/*
# Explicitly ignore OS X Finder thumbnail files.
.DS_Store
# Unignore the contents of `Library` as that's where our code lives.
!/Library/
# Ignore files within `Library` (again).
/Library/Homebrew/.npmignore
/Library/Homebrew/bin
/Library/Homebrew/doc
/Library/Homebrew/prof
/Library/Homebrew/test/.gem
/Library/Homebrew/test/.subversion
/Library/Homebrew/test/coverage
/Library/Homebrew/test/junit
/Library/Homebrew/test/fs_leak_log
/Library/Homebrew/vendor/portable-ruby
/Library/Taps
/Library/PinnedTaps
/Library/Homebrew/.byebug_history
/Library/Homebrew/sorbet/rbi/hidden-definitions/errors.txt
# Ignore Bundler files
**/.bundle/bin
**/.bundle/cache
**/vendor/bundle/ruby/*/bundler.lock
**/vendor/bundle/ruby/*/bin
**/vendor/bundle/ruby/*/build_info/
**/vendor/bundle/ruby/*/cache
**/vendor/bundle/ruby/*/extensions
**/vendor/bundle/ruby/*/gems/*/*
**/vendor/bundle/ruby/*/plugins
**/vendor/bundle/ruby/*/specifications
# Ignore YARD files
**/.yardoc
# Unignore vendored gems
!**/vendor/bundle/ruby/*/gems/*/lib
!**/vendor/bundle/ruby/*/gems/addressable-*/data
!**/vendor/bundle/ruby/*/gems/public_suffix-*/data
!**/vendor/bundle/ruby/*/gems/rubocop-performance-*/config
!**/vendor/bundle/ruby/*/gems/rubocop-rails-*/config
!**/vendor/bundle/ruby/*/gems/rubocop-rspec-*/config
!**/vendor/bundle/ruby/*/gems/rubocop-sorbet-*/config
# Ignore partially included gems where we don't need all files
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/all.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/cache.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/cache/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/concurrency/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/dependencies.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/dependencies/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/duration/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/json.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/json/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/log_subscriber.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/log_subscriber/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/messages/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/multibyte/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/number_helper.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/number_helper/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/testing/
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/atomic/
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/atomic_reference/
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/collection/
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/concern/
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/executor/
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/synchronization/
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/thread_safe/
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/utility/
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/*/*.jar
**/vendor/bundle/ruby/*/gems/i18n-*/lib/i18n/tests*
**/vendor/bundle/ruby/*/gems/mechanize-*/lib/*.rb
**/vendor/bundle/ruby/*/gems/mechanize-*/lib/mechanize/http/agent.rb
**/vendor/bundle/ruby/*/gems/mechanize-*/lib/mechanize/http/*auth*.rb
**/vendor/bundle/ruby/*/gems/mechanize-*/lib/mechanize/c*
**/vendor/bundle/ruby/*/gems/mechanize-*/lib/mechanize/d*
**/vendor/bundle/ruby/*/gems/mechanize-*/lib/mechanize/e*
**/vendor/bundle/ruby/*/gems/mechanize-*/lib/mechanize/f*
**/vendor/bundle/ruby/*/gems/mechanize-*/lib/mechanize/h*.rb
**/vendor/bundle/ruby/*/gems/mechanize-*/lib/mechanize/i*
**/vendor/bundle/ruby/*/gems/mechanize-*/lib/mechanize/p*
**/vendor/bundle/ruby/*/gems/mechanize-*/lib/mechanize/r*
**/vendor/bundle/ruby/*/gems/mechanize-*/lib/mechanize/t*
**/vendor/bundle/ruby/*/gems/mechanize-*/lib/mechanize/u*
**/vendor/bundle/ruby/*/gems/mechanize-*/lib/mechanize/x*
**/vendor/bundle/ruby/*/gems/thread_safe-*/lib/thread_safe/util
# Ignore dependencies we don't wish to vendor
**/vendor/bundle/ruby/*/gems/ast-*/
**/vendor/bundle/ruby/*/gems/bootsnap-*/
**/vendor/bundle/ruby/*/gems/bundler-*/
**/vendor/bundle/ruby/*/gems/byebug-*/
**/vendor/bundle/ruby/*/gems/coderay-*/
**/vendor/bundle/ruby/*/gems/colorize-*/
**/vendor/bundle/ruby/*/gems/commander-*/
**/vendor/bundle/ruby/*/gems/connection_pool-*/
**/vendor/bundle/ruby/*/gems/diff-lcs-*/
**/vendor/bundle/ruby/*/gems/docile-*/
**/vendor/bundle/ruby/*/gems/domain_name-*/
**/vendor/bundle/ruby/*/gems/ecma-re-validator-*/
**/vendor/bundle/ruby/*/gems/hana-*/
**/vendor/bundle/ruby/*/gems/highline-*/
**/vendor/bundle/ruby/*/gems/http-cookie-*/
**/vendor/bundle/ruby/*/gems/hpricot-*/
**/vendor/bundle/ruby/*/gems/jaro_winkler-*/
**/vendor/bundle/ruby/*/gems/json-*/
**/vendor/bundle/ruby/*/gems/json_schemer-*/
**/vendor/bundle/ruby/*/gems/mime-types-data-*/
**/vendor/bundle/ruby/*/gems/mime-types-*/
**/vendor/bundle/ruby/*/gems/mini_portile2-*/
**/vendor/bundle/ruby/*/gems/minitest-*/
**/vendor/bundle/ruby/*/gems/msgpack-*/
**/vendor/bundle/ruby/*/gems/mustache-*/
**/vendor/bundle/ruby/*/gems/net-http-digest_auth-*/
**/vendor/bundle/ruby/*/gems/net-http-persistent-*/
**/vendor/bundle/ruby/*/gems/nokogiri-*/
**/vendor/bundle/ruby/*/gems/ntlm-http-*/
**/vendor/bundle/ruby/*/gems/parallel-*/
**/vendor/bundle/ruby/*/gems/parallel_tests-*/
**/vendor/bundle/ruby/*/gems/parlour-*/
**/vendor/bundle/ruby/*/gems/parser-*/
**/vendor/bundle/ruby/*/gems/powerpack-*/
**/vendor/bundle/ruby/*/gems/psych-*/
**/vendor/bundle/ruby/*/gems/pry-*/
**/vendor/bundle/ruby/*/gems/racc-*/
**/vendor/bundle/ruby/*/gems/rainbow-*/
**/vendor/bundle/ruby/*/gems/rbi-*/
**/vendor/bundle/ruby/*/gems/rdiscount-*/
**/vendor/bundle/ruby/*/gems/regexp_parser-*/
**/vendor/bundle/ruby/*/gems/rexml-*/
**/vendor/bundle/ruby/*/gems/ronn-*/
**/vendor/bundle/ruby/*/gems/rspec-*/
**/vendor/bundle/ruby/*/gems/rspec-core-*/
**/vendor/bundle/ruby/*/gems/rspec-expectations-*/
**/vendor/bundle/ruby/*/gems/rspec_junit_formatter-*/
**/vendor/bundle/ruby/*/gems/rspec-its-*/
**/vendor/bundle/ruby/*/gems/rspec-mocks-*/
**/vendor/bundle/ruby/*/gems/rspec-retry-*/
**/vendor/bundle/ruby/*/gems/rspec-support-*/
**/vendor/bundle/ruby/*/gems/rspec-sorbet-*/
**/vendor/bundle/ruby/*/gems/rspec-wait-*/
**/vendor/bundle/ruby/*/gems/rubocop-1*/
**/vendor/bundle/ruby/*/gems/rubocop-ast-*/
**/vendor/bundle/ruby/*/gems/ruby-prof-*/
**/vendor/bundle/ruby/*/gems/simplecov-*/
**/vendor/bundle/ruby/*/gems/simplecov-html-*/
**/vendor/bundle/ruby/*/gems/sorbet-*/
!**/vendor/bundle/ruby/*/gems/sorbet-runtime-*/
**/vendor/bundle/ruby/*/gems/spoom-*/
**/vendor/bundle/ruby/*/gems/stackprof-*/
**/vendor/bundle/ruby/*/gems/strscan-*/
**/vendor/bundle/ruby/*/gems/tapioca-*/
**/vendor/bundle/ruby/*/gems/thor-*/
**/vendor/bundle/ruby/*/gems/unf_ext-*/
**/vendor/bundle/ruby/*/gems/unf-*/
**/vendor/bundle/ruby/*/gems/unicode-display_width-*/
**/vendor/bundle/ruby/*/gems/unparser-*/
**/vendor/bundle/ruby/*/gems/uri_template-*/
**/vendor/bundle/ruby/*/gems/webrobots-*/
**/vendor/bundle/ruby/*/gems/yard-*/
**/vendor/bundle/ruby/*/gems/yard-sorbet-*/
# Ignore `bin` contents (again).
/bin
# Unignore our `brew` script.
!/bin/brew
# Unignore our configuration/completions/documentation.
!/.devcontainer
!/.github
!/completions
!/docs
!/manpages
# Unignore our packaging files
!/package
# Ignore generated documentation site
/docs/_site
/docs/bin
/docs/.jekyll-metadata
/docs/vendor
/docs/Gemfile.lock
# Unignore our root-level metadata files.
!/.dockerignore
!/.editorconfig
!/.gitignore
!/.yardopts
!/.vale.ini
!/.shellcheckrc
!/CHANGELOG.md
!/CONTRIBUTING.md
!/Dockerfile
!/Dockerfile.test.yml
!/LICENSE.txt
!/README.md
# Unignore tests' bundle config
!/Library/Homebrew/test/.bundle/config
# Unignore editor configuration
!/.sublime
/.sublime/homebrew.sublime-workspace
!/.vscode
# Look for 'source'd files relative to the current working directory where the
# shellcheck command is invoked.
source-path=Library
# Allow opening any 'source'd file, even if not specified as input
external-sources=true
# SC2310: This function is invoked in an 'if' / ! condition so set -e will be
# disabled. Invoke separately if failures should cause the script to exit.
# See: https://github.com/koalaman/shellcheck/wiki/SC2310
#
# We don't use the `set -e` feature in Bash scripts yet.
# We do need the return status as condition for if switches.
# Allow `if command` and `if ! command`.
disable=SC2310
# SC2311: Bash implicitly disabled set -e for this function invocation because
# it's inside a command substitution. Add set -e; before it or enable inherit_errexit.
# See: https://github.com/koalaman/shellcheck/wiki/SC2311
#
# We don't use the `set -e` feature in Bash scripts yet.
# We don't need return codes for "$(command)", only stdout is needed.
# Allow `var="$(command)"`, etc.
disable=SC2311
# SC2312: Consider invoking this command separately to avoid masking its return
# value (or use '|| true' to ignore).
# See: https://github.com/koalaman/shellcheck/wiki/SC2312
#
# We don't need return codes for "$(command)", only stdout is needed.
# Allow `[[ -n "$(command)" ]]`, `func "$(command)"`, pipes, etc.
disable=SC2312
{
"folders":
[
{
"//": "Generated based on the contents of our .gitignore",
"path": "..",
"folder_exclude_patterns": [
"Caskroom",
"Cellar",
"Frameworks",
"bin",
"etc",
"include",
"lib",
"opt",
"sbin",
"share",
"var",
"Library/Homebrew/LinkedKegs",
"Library/Homebrew/Aliases"
],
"follow_symlinks": true
}
],
"settings": {
"tab_size": 2
}
}
StylesPath = ./docs/vale-styles
[*.md]
BasedOnStyles = Homebrew
{
"recommendations": [
"kaiwood.endwise",
"misogi.ruby-rubocop",
"rebornix.ruby",
"wingrunr21.vscode-ruby",
"timonwong.shellcheck",
"foxundermoon.shell-format",
"editorconfig.editorconfig"
]
}
{
"editor.insertSpaces": true,
"editor.tabSize": 2,
"files.encoding": "utf8",
"files.eol": "\n",
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"ruby.rubocop.executePath": "Library/Homebrew/shims/gems/",
"shellcheck.customArgs": [
"--shell=bash",
"--enable=all",
"--external-sources",
"--source-path=${workspaceFolder}/Library"
],
"shellformat.effectLanguages": [
"shellscript"
],
"shellformat.path": "${workspaceFolder}/Library/Homebrew/utils/shfmt.sh",
"shellformat.flag": "-i 2 -ci -ln bash",
"[shellscript]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.shellcheck": true,
}
}
}

Contributing to Homebrew

First time contributing to Homebrew? Read our Code of Conduct and review How To Open a Homebrew Pull Request.

Report a bug

  • Run brew update (twice).
  • Run and read brew doctor.
  • Read the Troubleshooting checklist.
  • Open an issue on the formula's repository or on Homebrew/brew if it's not a formula-specific issue.

Propose a feature

  • Open an issue with a detailed description of your proposed feature, the motivation for it and alternatives considered. Please note we may close this issue or ask you to create a pull-request if this is not something we see as sufficiently high priority.

Thanks!

ARG version=22.04
# version is passed through by Docker.
# shellcheck disable=SC2154
FROM ubuntu:"${version}"
ARG DEBIAN_FRONTEND=noninteractive
# We don't want to manually pin versions, happy to use whatever
# Ubuntu thinks is best.
# hadolint ignore=DL3008
# /etc/lsb-release is checked inside the container and sets DISTRIB_RELEASE.
# shellcheck disable=SC1091,SC2154
RUN apt-get update \
&& apt-get install -y --no-install-recommends software-properties-common gnupg-agent \
&& add-apt-repository -y ppa:git-core/ppa \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
acl \
bzip2 \
ca-certificates \
curl \
file \
fonts-dejavu-core \
g++ \
gawk \
git \
less \
libz-dev \
locales \
make \
netbase \
openssh-client \
patch \
sudo \
uuid-runtime \
tzdata \
jq \
&& if [[ "$(. /etc/lsb-release; echo "${DISTRIB_RELEASE}" | cut -d. -f1)" -ge 18 ]]; then apt-get install gpg; fi \
&& apt-get remove --purge -y software-properties-common \
&& apt-get autoremove --purge -y \
&& rm -rf /var/lib/apt/lists/* \
&& localedef -i en_US -f UTF-8 en_US.UTF-8 \
&& useradd -m -s /bin/bash linuxbrew \
&& echo 'linuxbrew ALL=(ALL) NOPASSWD:ALL' >>/etc/sudoers \
&& su - linuxbrew -c 'mkdir ~/.linuxbrew'
USER linuxbrew
COPY --chown=linuxbrew:linuxbrew . /home/linuxbrew/.linuxbrew/Homebrew
ENV PATH="/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:${PATH}"
WORKDIR /home/linuxbrew
RUN mkdir -p \
.linuxbrew/bin \
.linuxbrew/etc \
.linuxbrew/include \
.linuxbrew/lib \
.linuxbrew/opt \
.linuxbrew/sbin \
.linuxbrew/share \
.linuxbrew/var/homebrew/linked \
.linuxbrew/Cellar \
&& ln -s ../Homebrew/bin/brew .linuxbrew/bin/brew \
&& git -C .linuxbrew/Homebrew remote set-url origin https://github.com/Homebrew/brew \
&& git -C .linuxbrew/Homebrew fetch origin \
&& HOMEBREW_NO_ANALYTICS=1 HOMEBREW_NO_AUTO_UPDATE=1 brew tap homebrew/core \
&& brew install-bundler-gems \
&& brew cleanup \
&& { git -C .linuxbrew/Homebrew config --unset gc.auto; true; } \
&& { git -C .linuxbrew/Homebrew config --unset homebrew.devcmdrun; true; } \
&& rm -rf .cache
# TODO: Try getting more rules in sync.
require:
- ./Homebrew/rubocops.rb
- rubocop-performance
- rubocop-rails
inherit_mode:
merge:
- Include
- Exclude
AllCops:
TargetRubyVersion: 2.6
DisplayCopNames: false
ActiveSupportExtensionsEnabled: true
# enable all pending rubocops
NewCops: enable
Include:
- "**/*.rbi"
Exclude:
- "Homebrew/sorbet/rbi/gems/**/*.rbi"
- "Homebrew/sorbet/rbi/hidden-definitions/**/*.rbi"
- "Homebrew/sorbet/rbi/todo.rbi"
- "Homebrew/sorbet/rbi/upstream.rbi"
- "Homebrew/bin/*"
- "Homebrew/vendor/**/*"
- "Taps/*/*/vendor/**/*"
Cask/Desc:
Description: "Ensure that the desc stanza conforms to various content and style checks."
Enabled: true
Cask/HomepageUrlTrailingSlash:
Description: "Ensure that the homepage url has a slash after the domain name."
Enabled: true
Cask/NoDslVersion:
Description: "Do not use the deprecated DSL version syntax in your cask header."
Enabled: true
Cask/StanzaGrouping:
Description: "Ensure that cask stanzas are grouped correctly. More info at https://docs.brew.sh/Cask-Cookbook#stanza-order"
Enabled: true
Cask/StanzaOrder:
Description: "Ensure that cask stanzas are sorted correctly. More info at https://docs.brew.sh/Cask-Cookbook#stanza-order"
Enabled: true
# enable all formulae audits
FormulaAudit:
Enabled: true
# enable all formulae strict audits
FormulaAuditStrict:
Enabled: true
# enable all Homebrew custom cops
Homebrew:
Enabled: true
# only used internally
Homebrew/MoveToExtendOS:
Enabled: false
# makes DSL usage ugly.
Layout/SpaceBeforeBrackets:
Exclude:
- "**/*_spec.rb"
- "Taps/*/*/*.rb"
- "/**/{Formula,Casks}/*.rb"
- "**/{Formula,Casks}/*.rb"
# Use `<<~` for heredocs.
Layout/HeredocIndentation:
Enabled: true
# Keyword arguments don't have the same readability
# problems as normal parameters.
Metrics/ParameterLists:
CountKeywordArgs: false
# Allow dashes in filenames.
Naming/FileName:
Regex: !ruby/regexp /^[\w\@\-\+\.]+(\.rb)?$/
# Implicitly allow EOS as we use it everywhere.
Naming/HeredocDelimiterNaming:
ForbiddenDelimiters:
- END, EOD, EOF
Naming/InclusiveLanguage:
CheckStrings: true
FlaggedTerms:
# TODO: If possible, make this stricter.
slave:
AllowedRegex:
- "gitslave" # Used in formula `gitslave`
- "log_slave" # Used in formula `ssdb`
- "ssdb_slave" # Used in formula `ssdb`
- "var_slave" # Used in formula `ssdb`
- "patches/13_fix_scope_for_show_slave_status_data.patch" # Used in formula `mytop`
Naming/MethodName:
AllowedPatterns:
- '\A(fetch_)?HEAD\?\Z'
# Both styles are used depending on context,
# e.g. `sha256` and `something_countable_1`.
Naming/VariableNumber:
Enabled: false
# Require &&/|| instead of and/or
Style/AndOr:
Enabled: true
EnforcedStyle: always
# Avoid leaking resources.
Style/AutoResourceCleanup:
Enabled: true
# This makes these a little more obvious.
Style/BarePercentLiterals:
EnforcedStyle: percent_q
# Use consistent style for better readability.
Style/CollectionMethods:
Enabled: true
# This is quite a large change, so don't enforce this yet for formulae.
# We should consider doing so in the future, but be aware of the impact on third-party taps.
Style/FetchEnvVar:
Exclude:
- "Taps/*/*/*.rb"
- "/**/Formula/*.rb"
- "**/Formula/*.rb"
# Prefer tokens with type annotations for consistency
# between formatting numbers and strings.
Style/FormatStringToken:
EnforcedStyle: annotated
# autocorrectable and more readable
Style/HashEachMethods:
Enabled: true
Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: true
# Allow for license expressions
Style/HashAsLastArrayItem:
Exclude:
- "Taps/*/*/*.rb"
- "/**/Formula/*.rb"
- "**/Formula/*.rb"
# Enabled now LineLength is lowish.
Style/IfUnlessModifier:
Enabled: true
# Only use this for numbers >= `1_000_000`.
Style/NumericLiterals:
MinDigits: 7
Strict: true
Exclude:
- "**/Brewfile"
# Zero-prefixed octal literals are widely used and understood.
Style/NumericLiteralPrefix:
EnforcedOctalStyle: zero_only
# Rescuing `StandardError` is an understood default.
Style/RescueStandardError:
EnforcedStyle: implicit
# Returning `nil` is unnecessary.
Style/ReturnNil:
Enabled: true
# We have no use for using `warn` because we
# are calling Ruby with warnings disabled.
Style/StderrPuts:
Enabled: false
# Use consistent method names.
Style/StringMethods:
Enabled: true
# An array of symbols is more readable than a symbol array
# and also allows for easier grepping.
Style/SymbolArray:
EnforcedStyle: brackets
# Trailing commas make diffs nicer.
Style/TrailingCommaInArguments:
EnforcedStyleForMultiline: comma
Style/TrailingCommaInArrayLiteral:
EnforcedStyleForMultiline: comma
Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: comma
# Does not hinder readability, so might as well enable it.
Performance/CaseWhenSplat:
Enabled: true
# Makes code less readable for minor performance increases.
Performance/Caller:
Enabled: false
# Makes code less readable for minor performance increases.
Performance/MethodObjectAsBlock:
Enabled: false
Rails:
# Selectively enable what we want.
Enabled: false
# Cannot use ActiveSupport in RuboCops.
Exclude:
- "Homebrew/rubocops/**/*"
# These relate to ActiveSupport and not other parts of Rails.
Rails/ActiveSupportAliases:
Enabled: true
Rails/Blank:
Enabled: true
Rails/CompactBlank:
Enabled: true
Rails/Delegate:
Enabled: false # TODO
Rails/DelegateAllowBlank:
Enabled: true
Rails/DurationArithmetic:
Enabled: true
Rails/ExpandedDateRange:
Enabled: true
Rails/Inquiry:
Enabled: true
Rails/NegateInclude:
Enabled: true
Rails/PluralizationGrammar:
Enabled: true
Rails/Presence:
Enabled: true
Rails/Present:
Enabled: true
Rails/RelativeDateConstant:
Enabled: true
Rails/SafeNavigation:
Enabled: true
Rails/SafeNavigationWithBlank:
Enabled: true
Rails/StripHeredoc:
Enabled: true
Rails/ToFormattedS:
Enabled: true
# Don't allow cops to be disabled in casks and formulae.
Style/DisableCopsWithinSourceCodeDirective:
Enabled: true
Include:
- "Taps/*/*/*.rb"
- "/**/{Formula,Casks}/*.rb"
- "**/{Formula,Casks}/*.rb"
# make our hashes consistent
Layout/HashAlignment:
EnforcedHashRocketStyle: table
EnforcedColonStyle: table
# `system` is a special case and aligns on second argument, so allow this for formulae.
Layout/ArgumentAlignment:
Exclude:
- "Taps/*/*/*.rb"
- "/**/Formula/*.rb"
- "**/Formula/*.rb"
# this is a bit less "floaty"
Layout/CaseIndentation:
EnforcedStyle: end
# Need to allow #: for external commands.
Layout/LeadingCommentSpace:
Exclude:
- "Taps/*/*/cmd/*.rb"
# this is a bit less "floaty"
Layout/EndAlignment:
EnforcedStyleAlignWith: start_of_line
# conflicts with DSL-style path concatenation with `/`
Layout/SpaceAroundOperators:
Enabled: false
# layout is not configurable (https://github.com/rubocop-hq/rubocop/issues/6254).
Layout/RescueEnsureAlignment:
Enabled: false
# significantly less indentation involved; more consistent
Layout/FirstArrayElementIndentation:
EnforcedStyle: consistent
Layout/FirstHashElementIndentation:
EnforcedStyle: consistent
# favour parens-less DSL-style arguments
Lint/AmbiguousBlockAssociation:
Enabled: false
Lint/RequireRelativeSelfPath:
# bugged on formula-analytics
# https://github.com/Homebrew/brew/pull/12152/checks?check_run_id=3755137378#step:15:60
Exclude:
- "Taps/homebrew/homebrew-formula-analytics/*/*.rb"
Lint/DuplicateBranch:
Exclude:
- "Taps/*/*/*.rb"
- "/**/{Formula,Casks}/*.rb"
- "**/{Formula,Casks}/*.rb"
# needed for lazy_object magic
Naming/MemoizedInstanceVariableName:
Exclude:
- "Homebrew/lazy_object.rb"
# useful for metaprogramming in RSpec
Lint/ConstantDefinitionInBlock:
Exclude:
- "**/*_spec.rb"
# so many of these in formulae and can't be autocorrected
Lint/ParenthesesAsGroupedExpression:
Exclude:
- "Taps/*/*/*.rb"
- "/**/Formula/*.rb"
- "**/Formula/*.rb"
# Most metrics don't make sense to apply for casks/formulae/taps.
Metrics/AbcSize:
Exclude:
- "Taps/**/*"
- "/**/{Formula,Casks}/*.rb"
- "**/{Formula,Casks}/*.rb"
Metrics/BlockLength:
Exclude:
- "Taps/**/*"
- "/**/{Formula,Casks}/*.rb"
- "**/{Formula,Casks}/*.rb"
Metrics/ClassLength:
Exclude:
- "Taps/**/*"
- "/**/{Formula,Casks}/*.rb"
- "**/{Formula,Casks}/*.rb"
Metrics/CyclomaticComplexity:
Exclude:
- "Taps/**/*"
- "/**/{Formula,Casks}/*.rb"
- "**/{Formula,Casks}/*.rb"
Metrics/MethodLength:
Exclude:
- "Taps/**/*"
- "/**/{Formula,Casks}/*.rb"
- "**/{Formula,Casks}/*.rb"
Metrics/ModuleLength:
Exclude:
- "Taps/**/*"
- "/**/{Formula,Casks}/*.rb"
- "**/{Formula,Casks}/*.rb"
Metrics/PerceivedComplexity:
Exclude:
- "Taps/**/*"
- "/**/{Formula,Casks}/*.rb"
- "**/{Formula,Casks}/*.rb"
# allow those that are standard
# TODO: try to remove some of these
Naming/MethodParameterName:
inherit_mode:
merge:
- AllowedNames
AllowedNames:
- "a"
- "b"
- "cc"
- "c1"
- "c2"
- "d"
- "e"
- "f"
- "ff"
- "fn"
- "id"
- "o"
- "p"
- "pr"
- "r"
- "rb"
- "s"
- "v"
# GitHub diff UI wraps beyond 118 characters
Layout/LineLength:
Max: 118
# ignore manpage comments and long single-line strings
AllowedPatterns:
[
"#: ",
' url "',
' mirror "',
" plist_options ",
' appcast "',
' executable: "',
' font "',
' homepage "',
' name "',
' pkg "',
' pkgutil: "',
" sha256 cellar: ",
" sha256 ",
"#{language}",
"#{version.",
' "/Library/Application Support/',
'"/Library/Caches/',
'"/Library/PreferencePanes/',
' "~/Library/Application Support/',
'"~/Library/Caches/',
'"~/Application Support',
" was verified as official when first introduced to the cask",
]
Sorbet/FalseSigil:
Exclude:
- "Taps/**/*"
- "/**/{Formula,Casks}/*.rb"
- "**/{Formula,Casks}/*.rb"
- "Homebrew/test/**/Casks/**/*.rb"
Sorbet/StrictSigil:
inherit_mode:
override:
- Include
Enabled: true
Include:
- "**/*.rbi"
# Try getting rid of these.
Sorbet/ConstantsFromStrings:
Enabled: false
# Avoid false positives on modifiers used on symbols of methods
# See https://github.com/rubocop-hq/rubocop/issues/5953
Style/AccessModifierDeclarations:
Enabled: false
# Conflicts with type signatures on `attr_*`s.
Style/AccessorGrouping:
Enabled: false
# make rspec formatting more flexible
Style/BlockDelimiters:
Exclude:
- "Homebrew/**/*_spec.rb"
- "Homebrew/**/shared_examples/**/*.rb"
# TODO: remove this when possible.
Style/ClassVars:
Exclude:
- "**/developer/bin/*"
# Don't enforce documentation in casks or formulae.
Style/Documentation:
Exclude:
- "Taps/**/*"
- "/**/{Formula,Casks}/*.rb"
- "**/{Formula,Casks}/*.rb"
- "**/*.rbi"
Style/DocumentationMethod:
Include:
- "Homebrew/formula.rb"
# Not used for casks and formulae.
Style/FrozenStringLiteralComment:
EnforcedStyle: always
Exclude:
- "Taps/*/*/*.rb"
- "/**/{Formula,Casks}/*.rb"
- "**/{Formula,Casks}/*.rb"
- "Homebrew/test/**/Casks/**/*.rb"
- "**/*.rbi"
- "**/Brewfile"
# TODO: remove this when possible.
Style/GlobalVars:
Exclude:
- "**/developer/bin/*"
# potential for errors in formulae too high with this
Style/GuardClause:
Exclude:
- "Taps/*/*/*.rb"
- "/**/{Formula,Casks}/*.rb"
- "**/{Formula,Casks}/*.rb"
# avoid hash rockets where possible
Style/HashSyntax:
EnforcedStyle: ruby19
# OpenStruct is a nice helper.
Style/OpenStructUse:
Enabled: false
# so many of these in formulae and can't be autocorrected
Style/StringConcatenation:
Exclude:
- "Taps/*/*/*.rb"
- "/**/{Formula,Casks}/*.rb"
- "**/{Formula,Casks}/*.rb"
# ruby style guide favorite
Style/StringLiterals:
EnforcedStyle: double_quotes
# consistency with above
Style/StringLiteralsInInterpolation:
EnforcedStyle: double_quotes
# make things a bit easier to read
Style/TernaryParentheses:
EnforcedStyle: require_parentheses_when_complex
# `unless ... ||` and `unless ... &&` are hard to mentally parse
Style/UnlessLogicalOperators:
Enabled: true
EnforcedStyle: forbid_logical_operators
# a bit confusing to non-Rubyists but useful for longer arrays
Style/WordArray:
MinSize: 4
# would rather freeze too much than too little
Style/MutableConstant:
EnforcedStyle: strict
# unused keyword arguments improve APIs
Lint/UnusedMethodArgument:
AllowUnusedKeywordArguments: true
inherit_from: ./.rubocop.yml
# Intentionally disabled as it doesn't fit with our code style.
RSpec/AnyInstance:
Enabled: false
RSpec/FilePath:
Enabled: false
RSpec/ImplicitBlockExpectation:
Enabled: false
RSpec/SubjectStub:
Enabled: false
# TODO: try to enable these
RSpec/DescribeClass:
Enabled: false
RSpec/LeakyConstantDeclaration:
Enabled: false
RSpec/MessageSpies:
Enabled: false
RSpec/RepeatedDescription:
Enabled: false
RSpec/StubbedMock:
Enabled: false
RSpec/NoExpectationExample:
Enabled: false
# TODO: try to reduce these
RSpec/ExampleLength:
Max: 75
RSpec/MultipleExpectations:
Max: 26
RSpec/NestedGroups:
Max: 5
RSpec/MultipleMemoizedHelpers:
Max: 12
# Annoying to have these autoremoved.
RSpec/Focus:
AutoCorrect: false
# Gets confused on these tests for a `skip` DSL
RSpec/PendingWithoutReason:
Exclude:
- "**/dependency_expansion_spec.rb"
- "**/livecheck/skip_conditions_spec.rb"
- "**/livecheck_spec.rb"
---
BUNDLE_BIN: "false"
BUNDLE_CLEAN: "true"
BUNDLE_DISABLE_SHARED_GEMS: "true"
BUNDLE_FORGET_CLI_OPTIONS: "true"
BUNDLE_JOBS: "4"
BUNDLE_PATH: "vendor/bundle"
BUNDLE_RETRY: "3"
parser: false
output_file:
rbi: sorbet/rbi/parlour.rbi
relative_requires:
- sorbet/parlour.rb
plugins:
Attr: {}
--format NoSeedProgressFormatter
--format ParallelTests::RSpec::RuntimeLogger
--out <%= ENV["PARALLEL_RSPEC_LOG_PATH"] %>
--format RspecJunitFormatter
--out test/junit/rspec<%= ENV["TEST_ENV_NUMBER"] %>.xml
<%= "--format RSpec::Github::Formatter" if ENV["GITHUB_ACTIONS"] %>
inherit_from:
- ../.rubocop_rspec.yml
- .rubocop_todo.yml
Homebrew/MoveToExtendOS:
Enabled: true
Exclude:
- "{extend,test,requirements}/**/*"
- "os.rb"
# make rspec formatting more flexible
Layout/MultilineMethodCallIndentation:
Exclude:
- "**/*_spec.rb"
# `formula do` uses nested method definitions
Lint/NestedMethodDefinition:
Exclude:
- "test/**/*"
# TODO: Try to bring down all metrics maximums.
Metrics/AbcSize:
Max: 280
Metrics/BlockLength:
Max: 106
Exclude:
# TODO: extract more of the bottling logic
- "dev-cmd/bottle.rb"
- "test/**/*"
- "cmd/install.rb"
Metrics/BlockNesting:
Max: 5
Metrics/ClassLength:
Max: 800
Exclude:
- "formula.rb"
- "formula_installer.rb"
Metrics/CyclomaticComplexity:
Max: 80
Metrics/PerceivedComplexity:
Max: 90
Metrics/MethodLength:
Max: 260
Metrics/ModuleLength:
Max: 500
Exclude:
# TODO: extract more of the bottling logic
- "dev-cmd/bottle.rb"
# TODO: try break this down
- "utils/github.rb"
- "test/**/*"
Naming/PredicateName:
# Can't rename these.
AllowedMethods:
- is_a?
- is_32_bit?
- is_64_bit?
Style/HashAsLastArrayItem:
Exclude:
- "test/utils/spdx_spec.rb"
Style/BlockDelimiters:
BracesRequiredMethods:
- "sig"
Bundler/GemFilename:
Exclude:
- "utils/gems.rb"
Style/Documentation:
Exclude:
- "compat/**/*.rb"
- "extend/**/*.rb"
- "cmd/**/*.rb"
- "dev-cmd/**/*.rb"
- "test/**/*.rb"
- "cask/macos.rb"
- "cli/args.rb"
- "cli/parser.rb"
- "default_prefix.rb"
- "global.rb"
- "keg_relocate.rb"
- "os/mac/keg.rb"
- "reinstall.rb"
- "software_spec.rb"
- "upgrade.rb"
- "utils.rb"
- "utils/fork.rb"
- "utils/gems.rb"
- "utils/git_repository.rb"
- "utils/popen.rb"
- "utils/shell.rb"
- "version.rb"
Lint/EmptyBlock:
Exclude:
- "dev-cmd/extract.rb"
- "test/cache_store_spec.rb"
- "test/checksum_verification_spec.rb"
- "test/compiler_failure_spec.rb"
- "test/formula_spec.rb"
- "test/formula_validation_spec.rb"
- "test/pathname_spec.rb"
- "test/support/fixtures/cask/Casks/*flight*.rb"
#!/usr/bin/env ruby
# frozen_string_literal: true
require "English"
SimpleCov.enable_for_subprocesses true
SimpleCov.start do
coverage_dir File.expand_path("../test/coverage", File.realpath(__FILE__))
root File.expand_path("..", File.realpath(__FILE__))
# enables branch coverage as well as, the default, line coverage
enable_coverage :branch
# We manage the result cache ourselves and the default of 10 minutes can be
# too low (particularly on Travis CI), causing results from some integration
# tests to be dropped. This causes random fluctuations in test coverage.
merge_timeout 86400
at_fork do |pid|
# This needs a unique name so it won't be ovewritten
command_name "#{SimpleCov.command_name} (#{pid})"
# be quiet, the parent process will be in charge of output and checking coverage totals
SimpleCov.print_error_status = false
end
excludes = ["test", "vendor"]
subdirs = Dir.chdir(SimpleCov.root) { Pathname.glob("*") }
.reject { |p| p.extname == ".rb" || excludes.include?(p.to_s) }
.map { |p| "#{p}/**/*.rb" }.join(",")
files = "#{SimpleCov.root}/{#{subdirs},*.rb}"
if ENV["HOMEBREW_INTEGRATION_TEST"]
# This needs a unique name so it won't be ovewritten
command_name "#{ENV["HOMEBREW_INTEGRATION_TEST"]} (#{$PROCESS_ID})"
# be quiet, the parent process will be in charge of output and checking coverage totals
SimpleCov.print_error_status = false
SimpleCov.at_exit do
# Just save result, but don't write formatted output.
coverage_result = Coverage.result.dup
Dir[files].each do |file|
absolute_path = File.expand_path(file)
coverage_result[absolute_path] ||= SimpleCov::SimulateCoverage.call(absolute_path)
end
simplecov_result = SimpleCov::Result.new(coverage_result)
SimpleCov::ResultMerger.store_result(simplecov_result)
# If an integration test raises a `SystemExit` exception on exit,
# exit immediately using the same status code to avoid reporting
# an error when expecting a non-successful exit status.
raise if $ERROR_INFO.is_a?(SystemExit)
end
else
command_name "#{command_name} (#{$PROCESS_ID})"
# Not using this during integration tests makes the tests 4x times faster
# without changing the coverage.
track_files files
end
add_filter %r{^/build.rb$}
add_filter %r{^/config.rb$}
add_filter %r{^/constants.rb$}
add_filter %r{^/postinstall.rb$}
add_filter %r{^/test.rb$}
add_filter %r{^/compat/}
add_filter %r{^/dev-cmd/tests.rb$}
add_filter %r{^/test/}
add_filter %r{^/vendor/}
require "rbconfig"
host_os = RbConfig::CONFIG["host_os"]
add_filter %r{/os/mac} unless /darwin/.match?(host_os)
add_filter %r{/os/linux} unless /linux/.match?(host_os)
# Add groups and the proper project name to the output.
project_name "Homebrew"
add_group "Cask", %r{^/cask/}
add_group "Commands", [%r{/cmd/}, %r{^/dev-cmd/}]
add_group "Extensions", %r{^/extend/}
add_group "OS", [%r{^/extend/os/}, %r{^/os/}]
add_group "Requirements", %r{^/requirements/}
add_group "Scripts", [
%r{^/brew.rb$},
%r{^/build.rb$},
%r{^/postinstall.rb$},
%r{^/test.rb$},
]
end
--title "Homebrew Ruby API"
--main README.md
--markup markdown
--no-private
--load yard/ignore_directives.rb
--template-path yard/templates
--exclude test/
--exclude vendor/
--exclude compat/
extend/os/**/*.rb
**/*.rb
-
*.md
# typed: false
# frozen_string_literal: true
require "api/analytics"
require "api/cask"
require "api/cask-source"
require "api/formula"
require "api/versions"
require "extend/cachable"
module Homebrew
# Helper functions for using Homebrew's formulae.brew.sh API.
#
# @api private
module API
extend T::Sig
extend Cachable
module_function
API_DOMAIN = "https://formulae.brew.sh/api"
HOMEBREW_CACHE_API = (HOMEBREW_CACHE/"api").freeze
MAX_RETRIES = 3
sig { params(endpoint: String, json: T::Boolean).returns(T.any(String, Hash)) }
def fetch(endpoint, json: true)
return cache[endpoint] if cache.present? && cache.key?(endpoint)
api_url = "#{API_DOMAIN}/#{endpoint}"
output = Utils::Curl.curl_output("--fail", api_url, max_time: 5)
raise ArgumentError, "No file found at #{Tty.underline}#{api_url}#{Tty.reset}" unless output.success?
cache[endpoint] = if json
JSON.parse(output.stdout)
else
output.stdout
end
rescue JSON::ParserError
raise ArgumentError, "Invalid JSON file: #{Tty.underline}#{api_url}#{Tty.reset}"
end
def fetch_json_api_file(endpoint, target:)
retry_count = 0
url = "#{API_DOMAIN}/#{endpoint}"
begin
curl_args = %W[--compressed --silent #{url}]
curl_args.prepend("--time-cond", target) if target.exist? && !target.empty?
Utils::Curl.curl_download(*curl_args, to: target, max_time: 5)
JSON.parse(target.read)
rescue JSON::ParserError
target.unlink
retry_count += 1
odie "Cannot download non-corrupt #{url}!" if retry_count > MAX_RETRIES
retry
end
end
end
end
# typed: false
# frozen_string_literal: true
module Homebrew
module API
# Helper functions for using the analytics JSON API.
#
# @api private
module Analytics
class << self
extend T::Sig
sig { returns(String) }
def analytics_api_path
"analytics"
end
alias generic_analytics_api_path analytics_api_path
sig { params(category: String, days: T.any(Integer, String)).returns(Hash) }
def fetch(category, days)
Homebrew::API.fetch "#{analytics_api_path}/#{category}/#{days}d.json"
end
end
end
end
end
# typed: false
# frozen_string_literal: true
module Homebrew
module API
# Helper functions for using the cask source API.
#
# @api private
module CaskSource
class << self
extend T::Sig
sig { params(token: String).returns(Hash) }
def fetch(token)
token = token.sub(%r{^homebrew/cask/}, "")
Homebrew::API.fetch "cask-source/#{token}.rb", json: false
end
sig { params(token: String).returns(T::Boolean) }
def available?(token)
fetch token
true
rescue ArgumentError
false
end
end
end
end
end
# typed: false
# frozen_string_literal: true
module Homebrew
module API
# Helper functions for using the cask JSON API.
#
# @api private
module Cask
class << self
extend T::Sig
sig { params(name: String).returns(Hash) }
def fetch(name)
Homebrew::API.fetch "cask/#{name}.json"
end
sig { returns(Hash) }
def all_casks
@all_casks ||= begin
json_casks = Homebrew::API.fetch_json_api_file "cask.json",
target: HOMEBREW_CACHE_API/"cask.json"
json_casks.to_h do |json_cask|
[json_cask["token"], json_cask.except("token")]
end
end
end
end
end
end
end
# typed: false
# frozen_string_literal: true
module Homebrew
module API
# Helper functions for using the formula JSON API.
#
# @api private
module Formula
class << self
extend T::Sig
sig { params(name: String).returns(Hash) }
def fetch(name)
Homebrew::API.fetch "formula/#{name}.json"
end
sig { returns(Hash) }
def all_formulae
@all_formulae ||= begin
json_formulae = Homebrew::API.fetch_json_api_file "formula.json",
target: HOMEBREW_CACHE_API/"formula.json"
@all_aliases = {}
json_formulae.to_h do |json_formula|
json_formula["aliases"].each do |alias_name|
@all_aliases[alias_name] = json_formula["name"]
end
[json_formula["name"], json_formula.except("name")]
end
end
end
sig { returns(Hash) }
def all_aliases
all_formulae if @all_aliases.blank?
@all_aliases
end
end
end
end
end
# typed: true
# frozen_string_literal: true
module Homebrew
module API
# Helper functions for using the versions JSON API.
#
# @api private
module Versions
class << self
extend T::Sig
def formulae
# The result is cached by Homebrew::API.fetch
Homebrew::API.fetch "versions-formulae.json"
end
def casks
# The result is cached by Homebrew::API.fetch
Homebrew::API.fetch "versions-casks.json"
end
sig { params(name: String).returns(T.nilable(PkgVersion)) }
def latest_formula_version(name)
versions = formulae
return unless versions.key? name
version = Version.new(versions[name]["version"])
revision = versions[name]["revision"]
PkgVersion.new(version, revision)
end
sig { params(token: String).returns(T.nilable(Version)) }
def latest_cask_version(token)
return unless casks.key? token
version = if casks[token]["versions"].key? MacOS.version.to_sym.to_s
casks[token]["versions"][MacOS.version.to_sym.to_s]
else
casks[token]["version"]
end
Version.new(version)
end
end
end
end
end
# typed: true
# frozen_string_literal: true
require "macos_versions"
FORMULA_COMPONENT_PRECEDENCE_LIST = [
[{ name: :include, type: :method_call }],
[{ name: :desc, type: :method_call }],
[{ name: :homepage, type: :method_call }],
[{ name: :url, type: :method_call }],
[{ name: :mirror, type: :method_call }],
[{ name: :version, type: :method_call }],
[{ name: :sha256, type: :method_call }],
[{ name: :license, type: :method_call }],
[{ name: :revision, type: :method_call }],
[{ name: :version_scheme, type: :method_call }],
[{ name: :head, type: :method_call }],
[{ name: :stable, type: :block_call }],
[{ name: :livecheck, type: :block_call }],
[{ name: :bottle, type: :block_call }],
[{ name: :pour_bottle?, type: :block_call }],
[{ name: :head, type: :block_call }],
[{ name: :bottle, type: :method_call }],
[{ name: :keg_only, type: :method_call }],
[{ name: :option, type: :method_call }],
[{ name: :deprecated_option, type: :method_call }],
[{ name: :disable!, type: :method_call }],
[{ name: :deprecate!, type: :method_call }],
[{ name: :depends_on, type: :method_call }],
[{ name: :uses_from_macos, type: :method_call }],
[{ name: :on_macos, type: :block_call }],
*MacOSVersions::SYMBOLS.keys.map do |os_name|
[{ name: :"on_#{os_name}", type: :block_call }]
end,
[{ name: :on_system, type: :block_call }],
[{ name: :on_linux, type: :block_call }],
[{ name: :on_arm, type: :block_call }],
[{ name: :on_intel, type: :block_call }],
[{ name: :conflicts_with, type: :method_call }],
[{ name: :skip_clean, type: :method_call }],
[{ name: :cxxstdlib_check, type: :method_call }],
[{ name: :link_overwrite, type: :method_call }],
[{ name: :fails_with, type: :method_call }, { name: :fails_with, type: :block_call }],
[{ name: :go_resource, type: :block_call }, { name: :resource, type: :block_call }],
[{ name: :patch, type: :method_call }, { name: :patch, type: :block_call }],
[{ name: :needs, type: :method_call }],
[{ name: :install, type: :method_definition }],
[{ name: :post_install, type: :method_definition }],
[{ name: :caveats, type: :method_definition }],
[{ name: :plist_options, type: :method_call }, { name: :plist, type: :method_definition }],
[{ name: :test, type: :block_call }],
].freeze
# typed: false
# frozen_string_literal: true
if ENV["HOMEBREW_STACKPROF"]
require "rubygems"
require "stackprof"
StackProf.start(mode: :wall, raw: true)
end
raise "HOMEBREW_BREW_FILE was not exported! Please call bin/brew directly!" unless ENV["HOMEBREW_BREW_FILE"]
if $PROGRAM_NAME != __FILE__ && !$PROGRAM_NAME.end_with?("/bin/ruby-prof")
raise "#{__FILE__} must not be loaded via `require`."
end
std_trap = trap("INT") { exit! 130 } # no backtrace thanks
# check ruby version before requiring any modules.
REQUIRED_RUBY_X, REQUIRED_RUBY_Y, = ENV.fetch("HOMEBREW_REQUIRED_RUBY_VERSION").split(".").map(&:to_i)
RUBY_X, RUBY_Y, = RUBY_VERSION.split(".").map(&:to_i)
if RUBY_X < REQUIRED_RUBY_X || (RUBY_X == REQUIRED_RUBY_X && RUBY_Y < REQUIRED_RUBY_Y)
raise "Homebrew must be run under Ruby #{REQUIRED_RUBY_X}.#{REQUIRED_RUBY_Y}! " \
"You're running #{RUBY_VERSION}."
end
require_relative "global"
begin
trap("INT", std_trap) # restore default CTRL-C handler
if ENV["CI"]
$stdout.sync = true
$stderr.sync = true
end
empty_argv = ARGV.empty?
help_flag_list = %w[-h --help --usage -?]
help_flag = !ENV["HOMEBREW_HELP"].nil?
help_cmd_index = nil
cmd = nil
ARGV.each_with_index do |arg, i|
break if help_flag && cmd
if arg == "help" && !cmd
# Command-style help: `help <cmd>` is fine, but `<cmd> help` is not.
help_flag = true
help_cmd_index = i
elsif !cmd && help_flag_list.exclude?(arg)
cmd = ARGV.delete_at(i)
cmd = Commands::HOMEBREW_INTERNAL_COMMAND_ALIASES.fetch(cmd, cmd)
end
end
ARGV.delete_at(help_cmd_index) if help_cmd_index
require "cli/parser"
args = Homebrew::CLI::Parser.new.parse(ARGV.dup.freeze, ignore_invalid_options: true)
Context.current = args.context
path = PATH.new(ENV.fetch("PATH"))
homebrew_path = PATH.new(ENV.fetch("HOMEBREW_PATH"))
# Add shared wrappers.
path.prepend(HOMEBREW_SHIMS_PATH/"shared")
homebrew_path.prepend(HOMEBREW_SHIMS_PATH/"shared")
ENV["PATH"] = path
require "commands"
require "settings"
internal_cmd = Commands.valid_internal_cmd?(cmd) || Commands.valid_internal_dev_cmd?(cmd) if cmd
unless internal_cmd
# Add contributed commands to PATH before checking.
homebrew_path.append(Tap.cmd_directories)
# External commands expect a normal PATH
ENV["PATH"] = homebrew_path
end
# Usage instructions should be displayed if and only if one of:
# - a help flag is passed AND a command is matched
# - a help flag is passed AND there is no command specified
# - no arguments are passed
if empty_argv || help_flag
require "help"
Homebrew::Help.help cmd, remaining_args: args.remaining, empty_argv: empty_argv
# `Homebrew::Help.help` never returns, except for unknown commands.
end
if internal_cmd || Commands.external_ruby_v2_cmd_path(cmd)
if Commands::INSTALL_FROM_API_FORBIDDEN_COMMANDS.include?(cmd) &&
Homebrew::EnvConfig.install_from_api? && !Homebrew::EnvConfig.developer?
odie "This command cannot be run while HOMEBREW_INSTALL_FROM_API is set!"
end
Homebrew.send Commands.method_name(cmd)
elsif (path = Commands.external_ruby_cmd_path(cmd))
require?(path)
exit Homebrew.failed? ? 1 : 0
elsif Commands.external_cmd_path(cmd)
%w[CACHE LIBRARY_PATH].each do |env|
ENV["HOMEBREW_#{env}"] = Object.const_get("HOMEBREW_#{env}").to_s
end
exec "brew-#{cmd}", *ARGV
else
possible_tap = OFFICIAL_CMD_TAPS.find { |_, cmds| cmds.include?(cmd) }
possible_tap = Tap.fetch(possible_tap.first) if possible_tap
if !possible_tap ||
possible_tap.installed? ||
(blocked_tap = Tap.untapped_official_taps.include?(possible_tap.name))
if blocked_tap
onoe <<~EOS
`brew #{cmd}` is unavailable because #{possible_tap.name} was manually untapped.
Run `brew tap #{possible_tap.name}` to reenable `brew #{cmd}`.
EOS
end
# Check for cask explicitly because it's very common in old guides
odie "`brew cask` is no longer a `brew` command. Use `brew <command> --cask` instead." if cmd == "cask"
odie "Unknown command: #{cmd}"
end
# Unset HOMEBREW_HELP to avoid confusing the tap
with_env HOMEBREW_HELP: nil do
tap_commands = []
if File.exist?("/.dockerenv") ||
Process.uid.zero? ||
((cgroup = Utils.popen_read("cat", "/proc/1/cgroup").presence) &&
%w[azpl_job actions_job docker garden kubepods].none? { |type| cgroup.include?(type) })
brew_uid = HOMEBREW_BREW_FILE.stat.uid
tap_commands += %W[/usr/bin/sudo -u ##{brew_uid}] if Process.uid.zero? && !brew_uid.zero?
end
quiet_arg = args.quiet? ? "--quiet" : nil
tap_commands += [HOMEBREW_BREW_FILE, "tap", *quiet_arg, possible_tap.name]
safe_system(*tap_commands)
end
ARGV << "--help" if help_flag
exec HOMEBREW_BREW_FILE, cmd, *ARGV
end
rescue UsageError => e
require "help"
Homebrew::Help.help cmd, remaining_args: args.remaining, usage_error: e.message
rescue SystemExit => e
onoe "Kernel.exit" if args.debug? && !e.success?
$stderr.puts e.backtrace if args.debug?
raise
rescue Interrupt
$stderr.puts # seemingly a newline is typical
exit 130
rescue BuildError => e
Utils::Analytics.report_build_error(e)
e.dump(verbose: args.verbose?)
if e.formula.head? || e.formula.deprecated? || e.formula.disabled?
reason = if e.formula.head?
"was built from an unstable upstream --HEAD"
elsif e.formula.deprecated?
"is deprecated"
elsif e.formula.disabled?
"is disabled"
end
$stderr.puts <<~EOS
#{e.formula.name}'s formula #{reason}.
This build failure is expected behaviour.
Do not create issues about this on Homebrew's GitHub repositories.
Any opened issues will be immediately closed without response.
Do not ask for help from MacHomebrew on Twitter.
You may ask for help in Homebrew's discussions but are unlikely to receive a response.
Try to figure out the problem yourself and submit a fix as a pull request.
We will review it but may or may not accept it.
EOS
end
exit 1
rescue RuntimeError, SystemCallError => e
raise if e.message.empty?
onoe e
$stderr.puts e.backtrace if args.debug?
exit 1
rescue MethodDeprecatedError => e
onoe e
if e.issues_url
$stderr.puts "If reporting this issue please do so at (not Homebrew/brew or Homebrew/core):"
$stderr.puts " #{Formatter.url(e.issues_url)}"
end
$stderr.puts e.backtrace if args.debug?
exit 1
rescue Exception => e # rubocop:disable Lint/RescueException
onoe e
if internal_cmd && defined?(OS::ISSUES_URL)
if Homebrew::EnvConfig.no_auto_update?
$stderr.puts "#{Tty.bold}Do not report this issue until you've run `brew update` and tried again.#{Tty.reset}"
else
$stderr.puts "#{Tty.bold}Please report this issue:#{Tty.reset}"
$stderr.puts " #{Formatter.url(OS::ISSUES_URL)}"
end
end
$stderr.puts e.backtrace
exit 1
else
exit 1 if Homebrew.failed?
ensure
if ENV["HOMEBREW_STACKPROF"]
StackProf.stop
StackProf.results("prof/stackprof.dump")
end
end
#####
##### First do the essential, fast things to be able to make e.g. brew --prefix and other commands we want to be
##### able to `source` in shell configuration quick.
#####
# Doesn't need a default case because we don't support other OSs
# shellcheck disable=SC2249
HOMEBREW_PROCESSOR="$(uname -m)"
HOMEBREW_PHYSICAL_PROCESSOR="${HOMEBREW_PROCESSOR}"
HOMEBREW_SYSTEM="$(uname -s)"
case "${HOMEBREW_SYSTEM}" in
Darwin) HOMEBREW_MACOS="1" ;;
Linux) HOMEBREW_LINUX="1" ;;
esac
# Where we store built products; a Cellar in HOMEBREW_PREFIX (often /usr/local
# for bottles) unless there's already a Cellar in HOMEBREW_REPOSITORY.
# These variables are set by bin/brew
# shellcheck disable=SC2154
if [[ -d "${HOMEBREW_REPOSITORY}/Cellar" ]]
then
HOMEBREW_CELLAR="${HOMEBREW_REPOSITORY}/Cellar"
else
HOMEBREW_CELLAR="${HOMEBREW_PREFIX}/Cellar"
fi
if [[ -n "${HOMEBREW_MACOS}" ]]
then
HOMEBREW_DEFAULT_CACHE="${HOME}/Library/Caches/Homebrew"
HOMEBREW_DEFAULT_LOGS="${HOME}/Library/Logs/Homebrew"
HOMEBREW_DEFAULT_TEMP="/private/tmp"
else
CACHE_HOME="${XDG_CACHE_HOME:-${HOME}/.cache}"
HOMEBREW_DEFAULT_CACHE="${CACHE_HOME}/Homebrew"
HOMEBREW_DEFAULT_LOGS="${CACHE_HOME}/Homebrew/Logs"
HOMEBREW_DEFAULT_TEMP="/tmp"
fi
HOMEBREW_CACHE="${HOMEBREW_CACHE:-${HOMEBREW_DEFAULT_CACHE}}"
HOMEBREW_LOGS="${HOMEBREW_LOGS:-${HOMEBREW_DEFAULT_LOGS}}"
HOMEBREW_TEMP="${HOMEBREW_TEMP:-${HOMEBREW_DEFAULT_TEMP}}"
# Don't need to handle a default case.
# HOMEBREW_LIBRARY set by bin/brew
# shellcheck disable=SC2249,SC2154
case "$*" in
--cellar)
echo "${HOMEBREW_CELLAR}"
exit 0
;;
--repository | --repo)
echo "${HOMEBREW_REPOSITORY}"
exit 0
;;
--caskroom)
echo "${HOMEBREW_PREFIX}/Caskroom"
exit 0
;;
--cache)
echo "${HOMEBREW_CACHE}"
exit 0
;;
shellenv)
source "${HOMEBREW_LIBRARY}/Homebrew/cmd/shellenv.sh"
homebrew-shellenv
exit 0
;;
formulae)
source "${HOMEBREW_LIBRARY}/Homebrew/cmd/formulae.sh"
homebrew-formulae
exit 0
;;
casks)
source "${HOMEBREW_LIBRARY}/Homebrew/cmd/casks.sh"
homebrew-casks
exit 0
;;
# falls back to cmd/prefix.rb on a non-zero return
--prefix*)
source "${HOMEBREW_LIBRARY}/Homebrew/prefix.sh"
homebrew-prefix "$@" && exit 0
;;
esac
#####
##### Next, define all helper functions.
#####
# These variables are set from the user environment.
# shellcheck disable=SC2154
ohai() {
# Check whether stdout is a tty.
if [[ -n "${HOMEBREW_COLOR}" || (-t 1 && -z "${HOMEBREW_NO_COLOR}") ]]
then
echo -e "\\033[34m==>\\033[0m \\033[1m$*\\033[0m" # blue arrow and bold text
else
echo "==> $*"
fi
}
opoo() {
# Check whether stderr is a tty.
if [[ -n "${HOMEBREW_COLOR}" || (-t 2 && -z "${HOMEBREW_NO_COLOR}") ]]
then
echo -ne "\\033[4;33mWarning\\033[0m: " >&2 # highlight Warning with underline and yellow color
else
echo -n "Warning: " >&2
fi
if [[ $# -eq 0 ]]
then
cat >&2
else
echo "$*" >&2
fi
}
bold() {
# Check whether stderr is a tty.
if [[ -n "${HOMEBREW_COLOR}" || (-t 2 && -z "${HOMEBREW_NO_COLOR}") ]]
then
echo -e "\\033[1m""$*""\\033[0m"
else
echo "$*"
fi
}
onoe() {
# Check whether stderr is a tty.
if [[ -n "${HOMEBREW_COLOR}" || (-t 2 && -z "${HOMEBREW_NO_COLOR}") ]]
then
echo -ne "\\033[4;31mError\\033[0m: " >&2 # highlight Error with underline and red color
else
echo -n "Error: " >&2
fi
if [[ $# -eq 0 ]]
then
cat >&2
else
echo "$*" >&2
fi
}
odie() {
onoe "$@"
exit 1
}
safe_cd() {
cd "$@" >/dev/null || odie "Failed to cd to $*!"
}
brew() {
# This variable is set by bin/brew
# shellcheck disable=SC2154
"${HOMEBREW_BREW_FILE}" "$@"
}
curl() {
"${HOMEBREW_LIBRARY}/Homebrew/shims/shared/curl" "$@"
}
git() {
"${HOMEBREW_LIBRARY}/Homebrew/shims/shared/git" "$@"
}
# Search given executable in PATH (remove dependency for `which` command)
which() {
# Alias to Bash built-in command `type -P`
type -P "$@"
}
numeric() {
# Condense the exploded argument into a single return value.
# shellcheck disable=SC2086,SC2183
printf "%01d%02d%02d%03d" ${1//[.rc]/ } 2>/dev/null
}
check-run-command-as-root() {
[[ "$(id -u)" == 0 ]] || return
# Allow Azure Pipelines/GitHub Actions/Docker/Concourse/Kubernetes to do everything as root (as it's normal there)
[[ -f /.dockerenv ]] && return
[[ -f /proc/1/cgroup ]] && grep -E "azpl_job|actions_job|docker|garden|kubepods" -q /proc/1/cgroup && return
# Homebrew Services may need `sudo` for system-wide daemons.
[[ "${HOMEBREW_COMMAND}" == "services" ]] && return
# It's fine to run this as root as it's not changing anything.
[[ "${HOMEBREW_COMMAND}" == "--prefix" ]] && return
odie <<EOS
Running Homebrew as root is extremely dangerous and no longer supported.
As Homebrew does not drop privileges on installation you would be giving all
build scripts full access to your system.
EOS
}
check-prefix-is-not-tmpdir() {
[[ -z "${HOMEBREW_MACOS}" ]] && return
if [[ "${HOMEBREW_PREFIX}" == "${HOMEBREW_TEMP}"* ]]
then
odie <<EOS
Your HOMEBREW_PREFIX is in the Homebrew temporary directory, which Homebrew
uses to store downloads and builds. You can resolve this by installing Homebrew to
either the standard prefix (/usr/local) or to a non-standard prefix that is not
in the Homebrew temporary directory.
EOS
fi
}
# Let user know we're still updating Homebrew if brew update --auto-update
# exceeds 3 seconds.
auto-update-timer() {
sleep 3
# Outputting a command but don't want to run it, hence single quotes.
# shellcheck disable=SC2016
echo 'Running `brew update --auto-update`...' >&2
if [[ -z "${HOMEBREW_NO_ENV_HINTS}" && -z "${HOMEBREW_AUTO_UPDATE_SECS}" ]]
then
# shellcheck disable=SC2016
echo 'Adjust how often this is run with HOMEBREW_AUTO_UPDATE_SECS or disable with' >&2
# shellcheck disable=SC2016
echo 'HOMEBREW_NO_AUTO_UPDATE. Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).' >&2
fi
}
# These variables are set from various Homebrew scripts.
# shellcheck disable=SC2154
auto-update() {
[[ -z "${HOMEBREW_HELP}" ]] || return
[[ -z "${HOMEBREW_NO_AUTO_UPDATE}" ]] || return
[[ -z "${HOMEBREW_AUTO_UPDATING}" ]] || return
[[ -z "${HOMEBREW_UPDATE_AUTO}" ]] || return
[[ -z "${HOMEBREW_AUTO_UPDATE_CHECKED}" ]] || return
# If we've checked for updates, we don't need to check again.
export HOMEBREW_AUTO_UPDATE_CHECKED="1"
if [[ "${HOMEBREW_COMMAND}" == "install" ]] ||
[[ "${HOMEBREW_COMMAND}" == "upgrade" ]] ||
[[ "${HOMEBREW_COMMAND}" == "bump-formula-pr" ]] ||
[[ "${HOMEBREW_COMMAND}" == "bump-cask-pr" ]] ||
[[ "${HOMEBREW_COMMAND}" == "bundle" ]] ||
[[ "${HOMEBREW_COMMAND}" == "release" ]] ||
[[ "${HOMEBREW_COMMAND}" == "tap" && "${HOMEBREW_ARG_COUNT}" -gt 1 ]]
then
export HOMEBREW_AUTO_UPDATING="1"
if [[ -z "${HOMEBREW_AUTO_UPDATE_SECS}" ]]
then
if [[ -z "${HOMEBREW_NO_INSTALL_FROM_API}" && -n "${HOMEBREW_INSTALL_FROM_API}" ]]
then
# 24 hours
HOMEBREW_AUTO_UPDATE_SECS="86400"
else
# 5 minutes
HOMEBREW_AUTO_UPDATE_SECS="300"
fi
fi
# Skip auto-update if the repository has been updated in the
# last $HOMEBREW_AUTO_UPDATE_SECS.
repo_fetch_head="${HOMEBREW_REPOSITORY}/.git/FETCH_HEAD"
if [[ -f "${repo_fetch_head}" ]] &&
[[ -n "$(find "${repo_fetch_head}" -type f -mtime -"${HOMEBREW_AUTO_UPDATE_SECS}"s 2>/dev/null)" ]]
then
return
fi
if [[ -z "${HOMEBREW_VERBOSE}" ]]
then
auto-update-timer &
timer_pid=$!
fi
brew update --auto-update
if [[ -n "${timer_pid}" ]]
then
kill "${timer_pid}" 2>/dev/null
wait "${timer_pid}" 2>/dev/null
fi
unset HOMEBREW_AUTO_UPDATING
# Restore user path as it'll be refiltered by HOMEBREW_BREW_FILE (bin/brew)
export PATH=${HOMEBREW_PATH}
# exec a new process to set any new environment variables.
exec "${HOMEBREW_BREW_FILE}" "$@"
fi
}
#####
##### Setup output so e.g. odie looks as nice as possible.
#####
# Colorize output on GitHub Actions.
# This is set by the user environment.
# shellcheck disable=SC2154
if [[ -n "${GITHUB_ACTIONS}" ]]
then
export HOMEBREW_COLOR="1"
fi
# Force UTF-8 to avoid encoding issues for users with broken locale settings.
if [[ -n "${HOMEBREW_MACOS}" ]]
then
if [[ "$(locale charmap)" != "UTF-8" ]]
then
export LC_ALL="en_US.UTF-8"
fi
else
if ! command -v locale >/dev/null
then
export LC_ALL=C
elif [[ "$(locale charmap)" != "UTF-8" ]]
then
locales="$(locale -a)"
c_utf_regex='\bC\.(utf8|UTF-8)\b'
en_us_regex='\ben_US\.(utf8|UTF-8)\b'
utf_regex='\b[a-z][a-z]_[A-Z][A-Z]\.(utf8|UTF-8)\b'
if [[ ${locales} =~ ${c_utf_regex} || ${locales} =~ ${en_us_regex} || ${locales} =~ ${utf_regex} ]]
then
export LC_ALL="${BASH_REMATCH[0]}"
else
export LC_ALL=C
fi
fi
fi
#####
##### odie as quickly as possible.
#####
if [[ "${HOMEBREW_PREFIX}" == "/" || "${HOMEBREW_PREFIX}" == "/usr" ]]
then
# it may work, but I only see pain this route and don't want to support it
odie "Cowardly refusing to continue at this prefix: ${HOMEBREW_PREFIX}"
fi
# Many Pathname operations use getwd when they shouldn't, and then throw
# odd exceptions. Reduce our support burden by showing a user-friendly error.
if [[ ! -d "$(pwd)" ]]
then
odie "The current working directory doesn't exist, cannot proceed."
fi
#####
##### Now, do everything else (that may be a bit slower).
#####
# Docker image deprecation
if [[ -f "${HOMEBREW_REPOSITORY}/.docker-deprecate" ]]
then
DOCKER_DEPRECATION_MESSAGE="$(cat "${HOMEBREW_REPOSITORY}/.docker-deprecate")"
if [[ -n "${GITHUB_ACTIONS}" ]]
then
echo "::warning::${DOCKER_DEPRECATION_MESSAGE}" >&2
else
opoo "${DOCKER_DEPRECATION_MESSAGE}"
fi
fi
# USER isn't always set so provide a fall back for `brew` and subprocesses.
export USER="${USER:-$(id -un)}"
# A depth of 1 means this command was directly invoked by a user.
# Higher depths mean this command was invoked by another Homebrew command.
export HOMEBREW_COMMAND_DEPTH="$((HOMEBREW_COMMAND_DEPTH + 1))"
setup_curl() {
# This is set by the user environment.
# shellcheck disable=SC2154
HOMEBREW_BREWED_CURL_PATH="${HOMEBREW_PREFIX}/opt/curl/bin/curl"
if [[ -n "${HOMEBREW_FORCE_BREWED_CURL}" && -x "${HOMEBREW_BREWED_CURL_PATH}" ]] &&
"${HOMEBREW_BREWED_CURL_PATH}" --version &>/dev/null
then
HOMEBREW_CURL="${HOMEBREW_BREWED_CURL_PATH}"
elif [[ -n "${HOMEBREW_CURL_PATH}" ]]
then
HOMEBREW_CURL="${HOMEBREW_CURL_PATH}"
else
HOMEBREW_CURL="curl"
fi
}
setup_git() {
# This is set by the user environment.
# shellcheck disable=SC2154
if [[ -n "${HOMEBREW_FORCE_BREWED_GIT}" && -x "${HOMEBREW_PREFIX}/opt/git/bin/git" ]] &&
"${HOMEBREW_PREFIX}/opt/git/bin/git" --version &>/dev/null
then
HOMEBREW_GIT="${HOMEBREW_PREFIX}/opt/git/bin/git"
elif [[ -n "${HOMEBREW_GIT_PATH}" ]]
then
HOMEBREW_GIT="${HOMEBREW_GIT_PATH}"
else
HOMEBREW_GIT="git"
fi
}
setup_curl
setup_git
HOMEBREW_VERSION="$("${HOMEBREW_GIT}" -C "${HOMEBREW_REPOSITORY}" describe --tags --dirty --abbrev=7 2>/dev/null)"
HOMEBREW_USER_AGENT_VERSION="${HOMEBREW_VERSION}"
if [[ -z "${HOMEBREW_VERSION}" ]]
then
HOMEBREW_VERSION=">=2.5.0 (shallow or no git repository)"
HOMEBREW_USER_AGENT_VERSION="2.X.Y"
fi
HOMEBREW_CORE_REPOSITORY="${HOMEBREW_LIBRARY}/Taps/homebrew/homebrew-core"
# Used in --version.sh
# shellcheck disable=SC2034
HOMEBREW_CASK_REPOSITORY="${HOMEBREW_LIBRARY}/Taps/homebrew/homebrew-cask"
case "$*" in
--version | -v)
source "${HOMEBREW_LIBRARY}/Homebrew/cmd/--version.sh"
homebrew-version
exit 0
;;
esac
if [[ -n "${HOMEBREW_MACOS}" ]]
then
HOMEBREW_PRODUCT="Homebrew"
HOMEBREW_SYSTEM="Macintosh"
[[ "${HOMEBREW_PROCESSOR}" == "x86_64" ]] && HOMEBREW_PROCESSOR="Intel"
HOMEBREW_MACOS_VERSION="$(/usr/bin/sw_vers -productVersion)"
# Don't change this from Mac OS X to match what macOS itself does in Safari on 10.12
HOMEBREW_OS_USER_AGENT_VERSION="Mac OS X ${HOMEBREW_MACOS_VERSION}"
if [[ "$(sysctl -n hw.optional.arm64 2>/dev/null)" == "1" ]]
then
# used in vendor-install.sh
# shellcheck disable=SC2034
HOMEBREW_PHYSICAL_PROCESSOR="arm64"
fi
# Intentionally set this variable by exploding another.
# shellcheck disable=SC2086,SC2183
printf -v HOMEBREW_MACOS_VERSION_NUMERIC "%02d%02d%02d" ${HOMEBREW_MACOS_VERSION//./ }
# Don't include minor versions for Big Sur and later.
if [[ "${HOMEBREW_MACOS_VERSION_NUMERIC}" -gt "110000" ]]
then
HOMEBREW_OS_VERSION="macOS ${HOMEBREW_MACOS_VERSION%.*}"
else
HOMEBREW_OS_VERSION="macOS ${HOMEBREW_MACOS_VERSION}"
fi
# Refuse to run on pre-El Capitan
if [[ "${HOMEBREW_MACOS_VERSION_NUMERIC}" -lt "101100" ]]
then
printf "ERROR: Your version of macOS (%s) is too old to run Homebrew!\\n" "${HOMEBREW_MACOS_VERSION}" >&2
if [[ "${HOMEBREW_MACOS_VERSION_NUMERIC}" -lt "100700" ]]
then
printf " For 10.4 - 10.6 support see: https://github.com/mistydemeo/tigerbrew\\n" >&2
fi
printf "\\n" >&2
fi
# Versions before Sierra don't handle custom cert files correctly, so need a full brewed curl.
if [[ "${HOMEBREW_MACOS_VERSION_NUMERIC}" -lt "101200" ]]
then
HOMEBREW_SYSTEM_CURL_TOO_OLD="1"
HOMEBREW_FORCE_BREWED_CURL="1"
fi
# The system libressl has a bug before macOS 10.15.6 where it incorrectly handles expired roots.
if [[ -z "${HOMEBREW_SYSTEM_CURL_TOO_OLD}" && "${HOMEBREW_MACOS_VERSION_NUMERIC}" -lt "101506" ]]
then
HOMEBREW_SYSTEM_CA_CERTIFICATES_TOO_OLD="1"
HOMEBREW_FORCE_BREWED_CA_CERTIFICATES="1"
fi
if [[ -n "${HOMEBREW_FAKE_EL_CAPITAN}" ]]
then
# We only need this to work enough to update brew and build the set portable formulae, so relax the requirement.
HOMEBREW_MINIMUM_GIT_VERSION="2.7.4"
else
# The system Git on macOS versions before Sierra is too old for some Homebrew functionality we rely on.
HOMEBREW_MINIMUM_GIT_VERSION="2.14.3"
if [[ "${HOMEBREW_MACOS_VERSION_NUMERIC}" -lt "101200" ]]
then
HOMEBREW_FORCE_BREWED_GIT="1"
fi
fi
# Set a variable when the macOS system Ruby is new enough to avoid spawning
# a Ruby process unnecessarily.
if [[ "${HOMEBREW_MACOS_VERSION_NUMERIC}" -lt "120000" ]]
then
unset HOMEBREW_MACOS_SYSTEM_RUBY_NEW_ENOUGH
else
# Used in ruby.sh.
# shellcheck disable=SC2034
HOMEBREW_MACOS_SYSTEM_RUBY_NEW_ENOUGH="1"
fi
else
HOMEBREW_PRODUCT="${HOMEBREW_SYSTEM}brew"
[[ -n "${HOMEBREW_LINUX}" ]] && HOMEBREW_OS_VERSION="$(lsb_release -s -d 2>/dev/null)"
: "${HOMEBREW_OS_VERSION:=$(uname -r)}"
HOMEBREW_OS_USER_AGENT_VERSION="${HOMEBREW_OS_VERSION}"
# Ensure the system Curl is a version that supports modern HTTPS certificates.
HOMEBREW_MINIMUM_CURL_VERSION="7.41.0"
curl_version_output="$(${HOMEBREW_CURL} --version 2>/dev/null)"
curl_name_and_version="${curl_version_output%% (*}"
# shellcheck disable=SC2248
if [[ "$(numeric "${curl_name_and_version##* }")" -lt "$(numeric "${HOMEBREW_MINIMUM_CURL_VERSION}")" ]]
then
message="Please update your system curl or set HOMEBREW_CURL_PATH to a newer version.
Minimum required version: ${HOMEBREW_MINIMUM_CURL_VERSION}
Your curl version: ${curl_name_and_version##* }
Your curl executable: $(type -p "${HOMEBREW_CURL}")"
if [[ -z ${HOMEBREW_CURL_PATH} ]]
then
HOMEBREW_SYSTEM_CURL_TOO_OLD=1
HOMEBREW_FORCE_BREWED_CURL=1
if [[ -z ${HOMEBREW_CURL_WARNING} ]]
then
onoe "${message}"
HOMEBREW_CURL_WARNING=1
fi
else
odie "${message}"
fi
fi
# Ensure the system Git is at or newer than the minimum required version.
# Git 2.7.4 is the version of git on Ubuntu 16.04 LTS (Xenial Xerus).
HOMEBREW_MINIMUM_GIT_VERSION="2.7.0"
git_version_output="$(${HOMEBREW_GIT} --version 2>/dev/null)"
# $extra is intentionally discarded.
# shellcheck disable=SC2034
IFS='.' read -r major minor micro build extra <<<"${git_version_output##* }"
# shellcheck disable=SC2248
if [[ "$(numeric "${major}.${minor}.${micro}.${build}")" -lt "$(numeric "${HOMEBREW_MINIMUM_GIT_VERSION}")" ]]
then
message="Please update your system Git or set HOMEBREW_GIT_PATH to a newer version.
Minimum required version: ${HOMEBREW_MINIMUM_GIT_VERSION}
Your Git version: ${major}.${minor}.${micro}.${build}
Your Git executable: $(unset git && type -p "${HOMEBREW_GIT}")"
if [[ -z ${HOMEBREW_GIT_PATH} ]]
then
HOMEBREW_FORCE_BREWED_GIT="1"
if [[ -z ${HOMEBREW_GIT_WARNING} ]]
then
onoe "${message}"
HOMEBREW_GIT_WARNING=1
fi
else
odie "${message}"
fi
fi
HOMEBREW_LINUX_MINIMUM_GLIBC_VERSION="2.13"
unset HOMEBREW_MACOS_SYSTEM_RUBY_NEW_ENOUGH
HOMEBREW_CORE_REPOSITORY_ORIGIN="$("${HOMEBREW_GIT}" -C "${HOMEBREW_CORE_REPOSITORY}" remote get-url origin 2>/dev/null)"
if [[ "${HOMEBREW_CORE_REPOSITORY_ORIGIN}" =~ (/linuxbrew|Linuxbrew/homebrew)-core(\.git)?$ ]]
then
# triggers migration code in update.sh
# shellcheck disable=SC2034
HOMEBREW_LINUXBREW_CORE_MIGRATION=1
fi
fi
setup_ca_certificates() {
if [[ -n "${HOMEBREW_FORCE_BREWED_CA_CERTIFICATES}" && -f "${HOMEBREW_PREFIX}/etc/ca-certificates/cert.pem" ]]
then
export SSL_CERT_FILE="${HOMEBREW_PREFIX}/etc/ca-certificates/cert.pem"
export GIT_SSL_CAINFO="${HOMEBREW_PREFIX}/etc/ca-certificates/cert.pem"
export GIT_SSL_CAPATH="${HOMEBREW_PREFIX}/etc/ca-certificates"
fi
}
setup_ca_certificates
# Redetermine curl and git paths as we may have forced some options above.
setup_curl
setup_git
# A bug in the auto-update process prior to 3.1.2 means $HOMEBREW_BOTTLE_DOMAIN
# could be passed down with the default domain.
# This is problematic as this is will be the old bottle domain.
# This workaround is necessary for many CI images starting on old version,
# and will only be unnecessary when updating from <3.1.2 is not a concern.
# That will be when macOS 12 is the minimum required version.
# HOMEBREW_BOTTLE_DOMAIN is set from the user environment
# shellcheck disable=SC2154
if [[ -n "${HOMEBREW_BOTTLE_DEFAULT_DOMAIN}" ]] &&
[[ "${HOMEBREW_BOTTLE_DOMAIN}" == "${HOMEBREW_BOTTLE_DEFAULT_DOMAIN}" ]]
then
unset HOMEBREW_BOTTLE_DOMAIN
fi
HOMEBREW_BOTTLE_DEFAULT_DOMAIN="https://ghcr.io/v2/homebrew/core"
HOMEBREW_USER_AGENT="${HOMEBREW_PRODUCT}/${HOMEBREW_USER_AGENT_VERSION} (${HOMEBREW_SYSTEM}; ${HOMEBREW_PROCESSOR} ${HOMEBREW_OS_USER_AGENT_VERSION})"
curl_version_output="$(curl --version 2>/dev/null)"
curl_name_and_version="${curl_version_output%% (*}"
HOMEBREW_USER_AGENT_CURL="${HOMEBREW_USER_AGENT} ${curl_name_and_version// //}"
export HOMEBREW_VERSION
export HOMEBREW_DEFAULT_CACHE
export HOMEBREW_CACHE
export HOMEBREW_DEFAULT_LOGS
export HOMEBREW_LOGS
export HOMEBREW_DEFAULT_TEMP
export HOMEBREW_TEMP
export HOMEBREW_CELLAR
export HOMEBREW_SYSTEM
export HOMEBREW_SYSTEM_CA_CERTIFICATES_TOO_OLD
export HOMEBREW_CURL
export HOMEBREW_BREWED_CURL_PATH
export HOMEBREW_CURL_WARNING
export HOMEBREW_SYSTEM_CURL_TOO_OLD
export HOMEBREW_GIT
export HOMEBREW_GIT_WARNING
export HOMEBREW_MINIMUM_GIT_VERSION
export HOMEBREW_LINUX_MINIMUM_GLIBC_VERSION
export HOMEBREW_PROCESSOR
export HOMEBREW_PRODUCT
export HOMEBREW_OS_VERSION
export HOMEBREW_MACOS_VERSION
export HOMEBREW_MACOS_VERSION_NUMERIC
export HOMEBREW_USER_AGENT
export HOMEBREW_USER_AGENT_CURL
export HOMEBREW_BOTTLE_DEFAULT_DOMAIN
export HOMEBREW_MACOS_SYSTEM_RUBY_NEW_ENOUGH
if [[ -n "${HOMEBREW_MACOS}" && -x "/usr/bin/xcode-select" ]]
then
XCODE_SELECT_PATH="$('/usr/bin/xcode-select' --print-path 2>/dev/null)"
if [[ "${XCODE_SELECT_PATH}" == "/" ]]
then
odie <<EOS
Your xcode-select path is currently set to '/'.
This causes the 'xcrun' tool to hang, and can render Homebrew unusable.
If you are using Xcode, you should:
sudo xcode-select --switch /Applications/Xcode.app
Otherwise, you should:
sudo rm -rf /usr/share/xcode-select
EOS
fi
# Don't check xcrun if Xcode and the CLT aren't installed, as that opens
# a popup window asking the user to install the CLT
if [[ -n "${XCODE_SELECT_PATH}" ]]
then
XCRUN_OUTPUT="$(/usr/bin/xcrun clang 2>&1)"
XCRUN_STATUS="$?"
if [[ "${XCRUN_STATUS}" -ne 0 && "${XCRUN_OUTPUT}" == *license* ]]
then
odie <<EOS
You have not agreed to the Xcode license. Please resolve this by running:
sudo xcodebuild -license accept
EOS
fi
fi
fi
if [[ "$1" == "-v" ]]
then
# Shift the -v to the end of the parameter list
shift
set -- "$@" -v
fi
for arg in "$@"
do
[[ "${arg}" == "--" ]] && break
if [[ "${arg}" == "--help" || "${arg}" == "-h" || "${arg}" == "--usage" || "${arg}" == "-?" ]]
then
export HOMEBREW_HELP="1"
break
fi
done
HOMEBREW_ARG_COUNT="$#"
HOMEBREW_COMMAND="$1"
shift
case "${HOMEBREW_COMMAND}" in
ls) HOMEBREW_COMMAND="list" ;;
homepage) HOMEBREW_COMMAND="home" ;;
-S) HOMEBREW_COMMAND="search" ;;
up) HOMEBREW_COMMAND="update" ;;
ln) HOMEBREW_COMMAND="link" ;;
instal) HOMEBREW_COMMAND="install" ;; # gem does the same
uninstal) HOMEBREW_COMMAND="uninstall" ;;
rm) HOMEBREW_COMMAND="uninstall" ;;
remove) HOMEBREW_COMMAND="uninstall" ;;
abv) HOMEBREW_COMMAND="info" ;;
dr) HOMEBREW_COMMAND="doctor" ;;
--repo) HOMEBREW_COMMAND="--repository" ;;
environment) HOMEBREW_COMMAND="--env" ;;
--config) HOMEBREW_COMMAND="config" ;;
-v) HOMEBREW_COMMAND="--version" ;;
esac
# Set HOMEBREW_DEV_CMD_RUN for users who have run a development command.
# This makes them behave like HOMEBREW_DEVELOPERs for brew update.
if [[ -z "${HOMEBREW_DEVELOPER}" ]]
then
export HOMEBREW_GIT_CONFIG_FILE="${HOMEBREW_REPOSITORY}/.git/config"
HOMEBREW_GIT_CONFIG_DEVELOPERMODE="$(git config --file="${HOMEBREW_GIT_CONFIG_FILE}" --get homebrew.devcmdrun 2>/dev/null)"
if [[ "${HOMEBREW_GIT_CONFIG_DEVELOPERMODE}" == "true" ]]
then
export HOMEBREW_DEV_CMD_RUN="1"
fi
# Don't allow non-developers to customise Ruby warnings.
unset HOMEBREW_RUBY_WARNINGS
fi
# Disable Ruby options we don't need.
export HOMEBREW_RUBY_DISABLE_OPTIONS="--disable=gems,rubyopt"
if [[ -z "${HOMEBREW_RUBY_WARNINGS}" ]]
then
export HOMEBREW_RUBY_WARNINGS="-W1"
fi
export HOMEBREW_BREW_DEFAULT_GIT_REMOTE="https://github.com/Homebrew/brew"
if [[ -z "${HOMEBREW_BREW_GIT_REMOTE}" ]]
then
HOMEBREW_BREW_GIT_REMOTE="${HOMEBREW_BREW_DEFAULT_GIT_REMOTE}"
fi
export HOMEBREW_BREW_GIT_REMOTE
export HOMEBREW_CORE_DEFAULT_GIT_REMOTE="https://github.com/Homebrew/homebrew-core"
if [[ -z "${HOMEBREW_CORE_GIT_REMOTE}" ]]
then
HOMEBREW_CORE_GIT_REMOTE="${HOMEBREW_CORE_DEFAULT_GIT_REMOTE}"
fi
export HOMEBREW_CORE_GIT_REMOTE
# Set HOMEBREW_DEVELOPER_COMMAND if the command being run is a developer command
if [[ -f "${HOMEBREW_LIBRARY}/Homebrew/dev-cmd/${HOMEBREW_COMMAND}.sh" ]] ||
[[ -f "${HOMEBREW_LIBRARY}/Homebrew/dev-cmd/${HOMEBREW_COMMAND}.rb" ]]
then
export HOMEBREW_DEVELOPER_COMMAND="1"
fi
if [[ -n "${HOMEBREW_DEVELOPER_COMMAND}" && -z "${HOMEBREW_DEVELOPER}" ]]
then
if [[ -z "${HOMEBREW_DEV_CMD_RUN}" ]]
then
message="$(bold "${HOMEBREW_COMMAND}") is a developer command, so
Homebrew's developer mode has been automatically turned on.
To turn developer mode off, run $(bold "brew developer off")
"
opoo "${message}"
fi
git config --file="${HOMEBREW_GIT_CONFIG_FILE}" --replace-all homebrew.devcmdrun true 2>/dev/null
export HOMEBREW_DEV_CMD_RUN="1"
fi
if [[ -f "${HOMEBREW_LIBRARY}/Homebrew/cmd/${HOMEBREW_COMMAND}.sh" ]]
then
HOMEBREW_BASH_COMMAND="${HOMEBREW_LIBRARY}/Homebrew/cmd/${HOMEBREW_COMMAND}.sh"
elif [[ -f "${HOMEBREW_LIBRARY}/Homebrew/dev-cmd/${HOMEBREW_COMMAND}.sh" ]]
then
HOMEBREW_BASH_COMMAND="${HOMEBREW_LIBRARY}/Homebrew/dev-cmd/${HOMEBREW_COMMAND}.sh"
fi
check-run-command-as-root
check-prefix-is-not-tmpdir
# shellcheck disable=SC2250
if [[ "${HOMEBREW_PREFIX}" == "/usr/local" ]] &&
[[ "${HOMEBREW_PREFIX}" != "${HOMEBREW_REPOSITORY}" ]] &&
[[ "${HOMEBREW_CELLAR}" == "${HOMEBREW_REPOSITORY}/Cellar" ]]
then
cat >&2 <<EOS
Warning: your HOMEBREW_PREFIX is set to /usr/local but HOMEBREW_CELLAR is set
to $HOMEBREW_CELLAR. Your current HOMEBREW_CELLAR location will stop
you being able to use all the binary packages (bottles) Homebrew provides. We
recommend you move your HOMEBREW_CELLAR to /usr/local/Cellar which will get you
access to all bottles."
EOS
fi
source "${HOMEBREW_LIBRARY}/Homebrew/utils/analytics.sh"
setup-analytics
# Use this configuration file instead of ~/.ssh/config when fetching git over SSH.
if [[ -n "${HOMEBREW_SSH_CONFIG_PATH}" ]]
then
export GIT_SSH_COMMAND="ssh -F${HOMEBREW_SSH_CONFIG_PATH}"
fi
if [[ -n "${HOMEBREW_DOCKER_REGISTRY_TOKEN}" ]]
then
export HOMEBREW_GITHUB_PACKAGES_AUTH="Bearer ${HOMEBREW_DOCKER_REGISTRY_TOKEN}"
elif [[ -n "${HOMEBREW_DOCKER_REGISTRY_BASIC_AUTH_TOKEN}" ]]
then
export HOMEBREW_GITHUB_PACKAGES_AUTH="Basic ${HOMEBREW_DOCKER_REGISTRY_BASIC_AUTH_TOKEN}"
else
export HOMEBREW_GITHUB_PACKAGES_AUTH="Bearer QQ=="
fi
if [[ -n "${HOMEBREW_BASH_COMMAND}" ]]
then
# source rather than executing directly to ensure the entire file is read into
# memory before it is run. This makes running a Bash script behave more like
# a Ruby script and avoids hard-to-debug issues if the Bash script is updated
# at the same time as being run.
#
# Shellcheck can't follow this dynamic `source`.
# shellcheck disable=SC1090
source "${HOMEBREW_BASH_COMMAND}"
{
auto-update "$@"
"homebrew-${HOMEBREW_COMMAND}" "$@"
exit $?
}
else
source "${HOMEBREW_LIBRARY}/Homebrew/utils/ruby.sh"
setup-ruby-path
# Unshift command back into argument list (unless argument list was empty).
[[ "${HOMEBREW_ARG_COUNT}" -gt 0 ]] && set -- "${HOMEBREW_COMMAND}" "$@"
# HOMEBREW_RUBY_PATH set by utils/ruby.sh
# shellcheck disable=SC2154
{
auto-update "$@"
exec "${HOMEBREW_RUBY_PATH}" "${HOMEBREW_RUBY_WARNINGS}" "${HOMEBREW_RUBY_DISABLE_OPTIONS}" \
"${HOMEBREW_LIBRARY}/Homebrew/brew.rb" "$@"
}
fi
# typed: false
# frozen_string_literal: true
# This script is loaded by formula_installer as a separate instance.
# Thrown exceptions are propagated back to the parent process over a pipe
raise "#{__FILE__} must not be loaded via `require`." if $PROGRAM_NAME != __FILE__
old_trap = trap("INT") { exit! 130 }
require_relative "global"
require "build_options"
require "keg"
require "extend/ENV"
require "debrew"
require "fcntl"
require "socket"
require "cmd/install"
# A formula build.
#
# @api private
class Build
attr_reader :formula, :deps, :reqs, :args
def initialize(formula, options, args:)
@formula = formula
@formula.build = BuildOptions.new(options, formula.options)
@args = args
if args.ignore_dependencies?
@deps = []
@reqs = []
else
@deps = expand_deps
@reqs = expand_reqs
end
end
def effective_build_options_for(dependent)
args = dependent.build.used_options
args |= Tab.for_formula(dependent).used_options
BuildOptions.new(args, dependent.options)
end
def expand_reqs
formula.recursive_requirements do |dependent, req|
build = effective_build_options_for(dependent)
if req.prune_from_option?(build) || req.prune_if_build_and_not_dependent?(dependent, formula) || req.test?
Requirement.prune
end
end
end
def expand_deps
formula.recursive_dependencies do |dependent, dep|
build = effective_build_options_for(dependent)
if dep.prune_from_option?(build) ||
dep.prune_if_build_and_not_dependent?(dependent, formula) ||
(dep.test? && !dep.build?)
Dependency.prune
elsif dep.build?
Dependency.keep_but_prune_recursive_deps
end
end
end
def install
formula_deps = deps.map(&:to_formula)
keg_only_deps = formula_deps.select(&:keg_only?)
run_time_deps = deps.reject(&:build?).map(&:to_formula)
formula_deps.each do |dep|
fixopt(dep) unless dep.opt_prefix.directory?
end
ENV.activate_extensions!(env: args.env)
if superenv?(args.env)
ENV.keg_only_deps = keg_only_deps
ENV.deps = formula_deps
ENV.run_time_deps = run_time_deps
ENV.setup_build_environment(
formula: formula,
cc: args.cc,
build_bottle: args.build_bottle?,
bottle_arch: args.bottle_arch,
debug_symbols: args.debug_symbols?,
)
reqs.each do |req|
req.modify_build_environment(
env: args.env, cc: args.cc, build_bottle: args.build_bottle?, bottle_arch: args.bottle_arch,
)
end
deps.each(&:modify_build_environment)
else
ENV.setup_build_environment(
formula: formula,
cc: args.cc,
build_bottle: args.build_bottle?,
bottle_arch: args.bottle_arch,
debug_symbols: args.debug_symbols?,
)
reqs.each do |req|
req.modify_build_environment(
env: args.env, cc: args.cc, build_bottle: args.build_bottle?, bottle_arch: args.bottle_arch,
)
end
deps.each(&:modify_build_environment)
keg_only_deps.each do |dep|
ENV.prepend_path "PATH", dep.opt_bin.to_s
ENV.prepend_path "PKG_CONFIG_PATH", "#{dep.opt_lib}/pkgconfig"
ENV.prepend_path "PKG_CONFIG_PATH", "#{dep.opt_share}/pkgconfig"
ENV.prepend_path "ACLOCAL_PATH", "#{dep.opt_share}/aclocal"
ENV.prepend_path "CMAKE_PREFIX_PATH", dep.opt_prefix.to_s
ENV.prepend "LDFLAGS", "-L#{dep.opt_lib}" if dep.opt_lib.directory?
ENV.prepend "CPPFLAGS", "-I#{dep.opt_include}" if dep.opt_include.directory?
end
end
new_env = {
"TMPDIR" => HOMEBREW_TEMP,
"TEMP" => HOMEBREW_TEMP,
"TMP" => HOMEBREW_TEMP,
}
with_env(new_env) do
formula.extend(Debrew::Formula) if args.debug?
formula.update_head_version
formula.brew(
fetch: false,
keep_tmp: args.keep_tmp?,
debug_symbols: args.debug_symbols?,
interactive: args.interactive?,
) do
with_env(
# For head builds, HOMEBREW_FORMULA_PREFIX should include the commit,
# which is not known until after the formula has been staged.
HOMEBREW_FORMULA_PREFIX: formula.prefix,
# https://reproducible-builds.org/docs/source-date-epoch/
SOURCE_DATE_EPOCH: formula.source_modified_time.to_i.to_s,
# Avoid make getting confused about timestamps.
# https://github.com/Homebrew/homebrew-core/pull/87470
TZ: "UTC0",
) do
formula.patch
if args.git?
system "git", "init"
system "git", "add", "-A"
end
if args.interactive?
ohai "Entering interactive mode..."
puts <<~EOS
Type `exit` to return and finalize the installation.
Install to this prefix: #{formula.prefix}
EOS
if args.git?
puts <<~EOS
This directory is now a Git repository. Make your changes and then use:
git diff | pbcopy
to copy the diff to the clipboard.
EOS
end
interactive_shell(formula)
else
formula.prefix.mkpath
formula.logs.mkpath
(formula.logs/"00.options.out").write \
"#{formula.full_name} #{formula.build.used_options.sort.join(" ")}".strip
formula.install
stdlibs = detect_stdlibs
tab = Tab.create(formula, ENV.compiler, stdlibs.first)
tab.write
# Find and link metafiles
formula.prefix.install_metafiles formula.buildpath
formula.prefix.install_metafiles formula.libexec if formula.libexec.exist?
end
end
end
end
end
def detect_stdlibs
keg = Keg.new(formula.prefix)
# The stdlib recorded in the install receipt is used during dependency
# compatibility checks, so we only care about the stdlib that libraries
# link against.
keg.detect_cxx_stdlibs(skip_executables: true)
end
def fixopt(f)
path = if f.linked_keg.directory? && f.linked_keg.symlink?
f.linked_keg.resolved_path
elsif f.prefix.directory?
f.prefix
elsif (kids = f.rack.children).size == 1 && kids.first.directory?
kids.first
else
raise
end
Keg.new(path).optlink(verbose: args.verbose?)
rescue
raise "#{f.opt_prefix} not present or broken\nPlease reinstall #{f.full_name}. Sorry :("
end
end
begin
args = Homebrew.install_args.parse
Context.current = args.context
error_pipe = UNIXSocket.open(ENV.fetch("HOMEBREW_ERROR_PIPE"), &:recv_io)
error_pipe.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
trap("INT", old_trap)
formula = args.named.to_formulae.first
options = Options.create(args.flags_only)
build = Build.new(formula, options, args: args)
build.install
rescue Exception => e # rubocop:disable Lint/RescueException
error_hash = JSON.parse e.to_json
# Special case: need to recreate BuildErrors in full
# for proper analytics reporting and error messages.
# BuildErrors are specific to build processes and not other
# children, which is why we create the necessary state here
# and not in Utils.safe_fork.
case error_hash["json_class"]
when "BuildError"
error_hash["cmd"] = e.cmd
error_hash["args"] = e.args
error_hash["env"] = e.env
when "ErrorDuringExecution"
error_hash["cmd"] = e.cmd
error_hash["status"] = if e.status.is_a?(Process::Status)
{
exitstatus: e.status.exitstatus,
termsig: e.status.termsig,
}
else
e.status
end
error_hash["output"] = e.output
end
error_pipe.puts error_hash.to_json
error_pipe.close
exit! 1
end
# typed: true
# frozen_string_literal: true
# Settings for the build environment.
#
# @api private
class BuildEnvironment
extend T::Sig
sig { params(settings: Symbol).void }
def initialize(*settings)
@settings = Set.new(settings)
end
sig { params(args: T::Enumerable[Symbol]).returns(T.self_type) }
def merge(*args)
@settings.merge(*args)
self
end
sig { params(o: Symbol).returns(T.self_type) }
def <<(o)
@settings << o
self
end
sig { returns(T::Boolean) }
def std?
@settings.include? :std
end
# DSL for specifying build environment settings.
module DSL
extend T::Sig
# Initialise @env for each class which may use this DSL (e.g. each formula subclass).
# `env` may never be called, and it needs to be initialised before the class is frozen.
def inherited(child)
super
child.instance_eval do
@env = BuildEnvironment.new
end
end
sig { params(settings: Symbol).returns(BuildEnvironment) }
def env(*settings)
@env.merge(settings)
end
end
KEYS = %w[
CC CXX LD OBJC OBJCXX
HOMEBREW_CC HOMEBREW_CXX
CFLAGS CXXFLAGS CPPFLAGS LDFLAGS SDKROOT MAKEFLAGS
CMAKE_PREFIX_PATH CMAKE_INCLUDE_PATH CMAKE_LIBRARY_PATH CMAKE_FRAMEWORK_PATH
MACOSX_DEPLOYMENT_TARGET PKG_CONFIG_PATH PKG_CONFIG_LIBDIR
HOMEBREW_DEBUG HOMEBREW_MAKE_JOBS HOMEBREW_VERBOSE
HOMEBREW_SVN HOMEBREW_GIT
HOMEBREW_SDKROOT
MAKE GIT CPP
ACLOCAL_PATH PATH CPATH
LD_LIBRARY_PATH LD_RUN_PATH LD_PRELOAD LIBRARY_PATH
].freeze
private_constant :KEYS
sig { params(env: T::Hash[String, T.nilable(T.any(String, Pathname))]).returns(T::Array[String]) }
def self.keys(env)
KEYS & env.keys
end
sig { params(env: T::Hash[String, T.nilable(T.any(String, Pathname))], f: IO).void }
def self.dump(env, f = $stdout)
keys = self.keys(env)
keys -= %w[CC CXX OBJC OBJCXX] if env["CC"] == env["HOMEBREW_CC"]
keys.each do |key|
value = env.fetch(key)
s = +"#{key}: #{value}"
case key
when "CC", "CXX", "LD"
s << " => #{Pathname.new(value).realpath}" if value.present? && File.symlink?(value)
end
s.freeze
f.puts s
end
end
end
# typed: true
# frozen_string_literal: true
# Options for a formula build.
#
# @api private
class BuildOptions
# @private
def initialize(args, options)
@args = args
@options = options
end
# True if a {Formula} is being built with a specific option.
# <pre>args << "--i-want-spam" if build.with? "spam"
#
# args << "--qt-gui" if build.with? "qt" # "--with-qt" ==> build.with? "qt"
#
# # If a formula presents a user with a choice, but the choice must be fulfilled:
# if build.with? "example2"
# args << "--with-example2"
# else
# args << "--with-example1"
# end</pre>
def with?(val)
option_names = val.respond_to?(:option_names) ? val.option_names : [val]
option_names.any? do |name|
if option_defined? "with-#{name}"
include? "with-#{name}"
elsif option_defined? "without-#{name}"
!include? "without-#{name}" # rubocop:disable Rails/NegateInclude
else
false
end
end
end
# True if a {Formula} is being built without a specific option.
# <pre>args << "--no-spam-plz" if build.without? "spam"</pre>
def without?(val)
!with?(val)
end
# True if a {Formula} is being built as a bottle (i.e. binary package).
def bottle?
include? "build-bottle"
end
# True if a {Formula} is being built with {Formula.head} instead of {Formula.stable}.
# <pre>args << "--some-new-stuff" if build.head?</pre>
# <pre># If there are multiple conditional arguments use a block instead of lines.
# if build.head?
# args << "--i-want-pizza"
# args << "--and-a-cold-beer" if build.with? "cold-beer"
# end</pre>
def head?
include? "HEAD"
end
# True if a {Formula} is being built with {Formula.stable} instead of {Formula.head}.
# This is the default.
# <pre>args << "--some-beta" if build.head?</pre>
def stable?
!head?
end
# True if the build has any arguments or options specified.
def any_args_or_options?
[email protected]? || [email protected]?
end
# @private
def used_options
@options & @args
end
# @private
def unused_options
@options - @args
end
private
def include?(name)
@args.include?("--#{name}")
end
def option_defined?(name)
@options.include? name
end
end
# typed: true
# frozen_string_literal: true
require "system_command"
module Homebrew
# Representation of a macOS bundle version, commonly found in `Info.plist` files.
#
# @api private
class BundleVersion
extend T::Sig
include Comparable
extend SystemCommand::Mixin
sig { params(info_plist_path: Pathname).returns(T.nilable(T.attached_class)) }
def self.from_info_plist(info_plist_path)
plist = system_command!("plutil", args: ["-convert", "xml1", "-o", "-", info_plist_path]).plist
from_info_plist_content(plist)
end
sig { params(plist: T::Hash[String, T.untyped]).returns(T.nilable(T.attached_class)) }
def self.from_info_plist_content(plist)
short_version = plist["CFBundleShortVersionString"].presence
version = plist["CFBundleVersion"].presence
new(short_version, version) if short_version || version
end
sig { params(package_info_path: Pathname).returns(T.nilable(T.attached_class)) }
def self.from_package_info(package_info_path)
require "rexml/document"
xml = REXML::Document.new(package_info_path.read)
bundle_version_bundle = xml.get_elements("//pkg-info//bundle-version//bundle").first
bundle_id = bundle_version_bundle["id"] if bundle_version_bundle
return if bundle_id.blank?
bundle = xml.get_elements("//pkg-info//bundle").find { |b| b["id"] == bundle_id }
return unless bundle
short_version = bundle["CFBundleShortVersionString"]
version = bundle["CFBundleVersion"]
new(short_version, version) if short_version || version
end
sig { returns(T.nilable(String)) }
attr_reader :short_version, :version
sig { params(short_version: T.nilable(String), version: T.nilable(String)).void }
def initialize(short_version, version)
@short_version = short_version.presence
@version = version.presence
return if @short_version || @version
raise ArgumentError, "`short_version` and `version` cannot both be `nil` or empty"
end
def <=>(other)
[version, short_version].map { |v| v&.yield_self(&Version.public_method(:new)) || Version::NULL } <=>
[other.version, other.short_version].map { |v| v&.yield_self(&Version.public_method(:new)) || Version::NULL }
end
def ==(other)
instance_of?(other.class) && short_version == other.short_version && version == other.version
end
alias eql? ==
# Create a nicely formatted version (on a best effort basis).
sig { returns(String) }
def nice_version
nice_parts.join(",")
end
sig { returns(T::Array[String]) }
def nice_parts
short_version = self.short_version
version = self.version
short_version = short_version&.delete_suffix("(#{version})") if version
return [T.must(short_version)] if short_version == version
if short_version && version
return [version] if version.match?(/\A\d+(\.\d+)+\Z/) && version.start_with?("#{short_version}.")
return [short_version] if short_version.match?(/\A\d+(\.\d+)+\Z/) && short_version.start_with?("#{version}.")
if short_version.match?(/\A\d+(\.\d+)*\Z/) && version.match?(/\A\d+\Z/)
return [short_version] if short_version.start_with?("#{version}.") || short_version.end_with?(".#{version}")
return [short_version, version]
end
end
[short_version, version].compact
end
private :nice_parts
end
end
# typed: true
# frozen_string_literal: true
require "json"
#
# {CacheStoreDatabase} acts as an interface to a persistent storage mechanism
# residing in the `HOMEBREW_CACHE`.
#
class CacheStoreDatabase
# Yields the cache store database.
# Closes the database after use if it has been loaded.
#
# @param [Symbol] type
# @yield [CacheStoreDatabase] self
def self.use(type)
@db_type_reference_hash ||= {}
@db_type_reference_hash[type] ||= {}
type_ref = @db_type_reference_hash[type]
type_ref[:count] ||= 0
type_ref[:count] += 1
type_ref[:db] ||= CacheStoreDatabase.new(type)
return_value = yield(type_ref[:db])
if type_ref[:count].positive?
type_ref[:count] -= 1
else
type_ref[:count] = 0
end
if type_ref[:count].zero?
type_ref[:db].write_if_dirty!
type_ref.delete(:db)
end
return_value
end
# Creates a CacheStoreDatabase.
#
# @param [Symbol] type
# @return [nil]
def initialize(type)
@type = type
@dirty = false
end
# Sets a value in the underlying database (and creates it if necessary).
def set(key, value)
dirty!
db[key] = value
end
# Gets a value from the underlying database (if it already exists).
def get(key)
return unless created?
db[key]
end
# Deletes a value from the underlying database (if it already exists).
def delete(key)
return unless created?
dirty!
db.delete(key)
end
# Closes the underlying database (if it is created and open).
def write_if_dirty!
return unless dirty?
cache_path.dirname.mkpath
cache_path.atomic_write(JSON.dump(@db))
end
# Returns `true` if the cache file has been created for the given `@type`.
#
# @return [Boolean]
def created?
cache_path.exist?
end
# Returns the modification time of the cache file (if it already exists).
#
# @return [Time]
def mtime
return unless created?
cache_path.mtime
end
# Performs a `select` on the underlying database.
#
# @return [Array]
def select(&block)
db.select(&block)
end
# Returns `true` if the cache is empty.
#
# @return [Boolean]
def empty?
db.empty?
end
# Performs a `each_key` on the underlying database.
#
# @return [Array]
def each_key(&block)
db.each_key(&block)
end
private
# Lazily loaded database in read/write mode. If this method is called, a
# database file will be created in the `HOMEBREW_CACHE` with a name
# corresponding to the `@type` instance variable.
#
# @return [Hash] db
def db
@db ||= begin
JSON.parse(cache_path.read) if created?
rescue JSON::ParserError
nil
end
@db ||= {}
end
# The path where the database resides in the `HOMEBREW_CACHE` for the given
# `@type`.
#
# @return [String]
def cache_path
HOMEBREW_CACHE/"#{@type}.json"
end
# Sets that the cache needs to be written to disk.
def dirty!
@dirty = true
end
# Returns `true` if the cache needs to be written to disk.
#
# @return [Boolean]
def dirty?
@dirty
end
end
#
# {CacheStore} provides methods to mutate and fetch data from a persistent
# storage mechanism.
#
class CacheStore
# @param [CacheStoreDatabase] database
# @return [nil]
def initialize(database)
@database = database
end
# Inserts new values or updates existing cached values to persistent storage.
#
# @abstract
def update!(*)
raise NotImplementedError
end
# Fetches cached values in persistent storage according to the type of data
# stored.
#
# @abstract
def fetch(*)
raise NotImplementedError
end
# Deletes data from the cache based on a condition defined in a concrete class.
#
# @abstract
def delete!(*)
raise NotImplementedError
end
protected
# @return [CacheStoreDatabase]
attr_reader :database
end
# typed: strict
# frozen_string_literal: true
require "cask/artifact"
require "cask/audit"
require "cask/auditor"
require "cask/cache"
require "cask/cask_loader"
require "cask/cask"
require "cask/caskroom"
require "cask/cmd"
require "cask/config"
require "cask/exceptions"
require "cask/denylist"
require "cask/download"
require "cask/dsl"
require "cask/installer"
require "cask/macos"
require "cask/metadata"
require "cask/pkg"
require "cask/quarantine"
require "cask/staged"
require "cask/url"
require "cask/utils"
# typed: strict
# frozen_string_literal: true
require "cask/artifact/app"
require "cask/artifact/artifact" # generic 'artifact' stanza
require "cask/artifact/audio_unit_plugin"
require "cask/artifact/binary"
require "cask/artifact/colorpicker"
require "cask/artifact/dictionary"
require "cask/artifact/font"
require "cask/artifact/input_method"
require "cask/artifact/installer"
require "cask/artifact/internet_plugin"
require "cask/artifact/manpage"
require "cask/artifact/vst_plugin"
require "cask/artifact/vst3_plugin"
require "cask/artifact/pkg"
require "cask/artifact/postflight_block"
require "cask/artifact/preflight_block"
require "cask/artifact/prefpane"
require "cask/artifact/qlplugin"
require "cask/artifact/mdimporter"
require "cask/artifact/screen_saver"
require "cask/artifact/service"
require "cask/artifact/stage_only"
require "cask/artifact/suite"
require "cask/artifact/uninstall"
require "cask/artifact/zap"
module Cask
# Module containing all cask artifact classes.
#
# @api private
module Artifact
end
end
# typed: false
# frozen_string_literal: true
require "active_support/core_ext/object/deep_dup"
module Cask
module Artifact
# Abstract superclass for all artifacts.
#
# @api private
class AbstractArtifact
extend T::Sig
include Comparable
extend Predicable
def self.english_name
@english_name ||= name.sub(/^.*:/, "").gsub(/(.)([A-Z])/, '\1 \2')
end
def self.english_article
@english_article ||= (english_name =~ /^[aeiou]/i) ? "an" : "a"
end
def self.dsl_key
@dsl_key ||= name.sub(/^.*:/, "").gsub(/(.)([A-Z])/, '\1_\2').downcase.to_sym
end
def self.dirmethod
@dirmethod ||= "#{dsl_key}dir".to_sym
end
def staged_path_join_executable(path)
path = Pathname(path)
path = path.expand_path if path.to_s.start_with?("~")
absolute_path = if path.absolute?
path
else
cask.staged_path.join(path)
end
FileUtils.chmod "+x", absolute_path if absolute_path.exist? && !absolute_path.executable?
if absolute_path.exist?
absolute_path
else
path
end
end
def <=>(other)
return unless other.class < AbstractArtifact
return 0 if instance_of?(other.class)
@@sort_order ||= [ # rubocop:disable Style/ClassVars
PreflightBlock,
# The `uninstall` stanza should be run first, as it may
# depend on other artifacts still being installed.
Uninstall,
Installer,
# `pkg` should be run before `binary`, so
# targets are created prior to linking.
# `pkg` should be run before `app`, since an `app` could
# contain a nested installer (e.g. `wireshark`).
Pkg,
[
App,
Suite,
Artifact,
Colorpicker,
Prefpane,
Qlplugin,
Mdimporter,
Dictionary,
Font,
Service,
InputMethod,
InternetPlugin,
AudioUnitPlugin,
VstPlugin,
Vst3Plugin,
ScreenSaver,
],
Binary,
Manpage,
PostflightBlock,
Zap,
].each_with_index.flat_map { |classes, i| Array(classes).map { |c| [c, i] } }.to_h
(@@sort_order[self.class] <=> @@sort_order[other.class]).to_i
end
# TODO: this sort of logic would make more sense in dsl.rb, or a
# constructor called from dsl.rb, so long as that isn't slow.
def self.read_script_arguments(arguments, stanza, default_arguments = {}, override_arguments = {}, key = nil)
# TODO: when stanza names are harmonized with class names,
# stanza may not be needed as an explicit argument
description = key ? "#{stanza} #{key.inspect}" : stanza.to_s
# backward-compatible string value
arguments = { executable: arguments } if arguments.is_a?(String)
# key sanity
permitted_keys = [:args, :input, :executable, :must_succeed, :sudo, :print_stdout, :print_stderr]
unknown_keys = arguments.keys - permitted_keys
unless unknown_keys.empty?
opoo "Unknown arguments to #{description} -- " \
"#{unknown_keys.inspect} (ignored). Running " \
"`brew update; brew cleanup` will likely fix it."
end
arguments.select! { |k| permitted_keys.include?(k) }
# key warnings
override_keys = override_arguments.keys
ignored_keys = arguments.keys & override_keys
unless ignored_keys.empty?
onoe "Some arguments to #{description} will be ignored -- :#{unknown_keys.inspect} (overridden)."
end
# extract executable
executable = arguments.key?(:executable) ? arguments.delete(:executable) : nil
arguments = default_arguments.merge arguments
arguments.merge! override_arguments
[executable, arguments]
end
attr_reader :cask
def initialize(cask, *dsl_args)
@cask = cask
@dsl_args = dsl_args.deep_dup
end
def config
cask.config
end
sig { returns(String) }
def to_s
"#{summarize} (#{self.class.english_name})"
end
def to_args
@dsl_args.reject(&:blank?)
end
end
end
end
# typed: true
# frozen_string_literal: true
require "cask/artifact/abstract_artifact"
module Cask
module Artifact
# Abstract superclass for block artifacts.
#
# @api private
class AbstractFlightBlock < AbstractArtifact
def self.dsl_key
super.to_s.sub(/_block$/, "").to_sym
end
def self.uninstall_dsl_key
dsl_key.to_s.prepend("uninstall_").to_sym
end
attr_reader :directives
def initialize(cask, **directives)
super(cask)
@directives = directives
end
def install_phase(**)
abstract_phase(self.class.dsl_key)
end
def uninstall_phase(**)
abstract_phase(self.class.uninstall_dsl_key)
end
def summarize
directives.keys.map(&:to_s).join(", ")
end
def to_h
require "method_source"
directives.transform_values(&:source)
end
private
def class_for_dsl_key(dsl_key)
namespace = self.class.name.to_s.sub(/::.*::.*$/, "")
self.class.const_get("#{namespace}::DSL::#{dsl_key.to_s.split("_").map(&:capitalize).join}")
end
def abstract_phase(dsl_key)
return if (block = directives[dsl_key]).nil?
class_for_dsl_key(dsl_key).new(cask).instance_eval(&block)
end
end
end
end
# typed: false
# frozen_string_literal: true
require "timeout"
require "utils/user"
require "cask/artifact/abstract_artifact"
require "cask/pkg"
require "extend/hash_validator"
using HashValidator
module Cask
module Artifact
# Abstract superclass for uninstall artifacts.
#
# @api private
class AbstractUninstall < AbstractArtifact
extend T::Sig
ORDERED_DIRECTIVES = [
:early_script,
:launchctl,
:quit,
:signal,
:login_item,
:kext,
:script,
:pkgutil,
:delete,
:trash,
:rmdir,
].freeze
def self.from_args(cask, **directives)
new(cask, directives)
end
attr_reader :directives
def initialize(cask, directives)
directives.assert_valid_keys!(*ORDERED_DIRECTIVES)
super(cask, **directives)
directives[:signal] = Array(directives[:signal]).flatten.each_slice(2).to_a
@directives = directives
return unless directives.key?(:kext)
cask.caveats do
kext
end
end
def to_h
directives.to_h
end
sig { returns(String) }
def summarize
to_h.flat_map { |key, val| Array(val).map { |v| "#{key.inspect} => #{v.inspect}" } }.join(", ")
end
private
def dispatch_uninstall_directives(**options)
ORDERED_DIRECTIVES.each do |directive_sym|
dispatch_uninstall_directive(directive_sym, **options)
end
end
def dispatch_uninstall_directive(directive_sym, **options)
return unless directives.key?(directive_sym)
args = directives[directive_sym]
send("uninstall_#{directive_sym}", *(args.is_a?(Hash) ? [args] : args), **options)
end
def stanza
self.class.dsl_key
end
# Preserve prior functionality of script which runs first. Should rarely be needed.
# :early_script should not delete files, better defer that to :script.
# If cask writers never need :early_script it may be removed in the future.
def uninstall_early_script(directives, **options)
uninstall_script(directives, directive_name: :early_script, **options)
end
# :launchctl must come before :quit/:signal for cases where app would instantly re-launch
def uninstall_launchctl(*services, command: nil, **_)
booleans = [false, true]
all_services = []
# if launchctl item contains a wildcard, find matching process(es)
services.each do |service|
all_services << service unless service.include?("*")
next unless service.include?("*")
found_services = find_launchctl_with_wildcard(service)
next if found_services.blank?
found_services.each { |found_service| all_services << found_service }
end
all_services.each do |service|
ohai "Removing launchctl service #{service}"
booleans.each do |with_sudo|
plist_status = command.run(
"/bin/launchctl",
args: ["list", service],
sudo: with_sudo, print_stderr: false
).stdout
if plist_status.start_with?("{")
command.run!("/bin/launchctl", args: ["remove", service], sudo: with_sudo)
sleep 1
end
paths = [
+"/Library/LaunchAgents/#{service}.plist",
+"/Library/LaunchDaemons/#{service}.plist",
]
paths.each { |elt| elt.prepend(Dir.home).freeze } unless with_sudo
paths = paths.map { |elt| Pathname(elt) }.select(&:exist?)
paths.each do |path|
command.run!("/bin/rm", args: ["-f", "--", path], sudo: with_sudo)
end
# undocumented and untested: pass a path to uninstall :launchctl
next unless Pathname(service).exist?
command.run!("/bin/launchctl", args: ["unload", "-w", "--", service], sudo: with_sudo)
command.run!("/bin/rm", args: ["-f", "--", service], sudo: with_sudo)
sleep 1
end
end
end
def running_processes(bundle_id)
system_command!("/bin/launchctl", args: ["list"])
.stdout.lines.drop(1)
.map { |line| line.chomp.split("\t") }
.map { |pid, state, id| [pid.to_i, state.to_i, id] }
.select do |(pid, _, id)|
pid.nonzero? && /\A(?:application\.)?#{Regexp.escape(bundle_id)}(?:\.\d+){0,2}\Z/.match?(id)
end
end
def find_launchctl_with_wildcard(search)
regex = Regexp.escape(search).gsub("\\*", ".*")
system_command!("/bin/launchctl", args: ["list"])
.stdout.lines.drop(1) # skip stdout column headers
.map do |line|
pid, _state, id = line.chomp.split(/\s+/)
id if pid.to_i.nonzero? && id.match?(regex)
end.compact
end
sig { returns(String) }
def automation_access_instructions
<<~EOS
Enable Automation access for "Terminal → System Events" in:
System Preferences → Security & Privacy → Privacy → Automation
if you haven't already.
EOS
end
# :quit/:signal must come before :kext so the kext will not be in use by a running process
def uninstall_quit(*bundle_ids, command: nil, **_)
bundle_ids.each do |bundle_id|
next unless running?(bundle_id)
unless User.current.gui?
opoo "Not logged into a GUI; skipping quitting application ID '#{bundle_id}'."
next
end
ohai "Quitting application '#{bundle_id}'..."
begin
Timeout.timeout(10) do
Kernel.loop do
next unless quit(bundle_id).success?
next if running?(bundle_id)
puts "Application '#{bundle_id}' quit successfully."
break
end
end
rescue Timeout::Error
opoo "Application '#{bundle_id}' did not quit. #{automation_access_instructions}"
end
end
end
def running?(bundle_id)
script = <<~JAVASCRIPT
'use strict';
ObjC.import('stdlib')
function run(argv) {
try {
var app = Application(argv[0])
if (app.running()) {
$.exit(0)
}
} catch (err) { }
$.exit(1)
}
JAVASCRIPT
system_command("osascript", args: ["-l", "JavaScript", "-e", script, bundle_id],
print_stderr: true).status.success?
end
def quit(bundle_id)
script = <<~JAVASCRIPT
'use strict';
ObjC.import('stdlib')
function run(argv) {
var app = Application(argv[0])
try {
app.quit()
} catch (err) {
if (app.running()) {
$.exit(1)
}
}
$.exit(0)
}
JAVASCRIPT
system_command "osascript", args: ["-l", "JavaScript", "-e", script, bundle_id],
print_stderr: false
end
private :quit
# :signal should come after :quit so it can be used as a backup when :quit fails
def uninstall_signal(*signals, command: nil, **_)
signals.each do |pair|
raise CaskInvalidError.new(cask, "Each #{stanza} :signal must consist of 2 elements.") unless pair.size == 2
signal, bundle_id = pair
ohai "Signalling '#{signal}' to application ID '#{bundle_id}'"
pids = running_processes(bundle_id).map(&:first)
next unless pids.any?
# Note that unlike :quit, signals are sent from the current user (not
# upgraded to the superuser). This is a todo item for the future, but
# there should be some additional thought/safety checks about that, as a
# misapplied "kill" by root could bring down the system. The fact that we
# learned the pid from AppleScript is already some degree of protection,
# though indirect.
odebug "Unix ids are #{pids.inspect} for processes with bundle identifier #{bundle_id}"
Process.kill(signal, *pids)
sleep 3
end
end
def uninstall_login_item(*login_items, command: nil, upgrade: false, **_)
return if upgrade
apps = cask.artifacts.select { |a| a.class.dsl_key == :app }
derived_login_items = apps.map { |a| { path: a.target } }
[*derived_login_items, *login_items].each do |item|
type, id = if item.respond_to?(:key) && item.key?(:path)
["path", item[:path]]
else
["name", item]
end
ohai "Removing login item #{id}"
result = system_command(
"osascript",
args: [
"-e",
%Q(tell application "System Events" to delete every login item whose #{type} is #{id.to_s.inspect}),
],
)
opoo "Removal of login item #{id} failed. #{automation_access_instructions}" unless result.success?
sleep 1
end
end
# :kext should be unloaded before attempting to delete the relevant file
def uninstall_kext(*kexts, command: nil, **_)
kexts.each do |kext|
ohai "Unloading kernel extension #{kext}"
is_loaded = system_command!("/usr/sbin/kextstat", args: ["-l", "-b", kext], sudo: true).stdout
if is_loaded.length > 1
system_command!("/sbin/kextunload", args: ["-b", kext], sudo: true)
sleep 1
end
system_command!("/usr/sbin/kextfind", args: ["-b", kext], sudo: true).stdout.chomp.lines.each do |kext_path|
ohai "Removing kernel extension #{kext_path}"
system_command!("/bin/rm", args: ["-rf", kext_path], sudo: true)
end
end
end
# :script must come before :pkgutil, :delete, or :trash so that the script file is not already deleted
def uninstall_script(directives, directive_name: :script, force: false, command: nil, **_)
# TODO: Create a common `Script` class to run this and Artifact::Installer.
executable, script_arguments = self.class.read_script_arguments(directives,
"uninstall",
{ must_succeed: true, sudo: false },
{ print_stdout: true },
directive_name)
ohai "Running uninstall script #{executable}"
raise CaskInvalidError.new(cask, "#{stanza} :#{directive_name} without :executable.") if executable.nil?
executable_path = staged_path_join_executable(executable)
if (executable_path.absolute? && !executable_path.exist?) ||
(!executable_path.absolute? && (which executable_path).nil?)
message = "uninstall script #{executable} does not exist"
raise CaskError, "#{message}." unless force
opoo "#{message}; skipping."
return
end
command.run(executable_path, **script_arguments)
sleep 1
end
def uninstall_pkgutil(*pkgs, command: nil, **_)
ohai "Uninstalling packages; your password may be necessary:"
pkgs.each do |regex|
::Cask::Pkg.all_matching(regex, command).each do |pkg|
puts pkg.package_id
pkg.uninstall
end
end
end
def each_resolved_path(action, paths)
return enum_for(:each_resolved_path, action, paths) unless block_given?
paths.each do |path|
resolved_path = Pathname.new(path)
resolved_path = resolved_path.expand_path if path.to_s.start_with?("~")
if resolved_path.relative? || resolved_path.split.any? { |part| part.to_s == ".." }
opoo "Skipping #{Formatter.identifier(action)} for relative path '#{path}'."
next
end
if MacOS.undeletable?(resolved_path)
opoo "Skipping #{Formatter.identifier(action)} for undeletable path '#{path}'."
next
end
begin
yield path, Pathname.glob(resolved_path)
rescue Errno::EPERM
raise if File.readable?(File.expand_path("~/Library/Application Support/com.apple.TCC"))
odie "Unable to remove some files. Please enable Full Disk Access for your terminal under " \
"System Preferences → Security & Privacy → Privacy → Full Disk Access."
end
end
end
def uninstall_delete(*paths, command: nil, **_)
return if paths.empty?
ohai "Removing files:"
each_resolved_path(:delete, paths) do |path, resolved_paths|
puts path
command.run!(
"/usr/bin/xargs",
args: ["-0", "--", "/bin/rm", "-r", "-f", "--"],
input: resolved_paths.join("\0"),
sudo: true,
)
end
end
def uninstall_trash(*paths, **options)
return if paths.empty?
resolved_paths = each_resolved_path(:trash, paths).to_a
ohai "Trashing files:", resolved_paths.map(&:first)
trash_paths(*resolved_paths.flat_map(&:last), **options)
end
def trash_paths(*paths, command: nil, **_)
return if paths.empty?
stdout, stderr, = system_command HOMEBREW_LIBRARY_PATH/"cask/utils/trash.swift",
args: paths,
print_stderr: false
trashed = stdout.split(":").sort
untrashable = stderr.split(":").sort
return trashed, untrashable if untrashable.empty?
untrashable.delete_if do |path|
Utils.gain_permissions(path, ["-R"], SystemCommand) do
system_command! HOMEBREW_LIBRARY_PATH/"cask/utils/trash.swift",
args: [path],
print_stderr: false
end
true
rescue
false
end
opoo "The following files could not be trashed, please do so manually:"
$stderr.puts untrashable
[trashed, untrashable]
end
def all_dirs?(*directories)
directories.all?(&:directory?)
end
def recursive_rmdir(*directories, command: nil, **_)
success = true
each_resolved_path(:rmdir, directories) do |_path, resolved_paths|
resolved_paths.select(&method(:all_dirs?)).each do |resolved_path|
puts resolved_path.sub(Dir.home, "~")
if (ds_store = resolved_path.join(".DS_Store")).exist?
command.run!("/bin/rm", args: ["-f", "--", ds_store], sudo: true, print_stderr: false)
end
unless recursive_rmdir(*resolved_path.children, command: command)
success = false
next
end
status = command.run("/bin/rmdir", args: ["--", resolved_path], sudo: true, print_stderr: false).success?
success &= status
end
end
success
end
def uninstall_rmdir(*args, **kwargs)
return if args.empty?
ohai "Removing directories if empty:"
recursive_rmdir(*args, **kwargs)
end
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/artifact/moved"
module Cask
module Artifact
# Artifact corresponding to the `app` stanza.
#
# @api private
class App < Moved
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/artifact/moved"
require "extend/hash_validator"
using HashValidator
module Cask
module Artifact
# Generic artifact corresponding to the `artifact` stanza.
#
# @api private
class Artifact < Moved
extend T::Sig
sig { returns(String) }
def self.english_name
"Generic Artifact"
end
sig { params(cask: Cask, args: T.untyped).returns(T.attached_class) }
def self.from_args(cask, *args)
source, options = args
raise CaskInvalidError.new(cask.token, "No source provided for #{english_name}.") if source.blank?
unless options.try(:key?, :target)
raise CaskInvalidError.new(cask.token, "#{english_name} '#{source}' requires a target.")
end
new(cask, source, **options)
end
sig { params(target: T.any(String, Pathname)).returns(Pathname) }
def resolve_target(target)
super(target, base_dir: nil)
end
sig { params(cask: Cask, source: T.any(String, Pathname), target: T.any(String, Pathname)).void }
def initialize(cask, source, target:)
super(cask, source, target: target)
end
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/artifact/moved"
module Cask
module Artifact
# Artifact corresponding to the `audio_unit_plugin` stanza.
#
# @api private
class AudioUnitPlugin < Moved
end
end
end
# typed: true
# frozen_string_literal: true
require "cask/artifact/symlinked"
module Cask
module Artifact
# Artifact corresponding to the `binary` stanza.
#
# @api private
class Binary < Symlinked
def link(command: nil, **options)
super(command: command, **options)
return if source.executable?
if source.writable?
FileUtils.chmod "+x", source
else
command.run!("/bin/chmod", args: ["+x", source], sudo: true)
end
end
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/artifact/moved"
module Cask
module Artifact
# Artifact corresponding to the `colorpicker` stanza.
#
# @api private
class Colorpicker < Moved
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/artifact/moved"
module Cask
module Artifact
# Artifact corresponding to the `dictionary` stanza.
#
# @api private
class Dictionary < Moved
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/artifact/moved"
module Cask
module Artifact
# Artifact corresponding to the `font` stanza.
#
# @api private
class Font < Moved
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/artifact/moved"
module Cask
module Artifact
# Artifact corresponding to the `input_method` stanza.
#
# @api private
class InputMethod < Moved
end
end
end
# typed: false
# frozen_string_literal: true
require "cask/artifact/abstract_artifact"
require "extend/hash_validator"
using HashValidator
module Cask
module Artifact
# Artifact corresponding to the `installer` stanza.
#
# @api private
class Installer < AbstractArtifact
VALID_KEYS = Set.new([
:manual,
:script,
]).freeze
# Extension module for manual installers.
module ManualInstaller
def install_phase(**)
puts <<~EOS
To complete the installation of Cask #{cask}, you must also
run the installer at:
#{cask.staged_path.join(path)}
EOS
end
end
# Extension module for script installers.
module ScriptInstaller
def install_phase(command: nil, **_)
ohai "Running #{self.class.dsl_key} script '#{path}'"
executable_path = staged_path_join_executable(path)
command.run!(
executable_path,
**args,
env: { "PATH" => PATH.new(
HOMEBREW_PREFIX/"bin", HOMEBREW_PREFIX/"sbin", ENV.fetch("PATH")
) },
)
end
end
def self.from_args(cask, **args)
raise CaskInvalidError.new(cask, "'installer' stanza requires an argument.") if args.empty?
if args.key?(:script) && !args[:script].respond_to?(:key?)
if args.key?(:executable)
raise CaskInvalidError.new(cask, "'installer' stanza gave arguments for both :script and :executable.")
end
args[:executable] = args[:script]
args.delete(:script)
args = { script: args }
end
unless args.keys.count == 1
raise CaskInvalidError.new(
cask,
"invalid 'installer' stanza: Only one of #{VALID_KEYS.inspect} is permitted.",
)
end
args.assert_valid_keys!(*VALID_KEYS)
new(cask, **args)
end
attr_reader :path, :args
def initialize(cask, **args)
super(cask, **args)
if args.key?(:manual)
@path = Pathname(args[:manual])
@args = []
extend(ManualInstaller)
return
end
path, @args = self.class.read_script_arguments(
args[:script], self.class.dsl_key.to_s, { must_succeed: true, sudo: false }, print_stdout: true
)
raise CaskInvalidError.new(cask, "#{self.class.dsl_key} missing executable") if path.nil?
@path = Pathname(path)
extend(ScriptInstaller)
end
def summarize
path.to_s
end
def to_h
{ path: path }.tap do |h|
h[:args] = args unless is_a?(ManualInstaller)
end
end
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/artifact/moved"
module Cask
module Artifact
# Artifact corresponding to the `internet_plugin` stanza.
#
# @api private
class InternetPlugin < Moved
end
end
end
# typed: true
# frozen_string_literal: true
require "cask/artifact/symlinked"
module Cask
module Artifact
# Artifact corresponding to the `manpage` stanza.
#
# @api private
class Manpage < Symlinked
attr_reader :section
def self.from_args(cask, source)
section = source.to_s[/\.([1-8]|n|l)(?:\.gz)?$/, 1]
raise CaskInvalidError, "'#{source}' is not a valid man page name" unless section
new(cask, source, section)
end
def initialize(cask, source, section)
@section = section
super(cask, source)
end
def resolve_target(target)
config.manpagedir.join("man#{section}", target)
end
end
end
end
# typed: true
# frozen_string_literal: true
require "cask/artifact/moved"
module Cask
module Artifact
# Artifact corresponding to the `mdimporter` stanza.
#
# @api private
class Mdimporter < Moved
extend T::Sig
sig { returns(String) }
def self.english_name
"Spotlight metadata importer"
end
def install_phase(**options)
super(**options)
reload_spotlight(**options)
end
private
def reload_spotlight(command: nil, **_)
command.run!("/usr/bin/mdimport", args: ["-r", target])
end
end
end
end
# typed: true
# frozen_string_literal: true
require "cask/artifact/relocated"
module Cask
module Artifact
# Superclass for all artifacts that are installed by moving them to the target location.
#
# @api private
class Moved < Relocated
extend T::Sig
sig { returns(String) }
def self.english_description
"#{english_name}s"
end
def install_phase(**options)
move(**options)
end
def uninstall_phase(**options)
move_back(**options)
end
def summarize_installed
if target.exist?
"#{printable_target} (#{target.abv})"
else
Formatter.error(printable_target, label: "Missing #{self.class.english_name}")
end
end
private
def move(adopt: false, force: false, verbose: false, command: nil, **options)
unless source.exist?
raise CaskError, "It seems the #{self.class.english_name} source '#{source}' is not there."
end
if Utils.path_occupied?(target)
if adopt
ohai "Adopting existing #{self.class.english_name} at '#{target}'"
same = command.run(
"/usr/bin/diff",
args: ["--recursive", "--brief", source, target],
verbose: verbose,
print_stdout: verbose,
).success?
unless same
raise CaskError,
"It seems the existing #{self.class.english_name} is different from " \
"the one being installed."
end
# Remove the source as we don't need to move it to the target location
source.rmtree
return post_move(command)
end
message = "It seems there is already #{self.class.english_article} " \
"#{self.class.english_name} at '#{target}'"
raise CaskError, "#{message}." unless force
opoo "#{message}; overwriting."
delete(target, force: force, command: command, **options)
end
ohai "Moving #{self.class.english_name} '#{source.basename}' to '#{target}'"
unless target.dirname.exist?
if target.dirname.ascend.find(&:directory?).writable?
target.dirname.mkpath
else
command.run!("/bin/mkdir", args: ["-p", target.dirname], sudo: true)
end
end
if target.dirname.writable?
FileUtils.move(source, target)
else
# default sudo user isn't necessarily able to write to Homebrew's locations
# e.g. with runas_default set in the sudoers (5) file.
command.run!("/bin/cp", args: ["-pR", source, target], sudo: true)
source.rmtree
end
post_move(command)
end
# Performs any actions necessary after the source has been moved to the target location.
def post_move(command)
FileUtils.ln_sf target, source
add_altname_metadata(target, source.basename, command: command)
end
def move_back(skip: false, force: false, command: nil, **options)
FileUtils.rm source if source.symlink? && source.dirname.join(source.readlink) == target
if Utils.path_occupied?(source)
message = "It seems there is already #{self.class.english_article} " \
"#{self.class.english_name} at '#{source}'"
raise CaskError, "#{message}." unless force
opoo "#{message}; overwriting."
delete(source, force: force, command: command, **options)
end
unless target.exist?
return if skip || force
raise CaskError, "It seems the #{self.class.english_name} source '#{target}' is not there."
end
ohai "Backing #{self.class.english_name} '#{target.basename}' up to '#{source}'"
source.dirname.mkpath
# We need to preserve extended attributes between copies.
command.run!("/bin/cp", args: ["-pR", target, source], sudo: !source.parent.writable?)
delete(target, force: force, command: command, **options)
end
def delete(target, force: false, command: nil, **_)
ohai "Removing #{self.class.english_name} '#{target}'"
raise CaskError, "Cannot remove undeletable #{self.class.english_name}." if MacOS.undeletable?(target)
return unless Utils.path_occupied?(target)
if target.parent.writable? && !force
target.rmtree
else
Utils.gain_permissions_remove(target, command: command)
end
end
end
end
end
# typed: true
# frozen_string_literal: true
require "plist"
require "utils/user"
require "cask/artifact/abstract_artifact"
require "extend/hash_validator"
using HashValidator
module Cask
module Artifact
# Artifact corresponding to the `pkg` stanza.
#
# @api private
class Pkg < AbstractArtifact
attr_reader :path, :stanza_options
def self.from_args(cask, path, **stanza_options)
stanza_options.assert_valid_keys!(:allow_untrusted, :choices)
new(cask, path, **stanza_options)
end
def initialize(cask, path, **stanza_options)
super(cask, path, **stanza_options)
@path = cask.staged_path.join(path)
@stanza_options = stanza_options
end
def summarize
path.relative_path_from(cask.staged_path).to_s
end
def install_phase(**options)
run_installer(**options)
end
private
def run_installer(command: nil, verbose: false, **_options)
ohai "Running installer for #{cask}; your password may be necessary.",
"Package installers may write to any location; options such as `--appdir` are ignored."
unless path.exist?
pkg = path.relative_path_from(cask.staged_path)
pkgs = Pathname.glob(cask.staged_path/"**"/"*.pkg").map { |path| path.relative_path_from(cask.staged_path) }
message = "Could not find PKG source file '#{pkg}'"
message += ", found #{pkgs.map { |path| "'#{path}'" }.to_sentence} instead" if pkgs.any?
message += "."
raise CaskError, message
end
args = [
"-pkg", path,
"-target", "/"
]
args << "-verboseR" if verbose
args << "-allowUntrusted" if stanza_options.fetch(:allow_untrusted, false)
with_choices_file do |choices_path|
args << "-applyChoiceChangesXML" << choices_path if choices_path
env = {
"LOGNAME" => User.current,
"USER" => User.current,
"USERNAME" => User.current,
}
command.run!("/usr/sbin/installer", sudo: true, args: args, print_stdout: true, env: env)
end
end
def with_choices_file
choices = stanza_options.fetch(:choices, {})
return yield nil if choices.empty?
Tempfile.open(["choices", ".xml"]) do |file|
file.write Plist::Emit.dump(choices)
file.close
yield file.path
ensure
file.unlink
end
end
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/artifact/abstract_flight_block"
module Cask
module Artifact
# Artifact corresponding to the `postflight` stanza.
#
# @api private
class PostflightBlock < AbstractFlightBlock
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/artifact/abstract_flight_block"
module Cask
module Artifact
# Artifact corresponding to the `preflight` stanza.
#
# @api private
class PreflightBlock < AbstractFlightBlock
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/artifact/moved"
module Cask
module Artifact
# Artifact corresponding to the `prefpane` stanza.
#
# @api private
class Prefpane < Moved
extend T::Sig
sig { returns(String) }
def self.english_name
"Preference Pane"
end
end
end
end
# typed: true
# frozen_string_literal: true
require "cask/artifact/moved"
module Cask
module Artifact
# Artifact corresponding to the `qlplugin` stanza.
#
# @api private
class Qlplugin < Moved
extend T::Sig
sig { returns(String) }
def self.english_name
"QuickLook Plugin"
end
def install_phase(**options)
super(**options)
reload_quicklook(**options)
end
def uninstall_phase(**options)
super(**options)
reload_quicklook(**options)
end
private
def reload_quicklook(command: nil, **_)
command.run!("/usr/bin/qlmanage", args: ["-r"])
end
end
end
end
# typed: true
# frozen_string_literal: true
require "cask/artifact/abstract_artifact"
require "extend/hash_validator"
using HashValidator
module Cask
module Artifact
# Superclass for all artifacts which have a source and a target location.
#
# @api private
class Relocated < AbstractArtifact
extend T::Sig
def self.from_args(cask, *args)
source_string, target_hash = args
if target_hash
raise CaskInvalidError unless target_hash.respond_to?(:keys)
target_hash.assert_valid_keys!(:target)
end
target_hash ||= {}
new(cask, source_string, **target_hash)
end
def resolve_target(target, base_dir: config.public_send(self.class.dirmethod))
target = Pathname(target)
if target.relative?
return target.expand_path if target.descend.first.to_s == "~"
return base_dir/target if base_dir
end
target
end
sig {
params(cask: Cask, source: T.nilable(T.any(String, Pathname)), target_hash: T.any(String, Pathname))
.void
}
def initialize(cask, source, **target_hash)
super(cask, source, **target_hash)
target = target_hash[:target]
@source_string = source.to_s
@target_string = target.to_s
end
def source
@source ||= begin
base_path = cask.staged_path
base_path = base_path.join(cask.url.only_path) if cask.url&.only_path.present?
base_path.join(@source_string)
end
end
def target
@target ||= resolve_target(@target_string.presence || source.basename)
end
def to_a
[@source_string].tap do |ary|
ary << { target: @target_string } unless @target_string.empty?
end
end
sig { returns(String) }
def summarize
target_string = @target_string.empty? ? "" : " -> #{@target_string}"
"#{@source_string}#{target_string}"
end
private
ALT_NAME_ATTRIBUTE = "com.apple.metadata:kMDItemAlternateNames"
# Try to make the asset searchable under the target name. Spotlight
# respects this attribute for many filetypes, but ignores it for App
# bundles. Alfred 2.2 respects it even for App bundles.
def add_altname_metadata(file, altname, command: nil)
return if altname.to_s.casecmp(file.basename.to_s).zero?
odebug "Adding #{ALT_NAME_ATTRIBUTE} metadata"
altnames = command.run("/usr/bin/xattr",
args: ["-p", ALT_NAME_ATTRIBUTE, file],
print_stderr: false).stdout.sub(/\A\((.*)\)\Z/, '\1')
odebug "Existing metadata is: #{altnames}"
altnames.concat(", ") unless altnames.empty?
altnames.concat(%Q("#{altname}"))
altnames = "(#{altnames})"
# Some packages are shipped as u=rx (e.g. Bitcoin Core)
command.run!("/bin/chmod", args: ["--", "u+rw", file, file.realpath])
command.run!("/usr/bin/xattr",
args: ["-w", ALT_NAME_ATTRIBUTE, altnames, file],
print_stderr: false)
end
def printable_target
target.to_s.sub(/^#{Dir.home}(#{File::SEPARATOR}|$)/, "~/")
end
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/artifact/moved"
module Cask
module Artifact
# Artifact corresponding to the `screen_saver` stanza.
#
# @api private
class ScreenSaver < Moved
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/artifact/moved"
module Cask
module Artifact
# Artifact corresponding to the `service` stanza.
#
# @api private
class Service < Moved
end
end
end
# typed: false
# frozen_string_literal: true
require "set"
module Cask
# Sorted set containing all cask artifacts.
#
# @api private
class ArtifactSet < ::Set
def each(&block)
return enum_for(__method__) { size } unless block
to_a.each(&block)
self
end
def to_a
super.sort
end
end
end
# typed: true
# frozen_string_literal: true
require "cask/artifact/abstract_artifact"
module Cask
module Artifact
# Artifact corresponding to the `stage_only` stanza.
#
# @api private
class StageOnly < AbstractArtifact
extend T::Sig
def self.from_args(cask, *args, **kwargs)
if args != [true] || kwargs.present?
raise CaskInvalidError.new(cask.token, "'stage_only' takes only a single argument: true")
end
new(cask, true)
end
sig { returns(T::Array[T::Boolean]) }
def to_a
[true]
end
sig { returns(String) }
def summarize
"true"
end
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/artifact/moved"
module Cask
module Artifact
# Artifact corresponding to the `suite` stanza.
#
# @api private
class Suite < Moved
extend T::Sig
sig { returns(String) }
def self.english_name
"App Suite"
end
sig { returns(Symbol) }
def self.dirmethod
:appdir
end
end
end
end
# typed: true
# frozen_string_literal: true
require "cask/artifact/relocated"
module Cask
module Artifact
# Superclass for all artifacts which are installed by symlinking them to the target location.
#
# @api private
class Symlinked < Relocated
extend T::Sig
sig { returns(String) }
def self.link_type_english_name
"Symlink"
end
sig { returns(String) }
def self.english_description
"#{english_name} #{link_type_english_name}s"
end
def install_phase(**options)
link(**options)
end
def uninstall_phase(**options)
unlink(**options)
end
def summarize_installed
if target.symlink? && target.exist? && target.readlink.exist?
"#{printable_target} -> #{target.readlink} (#{target.readlink.abv})"
else
string = if target.symlink?
"#{printable_target} -> #{target.readlink}"
else
printable_target
end
Formatter.error(string, label: "Broken Link")
end
end
private
def link(force: false, **options)
unless source.exist?
raise CaskError,
"It seems the #{self.class.link_type_english_name.downcase} " \
"source '#{source}' is not there."
end
if target.exist?
message = "It seems there is already #{self.class.english_article} " \
"#{self.class.english_name} at '#{target}'"
if force && target.symlink? && \
(target.realpath == source.realpath || target.realpath.to_s.start_with?("#{cask.caskroom_path}/"))
opoo "#{message}; overwriting."
target.delete
else
raise CaskError, "#{message}."
end
end
ohai "Linking #{self.class.english_name} '#{source.basename}' to '#{target}'"
create_filesystem_link(**options)
end
def unlink(**)
return unless target.symlink?
ohai "Unlinking #{self.class.english_name} '#{target}'"
target.delete
end
def create_filesystem_link(command: nil, **_)
target.dirname.mkpath
command.run!("/bin/ln", args: ["-h", "-f", "-s", "--", source, target])
add_altname_metadata(source, target.basename, command: command)
end
end
end
end
# typed: true
# frozen_string_literal: true
require "cask/artifact/abstract_uninstall"
module Cask
module Artifact
# Artifact corresponding to the `uninstall` stanza.
#
# @api private
class Uninstall < AbstractUninstall
def uninstall_phase(**options)
ORDERED_DIRECTIVES.reject { |directive_sym| directive_sym == :rmdir }
.each do |directive_sym|
dispatch_uninstall_directive(directive_sym, **options)
end
end
def post_uninstall_phase(**options)
dispatch_uninstall_directive(:rmdir, **options)
end
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/artifact/moved"
module Cask
module Artifact
# Artifact corresponding to the `vst3_plugin` stanza.
#
# @api private
class Vst3Plugin < Moved
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/artifact/moved"
module Cask
module Artifact
# Artifact corresponding to the `vst_plugin` stanza.
#
# @api private
class VstPlugin < Moved
end
end
end
# typed: true
# frozen_string_literal: true
require "cask/artifact/abstract_uninstall"
module Cask
module Artifact
# Artifact corresponding to the `zap` stanza.
#
# @api private
class Zap < AbstractUninstall
def zap_phase(**options)
dispatch_uninstall_directives(**options)
end
end
end
end
# typed: false
# frozen_string_literal: true
require "cask/denylist"
require "cask/download"
require "digest"
require "livecheck/livecheck"
require "utils/curl"
require "utils/git"
require "utils/shared_audits"
module Cask
# Audit a cask for various problems.
#
# @api private
class Audit
extend T::Sig
extend Predicable
attr_reader :cask, :download
attr_predicate :appcast?, :new_cask?, :strict?, :signing?, :online?, :token_conflicts?
def initialize(cask, appcast: nil, download: nil, quarantine: nil,
token_conflicts: nil, online: nil, strict: nil, signing: nil,
new_cask: nil, only: [], except: [])
# `new_cask` implies `online`, `token_conflicts`, `strict` and `signing`
online = new_cask if online.nil?
strict = new_cask if strict.nil?
signing = new_cask if signing.nil?
token_conflicts = new_cask if token_conflicts.nil?
# `online` implies `appcast` and `download`
appcast = online if appcast.nil?
download = online if download.nil?
# `signing` implies `download`
download = signing if download.nil?
@cask = cask
@appcast = appcast
@download = Download.new(cask, quarantine: quarantine) if download
@online = online
@strict = strict
@signing = signing
@new_cask = new_cask
@token_conflicts = token_conflicts
@only = only
@except = except
end
def run!
only_audits = @only
except_audits = @except
private_methods.map(&:to_s).grep(/^check_/).each do |audit_method_name|
name = audit_method_name.delete_prefix("check_")
next if !only_audits.empty? && only_audits&.exclude?(name)
next if except_audits&.include?(name)
send(audit_method_name)
end
self
rescue => e
odebug e, e.backtrace
add_error "exception while auditing #{cask}: #{e.message}"
self
end
def errors
@errors ||= []
end
def warnings
@warnings ||= []
end
sig { returns(T::Boolean) }
def errors?
errors.any?
end
sig { returns(T::Boolean) }
def warnings?
warnings.any?
end
sig { returns(T::Boolean) }
def success?
!(errors? || warnings?)
end
sig { params(message: T.nilable(String), location: T.nilable(String)).void }
def add_error(message, location: nil)
errors << ({ message: message, location: location })
end
sig { params(message: T.nilable(String), location: T.nilable(String)).void }
def add_warning(message, location: nil)
if strict?
add_error message, location: location
else
warnings << ({ message: message, location: location })
end
end
def result
if errors?
Formatter.error("failed")
elsif warnings?
Formatter.warning("warning")
else
Formatter.success("passed")
end
end
sig { params(include_passed: T::Boolean, include_warnings: T::Boolean).returns(String) }
def summary(include_passed: false, include_warnings: true)
return if success? && !include_passed
return if warnings? && !errors? && !include_warnings
summary = ["audit for #{cask}: #{result}"]
errors.each do |error|
summary << " #{Formatter.error("-")} #{error[:message]}"
end
if include_warnings
warnings.each do |warning|
summary << " #{Formatter.warning("-")} #{warning[:message]}"
end
end
summary.join("\n")
end
private
sig { void }
def check_untrusted_pkg
odebug "Auditing pkg stanza: allow_untrusted"
return if @cask.sourcefile_path.nil?
tap = @cask.tap
return if tap.nil?
return if tap.user != "Homebrew"
return if cask.artifacts.none? { |k| k.is_a?(Artifact::Pkg) && k.stanza_options.key?(:allow_untrusted) }
add_error "allow_untrusted is not permitted in official Homebrew Cask taps"
end
sig { void }
def check_stanza_requires_uninstall
odebug "Auditing stanzas which require an uninstall"
return if cask.artifacts.none? { |k| k.is_a?(Artifact::Pkg) || k.is_a?(Artifact::Installer) }
return if cask.artifacts.any?(Artifact::Uninstall)
add_error "installer and pkg stanzas require an uninstall stanza"
end
sig { void }
def check_single_pre_postflight
odebug "Auditing preflight and postflight stanzas"
if cask.artifacts.count { |k| k.is_a?(Artifact::PreflightBlock) && k.directives.key?(:preflight) } > 1
add_error "only a single preflight stanza is allowed"
end
count = cask.artifacts.count do |k|
k.is_a?(Artifact::PostflightBlock) &&
k.directives.key?(:postflight)
end
return unless count > 1
add_error "only a single postflight stanza is allowed"
end
sig { void }
def check_single_uninstall_zap
odebug "Auditing single uninstall_* and zap stanzas"
if cask.artifacts.count { |k| k.is_a?(Artifact::Uninstall) } > 1
add_error "only a single uninstall stanza is allowed"
end
count = cask.artifacts.count do |k|
k.is_a?(Artifact::PreflightBlock) &&
k.directives.key?(:uninstall_preflight)
end
add_error "only a single uninstall_preflight stanza is allowed" if count > 1
count = cask.artifacts.count do |k|
k.is_a?(Artifact::PostflightBlock) &&
k.directives.key?(:uninstall_postflight)
end
add_error "only a single uninstall_postflight stanza is allowed" if count > 1
return unless cask.artifacts.count { |k| k.is_a?(Artifact::Zap) } > 1
add_error "only a single zap stanza is allowed"
end
sig { void }
def check_required_stanzas
odebug "Auditing required stanzas"
[:version, :sha256, :url, :homepage].each do |sym|
add_error "a #{sym} stanza is required" unless cask.send(sym)
end
add_error "at least one name stanza is required" if cask.name.empty?
# TODO: specific DSL knowledge should not be spread around in various files like this
rejected_artifacts = [:uninstall, :zap]
installable_artifacts = cask.artifacts.reject { |k| rejected_artifacts.include?(k) }
add_error "at least one activatable artifact stanza is required" if installable_artifacts.empty?
end
sig { void }
def check_description_present
# Fonts seldom benefit from descriptions and requiring them disproportionately increases the maintenance burden
return if cask.tap == "homebrew/cask-fonts"
add_warning "Cask should have a description. Please add a `desc` stanza." if cask.desc.blank?
end
sig { void }
def check_no_string_version_latest
return unless cask.version
odebug "Auditing version :latest does not appear as a string ('latest')"
return unless cask.version.raw_version == "latest"
add_error "you should use version :latest instead of version 'latest'"
end
sig { void }
def check_sha256_no_check_if_latest
return unless cask.sha256
odebug "Auditing sha256 :no_check with version :latest"
return unless cask.version.latest?
return if cask.sha256 == :no_check
add_error "you should use sha256 :no_check when version is :latest"
end
sig { void }
def check_sha256_no_check_if_unversioned
return unless cask.sha256
return if cask.sha256 == :no_check
add_error "Use `sha256 :no_check` when URL is unversioned." if cask.url&.unversioned?
end
sig { void }
def check_sha256_actually_256
return unless cask.sha256
odebug "Auditing sha256 string is a legal SHA-256 digest"
return unless cask.sha256.is_a?(Checksum)
return if cask.sha256.length == 64 && cask.sha256[/^[0-9a-f]+$/i]
add_error "sha256 string must be of 64 hexadecimal characters"
end
sig { void }
def check_sha256_invalid
return unless cask.sha256
odebug "Auditing sha256 is not a known invalid value"
empty_sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
return unless cask.sha256 == empty_sha256
add_error "cannot use the sha256 for an empty string: #{empty_sha256}"
end
sig { void }
def check_appcast_and_livecheck
return unless cask.appcast
if cask.livecheckable?
add_error "Cask has a `livecheck`, the `appcast` should be removed."
elsif new_cask?
add_error "New casks should use a `livecheck` instead of an `appcast`."
end
end
sig { void }
def check_latest_with_appcast_or_livecheck
return unless cask.version.latest?
add_error "Casks with an `appcast` should not use `version :latest`." if cask.appcast
add_error "Casks with a `livecheck` should not use `version :latest`." if cask.livecheckable?
end
sig { void }
def check_latest_with_auto_updates
return unless cask.version.latest?
return unless cask.auto_updates
add_error "Casks with `version :latest` should not use `auto_updates`."
end
LIVECHECK_REFERENCE_URL = "https://docs.brew.sh/Cask-Cookbook#stanza-livecheck"
sig { params(livecheck_result: T::Boolean).void }
def check_hosting_with_livecheck(livecheck_result: check_livecheck_version)
return if cask.discontinued? || cask.version.latest?
return if block_url_offline? || cask.appcast || cask.livecheckable?
return if livecheck_result == :auto_detected
add_livecheck = "please add a livecheck. See #{Formatter.url(LIVECHECK_REFERENCE_URL)}"
case cask.url.to_s
when %r{sourceforge.net/(\S+)}
return unless online?
add_error "Download is hosted on SourceForge, #{add_livecheck}"
when %r{dl.devmate.com/(\S+)}
add_error "Download is hosted on DevMate, #{add_livecheck}"
when %r{rink.hockeyapp.net/(\S+)}
add_error "Download is hosted on HockeyApp, #{add_livecheck}"
end
end
SOURCEFORGE_OSDN_REFERENCE_URL = "https://docs.brew.sh/Cask-Cookbook#sourceforgeosdn-urls"
sig { void }
def check_download_url_format
return unless cask.url
odebug "Auditing URL format"
if bad_sourceforge_url?
add_error "SourceForge URL format incorrect. See #{Formatter.url(SOURCEFORGE_OSDN_REFERENCE_URL)}"
elsif bad_osdn_url?
add_error "OSDN URL format incorrect. See #{Formatter.url(SOURCEFORGE_OSDN_REFERENCE_URL)}"
end
end
VERIFIED_URL_REFERENCE_URL = "https://docs.brew.sh/Cask-Cookbook#when-url-and-homepage-domains-differ-add-verified"
sig { void }
def check_unnecessary_verified
return if block_url_offline?
return unless verified_present?
return unless url_match_homepage?
return unless verified_matches_url?
add_error "The URL's domain #{Formatter.url(domain)} matches the homepage domain " \
"#{Formatter.url(homepage)}, the 'verified' parameter of the 'url' stanza is unnecessary. " \
"See #{Formatter.url(VERIFIED_URL_REFERENCE_URL)}"
end
sig { void }
def check_missing_verified
return if block_url_offline?
return if file_url?
return if url_match_homepage?
return if verified_present?
add_error "The URL's domain #{Formatter.url(domain)} does not match the homepage domain " \
"#{Formatter.url(homepage)}, a 'verified' parameter has to be added to the 'url' stanza. " \
"See #{Formatter.url(VERIFIED_URL_REFERENCE_URL)}"
end
sig { void }
def check_no_match
return if block_url_offline?
return unless verified_present?
return if verified_matches_url?
add_error "Verified URL #{Formatter.url(url_from_verified)} does not match URL " \
"#{Formatter.url(strip_url_scheme(cask.url.to_s))}. " \
"See #{Formatter.url(VERIFIED_URL_REFERENCE_URL)}"
end
sig { void }
def check_generic_artifacts
cask.artifacts.select { |a| a.is_a?(Artifact::Artifact) }.each do |artifact|
unless artifact.target.absolute?
add_error "target must be absolute path for #{artifact.class.english_name} #{artifact.source}"
end
end
end
sig { void }
def check_languages
@cask.languages.each do |language|
Locale.parse(language)
rescue Locale::ParserError
add_error "Locale '#{language}' is invalid."
end
end
sig { void }
def check_token_conflicts
return unless token_conflicts?
return unless core_formula_names.include?(cask.token)
add_warning "possible duplicate, cask token conflicts with Homebrew core formula: " \
"#{Formatter.url(core_formula_url)}"
end
sig { void }
def check_token_valid
add_error "cask token contains non-ascii characters" unless cask.token.ascii_only?
add_error "cask token + should be replaced by -plus-" if cask.token.include? "+"
add_error "cask token whitespace should be replaced by hyphens" if cask.token.include? " "
add_error "cask token @ should be replaced by -at-" if cask.token.include? "@"
add_error "cask token underscores should be replaced by hyphens" if cask.token.include? "_"
add_error "cask token should not contain double hyphens" if cask.token.include? "--"
if cask.token.match?(/[^a-z0-9-]/)
add_error "cask token should only contain lowercase alphanumeric characters and hyphens"
end
return if !cask.token.start_with?("-") && !cask.token.end_with?("-")
add_error "cask token should not have leading or trailing hyphens"
end
sig { void }
def check_token_bad_words
return unless new_cask?
token = cask.token
add_error "cask token contains .app" if token.end_with? ".app"
if /-(?<designation>alpha|beta|rc|release-candidate)$/ =~ cask.token &&
cask.tap&.official? &&
cask.tap != "homebrew/cask-versions"
add_error "cask token contains version designation '#{designation}'"
end
add_warning "cask token mentions launcher" if token.end_with? "launcher"
add_warning "cask token mentions desktop" if token.end_with? "desktop"
add_warning "cask token mentions platform" if token.end_with? "mac", "osx", "macos"
add_warning "cask token mentions architecture" if token.end_with? "x86", "32_bit", "x86_64", "64_bit"
frameworks = %w[cocoa qt gtk wx java]
return if frameworks.include?(token) || !token.end_with?(*frameworks)
add_warning "cask token mentions framework"
end
sig { void }
def check_download
return if download.blank? || cask.url.blank?
odebug "Auditing download"
download.fetch
rescue => e
add_error "download not possible: #{e}"
end
sig { void }
def check_signing
return if !signing? || download.blank? || cask.url.blank?
odebug "Auditing signing"
artifacts = cask.artifacts.select { |k| k.is_a?(Artifact::Pkg) || k.is_a?(Artifact::App) }
return if artifacts.empty?
downloaded_path = download.fetch
primary_container = UnpackStrategy.detect(downloaded_path, type: @cask.container&.type, merge_xattrs: true)
return if primary_container.nil?
Dir.mktmpdir do |tmpdir|
tmpdir = Pathname(tmpdir)
primary_container.extract_nestedly(to: tmpdir, basename: downloaded_path.basename, verbose: false)
artifacts.each do |artifact|
path = case artifact
when Artifact::Moved
tmpdir/artifact.source.basename
when Artifact::Pkg
artifact.path
end
next unless path.exist?
result = system_command("codesign", args: ["--verify", path], print_stderr: false)
next if result.success?
message = "Signature verification failed:\n#{result.merged_output}\nmacOS on ARM requires applications " \
"to be signed. Please contact the upstream developer to let them know they should "
message += if result.stderr.include?("not signed at all")
"sign their app."
else
"fix the signature of their app."
end
add_warning message
end
end
end
sig { returns(T.nilable(T.any(T::Boolean, Symbol))) }
def check_livecheck_version
return unless appcast?
referenced_cask, = Homebrew::Livecheck.resolve_livecheck_reference(cask)
# Respect skip conditions for a referenced cask
if referenced_cask
skip_info = Homebrew::Livecheck::SkipConditions.referenced_skip_information(
referenced_cask,
Homebrew::Livecheck.cask_name(cask),
)
end
# Respect cask skip conditions (e.g. discontinued, latest, unversioned)
skip_info ||= Homebrew::Livecheck::SkipConditions.skip_information(cask)
return :skip if skip_info.present?
latest_version = Homebrew::Livecheck.latest_version(
cask,
referenced_formula_or_cask: referenced_cask,
)&.fetch(:latest)
if cask.version.to_s == latest_version.to_s
if cask.appcast
add_error "Version '#{latest_version}' was automatically detected by livecheck; " \
"the appcast should be removed."
end
return :auto_detected
end
return :appcast if cask.appcast && !cask.livecheckable?
add_error "Version '#{cask.version}' differs from '#{latest_version}' retrieved by livecheck."
false
end
def check_livecheck_min_os
return unless online?
return unless cask.livecheckable?
return unless cask.livecheck.strategy == :sparkle
out, _, status = curl_output("--fail", "--silent", "--location", cask.livecheck.url)
return unless status.success?
require "rexml/document"
xml = begin
REXML::Document.new(out)
rescue REXML::ParseException
nil
end
return if xml.blank?
item = xml.elements["//rss//channel//item"]
return if item.blank?
min_os = item.elements["sparkle:minimumSystemVersion"]&.text
min_os = "11" if min_os == "10.16"
return if min_os.blank?
begin
min_os_string = OS::Mac::Version.new(min_os).strip_patch
rescue MacOSVersionError
return
end
return if min_os_string <= MacOS::Version::OLDEST_ALLOWED
cask_min_os = cask.depends_on.macos&.version
return if cask_min_os == min_os_string
min_os_symbol = if cask_min_os.present?
cask_min_os.to_sym.inspect
else
"no minimum OS version"
end
add_error "Upstream defined #{min_os_string.to_sym.inspect} as the minimum OS version " \
"and the cask defined #{min_os_symbol}"
end
sig { void }
def check_appcast_contains_version
return unless appcast?
return if cask.appcast.to_s.empty?
return if cask.appcast.must_contain == :no_check
appcast_url = cask.appcast.to_s
begin
details = curl_http_content_headers_and_checksum(appcast_url, user_agent: HOMEBREW_USER_AGENT_FAKE_SAFARI)
appcast_contents = details[:file]
rescue
add_error "appcast at URL '#{Formatter.url(appcast_url)}' offline or looping"
return
end
version_stanza = cask.version.to_s
adjusted_version_stanza = cask.appcast.must_contain.presence || version_stanza.match(/^[[:alnum:].]+/)[0]
return if appcast_contents.blank?
return if appcast_contents.include?(adjusted_version_stanza)
add_error <<~EOS.chomp
appcast at URL '#{Formatter.url(appcast_url)}' does not contain \
the version number '#{adjusted_version_stanza}':
#{appcast_contents}
EOS
end
sig { void }
def check_github_prerelease_version
return if cask.tap == "homebrew/cask-versions"
odebug "Auditing GitHub prerelease"
user, repo = get_repo_data(%r{https?://github\.com/([^/]+)/([^/]+)/?.*}) if online?
return if user.nil?
tag = SharedAudits.github_tag_from_url(cask.url)
tag ||= cask.version
error = SharedAudits.github_release(user, repo, tag, cask: cask)
add_error error if error
end
sig { void }
def check_gitlab_prerelease_version
return if cask.tap == "homebrew/cask-versions"
user, repo = get_repo_data(%r{https?://gitlab\.com/([^/]+)/([^/]+)/?.*}) if online?
return if user.nil?
odebug "Auditing GitLab prerelease"
tag = SharedAudits.gitlab_tag_from_url(cask.url)
tag ||= cask.version
error = SharedAudits.gitlab_release(user, repo, tag, cask: cask)
add_error error if error
end
sig { void }
def check_github_repository_archived
user, repo = get_repo_data(%r{https?://github\.com/([^/]+)/([^/]+)/?.*}) if online?
return if user.nil?
odebug "Auditing GitHub repo archived"
metadata = SharedAudits.github_repo_data(user, repo)
return if metadata.nil?
return unless metadata["archived"]
message = "GitHub repo is archived"
if cask.discontinued?
add_warning message
else
add_error message
end
end
sig { void }
def check_gitlab_repository_archived
user, repo = get_repo_data(%r{https?://gitlab\.com/([^/]+)/([^/]+)/?.*}) if online?
return if user.nil?
odebug "Auditing GitLab repo archived"
metadata = SharedAudits.gitlab_repo_data(user, repo)
return if metadata.nil?
return unless metadata["archived"]
message = "GitLab repo is archived"
if cask.discontinued?
add_warning message
else
add_error message
end
end
sig { void }
def check_github_repository
return unless new_cask?
user, repo = get_repo_data(%r{https?://github\.com/([^/]+)/([^/]+)/?.*})
return if user.nil?
odebug "Auditing GitHub repo"
error = SharedAudits.github(user, repo)
add_error error if error
end
sig { void }
def check_gitlab_repository
return unless new_cask?
user, repo = get_repo_data(%r{https?://gitlab\.com/([^/]+)/([^/]+)/?.*})
return if user.nil?
odebug "Auditing GitLab repo"
error = SharedAudits.gitlab(user, repo)
add_error error if error
end
sig { void }
def check_bitbucket_repository
return unless new_cask?
user, repo = get_repo_data(%r{https?://bitbucket\.org/([^/]+)/([^/]+)/?.*})
return if user.nil?
odebug "Auditing Bitbucket repo"
error = SharedAudits.bitbucket(user, repo)
add_error error if error
end
sig { void }
def check_denylist
return unless cask.tap
return unless cask.tap.official?
return unless (reason = Denylist.reason(cask.token))
add_error "#{cask.token} is not allowed: #{reason}"
end
sig { void }
def check_reverse_migration
return unless new_cask?
return unless cask.tap
return unless cask.tap.official?
return unless cask.tap.tap_migrations.key?(cask.token)
add_error "#{cask.token} is listed in tap_migrations.json"
end
sig { void }
def check_https_availability
return unless download
if cask.url && !cask.url.using
validate_url_for_https_availability(cask.url, "binary URL", cask.token, cask.tap,
user_agents: [cask.url.user_agent])
end
if cask.appcast && appcast?
validate_url_for_https_availability(cask.appcast, "appcast URL", cask.token, cask.tap, check_content: true)
end
return unless cask.homepage
validate_url_for_https_availability(cask.homepage, SharedAudits::URL_TYPE_HOMEPAGE, cask.token, cask.tap,
user_agents: [:browser, :default],
check_content: true,
strict: strict?)
end
# sig {
# params(url_to_check: T.any(String, URL), url_type: String, cask_token: String, tap: Tap,
# options: T.untyped).void
# }
def validate_url_for_https_availability(url_to_check, url_type, cask_token, tap, **options)
problem = curl_check_http_content(url_to_check.to_s, url_type, **options)
exception = tap&.audit_exception(:secure_connection_audit_skiplist, cask_token, url_to_check.to_s)
if problem
add_error problem unless exception
elsif exception
add_error "#{url_to_check} is in the secure connection audit skiplist but does not need to be skipped"
end
end
sig { params(regex: T.any(String, Regexp)).returns(T.nilable(T::Array[String])) }
def get_repo_data(regex)
return unless online?
_, user, repo = *regex.match(cask.url.to_s)
_, user, repo = *regex.match(cask.homepage) unless user
_, user, repo = *regex.match(cask.appcast.to_s) unless user
return if !user || !repo
repo.gsub!(/.git$/, "")
[user, repo]
end
sig {
params(regex: T.any(String, Regexp), valid_formats_array: T::Array[T.any(String, Regexp)]).returns(T::Boolean)
}
def bad_url_format?(regex, valid_formats_array)
return false unless cask.url.to_s.match?(regex)
valid_formats_array.none? { |format| cask.url.to_s =~ format }
end
sig { returns(T::Boolean) }
def bad_sourceforge_url?
bad_url_format?(/sourceforge/,
[
%r{\Ahttps://sourceforge\.net/projects/[^/]+/files/latest/download\Z},
%r{\Ahttps://downloads\.sourceforge\.net/(?!(project|sourceforge)/)},
])
end
sig { returns(T::Boolean) }
def bad_osdn_url?
bad_url_format?(/osd/, [%r{\Ahttps?://([^/]+.)?dl\.osdn\.jp/}])
end
# sig { returns(String) }
def homepage
URI(cask.homepage.to_s).host
end
# sig { returns(String) }
def domain
URI(cask.url.to_s).host
end
sig { returns(T::Boolean) }
def url_match_homepage?
host = cask.url.to_s
host_uri = URI(host)
host = if host.match?(/:\d/) && host_uri.port != 80
"#{host_uri.host}:#{host_uri.port}"
else
host_uri.host
end
return false if homepage.blank?
home = homepage.downcase
if (split_host = host.split(".")).length >= 3
host = split_host[-2..].join(".")
end
if (split_home = homepage.split(".")).length >= 3
home = split_home[-2..].join(".")
end
host == home
end
# sig { params(url: String).returns(String) }
def strip_url_scheme(url)
url.sub(%r{^[^:/]+://(www\.)?}, "")
end
# sig { returns(String) }
def url_from_verified
strip_url_scheme(cask.url.verified)
end
sig { returns(T::Boolean) }
def verified_matches_url?
url_domain, url_path = strip_url_scheme(cask.url.to_s).split("/", 2)
verified_domain, verified_path = url_from_verified.split("/", 2)
(url_domain == verified_domain || (verified_domain && url_domain&.end_with?(".#{verified_domain}"))) &&
(!verified_path || url_path&.start_with?(verified_path))
end
sig { returns(T::Boolean) }
def verified_present?
cask.url.verified.present?
end
sig { returns(T::Boolean) }
def file_url?
URI(cask.url.to_s).scheme == "file"
end
sig { returns(T::Boolean) }
def block_url_offline?
return false if online?
cask.url.from_block?
end
sig { returns(Tap) }
def core_tap
@core_tap ||= CoreTap.instance
end
# sig { returns(T::Array[String]) }
def core_formula_names
core_tap.formula_names
end
sig { returns(String) }
def core_formula_url
"#{core_tap.default_remote}/blob/HEAD/Formula/#{cask.token}.rb"
end
end
end
# typed: true
# frozen_string_literal: true
require "cask/audit"
module Cask
# Helper class for auditing all available languages of a cask.
#
# @api private
class Auditor
def self.audit(
cask,
audit_download: nil,
audit_appcast: nil,
audit_online: nil,
audit_new_cask: nil,
audit_strict: nil,
audit_signing: nil,
audit_token_conflicts: nil,
quarantine: nil,
any_named_args: nil,
language: nil,
display_passes: nil,
display_failures_only: nil
)
new(
cask,
audit_download: audit_download,
audit_appcast: audit_appcast,
audit_online: audit_online,
audit_new_cask: audit_new_cask,
audit_strict: audit_strict,
audit_signing: audit_signing,
audit_token_conflicts: audit_token_conflicts,
quarantine: quarantine,
any_named_args: any_named_args,
language: language,
display_passes: display_passes,
display_failures_only: display_failures_only,
).audit
end
attr_reader :cask, :language
def initialize(
cask,
audit_download: nil,
audit_appcast: nil,
audit_online: nil,
audit_strict: nil,
audit_signing: nil,
audit_token_conflicts: nil,
audit_new_cask: nil,
quarantine: nil,
any_named_args: nil,
language: nil,
display_passes: nil,
display_failures_only: nil
)
@cask = cask
@audit_download = audit_download
@audit_appcast = audit_appcast
@audit_online = audit_online
@audit_new_cask = audit_new_cask
@audit_strict = audit_strict
@audit_signing = audit_signing
@quarantine = quarantine
@audit_token_conflicts = audit_token_conflicts
@any_named_args = any_named_args
@language = language
@display_passes = display_passes
@display_failures_only = display_failures_only
end
def audit
warnings = Set.new
errors = Set.new
if !language && language_blocks
language_blocks.each_key do |l|
audit = audit_languages(l)
summary = audit.summary(include_passed: output_passed?, include_warnings: output_warnings?)
if summary.present? && output_summary?(audit)
ohai "Auditing language: #{l.map { |lang| "'#{lang}'" }.to_sentence}" if output_summary?
puts summary
end
warnings += audit.warnings
errors += audit.errors
end
else
audit = audit_cask_instance(cask)
summary = audit.summary(include_passed: output_passed?, include_warnings: output_warnings?)
puts summary if summary.present? && output_summary?(audit)
warnings += audit.warnings
errors += audit.errors
end
{ warnings: warnings, errors: errors }
end
private
def output_summary?(audit = nil)
return true if @any_named_args.present?
return true if @audit_strict.present?
return false if audit.blank?
audit.errors?
end
def output_passed?
return false if @display_failures_only.present?
return true if @display_passes.present?
false
end
def output_warnings?
return false if @display_failures_only.present?
true
end
def audit_languages(languages)
original_config = cask.config
localized_config = original_config.merge(Config.new(explicit: { languages: languages }))
cask.config = localized_config
audit_cask_instance(cask)
ensure
cask.config = original_config
end
def audit_cask_instance(cask)
audit = Audit.new(
cask,
appcast: @audit_appcast,
online: @audit_online,
strict: @audit_strict,
signing: @audit_signing,
new_cask: @audit_new_cask,
token_conflicts: @audit_token_conflicts,
download: @audit_download,
quarantine: @quarantine,
)
audit.run!
end
def language_blocks
cask.instance_variable_get(:@dsl).instance_variable_get(:@language_blocks)
end
end
end
# typed: true
# frozen_string_literal: true
module Cask
# Helper functions for the cask cache.
#
# @api private
module Cache
extend T::Sig
sig { returns(Pathname) }
def self.path
@path ||= HOMEBREW_CACHE/"Cask"
end
end
end
# typed: false
# frozen_string_literal: true
require "cask/cask_loader"
require "cask/config"
require "cask/dsl"
require "cask/metadata"
require "searchable"
require "utils/bottles"
module Cask
# An instance of a cask.
#
# @api private
class Cask
extend T::Sig
extend Forwardable
extend Searchable
include Metadata
attr_reader :token, :sourcefile_path, :source, :config, :default_config
attr_accessor :download, :allow_reassignment
def self.all
# TODO: uncomment for 3.7.0 and ideally avoid using ARGV by moving to e.g. CLI::Parser
# if !ARGV.include?("--eval-all") && !Homebrew::EnvConfig.eval_all?
# odeprecated "Cask::Cask#all without --all or HOMEBREW_EVAL_ALL"
# end
Tap.flat_map(&:cask_files).map do |f|
CaskLoader::FromTapPathLoader.new(f).load(config: nil)
rescue CaskUnreadableError => e
opoo e.message
nil
end.compact
end
def tap
return super if block_given? # Object#tap
@tap
end
def initialize(token, sourcefile_path: nil, source: nil, tap: nil, config: nil, allow_reassignment: false, &block)
@token = token
@sourcefile_path = sourcefile_path
@source = source
@tap = tap
@allow_reassignment = allow_reassignment
@block = block
@default_config = config || Config.new
self.config = if config_path.exist?
Config.from_json(File.read(config_path), ignore_invalid_keys: true)
else
@default_config
end
end
def config=(config)
@config = config
refresh
end
def refresh
@dsl = DSL.new(self)
return unless @block
@dsl.instance_eval(&@block)
@dsl.language_eval
end
DSL::DSL_METHODS.each do |method_name|
define_method(method_name) { |&block| @dsl.send(method_name, &block) }
end
sig { returns(T::Array[[String, String]]) }
def timestamped_versions
Pathname.glob(metadata_timestamped_path(version: "*", timestamp: "*"))
.map { |p| p.relative_path_from(p.parent.parent) }
.sort_by(&:basename) # sort by timestamp
.map { |p| p.split.map(&:to_s) }
end
def versions
timestamped_versions.map(&:first)
.reverse
.uniq
.reverse
end
def os_versions
# TODO: use #to_hash_with_variations instead once all casks use on_system blocks
@os_versions ||= begin
version_os_hash = {}
actual_version = MacOS.full_version.to_s
MacOSVersions::SYMBOLS.each do |os_name, os_version|
MacOS.full_version = os_version
cask = CaskLoader.load(full_name)
version_os_hash[os_name] = cask.version if cask.version != version
end
version_os_hash
ensure
MacOS.full_version = actual_version
end
end
def full_name
return token if tap.nil?
return token if tap.user == "Homebrew"
"#{tap.name}/#{token}"
end
def installed?
!versions.empty?
end
sig { returns(T.nilable(Time)) }
def install_time
_, time = timestamped_versions.last
return unless time
Time.strptime(time, Metadata::TIMESTAMP_FORMAT)
end
def installed_caskfile
installed_version = timestamped_versions.last
metadata_main_container_path.join(*installed_version, "Casks", "#{token}.rb")
end
def config_path
metadata_main_container_path/"config.json"
end
def checksumable?
DownloadStrategyDetector.detect(url.to_s, url.using) <= AbstractFileDownloadStrategy
end
def download_sha_path
metadata_main_container_path/"LATEST_DOWNLOAD_SHA256"
end
def new_download_sha
require "cask/installer"
# Call checksumable? before hashing
@new_download_sha ||= Installer.new(self, verify_download_integrity: false)
.download(quiet: true)
.instance_eval { |x| Digest::SHA256.file(x).hexdigest }
end
def outdated_download_sha?
return true unless checksumable?
current_download_sha = download_sha_path.read if download_sha_path.exist?
current_download_sha.blank? || current_download_sha != new_download_sha
end
def caskroom_path
@caskroom_path ||= Caskroom.path.join(token)
end
def outdated?(greedy: false, greedy_latest: false, greedy_auto_updates: false)
!outdated_versions(greedy: greedy, greedy_latest: greedy_latest,
greedy_auto_updates: greedy_auto_updates).empty?
end
def outdated_versions(greedy: false, greedy_latest: false, greedy_auto_updates: false)
# special case: tap version is not available
return [] if version.nil?
if version.latest?
return versions if (greedy || greedy_latest) && outdated_download_sha?
return []
elsif auto_updates && !greedy && !greedy_auto_updates
return []
end
installed = versions
current = installed.last
# not outdated unless there is a different version on tap
return [] if current == version
# collect all installed versions that are different than tap version and return them
installed.reject { |v| v == version }
end
def outdated_info(greedy, verbose, json, greedy_latest, greedy_auto_updates)
return token if !verbose && !json
installed_versions = outdated_versions(greedy: greedy, greedy_latest: greedy_latest,
greedy_auto_updates: greedy_auto_updates).join(", ")
if json
{
name: token,
installed_versions: installed_versions,
current_version: version,
}
else
"#{token} (#{installed_versions}) != #{version}"
end
end
def to_s
@token
end
def hash
token.hash
end
def eql?(other)
instance_of?(other.class) && token == other.token
end
alias == eql?
def to_h
{
"token" => token,
"full_token" => full_name,
"tap" => tap&.name,
"name" => name,
"desc" => desc,
"homepage" => homepage,
"url" => url,
"appcast" => appcast,
"version" => version,
"versions" => os_versions,
"installed" => versions.last,
"outdated" => outdated?,
"sha256" => sha256,
"artifacts" => artifacts_list,
"caveats" => (to_h_string_gsubs(caveats) unless caveats.empty?),
"depends_on" => depends_on,
"conflicts_with" => conflicts_with,
"container" => container&.pairs,
"auto_updates" => auto_updates,
}
end
def to_hash_with_variations
hash = to_h
variations = {}
hash_keys_to_skip = %w[outdated installed versions]
if @dsl.on_system_blocks_exist?
[:arm, :intel].each do |arch|
MacOSVersions::SYMBOLS.each_key do |os_name|
bottle_tag = ::Utils::Bottles::Tag.new(system: os_name, arch: arch)
next unless bottle_tag.valid_combination?
Homebrew::SimulateSystem.os = os_name
Homebrew::SimulateSystem.arch = arch
refresh
to_h.each do |key, value|
next if hash_keys_to_skip.include? key
next if value.to_s == hash[key].to_s
variations[bottle_tag.to_sym] ||= {}
variations[bottle_tag.to_sym][key] = value
end
end
end
end
Homebrew::SimulateSystem.clear
refresh
hash["variations"] = variations
hash
end
private
def artifacts_list
artifacts.map do |artifact|
next artifact.to_h if artifact.is_a? Artifact::AbstractFlightBlock
{ artifact.class.dsl_key => to_h_gsubs(artifact.to_args) }
end
end
def to_h_string_gsubs(string)
string.to_s
.gsub(Dir.home, "$HOME")
.gsub(HOMEBREW_PREFIX, "$(brew --prefix)")
end
def to_h_array_gsubs(array)
array.to_a.map do |value|
to_h_gsubs(value)
end
end
def to_h_hash_gsubs(hash)
hash.to_h.transform_values do |value|
to_h_gsubs(value)
end
rescue TypeError
to_h_array_gsubs(hash)
end
def to_h_gsubs(value)
if value.respond_to? :to_h
to_h_hash_gsubs(value)
elsif value.respond_to? :to_a
to_h_array_gsubs(value)
else
to_h_string_gsubs(value)
end
end
end
end
# typed: strict
module Cask
class Cask
def appcast; end
def artifacts; end
def auto_updates; end
def caveats; end
def conflicts_with; end
def container; end
def desc; end
def depends_on; end
def homepage; end
def language; end
def languages; end
def name; end
def sha256; end
def staged_path; end
def url; end
def version; end
def appdir; end
def discontinued?; end
def livecheck; end
def livecheckable?; end
end
end
# typed: false
# frozen_string_literal: true
require "cask/cache"
require "cask/cask"
require "uri"
module Cask
# Loads a cask from various sources.
#
# @api private
module CaskLoader
# Loads a cask from a string.
class FromContentLoader
attr_reader :content
def self.can_load?(ref)
return false unless ref.respond_to?(:to_str)
content = ref.to_str
# Cache compiled regex
@regex ||= begin
token = /(?:"[^"]*"|'[^']*')/
curly = /\(\s*#{token.source}\s*\)\s*\{.*\}/
do_end = /\s+#{token.source}\s+do(?:\s*;\s*|\s+).*end/
/\A\s*cask(?:#{curly.source}|#{do_end.source})\s*\Z/m
end
content.match?(@regex)
end
def initialize(content)
@content = content.force_encoding("UTF-8")
end
def load(config:)
@config = config
instance_eval(content, __FILE__, __LINE__)
end
private
def cask(header_token, **options, &block)
Cask.new(header_token, source: content, **options, config: @config, &block)
end
end
# Loads a cask from a path.
class FromPathLoader < FromContentLoader
def self.can_load?(ref)
path = Pathname(ref)
path.extname == ".rb" && path.expand_path.exist?
end
attr_reader :token, :path
def initialize(path) # rubocop:disable Lint/MissingSuper
path = Pathname(path).expand_path
@token = path.basename(".rb").to_s
@path = path
end
def load(config:)
raise CaskUnavailableError.new(token, "'#{path}' does not exist.") unless path.exist?
raise CaskUnavailableError.new(token, "'#{path}' is not readable.") unless path.readable?
raise CaskUnavailableError.new(token, "'#{path}' is not a file.") unless path.file?
@content = path.read(encoding: "UTF-8")
@config = config
begin
instance_eval(content, path).tap do |cask|
raise CaskUnreadableError.new(token, "'#{path}' does not contain a cask.") unless cask.is_a?(Cask)
end
rescue NameError, ArgumentError, ScriptError => e
error = CaskUnreadableError.new(token, e.message)
error.set_backtrace e.backtrace
raise error
end
end
private
def cask(header_token, **options, &block)
raise CaskTokenMismatchError.new(token, header_token) if token != header_token
super(header_token, **options, sourcefile_path: path, &block)
end
end
# Loads a cask from a URI.
class FromURILoader < FromPathLoader
extend T::Sig
def self.can_load?(ref)
# Cache compiled regex
@uri_regex ||= begin
uri_regex = ::URI::DEFAULT_PARSER.make_regexp
Regexp.new("\\A#{uri_regex.source}\\Z", uri_regex.options)
end
return false unless ref.to_s.match?(@uri_regex)
uri = URI(ref)
return false unless uri
return false unless uri.path
true
end
attr_reader :url
sig { params(url: T.any(URI::Generic, String)).void }
def initialize(url)
@url = URI(url)
super Cache.path/File.basename(@url.path)
end
def load(config:)
path.dirname.mkpath
begin
ohai "Downloading #{url}"
curl_download url, to: path
rescue ErrorDuringExecution
raise CaskUnavailableError.new(token, "Failed to download #{Formatter.url(url)}.")
end
super
end
end
# Loads a cask from a tap path.
class FromTapPathLoader < FromPathLoader
def self.can_load?(ref)
super && !Tap.from_path(ref).nil?
end
attr_reader :tap
def initialize(path)
@tap = Tap.from_path(path)
super(path)
end
private
def cask(*args, &block)
super(*args, tap: tap, &block)
end
end
# Loads a cask from a specific tap.
class FromTapLoader < FromTapPathLoader
def self.can_load?(ref)
ref.to_s.match?(HOMEBREW_TAP_CASK_REGEX)
end
def initialize(tapped_name)
user, repo, token = tapped_name.split("/", 3)
super Tap.fetch(user, repo).cask_dir/"#{token}.rb"
end
def load(config:)
raise TapCaskUnavailableError.new(tap, token) unless tap.installed?
super
end
end
# Loads a cask from an existing {Cask} instance.
class FromInstanceLoader
def self.can_load?(ref)
ref.is_a?(Cask)
end
def initialize(cask)
@cask = cask
end
def load(config:)
@cask
end
end
# Loads a cask from the JSON API.
class FromAPILoader
attr_reader :token, :path
FLIGHT_STANZAS = [:preflight, :postflight, :uninstall_preflight, :uninstall_postflight].freeze
def self.can_load?(ref)
Homebrew::API::Cask.all_casks.key? ref
end
def initialize(token)
@token = token
@path = CaskLoader.default_path(token)
end
def load(config:)
json_cask = Homebrew::API::Cask.all_casks[token]
if (bottle_tag = ::Utils::Bottles.tag.to_s.presence) &&
(variations = json_cask["variations"].presence) &&
(variation = variations[bottle_tag].presence)
json_cask = json_cask.merge(variation)
end
json_cask.deep_symbolize_keys!
# Use the cask-source API if there are any `*flight` blocks
if json_cask[:artifacts].any? { |artifact| FLIGHT_STANZAS.include?(artifact.keys.first) }
cask_source = Homebrew::API::CaskSource.fetch(token)
return FromContentLoader.new(cask_source).load(config: config)
end
Cask.new(token, source: cask_source, config: config) do
version json_cask[:version]
if json_cask[:sha256] == "no_check"
sha256 :no_check
else
sha256 json_cask[:sha256]
end
url json_cask[:url]
appcast json_cask[:appcast] if json_cask[:appcast].present?
json_cask[:name].each do |cask_name|
name cask_name
end
desc json_cask[:desc]
homepage json_cask[:homepage]
auto_updates json_cask[:auto_updates] if json_cask[:auto_updates].present?
conflicts_with(**json_cask[:conflicts_with]) if json_cask[:conflicts_with].present?
if json_cask[:depends_on].present?
dep_hash = json_cask[:depends_on].to_h do |dep_key, dep_value|
# Arch dependencies are encoded like `{ type: :intel, bits: 64 }`
# but `depends_on arch:` only accepts `:intel` or `:arm64`
if dep_key == :arch
next [:arch, :intel] if dep_value.first[:type] == "intel"
next [:arch, :arm64]
end
next [dep_key, dep_value] unless dep_key == :macos
dep_type = dep_value.keys.first
if dep_type == :==
version_symbols = dep_value[dep_type].map do |version|
MacOSVersions::SYMBOLS.key(version) || version
end
next [dep_key, version_symbols]
end
version_symbol = dep_value[dep_type].first
version_symbol = MacOSVersions::SYMBOLS.key(version_symbol) || version_symbol
[dep_key, "#{dep_type} :#{version_symbol}"]
end.compact
depends_on(**dep_hash)
end
if json_cask[:container].present?
container_hash = json_cask[:container].to_h do |container_key, container_value|
next [container_key, container_value] unless container_key == :type
[container_key, container_value.to_sym]
end
container(**container_hash)
end
json_cask[:artifacts].each do |artifact|
key = artifact.keys.first
if FLIGHT_STANZAS.include?(key)
instance_eval(artifact[key])
else
send(key, *artifact[key])
end
end
caveats json_cask[:caveats] if json_cask[:caveats].present?
end
end
end
# Pseudo-loader which raises an error when trying to load the corresponding cask.
class NullLoader < FromPathLoader
extend T::Sig
def self.can_load?(*)
true
end
sig { params(ref: T.any(String, Pathname)).void }
def initialize(ref)
token = File.basename(ref, ".rb")
super CaskLoader.default_path(token)
end
def load(config:)
raise CaskUnavailableError.new(token, "No Cask with this name exists.")
end
end
def self.path(ref)
self.for(ref, need_path: true).path
end
def self.load(ref, config: nil)
self.for(ref).load(config: config)
end
def self.for(ref, need_path: false)
[
FromInstanceLoader,
FromContentLoader,
FromURILoader,
FromTapLoader,
FromTapPathLoader,
FromPathLoader,
].each do |loader_class|
next unless loader_class.can_load?(ref)
if loader_class == FromTapLoader && Homebrew::EnvConfig.install_from_api? &&
ref.start_with?("homebrew/cask/") && FromAPILoader.can_load?(ref)
return FromAPILoader.new(ref)
end
return loader_class.new(ref)
end
if Homebrew::EnvConfig.install_from_api? && !need_path && Homebrew::API::CaskSource.available?(ref)
return FromAPILoader.new(ref)
end
return FromTapPathLoader.new(default_path(ref)) if FromTapPathLoader.can_load?(default_path(ref))
case (possible_tap_casks = tap_paths(ref)).count
when 1
return FromTapPathLoader.new(possible_tap_casks.first)
when 2..Float::INFINITY
loaders = possible_tap_casks.map(&FromTapPathLoader.method(:new))
raise TapCaskAmbiguityError.new(ref, loaders)
end
possible_installed_cask = Cask.new(ref)
return FromPathLoader.new(possible_installed_cask.installed_caskfile) if possible_installed_cask.installed?
NullLoader.new(ref)
end
def self.default_path(token)
Tap.default_cask_tap.cask_dir/"#{token.to_s.downcase}.rb"
end
def self.tap_paths(token)
Tap.map { |t| t.cask_dir/"#{token.to_s.downcase}.rb" }
.select(&:exist?)
end
end
end
# typed: true
# frozen_string_literal: true
require "utils/user"
module Cask
# Helper functions for interacting with the `Caskroom` directory.
#
# @api private
module Caskroom
extend T::Sig
sig { returns(Pathname) }
def self.path
@path ||= HOMEBREW_PREFIX/"Caskroom"
end
sig { returns(T::Boolean) }
def self.any_casks_installed?
return false unless path.exist?
path.children.select(&:directory?).any?
end
sig { void }
def self.ensure_caskroom_exists
return if path.exist?
sudo = !path.parent.writable?
if sudo && !ENV.key?("SUDO_ASKPASS") && $stdout.tty?
ohai "Creating Caskroom directory: #{path}",
"We'll set permissions properly so we won't need sudo in the future."
end
SystemCommand.run("/bin/mkdir", args: ["-p", path], sudo: sudo)
SystemCommand.run("/bin/chmod", args: ["g+rwx", path], sudo: sudo)
SystemCommand.run("/usr/sbin/chown", args: [User.current, path], sudo: sudo)
SystemCommand.run("/usr/bin/chgrp", args: ["admin", path], sudo: sudo)
end
sig { params(config: T.nilable(Config)).returns(T::Array[Cask]) }
def self.casks(config: nil)
return [] unless path.exist?
path.children.select(&:directory?).sort.map do |path|
token = path.basename.to_s
begin
CaskLoader.load(token, config: config)
rescue TapCaskAmbiguityError
tap_path = CaskLoader.tap_paths(token).first
CaskLoader::FromTapPathLoader.new(tap_path).load(config: config)
rescue CaskUnavailableError
# Don't blow up because of a single unavailable cask.
nil
end
end.compact
end
end
end
# typed: true
# frozen_string_literal: true
require "optparse"
require "shellwords"
require "cli/parser"
require "extend/optparse"
require "cask/config"
require "cask/cmd/abstract_command"
require "cask/cmd/audit"
require "cask/cmd/fetch"
require "cask/cmd/info"
require "cask/cmd/install"
require "cask/cmd/list"
require "cask/cmd/reinstall"
require "cask/cmd/uninstall"
require "cask/cmd/upgrade"
require "cask/cmd/zap"
module Cask
# Implementation of the `brew cask` command-line interface.
#
# @api private
class Cmd
extend T::Sig
include Context
def self.parser(&block)
Homebrew::CLI::Parser.new do
instance_eval(&block) if block
cask_options
end
end
end
end
# typed: false
# frozen_string_literal: true
require "search"
module Cask
class Cmd
# Abstract superclass for all Cask implementations of commands.
#
# @api private
class AbstractCommand
extend T::Sig
extend T::Helpers
OPTIONS = [
[:switch, "--[no-]binaries", {
description: "Disable/enable linking of helper executables (default: enabled).",
env: :cask_opts_binaries,
}],
[:switch, "--require-sha", {
description: "Require all casks to have a checksum.",
env: :cask_opts_require_sha,
}],
[:switch, "--[no-]quarantine", {
description: "Disable/enable quarantining of downloads (default: enabled).",
env: :cask_opts_quarantine,
}],
].freeze
def self.parser(&block)
Cmd.parser do
instance_eval(&block) if block
OPTIONS.map(&:dup).each do |option|
kwargs = option.pop
send(*option, **kwargs)
end
end
end
sig { returns(String) }
def self.command_name
@command_name ||= name.sub(/^.*:/, "").gsub(/(.)([A-Z])/, '\1_\2').downcase
end
sig { returns(T::Boolean) }
def self.abstract?
name.split("::").last.match?(/^Abstract[^a-z]/)
end
sig { returns(T::Boolean) }
def self.visible?
true
end
sig { returns(String) }
def self.help
parser.generate_help_text
end
sig { returns(String) }
def self.short_description
description[/\A[^.]*\./]
end
def self.run(*args)
new(*args).run
end
attr_reader :args
def initialize(*args)
@args = self.class.parser.parse(args)
end
private
def casks(alternative: -> { [] })
return @casks if defined?(@casks)
@casks = args.named.empty? ? alternative.call : args.named.to_casks
rescue CaskUnavailableError => e
reason = [e.reason, *suggestion_message(e.token)].join(" ")
raise e.class.new(e.token, reason)
end
def suggestion_message(cask_token)
matches = Homebrew::Search.search_casks(cask_token)
if matches.one?
"Did you mean '#{matches.first}'?"
elsif !matches.empty?
"Did you mean one of these?\n#{Formatter.columns(matches.take(20))}"
end
end
end
end
end
# typed: false
# frozen_string_literal: true
require "utils/github/actions"
module Cask
class Cmd
# Cask implementation of the `brew audit` command.
#
# @api private
class Audit < AbstractCommand
extend T::Sig
def self.parser
super do
switch "--[no-]download",
description: "Audit the downloaded file"
switch "--[no-]appcast",
description: "Audit the appcast"
switch "--[no-]token-conflicts",
description: "Audit for token conflicts"
switch "--[no-]signing",
description: "Audit for signed apps, which is required on ARM"
switch "--[no-]strict",
description: "Run additional, stricter style checks"
switch "--[no-]online",
description: "Run additional, slower style checks that require a network connection"
switch "--new-cask",
description: "Run various additional style checks to determine if a new cask is eligible " \
"for Homebrew. This should be used when creating new casks and implies " \
"`--strict` and `--online`"
switch "--display-failures-only",
description: "Only display casks that fail the audit. This is the default for formulae."
end
end
sig { void }
def run
casks = args.named.flat_map do |name|
next name if File.exist?(name)
next Tap.fetch(name).cask_files if name.count("/") == 1
name
end
casks = casks.map { |c| CaskLoader.load(c, config: Config.from_args(args)) }
any_named_args = casks.any?
casks = Cask.to_a if casks.empty?
results = self.class.audit_casks(
*casks,
download: args.download?,
appcast: args.appcast?,
online: args.online?,
strict: args.strict?,
signing: args.signing?,
new_cask: args.new_cask?,
token_conflicts: args.token_conflicts?,
quarantine: args.quarantine?,
any_named_args: any_named_args,
language: args.language,
display_passes: args.verbose? || args.named.count == 1,
display_failures_only: args.display_failures_only?,
)
failed_casks = results.reject { |_, result| result[:errors].empty? }.map(&:first)
return if failed_casks.empty?
raise CaskError, "audit failed for casks: #{failed_casks.join(" ")}"
end
def self.audit_casks(
*casks,
download: nil,
appcast: nil,
online: nil,
strict: nil,
signing: nil,
new_cask: nil,
token_conflicts: nil,
quarantine: nil,
any_named_args: nil,
language: nil,
display_passes: nil,
display_failures_only: nil
)
options = {
audit_download: download,
audit_appcast: appcast,
audit_online: online,
audit_strict: strict,
audit_signing: signing,
audit_new_cask: new_cask,
audit_token_conflicts: token_conflicts,
quarantine: quarantine,
language: language,
any_named_args: any_named_args,
display_passes: display_passes,
display_failures_only: display_failures_only,
}.compact
options[:quarantine] = true if options[:quarantine].nil?
Homebrew.auditing = true
require "cask/auditor"
casks.to_h do |cask|
odebug "Auditing Cask #{cask}"
[cask.sourcefile_path, Auditor.audit(cask, **options)]
end
end
end
end
end
# typed: false
# frozen_string_literal: true
module Cask
class Cmd
# Cask implementation of the `brew fetch` command.
#
# @api private
class Fetch < AbstractCommand
extend T::Sig
def self.parser
super do
switch "--force",
description: "Force redownloading even if files already exist in local cache."
end
end
sig { void }
def run
require "cask/download"
require "cask/installer"
options = {
quarantine: args.quarantine?,
}.compact
options[:quarantine] = true if options[:quarantine].nil?
casks.each do |cask|
puts Installer.caveats(cask)
ohai "Downloading external files for Cask #{cask}"
download = Download.new(cask, **options)
download.clear_cache if args.force?
downloaded_path = download.fetch
ohai "Success! Downloaded to: #{downloaded_path}"
end
end
end
end
end
# typed: false
# frozen_string_literal: true
require "json"
module Cask
class Cmd
# Cask implementation of the `brew info` command.
#
# @api private
class Info < AbstractCommand
extend T::Sig
def self.parser
super do
flag "--json=",
description: "Output information in JSON format."
switch "--github",
description: "Open the GitHub source page for <Cask> in a browser. "
end
end
def github_info(cask)
sourcefile_path = cask.sourcefile_path
dir = cask.tap.path
path = sourcefile_path.relative_path_from(dir)
remote = cask.tap.remote
github_remote_path(remote, path)
end
def github_remote_path(remote, path)
if remote =~ %r{^(?:https?://|git(?:@|://))github\.com[:/](.+)/(.+?)(?:\.git)?$}
"https://github.com/#{Regexp.last_match(1)}/#{Regexp.last_match(2)}/blob/HEAD/#{path}"
else
"#{remote}/#{path}"
end
end
sig { void }
def run
if args.json == "v1"
puts JSON.pretty_generate(args.named.to_casks.map(&:to_h))
elsif args.github?
raise CaskUnspecifiedError if args.no_named?
args.named.to_casks.map do |cask|
exec_browser(github_info(cask))
end
else
args.named.to_casks.each_with_index do |cask, i|
puts unless i.zero?
odebug "Getting info for Cask #{cask}"
self.class.info(cask)
end
end
end
def self.get_info(cask)
require "cask/installer"
output = +"#{title_info(cask)}\n"
output << "#{Formatter.url(cask.homepage)}\n" if cask.homepage
output << installation_info(cask)
repo = repo_info(cask)
output << "#{repo}\n" if repo
output << name_info(cask)
output << desc_info(cask)
language = language_info(cask)
output << language if language
output << "#{artifact_info(cask)}\n"
caveats = Installer.caveats(cask)
output << caveats if caveats
output
end
def self.info(cask)
puts get_info(cask)
::Utils::Analytics.cask_output(cask, args: Homebrew::CLI::Args.new)
end
def self.title_info(cask)
title = "#{oh1_title(cask.token)}: #{cask.version}"
title += " (auto_updates)" if cask.auto_updates
title
end
def self.formatted_url(url)
"#{Tty.underline}#{url}#{Tty.reset}"
end
def self.installation_info(cask)
return "Not installed\n" unless cask.installed?
install_info = +""
cask.versions.each do |version|
versioned_staged_path = cask.caskroom_path.join(version)
path_details = if versioned_staged_path.exist?
versioned_staged_path.abv
else
Formatter.error("does not exist")
end
install_info << "#{versioned_staged_path} (#{path_details})\n"
end
install_info.freeze
end
def self.name_info(cask)
<<~EOS
#{ohai_title((cask.name.size > 1) ? "Names" : "Name")}
#{cask.name.empty? ? Formatter.error("None") : cask.name.join("\n")}
EOS
end
def self.desc_info(cask)
<<~EOS
#{ohai_title("Description")}
#{cask.desc.nil? ? Formatter.error("None") : cask.desc}
EOS
end
def self.language_info(cask)
return if cask.languages.empty?
<<~EOS
#{ohai_title("Languages")}
#{cask.languages.join(", ")}
EOS
end
def self.repo_info(cask)
return if cask.tap.nil?
url = if cask.tap.custom_remote? && !cask.tap.remote.nil?
cask.tap.remote
else
"#{cask.tap.default_remote}/blob/HEAD/Casks/#{cask.token}.rb"
end
"From: #{Formatter.url(url)}"
end
def self.artifact_info(cask)
artifact_output = ohai_title("Artifacts").dup
cask.artifacts.each do |artifact|
next unless artifact.respond_to?(:install_phase)
next unless DSL::ORDINARY_ARTIFACT_CLASSES.include?(artifact.class)
artifact_output << "\n" << artifact.to_s
end
artifact_output.freeze
end
end
end
end
# typed: false
# frozen_string_literal: true
require "cask_dependent"
module Cask
class Cmd
# Cask implementation of the `brew install` command.
#
# @api private
class Install < AbstractCommand
extend T::Sig
OPTIONS = [
[:switch, "--adopt", {
description: "Adopt existing artifacts in the destination that are identical to those being installed. " \
"Cannot be combined with --force.",
}],
[:switch, "--skip-cask-deps", {
description: "Skip installing cask dependencies.",
}],
[:switch, "--zap", {
description: "For use with `brew reinstall --cask`. Remove all files associated with a cask. " \
"*May remove files which are shared between applications.*",
}],
].freeze
def self.parser(&block)
super do
switch "--force",
description: "Force overwriting existing files."
OPTIONS.map(&:dup).each do |option|
kwargs = option.pop
send(*option, **kwargs)
end
instance_eval(&block) if block
end
end
sig { void }
def run
self.class.install_casks(
*casks,
binaries: args.binaries?,
verbose: args.verbose?,
force: args.force?,
adopt: args.adopt?,
skip_cask_deps: args.skip_cask_deps?,
require_sha: args.require_sha?,
quarantine: args.quarantine?,
quiet: args.quiet?,
zap: args.zap?,
)
end
def self.install_casks(
*casks,
verbose: nil,
force: nil,
adopt: nil,
binaries: nil,
skip_cask_deps: nil,
require_sha: nil,
quarantine: nil,
quiet: nil,
zap: nil,
dry_run: nil
)
# TODO: Refactor and move to extend/os
odie "Installing casks is supported only on macOS" unless OS.mac? # rubocop:disable Homebrew/MoveToExtendOS
options = {
verbose: verbose,
force: force,
adopt: adopt,
binaries: binaries,
skip_cask_deps: skip_cask_deps,
require_sha: require_sha,
quarantine: quarantine,
quiet: quiet,
zap: zap,
}.compact
options[:quarantine] = true if options[:quarantine].nil?
if dry_run
if (casks_to_install = casks.reject(&:installed?).presence)
plural = "cask".pluralize(casks_to_install.count)
ohai "Would install #{casks_to_install.count} #{plural}:"
puts casks_to_install.map(&:full_name).join(" ")
end
casks.each do |cask|
dep_names = CaskDependent.new(cask)
.runtime_dependencies
.reject(&:installed?)
.map(&:to_formula)
.map(&:name)
next if dep_names.blank?
plural = "dependency".pluralize(dep_names.count)
ohai "Would install #{dep_names.count} #{plural} for #{cask.full_name}:"
puts dep_names.join(" ")
end
return
end
require "cask/installer"
casks.each do |cask|
Installer.new(cask, **options).install
rescue CaskAlreadyInstalledError => e
opoo e.message
end
end
end
end
end
# typed: false
# frozen_string_literal: true
require "cask/artifact/relocated"
module Cask
class Cmd
# Cask implementation of the `brew list` command.
#
# @api private
class List < AbstractCommand
extend T::Sig
def self.parser
super do
switch "-1",
description: "Force output to be one entry per line."
switch "--versions",
description: "Show the version number the listed casks."
switch "--full-name",
description: "Print casks with fully-qualified names."
switch "--json",
description: "Print a JSON representation of the listed casks. "
end
end
sig { void }
def run
self.class.list_casks(
*casks,
json: args.json?,
one: args.public_send(:"1?"),
full_name: args.full_name?,
versions: args.versions?,
)
end
def self.list_casks(*casks, json: false, one: false, full_name: false, versions: false)
output = if casks.any?
casks.each do |cask|
raise CaskNotInstalledError, cask unless cask.installed?
end
else
Caskroom.casks
end
if json
puts JSON.pretty_generate(output.map(&:to_h))
elsif one
puts output.map(&:to_s)
elsif full_name
puts output.map(&:full_name).sort(&tap_and_name_comparison)
elsif versions
puts output.map(&method(:format_versioned))
elsif !output.empty? && casks.any?
output.map(&method(:list_artifacts))
elsif !output.empty?
puts Formatter.columns(output.map(&:to_s))
end
end
def self.list_artifacts(cask)
cask.artifacts.group_by(&:class).sort_by { |klass, _| klass.english_name }.each do |klass, artifacts|
next if [Artifact::Uninstall, Artifact::Zap].include? klass
ohai klass.english_name
artifacts.each do |artifact|
puts artifact.summarize_installed if artifact.respond_to?(:summarize_installed)
next if artifact.respond_to?(:summarize_installed)
puts artifact
end
end
end
def self.format_versioned(cask)
cask.to_s.concat(cask.versions.map(&:to_s).join(" ").prepend(" "))
end
end
end
end
# typed: true
# frozen_string_literal: true
module Cask
class Cmd
# Cask implementation of the `brew reinstall` command.
#
# @api private
class Reinstall < Install
extend T::Sig
sig { void }
def run
self.class.reinstall_casks(
*casks,
binaries: args.binaries?,
verbose: args.verbose?,
force: args.force?,
skip_cask_deps: args.skip_cask_deps?,
require_sha: args.require_sha?,
quarantine: args.quarantine?,
zap: args.zap?,
)
end
def self.reinstall_casks(
*casks,
verbose: nil,
force: nil,
skip_cask_deps: nil,
binaries: nil,
require_sha: nil,
quarantine: nil,
zap: nil
)
require "cask/installer"
options = {
binaries: binaries,
verbose: verbose,
force: force,
skip_cask_deps: skip_cask_deps,
require_sha: require_sha,
quarantine: quarantine,
zap: zap,
}.compact
options[:quarantine] = true if options[:quarantine].nil?
casks.each do |cask|
Installer.new(cask, **options).reinstall
end
end
end
end
end
# typed: false
# frozen_string_literal: true
module Cask
class Cmd
# Cask implementation of the `brew uninstall` command.
#
# @api private
class Uninstall < AbstractCommand
extend T::Sig
def self.parser
super do
switch "--force",
description: "Uninstall even if the <cask> is not installed, overwrite " \
"existing files and ignore errors when removing files."
end
end
sig { void }
def run
self.class.uninstall_casks(
*casks,
binaries: args.binaries?,
verbose: args.verbose?,
force: args.force?,
)
end
def self.uninstall_casks(*casks, binaries: nil, force: false, verbose: false)
require "cask/installer"
options = {
binaries: binaries,
force: force,
verbose: verbose,
}.compact
casks.each do |cask|
odebug "Uninstalling Cask #{cask}"
raise CaskNotInstalledError, cask if !cask.installed? && !force
Installer.new(cask, **options).uninstall
next if (versions = cask.versions).empty?
puts <<~EOS
#{cask} #{versions.to_sentence} #{"is".pluralize(versions.count)} still installed.
Remove #{(versions.count == 1) ? "it" : "them all"} with `brew uninstall --cask --force #{cask}`.
EOS
end
end
end
end
end
# typed: false
# frozen_string_literal: true
require "env_config"
require "cask/config"
module Cask
class Cmd
# Cask implementation of the `brew upgrade` command.
#
# @api private
class Upgrade < AbstractCommand
extend T::Sig
OPTIONS = [
[:switch, "--skip-cask-deps", {
description: "Skip installing cask dependencies.",
}],
[:switch, "--greedy", {
description: "Also include casks with `auto_updates true` or `version :latest`.",
}],
[:switch, "--greedy-latest", {
description: "Also include casks with `version :latest`.",
}],
[:switch, "--greedy-auto-updates", {
description: "Also include casks with `auto_updates true`.",
}],
].freeze
sig { returns(Homebrew::CLI::Parser) }
def self.parser
super do
switch "--force",
description: "Force overwriting existing files."
switch "--dry-run",
description: "Show what would be upgraded, but do not actually upgrade anything."
OPTIONS.map(&:dup).each do |option|
kwargs = option.pop
send(*option, **kwargs)
end
end
end
sig { void }
def run
verbose = ($stdout.tty? || args.verbose?) && !args.quiet?
self.class.upgrade_casks(
*casks,
force: args.force?,
greedy: args.greedy?,
greedy_latest: args.greedy_latest?,
greedy_auto_updates: args.greedy_auto_updates?,
dry_run: args.dry_run?,
binaries: args.binaries?,
quarantine: args.quarantine?,
require_sha: args.require_sha?,
skip_cask_deps: args.skip_cask_deps?,
verbose: verbose,
args: args,
)
end
sig {
params(
casks: Cask,
args: Homebrew::CLI::Args,
force: T.nilable(T::Boolean),
greedy: T.nilable(T::Boolean),
greedy_latest: T.nilable(T::Boolean),
greedy_auto_updates: T.nilable(T::Boolean),
dry_run: T.nilable(T::Boolean),
skip_cask_deps: T.nilable(T::Boolean),
verbose: T.nilable(T::Boolean),
binaries: T.nilable(T::Boolean),
quarantine: T.nilable(T::Boolean),
require_sha: T.nilable(T::Boolean),
).returns(T::Boolean)
}
def self.upgrade_casks(
*casks,
args:,
force: false,
greedy: false,
greedy_latest: false,
greedy_auto_updates: false,
dry_run: false,
skip_cask_deps: false,
verbose: false,
binaries: nil,
quarantine: nil,
require_sha: nil
)
quarantine = true if quarantine.nil?
outdated_casks = if casks.empty?
Caskroom.casks(config: Config.from_args(args)).select do |cask|
cask.outdated?(greedy: greedy, greedy_latest: greedy_latest,
greedy_auto_updates: greedy_auto_updates)
end
else
casks.select do |cask|
raise CaskNotInstalledError, cask if !cask.installed? && !force
if cask.outdated?(greedy: true)
true
elsif cask.version.latest?
opoo "Not upgrading #{cask.token}, the downloaded artifact has not changed"
false
else
opoo "Not upgrading #{cask.token}, the latest version is already installed"
false
end
end
end
manual_installer_casks = outdated_casks.select do |cask|
cask.artifacts.any?(Artifact::Installer::ManualInstaller)
end
if manual_installer_casks.present?
count = manual_installer_casks.count
ofail "Not upgrading #{count} `installer manual` #{"cask".pluralize(count)}."
puts manual_installer_casks.map(&:to_s)
outdated_casks -= manual_installer_casks
end
return false if outdated_casks.empty?
if casks.empty? && !greedy
if !greedy_auto_updates && !greedy_latest
ohai "Casks with 'auto_updates true' or 'version :latest' " \
"will not be upgraded; pass `--greedy` to upgrade them."
end
if greedy_auto_updates && !greedy_latest
ohai "Casks with 'version :latest' will not be upgraded; pass `--greedy-latest` to upgrade them."
end
if !greedy_auto_updates && greedy_latest
ohai "Casks with 'auto_updates true' will not be upgraded; pass `--greedy-auto-updates` to upgrade them."
end
end
verb = dry_run ? "Would upgrade" : "Upgrading"
oh1 "#{verb} #{outdated_casks.count} outdated #{"package".pluralize(outdated_casks.count)}:"
caught_exceptions = []
upgradable_casks = outdated_casks.map { |c| [CaskLoader.load(c.installed_caskfile), c] }
puts upgradable_casks
.map { |(old_cask, new_cask)| "#{new_cask.full_name} #{old_cask.version} -> #{new_cask.version}" }
.join("\n")
return true if dry_run
upgradable_casks.each do |(old_cask, new_cask)|
upgrade_cask(
old_cask, new_cask,
binaries: binaries, force: force, skip_cask_deps: skip_cask_deps, verbose: verbose,
quarantine: quarantine, require_sha: require_sha
)
rescue => e
caught_exceptions << e.exception("#{new_cask.full_name}: #{e}")
next
end
return true if caught_exceptions.empty?
raise MultipleCaskErrors, caught_exceptions if caught_exceptions.count > 1
raise caught_exceptions.first if caught_exceptions.count == 1
end
def self.upgrade_cask(
old_cask, new_cask,
binaries:, force:, quarantine:, require_sha:, skip_cask_deps:, verbose:
)
require "cask/installer"
start_time = Time.now
odebug "Started upgrade process for Cask #{old_cask}"
old_config = old_cask.config
old_options = {
binaries: binaries,
verbose: verbose,
force: force,
upgrade: true,
}.compact
old_cask_installer =
Installer.new(old_cask, **old_options)
new_cask.config = new_cask.default_config.merge(old_config)
new_options = {
binaries: binaries,
verbose: verbose,
force: force,
skip_cask_deps: skip_cask_deps,
require_sha: require_sha,
upgrade: true,
quarantine: quarantine,
}.compact
new_cask_installer =
Installer.new(new_cask, **new_options)
started_upgrade = false
new_artifacts_installed = false
begin
oh1 "Upgrading #{Formatter.identifier(old_cask)}"
# Start new cask's installation steps
new_cask_installer.check_conflicts
if (caveats = new_cask_installer.caveats)
puts caveats
end
new_cask_installer.fetch
# Move the old cask's artifacts back to staging
old_cask_installer.start_upgrade
# And flag it so in case of error
started_upgrade = true
# Install the new cask
new_cask_installer.stage
new_cask_installer.install_artifacts
new_artifacts_installed = true
# If successful, wipe the old cask from staging
old_cask_installer.finalize_upgrade
rescue => e
new_cask_installer.uninstall_artifacts if new_artifacts_installed
new_cask_installer.purge_versioned_files
old_cask_installer.revert_upgrade if started_upgrade
raise e
end
end_time = Time.now
Homebrew.messages.package_installed(new_cask.token, end_time - start_time)
end
end
end
end
# typed: false
# frozen_string_literal: true
module Cask
class Cmd
# Cask implementation for the `brew uninstall` command.
#
# @api private
class Zap < AbstractCommand
extend T::Sig
def self.parser
super do
switch "--force",
description: "Ignore errors when removing files."
end
end
sig { void }
def run
self.class.zap_casks(*casks, verbose: args.verbose?, force: args.force?)
end
sig { params(casks: Cask, force: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean)).void }
def self.zap_casks(
*casks,
force: nil,
verbose: nil
)
require "cask/installer"
casks.each do |cask|
odebug "Zapping Cask #{cask}"
raise CaskNotInstalledError, cask if !cask.installed? && !force
Installer.new(cask, verbose: verbose, force: force).zap
end
end
end
end
end
# typed: true
# frozen_string_literal: true
require "json"
require "lazy_object"
require "locale"
require "extend/hash_validator"
using HashValidator
module Cask
# Configuration for installing casks.
#
# @api private
class Config
extend T::Sig
DEFAULT_DIRS = {
appdir: "/Applications",
colorpickerdir: "~/Library/ColorPickers",
prefpanedir: "~/Library/PreferencePanes",
qlplugindir: "~/Library/QuickLook",
mdimporterdir: "~/Library/Spotlight",
dictionarydir: "~/Library/Dictionaries",
fontdir: "~/Library/Fonts",
servicedir: "~/Library/Services",
input_methoddir: "~/Library/Input Methods",
internet_plugindir: "~/Library/Internet Plug-Ins",
audio_unit_plugindir: "~/Library/Audio/Plug-Ins/Components",
vst_plugindir: "~/Library/Audio/Plug-Ins/VST",
vst3_plugindir: "~/Library/Audio/Plug-Ins/VST3",
screen_saverdir: "~/Library/Screen Savers",
}.freeze
def self.defaults
{
languages: LazyObject.new { MacOS.languages },
}.merge(DEFAULT_DIRS).freeze
end
sig { params(args: Homebrew::CLI::Args).returns(T.attached_class) }
def self.from_args(args)
new(explicit: {
appdir: args.appdir,
colorpickerdir: args.colorpickerdir,
prefpanedir: args.prefpanedir,
qlplugindir: args.qlplugindir,
mdimporterdir: args.mdimporterdir,
dictionarydir: args.dictionarydir,
fontdir: args.fontdir,
servicedir: args.servicedir,
input_methoddir: args.input_methoddir,
internet_plugindir: args.internet_plugindir,
audio_unit_plugindir: args.audio_unit_plugindir,
vst_plugindir: args.vst_plugindir,
vst3_plugindir: args.vst3_plugindir,
screen_saverdir: args.screen_saverdir,
languages: args.language,
}.compact)
end
sig { params(json: String, ignore_invalid_keys: T::Boolean).returns(T.attached_class) }
def self.from_json(json, ignore_invalid_keys: false)
config = JSON.parse(json)
new(
default: config.fetch("default", {}),
env: config.fetch("env", {}),
explicit: config.fetch("explicit", {}),
ignore_invalid_keys: ignore_invalid_keys,
)
end
sig {
params(config: T::Enumerable[[T.any(String, Symbol), T.any(String, Pathname, T::Array[String])]])
.returns(T::Hash[Symbol, T.any(String, Pathname, T::Array[String])])
}
def self.canonicalize(config)
config.to_h do |k, v|
key = k.to_sym
if DEFAULT_DIRS.key?(key)
[key, Pathname(v).expand_path]
else
[key, v]
end
end
end
sig { returns(T::Hash[Symbol, T.any(String, Pathname, T::Array[String])]) }
attr_accessor :explicit
sig {
params(
default: T.nilable(T::Hash[Symbol, T.any(String, Pathname, T::Array[String])]),
env: T.nilable(T::Hash[Symbol, T.any(String, Pathname, T::Array[String])]),
explicit: T::Hash[Symbol, T.any(String, Pathname, T::Array[String])],
ignore_invalid_keys: T::Boolean,
).void
}
def initialize(default: nil, env: nil, explicit: {}, ignore_invalid_keys: false)
@default = self.class.canonicalize(self.class.defaults.merge(default)) if default
@env = self.class.canonicalize(env) if env
@explicit = self.class.canonicalize(explicit)
if ignore_invalid_keys
@env&.delete_if { |key, _| self.class.defaults.keys.exclude?(key) }
@explicit.delete_if { |key, _| self.class.defaults.keys.exclude?(key) }
return
end
@env&.assert_valid_keys!(*self.class.defaults.keys)
@explicit.assert_valid_keys!(*self.class.defaults.keys)
end
sig { returns(T::Hash[Symbol, T.any(String, Pathname, T::Array[String])]) }
def default
@default ||= self.class.canonicalize(self.class.defaults)
end
sig { returns(T::Hash[Symbol, T.any(String, Pathname, T::Array[String])]) }
def env
@env ||= self.class.canonicalize(
Homebrew::EnvConfig.cask_opts
.select { |arg| arg.include?("=") }
.map { |arg| T.cast(arg.split("=", 2), [String, String]) }
.map do |(flag, value)|
key = flag.sub(/^--/, "")
# converts --language flag to :languages config key
if key == "language"
key = "languages"
value = value.split(",")
end
[key, value]
end,
)
end
sig { returns(Pathname) }
def binarydir
@binarydir ||= HOMEBREW_PREFIX/"bin"
end
sig { returns(Pathname) }
def manpagedir
@manpagedir ||= HOMEBREW_PREFIX/"share/man"
end
sig { returns(T::Array[String]) }
def languages
[
*T.cast(explicit.fetch(:languages, []), T::Array[String]),
*T.cast(env.fetch(:languages, []), T::Array[String]),
*T.cast(default.fetch(:languages, []), T::Array[String]),
].uniq.select do |lang|
# Ensure all languages are valid.
Locale.parse(lang)
true
rescue Locale::ParserError
false
end
end
def languages=(languages)
explicit[:languages] = languages
end
DEFAULT_DIRS.each_key do |dir|
define_method(dir) do
explicit.fetch(dir, env.fetch(dir, default.fetch(dir)))
end
define_method(:"#{dir}=") do |path|
explicit[dir] = Pathname(path).expand_path
end
end
sig { params(other: Config).returns(T.self_type) }
def merge(other)
self.class.new(explicit: other.explicit.merge(explicit))
end
sig { returns(String) }
def explicit_s
explicit.map do |key, value|
# inverse of #env - converts :languages config key back to --language flag
if key == :languages
key = "language"
value = T.cast(explicit.fetch(:languages, []), T::Array[String]).join(",")
end
"#{key}: \"#{value.to_s.sub(/^#{Dir.home}/, "~")}\""
end.join(", ")
end
sig { params(options: T.untyped).returns(String) }
def to_json(**options)
{
default: default,
env: env,
explicit: explicit,
}.to_json(**options)
end
end
end
# typed: strict
# frozen_string_literal: true
module Cask
# List of casks which are not allowed in official taps.
#
# @api private
module Denylist
extend T::Sig
sig { params(name: String).returns(T.nilable(String)) }
def self.reason(name)
case name
when /^adobe-(after|illustrator|indesign|photoshop|premiere)/
"Adobe casks were removed because they are too difficult to maintain."
when /^pharo$/
"Pharo developers maintain their own tap."
end
end
end
end
# typed: true
# frozen_string_literal: true
require "requirement"
# An adapter for casks to provide dependency information in a formula-like interface.
class CaskDependent
# Defines a dependency on another cask
class Requirement < ::Requirement
satisfy(build_env: false) do
Cask::CaskLoader.load(cask).installed?
end
end
attr_reader :cask
def initialize(cask)
@cask = cask
end
def name
@cask.token
end
def full_name
@cask.full_name
end
def runtime_dependencies
deps.flat_map { |dep| [dep, *dep.to_formula.runtime_dependencies] }.uniq
end
def deps
@deps ||= @cask.depends_on.formula.map do |f|
Dependency.new f
end
end
def requirements
@requirements ||= begin
requirements = []
dsl_reqs = @cask.depends_on
dsl_reqs.arch&.each do |arch|
arch = if arch[:bits] == 64
if arch[:type] == :intel
:x86_64
else
:"#{arch[:type]}64"
end
elsif arch[:type] == :intel && arch[:bits] == 32
:i386
else
arch[:type]
end
requirements << ArchRequirement.new([arch])
end
dsl_reqs.cask.each do |cask_ref|
requirements << CaskDependent::Requirement.new([{ cask: cask_ref }])
end
requirements << dsl_reqs.macos if dsl_reqs.macos
requirements
end
end
def recursive_dependencies(&block)
Dependency.expand(self, &block)
end
def recursive_requirements(&block)
Requirement.expand(self, &block)
end
def any_version_installed?
@cask.installed?
end
end
# typed: true
# frozen_string_literal: true
require "fileutils"
require "cask/cache"
require "cask/quarantine"
module Cask
# A download corresponding to a {Cask}.
#
# @api private
class Download
include Context
attr_reader :cask
def initialize(cask, quarantine: nil)
@cask = cask
@quarantine = quarantine
end
def fetch(quiet: nil, verify_download_integrity: true, timeout: nil)
downloaded_path = begin
downloader.shutup! if quiet
downloader.fetch(timeout: timeout)
downloader.cached_location
rescue => e
error = CaskError.new("Download failed on Cask '#{cask}' with message: #{e}")
error.set_backtrace e.backtrace
raise error
end
quarantine(downloaded_path)
self.verify_download_integrity(downloaded_path) if verify_download_integrity
downloaded_path
end
def downloader
@downloader ||= begin
strategy = DownloadStrategyDetector.detect(cask.url.to_s, cask.url.using)
strategy.new(cask.url.to_s, cask.token, cask.version, cache: Cache.path, **cask.url.specs)
end
end
def time_file_size(timeout: nil)
downloader.resolved_time_file_size(timeout: timeout)
end
def clear_cache
downloader.clear_cache
end
def cached_download
downloader.cached_location
end
def basename
downloader.basename
end
def verify_download_integrity(fn)
if @cask.sha256 == :no_check
opoo "No checksum defined for cask '#{@cask}', skipping verification."
return
end
begin
ohai "Verifying checksum for cask '#{@cask}'" if verbose?
fn.verify_checksum(@cask.sha256)
rescue ChecksumMissingError
opoo <<~EOS
Cannot verify integrity of '#{fn.basename}'.
No checksum was provided for this cask.
For your reference, the checksum is:
sha256 "#{fn.sha256}"
EOS
end
end
private
def quarantine(path)
return if @quarantine.nil?
return unless Quarantine.available?
if @quarantine
Quarantine.cask!(cask: @cask, download_path: path)
else
Quarantine.release!(download_path: path)
end
end
end
end
# typed: false
# frozen_string_literal: true
require "locale"
require "lazy_object"
require "livecheck"
require "cask/artifact"
require "cask/artifact_set"
require "cask/caskroom"
require "cask/exceptions"
require "cask/dsl/appcast"
require "cask/dsl/base"
require "cask/dsl/caveats"
require "cask/dsl/conflicts_with"
require "cask/dsl/container"
require "cask/dsl/depends_on"
require "cask/dsl/postflight"
require "cask/dsl/preflight"
require "cask/dsl/uninstall_postflight"
require "cask/dsl/uninstall_preflight"
require "cask/dsl/version"
require "cask/url"
require "cask/utils"
require "extend/on_system"
module Cask
# Class representing the domain-specific language used for casks.
#
# @api private
class DSL
ORDINARY_ARTIFACT_CLASSES = [
Artifact::Installer,
Artifact::App,
Artifact::Artifact,
Artifact::AudioUnitPlugin,
Artifact::Binary,
Artifact::Colorpicker,
Artifact::Dictionary,
Artifact::Font,
Artifact::InputMethod,
Artifact::InternetPlugin,
Artifact::Manpage,
Artifact::Pkg,
Artifact::Prefpane,
Artifact::Qlplugin,
Artifact::Mdimporter,
Artifact::ScreenSaver,
Artifact::Service,
Artifact::StageOnly,
Artifact::Suite,
Artifact::VstPlugin,
Artifact::Vst3Plugin,
Artifact::Uninstall,
Artifact::Zap,
].freeze
ACTIVATABLE_ARTIFACT_CLASSES = (ORDINARY_ARTIFACT_CLASSES - [Artifact::StageOnly]).freeze
ARTIFACT_BLOCK_CLASSES = [
Artifact::PreflightBlock,
Artifact::PostflightBlock,
].freeze
DSL_METHODS = Set.new([
:appcast,
:artifacts,
:auto_updates,
:caveats,
:conflicts_with,
:container,
:desc,
:depends_on,
:homepage,
:language,
:languages,
:name,
:sha256,
:staged_path,
:url,
:version,
:appdir,
:discontinued?,
:livecheck,
:livecheckable?,
:on_system_blocks_exist?,
*ORDINARY_ARTIFACT_CLASSES.map(&:dsl_key),
*ACTIVATABLE_ARTIFACT_CLASSES.map(&:dsl_key),
*ARTIFACT_BLOCK_CLASSES.flat_map { |klass| [klass.dsl_key, klass.uninstall_dsl_key] },
]).freeze
extend Predicable
include OnSystem::MacOSOnly
attr_reader :cask, :token
attr_predicate :on_system_blocks_exist?
def initialize(cask)
@cask = cask
@token = cask.token
end
# @api public
def name(*args)
@name ||= []
return @name if args.empty?
@name.concat(args.flatten)
end
# @api public
def desc(description = nil)
set_unique_stanza(:desc, description.nil?) { description }
end
def set_unique_stanza(stanza, should_return)
return instance_variable_get("@#{stanza}") if should_return
unless @cask.allow_reassignment
if instance_variable_defined?("@#{stanza}") && !@called_in_on_system_block
raise CaskInvalidError.new(cask, "'#{stanza}' stanza may only appear once.")
end
if instance_variable_defined?("@#{stanza}_set_in_block") && @called_in_on_system_block
raise CaskInvalidError.new(cask, "'#{stanza}' stanza may only be overridden once.")
end
end
instance_variable_set("@#{stanza}_set_in_block", true) if @called_in_on_system_block
instance_variable_set("@#{stanza}", yield)
rescue CaskInvalidError
raise
rescue => e
raise CaskInvalidError.new(cask, "'#{stanza}' stanza failed with: #{e}")
end
# @api public
def homepage(homepage = nil)
set_unique_stanza(:homepage, homepage.nil?) { homepage }
end
def language(*args, default: false, &block)
if args.empty?
language_eval
elsif block
@language_blocks ||= {}
@language_blocks[args] = block
return unless default
if [email protected]_reassignment && @language_blocks.default.present?
raise CaskInvalidError.new(cask, "Only one default language may be defined.")
end
@language_blocks.default = block
else
raise CaskInvalidError.new(cask, "No block given to language stanza.")
end
end
def language_eval
return @language_eval if defined?(@language_eval)
return @language_eval = nil if @language_blocks.blank?
raise CaskInvalidError.new(cask, "No default language specified.") if @language_blocks.default.nil?
locales = cask.config.languages
.map do |language|
Locale.parse(language)
rescue Locale::ParserError
nil
end
.compact
locales.each do |locale|
key = locale.detect(@language_blocks.keys)
next if key.nil?
return @language_eval = @language_blocks[key].call
end
@language_eval = @language_blocks.default.call
end
def languages
return [] if @language_blocks.nil?
@language_blocks.keys.flatten
end
# @api public
def url(*args, **options, &block)
caller_location = caller_locations[0]
set_unique_stanza(:url, args.empty? && options.empty? && !block) do
if block
URL.new(*args, **options, caller_location: caller_location, dsl: self, &block)
else
URL.new(*args, **options, caller_location: caller_location)
end
end
end
# @api public
def appcast(*args, **kwargs)
set_unique_stanza(:appcast, args.empty? && kwargs.empty?) { DSL::Appcast.new(*args, **kwargs) }
end
# @api public
def container(**kwargs)
set_unique_stanza(:container, kwargs.empty?) do
DSL::Container.new(**kwargs)
end
end
# @api public
def version(arg = nil)
set_unique_stanza(:version, arg.nil?) do
if !arg.is_a?(String) && arg != :latest
raise CaskInvalidError.new(cask, "invalid 'version' value: #{arg.inspect}")
end
DSL::Version.new(arg)
end
end
# @api public
def sha256(arg = nil, arm: nil, intel: nil)
should_return = arg.nil? && arm.nil? && intel.nil?
set_unique_stanza(:sha256, should_return) do
@on_system_blocks_exist = true if arm.present? || intel.present?
arg ||= on_arch_conditional(arm: arm, intel: intel)
case arg
when :no_check
arg
when String
Checksum.new(arg)
else
raise CaskInvalidError.new(cask, "invalid 'sha256' value: #{arg.inspect}")
end
end
end
# @api public
def arch(arm: nil, intel: nil)
should_return = arm.nil? && intel.nil?
set_unique_stanza(:arch, should_return) do
@on_system_blocks_exist = true
on_arch_conditional(arm: arm, intel: intel)
end
end
# `depends_on` uses a load method so that multiple stanzas can be merged.
# @api public
def depends_on(**kwargs)
@depends_on ||= DSL::DependsOn.new
return @depends_on if kwargs.empty?
begin
@depends_on.load(**kwargs)
rescue RuntimeError => e
raise CaskInvalidError.new(cask, e)
end
@depends_on
end
# @api public
def conflicts_with(**kwargs)
# TODO: remove this constraint, and instead merge multiple conflicts_with stanzas
set_unique_stanza(:conflicts_with, kwargs.empty?) { DSL::ConflictsWith.new(**kwargs) }
end
def artifacts
@artifacts ||= ArtifactSet.new
end
def caskroom_path
cask.caskroom_path
end
# @api public
def staged_path
return @staged_path if @staged_path
cask_version = version || :unknown
@staged_path = caskroom_path.join(cask_version.to_s)
end
# @api public
def caveats(*strings, &block)
@caveats ||= DSL::Caveats.new(cask)
if block
@caveats.eval_caveats(&block)
elsif strings.any?
strings.each do |string|
@caveats.eval_caveats { string }
end
else
return @caveats.to_s
end
@caveats
end
def discontinued?
@caveats&.discontinued? == true
end
# @api public
def auto_updates(auto_updates = nil)
set_unique_stanza(:auto_updates, auto_updates.nil?) { auto_updates }
end
# @api public
def livecheck(&block)
@livecheck ||= Livecheck.new(self)
return @livecheck unless block
if [email protected]_reassignment && @livecheckable
raise CaskInvalidError.new(cask, "'livecheck' stanza may only appear once.")
end
@livecheckable = true
@livecheck.instance_eval(&block)
end
def livecheckable?
@livecheckable == true
end
ORDINARY_ARTIFACT_CLASSES.each do |klass|
define_method(klass.dsl_key) do |*args, **kwargs|
if [*artifacts.map(&:class), klass].include?(Artifact::StageOnly) &&
(artifacts.map(&:class) & ACTIVATABLE_ARTIFACT_CLASSES).any?
raise CaskInvalidError.new(cask, "'stage_only' must be the only activatable artifact.")
end
artifacts.add(klass.from_args(cask, *args, **kwargs))
rescue CaskInvalidError
raise
rescue => e
raise CaskInvalidError.new(cask, "invalid '#{klass.dsl_key}' stanza: #{e}")
end
end
ARTIFACT_BLOCK_CLASSES.each do |klass|
[klass.dsl_key, klass.uninstall_dsl_key].each do |dsl_key|
define_method(dsl_key) do |&block|
artifacts.add(klass.new(cask, dsl_key => block))
end
end
end
def method_missing(method, *)
if method
Utils.method_missing_message(method, token)
nil
else
super
end
end
def respond_to_missing?(*)
true
end
# @api public
def appdir
cask.config.appdir
end
end
end
# typed: true
# frozen_string_literal: true
module Cask
class DSL
# Class corresponding to the `appcast` stanza.
#
# @api private
class Appcast
attr_reader :uri, :parameters, :must_contain
def initialize(uri, **parameters)
@uri = URI(uri)
@parameters = parameters
@must_contain = parameters[:must_contain] if parameters.key?(:must_contain)
end
def to_yaml
[uri, parameters].to_yaml
end
def to_s
uri.to_s
end
end
end
end
# typed: false
# frozen_string_literal: true
require "cask/utils"
module Cask
class DSL
# Superclass for all stanzas which take a block.
#
# @api private
class Base
extend Forwardable
def initialize(cask, command = SystemCommand)
@cask = cask
@command = command
end
def_delegators :@cask, :token, :version, :caskroom_path, :staged_path, :appdir, :language
def system_command(executable, **options)
@command.run!(executable, **options)
end
# No need to define it as it's the default/superclass implementation.
# rubocop:disable Style/MissingRespondToMissing
def method_missing(method, *)
if method
underscored_class = self.class.name.gsub(/([[:lower:]])([[:upper:]][[:lower:]])/, '\1_\2').downcase
section = underscored_class.split("::").last
Utils.method_missing_message(method, @cask.to_s, section)
nil
else
super
end
end
# rubocop:enable Style/MissingRespondToMissing
end
end
end
# typed: false
# frozen_string_literal: true
module Cask
class DSL
# Class corresponding to the `caveats` stanza.
#
# Each method should handle output, following the
# convention of at least one trailing blank line so that the user
# can distinguish separate caveats.
#
# The return value of the last method in the block is also sent
# to the output by the caller, but that feature is only for the
# convenience of cask authors.
#
# @api private
class Caveats < Base
extend Predicable
attr_predicate :discontinued?
def initialize(*args)
super(*args)
@built_in_caveats = {}
@custom_caveats = []
@discontinued = false
end
def self.caveat(name, &block)
define_method(name) do |*args|
key = [name, *args]
text = instance_exec(*args, &block)
@built_in_caveats[key] = text if text
:built_in_caveat
end
end
private_class_method :caveat
def to_s
(@custom_caveats + @built_in_caveats.values).join("\n")
end
# Override `puts` to collect caveats.
def puts(*args)
@custom_caveats += args
:built_in_caveat
end
def eval_caveats(&block)
result = instance_eval(&block)
return unless result
return if result == :built_in_caveat
@custom_caveats << result.to_s.sub(/\s*\Z/, "\n")
end
caveat :kext do
next if MacOS.version < :high_sierra
<<~EOS
#{@cask} requires a kernel extension to work.
If the installation fails, retry after you enable it in:
System Preferences → Security & Privacy → General
For more information, refer to vendor documentation or this Apple Technical Note:
#{Formatter.url("https://developer.apple.com/library/content/technotes/tn2459/_index.html")}
EOS
end
caveat :unsigned_accessibility do |access = "Accessibility"|
# access: the category in the privacy settings the app requires.
navigation_path = if MacOS.version < :ventura
"System Preferences → Security & Privacy → Privacy"
else
"System Settings → Privacy & Security"
end
<<~EOS
#{@cask} is not signed and requires Accessibility access,
so you will need to re-grant Accessibility access every time the app is updated.
Enable or re-enable it in:
#{navigation_path} → #{access}
To re-enable, untick and retick #{@cask}.app.
EOS
end
caveat :path_environment_variable do |path|
<<~EOS
To use #{@cask}, you may need to add the #{path} directory
to your PATH environment variable, e.g. (for Bash shell):
export PATH=#{path}:"$PATH"
EOS
end
caveat :zsh_path_helper do |path|
<<~EOS
To use #{@cask}, zsh users may need to add the following line to their
~/.zprofile. (Among other effects, #{path} will be added to the
PATH environment variable):
eval `/usr/libexec/path_helper -s`
EOS
end
caveat :files_in_usr_local do
next unless HOMEBREW_PREFIX.to_s.downcase.start_with?("/usr/local")
<<~EOS
Cask #{@cask} installs files under /usr/local. The presence of such
files can cause warnings when running `brew doctor`, which is considered
to be a bug in Homebrew Cask.
EOS
end
caveat :depends_on_java do |java_version = :any|
if java_version == :any
<<~EOS
#{@cask} requires Java. You can install the latest version with:
brew install --cask temurin
EOS
elsif java_version.include?("+")
<<~EOS
#{@cask} requires Java #{java_version}. You can install the latest version with:
brew install --cask temurin
EOS
else
<<~EOS
#{@cask} requires Java #{java_version}. You can install it with:
brew install --cask homebrew/cask-versions/temurin#{java_version}
EOS
end
end
caveat :requires_rosetta do
next unless Hardware::CPU.arm?
<<~EOS
#{@cask} is built for Intel macOS and so requires Rosetta 2 to be installed.
You can install Rosetta 2 with:
softwareupdate --install-rosetta --agree-to-license
Note that it is very difficult to remove Rosetta 2 once it is installed.
EOS
end
caveat :logout do
<<~EOS
You must log out and log back in for the installation of #{@cask} to take effect.
EOS
end
caveat :reboot do
<<~EOS
You must reboot for the installation of #{@cask} to take effect.
EOS
end
caveat :discontinued do
@discontinued = true
<<~EOS
#{@cask} has been officially discontinued upstream.
It may stop working correctly (or at all) in recent versions of macOS.
EOS
end
caveat :license do |web_page|
<<~EOS
Installing #{@cask} means you have AGREED to the license at:
#{Formatter.url(web_page.to_s)}
EOS
end
caveat :free_license do |web_page|
<<~EOS
The vendor offers a free license for #{@cask} at:
#{Formatter.url(web_page.to_s)}
EOS
end
end
end
end
# typed: false
# frozen_string_literal: true
require "delegate"
require "extend/hash_validator"
using HashValidator
module Cask
class DSL
# Class corresponding to the `conflicts_with` stanza.
#
# @api private
class ConflictsWith < SimpleDelegator
VALID_KEYS = [
:formula,
:cask,
:macos,
:arch,
:x11,
:java,
].freeze
def initialize(**options)
options.assert_valid_keys!(*VALID_KEYS)
conflicts = options.transform_values { |v| Set.new(Array(v)) }
conflicts.default = Set.new
super(conflicts)
end
def to_json(generator)
transform_values(&:to_a).to_json(generator)
end
end
end
end
# typed: true
# frozen_string_literal: true
require "unpack_strategy"
module Cask
class DSL
# Class corresponding to the `container` stanza.
#
# @api private
class Container
attr_accessor :nested, :type
def initialize(nested: nil, type: nil)
@nested = nested
@type = type
return if type.nil?
return unless UnpackStrategy.from_type(type).nil?
raise "invalid container type: #{type.inspect}"
end
def pairs
instance_variables.to_h { |ivar| [ivar[1..].to_sym, instance_variable_get(ivar)] }.compact
end
def to_yaml
pairs.to_yaml
end
def to_s
pairs.inspect
end
end
end
end
# typed: false
# frozen_string_literal: true
require "delegate"
require "requirements/macos_requirement"
module Cask
class DSL
# Class corresponding to the `depends_on` stanza.
#
# @api private
class DependsOn < SimpleDelegator
extend T::Sig
VALID_KEYS = Set.new([
:formula,
:cask,
:macos,
:arch,
]).freeze
VALID_ARCHES = {
intel: { type: :intel, bits: 64 },
# specific
x86_64: { type: :intel, bits: 64 },
arm64: { type: :arm, bits: 64 },
}.freeze
attr_reader :arch, :cask, :formula, :macos
def initialize
super({})
@cask ||= []
@formula ||= []
end
def load(**pairs)
pairs.each do |key, value|
raise "invalid depends_on key: '#{key.inspect}'" unless VALID_KEYS.include?(key)
self[key] = send(:"#{key}=", *value)
end
end
def formula=(*args)
@formula.concat(args)
end
def cask=(*args)
@cask.concat(args)
end
sig { params(args: T.any(String, Symbol)).returns(T.nilable(MacOSRequirement)) }
def macos=(*args)
raise "Only a single 'depends_on macos' is allowed." if defined?(@macos)
begin
@macos = if args.count > 1
MacOSRequirement.new([args], comparator: "==")
elsif MacOSVersions::SYMBOLS.key?(args.first)
MacOSRequirement.new([args.first], comparator: "==")
elsif /^\s*(?<comparator><|>|[=<>]=)\s*:(?<version>\S+)\s*$/ =~ args.first
MacOSRequirement.new([version.to_sym], comparator: comparator)
elsif /^\s*(?<comparator><|>|[=<>]=)\s*(?<version>\S+)\s*$/ =~ args.first
MacOSRequirement.new([version], comparator: comparator)
else # rubocop:disable Lint/DuplicateBranch
MacOSRequirement.new([args.first], comparator: "==")
end
rescue MacOSVersionError => e
raise "invalid 'depends_on macos' value: #{e}"
end
end
def arch=(*args)
@arch ||= []
arches = args.map do |elt|
elt.to_s.downcase.sub(/^:/, "").tr("-", "_").to_sym
end
invalid_arches = arches - VALID_ARCHES.keys
raise "invalid 'depends_on arch' values: #{invalid_arches.inspect}" unless invalid_arches.empty?
@arch.concat(arches.map { |arch| VALID_ARCHES[arch] })
end
end
end
end
# typed: true
# frozen_string_literal: true
require "cask/staged"
module Cask
class DSL
# Class corresponding to the `postflight` stanza.
#
# @api private
class Postflight < Base
include Staged
def suppress_move_to_applications(options = {})
# TODO: Remove from all casks because it is no longer needed
end
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/staged"
module Cask
class DSL
# Class corresponding to the `preflight` stanza.
#
# @api private
class Preflight < Base
include Staged
end
end
end
# typed: strict
# frozen_string_literal: true
module Cask
class DSL
# Class corresponding to the `uninstall_postflight` stanza.
#
# @api private
class UninstallPostflight < Base
end
end
end
# typed: strict
# frozen_string_literal: true
require "cask/staged"
module Cask
class DSL
# Class corresponding to the `uninstall_preflight` stanza.
#
# @api private
class UninstallPreflight < Base
include Staged
end
end
end
# typed: true
# frozen_string_literal: true
module Cask
class DSL
# Class corresponding to the `version` stanza.
#
# @api private
class Version < ::String
extend T::Sig
DIVIDERS = {
"." => :dots,
"-" => :hyphens,
"_" => :underscores,
}.freeze
DIVIDER_REGEX = /(#{DIVIDERS.keys.map { |v| Regexp.quote(v) }.join('|')})/.freeze
MAJOR_MINOR_PATCH_REGEX = /^([^.,:]+)(?:.([^.,:]+)(?:.([^.,:]+))?)?/.freeze
INVALID_CHARACTERS = /[^0-9a-zA-Z.,:\-_+ ]/.freeze
class << self
private
def define_divider_methods(divider)
define_divider_deletion_method(divider)
define_divider_conversion_methods(divider)
end
def define_divider_deletion_method(divider)
method_name = deletion_method_name(divider)
define_method(method_name) do
version { delete(divider) }
end
end
def deletion_method_name(divider)
"no_#{DIVIDERS[divider]}"
end
def define_divider_conversion_methods(left_divider)
(DIVIDERS.keys - [left_divider]).each do |right_divider|
define_divider_conversion_method(left_divider, right_divider)
end
end
def define_divider_conversion_method(left_divider, right_divider)
method_name = conversion_method_name(left_divider, right_divider)
define_method(method_name) do
version { gsub(left_divider, right_divider) }
end
end
def conversion_method_name(left_divider, right_divider)
"#{DIVIDERS[left_divider]}_to_#{DIVIDERS[right_divider]}"
end
end
DIVIDERS.each_key do |divider|
define_divider_methods(divider)
end
attr_reader :raw_version
sig { params(raw_version: T.nilable(T.any(String, Symbol))).void }
def initialize(raw_version)
@raw_version = raw_version
super(raw_version.to_s)
invalid = invalid_characters
raise TypeError, "#{raw_version} contains invalid characters: #{invalid.uniq.join}!" if invalid.present?
end
def invalid_characters
return [] if raw_version.blank? || latest?
raw_version.scan(INVALID_CHARACTERS)
end
sig { returns(T::Boolean) }
def unstable?
return false if latest?
s = downcase.delete(".").gsub(/[^a-z\d]+/, "-")
return true if s.match?(/(\d+|\b)(alpha|beta|preview|rc|dev|canary|snapshot)(\d+|\b)/i)
return true if s.match?(/\A[a-z\d]+(-\d+)*-?(a|b|pre)(\d+|\b)/i)
false
end
sig { returns(T::Boolean) }
def latest?
to_s == "latest"
end
# @api public
sig { returns(T.self_type) }
def major
version { slice(MAJOR_MINOR_PATCH_REGEX, 1) }
end
# @api public
sig { returns(T.self_type) }
def minor
version { slice(MAJOR_MINOR_PATCH_REGEX, 2) }
end
# @api public
sig { returns(T.self_type) }
def patch
version { slice(MAJOR_MINOR_PATCH_REGEX, 3) }
end
# @api public
sig { returns(T.self_type) }
def major_minor
version { [major, minor].reject(&:empty?).join(".") }
end
# @api public
sig { returns(T.self_type) }
def major_minor_patch
version { [major, minor, patch].reject(&:empty?).join(".") }
end
# @api public
sig { returns(T.self_type) }
def minor_patch
version { [minor, patch].reject(&:empty?).join(".") }
end
# @api public
sig { returns(T::Array[Version]) } # Only top-level T.self_type is supported https://sorbet.org/docs/self-type
def csv
split(",").map(&self.class.method(:new))
end
# @api public
sig { returns(T.self_type) }
def before_comma
version { split(",", 2).first }
end
# @api public
sig { returns(T.self_type) }
def after_comma
version { split(",", 2).second }
end
# @api public
sig { returns(T.self_type) }
def no_dividers
version { gsub(DIVIDER_REGEX, "") }
end
# @api public
sig { params(separator: T.nilable(String)).returns(T.self_type) }
def chomp(separator = nil)
version { to_s.chomp(T.unsafe(separator)) }
end
private
sig { returns(T.self_type) }
def version
return self if empty? || latest?
self.class.new(yield)
end
end
end
end
# typed: true
# frozen_string_literal: true
module Cask
# General cask error.
#
# @api private
class CaskError < RuntimeError; end
# Cask error containing multiple other errors.
#
# @api private
class MultipleCaskErrors < CaskError
extend T::Sig
def initialize(errors)
super()
@errors = errors
end
sig { returns(String) }
def to_s
<<~EOS
Problems with multiple casks:
#{@errors.map(&:to_s).join("\n")}
EOS
end
end
# Abstract cask error containing a cask token.
#
# @api private
class AbstractCaskErrorWithToken < CaskError
extend T::Sig
sig { returns(String) }
attr_reader :token
sig { returns(String) }
attr_reader :reason
def initialize(token, reason = nil)
super()
@token = token.to_s
@reason = reason.to_s
end
end
# Error when a cask is not installed.
#
# @api private
class CaskNotInstalledError < AbstractCaskErrorWithToken
extend T::Sig
sig { returns(String) }
def to_s
"Cask '#{token}' is not installed."
end
end
# Error when a cask conflicts with another cask.
#
# @api private
class CaskConflictError < AbstractCaskErrorWithToken
extend T::Sig
attr_reader :conflicting_cask
def initialize(token, conflicting_cask)
super(token)
@conflicting_cask = conflicting_cask
end
sig { returns(String) }
def to_s
"Cask '#{token}' conflicts with '#{conflicting_cask}'."
end
end
# Error when a cask is not available.
#
# @api private
class CaskUnavailableError < AbstractCaskErrorWithToken
extend T::Sig
sig { returns(String) }
def to_s
"Cask '#{token}' is unavailable#{reason.empty? ? "." : ": #{reason}"}"
end
end
# Error when a cask is unreadable.
#
# @api private
class CaskUnreadableError < CaskUnavailableError
extend T::Sig
sig { returns(String) }
def to_s
"Cask '#{token}' is unreadable#{reason.empty? ? "." : ": #{reason}"}"
end
end
# Error when a cask in a specific tap is not available.
#
# @api private
class TapCaskUnavailableError < CaskUnavailableError
extend T::Sig
attr_reader :tap
def initialize(tap, token)
super("#{tap}/#{token}")
@tap = tap
end
sig { returns(String) }
def to_s
s = super
s += "\nPlease tap it and then try again: brew tap #{tap}" unless tap.installed?
s
end
end
# Error when a cask with the same name is found in multiple taps.
#
# @api private
class TapCaskAmbiguityError < CaskError
extend T::Sig
def initialize(ref, loaders)
super <<~EOS
Cask #{ref} exists in multiple taps:
#{loaders.map { |loader| " #{loader.tap}/#{loader.token}" }.join("\n")}
EOS
end
end
# Error when a cask already exists.
#
# @api private
class CaskAlreadyCreatedError < AbstractCaskErrorWithToken
extend T::Sig
sig { returns(String) }
def to_s
%Q(Cask '#{token}' already exists. Run #{Formatter.identifier("brew edit --cask #{token}")} to edit it.)
end
end
# Error when a cask is already installed.
#
# @api private
class CaskAlreadyInstalledError < AbstractCaskErrorWithToken
extend T::Sig
sig { returns(String) }
def to_s
<<~EOS
Cask '#{token}' is already installed.
To re-install #{token}, run:
#{Formatter.identifier("brew reinstall --cask #{token}")}
EOS
end
end
# Error when there is a cyclic cask dependency.
#
# @api private
class CaskCyclicDependencyError < AbstractCaskErrorWithToken
extend T::Sig
sig { returns(String) }
def to_s
"Cask '#{token}' includes cyclic dependencies on other Casks#{reason.empty? ? "." : ": #{reason}"}"
end
end
# Error when a cask depends on itself.
#
# @api private
class CaskSelfReferencingDependencyError < CaskCyclicDependencyError
extend T::Sig
sig { returns(String) }
def to_s
"Cask '#{token}' depends on itself."
end
end
# Error when no cask is specified.
#
# @api private
class CaskUnspecifiedError < CaskError
extend T::Sig
sig { returns(String) }
def to_s
"This command requires a Cask token."
end
end
# Error when a cask is invalid.
#
# @api private
class CaskInvalidError < AbstractCaskErrorWithToken
extend T::Sig
sig { returns(String) }
def to_s
"Cask '#{token}' definition is invalid#{reason.empty? ? "." : ": #{reason}"}"
end
end
# Error when a cask token does not match the file name.
#
# @api private
class CaskTokenMismatchError < CaskInvalidError
def initialize(token, header_token)
super(token, "Token '#{header_token}' in header line does not match the file name.")
end
end
# Error during quarantining of a file.
#
# @api private
class CaskQuarantineError < CaskError
extend T::Sig
attr_reader :path, :reason
def initialize(path, reason)
super()
@path = path
@reason = reason
end
sig { returns(String) }
def to_s
s = +"Failed to quarantine #{path}."
unless reason.empty?
s << " Here's the reason:\n"
s << Formatter.error(reason)
s << "\n" unless reason.end_with?("\n")
end
s.freeze
end
end
# Error while propagating quarantine information to subdirectories.
#
# @api private
class CaskQuarantinePropagationError < CaskQuarantineError
extend T::Sig
sig { returns(String) }
def to_s
s = +"Failed to quarantine one or more files within #{path}."
unless reason.empty?
s << " Here's the reason:\n"
s << Formatter.error(reason)
s << "\n" unless reason.end_with?("\n")
end
s.freeze
end
end
# Error while removing quarantine information.
#
# @api private
class CaskQuarantineReleaseError < CaskQuarantineError
extend T::Sig
sig { returns(String) }
def to_s
s = +"Failed to release #{path} from quarantine."
unless reason.empty?
s << " Here's the reason:\n"
s << Formatter.error(reason)
s << "\n" unless reason.end_with?("\n")
end
s.freeze
end
end
end
# typed: false
# frozen_string_literal: true
require "formula_installer"
require "unpack_strategy"
require "utils/topological_hash"
require "cask/config"
require "cask/download"
require "cask/quarantine"
require "cgi"
module Cask
# Installer for a {Cask}.
#
# @api private
class Installer
extend T::Sig
extend Predicable
def initialize(cask, command: SystemCommand, force: false, adopt: false,
skip_cask_deps: false, binaries: true, verbose: false,
zap: false, require_sha: false, upgrade: false,
installed_as_dependency: false, quarantine: true,
verify_download_integrity: true, quiet: false)
@cask = cask
@command = command
@force = force
@adopt = adopt
@skip_cask_deps = skip_cask_deps
@binaries = binaries
@verbose = verbose
@zap = zap
@require_sha = require_sha
@reinstall = false
@upgrade = upgrade
@installed_as_dependency = installed_as_dependency
@quarantine = quarantine
@verify_download_integrity = verify_download_integrity
@quiet = quiet
end
attr_predicate :binaries?, :force?, :adopt?, :skip_cask_deps?, :require_sha?,
:reinstall?, :upgrade?, :verbose?, :zap?, :installed_as_dependency?,
:quarantine?, :quiet?
def self.caveats(cask)
odebug "Printing caveats"
caveats = cask.caveats
return if caveats.empty?
Homebrew.messages.record_caveats(cask.token, caveats)
<<~EOS
#{ohai_title "Caveats"}
#{caveats}
EOS
end
sig { params(quiet: T.nilable(T::Boolean), timeout: T.nilable(T.any(Integer, Float))).void }
def fetch(quiet: nil, timeout: nil)
odebug "Cask::Installer#fetch"
verify_has_sha if require_sha? && !force?
download(quiet: quiet, timeout: timeout)
satisfy_dependencies
end
def stage
odebug "Cask::Installer#stage"
Caskroom.ensure_caskroom_exists
extract_primary_container
save_caskfile
rescue => e
purge_versioned_files
raise e
end
def install
start_time = Time.now
odebug "Cask::Installer#install"
old_config = @cask.config
if @cask.installed? && !force? && !reinstall? && !upgrade?
return if quiet?
raise CaskAlreadyInstalledError, @cask
end
check_conflicts
print caveats
fetch
uninstall_existing_cask if reinstall?
backup if force? && @cask.staged_path.exist? && @cask.metadata_versioned_path.exist?
oh1 "Installing Cask #{Formatter.identifier(@cask)}"
opoo "macOS's Gatekeeper has been disabled for this Cask" unless quarantine?
stage
@cask.config = @cask.default_config.merge(old_config)
install_artifacts
::Utils::Analytics.report_event("cask_install", @cask.token, on_request: true) unless @cask.tap&.private?
purge_backed_up_versioned_files
puts summary
end_time = Time.now
Homebrew.messages.package_installed(@cask.token, end_time - start_time)
rescue
restore_backup
raise
end
def check_conflicts
return unless @cask.conflicts_with
@cask.conflicts_with[:cask].each do |conflicting_cask|
if (match = conflicting_cask.match(HOMEBREW_TAP_CASK_REGEX))
conflicting_cask_tap = Tap.fetch(match[1], match[2])
next unless conflicting_cask_tap.installed?
end
conflicting_cask = CaskLoader.load(conflicting_cask)
raise CaskConflictError.new(@cask, conflicting_cask) if conflicting_cask.installed?
rescue CaskUnavailableError
next # Ignore conflicting Casks that do not exist.
end
end
def reinstall
odebug "Cask::Installer#reinstall"
@reinstall = true
install
end
def uninstall_existing_cask
return unless @cask.installed?
# use the same cask file that was used for installation, if possible
installed_caskfile = @cask.installed_caskfile
installed_cask = begin
installed_caskfile.exist? ? CaskLoader.load(installed_caskfile) : @cask
rescue CaskInvalidError # could be thrown by call to CaskLoader#load with outdated caskfile
@cask # default
end
# Always force uninstallation, ignore method parameter
cask_installer = Installer.new(installed_cask, verbose: verbose?, force: true, upgrade: upgrade?)
zap? ? cask_installer.zap : cask_installer.uninstall
end
sig { returns(String) }
def summary
s = +""
s << "#{Homebrew::EnvConfig.install_badge} " unless Homebrew::EnvConfig.no_emoji?
s << "#{@cask} was successfully #{upgrade? ? "upgraded" : "installed"}!"
s.freeze
end
sig { returns(Download) }
def downloader
@downloader ||= Download.new(@cask, quarantine: quarantine?)
end
sig { params(quiet: T.nilable(T::Boolean), timeout: T.nilable(T.any(Integer, Float))).returns(Pathname) }
def download(quiet: nil, timeout: nil)
# Store cask download path in cask to prevent multiple downloads in a row when checking if it's outdated
@cask.download ||= downloader.fetch(quiet: quiet, verify_download_integrity: @verify_download_integrity,
timeout: timeout)
end
def verify_has_sha
odebug "Checking cask has checksum"
return unless @cask.sha256 == :no_check
raise CaskError, <<~EOS
Cask '#{@cask}' does not have a sha256 checksum defined and was not installed.
This means you have the #{Formatter.identifier("--require-sha")} option set, perhaps in your HOMEBREW_CASK_OPTS.
EOS
end
def primary_container
@primary_container ||= begin
downloaded_path = download(quiet: true)
UnpackStrategy.detect(downloaded_path, type: @cask.container&.type, merge_xattrs: true)
end
end
def extract_primary_container(to: @cask.staged_path)
odebug "Extracting primary container"
odebug "Using container class #{primary_container.class} for #{primary_container.path}"
basename = downloader.basename
if (nested_container = @cask.container&.nested)
Dir.mktmpdir do |tmpdir|
tmpdir = Pathname(tmpdir)
primary_container.extract(to: tmpdir, basename: basename, verbose: verbose?)
FileUtils.chmod_R "+rw", tmpdir/nested_container, force: true, verbose: verbose?
UnpackStrategy.detect(tmpdir/nested_container, merge_xattrs: true)
.extract_nestedly(to: to, verbose: verbose?)
end
else
primary_container.extract_nestedly(to: to, basename: basename, verbose: verbose?)
end
return unless quarantine?
return unless Quarantine.available?
Quarantine.propagate(from: primary_container.path, to: to)
end
def install_artifacts
artifacts = @cask.artifacts
already_installed_artifacts = []
odebug "Installing artifacts"
odebug "#{artifacts.length} #{"artifact".pluralize(artifacts.length)} defined", artifacts
artifacts.each do |artifact|
next unless artifact.respond_to?(:install_phase)
odebug "Installing artifact of class #{artifact.class}"
next if artifact.is_a?(Artifact::Binary) && !binaries?
artifact.install_phase(command: @command, verbose: verbose?, adopt: adopt?, force: force?)
already_installed_artifacts.unshift(artifact)
end
save_config_file
save_download_sha if @cask.version.latest?
rescue => e
begin
already_installed_artifacts.each do |artifact|
if artifact.respond_to?(:uninstall_phase)
odebug "Reverting installation of artifact of class #{artifact.class}"
artifact.uninstall_phase(command: @command, verbose: verbose?, force: force?)
end
next unless artifact.respond_to?(:post_uninstall_phase)
odebug "Reverting installation of artifact of class #{artifact.class}"
artifact.post_uninstall_phase(command: @command, verbose: verbose?, force: force?)
end
ensure
purge_versioned_files
raise e
end
end
# TODO: move dependencies to a separate class,
# dependencies should also apply for `brew cask stage`,
# override dependencies with `--force` or perhaps `--force-deps`
def satisfy_dependencies
return unless @cask.depends_on
macos_dependencies
arch_dependencies
cask_and_formula_dependencies
end
def macos_dependencies
return unless @cask.depends_on.macos
return if @cask.depends_on.macos.satisfied?
raise CaskError, @cask.depends_on.macos.message(type: :cask)
end
def arch_dependencies
return if @cask.depends_on.arch.nil?
@current_arch ||= { type: Hardware::CPU.type, bits: Hardware::CPU.bits }
return if @cask.depends_on.arch.any? do |arch|
arch[:type] == @current_arch[:type] &&
Array(arch[:bits]).include?(@current_arch[:bits])
end
raise CaskError,
"Cask #{@cask} depends on hardware architecture being one of " \
"[#{@cask.depends_on.arch.map(&:to_s).join(", ")}], " \
"but you are running #{@current_arch}."
end
def collect_cask_and_formula_dependencies
return @cask_and_formula_dependencies if @cask_and_formula_dependencies
graph = ::Utils::TopologicalHash.graph_package_dependencies(@cask)
raise CaskSelfReferencingDependencyError, cask.token if graph[@cask].include?(@cask)
::Utils::TopologicalHash.graph_package_dependencies(primary_container.dependencies, graph)
begin
@cask_and_formula_dependencies = graph.tsort - [@cask]
rescue TSort::Cyclic
strongly_connected_components = graph.strongly_connected_components.sort_by(&:count)
cyclic_dependencies = strongly_connected_components.last - [@cask]
raise CaskCyclicDependencyError.new(@cask.token, cyclic_dependencies.to_sentence)
end
end
def missing_cask_and_formula_dependencies
collect_cask_and_formula_dependencies.reject do |cask_or_formula|
installed = if cask_or_formula.respond_to?(:any_version_installed?)
cask_or_formula.any_version_installed?
else
cask_or_formula.try(:installed?)
end
installed && (cask_or_formula.respond_to?(:optlinked?) ? cask_or_formula.optlinked? : true)
end
end
def cask_and_formula_dependencies
return if installed_as_dependency?
formulae_and_casks = collect_cask_and_formula_dependencies
return if formulae_and_casks.empty?
missing_formulae_and_casks = missing_cask_and_formula_dependencies
if missing_formulae_and_casks.empty?
puts "All formula dependencies satisfied."
return
end
ohai "Installing dependencies: #{missing_formulae_and_casks.map(&:to_s).join(", ")}"
missing_formulae_and_casks.each do |cask_or_formula|
if cask_or_formula.is_a?(Cask)
if skip_cask_deps?
opoo "`--skip-cask-deps` is set; skipping installation of #{cask_or_formula}."
next
end
Installer.new(
cask_or_formula,
adopt: adopt?,
binaries: binaries?,
verbose: verbose?,
installed_as_dependency: true,
force: false,
).install
else
fi = FormulaInstaller.new(
cask_or_formula,
**{
show_header: true,
installed_as_dependency: true,
installed_on_request: false,
verbose: verbose?,
}.compact,
)
fi.prelude
fi.fetch
fi.install
fi.finish
end
end
end
def caveats
self.class.caveats(@cask)
end
def metadata_subdir
@metadata_subdir ||= @cask.metadata_subdir("Casks", timestamp: :now, create: true)
end
def save_caskfile
old_savedir = @cask.metadata_timestamped_path
return if @cask.source.blank?
(metadata_subdir/"#{@cask.token}.rb").write @cask.source
old_savedir&.rmtree
end
def save_config_file
metadata_subdir
@cask.config_path.atomic_write(@cask.config.to_json)
end
def save_download_sha
@cask.download_sha_path.atomic_write(@cask.new_download_sha) if @cask.checksumable?
end
def uninstall
oh1 "Uninstalling Cask #{Formatter.identifier(@cask)}"
uninstall_artifacts(clear: true)
if !reinstall? && !upgrade?
remove_download_sha
remove_config_file
end
purge_versioned_files
purge_caskroom_path if force?
end
def remove_config_file
FileUtils.rm_f @cask.config_path
@cask.config_path.parent.rmdir_if_possible
end
def remove_download_sha
FileUtils.rm_f @cask.download_sha_path if @cask.download_sha_path.exist?
end
def start_upgrade
uninstall_artifacts
backup
end
def backup
@cask.staged_path.rename backup_path
@cask.metadata_versioned_path.rename backup_metadata_path
end
def restore_backup
return if !backup_path.directory? || !backup_metadata_path.directory?
Pathname.new(@cask.staged_path).rmtree if @cask.staged_path.exist?
Pathname.new(@cask.metadata_versioned_path).rmtree if @cask.metadata_versioned_path.exist?
backup_path.rename @cask.staged_path
backup_metadata_path.rename @cask.metadata_versioned_path
end
def revert_upgrade
opoo "Reverting upgrade for Cask #{@cask}"
restore_backup
install_artifacts
end
def finalize_upgrade
ohai "Purging files for version #{@cask.version} of Cask #{@cask}"
purge_backed_up_versioned_files
puts summary
end
def uninstall_artifacts(clear: false)
artifacts = @cask.artifacts
odebug "Uninstalling artifacts"
odebug "#{artifacts.length} #{"artifact".pluralize(artifacts.length)} defined", artifacts
artifacts.each do |artifact|
if artifact.respond_to?(:uninstall_phase)
odebug "Uninstalling artifact of class #{artifact.class}"
artifact.uninstall_phase(
command: @command, verbose: verbose?, skip: clear, force: force?, upgrade: upgrade?,
)
end
next unless artifact.respond_to?(:post_uninstall_phase)
odebug "Post-uninstalling artifact of class #{artifact.class}"
artifact.post_uninstall_phase(
command: @command, verbose: verbose?, skip: clear, force: force?, upgrade: upgrade?,
)
end
end
def zap
ohai "Implied `brew uninstall --cask #{@cask}`"
uninstall_artifacts
if (zap_stanzas = @cask.artifacts.select { |a| a.is_a?(Artifact::Zap) }).empty?
opoo "No zap stanza present for Cask '#{@cask}'"
else
ohai "Dispatching zap stanza"
zap_stanzas.each do |stanza|
stanza.zap_phase(command: @command, verbose: verbose?, force: force?)
end
end
ohai "Removing all staged versions of Cask '#{@cask}'"
purge_caskroom_path
end
def backup_path
return if @cask.staged_path.nil?
Pathname("#{@cask.staged_path}.upgrading")
end
def backup_metadata_path
return if @cask.metadata_versioned_path.nil?
Pathname("#{@cask.metadata_versioned_path}.upgrading")
end
def gain_permissions_remove(path)
Utils.gain_permissions_remove(path, command: @command)
end
def purge_backed_up_versioned_files
# versioned staged distribution
gain_permissions_remove(backup_path) if backup_path&.exist?
# Homebrew Cask metadata
return unless backup_metadata_path.directory?
backup_metadata_path.children.each do |subdir|
gain_permissions_remove(subdir)
end
backup_metadata_path.rmdir_if_possible
end
def purge_versioned_files
ohai "Purging files for version #{@cask.version} of Cask #{@cask}"
# versioned staged distribution
gain_permissions_remove(@cask.staged_path) if @cask.staged_path&.exist?
# Homebrew Cask metadata
if @cask.metadata_versioned_path.directory?
@cask.metadata_versioned_path.children.each do |subdir|
gain_permissions_remove(subdir)
end
@cask.metadata_versioned_path.rmdir_if_possible
end
@cask.metadata_main_container_path.rmdir_if_possible unless upgrade?
# toplevel staged distribution
@cask.caskroom_path.rmdir_if_possible unless upgrade?
end
def purge_caskroom_path
odebug "Purging all staged versions of Cask #{@cask}"
gain_permissions_remove(@cask.caskroom_path)
end
end
end
# typed: true
# frozen_string_literal: true
require "os/mac/version"
module OS
module Mac
module_function
SYSTEM_DIRS = [
"/",
"/Applications",
"/Applications/Utilities",
"/Incompatible Software",
"/Library",
"/Library/Application Support",
"/Library/Audio",
"/Library/Caches",
"/Library/ColorPickers",
"/Library/ColorSync",
"/Library/Components",
"/Library/Compositions",
"/Library/Contextual Menu Items",
"/Library/CoreMediaIO",
"/Library/Desktop Pictures",
"/Library/Developer",
"/Library/Dictionaries",
"/Library/DirectoryServices",
"/Library/Documentation",
"/Library/Extensions",
"/Library/Filesystems",
"/Library/Fonts",
"/Library/Frameworks",
"/Library/Graphics",
"/Library/Image Capture",
"/Library/Input Methods",
"/Library/Internet Plug-Ins",
"/Library/Java",
"/Library/Java/Extensions",
"/Library/Java/JavaVirtualMachines",
"/Library/Keyboard Layouts",
"/Library/Keychains",
"/Library/LaunchAgents",
"/Library/LaunchDaemons",
"/Library/Logs",
"/Library/Messages",
"/Library/Modem Scripts",
"/Library/OpenDirectory",
"/Library/PDF Services",
"/Library/Perl",
"/Library/PreferencePanes",
"/Library/Preferences",
"/Library/Printers",
"/Library/PrivilegedHelperTools",
"/Library/Python",
"/Library/QuickLook",
"/Library/QuickTime",
"/Library/Receipts",
"/Library/Ruby",
"/Library/Sandbox",
"/Library/Screen Savers",
"/Library/ScriptingAdditions",
"/Library/Scripts",
"/Library/Security",
"/Library/Speech",
"/Library/Spelling",
"/Library/Spotlight",
"/Library/StartupItems",
"/Library/SystemProfiler",
"/Library/Updates",
"/Library/User Pictures",
"/Library/Video",
"/Library/WebServer",
"/Library/Widgets",
"/Library/iTunes",
"/Network",
"/System",
"/System/Library",
"/System/Library/Accessibility",
"/System/Library/Accounts",
"/System/Library/Address Book Plug-Ins",
"/System/Library/Assistant",
"/System/Library/Automator",
"/System/Library/BridgeSupport",
"/System/Library/Caches",
"/System/Library/ColorPickers",
"/System/Library/ColorSync",
"/System/Library/Colors",
"/System/Library/Components",
"/System/Library/Compositions",
"/System/Library/CoreServices",
"/System/Library/DTDs",
"/System/Library/DirectoryServices",
"/System/Library/Displays",
"/System/Library/Extensions",
"/System/Library/Filesystems",
"/System/Library/Filters",
"/System/Library/Fonts",
"/System/Library/Frameworks",
"/System/Library/Graphics",
"/System/Library/IdentityServices",
"/System/Library/Image Capture",
"/System/Library/Input Methods",
"/System/Library/InternetAccounts",
"/System/Library/Java",
"/System/Library/KerberosPlugins",
"/System/Library/Keyboard Layouts",
"/System/Library/Keychains",
"/System/Library/LaunchAgents",
"/System/Library/LaunchDaemons",
"/System/Library/LinguisticData",
"/System/Library/LocationBundles",
"/System/Library/LoginPlugins",
"/System/Library/Messages",
"/System/Library/Metadata",
"/System/Library/MonitorPanels",
"/System/Library/OpenDirectory",
"/System/Library/OpenSSL",
"/System/Library/Password Server Filters",
"/System/Library/PerformanceMetrics",
"/System/Library/Perl",
"/System/Library/PreferencePanes",
"/System/Library/Printers",
"/System/Library/PrivateFrameworks",
"/System/Library/QuickLook",
"/System/Library/QuickTime",
"/System/Library/QuickTimeJava",
"/System/Library/Recents",
"/System/Library/SDKSettingsPlist",
"/System/Library/Sandbox",
"/System/Library/Screen Savers",
"/System/Library/ScreenReader",
"/System/Library/ScriptingAdditions",
"/System/Library/ScriptingDefinitions",
"/System/Library/Security",
"/System/Library/Services",
"/System/Library/Sounds",
"/System/Library/Speech",
"/System/Library/Spelling",
"/System/Library/Spotlight",
"/System/Library/StartupItems",
"/System/Library/SyncServices",
"/System/Library/SystemConfiguration",
"/System/Library/SystemProfiler",
"/System/Library/Tcl",
"/System/Library/TextEncodings",
"/System/Library/User Template",
"/System/Library/UserEventPlugins",
"/System/Library/Video",
"/System/Library/WidgetResources",
"/User Information",
"/Users",
"/Volumes",
"/bin",
"/boot",
"/cores",
"/dev",
"/etc",
"/etc/X11",
"/etc/opt",
"/etc/sgml",
"/etc/xml",
"/home",
"/libexec",
"/lost+found",
"/media",
"/mnt",
"/net",
"/opt",
"/private",
"/private/etc",
"/private/tftpboot",
"/private/tmp",
"/private/var",
"/proc",
"/root",
"/sbin",
"/srv",
"/tmp",
"/usr",
"/usr/X11R6",
"/usr/bin",
"/usr/etc",
"/usr/include",
"/usr/lib",
"/usr/libexec",
"/usr/libexec/cups",
"/usr/local",
"/usr/local/Cellar",
"/usr/local/Frameworks",
"/usr/local/Library",
"/usr/local/bin",
"/usr/local/etc",
"/usr/local/include",
"/usr/local/lib",
"/usr/local/libexec",
"/usr/local/opt",
"/usr/local/share",
"/usr/local/share/man",
"/usr/local/share/man/man1",
"/usr/local/share/man/man2",
"/usr/local/share/man/man3",
"/usr/local/share/man/man4",
"/usr/local/share/man/man5",
"/usr/local/share/man/man6",
"/usr/local/share/man/man7",
"/usr/local/share/man/man8",
"/usr/local/share/man/man9",
"/usr/local/share/man/mann",
"/usr/local/var",
"/usr/local/var/lib",
"/usr/local/var/lock",
"/usr/local/var/run",
"/usr/sbin",
"/usr/share",
"/usr/share/man",
"/usr/share/man/man1",
"/usr/share/man/man2",
"/usr/share/man/man3",
"/usr/share/man/man4",
"/usr/share/man/man5",
"/usr/share/man/man6",
"/usr/share/man/man7",
"/usr/share/man/man8",
"/usr/share/man/man9",
"/usr/share/man/mann",
"/usr/src",
"/var",
"/var/cache",
"/var/lib",
"/var/lock",
"/var/log",
"/var/mail",
"/var/run",
"/var/spool",
"/var/spool/mail",
"/var/tmp",
]
.map(&method(:Pathname))
.to_set
.freeze
private_constant :SYSTEM_DIRS
# TODO: There should be a way to specify a containing
# directory under which nothing can be deleted.
UNDELETABLE_PATHS = [
"~/",
"~/Applications",
"~/Applications/.localized",
"~/Desktop",
"~/Desktop/.localized",
"~/Documents",
"~/Documents/.localized",
"~/Downloads",
"~/Downloads/.localized",
"~/Mail",
"~/Movies",
"~/Movies/.localized",
"~/Music",
"~/Music/.localized",
"~/Music/iTunes",
"~/Music/iTunes/iTunes Music",
"~/Music/iTunes/Album Artwork",
"~/News",
"~/Pictures",
"~/Pictures/.localized",
"~/Pictures/Desktops",
"~/Pictures/Photo Booth",
"~/Pictures/iChat Icons",
"~/Pictures/iPhoto Library",
"~/Public",
"~/Public/.localized",
"~/Sites",
"~/Sites/.localized",
"~/Library",
"~/Library/.localized",
"~/Library/Accessibility",
"~/Library/Accounts",
"~/Library/Address Book Plug-Ins",
"~/Library/Application Scripts",
"~/Library/Application Support",
"~/Library/Application Support/Apple",
"~/Library/Application Support/com.apple.AssistiveControl",
"~/Library/Application Support/com.apple.QuickLook",
"~/Library/Application Support/com.apple.TCC",
"~/Library/Assistants",
"~/Library/Audio",
"~/Library/Automator",
"~/Library/Autosave Information",
"~/Library/Caches",
"~/Library/Calendars",
"~/Library/ColorPickers",
"~/Library/ColorSync",
"~/Library/Colors",
"~/Library/Components",
"~/Library/Compositions",
"~/Library/Containers",
"~/Library/Contextual Menu Items",
"~/Library/Cookies",
"~/Library/DTDs",
"~/Library/Desktop Pictures",
"~/Library/Developer",
"~/Library/Dictionaries",
"~/Library/DirectoryServices",
"~/Library/Displays",
"~/Library/Documentation",
"~/Library/Extensions",
"~/Library/Favorites",
"~/Library/FileSync",
"~/Library/Filesystems",
"~/Library/Filters",
"~/Library/FontCollections",
"~/Library/Fonts",
"~/Library/Frameworks",
"~/Library/GameKit",
"~/Library/Graphics",
"~/Library/Group Containers",
"~/Library/Icons",
"~/Library/IdentityServices",
"~/Library/Image Capture",
"~/Library/Images",
"~/Library/Input Methods",
"~/Library/Internet Plug-Ins",
"~/Library/InternetAccounts",
"~/Library/iTunes",
"~/Library/KeyBindings",
"~/Library/Keyboard Layouts",
"~/Library/Keychains",
"~/Library/LaunchAgents",
"~/Library/LaunchDaemons",
"~/Library/LocationBundles",
"~/Library/LoginPlugins",
"~/Library/Logs",
"~/Library/Mail",
"~/Library/Mail Downloads",
"~/Library/Messages",
"~/Library/Metadata",
"~/Library/Mobile Documents",
"~/Library/MonitorPanels",
"~/Library/OpenDirectory",
"~/Library/PDF Services",
"~/Library/PhonePlugins",
"~/Library/Phones",
"~/Library/PreferencePanes",
"~/Library/Preferences",
"~/Library/Printers",
"~/Library/PrivateFrameworks",
"~/Library/PubSub",
"~/Library/QuickLook",
"~/Library/QuickTime",
"~/Library/Receipts",
"~/Library/Recent Servers",
"~/Library/Recents",
"~/Library/Safari",
"~/Library/Saved Application State",
"~/Library/Screen Savers",
"~/Library/ScreenReader",
"~/Library/ScriptingAdditions",
"~/Library/ScriptingDefinitions",
"~/Library/Scripts",
"~/Library/Security",
"~/Library/Services",
"~/Library/Sounds",
"~/Library/Speech",
"~/Library/Spelling",
"~/Library/Spotlight",
"~/Library/StartupItems",
"~/Library/StickiesDatabase",
"~/Library/Sync Services",
"~/Library/SyncServices",
"~/Library/SyncedPreferences",
"~/Library/TextEncodings",
"~/Library/User Pictures",
"~/Library/Video",
"~/Library/Voices",
"~/Library/WebKit",
"~/Library/WidgetResources",
"~/Library/Widgets",
"~/Library/Workflows",
]
.to_set { |path| Pathname(path.sub(%r{^~(?=(/|$))}, Dir.home)).expand_path }
.union(SYSTEM_DIRS)
.freeze
private_constant :UNDELETABLE_PATHS
def system_dir?(dir)
SYSTEM_DIRS.include?(Pathname.new(dir).expand_path)
end
def undeletable?(path)
UNDELETABLE_PATHS.include?(Pathname.new(path).expand_path)
end
end
end
# typed: false
# frozen_string_literal: true
module Cask
# Helper module for reading and writing cask metadata.
#
# @api private
module Metadata
METADATA_SUBDIR = ".metadata"
TIMESTAMP_FORMAT = "%Y%m%d%H%M%S.%L"
def metadata_main_container_path
@metadata_main_container_path ||= caskroom_path.join(METADATA_SUBDIR)
end
def metadata_versioned_path(version: self.version)
cask_version = (version || :unknown).to_s
raise CaskError, "Cannot create metadata path with empty version." if cask_version.empty?
metadata_main_container_path.join(cask_version)
end
def metadata_timestamped_path(version: self.version, timestamp: :latest, create: false)
raise CaskError, "Cannot create metadata path when timestamp is :latest." if create && timestamp == :latest
path = if timestamp == :latest
Pathname.glob(metadata_versioned_path(version: version).join("*")).max
else
timestamp = new_timestamp if timestamp == :now
metadata_versioned_path(version: version).join(timestamp)
end
if create && !path.directory?
odebug "Creating metadata directory: #{path}"
path.mkpath
end
path
end
def metadata_subdir(leaf, version: self.version, timestamp: :latest, create: false)
raise CaskError, "Cannot create metadata subdir when timestamp is :latest." if create && timestamp == :latest
raise CaskError, "Cannot create metadata subdir for empty leaf." if !leaf.respond_to?(:empty?) || leaf.empty?
parent = metadata_timestamped_path(version: version, timestamp: timestamp, create: create)
return if parent.nil?
subdir = parent.join(leaf)
if create && !subdir.directory?
odebug "Creating metadata subdirectory: #{subdir}"
subdir.mkpath
end
subdir
end
private
def new_timestamp(time = Time.now)
time.utc.strftime(TIMESTAMP_FORMAT)
end
end
end
# typed: true
# frozen_string_literal: true
require "cask/macos"
module Cask
# Helper class for uninstalling `.pkg` installers.
#
# @api private
class Pkg
extend T::Sig
sig { params(regexp: String, command: T.class_of(SystemCommand)).returns(T::Array[Pkg]) }
def self.all_matching(regexp, command)
command.run("/usr/sbin/pkgutil", args: ["--pkgs=#{regexp}"]).stdout.split("\n").map do |package_id|
new(package_id.chomp, command)
end
end
sig { returns(String) }
attr_reader :package_id
sig { params(package_id: String, command: T.class_of(SystemCommand)).void }
def initialize(package_id, command = SystemCommand)
@package_id = package_id
@command = command
end
sig { void }
def uninstall
unless pkgutil_bom_files.empty?
odebug "Deleting pkg files"
@command.run!(
"/usr/bin/xargs",
args: ["-0", "--", "/bin/rm", "--"],
input: pkgutil_bom_files.join("\0"),
sudo: true,
)
end
unless pkgutil_bom_specials.empty?
odebug "Deleting pkg symlinks and special files"
@command.run!(
"/usr/bin/xargs",
args: ["-0", "--", "/bin/rm", "--"],
input: pkgutil_bom_specials.join("\0"),
sudo: true,
)
end
unless pkgutil_bom_dirs.empty?
odebug "Deleting pkg directories"
rmdir(deepest_path_first(pkgutil_bom_dirs))
end
rmdir(root) unless MacOS.undeletable?(root)
forget
end
sig { void }
def forget
odebug "Unregistering pkg receipt (aka forgetting)"
@command.run!("/usr/sbin/pkgutil", args: ["--forget", package_id], sudo: true)
end
sig { returns(T::Array[Pathname]) }
def pkgutil_bom_files
@pkgutil_bom_files ||= pkgutil_bom_all.select(&:file?) - pkgutil_bom_specials
end
sig { returns(T::Array[Pathname]) }
def pkgutil_bom_specials
@pkgutil_bom_specials ||= pkgutil_bom_all.select(&method(:special?))
end
sig { returns(T::Array[Pathname]) }
def pkgutil_bom_dirs
@pkgutil_bom_dirs ||= pkgutil_bom_all.select(&:directory?) - pkgutil_bom_specials
end
sig { returns(T::Array[Pathname]) }
def pkgutil_bom_all
@pkgutil_bom_all ||= @command.run!("/usr/sbin/pkgutil", args: ["--files", package_id])
.stdout
.split("\n")
.map { |path| root.join(path) }
.reject(&MacOS.public_method(:undeletable?))
end
sig { returns(Pathname) }
def root
@root ||= Pathname.new(info.fetch("volume")).join(info.fetch("install-location"))
end
def info
@info ||= @command.run!("/usr/sbin/pkgutil", args: ["--pkg-info-plist", package_id])
.plist
end
private
sig { params(path: Pathname).returns(T::Boolean) }
def special?(path)
path.symlink? || path.chardev? || path.blockdev?
end
# Helper script to delete empty directories after deleting `.DS_Store` files and broken symlinks.
# Needed in order to execute all file operations with `sudo`.
RMDIR_SH = (HOMEBREW_LIBRARY_PATH/"cask/utils/rmdir.sh").freeze
private_constant :RMDIR_SH
sig { params(path: T.any(Pathname, T::Array[Pathname])).void }
def rmdir(path)
@command.run!(
"/usr/bin/xargs",
args: ["-0", "--", RMDIR_SH.to_s],
input: Array(path).join("\0"),
sudo: true,
)
end
sig { params(paths: T::Array[Pathname]).returns(T::Array[Pathname]) }
def deepest_path_first(paths)
paths.sort_by { |path| -path.to_s.split(File::SEPARATOR).count }
end
end
end
# typed: false
# frozen_string_literal: true
require "development_tools"
require "cask/exceptions"
module Cask
# Helper module for quarantining files.
#
# @api private
module Quarantine
extend T::Sig
module_function
QUARANTINE_ATTRIBUTE = "com.apple.quarantine"
QUARANTINE_SCRIPT = (HOMEBREW_LIBRARY_PATH/"cask/utils/quarantine.swift").freeze
def swift
@swift ||= DevelopmentTools.locate("swift")
end
private :swift
def xattr
@xattr ||= DevelopmentTools.locate("xattr")
end
private :xattr
def swift_target_args
["-target", "#{Hardware::CPU.arch}-apple-macosx#{MacOS.version}"]
end
private :swift_target_args
sig { returns(Symbol) }
def check_quarantine_support
odebug "Checking quarantine support"
if !system_command(xattr, args: ["-h"], print_stderr: false).success?
odebug "There's no working version of `xattr` on this system."
:xattr_broken
elsif swift.nil?
odebug "Swift is not available on this system."
:no_swift
else
api_check = system_command(swift,
args: [*swift_target_args, QUARANTINE_SCRIPT],
print_stderr: false)
case api_check.exit_status
when 2
odebug "Quarantine is available."
:quarantine_available
else
odebug "Unknown support status"
:unknown
end
end
end
def available?
@status ||= check_quarantine_support
@status == :quarantine_available
end
def detect(file)
return if file.nil?
odebug "Verifying Gatekeeper status of #{file}"
quarantine_status = !status(file).empty?
odebug "#{file} is #{quarantine_status ? "quarantined" : "not quarantined"}"
quarantine_status
end
def status(file)
system_command(xattr,
args: ["-p", QUARANTINE_ATTRIBUTE, file],
print_stderr: false).stdout.rstrip
end
def toggle_no_translocation_bit(attribute)
fields = attribute.split(";")
# Fields: status, epoch, download agent, event ID
# Let's toggle the app translocation bit, bit 8
# http://www.openradar.me/radar?id=5022734169931776
fields[0] = (fields[0].to_i(16) | 0x0100).to_s(16).rjust(4, "0")
fields.join(";")
end
def release!(download_path: nil)
return unless detect(download_path)
odebug "Releasing #{download_path} from quarantine"
quarantiner = system_command(xattr,
args: [
"-d",
QUARANTINE_ATTRIBUTE,
download_path,
],
print_stderr: false)
return if quarantiner.success?
raise CaskQuarantineReleaseError.new(download_path, quarantiner.stderr)
end
def cask!(cask: nil, download_path: nil, action: true)
return if cask.nil? || download_path.nil?
return if detect(download_path)
odebug "Quarantining #{download_path}"
quarantiner = system_command(swift,
args: [
*swift_target_args,
QUARANTINE_SCRIPT,
download_path,
cask.url.to_s,
cask.homepage.to_s,
],
print_stderr: false)
return if quarantiner.success?
case quarantiner.exit_status
when 2
raise CaskQuarantineError.new(download_path, "Insufficient parameters")
else
raise CaskQuarantineError.new(download_path, quarantiner.stderr)
end
end
def propagate(from: nil, to: nil)
return if from.nil? || to.nil?
raise CaskError, "#{from} was not quarantined properly." unless detect(from)
odebug "Propagating quarantine from #{from} to #{to}"
quarantine_status = toggle_no_translocation_bit(status(from))
resolved_paths = Pathname.glob(to/"**/*", File::FNM_DOTMATCH).reject(&:symlink?)
system_command!("/usr/bin/xargs",
args: [
"-0",
"--",
"/bin/chmod",
"-h",
"u+w",
],
input: resolved_paths.join("\0"))
quarantiner = system_command("/usr/bin/xargs",
args: [
"-0",
"--",
xattr,
"-w",
QUARANTINE_ATTRIBUTE,
quarantine_status,
],
input: resolved_paths.join("\0"),
print_stderr: false)
return if quarantiner.success?
raise CaskQuarantinePropagationError.new(to, quarantiner.stderr)
end
end
end
# typed: true
# frozen_string_literal: true
require "utils/user"
module Cask
# Helper functions for staged casks.
#
# @api private
module Staged
extend T::Sig
# FIXME: Enable cop again when https://github.com/sorbet/sorbet/issues/3532 is fixed.
# rubocop:disable Style/MutableConstant
Paths = T.type_alias { T.any(String, Pathname, T::Array[T.any(String, Pathname)]) }
# rubocop:enable Style/MutableConstant
sig { params(paths: Paths, permissions_str: String).void }
def set_permissions(paths, permissions_str)
full_paths = remove_nonexistent(paths)
return if full_paths.empty?
@command.run!("/bin/chmod", args: ["-R", "--", permissions_str, *full_paths],
sudo: false)
end
sig { params(paths: Paths, user: T.any(String, User), group: String).void }
def set_ownership(paths, user: T.must(User.current), group: "staff")
full_paths = remove_nonexistent(paths)
return if full_paths.empty?
ohai "Changing ownership of paths required by #{@cask}; your password may be necessary."
@command.run!("/usr/sbin/chown", args: ["-R", "--", "#{user}:#{group}", *full_paths],
sudo: true)
end
private
sig { params(paths: Paths).returns(T::Array[Pathname]) }
def remove_nonexistent(paths)
Array(paths).map { |p| Pathname(p).expand_path }.select(&:exist?)
end
end
end
# typed: strict
module Cask
module Staged
include Kernel
end
end
# typed: true
# frozen_string_literal: true
# Class corresponding to the `url` stanza.
#
# @api private
class URL < Delegator
extend T::Sig
# @api private
class DSL
extend T::Sig
attr_reader :uri, :specs,
:verified, :using,
:tag, :branch, :revisions, :revision,
:trust_cert, :cookies, :referer, :header, :user_agent,
:data, :only_path
extend Forwardable
def_delegators :uri, :path, :scheme, :to_s
# @api public
sig {
params(
uri: T.any(URI::Generic, String),
verified: T.nilable(String),
using: T.nilable(Symbol),
tag: T.nilable(String),
branch: T.nilable(String),
revisions: T.nilable(T::Array[String]),
revision: T.nilable(String),
trust_cert: T.nilable(T::Boolean),
cookies: T.nilable(T::Hash[String, String]),
referer: T.nilable(T.any(URI::Generic, String)),
header: T.nilable(String),
user_agent: T.nilable(T.any(Symbol, String)),
data: T.nilable(T::Hash[String, String]),
only_path: T.nilable(String),
).void
}
def initialize(
uri,
verified: nil,
using: nil,
tag: nil,
branch: nil,
revisions: nil,
revision: nil,
trust_cert: nil,
cookies: nil,
referer: nil,
header: nil,
user_agent: nil,
data: nil,
only_path: nil
)
@uri = URI(uri)
specs = {}
specs[:verified] = @verified = verified
specs[:using] = @using = using
specs[:tag] = @tag = tag
specs[:branch] = @branch = branch
specs[:revisions] = @revisions = revisions
specs[:revision] = @revision = revision
specs[:trust_cert] = @trust_cert = trust_cert
specs[:cookies] = @cookies = cookies
specs[:referer] = @referer = referer
specs[:header] = @header = header
specs[:user_agent] = @user_agent = user_agent || :default
specs[:data] = @data = data
specs[:only_path] = @only_path = only_path
@specs = specs.compact
end
end
# @api private
class BlockDSL
extend T::Sig
# To access URL associated with page contents.
module PageWithURL
extend T::Sig
# @api public
sig { returns(URI::Generic) }
attr_accessor :url
end
sig {
params(
uri: T.nilable(T.any(URI::Generic, String)),
dsl: T.nilable(Cask::DSL),
block: T.proc.params(arg0: T.all(String, PageWithURL)).returns(T.untyped),
).void
}
def initialize(uri, dsl: nil, &block)
@uri = uri
@dsl = dsl
@block = block
end
sig { returns(T.untyped) }
def call
if @uri
result = curl_output("--fail", "--silent", "--location", @uri)
result.assert_success!
page = result.stdout
page.extend PageWithURL
page.url = URI(@uri)
instance_exec(page, &@block)
else
instance_exec(&@block)
end
end
# @api public
sig {
params(
uri: T.any(URI::Generic, String),
block: T.proc.params(arg0: T.all(String, PageWithURL)).returns(T.untyped),
).void
}
def url(uri, &block)
self.class.new(uri, &block).call
end
private :url
# @api public
def method_missing(method, *args, &block)
if @dsl.respond_to?(method)
T.unsafe(@dsl).public_send(method, *args, &block)
else
super
end
end
def respond_to_missing?(method, include_all)
@dsl.respond_to?(method, include_all) || super
end
end
sig {
params(
uri: T.nilable(T.any(URI::Generic, String)),
verified: T.nilable(String),
using: T.nilable(Symbol),
tag: T.nilable(String),
branch: T.nilable(String),
revisions: T.nilable(T::Array[String]),
revision: T.nilable(String),
trust_cert: T.nilable(T::Boolean),
cookies: T.nilable(T::Hash[String, String]),
referer: T.nilable(T.any(URI::Generic, String)),
header: T.nilable(String),
user_agent: T.nilable(T.any(Symbol, String)),
data: T.nilable(T::Hash[String, String]),
only_path: T.nilable(String),
caller_location: Thread::Backtrace::Location,
dsl: T.nilable(Cask::DSL),
block: T.nilable(T.proc.params(arg0: T.all(String, BlockDSL::PageWithURL)).returns(T.untyped)),
).void
}
def initialize(
uri = nil,
verified: nil,
using: nil,
tag: nil,
branch: nil,
revisions: nil,
revision: nil,
trust_cert: nil,
cookies: nil,
referer: nil,
header: nil,
user_agent: nil,
data: nil,
only_path: nil,
caller_location: T.must(caller_locations).fetch(0),
dsl: nil,
&block
)
super(
if block
LazyObject.new do
*args = BlockDSL.new(uri, dsl: dsl, &block).call
options = args.last.is_a?(Hash) ? args.pop : {}
uri = T.let(args.first, T.any(URI::Generic, String))
DSL.new(uri, **options)
end
else
DSL.new(
T.must(uri),
verified: verified,
using: using,
tag: tag,
branch: branch,
revisions: revisions,
revision: revision,
trust_cert: trust_cert,
cookies: cookies,
referer: referer,
header: header,
user_agent: user_agent,
data: data,
only_path: only_path,
)
end
)
@from_block = !block.nil?
@caller_location = caller_location
end
def __getobj__
@dsl
end
def __setobj__(dsl)
@dsl = dsl
end
sig { returns(T.nilable(String)) }
def raw_interpolated_url
return @raw_interpolated_url if defined?(@raw_interpolated_url)
@raw_interpolated_url =
Pathname(@caller_location.path)
.each_line.drop(@caller_location.lineno - 1)
.first&.then { |line| line[/url\s+"([^"]+)"/, 1] }
end
private :raw_interpolated_url
sig { params(ignore_major_version: T::Boolean).returns(T::Boolean) }
def unversioned?(ignore_major_version: false)
interpolated_url = raw_interpolated_url
return false unless interpolated_url
interpolated_url = interpolated_url.gsub(/\#{\s*version\s*\.major\s*}/, "") if ignore_major_version
interpolated_url.exclude?('#{')
end
sig { returns(T::Boolean) }
def from_block?
@from_block
end
end
# typed: strict
# typed: false
class URL
include Kernel
end
# typed: true
# frozen_string_literal: true
require "utils/user"
require "yaml"
require "open3"
require "stringio"
BUG_REPORTS_URL = "https://github.com/Homebrew/homebrew-cask#reporting-bugs"
module Cask
# Helper functions for various cask operations.
#
# @api private
module Utils
extend T::Sig
def self.gain_permissions_remove(path, command: SystemCommand)
if path.respond_to?(:rmtree) && path.exist?
gain_permissions(path, ["-R"], command) do |p|
if p.parent.writable?
p.rmtree
else
command.run("/bin/rm",
args: ["-r", "-f", "--", p],
sudo: true)
end
end
elsif File.symlink?(path)
gain_permissions(path, ["-h"], command, &FileUtils.method(:rm_f))
end
end
def self.gain_permissions(path, command_args, command)
tried_permissions = false
tried_ownership = false
begin
yield path
rescue
# in case of permissions problems
unless tried_permissions
# TODO: Better handling for the case where path is a symlink.
# The -h and -R flags cannot be combined, and behavior is
# dependent on whether the file argument has a trailing
# slash. This should do the right thing, but is fragile.
command.run("/usr/bin/chflags",
must_succeed: false,
args: command_args + ["--", "000", path])
command.run("/bin/chmod",
must_succeed: false,
args: command_args + ["--", "u+rwx", path])
command.run("/bin/chmod",
must_succeed: false,
args: command_args + ["-N", path])
tried_permissions = true
retry # rmtree
end
unless tried_ownership
# in case of ownership problems
# TODO: Further examine files to see if ownership is the problem
# before using sudo+chown
ohai "Using sudo to gain ownership of path '#{path}'"
command.run("/usr/sbin/chown",
args: command_args + ["--", User.current, path],
sudo: true)
tried_ownership = true
# retry chflags/chmod after chown
tried_permissions = false
retry # rmtree
end
raise
end
end
sig { params(path: Pathname).returns(T::Boolean) }
def self.path_occupied?(path)
path.exist? || path.symlink?
end
sig { params(name: String).returns(String) }
def self.token_from(name)
name.downcase
.gsub("+", "-plus-")
.gsub("@", "-at-")
.gsub(/[ _·•]/, "-")
.gsub(/[^\w-]/, "")
.gsub(/--+/, "-")
.delete_prefix("-")
.delete_suffix("-")
end
sig { returns(String) }
def self.error_message_with_suggestions
<<~EOS
Follow the instructions here:
#{Formatter.url(BUG_REPORTS_URL)}
EOS
end
def self.method_missing_message(method, token, section = nil)
message = +"Unexpected method '#{method}' called "
message << "during #{section} " if section
message << "on Cask #{token}."
opoo "#{message}\n#{error_message_with_suggestions}"
end
end
end
#!/usr/bin/swift
import Foundation
struct SwiftErr: TextOutputStream {
public static var stream = SwiftErr()
mutating func write(_ string: String) {
fputs(string, stderr)
}
}
// TODO: tell which arguments have to be provided
guard CommandLine.arguments.count >= 4 else {
exit(2)
}
var dataLocationURL = URL(fileURLWithPath: CommandLine.arguments[1])
let quarantineProperties: [String: Any] = [
kLSQuarantineAgentNameKey as String: "Homebrew Cask",
kLSQuarantineTypeKey as String: kLSQuarantineTypeWebDownload,
kLSQuarantineDataURLKey as String: CommandLine.arguments[2],
kLSQuarantineOriginURLKey as String: CommandLine.arguments[3]
]
// Check for if the data location URL is reachable
do {
let isDataLocationURLReachable = try dataLocationURL.checkResourceIsReachable()
guard isDataLocationURLReachable else {
print("URL \(dataLocationURL.path) is not reachable. Not proceeding.", to: &SwiftErr.stream)
exit(1)
}
} catch {
print(error.localizedDescription, to: &SwiftErr.stream)
exit(1)
}
// Quarantine the file
do {
var resourceValues = URLResourceValues()
resourceValues.quarantineProperties = quarantineProperties
try dataLocationURL.setResourceValues(resourceValues)
} catch {
print(error.localizedDescription, to: &SwiftErr.stream)
exit(1)
}
#!/bin/bash
set -euo pipefail
# Try removing as many empty directories as possible with a single
# `rmdir` call to avoid or at least speed up the loop below.
if /bin/rmdir -- "${@}" &>/dev/null
then
exit
fi
for path in "${@}"
do
symlink=true
[[ -L "${path}" ]] || symlink=false
directory=false
if [[ -d "${path}" ]]
then
directory=true
if [[ -e "${path}/.DS_Store" ]]
then
/bin/rm -- "${path}/.DS_Store"
fi
# Some packages leave broken symlinks around; we clean them out before
# attempting to `rmdir` to prevent extra cruft from accumulating.
/usr/bin/find -f "${path}" -- -mindepth 1 -maxdepth 1 -type l ! -exec /bin/test -e {} \; -delete
elif ! ${symlink} && [[ ! -e "${path}" ]]
then
# Skip paths that don't exists and aren't a broken symlink.
continue
fi
if ${symlink}
then
# Delete directory symlink.
/bin/rm -- "${path}"
elif ${directory}
then
# Delete directory if empty.
/usr/bin/find -f "${path}" -- -maxdepth 0 -type d -empty -exec /bin/rmdir -- {} \;
else
# Try `rmdir` anyways to show a proper error.
/bin/rmdir -- "${path}"
fi
done
#!/usr/bin/swift
import Foundation
extension FileHandle : TextOutputStream {
public func write(_ string: String) {
if let data = string.data(using: .utf8) { self.write(data) }
}
}
var stderr = FileHandle.standardError
let manager = FileManager.default
var success = true
// The command line arguments given but without the script's name
let CMDLineArgs = Array(CommandLine.arguments.dropFirst())
for item in CMDLineArgs {
do {
let url = URL(fileURLWithPath: item)
var trashedPath: NSURL!
try manager.trashItem(at: url, resultingItemURL: &trashedPath)
print((trashedPath as URL).path, terminator: ":")
success = true
} catch {
print(item, terminator: ":", to: &stderr)
success = false
}
}
guard success else {
exit(1)
}
# typed: true
# frozen_string_literal: true
require "language/python"
# A formula's caveats.
#
# @api private
class Caveats
extend Forwardable
attr_reader :f
def initialize(f)
@f = f
end
def caveats
caveats = []
begin
build = f.build
f.build = Tab.for_formula(f)
s = f.caveats.to_s
caveats << "#{s.chomp}\n" unless s.empty?
ensure
f.build = build
end
caveats << keg_only_text
valid_shells = [:bash, :zsh, :fish].freeze
current_shell = Utils::Shell.preferred || Utils::Shell.parent
shells = if current_shell.present? &&
(shell_sym = current_shell.to_sym) &&
valid_shells.include?(shell_sym)
[shell_sym]
else
valid_shells
end
shells.each do |shell|
caveats << function_completion_caveats(shell)
end
caveats << service_caveats
caveats << elisp_caveats
caveats.compact.join("\n")
end
delegate [:empty?, :to_s] => :caveats
def keg_only_text(skip_reason: false)
return unless f.keg_only?
s = if skip_reason
""
else
<<~EOS
#{f.name} is keg-only, which means it was not symlinked into #{HOMEBREW_PREFIX},
because #{f.keg_only_reason.to_s.chomp}.
EOS
end.dup
if f.bin.directory? || f.sbin.directory?
s << <<~EOS
If you need to have #{f.name} first in your PATH, run:
EOS
s << " #{Utils::Shell.prepend_path_in_profile(f.opt_bin.to_s)}\n" if f.bin.directory?
s << " #{Utils::Shell.prepend_path_in_profile(f.opt_sbin.to_s)}\n" if f.sbin.directory?
end
if f.lib.directory? || f.include.directory?
s << <<~EOS
For compilers to find #{f.name} you may need to set:
EOS
s << " #{Utils::Shell.export_value("LDFLAGS", "-L#{f.opt_lib}")}\n" if f.lib.directory?
s << " #{Utils::Shell.export_value("CPPFLAGS", "-I#{f.opt_include}")}\n" if f.include.directory?
if which("pkg-config", ORIGINAL_PATHS) &&
((f.lib/"pkgconfig").directory? || (f.share/"pkgconfig").directory?)
s << <<~EOS
For pkg-config to find #{f.name} you may need to set:
EOS
if (f.lib/"pkgconfig").directory?
s << " #{Utils::Shell.export_value("PKG_CONFIG_PATH", "#{f.opt_lib}/pkgconfig")}\n"
end
if (f.share/"pkgconfig").directory?
s << " #{Utils::Shell.export_value("PKG_CONFIG_PATH", "#{f.opt_share}/pkgconfig")}\n"
end
end
end
s << "\n"
end
private
def keg
@keg ||= [f.prefix, f.opt_prefix, f.linked_keg].map do |d|
Keg.new(d.resolved_path)
rescue
nil
end.compact.first
end
def function_completion_caveats(shell)
return unless keg
return unless which(shell.to_s, ORIGINAL_PATHS)
completion_installed = keg.completion_installed?(shell)
functions_installed = keg.functions_installed?(shell)
return if !completion_installed && !functions_installed
installed = []
installed << "completions" if completion_installed
installed << "functions" if functions_installed
root_dir = f.keg_only? ? f.opt_prefix : HOMEBREW_PREFIX
case shell
when :bash
<<~EOS
Bash completion has been installed to:
#{root_dir}/etc/bash_completion.d
EOS
when :zsh
<<~EOS
zsh #{installed.join(" and ")} have been installed to:
#{root_dir}/share/zsh/site-functions
EOS
when :fish
fish_caveats = +"fish #{installed.join(" and ")} have been installed to:"
fish_caveats << "\n #{root_dir}/share/fish/vendor_completions.d" if completion_installed
fish_caveats << "\n #{root_dir}/share/fish/vendor_functions.d" if functions_installed
fish_caveats.freeze
end
end
def elisp_caveats
return if f.keg_only?
return unless keg
return unless keg.elisp_installed?
<<~EOS
Emacs Lisp files have been installed to:
#{HOMEBREW_PREFIX}/share/emacs/site-lisp/#{f.name}
EOS
end
def service_caveats
return if !f.plist && !f.service? && !keg&.plist_installed?
return if f.service? && f.service.command.blank?
s = []
command = if f.service?
f.service.manual_command
else
f.plist_manual
end
return <<~EOS if !which("launchctl") && f.plist
#{Formatter.warning("Warning:")} #{f.name} provides a launchd plist which can only be used on macOS!
You can manually execute the service instead with:
#{command}
EOS
# Brew services only works with these two tools
return <<~EOS if !which("systemctl") && !which("launchctl") && f.service?
#{Formatter.warning("Warning:")} #{f.name} provides a service which can only be used on macOS or systemd!
You can manually execute the service instead with:
#{command}
EOS
is_running_service = f.service? && quiet_system("ps aux | grep #{f.service.command&.first}")
startup = f.service&.requires_root? || f.plist_startup
if is_running_service || (f.plist && quiet_system("/bin/launchctl list #{f.plist_name} &>/dev/null"))
s << "To restart #{f.full_name} after an upgrade:"
s << " #{startup ? "sudo " : ""}brew services restart #{f.full_name}"
elsif startup
s << "To start #{f.full_name} now and restart at startup:"
s << " sudo brew services start #{f.full_name}"
else
s << "To start #{f.full_name} now and restart at login:"
s << " brew services start #{f.full_name}"
end
if f.plist_manual || f.service?
s << "Or, if you don't want/need a background service you can just run:"
s << " #{command}"
end
# pbpaste is the system clipboard tool on macOS and fails with `tmux` by default
# check if this is being run under `tmux` to avoid failing
if ENV["HOMEBREW_TMUX"] && !quiet_system("/usr/bin/pbpaste")
s << "" << "WARNING: brew services will fail when run under tmux."
end
"#{s.join("\n")}\n" unless s.empty?
end
end
# typed: true
# frozen_string_literal: true
# A formula's checksum.
#
# @api private
class Checksum
extend Forwardable
attr_reader :hexdigest
def initialize(hexdigest)
@hexdigest = hexdigest.downcase
end
delegate [:empty?, :to_s, :length, :[]] => :@hexdigest
def ==(other)
case other
when String
to_s == other.downcase
when Checksum
hexdigest == other.hexdigest
else
false
end
end
end
# typed: true
# frozen_string_literal: true
# Cleans a newly installed keg.
# By default:
#
# * removes `.la` files
# * removes `perllocal.pod` files
# * removes `.packlist` files
# * removes empty directories
# * sets permissions on executables
# * removes unresolved symlinks
class Cleaner
include Context
# Create a cleaner for the given formula.
def initialize(f)
@f = f
end
# Clean the keg of the formula.
def clean
ObserverPathnameExtension.reset_counts!
# Many formulae include 'lib/charset.alias', but it is not strictly needed
# and will conflict if more than one formula provides it
observe_file_removal @f.lib/"charset.alias"
[@f.bin, @f.sbin, @f.lib].each { |d| clean_dir(d) if d.exist? }
# Get rid of any info 'dir' files, so they don't conflict at the link stage
#
# The 'dir' files come in at least 3 locations:
#
# 1. 'info/dir'
# 2. 'info/#{name}/dir'
# 3. 'info/#{arch}/dir'
#
# Of these 3 only 'info/#{name}/dir' is safe to keep since the rest will
# conflict with other formulae because they use a shared location.
#
# See [cleaner: recursively delete info `dir`s by gromgit · Pull Request
# #11597][1], [emacs 28.1 bottle does not contain `dir` file · Issue
# #100190][2], and [Keep `info/#{f.name}/dir` files in cleaner by
# timvisher][3] for more info.
#
# [1]: https://github.com/Homebrew/brew/pull/11597
# [2]: https://github.com/Homebrew/homebrew-core/issues/100190
# [3]: https://github.com/Homebrew/brew/pull/13215
Dir.glob(@f.info/"**/dir").each do |f|
info_dir_file = Pathname(f)
next unless info_dir_file.file?
next if info_dir_file == @f.info/@f.name/"dir"
next if @f.skip_clean?(info_dir_file)
observe_file_removal info_dir_file
end
rewrite_shebangs
prune
end
private
def observe_file_removal(path)
path.extend(ObserverPathnameExtension).unlink if path.exist?
end
# Removes any empty directories in the formula's prefix subtree
# Keeps any empty directories protected by skip_clean
# Removes any unresolved symlinks
def prune
dirs = []
symlinks = []
@f.prefix.find do |path|
if path == @f.libexec || @f.skip_clean?(path)
Find.prune
elsif path.symlink?
symlinks << path
elsif path.directory?
dirs << path
end
end
# Remove directories opposite from traversal, so that a subtree with no
# actual files gets removed correctly.
dirs.reverse_each do |d|
if d.children.empty?
puts "rmdir: #{d} (empty)" if verbose?
d.rmdir
end
end
# Remove unresolved symlinks
symlinks.reverse_each do |s|
s.unlink unless s.resolved_path_exists?
end
end
def executable_path?(path)
path.text_executable? || path.executable?
end
# Both these files are completely unnecessary to package and cause
# pointless conflicts with other formulae. They are removed by Debian,
# Arch & MacPorts amongst other packagers as well. The files are
# created as part of installing any Perl module.
PERL_BASENAMES = Set.new(%w[perllocal.pod .packlist]).freeze
# Clean a top-level (bin, sbin, lib) directory, recursively, by fixing file
# permissions and removing .la files, unless the files (or parent
# directories) are protected by skip_clean.
#
# bin and sbin should not have any subdirectories; if either do that is
# caught as an audit warning
#
# lib may have a large directory tree (see Erlang for instance), and
# clean_dir applies cleaning rules to the entire tree
def clean_dir(d)
d.find do |path|
path.extend(ObserverPathnameExtension)
Find.prune if @f.skip_clean? path
next if path.directory?
if path.extname == ".la" || PERL_BASENAMES.include?(path.basename.to_s)
path.unlink
elsif path.symlink?
# Skip it.
else
# Set permissions for executables and non-executables
perms = if executable_path?(path)
0555
else
0444
end
if debug?
old_perms = path.stat.mode & 0777
odebug "Fixing #{path} permissions from #{old_perms.to_s(8)} to #{perms.to_s(8)}" if perms != old_perms
end
path.chmod perms
end
end
end
def rewrite_shebangs
require "language/perl"
require "utils/shebang"
basepath = @f.prefix.realpath
basepath.find do |path|
Find.prune if @f.skip_clean? path
next if path.directory? || path.symlink?
begin
Utils::Shebang.rewrite_shebang Language::Perl::Shebang.detected_perl_shebang(@f), path
rescue ShebangDetectionError
break
end
end
end
end
require "extend/os/cleaner"
# typed: false
# frozen_string_literal: true
require "utils/bottles"
require "formula"
require "cask/cask_loader"
require "set"
module Homebrew
# Helper class for cleaning up the Homebrew cache.
#
# @api private
class Cleanup
CLEANUP_DEFAULT_DAYS = Homebrew::EnvConfig.cleanup_periodic_full_days.to_i.freeze
private_constant :CLEANUP_DEFAULT_DAYS
# {Pathname} refinement with helper functions for cleaning up files.
module CleanupRefinement
refine Pathname do
def incomplete?
extname.end_with?(".incomplete")
end
def nested_cache?
directory? && %w[
cargo_cache
go_cache
go_mod_cache
glide_home
java_cache
npm_cache
gclient_cache
].include?(basename.to_s)
end
def go_cache_directory?
# Go makes its cache contents read-only to ensure cache integrity,
# which makes sense but is something we need to undo for cleanup.
directory? && %w[go_cache go_mod_cache].include?(basename.to_s)
end
def prune?(days)
return false unless days
return true if days.zero?
return true if symlink? && !exist?
mtime < days.days.ago && ctime < days.days.ago
end
def stale?(scrub: false)
return false unless resolved_path.file?
if dirname.basename.to_s == "Cask"
stale_cask?(scrub)
else
stale_formula?(scrub)
end
end
private
def stale_formula?(scrub)
return false unless HOMEBREW_CELLAR.directory?
version = if HOMEBREW_BOTTLES_EXTNAME_REGEX.match?(to_s)
begin
Utils::Bottles.resolve_version(self)
rescue
nil
end
end
version ||= basename.to_s[/\A.*(?:--.*?)*--(.*?)#{Regexp.escape(extname)}\Z/, 1]
version ||= basename.to_s[/\A.*--?(.*?)#{Regexp.escape(extname)}\Z/, 1]
return false unless version
version = Version.new(version)
return false unless (formula_name = basename.to_s[/\A(.*?)(?:--.*?)*--?(?:#{Regexp.escape(version)})/, 1])
formula = begin
Formulary.from_rack(HOMEBREW_CELLAR/formula_name)
rescue FormulaUnavailableError, TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError
nil
end
return false if formula.blank?
resource_name = basename.to_s[/\A.*?--(.*?)--?(?:#{Regexp.escape(version)})/, 1]
if resource_name == "patch"
patch_hashes = formula.stable&.patches&.select(&:external?)&.map(&:resource)&.map(&:version)
return true unless patch_hashes&.include?(Checksum.new(version.to_s))
elsif resource_name && (resource_version = formula.stable&.resources&.dig(resource_name)&.version)
return true if resource_version != version
elsif version.is_a?(PkgVersion)
return true if formula.pkg_version > version
elsif formula.version > version
return true
end
return true if scrub && !formula.latest_version_installed?
return true if Utils::Bottles.file_outdated?(formula, self)
false
end
def stale_cask?(scrub)
return false unless (name = basename.to_s[/\A(.*?)--/, 1])
cask = begin
Cask::CaskLoader.load(name)
rescue Cask::CaskError
nil
end
return false if cask.blank?
return true unless basename.to_s.match?(/\A#{Regexp.escape(name)}--#{Regexp.escape(cask.version)}\b/)
return true if scrub && cask.versions.exclude?(cask.version)
if cask.version.latest?
return mtime < CLEANUP_DEFAULT_DAYS.days.ago &&
ctime < CLEANUP_DEFAULT_DAYS.days.ago
end
false
end
end
end
using CleanupRefinement
extend Predicable
PERIODIC_CLEAN_FILE = (HOMEBREW_CACHE/".cleaned").freeze
attr_predicate :dry_run?, :scrub?, :prune?
attr_reader :args, :days, :cache, :disk_cleanup_size
def initialize(*args, dry_run: false, scrub: false, days: nil, cache: HOMEBREW_CACHE)
@disk_cleanup_size = 0
@args = args
@dry_run = dry_run
@scrub = scrub
@prune = days.present?
@days = days || Homebrew::EnvConfig.cleanup_max_age_days.to_i
@cache = cache
@cleaned_up_paths = Set.new
end
def self.install_formula_clean!(f, dry_run: false)
return if Homebrew::EnvConfig.no_install_cleanup?
return unless f.latest_version_installed?
return if skip_clean_formula?(f)
if dry_run
ohai "Would run `brew cleanup #{f}`"
else
ohai "Running `brew cleanup #{f}`..."
end
puts_no_install_cleanup_disable_message_if_not_already!
return if dry_run
Cleanup.new.cleanup_formula(f)
end
def self.puts_no_install_cleanup_disable_message
return if Homebrew::EnvConfig.no_env_hints?
return if Homebrew::EnvConfig.no_install_cleanup?
puts "Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP."
puts "Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`)."
end
def self.puts_no_install_cleanup_disable_message_if_not_already!
return if @puts_no_install_cleanup_disable_message_if_not_already
puts_no_install_cleanup_disable_message
@puts_no_install_cleanup_disable_message_if_not_already = true
end
def self.skip_clean_formula?(f)
return false if Homebrew::EnvConfig.no_cleanup_formulae.blank?
@skip_clean_formulae ||= Homebrew::EnvConfig.no_cleanup_formulae.split(",")
@skip_clean_formulae.include?(f.name) || (@skip_clean_formulae & f.aliases).present?
end
def self.periodic_clean_due?
return false if Homebrew::EnvConfig.no_install_cleanup?
unless PERIODIC_CLEAN_FILE.exist?
HOMEBREW_CACHE.mkpath
FileUtils.touch PERIODIC_CLEAN_FILE
return false
end
PERIODIC_CLEAN_FILE.mtime < CLEANUP_DEFAULT_DAYS.days.ago
end
def self.periodic_clean!(dry_run: false)
return if Homebrew::EnvConfig.no_install_cleanup?
return unless periodic_clean_due?
if dry_run
oh1 "Would run `brew cleanup` which has not been run in the last #{CLEANUP_DEFAULT_DAYS} days"
else
oh1 "`brew cleanup` has not been run in the last #{CLEANUP_DEFAULT_DAYS} days, running now..."
end
puts_no_install_cleanup_disable_message
return if dry_run
Cleanup.new.clean!(quiet: true, periodic: true)
end
def clean!(quiet: false, periodic: false)
if args.empty?
Formula.installed
.sort_by(&:name)
.reject { |f| Cleanup.skip_clean_formula?(f) }
.each do |formula|
cleanup_formula(formula, quiet: quiet, ds_store: false, cache_db: false)
end
Cleanup.autoremove(dry_run: dry_run?) if Homebrew::EnvConfig.autoremove?
cleanup_cache
cleanup_logs
cleanup_lockfiles
cleanup_python_site_packages
prune_prefix_symlinks_and_directories
unless dry_run?
cleanup_cache_db
rm_ds_store
HOMEBREW_CACHE.mkpath
FileUtils.touch PERIODIC_CLEAN_FILE
end
# Cleaning up Ruby needs to be done last to avoid requiring additional
# files afterwards. Additionally, don't allow it on periodic cleans to
# avoid having to try to do a `brew install` when we've just deleted
# the running Ruby process...
return if periodic
cleanup_portable_ruby
cleanup_bootsnap
else
args.each do |arg|
formula = begin
Formulary.resolve(arg)
rescue FormulaUnavailableError, TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError
nil
end
cask = begin
Cask::CaskLoader.load(arg)
rescue Cask::CaskError
nil
end
if formula && Cleanup.skip_clean_formula?(formula)
onoe "Refusing to clean #{formula} because it is listed in " \
"#{Tty.bold}HOMEBREW_NO_CLEANUP_FORMULAE#{Tty.reset}!"
elsif formula
cleanup_formula(formula)
end
cleanup_cask(cask) if cask
end
end
end
def unremovable_kegs
@unremovable_kegs ||= []
end
def cleanup_formula(formula, quiet: false, ds_store: true, cache_db: true)
formula.eligible_kegs_for_cleanup(quiet: quiet)
.each(&method(:cleanup_keg))
cleanup_cache(Pathname.glob(cache/"#{formula.name}--*"))
rm_ds_store([formula.rack]) if ds_store
cleanup_cache_db(formula.rack) if cache_db
cleanup_lockfiles(FormulaLock.new(formula.name).path)
end
def cleanup_cask(cask, ds_store: true)
cleanup_cache(Pathname.glob(cache/"Cask/#{cask.token}--*"))
rm_ds_store([cask.caskroom_path]) if ds_store
cleanup_lockfiles(CaskLock.new(cask.token).path)
end
def cleanup_keg(keg)
cleanup_path(keg) { keg.uninstall(raise_failures: true) }
rescue Errno::EACCES, Errno::ENOTEMPTY => e
opoo e.message
unremovable_kegs << keg
end
def cleanup_logs
return unless HOMEBREW_LOGS.directory?
logs_days = [days, CLEANUP_DEFAULT_DAYS].min
HOMEBREW_LOGS.subdirs.each do |dir|
cleanup_path(dir) { dir.rmtree } if dir.prune?(logs_days)
end
end
def cleanup_unreferenced_downloads
return if dry_run?
return unless (cache/"downloads").directory?
downloads = (cache/"downloads").children
referenced_downloads = [cache, cache/"Cask"].select(&:directory?)
.flat_map(&:children)
.select(&:symlink?)
.map(&:resolved_path)
(downloads - referenced_downloads).each do |download|
if download.incomplete?
begin
LockFile.new(download.basename).with_lock do
download.unlink
end
rescue OperationInProgressError
# Skip incomplete downloads which are still in progress.
next
end
elsif download.directory?
FileUtils.rm_rf download
else
download.unlink
end
end
end
def cleanup_cache(entries = nil)
entries ||= [cache, cache/"Cask"].select(&:directory?).flat_map(&:children)
entries.each do |path|
next if path == PERIODIC_CLEAN_FILE
FileUtils.chmod_R 0755, path if path.go_cache_directory? && !dry_run?
next cleanup_path(path) { path.unlink } if path.incomplete?
next cleanup_path(path) { FileUtils.rm_rf path } if path.nested_cache?
if path.prune?(days)
if path.file? || path.symlink?
cleanup_path(path) { path.unlink }
elsif path.directory? && path.to_s.include?("--")
cleanup_path(path) { FileUtils.rm_rf path }
end
next
end
# If we've specified --prune don't do the (expensive) .stale? check.
cleanup_path(path) { path.unlink } if !prune? && path.stale?(scrub: scrub?)
end
cleanup_unreferenced_downloads
end
def cleanup_path(path)
return unless path.exist?
return unless @cleaned_up_paths.add?(path)
@disk_cleanup_size += path.disk_usage
if dry_run?
puts "Would remove: #{path} (#{path.abv})"
else
puts "Removing: #{path}... (#{path.abv})"
yield
end
end
def cleanup_lockfiles(*lockfiles)
return if dry_run?
lockfiles = HOMEBREW_LOCKS.children.select(&:file?) if lockfiles.empty? && HOMEBREW_LOCKS.directory?
lockfiles.each do |file|
next unless file.readable?
next unless file.open(File::RDWR).flock(File::LOCK_EX | File::LOCK_NB)
begin
file.unlink
ensure
file.open(File::RDWR).flock(File::LOCK_UN) if file.exist?
end
end
end
def cleanup_portable_ruby
vendor_dir = HOMEBREW_LIBRARY/"Homebrew/vendor"
portable_ruby_latest_version = (vendor_dir/"portable-ruby-version").read.chomp
portable_rubies_to_remove = []
Pathname.glob(vendor_dir/"portable-ruby/*.*").select(&:directory?).each do |path|
next if !use_system_ruby? && portable_ruby_latest_version == path.basename.to_s
portable_rubies_to_remove << path
end
return if portable_rubies_to_remove.empty?
bundler_path = vendor_dir/"bundle/ruby"
if dry_run?
puts Utils.popen_read("git", "-C", HOMEBREW_REPOSITORY, "clean", "-nx", bundler_path).chomp
else
puts Utils.popen_read("git", "-C", HOMEBREW_REPOSITORY, "clean", "-ffqx", bundler_path).chomp
end
portable_rubies_to_remove.each do |portable_ruby|
cleanup_path(portable_ruby) { portable_ruby.rmtree }
end
end
def use_system_ruby?; end
def cleanup_bootsnap
bootsnap = cache/"bootsnap"
return unless bootsnap.exist?
cleanup_path(bootsnap) { bootsnap.rmtree }
end
def cleanup_cache_db(rack = nil)
FileUtils.rm_rf [
cache/"desc_cache.json",
cache/"linkage.db",
cache/"linkage.db.db",
]
CacheStoreDatabase.use(:linkage) do |db|
break unless db.created?
db.each_key do |keg|
next if rack.present? && !keg.start_with?("#{rack}/")
next if File.directory?(keg)
LinkageCacheStore.new(keg, db).delete!
end
end
end
def rm_ds_store(dirs = nil)
dirs ||= Keg::MUST_EXIST_DIRECTORIES + [
HOMEBREW_PREFIX/"Caskroom",
]
dirs.select(&:directory?)
.flat_map { |d| Pathname.glob("#{d}/**/.DS_Store") }
.each do |dir|
dir.unlink
rescue Errno::EACCES
# don't care if we can't delete a .DS_Store
nil
end
end
def cleanup_python_site_packages
pyc_files = Hash.new { |h, k| h[k] = [] }
seen_non_pyc_file = Hash.new { |h, k| h[k] = false }
unused_pyc_files = []
HOMEBREW_PREFIX.glob("lib/python*/site-packages").each do |site_packages|
site_packages.each_child do |child|
next unless child.directory?
# TODO: Work out a sensible way to clean up pip's, setuptools', and wheel's
# {dist,site}-info directories. Alternatively, consider always removing
# all `-info` directories, because we may not be making use of them.
next if child.basename.to_s.end_with?("-info")
# Clean up old *.pyc files in the top-level __pycache__.
if child.basename.to_s == "__pycache__"
child.find do |path|
next unless path.extname == ".pyc"
next unless path.prune?(days)
unused_pyc_files << path
end
next
end
# Look for directories that contain only *.pyc files.
child.find do |path|
next if path.directory?
if path.extname == ".pyc"
pyc_files[child] << path
else
seen_non_pyc_file[child] = true
break
end
end
end
end
unused_pyc_files += pyc_files.reject { |k,| seen_non_pyc_file[k] }
.values
.flatten
return if unused_pyc_files.blank?
unused_pyc_files.each do |pyc|
cleanup_path(pyc) { pyc.unlink }
end
end
def prune_prefix_symlinks_and_directories
ObserverPathnameExtension.reset_counts!
dirs = []
Keg::MUST_EXIST_SUBDIRECTORIES.each do |dir|
next unless dir.directory?
dir.find do |path|
path.extend(ObserverPathnameExtension)
if path.symlink?
unless path.resolved_path_exists?
path.uninstall_info if path.to_s.match?(Keg::INFOFILE_RX) && !dry_run?
if dry_run?
puts "Would remove (broken link): #{path}"
else
path.unlink
end
end
elsif path.directory? && Keg::MUST_EXIST_SUBDIRECTORIES.exclude?(path)
dirs << path
end
end
end
dirs.reverse_each do |d|
if dry_run? && d.children.empty?
puts "Would remove (empty directory): #{d}"
else
d.rmdir_if_possible
end
end
return if dry_run?
return if ObserverPathnameExtension.total.zero?
n, d = ObserverPathnameExtension.counts
print "Pruned #{n} symbolic links "
print "and #{d} directories " if d.positive?
puts "from #{HOMEBREW_PREFIX}"
end
def self.autoremove(dry_run: false)
require "utils/autoremove"
require "cask/caskroom"
# If this runs after install, uninstall, reinstall or upgrade,
# the cache of installed formulae may no longer be valid.
Formula.clear_cache unless dry_run
formulae = Formula.installed
# Remove formulae listed in HOMEBREW_NO_CLEANUP_FORMULAE and their dependencies.
if Homebrew::EnvConfig.no_cleanup_formulae.present?
formulae -= formulae.select(&method(:skip_clean_formula?))
.flat_map { |f| [f, *f.runtime_formula_dependencies] }
end
casks = Cask::Caskroom.casks
removable_formulae = Utils::Autoremove.removable_formulae(formulae, casks)
return if removable_formulae.blank?
formulae_names = removable_formulae.map(&:full_name).sort
verb = dry_run ? "Would autoremove" : "Autoremoving"
oh1 "#{verb} #{formulae_names.count} unneeded #{"formula".pluralize(formulae_names.count)}:"
puts formulae_names.join("\n")
return if dry_run
require "uninstall"
kegs_by_rack = removable_formulae.map(&:any_installed_keg).group_by(&:rack)
Uninstall.uninstall_kegs(kegs_by_rack)
# The installed formula cache will be invalid after uninstalling.
Formula.clear_cache
end
end
end
require "extend/os/cleanup"
# typed: true
# frozen_string_literal: true
require "ostruct"
module Homebrew
module CLI
class Args < OpenStruct
extend T::Sig
attr_reader :options_only, :flags_only
# undefine tap to allow --tap argument
undef tap
sig { void }
def initialize
require "cli/named_args"
super()
@processed_options = []
@options_only = []
@flags_only = []
@cask_options = false
# Can set these because they will be overwritten by freeze_named_args!
# (whereas other values below will only be overwritten if passed).
self[:named] = NamedArgs.new(parent: self)
self[:remaining] = []
end
def freeze_remaining_args!(remaining_args)
self[:remaining] = remaining_args.freeze
end
def freeze_named_args!(named_args, cask_options:)
self[:named] = NamedArgs.new(
*named_args.freeze,
override_spec: spec(nil),
force_bottle: self[:force_bottle?],
flags: flags_only,
cask_options: cask_options,
parent: self,
)
end
def freeze_processed_options!(processed_options)
# Reset cache values reliant on processed_options
@cli_args = nil
@processed_options += processed_options
@processed_options.freeze
@options_only = cli_args.select { |a| a.start_with?("-") }.freeze
@flags_only = cli_args.select { |a| a.start_with?("--") }.freeze
end
sig { returns(NamedArgs) }
def named
require "formula"
self[:named]
end
def no_named?
named.blank?
end
def build_from_source_formulae
if build_from_source? || self[:HEAD?] || self[:build_bottle?]
named.to_formulae.map(&:full_name)
else
[]
end
end
def include_test_formulae
if include_test?
named.to_formulae.map(&:full_name)
else
[]
end
end
def value(name)
arg_prefix = "--#{name}="
flag_with_value = flags_only.find { |arg| arg.start_with?(arg_prefix) }
return unless flag_with_value
flag_with_value.delete_prefix(arg_prefix)
end
sig { returns(Context::ContextStruct) }
def context
Context::ContextStruct.new(debug: debug?, quiet: quiet?, verbose: verbose?)
end
def only_formula_or_cask
return :formula if formula? && !cask?
return :cask if cask? && !formula?
end
private
def option_to_name(option)
option.sub(/\A--?/, "")
.tr("-", "_")
end
def cli_args
return @cli_args if @cli_args
@cli_args = []
@processed_options.each do |short, long|
option = long || short
switch = "#{option_to_name(option)}?".to_sym
flag = option_to_name(option).to_sym
if @table[switch] == true || @table[flag] == true
@cli_args << option
elsif @table[flag].instance_of? String
@cli_args << "#{option}=#{@table[flag]}"
elsif @table[flag].instance_of? Array
@cli_args << "#{option}=#{@table[flag].join(",")}"
end
end
@cli_args.freeze
end
def spec(default = :stable)
if self[:HEAD?]
:head
else
default
end
end
def respond_to_missing?(method_name, *)
@table.key?(method_name)
end
def method_missing(method_name, *args)
return_value = super
# Once we are frozen, verify any arg method calls are already defined in the table.
# The default OpenStruct behaviour is to return nil for anything unknown.
if frozen? && args.empty? && [email protected]?(method_name)
raise NoMethodError, "CLI arg for `#{method_name}` is not declared for this command"
end
return_value
end
end
end
end
# typed: strict
module Homebrew
module CLI
class Args < OpenStruct
sig { returns(T::Boolean) }
def remove_bottle_block?; end
sig { returns(T::Boolean) }
def strict?; end
sig { returns(T::Boolean) }
def HEAD?; end
sig { returns(T::Boolean) }
def include_test?; end
sig { returns(T::Boolean) }
def build_bottle?; end
sig { returns(T::Boolean) }
def build_universal?; end
sig { returns(T::Boolean) }
def build_from_source?; end
sig { returns(T::Boolean) }
def force_bottle?; end
sig { returns(T::Boolean) }
def newer_only?; end
sig { returns(T::Boolean) }
def resources?; end
sig { returns(T::Boolean) }
def full_name?; end
sig { returns(T::Boolean) }
def json?; end
sig { returns(T::Boolean) }
def debug?; end
sig { returns(T::Boolean) }
def quiet?; end
sig { returns(T::Boolean) }
def verbose?; end
sig { returns(T::Boolean) }
def fetch_HEAD?; end
sig { returns(T::Boolean) }
def cask?; end
sig { returns(T::Boolean) }
def dry_run?; end
sig { returns(T::Boolean) }
def skip_cask_deps?; end
sig { returns(T::Boolean) }
def greedy?; end
sig { returns(T::Boolean) }
def force?; end
sig { returns(T::Boolean) }
def ignore_pinned?; end
sig { returns(T::Boolean) }
def display_times?; end
sig { returns(T::Boolean) }
def formula?; end
sig { returns(T::Boolean) }
def zap?; end
sig { returns(T::Boolean) }
def ignore_dependencies?; end
sig { returns(T::Boolean) }
def aliases?; end
sig { returns(T::Boolean) }
def fix?; end
sig { returns(T::Boolean) }
def keep_tmp?; end
sig { returns(T::Boolean) }
def debug_symbols?; end
sig { returns(T::Boolean) }
def overwrite?; end
sig { returns(T::Boolean) }
def silent?; end
sig { returns(T::Boolean) }
def repair?; end
sig { returns(T::Boolean) }
def prune_prefix?; end
sig { returns(T::Boolean) }
def upload?; end
sig { returns(T::Boolean) }
def linux?; end
sig { returns(T::Boolean) }
def linux_self_hosted?; end
sig { returns(T::Boolean) }
def linux_wheezy?; end
sig { returns(T::Boolean) }
def total?; end
sig { returns(T::Boolean) }
def dependents?; end
sig { returns(T::Boolean) }
def installed?; end
sig { returns(T::Boolean) }
def installed_on_request?; end
sig { returns(T::Boolean) }
def installed_as_dependency?; end
sig { returns(T::Boolean) }
def all?; end
sig { returns(T::Boolean) }
def eval_all?; end
sig { returns(T::Boolean) }
def full?; end
sig { returns(T::Boolean) }
def list_pinned?; end
sig { returns(T::Boolean) }
def display_cop_names?; end
sig { returns(T::Boolean) }
def syntax?; end
sig { returns(T::Boolean) }
def no_simulate?; end
sig { returns(T::Boolean) }
def ignore_non_pypi_packages?; end
sig { returns(T::Boolean) }
def test?; end
sig { returns(T::Boolean) }
def reverse?; end
sig { returns(T::Boolean) }
def print_only?; end
sig { returns(T::Boolean) }
def markdown?; end
sig { returns(T::Boolean) }
def reset_cache?; end
sig { returns(T::Boolean) }
def major?; end
sig { returns(T::Boolean) }
def minor?; end
sig { returns(T.nilable(String)) }
def bottle_tag; end
sig { returns(T.nilable(String)) }
def tag; end
sig { returns(T.nilable(String)) }
def tap; end
sig { returns(T.nilable(T::Array[String])) }
def macos; end
sig { returns(T.nilable(T::Array[String])) }
def hide; end
sig { returns(T.nilable(String)) }
def version; end
sig { returns(T.nilable(String)) }
def name; end
sig { returns(T::Boolean) }
def no_publish?; end
sig { returns(T::Boolean) }
def shallow?; end
sig { returns(T::Boolean) }
def fail_if_not_changed?; end
sig { returns(T.nilable(String)) }
def limit; end
sig { returns(T.nilable(String)) }
def start_with; end
sig { returns(T.nilable(String)) }
def message; end
sig { returns(T.nilable(String)) }
def timeout; end
sig { returns(T.nilable(String)) }
def issue; end
sig { returns(T.nilable(String)) }
def workflow; end
sig { returns(T.nilable(String)) }
def package_name; end
sig { returns(T.nilable(String)) }
def prune; end
sig { returns(T.nilable(T::Array[String])) }
def only_cops; end
sig { returns(T.nilable(T::Array[String])) }
def except_cops; end
sig { returns(T.nilable(T::Array[String])) }
def only; end
sig { returns(T.nilable(T::Array[String])) }
def except; end
sig { returns(T.nilable(T::Array[String])) }
def mirror; end
sig { returns(T.nilable(T::Array[String])) }
def without_labels; end
sig { returns(T.nilable(T::Array[String])) }
def workflows; end
sig { returns(T.nilable(T::Array[String])) }
def ignore_missing_artifacts; end
sig { returns(T.nilable(T::Array[String])) }
def language; end
sig { returns(T.nilable(T::Array[String])) }
def extra_packages; end
sig { returns(T.nilable(T::Array[String])) }
def exclude_packages; end
sig { returns(T.nilable(T::Array[String])) }
def update; end
sig { returns(T::Boolean) }
def s?; end
sig { returns(T.nilable(String)) }
def appdir; end
sig { returns(T.nilable(String)) }
def fontdir; end
sig { returns(T.nilable(String)) }
def colorpickerdir; end
sig { returns(T.nilable(String)) }
def prefpanedir; end
sig { returns(T.nilable(String)) }
def qlplugindir; end
sig { returns(T.nilable(String)) }
def dictionarydir; end
sig { returns(T.nilable(String)) }
def servicedir; end
sig { returns(T.nilable(String)) }
def input_methoddir; end
sig { returns(T.nilable(String)) }
def mdimporterdir; end
sig { returns(T.nilable(String)) }
def internet_plugindir; end
sig { returns(T.nilable(String)) }
def audio_unit_plugindir; end
sig { returns(T.nilable(String)) }
def vst_plugindir; end
sig { returns(T.nilable(String)) }
def vst3_plugindir; end
sig { returns(T.nilable(String)) }
def screen_saverdir; end
sig { returns(T::Array[String])}
def repositories; end
sig { returns(T.nilable(String)) }
def from; end
sig { returns(T.nilable(String)) }
def to; end
sig { returns(T.nilable(T::Array[String])) }
def groups; end
sig { returns(T::Boolean) }
def write_only?; end
sig { returns(T::Boolean) }
def custom_remote?; end
sig { returns(T::Boolean) }
def print_path?; end
sig { returns(T.nilable(T::Boolean)) }
def force_auto_update?; end
end
end
end
# typed: false
# frozen_string_literal: true
require "delegate"
require "api"
require "cli/args"
module Homebrew
module CLI
# Helper class for loading formulae/casks from named arguments.
#
# @api private
class NamedArgs < Array
extend T::Sig
def initialize(*args, parent: Args.new, override_spec: nil, force_bottle: false, flags: [], cask_options: false)
require "cask/cask"
require "cask/cask_loader"
require "formulary"
require "keg"
require "missing_formula"
@args = args
@override_spec = override_spec
@force_bottle = force_bottle
@flags = flags
@cask_options = cask_options
@parent = parent
super(@args)
end
attr_reader :parent
def to_casks
@to_casks ||= to_formulae_and_casks(only: :cask).freeze
end
def to_formulae
@to_formulae ||= to_formulae_and_casks(only: :formula).freeze
end
# Convert named arguments to {Formula} or {Cask} objects.
# If both a formula and cask with the same name exist, returns
# the formula and prints a warning unless `only` is specified.
sig {
params(
only: T.nilable(Symbol),
ignore_unavailable: T.nilable(T::Boolean),
method: T.nilable(Symbol),
uniq: T::Boolean,
).returns(T::Array[T.any(Formula, Keg, Cask::Cask)])
}
def to_formulae_and_casks(only: parent&.only_formula_or_cask, ignore_unavailable: nil, method: nil, uniq: true)
@to_formulae_and_casks ||= {}
@to_formulae_and_casks[only] ||= downcased_unique_named.flat_map do |name|
load_formula_or_cask(name, only: only, method: method)
rescue FormulaUnreadableError, FormulaClassUnavailableError,
TapFormulaUnreadableError, TapFormulaClassUnavailableError,
Cask::CaskUnreadableError
# Need to rescue before `*UnavailableError` (superclass of this)
# The formula/cask was found, but there's a problem with its implementation
raise
rescue NoSuchKegError, FormulaUnavailableError, Cask::CaskUnavailableError, FormulaOrCaskUnavailableError
ignore_unavailable ? [] : raise
end.freeze
if uniq
@to_formulae_and_casks[only].uniq.freeze
else
@to_formulae_and_casks[only]
end
end
def to_formulae_to_casks(only: parent&.only_formula_or_cask, method: nil)
@to_formulae_to_casks ||= {}
@to_formulae_to_casks[[method, only]] = to_formulae_and_casks(only: only, method: method)
.partition { |o| o.is_a?(Formula) || o.is_a?(Keg) }
.map(&:freeze).freeze
end
def to_formulae_and_casks_and_unavailable(only: parent&.only_formula_or_cask, method: nil)
@to_formulae_casks_unknowns ||= {}
@to_formulae_casks_unknowns[method] = downcased_unique_named.map do |name|
load_formula_or_cask(name, only: only, method: method)
rescue FormulaOrCaskUnavailableError => e
e
end.uniq.freeze
end
def load_formula_or_cask(name, only: nil, method: nil)
unreadable_error = nil
if only != :cask
begin
formula = case method
when nil, :factory
Formulary.factory(name, *spec, force_bottle: @force_bottle, flags: @flags)
when :resolve
resolve_formula(name)
when :latest_kegs
resolve_latest_keg(name)
when :default_kegs
resolve_default_keg(name)
when :kegs
_, kegs = resolve_kegs(name)
kegs
else
raise
end
warn_if_cask_conflicts(name, "formula") unless only == :formula
return formula
rescue FormulaUnreadableError, FormulaClassUnavailableError,
TapFormulaUnreadableError, TapFormulaClassUnavailableError => e
# Need to rescue before `FormulaUnavailableError` (superclass of this)
# The formula was found, but there's a problem with its implementation
unreadable_error ||= e
rescue NoSuchKegError, FormulaUnavailableError => e
raise e if only == :formula
end
end
if only != :formula
want_keg_like_cask = [:latest_kegs, :default_kegs, :kegs].include?(method)
begin
config = Cask::Config.from_args(@parent) if @cask_options
cask = Cask::CaskLoader.load(name, config: config)
if unreadable_error.present?
onoe <<~EOS
Failed to load formula: #{name}
#{unreadable_error}
EOS
opoo "Treating #{name} as a cask."
end
# If we're trying to get a keg-like Cask, do our best to use the same cask
# file that was used for installation, if possible.
if want_keg_like_cask && (installed_caskfile = cask.installed_caskfile) && installed_caskfile.exist?
cask = Cask::CaskLoader.load(installed_caskfile)
end
return cask
rescue Cask::CaskUnreadableError, Cask::CaskInvalidError => e
# If we're trying to get a keg-like Cask, do our best to handle it
# not being readable and return something that can be used.
if want_keg_like_cask
cask_version = Cask::Cask.new(name, config: config).versions.first
cask = Cask::Cask.new(name, config: config) do
version cask_version if cask_version
end
return cask
end
# Need to rescue before `CaskUnavailableError` (superclass of this)
# The cask was found, but there's a problem with its implementation
unreadable_error ||= e
rescue Cask::CaskUnavailableError => e
raise e if only == :cask
end
end
raise unreadable_error if unreadable_error.present?
user, repo, short_name = name.downcase.split("/", 3)
if repo.present? && short_name.present?
tap = Tap.fetch(user, repo)
raise TapFormulaOrCaskUnavailableError.new(tap, short_name)
end
raise NoSuchKegError, name if resolve_formula(name)
raise FormulaOrCaskUnavailableError, name
end
private :load_formula_or_cask
def resolve_formula(name)
Formulary.resolve(name, spec: spec, force_bottle: @force_bottle, flags: @flags)
end
private :resolve_formula
sig { params(uniq: T::Boolean).returns(T::Array[Formula]) }
def to_resolved_formulae(uniq: true)
@to_resolved_formulae ||= to_formulae_and_casks(only: :formula, method: :resolve, uniq: uniq)
.freeze
end
def to_resolved_formulae_to_casks(only: parent&.only_formula_or_cask)
to_formulae_to_casks(only: only, method: :resolve)
end
# Keep existing paths and try to convert others to tap, formula or cask paths.
# If a cask and formula with the same name exist, includes both their paths
# unless `only` is specified.
sig { params(only: T.nilable(Symbol), recurse_tap: T::Boolean).returns(T::Array[Pathname]) }
def to_paths(only: parent&.only_formula_or_cask, recurse_tap: false)
@to_paths ||= {}
@to_paths[only] ||= downcased_unique_named.flat_map do |name|
if File.exist?(name)
Pathname(name)
elsif name.count("/") == 1 && !name.start_with?("./", "/")
tap = Tap.fetch(name)
if recurse_tap
next tap.formula_files if only == :formula
next tap.cask_files if only == :cask
end
tap.path
else
next Formulary.path(name) if only == :formula
next Cask::CaskLoader.path(name) if only == :cask
formula_path = Formulary.path(name)
cask_path = Cask::CaskLoader.path(name)
paths = []
paths << formula_path if formula_path.exist?
paths << cask_path if cask_path.exist?
paths.empty? ? Pathname(name) : paths
end
end.uniq.freeze
end
sig { returns(T::Array[Keg]) }
def to_default_kegs
@to_default_kegs ||= begin
to_formulae_and_casks(only: :formula, method: :default_kegs).freeze
rescue NoSuchKegError => e
if (reason = MissingFormula.suggest_command(e.name, "uninstall"))
$stderr.puts reason
end
raise e
end
end
sig { returns(T::Array[Keg]) }
def to_latest_kegs
@to_latest_kegs ||= begin
to_formulae_and_casks(only: :formula, method: :latest_kegs).freeze
rescue NoSuchKegError => e
if (reason = MissingFormula.suggest_command(e.name, "uninstall"))
$stderr.puts reason
end
raise e
end
end
sig { returns(T::Array[Keg]) }
def to_kegs
@to_kegs ||= begin
to_formulae_and_casks(only: :formula, method: :kegs).freeze
rescue NoSuchKegError => e
if (reason = MissingFormula.suggest_command(e.name, "uninstall"))
$stderr.puts reason
end
raise e
end
end
sig {
params(only: T.nilable(Symbol), ignore_unavailable: T.nilable(T::Boolean), all_kegs: T.nilable(T::Boolean))
.returns([T::Array[Keg], T::Array[Cask::Cask]])
}
def to_kegs_to_casks(only: parent&.only_formula_or_cask, ignore_unavailable: nil, all_kegs: nil)
method = all_kegs ? :kegs : :default_kegs
@to_kegs_to_casks ||= {}
@to_kegs_to_casks[method] ||=
to_formulae_and_casks(only: only, ignore_unavailable: ignore_unavailable, method: method)
.partition { |o| o.is_a?(Keg) }
.map(&:freeze).freeze
end
sig { returns(T::Array[Tap]) }
def to_taps
@to_taps ||= downcased_unique_named.map { |name| Tap.fetch name }.uniq.freeze
end
sig { returns(T::Array[Tap]) }
def to_installed_taps
@to_installed_taps ||= to_taps.each do |tap|
raise TapUnavailableError, tap.name unless tap.installed?
end.uniq.freeze
end
sig { returns(T::Array[String]) }
def homebrew_tap_cask_names
downcased_unique_named.grep(HOMEBREW_CASK_TAP_CASK_REGEX)
end
private
sig { returns(T::Array[String]) }
def downcased_unique_named
# Only lowercase names, not paths, bottle filenames or URLs
map do |arg|
if arg.include?("/") || arg.end_with?(".tar.gz") || File.exist?(arg)
arg
else
arg.downcase
end
end.uniq
end
def spec
@override_spec
end
private :spec
def resolve_kegs(name)
raise UsageError if name.blank?
require "keg"
rack = Formulary.to_rack(name.downcase)
kegs = rack.directory? ? rack.subdirs.map { |d| Keg.new(d) } : []
raise NoSuchKegError, name if kegs.none?
[rack, kegs]
end
def resolve_latest_keg(name)
_, kegs = resolve_kegs(name)
# Return keg if it is the only installed keg
return kegs if kegs.length == 1
stable_kegs = kegs.reject { |k| k.version.head? }
if stable_kegs.blank?
return kegs.max_by do |keg|
[Tab.for_keg(keg).source_modified_time, keg.version.revision]
end
end
stable_kegs.max_by(&:version)
end
def resolve_default_keg(name)
rack, kegs = resolve_kegs(name)
linked_keg_ref = HOMEBREW_LINKED_KEGS/rack.basename
opt_prefix = HOMEBREW_PREFIX/"opt/#{rack.basename}"
begin
return Keg.new(opt_prefix.resolved_path) if opt_prefix.symlink? && opt_prefix.directory?
return Keg.new(linked_keg_ref.resolved_path) if linked_keg_ref.symlink? && linked_keg_ref.directory?
return kegs.first if kegs.length == 1
f = if name.include?("/") || File.exist?(name)
Formulary.factory(name)
else
Formulary.from_rack(rack)
end
unless (prefix = f.latest_installed_prefix).directory?
raise MultipleVersionsInstalledError, <<~EOS
#{rack.basename} has multiple installed versions
Run `brew uninstall --force #{rack.basename}` to remove all versions.
EOS
end
Keg.new(prefix)
rescue FormulaUnavailableError
raise MultipleVersionsInstalledError, <<~EOS
Multiple kegs installed to #{rack}
However we don't know which one you refer to.
Please delete (with rm -rf!) all but one and then try again.
EOS
end
end
def warn_if_cask_conflicts(ref, loaded_type)
message = "Treating #{ref} as a #{loaded_type}."
begin
cask = Cask::CaskLoader.load ref
message += " For the cask, use #{cask.tap.name}/#{cask.token}" if cask.tap.present?
rescue Cask::CaskUnreadableError => e
# Need to rescue before `CaskUnavailableError` (superclass of this)
# The cask was found, but there's a problem with its implementation
onoe <<~EOS
Failed to load cask: #{ref}
#{e}
EOS
rescue Cask::CaskUnavailableError
# No ref conflict with a cask, do nothing
return
end
opoo message.freeze
end
end
end
end
# typed: false
# frozen_string_literal: true
require "env_config"
require "cask/config"
require "cli/args"
require "optparse"
require "set"
require "utils/tty"
COMMAND_DESC_WIDTH = 80
OPTION_DESC_WIDTH = 45
HIDDEN_DESC_PLACEHOLDER = "@@HIDDEN@@"
module Homebrew
module CLI
class Parser
extend T::Sig
attr_reader :processed_options, :hide_from_man_page, :named_args_type
def self.from_cmd_path(cmd_path)
cmd_args_method_name = Commands.args_method_name(cmd_path)
begin
Homebrew.send(cmd_args_method_name) if require?(cmd_path)
rescue NoMethodError => e
raise if e.name.to_sym != cmd_args_method_name
nil
end
end
def self.global_cask_options
[
[:flag, "--appdir=", {
description: "Target location for Applications " \
"(default: `#{Cask::Config::DEFAULT_DIRS[:appdir]}`).",
}],
[:flag, "--colorpickerdir=", {
description: "Target location for Color Pickers " \
"(default: `#{Cask::Config::DEFAULT_DIRS[:colorpickerdir]}`).",
}],
[:flag, "--prefpanedir=", {
description: "Target location for Preference Panes " \
"(default: `#{Cask::Config::DEFAULT_DIRS[:prefpanedir]}`).",
}],
[:flag, "--qlplugindir=", {
description: "Target location for QuickLook Plugins " \
"(default: `#{Cask::Config::DEFAULT_DIRS[:qlplugindir]}`).",
}],
[:flag, "--mdimporterdir=", {
description: "Target location for Spotlight Plugins " \
"(default: `#{Cask::Config::DEFAULT_DIRS[:mdimporterdir]}`).",
}],
[:flag, "--dictionarydir=", {
description: "Target location for Dictionaries " \
"(default: `#{Cask::Config::DEFAULT_DIRS[:dictionarydir]}`).",
}],
[:flag, "--fontdir=", {
description: "Target location for Fonts " \
"(default: `#{Cask::Config::DEFAULT_DIRS[:fontdir]}`).",
}],
[:flag, "--servicedir=", {
description: "Target location for Services " \
"(default: `#{Cask::Config::DEFAULT_DIRS[:servicedir]}`).",
}],
[:flag, "--input-methoddir=", {
description: "Target location for Input Methods " \
"(default: `#{Cask::Config::DEFAULT_DIRS[:input_methoddir]}`).",
}],
[:flag, "--internet-plugindir=", {
description: "Target location for Internet Plugins " \
"(default: `#{Cask::Config::DEFAULT_DIRS[:internet_plugindir]}`).",
}],
[:flag, "--audio-unit-plugindir=", {
description: "Target location for Audio Unit Plugins " \
"(default: `#{Cask::Config::DEFAULT_DIRS[:audio_unit_plugindir]}`).",
}],
[:flag, "--vst-plugindir=", {
description: "Target location for VST Plugins " \
"(default: `#{Cask::Config::DEFAULT_DIRS[:vst_plugindir]}`).",
}],
[:flag, "--vst3-plugindir=", {
description: "Target location for VST3 Plugins " \
"(default: `#{Cask::Config::DEFAULT_DIRS[:vst3_plugindir]}`).",
}],
[:flag, "--screen-saverdir=", {
description: "Target location for Screen Savers " \
"(default: `#{Cask::Config::DEFAULT_DIRS[:screen_saverdir]}`).",
}],
[:comma_array, "--language", {
description: "Comma-separated list of language codes to prefer for cask installation. " \
"The first matching language is used, otherwise it reverts to the cask's " \
"default language. The default value is the language of your system.",
}],
]
end
sig { returns(T::Array[[String, String, String]]) }
def self.global_options
[
["-d", "--debug", "Display any debugging information."],
["-q", "--quiet", "Make some output more quiet."],
["-v", "--verbose", "Make some output more verbose."],
["-h", "--help", "Show this message."],
]
end
sig { params(block: T.nilable(T.proc.bind(Parser).void)).void }
def initialize(&block)
@parser = OptionParser.new
@parser.summary_indent = " " * 2
# Disable default handling of `--version` switch.
@parser.base.long.delete("version")
# Disable default handling of `--help` switch.
@parser.base.long.delete("help")
@args = Homebrew::CLI::Args.new
# Filter out Sorbet runtime type checking method calls.
cmd_location = caller_locations.select { |location| location.path.exclude?("/gems/sorbet-runtime-") }.second
@command_name = cmd_location.label.chomp("_args").tr("_", "-")
@is_dev_cmd = cmd_location.absolute_path.start_with?(Commands::HOMEBREW_DEV_CMD_PATH)
@constraints = []
@conflicts = []
@switch_sources = {}
@processed_options = []
@non_global_processed_options = []
@named_args_type = nil
@max_named_args = nil
@min_named_args = nil
@description = nil
@usage_banner = nil
@hide_from_man_page = false
@formula_options = false
@cask_options = false
self.class.global_options.each do |short, long, desc|
switch short, long, description: desc, env: option_to_name(long), method: :on_tail
end
instance_eval(&block) if block
generate_banner
end
def switch(*names, description: nil, replacement: nil, env: nil, depends_on: nil,
method: :on, hidden: false)
global_switch = names.first.is_a?(Symbol)
return if global_switch
description = option_description(description, *names, hidden: hidden)
if replacement.nil?
process_option(*names, description, type: :switch, hidden: hidden)
else
description += " (disabled#{"; replaced by #{replacement}" if replacement.present?})"
end
@parser.public_send(method, *names, *wrap_option_desc(description)) do |value|
odisabled "the `#{names.first}` switch", replacement unless replacement.nil?
value = true if names.none? { |name| name.start_with?("--[no-]") }
set_switch(*names, value: value, from: :args)
end
names.each do |name|
set_constraints(name, depends_on: depends_on)
end
env_value = env?(env)
set_switch(*names, value: env_value, from: :env) unless env_value.nil?
end
alias switch_option switch
def env?(env)
return if env.blank?
Homebrew::EnvConfig.try(:"#{env}?")
end
def description(text = nil)
return @description if text.blank?
@description = text.chomp
end
def usage_banner(text)
@usage_banner, @description = text.chomp.split("\n\n", 2)
end
def usage_banner_text
@parser.banner
end
def comma_array(name, description: nil, hidden: false)
name = name.chomp "="
description = option_description(description, name, hidden: hidden)
process_option(name, description, type: :comma_array, hidden: hidden)
@parser.on(name, OptionParser::REQUIRED_ARGUMENT, Array, *wrap_option_desc(description)) do |list|
@args[option_to_name(name)] = list
end
end
def flag(*names, description: nil, replacement: nil, depends_on: nil, hidden: false)
required, flag_type = if names.any? { |name| name.end_with? "=" }
[OptionParser::REQUIRED_ARGUMENT, :required_flag]
else
[OptionParser::OPTIONAL_ARGUMENT, :optional_flag]
end
names.map! { |name| name.chomp "=" }
description = option_description(description, *names, hidden: hidden)
if replacement.nil?
process_option(*names, description, type: flag_type, hidden: hidden)
else
description += " (disabled#{"; replaced by #{replacement}" if replacement.present?})"
end
@parser.on(*names, *wrap_option_desc(description), required) do |option_value|
odisabled "the `#{names.first}` flag", replacement unless replacement.nil?
names.each do |name|
@args[option_to_name(name)] = option_value
end
end
names.each do |name|
set_constraints(name, depends_on: depends_on)
end
end
def conflicts(*options)
@conflicts << options.map { |option| option_to_name(option) }
end
def option_to_name(option)
option.sub(/\A--?(\[no-\])?/, "")
.tr("-", "_")
.delete("=")
end
def name_to_option(name)
if name.length == 1
"-#{name}"
else
"--#{name.tr("_", "-")}"
end
end
def option_to_description(*names)
names.map { |name| name.to_s.sub(/\A--?/, "").tr("-", " ") }.max
end
def option_description(description, *names, hidden: false)
return HIDDEN_DESC_PLACEHOLDER if hidden
return description if description.present?
option_to_description(*names)
end
def parse_remaining(argv, ignore_invalid_options: false)
i = 0
remaining = []
argv, non_options = split_non_options(argv)
allow_commands = Array(@named_args_type).include?(:command)
while i < argv.count
begin
begin
arg = argv[i]
remaining << arg unless @parser.parse([arg]).empty?
rescue OptionParser::MissingArgument
raise if i + 1 >= argv.count
args = argv[i..(i + 1)]
@parser.parse(args)
i += 1
end
rescue OptionParser::InvalidOption
if ignore_invalid_options || (allow_commands && Commands.path(arg))
remaining << arg
else
$stderr.puts generate_help_text
raise
end
end
i += 1
end
[remaining, non_options]
end
sig { params(argv: T::Array[String], ignore_invalid_options: T::Boolean).returns(Args) }
def parse(argv = ARGV.freeze, ignore_invalid_options: false)
raise "Arguments were already parsed!" if @args_parsed
# If we accept formula options, parse once allowing invalid options
# so we can get the remaining list containing formula names.
if @formula_options
remaining, non_options = parse_remaining(argv, ignore_invalid_options: true)
argv = [*remaining, "--", *non_options]
formulae(argv).each do |f|
next if f.options.empty?
f.options.each do |o|
name = o.flag
description = "`#{f.name}`: #{o.description}"
if name.end_with? "="
flag name, description: description
else
switch name, description: description
end
conflicts "--cask", name
end
end
end
remaining, non_options = parse_remaining(argv, ignore_invalid_options: ignore_invalid_options)
named_args = if ignore_invalid_options
[]
else
remaining + non_options
end
unless ignore_invalid_options
unless @is_dev_cmd
set_default_options
validate_options
end
check_constraint_violations
check_named_args(named_args)
end
@args.freeze_named_args!(named_args, cask_options: @cask_options)
@args.freeze_remaining_args!(non_options.empty? ? remaining : [*remaining, "--", non_options])
@args.freeze_processed_options!(@processed_options)
@args.freeze
@args_parsed = true
if !ignore_invalid_options && @args.help?
puts generate_help_text
exit
end
@args
end
def set_default_options; end
def validate_options; end
def generate_help_text
Formatter.format_help_text(@parser.to_s, width: COMMAND_DESC_WIDTH)
.gsub(/\n.*?@@HIDDEN@@.*?(?=\n)/, "")
.sub(/^/, "#{Tty.bold}Usage: brew#{Tty.reset} ")
.gsub(/`(.*?)`/m, "#{Tty.bold}\\1#{Tty.reset}")
.gsub(%r{<([^\s]+?://[^\s]+?)>}) { |url| Formatter.url(url) }
.gsub(/\*(.*?)\*|<(.*?)>/m) do |underlined|
underlined[1...-1].gsub(/^(\s*)(.*?)$/, "\\1#{Tty.underline}\\2#{Tty.reset}")
end
end
def cask_options
self.class.global_cask_options.each do |args|
options = args.pop
send(*args, **options)
conflicts "--formula", args.last
end
@cask_options = true
end
sig { void }
def formula_options
@formula_options = true
end
sig {
params(
type: T.any(Symbol, T::Array[String], T::Array[Symbol]),
number: T.nilable(Integer),
min: T.nilable(Integer),
max: T.nilable(Integer),
).void
}
def named_args(type = nil, number: nil, min: nil, max: nil)
if number.present? && (min.present? || max.present?)
raise ArgumentError, "Do not specify both `number` and `min` or `max`"
end
if type == :none && (number.present? || min.present? || max.present?)
raise ArgumentError, "Do not specify both `number`, `min` or `max` with `named_args :none`"
end
@named_args_type = type
if type == :none
@max_named_args = 0
elsif number.present?
@min_named_args = @max_named_args = number
elsif min.present? || max.present?
@min_named_args = min
@max_named_args = max
end
end
sig { void }
def hide_from_man_page!
@hide_from_man_page = true
end
private
SYMBOL_TO_USAGE_MAPPING = {
text_or_regex: "<text>|`/`<regex>`/`",
url: "<URL>",
}.freeze
def generate_usage_banner
command_names = ["`#{@command_name}`"]
aliases_to_skip = %w[instal uninstal]
command_names += Commands::HOMEBREW_INTERNAL_COMMAND_ALIASES.map do |command_alias, command|
next if aliases_to_skip.include? command_alias
"`#{command_alias}`" if command == @command_name
end.compact.sort
options = if @non_global_processed_options.empty?
""
elsif @non_global_processed_options.count > 2
" [<options>]"
else
required_argument_types = [:required_flag, :comma_array]
@non_global_processed_options.map do |option, type|
next " [<#{option}>`=`]" if required_argument_types.include? type
" [<#{option}>]"
end.join
end
named_args = ""
if @named_args_type.present? && @named_args_type != :none
arg_type = if @named_args_type.is_a? Array
types = @named_args_type.map do |type|
next unless type.is_a? Symbol
next SYMBOL_TO_USAGE_MAPPING[type] if SYMBOL_TO_USAGE_MAPPING.key?(type)
"<#{type}>"
end.compact
types << "<subcommand>" if @named_args_type.any?(String)
types.join("|")
elsif SYMBOL_TO_USAGE_MAPPING.key? @named_args_type
SYMBOL_TO_USAGE_MAPPING[@named_args_type]
else
"<#{@named_args_type}>"
end
named_args = if @min_named_args.blank? && @max_named_args == 1
" [#{arg_type}]"
elsif @min_named_args.blank?
" [#{arg_type} ...]"
elsif @min_named_args == 1 && @max_named_args == 1
" #{arg_type}"
elsif @min_named_args == 1
" #{arg_type} [...]"
else
" #{arg_type} ..."
end
end
"#{command_names.join(", ")}#{options}#{named_args}"
end
def generate_banner
@usage_banner ||= generate_usage_banner
@parser.banner = <<~BANNER
#{@usage_banner}
#{@description}
BANNER
end
def set_switch(*names, value:, from:)
names.each do |name|
@switch_sources[option_to_name(name)] = from
@args["#{option_to_name(name)}?"] = value
end
end
def disable_switch(*names)
names.each do |name|
@args["#{option_to_name(name)}?"] = if name.start_with?("--[no-]")
nil
else
false
end
end
end
def option_passed?(name)
@args[name.to_sym] || @args["#{name}?".to_sym]
end
def wrap_option_desc(desc)
Formatter.format_help_text(desc, width: OPTION_DESC_WIDTH).split("\n")
end
def set_constraints(name, depends_on:)
return if depends_on.nil?
primary = option_to_name(depends_on)
secondary = option_to_name(name)
@constraints << [primary, secondary]
end
def check_constraints
@constraints.each do |primary, secondary|
primary_passed = option_passed?(primary)
secondary_passed = option_passed?(secondary)
next if !secondary_passed || (primary_passed && secondary_passed)
primary = name_to_option(primary)
secondary = name_to_option(secondary)
raise OptionConstraintError.new(primary, secondary, missing: true)
end
end
def check_conflicts
@conflicts.each do |mutually_exclusive_options_group|
violations = mutually_exclusive_options_group.select do |option|
option_passed? option
end
next if violations.count < 2
env_var_options = violations.select do |option|
@switch_sources[option_to_name(option)] == :env
end
select_cli_arg = violations.count - env_var_options.count == 1
raise OptionConflictError, violations.map(&method(:name_to_option)) unless select_cli_arg
env_var_options.each(&method(:disable_switch))
end
end
def check_invalid_constraints
@conflicts.each do |mutually_exclusive_options_group|
@constraints.each do |p, s|
next unless Set[p, s].subset?(Set[*mutually_exclusive_options_group])
raise InvalidConstraintError.new(p, s)
end
end
end
def check_constraint_violations
check_invalid_constraints
check_conflicts
check_constraints
end
def check_named_args(args)
types = Array(@named_args_type).map do |type|
next type if type.is_a? Symbol
:subcommand
end.compact.uniq
exception = if @min_named_args && @max_named_args && @min_named_args == @max_named_args &&
args.size != @max_named_args
NumberOfNamedArgumentsError.new(@min_named_args, types: types)
elsif @min_named_args && args.size < @min_named_args
MinNamedArgumentsError.new(@min_named_args, types: types)
elsif @max_named_args && args.size > @max_named_args
MaxNamedArgumentsError.new(@max_named_args, types: types)
end
raise exception if exception
end
def process_option(*args, type:, hidden: false)
option, = @parser.make_switch(args)
@processed_options.reject! { |existing| existing.second == option.long.first } if option.long.first.present?
@processed_options << [option.short.first, option.long.first, option.arg, option.desc.first, hidden]
if type == :switch
disable_switch(*args)
else
args.each do |name|
@args[option_to_name(name)] = nil
end
end
return if hidden
return if self.class.global_options.include? [option.short.first, option.long.first, option.desc.first]
@non_global_processed_options << [option.long.first || option.short.first, type]
end
def split_non_options(argv)
if (sep = argv.index("--"))
[argv.take(sep), argv.drop(sep + 1)]
else
[argv, []]
end
end
def formulae(argv)
argv, non_options = split_non_options(argv)
named_args = argv.reject { |arg| arg.start_with?("-") } + non_options
spec = if argv.include?("--HEAD")
:head
else
:stable
end
# Only lowercase names, not paths, bottle filenames or URLs
named_args.map do |arg|
next if arg.match?(HOMEBREW_CASK_TAP_CASK_REGEX)
begin
Formulary.factory(arg, spec, flags: argv.select { |a| a.start_with?("--") })
rescue FormulaUnavailableError
nil
end
end.compact.uniq(&:name)
end
end
class OptionConstraintError < UsageError
def initialize(arg1, arg2, missing: false)
message = if missing
"`#{arg2}` cannot be passed without `#{arg1}`."
else
"`#{arg1}` and `#{arg2}` should be passed together."
end
super message
end
end
class OptionConflictError < UsageError
def initialize(args)
args_list = args.map(&Formatter.public_method(:option))
.join(" and ")
super "Options #{args_list} are mutually exclusive."
end
end
class InvalidConstraintError < UsageError
def initialize(arg1, arg2)
super "`#{arg1}` and `#{arg2}` cannot be mutually exclusive and mutually dependent simultaneously."
end
end
class MaxNamedArgumentsError < UsageError
extend T::Sig
sig { params(maximum: Integer, types: T::Array[Symbol]).void }
def initialize(maximum, types: [])
super case maximum
when 0
"This command does not take named arguments."
else
types << :named if types.empty?
arg_types = types.map { |type| type.to_s.tr("_", " ") }
.to_sentence two_words_connector: " or ", last_word_connector: " or "
"This command does not take more than #{maximum} #{arg_types} #{"argument".pluralize(maximum)}."
end
end
end
class MinNamedArgumentsError < UsageError
extend T::Sig
sig { params(minimum: Integer, types: T::Array[Symbol]).void }
def initialize(minimum, types: [])
types << :named if types.empty?
arg_types = types.map { |type| type.to_s.tr("_", " ") }
.to_sentence two_words_connector: " or ", last_word_connector: " or "
super "This command requires at least #{minimum} #{arg_types} #{"argument".pluralize(minimum)}."
end
end
class NumberOfNamedArgumentsError < UsageError
extend T::Sig
sig { params(minimum: Integer, types: T::Array[Symbol]).void }
def initialize(minimum, types: [])
types << :named if types.empty?
arg_types = types.map { |type| type.to_s.tr("_", " ") }
.to_sentence two_words_connector: " or ", last_word_connector: " or "
super "This command requires exactly #{minimum} #{arg_types} #{"argument".pluralize(minimum)}."
end
end
end
end
require "extend/os/parser"
# typed: false
# frozen_string_literal: true
require "fetch"
require "cli/parser"
require "cask/download"
module Homebrew
extend T::Sig
extend Fetch
module_function
sig { returns(CLI::Parser) }
def __cache_args
Homebrew::CLI::Parser.new do
description <<~EOS
Display Homebrew's download cache. See also `HOMEBREW_CACHE`.
If <formula> is provided, display the file or directory used to cache <formula>.
EOS
switch "-s", "--build-from-source",
description: "Show the cache file used when building from source."
switch "--force-bottle",
description: "Show the cache file used when pouring a bottle."
flag "--bottle-tag=",
description: "Show the cache file used when pouring a bottle for the given tag."
switch "--HEAD",
description: "Show the cache file used when building from HEAD."
switch "--formula", "--formulae",
description: "Only show cache files for formulae."
switch "--cask", "--casks",
description: "Only show cache files for casks."
conflicts "--build-from-source", "--force-bottle", "--bottle-tag", "--HEAD", "--cask"
conflicts "--formula", "--cask"
named_args [:formula, :cask]
end
end
sig { void }
def __cache
args = __cache_args.parse
if args.no_named?
puts HOMEBREW_CACHE
return
end
formulae_or_casks = args.named.to_formulae_and_casks
formulae_or_casks.each do |formula_or_cask|
if formula_or_cask.is_a? Formula
print_formula_cache formula_or_cask, args: args
else
print_cask_cache formula_or_cask
end
end
end
sig { params(formula: Formula, args: CLI::Args).void }
def print_formula_cache(formula, args:)
if fetch_bottle?(formula, args: args)
puts formula.bottle_for_tag(args.bottle_tag&.to_sym).cached_download
elsif args.HEAD?
puts formula.head.cached_download
else
puts formula.cached_download
end
end
sig { params(cask: Cask::Cask).void }
def print_cask_cache(cask)
puts Cask::Download.new(cask).downloader.cached_location
end
end
# typed: strict
# frozen_string_literal: true
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def __caskroom_args
Homebrew::CLI::Parser.new do
description <<~EOS
Display Homebrew's Caskroom path.
If <cask> is provided, display the location in the Caskroom where <cask>
would be installed, without any sort of versioned directory as the last path.
EOS
named_args :cask
end
end
sig { void }
def __caskroom
args = __caskroom_args.parse
if args.named.to_casks.blank?
puts Cask::Caskroom.path
else
args.named.to_casks.each do |cask|
puts "#{Cask::Caskroom.path}/#{cask.token}"
end
end
end
end
# typed: true
# frozen_string_literal: true
require "cli/parser"
module Homebrew
module_function
def __cellar_args
Homebrew::CLI::Parser.new do
description <<~EOS
Display Homebrew's Cellar path. *Default:* `$(brew --prefix)/Cellar`, or if
that directory doesn't exist, `$(brew --repository)/Cellar`.
If <formula> is provided, display the location in the Cellar where <formula>
would be installed, without any sort of versioned directory as the last path.
EOS
named_args :formula
end
end
def __cellar
args = __cellar_args.parse
if args.no_named?
puts HOMEBREW_CELLAR
else
puts args.named.to_resolved_formulae.map(&:rack)
end
end
end
# typed: false
# frozen_string_literal: true
require "extend/ENV"
require "build_environment"
require "utils/shell"
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def __env_args
Homebrew::CLI::Parser.new do
description <<~EOS
Summarise Homebrew's build environment as a plain list.
If the command's output is sent through a pipe and no shell is specified,
the list is formatted for export to `bash`(1) unless `--plain` is passed.
EOS
flag "--shell=",
description: "Generate a list of environment variables for the specified shell, " \
"or `--shell=auto` to detect the current shell."
switch "--plain",
description: "Generate plain output even when piped."
named_args :formula
end
end
sig { void }
def __env
args = __env_args.parse
ENV.activate_extensions!
ENV.deps = args.named.to_formulae if superenv?(nil)
ENV.setup_build_environment
shell = if args.plain?
nil
elsif args.shell.nil?
:bash unless $stdout.tty?
elsif args.shell == "auto"
Utils::Shell.parent || Utils::Shell.preferred
elsif args.shell
Utils::Shell.from_path(args.shell)
end
if shell.nil?
BuildEnvironment.dump ENV
else
BuildEnvironment.keys(ENV.to_h).each do |key|
puts Utils::Shell.export_value(key, ENV.fetch(key), shell)
end
end
end
end
# typed: false
# frozen_string_literal: true
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def __prefix_args
Homebrew::CLI::Parser.new do
description <<~EOS
Display Homebrew's install path. *Default:*
- macOS Intel: `#{HOMEBREW_DEFAULT_PREFIX}`
- macOS ARM: `#{HOMEBREW_MACOS_ARM_DEFAULT_PREFIX}`
- Linux: `#{HOMEBREW_LINUX_DEFAULT_PREFIX}`
If <formula> is provided, display the location where <formula> is or would be installed.
EOS
switch "--unbrewed",
description: "List files in Homebrew's prefix not installed by Homebrew."
switch "--installed",
description: "Outputs nothing and returns a failing status code if <formula> is not installed."
conflicts "--unbrewed", "--installed"
named_args :formula
end
end
def __prefix
args = __prefix_args.parse
raise UsageError, "`--installed` requires a formula argument." if args.installed? && args.no_named?
if args.unbrewed?
raise UsageError, "`--unbrewed` does not take a formula argument." unless args.no_named?
list_unbrewed
elsif args.no_named?
puts HOMEBREW_PREFIX
else
formulae = args.named.to_resolved_formulae
prefixes = formulae.map do |f|
next nil if args.installed? && !f.opt_prefix.exist?
# this case will be short-circuited by brew.sh logic for a single formula
f.opt_prefix
end.compact
puts prefixes
if args.installed?
missing_formulae = formulae.reject(&:optlinked?)
.map(&:name)
return if missing_formulae.blank?
raise NotAKegError, <<~EOS
The following formulae are not installed:
#{missing_formulae.join(" ")}
EOS
end
end
end
UNBREWED_EXCLUDE_FILES = %w[.DS_Store].freeze
UNBREWED_EXCLUDE_PATHS = %w[
*/.keepme
.github/*
bin/brew
completions/zsh/_brew
docs/*
lib/gdk-pixbuf-2.0/*
lib/gio/*
lib/node_modules/*
lib/python[23].[0-9]/*
lib/python3.[0-9][0-9]/*
lib/pypy/*
lib/pypy3/*
lib/ruby/gems/[12].*
lib/ruby/site_ruby/[12].*
lib/ruby/vendor_ruby/[12].*
manpages/brew.1
share/pypy/*
share/pypy3/*
share/info/dir
share/man/whatis
share/mime/*
texlive/*
].freeze
def list_unbrewed
dirs = HOMEBREW_PREFIX.subdirs.map { |dir| dir.basename.to_s }
dirs -= %w[Library Cellar Caskroom .git]
# Exclude cache, logs, and repository, if they are located under the prefix.
[HOMEBREW_CACHE, HOMEBREW_LOGS, HOMEBREW_REPOSITORY].each do |dir|
dirs.delete dir.relative_path_from(HOMEBREW_PREFIX).to_s
end
dirs.delete "etc"
dirs.delete "var"
arguments = dirs.sort + %w[-type f (]
arguments.concat UNBREWED_EXCLUDE_FILES.flat_map { |f| %W[! -name #{f}] }
arguments.concat UNBREWED_EXCLUDE_PATHS.flat_map { |d| %W[! -path #{d}] }
arguments.push ")"
cd HOMEBREW_PREFIX
safe_system "find", *arguments
end
end
# typed: true
# frozen_string_literal: true
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def __repository_args
Homebrew::CLI::Parser.new do
description <<~EOS
Display where Homebrew's git repository is located.
If <user>`/`<repo> are provided, display where tap <user>`/`<repo>'s directory is located.
EOS
named_args :tap
end
end
def __repository
args = __repository_args.parse
if args.no_named?
puts HOMEBREW_REPOSITORY
else
puts args.named.to_taps.map(&:path)
end
end
end
#: * `--version`, `-v`
#:
#: Print the version numbers of Homebrew, Homebrew/homebrew-core and Homebrew/homebrew-cask (if tapped) to standard output.
# HOMEBREW_CORE_REPOSITORY, HOMEBREW_CASK_REPOSITORY, HOMEBREW_VERSION are set by brew.sh
# shellcheck disable=SC2154
version_string() {
local repo="$1"
if ! [[ -d "${repo}" ]]
then
echo "N/A"
return
fi
local pretty_revision
pretty_revision="$(git -C "${repo}" rev-parse --short --verify --quiet HEAD)"
if [[ -z "${pretty_revision}" ]]
then
echo "(no Git repository)"
return
fi
local git_last_commit_date
git_last_commit_date="$(git -C "${repo}" show -s --format='%cd' --date=short HEAD)"
echo "(git revision ${pretty_revision}; last commit ${git_last_commit_date})"
}
homebrew-version() {
echo "Homebrew ${HOMEBREW_VERSION}"
echo "Homebrew/homebrew-core $(version_string "${HOMEBREW_CORE_REPOSITORY}")"
if [[ -d "${HOMEBREW_CASK_REPOSITORY}" ]]
then
echo "Homebrew/homebrew-cask $(version_string "${HOMEBREW_CASK_REPOSITORY}")"
fi
}
# typed: true
# frozen_string_literal: true
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def analytics_args
Homebrew::CLI::Parser.new do
description <<~EOS
Control Homebrew's anonymous aggregate user behaviour analytics.
Read more at <https://docs.brew.sh/Analytics>.
`brew analytics` [`state`]:
Display the current state of Homebrew's analytics.
`brew analytics` (`on`|`off`):
Turn Homebrew's analytics on or off respectively.
`brew analytics regenerate-uuid`:
Regenerate the UUID used for Homebrew's analytics.
EOS
named_args %w[state on off regenerate-uuid], max: 1
end
end
def analytics
args = analytics_args.parse
case args.named.first
when nil, "state"
if Utils::Analytics.disabled?
puts "Analytics are disabled."
else
puts "Analytics are enabled."
puts "UUID: #{Utils::Analytics.uuid}" if Utils::Analytics.uuid.present?
end
when "on"
Utils::Analytics.enable!
when "off"
Utils::Analytics.disable!
when "regenerate-uuid"
Utils::Analytics.regenerate_uuid!
else
raise UsageError, "unknown subcommand: #{args.named.first}"
end
end
end
# typed: true
# frozen_string_literal: true
require "cleanup"
require "cli/parser"
module Homebrew
module_function
def autoremove_args
Homebrew::CLI::Parser.new do
description <<~EOS
Uninstall formulae that were only installed as a dependency of another formula and are now no longer needed.
EOS
switch "-n", "--dry-run",
description: "List what would be uninstalled, but do not actually uninstall anything."
named_args :none
end
end
def autoremove
args = autoremove_args.parse
Cleanup.autoremove(dry_run: args.dry_run?)
end
end
#: * `casks`
#:
#: List all locally installable casks including short names.
#:
# HOMEBREW_LIBRARY is set in bin/brew
# shellcheck disable=SC2154
source "${HOMEBREW_LIBRARY}/Homebrew/items.sh"
homebrew-casks() {
homebrew-items '*/Casks/*\.rb' '' 's|/Casks/|/|' '^homebrew/cask'
}
# typed: false
# frozen_string_literal: true
require "cleanup"
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def cleanup_args
Homebrew::CLI::Parser.new do
days = Homebrew::EnvConfig::ENVS[:HOMEBREW_CLEANUP_MAX_AGE_DAYS][:default]
description <<~EOS
Remove stale lock files and outdated downloads for all formulae and casks,
and remove old versions of installed formulae. If arguments are specified,
only do this for the given formulae and casks. Removes all downloads more than
#{days} days old. This can be adjusted with `HOMEBREW_CLEANUP_MAX_AGE_DAYS`.
EOS
flag "--prune=",
description: "Remove all cache files older than specified <days>. " \
"If you want to remove everything, use `--prune=all`."
switch "-n", "--dry-run",
description: "Show what would be removed, but do not actually remove anything."
switch "-s",
description: "Scrub the cache, including downloads for even the latest versions. " \
"Note that downloads for any installed formulae or casks will still not be deleted. " \
"If you want to delete those too: `rm -rf \"$(brew --cache)\"`"
switch "--prune-prefix",
description: "Only prune the symlinks and directories from the prefix and remove no other files."
named_args [:formula, :cask]
end
end
def cleanup
args = cleanup_args.parse
days = args.prune.presence&.then do |prune|
case prune
when /\A\d+\Z/
prune.to_i
when "all"
0
else
raise UsageError, "`--prune=` expects an integer or `all`."
end
end
cleanup = Cleanup.new(*args.named, dry_run: args.dry_run?, scrub: args.s?, days: days)
if args.prune_prefix?
cleanup.prune_prefix_symlinks_and_directories
return
end
cleanup.clean!
unless cleanup.disk_cleanup_size.zero?
disk_space = disk_usage_readable(cleanup.disk_cleanup_size)
if args.dry_run?
ohai "This operation would free approximately #{disk_space} of disk space."
else
ohai "This operation has freed approximately #{disk_space} of disk space."
end
end
return if cleanup.unremovable_kegs.empty?
ofail <<~EOS
Could not cleanup old kegs! Fix your permissions on:
#{cleanup.unremovable_kegs.join "\n "}
EOS
end
end
# typed: false
# frozen_string_literal: true
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def commands_args
Homebrew::CLI::Parser.new do
description <<~EOS
Show lists of built-in and external commands.
EOS
switch "-q", "--quiet",
description: "List only the names of commands without category headers."
switch "--include-aliases",
depends_on: "--quiet",
description: "Include aliases of internal commands."
named_args :none
end
end
def commands
args = commands_args.parse
if args.quiet?
puts Formatter.columns(Commands.commands(aliases: args.include_aliases?))
return
end
prepend_separator = false
{
"Built-in commands" => Commands.internal_commands,
"Built-in developer commands" => Commands.internal_developer_commands,
"External commands" => Commands.external_commands,
}.each do |title, commands|
next if commands.blank?
puts if prepend_separator
ohai title, Formatter.columns(commands)
prepend_separator ||= true
end
end
end
# typed: true
# frozen_string_literal: true
require "cli/parser"
require "completions"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def completions_args
Homebrew::CLI::Parser.new do
description <<~EOS
Control whether Homebrew automatically links external tap shell completion files.
Read more at <https://docs.brew.sh/Shell-Completion>.
`brew completions` [`state`]:
Display the current state of Homebrew's completions.
`brew completions` (`link`|`unlink`):
Link or unlink Homebrew's completions.
EOS
named_args %w[state link unlink], max: 1
end
end
def completions
args = completions_args.parse
case args.named.first
when nil, "state"
if Completions.link_completions?
puts "Completions are linked."
else
puts "Completions are not linked."
end
when "link"
Completions.link!
puts "Completions are now linked."
when "unlink"
Completions.unlink!
puts "Completions are no longer linked."
else
raise UsageError, "unknown subcommand: #{args.named.first}"
end
end
end
# typed: true
# frozen_string_literal: true
require "system_config"
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def config_args
Homebrew::CLI::Parser.new do
description <<~EOS
Show Homebrew and system configuration info useful for debugging. If you file
a bug report, you will be required to provide this information.
EOS
named_args :none
end
end
def config
config_args.parse
SystemConfig.dump_verbose_config
end
end
# typed: false
# frozen_string_literal: true
require "formula"
require "ostruct"
require "cli/parser"
require "cask/caskroom"
require "dependencies_helpers"
module Homebrew
extend T::Sig
extend DependenciesHelpers
module_function
sig { returns(CLI::Parser) }
def deps_args
Homebrew::CLI::Parser.new do
description <<~EOS
Show dependencies for <formula>. Additional options specific to <formula>
may be appended to the command. When given multiple formula arguments,
show the intersection of dependencies for each formula.
EOS
switch "-n", "--topological",
description: "Sort dependencies in topological order."
switch "-1", "--direct", "--declared", "--1",
description: "Show only the direct dependencies declared in the formula."
switch "--union",
description: "Show the union of dependencies for multiple <formula>, instead of the intersection."
switch "--full-name",
description: "List dependencies by their full name."
switch "--include-build",
description: "Include `:build` dependencies for <formula>."
switch "--include-optional",
description: "Include `:optional` dependencies for <formula>."
switch "--include-test",
description: "Include `:test` dependencies for <formula> (non-recursive)."
switch "--skip-recommended",
description: "Skip `:recommended` dependencies for <formula>."
switch "--include-requirements",
description: "Include requirements in addition to dependencies for <formula>."
switch "--tree",
description: "Show dependencies as a tree. When given multiple formula arguments, " \
"show individual trees for each formula."
switch "--graph",
description: "Show dependencies as a directed graph."
switch "--dot",
depends_on: "--graph",
description: "Show text-based graph description in DOT format."
switch "--annotate",
description: "Mark any build, test, optional, or recommended dependencies as " \
"such in the output."
switch "--installed",
description: "List dependencies for formulae that are currently installed. If <formula> is " \
"specified, list only its dependencies that are currently installed."
switch "--eval-all",
description: "Evaluate all available formulae and casks, whether installed or not, to list " \
"their dependencies."
switch "--all",
hidden: true
switch "--for-each",
description: "Switch into the mode used by the `--all` option, but only list dependencies " \
"for each provided <formula>, one formula per line. This is used for " \
"debugging the `--installed`/`--all` display mode."
switch "--formula", "--formulae",
description: "Treat all named arguments as formulae."
switch "--cask", "--casks",
description: "Treat all named arguments as casks."
conflicts "--tree", "--graph"
conflicts "--installed", "--eval-all"
conflicts "--installed", "--all"
conflicts "--formula", "--cask"
formula_options
named_args [:formula, :cask]
end
end
def deps
args = deps_args.parse
all = args.eval_all?
if args.all?
unless all
odeprecated "brew deps --all",
"brew deps --eval-all or HOMEBREW_EVAL_ALL"
end
all = true
end
Formulary.enable_factory_cache!
recursive = !args.direct?
installed = args.installed? || dependents(args.named.to_formulae_and_casks).all?(&:any_version_installed?)
@use_runtime_dependencies = installed && recursive &&
!args.tree? &&
!args.graph? &&
!args.include_build? &&
!args.include_test? &&
!args.include_optional? &&
!args.skip_recommended?
if args.tree? || args.graph?
dependents = if args.named.present?
sorted_dependents(args.named.to_formulae_and_casks)
elsif args.installed?
case args.only_formula_or_cask
when :formula
sorted_dependents(Formula.installed)
when :cask
sorted_dependents(Cask::Caskroom.casks)
else
sorted_dependents(Formula.installed + Cask::Caskroom.casks)
end
else
raise FormulaUnspecifiedError
end
if args.graph?
dot_code = dot_code(dependents, recursive: recursive, args: args)
if args.dot?
puts dot_code
else
exec_browser "https://dreampuf.github.io/GraphvizOnline/##{ERB::Util.url_encode(dot_code)}"
end
return
end
puts_deps_tree dependents, recursive: recursive, args: args
return
elsif all
puts_deps sorted_dependents(Formula.all + Cask::Cask.all), recursive: recursive, args: args
return
elsif !args.no_named? && args.for_each?
puts_deps sorted_dependents(args.named.to_formulae_and_casks), recursive: recursive, args: args
return
end
if args.no_named?
raise FormulaUnspecifiedError unless args.installed?
sorted_dependents_formulae_and_casks = case args.only_formula_or_cask
when :formula
sorted_dependents(Formula.installed)
when :cask
sorted_dependents(Cask::Caskroom.casks)
else
sorted_dependents(Formula.installed + Cask::Caskroom.casks)
end
puts_deps sorted_dependents_formulae_and_casks, recursive: recursive, args: args
return
end
dependents = dependents(args.named.to_formulae_and_casks)
all_deps = deps_for_dependents(dependents, recursive: recursive, args: args, &(args.union? ? :| : :&))
condense_requirements(all_deps, args: args)
all_deps.map! { |d| dep_display_name(d, args: args) }
all_deps.uniq!
all_deps.sort! unless args.topological?
puts all_deps
end
def sorted_dependents(formulae_or_casks)
dependents(formulae_or_casks).sort_by(&:name)
end
def condense_requirements(deps, args:)
deps.select! { |dep| dep.is_a?(Dependency) } unless args.include_requirements?
deps.select! { |dep| dep.is_a?(Requirement) || dep.installed? } if args.installed?
end
def dep_display_name(dep, args:)
str = if dep.is_a? Requirement
if args.include_requirements?
":#{dep.display_s}"
else
# This shouldn't happen, but we'll put something here to help debugging
"::#{dep.name}"
end
elsif args.full_name?
dep.to_formula.full_name
else
dep.name
end
if args.annotate?
str = "#{str} " if args.tree?
str = "#{str} [build]" if dep.build?
str = "#{str} [test]" if dep.test?
str = "#{str} [optional]" if dep.optional?
str = "#{str} [recommended]" if dep.recommended?
end
str
end
def deps_for_dependent(d, args:, recursive: false)
includes, ignores = args_includes_ignores(args)
deps = d.runtime_dependencies if @use_runtime_dependencies
if recursive
deps ||= recursive_includes(Dependency, d, includes, ignores)
reqs = recursive_includes(Requirement, d, includes, ignores)
else
deps ||= reject_ignores(d.deps, ignores, includes)
reqs = reject_ignores(d.requirements, ignores, includes)
end
deps + reqs.to_a
end
def deps_for_dependents(dependents, args:, recursive: false, &block)
dependents.map { |d| deps_for_dependent(d, recursive: recursive, args: args) }.reduce(&block)
end
def puts_deps(dependents, args:, recursive: false)
dependents.each do |dependent|
deps = deps_for_dependent(dependent, recursive: recursive, args: args)
condense_requirements(deps, args: args)
deps.sort_by!(&:name)
deps.map! { |d| dep_display_name(d, args: args) }
puts "#{dependent.full_name}: #{deps.join(" ")}"
end
end
def dot_code(dependents, recursive:, args:)
dep_graph = {}
dependents.each do |d|
graph_deps(d, dep_graph: dep_graph, recursive: recursive, args: args)
end
dot_code = dep_graph.map do |d, deps|
deps.map do |dep|
attributes = []
attributes << "style = dotted" if dep.build?
attributes << "arrowhead = empty" if dep.test?
if dep.optional?
attributes << "color = red"
elsif dep.recommended?
attributes << "color = green"
end
comment = " # #{dep.tags.map(&:inspect).join(", ")}" if dep.tags.any?
" \"#{d.name}\" -> \"#{dep}\"#{" [#{attributes.join(", ")}]" if attributes.any?}#{comment}"
end
end.flatten.join("\n")
"digraph {\n#{dot_code}\n}"
end
def graph_deps(f, dep_graph:, recursive:, args:)
return if dep_graph.key?(f)
dependables = dependables(f, args: args)
dep_graph[f] = dependables
return unless recursive
dependables.each do |dep|
next unless dep.is_a? Dependency
graph_deps(Formulary.factory(dep.name),
dep_graph: dep_graph,
recursive: true,
args: args)
end
end
def puts_deps_tree(dependents, args:, recursive: false)
dependents.each do |d|
puts d.full_name
recursive_deps_tree(d, dep_stack: [], prefix: "", recursive: recursive, args: args)
puts
end
end
def dependables(f, args:)
includes, ignores = args_includes_ignores(args)
deps = @use_runtime_dependencies ? f.runtime_dependencies : f.deps
deps = reject_ignores(deps, ignores, includes)
reqs = reject_ignores(f.requirements, ignores, includes) if args.include_requirements?
reqs ||= []
reqs + deps
end
def recursive_deps_tree(f, dep_stack:, prefix:, recursive:, args:)
dependables = dependables(f, args: args)
max = dependables.length - 1
dep_stack.push f.name
dependables.each_with_index do |dep, i|
tree_lines = if i == max
"└──"
else
"├──"
end
display_s = "#{tree_lines} #{dep_display_name(dep, args: args)}"
# Detect circular dependencies and consider them a failure if present.
is_circular = dep_stack.include?(dep.name)
if is_circular
display_s = "#{display_s} (CIRCULAR DEPENDENCY)"
Homebrew.failed = true
end
puts "#{prefix}#{display_s}"
next if !recursive || is_circular
prefix_addition = if i == max
" "
else
"│ "
end
next unless dep.is_a? Dependency
recursive_deps_tree(Formulary.factory(dep.name),
dep_stack: dep_stack,
prefix: prefix + prefix_addition,
recursive: true,
args: args)
end
dep_stack.pop
end
end
# typed: false
# frozen_string_literal: true
require "descriptions"
require "search"
require "description_cache_store"
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def desc_args
Homebrew::CLI::Parser.new do
description <<~EOS
Display <formula>'s name and one-line description.
The cache is created on the first search, making that search slower than subsequent ones.
EOS
switch "-s", "--search",
description: "Search both names and descriptions for <text>. If <text> is flanked by " \
"slashes, it is interpreted as a regular expression."
switch "-n", "--name",
description: "Search just names for <text>. If <text> is flanked by slashes, it is " \
"interpreted as a regular expression."
switch "-d", "--description",
description: "Search just descriptions for <text>. If <text> is flanked by slashes, " \
"it is interpreted as a regular expression."
switch "--eval-all",
description: "Evaluate all available formulae and casks, whether installed or not, to search their " \
"descriptions. Implied if HOMEBREW_EVAL_ALL is set."
switch "--formula", "--formulae",
description: "Treat all named arguments as formulae."
switch "--cask", "--casks",
description: "Treat all named arguments as casks."
conflicts "--search", "--name", "--description"
named_args [:formula, :cask, :text_or_regex], min: 1
end
end
def desc
args = desc_args.parse
if !args.eval_all? && !Homebrew::EnvConfig.eval_all?
odeprecated "brew desc", "brew desc --eval-all or HOMEBREW_EVAL_ALL"
end
search_type = if args.search?
:either
elsif args.name?
:name
elsif args.description?
:desc
end
if search_type.blank?
desc = {}
args.named.to_formulae_and_casks.each do |formula_or_cask|
if formula_or_cask.is_a? Formula
desc[formula_or_cask.full_name] = formula_or_cask.desc
else
description = formula_or_cask.desc.presence || Formatter.warning("[no description]")
desc[formula_or_cask.full_name] = "(#{formula_or_cask.name.join(", ")}) #{description}"
end
end
Descriptions.new(desc).print
else
query = args.named.join(" ")
string_or_regex = Search.query_regexp(query)
Search.search_descriptions(string_or_regex, args, search_type: search_type)
end
end
end
# typed: true
# frozen_string_literal: true
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def developer_args
Homebrew::CLI::Parser.new do
description <<~EOS
Control Homebrew's developer mode. When developer mode is enabled,
`brew update` will update Homebrew to the latest commit on the `master`
branch instead of the latest stable version along with some other behaviour changes.
`brew developer` [`state`]:
Display the current state of Homebrew's developer mode.
`brew developer` (`on`|`off`):
Turn Homebrew's developer mode on or off respectively.
EOS
named_args %w[state on off], max: 1
end
end
def developer
args = developer_args.parse
env_vars = []
env_vars << "HOMEBREW_DEVELOPER" if Homebrew::EnvConfig.developer?
env_vars << "HOMEBREW_UPDATE_TO_TAG" if Homebrew::EnvConfig.update_to_tag?
env_vars.map! do |var|
"#{Tty.bold}#{var}#{Tty.reset}"
end
case args.named.first
when nil, "state"
if env_vars.any?
puts "Developer mode is enabled because #{env_vars.to_sentence} #{"is".pluralize(env_vars.count)} set."
elsif Homebrew::Settings.read("devcmdrun") == "true"
puts "Developer mode is enabled."
else
puts "Developer mode is disabled."
end
when "on"
Homebrew::Settings.write "devcmdrun", true
when "off"
Homebrew::Settings.delete "devcmdrun"
puts "To fully disable developer mode, you must unset #{env_vars.to_sentence}." if env_vars.any?
else
raise UsageError, "unknown subcommand: #{args.named.first}"
end
end
end
# typed: true
# frozen_string_literal: true
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def docs_args
Homebrew::CLI::Parser.new do
description <<~EOS
Open Homebrew's online documentation (#{HOMEBREW_DOCS_WWW}) in a browser.
EOS
end
end
sig { void }
def docs
exec_browser HOMEBREW_DOCS_WWW
end
end
# typed: false
# frozen_string_literal: true
require "diagnostic"
require "cli/parser"
require "cask/caskroom"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def doctor_args
Homebrew::CLI::Parser.new do
description <<~EOS
Check your system for potential problems. Will exit with a non-zero status
if any potential problems are found. Please note that these warnings are just
used to help the Homebrew maintainers with debugging if you file an issue. If
everything you use Homebrew for is working fine: please don't worry or file
an issue; just ignore this.
EOS
switch "--list-checks",
description: "List all audit methods, which can be run individually " \
"if provided as arguments."
switch "-D", "--audit-debug",
description: "Enable debugging and profiling of audit methods."
named_args :diagnostic_check
end
end
def doctor
args = doctor_args.parse
inject_dump_stats!(Diagnostic::Checks, /^check_*/) if args.audit_debug?
checks = Diagnostic::Checks.new(verbose: args.verbose?)
if args.list_checks?
puts checks.all
return
end
if args.no_named?
slow_checks = %w[
check_for_broken_symlinks
check_missing_deps
]
methods = (checks.all - slow_checks) + slow_checks
methods -= checks.cask_checks unless Cask::Caskroom.any_casks_installed?
else
methods = args.named
end
first_warning = true
methods.each do |method|
$stderr.puts Formatter.headline("Checking #{method}", color: :magenta) if args.debug?
unless checks.respond_to?(method)
ofail "No check available by the name: #{method}"
next
end
out = checks.send(method)
next if out.blank?
if first_warning
$stderr.puts <<~EOS
#{Tty.bold}Please note that these warnings are just used to help the Homebrew maintainers
with debugging if you file an issue. If everything you use Homebrew for is
working fine: please don't worry or file an issue; just ignore this. Thanks!#{Tty.reset}
EOS
end
$stderr.puts
opoo out
Homebrew.failed = true
first_warning = false
end
puts "Your system is ready to brew." unless Homebrew.failed?
end
end
# typed: false
# frozen_string_literal: true
require "formula"
require "fetch"
require "cli/parser"
require "cask/download"
module Homebrew
extend T::Sig
extend Fetch
module_function
FETCH_MAX_TRIES = 5
sig { returns(CLI::Parser) }
def fetch_args
Homebrew::CLI::Parser.new do
description <<~EOS
Download a bottle (if available) or source packages for <formula>e
and binaries for <cask>s. For files, also print SHA-256 checksums.
EOS
flag "--bottle-tag=",
description: "Download a bottle for given tag."
switch "--HEAD",
description: "Fetch HEAD version instead of stable version."
switch "-f", "--force",
description: "Remove a previously cached version and re-fetch."
switch "-v", "--verbose",
description: "Do a verbose VCS checkout, if the URL represents a VCS. This is useful for " \
"seeing if an existing VCS cache has been updated."
switch "--retry",
description: "Retry if downloading fails or re-download if the checksum of a previously cached " \
"version no longer matches. Tries at most #{FETCH_MAX_TRIES} times with " \
"exponential backoff."
switch "--deps",
description: "Also download dependencies for any listed <formula>."
switch "-s", "--build-from-source",
description: "Download source packages rather than a bottle."
switch "--build-bottle",
description: "Download source packages (for eventual bottling) rather than a bottle."
switch "--force-bottle",
description: "Download a bottle if it exists for the current or newest version of macOS, " \
"even if it would not be used during installation."
switch "--[no-]quarantine",
description: "Disable/enable quarantining of downloads (default: enabled).",
env: :cask_opts_quarantine
switch "--formula", "--formulae",
description: "Treat all named arguments as formulae."
switch "--cask", "--casks",
description: "Treat all named arguments as casks."
conflicts "--build-from-source", "--build-bottle", "--force-bottle", "--bottle-tag"
conflicts "--cask", "--HEAD"
conflicts "--cask", "--deps"
conflicts "--cask", "-s"
conflicts "--cask", "--build-bottle"
conflicts "--cask", "--force-bottle"
conflicts "--cask", "--bottle-tag"
conflicts "--formula", "--cask"
named_args [:formula, :cask], min: 1
end
end
def fetch
args = fetch_args.parse
bucket = if args.deps?
args.named.to_formulae_and_casks.flat_map do |formula_or_cask|
case formula_or_cask
when Formula
f = formula_or_cask
[f, *f.recursive_dependencies.map(&:to_formula)]
else
formula_or_cask
end
end
else
args.named.to_formulae_and_casks
end.uniq
puts "Fetching: #{bucket * ", "}" if bucket.size > 1
bucket.each do |formula_or_cask|
case formula_or_cask
when Formula
f = formula_or_cask
f.print_tap_action verb: "Fetching"
fetched_bottle = false
if fetch_bottle?(f, args: args)
begin
f.clear_cache if args.force?
f.fetch_bottle_tab
fetch_formula(f.bottle_for_tag(args.bottle_tag&.to_sym), args: args)
rescue Interrupt
raise
rescue => e
raise if Homebrew::EnvConfig.developer?
fetched_bottle = false
onoe e.message
opoo "Bottle fetch failed, fetching the source instead."
else
fetched_bottle = true
end
end
next if fetched_bottle
fetch_formula(f, args: args)
f.resources.each do |r|
fetch_resource(r, args: args)
r.patches.each { |p| fetch_patch(p, args: args) if p.external? }
end
f.patchlist.each { |p| fetch_patch(p, args: args) if p.external? }
else
cask = formula_or_cask
quarantine = args.quarantine?
quarantine = true if quarantine.nil?
download = Cask::Download.new(cask, quarantine: quarantine)
fetch_cask(download, args: args)
end
end
end
def fetch_resource(r, args:)
puts "Resource: #{r.name}"
fetch_fetchable r, args: args
rescue ChecksumMismatchError => e
retry if retry_fetch?(r, args: args)
opoo "Resource #{r.name} reports different sha256: #{e.expected}"
end
def fetch_formula(f, args:)
fetch_fetchable f, args: args
rescue ChecksumMismatchError => e
retry if retry_fetch?(f, args: args)
opoo "Formula reports different sha256: #{e.expected}"
end
def fetch_cask(cask_download, args:)
fetch_fetchable cask_download, args: args
rescue ChecksumMismatchError => e
retry if retry_fetch?(cask_download, args: args)
opoo "Cask reports different sha256: #{e.expected}"
end
def fetch_patch(p, args:)
fetch_fetchable p, args: args
rescue ChecksumMismatchError => e
opoo "Patch reports different sha256: #{e.expected}"
Homebrew.failed = true
end
def retry_fetch?(f, args:)
@fetch_tries ||= Hash.new { |h, k| h[k] = 1 }
if args.retry? && (@fetch_tries[f] < FETCH_MAX_TRIES)
wait = 2 ** @fetch_tries[f]
remaining = FETCH_MAX_TRIES - @fetch_tries[f]
what = "try".pluralize(remaining)
ohai "Retrying download in #{wait}s... (#{remaining} #{what} left)"
sleep wait
f.clear_cache
@fetch_tries[f] += 1
true
else
Homebrew.failed = true
false
end
end
def fetch_fetchable(f, args:)
f.clear_cache if args.force?
already_fetched = f.cached_download.exist?
begin
download = f.fetch(verify_download_integrity: false)
rescue DownloadError
retry if retry_fetch?(f, args: args)
raise
end
return unless download.file?
puts "Downloaded to: #{download}" unless already_fetched
puts "SHA256: #{download.sha256}"
f.verify_download_integrity(download)
end
end
#: * `formulae`
#:
#: List all locally installable formulae including short names.
#:
# HOMEBREW_LIBRARY is set by bin/brew
# shellcheck disable=SC2154
source "${HOMEBREW_LIBRARY}/Homebrew/items.sh"
homebrew-formulae() {
local formulae
formulae="$(homebrew-items '*\.rb' 'Casks' 's|/Formula/|/|' '^homebrew/core')"
# HOMEBREW_CACHE is set by brew.sh
# shellcheck disable=SC2154
if [[ -z "${HOMEBREW_NO_INSTALL_FROM_API}" &&
-n "${HOMEBREW_INSTALL_FROM_API}" &&
-f "${HOMEBREW_CACHE}/api/formula.json" ]]
then
local api_formulae
api_formulae="$(ruby -e "require 'json'; JSON.parse(File.read('${HOMEBREW_CACHE}/api/formula.json')).each { |f| puts f['name'] }" 2>/dev/null)"
formulae="$(echo -e "${formulae}\n${api_formulae}" | sort -uf | grep .)"
fi
echo "${formulae}"
}
# typed: true
# frozen_string_literal: true
require "formula"
require "install"
require "system_config"
require "stringio"
require "socket"
require "cli/parser"
module Homebrew
extend T::Sig
extend Install
module_function
sig { returns(CLI::Parser) }
def gist_logs_args
Homebrew::CLI::Parser.new do
description <<~EOS
Upload logs for a failed build of <formula> to a new Gist. Presents an
error message if no logs are found.
EOS
switch "--with-hostname",
description: "Include the hostname in the Gist."
switch "-n", "--new-issue",
description: "Automatically create a new issue in the appropriate GitHub repository " \
"after creating the Gist."
switch "-p", "--private",
description: "The Gist will be marked private and will not appear in listings but will " \
"be accessible with its link."
named_args :formula, number: 1
end
end
def gistify_logs(f, args:)
files = load_logs(f.logs)
build_time = f.logs.ctime
timestamp = build_time.strftime("%Y-%m-%d_%H-%M-%S")
s = StringIO.new
SystemConfig.dump_verbose_config s
# Dummy summary file, asciibetically first, to control display title of gist
files["# #{f.name} - #{timestamp}.txt"] = { content: brief_build_info(f, with_hostname: args.with_hostname?) }
files["00.config.out"] = { content: s.string }
files["00.doctor.out"] = { content: Utils.popen_read("#{HOMEBREW_PREFIX}/bin/brew", "doctor", err: :out) }
unless f.core_formula?
tap = <<~EOS
Formula: #{f.name}
Tap: #{f.tap}
Path: #{f.path}
EOS
files["00.tap.out"] = { content: tap }
end
odie "`brew gist-logs` requires HOMEBREW_GITHUB_API_TOKEN to be set!" if GitHub::API.credentials_type == :none
# Description formatted to work well as page title when viewing gist
descr = if f.core_formula?
"#{f.name} on #{OS_VERSION} - Homebrew build logs"
else
"#{f.name} (#{f.full_name}) on #{OS_VERSION} - Homebrew build logs"
end
url = GitHub.create_gist(files, descr, private: args.private?)
url = GitHub.create_issue(f.tap, "#{f.name} failed to build on #{MacOS.full_version}", url) if args.new_issue?
puts url if url
end
def brief_build_info(f, with_hostname:)
build_time_str = f.logs.ctime.strftime("%Y-%m-%d %H:%M:%S")
s = +<<~EOS
Homebrew build logs for #{f.full_name} on #{OS_VERSION}
EOS
if with_hostname
hostname = Socket.gethostname
s << "Host: #{hostname}\n"
end
s << "Build date: #{build_time_str}\n"
s.freeze
end
# Causes some terminals to display secure password entry indicators.
def noecho_gets
system "stty", "-echo"
result = $stdin.gets
system "stty", "echo"
puts
result
end
def load_logs(dir, basedir = dir)
logs = {}
if dir.exist?
dir.children.sort.each do |file|
if file.directory?
logs.merge! load_logs(file, basedir)
else
contents = file.size? ? file.read : "empty log"
# small enough to avoid GitHub "unicorn" page-load-timeout errors
max_file_size = 1_000_000
contents = truncate_text_to_approximate_size(contents, max_file_size, front_weight: 0.2)
logs[file.relative_path_from(basedir).to_s.tr("/", ":")] = { content: contents }
end
end
end
odie "No logs." if logs.empty?
logs
end
def gist_logs
args = gist_logs_args.parse
Install.perform_preinstall_checks(all_fatal: true)
Install.perform_build_from_source_checks(all_fatal: true)
gistify_logs(args.named.to_resolved_formulae.first, args: args)
end
end
# typed: true
# frozen_string_literal: true
require "help"
module Homebrew
def help
Help.help
end
end
# typed: true
# frozen_string_literal: true
require "cli/parser"
require "formula"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def home_args
Homebrew::CLI::Parser.new do
description <<~EOS
Open a <formula> or <cask>'s homepage in a browser, or open
Homebrew's own homepage if no argument is provided.
EOS
switch "--formula", "--formulae",
description: "Treat all named arguments as formulae."
switch "--cask", "--casks",
description: "Treat all named arguments as casks."
conflicts "--formula", "--cask"
named_args [:formula, :cask]
end
end
sig { void }
def home
args = home_args.parse
if args.no_named?
exec_browser HOMEBREW_WWW
return
end
# to_formulae_and_casks is typed to possibly return Kegs (but won't without explicitly asking)
formulae_or_casks = T.cast(args.named.to_formulae_and_casks, T::Array[T.any(Formula, Cask::Cask)])
homepages = formulae_or_casks.map do |formula_or_cask|
puts "Opening homepage for #{name_of(formula_or_cask)}"
formula_or_cask.homepage
end
exec_browser(*T.unsafe(homepages))
end
def name_of(formula_or_cask)
if formula_or_cask.is_a? Formula
"Formula #{formula_or_cask.name}"
else
"Cask #{formula_or_cask.token}"
end
end
end
# typed: false
# frozen_string_literal: true
require "missing_formula"
require "caveats"
require "cli/parser"
require "options"
require "formula"
require "keg"
require "tab"
require "json"
require "utils/spdx"
require "deprecate_disable"
require "api"
module Homebrew
extend T::Sig
module_function
VALID_DAYS = %w[30 90 365].freeze
VALID_FORMULA_CATEGORIES = %w[install install-on-request build-error].freeze
VALID_CATEGORIES = (VALID_FORMULA_CATEGORIES + %w[cask-install os-version]).freeze
sig { returns(CLI::Parser) }
def info_args
Homebrew::CLI::Parser.new do
description <<~EOS
Display brief statistics for your Homebrew installation.
If a <formula> or <cask> is provided, show summary of information about it.
EOS
switch "--analytics",
description: "List global Homebrew analytics data or, if specified, installation and " \
"build error data for <formula> (provided neither `HOMEBREW_NO_ANALYTICS` " \
"nor `HOMEBREW_NO_GITHUB_API` are set)."
flag "--days=",
depends_on: "--analytics",
description: "How many days of analytics data to retrieve. " \
"The value for <days> must be `30`, `90` or `365`. The default is `30`."
flag "--category=",
depends_on: "--analytics",
description: "Which type of analytics data to retrieve. " \
"The value for <category> must be `install`, `install-on-request` or `build-error`; " \
"`cask-install` or `os-version` may be specified if <formula> is not. " \
"The default is `install`."
switch "--github",
description: "Open the GitHub source page for <formula> and <cask> in a browser. " \
"To view the history locally: `brew log -p` <formula> or <cask>"
flag "--json",
description: "Print a JSON representation. Currently the default value for <version> is `v1` for " \
"<formula>. For <formula> and <cask> use `v2`. See the docs for examples of using the " \
"JSON output: <https://docs.brew.sh/Querying-Brew>"
switch "--bottle",
depends_on: "--json",
description: "Output information about the bottles for <formula> and its dependencies.",
hidden: true
switch "--installed",
depends_on: "--json",
description: "Print JSON of formulae that are currently installed."
switch "--eval-all",
depends_on: "--json",
description: "Evaluate all available formulae and casks, whether installed or not, to print their " \
"JSON. Implied if HOMEBREW_EVAL_ALL is set."
switch "--all",
hidden: true,
depends_on: "--json"
switch "--variations",
depends_on: "--json",
description: "Include the variations hash in each formula's JSON output."
switch "-v", "--verbose",
description: "Show more verbose analytics data for <formula>."
switch "--formula", "--formulae",
description: "Treat all named arguments as formulae."
switch "--cask", "--casks",
description: "Treat all named arguments as casks."
conflicts "--installed", "--eval-all"
conflicts "--installed", "--all"
conflicts "--formula", "--cask"
%w[--cask --analytics --github].each do |conflict|
conflicts "--bottle", conflict
end
named_args [:formula, :cask]
end
end
sig { void }
def info
args = info_args.parse
if args.analytics?
if args.days.present? && VALID_DAYS.exclude?(args.days)
raise UsageError, "--days must be one of #{VALID_DAYS.join(", ")}"
end
if args.category.present?
if args.named.present? && VALID_FORMULA_CATEGORIES.exclude?(args.category)
raise UsageError, "--category must be one of #{VALID_FORMULA_CATEGORIES.join(", ")} when querying formulae"
end
unless VALID_CATEGORIES.include?(args.category)
raise UsageError, "--category must be one of #{VALID_CATEGORIES.join(", ")}"
end
end
print_analytics(args: args)
elsif args.json
all = args.eval_all?
if !all && args.all? && !Homebrew::EnvConfig.eval_all?
odeprecated "brew info --all", "brew info --eval-all or HOMEBREW_EVAL_ALL"
all = true
end
print_json(all, args: args)
elsif args.github?
raise FormulaOrCaskUnspecifiedError if args.no_named?
exec_browser(*args.named.to_formulae_and_casks.map { |f| github_info(f) })
elsif args.no_named?
print_statistics
else
print_info(args: args)
end
end
sig { void }
def print_statistics
return unless HOMEBREW_CELLAR.exist?
count = Formula.racks.length
puts "#{count} #{"keg".pluralize(count)}, #{HOMEBREW_CELLAR.dup.abv}"
end
sig { params(args: CLI::Args).void }
def print_analytics(args:)
if args.no_named?
Utils::Analytics.output(args: args)
return
end
args.named.to_formulae_and_casks_and_unavailable.each_with_index do |obj, i|
puts unless i.zero?
case obj
when Formula
Utils::Analytics.formula_output(obj, args: args)
when Cask::Cask
Utils::Analytics.cask_output(obj, args: args)
when FormulaOrCaskUnavailableError
Utils::Analytics.output(filter: obj.name, args: args)
else
raise
end
end
end
sig { params(args: CLI::Args).void }
def print_info(args:)
args.named.to_formulae_and_casks_and_unavailable.each_with_index do |obj, i|
puts unless i.zero?
case obj
when Formula
info_formula(obj, args: args)
when Cask::Cask
info_cask(obj, args: args)
when FormulaUnreadableError, FormulaClassUnavailableError,
TapFormulaUnreadableError, TapFormulaClassUnavailableError,
Cask::CaskUnreadableError
# We found the formula/cask, but failed to read it
$stderr.puts obj.backtrace if Homebrew::EnvConfig.developer?
ofail obj.message
when FormulaOrCaskUnavailableError
# The formula/cask could not be found
ofail obj.message
# No formula with this name, try a missing formula lookup
if (reason = MissingFormula.reason(obj.name, show_info: true))
$stderr.puts reason
end
else
raise
end
end
end
def json_version(version)
version_hash = {
true => :default,
"v1" => :v1,
"v2" => :v2,
}
raise UsageError, "invalid JSON version: #{version}" unless version_hash.include?(version)
version_hash[version]
end
sig { params(all: T::Boolean, args: CLI::Args).void }
def print_json(all, args:)
raise FormulaOrCaskUnspecifiedError if !(all || args.installed?) && args.no_named?
json = case json_version(args.json)
when :v1, :default
raise UsageError, "cannot specify --cask with --json=v1!" if args.cask?
formulae = if all
Formula.all.sort
elsif args.installed?
Formula.installed.sort
else
args.named.to_formulae
end
if args.bottle?
formulae.map(&:to_recursive_bottle_hash)
elsif args.variations?
formulae.map(&:to_hash_with_variations)
else
formulae.map(&:to_hash)
end
when :v2
formulae, casks = if all
[Formula.all.sort, Cask::Cask.all.sort_by(&:full_name)]
elsif args.installed?
[Formula.installed.sort, Cask::Caskroom.casks.sort_by(&:full_name)]
else
args.named.to_formulae_to_casks
end
if args.bottle?
{ "formulae" => formulae.map(&:to_recursive_bottle_hash) }
elsif args.variations?
{
"formulae" => formulae.map(&:to_hash_with_variations),
"casks" => casks.map(&:to_hash_with_variations),
}
else
{
"formulae" => formulae.map(&:to_hash),
"casks" => casks.map(&:to_h),
}
end
else
raise
end
puts JSON.pretty_generate(json)
end
def github_remote_path(remote, path)
if remote =~ %r{^(?:https?://|git(?:@|://))github\.com[:/](.+)/(.+?)(?:\.git)?$}
"https://github.com/#{Regexp.last_match(1)}/#{Regexp.last_match(2)}/blob/HEAD/#{path}"
else
"#{remote}/#{path}"
end
end
def github_info(f)
return f.path if f.tap.blank? || f.tap.remote.blank?
path = case f
when Formula
f.path.relative_path_from(f.tap.path)
when Cask::Cask
f.sourcefile_path.relative_path_from(f.tap.path)
end
github_remote_path(f.tap.remote, path)
end
def info_formula(f, args:)
specs = []
if (stable = f.stable)
s = "stable #{stable.version}"
s += " (bottled)" if stable.bottled? && f.pour_bottle?
specs << s
end
specs << "HEAD" if f.head
attrs = []
attrs << "pinned at #{f.pinned_version}" if f.pinned?
attrs << "keg-only" if f.keg_only?
puts "#{oh1_title(f.full_name)}: #{specs * ", "}#{" [#{attrs * ", "}]" unless attrs.empty?}"
puts f.desc if f.desc
puts Formatter.url(f.homepage) if f.homepage
deprecate_disable_type, deprecate_disable_reason = DeprecateDisable.deprecate_disable_info f
if deprecate_disable_type.present?
if deprecate_disable_reason.present?
puts "#{deprecate_disable_type.capitalize} because it #{deprecate_disable_reason}!"
else
puts "#{deprecate_disable_type.capitalize}!"
end
end
conflicts = f.conflicts.map do |c|
reason = " (because #{c.reason})" if c.reason
"#{c.name}#{reason}"
end.sort!
unless conflicts.empty?
puts <<~EOS
Conflicts with:
#{conflicts.join("\n ")}
EOS
end
kegs = f.installed_kegs
heads, versioned = kegs.partition { |k| k.version.head? }
kegs = [
*heads.sort_by { |k| -Tab.for_keg(k).time.to_i },
*versioned.sort_by(&:version),
]
if kegs.empty?
puts "Not installed"
else
kegs.each do |keg|
puts "#{keg} (#{keg.abv})#{" *" if keg.linked?}"
tab = Tab.for_keg(keg).to_s
puts " #{tab}" unless tab.empty?
end
end
puts "From: #{Formatter.url(github_info(f))}"
puts "License: #{SPDX.license_expression_to_string f.license}" if f.license.present?
unless f.deps.empty?
ohai "Dependencies"
%w[build required recommended optional].map do |type|
deps = f.deps.send(type).uniq
puts "#{type.capitalize}: #{decorate_dependencies deps}" unless deps.empty?
end
end
unless f.requirements.to_a.empty?
ohai "Requirements"
%w[build required recommended optional].map do |type|
reqs = f.requirements.select(&:"#{type}?")
next if reqs.to_a.empty?
puts "#{type.capitalize}: #{decorate_requirements(reqs)}"
end
end
if !f.options.empty? || f.head
ohai "Options"
Options.dump_for_formula f
end
caveats = Caveats.new(f)
ohai "Caveats", caveats.to_s unless caveats.empty?
Utils::Analytics.formula_output(f, args: args)
end
def decorate_dependencies(dependencies)
deps_status = dependencies.map do |dep|
if dep.satisfied?([])
pretty_installed(dep_display_s(dep))
else
pretty_uninstalled(dep_display_s(dep))
end
end
deps_status.join(", ")
end
def decorate_requirements(requirements)
req_status = requirements.map do |req|
req_s = req.display_s
req.satisfied? ? pretty_installed(req_s) : pretty_uninstalled(req_s)
end
req_status.join(", ")
end
def dep_display_s(dep)
return dep.name if dep.option_tags.empty?
"#{dep.name} #{dep.option_tags.map { |o| "--#{o}" }.join(" ")}"
end
def info_cask(cask, args:)
require "cask/cmd"
require "cask/cmd/info"
Cask::Cmd::Info.info(cask)
end
end
# typed: false
# frozen_string_literal: true
require "cask/config"
require "cask/cmd"
require "cask/cmd/install"
require "missing_formula"
require "formula_installer"
require "development_tools"
require "install"
require "cleanup"
require "cli/parser"
require "upgrade"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def install_args
Homebrew::CLI::Parser.new do
description <<~EOS
Install a <formula> or <cask>. Additional options specific to a <formula> may be
appended to the command.
Unless `HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK` is set, `brew upgrade` or `brew reinstall` will be run for
outdated dependents and dependents with broken linkage, respectively.
Unless `HOMEBREW_NO_INSTALL_CLEANUP` is set, `brew cleanup` will then be run for
the installed formulae or, every 30 days, for all formulae.
Unless `HOMEBREW_NO_INSTALL_UPGRADE` is set, `brew install <formula>` will upgrade <formula> if it
is already installed but outdated.
EOS
switch "-d", "--debug",
description: "If brewing fails, open an interactive debugging session with access to IRB " \
"or a shell inside the temporary build directory."
switch "-f", "--force",
description: "Install formulae without checking for previously installed keg-only or " \
"non-migrated versions. When installing casks, overwrite existing files " \
"(binaries and symlinks are excluded, unless originally from the same cask)."
switch "-v", "--verbose",
description: "Print the verification and postinstall steps."
switch "-n", "--dry-run",
description: "Show what would be installed, but do not actually install anything."
[
[:switch, "--formula", "--formulae", {
description: "Treat all named arguments as formulae.",
}],
[:flag, "--env=", {
description: "Disabled other than for internal Homebrew use.",
hidden: true,
}],
[:switch, "--ignore-dependencies", {
description: "An unsupported Homebrew development flag to skip installing any dependencies of any kind. " \
"If the dependencies are not already present, the formula will have issues. If you're not " \
"developing Homebrew, consider adjusting your PATH rather than using this flag.",
}],
[:switch, "--only-dependencies", {
description: "Install the dependencies with specified options but do not install the " \
"formula itself.",
}],
[:flag, "--cc=", {
description: "Attempt to compile using the specified <compiler>, which should be the name of the " \
"compiler's executable, e.g. `gcc-7` for GCC 7. In order to use LLVM's clang, specify " \
"`llvm_clang`. To use the Apple-provided clang, specify `clang`. This option will only " \
"accept compilers that are provided by Homebrew or bundled with macOS. Please do not " \
"file issues if you encounter errors while using this option.",
}],
[:switch, "-s", "--build-from-source", {
description: "Compile <formula> from source even if a bottle is provided. " \
"Dependencies will still be installed from bottles if they are available.",
}],
[:switch, "--force-bottle", {
description: "Install from a bottle if it exists for the current or newest version of " \
"macOS, even if it would not normally be used for installation.",
}],
[:switch, "--include-test", {
description: "Install testing dependencies required to run `brew test` <formula>.",
}],
[:switch, "--HEAD", {
description: "If <formula> defines it, install the HEAD version, aka. main, trunk, unstable, master.",
}],
[:switch, "--fetch-HEAD", {
description: "Fetch the upstream repository to detect if the HEAD installation of the " \
"formula is outdated. Otherwise, the repository's HEAD will only be checked for " \
"updates when a new stable or development version has been released.",
}],
[:switch, "--keep-tmp", {
description: "Retain the temporary files created during installation.",
}],
[:switch, "--debug-symbols", {
depends_on: "--build-from-source",
description: "Generate debug symbols on build. Source will be retained in a cache directory. ",
}],
[:switch, "--build-bottle", {
description: "Prepare the formula for eventual bottling during installation, skipping any " \
"post-install steps.",
}],
[:flag, "--bottle-arch=", {
depends_on: "--build-bottle",
description: "Optimise bottles for the specified architecture rather than the oldest " \
"architecture supported by the version of macOS the bottles are built on.",
}],
[:switch, "--display-times", {
env: :display_install_times,
description: "Print install times for each package at the end of the run.",
}],
[:switch, "-i", "--interactive", {
description: "Download and patch <formula>, then open a shell. This allows the user to " \
"run `./configure --help` and otherwise determine how to turn the software " \
"package into a Homebrew package.",
}],
[:switch, "-g", "--git", {
description: "Create a Git repository, useful for creating patches to the software.",
}],
[:switch, "--overwrite", {
description: "Delete files that already exist in the prefix while linking.",
}],
].each do |args|
options = args.pop
send(*args, **options)
conflicts "--cask", args.last
end
formula_options
[
[:switch, "--cask", "--casks", { description: "Treat all named arguments as casks." }],
*Cask::Cmd::AbstractCommand::OPTIONS.map(&:dup),
*Cask::Cmd::Install::OPTIONS.map(&:dup),
].each do |args|
options = args.pop
send(*args, **options)
conflicts "--formula", args.last
end
cask_options
conflicts "--ignore-dependencies", "--only-dependencies"
conflicts "--build-from-source", "--build-bottle", "--force-bottle"
conflicts "--adopt", "--force"
named_args [:formula, :cask], min: 1
end
end
def install
args = install_args.parse
if args.build_from_source? && Homebrew::EnvConfig.install_from_api?
raise UsageError, "--build-from-source is not supported when using HOMEBREW_INSTALL_FROM_API."
end
if args.env.present?
# Can't use `replacement: false` because `install_args` are used by
# `build.rb`. Instead, `hide_from_man_page` and don't do anything with
# this argument here.
odisabled "brew install --env", "`env :std` in specific formula files"
end
args.named.each do |name|
next if File.exist?(name)
next unless name =~ HOMEBREW_TAP_FORMULA_REGEX
tap = Tap.fetch(Regexp.last_match(1), Regexp.last_match(2))
next if (tap.core_tap? || tap == "homebrew/cask") && EnvConfig.install_from_api?
tap.install unless tap.installed?
end
if args.ignore_dependencies?
opoo <<~EOS
#{Tty.bold}`--ignore-dependencies` is an unsupported Homebrew developer flag!#{Tty.reset}
Adjust your PATH to put any preferred versions of applications earlier in the
PATH rather than using this unsupported flag!
EOS
end
begin
formulae, casks = args.named.to_formulae_and_casks
.partition { |formula_or_cask| formula_or_cask.is_a?(Formula) }
rescue FormulaOrCaskUnavailableError, Cask::CaskUnavailableError => e
retry if Tap.install_default_cask_tap_if_necessary(force: args.cask?)
raise e
end
if casks.any?
Cask::Cmd::Install.install_casks(
*casks,
binaries: args.binaries?,
verbose: args.verbose?,
force: args.force?,
adopt: args.adopt?,
require_sha: args.require_sha?,
skip_cask_deps: args.skip_cask_deps?,
quarantine: args.quarantine?,
quiet: args.quiet?,
dry_run: args.dry_run?,
)
end
# if the user's flags will prevent bottle only-installations when no
# developer tools are available, we need to stop them early on
unless DevelopmentTools.installed?
build_flags = []
build_flags << "--HEAD" if args.HEAD?
build_flags << "--build-bottle" if args.build_bottle?
build_flags << "--build-from-source" if args.build_from_source?
raise BuildFlagsError.new(build_flags, bottled: formulae.all?(&:bottled?)) if build_flags.present?
end
installed_formulae = formulae.select do |f|
Install.install_formula?(
f,
head: args.HEAD?,
fetch_head: args.fetch_HEAD?,
only_dependencies: args.only_dependencies?,
force: args.force?,
quiet: args.quiet?,
)
end
return if installed_formulae.empty?
Install.perform_preinstall_checks(cc: args.cc)
Install.install_formulae(
installed_formulae,
build_bottle: args.build_bottle?,
force_bottle: args.force_bottle?,
bottle_arch: args.bottle_arch,
ignore_deps: args.ignore_dependencies?,
only_deps: args.only_dependencies?,
include_test_formulae: args.include_test_formulae,
build_from_source_formulae: args.build_from_source_formulae,
cc: args.cc,
git: args.git?,
interactive: args.interactive?,
keep_tmp: args.keep_tmp?,
debug_symbols: args.debug_symbols?,
force: args.force?,
overwrite: args.overwrite?,
debug: args.debug?,
quiet: args.quiet?,
verbose: args.verbose?,
dry_run: args.dry_run?,
)
Upgrade.check_installed_dependents(
installed_formulae,
flags: args.flags_only,
installed_on_request: args.named.present?,
force_bottle: args.force_bottle?,
build_from_source_formulae: args.build_from_source_formulae,
interactive: args.interactive?,
keep_tmp: args.keep_tmp?,
debug_symbols: args.debug_symbols?,
force: args.force?,
debug: args.debug?,
quiet: args.quiet?,
verbose: args.verbose?,
dry_run: args.dry_run?,
)
Cleanup.periodic_clean!(dry_run: args.dry_run?)
Homebrew.messages.display_messages(display_times: args.display_times?)
rescue FormulaUnreadableError, FormulaClassUnavailableError,
TapFormulaUnreadableError, TapFormulaClassUnavailableError => e
# Need to rescue before `FormulaUnavailableError` (superclass of this)
# is handled, as searching for a formula doesn't make sense here (the
# formula was found, but there's a problem with its implementation).
$stderr.puts e.backtrace if Homebrew::EnvConfig.developer?
ofail e.message
rescue FormulaOrCaskUnavailableError, Cask::CaskUnavailableError => e
# formula name or cask token
name = e.try(:name) || e.token
if name == "updog"
ofail "What's updog?"
return
end
opoo e
reason = MissingFormula.reason(name, silent: true)
if !args.cask? && reason
$stderr.puts reason
return
end
# We don't seem to get good search results when the tap is specified
# so we might as well return early.
return if name.include?("/")
require "search"
package_types = []
package_types << "formulae" unless args.cask?
package_types << "casks" unless args.formula?
ohai "Searching for similarly named #{package_types.join(" and ")}..."
# Don't treat formula/cask name as a regex
query = string_or_regex = name
all_formulae, all_casks = Search.search_names(query, string_or_regex, args)
if all_formulae.any?
ohai "Formulae", Formatter.columns(all_formulae)
first_formula = all_formulae.first.to_s
puts <<~EOS
To install #{first_formula}, run:
brew install #{first_formula}
EOS
end
puts if all_formulae.any? && all_casks.any?
if all_casks.any?
ohai "Casks", Formatter.columns(all_casks)
first_cask = all_casks.first.to_s
puts <<~EOS
To install #{first_cask}, run:
brew install --cask #{first_cask}
EOS
end
return if all_formulae.any? || all_casks.any?
odie "No #{package_types.join(" or ")} found for #{name}."
end
end
# typed: true
# frozen_string_literal: true
require "formula"
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def leaves_args
Homebrew::CLI::Parser.new do
description <<~EOS
List installed formulae that are not dependencies of another installed formula.
EOS
switch "-r", "--installed-on-request",
description: "Only list leaves that were manually installed."
switch "-p", "--installed-as-dependency",
description: "Only list leaves that were installed as dependencies."
conflicts "--installed-on-request", "--installed-as-dependency"
named_args :none
end
end
def installed_on_request?(formula)
Tab.for_keg(formula.any_installed_keg).installed_on_request
end
def installed_as_dependency?(formula)
Tab.for_keg(formula.any_installed_keg).installed_as_dependency
end
def leaves
args = leaves_args.parse
leaves_list = Formula.installed - Formula.installed.flat_map(&:runtime_formula_dependencies)
leaves_list.select!(&method(:installed_on_request?)) if args.installed_on_request?
leaves_list.select!(&method(:installed_as_dependency?)) if args.installed_as_dependency?
leaves_list.map(&:full_name)
.sort
.each(&method(:puts))
end
end
# typed: false
# frozen_string_literal: true
require "metafiles"
require "formula"
require "cli/parser"
require "cask/cmd"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def list_args
Homebrew::CLI::Parser.new do
description <<~EOS
List all installed formulae and casks.
If <formula> is provided, summarise the paths within its current keg.
If <cask> is provided, list its artifacts.
EOS
switch "--formula", "--formulae",
description: "List only formulae, or treat all named arguments as formulae."
switch "--cask", "--casks",
description: "List only casks, or treat all named arguments as casks."
switch "--full-name",
description: "Print formulae with fully-qualified names. Unless `--full-name`, `--versions` " \
"or `--pinned` are passed, other options (i.e. `-1`, `-l`, `-r` and `-t`) are " \
"passed to `ls`(1) which produces the actual output."
switch "--versions",
description: "Show the version number for installed formulae, or only the specified " \
"formulae if <formula> are provided."
switch "--multiple",
depends_on: "--versions",
description: "Only show formulae with multiple versions installed."
switch "--pinned",
description: "List only pinned formulae, or only the specified (pinned) " \
"formulae if <formula> are provided. See also `pin`, `unpin`."
# passed through to ls
switch "-1",
description: "Force output to be one entry per line. " \
"This is the default when output is not to a terminal."
switch "-l",
description: "List formulae and/or casks in long format. " \
"Has no effect when a formula or cask name is passed as an argument."
switch "-r",
description: "Reverse the order of the formulae and/or casks sort to list the oldest entries first. " \
"Has no effect when a formula or cask name is passed as an argument."
switch "-t",
description: "Sort formulae and/or casks by time modified, listing most recently modified first. " \
"Has no effect when a formula or cask name is passed as an argument."
conflicts "--formula", "--cask"
conflicts "--pinned", "--cask"
conflicts "--multiple", "--cask"
conflicts "--pinned", "--multiple"
["-1", "-l", "-r", "-t"].each do |flag|
conflicts "--versions", flag
conflicts "--pinned", flag
end
["--versions", "--pinned", "-l", "-r", "-t"].each do |flag|
conflicts "--full-name", flag
end
named_args [:installed_formula, :installed_cask]
end
end
def list
args = list_args.parse
if args.full_name?
unless args.cask?
formula_names = args.no_named? ? Formula.installed : args.named.to_resolved_formulae
full_formula_names = formula_names.map(&:full_name).sort(&tap_and_name_comparison)
full_formula_names = Formatter.columns(full_formula_names) unless args.public_send(:"1?")
puts full_formula_names if full_formula_names.present?
end
if args.cask? || (!args.formula? && args.no_named?)
cask_names = if args.no_named?
Cask::Caskroom.casks
else
args.named.to_formulae_and_casks(only: :cask, method: :resolve)
end
full_cask_names = cask_names.map(&:full_name).sort(&tap_and_name_comparison)
full_cask_names = Formatter.columns(full_cask_names) unless args.public_send(:"1?")
puts full_cask_names if full_cask_names.present?
end
elsif args.pinned?
filtered_list(args: args)
elsif args.versions?
filtered_list(args: args) unless args.cask?
list_casks(args: args) if args.cask? || (!args.formula? && !args.multiple? && args.no_named?)
elsif args.no_named?
ENV["CLICOLOR"] = nil
ls_args = []
ls_args << "-1" if args.public_send(:"1?")
ls_args << "-l" if args.l?
ls_args << "-r" if args.r?
ls_args << "-t" if args.t?
if !args.cask? && HOMEBREW_CELLAR.exist? && HOMEBREW_CELLAR.children.any?
ohai "Formulae" if $stdout.tty? && !args.formula?
safe_system "ls", *ls_args, HOMEBREW_CELLAR
puts if $stdout.tty? && !args.formula?
end
if !args.formula? && Cask::Caskroom.any_casks_installed?
ohai "Casks" if $stdout.tty? && !args.cask?
safe_system "ls", *ls_args, Cask::Caskroom.path
end
else
kegs, casks = args.named.to_kegs_to_casks
if args.verbose? || !$stdout.tty?
find_args = %w[-not -type d -not -name .DS_Store -print]
system_command! "find", args: kegs.map(&:to_s) + find_args, print_stdout: true if kegs.present?
system_command! "find", args: casks.map(&:caskroom_path) + find_args, print_stdout: true if casks.present?
else
kegs.each { |keg| PrettyListing.new keg } if kegs.present?
list_casks(args: args) if casks.present?
end
end
end
def filtered_list(args:)
names = if args.no_named?
Formula.racks
else
racks = args.named.map { |n| Formulary.to_rack(n) }
racks.select do |rack|
Homebrew.failed = true unless rack.exist?
rack.exist?
end
end
if args.pinned?
pinned_versions = {}
names.sort.each do |d|
keg_pin = (HOMEBREW_PINNED_KEGS/d.basename.to_s)
pinned_versions[d] = keg_pin.readlink.basename.to_s if keg_pin.exist? || keg_pin.symlink?
end
pinned_versions.each do |d, version|
puts d.basename.to_s.concat(args.versions? ? " #{version}" : "")
end
else # --versions without --pinned
names.sort.each do |d|
versions = d.subdirs.map { |pn| pn.basename.to_s }
next if args.multiple? && versions.length < 2
puts "#{d.basename} #{versions * " "}"
end
end
end
def list_casks(args:)
casks = if args.no_named?
Cask::Caskroom.casks
else
args.named.dup.delete_if do |n|
Homebrew.failed = true unless Cask::Caskroom.path.join(n).exist?
!Cask::Caskroom.path.join(n).exist?
end.to_formulae_and_casks(only: :cask)
end
return if casks.blank?
Cask::Cmd::List.list_casks(
*casks,
one: args.public_send(:"1?"),
full_name: args.full_name?,
versions: args.versions?,
)
end
end
class PrettyListing
def initialize(path)
Pathname.new(path).children.sort_by { |p| p.to_s.downcase }.each do |pn|
case pn.basename.to_s
when "bin", "sbin"
pn.find { |pnn| puts pnn unless pnn.directory? }
when "lib"
print_dir pn do |pnn|
# dylibs have multiple symlinks and we don't care about them
(pnn.extname == ".dylib" || pnn.extname == ".pc") && !pnn.symlink?
end
when ".brew"
next # Ignore .brew
else
if pn.directory?
if pn.symlink?
puts "#{pn} -> #{pn.readlink}"
else
print_dir pn
end
elsif Metafiles.list?(pn.basename.to_s)
puts pn
end
end
end
end
def print_dir(root)
dirs = []
remaining_root_files = []
other = ""
root.children.sort.each do |pn|
if pn.directory?
dirs << pn
elsif block_given? && yield(pn)
puts pn
other = "other "
else
remaining_root_files << pn unless pn.basename.to_s == ".DS_Store"
end
end
dirs.each do |d|
files = []
d.find { |pn| files << pn unless pn.directory? }
print_remaining_files files, d
end
print_remaining_files remaining_root_files, root, other
end
def print_remaining_files(files, root, other = "")
if files.length == 1
puts files
elsif files.length > 1
puts "#{root}/ (#{files.length} #{other}files)"
end
end
end
# typed: false
# frozen_string_literal: true
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def log_args
Homebrew::CLI::Parser.new do
description <<~EOS
Show the `git log` for <formula> or <cask>, or show the log for the Homebrew repository
if no formula or cask is provided.
EOS
switch "-p", "-u", "--patch",
description: "Also print patch from commit."
switch "--stat",
description: "Also print diffstat from commit."
switch "--oneline",
description: "Print only one line per commit."
switch "-1",
description: "Print only one commit."
flag "-n", "--max-count=",
description: "Print only a specified number of commits."
switch "--formula", "--formulae",
description: "Treat all named arguments as formulae."
switch "--cask", "--casks",
description: "Treat all named arguments as casks."
conflicts "-1", "--max-count"
conflicts "--formula", "--cask"
named_args [:formula, :cask], max: 1
end
end
def log
args = log_args.parse
# As this command is simplifying user-run commands then let's just use a
# user path, too.
ENV["PATH"] = PATH.new(ORIGINAL_PATHS).to_s
if args.no_named?
git_log HOMEBREW_REPOSITORY, args: args
else
path = args.named.to_paths.first
tap = Tap.from_path(path)
git_log path.dirname, path, tap, args: args
end
end
def git_log(cd_dir, path = nil, tap = nil, args:)
cd cd_dir
repo = Utils.popen_read("git", "rev-parse", "--show-toplevel").chomp
if tap
name = tap.to_s
git_cd = "$(brew --repo #{tap})"
elsif cd_dir == HOMEBREW_REPOSITORY
name = "Homebrew/brew"
git_cd = "$(brew --repo)"
else
name, git_cd = cd_dir
end
if File.exist? "#{repo}/.git/shallow"
opoo <<~EOS
#{name} is a shallow clone so only partial output will be shown.
To get a full clone, run:
git -C "#{git_cd}" fetch --unshallow
EOS
end
git_args = []
git_args << "--patch" if args.patch?
git_args << "--stat" if args.stat?
git_args << "--oneline" if args.oneline?
git_args << "-1" if args.public_send(:"1?")
git_args << "--max-count" << args.max_count if args.max_count
git_args += ["--follow", "--", path] if path.present?
system "git", "log", *git_args
end
end
# typed: true
# frozen_string_literal: true
require "migrator"
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def migrate_args
Homebrew::CLI::Parser.new do
description <<~EOS
Migrate renamed packages to new names, where <formula> are old names of
packages.
EOS
switch "-f", "--force",
description: "Treat installed <formula> and provided <formula> as if they are from " \
"the same taps and migrate them anyway."
switch "-n", "--dry-run",
description: "Show what would be migrated, but do not actually migrate anything."
named_args :installed_formula, min: 1
end
end
def migrate
args = migrate_args.parse
args.named.to_kegs.each do |keg|
f = Formulary.from_keg(keg)
if f.oldname
rack = HOMEBREW_CELLAR/f.oldname
raise NoSuchKegError, f.oldname if !rack.exist? || rack.subdirs.empty?
odie "#{rack} is a symlink" if rack.symlink?
end
Migrator.migrate_if_needed(f, force: args.force?, dry_run: args.dry_run?)
end
end
end
# typed: true
# frozen_string_literal: true
require "formula"
require "tab"
require "diagnostic"
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def missing_args
Homebrew::CLI::Parser.new do
description <<~EOS
Check the given <formula> kegs for missing dependencies. If no <formula> are
provided, check all kegs. Will exit with a non-zero status if any kegs are found
to be missing dependencies.
EOS
comma_array "--hide",
description: "Act as if none of the specified <hidden> are installed. <hidden> should be " \
"a comma-separated list of formulae."
named_args :formula
end
end
def missing
args = missing_args.parse
return unless HOMEBREW_CELLAR.exist?
ff = if args.no_named?
Formula.installed.sort
else
args.named.to_resolved_formulae.sort
end
ff.each do |f|
missing = f.missing_dependencies(hide: args.hide)
next if missing.empty?
Homebrew.failed = true
print "#{f}: " if ff.size > 1
puts missing.join(" ")
end
end
end
# typed: false
# frozen_string_literal: true
require "formula"
require "options"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def options_args
Homebrew::CLI::Parser.new do
description <<~EOS
Show install options specific to <formula>.
EOS
switch "--compact",
description: "Show all options on a single line separated by spaces."
switch "--installed",
description: "Show options for formulae that are currently installed."
switch "--eval-all",
description: "Evaluate all available formulae and casks, whether installed or not, to show their " \
"options."
switch "--all",
hidden: true
flag "--command=",
description: "Show options for the specified <command>."
conflicts "--installed", "--all", "--command"
named_args :formula
end
end
def options
args = options_args.parse
all = args.eval_all?
if args.all?
odeprecated "brew info --all", "brew info --eval-all" if !all && !Homebrew::EnvConfig.eval_all?
all = true
end
if all
puts_options Formula.all.sort, args: args
elsif args.installed?
puts_options Formula.installed.sort, args: args
elsif args.command.present?
cmd_options = Commands.command_options(args.command)
odie "Unknown command: #{args.command}" if cmd_options.nil?
if args.compact?
puts cmd_options.sort.map(&:first) * " "
else
cmd_options.sort.each { |option, desc| puts "#{option}\n\t#{desc}" }
puts
end
elsif args.no_named?
raise FormulaUnspecifiedError
else
puts_options args.named.to_formulae, args: args
end
end
def puts_options(formulae, args:)
formulae.each do |f|
next if f.options.empty?
if args.compact?
puts f.options.as_flags.sort * " "
else
puts f.full_name if formulae.length > 1
Options.dump_for_formula f
puts
end
end
end
end
# typed: false
# frozen_string_literal: true
require "formula"
require "keg"
require "cli/parser"
require "cask/cmd"
require "cask/caskroom"
require "api"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def outdated_args
Homebrew::CLI::Parser.new do
description <<~EOS
List installed casks and formulae that have an updated version available. By default, version
information is displayed in interactive shells, and suppressed otherwise.
EOS
switch "-q", "--quiet",
description: "List only the names of outdated kegs (takes precedence over `--verbose`)."
switch "-v", "--verbose",
description: "Include detailed version information."
switch "--formula", "--formulae",
description: "List only outdated formulae."
switch "--cask", "--casks",
description: "List only outdated casks."
flag "--json",
description: "Print output in JSON format. There are two versions: `v1` and `v2`. " \
"`v1` is deprecated and is currently the default if no version is specified. " \
"`v2` prints outdated formulae and casks."
switch "--fetch-HEAD",
description: "Fetch the upstream repository to detect if the HEAD installation of the " \
"formula is outdated. Otherwise, the repository's HEAD will only be checked for " \
"updates when a new stable or development version has been released."
switch "--greedy",
description: "Also include outdated casks with `auto_updates true` or `version :latest`."
switch "--greedy-latest",
description: "Also include outdated casks including those with `version :latest`."
switch "--greedy-auto-updates",
description: "Also include outdated casks including those with `auto_updates true`."
conflicts "--quiet", "--verbose", "--json"
conflicts "--formula", "--cask"
named_args [:formula, :cask]
end
end
def outdated
args = outdated_args.parse
case json_version(args.json)
when :v1
odie "`brew outdated --json=v1` is no longer supported. Use brew outdated --json=v2 instead."
when :v2, :default
formulae, casks = if args.formula?
[outdated_formulae(args: args), []]
elsif args.cask?
[[], outdated_casks(args: args)]
else
outdated_formulae_casks args: args
end
json = {
"formulae" => json_info(formulae, args: args),
"casks" => json_info(casks, args: args),
}
puts JSON.pretty_generate(json)
outdated = formulae + casks
else
outdated = if args.formula?
outdated_formulae args: args
elsif args.cask?
outdated_casks args: args
else
outdated_formulae_casks(args: args).flatten
end
print_outdated(outdated, args: args)
end
Homebrew.failed = args.named.present? && outdated.present?
end
def print_outdated(formulae_or_casks, args:)
formulae_or_casks.each do |formula_or_cask|
if formula_or_cask.is_a?(Formula)
f = formula_or_cask
if verbose?
outdated_kegs = f.outdated_kegs(fetch_head: args.fetch_HEAD?)
current_version = if f.alias_changed? && !f.latest_formula.latest_version_installed?
latest = f.latest_formula
"#{latest.name} (#{latest.pkg_version})"
elsif f.head? && outdated_kegs.any? { |k| k.version.to_s == f.pkg_version.to_s }
# There is a newer HEAD but the version number has not changed.
"latest HEAD"
else
f.pkg_version.to_s
end
outdated_versions = outdated_kegs.group_by { |keg| Formulary.from_keg(keg).full_name }
.sort_by { |full_name, _kegs| full_name }
.map do |full_name, kegs|
"#{full_name} (#{kegs.map(&:version).join(", ")})"
end.join(", ")
pinned_version = " [pinned at #{f.pinned_version}]" if f.pinned?
puts "#{outdated_versions} < #{current_version}#{pinned_version}"
else
puts f.full_installed_specified_name
end
else
c = formula_or_cask
puts c.outdated_info(args.greedy?, verbose?, false, args.greedy_latest?, args.greedy_auto_updates?)
end
end
end
def json_info(formulae_or_casks, args:)
formulae_or_casks.map do |formula_or_cask|
if formula_or_cask.is_a?(Formula)
f = formula_or_cask
outdated_versions = f.outdated_kegs(fetch_head: args.fetch_HEAD?).map(&:version)
current_version = if f.head? && outdated_versions.any? { |v| v.to_s == f.pkg_version.to_s }
"HEAD"
else
f.pkg_version.to_s
end
{ name: f.full_name,
installed_versions: outdated_versions.map(&:to_s),
current_version: current_version,
pinned: f.pinned?,
pinned_version: f.pinned_version }
else
c = formula_or_cask
c.outdated_info(args.greedy?, verbose?, true, args.greedy_latest?, args.greedy_auto_updates?)
end
end
end
def verbose?
($stdout.tty? || super) && !quiet?
end
def json_version(version)
version_hash = {
nil => nil,
true => :default,
"v1" => :v1,
"v2" => :v2,
}
raise UsageError, "invalid JSON version: #{version}" unless version_hash.include?(version)
version_hash[version]
end
def outdated_formulae(args:)
select_outdated((args.named.to_resolved_formulae.presence || Formula.installed), args: args).sort
end
def outdated_casks(args:)
if args.named.present?
select_outdated(args.named.to_casks, args: args)
else
select_outdated(Cask::Caskroom.casks, args: args)
end
end
def outdated_formulae_casks(args:)
formulae, casks = args.named.to_resolved_formulae_to_casks
if formulae.blank? && casks.blank?
formulae = Formula.installed
casks = Cask::Caskroom.casks
end
[select_outdated(formulae, args: args).sort, select_outdated(casks, args: args)]
end
def select_outdated(formulae_or_casks, args:)
formulae_or_casks.select do |formula_or_cask|
if formula_or_cask.is_a?(Formula)
formula_or_cask.outdated?(fetch_head: args.fetch_HEAD?)
else
formula_or_cask.outdated?(greedy: args.greedy?, greedy_latest: args.greedy_latest?,
greedy_auto_updates: args.greedy_auto_updates?)
end
end
end
end
# typed: true
# frozen_string_literal: true
require "formula"
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def pin_args
Homebrew::CLI::Parser.new do
description <<~EOS
Pin the specified <formula>, preventing them from being upgraded when
issuing the `brew upgrade` <formula> command. See also `unpin`.
EOS
named_args :installed_formula, min: 1
end
end
def pin
args = pin_args.parse
args.named.to_resolved_formulae.each do |f|
if f.pinned?
opoo "#{f.name} already pinned"
elsif !f.pinnable?
onoe "#{f.name} not installed"
else
f.pin
end
end
end
end
# typed: true
# frozen_string_literal: true
require "sandbox"
require "formula_installer"
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def postinstall_args
Homebrew::CLI::Parser.new do
description <<~EOS
Rerun the post-install steps for <formula>.
EOS
named_args :installed_formula, min: 1
end
end
def postinstall
args = postinstall_args.parse
args.named.to_resolved_formulae.each do |f|
ohai "Postinstalling #{f}"
fi = FormulaInstaller.new(f, **{ debug: args.debug?, quiet: args.quiet?, verbose: args.verbose? }.compact)
fi.post_install
end
end
end
# typed: true
# frozen_string_literal: true
require "readall"
require "cli/parser"
require "env_config"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def readall_args
Homebrew::CLI::Parser.new do
description <<~EOS
Import all items from the specified <tap>, or from all installed taps if none is provided.
This can be useful for debugging issues across all items when making
significant changes to `formula.rb`, testing the performance of loading
all items or checking if any current formulae/casks have Ruby issues.
EOS
switch "--aliases",
description: "Verify any alias symlinks in each tap."
switch "--syntax",
description: "Syntax-check all of Homebrew's Ruby files (if no `<tap>` is passed)."
switch "--eval-all",
description: "Evaluate all available formulae and casks, whether installed or not. " \
"Implied if HOMEBREW_EVAL_ALL is set."
switch "--no-simulate",
description: "Don't simulate other system configurations when checking formulae and casks."
named_args :tap
end
end
def readall
args = readall_args.parse
if args.syntax? && args.no_named?
scan_files = "#{HOMEBREW_LIBRARY_PATH}/**/*.rb"
ruby_files = Dir.glob(scan_files).grep_v(%r{/(vendor)/})
Homebrew.failed = true unless Readall.valid_ruby_syntax?(ruby_files)
end
options = { aliases: args.aliases?, no_simulate: args.no_simulate? }
taps = if args.no_named?
if !args.eval_all? && !Homebrew::EnvConfig.eval_all?
odeprecated "brew readall", "brew readall --eval-all or HOMEBREW_EVAL_ALL"
end
Tap
else
args.named.to_installed_taps
end
taps.each do |tap|
Homebrew.failed = true unless Readall.valid_tap?(tap, options)
end
end
end
# typed: false
# frozen_string_literal: true
require "formula_installer"
require "development_tools"
require "messages"
require "install"
require "reinstall"
require "cli/parser"
require "cleanup"
require "cask/cmd"
require "cask/utils"
require "cask/macos"
require "upgrade"
require "api"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def reinstall_args
Homebrew::CLI::Parser.new do
description <<~EOS
Uninstall and then reinstall a <formula> or <cask> using the same options it was
originally installed with, plus any appended options specific to a <formula>.
Unless `HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK` is set, `brew upgrade` or `brew reinstall` will be run for
outdated dependents and dependents with broken linkage, respectively.
Unless `HOMEBREW_NO_INSTALL_CLEANUP` is set, `brew cleanup` will then be run for the
reinstalled formulae or, every 30 days, for all formulae.
EOS
switch "-d", "--debug",
description: "If brewing fails, open an interactive debugging session with access to IRB " \
"or a shell inside the temporary build directory."
switch "-f", "--force",
description: "Install without checking for previously installed keg-only or " \
"non-migrated versions."
switch "-v", "--verbose",
description: "Print the verification and postinstall steps."
[
[:switch, "--formula", "--formulae", { description: "Treat all named arguments as formulae." }],
[:switch, "-s", "--build-from-source", {
description: "Compile <formula> from source even if a bottle is available.",
}],
[:switch, "-i", "--interactive", {
description: "Download and patch <formula>, then open a shell. This allows the user to " \
"run `./configure --help` and otherwise determine how to turn the software " \
"package into a Homebrew package.",
}],
[:switch, "--force-bottle", {
description: "Install from a bottle if it exists for the current or newest version of " \
"macOS, even if it would not normally be used for installation.",
}],
[:switch, "--keep-tmp", {
description: "Retain the temporary files created during installation.",
}],
[:switch, "--debug-symbols", {
depends_on: "--build-from-source",
description: "Generate debug symbols on build. Source will be retained in a cache directory. ",
}],
[:switch, "--display-times", {
env: :display_install_times,
description: "Print install times for each formula at the end of the run.",
}],
[:switch, "-g", "--git", {
description: "Create a Git repository, useful for creating patches to the software.",
}],
].each do |args|
options = args.pop
send(*args, **options)
conflicts "--cask", args.last
end
formula_options
[
[:switch, "--cask", "--casks", { description: "Treat all named arguments as casks." }],
*Cask::Cmd::AbstractCommand::OPTIONS.map(&:dup),
*Cask::Cmd::Install::OPTIONS.map(&:dup),
].each do |args|
options = args.pop
send(*args, **options)
conflicts "--formula", args.last
end
cask_options
conflicts "--build-from-source", "--force-bottle"
named_args [:formula, :cask], min: 1
end
end
def reinstall
args = reinstall_args.parse
if args.build_from_source? && Homebrew::EnvConfig.install_from_api?
raise UsageError, "--build-from-source is not supported when using HOMEBREW_INSTALL_FROM_API."
end
formulae, casks = args.named.to_formulae_and_casks(method: :resolve)
.partition { |o| o.is_a?(Formula) }
if args.build_from_source? && !DevelopmentTools.installed?
raise BuildFlagsError.new(["--build-from-source"], bottled: formulae.all?(&:bottled?))
end
Install.perform_preinstall_checks
formulae.each do |formula|
if formula.pinned?
onoe "#{formula.full_name} is pinned. You must unpin it to reinstall."
next
end
Migrator.migrate_if_needed(formula, force: args.force?)
reinstall_formula(
formula,
flags: args.flags_only,
installed_on_request: args.named.present?,
force_bottle: args.force_bottle?,
build_from_source_formulae: args.build_from_source_formulae,
interactive: args.interactive?,
keep_tmp: args.keep_tmp?,
debug_symbols: args.debug_symbols?,
force: args.force?,
debug: args.debug?,
quiet: args.quiet?,
verbose: args.verbose?,
git: args.git?,
)
Cleanup.install_formula_clean!(formula)
end
Upgrade.check_installed_dependents(
formulae,
flags: args.flags_only,
installed_on_request: args.named.present?,
force_bottle: args.force_bottle?,
build_from_source_formulae: args.build_from_source_formulae,
interactive: args.interactive?,
keep_tmp: args.keep_tmp?,
debug_symbols: args.debug_symbols?,
force: args.force?,
debug: args.debug?,
quiet: args.quiet?,
verbose: args.verbose?,
)
if casks.any?
Cask::Cmd::Reinstall.reinstall_casks(
*casks,
binaries: args.binaries?,
verbose: args.verbose?,
force: args.force?,
require_sha: args.require_sha?,
skip_cask_deps: args.skip_cask_deps?,
quarantine: args.quarantine?,
zap: args.zap?,
)
end
Cleanup.periodic_clean!
Homebrew.messages.display_messages(display_times: args.display_times?)
end
end
# typed: false
# frozen_string_literal: true
require "formula"
require "missing_formula"
require "descriptions"
require "cli/parser"
require "search"
module Homebrew
extend T::Sig
module_function
PACKAGE_MANAGERS = {
repology: ->(query) { "https://repology.org/projects/?search=#{query}" },
macports: ->(query) { "https://ports.macports.org/search/?q=#{query}" },
fink: ->(query) { "https://pdb.finkproject.org/pdb/browse.php?summary=#{query}" },
opensuse: ->(query) { "https://software.opensuse.org/search?q=#{query}" },
fedora: ->(query) { "https://packages.fedoraproject.org/search?query=#{query}" },
archlinux: ->(query) { "https://archlinux.org/packages/?q=#{query}" },
debian: lambda { |query|
"https://packages.debian.org/search?keywords=#{query}&searchon=names&suite=all&section=all"
},
ubuntu: lambda { |query|
"https://packages.ubuntu.com/search?keywords=#{query}&searchon=names&suite=all&section=all"
},
}.freeze
sig { returns(CLI::Parser) }
def search_args
Homebrew::CLI::Parser.new do
description <<~EOS
Perform a substring search of cask tokens and formula names for <text>. If <text>
is flanked by slashes, it is interpreted as a regular expression.
The search for <text> is extended online to `homebrew/core` and `homebrew/cask`.
EOS
switch "--formula", "--formulae",
description: "Search online and locally for formulae."
switch "--cask", "--casks",
description: "Search online and locally for casks."
switch "--desc",
description: "Search for formulae with a description matching <text> and casks with " \
"a name or description matching <text>."
switch "--eval-all",
depends_on: "--desc",
description: "Evaluate all available formulae and casks, whether installed or not, to search their " \
"descriptions. Implied if HOMEBREW_EVAL_ALL is set."
switch "--pull-request",
description: "Search for GitHub pull requests containing <text>."
switch "--open",
depends_on: "--pull-request",
description: "Search for only open GitHub pull requests."
switch "--closed",
depends_on: "--pull-request",
description: "Search for only closed GitHub pull requests."
package_manager_switches = PACKAGE_MANAGERS.keys.map { |name| "--#{name}" }
package_manager_switches.each do |s|
switch s,
description: "Search for <text> in the given database."
end
conflicts "--desc", "--pull-request"
conflicts "--open", "--closed"
conflicts(*package_manager_switches)
named_args :text_or_regex, min: 1
end
end
def search
args = search_args.parse
return if search_package_manager(args)
query = args.named.join(" ")
string_or_regex = Search.query_regexp(query)
if args.desc?
if !args.eval_all? && !Homebrew::EnvConfig.eval_all?
odeprecated "brew search --desc", "brew search --desc --eval-all or HOMEBREW_EVAL_ALL"
end
Search.search_descriptions(string_or_regex, args)
elsif args.pull_request?
search_pull_requests(query, args)
else
formulae, casks = Search.search_names(query, string_or_regex, args)
print_results(formulae, casks, query)
end
puts "Use `brew desc` to list packages with a short description." if args.verbose?
print_regex_help(args)
end
def print_regex_help(args)
return unless $stdout.tty?
metacharacters = %w[\\ | ( ) [ ] { } ^ $ * + ?].freeze
return unless metacharacters.any? do |char|
args.named.any? do |arg|
arg.include?(char) && !arg.start_with?("/")
end
end
opoo <<~EOS
Did you mean to perform a regular expression search?
Surround your query with /slashes/ to search locally by regex.
EOS
end
def search_package_manager(args)
package_manager = PACKAGE_MANAGERS.find { |name,| args[:"#{name}?"] }
return false if package_manager.nil?
_, url = package_manager
exec_browser url.call(URI.encode_www_form_component(args.named.join(" ")))
true
end
def search_pull_requests(query, args)
only = if args.open? && !args.closed?
"open"
elsif args.closed? && !args.open?
"closed"
end
GitHub.print_pull_requests_matching(query, only)
end
def print_results(all_formulae, all_casks, query)
count = all_formulae.size + all_casks.size
if all_formulae.any?
if $stdout.tty?
ohai "Formulae", Formatter.columns(all_formulae)
else
puts all_formulae
end
end
puts if all_formulae.any? && all_casks.any?
if all_casks.any?
if $stdout.tty?
ohai "Casks", Formatter.columns(all_casks)
else
puts all_casks
end
end
print_missing_formula_help(query, count.positive?) if all_casks.exclude?(query)
odie "No formulae or casks found for #{query.inspect}." if count.zero?
end
def print_missing_formula_help(query, found_matches)
return unless $stdout.tty?
reason = MissingFormula.reason(query, silent: true)
return if reason.nil?
if found_matches
puts
puts "If you meant #{query.inspect} specifically:"
end
puts reason
end
end
#: * `shellenv`
#:
#: Print export statements. When run in a shell, this installation of Homebrew will be added to your `PATH`, `MANPATH`, and `INFOPATH`.
#:
#: The variables `HOMEBREW_PREFIX`, `HOMEBREW_CELLAR` and `HOMEBREW_REPOSITORY` are also exported to avoid querying them multiple times.
#: To help guarantee idempotence, this command produces no output when Homebrew's `bin` and `sbin` directories are first and second
#: respectively in your `PATH`. Consider adding evaluation of this command's output to your dotfiles (e.g. `~/.profile`,
#: `~/.bash_profile`, or `~/.zprofile`) with: `eval "$(brew shellenv)"`
# HOMEBREW_CELLAR and HOMEBREW_PREFIX are set by extend/ENV/super.rb
# HOMEBREW_REPOSITORY is set by bin/brew
# Trailing colon in MANPATH adds default man dirs to search path in Linux, does no harm in macOS.
# Please do not submit PRs to remove it!
# shellcheck disable=SC2154
homebrew-shellenv() {
if [[ "${HOMEBREW_PATH%%:"${HOMEBREW_PREFIX}"/sbin*}" == "${HOMEBREW_PREFIX}/bin" ]]
then
return
fi
case "$(/bin/ps -p "${PPID}" -c -o comm=)" in
fish | -fish)
echo "set -gx HOMEBREW_PREFIX \"${HOMEBREW_PREFIX}\";"
echo "set -gx HOMEBREW_CELLAR \"${HOMEBREW_CELLAR}\";"
echo "set -gx HOMEBREW_REPOSITORY \"${HOMEBREW_REPOSITORY}\";"
echo "set -q PATH; or set PATH ''; set -gx PATH \"${HOMEBREW_PREFIX}/bin\" \"${HOMEBREW_PREFIX}/sbin\" \$PATH;"
echo "set -q MANPATH; or set MANPATH ''; set -gx MANPATH \"${HOMEBREW_PREFIX}/share/man\" \$MANPATH;"
echo "set -q INFOPATH; or set INFOPATH ''; set -gx INFOPATH \"${HOMEBREW_PREFIX}/share/info\" \$INFOPATH;"
;;
csh | -csh | tcsh | -tcsh)
echo "setenv HOMEBREW_PREFIX ${HOMEBREW_PREFIX};"
echo "setenv HOMEBREW_CELLAR ${HOMEBREW_CELLAR};"
echo "setenv HOMEBREW_REPOSITORY ${HOMEBREW_REPOSITORY};"
echo "setenv PATH ${HOMEBREW_PREFIX}/bin:${HOMEBREW_PREFIX}/sbin:\$PATH;"
echo "setenv MANPATH ${HOMEBREW_PREFIX}/share/man\`[ \${?MANPATH} == 1 ] && echo \":\${MANPATH}\"\`:;"
echo "setenv INFOPATH ${HOMEBREW_PREFIX}/share/info\`[ \${?INFOPATH} == 1 ] && echo \":\${INFOPATH}\"\`;"
;;
pwsh | -pwsh | pwsh-preview | -pwsh-preview)
echo "[System.Environment]::SetEnvironmentVariable('HOMEBREW_PREFIX','${HOMEBREW_PREFIX}',[System.EnvironmentVariableTarget]::Process)"
echo "[System.Environment]::SetEnvironmentVariable('HOMEBREW_CELLAR','${HOMEBREW_CELLAR}',[System.EnvironmentVariableTarget]::Process)"
echo "[System.Environment]::SetEnvironmentVariable('HOMEBREW_REPOSITORY','${HOMEBREW_REPOSITORY}',[System.EnvironmentVariableTarget]::Process)"
echo "[System.Environment]::SetEnvironmentVariable('PATH',\$('${HOMEBREW_PREFIX}/bin:${HOMEBREW_PREFIX}/sbin:'+\$ENV:PATH),[System.EnvironmentVariableTarget]::Process)"
echo "[System.Environment]::SetEnvironmentVariable('MANPATH',\$('${HOMEBREW_PREFIX}/share/man'+\$(if(\${ENV:MANPATH}){':'+\${ENV:MANPATH}})+':'),[System.EnvironmentVariableTarget]::Process)"
echo "[System.Environment]::SetEnvironmentVariable('INFOPATH',\$('${HOMEBREW_PREFIX}/share/info'+\$(if(\${ENV:INFOPATH}){':'+\${ENV:INFOPATH}})),[System.EnvironmentVariableTarget]::Process)"
;;
*)
echo "export HOMEBREW_PREFIX=\"${HOMEBREW_PREFIX}\";"
echo "export HOMEBREW_CELLAR=\"${HOMEBREW_CELLAR}\";"
echo "export HOMEBREW_REPOSITORY=\"${HOMEBREW_REPOSITORY}\";"
echo "export PATH=\"${HOMEBREW_PREFIX}/bin:${HOMEBREW_PREFIX}/sbin\${PATH+:\$PATH}\";"
echo "export MANPATH=\"${HOMEBREW_PREFIX}/share/man\${MANPATH+:\$MANPATH}:\";"
echo "export INFOPATH=\"${HOMEBREW_PREFIX}/share/info:\${INFOPATH:-}\";"
;;
esac
}
# typed: false
# frozen_string_literal: true
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def tap_info_args
Homebrew::CLI::Parser.new do
description <<~EOS
Show detailed information about one or more <tap>s.
If no <tap> names are provided, display brief statistics for all installed taps.
EOS
switch "--installed",
description: "Show information on each installed tap."
flag "--json",
description: "Print a JSON representation of <tap>. Currently the default and only accepted " \
"value for <version> is `v1`. See the docs for examples of using the JSON " \
"output: <https://docs.brew.sh/Querying-Brew>"
named_args :tap
end
end
def tap_info
args = tap_info_args.parse
taps = if args.installed?
Tap
else
args.named.to_taps
end
if args.json
raise UsageError, "invalid JSON version: #{args.json}" unless ["v1", true].include? args.json
print_tap_json(taps.sort_by(&:to_s))
else
print_tap_info(taps.sort_by(&:to_s))
end
end
def print_tap_info(taps)
if taps.none?
tap_count = 0
formula_count = 0
command_count = 0
pinned_count = 0
private_count = 0
Tap.each do |tap|
tap_count += 1
formula_count += tap.formula_files.size
command_count += tap.command_files.size
pinned_count += 1 if tap.pinned?
private_count += 1 if tap.private?
end
info = "#{tap_count} #{"tap".pluralize(tap_count)}"
info += ", #{pinned_count} pinned"
info += ", #{private_count} private"
info += ", #{formula_count} #{"formula".pluralize(formula_count)}"
info += ", #{command_count} #{"command".pluralize(command_count)}"
info += ", #{Tap::TAP_DIRECTORY.dup.abv}" if Tap::TAP_DIRECTORY.directory?
puts info
else
taps.each_with_index do |tap, i|
puts unless i.zero?
info = "#{tap}: "
if tap.installed?
info += if (contents = tap.contents).blank?
"no commands/casks/formulae"
else
contents.join(", ")
end
info += ", private" if tap.private?
info += "\n#{tap.path} (#{tap.path.abv})"
info += "\nFrom: #{tap.remote.presence || "N/A"}"
else
info += "Not installed"
end
puts info
end
end
end
def print_tap_json(taps)
puts JSON.pretty_generate(taps.map(&:to_hash))
end
end
# typed: true
# frozen_string_literal: true
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def tap_args
Homebrew::CLI::Parser.new do
usage_banner "`tap` [<options>] [<user>`/`<repo>] [<URL>]"
description <<~EOS
Tap a formula repository.
If no arguments are provided, list all installed taps.
With <URL> unspecified, tap a formula repository from GitHub using HTTPS.
Since so many taps are hosted on GitHub, this command is a shortcut for
`brew tap` <user>`/`<repo> `https://github.com/`<user>`/homebrew-`<repo>.
With <URL> specified, tap a formula repository from anywhere, using
any transport protocol that `git`(1) handles. The one-argument form of `tap`
simplifies but also limits. This two-argument command makes no
assumptions, so taps can be cloned from places other than GitHub and
using protocols other than HTTPS, e.g. SSH, git, HTTP, FTP(S), rsync.
EOS
switch "--full",
description: "Convert a shallow clone to a full clone without untapping. Taps are only cloned as " \
"shallow clones if `--shallow` was originally passed.",
replacement: false
switch "--shallow",
description: "Fetch tap as a shallow clone rather than a full clone. Useful for continuous integration.",
replacement: false
switch "--[no-]force-auto-update",
description: "Auto-update tap even if it is not hosted on GitHub. By default, only taps " \
"hosted on GitHub are auto-updated (for performance reasons)."
switch "--custom-remote",
description: "Install or change a tap with a custom remote. Useful for mirrors."
switch "--repair",
description: "Migrate tapped formulae from symlink-based to directory-based structure."
switch "--list-pinned",
description: "List all pinned taps."
switch "--eval-all",
description: "Evaluate all the formulae, casks and aliases in the new tap to check validity. " \
"Implied if HOMEBREW_EVAL_ALL is set."
named_args :tap, max: 2
end
end
sig { void }
def tap
args = tap_args.parse
if args.repair?
Tap.each(&:link_completions_and_manpages)
Tap.each(&:fix_remote_configuration)
elsif args.list_pinned?
puts Tap.select(&:pinned?).map(&:name)
elsif args.no_named?
puts Tap.names
else
tap = Tap.fetch(args.named.first)
begin
tap.install clone_target: args.named.second,
force_auto_update: args.force_auto_update?,
custom_remote: args.custom_remote?,
quiet: args.quiet?,
verify: args.eval_all? || Homebrew::EnvConfig.eval_all?
rescue TapRemoteMismatchError, TapNoCustomRemoteError => e
odie e
rescue TapAlreadyTappedError
nil
end
end
end
end
# typed: true
# frozen_string_literal: true
require "keg"
require "formula"
require "diagnostic"
require "migrator"
require "cli/parser"
require "cask/cmd"
require "cask/cask_loader"
require "uninstall"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def uninstall_args
Homebrew::CLI::Parser.new do
description <<~EOS
Uninstall a <formula> or <cask>.
EOS
switch "-f", "--force",
description: "Delete all installed versions of <formula>. Uninstall even if <cask> is not " \
"installed, overwrite existing files and ignore errors when removing files."
switch "--zap",
description: "Remove all files associated with a <cask>. " \
"*May remove files which are shared between applications.*"
switch "--ignore-dependencies",
description: "Don't fail uninstall, even if <formula> is a dependency of any installed " \
"formulae."
switch "--formula", "--formulae",
description: "Treat all named arguments as formulae."
switch "--cask", "--casks",
description: "Treat all named arguments as casks."
conflicts "--formula", "--cask"
conflicts "--formula", "--zap"
named_args [:installed_formula, :installed_cask], min: 1
end
end
def uninstall
args = uninstall_args.parse
all_kegs, casks = args.named.to_kegs_to_casks(
ignore_unavailable: args.force?,
all_kegs: args.force?,
)
# If ignore_unavailable is true and the named args
# are a series of invalid kegs and casks,
# #to_kegs_to_casks will return empty arrays.
return if all_kegs.blank? && casks.blank?
kegs_by_rack = all_kegs.group_by(&:rack)
Uninstall.uninstall_kegs(
kegs_by_rack,
casks: casks,
force: args.force?,
ignore_dependencies: args.ignore_dependencies?,
named_args: args.named,
)
if args.zap?
T.unsafe(Cask::Cmd::Zap).zap_casks(
*casks,
verbose: args.verbose?,
force: args.force?,
)
else
T.unsafe(Cask::Cmd::Uninstall).uninstall_casks(
*casks,
verbose: args.verbose?,
force: args.force?,
)
end
Cleanup.autoremove if Homebrew::EnvConfig.autoremove?
end
end
# typed: true
# frozen_string_literal: true
require "formula"
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def unpin_args
Homebrew::CLI::Parser.new do
description <<~EOS
Unpin <formula>, allowing them to be upgraded by `brew upgrade` <formula>.
See also `pin`.
EOS
named_args :installed_formula, min: 1
end
end
def unpin
args = unpin_args.parse
args.named.to_resolved_formulae.each do |f|
if f.pinned?
f.unpin
elsif !f.pinnable?
onoe "#{f.name} not installed"
else
opoo "#{f.name} not pinned"
end
end
end
end
# typed: true
# frozen_string_literal: true
require "cli/parser"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def untap_args
Homebrew::CLI::Parser.new do
description <<~EOS
Remove a tapped formula repository.
EOS
switch "-f", "--force",
description: "Untap even if formulae or casks from this tap are currently installed."
named_args :tap, min: 1
end
end
def untap
args = untap_args.parse
args.named.to_installed_taps.each do |tap|
odie "Untapping #{tap} is not allowed" if tap.core_tap? && !Homebrew::EnvConfig.install_from_api?
if !Homebrew::EnvConfig.install_from_api? || (!tap.core_tap? && tap != "homebrew/cask")
installed_tap_formulae = Formula.installed.select { |formula| formula.tap == tap }
installed_tap_casks = Cask::Caskroom.casks.select { |cask| cask.tap == tap }
if installed_tap_formulae.present? || installed_tap_casks.present?
installed_names = (installed_tap_formulae + installed_tap_casks.map(&:token)).join("\n")
if args.force? || Homebrew::EnvConfig.developer?
opoo <<~EOS
Untapping #{tap} even though it contains the following installed formulae or casks:
#{installed_names}
EOS
else
odie <<~EOS
Refusing to untap #{tap} because it contains the following installed formulae or casks:
#{installed_names}
EOS
end
end
end
tap.uninstall manual: true
end
end
end
# typed: false
# frozen_string_literal: true
require "migrator"
require "formulary"
require "descriptions"
require "cleanup"
require "description_cache_store"
require "cli/parser"
require "settings"
require "linuxbrew-core-migration"
module Homebrew
extend T::Sig
module_function
def auto_update_header(args:)
@auto_update_header ||= begin
ohai "Auto-updated Homebrew!" if args.auto_update?
true
end
end
sig { returns(CLI::Parser) }
def update_report_args
Homebrew::CLI::Parser.new do
description <<~EOS
The Ruby implementation of `brew update`. Never called manually.
EOS
switch "--auto-update", "--preinstall",
description: "Run in 'auto-update' mode (faster, less output)."
switch "-f", "--force",
description: "Treat installed and updated formulae as if they are from " \
"the same taps and migrate them anyway."
hide_from_man_page!
end
end
def update_report
return output_update_report if $stdout.tty?
redirect_stdout($stderr) do
output_update_report
end
end
def output_update_report
args = update_report_args.parse
# Run `brew update` (again) if we've got a linuxbrew-core CoreTap
if CoreTap.instance.installed? && CoreTap.instance.linuxbrew_core? &&
ENV["HOMEBREW_LINUXBREW_CORE_MIGRATION"].blank?
ohai "Re-running `brew update` for linuxbrew-core migration"
if HOMEBREW_CORE_DEFAULT_GIT_REMOTE != Homebrew::EnvConfig.core_git_remote
opoo <<~EOS
HOMEBREW_CORE_GIT_REMOTE was set: #{Homebrew::EnvConfig.core_git_remote}.
It has been unset for the migration.
You may need to change this from a linuxbrew-core mirror to a homebrew-core one.
EOS
end
ENV.delete("HOMEBREW_CORE_GIT_REMOTE")
if HOMEBREW_BOTTLE_DEFAULT_DOMAIN != Homebrew::EnvConfig.bottle_domain
opoo <<~EOS
HOMEBREW_BOTTLE_DOMAIN was set: #{Homebrew::EnvConfig.bottle_domain}.
It has been unset for the migration.
You may need to change this from a Linuxbrew package mirror to a Homebrew one.
EOS
end
ENV.delete("HOMEBREW_BOTTLE_DOMAIN")
ENV["HOMEBREW_LINUXBREW_CORE_MIGRATION"] = "1"
FileUtils.rm_f HOMEBREW_LOCKS/"update"
update_args = []
update_args << "--auto-update" if args.auto_update?
update_args << "--force" if args.force?
exec HOMEBREW_BREW_FILE, "update", *update_args
end
if !Utils::Analytics.messages_displayed? &&
!Utils::Analytics.disabled? &&
!Utils::Analytics.no_message_output?
ENV["HOMEBREW_NO_ANALYTICS_THIS_RUN"] = "1"
# Use the shell's audible bell.
print "\a"
# Use an extra newline and bold to avoid this being missed.
ohai "Homebrew has enabled anonymous aggregate formula and cask analytics."
puts <<~EOS
#{Tty.bold}Read the analytics documentation (and how to opt-out) here:
#{Formatter.url("https://docs.brew.sh/Analytics")}#{Tty.reset}
No analytics have been recorded yet (nor will be during this `brew` run).
EOS
# Consider the messages possibly missed if not a TTY.
Utils::Analytics.messages_displayed! if $stdout.tty?
end
if Settings.read("donationmessage") != "true" && !args.quiet?
ohai "Homebrew is run entirely by unpaid volunteers. Please consider donating:"
puts " #{Formatter.url("https://github.com/Homebrew/brew#donations")}\n\n"
# Consider the message possibly missed if not a TTY.
Settings.write "donationmessage", true if $stdout.tty?
end
install_core_tap_if_necessary
updated = false
new_tag = nil
initial_revision = ENV["HOMEBREW_UPDATE_BEFORE"].to_s
current_revision = ENV["HOMEBREW_UPDATE_AFTER"].to_s
odie "update-report should not be called directly!" if initial_revision.empty? || current_revision.empty?
if initial_revision != current_revision
auto_update_header args: args
updated = true
old_tag = Settings.read "latesttag"
new_tag = Utils.popen_read(
"git", "-C", HOMEBREW_REPOSITORY, "tag", "--list", "--sort=-version:refname", "*.*"
).lines.first.chomp
Settings.write "latesttag", new_tag if new_tag != old_tag
if new_tag == old_tag
ohai "Updated Homebrew from #{shorten_revision(initial_revision)} to #{shorten_revision(current_revision)}."
elsif old_tag.blank?
ohai "Updated Homebrew from #{shorten_revision(initial_revision)} " \
"to #{new_tag} (#{shorten_revision(current_revision)})."
else
ohai "Updated Homebrew from #{old_tag} (#{shorten_revision(initial_revision)}) " \
"to #{new_tag} (#{shorten_revision(current_revision)})."
end
end
Homebrew.failed = true if ENV["HOMEBREW_UPDATE_FAILED"]
return if Homebrew::EnvConfig.disable_load_formula?
migrate_gcc_dependents_if_needed
hub = ReporterHub.new
updated_taps = []
Tap.each do |tap|
next unless tap.git?
next if (tap.core_tap? || tap == "homebrew/cask") && Homebrew::EnvConfig.install_from_api?
if ENV["HOMEBREW_MIGRATE_LINUXBREW_FORMULAE"].present? && tap.core_tap? &&
Settings.read("linuxbrewmigrated") != "true"
ohai "Migrating formulae from linuxbrew-core to homebrew-core"
LINUXBREW_CORE_MIGRATION_LIST.each do |name|
begin
formula = Formula[name]
rescue FormulaUnavailableError
next
end
next unless formula.any_version_installed?
keg = formula.installed_kegs.last
tab = Tab.for_keg(keg)
# force a `brew upgrade` from the linuxbrew-core version to the homebrew-core version (even if lower)
tab.source["versions"]["version_scheme"] = -1
tab.write
end
Settings.write "linuxbrewmigrated", true
end
begin
reporter = Reporter.new(tap)
rescue Reporter::ReporterRevisionUnsetError => e
onoe "#{e.message}\n#{e.backtrace.join "\n"}" if Homebrew::EnvConfig.developer?
next
end
if reporter.updated?
updated_taps << tap.name
hub.add(reporter, auto_update: args.auto_update?)
end
end
unless updated_taps.empty?
auto_update_header args: args
puts "Updated #{updated_taps.count} #{"tap".pluralize(updated_taps.count)} (#{updated_taps.to_sentence})."
updated = true
end
if updated
if hub.empty?
puts "No changes to formulae." unless args.quiet?
else
if ENV.fetch("HOMEBREW_UPDATE_REPORT_ONLY_INSTALLED", false)
opoo "HOMEBREW_UPDATE_REPORT_ONLY_INSTALLED is now the default behaviour, " \
"so you can unset it from your environment."
end
hub.dump(updated_formula_report: !args.auto_update?) unless args.quiet?
hub.reporters.each(&:migrate_tap_migration)
hub.reporters.each { |r| r.migrate_formula_rename(force: args.force?, verbose: args.verbose?) }
CacheStoreDatabase.use(:descriptions) do |db|
DescriptionCacheStore.new(db)
.update_from_report!(hub)
end
CacheStoreDatabase.use(:cask_descriptions) do |db|
CaskDescriptionCacheStore.new(db)
.update_from_report!(hub)
end
end
puts if args.auto_update?
elsif !args.auto_update? && !ENV["HOMEBREW_UPDATE_FAILED"] && !ENV["HOMEBREW_MIGRATE_LINUXBREW_FORMULAE"]
puts "Already up-to-date." unless args.quiet?
end
Commands.rebuild_commands_completion_list
link_completions_manpages_and_docs
Tap.each(&:link_completions_and_manpages)
failed_fetch_dirs = ENV["HOMEBREW_MISSING_REMOTE_REF_DIRS"]&.split("\n")
if failed_fetch_dirs.present?
failed_fetch_taps = failed_fetch_dirs.map { |dir| Tap.from_path(dir) }
ofail <<~EOS
Some taps failed to update!
The following taps can not read their remote branches:
#{failed_fetch_taps.join("\n ")}
This is happening because the remote branch was renamed or deleted.
Reset taps to point to the correct remote branches by running `brew tap --repair`
EOS
end
return if new_tag.blank? || new_tag == old_tag || args.quiet?
puts
new_major_version, new_minor_version, new_patch_version = new_tag.split(".").map(&:to_i)
old_major_version, old_minor_version = (old_tag.split(".")[0, 2]).map(&:to_i) if old_tag.present?
if old_tag.blank? || new_major_version > old_major_version \
|| new_minor_version > old_minor_version
puts <<~EOS
The #{new_major_version}.#{new_minor_version}.0 release notes are available on the Homebrew Blog:
#{Formatter.url("https://brew.sh/blog/#{new_major_version}.#{new_minor_version}.0")}
EOS
end
return if new_patch_version.zero?
puts <<~EOS
The #{new_tag} changelog can be found at:
#{Formatter.url("https://github.com/Homebrew/brew/releases/tag/#{new_tag}")}
EOS
end
def shorten_revision(revision)
Utils.popen_read("git", "-C", HOMEBREW_REPOSITORY, "rev-parse", "--short", revision).chomp
end
def install_core_tap_if_necessary
return if ENV["HOMEBREW_UPDATE_TEST"]
return if Homebrew::EnvConfig.install_from_api?
core_tap = CoreTap.instance
return if core_tap.installed?
CoreTap.ensure_installed!
revision = core_tap.git_head
ENV["HOMEBREW_UPDATE_BEFORE_HOMEBREW_HOMEBREW_CORE"] = revision
ENV["HOMEBREW_UPDATE_AFTER_HOMEBREW_HOMEBREW_CORE"] = revision
end
def link_completions_manpages_and_docs(repository = HOMEBREW_REPOSITORY)
command = "brew update"
Utils::Link.link_completions(repository, command)
Utils::Link.link_manpages(repository, command)
Utils::Link.link_docs(repository, command)
rescue => e
ofail <<~EOS
Failed to link all completions, docs and manpages:
#{e}
EOS
end
def migrate_gcc_dependents_if_needed
# TODO: Refactor and move to extend/os
return if OS.mac? # rubocop:disable Homebrew/MoveToExtendOS
return if Settings.read("gcc-rpaths.fixed") == "true"
Formula.installed.each do |formula|
next unless formula.tap&.core_tap?
recursive_runtime_dependencies = Dependency.expand(
formula,
cache_key: "update-report",
) do |_, dependency|
Dependency.prune if dependency.build? || dependency.test?
end
next unless recursive_runtime_dependencies.map(&:name).include? "gcc"
keg = formula.installed_kegs.last
tab = Tab.for_keg(keg)
# Force reinstallation upon `brew upgrade` to fix the bottle RPATH.
tab.source["versions"]["version_scheme"] = -1
tab.write
rescue TapFormulaUnavailableError
nil
end
Settings.write "gcc-rpaths.fixed", true
end
end
class Reporter
class ReporterRevisionUnsetError < RuntimeError
def initialize(var_name)
super "#{var_name} is unset!"
end
end
attr_reader :tap, :initial_revision, :current_revision
def initialize(tap)
@tap = tap
initial_revision_var = "HOMEBREW_UPDATE_BEFORE#{tap.repo_var}"
@initial_revision = ENV[initial_revision_var].to_s
raise ReporterRevisionUnsetError, initial_revision_var if @initial_revision.empty?
current_revision_var = "HOMEBREW_UPDATE_AFTER#{tap.repo_var}"
@current_revision = ENV[current_revision_var].to_s
raise ReporterRevisionUnsetError, current_revision_var if @current_revision.empty?
end
def report(auto_update: false)
return @report if @report
@report = Hash.new { |h, k| h[k] = [] }
return @report unless updated?
diff.each_line do |line|
status, *paths = line.split
src = Pathname.new paths.first
dst = Pathname.new paths.last
next unless dst.extname == ".rb"
if paths.any? { |p| tap.cask_file?(p) }
case status
when "A"
# Have a dedicated report array for new casks.
@report[:AC] << tap.formula_file_to_name(src)
when "D"
# Have a dedicated report array for deleted casks.
@report[:DC] << tap.formula_file_to_name(src)
when "M"
# Report updated casks
@report[:MC] << tap.formula_file_to_name(src)
end
end
next unless paths.any? { |p| tap.formula_file?(p) }
case status
when "A", "D"
full_name = tap.formula_file_to_name(src)
name = full_name.split("/").last
new_tap = tap.tap_migrations[name]
@report[status.to_sym] << full_name unless new_tap
when "M"
name = tap.formula_file_to_name(src)
@report[:M] << name
when /^R\d{0,3}/
src_full_name = tap.formula_file_to_name(src)
dst_full_name = tap.formula_file_to_name(dst)
# Don't report formulae that are moved within a tap but not renamed
next if src_full_name == dst_full_name
@report[:D] << src_full_name
@report[:A] << dst_full_name
end
end
renamed_formulae = Set.new
@report[:D].each do |old_full_name|
old_name = old_full_name.split("/").last
new_name = tap.formula_renames[old_name]
next unless new_name
new_full_name = if tap.core_tap?
new_name
else
"#{tap}/#{new_name}"
end
renamed_formulae << [old_full_name, new_full_name] if @report[:A].include? new_full_name
end
@report[:A].each do |new_full_name|
new_name = new_full_name.split("/").last
old_name = tap.formula_renames.key(new_name)
next unless old_name
old_full_name = if tap.core_tap?
old_name
else
"#{tap}/#{old_name}"
end
renamed_formulae << [old_full_name, new_full_name]
end
if renamed_formulae.present?
@report[:A] -= renamed_formulae.map(&:last)
@report[:D] -= renamed_formulae.map(&:first)
@report[:R] = renamed_formulae.to_a
end
@report
end
def updated?
initial_revision != current_revision
end
def migrate_tap_migration
(report[:D] + report[:DC]).each do |full_name|
name = full_name.split("/").last
new_tap_name = tap.tap_migrations[name]
next if new_tap_name.nil? # skip if not in tap_migrations list.
new_tap_user, new_tap_repo, new_tap_new_name = new_tap_name.split("/")
new_name = if new_tap_new_name
new_full_name = new_tap_new_name
new_tap_name = "#{new_tap_user}/#{new_tap_repo}"
new_tap_new_name
else
new_full_name = "#{new_tap_name}/#{name}"
name
end
# This means it is a cask
if report[:DC].include? full_name
next unless (HOMEBREW_PREFIX/"Caskroom"/new_name).exist?
new_tap = Tap.fetch(new_tap_name)
new_tap.install unless new_tap.installed?
ohai "#{name} has been moved to Homebrew.", <<~EOS
To uninstall the cask, run:
brew uninstall --cask --force #{name}
EOS
next if (HOMEBREW_CELLAR/new_name.split("/").last).directory?
ohai "Installing #{new_name}..."
system HOMEBREW_BREW_FILE, "install", new_full_name
begin
unless Formulary.factory(new_full_name).keg_only?
system HOMEBREW_BREW_FILE, "link", new_full_name, "--overwrite"
end
rescue Exception => e # rubocop:disable Lint/RescueException
onoe "#{e.message}\n#{e.backtrace.join "\n"}" if Homebrew::EnvConfig.developer?
end
next
end
next unless (dir = HOMEBREW_CELLAR/name).exist? # skip if formula is not installed.
tabs = dir.subdirs.map { |d| Tab.for_keg(Keg.new(d)) }
next unless tabs.first.tap == tap # skip if installed formula is not from this tap.
new_tap = Tap.fetch(new_tap_name)
# For formulae migrated to cask: Auto-install cask or provide install instructions.
if new_tap_name.start_with?("homebrew/cask")
if new_tap.installed? && (HOMEBREW_PREFIX/"Caskroom").directory?
ohai "#{name} has been moved to Homebrew Cask."
ohai "brew unlink #{name}"
system HOMEBREW_BREW_FILE, "unlink", name
ohai "brew cleanup"
system HOMEBREW_BREW_FILE, "cleanup"
ohai "brew install --cask #{new_name}"
system HOMEBREW_BREW_FILE, "install", "--cask", new_name
ohai <<~EOS
#{name} has been moved to Homebrew Cask.
The existing keg has been unlinked.
Please uninstall the formula when convenient by running:
brew uninstall --force #{name}
EOS
else
ohai "#{name} has been moved to Homebrew Cask.", <<~EOS
To uninstall the formula and install the cask, run:
brew uninstall --force #{name}
brew tap #{new_tap_name}
brew install --cask #{new_name}
EOS
end
else
new_tap.install unless new_tap.installed?
# update tap for each Tab
tabs.each { |tab| tab.tap = new_tap }
tabs.each(&:write)
end
end
end
def migrate_formula_rename(force:, verbose:)
Formula.installed.each do |formula|
next unless Migrator.needs_migration?(formula)
oldname = formula.oldname
oldname_rack = HOMEBREW_CELLAR/oldname
if oldname_rack.subdirs.empty?
oldname_rack.rmdir_if_possible
next
end
new_name = tap.formula_renames[oldname]
next unless new_name
new_full_name = "#{tap}/#{new_name}"
begin
f = Formulary.factory(new_full_name)
rescue Exception => e # rubocop:disable Lint/RescueException
onoe "#{e.message}\n#{e.backtrace.join "\n"}" if Homebrew::EnvConfig.developer?
next
end
Migrator.migrate_if_needed(f, force: force)
end
end
private
def diff
Utils.popen_read(
"git", "-C", tap.path, "diff-tree", "-r", "--name-status", "--diff-filter=AMDR",
"-M85%", initial_revision, current_revision
)
end
end
class ReporterHub
extend T::Sig
extend Forwardable
attr_reader :reporters
sig { void }
def initialize
@hash = {}
@reporters = []
end
def select_formula_or_cask(key)
@hash.fetch(key, [])
end
def add(reporter, auto_update: false)
@reporters << reporter
report = reporter.report(auto_update: auto_update).delete_if { |_k, v| v.empty? }
@hash.update(report) { |_key, oldval, newval| oldval.concat(newval) }
end
delegate empty?: :@hash
def dump(updated_formula_report: true)
report_all = Homebrew::EnvConfig.update_report_all_formulae?
dump_new_formula_report
dump_new_cask_report
dump_renamed_formula_report if report_all
dump_deleted_formula_report(report_all)
dump_deleted_cask_report(report_all)
outdated_formulae = nil
outdated_casks = nil
if updated_formula_report && report_all
dump_modified_formula_report
dump_modified_cask_report
elsif updated_formula_report
outdated_formulae = Formula.installed.select(&:outdated?).map(&:name)
output_dump_formula_or_cask_report "Outdated Formulae", outdated_formulae
outdated_casks = Cask::Caskroom.casks.select(&:outdated?).map(&:token)
output_dump_formula_or_cask_report "Outdated Casks", outdated_casks
elsif report_all
if (changed_formulae = select_formula_or_cask(:M).count) && changed_formulae.positive?
ohai "Modified Formulae", "Modified #{changed_formulae} #{"formula".pluralize(changed_formulae)}."
end
if (changed_casks = select_formula_or_cask(:MC).count) && changed_casks.positive?
ohai "Modified Casks", "Modified #{changed_casks} #{"cask".pluralize(changed_casks)}."
end
else
outdated_formulae = Formula.installed.select(&:outdated?).map(&:name)
outdated_casks = Cask::Caskroom.casks.select(&:outdated?).map(&:token)
end
return if outdated_formulae.blank? && outdated_casks.blank?
outdated_formulae = outdated_formulae.count
outdated_casks = outdated_casks.count
update_pronoun = if (outdated_formulae + outdated_casks) == 1
"it"
else
"them"
end
msg = ""
if outdated_formulae.positive?
msg += "#{Tty.bold}#{outdated_formulae}#{Tty.reset} outdated #{"formula".pluralize(outdated_formulae)}"
end
if outdated_casks.positive?
msg += " and " if msg.present?
msg += "#{Tty.bold}#{outdated_casks}#{Tty.reset} outdated #{"cask".pluralize(outdated_casks)}"
end
return if msg.blank?
puts
puts <<~EOS
You have #{msg} installed.
You can upgrade #{update_pronoun} with #{Tty.bold}brew upgrade#{Tty.reset}
or list #{update_pronoun} with #{Tty.bold}brew outdated#{Tty.reset}.
EOS
end
private
def dump_new_formula_report
formulae = select_formula_or_cask(:A).sort.reject { |name| installed?(name) }
output_dump_formula_or_cask_report "New Formulae", formulae
end
def dump_new_cask_report
casks = select_formula_or_cask(:AC).sort.map do |name|
name.split("/").last unless cask_installed?(name)
end.compact
output_dump_formula_or_cask_report "New Casks", casks
end
def dump_renamed_formula_report
formulae = select_formula_or_cask(:R).sort.map do |name, new_name|
name = pretty_installed(name) if installed?(name)
new_name = pretty_installed(new_name) if installed?(new_name)
"#{name} -> #{new_name}"
end
output_dump_formula_or_cask_report "Renamed Formulae", formulae
end
def dump_deleted_formula_report(report_all)
formulae = select_formula_or_cask(:D).sort.map do |name|
if installed?(name)
pretty_uninstalled(name)
elsif report_all
name
end
end.compact
output_dump_formula_or_cask_report "Deleted Formulae", formulae
end
def dump_deleted_cask_report(report_all)
casks = select_formula_or_cask(:DC).sort.map do |name|
name = name.split("/").last
if cask_installed?(name)
pretty_uninstalled(name)
elsif report_all
name
end
end.compact
output_dump_formula_or_cask_report "Deleted Casks", casks
end
def dump_modified_formula_report
formulae = select_formula_or_cask(:M).sort.map do |name|
if installed?(name)
if outdated?(name)
pretty_outdated(name)
else
pretty_installed(name)
end
else
name
end
end
output_dump_formula_or_cask_report "Modified Formulae", formulae
end
def dump_modified_cask_report
casks = select_formula_or_cask(:MC).sort.map do |name|
name = name.split("/").last
if cask_installed?(name)
if cask_outdated?(name)
pretty_outdated(name)
else
pretty_installed(name)
end
else
name
end
end
output_dump_formula_or_cask_report "Modified Casks", casks
end
def output_dump_formula_or_cask_report(title, formulae_or_casks)
return if formulae_or_casks.blank?
ohai title, Formatter.columns(formulae_or_casks.sort)
end
def installed?(formula)
(HOMEBREW_CELLAR/formula.split("/").last).directory?
end
def outdated?(formula)
Formula[formula].outdated?
rescue FormulaUnavailableError
false
end
def cask_installed?(cask)
(Cask::Caskroom.path/cask).directory?
end
def cask_outdated?(cask)
Cask::CaskLoader.load(cask).outdated?
rescue Cask::CaskError
false
end
end
#: * `update-reset` [<repository> ...]
#:
#: Fetch and reset Homebrew and all tap repositories (or any specified <repository>) using `git`(1) to their latest `origin/HEAD`.
#:
#: *Note:* this will destroy all your uncommitted or committed changes.
# Replaces the function in Library/Homebrew/brew.sh to cache the Git executable to provide
# speedup when using Git repeatedly and prevent errors if the shim changes mid-update.
git() {
if [[ -z "${GIT_EXECUTABLE}" ]]
then
# HOMEBREW_LIBRARY is set by bin/brew
# shellcheck disable=SC2154
GIT_EXECUTABLE="$("${HOMEBREW_LIBRARY}/Homebrew/shims/shared/git" --homebrew=print-path)"
fi
"${GIT_EXECUTABLE}" "$@"
}
homebrew-update-reset() {
local option
local DIR
local -a REPOS=()
for option in "$@"
do
case "${option}" in
-\? | -h | --help | --usage)
brew help update-reset
exit $?
;;
--debug) HOMEBREW_DEBUG=1 ;;
-*)
[[ "${option}" == *d* ]] && HOMEBREW_DEBUG=1
;;
*)
REPOS+=("${option}")
;;
esac
done
if [[ -n "${HOMEBREW_DEBUG}" ]]
then
set -x
fi
if [[ -z "${REPOS[*]}" ]]
then
REPOS+=("${HOMEBREW_REPOSITORY}" "${HOMEBREW_LIBRARY}"/Taps/*/*)
fi
for DIR in "${REPOS[@]}"
do
[[ -d "${DIR}/.git" ]] || continue
if ! git -C "${DIR}" config --local --get remote.origin.url &>/dev/null
then
opoo "No remote 'origin' in ${DIR}, skipping update and reset!"
continue
fi
git -C "${DIR}" config --bool core.autocrlf false
git -C "${DIR}" config --bool core.symlinks true
ohai "Fetching ${DIR}..."
git -C "${DIR}" fetch --force --tags origin
git -C "${DIR}" remote set-head origin --auto >/dev/null
echo
ohai "Resetting ${DIR}..."
head="$(git -C "${DIR}" symbolic-ref refs/remotes/origin/HEAD)"
head="${head#refs/remotes/origin/}"
git -C "${DIR}" checkout --force -B "${head}" origin/HEAD
echo
done
}
#: * `update` [<options>]
#:
#: Fetch the newest version of Homebrew and all formulae from GitHub using `git`(1) and perform any necessary migrations.
#:
#: --merge Use `git merge` to apply updates (rather than `git rebase`).
#: --auto-update Run on auto-updates (e.g. before `brew install`). Skips some slower steps.
#: -f, --force Always do a slower, full update check (even if unnecessary).
#: -q, --quiet Make some output more quiet
#: -v, --verbose Print the directories checked and `git` operations performed.
#: -d, --debug Display a trace of all shell commands as they are executed.
#: -h, --help Show this message.
# HOMEBREW_CURLRC, HOMEBREW_DEVELOPER, HOMEBREW_GIT_EMAIL, HOMEBREW_GIT_NAME
# HOMEBREW_UPDATE_CLEANUP, HOMEBREW_UPDATE_TO_TAG are from the user environment
# HOMEBREW_LIBRARY, HOMEBREW_PREFIX, HOMEBREW_REPOSITORY are set by bin/brew
# HOMEBREW_BREW_DEFAULT_GIT_REMOTE, HOMEBREW_BREW_GIT_REMOTE, HOMEBREW_CACHE, HOMEBREW_CELLAR, HOMEBREW_CURL
# HOMEBREW_DEV_CMD_RUN, HOMEBREW_FORCE_BREWED_CURL, HOMEBREW_FORCE_BREWED_GIT, HOMEBREW_SYSTEM_CURL_TOO_OLD
# HOMEBREW_USER_AGENT_CURL are set by brew.sh
# shellcheck disable=SC2154
source "${HOMEBREW_LIBRARY}/Homebrew/utils/lock.sh"
# Replaces the function in Library/Homebrew/brew.sh to cache the Curl/Git executable to
# provide speedup when using Curl/Git repeatedly (as update.sh does).
curl() {
if [[ -z "${CURL_EXECUTABLE}" ]]
then
CURL_EXECUTABLE="$("${HOMEBREW_LIBRARY}/Homebrew/shims/shared/curl" --homebrew=print-path)"
fi
"${CURL_EXECUTABLE}" "$@"
}
git() {
if [[ -z "${GIT_EXECUTABLE}" ]]
then
GIT_EXECUTABLE="$("${HOMEBREW_LIBRARY}/Homebrew/shims/shared/git" --homebrew=print-path)"
fi
"${GIT_EXECUTABLE}" "$@"
}
git_init_if_necessary() {
safe_cd "${HOMEBREW_REPOSITORY}"
if [[ ! -d ".git" ]]
then
set -e
trap '{ rm -rf .git; exit 1; }' EXIT
git init
git config --bool core.autocrlf false
git config --bool core.symlinks true
if [[ "${HOMEBREW_BREW_DEFAULT_GIT_REMOTE}" != "${HOMEBREW_BREW_GIT_REMOTE}" ]]
then
echo "HOMEBREW_BREW_GIT_REMOTE set: using ${HOMEBREW_BREW_GIT_REMOTE} for Homebrew/brew Git remote URL."
fi
git config remote.origin.url "${HOMEBREW_BREW_GIT_REMOTE}"
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git fetch --force --tags origin
git remote set-head origin --auto >/dev/null
git reset --hard origin/master
SKIP_FETCH_BREW_REPOSITORY=1
set +e
trap - EXIT
fi
[[ -d "${HOMEBREW_CORE_REPOSITORY}" ]] || return
safe_cd "${HOMEBREW_CORE_REPOSITORY}"
if [[ ! -d ".git" ]]
then
set -e
trap '{ rm -rf .git; exit 1; }' EXIT
git init
git config --bool core.autocrlf false
git config --bool core.symlinks true
if [[ "${HOMEBREW_CORE_DEFAULT_GIT_REMOTE}" != "${HOMEBREW_CORE_GIT_REMOTE}" ]]
then
echo "HOMEBREW_CORE_GIT_REMOTE set: using ${HOMEBREW_CORE_GIT_REMOTE} for Homebrew/core Git remote URL."
fi
git config remote.origin.url "${HOMEBREW_CORE_GIT_REMOTE}"
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git fetch --force origin refs/heads/master:refs/remotes/origin/master
git remote set-head origin --auto >/dev/null
git reset --hard origin/master
SKIP_FETCH_CORE_REPOSITORY=1
set +e
trap - EXIT
fi
}
repo_var() {
local repo_var
repo_var="$1"
if [[ "${repo_var}" == "${HOMEBREW_REPOSITORY}" ]]
then
repo_var=""
else
repo_var="${repo_var#"${HOMEBREW_LIBRARY}/Taps"}"
repo_var="$(echo -n "${repo_var}" | tr -C "A-Za-z0-9" "_" | tr "[:lower:]" "[:upper:]")"
fi
echo "${repo_var}"
}
upstream_branch() {
local upstream_branch
upstream_branch="$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null)"
if [[ -z "${upstream_branch}" ]]
then
git remote set-head origin --auto >/dev/null
upstream_branch="$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null)"
fi
upstream_branch="${upstream_branch#refs/remotes/origin/}"
[[ -z "${upstream_branch}" ]] && upstream_branch="master"
echo "${upstream_branch}"
}
read_current_revision() {
git rev-parse -q --verify HEAD
}
pop_stash() {
[[ -z "${STASHED}" ]] && return
if [[ -n "${HOMEBREW_VERBOSE}" ]]
then
echo "Restoring your stashed changes to ${DIR}..."
git stash pop
else
git stash pop "${QUIET_ARGS[@]}" 1>/dev/null
fi
unset STASHED
}
pop_stash_message() {
[[ -z "${STASHED}" ]] && return
echo "To restore the stashed changes to ${DIR}, run:"
echo " cd ${DIR} && git stash pop"
unset STASHED
}
reset_on_interrupt() {
if [[ "${INITIAL_BRANCH}" != "${UPSTREAM_BRANCH}" && -n "${INITIAL_BRANCH}" ]]
then
git checkout "${INITIAL_BRANCH}" "${QUIET_ARGS[@]}"
fi
if [[ -n "${INITIAL_REVISION}" ]]
then
git rebase --abort &>/dev/null
git merge --abort &>/dev/null
git reset --hard "${INITIAL_REVISION}" "${QUIET_ARGS[@]}"
fi
if [[ -n "${HOMEBREW_NO_UPDATE_CLEANUP}" ]]
then
pop_stash
else
pop_stash_message
fi
exit 130
}
# Used for testing purposes, e.g. for testing formula migration after
# renaming it in the currently checked-out branch. To test run
# "brew update --simulate-from-current-branch"
simulate_from_current_branch() {
local DIR
local TAP_VAR
local UPSTREAM_BRANCH
local CURRENT_REVISION
DIR="$1"
cd "${DIR}" || return
TAP_VAR="$2"
UPSTREAM_BRANCH="$3"
CURRENT_REVISION="$4"
INITIAL_REVISION="$(git rev-parse -q --verify "${UPSTREAM_BRANCH}")"
export HOMEBREW_UPDATE_BEFORE"${TAP_VAR}"="${INITIAL_REVISION}"
export HOMEBREW_UPDATE_AFTER"${TAP_VAR}"="${CURRENT_REVISION}"
if [[ "${INITIAL_REVISION}" != "${CURRENT_REVISION}" ]]
then
HOMEBREW_UPDATED="1"
fi
if ! git merge-base --is-ancestor "${INITIAL_REVISION}" "${CURRENT_REVISION}"
then
odie "Your ${DIR} HEAD is not a descendant of ${UPSTREAM_BRANCH}!"
fi
}
merge_or_rebase() {
if [[ -n "${HOMEBREW_VERBOSE}" ]]
then
echo "Updating ${DIR}..."
fi
local DIR
local TAP_VAR
local UPSTREAM_BRANCH
DIR="$1"
cd "${DIR}" || return
TAP_VAR="$2"
UPSTREAM_BRANCH="$3"
unset STASHED
trap reset_on_interrupt SIGINT
if [[ "${DIR}" == "${HOMEBREW_REPOSITORY}" && -n "${HOMEBREW_UPDATE_TO_TAG}" ]]
then
UPSTREAM_TAG="$(
git tag --list |
sort --field-separator=. --key=1,1nr -k 2,2nr -k 3,3nr |
grep --max-count=1 '^[0-9]*\.[0-9]*\.[0-9]*$'
)"
else
UPSTREAM_TAG=""
fi
if [[ -n "${UPSTREAM_TAG}" ]]
then
REMOTE_REF="refs/tags/${UPSTREAM_TAG}"
UPSTREAM_BRANCH="stable"
else
REMOTE_REF="origin/${UPSTREAM_BRANCH}"
fi
if [[ -n "$(git status --untracked-files=all --porcelain 2>/dev/null)" ]]
then
if [[ -n "${HOMEBREW_VERBOSE}" ]]
then
echo "Stashing uncommitted changes to ${DIR}..."
fi
git merge --abort &>/dev/null
git rebase --abort &>/dev/null
git reset --mixed "${QUIET_ARGS[@]}"
if ! git -c "user.email=brew-update@localhost" \
-c "user.name=brew update" \
stash save --include-untracked "${QUIET_ARGS[@]}"
then
odie <<EOS
Could not 'git stash' in ${DIR}!
Please stash/commit manually if you need to keep your changes or, if not, run:
cd ${DIR}
git reset --hard origin/master
EOS
fi
git reset --hard "${QUIET_ARGS[@]}"
STASHED="1"
fi
INITIAL_BRANCH="$(git symbolic-ref --short HEAD 2>/dev/null)"
if [[ -n "${UPSTREAM_TAG}" ]] ||
[[ "${INITIAL_BRANCH}" != "${UPSTREAM_BRANCH}" && -n "${INITIAL_BRANCH}" ]]
then
# Recreate and check out `#{upstream_branch}` if unable to fast-forward
# it to `origin/#{@upstream_branch}`. Otherwise, just check it out.
if [[ -z "${UPSTREAM_TAG}" ]] &&
git merge-base --is-ancestor "${UPSTREAM_BRANCH}" "${REMOTE_REF}" &>/dev/null
then
git checkout --force "${UPSTREAM_BRANCH}" "${QUIET_ARGS[@]}"
else
if [[ -n "${UPSTREAM_TAG}" && "${UPSTREAM_BRANCH}" != "master" ]]
then
git checkout --force -B "master" "origin/master" "${QUIET_ARGS[@]}"
fi
git checkout --force -B "${UPSTREAM_BRANCH}" "${REMOTE_REF}" "${QUIET_ARGS[@]}"
fi
fi
INITIAL_REVISION="$(read_current_revision)"
export HOMEBREW_UPDATE_BEFORE"${TAP_VAR}"="${INITIAL_REVISION}"
# ensure we don't munge line endings on checkout
git config --bool core.autocrlf false
# make sure symlinks are saved as-is
git config --bool core.symlinks true
if [[ "${DIR}" == "${HOMEBREW_CORE_REPOSITORY}" && -n "${HOMEBREW_LINUXBREW_CORE_MIGRATION}" ]]
then
# Don't even try to rebase/merge on linuxbrew-core migration but rely on
# stashing etc. above.
git reset --hard "${QUIET_ARGS[@]}" "${REMOTE_REF}"
unset HOMEBREW_LINUXBREW_CORE_MIGRATION
elif [[ -z "${HOMEBREW_MERGE}" ]]
then
# Work around bug where git rebase --quiet is not quiet
if [[ -z "${HOMEBREW_VERBOSE}" ]]
then
git rebase "${QUIET_ARGS[@]}" "${REMOTE_REF}" >/dev/null
else
git rebase "${QUIET_ARGS[@]}" "${REMOTE_REF}"
fi
else
git merge --no-edit --ff "${QUIET_ARGS[@]}" "${REMOTE_REF}" \
--strategy=recursive \
--strategy-option=ours \
--strategy-option=ignore-all-space
fi
CURRENT_REVISION="$(read_current_revision)"
export HOMEBREW_UPDATE_AFTER"${TAP_VAR}"="${CURRENT_REVISION}"
if [[ "${INITIAL_REVISION}" != "${CURRENT_REVISION}" ]]
then
HOMEBREW_UPDATED="1"
fi
trap '' SIGINT
if [[ -n "${HOMEBREW_NO_UPDATE_CLEANUP}" ]]
then
if [[ "${INITIAL_BRANCH}" != "${UPSTREAM_BRANCH}" && -n "${INITIAL_BRANCH}" ]] &&
[[ ! "${INITIAL_BRANCH}" =~ ^v[0-9]+\.[0-9]+\.[0-9]|stable$ ]]
then
git checkout "${INITIAL_BRANCH}" "${QUIET_ARGS[@]}"
fi
pop_stash
else
pop_stash_message
fi
trap - SIGINT
}
homebrew-update() {
local option
local DIR
local UPSTREAM_BRANCH
for option in "$@"
do
case "${option}" in
-\? | -h | --help | --usage)
brew help update
exit $?
;;
--verbose) HOMEBREW_VERBOSE=1 ;;
--debug) HOMEBREW_DEBUG=1 ;;
--quiet) HOMEBREW_QUIET=1 ;;
--merge) HOMEBREW_MERGE=1 ;;
--force) HOMEBREW_UPDATE_FORCE=1 ;;
--simulate-from-current-branch) HOMEBREW_SIMULATE_FROM_CURRENT_BRANCH=1 ;;
--auto-update) export HOMEBREW_UPDATE_AUTO=1 ;;
--*) ;;
-*)
[[ "${option}" == *v* ]] && HOMEBREW_VERBOSE=1
[[ "${option}" == *q* ]] && HOMEBREW_QUIET=1
[[ "${option}" == *d* ]] && HOMEBREW_DEBUG=1
[[ "${option}" == *f* ]] && HOMEBREW_UPDATE_FORCE=1
;;
*)
odie <<EOS
This command updates brew itself, and does not take formula names.
Use \`brew upgrade $@\` instead.
EOS
;;
esac
done
if [[ -n "${HOMEBREW_DEBUG}" ]]
then
set -x
fi
if [[ -z "${HOMEBREW_UPDATE_CLEANUP}" && -z "${HOMEBREW_UPDATE_TO_TAG}" ]]
then
if [[ -n "${HOMEBREW_DEVELOPER}" || -n "${HOMEBREW_DEV_CMD_RUN}" ]]
then
export HOMEBREW_NO_UPDATE_CLEANUP="1"
else
export HOMEBREW_UPDATE_TO_TAG="1"
fi
fi
# check permissions
if [[ -e "${HOMEBREW_CELLAR}" && ! -w "${HOMEBREW_CELLAR}" ]]
then
odie <<EOS
${HOMEBREW_CELLAR} is not writable. You should change the
ownership and permissions of ${HOMEBREW_CELLAR} back to your
user account:
sudo chown -R \$(whoami) ${HOMEBREW_CELLAR}
EOS
fi
if [[ -d "${HOMEBREW_CORE_REPOSITORY}" ]] ||
[[ -z "${HOMEBREW_NO_INSTALL_FROM_API}" && -n "${HOMEBREW_INSTALL_FROM_API}" ]]
then
HOMEBREW_CORE_AVAILABLE="1"
fi
if [[ ! -w "${HOMEBREW_REPOSITORY}" ]]
then
odie <<EOS
${HOMEBREW_REPOSITORY} is not writable. You should change the
ownership and permissions of ${HOMEBREW_REPOSITORY} back to your
user account:
sudo chown -R \$(whoami) ${HOMEBREW_REPOSITORY}
EOS
fi
# we may want to use Homebrew CA certificates
if [[ -n "${HOMEBREW_FORCE_BREWED_CA_CERTIFICATES}" && ! -f "${HOMEBREW_PREFIX}/etc/ca-certificates/cert.pem" ]]
then
# we cannot install Homebrew CA certificates if homebrew/core is unavailable.
if [[ -n "${HOMEBREW_CORE_AVAILABLE}" ]]
then
brew install ca-certificates
setup_ca_certificates
fi
fi
# we may want to use a Homebrew curl
if [[ -n "${HOMEBREW_FORCE_BREWED_CURL}" && ! -x "${HOMEBREW_PREFIX}/opt/curl/bin/curl" ]]
then
# we cannot install a Homebrew cURL if homebrew/core is unavailable.
if [[ -z "${HOMEBREW_CORE_AVAILABLE}" ]] || ! brew install curl
then
odie "'curl' must be installed and in your PATH!"
fi
setup_curl
fi
if ! git --version &>/dev/null ||
[[ -n "${HOMEBREW_FORCE_BREWED_GIT}" && ! -x "${HOMEBREW_PREFIX}/opt/git/bin/git" ]]
then
# we cannot install a Homebrew Git if homebrew/core is unavailable.
if [[ -z "${HOMEBREW_CORE_AVAILABLE}" ]] || ! brew install git
then
odie "'git' must be installed and in your PATH!"
fi
setup_git
fi
[[ -f "${HOMEBREW_CORE_REPOSITORY}/.git/shallow" ]] && HOMEBREW_CORE_SHALLOW=1
[[ -f "${HOMEBREW_LIBRARY}/Taps/homebrew/homebrew-cask/.git/shallow" ]] && HOMEBREW_CASK_SHALLOW=1
if [[ -n "${HOMEBREW_CORE_SHALLOW}" && -n "${HOMEBREW_CASK_SHALLOW}" ]]
then
SHALLOW_COMMAND_PHRASE="These commands"
SHALLOW_REPO_PHRASE="repositories"
else
SHALLOW_COMMAND_PHRASE="This command"
SHALLOW_REPO_PHRASE="repository"
fi
if [[ -n "${HOMEBREW_CORE_SHALLOW}" || -n "${HOMEBREW_CASK_SHALLOW}" ]]
then
odie <<EOS
${HOMEBREW_CORE_SHALLOW:+
homebrew-core is a shallow clone.}${HOMEBREW_CASK_SHALLOW:+
homebrew-cask is a shallow clone.}
To \`brew update\`, first run:${HOMEBREW_CORE_SHALLOW:+
git -C "${HOMEBREW_CORE_REPOSITORY}" fetch --unshallow}${HOMEBREW_CASK_SHALLOW:+
git -C "${HOMEBREW_LIBRARY}/Taps/homebrew/homebrew-cask" fetch --unshallow}
${SHALLOW_COMMAND_PHRASE} may take a few minutes to run due to the large size of the ${SHALLOW_REPO_PHRASE}.
This restriction has been made on GitHub's request because updating shallow
clones is an extremely expensive operation due to the tree layout and traffic of
Homebrew/homebrew-core and Homebrew/homebrew-cask. We don't do this for you
automatically to avoid repeatedly performing an expensive unshallow operation in
CI systems (which should instead be fixed to not use shallow clones). Sorry for
the inconvenience!
EOS
fi
export GIT_TERMINAL_PROMPT="0"
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh} -oBatchMode=yes"
if [[ -n "${HOMEBREW_GIT_NAME}" ]]
then
export GIT_AUTHOR_NAME="${HOMEBREW_GIT_NAME}"
export GIT_COMMITTER_NAME="${HOMEBREW_GIT_NAME}"
fi
if [[ -n "${HOMEBREW_GIT_EMAIL}" ]]
then
export GIT_AUTHOR_EMAIL="${HOMEBREW_GIT_EMAIL}"
export GIT_COMMITTER_EMAIL="${HOMEBREW_GIT_EMAIL}"
fi
if [[ -z "${HOMEBREW_VERBOSE}" ]]
then
QUIET_ARGS=(-q)
else
QUIET_ARGS=()
fi
# HOMEBREW_CURLRC is optionally defined in the user environment.
# shellcheck disable=SC2153
if [[ -z "${HOMEBREW_CURLRC}" ]]
then
CURL_DISABLE_CURLRC_ARGS=(-q)
else
CURL_DISABLE_CURLRC_ARGS=()
fi
# HOMEBREW_GITHUB_API_TOKEN is optionally defined in the user environment.
# shellcheck disable=SC2153
if [[ -n "${HOMEBREW_GITHUB_API_TOKEN}" ]]
then
CURL_GITHUB_API_ARGS=("--header" "Authorization: token ${HOMEBREW_GITHUB_API_TOKEN}")
else
CURL_GITHUB_API_ARGS=()
fi
# only allow one instance of brew update
lock update
git_init_if_necessary
if [[ "${HOMEBREW_BREW_DEFAULT_GIT_REMOTE}" != "${HOMEBREW_BREW_GIT_REMOTE}" ]]
then
safe_cd "${HOMEBREW_REPOSITORY}"
echo "HOMEBREW_BREW_GIT_REMOTE set: using ${HOMEBREW_BREW_GIT_REMOTE} for Homebrew/brew Git remote."
git remote set-url origin "${HOMEBREW_BREW_GIT_REMOTE}"
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git fetch --force --tags origin
SKIP_FETCH_BREW_REPOSITORY=1
fi
if [[ -d "${HOMEBREW_CORE_REPOSITORY}" ]] &&
[[ "${HOMEBREW_CORE_DEFAULT_GIT_REMOTE}" != "${HOMEBREW_CORE_GIT_REMOTE}" ||
-n "${HOMEBREW_LINUXBREW_CORE_MIGRATION}" ]]
then
if [[ -n "${HOMEBREW_LINUXBREW_CORE_MIGRATION}" ]]
then
# This means a migration is needed (in case it isn't run this time)
safe_cd "${HOMEBREW_REPOSITORY}"
git config --bool homebrew.linuxbrewmigrated false
fi
safe_cd "${HOMEBREW_CORE_REPOSITORY}"
echo "HOMEBREW_CORE_GIT_REMOTE set: using ${HOMEBREW_CORE_GIT_REMOTE} for Homebrew/core Git remote."
git remote set-url origin "${HOMEBREW_CORE_GIT_REMOTE}"
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git fetch --force origin refs/heads/master:refs/remotes/origin/master
SKIP_FETCH_CORE_REPOSITORY=1
fi
safe_cd "${HOMEBREW_REPOSITORY}"
# This means a migration is needed but hasn't completed (yet).
if [[ "$(git config homebrew.linuxbrewmigrated 2>/dev/null)" == "false" ]]
then
export HOMEBREW_MIGRATE_LINUXBREW_FORMULAE=1
fi
# if an older system had a newer curl installed, change each repo's remote URL from git to https
if [[ -n "${HOMEBREW_SYSTEM_CURL_TOO_OLD}" && -x "${HOMEBREW_PREFIX}/opt/curl/bin/curl" ]] &&
[[ "$(git config remote.origin.url)" =~ ^git:// ]]
then
git config remote.origin.url "${HOMEBREW_BREW_GIT_REMOTE}"
git config -f "${HOMEBREW_CORE_REPOSITORY}/.git/config" remote.origin.url "${HOMEBREW_CORE_GIT_REMOTE}"
fi
# kill all of subprocess on interrupt
trap '{ /usr/bin/pkill -P $$; wait; exit 130; }' SIGINT
local update_failed_file="${HOMEBREW_REPOSITORY}/.git/UPDATE_FAILED"
local missing_remote_ref_dirs_file="${HOMEBREW_REPOSITORY}/.git/FAILED_FETCH_DIRS"
rm -f "${update_failed_file}"
rm -f "${missing_remote_ref_dirs_file}"
for DIR in "${HOMEBREW_REPOSITORY}" "${HOMEBREW_LIBRARY}"/Taps/*/*
do
if [[ -z "${HOMEBREW_NO_INSTALL_FROM_API}" && -n "${HOMEBREW_INSTALL_FROM_API}" ]] &&
[[ -z "${HOMEBREW_DEVELOPER}" || -n "${HOMEBREW_UPDATE_AUTO}" ]] &&
[[ "${DIR}" == "${HOMEBREW_CORE_REPOSITORY}" ]]
then
continue
fi
[[ -d "${DIR}/.git" ]] || continue
cd "${DIR}" || continue
# Git's fsmonitor prevents the release of our locks
git config --bool core.fsmonitor false
if ! git config --local --get remote.origin.url &>/dev/null
then
opoo "No remote 'origin' in ${DIR}, skipping update!"
continue
fi
if [[ -n "${HOMEBREW_VERBOSE}" ]]
then
echo "Checking if we need to fetch ${DIR}..."
fi
TAP_VAR="$(repo_var "${DIR}")"
UPSTREAM_BRANCH_DIR="$(upstream_branch)"
declare UPSTREAM_BRANCH"${TAP_VAR}"="${UPSTREAM_BRANCH_DIR}"
declare PREFETCH_REVISION"${TAP_VAR}"="$(git rev-parse -q --verify refs/remotes/origin/"${UPSTREAM_BRANCH_DIR}")"
# Force a full update if we don't have any tags.
if [[ "${DIR}" == "${HOMEBREW_REPOSITORY}" && -z "$(git tag --list)" ]]
then
HOMEBREW_UPDATE_FORCE=1
fi
if [[ -z "${HOMEBREW_UPDATE_FORCE}" ]]
then
[[ -n "${SKIP_FETCH_BREW_REPOSITORY}" && "${DIR}" == "${HOMEBREW_REPOSITORY}" ]] && continue
[[ -n "${SKIP_FETCH_CORE_REPOSITORY}" && "${DIR}" == "${HOMEBREW_CORE_REPOSITORY}" ]] && continue
fi
# The upstream repository's default branch may not be master;
# check refs/remotes/origin/HEAD to see what the default
# origin branch name is, and use that. If not set, fall back to "master".
# the refspec ensures that the default upstream branch gets updated
(
UPSTREAM_REPOSITORY_URL="$(git config remote.origin.url)"
# HOMEBREW_UPDATE_FORCE and HOMEBREW_UPDATE_AUTO aren't modified here so ignore subshell warning.
# shellcheck disable=SC2030
if [[ "${UPSTREAM_REPOSITORY_URL}" == "https://github.com/"* ]]
then
UPSTREAM_REPOSITORY="${UPSTREAM_REPOSITORY_URL#https://github.com/}"
UPSTREAM_REPOSITORY="${UPSTREAM_REPOSITORY%.git}"
if [[ "${DIR}" == "${HOMEBREW_REPOSITORY}" && -n "${HOMEBREW_UPDATE_TO_TAG}" ]]
then
# Only try to `git fetch` when the upstream tags have changed
# (so the API does not return 304: unmodified).
GITHUB_API_ETAG="$(sed -n 's/^ETag: "\([a-f0-9]\{32\}\)".*/\1/p' ".git/GITHUB_HEADERS" 2>/dev/null)"
GITHUB_API_ACCEPT="application/vnd.github+json"
GITHUB_API_ENDPOINT="tags"
else
# Only try to `git fetch` when the upstream branch is at a different SHA
# (so the API does not return 304: unmodified).
GITHUB_API_ETAG="$(git rev-parse "refs/remotes/origin/${UPSTREAM_BRANCH_DIR}")"
GITHUB_API_ACCEPT="application/vnd.github.sha"
GITHUB_API_ENDPOINT="commits/${UPSTREAM_BRANCH_DIR}"
fi
# HOMEBREW_CURL is set by brew.sh (and isn't mispelt here)
# shellcheck disable=SC2153
UPSTREAM_SHA_HTTP_CODE="$(
curl \
"${CURL_DISABLE_CURLRC_ARGS[@]}" \
"${CURL_GITHUB_API_ARGS[@]}" \
--silent --max-time 3 \
--location --no-remote-time --output /dev/null --write-out "%{http_code}" \
--dump-header "${DIR}/.git/GITHUB_HEADERS" \
--user-agent "${HOMEBREW_USER_AGENT_CURL}" \
--header "X-GitHub-Api-Version:2022-11-28" \
--header "Accept: ${GITHUB_API_ACCEPT}" \
--header "If-None-Match: \"${GITHUB_API_ETAG}\"" \
"https://api.github.com/repos/${UPSTREAM_REPOSITORY}/${GITHUB_API_ENDPOINT}"
)"
# Touch FETCH_HEAD to confirm we've checked for an update.
[[ -f "${DIR}/.git/FETCH_HEAD" ]] && touch "${DIR}/.git/FETCH_HEAD"
[[ -z "${HOMEBREW_UPDATE_FORCE}" ]] && [[ "${UPSTREAM_SHA_HTTP_CODE}" == "304" ]] && exit
elif [[ -n "${HOMEBREW_UPDATE_AUTO}" ]]
then
FORCE_AUTO_UPDATE="$(git config homebrew.forceautoupdate 2>/dev/null || echo "false")"
if [[ "${FORCE_AUTO_UPDATE}" != "true" ]]
then
# Don't try to do a `git fetch` that may take longer than expected.
exit
fi
fi
# HOMEBREW_VERBOSE isn't modified here so ignore subshell warning.
# shellcheck disable=SC2030
if [[ -n "${HOMEBREW_VERBOSE}" ]]
then
echo "Fetching ${DIR}..."
fi
local tmp_failure_file="${DIR}/.git/TMP_FETCH_FAILURES"
rm -f "${tmp_failure_file}"
if [[ -n "${HOMEBREW_UPDATE_AUTO}" ]]
then
git fetch --tags --force "${QUIET_ARGS[@]}" origin \
"refs/heads/${UPSTREAM_BRANCH_DIR}:refs/remotes/origin/${UPSTREAM_BRANCH_DIR}" 2>/dev/null
else
# Capture stderr to tmp_failure_file
if ! git fetch --tags --force "${QUIET_ARGS[@]}" origin \
"refs/heads/${UPSTREAM_BRANCH_DIR}:refs/remotes/origin/${UPSTREAM_BRANCH_DIR}" 2>>"${tmp_failure_file}"
then
# Reprint fetch errors to stderr
[[ -f "${tmp_failure_file}" ]] && cat "${tmp_failure_file}" 1>&2
if [[ "${UPSTREAM_SHA_HTTP_CODE}" == "404" ]]
then
TAP="${DIR#"${HOMEBREW_LIBRARY}"/Taps/}"
echo "${TAP} does not exist! Run \`brew untap ${TAP}\` to remove it." >>"${update_failed_file}"
else
echo "Fetching ${DIR} failed!" >>"${update_failed_file}"
if [[ -f "${tmp_failure_file}" ]] &&
[[ "$(cat "${tmp_failure_file}")" == "fatal: couldn't find remote ref refs/heads/${UPSTREAM_BRANCH_DIR}" ]]
then
echo "${DIR}" >>"${missing_remote_ref_dirs_file}"
fi
fi
fi
fi
rm -f "${tmp_failure_file}"
) &
done
wait
trap - SIGINT
if [[ -f "${update_failed_file}" ]]
then
onoe <"${update_failed_file}"
rm -f "${update_failed_file}"
export HOMEBREW_UPDATE_FAILED="1"
fi
if [[ -f "${missing_remote_ref_dirs_file}" ]]
then
HOMEBREW_MISSING_REMOTE_REF_DIRS="$(cat "${missing_remote_ref_dirs_file}")"
rm -f "${missing_remote_ref_dirs_file}"
export HOMEBREW_MISSING_REMOTE_REF_DIRS
fi
for DIR in "${HOMEBREW_REPOSITORY}" "${HOMEBREW_LIBRARY}"/Taps/*/*
do
if [[ -z "${HOMEBREW_NO_INSTALL_FROM_API}" && -n "${HOMEBREW_INSTALL_FROM_API}" ]] &&
[[ -z "${HOMEBREW_DEVELOPER}" || -n "${HOMEBREW_UPDATE_AUTO}" ]] &&
[[ "${DIR}" == "${HOMEBREW_CORE_REPOSITORY}" ||
"${DIR}" == "${HOMEBREW_LIBRARY}/Taps/homebrew/homebrew-cask" ]]
then
continue
fi
[[ -d "${DIR}/.git" ]] || continue
cd "${DIR}" || continue
if ! git config --local --get remote.origin.url &>/dev/null
then
# No need to display a (duplicate) warning here
continue
fi
TAP_VAR="$(repo_var "${DIR}")"
UPSTREAM_BRANCH_VAR="UPSTREAM_BRANCH${TAP_VAR}"
UPSTREAM_BRANCH="${!UPSTREAM_BRANCH_VAR}"
CURRENT_REVISION="$(read_current_revision)"
PREFETCH_REVISION_VAR="PREFETCH_REVISION${TAP_VAR}"
PREFETCH_REVISION="${!PREFETCH_REVISION_VAR}"
POSTFETCH_REVISION="$(git rev-parse -q --verify refs/remotes/origin/"${UPSTREAM_BRANCH}")"
# HOMEBREW_UPDATE_FORCE and HOMEBREW_VERBOSE weren't modified in subshell.
# shellcheck disable=SC2031
if [[ -n "${HOMEBREW_SIMULATE_FROM_CURRENT_BRANCH}" ]]
then
simulate_from_current_branch "${DIR}" "${TAP_VAR}" "${UPSTREAM_BRANCH}" "${CURRENT_REVISION}"
elif [[ -z "${HOMEBREW_UPDATE_FORCE}" ]] &&
[[ "${PREFETCH_REVISION}" == "${POSTFETCH_REVISION}" ]] &&
[[ "${CURRENT_REVISION}" == "${POSTFETCH_REVISION}" ]]
then
export HOMEBREW_UPDATE_BEFORE"${TAP_VAR}"="${CURRENT_REVISION}"
export HOMEBREW_UPDATE_AFTER"${TAP_VAR}"="${CURRENT_REVISION}"
else
merge_or_rebase "${DIR}" "${TAP_VAR}" "${UPSTREAM_BRANCH}"
[[ -n "${HOMEBREW_VERBOSE}" ]] && echo
fi
done
if [[ -z "${HOMEBREW_NO_INSTALL_FROM_API}" && -n "${HOMEBREW_INSTALL_FROM_API}" ]]
then
mkdir -p "${HOMEBREW_CACHE}/api"
for formula_or_cask in formula cask
do
if [[ -f "${HOMEBREW_CACHE}/api/${formula_or_cask}.json" ]]
then
INITIAL_JSON_BYTESIZE="$(wc -c "${HOMEBREW_CACHE}"/api/"${formula_or_cask}".json)"
fi
curl \
"${CURL_DISABLE_CURLRC_ARGS[@]}" \
--fail --compressed --silent --max-time 5 \
--location --remote-time --output "${HOMEBREW_CACHE}/api/${formula_or_cask}.json" \
--time-cond "${HOMEBREW_CACHE}/api/${formula_or_cask}.json" \
--user-agent "${HOMEBREW_USER_AGENT_CURL}" \
"https://formulae.brew.sh/api/${formula_or_cask}.json"
curl_exit_code=$?
if [[ ${curl_exit_code} -eq 0 ]]
then
CURRENT_JSON_BYTESIZE="$(wc -c "${HOMEBREW_CACHE}"/api/"${formula_or_cask}".json)"
if [[ "${INITIAL_JSON_BYTESIZE}" != "${CURRENT_JSON_BYTESIZE}" ]]
then
HOMEBREW_UPDATED="1"
fi
else
echo "Failed to download ${formula_or_cask}.json!" >>"${update_failed_file}"
fi
done
fi
safe_cd "${HOMEBREW_REPOSITORY}"
# HOMEBREW_UPDATE_AUTO wasn't modified in subshell.
# shellcheck disable=SC2031
if [[ -n "${HOMEBREW_UPDATED}" ]] ||
[[ -n "${HOMEBREW_UPDATE_FAILED}" ]] ||
[[ -n "${HOMEBREW_MISSING_REMOTE_REF_DIRS}" ]] ||
[[ -n "${HOMEBREW_UPDATE_FORCE}" ]] ||
[[ -n "${HOMEBREW_MIGRATE_LINUXBREW_FORMULAE}" ]] ||
[[ -d "${HOMEBREW_LIBRARY}/LinkedKegs" ]] ||
[[ ! -f "${HOMEBREW_CACHE}/all_commands_list.txt" ]] ||
[[ -n "${HOMEBREW_DEVELOPER}" && -z "${HOMEBREW_UPDATE_AUTO}" ]]
then
brew update-report "$@"
return $?
elif [[ -z "${HOMEBREW_UPDATE_AUTO}" && -z "${HOMEBREW_QUIET}" ]]
then
echo "Already up-to-date."
fi
}
# typed: false
# frozen_string_literal: true
require "cli/parser"
require "formula_installer"
require "install"
require "upgrade"
require "cask/cmd"
require "cask/utils"
require "cask/macos"
require "api"
module Homebrew
extend T::Sig
module_function
sig { returns(CLI::Parser) }
def upgrade_args
Homebrew::CLI::Parser.new do
description <<~EOS
Upgrade outdated casks and outdated, unpinned formulae using the same options they were originally
installed with, plus any appended brew formula options. If <cask> or <formula> are specified,
upgrade only the given <cask> or <formula> kegs (unless they are pinned; see `pin`, `unpin`).
Unless `HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK` is set, `brew upgrade` or `brew reinstall` will be run for
outdated dependents and dependents with broken linkage, respectively.
Unless `HOMEBREW_NO_INSTALL_CLEANUP` is set, `brew cleanup` will then be run for the
upgraded formulae or, every 30 days, for all formulae.
EOS
switch "-d", "--debug",
description: "If brewing fails, open an interactive debugging session with access to IRB " \
"or a shell inside the temporary build directory."
switch "-f", "--force",
description: "Install formulae without checking for previously installed keg-only or " \
"non-migrated versions. When installing casks, overwrite existing files " \
"(binaries and symlinks are excluded, unless originally from the same cask)."
switch "-v", "--verbose",
description: "Print the verification and postinstall steps."
switch "-n", "--dry-run",
description: "Show what would be upgraded, but do not actually upgrade anything."
[
[:switch, "--formula", "--formulae", {
description: "Treat all named arguments as formulae. If no named arguments " \
"are specified, upgrade only outdated formulae.",
}],
[:switch, "-s", "--build-from-source", {
description: "Compile <formula> from source even if a bottle is available.",
}],
[:switch, "-i", "--interactive", {
description: "Download and patch <formula>, then open a shell. This allows the user to " \
"run `./configure --help` and otherwise determine how to turn the software " \
"package into a Homebrew package.",
}],
[:switch, "--force-bottle", {
description: "Install from a bottle if it exists for the current or newest version of " \
"macOS, even if it would not normally be used for installation.",
}],
[:switch, "--fetch-HEAD", {
description: "Fetch the upstream repository to detect if the HEAD installation of the " \
"formula is outdated. Otherwise, the repository's HEAD will only be checked for " \
"updates when a new stable or development version has been released.",
}],
[:switch, "--ignore-pinned", {
description: "Set a successful exit status even if pinned formulae are not upgraded.",
}],
[:switch, "--keep-tmp", {
description: "Retain the temporary files created during installation.",
}],
[:switch, "--debug-symbols", {
depends_on: "--build-from-source",
description: "Generate debug symbols on build. Source will be retained in a cache directory. ",
}],
[:switch, "--display-times", {
env: :display_install_times,
description: "Print install times for each package at the end of the run.",
}],
].each do |args|
options = args.pop
send(*args, **options)
conflicts "--cask", args.last
end
formula_options
[
[:switch, "--cask", "--casks", {
description: "Treat all named arguments as casks. If no named arguments " \
"are specified, upgrade only outdated casks.",
}],
*Cask::Cmd::AbstractCommand::OPTIONS.map(&:dup),
*Cask::Cmd::Upgrade::OPTIONS.map(&:dup),
].each do |args|
options = args.pop
send(*args, **options)
conflicts "--formula", args.last
end
cask_options
conflicts "--build-from-source", "--force-bottle"
named_args [:outdated_formula, :outdated_cask]
end
end
sig { void }
def upgrade
args = upgrade_args.parse
formulae, casks = args.named.to_resolved_formulae_to_casks
# If one or more formulae are specified, but no casks were
# specified, we want to make note of that so we don't
# try to upgrade all outdated casks.
only_upgrade_formulae = formulae.present? && casks.blank?
only_upgrade_casks = casks.present? && formulae.blank?
upgrade_outdated_formulae(formulae, args: args) unless only_upgrade_casks
upgrade_outdated_casks(casks, args: args) unless only_upgrade_formulae
Cleanup.periodic_clean!(dry_run: args.dry_run?)
Homebrew.messages.display_messages(display_times: args.display_times?)
end
sig { params(formulae: T::Array[Formula], args: CLI::Args).returns(T::Boolean) }
def upgrade_outdated_formulae(formulae, args:)
return false if args.cask?
if args.build_from_source? && !DevelopmentTools.installed?
raise BuildFlagsError.new(["--build-from-source"], bottled: formulae.all?(&:bottled?))
end
Install.perform_preinstall_checks
if formulae.blank?
outdated = Formula.installed.select do |f|
f.outdated?(fetch_head: args.fetch_HEAD?)
end
else
outdated, not_outdated = formulae.partition do |f|
f.outdated?(fetch_head: args.fetch_HEAD?)
end
not_outdated.each do |f|
versions = f.installed_kegs.map(&:version)
if versions.empty?
ofail "#{f.full_specified_name} not installed"
else
version = versions.max
opoo "#{f.full_specified_name} #{version} already installed"
end
end
end
return false if outdated.blank?
pinned = outdated.select(&:pinned?)
outdated -= pinned
formulae_to_install = outdated.map do |f|
f_latest = f.latest_formula
if f_latest.latest_version_installed?
f
else
f_latest
end
end
if !pinned.empty? && !args.ignore_pinned?
ofail "Not upgrading #{pinned.count} pinned #{"package".pluralize(pinned.count)}:"
puts pinned.map { |f| "#{f.full_specified_name} #{f.pkg_version}" } * ", "
end
if formulae_to_install.empty?
oh1 "No packages to upgrade"
else
verb = args.dry_run? ? "Would upgrade" : "Upgrading"
oh1 "#{verb} #{formulae_to_install.count} outdated #{"package".pluralize(formulae_to_install.count)}:"
formulae_upgrades = formulae_to_install.map do |f|
if f.optlinked?
"#{f.full_specified_name} #{Keg.new(f.opt_prefix).version} -> #{f.pkg_version}"
else
"#{f.full_specified_name} #{f.pkg_version}"
end
end
puts formulae_upgrades.join("\n")
end
Upgrade.upgrade_formulae(
formulae_to_install,
flags: args.flags_only,
dry_run: args.dry_run?,
installed_on_request: args.named.present?,
force_bottle: args.force_bottle?,
build_from_source_formulae: args.build_from_source_formulae,
interactive: args.interactive?,
keep_tmp: args.keep_tmp?,
debug_symbols: args.debug_symbols?,
force: args.force?,
debug: args.debug?,
quiet: args.quiet?,
verbose: args.verbose?,
)
Upgrade.check_installed_dependents(
formulae_to_install,
flags: args.flags_only,
dry_run: args.dry_run?,
installed_on_request: args.named.present?,
force_bottle: args.force_bottle?,
build_from_source_formulae: args.build_from_source_formulae,
interactive: args.interactive?,
keep_tmp: args.keep_tmp?,
debug_symbols: args.debug_symbols?,
force: args.force?,
debug: args.debug?,
quiet: args.quiet?,
verbose: args.verbose?,
)
true
end
sig { params(casks: T::Array[Cask::Cask], args: CLI::Args).returns(T::Boolean) }
def upgrade_outdated_casks(casks, args:)
return false if args.formula?
Cask::Cmd::Upgrade.upgrade_casks(
*casks,
force: args.force?,
greedy: args.greedy?,
greedy_latest: args.greedy_latest?,
greedy_auto_updates: args.greedy_auto_updates?,
dry_run: args.dry_run?,
binaries: args.binaries?,
quarantine: args.quarantine?,
require_sha: args.require_sha?,
skip_cask_deps: args.skip_cask_deps?,
verbose: args.verbose?,
args: args,
)
end
end
# typed: false
# frozen_string_literal: true
# `brew uses foo bar` returns formulae that use both foo and bar
# If you want the union, run the command twice and concatenate the results.
# The intersection is harder to achieve with shell tools.
require "formula"
require "cli/parser"
require "cask/caskroom"
require "dependencies_helpers"
module Homebrew
extend T::Sig
extend DependenciesHelpers
module_function
sig { returns(CLI::Parser) }
def uses_args
Homebrew::CLI::Parser.new do
description <<~EOS
Show formulae and casks that specify <formula> as a dependency; that is, show dependents
of <formula>. When given multiple formula arguments, show the intersection
of formulae that use <formula>. By default, `uses` shows all formulae and casks that
specify <formula> as a required or recommended dependency for their stable builds.
EOS
switch "--recursive",
description: "Resolve more than one level of dependencies."
switch "--installed",
description: "Only list formulae and casks that are currently installed."
switch "--eval-all",
description: "Evaluate all available formulae and casks, whether installed or not, to show " \
"their dependents."
switch "--all",
hidden: true
switch "--include-build",
description: "Include all formulae that specify <formula> as `:build` type dependency."
switch "--include-test",
description: "Include all formulae that specify <formula> as `:test` type dependency."
switch "--include-optional",
description: "Include all formulae that specify <formula> as `:optional` type dependency."
switch "--skip-recommended",
description: "Skip all formulae that specify <formula> as `:recommended` type dependency."
switch "--formula", "--formulae",
description: "Include only formulae."
switch "--cask", "--casks",
description: "Include only casks."
conflicts "--formula", "--cask"
conflicts "--installed", "--all"
named_args :formula, min: 1
end
end
def uses
args = uses_args.parse
Formulary.enable_factory_cache!
used_formulae_missing = false
used_formulae = begin
args.named.to_formulae
rescue FormulaUnavailableError => e
opoo e
used_formulae_missing = true
# If the formula doesn't exist: fake the needed formula object name.
args.named.map { |name| OpenStruct.new name: name, full_name: name }
end
use_runtime_dependents = args.installed? &&
!used_formulae_missing &&
!args.include_build? &&
!args.include_test? &&
!args.include_optional? &&
!args.skip_recommended?
uses = intersection_of_dependents(use_runtime_dependents, used_formulae, args: args)
return if uses.empty?
puts Formatter.columns(uses.map(&:full_name).sort)
odie "Missing formulae should not have dependents!" if used_formulae_missing
end
def intersection_of_dependents(use_runtime_dependents, used_formulae, args:)
recursive = args.recursive?
show_formulae_and_casks = !args.formula? && !args.cask?
includes, ignores = args_includes_ignores(args)
deps = []
if use_runtime_dependents
if show_formulae_and_casks || args.formula?
deps += used_formulae.map(&:runtime_installed_formula_dependents)
.reduce(&:&)
.select(&:any_version_installed?)
end
if show_formulae_and_casks || args.cask?
deps += select_used_dependents(
dependents(Cask::Caskroom.casks),
used_formulae, recursive, includes, ignores
)
end
deps
else
all = args.eval_all?
if args.all?
unless all
odeprecated "brew uses --all",
"brew uses --eval-all or HOMEBREW_EVAL_ALL"
end
all = true
end
if !args.installed? && !(all || Homebrew::EnvConfig.eval_all?)
odeprecated "brew uses", "brew uses --eval-all or HOMEBREW_EVAL_ALL"
end
if show_formulae_and_casks || args.formula?
deps += args.installed? ? Formula.installed : Formula.all
end
if show_formulae_and_casks || args.cask?
deps += args.installed? ? Cask::Caskroom.casks : Cask::Cask.all
end
select_used_dependents(dependents(deps), used_formulae, recursive, includes, ignores)
end
end
def select_used_dependents(dependents, used_formulae, recursive, includes, ignores)
dependents.select do |d|
deps = if recursive
recursive_includes(Dependency, d, includes, ignores)
else
reject_ignores(d.deps, ignores, includes)
end
used_formulae.all? do |ff|
deps.any? do |dep|
match = begin
dep.to_formula.full_name == ff.full_name if dep.name.include?("/")
rescue
nil
end
next match unless match.nil?
dep.name == ff.name
end
rescue FormulaUnavailableError
# Silently ignore this case as we don't care about things used in
# taps that aren't currently tapped.
next
end
end
end
end
#: @hide_from_man_page
#: * `vendor-install` [<target>]
#:
#: Install Homebrew's portable Ruby.
# HOMEBREW_CURLRC, HOMEBREW_LIBRARY, HOMEBREW_STDERR is from the user environment
# HOMEBREW_CACHE, HOMEBREW_CURL, HOMEBREW_LINUX, HOMEBREW_LINUX_MINIMUM_GLIBC_VERSION, HOMEBREW_MACOS,
# HOMEBREW_MACOS_VERSION_NUMERIC and HOMEBREW_PROCESSOR are set by brew.sh
# shellcheck disable=SC2154
source "${HOMEBREW_LIBRARY}/Homebrew/utils/lock.sh"
VENDOR_DIR="${HOMEBREW_LIBRARY}/Homebrew/vendor"
# Built from https://github.com/Homebrew/homebrew-portable-ruby.
if [[ -n "${HOMEBREW_MACOS}" ]]
then
if [[ "${HOMEBREW_PHYSICAL_PROCESSOR}" == "x86_64" ]] ||
# Handle the case where /usr/local/bin/brew is run under arm64.
# It's a x86_64 installation there (we refuse to install arm64 binaries) so
# use a x86_64 Portable Ruby.
[[ "${HOMEBREW_PHYSICAL_PROCESSOR}" == "arm64" && "${HOMEBREW_PREFIX}" == "/usr/local" ]]
then
ruby_FILENAME="portable-ruby-2.6.8_1.el_capitan.bottle.tar.gz"
ruby_SHA="1f50bf80583bd436c9542d4fa5ad47df0ef0f0bea22ae710c4f04c42d7560bca"
elif [[ "${HOMEBREW_PHYSICAL_PROCESSOR}" == "arm64" ]]
then
ruby_FILENAME="portable-ruby-2.6.8_1.arm64_big_sur.bottle.tar.gz"
ruby_SHA="cf9137b1da5568d4949f71161a69b101f60ddb765e94d2b423c9801b67a1cb43"
fi
elif [[ -n "${HOMEBREW_LINUX}" ]]
then
case "${HOMEBREW_PROCESSOR}" in
x86_64)
ruby_FILENAME="portable-ruby-2.6.8_1.x86_64_linux.bottle.tar.gz"
ruby_SHA="fc45ee6eddf4c7a17f4373dde7b1bc8a58255ea61e6847d3bf895225b28d072a"
;;
*) ;;
esac
fi
# Dynamic variables can't be detected by shellcheck
# shellcheck disable=SC2034
if [[ -n "${ruby_SHA}" && -n "${ruby_FILENAME}" ]]
then
ruby_URLs=()
if [[ -n "${HOMEBREW_ARTIFACT_DOMAIN}" ]]
then
ruby_URLs+=("${HOMEBREW_ARTIFACT_DOMAIN}/v2/homebrew/portable-ruby/portable-ruby/blobs/sha256:${ruby_SHA}")
fi
if [[ -n "${HOMEBREW_BOTTLE_DOMAIN}" ]]
then
ruby_URLs+=("${HOMEBREW_BOTTLE_DOMAIN}/bottles-portable-ruby/${ruby_FILENAME}")
fi
ruby_URLs+=(
"https://ghcr.io/v2/homebrew/portable-ruby/portable-ruby/blobs/sha256:${ruby_SHA}"
"https://github.com/Homebrew/homebrew-portable-ruby/releases/download/2.6.8_1/${ruby_FILENAME}"
)
ruby_URL="${ruby_URLs[0]}"
fi
check_linux_glibc_version() {
if [[ -z "${HOMEBREW_LINUX}" || -z "${HOMEBREW_LINUX_MINIMUM_GLIBC_VERSION}" ]]
then
return 0
fi
local glibc_version
local glibc_version_major
local glibc_version_minor
local minimum_required_major="${HOMEBREW_LINUX_MINIMUM_GLIBC_VERSION%.*}"
local minimum_required_minor="${HOMEBREW_LINUX_MINIMUM_GLIBC_VERSION#*.}"
if [[ "$(/usr/bin/ldd --version)" =~ \ [0-9]\.[0-9]+ ]]
then
glibc_version="${BASH_REMATCH[0]// /}"
glibc_version_major="${glibc_version%.*}"
glibc_version_minor="${glibc_version#*.}"
if ((glibc_version_major < minimum_required_major || glibc_version_minor < minimum_required_minor))
then
odie "Vendored tools require system Glibc ${HOMEBREW_LINUX_MINIMUM_GLIBC_VERSION} or later (yours is ${glibc_version})."
fi
else
odie "Failed to detect system Glibc version."
fi
}
# Execute the specified command, and suppress stderr unless HOMEBREW_STDERR is set.
quiet_stderr() {
if [[ -z "${HOMEBREW_STDERR}" ]]
then
command "$@" 2>/dev/null
else
command "$@"
fi
}
fetch() {
local -a curl_args
local url
local sha
local first_try=1
local vendor_locations
local temporary_path
curl_args=()
# do not load .curlrc unless requested (must be the first argument)
# HOMEBREW_CURLRC isn't misspelt here
# shellcheck disable=SC2153
if [[ -z "${HOMEBREW_CURLRC}" ]]
then
curl_args[${#curl_args[*]}]="-q"
fi
# Authorization is needed for GitHub Packages but harmless on GitHub Releases
curl_args+=(
--fail
--remote-time
--location
--user-agent "${HOMEBREW_USER_AGENT_CURL}"
--header "Authorization: ${HOMEBREW_GITHUB_PACKAGES_AUTH}"
)
if [[ -n "${HOMEBREW_QUIET}" ]]
then
curl_args[${#curl_args[*]}]="--silent"
elif [[ -z "${HOMEBREW_VERBOSE}" ]]
then
curl_args[${#curl_args[*]}]="--progress-bar"
fi
if [[ "${HOMEBREW_MACOS_VERSION_NUMERIC}" -lt "100600" ]]
then
curl_args[${#curl_args[*]}]="--insecure"
fi
temporary_path="${CACHED_LOCATION}.incomplete"
mkdir -p "${HOMEBREW_CACHE}"
[[ -n "${HOMEBREW_QUIET}" ]] || ohai "Downloading ${VENDOR_URL}" >&2
if [[ -f "${CACHED_LOCATION}" ]]
then
[[ -n "${HOMEBREW_QUIET}" ]] || echo "Already downloaded: ${CACHED_LOCATION}" >&2
else
for url in "${VENDOR_URLs[@]}"
do
[[ -n "${HOMEBREW_QUIET}" || -n "${first_try}" ]] || ohai "Downloading ${url}" >&2
first_try=''
if [[ -f "${temporary_path}" ]]
then
# HOMEBREW_CURL is set by brew.sh (and isn't mispelt here)
# shellcheck disable=SC2153
"${HOMEBREW_CURL}" "${curl_args[@]}" -C - "${url}" -o "${temporary_path}"
if [[ $? -eq 33 ]]
then
[[ -n "${HOMEBREW_QUIET}" ]] || echo "Trying a full download" >&2
rm -f "${temporary_path}"
"${HOMEBREW_CURL}" "${curl_args[@]}" "${url}" -o "${temporary_path}"
fi
else
"${HOMEBREW_CURL}" "${curl_args[@]}" "${url}" -o "${temporary_path}"
fi
[[ -f "${temporary_path}" ]] && break
done
if [[ ! -f "${temporary_path}" ]]
then
vendor_locations="$(printf " - %s\n" "${VENDOR_URLs[@]}")"
odie <<EOS
Failed to download ${VENDOR_NAME} from the following locations:
${vendor_locations}
Do not file an issue on GitHub about this; you will need to figure out for
yourself what issue with your internet connection restricts your access to
GitHub (used for Homebrew updates and binary packages).
EOS
fi
trap '' SIGINT
mv "${temporary_path}" "${CACHED_LOCATION}"
trap - SIGINT
fi
if [[ -x "/usr/bin/shasum" ]]
then
sha="$(/usr/bin/shasum -a 256 "${CACHED_LOCATION}" | cut -d' ' -f1)"
elif [[ -x "$(type -P sha256sum)" ]]
then
sha="$(sha256sum "${CACHED_LOCATION}" | cut -d' ' -f1)"
elif [[ -x "$(type -P ruby)" ]]
then
sha="$(
ruby <<EOSCRIPT
require 'digest/sha2'
digest = Digest::SHA256.new
File.open('${CACHED_LOCATION}', 'rb') { |f| digest.update(f.read) }
puts digest.hexdigest
EOSCRIPT
)"
else
odie "Cannot verify checksum ('shasum' or 'sha256sum' not found)!"
fi
if [[ "${sha}" != "${VENDOR_SHA}" ]]
then
odie <<EOS
Checksum mismatch.
Expected: ${VENDOR_SHA}
Actual: ${sha}
Archive: ${CACHED_LOCATION}
To retry an incomplete download, remove the file above.
EOS
fi
}
install() {
local tar_args
if [[ -n "${HOMEBREW_VERBOSE}" ]]
then
tar_args="xvzf"
else
tar_args="xzf"
fi
mkdir -p "${VENDOR_DIR}/portable-${VENDOR_NAME}"
safe_cd "${VENDOR_DIR}/portable-${VENDOR_NAME}"
trap '' SIGINT
if [[ -d "${VENDOR_VERSION}" ]]
then
mv "${VENDOR_VERSION}" "${VENDOR_VERSION}.reinstall"
fi
safe_cd "${VENDOR_DIR}"
[[ -n "${HOMEBREW_QUIET}" ]] || ohai "Pouring ${VENDOR_FILENAME}" >&2
tar "${tar_args}" "${CACHED_LOCATION}"
safe_cd "${VENDOR_DIR}/portable-${VENDOR_NAME}"
if quiet_stderr "./${VENDOR_VERSION}/bin/${VENDOR_NAME}" --version >/dev/null
then
ln -sfn "${VENDOR_VERSION}" current
if [[ -d "${VENDOR_VERSION}.reinstall" ]]
then
rm -rf "${VENDOR_VERSION}.reinstall"
fi
else
rm -rf "${VENDOR_VERSION}"
if [[ -d "${VENDOR_VERSION}.reinstall" ]]
then
mv "${VENDOR_VERSION}.reinstall" "${VENDOR_VERSION}"
fi
odie "Failed to install ${VENDOR_NAME} ${VENDOR_VERSION}!"
fi
trap - SIGINT
}
homebrew-vendor-install() {
local option
local url_var
local sha_var
for option in "$@"
do
case "${option}" in
-\? | -h | --help | --usage)
brew help vendor-install
exit $?
;;
--verbose) HOMEBREW_VERBOSE=1 ;;
--quiet) HOMEBREW_QUIET=1 ;;
--debug) HOMEBREW_DEBUG=1 ;;
--*) ;;
-*)
[[ "${option}" == *v* ]] && HOMEBREW_VERBOSE=1
[[ "${option}" == *q* ]] && HOMEBREW_QUIET=1
[[ "${option}" == *d* ]] && HOMEBREW_DEBUG=1
;;
*)
[[ -n "${VENDOR_NAME}" ]] && odie "This command does not take multiple vendor targets!"
VENDOR_NAME="${option}"
;;
esac
done
[[ -z "${VENDOR_NAME}" ]] && odie "This command requires a vendor target!"
[[ -n "${HOMEBREW_DEBUG}" ]] && set -x
check_linux_glibc_version
filename_var="${VENDOR_NAME}_FILENAME"
sha_var="${VENDOR_NAME}_SHA"
url_var="${VENDOR_NAME}_URL"
VENDOR_FILENAME="${!filename_var}"
VENDOR_SHA="${!sha_var}"
VENDOR_URL="${!url_var}"
VENDOR_VERSION="$(cat "${VENDOR_DIR}/portable-${VENDOR_NAME}-version")"
if [[ -z "${VENDOR_URL}" || -z "${VENDOR_SHA}" ]]
then
odie "No Homebrew ${VENDOR_NAME} ${VENDOR_VERSION} available for ${HOMEBREW_PROCESSOR} processors!"
fi
# Expand the name to an array of variables
# The array name must be "${VENDOR_NAME}_URLs"! Otherwise substitution errors will occur!
# shellcheck disable=SC2086
read -r -a VENDOR_URLs <<<"$(eval "echo "\$\{${url_var}s[@]\}"")"
CACHED_LOCATION="${HOMEBREW_CACHE}/${VENDOR_FILENAME}"
lock "vendor-install-${VENDOR_NAME}"
fetch
install
}
# typed: false
# frozen_string_literal: true
require "completions"
# Helper functions for commands.
#
# @api private
module Commands
module_function
HOMEBREW_CMD_PATH = (HOMEBREW_LIBRARY_PATH/"cmd").freeze
HOMEBREW_DEV_CMD_PATH = (HOMEBREW_LIBRARY_PATH/"dev-cmd").freeze
HOMEBREW_INTERNAL_COMMAND_ALIASES = {
"ls" => "list",
"homepage" => "home",
"-S" => "search",
"up" => "update",
"ln" => "link",
"instal" => "install", # gem does the same
"uninstal" => "uninstall",
"rm" => "uninstall",
"remove" => "uninstall",
"abv" => "info",
"dr" => "doctor",
"--repo" => "--repository",
"environment" => "--env",
"--config" => "config",
"-v" => "--version",
"lc" => "livecheck",
"tc" => "typecheck",
}.freeze
INSTALL_FROM_API_FORBIDDEN_COMMANDS = %w[
audit
bottle
bump-cask-pr
bump-formula-pr
bump-revision
bump-unversioned-casks
cat
create
edit
extract
formula
livecheck
pr-pull
pr-upload
test
update-python-resources
].freeze
def valid_internal_cmd?(cmd)
require?(HOMEBREW_CMD_PATH/cmd)
end
def valid_internal_dev_cmd?(cmd)
require?(HOMEBREW_DEV_CMD_PATH/cmd)
end
def method_name(cmd)
cmd.to_s
.tr("-", "_")
.downcase
.to_sym
end
def args_method_name(cmd_path)
cmd_path_basename = basename_without_extension(cmd_path)
cmd_method_prefix = method_name(cmd_path_basename)
"#{cmd_method_prefix}_args".to_sym
end
def internal_cmd_path(cmd)
[
HOMEBREW_CMD_PATH/"#{cmd}.rb",
HOMEBREW_CMD_PATH/"#{cmd}.sh",
].find(&:exist?)
end
def internal_dev_cmd_path(cmd)
[
HOMEBREW_DEV_CMD_PATH/"#{cmd}.rb",
HOMEBREW_DEV_CMD_PATH/"#{cmd}.sh",
].find(&:exist?)
end
# Ruby commands which can be `require`d without being run.
def external_ruby_v2_cmd_path(cmd)
path = which("#{cmd}.rb", Tap.cmd_directories)
path if require?(path)
end
# Ruby commands which are run by being `require`d.
def external_ruby_cmd_path(cmd)
which("brew-#{cmd}.rb", PATH.new(ENV.fetch("PATH")).append(Tap.cmd_directories))
end
def external_cmd_path(cmd)
which("brew-#{cmd}", PATH.new(ENV.fetch("PATH")).append(Tap.cmd_directories))
end
def path(cmd)
internal_cmd = HOMEBREW_INTERNAL_COMMAND_ALIASES.fetch(cmd, cmd)
path ||= internal_cmd_path(internal_cmd)
path ||= internal_dev_cmd_path(internal_cmd)
path ||= external_ruby_v2_cmd_path(cmd)
path ||= external_ruby_cmd_path(cmd)
path ||= external_cmd_path(cmd)
path
end
def commands(external: true, aliases: false)
cmds = internal_commands
cmds += internal_developer_commands
cmds += external_commands if external
cmds += internal_commands_aliases if aliases
cmds.sort
end
def internal_commands_paths
find_commands HOMEBREW_CMD_PATH
end
def internal_developer_commands_paths
find_commands HOMEBREW_DEV_CMD_PATH
end
def official_external_commands_paths(quiet:)
OFFICIAL_CMD_TAPS.flat_map do |tap_name, cmds|
tap = Tap.fetch(tap_name)
tap.install(quiet: quiet) unless tap.installed?
cmds.map(&method(:external_ruby_v2_cmd_path)).compact
end
end
def internal_commands
find_internal_commands(HOMEBREW_CMD_PATH).map(&:to_s)
end
def internal_developer_commands
find_internal_commands(HOMEBREW_DEV_CMD_PATH).map(&:to_s)
end
def internal_commands_aliases
HOMEBREW_INTERNAL_COMMAND_ALIASES.keys
end
def find_internal_commands(path)
find_commands(path).map(&:basename)
.map(&method(:basename_without_extension))
end
def external_commands
Tap.cmd_directories.flat_map do |path|
find_commands(path).select(&:executable?)
.map(&method(:basename_without_extension))
.map { |p| p.to_s.delete_prefix("brew-").strip }
end.map(&:to_s)
.sort
end
def basename_without_extension(path)
path.basename(path.extname)
end
def find_commands(path)
Pathname.glob("#{path}/*")
.select(&:file?)
.sort
end
def rebuild_internal_commands_completion_list
cmds = internal_commands + internal_developer_commands + internal_commands_aliases
cmds.reject! { |cmd| Homebrew::Completions::COMPLETIONS_EXCLUSION_LIST.include? cmd }
file = HOMEBREW_REPOSITORY/"completions/internal_commands_list.txt"
file.atomic_write("#{cmds.sort.join("\n")}\n")
end
def rebuild_commands_completion_list
# Ensure that the cache exists so we can build the commands list
HOMEBREW_CACHE.mkpath
cmds = commands(aliases: true) - Homebrew::Completions::COMPLETIONS_EXCLUSION_LIST
all_commands_file = HOMEBREW_CACHE/"all_commands_list.txt"
external_commands_file = HOMEBREW_CACHE/"external_commands_list.txt"
all_commands_file.atomic_write("#{cmds.sort.join("\n")}\n")
external_commands_file.atomic_write("#{external_commands.sort.join("\n")}\n")
end
def command_options(command)
path = self.path(command)
return if path.blank?
if (cmd_parser = Homebrew::CLI::Parser.from_cmd_path(path))
cmd_parser.processed_options.map do |short, long, _, desc, hidden|
next if hidden
[long || short, desc]
end.compact
else
options = []
comment_lines = path.read.lines.grep(/^#:/)
return options if comment_lines.empty?
# skip the comment's initial usage summary lines
comment_lines.slice(2..-1).each do |line|
if / (?<option>-[-\w]+) +(?<desc>.*)$/ =~ line
options << [option, desc]
end
end
options
end
end
def command_description(command, short: false)
path = self.path(command)
return if path.blank?
if (cmd_parser = Homebrew::CLI::Parser.from_cmd_path(path))
if short
cmd_parser.description.split(".").first
else
cmd_parser.description
end
else
comment_lines = path.read.lines.grep(/^#:/)
# skip the comment's initial usage summary lines
comment_lines.slice(2..-1)&.each do |line|
if /^#: (?<desc>\w.*+)$/ =~ line
return desc.split(".").first if short
return desc
end
end
end
end
def named_args_type(command)
path = self.path(command)
return if path.blank?
cmd_parser = Homebrew::CLI::Parser.from_cmd_path(path)
return if cmd_parser.blank?
Array(cmd_parser.named_args_type)
end
# Returns the conflicts of a given `option` for `command`.
def option_conflicts(command, option)
path = self.path(command)
return if path.blank?
cmd_parser = Homebrew::CLI::Parser.from_cmd_path(path)
return if cmd_parser.blank?
cmd_parser.conflicts.map do |set|
set.map! { |s| s.tr "_", "-" }
set - [option] if set.include? option
end.flatten.compact
end
end
# typed: true
# frozen_string_literal: true
# typed: strict
# frozen_string_literal: true
# typed: strict
# frozen_string_literal: true
require "compat/formula"
require "compat/cask"
# typed: true
# frozen_string_literal: true
# @private
module CompilerConstants
GNU_GCC_VERSIONS = %w[4.9 5 6 7 8 9 10 11 12].freeze
GNU_GCC_REGEXP = /^gcc-(4\.9|[5-9]|10|11|12)$/.freeze
COMPILER_SYMBOL_MAP = {
"gcc" => :gcc,
"clang" => :clang,
"llvm_clang" => :llvm_clang,
}.freeze
COMPILERS = (COMPILER_SYMBOL_MAP.values +
GNU_GCC_VERSIONS.map { |n| "gcc-#{n}" }).freeze
end
# Class for checking compiler compatibility for a formula.
#
# @api private
class CompilerFailure
attr_reader :type
def version(val = nil)
@version = Version.parse(val.to_s) if val
@version
end
# Allows Apple compiler `fails_with` statements to keep using `build`
# even though `build` and `version` are the same internally.
alias build version
# The cause is no longer used so we need not hold a reference to the string.
def cause(_); end
def self.for_standard(standard)
COLLECTIONS.fetch(standard) do
raise ArgumentError, "\"#{standard}\" is not a recognized standard"
end
end
def self.create(spec, &block)
# Non-Apple compilers are in the format fails_with compiler => version
if spec.is_a?(Hash)
compiler, major_version = spec.first
raise ArgumentError, "The hash `fails_with` syntax only supports GCC" if compiler != :gcc
type = compiler
# so fails_with :gcc => '7' simply marks all 7 releases incompatible
version = "#{major_version}.999"
exact_major_match = true
else
type = spec
version = 9999
exact_major_match = false
end
new(type, version, exact_major_match: exact_major_match, &block)
end
def fails_with?(compiler)
version_matched = if type != :gcc
version >= compiler.version
elsif @exact_major_match
gcc_major(version) == gcc_major(compiler.version) && version >= compiler.version
else
gcc_major(version) >= gcc_major(compiler.version)
end
type == compiler.type && version_matched
end
def inspect
"#<#{self.class.name}: #{type} #{version}>"
end
private
def initialize(type, version, exact_major_match:, &block)
@type = type
@version = Version.parse(version.to_s)
@exact_major_match = exact_major_match
instance_eval(&block) if block
end
def gcc_major(version)
if version.major >= 5
Version.new(version.major.to_s)
else
version.major_minor
end
end
COLLECTIONS = {
openmp: [
create(:clang),
],
}.freeze
end
# Class for selecting a compiler for a formula.
#
# @api private
class CompilerSelector
extend T::Sig
include CompilerConstants
Compiler = Struct.new(:type, :name, :version)
COMPILER_PRIORITY = {
clang: [:clang, :gnu, :llvm_clang],
gcc: [:gnu, :gcc, :llvm_clang, :clang],
}.freeze
def self.select_for(formula, compilers = self.compilers)
new(formula, DevelopmentTools, compilers).compiler
end
def self.compilers
COMPILER_PRIORITY.fetch(DevelopmentTools.default_compiler)
end
attr_reader :formula, :failures, :versions, :compilers
def initialize(formula, versions, compilers)
@formula = formula
@failures = formula.compiler_failures
@versions = versions
@compilers = compilers
end
def compiler
find_compiler { |c| return c.name unless fails_with?(c) }
raise CompilerSelectionError, formula
end
sig { returns(String) }
def self.preferred_gcc
"gcc"
end
private
def gnu_gcc_versions
# prioritize gcc version provided by gcc formula.
v = Formulary.factory(CompilerSelector.preferred_gcc).version.to_s.slice(/\d+/)
GNU_GCC_VERSIONS - [v] + [v] # move the version to the end of the list
rescue FormulaUnavailableError
GNU_GCC_VERSIONS
end
def find_compiler
compilers.each do |compiler|
case compiler
when :gnu
gnu_gcc_versions.reverse_each do |v|
executable = "gcc-#{v}"
version = compiler_version(executable)
yield Compiler.new(:gcc, executable, version) unless version.null?
end
when :llvm
next # no-op. DSL supported, compiler is not.
else
version = compiler_version(compiler)
yield Compiler.new(compiler, compiler, version) unless version.null?
end
end
end
def fails_with?(compiler)
failures.any? { |failure| failure.fails_with?(compiler) }
end
def compiler_version(name)
case name.to_s
when "gcc", GNU_GCC_REGEXP
versions.gcc_version(name.to_s)
else
versions.send("#{name}_build_version")
end
end
end
require "extend/os/compilers"
# typed: true
# frozen_string_literal: true
require "utils/link"
require "settings"
require "erb"
module Homebrew
# Helper functions for generating shell completions.
#
# @api private
module Completions
extend T::Sig
module_function
COMPLETIONS_DIR = (HOMEBREW_REPOSITORY/"completions").freeze
TEMPLATE_DIR = (HOMEBREW_LIBRARY_PATH/"completions").freeze
SHELLS = %w[bash fish zsh].freeze
COMPLETIONS_EXCLUSION_LIST = %w[
instal
uninstal
update-report
].freeze
BASH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING = {
formula: "__brew_complete_formulae",
installed_formula: "__brew_complete_installed_formulae",
outdated_formula: "__brew_complete_outdated_formulae",
cask: "__brew_complete_casks",
installed_cask: "__brew_complete_installed_casks",
outdated_cask: "__brew_complete_outdated_casks",
tap: "__brew_complete_tapped",
installed_tap: "__brew_complete_tapped",
command: "__brew_complete_commands",
diagnostic_check: '__brewcomp "$(brew doctor --list-checks)"',
file: "__brew_complete_files",
}.freeze
ZSH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING = {
formula: "__brew_formulae",
installed_formula: "__brew_installed_formulae",
outdated_formula: "__brew_outdated_formulae",
cask: "__brew_casks",
installed_cask: "__brew_installed_casks",
outdated_cask: "__brew_outdated_casks",
tap: "__brew_any_tap",
installed_tap: "__brew_installed_taps",
command: "__brew_commands",
diagnostic_check: "__brew_diagnostic_checks",
file: "__brew_formulae_or_ruby_files",
}.freeze
FISH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING = {
formula: "__fish_brew_suggest_formulae_all",
installed_formula: "__fish_brew_suggest_formulae_installed",
outdated_formula: "__fish_brew_suggest_formulae_outdated",
cask: "__fish_brew_suggest_casks_all",
installed_cask: "__fish_brew_suggest_casks_installed",
outdated_cask: "__fish_brew_suggest_casks_outdated",
tap: "__fish_brew_suggest_taps_installed",
installed_tap: "__fish_brew_suggest_taps_installed",
command: "__fish_brew_suggest_commands",
diagnostic_check: "__fish_brew_suggest_diagnostic_checks",
}.freeze
sig { void }
def link!
Settings.write :linkcompletions, true
Tap.each do |tap|
Utils::Link.link_completions tap.path, "brew completions link"
end
end
sig { void }
def unlink!
Settings.write :linkcompletions, false
Tap.each do |tap|
next if tap.official?
Utils::Link.unlink_completions tap.path
end
end
sig { returns(T::Boolean) }
def link_completions?
Settings.read(:linkcompletions) == "true"
end
sig { returns(T::Boolean) }
def completions_to_link?
Tap.each do |tap|
next if tap.official?
SHELLS.each do |shell|
return true if (tap.path/"completions/#{shell}").exist?
end
end
false
end
sig { void }
def show_completions_message_if_needed
return if Settings.read(:completionsmessageshown) == "true"
return unless completions_to_link?
ohai "Homebrew completions for external commands are unlinked by default!"
puts <<~EOS
To opt-in to automatically linking external tap shell completion files, run:
brew completions link
Then, follow the directions at #{Formatter.url("https://docs.brew.sh/Shell-Completion")}
EOS
Settings.write :completionsmessageshown, true
end
sig { void }
def update_shell_completions!
commands = Commands.commands(external: false, aliases: true).sort
puts "Writing completions to #{COMPLETIONS_DIR}"
(COMPLETIONS_DIR/"bash/brew").atomic_write generate_bash_completion_file(commands)
(COMPLETIONS_DIR/"zsh/_brew").atomic_write generate_zsh_completion_file(commands)
(COMPLETIONS_DIR/"fish/brew.fish").atomic_write generate_fish_completion_file(commands)
end
sig { params(command: String).returns(T::Boolean) }
def command_gets_completions?(command)
command_options(command).any?
end
sig { params(description: String, fish: T::Boolean).returns(String) }
def format_description(description, fish: false)
description = if fish
description.gsub("'", "\\\\'")
else
description.gsub("'", "'\\\\''")
end
description.gsub(/[<>]/, "").tr("\n", " ").chomp(".")
end
sig { params(command: String).returns(T::Hash[String, String]) }
def command_options(command)
options = {}
Commands.command_options(command)&.each do |option|
next if option.blank?
name = option.first
desc = option.second
if name.start_with? "--[no-]"
options[name.remove("[no-]")] = desc
options[name.sub("[no-]", "no-")] = desc
else
options[name] = desc
end
end
options
end
sig { params(command: String).returns(T.nilable(String)) }
def generate_bash_subcommand_completion(command)
return unless command_gets_completions? command
named_completion_string = ""
if (types = Commands.named_args_type(command))
named_args_strings, named_args_types = types.partition { |type| type.is_a? String }
named_args_types.each do |type|
next unless BASH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING.key? type
named_completion_string += "\n #{BASH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING[type]}"
end
named_completion_string += "\n __brewcomp \"#{named_args_strings.join(" ")}\"" if named_args_strings.any?
end
<<~COMPLETION
_brew_#{Commands.method_name command}() {
local cur="${COMP_WORDS[COMP_CWORD]}"
case "${cur}" in
-*)
__brewcomp "
#{command_options(command).keys.sort.join("\n ")}
"
return
;;
*)
esac#{named_completion_string}
}
COMPLETION
end
sig { params(commands: T::Array[String]).returns(String) }
def generate_bash_completion_file(commands)
variables = OpenStruct.new
variables[:completion_functions] = commands.map do |command|
generate_bash_subcommand_completion command
end.compact
variables[:function_mappings] = commands.map do |command|
next unless command_gets_completions? command
"#{command}) _brew_#{Commands.method_name command} ;;"
end.compact
ERB.new((TEMPLATE_DIR/"bash.erb").read, trim_mode: ">").result(variables.instance_eval { binding })
end
sig { params(command: String).returns(T.nilable(String)) }
def generate_zsh_subcommand_completion(command)
return unless command_gets_completions? command
options = command_options(command)
args_options = []
if (types = Commands.named_args_type(command))
named_args_strings, named_args_types = types.partition { |type| type.is_a? String }
named_args_types.each do |type|
next unless ZSH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING.key? type
args_options << "- #{type}"
opt = "--#{type.to_s.gsub(/(installed|outdated)_/, "")}"
if options.key?(opt)
desc = options[opt]
if desc.blank?
args_options << opt
else
conflicts = generate_zsh_option_exclusions(command, opt)
args_options << "#{conflicts}#{opt}[#{format_description desc}]"
end
options.delete(opt)
end
args_options << "*::#{type}:#{ZSH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING[type]}"
end
if named_args_strings.any?
args_options << "- subcommand"
args_options << "*::subcommand:(#{named_args_strings.join(" ")})"
end
end
options = options.sort.map do |opt, desc|
next opt if desc.blank?
conflicts = generate_zsh_option_exclusions(command, opt)
"#{conflicts}#{opt}[#{format_description desc}]"
end
options += args_options
<<~COMPLETION
# brew #{command}
_brew_#{Commands.method_name command}() {
_arguments \\
#{options.map! { |opt| opt.start_with?("- ") ? opt : "'#{opt}'" }.join(" \\\n ")}
}
COMPLETION
end
def generate_zsh_option_exclusions(command, option)
conflicts = Commands.option_conflicts(command, option.gsub(/^--/, ""))
return "" unless conflicts.presence
"(#{conflicts.map { |conflict| "--#{conflict}" }.join(" ")})"
end
sig { params(commands: T::Array[String]).returns(String) }
def generate_zsh_completion_file(commands)
variables = OpenStruct.new
variables[:aliases] = Commands::HOMEBREW_INTERNAL_COMMAND_ALIASES.map do |alias_command, command|
alias_command = "'#{alias_command}'" if alias_command.start_with? "-"
command = "'#{command}'" if command.start_with? "-"
"#{alias_command} #{command}"
end.compact
variables[:builtin_command_descriptions] = commands.map do |command|
next if Commands::HOMEBREW_INTERNAL_COMMAND_ALIASES.key? command
description = Commands.command_description(command, short: true)
next if description.blank?
description = format_description description
"'#{command}:#{description}'"
end.compact
variables[:completion_functions] = commands.map do |command|
generate_zsh_subcommand_completion command
end.compact
ERB.new((TEMPLATE_DIR/"zsh.erb").read, trim_mode: ">").result(variables.instance_eval { binding })
end
sig { params(command: String).returns(T.nilable(String)) }
def generate_fish_subcommand_completion(command)
return unless command_gets_completions? command
command_description = format_description Commands.command_description(command, short: true), fish: true
lines = ["__fish_brew_complete_cmd '#{command}' '#{command_description}'"]
options = command_options(command).sort.map do |opt, desc|
arg_line = "__fish_brew_complete_arg '#{command}' -l #{opt.sub(/^-+/, "")}"
arg_line += " -d '#{format_description desc, fish: true}'" if desc.present?
arg_line
end.compact
subcommands = []
named_args = []
if (types = Commands.named_args_type(command))
named_args_strings, named_args_types = types.partition { |type| type.is_a? String }
named_args_types.each do |type|
next unless FISH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING.key? type
named_arg_function = FISH_NAMED_ARGS_COMPLETION_FUNCTION_MAPPING[type]
named_arg_prefix = "__fish_brew_complete_arg '#{command}; and not __fish_seen_argument"
formula_option = command_options(command).key?("--formula")
cask_option = command_options(command).key?("--cask")
named_args << if formula_option && cask_option && type.to_s.end_with?("formula")
"#{named_arg_prefix} -l cask -l casks' -a '(#{named_arg_function})'"
elsif formula_option && cask_option && type.to_s.end_with?("cask")
"#{named_arg_prefix} -l formula -l formulae' -a '(#{named_arg_function})'"
else
"__fish_brew_complete_arg '#{command}' -a '(#{named_arg_function})'"
end
end
named_args_strings.each do |subcommand|
subcommands << "__fish_brew_complete_sub_cmd '#{command}' '#{subcommand}'"
end
end
lines += subcommands + options + named_args
<<~COMPLETION
#{lines.join("\n").chomp}
COMPLETION
end
sig { params(commands: T::Array[String]).returns(String) }
def generate_fish_completion_file(commands)
variables = OpenStruct.new
variables[:completion_functions] = commands.map do |command|
generate_fish_subcommand_completion command
end.compact
ERB.new((TEMPLATE_DIR/"fish.erb").read, trim_mode: ">").result(variables.instance_eval { binding })
end
end
end
# typed: strict
module Homebrew
module Completions
include Kernel
end
end
<%
# To make changes to the completions:
#
# - For changes to a command under `COMMANDS` or `DEVELOPER COMMANDS` sections):
# - Find the source file in `Library/Homebrew/[dev-]cmd/<command>.{rb,sh}`.
# - For `.rb` files, edit the `<command>_args` method.
# - For `.sh` files, edit the top comment, being sure to use the line prefix
# `#:` for the comments to be recognized as documentation. If in doubt,
# compare with already documented commands.
# - For other changes: Edit this file.
#
# When done, regenerate the completions by running `brew generate-man-completions`.
%>
# Bash completion script for brew(1)
__brewcomp_words_include() {
local i=1
while [[ "${i}" -lt "${COMP_CWORD}" ]]
do
if [[ "${COMP_WORDS[i]}" = "$1" ]]
then
return 0
fi
(( i++ ))
done
return 1
}
# Find the previous non-switch word
__brewcomp_prev() {
local idx="$((COMP_CWORD - 1))"
local prv="${COMP_WORDS[idx]}"
while [[ "${prv}" = -* ]]
do
(( idx-- ))
prv="${COMP_WORDS[idx]}"
done
echo "${prv}"
}
__brewcomp() {
# break $1 on space, tab, and newline characters,
# and turn it into a newline separated list of words
local list s sep=$'\n' IFS=$' \t\n'
local cur="${COMP_WORDS[COMP_CWORD]}"
for s in $1
do
__brewcomp_words_include "${s}" && continue
list="${list}${s}${sep}"
done
IFS="${sep}"
while read -r line; do COMPREPLY+=("${line}"); done < <(compgen -W "${list}" -- "${cur}")
}
# Don't use __brewcomp() in any of the __brew_complete_foo functions, as
# it is too slow and is not worth it just for duplicate elimination.
__brew_complete_formulae() {
local cur="${COMP_WORDS[COMP_CWORD]}"
local formulae
formulae="$(brew formulae)"
while read -r line; do COMPREPLY+=("${line}"); done < <(compgen -W "${formulae}" -- "${cur}")
}
__brew_complete_casks() {
local cur="${COMP_WORDS[COMP_CWORD]}"
local casks
casks="$(brew casks)"
while read -r line; do COMPREPLY+=("${line}"); done < <(compgen -W "${casks}" -- "${cur}")
}
__brew_complete_installed_formulae() {
local cur="${COMP_WORDS[COMP_CWORD]}"
local installed_formulae
installed_formulae="$(command ls "$(brew --cellar)" 2>/dev/null)"
while read -r line; do COMPREPLY+=("${line}"); done < <(compgen -W "${installed_formulae}" -- "${cur}")
}
__brew_complete_installed_casks() {
local cur="${COMP_WORDS[COMP_CWORD]}"
local installed_casks
installed_casks="$(command ls "$(brew --caskroom)" 2>/dev/null)"
while read -r line; do COMPREPLY+=("${line}"); done < <(compgen -W "${installed_casks}" -- "${cur}")
}
__brew_complete_outdated_formulae() {
local cur="${COMP_WORDS[COMP_CWORD]}"
local outdated_formulae
outdated_formulae="$(brew outdated --formula --quiet)"
while read -r line; do COMPREPLY+=("${line}"); done < <(compgen -W "${outdated_formulae}" -- "${cur}")
}
__brew_complete_outdated_casks() {
local cur="${COMP_WORDS[COMP_CWORD]}"
local outdated_casks
outdated_casks="$(brew outdated --cask --quiet)"
while read -r line; do COMPREPLY+=("${line}"); done < <(compgen -W "${outdated_casks}" -- "${cur}")
}
__brew_complete_tapped() {
local dir taps taplib
taplib="$(brew --repository)/Library/Taps"
for dir in "${taplib}"/*/*
do
[[ -d "${dir}" ]] || continue
dir="${dir#"${taplib}"/}"
dir="${dir/homebrew-/}"
taps="${taps} ${dir}"
done
__brewcomp "${taps}"
}
__brew_complete_commands() {
local cur="${COMP_WORDS[COMP_CWORD]}"
local cmds
HOMEBREW_CACHE=$(brew --cache)
HOMEBREW_REPOSITORY=$(brew --repo)
# Do not auto-complete "*instal" or "*uninstal" aliases for "*install" commands.
if [[ -f "${HOMEBREW_CACHE}/all_commands_list.txt" ]]
then
cmds="$(< "${HOMEBREW_CACHE}/all_commands_list.txt" \grep -v instal$)"
else
cmds="$(< "${HOMEBREW_REPOSITORY}/completions/internal_commands_list.txt" \grep -v instal$)"
fi
while read -r line; do COMPREPLY+=("${line}"); done < <(compgen -W "${cmds}" -- "${cur}")
}
# compopt is only available in newer versions of bash
__brew_complete_files() {
command -v compopt &> /dev/null && compopt -o default
}
<%= completion_functions.join("\n") %>
_brew() {
local i=1 cmd
# find the subcommand
while [[ "${i}" -lt "${COMP_CWORD}" ]]
do
local s="${COMP_WORDS[i]}"
case "${s}" in
--*)
cmd="${s}"
break
;;
-*)
;;
*)
cmd="${s}"
break
;;
esac
(( i++ ))
done
if [[ "${i}" -eq "${COMP_CWORD}" ]]
then
__brew_complete_commands
return
fi
# subcommands have their own completion functions
case "${cmd}" in
<%= function_mappings.join("\n ").concat("\n") %>
*) ;;
esac
}
# keep around for compatibility
_brew_to_completion() {
_brew
}
complete -o bashdefault -o default -F _brew brew
<%
# To make changes to the completions:
#
# - For changes to a command under `COMMANDS` or `DEVELOPER COMMANDS` sections):
# - Find the source file in `Library/Homebrew/[dev-]cmd/<command>.{rb,sh}`.
# - For `.rb` files, edit the `<command>_args` method.
# - For `.sh` files, edit the top comment, being sure to use the line prefix
# `#:` for the comments to be recognized as documentation. If in doubt,
# compare with already documented commands.
# - For other changes: Edit this file.
#
# When done, regenerate the completions by running `brew generate-man-completions`.
%>
# Fish shell completions for Homebrew
# A note about aliases:
#
# * When defining completions for the (sub)commands themselves, only the full names are used, as they
# are more descriptive and worth completing. Aliases are usually shorter than the full names, and
# exist exactly to save time for users who already know what they want and are going to type the
# command anyway (i.e. without completion).
# * Nevertheless, it's important to support aliases in the completions for their arguments/options.
##########################
## COMMAND LINE PARSING ##
##########################
function __fish_brew_args -d "Returns a list of all arguments given to brew"
set -l tokens (commandline -opc)
set -e tokens[1] # remove 'brew'
for t in $tokens
echo $t
end
end
function __fish_brew_opts -d "Only arguments starting with a dash (options)"
string match --all -- '-*' (__fish_brew_args)
end
# This can be used either to get the first argument or to match it against a given list of commands
#
# Usage examples (for `completion -n '...'`):
# * `__fish_brew_command` returns the command (first arg of brew) or exits with 1
# * `not __fish_brew_command` returns true when brew doesn't have a command yet
# * `__fish_brew_command list ls` returns true when brew command is _either_ `list` _or_ `ls`
#
function __fish_brew_command -d "Helps matching the first argument of brew"
set args (__fish_brew_args)
set -q args[1]; or return 1
if count $argv
contains -- $args[1] $argv
else
echo $args[1]
end
end
function __fish_brew_subcommand -a cmd -d "Helps matching the second argument of brew"
set args (__fish_brew_args)
__fish_brew_command $cmd
and set -q args[2]
and set -l sub $args[2]
or return 1
set -e argv[1]
if count $argv
contains -- $sub $argv
else
echo $sub
end
end
# This can be used to match any given option against the given list of arguments:
# * to add condition on interdependent options
# * to add condition on mutually exclusive options
#
# Usage examples (for `completion -n '...'`):
# * `__fish_brew_opt -s --long` returns true if _either_ `-s` _or_ `--long` is present
# * `not __fish_brew_opt --foo --bar` will work only if _neither_ `--foo` _nor_ `--bar` are present
#
function __fish_brew_opt -d "Helps matching brew options against the given list"
not count $argv
or contains -- $argv[1] (__fish_brew_opts)
or begin
set -q argv[2]
and __fish_brew_opt $argv[2..-1]
end
end
######################
## SUGGESTION LISTS ##
######################
# These functions return lists of suggestions for arguments completion
function __fish_brew_ruby_parse_json -a file parser -d 'Parses given JSON file with Ruby'
# parser is any chain of methods to call on the parsed JSON
ruby -e "require('json'); JSON.parse(File.read('$file'))$parser"
end
function __fish_brew_suggest_formulae_all -d 'Lists all available formulae with their descriptions'
# store the brew cache path in a var (because calling (brew --cache) is slow)
set -q __brew_cache_path
or set -gx __brew_cache_path (brew --cache)
if test -f "$__brew_cache_path/descriptions.json"
__fish_brew_ruby_parse_json "$__brew_cache_path/descriptions.json" \
'.each{ |k, v| puts([k, v].reject(&:nil?).join("\t")) }'
else
brew formulae
end
end
function __fish_brew_suggest_formulae_installed
brew list --formula
end
function __fish_brew_suggest_formulae_outdated -d "List of outdated formulae with the information about potential upgrade"
brew outdated --formula --verbose \
# replace first space with tab to make the following a description in the completions list:
| string replace -r '\s' '\t'
end
function __fish_brew_suggest_formula_options -a formula -d "List installation options for a given formula"
function list_pairs
set -q argv[2]; or return 0
echo $argv[1]\t$argv[2]
set -e argv[1..2]
list_pairs $argv
end
# brew options lists options name and its description on different lines
list_pairs (brew options $formula | string trim)
end
function __fish_brew_suggest_casks_all -d "Lists locally available casks"
brew casks
end
function __fish_brew_suggest_casks_installed -d "Lists installed casks"
brew list --cask -1
end
function __fish_brew_suggest_casks_outdated -d "Lists outdated casks with the information about potential upgrade"
brew outdated --cask --verbose \
# replace first space with tab to make the following a description in the completions list:
| string replace -r '\s' '\t'
end
function __fish_brew_suggest_taps_installed -d "List all available taps"
brew tap
end
function __fish_brew_suggest_commands -d "Lists all commands names, including aliases"
if test -f (brew --cache)/all_commands_list.txt
cat (brew --cache)/all_commands_list.txt | \grep -v instal\$
else
cat (brew --repo)/completions/internal_commands_list.txt | \grep -v instal\$
end
end
function __fish_brew_suggest_diagnostic_checks -d "List available diagnostic checks"
brew doctor --list-checks
end
# TODO: any better way to list available services?
function __fish_brew_suggest_services -d "Lists available services"
set -l list (brew services list)
set -e list[1] # Header
for line in $list
echo (string split ' ' $line)[1]
end
end
##########################
## COMPLETION SHORTCUTS ##
##########################
function __fish_brew_complete_cmd -a cmd -d "A shortcut for defining brew commands completions"
set -e argv[1]
complete -f -c brew -n 'not __fish_brew_command' -a $cmd -d $argv
end
function __fish_brew_complete_arg -a cond -d "A shortcut for defining arguments completion for brew commands"
set -e argv[1]
# NOTE: $cond can be just a name of a command (or several) or additionally any other condition
complete -f -c brew -n "__fish_brew_command $cond" $argv
end
function __fish_brew_complete_sub_cmd -a cmd sub -d "A shortcut for defining brew subcommands completions"
set -e argv[1..2]
if count $argv > /dev/null
__fish_brew_complete_arg "$cmd; and [ (count (__fish_brew_args)) = 1 ]" -a $sub -d $argv
else
__fish_brew_complete_arg "$cmd; and [ (count (__fish_brew_args)) = 1 ]" -a $sub
end
end
function __fish_brew_complete_sub_arg -a cmd sub -d "A shortcut for defining brew subcommand arguments completions"
set -e argv[1..2]
# NOTE: $sub can be just a name of a subcommand (or several) or additionally any other condition
complete -f -c brew -n "__fish_brew_subcommand $cmd $sub" $argv
end
##############
## COMMANDS ##
##############
<%= completion_functions.join("\n\n") %>
################################
## OFFICIAL EXTERNAL COMMANDS ##
################################
# TODO: These commands are installed/tapped separately, so they should be completed only when present
##############
### BUNDLE ###
__fish_brew_complete_cmd 'bundle' "Install or upgrade all dependencies in a Brewfile"
__fish_brew_complete_arg 'bundle; and [ (count (__fish_brew_args)) = 1 ]' -s v -l verbose -d "Print more details"
# --file/--global option is available for bundle command and all its subcommands except exec
__fish_brew_complete_arg 'bundle;
and not __fish_brew_subcommand bundle exec;
and not __fish_brew_opt --file --global
' -l file -r -d "Specify Brewfile"
__fish_brew_complete_arg 'bundle;
and not __fish_brew_subcommand bundle exec;
and not __fish_brew_opt --file --global
' -l global -d "Use \$HOME/.Brewfile"
__fish_brew_complete_sub_cmd 'bundle' 'dump' "Write all installed casks/formulae/taps into a Brewfile"
__fish_brew_complete_sub_cmd 'bundle' 'cleanup' "Uninstall all dependencies not listed in a Brewfile"
__fish_brew_complete_sub_cmd 'bundle' 'check' "Check if all dependencies are installed in a Brewfile"
__fish_brew_complete_sub_cmd 'bundle' 'exec' "Run an external command in an isolated build environment"
# --force is available only for the dump/cleanup subcommands
__fish_brew_complete_sub_arg 'bundle' 'dump cleanup' -l force -d "Uninstall dependencies or overwrite an existing Brewfile"
# --no-upgrade is available for bundle command and its check subcommand
__fish_brew_complete_arg 'bundle; and [ (count (__fish_brew_args)) = 1 ];
or __fish_brew_subcommand bundle check
' -l no-upgrade -d "Don't run brew upgrade for outdated dependencies"
################
### SERVICES ###
__fish_brew_complete_cmd 'services' "Integrates Homebrew formulae with macOS's launchctl manager"
__fish_brew_complete_arg 'services; and [ (count (__fish_brew_args)) = 1 ]' -s v -l verbose -d "Print more details"
__fish_brew_complete_sub_cmd 'services' 'list' "List all running services for the current user"
__fish_brew_complete_sub_cmd 'services' 'run' "Run service without starting at login/boot"
__fish_brew_complete_sub_cmd 'services' 'start' "Start service immediately and register it to launch at login/boot"
__fish_brew_complete_sub_cmd 'services' 'stop' "Stop service immediately and unregister it from launching at login/boot"
__fish_brew_complete_sub_cmd 'services' 'restart' "Stop and start service immediately and register it to launch at login/boot"
__fish_brew_complete_sub_cmd 'services' 'cleanup' "Remove all unused services"
__fish_brew_complete_sub_arg 'services' 'run start stop restart' -l all -d "Run all available services"
__fish_brew_complete_sub_arg 'services' 'run start stop restart' -a '(__fish_brew_suggest_services)'
<%
# To make changes to the completions:
#
# - For changes to a command under `COMMANDS` or `DEVELOPER COMMANDS` sections):
# - Find the source file in `Library/Homebrew/[dev-]cmd/<command>.{rb,sh}`.
# - For `.rb` files, edit the `<command>_args` method.
# - For `.sh` files, edit the top comment, being sure to use the line prefix
# `#:` for the comments to be recognized as documentation. If in doubt,
# compare with already documented commands.
# - For other changes: Edit this file.
#
# When done, regenerate the completions by running `brew generate-man-completions`.
%>
#compdef brew
#autoload
# Brew ZSH completion function
# functions starting with __brew are helper functions that complete or list
# various types of items.
# functions starting with _brew_ are completions for brew commands
# this mechanism can be extended by external commands by defining a function
# named _brew_<external-name>. See _brew_cask for an example of this.
# a list of aliased internal commands
__brew_list_aliases() {
local -a aliases
aliases=(
<%= aliases.join("\n ") + "\n" %>
)
echo "${aliases}"
}
__brew_formulae_or_ruby_files() {
_alternative 'files:files:{_files -g "*.rb"}'
}
# completions remain in cache until any tap has new commits
__brew_completion_caching_policy() {
local -a tmp
# invalidate if cache file is missing or >=2 weeks old
tmp=( $1(mw-2N) )
(( $#tmp )) || return 0
# otherwise, invalidate if latest tap index file is missing or newer than cache file
tmp=( $(brew --repository)/Library/Taps/*/*/.git/index(om[1]N) )
[[ -z $tmp || $tmp -nt $1 ]]
}
__brew_formulae() {
[[ -prefix '-' ]] && return 0
local -a list
local comp_cachename=brew_formulae
if ! _retrieve_cache $comp_cachename; then
list=( $(brew formulae) )
_store_cache $comp_cachename list
fi
_describe -t formulae 'all formulae' list
}
__brew_installed_formulae() {
[[ -prefix '-' ]] && return 0
local -a formulae
formulae=($(brew list --formula))
_describe -t formulae 'installed formulae' formulae
}
__brew_outdated_formulae() {
[[ -prefix '-' ]] && return 0
local -a formulae
formulae=($(brew outdated --formula))
_describe -t formulae 'outdated formulae' formulae
}
__brew_casks() {
[[ -prefix '-' ]] && return 0
local -a list
local expl
local comp_cachename=brew_casks
if ! _retrieve_cache $comp_cachename; then
list=( $(brew casks) )
_store_cache $comp_cachename list
fi
_wanted list expl 'all casks' compadd -a list
}
__brew_installed_casks() {
[[ -prefix '-' ]] && return 0
local -a list
local expl
list=( $(brew list --cask) )
_wanted list expl 'installed casks' compadd -a list
}
__brew_outdated_casks() {
[[ -prefix '-' ]] && return 0
local -a casks
casks=($(brew outdated --cask))
_describe -t casks 'outdated casks' casks
}
__brew_installed_taps() {
[[ -prefix '-' ]] && return 0
local -a taps
taps=($(brew tap))
_describe -t installed-taps 'installed taps' taps
}
__brew_any_tap() {
[[ -prefix '-' ]] && return 0
_alternative \
'installed-taps:installed taps:__brew_installed_taps'
}
__brew_internal_commands() {
local -a commands
commands=(
<%= builtin_command_descriptions.join("\n ") + "\n" %>
)
_describe -t internal-commands 'internal commands' commands
}
__brew_external_commands() {
local -a list
local comp_cachename=brew_all_commands
if ! _retrieve_cache $comp_cachename; then
local cache_dir=$(brew --cache)
[[ -f $cache_dir/external_commands_list.txt ]] &&
list=( $(<$cache_dir/external_commands_list.txt) )
_store_cache $comp_cachename list
fi
_describe -t all-commands 'all commands' list
}
__brew_commands() {
_alternative \
'internal-commands:command:__brew_internal_commands' \
'external-commands:command:__brew_external_commands'
}
__brew_diagnostic_checks() {
local -a diagnostic_checks
diagnostic_checks=($(brew doctor --list-checks))
_describe -t diagnostic-checks 'diagnostic checks' diagnostic_checks
}
<%= completion_functions.join("\n") %>
# The main completion function
_brew() {
local curcontext="$curcontext" state state_descr line expl
local tmp ret=1
_arguments -C : \
'(-v)-v[verbose]' \
'1:command:->command' \
'*::options:->options' && return 0
case "$state" in
command)
# set default cache policy
zstyle -s ":completion:${curcontext%:*}:*" cache-policy tmp ||
zstyle ":completion:${curcontext%:*}:*" cache-policy __brew_completion_caching_policy
zstyle -s ":completion:${curcontext%:*}:*" use-cache tmp ||
zstyle ":completion:${curcontext%:*}:*" use-cache true
__brew_commands && return 0
;;
options)
local command_or_alias command
local -A aliases
# expand alias e.g. ls -> list
command_or_alias="${line[1]}"
aliases=($(__brew_list_aliases))
command="${aliases[$command_or_alias]:-$command_or_alias}"
# change context to e.g. brew-list
curcontext="${curcontext%:*}-${command}:${curcontext##*:}"
# set default cache policy (we repeat this dance because the context
# service differs from above)
zstyle -s ":completion:${curcontext%:*}:*" cache-policy tmp ||
zstyle ":completion:${curcontext%:*}:*" cache-policy __brew_completion_caching_policy
zstyle -s ":completion:${curcontext%:*}:*" use-cache tmp ||
zstyle ":completion:${curcontext%:*}:*" use-cache true
# call completion for named command e.g. _brew_list
local completion_func="_brew_${command//-/_}"
_call_function ret "${completion_func}" && return ret
_message "a completion function is not defined for command or alias: ${command_or_alias}"
return 1
;;
esac
}
_brew "$@"
# typed: strict
module EnvVar
sig { params(env: String).returns(String) }
def self.[](env); end
end
# typed: true
# frozen_string_literal: true
require "monitor"
# Module for querying the current execution context.
#
# @api private
module Context
extend MonitorMixin
def self.current=(context)
synchronize do
@current = context
end
end
def self.current
if (current_context = Thread.current[:context])
return current_context
end
synchronize do
@current ||= ContextStruct.new
end
end
# Struct describing the current execution context.
class ContextStruct
def initialize(debug: nil, quiet: nil, verbose: nil)
@debug = debug
@quiet = quiet
@verbose = verbose
end
def debug?
@debug == true
end
def quiet?
@quiet == true
end
def verbose?
@verbose == true
end
end
def debug?
Context.current.debug?
end
def quiet?
Context.current.quiet?
end
def verbose?
Context.current.verbose?
end
def with_context(**options)
old_context = Thread.current[:context]
new_context = ContextStruct.new(
debug: options.fetch(:debug, old_context&.debug?),
quiet: options.fetch(:quiet, old_context&.quiet?),
verbose: options.fetch(:verbose, old_context&.verbose?),
)
Thread.current[:context] = new_context
yield
ensure
Thread.current[:context] = old_context
end
end
# typed: true
# frozen_string_literal: true
require "compilers"
# Combination of C++ standard library and compiler.
class CxxStdlib
extend T::Sig
def self.create(type, compiler)
raise ArgumentError, "Invalid C++ stdlib type: #{type}" if type && [:libstdcxx, :libcxx].exclude?(type)
CxxStdlib.new(type, compiler)
end
attr_reader :type, :compiler
def initialize(type, compiler)
@type = type
@compiler = compiler.to_sym
end
def type_string
type.to_s.gsub(/cxx$/, "c++")
end
sig { returns(String) }
def inspect
"#<#{self.class.name}: #{compiler} #{type}>"
end
end
{
"licenseListVersion": "3.19",
"exceptions": [
{
"reference": "./389-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./389-exception.html",
"referenceNumber": 37,
"name": "389 Directory Server Exception",
"licenseExceptionId": "389-exception",
"seeAlso": [
"http://directory.fedoraproject.org/wiki/GPL_Exception_License_Text",
"https://web.archive.org/web/20080828121337/http://directory.fedoraproject.org/wiki/GPL_Exception_License_Text"
]
},
{
"reference": "./Autoconf-exception-2.0.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./Autoconf-exception-2.0.html",
"referenceNumber": 13,
"name": "Autoconf exception 2.0",
"licenseExceptionId": "Autoconf-exception-2.0",
"seeAlso": [
"http://ac-archive.sourceforge.net/doc/copyright.html",
"http://ftp.gnu.org/gnu/autoconf/autoconf-2.59.tar.gz"
]
},
{
"reference": "./Autoconf-exception-3.0.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./Autoconf-exception-3.0.html",
"referenceNumber": 32,
"name": "Autoconf exception 3.0",
"licenseExceptionId": "Autoconf-exception-3.0",
"seeAlso": [
"http://www.gnu.org/licenses/autoconf-exception-3.0.html"
]
},
{
"reference": "./Bison-exception-2.2.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./Bison-exception-2.2.html",
"referenceNumber": 17,
"name": "Bison exception 2.2",
"licenseExceptionId": "Bison-exception-2.2",
"seeAlso": [
"http://git.savannah.gnu.org/cgit/bison.git/tree/data/yacc.c?id\u003d193d7c7054ba7197b0789e14965b739162319b5e#n141"
]
},
{
"reference": "./Bootloader-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./Bootloader-exception.html",
"referenceNumber": 21,
"name": "Bootloader Distribution Exception",
"licenseExceptionId": "Bootloader-exception",
"seeAlso": [
"https://github.com/pyinstaller/pyinstaller/blob/develop/COPYING.txt"
]
},
{
"reference": "./Classpath-exception-2.0.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./Classpath-exception-2.0.html",
"referenceNumber": 29,
"name": "Classpath exception 2.0",
"licenseExceptionId": "Classpath-exception-2.0",
"seeAlso": [
"http://www.gnu.org/software/classpath/license.html",
"https://fedoraproject.org/wiki/Licensing/GPL_Classpath_Exception"
]
},
{
"reference": "./CLISP-exception-2.0.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./CLISP-exception-2.0.html",
"referenceNumber": 6,
"name": "CLISP exception 2.0",
"licenseExceptionId": "CLISP-exception-2.0",
"seeAlso": [
"http://sourceforge.net/p/clisp/clisp/ci/default/tree/COPYRIGHT"
]
},
{
"reference": "./DigiRule-FOSS-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./DigiRule-FOSS-exception.html",
"referenceNumber": 26,
"name": "DigiRule FOSS License Exception",
"licenseExceptionId": "DigiRule-FOSS-exception",
"seeAlso": [
"http://www.digirulesolutions.com/drupal/foss"
]
},
{
"reference": "./eCos-exception-2.0.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./eCos-exception-2.0.html",
"referenceNumber": 11,
"name": "eCos exception 2.0",
"licenseExceptionId": "eCos-exception-2.0",
"seeAlso": [
"http://ecos.sourceware.org/license-overview.html"
]
},
{
"reference": "./Fawkes-Runtime-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./Fawkes-Runtime-exception.html",
"referenceNumber": 2,
"name": "Fawkes Runtime Exception",
"licenseExceptionId": "Fawkes-Runtime-exception",
"seeAlso": [
"http://www.fawkesrobotics.org/about/license/"
]
},
{
"reference": "./FLTK-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./FLTK-exception.html",
"referenceNumber": 34,
"name": "FLTK exception",
"licenseExceptionId": "FLTK-exception",
"seeAlso": [
"http://www.fltk.org/COPYING.php"
]
},
{
"reference": "./Font-exception-2.0.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./Font-exception-2.0.html",
"referenceNumber": 14,
"name": "Font exception 2.0",
"licenseExceptionId": "Font-exception-2.0",
"seeAlso": [
"http://www.gnu.org/licenses/gpl-faq.html#FontException"
]
},
{
"reference": "./freertos-exception-2.0.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./freertos-exception-2.0.html",
"referenceNumber": 45,
"name": "FreeRTOS Exception 2.0",
"licenseExceptionId": "freertos-exception-2.0",
"seeAlso": [
"https://web.archive.org/web/20060809182744/http://www.freertos.org/a00114.html"
]
},
{
"reference": "./GCC-exception-2.0.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./GCC-exception-2.0.html",
"referenceNumber": 28,
"name": "GCC Runtime Library exception 2.0",
"licenseExceptionId": "GCC-exception-2.0",
"seeAlso": [
"https://gcc.gnu.org/git/?p\u003dgcc.git;a\u003dblob;f\u003dgcc/libgcc1.c;h\u003d762f5143fc6eed57b6797c82710f3538aa52b40b;hb\u003dcb143a3ce4fb417c68f5fa2691a1b1b1053dfba9#l10"
]
},
{
"reference": "./GCC-exception-3.1.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./GCC-exception-3.1.html",
"referenceNumber": 3,
"name": "GCC Runtime Library exception 3.1",
"licenseExceptionId": "GCC-exception-3.1",
"seeAlso": [
"http://www.gnu.org/licenses/gcc-exception-3.1.html"
]
},
{
"reference": "./gnu-javamail-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./gnu-javamail-exception.html",
"referenceNumber": 19,
"name": "GNU JavaMail exception",
"licenseExceptionId": "gnu-javamail-exception",
"seeAlso": [
"http://www.gnu.org/software/classpathx/javamail/javamail.html"
]
},
{
"reference": "./GPL-3.0-linking-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./GPL-3.0-linking-exception.html",
"referenceNumber": 36,
"name": "GPL-3.0 Linking Exception",
"licenseExceptionId": "GPL-3.0-linking-exception",
"seeAlso": [
"https://www.gnu.org/licenses/gpl-faq.en.html#GPLIncompatibleLibs"
]
},
{
"reference": "./GPL-3.0-linking-source-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./GPL-3.0-linking-source-exception.html",
"referenceNumber": 25,
"name": "GPL-3.0 Linking Exception (with Corresponding Source)",
"licenseExceptionId": "GPL-3.0-linking-source-exception",
"seeAlso": [
"https://www.gnu.org/licenses/gpl-faq.en.html#GPLIncompatibleLibs",
"https://github.com/mirror/wget/blob/master/src/http.c#L20"
]
},
{
"reference": "./GPL-CC-1.0.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./GPL-CC-1.0.html",
"referenceNumber": 38,
"name": "GPL Cooperation Commitment 1.0",
"licenseExceptionId": "GPL-CC-1.0",
"seeAlso": [
"https://github.com/gplcc/gplcc/blob/master/Project/COMMITMENT",
"https://gplcc.github.io/gplcc/Project/README-PROJECT.html"
]
},
{
"reference": "./GStreamer-exception-2005.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./GStreamer-exception-2005.html",
"referenceNumber": 27,
"name": "GStreamer Exception (2005)",
"licenseExceptionId": "GStreamer-exception-2005",
"seeAlso": [
"https://gstreamer.freedesktop.org/documentation/frequently-asked-questions/licensing.html?gi-language\u003dc#licensing-of-applications-using-gstreamer"
]
},
{
"reference": "./GStreamer-exception-2008.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./GStreamer-exception-2008.html",
"referenceNumber": 16,
"name": "GStreamer Exception (2008)",
"licenseExceptionId": "GStreamer-exception-2008",
"seeAlso": [
"https://gstreamer.freedesktop.org/documentation/frequently-asked-questions/licensing.html?gi-language\u003dc#licensing-of-applications-using-gstreamer"
]
},
{
"reference": "./i2p-gpl-java-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./i2p-gpl-java-exception.html",
"referenceNumber": 18,
"name": "i2p GPL+Java Exception",
"licenseExceptionId": "i2p-gpl-java-exception",
"seeAlso": [
"http://geti2p.net/en/get-involved/develop/licenses#java_exception"
]
},
{
"reference": "./KiCad-libraries-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./KiCad-libraries-exception.html",
"referenceNumber": 39,
"name": "KiCad Libraries Exception",
"licenseExceptionId": "KiCad-libraries-exception",
"seeAlso": [
"https://www.kicad.org/libraries/license/"
]
},
{
"reference": "./LGPL-3.0-linking-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./LGPL-3.0-linking-exception.html",
"referenceNumber": 4,
"name": "LGPL-3.0 Linking Exception",
"licenseExceptionId": "LGPL-3.0-linking-exception",
"seeAlso": [
"https://raw.githubusercontent.com/go-xmlpath/xmlpath/v2/LICENSE",
"https://github.com/goamz/goamz/blob/master/LICENSE",
"https://github.com/juju/errors/blob/master/LICENSE"
]
},
{
"reference": "./Libtool-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./Libtool-exception.html",
"referenceNumber": 7,
"name": "Libtool Exception",
"licenseExceptionId": "Libtool-exception",
"seeAlso": [
"http://git.savannah.gnu.org/cgit/libtool.git/tree/m4/libtool.m4"
]
},
{
"reference": "./Linux-syscall-note.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./Linux-syscall-note.html",
"referenceNumber": 42,
"name": "Linux Syscall Note",
"licenseExceptionId": "Linux-syscall-note",
"seeAlso": [
"https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/COPYING"
]
},
{
"reference": "./LLVM-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./LLVM-exception.html",
"referenceNumber": 41,
"name": "LLVM Exception",
"licenseExceptionId": "LLVM-exception",
"seeAlso": [
"http://llvm.org/foundation/relicensing/LICENSE.txt"
]
},
{
"reference": "./LZMA-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./LZMA-exception.html",
"referenceNumber": 31,
"name": "LZMA exception",
"licenseExceptionId": "LZMA-exception",
"seeAlso": [
"http://nsis.sourceforge.net/Docs/AppendixI.html#I.6"
]
},
{
"reference": "./mif-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./mif-exception.html",
"referenceNumber": 1,
"name": "Macros and Inline Functions Exception",
"licenseExceptionId": "mif-exception",
"seeAlso": [
"http://www.scs.stanford.edu/histar/src/lib/cppsup/exception",
"http://dev.bertos.org/doxygen/",
"https://www.threadingbuildingblocks.org/licensing"
]
},
{
"reference": "./Nokia-Qt-exception-1.1.json",
"isDeprecatedLicenseId": true,
"detailsUrl": "./Nokia-Qt-exception-1.1.html",
"referenceNumber": 8,
"name": "Nokia Qt LGPL exception 1.1",
"licenseExceptionId": "Nokia-Qt-exception-1.1",
"seeAlso": [
"https://www.keepassx.org/dev/projects/keepassx/repository/revisions/b8dfb9cc4d5133e0f09cd7533d15a4f1c19a40f2/entry/LICENSE.NOKIA-LGPL-EXCEPTION"
]
},
{
"reference": "./OCaml-LGPL-linking-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./OCaml-LGPL-linking-exception.html",
"referenceNumber": 10,
"name": "OCaml LGPL Linking Exception",
"licenseExceptionId": "OCaml-LGPL-linking-exception",
"seeAlso": [
"https://caml.inria.fr/ocaml/license.en.html"
]
},
{
"reference": "./OCCT-exception-1.0.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./OCCT-exception-1.0.html",
"referenceNumber": 20,
"name": "Open CASCADE Exception 1.0",
"licenseExceptionId": "OCCT-exception-1.0",
"seeAlso": [
"http://www.opencascade.com/content/licensing"
]
},
{
"reference": "./OpenJDK-assembly-exception-1.0.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./OpenJDK-assembly-exception-1.0.html",
"referenceNumber": 30,
"name": "OpenJDK Assembly exception 1.0",
"licenseExceptionId": "OpenJDK-assembly-exception-1.0",
"seeAlso": [
"http://openjdk.java.net/legal/assembly-exception.html"
]
},
{
"reference": "./openvpn-openssl-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./openvpn-openssl-exception.html",
"referenceNumber": 44,
"name": "OpenVPN OpenSSL Exception",
"licenseExceptionId": "openvpn-openssl-exception",
"seeAlso": [
"http://openvpn.net/index.php/license.html"
]
},
{
"reference": "./PS-or-PDF-font-exception-20170817.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./PS-or-PDF-font-exception-20170817.html",
"referenceNumber": 35,
"name": "PS/PDF font exception (2017-08-17)",
"licenseExceptionId": "PS-or-PDF-font-exception-20170817",
"seeAlso": [
"https://github.com/ArtifexSoftware/urw-base35-fonts/blob/65962e27febc3883a17e651cdb23e783668c996f/LICENSE"
]
},
{
"reference": "./Qt-GPL-exception-1.0.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./Qt-GPL-exception-1.0.html",
"referenceNumber": 24,
"name": "Qt GPL exception 1.0",
"licenseExceptionId": "Qt-GPL-exception-1.0",
"seeAlso": [
"http://code.qt.io/cgit/qt/qtbase.git/tree/LICENSE.GPL3-EXCEPT"
]
},
{
"reference": "./Qt-LGPL-exception-1.1.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./Qt-LGPL-exception-1.1.html",
"referenceNumber": 5,
"name": "Qt LGPL exception 1.1",
"licenseExceptionId": "Qt-LGPL-exception-1.1",
"seeAlso": [
"http://code.qt.io/cgit/qt/qtbase.git/tree/LGPL_EXCEPTION.txt"
]
},
{
"reference": "./Qwt-exception-1.0.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./Qwt-exception-1.0.html",
"referenceNumber": 9,
"name": "Qwt exception 1.0",
"licenseExceptionId": "Qwt-exception-1.0",
"seeAlso": [
"http://qwt.sourceforge.net/qwtlicense.html"
]
},
{
"reference": "./SHL-2.0.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./SHL-2.0.html",
"referenceNumber": 33,
"name": "Solderpad Hardware License v2.0",
"licenseExceptionId": "SHL-2.0",
"seeAlso": [
"https://solderpad.org/licenses/SHL-2.0/"
]
},
{
"reference": "./SHL-2.1.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./SHL-2.1.html",
"referenceNumber": 43,
"name": "Solderpad Hardware License v2.1",
"licenseExceptionId": "SHL-2.1",
"seeAlso": [
"https://solderpad.org/licenses/SHL-2.1/"
]
},
{
"reference": "./Swift-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./Swift-exception.html",
"referenceNumber": 12,
"name": "Swift Exception",
"licenseExceptionId": "Swift-exception",
"seeAlso": [
"https://swift.org/LICENSE.txt",
"https://github.com/apple/swift-package-manager/blob/7ab2275f447a5eb37497ed63a9340f8a6d1e488b/LICENSE.txt#L205"
]
},
{
"reference": "./u-boot-exception-2.0.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./u-boot-exception-2.0.html",
"referenceNumber": 15,
"name": "U-Boot exception 2.0",
"licenseExceptionId": "u-boot-exception-2.0",
"seeAlso": [
"http://git.denx.de/?p\u003du-boot.git;a\u003dblob;f\u003dLicenses/Exceptions"
]
},
{
"reference": "./Universal-FOSS-exception-1.0.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./Universal-FOSS-exception-1.0.html",
"referenceNumber": 22,
"name": "Universal FOSS Exception, Version 1.0",
"licenseExceptionId": "Universal-FOSS-exception-1.0",
"seeAlso": [
"https://oss.oracle.com/licenses/universal-foss-exception/"
]
},
{
"reference": "./WxWindows-exception-3.1.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./WxWindows-exception-3.1.html",
"referenceNumber": 40,
"name": "WxWindows Library Exception 3.1",
"licenseExceptionId": "WxWindows-exception-3.1",
"seeAlso": [
"http://www.opensource.org/licenses/WXwindows"
]
},
{
"reference": "./x11vnc-openssl-exception.json",
"isDeprecatedLicenseId": false,
"detailsUrl": "./x11vnc-openssl-exception.html",
"referenceNumber": 23,
"name": "x11vnc OpenSSL Exception",
"licenseExceptionId": "x11vnc-openssl-exception",
"seeAlso": [
"https://github.com/LibVNC/x11vnc/blob/master/src/8to24.c#L22"
]
}
],
"releaseDate": "2022-11-30"
}
# frozen_string_literal: true
source "https://rubygems.org"
if ENV.fetch("HOMEBREW_DEVELOPER", "").empty? || ENV.fetch("HOMEBREW_USE_RUBY_FROM_PATH", "").empty?
ruby "~> 2.6.0"
else
ruby ">= 2.6.0"
end
# disallowed gems (should not be used)
# * nokogiri - use rexml instead for XML parsing
# installed gems (should all be require: false)
gem "bootsnap", require: false
gem "byebug", require: false
gem "json_schemer", require: false
gem "method_source", require: false
gem "minitest", require: false
gem "parallel_tests", require: false
gem "ronn", require: false
gem "rspec", require: false
gem "rspec-github", require: false
gem "rspec-its", require: false
gem "rspec_junit_formatter", require: false
gem "rspec-retry", require: false
gem "rspec-sorbet", require: false
gem "rspec-wait", require: false
gem "rubocop", require: false
gem "rubocop-ast", require: false
gem "simplecov", require: false
gem "simplecov-cobertura", require: false
gem "warning", require: false
group :sorbet, optional: true do
gem "parlour", require: false
gem "sorbet-static-and-runtime", require: false
gem "spoom", require: false
gem "tapioca", require: false
end
# vendored gems
gem "activesupport"
gem "addressable"
gem "concurrent-ruby"
gem "mechanize"
gem "patchelf"
gem "plist"
gem "rubocop-performance"
gem "rubocop-rails"
gem "rubocop-rspec"
gem "rubocop-sorbet"
gem "ruby-macho"
gem "sorbet-runtime"
# remove when HOMEBREW_REQUIRED_RUBY_VERSION >= 2.7
install_if -> { RUBY_VERSION < "2.7" } do
gem "did_you_mean"
end
GEM
remote: https://rubygems.org/
specs:
activesupport (6.1.7.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
bindata (2.4.14)
bootsnap (1.15.0)
msgpack (~> 1.2)
byebug (11.1.3)
coderay (1.1.3)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.1.10)
connection_pool (2.3.0)
did_you_mean (1.6.3)
diff-lcs (1.5.0)
docile (1.4.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
ecma-re-validator (0.4.0)
regexp_parser (~> 2.2)
elftools (1.2.0)
bindata (~> 2)
hana (1.3.7)
highline (2.0.3)
hpricot (0.8.6)
http-cookie (1.0.5)
domain_name (~> 0.5)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
json (2.6.3)
json_schemer (0.2.24)
ecma-re-validator (~> 0.3)
hana (~> 1.3)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
mechanize (2.8.5)
addressable (~> 2.8)
domain_name (~> 0.5, >= 0.5.20190701)
http-cookie (~> 1.0, >= 1.0.3)
mime-types (~> 3.0)
net-http-digest_auth (~> 1.4, >= 1.4.1)
net-http-persistent (>= 2.5.2, < 5.0.dev)
nokogiri (~> 1.11, >= 1.11.2)
rubyntlm (~> 0.6, >= 0.6.3)
webrick (~> 1.7)
webrobots (~> 0.1.2)
method_source (1.0.0)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2022.0105)
mini_portile2 (2.8.1)
minitest (5.17.0)
msgpack (1.6.0)
mustache (1.1.1)
net-http-digest_auth (1.4.1)
net-http-persistent (4.0.1)
connection_pool (~> 2.2)
nokogiri (1.13.10)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nokogiri (1.13.10-aarch64-linux)
racc (~> 1.4)
nokogiri (1.13.10-arm64-darwin)
racc (~> 1.4)
nokogiri (1.13.10-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.13.10-x86_64-linux)
racc (~> 1.4)
parallel (1.22.1)
parallel_tests (3.13.0)
parallel
parlour (8.1.0)
commander (~> 4.5)
parser
rainbow (~> 3.0)
sorbet-runtime (>= 0.5)
parser (3.2.0.0)
ast (~> 2.4.1)
patchelf (1.4.0)
elftools (>= 1.2)
plist (3.6.0)
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
public_suffix (5.0.1)
racc (1.6.2)
rack (3.0.4.1)
rainbow (3.1.1)
rbi (0.0.14)
ast
parser (>= 2.6.4.0)
sorbet-runtime (>= 0.5.9204)
unparser
rdiscount (2.2.7)
regexp_parser (2.6.1)
rexml (3.2.5)
ronn (0.7.3)
hpricot (>= 0.8.2)
mustache (>= 0.7.0)
rdiscount (>= 1.5.8)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
rspec-mocks (~> 3.12.0)
rspec-core (3.12.0)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-github (2.4.0)
rspec-core (~> 3.0)
rspec-its (1.3.0)
rspec-core (>= 3.0.0)
rspec-expectations (>= 3.0.0)
rspec-mocks (3.12.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-retry (0.6.2)
rspec-core (> 3.3)
rspec-sorbet (1.9.1)
sorbet-runtime
rspec-support (3.12.0)
rspec-wait (0.0.9)
rspec (>= 3, < 4)
rspec_junit_formatter (0.6.0)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.43.0)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.24.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.24.1)
parser (>= 3.1.1.0)
rubocop-capybara (2.17.0)
rubocop (~> 1.41)
rubocop-performance (1.15.2)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.17.4)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-rspec (2.18.1)
rubocop (~> 1.33)
rubocop-capybara (~> 2.17)
rubocop-sorbet (0.6.11)
rubocop (>= 0.90.0)
ruby-macho (3.0.0)
ruby-progressbar (1.11.0)
rubyntlm (0.6.3)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-cobertura (2.1.0)
rexml
simplecov (~> 0.19)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
sorbet (0.5.10461)
sorbet-static (= 0.5.10461)
sorbet-runtime (0.5.10461)
sorbet-static (0.5.10461-universal-darwin-14)
sorbet-static (0.5.10461-universal-darwin-15)
sorbet-static (0.5.10461-universal-darwin-16)
sorbet-static (0.5.10461-universal-darwin-17)
sorbet-static (0.5.10461-universal-darwin-18)
sorbet-static (0.5.10461-universal-darwin-19)
sorbet-static (0.5.10461-universal-darwin-20)
sorbet-static (0.5.10461-universal-darwin-21)
sorbet-static (0.5.10461-universal-darwin-22)
sorbet-static (0.5.10461-x86_64-linux)
sorbet-static-and-runtime (0.5.10461)
sorbet (= 0.5.10461)
sorbet-runtime (= 0.5.10461)
spoom (1.1.11)
sorbet (>= 0.5.9204)
sorbet-runtime (>= 0.5.9204)
thor (>= 0.19.2)
tapioca (0.7.3)
bundler (>= 1.17.3)
pry (>= 0.12.2)
rbi (~> 0.0.0, >= 0.0.14)
sorbet-runtime (>= 0.5.9204)
sorbet-static (>= 0.5.9204)
spoom (~> 1.1.0, >= 1.1.11)
thor (>= 1.2.0)
yard-sorbet
thor (1.2.1)
tzinfo (2.0.5)
concurrent-ruby (~> 1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (2.4.2)
unparser (0.6.4)
diff-lcs (~> 1.3)
parser (>= 3.1.0)
uri_template (0.7.0)
warning (1.3.0)
webrick (1.7.0)
webrobots (0.1.2)
yard (0.9.28)
webrick (~> 1.7.0)
yard-sorbet (0.6.1)
sorbet-runtime (>= 0.5)
yard (>= 0.9)
zeitwerk (2.6.6)
PLATFORMS
aarch64-linux
arm-linux
arm64-darwin
x86_64-darwin
x86_64-linux
DEPENDENCIES
activesupport
addressable
bootsnap
byebug
concurrent-ruby
did_you_mean
json_schemer
mechanize
method_source
minitest
parallel_tests
parlour
patchelf
plist
ronn
rspec
rspec-github
rspec-its
rspec-retry
rspec-sorbet
rspec-wait
rspec_junit_formatter
rubocop
rubocop-ast
rubocop-performance
rubocop-rails
rubocop-rspec
rubocop-sorbet
ruby-macho
simplecov
simplecov-cobertura
sorbet-runtime
sorbet-static-and-runtime
spoom
tapioca
warning
RUBY VERSION
ruby 2.6.8p205
BUNDLED WITH
2.3.26
# typed: true
# frozen_string_literal: true
# Representation of a `*PATH` environment variable.
#
# @api private
class PATH
extend T::Sig
include Enumerable
extend Forwardable
delegate each: :@paths
# FIXME: Enable cop again when https://github.com/sorbet/sorbet/issues/3532 is fixed.
# rubocop:disable Style/MutableConstant
Element = T.type_alias { T.nilable(T.any(Pathname, String, PATH)) }
private_constant :Element
Elements = T.type_alias { T.any(Element, T::Array[Element]) }
private_constant :Elements
# rubocop:enable Style/MutableConstant
sig { params(paths: Elements).void }
def initialize(*paths)
@paths = parse(paths)
end
sig { params(paths: Elements).returns(T.self_type) }
def prepend(*paths)
@paths = parse(paths + @paths)
self
end
sig { params(paths: Elements).returns(T.self_type) }
def append(*paths)
@paths = parse(@paths + paths)
self
end
sig { params(index: Integer, paths: Elements).returns(T.self_type) }
def insert(index, *paths)
@paths = parse(@paths.insert(index, *paths))
self
end
sig { params(block: T.proc.params(arg0: String).returns(T::Boolean)).returns(T.self_type) }
def select(&block)
self.class.new(@paths.select(&block))
end
sig { params(block: T.proc.params(arg0: String).returns(T::Boolean)).returns(T.self_type) }
def reject(&block)
self.class.new(@paths.reject(&block))
end
sig { returns(T::Array[String]) }
def to_ary
@paths.dup.to_ary
end
alias to_a to_ary
sig { returns(String) }
def to_str
@paths.join(File::PATH_SEPARATOR)
end
alias to_s to_str
sig { params(other: T.untyped).returns(T::Boolean) }
def ==(other)
(other.respond_to?(:to_ary) && to_ary == other.to_ary) ||
(other.respond_to?(:to_str) && to_str == other.to_str) ||
false
end
sig { returns(T::Boolean) }
def empty?
@paths.empty?
end
sig { returns(T.nilable(T.self_type)) }
def existing
existing_path = select(&File.method(:directory?))
# return nil instead of empty PATH, to unset environment variables
existing_path unless existing_path.empty?
end
private
sig { params(paths: T::Array[Elements]).returns(T::Array[String]) }
def parse(paths)
paths.flatten
.compact
.flat_map { |p| Pathname(p).to_path.split(File::PATH_SEPARATOR) }
.uniq
end
end

Homebrew Ruby API

This is the API for Homebrew.

The main class you should look at is the {Formula} class (and classes linked from there). That's the class that's used to create Homebrew formulae (i.e. package descriptions). Assume anything else you stumble upon is private.

You may also find the Formula Cookbook and Ruby Style Guide helpful in creating formulae.

Good luck!

BSD 2-Clause License
Copyright (c) 2009-present, Homebrew contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
__all__ = [
'UPPER', 'LOWER', "CONSTANT_SOUNDS"
]
class UPPER:
"""
Upper text register for press() method
"""
UPPER = 'upper'
class LOWER:
"""
Lower text register for press() method
"""
LOWER = 'lower'
class CONSTANT_SOUNDS:
class Submarinesound:
name = 'Submarine'
class Popsound:
name = 'Pop'
class Funksound:
name = 'Funk'
class Glasssound:
name = 'Glass'
class Pingsound:
name = 'Ping'
class Blowsound:
name = 'Blow'
MIT License
Copyright (c) 2022 Alexandr Bosov Vladimirovich.
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.
View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment