Homebrew install script does not respect HOMEBREW_CACHE variable. Using an empty home variable ensures we get the result we want, but it is hacky.
362 lines
12 KiB
Bash
Executable File
362 lines
12 KiB
Bash
Executable File
#!/usr/bin/env zsh
|
|
# vi: set ft=zsh tw=80 ts=2
|
|
|
|
export HOMEBREW_NO_ANALYTICS_THIS_RUN=1
|
|
export HOMEBREW_NO_ANALYTICS_MESSAGE_OUTPUT=1
|
|
|
|
function doesUserExist() {
|
|
local username=$1
|
|
dscl . -list /Users | grep "^${username}$" 2> /dev/null >&2
|
|
}
|
|
|
|
function runAsUser() {
|
|
local username=$1
|
|
shift
|
|
sudo -Hu "${username}" "${@}"
|
|
}
|
|
|
|
function runAsHomebrewUser() {
|
|
runAsUser ${homebrew_username} "$@"
|
|
}
|
|
|
|
function ensureUserIsInAdminGroup() {
|
|
local username=$1
|
|
dseditgroup -o edit -a "${username}" -t user admin
|
|
}
|
|
|
|
function ensureUserCanRunPasswordlessSudo() {
|
|
local username=$1
|
|
local sudoersFile="/etc/sudoers.d/no-auth-sudo-for-${username}"
|
|
[[ -f ${sudoersFile} ]] && return
|
|
cat <<- SUDOERS > "${sudoersFile}"
|
|
Defaults:${username} !authenticate
|
|
SUDOERS
|
|
chown root:wheel "${sudoersFile}" || return 10
|
|
chmod u=rw,g=r,o= "${sudoersFile}" || return 20
|
|
}
|
|
|
|
function ensureUserCanNoLongerRunPasswordlessSudo() {
|
|
local username=$1
|
|
local sudoersFile="/etc/sudoers.d/no-auth-sudo-for-${username}"
|
|
[[ ! -f ${sudoersFile} ]] || rm ${sudoersFile}
|
|
}
|
|
|
|
function getFirstFreeRoleAccountID() {
|
|
local minUserID=450
|
|
local maxUserID=499
|
|
local uname_machine=$(/usr/bin/uname -m)
|
|
if [[ ${uname_machine} != "arm64" ]]; then
|
|
minUserID=200
|
|
maxUserID=400
|
|
fi
|
|
dscl . -list '/Users' UniqueID | grep '_.*' | sort -n -k2 | awk -v i=${minUserID} '$2>='${minUserID}' && $2<'${maxUserID}' {if(i < $2) { print i; nextfile} else i=$2+1;} END {if(i <= '${maxUserID}' && ($2 < '${minUserID}' || $2 > '${maxUserID}')) print i;}'
|
|
}
|
|
|
|
function createHomebrewUser() {
|
|
local username=$1
|
|
local userID=`getFirstFreeRoleAccountID`
|
|
[[ -n $userID ]] || return 10
|
|
sysadminctl -addUser "${username}" -fullName "Homebrew User" -shell /usr/bin/false -home '/var/empty' -roleAccount -UID "${userID}" > /dev/null 2>&1
|
|
}
|
|
|
|
function createHomebrewUserIfNeccessary() {
|
|
if ! doesUserExist ${homebrew_username}; then
|
|
lop -y body:warn -y body -- -i "No Homebrew user named ${homebrew_username} found." -i 'Will create user.'
|
|
indicateActivity 'Creating Homebrew user' createHomebrewUser ${homebrew_username} || return 10
|
|
else
|
|
lop -y body:note -y body -- -i "Homebrew user named ${homebrew_username} already exists." -i 'Skipping.'
|
|
fi
|
|
}
|
|
|
|
function ensureDirectoryWithDefaultMod() {
|
|
local itemPath=${1}
|
|
mkdir -p ${itemPath}
|
|
ensureHomebrewOwnershipAndPermission ${itemPath}
|
|
}
|
|
|
|
function ensureHomebrewOwnershipAndPermission() {
|
|
local itemPath=${1}
|
|
local username=${homebrew_username}
|
|
[[ -f ${itemPath} || -d ${itemPath} ]] || return 1
|
|
chown -R "${username}:admin" ${itemPath}
|
|
chmod u=rwx,go=rx ${itemPath}
|
|
}
|
|
|
|
function ensureHomebrewCacheDirectory() {
|
|
ensureDirectoryWithDefaultMod "${homebrew_cache}"
|
|
runAsHomebrewUser touch "${homebrew_cache}/.cleaned"
|
|
}
|
|
|
|
function ensureHomebrewLogDirectory() {
|
|
ensureDirectoryWithDefaultMod ${homebrew_log}
|
|
}
|
|
|
|
function getHomebrewRepositoryPath() {
|
|
local uname_machine=$(/usr/bin/uname -m)
|
|
if [[ ${uname_machine} == "arm64" ]]; then
|
|
print -- "/opt/homebrew"
|
|
else
|
|
print "/usr/local/Homebrew"
|
|
fi
|
|
}
|
|
|
|
function createBrewCallerScript() {
|
|
ensureLocalBinFolder
|
|
local uname_machine=$(/usr/bin/uname -m)
|
|
local username=${homebrew_username}
|
|
local homebrewRepositoryPath="$(getHomebrewRepositoryPath)"
|
|
local brewCallerPath="${homebrewRepositoryPath}/bin/brew-caller"
|
|
local brewCallerSymlink="/usr/local/bin/brew"
|
|
[[ -f "${brewCallerPath}" ]] && rm "${brewCallerPath}"
|
|
[[ -f "${brewCallerSymlink}" || -h "${brewCallerSymlink}" ]] && rm "${brewCallerSymlink}"
|
|
[[ ${uname_machine} == "arm64" ]] && brewCallerPath=${brewCallerSymlink}
|
|
cat <<- BREWCALLER | clang -x c -O2 -Wall -o "${brewCallerPath}" -
|
|
#include <unistd.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <pwd.h>
|
|
#include <grp.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
|
|
extern char **environ;
|
|
|
|
char *format_env(const char *env_name, const char *format) {
|
|
if (!env_name || !format) return NULL;
|
|
const char *value = getenv(env_name);
|
|
if (!value) return NULL;
|
|
int formatted_len = snprintf(NULL, 0, format, value);
|
|
if (formatted_len < 0) return NULL;
|
|
char *formatted_value = malloc(formatted_len + 1);
|
|
if (!formatted_value) return NULL;
|
|
snprintf(formatted_value, formatted_len + 1, format, value);
|
|
size_t result_len = strlen(env_name) + 1 + formatted_len;
|
|
char *result = malloc(result_len + 1);
|
|
if (!result) { free(formatted_value); return NULL; }
|
|
sprintf(result, "%s=%s", env_name, formatted_value);
|
|
free(formatted_value);
|
|
return result;
|
|
}
|
|
|
|
#define HOMEBREW_USER "${username}"
|
|
#define HOMEBREW_DIR "${homebrewRepositoryPath}/bin"
|
|
#define BREW_PATH HOMEBREW_DIR "/brew"
|
|
|
|
int main(int argc, char *argv[]) {
|
|
struct passwd *pw;
|
|
uid_t target_uid;
|
|
gid_t target_gid;
|
|
|
|
pw = getpwnam(HOMEBREW_USER);
|
|
if (!pw) { fprintf(stderr, "Error: user %s not found\n", HOMEBREW_USER); return 1; }
|
|
target_uid = pw->pw_uid;
|
|
target_gid = pw->pw_gid;
|
|
|
|
char *new_environ[] = {
|
|
"PATH=" HOMEBREW_DIR ":/usr/bin:/bin:/usr/sbin:/sbin",
|
|
"HOMEBREW_CACHE=${homebrew_cache}",
|
|
"HOMEBREW_LOGS=${homebrew_log}",
|
|
format_env("HOME", "%s"),
|
|
format_env("HOMEBREW_CASK_OPTS", "--no-quarantine %s"),
|
|
"HOMEBREW_PREFIX=${homebrewRepositoryPath}",
|
|
"HOMEBREW_CELLAR=${homebrewRepositoryPath}/Cellar",
|
|
"HOMEBREW_NO_AUTO_UPDATE=1",
|
|
"HOMEBREW_NO_ANALYTICS=1",
|
|
"HOMEBREW_NO_ANALYTICS_THIS_RUN=1",
|
|
"HOMEBREW_NO_ANALYTICS_MESSAGE_OUTPUT=1",
|
|
NULL
|
|
};
|
|
|
|
if (setegid(target_gid) != 0) { fprintf(stderr, "setegid(%d): %s\n", target_gid, strerror(errno)); return 1; }
|
|
|
|
if (seteuid(target_uid) != 0) { fprintf(stderr, "seteuid(%d): %s\n", target_uid, strerror(errno)); return 1; }
|
|
|
|
char **newargv = malloc((argc + 1) * sizeof(char *));
|
|
if (!newargv) { fprintf(stderr, "malloc failed for new argv\n"); return 1; }
|
|
|
|
newargv[0] = (char *)BREW_PATH;
|
|
for (int i = 1; i < argc; ++i) newargv[i] = argv[i];
|
|
newargv[argc] = NULL;
|
|
umask(0002);
|
|
|
|
execve(BREW_PATH, newargv, new_environ);
|
|
fprintf(stderr, "execv(%s) failed: %s\n", BREW_PATH, strerror(errno));
|
|
return 1;
|
|
}
|
|
BREWCALLER
|
|
chown root:admin ${brewCallerPath}
|
|
chmod 4550 ${brewCallerPath}
|
|
[[ ${uname_machine} == "arm64" ]] || ln -s "${brewCallerPath}" "${brewCallerSymlink}"
|
|
}
|
|
|
|
function createBrewPeriodicScript() {
|
|
ensureLocalBinFolder
|
|
local scriptPath="/usr/local/bin/brew-periodic"
|
|
[ -f "${scriptPath}" ] && rm "${scriptPath}"
|
|
cat <<- BREWCALLER > ${scriptPath}
|
|
#!/usr/bin/env zsh
|
|
local brew='/usr/local/bin/brew'
|
|
\$brew update
|
|
\$brew upgrade --greedy
|
|
\$brew cleanup
|
|
BREWCALLER
|
|
chown root:admin ${scriptPath}
|
|
chmod ug=rx,o=r ${scriptPath}
|
|
}
|
|
|
|
function installHomebrewCore() {
|
|
[ ! -d $(getHomebrewRepositoryPath) ] || return
|
|
NONINTERACTIVE=1 HOME= sudo --preserve-env=NONINTERACTIVE,HOME -u "${homebrew_username}" /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
|
[ -d $(getHomebrewRepositoryPath) ]
|
|
}
|
|
|
|
function createLaunchDaemonsPlist() {
|
|
local username=${homebrew_username}
|
|
local launcherName="de.astzweig.macos.launchdaemons.$1"
|
|
local launcherPath="/Library/LaunchDaemons/${launcherName}.plist"
|
|
[[ -f $launcherPath ]] && return
|
|
local brewCommand="$2"
|
|
cat <<- LAUNCHDPLIST > ${launcherPath}
|
|
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
|
|
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
|
|
<plist version=\"1.0\">
|
|
<dict>
|
|
<key>Label</key>
|
|
<string>${launcherName}</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>/usr/local/bin/brew-periodic</string>
|
|
</array>
|
|
<key>StartInterval</key>
|
|
<integer>1800</integer>
|
|
<key>UserName</key>
|
|
<string>${username}</string>
|
|
<key>GroupName</key>
|
|
<string>admin</string>
|
|
<key>Umask</key>
|
|
<integer>2</integer>
|
|
</dict>
|
|
</plist>"
|
|
LAUNCHDPLIST
|
|
chown root:wheel ${launcherPath}
|
|
chmod u=rw,go=r ${launcherPath}
|
|
launchctl bootstrap system ${launcherPath}
|
|
}
|
|
|
|
function installHomebrewUpdater() {
|
|
createLaunchDaemonsPlist brew-periodic
|
|
return
|
|
}
|
|
|
|
function createEnvFileIfNotExists() {
|
|
local envFile=$1 owner=$2 permission=$3
|
|
[[ -f $envFile ]] && return
|
|
touch $envFile
|
|
ensureOwnerAndPermission $envFile $owner $permission
|
|
}
|
|
|
|
function extendPathInEnvFile() {
|
|
local homebrewBinPath="$(getHomebrewRepositoryPath)/bin"
|
|
local homebrewSBinPath="$(getHomebrewRepositoryPath)/sbin"
|
|
local pathsToAdd=()
|
|
createEnvFileIfNotExists $envFile $owner $permission
|
|
for p in ${homebrewBinPath} ${homebrewSBinPath}; do
|
|
{ cat $envFile 2> /dev/null | grep $p >&! /dev/null } || pathsToAdd+=( "\"${p}\"" )
|
|
done
|
|
[[ ${#pathsToAdd} -gt 0 ]] || return
|
|
print -- "path+=($pathsToAdd)" >> $envFile
|
|
}
|
|
|
|
function modifyPathForAll() {
|
|
local envFile=/etc/zshenv owner=root permission='u=rw,go=r'
|
|
[[ $(uname -m) == arm64 ]] || return
|
|
extendPathInEnvFile
|
|
}
|
|
|
|
function configure_system() {
|
|
lop -y h1 -- -i 'Install System Homebrew'
|
|
createHomebrewUserIfNeccessary || return 10
|
|
indicateActivity 'Ensure Homebrew user is in admin group' ensureUserIsInAdminGroup ${homebrew_username} || return 11
|
|
indicateActivity 'Ensure Homebrew user can run passwordless sudo' ensureUserCanRunPasswordlessSudo ${homebrew_username} || return 12
|
|
ensureHomebrewCacheDirectory || return 13
|
|
ensureHomebrewLogDirectory || return 14
|
|
indicateActivity 'Install Homebrew core' installHomebrewCore || return 15
|
|
indicateActivity 'Ensure Homebrew user can nolonger run passwordless sudo' ensureUserCanNoLongerRunPasswordlessSudo ${homebrew_username} || return 20
|
|
indicateActivity 'Create brew caller script' createBrewCallerScript || return 16
|
|
indicateActivity 'Create brew periodic script' createBrewPeriodicScript || return 17
|
|
indicateActivity 'Install Homebrew updater' installHomebrewUpdater || return 18
|
|
indicateActivity 'Modify PATH for all users' modifyPathForAll || return 19
|
|
}
|
|
|
|
function getExecPrerequisites() {
|
|
cmds=(
|
|
[dscl]=''
|
|
[dseditgroup]=''
|
|
[chown]=''
|
|
[chmod]=''
|
|
[sudo]=''
|
|
[grep]=''
|
|
[git]=''
|
|
[sort]=''
|
|
[awk]=''
|
|
[launchctl]=''
|
|
[sysadminctl]=''
|
|
)
|
|
requireRootPrivileges
|
|
}
|
|
|
|
function getDefaultHomebrewUsername() {
|
|
print -- _homebrew
|
|
}
|
|
|
|
function getDefaultHomebrewCachePath() {
|
|
print -- /Library/Caches/Homebrew
|
|
}
|
|
|
|
function getDefaultHomebrewLogPath() {
|
|
print -- /var/log/Homebrew
|
|
}
|
|
|
|
function getQuestions() {
|
|
questions=(
|
|
'i: homebrew-username=What shall the Homebrew user'\''s username be? # default:'"$(getDefaultHomebrewUsername)"
|
|
'i: homebrew-cache=What shall the Homebrew cache directory be? # default:'"$(getDefaultHomebrewCachePath)"
|
|
'i: homebrew-log=What shall the Homebrew log directory be? # default:'"$(getDefaultHomebrewLogPath)"
|
|
)
|
|
}
|
|
|
|
function getUsage() {
|
|
read -r -d '' text <<- USAGE
|
|
Usage:
|
|
$cmdName show-questions [<modkey> <modans>]...
|
|
$cmdName [-v] [-d FILE] --homebrew-username NAME --homebrew-cache PATH --homebrew-log PATH
|
|
|
|
Create a designated Homebrew user who may not login to the system but is the
|
|
only one able to install homebrew software systemwide. Install Homebrew at
|
|
given PREFIX and make the new Homebrew user the owner of that.
|
|
|
|
Options:
|
|
--homebrew-cache PATH Path to folder that shall be used as the
|
|
cache for Homebrew [default: $(getDefaultHomebrewCachePath)].
|
|
--homebrew-log PATH Path to folder that shall be used as the log
|
|
directory for Homebrew [default: $(getDefaultHomebrewLogPath)].
|
|
--homebrew-username NAME Username of the designated Homebrew user.
|
|
[default: $(getDefaultHomebrewUsername)].
|
|
-d FILE, --logfile FILE Print log message to logfile instead of stdout.
|
|
-v, --verbose Be more verbose.
|
|
----
|
|
$cmdName 0.1.0
|
|
Copyright (C) 2022 Rezart Qelibari, Astzweig GmbH & Co. KG
|
|
License EUPL-1.2. There is NO WARRANTY, to the extent permitted by law.
|
|
USAGE
|
|
print -- ${text}
|
|
}
|
|
|
|
if [[ "${ZSH_EVAL_CONTEXT}" == toplevel ]]; then
|
|
test -f "${ASTZWEIG_MACOS_SYSTEM_LIB}" || { echo 'This module requires macos-system library. Please run again with macos-system library provieded as a path in ASTZWEIG_MACOS_SYSTEM_LIB env variable.'; return 10 }
|
|
source "${ASTZWEIG_MACOS_SYSTEM_LIB}"
|
|
module_main $0 "$@"
|
|
fi
|