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=200
|
|
local maxUserID=400
|
|
if is-at-least 13.0 $(sw_vers -productVersion); then
|
|
minUserID=450
|
|
maxUserID=499
|
|
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 }
|
|
autoload is-at-least
|
|
source "${ASTZWEIG_MACOS_SYSTEM_LIB}"
|
|
module_main $0 "$@"
|
|
fi
|