#!/bin/bash
set -eou pipefail
umask 0022
# If the variable `$DEBUG` is set, then print the shell commands as we execute.
if [ -n "${DEBUG:-}" ]; then set -x; fi
readonly pcio_root="https://packages.chef.io/files"
export HAB_LICENSE="accept-no-persist"
# This is the main function that sets up the Habitat environment on macOS.
# It creates, mounts, and configures a designated volume (Habitat Store) with the necessary settings,
# including file system options and encryption (if needed).
#
# High-level steps performed:
#
# 1. **Volume Creation**:
# - Creates a new APFS volume for Habitat on the identified disk.
# - Encrypts the volume (if needed) with a randomly generated password.
#
# 2. **Volume Configuration**:
# - Verifies that the Habitat root (`/hab`) is properly configured.
# - Updates `/etc/synthetic.conf` to ensure the mount point is set correctly.
# - Configures `/etc/fstab` with the appropriate volume mount options.
#
# 3. **Volume Mounting and Daemon Configuration**:
# - Generates and installs a LaunchDaemon plist to mount the volume at system boot.
# - Ensures the volume is mounted automatically on startup.
setup_hab_root() {
SCRATCH=$(mktemp -d)
readonly SCRATCH
finish_cleanup() {
rm -rf "$SCRATCH"
}
readonly HAB_ROOT="/hab"
readonly HAB_VOLUME_LABEL="Habitat Store"
readonly HAB_SERVICE_TARGET="sh.habitat.bldr.darwin-store"
readonly HAB_VOLUME_MOUNTD_DEST="/Library/LaunchDaemons/$HAB_SERVICE_TARGET.plist"
root_disk() {
/usr/sbin/diskutil info -plist / | xmllint --xpath "/plist/dict/key[text()='ParentWholeDisk']/following-sibling::string[1]/text()" -
}
HAB_VOLUME_USE_DISK="$(root_disk)"
readonly HAB_VOLUME_USE_DISK
if /usr/bin/fdesetup isactive >/dev/null; then
test_filevault_in_use() { return 0; }
HAB_VOLUME_DO_ENCRYPT=1
else
test_filevault_in_use() { return 1; }
HAB_VOLUME_DO_ENCRYPT=0
fi
should_encrypt_volume() {
test_filevault_in_use && (( HAB_VOLUME_DO_ENCRYPT == 1 ))
}
volume_encrypted() {
local volume="$1" # (i.e., disk1s3)
/usr/sbin/diskutil apfs listCryptoUsers -plist "$volume" | /usr/bin/grep -q APFSCryptoUserUUID
}
test_fstab() {
/usr/bin/grep -q "$HAB_ROOT apfs rw" /etc/fstab 2>/dev/null
}
test_synthetic_conf_mountable() {
/usr/bin/grep -q "^${HAB_ROOT:1}$" /etc/synthetic.conf 2>/dev/null
}
create_synthetic_objects() {
{
/System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util -t || true # Big Sur and above
} >/dev/null 2>&1
}
test_hab() {
test -d "$HAB_ROOT"
}
test_volume_daemon() {
test -f "$HAB_VOLUME_MOUNTD_DEST"
}
generate_mount_command() {
local cmd_type="$1" # encrypted|unencrypted
local volume_uuid mountpoint cmd=()
printf -v volume_uuid "%q" "$2"
printf -v mountpoint "%q" "$HAB_ROOT"
case "$cmd_type" in
encrypted)
cmd=(/bin/sh -c "/usr/bin/security find-generic-password -s '$volume_uuid' -w | /usr/sbin/diskutil apfs unlockVolume '$volume_uuid' -mountpoint '$mountpoint' -stdinpassphrase")
;;
unencrypted)
cmd=(/usr/sbin/diskutil mount -mountPoint "$mountpoint" "$volume_uuid")
;;
*)
exit_with "Invalid first arg $cmd_type to generate_mount_command"
;;
esac
printf " %s\n" "${cmd[@]}"
}
generate_mount_daemon() {
local cmd_type="$1" # encrypted|unencrypted
local volume_uuid="$2"
cat <
RunAtLoad
Label
${HAB_SERVICE_TARGET}
ProgramArguments
$(generate_mount_command "$cmd_type" "$volume_uuid")
EOF
}
add_hab_vol_fstab_line() {
local uuid="$1"
local mountpoint="${HAB_ROOT}"
shift
cat > "$SCRATCH/ex_cleanroom_wrapper" <&2
/usr/bin/ex -u NONE -n /etc/synthetic.conf <&2
add_hab_vol_fstab_line "$volume_uuid" /usr/sbin/vifs
fi
}
encrypt_volume() {
local volume_uuid="$1"
local volume_label="$2"
local password
echo "Encrypt the Habitat volume" >&2
/usr/sbin/diskutil mount "$volume_label"
password="$(/usr/bin/xxd -l 32 -p -c 256 /dev/random)"
/usr/bin/security -i < 0 )); do
/usr/sbin/diskutil info "$HAB_ROOT" &>/dev/null && return 0 # If the volume is found, return successfully
((remaining_time--))
sleep 1
done
exit_with "Error: Volume did not appear within $timeout seconds."
}
setup_volume() {
local use_special use_uuid profile_packages
echo "Creating a Habitat volume" >&2
use_special="$(create_volume)"
/usr/sbin/diskutil unmount force "$use_special" || true # might not be mounted
use_uuid="$(volume_uuid_from_special "$use_special")"
readonly use_uuid
setup_fstab "$use_uuid"
if should_encrypt_volume; then
encrypt_volume "$use_uuid" "$HAB_VOLUME_LABEL"
setup_volume_daemon "encrypted" "$use_uuid"
elif volume_encrypted "$use_special"; then
setup_volume_daemon "encrypted" "$use_uuid"
else
setup_volume_daemon "unencrypted" "$use_uuid"
fi
await_volume
if [ "$(/usr/sbin/diskutil info -plist "$HAB_ROOT" | xmllint --xpath "(/plist/dict/key[text()='GlobalPermissionsEnabled'])/following-sibling::*[1]" -)" = "" ]; then
/usr/sbin/diskutil enableOwnership "$HAB_ROOT"
fi
}
setup_volume_daemon() {
local cmd_type="$1" # encrypted|unencrypted
local volume_uuid="$2"
if ! test_volume_daemon; then
echo "Configuring LaunchDaemon to mount '$HAB_VOLUME_LABEL'" >&2
/usr/bin/ex -u NONE -n "$HAB_VOLUME_MOUNTD_DEST" <&2
print_help >&2
exit_with "Invalid option" 1
;;
esac
done
info "Installing Habitat 'hab' program"
create_workdir
get_platform
validate_target
download_archive "$version" "$channel" "$target"
verify_archive
extract_archive
install_hab
print_hab_version
info "Installation of Habitat 'hab' program complete."
}
print_help() {
need_cmd cat
need_cmd basename
local _cmd
_cmd="$(basename "${0}")"
cat <<-HEREDOC
${_cmd}
Authors: The Habitat Maintainers
Installs the Habitat 'hab' program.
USAGE:
${_cmd} [FLAGS]
FLAGS:
-c Specifies a channel [values: stable, unstable] [default: stable]
-h Prints help information
-v Specifies a version (ex: 0.15.0, 0.15.0/20161222215311)
-t Specifies the ActiveTarget of the 'hab' program to download.
[values: x86_64-linux, aarch64-linux] [default: x86_64-linux]
This option is only valid on Linux platforms
ENVIRONMENT VARIABLES:
SSL_CERT_FILE allows you to verify against a custom cert such as one
generated from a corporate firewall
HEREDOC
}
create_workdir() {
need_cmd mktemp
need_cmd rm
need_cmd mkdir
if [ -n "${TMPDIR:-}" ]; then
local _tmp="${TMPDIR}"
elif [ -d /var/tmp ]; then
local _tmp=/var/tmp
else
local _tmp=/tmp
fi
workdir="$(mktemp -d -p "$_tmp" 2>/dev/null || mktemp -d "${_tmp}/hab.XXXX")"
# Add a trap to clean up any interrupted file downloads
# shellcheck disable=SC2154
trap 'code=$?; rm -rf $workdir; exit $code' INT TERM EXIT
cd "${workdir}"
}
get_platform() {
need_cmd uname
need_cmd tr
local _ostype
_ostype="$(uname -s)"
case "${_ostype}" in
Darwin | Linux)
sys="$(uname -s | tr '[:upper:]' '[:lower:]')"
arch="$(uname -m | tr '[:upper:]' '[:lower:]')"
arch=${arch/arm64/aarch64}
;;
*)
exit_with "Unrecognized OS type when determining platform: ${_ostype}" 2
;;
esac
case "${sys}" in
darwin)
need_cmd shasum
ext=zip
shasum_cmd="shasum -a 256"
;;
linux)
need_cmd sha256sum
ext=tar.gz
shasum_cmd="sha256sum"
;;
*)
exit_with "Unrecognized sys type when determining platform: ${sys}" 3
;;
esac
if [ -z "${target:-}" ]; then
target="${arch}-${sys}"
fi
}
# Validate the CLI Target requested. In most cases ${arch}-${sys}
# for the current system is the only valid Target. Creates an
# array of valid Targets for the current system,
# adding any valid alternate Targets, and checks if the requested
# Target is present in the array.
validate_target() {
local valid_targets=("${arch}-${sys}")
case "${sys}" in
linux)
valid_targets+=("x86_64-linux-kernel2")
;;
esac
if ! (_array_contains "${target}" "${valid_targets[@]}"); then
local _vts
printf -v _vts "%s, " "${valid_targets[@]}"
_e="${target} is not a valid target for this system. Please specify one of: [${_vts%, }]"
exit_with "$_e" 7
fi
}
download_archive() {
need_cmd mv
local _version="${1:-latest}"
local -r _channel="${2:?}"
local -r _target="${3:?}"
local url
if [ "$_version" == "latest" ]; then
url="${pcio_root}/${_channel}/habitat/latest/hab-${_target}.${ext}"
else
local -r _release="$(echo "${_version}" | cut -d'/' -f2)"
if [ "${_release:+release}" == "release" ]; then
_version="$(echo "${_version}" | cut -d'/' -f1)"
info "packages.chef.io does not support 'version/release' format. Using $_version for the version"
fi
url="${pcio_root}/habitat/${_version}/hab-${_target}.${ext}"
fi
dl_file "${url}" "${workdir}/hab-${_version}.${ext}"
dl_file "${url}.sha256sum" "${workdir}/hab-${_version}.${ext}.sha256sum"
archive="hab-${_target}.${ext}"
sha_file="hab-${_target}.${ext}.sha256sum"
mv -v "${workdir}/hab-${_version}.${ext}" "${archive}"
mv -v "${workdir}/hab-${_version}.${ext}.sha256sum" "${sha_file}"
if command -v gpg >/dev/null; then
info "GnuPG tooling found, downloading signatures"
sha_sig_file="${archive}.sha256sum.asc"
key_file="${workdir}/chef.asc"
local _key_url="https://packages.chef.io/chef.asc"
dl_file "${url}.sha256sum.asc" "${sha_sig_file}"
dl_file "${_key_url}" "${key_file}"
fi
}
verify_archive() {
if command -v gpg >/dev/null; then
info "GnuPG tooling found, verifying the shasum digest is properly signed"
gpg --no-permission-warning --dearmor "${key_file}"
gpg --no-permission-warning \
--keyring "${key_file}.gpg" --verify "${sha_sig_file}"
fi
info "Verifying the shasum digest matches the downloaded archive"
${shasum_cmd} -c "${sha_file}"
}
extract_archive() {
need_cmd sed
info "Extracting ${archive}"
case "${ext}" in
tar.gz)
need_cmd zcat
need_cmd tar
archive_dir="${archive%.tar.gz}"
mkdir "${archive_dir}"
zcat "${archive}" | tar --extract --directory "${archive_dir}" --strip-components=1
;;
zip)
need_cmd unzip
archive_dir="${archive%.zip}"
# -j "junk paths" Strips leading paths from files,
unzip -j "${archive}" -d "${archive_dir}"
;;
*)
exit_with "Unrecognized file extension when extracting: ${ext}" 4
;;
esac
}
install_hab() {
case "${sys}" in
darwin)
case "${arch}" in
x86_64)
# No core packages are available yet for x86_64; proceed with the old approach.
need_cmd mkdir
need_cmd install
info "Installing hab into /usr/local/bin"
mkdir -pv /usr/local/bin
install -v "${archive_dir}"/hab /usr/local/bin/hab
;;
aarch64)
setup_hab_root
local _ident="core/hab"
if [ -n "${version-}" ] && [ "${version}" != "latest" ]; then
_ident+="/$version"
fi
# The Habitat packages for macOS (aarch64) are not currently available in the SaaS Builder.
# This is a temporary fix until they become available.
_channel="${bldlChannel:-$channel}"
"${archive_dir}/hab" pkg install --binlink --force --channel "$_channel" "$_ident" ${bldrUrl:+-u "$bldrUrl"}
;;
*)
exit_with "Unrecognized sys when installing: ${sys}" 5
;;
esac
;;
linux)
local _ident="core/hab"
if [ -n "${version-}" ] && [ "${version}" != "latest" ]; then
_ident+="/$version"
fi
info "Installing Habitat package using temporarily downloaded hab"
# NOTE: For people (rightly) wondering why we download hab only to use it
# to install hab from Builder, the main reason is because it allows /bin/hab
# to be a binlink, meaning that future upgrades can be easily done via
# hab pkg install core/hab -bf and everything will Just Work. If we put
# the hab we downloaded into /bin, then future hab upgrades done via hab
# itself won't work - you'd need to run this script every time you wanted
# to upgrade hab, which is not intuitive. Putting it into a place other than
# /bin means now you have multiple copies of hab on your system and pathing
# shenanigans might ensue. Rather than deal with that mess, we do it this
# way.
"${archive_dir}/hab" pkg install --binlink --force --channel "$channel" "$_ident" ${bldrUrl:+-u "$bldrUrl"}
;;
*)
exit_with "Unrecognized sys when installing: ${sys}" 5
;;
esac
}
print_hab_version() {
need_cmd hab
info "Checking installed hab version"
hab --version
}
need_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
exit_with "Required command '$1' not found on PATH" 127
fi
}
info() {
echo "--> hab-install: $1"
}
warn() {
echo "xxx hab-install: $1" >&2
}
exit_with() {
warn "$1"
exit "${2:-10}"
}
_array_contains() {
local e
for e in "${@:2}"; do
if [[ "$e" == "$1" ]]; then
return 0
fi
done
return 1
}
dl_file() {
local _url="${1}"
local _dst="${2}"
local _code
local _wget_extra_args=""
local _curl_extra_args=""
# Attempt to download with wget, if found. If successful, quick return
if command -v wget >/dev/null; then
info "Downloading via wget: ${_url}"
if [ -n "${SSL_CERT_FILE:-}" ]; then
wget ${_wget_extra_args:+"--ca-certificate=${SSL_CERT_FILE}"} -q -O "${_dst}" "${_url}"
else
wget -q -O "${_dst}" "${_url}"
fi
_code="$?"
if [ $_code -eq 0 ]; then
return 0
else
local _e="wget failed to download file, perhaps wget doesn't have"
_e="$_e SSL support and/or no CA certificates are present?"
warn "$_e"
fi
fi
# Attempt to download with curl, if found. If successful, quick return
if command -v curl >/dev/null; then
info "Downloading via curl: ${_url}"
if [ -n "${SSL_CERT_FILE:-}" ]; then
curl ${_curl_extra_args:+"--cacert ${SSL_CERT_FILE}"} -sSfL "${_url}" -o "${_dst}"
else
curl -sSfL "${_url}" -o "${_dst}"
fi
_code="$?"
if [ $_code -eq 0 ]; then
return 0
else
local _e="curl failed to download file, perhaps curl doesn't have"
_e="$_e SSL support and/or no CA certificates are present?"
warn "$_e"
fi
fi
# If we reach this point, wget and curl have failed and we're out of options
exit_with "Required: SSL-enabled 'curl' or 'wget' on PATH with" 6
}
main "$@" || exit 99