feat: Add Brew module (#239)

* feat(brew): add new Brew module for Homebrew installation and management

* feat(brew): add new Brew module for Homebrew installation and management

* feat(brew): part 2 of adding new Brew module for Homebrew installation and management

* feat(brew): part 2.2 of adding new Brew module for Homebrew installation and management

* feat(brew): part 3 of adding new Brew module for Homebrew installation and management

* docs: Add kebab-cases for module entries in README

* docs: Don't add whitespaces in module.yml

* chore: Use arrays for package list & add quotes + brackets to some strings

to make it more safe.

* feat(brew): systemd services and timers written dynamically

* feat(brew): systemd services and timers written dynamically 2

* feat(brew): clean up fish completion file name

* chore: Remove duplicate brew completions file

* feat(brew): tweak in brew.sh 3

* chore: Rename shell completion to completions pt. 1

* chore: Rename shell completion to completions pt. 2

* chore: Rename shell completion to completions pt. 3

* feat(brew): Nofile Limits flag, update brew.sh module.yml and README.md

* feat(brew): Brew analytics opt-out flag

* Brew Analytics Enabled by default in docs

* docs: `build-time` instead of `build time`

* docs: Improve `gcc` package log explanation

* fix(brew): Fetching YAML entries due to miss of transition from `_` to `-`

* chore(brew): Improve disabling analytics write to `environment`

* chore(brew): Add script for modifying nofile limit

* chore(brew): Forgot to source the nofile script

* chore(brew): Disable option of installing packages pt.1

* chore(brew): Disable option of installing packages pt.2

* chore(brew): Disable option of installing packages pt.3

* docs(brew): Remove installing packages as a feature

* chore(brew): Add more logs

* chore(brew): Fix typos in logs

* fix(brew): Use `awk` instead of `grep` which fails for stupid reason

Without any error logs either.

* fix(brew): Update & upgrade permutation

* docs(brew): Fix upgrade & update permutation

* chore(brew): Make small tweaks to brew-analytics

If environment is empty, than don't make the extra whiteline

* docs(brew): Slight order adjustment in README inside "Features"

* docs(brew): Add link to Homebrew

* docs(brew): Add note about uninstalling brew

* docs(brew): Include explanation on why manual uninstallation is necessary

Also give big thanks to M2

* docs(brew): Further explain on why the manual uninstallation is needed

* docs(brew): Add link to files module documentation

* docs(brew): Be more detailed about how the module works

* docs(brew): Some small fixes & restore credits

* docs(brew): Rename Brew to Linuxbrew

Co-authored-by: xyny <60004820+xynydev@users.noreply.github.com>

* docs(brew): Change features wording

Co-authored-by: xyny <60004820+xynydev@users.noreply.github.com>

* docs(brew): Reword update & upgrade wording

* docs(brew): Move update & upgrade service labels above timers

* docs(brew): Change brew documentation wording

Co-authored-by: xyny <60004820+xynydev@users.noreply.github.com>

* docs(brew): Syntax highlighting for bash script

Co-authored-by: xyny <60004820+xynydev@users.noreply.github.com>

* docs(brew): Improve uninstallation intro wording

Co-authored-by: xyny <60004820+xynydev@users.noreply.github.com>

* docs(brew): Move brew-analytics above warning

* docs(brew): Update update/upgrade ordering pt. 1

* docs(brew): Update update/upgrade naming pt. 2

* docs(brew): Update update/upgrade order pt. 3

* chore(brew): Use `#!/usr/bin/env fish` instead of `#!/usr/bin/fish`

Co-authored-by: xyny <60004820+xynydev@users.noreply.github.com>

* chore(brew): Use `#!/usr/bin/env bash` instead of `#!/bin/sh`

Co-authored-by: xyny <60004820+xynydev@users.noreply.github.com>

* chore(brew): Set shellcheck from sh to bash

Co-authored-by: xyny <60004820+xynydev@users.noreply.github.com>

* fix(brew): Convert SH-ism to bash in bash completion

* chore(brew): Use double brackets in bash shell completion

* chore(brew): Make rc in brackets

* chore(brew): Use more double brackets

* chore(brew): Add more double brackets pt. 2

* chore(brew): Make bash shell completion spacing better

* chore(brew): Revert to `bash-completions` completion

* docs(brew): Add comment about why bash completion uses sh

* fix(brew): Apply brew path clash fix

* docs(brew): Explain brew path clash fix

* docs(brew): Add file location of brew path fix

* docs(brew): Reword exporting brew path

* docs(brew): Reword again

* docs(brew): Reword...

* chore(brew): Copy bash completion to `/usr/etc` instead of `/etc`

* chore(brew): Make `/usr/etc/profile.d` if it doesn't exist

* docs(brew): Fix typo in README

* docs(brew): Use caution card instead of warning string

* docs(brew): Brew path reword

Co-authored-by: xyny <60004820+xynydev@users.noreply.github.com>

* chore(brew): Implement code comment fix

Co-authored-by: xyny <60004820+xynydev@users.noreply.github.com>

* docs(brew): Try to improve `nofile-limits` section

* docs(brew): Use serverfault link instead

* docs(brew): Thread, not article

* docs(brew): Improve Configuration section

* docs(brew): Than vs then

Love to mess this one

* docs(brew): Uninstall script wording fix

---------

Co-authored-by: fiftydinar <65243233+fiftydinar@users.noreply.github.com>
Co-authored-by: xyny <60004820+xynydev@users.noreply.github.com>
This commit is contained in:
Thomas Mecattaf 2024-06-03 05:25:45 -04:00 committed by GitHub
parent 012d68d894
commit a7e074fe36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 713 additions and 0 deletions

154
modules/brew/README.md Normal file
View file

@ -0,0 +1,154 @@
# brew
The brew module installs [Homebrew/Linuxbrew](https://brew.sh/) on your system and ensures the package manager remains updated and maintained. This module also sets up systemd services to periodically update the installed Brew packages.
## Features
- Installs Brew at build-time.
- Sets up systemd services to automatically update Brew to the latest version.
- Sets up systemd services to automatically upgrade Brew packages.
- Sets up bash and fish completions for Brew.
## How it works
### Directory paths glossary:
- `/home/` is a symlink to `/var/home/`
- `/root/` is a symlink to `/var/roothome/`
### Build-time:
- Directories `/home/` & `/root/` are created
- Empty `.dockerenv` file is created in the root of the image-builder, to convince official Brew installation script that we are **not** running as root
- Official brew installation script is downloaded & executed
- Brew is extracted to `/home/linuxbrew/` by the official script (`/root/` is needed, since image-builds are running as root)
- Brew in `/home/linuxbrew/` is compressed in tar, copied to `/usr/share/homebrew/` & permissions to it are set to default user (UID 1000)
- `brew-update` & `brew-upgrade` SystemD service timers are enabled (by default)
- A fix for path conflicts between system & brew packages with the same name is applied by adding Brew to path only in interactive shells, unlike what Brew does by default.
- Brew bash & fish shell completions are copied to `/etc/profile.d/brew-bash-completions.sh` & `/usr/share/fish/vendor_conf.d/brew-fish-completions.fish`
- `tmpfiles.d` configuration `homebrew.conf` is written with these directory locations:
- `/var/lib/homebrew/`
- `/var/cache/homebrew/`
- `/home/linuxbrew/`
- `brew-setup` service is enabled
### Boot-time:
**`tmpfiles.d homebrew.conf`:**
- This configuration is telling SystemD to: automatically create these necessary directories on every system boot if not available & to give them permissions of the default user (UID 1000):
- `/var/lib/homebrew/`
- `/var/cache/homebrew/`
- `/home/linuxbrew/`
**`brew-setup`:**
- `brew-setup` SystemD service checks if main directory used by Brew exists (`/home/linuxbrew/.linuxbrew/`)
& if `brew-setup` state file exists (`/etc/.linuxbrew`)
- If one of those paths don't exist, then Homebrew tar is extracted from `/usr/share/homebrew/homebrew.tar.zst` to `/tmp/homebrew/`
- Extracted Homebrew is then copied from `/tmp/homebrew/` to `/home/linuxbrew/` & permissions to it are set to default user (UID 1000)
- Temporary directory `/tmp/homebrew/` is removed
- Empty file `/etc/.linuxbrew` is created, which indicates that brew-setup (installation) is successful & which allows setup to run again on next boot when removed
**Rest of the setup:**
- `brew-update` runs at the specified time to update Brew to the latest version
- `brew-upgrade` runs at the specified time to upgrade Brew packages
## Configuration Options
### Update
Brew update operation updates the Brew binary to latest version.
#### `auto-update` (optional: boolean, default: true)
If false, disables automatic activation of `brew-update.timer`.
#### `update-interval` (optional: string, default: '6h')
Defines how often the Brew update service should run. The string is passed directly to `OnUnitInactiveSec` in systemd timer. (Syntax: ['1d', '6h', '10m']).
#### `update-wait-after-boot` (optional: string, default: '10min')
Time delay after system boot before the first Brew update runs. The string is passed directly to `OnBootSec` in systemd timer. (Syntax: ['1d', '6h', '10m']).
### Upgrade
Brew upgrade operation upgrades all installed Brew packages to latest version.
#### `auto-upgrade` (optional: boolean, default: true)
If false, disables automatic activation of `brew-upgrade.timer`.
#### `upgrade-interval` (optional: string, default: '8h')
Defines how often the Brew upgrade service should run. The string is passed directly to `OnUnitInactiveSec` in systemd timer. (Syntax: ['1d', '6h', '10m']).
#### `upgrade-wait-after-boot` (optional: string, default: '30min')
Time delay after system boot before the first Brew package upgrade runs. The string is passed directly to `OnBootSec` in systemd timer. (Syntax: ['1d', '6h', '10m']).
### Analytics
Brew analytics are used to anonymously collect the information about Brew usage & system, in order to improve the experience of Brew users.
#### `brew-analytics` (optional: boolean, default: true)
Determines whether to opt-out of Brew analytics. When set to true, analytics are enabled.
:::caution Please review the Brew documentation carefully before modifying the settings above. :::
### Nofile limits
Nofile limit refers to the maximum number of open files for a single process. For more information about this, you can read this thread:
https://serverfault.com/questions/577437/what-is-the-impact-of-increasing-nofile-limits-in-etc-security-limits-conf
#### `nofile-limits` (optional: boolean, default: false)
Determines whether to increase nofile limits for Brew installations.
When set to true, it increases the nofile limits to prevent certain "I/O heavy" Brew packages from failing due to "too many open files" error. However, it's important to note that increasing nofile limits can have potential security implications for malicious applications which would try to abuse storage I/O. Defaults to false for security purposes.
## Development
Setting `DEBUG=true` inside `brew.sh` will enable additional output for debugging purposes during development.
## Uninstallation
Removing the `brew` module from the recipe is not enough to get it completely removed.
On a booted system, it's also necessary to run the `brew` uninstallation script.
Either a local-user can execute this script manually or the image-maintainer may make it automatic through a custom systemd service.
Uninstallation script:
```bash
#!/usr/bin/env bash
# Remove Homebrew cache
if [[ -d "${HOME}/cache/Homebrew/" ]]; then
echo "Removing '$HOME/cache/Homebrew/' directory"
rm -r "${HOME}/cache/Homebrew/"
else
echo "'${HOME}/cache/Homebrew/' directory is already removed"
fi
# Remove folders created by tmpfiles.d
if [[ -d "/var/lib/homebrew/" ]]; then
echo "Removing '/var/lib/homebrew/' directory"
sudo rm -rf "/var/lib/homebrew/"
else
echo "'/var/lib/homebrew/' directory is already removed"
fi
if [[ -d "/var/cache/homebrew/" ]]; then
echo "Removing '/var/cache/homebrew/' directory"
sudo rm -rf "/var/cache/homebrew/"
else
echo "'/var/cache/homebrew/' directory is already removed"
fi
## This is the main directory where brew is located
if [[ -d "/var/home/linuxbrew/" ]]; then
echo "Removing '/var/home/homebrew/' directory"
sudo rm -rf "/var/home/linuxbrew/"
else
echo "'/home/homebrew/' directory is already removed"
fi
# Remove redundant brew-setup service state file
if [[ -f "/etc/.linuxbrew" ]]; then
echo "Removing empty '/etc/.linuxbrew' file"
sudo rm -f "/etc/.linuxbrew"
else
echo "'/etc/.linuxbrew' file is already removed"
fi
```
## Credits
Thanks a lot to Bluefin custom image maintainer [m2giles](https://github.com/m2Giles), who made this entire module possible.
In fact, the module's logic of installing & updating/upgrading Brew is fully copied from him & Bluefin, we just made it easier & more convenient to use for BlueBuild users.

View file

@ -0,0 +1,27 @@
#!/bin/sh
# shellcheck shell=sh disable=SC1091,SC2039,SC2166
# Completion is in sh to account for the zsh syntax & when zsh tries to source scripts from /etc/profiles
# Check for interactive bash and that we haven't already been sourced.
if [ "x${BASH_VERSION-}" != x -a "x${PS1-}" != x -a "x${BREW_BASH_COMPLETION-}" = x ]; then
# Check for recent enough version of bash.
if [ "${BASH_VERSINFO[0]}" -gt 4 ] ||
[ "${BASH_VERSINFO[0]}" -eq 4 -a "${BASH_VERSINFO[1]}" -ge 2 ]; then
if [ -w /home/linuxbrew/.linuxbrew ]; then
if ! test -L /home/linuxbrew/.linuxbrew/etc/bash_completion.d/brew; then
/home/linuxbrew/.linuxbrew/bin/brew completions link > /dev/null
fi
fi
if test -d /home/linuxbrew/.linuxbrew/etc/bash_completion.d; then
for rc in /home/linuxbrew/.linuxbrew/etc/bash_completion.d/*; do
if test -r "$rc"; then
. "$rc"
fi
done
unset rc
fi
fi
BREW_BASH_COMPLETION=1
export BREW_BASH_COMPLETION
fi

View file

@ -0,0 +1,18 @@
#!/usr/bin/env fish
#shellcheck disable=all
if status --is-interactive
if [ -d /home/linuxbrew/.linuxbrew ]
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
if [ -w /home/linuxbrew/.linuxbrew ]
if [ ! -L (brew --prefix)/share/fish/vendor_completions.d/brew ]
brew completions link > /dev/null
end
end
if test -d (brew --prefix)/share/fish/completions
set -p fish_complete_path (brew --prefix)/share/fish/completions
end
if test -d (brew --prefix)/share/fish/vendor_completions.d
set -p fish_complete_path (brew --prefix)/share/fish/vendor_completions.d
end
end
end

View file

@ -0,0 +1,234 @@
#!/usr/bin/env bash
set -euo pipefail
# Script used to read nofile limits from the base image, since issuing easy systemctl status commands don't work in build-time.
# Takes into account config priorities & current config settings regarding nofile limits
# (if nofile limit is already applied with appropriate values in the base image, than this tweak is not applied)
# Modifies limits nofile value & systemd DefaultLimitNOFILE value
DESIRED_SOFT_LIMIT=4096
DESIRED_HARD_LIMIT=524288
BREW_LIMITS_D_CONFIG="/usr/etc/security/limits.d/zz1-brew-limits.conf"
BREW_SYSTEMD_SYSTEM_CONFIG="/usr/lib/systemd/system.conf.d/zz1-brew-limits.conf"
BREW_SYSTEMD_USER_CONFIG="/usr/lib/systemd/user.conf.d/zz1-brew-limits.conf"
# SSH/TTY nofile limit (security ulimit config)
# From least to most preferred
SSH_TTY_LIMIT_ORDER=(
"/usr/etc/security/limits.conf"
"/usr/etc/security/limits.d/"
"/etc/security/limits.conf"
"/etc/security/limits.d/"
)
updated_ssh_array=()
for path in "${SSH_TTY_LIMIT_ORDER[@]}"; do
if [ -e "${path}" ]; then
updated_ssh_array+=("${path}")
fi
done
# Update the original array with the existing paths
SSH_TTY_LIMIT_ORDER=("${updated_ssh_array[@]}")
# Soft SSH/TTY nofile limit
SSH_TTY_SOFT_INFO=$(find "${SSH_TTY_LIMIT_ORDER[@]}" -type f -name "*.conf" -exec awk -v OFS='\t' '/soft\s+nofile/ && !/^#/ {sub(/.*nofile/, ""); gsub(/^[ \t]+/, ""); print FILENAME, $0}' {} + | tail -n 1)
if [[ -n "${SSH_TTY_SOFT_INFO}" ]]; then
CURRENT_SSH_TTY_SOFT_VALUE=$(echo "${SSH_TTY_SOFT_INFO}" | awk '{print $2}')
else
CURRENT_SSH_TTY_SOFT_VALUE=0
fi
# Hard SSH/TTY nofile limit
SSH_TTY_HARD_INFO=$(find "${SSH_TTY_LIMIT_ORDER[@]}" -type f -name "*.conf" -exec awk -v OFS='\t' '/hard\s+nofile/ && !/^#/ {sub(/.*nofile/, ""); gsub(/^[ \t]+/, ""); print FILENAME, $0}' {} + | tail -n 1)
if [[ -n "${SSH_TTY_HARD_INFO}" ]]; then
CURRENT_SSH_TTY_HARD_VALUE=$(echo "${SSH_TTY_HARD_INFO}" | awk '{print $2}')
else
CURRENT_SSH_TTY_HARD_VALUE=0
fi
# SystemD nofile limit
# SystemD system soft & hard nofile limit
# From least to most preferred
SYSTEMD_SYSTEM_LIMIT_ORDER=(
"/usr/lib/systemd/system.conf"
"/usr/lib/systemd/system.conf.d/"
"/usr/etc/systemd/system.conf"
"/usr/etc/systemd/system.conf.d/"
"/etc/systemd/system.conf"
"/etc/systemd/system.conf.d/"
)
updated_systemd_system_array=()
for path in "${SYSTEMD_SYSTEM_LIMIT_ORDER[@]}"; do
if [ -e "${path}" ]; then
updated_systemd_system_array+=("${path}")
fi
done
# Update the original array with the existing paths
SYSTEMD_SYSTEM_LIMIT_ORDER=("${updated_systemd_system_array[@]}")
# SystemD system soft & hard nofile limit
SYSTEMD_SYSTEM_SOFT_INFO=$(find "${SYSTEMD_SYSTEM_LIMIT_ORDER[@]}" -type f -name "*.conf" -exec awk -F'[:=]' '/^[^#]*DefaultLimitNOFILE/ {print FILENAME, $2}' {} + | tail -n 1)
if [[ -n "${SYSTEMD_SYSTEM_SOFT_INFO}" ]]; then
CURRENT_SYSTEMD_SYSTEM_SOFT_VALUE=$(echo "${SYSTEMD_SYSTEM_SOFT_INFO}" | awk '{print $2}')
else
CURRENT_SYSTEMD_SYSTEM_SOFT_VALUE=0
fi
SYSTEMD_SYSTEM_HARD_INFO=$(find "${SYSTEMD_SYSTEM_LIMIT_ORDER[@]}" -type f -name "*.conf" -exec awk -F'[:=]' '/^[^#]*DefaultLimitNOFILE/ {print FILENAME, $3}' {} + | tail -n 1)
if [[ -n "${SYSTEMD_SYSTEM_HARD_INFO}" ]]; then
CURRENT_SYSTEMD_SYSTEM_HARD_VALUE=$(echo "${SYSTEMD_SYSTEM_HARD_INFO}" | awk '{print $2}')
else
CURRENT_SYSTEMD_SYSTEM_HARD_VALUE=0
fi
# SystemD user soft & hard nofile limit
SYSTEMD_USER_LIMIT_ORDER=(
"/usr/lib/systemd/user.conf"
"/usr/lib/systemd/user.conf.d/"
"/usr/etc/systemd/user.conf"
"/usr/etc/systemd/user.conf.d/"
"/etc/systemd/user.conf"
"/etc/systemd/user.conf.d/"
)
updated_systemd_user_array=()
for path in "${SYSTEMD_USER_LIMIT_ORDER[@]}"; do
if [ -e "${path}" ]; then
updated_systemd_user_array+=("${path}")
fi
done
# Update the original array with the existing paths
SYSTEMD_USER_LIMIT_ORDER=("${updated_systemd_user_array[@]}")
SYSTEMD_USER_SOFT_INFO=$(find "${SYSTEMD_USER_LIMIT_ORDER[@]}" -type f -name "*.conf" -exec awk -F'[:=]' '/^[^#]*DefaultLimitNOFILE/ {print FILENAME, $2}' {} + | tail -n 1)
if [[ -n "${SYSTEMD_USER_SOFT_INFO}" ]]; then
CURRENT_SYSTEMD_USER_SOFT_VALUE=$(echo "${SYSTEMD_USER_SOFT_INFO}" | awk '{print $2}')
else
CURRENT_SYSTEMD_USER_SOFT_VALUE=0
fi
SYSTEMD_USER_HARD_INFO=$(find "${SYSTEMD_USER_LIMIT_ORDER[@]}" -type f -name "*.conf" -exec awk -F'[:=]' '/^[^#]*DefaultLimitNOFILE/ {print FILENAME, $3}' {} + | tail -n 1)
if [[ -n "${SYSTEMD_USER_HARD_INFO}" ]]; then
CURRENT_SYSTEMD_USER_HARD_VALUE=$(echo "${SYSTEMD_USER_HARD_INFO}" | awk '{print $2}')
else
CURRENT_SYSTEMD_USER_HARD_VALUE=0
fi
# Check current state
echo "Current nofile limit values:"
check_and_print() {
if [[ "${1}" -eq 0 ]]; then
echo "UNSET"
else
echo "${1}"
fi
}
echo "SSH/TTY soft nofile limit: $(check_and_print ${CURRENT_SSH_TTY_SOFT_VALUE})"
echo "SSH/TTY hard nofile limit: $(check_and_print ${CURRENT_SSH_TTY_HARD_VALUE})"
echo "SystemD system soft nofile limit: $(check_and_print ${CURRENT_SYSTEMD_SYSTEM_SOFT_VALUE})"
echo "SystemD system hard nofile limit: $(check_and_print ${CURRENT_SYSTEMD_SYSTEM_HARD_VALUE})"
echo "SystemD user soft nofile limit: $(check_and_print ${CURRENT_SYSTEMD_USER_SOFT_VALUE})"
echo "SystemD user hard nofile limit: $(check_and_print ${CURRENT_SYSTEMD_USER_HARD_VALUE})"
# Write nofile limit values
# zz1- prefix is used for config, to assure that nofile limit is going to be applied, as it's high in lexical order.
# If config is higher in lexical order, that means that it's preferred over other ones.
# Downstreams can just go on with their own modifications in zz2-, zz3-, zz4-, zz5-, etc. increments
# if they wish to experiment with nofile limits differently
# Write SSH/TTY nolimit values
if [[ "${CURRENT_SSH_TTY_SOFT_VALUE}" -lt "${DESIRED_SOFT_LIMIT}" ]] || [[ "${CURRENT_SSH_TTY_HARD_VALUE}" -lt "${DESIRED_HARD_LIMIT}" ]]; then
if [[ ! -d "/usr/etc/security/limits.d/" ]]; then
mkdir -p "/usr/etc/security/limits.d/"
fi
echo "# This file sets the resource limits for users logged in via PAM,
# more specifically, users logged in via SSH or tty (console).
# Limits related to terminals in Wayland/Xorg sessions depend on a
# change to /etc/systemd/user.conf.
# This does not affect resource limits of the system services.
# This file overrides defaults set in /etc/security/limits.conf
" > "${BREW_LIMITS_D_CONFIG}"
fi
if [[ "${CURRENT_SSH_TTY_SOFT_VALUE}" -lt "${DESIRED_SOFT_LIMIT}" ]] && [[ "${CURRENT_SSH_TTY_HARD_VALUE}" -ge "${DESIRED_HARD_LIMIT}" ]]; then
echo "Writing increased SSH/TTY soft nofile limit value"
echo "* soft nofile ${DESIRED_SOFT_LIMIT}" >> "${BREW_LIMITS_D_CONFIG}"
elif [[ "${CURRENT_SSH_TTY_SOFT_VALUE}" -ge "${DESIRED_SOFT_LIMIT}" ]]; then
echo "Required SSH/TTY soft nofile limit value is already satisfied!"
fi
if [[ "${CURRENT_SSH_TTY_SOFT_VALUE}" -ge "${DESIRED_SOFT_LIMIT}" ]] && [[ "${CURRENT_SSH_TTY_HARD_VALUE}" -lt "${DESIRED_HARD_LIMIT}" ]]; then
echo "Writing increased SSH/TTY hard nofile limit value"
echo "* hard nofile ${DESIRED_HARD_LIMIT}" >> "${BREW_LIMITS_D_CONFIG}"
elif [[ "${CURRENT_SSH_TTY_HARD_VALUE}" -ge "${DESIRED_HARD_LIMIT}" ]]; then
echo "Required SSH/TTY hard nofile limit value is already satisfied!"
fi
if [[ "${CURRENT_SSH_TTY_SOFT_VALUE}" -ge "${DESIRED_SOFT_LIMIT}" ]] && [[ "${CURRENT_SSH_TTY_HARD_VALUE}" -lt "${DESIRED_HARD_LIMIT}" ]]; then
echo "Writing increased SSH/TTY soft & hard nofile limit value"
echo "* soft nofile ${DESIRED_SOFT_LIMIT}" >> "${BREW_LIMITS_D_CONFIG}"
echo "* hard nofile ${DESIRED_HARD_LIMIT}" >> "${BREW_LIMITS_D_CONFIG}"
fi
# Write SystemD nolimit values
# Writing SystemD system nolimit values
if [[ "${CURRENT_SYSTEMD_SYSTEM_SOFT_VALUE}" -lt "${DESIRED_SOFT_LIMIT}" ]] || [[ "${CURRENT_SYSTEMD_SYSTEM_HARD_VALUE}" -lt "${DESIRED_HARD_LIMIT}" ]]; then
if [[ ! -d "/usr/lib/systemd/system.conf.d/" ]]; then
mkdir -p "/usr/lib/systemd/system.conf.d/"
fi
fi
if [[ "${CURRENT_SYSTEMD_SYSTEM_SOFT_VALUE}" -lt "${DESIRED_SOFT_LIMIT}" ]] && [[ "${CURRENT_SYSTEMD_SYSTEM_HARD_VALUE}" -ge "${DESIRED_HARD_LIMIT}" ]]; then
echo "Writing increased SystemD system soft nofile limit value"
echo "[Manager]
DefaultLimitNOFILE=${DESIRED_SOFT_LIMIT}:${CURRENT_SYSTEMD_SYSTEM_HARD_VALUE}" > "${BREW_SYSTEMD_SYSTEM_CONFIG}"
elif [[ "${CURRENT_SYSTEMD_SYSTEM_SOFT_VALUE}" -ge "${DESIRED_SOFT_LIMIT}" ]]; then
echo "Required SystemD system soft nofile limit value is already satisfied!"
fi
if [[ "${CURRENT_SYSTEMD_SYSTEM_SOFT_VALUE}" -ge "${DESIRED_SOFT_LIMIT}" ]] && [[ "${CURRENT_SYSTEMD_SYSTEM_HARD_VALUE}" -lt "${DESIRED_HARD_LIMIT}" ]]; then
echo "Writing increased SystemD system hard nofile limit value"
echo "[Manager]
DefaultLimitNOFILE=${CURRENT_SYSTEMD_SYSTEM_SOFT_VALUE}:${DESIRED_HARD_LIMIT}" > "${BREW_SYSTEMD_SYSTEM_CONFIG}"
elif [[ "${CURRENT_SYSTEMD_SYSTEM_HARD_VALUE}" -ge "${DESIRED_HARD_LIMIT}" ]]; then
echo "Required SystemD system hard nofile limit value is already satisfied!"
fi
if [[ "${CURRENT_SYSTEMD_SYSTEM_SOFT_VALUE}" -lt "${DESIRED_SOFT_LIMIT}" ]] && [[ "${CURRENT_SYSTEMD_SYSTEM_HARD_VALUE}" -lt "${DESIRED_HARD_LIMIT}" ]]; then
echo "Writing increased SystemD system soft & hard nofile limit value"
echo "[Manager]
DefaultLimitNOFILE=${DESIRED_SOFT_LIMIT}:${DESIRED_HARD_LIMIT}" > "${BREW_SYSTEMD_SYSTEM_CONFIG}"
fi
# Writing SystemD user nolimit values
if [[ "${CURRENT_SYSTEMD_USER_SOFT_VALUE}" -lt "${DESIRED_SOFT_LIMIT}" ]] || [[ "${CURRENT_SYSTEMD_USER_HARD_VALUE}" -lt "${DESIRED_HARD_LIMIT}" ]]; then
if [[ ! -d "/usr/lib/systemd/user.conf.d/" ]]; then
mkdir -p "/usr/lib/systemd/user.conf.d/"
fi
fi
if [[ "${CURRENT_SYSTEMD_USER_SOFT_VALUE}" -lt "${DESIRED_SOFT_LIMIT}" ]] && [[ "${CURRENT_SYSTEMD_USER_HARD_VALUE}" -ge "${DESIRED_HARD_LIMIT}" ]]; then
echo "Writing increased SystemD user soft nofile limit value"
echo "[Manager]
DefaultLimitNOFILE=${DESIRED_SOFT_LIMIT}:${CURRENT_SYSTEMD_USER_HARD_VALUE}" > "${BREW_SYSTEMD_USER_CONFIG}"
elif [[ "${CURRENT_SYSTEMD_USER_SOFT_VALUE}" -ge "${DESIRED_SOFT_LIMIT}" ]]; then
echo "Required SystemD user soft nofile limit value is already satisfied!"
fi
if [[ "${CURRENT_SYSTEMD_USER_SOFT_VALUE}" -ge "${DESIRED_SOFT_LIMIT}" ]] && [[ "${CURRENT_SYSTEMD_USER_HARD_VALUE}" -lt "${DESIRED_HARD_LIMIT}" ]]; then
echo "Writing increased SystemD user hard nofile limit value"
echo "[Manager]
DefaultLimitNOFILE=${CURRENT_SYSTEMD_USER_SOFT_VALUE}:${DESIRED_HARD_LIMIT}" > "${BREW_SYSTEMD_USER_CONFIG}"
elif [[ "${CURRENT_SYSTEMD_USER_HARD_VALUE}" -ge "${DESIRED_HARD_LIMIT}" ]]; then
echo "Required SystemD user hard nofile limit value is already satisfied!"
fi
if [[ "${CURRENT_SYSTEMD_USER_SOFT_VALUE}" -lt "${DESIRED_SOFT_LIMIT}" ]] && [[ "${CURRENT_SYSTEMD_USER_HARD_VALUE}" -lt "${DESIRED_HARD_LIMIT}" ]]; then
echo "Writing increased SystemD user soft & hard nofile limit value"
echo "[Manager]
DefaultLimitNOFILE=${DESIRED_SOFT_LIMIT}:${DESIRED_HARD_LIMIT}" > "${BREW_SYSTEMD_USER_CONFIG}"
fi

259
modules/brew/brew.sh Normal file
View file

@ -0,0 +1,259 @@
#!/usr/bin/env bash
set -euo pipefail
# Convince the installer that we are in CI
touch /.dockerenv
# Debugging
DEBUG="${DEBUG:-false}"
if [[ "${DEBUG}" == true ]]; then
set -x
fi
# Check if gcc is installed
if ! command -v gcc &> /dev/null
then
echo "ERROR: \"gcc\" package could not be found."
echo " Brew depends on \"gcc\" in order to function"
echo " Please include \"gcc\" in the list of packages to install with the system package manager"
exit 1
fi
# Module-specific directories and paths
MODULE_DIRECTORY="${MODULE_DIRECTORY:-/tmp/modules}"
# Configuration values
AUTO_UPDATE=$(echo "${1}" | yq -I=0 ".auto-update")
if [[ -z "${AUTO_UPDATE}" || "${AUTO_UPDATE}" == "null" ]]; then
AUTO_UPDATE=true
fi
UPDATE_INTERVAL=$(echo "${1}" | yq -I=0 ".update-interval")
if [[ -z "${UPDATE_INTERVAL}" || "${UPDATE_INTERVAL}" == "null" ]]; then
UPDATE_INTERVAL="6h"
fi
UPDATE_WAIT_AFTER_BOOT=$(echo "${1}" | yq -I=0 ".update-wait-after-boot")
if [[ -z "${UPDATE_WAIT_AFTER_BOOT}" || "${UPDATE_WAIT_AFTER_BOOT}" == "null" ]]; then
UPDATE_WAIT_AFTER_BOOT="10min"
fi
AUTO_UPGRADE=$(echo "${1}" | yq -I=0 ".auto-upgrade")
if [[ -z "${AUTO_UPGRADE}" || "${AUTO_UPGRADE}" == "null" ]]; then
AUTO_UPGRADE=true
fi
UPGRADE_INTERVAL=$(echo "$1" | yq -I=0 ".upgrade-interval")
if [[ -z "${UPGRADE_INTERVAL}" || "${UPGRADE_INTERVAL}" == "null" ]]; then
UPGRADE_INTERVAL="8h"
fi
UPGRADE_WAIT_AFTER_BOOT=$(echo "${1}" | yq -I=0 ".upgrade-wait-after-boot")
if [[ -z "${UPGRADE_WAIT_AFTER_BOOT}" || "${UPGRADE_WAIT_AFTER_BOOT}" == "null" ]]; then
UPGRADE_WAIT_AFTER_BOOT="30min"
fi
NOFILE_LIMITS=$(echo "${1}" | yq -I=0 ".nofile-limits")
if [[ -z "${NOFILE_LIMITS}" || "${NOFILE_LIMITS}" == "null" ]]; then
NOFILE_LIMITS=false
fi
BREW_ANALYTICS=$(echo "${1}" | yq -I=0 ".brew-analytics")
if [[ -z "${BREW_ANALYTICS}" || "${BREW_ANALYTICS}" == "null" ]]; then
BREW_ANALYTICS=true
fi
# Create necessary directories
mkdir -p /var/home
mkdir -p /var/roothome
# Always install Brew
echo "Downloading and installing Brew..."
curl -Lo /tmp/brew-install https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh
chmod +x /tmp/brew-install
/tmp/brew-install
# Move Brew installation and set ownership to default user (UID 1000)
tar --zstd -cvf /usr/share/homebrew.tar.zst /home/linuxbrew/.linuxbrew
cp -R /home/linuxbrew /usr/share/homebrew
chown -R 1000:1000 /usr/share/homebrew
# Write systemd service files dynamically
echo "Writing brew-setup service"
cat >/usr/lib/systemd/system/brew-setup.service <<EOF
[Unit]
Description=Setup Brew
Wants=network-online.target
After=network-online.target
ConditionPathExists=!/etc/.linuxbrew
ConditionPathExists=!/var/home/linuxbrew/.linuxbrew
[Service]
Type=oneshot
ExecStart=/usr/bin/mkdir -p /tmp/homebrew
ExecStart=/usr/bin/tar --zstd -xvf /usr/share/homebrew.tar.zst -C /tmp/homebrew
ExecStart=/usr/bin/cp -R -n /tmp/homebrew/home/linuxbrew/.linuxbrew /var/home/linuxbrew
ExecStart=/usr/bin/chown -R 1000:1000 /var/home/linuxbrew
ExecStart=/usr/bin/rm -rf /tmp/homebrew
ExecStart=/usr/bin/touch /etc/.linuxbrew
[Install]
WantedBy=default.target multi-user.target
EOF
echo "Writing brew-update service"
cat >/usr/lib/systemd/system/brew-update.service <<EOF
[Unit]
Description=Auto-update Brew binary
After=local-fs.target
After=network-online.target
ConditionPathIsSymbolicLink=/home/linuxbrew/.linuxbrew/bin/brew
[Service]
User=1000
Type=oneshot
Environment=HOMEBREW_CELLAR=/home/linuxbrew/.linuxbrew/Cellar
Environment=HOMEBREW_PREFIX=/home/linuxbrew/.linuxbrew
Environment=HOMEBREW_REPOSITORY=/home/linuxbrew/.linuxbrew/Homebrew
ExecStart=/usr/bin/bash -c "/home/linuxbrew/.linuxbrew/bin/brew update"
EOF
echo "Writing brew-upgrade service"
cat >/usr/lib/systemd/system/brew-upgrade.service <<EOF
[Unit]
Description=Auto-upgrade Brew packages
After=local-fs.target
After=network-online.target
ConditionPathIsSymbolicLink=/home/linuxbrew/.linuxbrew/bin/brew
[Service]
User=1000
Type=oneshot
Environment=HOMEBREW_CELLAR=/home/linuxbrew/.linuxbrew/Cellar
Environment=HOMEBREW_PREFIX=/home/linuxbrew/.linuxbrew
Environment=HOMEBREW_REPOSITORY=/home/linuxbrew/.linuxbrew/Homebrew
ExecStart=/usr/bin/bash -c "/home/linuxbrew/.linuxbrew/bin/brew upgrade"
EOF
# Write systemd timer files dynamically
echo "Writing brew-update timer"
if [[ -n "${UPDATE_WAIT_AFTER_BOOT}" ]] && [[ "${UPDATE_WAIT_AFTER_BOOT}" != "10min" ]]; then
echo "Applying custom 'wait-after-boot' value in '${UPDATE_WAIT_AFTER_BOOT}' time interval for brew update timer"
fi
if [[ -n "${UPDATE_INTERVAL}" ]] && [[ "${UPDATE_INTERVAL}" != "6h" ]]; then
echo "Applying custom 'update-interval' value in '${UPDATE_INTERVAL}' time interval for brew update timer"
fi
cat >/usr/lib/systemd/system/brew-update.timer <<EOF
[Unit]
Description=Timer for updating Brew binary
Wants=network-online.target
[Timer]
OnBootSec=${UPDATE_WAIT_AFTER_BOOT}
OnUnitInactiveSec=${UPDATE_INTERVAL}
Persistent=true
[Install]
WantedBy=timers.target
EOF
echo "Writing brew-upgrade timer"
if [[ -n "${UPGRADE_WAIT_AFTER_BOOT}" ]] && [[ "${UPGRADE_WAIT_AFTER_BOOT}" != "30min" ]]; then
echo "Applying custom 'wait-after-boot' value in '${UPGRADE_WAIT_AFTER_BOOT}' time interval for brew upgrade timer"
fi
if [[ -n "${UPGRADE_INTERVAL}" ]] && [[ "${UPGRADE_INTERVAL}" != "8h" ]]; then
echo "Applying custom 'upgrade-interval' value in '${UPGRADE_INTERVAL}' time interval for brew upgrade timer"
fi
cat >/usr/lib/systemd/system/brew-upgrade.timer <<EOF
[Unit]
Description=Timer for upgrading Brew packages
Wants=network-online.target
[Timer]
OnBootSec=${UPGRADE_WAIT_AFTER_BOOT}
OnUnitInactiveSec=${UPGRADE_INTERVAL}
Persistent=true
[Install]
WantedBy=timers.target
EOF
# Apply brew shell environment only when shell is interactive
# Fish already includes this fix in brew-fish-completions.sh
# By default Brew applies the shell environment changes globally, which causes path conflicts between system & brew installed programs with same name.
# Universal Blue images include this same fix
if [[ ! -d "/usr/etc/profile.d/" ]]; then
mkdir -p "/usr/etc/profile.d/"
fi
if [[ ! -f "/usr/etc/profile.d/brew.sh" ]]; then
echo "Apply brew path export fix, to solve path conflicts between system & brew programs with same name"
echo "#!/usr/bin/env bash
[[ -d /home/linuxbrew/.linuxbrew && $- == *i* ]] && eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"" > "/usr/etc/profile.d/brew.sh"
fi
# Copy shell configuration files
echo "Copying Brew bash & fish shell completions"
cp -r "${MODULE_DIRECTORY}"/brew/brew-fish-completions.fish /usr/share/fish/vendor_conf.d/brew-fish-completions.fish
cp -r "${MODULE_DIRECTORY}"/brew/brew-bash-completions.sh /usr/etc/profile.d/brew-bash-completions.sh
# Register path symlink
# We do this via tmpfiles.d so that it is created by the live system.
echo "Writing brew tmpfiles.d configuration"
cat >/usr/lib/tmpfiles.d/homebrew.conf <<EOF
d /var/lib/homebrew 0755 1000 1000 - -
d /var/cache/homebrew 0755 1000 1000 - -
d /var/home/linuxbrew 0755 1000 1000 - -
EOF
# Enable the setup service
echo "Enabling brew-setup service"
systemctl enable brew-setup.service
# Always enable or disable update and upgrade services for consistency
if [[ "${AUTO_UPDATE}" == true ]]; then
echo "Enabling auto-updates for Brew binary"
systemctl enable brew-update.timer
else
echo "Disabling auto-updates for Brew binary"
systemctl disable brew-update.timer
fi
if [[ "${AUTO_UPGRADE}" == true ]]; then
echo "Enabling auto-upgrades for Brew packages"
systemctl enable brew-upgrade.timer
else
echo "Disabling auto-upgrades for Brew packages"
systemctl disable brew-upgrade.timer
fi
# Apply nofile limits if enabled
if [[ "${NOFILE_LIMITS}" == true ]]; then
source "${MODULE_DIRECTORY}"/brew/brew-nofile-limits-logic.sh
fi
# Disable homebrew analytics if the flag is set to false
# like secureblue: https://github.com/secureblue/secureblue/blob/live/config/scripts/homebrewanalyticsoptout.sh
if [[ "${BREW_ANALYTICS}" == false ]]; then
if [[ ! -f "/usr/etc/environment" ]]; then
echo "" > "/usr/etc/environment" # touch fails for some reason, probably a bug with it
fi
CURRENT_ENVIRONMENT=$(cat "/usr/etc/environment")
CURRENT_HOMEBREW_CONFIG=$(awk -F= '/HOMEBREW_NO_ANALYTICS/ {print $0}' "/usr/etc/environment")
if [[ -n "${CURRENT_ENVIRONMENT}" ]]; then
if [[ "${CURRENT_HOMEBREW_CONFIG}" == "HOMEBREW_NO_ANALYTICS=0" ]]; then
echo "Disabling Brew analytics"
sed -i 's/HOMEBREW_NO_ANALYTICS=0/HOMEBREW_NO_ANALYTICS=1/' "/usr/etc/environment"
elif [[ -z "${CURRENT_HOMEBREW_CONFIG}" ]]; then
echo "Disabling Brew analytics"
echo "HOMEBREW_NO_ANALYTICS=1" >> "/usr/etc/environment"
elif [[ "${CURRENT_HOMEBREW_CONFIG}" == "HOMEBREW_NO_ANALYTICS=1" ]]; then
echo "Brew analytics are already disabled!"
fi
elif [[ -z "${CURRENT_ENVIRONMENT}" ]]; then
echo "Disabling Brew analytics"
echo "HOMEBREW_NO_ANALYTICS=1" > "/usr/etc/environment"
fi
fi
echo "Brew setup completed"

21
modules/brew/module.yml Normal file
View file

@ -0,0 +1,21 @@
name: brew
shortdesc: The brew module installs Homebrew (Brew) at build time and ensures the package manager remains updated and maintained.
readme: https://raw.githubusercontent.com/blue-build/modules/main/modules/brew/README.md
example: |
type: brew
# Auto-update Brew binary
auto-update: true # Optional - Default: true - Expects type: boolean
# Interval between Brew updates
update-interval: '6h' # Optional - Default: '6h' - Expects type: string
# Time delay after boot before first Brew update
update-wait-after-boot: '10min' # Optional - Default: '10min' - Expects type: string
# Auto-upgrade Brew packages
auto-upgrade: true # Optional - Default: true - Expects type: boolean
# Interval between Brew package upgrades
upgrade-interval: '8h' # Optional - Default: '8h' - Expects type: string
# Time delay after boot before first Brew upgrade
upgrade-wait-after-boot: '30min' # Optional - Default: '30min' - Expects type: string
# Apply nofile limits for Brew installations
nofile-limits: false # Optional - Default: false - Expects type: boolean
# Control Brew analytics
brew-analytics: true # Optional - Default: true - Expects type: boolean