Add opt-in shell lint and format targets
Introduce make lint-shell / fmt-shell with shellcheck and shfmt (-i 2 -ci -bn), normalize first-party scripts, and document the new targets. Keep them out of make lint / fmt. Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
@@ -47,7 +47,7 @@ GITHUB_RELEASES_URL="https://github.com/getprobo/probo/releases/download"
|
||||
# Injected at release time by .github/workflows/release-probo-agent.yaml
|
||||
RELEASE_TAG="__PROBO_AGENT_RELEASE_TAG__"
|
||||
if [ -n "${PROBO_AGENT_RELEASE_TAG:-}" ]; then
|
||||
RELEASE_TAG="$PROBO_AGENT_RELEASE_TAG"
|
||||
RELEASE_TAG="$PROBO_AGENT_RELEASE_TAG"
|
||||
fi
|
||||
|
||||
RELEASE_BASE="${PROBO_AGENT_RELEASE_BASE:-}"
|
||||
@@ -58,25 +58,25 @@ NO_AUTO_UPDATE="${PROBO_NO_AUTO_UPDATE:-}"
|
||||
SKIP_SERVICE=false
|
||||
|
||||
die() {
|
||||
echo "error: $*" >&2
|
||||
exit 1
|
||||
echo "error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
can_prompt() {
|
||||
[ -t 0 ] && return 0
|
||||
[ -r /dev/tty ] && [ -w /dev/tty ]
|
||||
[ -t 0 ] && return 0
|
||||
[ -r /dev/tty ] && [ -w /dev/tty ]
|
||||
}
|
||||
|
||||
read_user() {
|
||||
if [ -t 0 ]; then
|
||||
IFS= read -r "$1"
|
||||
else
|
||||
IFS= read -r "$1" < /dev/tty
|
||||
fi
|
||||
if [ -t 0 ]; then
|
||||
IFS= read -r "$1"
|
||||
else
|
||||
IFS= read -r "$1" </dev/tty
|
||||
fi
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
cat <<'EOF'
|
||||
probo-agent installer for Darwin, Linux, and FreeBSD.
|
||||
|
||||
Usage:
|
||||
@@ -95,271 +95,271 @@ EOF
|
||||
}
|
||||
|
||||
embedded_checksums() {
|
||||
cat <<'EOF'
|
||||
cat <<'EOF'
|
||||
# __PROBO_AGENT_CHECKSUMS_BEGIN__
|
||||
# __PROBO_AGENT_CHECKSUMS_END__
|
||||
EOF
|
||||
}
|
||||
|
||||
resolve_embedded_release() {
|
||||
if [ "$RELEASE_TAG" = "__PROBO_AGENT_RELEASE_TAG__" ]; then
|
||||
die "this install.sh was not published by a probo-agent release; curl install.sh from the target release"
|
||||
fi
|
||||
if [ "$RELEASE_TAG" = "__PROBO_AGENT_RELEASE_TAG__" ]; then
|
||||
die "this install.sh was not published by a probo-agent release; curl install.sh from the target release"
|
||||
fi
|
||||
|
||||
if [ -n "$RELEASE_BASE" ]; then
|
||||
RELEASE_BASE="${RELEASE_BASE%/}"
|
||||
case "$RELEASE_BASE" in
|
||||
*/"$RELEASE_TAG") ;;
|
||||
*) die "PROBO_AGENT_RELEASE_BASE must end with release tag ${RELEASE_TAG}" ;;
|
||||
esac
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$RELEASE_BASE" ]; then
|
||||
RELEASE_BASE="${RELEASE_BASE%/}"
|
||||
case "$RELEASE_BASE" in
|
||||
*/"$RELEASE_TAG") ;;
|
||||
*) die "PROBO_AGENT_RELEASE_BASE must end with release tag ${RELEASE_TAG}" ;;
|
||||
esac
|
||||
return 0
|
||||
fi
|
||||
|
||||
RELEASE_BASE="${GITHUB_RELEASES_URL}/${RELEASE_TAG}"
|
||||
printf 'Using release %s\n' "$RELEASE_TAG"
|
||||
RELEASE_BASE="${GITHUB_RELEASES_URL}/${RELEASE_TAG}"
|
||||
printf 'Using release %s\n' "$RELEASE_TAG"
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
die "required command not found: $1"
|
||||
fi
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
die "required command not found: $1"
|
||||
fi
|
||||
}
|
||||
|
||||
detect_platform() {
|
||||
os="$(uname -s)"
|
||||
arch="$(uname -m)"
|
||||
os="$(uname -s)"
|
||||
arch="$(uname -m)"
|
||||
|
||||
case "$os" in
|
||||
Darwin) os_label="Darwin" ;;
|
||||
Linux) os_label="Linux" ;;
|
||||
FreeBSD) os_label="Freebsd" ;;
|
||||
*) die "unsupported operating system: $os (Darwin, Linux, and FreeBSD only)" ;;
|
||||
esac
|
||||
case "$os" in
|
||||
Darwin) os_label="Darwin" ;;
|
||||
Linux) os_label="Linux" ;;
|
||||
FreeBSD) os_label="Freebsd" ;;
|
||||
*) die "unsupported operating system: $os (Darwin, Linux, and FreeBSD only)" ;;
|
||||
esac
|
||||
|
||||
case "$arch" in
|
||||
x86_64 | amd64) arch_label="x86_64" ;;
|
||||
arm64 | aarch64) arch_label="arm64" ;;
|
||||
*) die "unsupported CPU architecture: $arch" ;;
|
||||
esac
|
||||
case "$arch" in
|
||||
x86_64 | amd64) arch_label="x86_64" ;;
|
||||
arm64 | aarch64) arch_label="arm64" ;;
|
||||
*) die "unsupported CPU architecture: $arch" ;;
|
||||
esac
|
||||
|
||||
archive_dir="probo-agent_${os_label}_${arch_label}"
|
||||
archive_name="${archive_dir}.tar.gz"
|
||||
archive_dir="probo-agent_${os_label}_${arch_label}"
|
||||
archive_name="${archive_dir}.tar.gz"
|
||||
}
|
||||
|
||||
sha256_file() {
|
||||
file="$1"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$file" | awk '{print $1}'
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "$file" | awk '{print $1}'
|
||||
elif command -v sha256 >/dev/null 2>&1; then
|
||||
sha256 -q "$file"
|
||||
else
|
||||
die "no sha256 tool found (need sha256sum, shasum, or sha256)"
|
||||
fi
|
||||
file="$1"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$file" | awk '{print $1}'
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "$file" | awk '{print $1}'
|
||||
elif command -v sha256 >/dev/null 2>&1; then
|
||||
sha256 -q "$file"
|
||||
else
|
||||
die "no sha256 tool found (need sha256sum, shasum, or sha256)"
|
||||
fi
|
||||
}
|
||||
|
||||
verify_embedded_checksum() {
|
||||
case "${PROBO_AGENT_SKIP_CHECKSUM_VERIFY:-}" in
|
||||
1 | true | TRUE | yes | YES) return 0 ;;
|
||||
esac
|
||||
case "${PROBO_AGENT_SKIP_CHECKSUM_VERIFY:-}" in
|
||||
1 | true | TRUE | yes | YES) return 0 ;;
|
||||
esac
|
||||
|
||||
archive_file="$1"
|
||||
archive_basename="$(basename "$archive_file")"
|
||||
archive_file="$1"
|
||||
archive_basename="$(basename "$archive_file")"
|
||||
|
||||
expected="$(
|
||||
embedded_checksums | awk -v name="$archive_basename" '
|
||||
expected="$(
|
||||
embedded_checksums | awk -v name="$archive_basename" '
|
||||
$0 ~ /^#/ { next }
|
||||
$2 == name { print $1; exit }
|
||||
'
|
||||
)"
|
||||
if [ -z "$expected" ]; then
|
||||
die "archive ${archive_basename} not found in embedded release checksums"
|
||||
fi
|
||||
)"
|
||||
if [ -z "$expected" ]; then
|
||||
die "archive ${archive_basename} not found in embedded release checksums"
|
||||
fi
|
||||
|
||||
actual="$(sha256_file "$archive_file")"
|
||||
if [ "$expected" != "$actual" ]; then
|
||||
die "checksum mismatch for ${archive_file}"
|
||||
fi
|
||||
actual="$(sha256_file "$archive_file")"
|
||||
if [ "$expected" != "$actual" ]; then
|
||||
die "checksum mismatch for ${archive_file}"
|
||||
fi
|
||||
}
|
||||
|
||||
read_secret() {
|
||||
prompt_text="$1"
|
||||
printf '%s' "$prompt_text"
|
||||
if command -v stty >/dev/null 2>&1; then
|
||||
if [ -t 0 ]; then
|
||||
old_stty="$(stty -g 2>/dev/null || true)"
|
||||
stty -echo 2>/dev/null || true
|
||||
IFS= read -r REPLY || REPLY=""
|
||||
printf '\n'
|
||||
if [ -n "${old_stty:-}" ]; then
|
||||
stty "$old_stty" 2>/dev/null || stty echo 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
old_stty="$(stty -g < /dev/tty 2>/dev/null || true)"
|
||||
stty -echo < /dev/tty 2>/dev/null || true
|
||||
IFS= read -r REPLY < /dev/tty || REPLY=""
|
||||
printf '\n' >/dev/tty
|
||||
if [ -n "${old_stty:-}" ]; then
|
||||
stty "$old_stty" < /dev/tty 2>/dev/null || stty echo < /dev/tty 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
else
|
||||
read_user REPLY
|
||||
fi
|
||||
ENROLLMENT_TOKEN="$REPLY"
|
||||
prompt_text="$1"
|
||||
printf '%s' "$prompt_text"
|
||||
if command -v stty >/dev/null 2>&1; then
|
||||
if [ -t 0 ]; then
|
||||
old_stty="$(stty -g 2>/dev/null || true)"
|
||||
stty -echo 2>/dev/null || true
|
||||
IFS= read -r REPLY || REPLY=""
|
||||
printf '\n'
|
||||
if [ -n "${old_stty:-}" ]; then
|
||||
stty "$old_stty" 2>/dev/null || stty echo 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
old_stty="$(stty -g </dev/tty 2>/dev/null || true)"
|
||||
stty -echo </dev/tty 2>/dev/null || true
|
||||
IFS= read -r REPLY </dev/tty || REPLY=""
|
||||
printf '\n' >/dev/tty
|
||||
if [ -n "${old_stty:-}" ]; then
|
||||
stty "$old_stty" </dev/tty 2>/dev/null || stty echo </dev/tty 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
else
|
||||
read_user REPLY
|
||||
fi
|
||||
ENROLLMENT_TOKEN="$REPLY"
|
||||
}
|
||||
|
||||
prompt_server_url() {
|
||||
if [ -n "$SERVER_URL" ]; then
|
||||
return 0
|
||||
fi
|
||||
if ! can_prompt; then
|
||||
die "PROBO_SERVER_URL is required in non-interactive mode"
|
||||
fi
|
||||
if [ -n "$SERVER_URL" ]; then
|
||||
return 0
|
||||
fi
|
||||
if ! can_prompt; then
|
||||
die "PROBO_SERVER_URL is required in non-interactive mode"
|
||||
fi
|
||||
|
||||
printf '\nProbo server URL:\n'
|
||||
printf ' 1) https://us.probo.com (United States)\n'
|
||||
printf ' 2) https://eu.probo.com (European Union)\n'
|
||||
printf ' 3) Enter a custom URL\n'
|
||||
printf 'Choice [1]: '
|
||||
read_user choice
|
||||
printf '\nProbo server URL:\n'
|
||||
printf ' 1) https://us.probo.com (United States)\n'
|
||||
printf ' 2) https://eu.probo.com (European Union)\n'
|
||||
printf ' 3) Enter a custom URL\n'
|
||||
printf 'Choice [1]: '
|
||||
read_user choice
|
||||
|
||||
case "${choice:-1}" in
|
||||
1 | "") SERVER_URL="https://us.probo.com" ;;
|
||||
2) SERVER_URL="https://eu.probo.com" ;;
|
||||
3)
|
||||
printf 'Server URL: '
|
||||
read_user SERVER_URL
|
||||
;;
|
||||
*) SERVER_URL="$choice" ;;
|
||||
esac
|
||||
case "${choice:-1}" in
|
||||
1 | "") SERVER_URL="https://us.probo.com" ;;
|
||||
2) SERVER_URL="https://eu.probo.com" ;;
|
||||
3)
|
||||
printf 'Server URL: '
|
||||
read_user SERVER_URL
|
||||
;;
|
||||
*) SERVER_URL="$choice" ;;
|
||||
esac
|
||||
|
||||
if [ -z "$SERVER_URL" ]; then
|
||||
die "server URL is required"
|
||||
fi
|
||||
if [ -z "$SERVER_URL" ]; then
|
||||
die "server URL is required"
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_enrollment_token() {
|
||||
if [ -n "$ENROLLMENT_TOKEN" ]; then
|
||||
return 0
|
||||
fi
|
||||
if ! can_prompt; then
|
||||
die "PROBO_ENROLLMENT_TOKEN is required in non-interactive mode"
|
||||
fi
|
||||
if [ -n "$ENROLLMENT_TOKEN" ]; then
|
||||
return 0
|
||||
fi
|
||||
if ! can_prompt; then
|
||||
die "PROBO_ENROLLMENT_TOKEN is required in non-interactive mode"
|
||||
fi
|
||||
|
||||
read_secret "Enrollment token: "
|
||||
if [ -z "$ENROLLMENT_TOKEN" ]; then
|
||||
die "enrollment token is required"
|
||||
fi
|
||||
read_secret "Enrollment token: "
|
||||
if [ -z "$ENROLLMENT_TOKEN" ]; then
|
||||
die "enrollment token is required"
|
||||
fi
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--server)
|
||||
[ $# -ge 2 ] || die "--server requires a value"
|
||||
SERVER_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--enrollment-token)
|
||||
[ $# -ge 2 ] || die "--enrollment-token requires a value"
|
||||
ENROLLMENT_TOKEN="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-auto-update)
|
||||
NO_AUTO_UPDATE=true
|
||||
shift
|
||||
;;
|
||||
--skip-service)
|
||||
SKIP_SERVICE=true
|
||||
shift
|
||||
;;
|
||||
--dir)
|
||||
[ $# -ge 2 ] || die "--dir requires a value"
|
||||
STATE_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "unknown option: $1 (try --help)"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--server)
|
||||
[ $# -ge 2 ] || die "--server requires a value"
|
||||
SERVER_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--enrollment-token)
|
||||
[ $# -ge 2 ] || die "--enrollment-token requires a value"
|
||||
ENROLLMENT_TOKEN="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-auto-update)
|
||||
NO_AUTO_UPDATE=true
|
||||
shift
|
||||
;;
|
||||
--skip-service)
|
||||
SKIP_SERVICE=true
|
||||
shift
|
||||
;;
|
||||
--dir)
|
||||
[ $# -ge 2 ] || die "--dir requires a value"
|
||||
STATE_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "unknown option: $1 (try --help)"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
run_agent_install() {
|
||||
set -- --server "$SERVER_URL" --enrollment-token "$ENROLLMENT_TOKEN"
|
||||
if [ -n "$STATE_DIR" ]; then
|
||||
set -- "$@" --dir "$STATE_DIR"
|
||||
fi
|
||||
case "$NO_AUTO_UPDATE" in
|
||||
1 | true | TRUE | yes | YES) set -- "$@" --no-auto-update ;;
|
||||
esac
|
||||
case "$SKIP_SERVICE" in
|
||||
1 | true | TRUE | yes | YES) set -- "$@" --skip-service ;;
|
||||
esac
|
||||
"$BINARY_PATH" install "$@"
|
||||
set -- --server "$SERVER_URL" --enrollment-token "$ENROLLMENT_TOKEN"
|
||||
if [ -n "$STATE_DIR" ]; then
|
||||
set -- "$@" --dir "$STATE_DIR"
|
||||
fi
|
||||
case "$NO_AUTO_UPDATE" in
|
||||
1 | true | TRUE | yes | YES) set -- "$@" --no-auto-update ;;
|
||||
esac
|
||||
case "$SKIP_SERVICE" in
|
||||
1 | true | TRUE | yes | YES) set -- "$@" --skip-service ;;
|
||||
esac
|
||||
"$BINARY_PATH" install "$@"
|
||||
}
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
parse_args "$@"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
die "this installer must run as root; re-run with: curl -fsSL \"…/install.sh\" | sudo sh"
|
||||
fi
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
die "this installer must run as root; re-run with: curl -fsSL \"…/install.sh\" | sudo sh"
|
||||
fi
|
||||
|
||||
require_cmd curl
|
||||
require_cmd tar
|
||||
require_cmd install
|
||||
require_cmd curl
|
||||
require_cmd tar
|
||||
require_cmd install
|
||||
|
||||
detect_platform
|
||||
resolve_embedded_release
|
||||
detect_platform
|
||||
resolve_embedded_release
|
||||
|
||||
workdir="$(mktemp -d "${TMPDIR:-/tmp}/probo-agent-install.XXXXXX")"
|
||||
trap 'rm -rf "$workdir"' EXIT INT HUP TERM
|
||||
workdir="$(mktemp -d "${TMPDIR:-/tmp}/probo-agent-install.XXXXXX")"
|
||||
trap 'rm -rf "$workdir"' EXIT INT HUP TERM
|
||||
|
||||
printf 'Downloading probo-agent %s …\n' "$archive_name"
|
||||
printf 'Downloading probo-agent %s …\n' "$archive_name"
|
||||
|
||||
curl -fsSL "${RELEASE_BASE}/${archive_name}" -o "${workdir}/${archive_name}"
|
||||
curl -fsSL "${RELEASE_BASE}/${archive_name}" -o "${workdir}/${archive_name}"
|
||||
|
||||
verify_embedded_checksum "${workdir}/${archive_name}"
|
||||
verify_embedded_checksum "${workdir}/${archive_name}"
|
||||
|
||||
tar -xzf "${workdir}/${archive_name}" -C "$workdir"
|
||||
if [ ! -f "${workdir}/${archive_dir}/probo-agent" ]; then
|
||||
die "archive did not contain probo-agent binary"
|
||||
fi
|
||||
tar -xzf "${workdir}/${archive_name}" -C "$workdir"
|
||||
if [ ! -f "${workdir}/${archive_dir}/probo-agent" ]; then
|
||||
die "archive did not contain probo-agent binary"
|
||||
fi
|
||||
|
||||
install -m 0755 "${workdir}/${archive_dir}/probo-agent" "$BINARY_PATH"
|
||||
printf 'Installed %s\n' "$BINARY_PATH"
|
||||
install -m 0755 "${workdir}/${archive_dir}/probo-agent" "$BINARY_PATH"
|
||||
printf 'Installed %s\n' "$BINARY_PATH"
|
||||
|
||||
prompt_server_url
|
||||
prompt_enrollment_token
|
||||
prompt_server_url
|
||||
prompt_enrollment_token
|
||||
|
||||
printf 'Enrolling device …\n'
|
||||
if run_agent_install; then
|
||||
enroll_ok=true
|
||||
else
|
||||
enroll_ok=false
|
||||
fi
|
||||
printf 'Enrolling device …\n'
|
||||
if run_agent_install; then
|
||||
enroll_ok=true
|
||||
else
|
||||
enroll_ok=false
|
||||
fi
|
||||
|
||||
if [ "$enroll_ok" = true ]; then
|
||||
case "$SKIP_SERVICE" in
|
||||
1 | true | TRUE | yes | YES)
|
||||
printf 'Device enrolled (service installation skipped).\n'
|
||||
;;
|
||||
*)
|
||||
printf 'Device enrolled and service installed.\n'
|
||||
;;
|
||||
esac
|
||||
else
|
||||
printf 'warning: probo-agent install failed; binary is at %s\n' "$BINARY_PATH" >&2
|
||||
printf 'Re-run: %s install --server … --enrollment-token …\n' "$BINARY_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$enroll_ok" = true ]; then
|
||||
case "$SKIP_SERVICE" in
|
||||
1 | true | TRUE | yes | YES)
|
||||
printf 'Device enrolled (service installation skipped).\n'
|
||||
;;
|
||||
*)
|
||||
printf 'Device enrolled and service installed.\n'
|
||||
;;
|
||||
esac
|
||||
else
|
||||
printf 'warning: probo-agent install failed; binary is at %s\n' "$BINARY_PATH" >&2
|
||||
printf 'Re-run: %s install --server … --enrollment-token …\n' "$BINARY_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -56,97 +56,116 @@ APPLE_TEAM_ID="${APPLE_TEAM_ID:-}"
|
||||
NOTARYTOOL_KEYCHAIN_PROFILE="${NOTARYTOOL_KEYCHAIN_PROFILE:-probo-agent-notary}"
|
||||
|
||||
usage() {
|
||||
sed -ne '/^#/!q; s/^# \{0,1\}//; 2,$ p' < "$0"
|
||||
sed -ne '/^#/!q; s/^# \{0,1\}//; 2,$ p' <"$0"
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--binary) BINARY="$2"; shift 2 ;;
|
||||
--version) VERSION="$2"; shift 2 ;;
|
||||
--output) OUTPUT="$2"; shift 2 ;;
|
||||
--identifier) IDENTIFIER="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "unknown flag: $1" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
case "$1" in
|
||||
--binary)
|
||||
BINARY="$2"
|
||||
shift 2
|
||||
;;
|
||||
--version)
|
||||
VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
OUTPUT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--identifier)
|
||||
IDENTIFIER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "unknown flag: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "${BINARY}" ] || [ ! -x "${BINARY}" ]; then
|
||||
echo "error: --binary <path-to-probo-agent> is required and must be executable" >&2
|
||||
exit 2
|
||||
echo "error: --binary <path-to-probo-agent> is required and must be executable" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Distribution.xml advertises hostArchitectures=arm64,x86_64. Refuse a
|
||||
# binary that lacks either slice so Installer cannot install on a CPU
|
||||
# the agent cannot run on.
|
||||
if ! command -v lipo >/dev/null 2>&1; then
|
||||
echo "error: lipo is required to validate --binary architecture (run on macOS)" >&2
|
||||
exit 1
|
||||
echo "error: lipo is required to validate --binary architecture (run on macOS)" >&2
|
||||
exit 1
|
||||
fi
|
||||
BINARY_ARCHS="$(lipo -archs "${BINARY}")"
|
||||
has_arm64=false
|
||||
has_x86_64=false
|
||||
for arch_slice in ${BINARY_ARCHS}; do
|
||||
case "${arch_slice}" in
|
||||
arm64) has_arm64=true ;;
|
||||
x86_64) has_x86_64=true ;;
|
||||
esac
|
||||
case "${arch_slice}" in
|
||||
arm64) has_arm64=true ;;
|
||||
x86_64) has_x86_64=true ;;
|
||||
esac
|
||||
done
|
||||
if [ "${has_arm64}" != true ] || [ "${has_x86_64}" != true ]; then
|
||||
echo "error: --binary must be a fat binary with arm64 and x86_64 slices (got: ${BINARY_ARCHS}); use lipo -create" >&2
|
||||
exit 2
|
||||
echo "error: --binary must be a fat binary with arm64 and x86_64 slices (got: ${BINARY_ARCHS}); use lipo -create" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ -z "${VERSION}" ]; then
|
||||
VERSION="$(cat "${REPO_ROOT}/cmd/probo-agent/VERSION")"
|
||||
VERSION="$(cat "${REPO_ROOT}/cmd/probo-agent/VERSION")"
|
||||
fi
|
||||
if [ -z "${OUTPUT}" ]; then
|
||||
mkdir -p "${REPO_ROOT}/dist"
|
||||
OUTPUT="${REPO_ROOT}/dist/probo-agent_${VERSION}_darwin.pkg"
|
||||
mkdir -p "${REPO_ROOT}/dist"
|
||||
OUTPUT="${REPO_ROOT}/dist/probo-agent_${VERSION}_darwin.pkg"
|
||||
fi
|
||||
|
||||
if ! command -v pkgbuild >/dev/null 2>&1 || ! command -v productbuild >/dev/null 2>&1; then
|
||||
echo "error: pkgbuild and productbuild are required (run on macOS)" >&2
|
||||
exit 1
|
||||
echo "error: pkgbuild and productbuild are required (run on macOS)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v swift >/dev/null 2>&1; then
|
||||
echo "error: swift is required to build Probo Agent.app (run on macOS)" >&2
|
||||
exit 1
|
||||
echo "error: swift is required to build Probo Agent.app (run on macOS)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${CODESIGN_IDENTITY}" ]; then
|
||||
echo "error: CODESIGN_IDENTITY is required (privileged helper must be signed)" >&2
|
||||
exit 2
|
||||
echo "error: CODESIGN_IDENTITY is required (privileged helper must be signed)" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ -z "${APPLE_TEAM_ID}" ]; then
|
||||
echo "error: APPLE_TEAM_ID is required (SMAuthorizedClients team requirement)" >&2
|
||||
exit 2
|
||||
echo "error: APPLE_TEAM_ID is required (SMAuthorizedClients team requirement)" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
notarize_enabled=false
|
||||
if [ -n "${APPLE_ID}" ] && [ -n "${APPLE_ID_PASSWORD}" ]; then
|
||||
notarize_enabled=true
|
||||
notarize_enabled=true
|
||||
fi
|
||||
if [ "${notarize_enabled}" = true ] && [ -z "${INSTALLER_IDENTITY}" ]; then
|
||||
echo "error: notarization requires INSTALLER_IDENTITY" >&2
|
||||
exit 2
|
||||
echo "error: notarization requires INSTALLER_IDENTITY" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
codesign_runtime() {
|
||||
local path="$1"
|
||||
codesign \
|
||||
--force \
|
||||
--options runtime \
|
||||
--timestamp \
|
||||
--sign "${CODESIGN_IDENTITY}" \
|
||||
"${path}"
|
||||
codesign --verify --verbose=2 "${path}"
|
||||
local path="$1"
|
||||
codesign \
|
||||
--force \
|
||||
--options runtime \
|
||||
--timestamp \
|
||||
--sign "${CODESIGN_IDENTITY}" \
|
||||
"${path}"
|
||||
codesign --verify --verbose=2 "${path}"
|
||||
}
|
||||
|
||||
client_requirement() {
|
||||
printf 'anchor apple generic and identifier "com.probo.agent.url-handler" and certificate leaf[subject.OU] = "%s"' "${APPLE_TEAM_ID}"
|
||||
printf 'anchor apple generic and identifier "com.probo.agent.url-handler" and certificate leaf[subject.OU] = "%s"' "${APPLE_TEAM_ID}"
|
||||
}
|
||||
|
||||
team_id_option() {
|
||||
printf '"%s"' "${APPLE_TEAM_ID}"
|
||||
printf '"%s"' "${APPLE_TEAM_ID}"
|
||||
}
|
||||
|
||||
# Build AppIcon.icns from the single committed master PNG
|
||||
@@ -154,213 +173,213 @@ team_id_option() {
|
||||
# pad-then-resize pipeline. Writes under STAGE; does not touch the
|
||||
# source tree.
|
||||
generate_app_icon_icns() {
|
||||
local icon_original="$1"
|
||||
local icns_out="$2"
|
||||
local icon_dir padded tmp iconset
|
||||
local icon_original="$1"
|
||||
local icns_out="$2"
|
||||
local icon_dir padded tmp iconset
|
||||
|
||||
if [ ! -f "${icon_original}" ]; then
|
||||
echo "error: missing app icon source ${icon_original}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "${icon_original}" ]; then
|
||||
echo "error: missing app icon source ${icon_original}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
icon_dir="${STAGE}/app-icon"
|
||||
padded="${icon_dir}/icon-padded.png"
|
||||
tmp="${icon_dir}/icon-padded.tmp.png"
|
||||
iconset="${icon_dir}/AppIcon.iconset"
|
||||
rm -rf "${icon_dir}"
|
||||
mkdir -p "${iconset}"
|
||||
icon_dir="${STAGE}/app-icon"
|
||||
padded="${icon_dir}/icon-padded.png"
|
||||
tmp="${icon_dir}/icon-padded.tmp.png"
|
||||
iconset="${icon_dir}/AppIcon.iconset"
|
||||
rm -rf "${icon_dir}"
|
||||
mkdir -p "${iconset}"
|
||||
|
||||
# 10% padding on all sides (960 content inside 1200 canvas).
|
||||
sips -z 960 960 "${icon_original}" --out "${tmp}" >/dev/null
|
||||
sips --padToHeightWidth 1200 1200 "${tmp}" --out "${padded}" >/dev/null
|
||||
rm -f "${tmp}"
|
||||
# 10% padding on all sides (960 content inside 1200 canvas).
|
||||
sips -z 960 960 "${icon_original}" --out "${tmp}" >/dev/null
|
||||
sips --padToHeightWidth 1200 1200 "${tmp}" --out "${padded}" >/dev/null
|
||||
rm -f "${tmp}"
|
||||
|
||||
# Write through temp names: sips mishandles @2x suffixes in --out paths.
|
||||
sips -z 16 16 "${padded}" --out "${icon_dir}/16.png" >/dev/null
|
||||
sips -z 32 32 "${padded}" --out "${icon_dir}/32.png" >/dev/null
|
||||
sips -z 64 64 "${padded}" --out "${icon_dir}/64.png" >/dev/null
|
||||
sips -z 128 128 "${padded}" --out "${icon_dir}/128.png" >/dev/null
|
||||
sips -z 256 256 "${padded}" --out "${icon_dir}/256.png" >/dev/null
|
||||
sips -z 512 512 "${padded}" --out "${icon_dir}/512.png" >/dev/null
|
||||
sips -z 1024 1024 "${padded}" --out "${icon_dir}/1024.png" >/dev/null
|
||||
# Write through temp names: sips mishandles @2x suffixes in --out paths.
|
||||
sips -z 16 16 "${padded}" --out "${icon_dir}/16.png" >/dev/null
|
||||
sips -z 32 32 "${padded}" --out "${icon_dir}/32.png" >/dev/null
|
||||
sips -z 64 64 "${padded}" --out "${icon_dir}/64.png" >/dev/null
|
||||
sips -z 128 128 "${padded}" --out "${icon_dir}/128.png" >/dev/null
|
||||
sips -z 256 256 "${padded}" --out "${icon_dir}/256.png" >/dev/null
|
||||
sips -z 512 512 "${padded}" --out "${icon_dir}/512.png" >/dev/null
|
||||
sips -z 1024 1024 "${padded}" --out "${icon_dir}/1024.png" >/dev/null
|
||||
|
||||
# Build @2x names via concatenation so the shell never treats @ as a
|
||||
# glob qualifier (and sips is never asked to write those paths).
|
||||
local at2x
|
||||
at2x='@2x.png'
|
||||
cp "${icon_dir}/16.png" "${iconset}/icon_16x16.png"
|
||||
cp "${icon_dir}/32.png" "${iconset}/icon_16x16${at2x}"
|
||||
cp "${icon_dir}/32.png" "${iconset}/icon_32x32.png"
|
||||
cp "${icon_dir}/64.png" "${iconset}/icon_32x32${at2x}"
|
||||
cp "${icon_dir}/128.png" "${iconset}/icon_128x128.png"
|
||||
cp "${icon_dir}/256.png" "${iconset}/icon_128x128${at2x}"
|
||||
cp "${icon_dir}/256.png" "${iconset}/icon_256x256.png"
|
||||
cp "${icon_dir}/512.png" "${iconset}/icon_256x256${at2x}"
|
||||
cp "${icon_dir}/512.png" "${iconset}/icon_512x512.png"
|
||||
cp "${icon_dir}/1024.png" "${iconset}/icon_512x512${at2x}"
|
||||
# Build @2x names via concatenation so the shell never treats @ as a
|
||||
# glob qualifier (and sips is never asked to write those paths).
|
||||
local at2x
|
||||
at2x='@2x.png'
|
||||
cp "${icon_dir}/16.png" "${iconset}/icon_16x16.png"
|
||||
cp "${icon_dir}/32.png" "${iconset}/icon_16x16${at2x}"
|
||||
cp "${icon_dir}/32.png" "${iconset}/icon_32x32.png"
|
||||
cp "${icon_dir}/64.png" "${iconset}/icon_32x32${at2x}"
|
||||
cp "${icon_dir}/128.png" "${iconset}/icon_128x128.png"
|
||||
cp "${icon_dir}/256.png" "${iconset}/icon_128x128${at2x}"
|
||||
cp "${icon_dir}/256.png" "${iconset}/icon_256x256.png"
|
||||
cp "${icon_dir}/512.png" "${iconset}/icon_256x256${at2x}"
|
||||
cp "${icon_dir}/512.png" "${iconset}/icon_512x512.png"
|
||||
cp "${icon_dir}/1024.png" "${iconset}/icon_512x512${at2x}"
|
||||
|
||||
iconutil -c icns "${iconset}" -o "${icns_out}"
|
||||
iconutil -c icns "${iconset}" -o "${icns_out}"
|
||||
}
|
||||
|
||||
# Build Probo Agent.app (URL handler + embedded privileged helper) into
|
||||
# parent_dir. Signs nested Mach-Os then the .app bundle (bottom-up).
|
||||
build_probo_agent_app() {
|
||||
local parent_dir="$1"
|
||||
local build_dir render_dir
|
||||
local helper_info_plist helper_launchd_plist
|
||||
local helper_binary url_handler_binary bin_dir
|
||||
local app_root contents macos resources launch_services launch_daemons
|
||||
local plist embedded_helper embedded_launchd
|
||||
local helper_requirement app_icon_original app_icon_icns
|
||||
local -a helper_linker_flags swift_arch_args
|
||||
local parent_dir="$1"
|
||||
local build_dir render_dir
|
||||
local helper_info_plist helper_launchd_plist
|
||||
local helper_binary url_handler_binary bin_dir
|
||||
local app_root contents macos resources launch_services launch_daemons
|
||||
local plist embedded_helper embedded_launchd
|
||||
local helper_requirement app_icon_original app_icon_icns
|
||||
local -a helper_linker_flags swift_arch_args
|
||||
|
||||
build_dir="${STAGE}/enroll-ui-build"
|
||||
render_dir="${build_dir}/rendered"
|
||||
mkdir -p "${render_dir}"
|
||||
build_dir="${STAGE}/enroll-ui-build"
|
||||
render_dir="${build_dir}/rendered"
|
||||
mkdir -p "${render_dir}"
|
||||
|
||||
swift_arch_args=(--arch arm64 --arch x86_64)
|
||||
swift_arch_args=(--arch arm64 --arch x86_64)
|
||||
|
||||
sed \
|
||||
-e "s|@@VERSION@@|${VERSION}|g" \
|
||||
"${ENROLL_UI_DIR}/Shared/HelperVersion.generated.swift.tmpl" \
|
||||
> "${ENROLL_UI_DIR}/Shared/HelperVersion.generated.swift"
|
||||
sed \
|
||||
-e "s|@@VERSION@@|${VERSION}|g" \
|
||||
"${ENROLL_UI_DIR}/Shared/HelperVersion.generated.swift.tmpl" \
|
||||
>"${ENROLL_UI_DIR}/Shared/HelperVersion.generated.swift"
|
||||
|
||||
sed \
|
||||
-e "s|@@TEAM_ID_OPTION@@|$(team_id_option)|g" \
|
||||
"${ENROLL_UI_DIR}/Shared/SigningConstants.generated.swift.tmpl" \
|
||||
> "${ENROLL_UI_DIR}/Shared/SigningConstants.generated.swift"
|
||||
sed \
|
||||
-e "s|@@TEAM_ID_OPTION@@|$(team_id_option)|g" \
|
||||
"${ENROLL_UI_DIR}/Shared/SigningConstants.generated.swift.tmpl" \
|
||||
>"${ENROLL_UI_DIR}/Shared/SigningConstants.generated.swift"
|
||||
|
||||
helper_info_plist="${render_dir}/helper-info.plist"
|
||||
helper_launchd_plist="${render_dir}/helper-launchd.plist"
|
||||
helper_info_plist="${render_dir}/helper-info.plist"
|
||||
helper_launchd_plist="${render_dir}/helper-launchd.plist"
|
||||
|
||||
sed \
|
||||
-e "s|@@VERSION@@|${VERSION}|g" \
|
||||
-e "s|@@CLIENT_DESIGNATED_REQUIREMENT@@|$(client_requirement)|g" \
|
||||
"${ENROLL_UI_DIR}/HelperTool/Info.plist.tmpl" > "${helper_info_plist}"
|
||||
sed \
|
||||
-e "s|@@VERSION@@|${VERSION}|g" \
|
||||
-e "s|@@CLIENT_DESIGNATED_REQUIREMENT@@|$(client_requirement)|g" \
|
||||
"${ENROLL_UI_DIR}/HelperTool/Info.plist.tmpl" >"${helper_info_plist}"
|
||||
|
||||
cp "${ENROLL_UI_DIR}/HelperTool/Launchd.plist.tmpl" "${helper_launchd_plist}"
|
||||
cp "${ENROLL_UI_DIR}/HelperTool/Launchd.plist.tmpl" "${helper_launchd_plist}"
|
||||
|
||||
helper_linker_flags=(
|
||||
-Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist
|
||||
-Xlinker "${helper_info_plist}"
|
||||
-Xlinker -sectcreate -Xlinker __TEXT -Xlinker __launchd_plist
|
||||
-Xlinker "${helper_launchd_plist}"
|
||||
)
|
||||
helper_linker_flags=(
|
||||
-Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist
|
||||
-Xlinker "${helper_info_plist}"
|
||||
-Xlinker -sectcreate -Xlinker __TEXT -Xlinker __launchd_plist
|
||||
-Xlinker "${helper_launchd_plist}"
|
||||
)
|
||||
|
||||
pushd "${ENROLL_UI_DIR}" >/dev/null
|
||||
swift build -c release "${swift_arch_args[@]}" \
|
||||
--scratch-path "${build_dir}/swift" \
|
||||
--product "${HELPER_LABEL}" \
|
||||
"${helper_linker_flags[@]}"
|
||||
pushd "${ENROLL_UI_DIR}" >/dev/null
|
||||
swift build -c release "${swift_arch_args[@]}" \
|
||||
--scratch-path "${build_dir}/swift" \
|
||||
--product "${HELPER_LABEL}" \
|
||||
"${helper_linker_flags[@]}"
|
||||
|
||||
swift build -c release "${swift_arch_args[@]}" \
|
||||
--scratch-path "${build_dir}/swift" \
|
||||
--product "${URL_HANDLER_NAME}"
|
||||
swift build -c release "${swift_arch_args[@]}" \
|
||||
--scratch-path "${build_dir}/swift" \
|
||||
--product "${URL_HANDLER_NAME}"
|
||||
|
||||
bin_dir="$(swift build -c release "${swift_arch_args[@]}" \
|
||||
--scratch-path "${build_dir}/swift" --show-bin-path)"
|
||||
helper_binary="${bin_dir}/${HELPER_LABEL}"
|
||||
url_handler_binary="${bin_dir}/${URL_HANDLER_NAME}"
|
||||
popd >/dev/null
|
||||
bin_dir="$(swift build -c release "${swift_arch_args[@]}" \
|
||||
--scratch-path "${build_dir}/swift" --show-bin-path)"
|
||||
helper_binary="${bin_dir}/${HELPER_LABEL}"
|
||||
url_handler_binary="${bin_dir}/${URL_HANDLER_NAME}"
|
||||
popd >/dev/null
|
||||
|
||||
if [ ! -x "${helper_binary}" ] || [ ! -x "${url_handler_binary}" ]; then
|
||||
echo "error: expected release binaries were not produced" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -x "${helper_binary}" ] || [ ! -x "${url_handler_binary}" ]; then
|
||||
echo "error: expected release binaries were not produced" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
app_root="${parent_dir}/${APP_NAME}"
|
||||
contents="${app_root}/Contents"
|
||||
macos="${contents}/MacOS"
|
||||
resources="${contents}/Resources"
|
||||
launch_services="${contents}/Library/LaunchServices"
|
||||
launch_daemons="${contents}/Library/LaunchDaemons"
|
||||
plist="${contents}/Info.plist"
|
||||
embedded_helper="${launch_services}/${HELPER_LABEL}"
|
||||
embedded_launchd="${launch_daemons}/${HELPER_LABEL}.plist"
|
||||
app_icon_original="${ENROLL_UI_DIR}/Resources/icon-original.png"
|
||||
app_icon_icns="${STAGE}/AppIcon.icns"
|
||||
app_root="${parent_dir}/${APP_NAME}"
|
||||
contents="${app_root}/Contents"
|
||||
macos="${contents}/MacOS"
|
||||
resources="${contents}/Resources"
|
||||
launch_services="${contents}/Library/LaunchServices"
|
||||
launch_daemons="${contents}/Library/LaunchDaemons"
|
||||
plist="${contents}/Info.plist"
|
||||
embedded_helper="${launch_services}/${HELPER_LABEL}"
|
||||
embedded_launchd="${launch_daemons}/${HELPER_LABEL}.plist"
|
||||
app_icon_original="${ENROLL_UI_DIR}/Resources/icon-original.png"
|
||||
app_icon_icns="${STAGE}/AppIcon.icns"
|
||||
|
||||
rm -rf "${app_root}"
|
||||
mkdir -p "${macos}" "${resources}" "${launch_services}" "${launch_daemons}"
|
||||
rm -rf "${app_root}"
|
||||
mkdir -p "${macos}" "${resources}" "${launch_services}" "${launch_daemons}"
|
||||
|
||||
generate_app_icon_icns "${app_icon_original}" "${app_icon_icns}"
|
||||
generate_app_icon_icns "${app_icon_original}" "${app_icon_icns}"
|
||||
|
||||
install -m 0755 "${url_handler_binary}" "${macos}/${URL_HANDLER_NAME}"
|
||||
install -m 0755 "${helper_binary}" "${embedded_helper}"
|
||||
install -m 0644 "${helper_launchd_plist}" "${embedded_launchd}"
|
||||
ditto --norsrc --noextattr "${app_icon_icns}" "${resources}/AppIcon.icns"
|
||||
install -m 0755 "${url_handler_binary}" "${macos}/${URL_HANDLER_NAME}"
|
||||
install -m 0755 "${helper_binary}" "${embedded_helper}"
|
||||
install -m 0644 "${helper_launchd_plist}" "${embedded_launchd}"
|
||||
ditto --norsrc --noextattr "${app_icon_icns}" "${resources}/AppIcon.icns"
|
||||
|
||||
# Sign helper before writing Info.plist so SMPrivilegedExecutables
|
||||
# can embed the helper's designated requirement.
|
||||
codesign_runtime "${embedded_helper}"
|
||||
# codesign prints "Executable=…" on stderr and either
|
||||
# "# designated => …" (modern) or "designated => …" (older) on stdout.
|
||||
helper_requirement="$(
|
||||
codesign -d -r- "${embedded_helper}" 2>&1 \
|
||||
| sed -n -e 's/^# designated => //p' -e 's/^designated => //p'
|
||||
)"
|
||||
if [ -z "${helper_requirement}" ]; then
|
||||
echo "error: cannot extract designated requirement from signed helper" >&2
|
||||
codesign -d -r- "${embedded_helper}" 2>&1 >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
echo "Helper designated requirement: ${helper_requirement}"
|
||||
# Sign helper before writing Info.plist so SMPrivilegedExecutables
|
||||
# can embed the helper's designated requirement.
|
||||
codesign_runtime "${embedded_helper}"
|
||||
# codesign prints "Executable=…" on stderr and either
|
||||
# "# designated => …" (modern) or "designated => …" (older) on stdout.
|
||||
helper_requirement="$(
|
||||
codesign -d -r- "${embedded_helper}" 2>&1 \
|
||||
| sed -n -e 's/^# designated => //p' -e 's/^designated => //p'
|
||||
)"
|
||||
if [ -z "${helper_requirement}" ]; then
|
||||
echo "error: cannot extract designated requirement from signed helper" >&2
|
||||
codesign -d -r- "${embedded_helper}" 2>&1 >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
echo "Helper designated requirement: ${helper_requirement}"
|
||||
|
||||
sed \
|
||||
-e "s|@@VERSION@@|${VERSION}|g" \
|
||||
-e "s|@@HELPER_DESIGNATED_REQUIREMENT@@|${helper_requirement}|g" \
|
||||
"${ENROLL_UI_DIR}/Info.plist.tmpl" > "${plist}"
|
||||
sed \
|
||||
-e "s|@@VERSION@@|${VERSION}|g" \
|
||||
-e "s|@@HELPER_DESIGNATED_REQUIREMENT@@|${helper_requirement}|g" \
|
||||
"${ENROLL_UI_DIR}/Info.plist.tmpl" >"${plist}"
|
||||
|
||||
if ! plutil -lint "${plist}" >/dev/null; then
|
||||
echo "error: rendered Info.plist failed plutil -lint" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q '<string>probo</string>' "${plist}"; then
|
||||
echo "error: Info.plist is missing probo URL scheme" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! plutil -lint "${plist}" >/dev/null; then
|
||||
echo "error: rendered Info.plist failed plutil -lint" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q '<string>probo</string>' "${plist}"; then
|
||||
echo "error: Info.plist is missing probo URL scheme" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
codesign_runtime "${macos}/${URL_HANDLER_NAME}"
|
||||
codesign_runtime "${app_root}"
|
||||
codesign_runtime "${macos}/${URL_HANDLER_NAME}"
|
||||
codesign_runtime "${app_root}"
|
||||
|
||||
echo "Built ${app_root}"
|
||||
echo "Built ${app_root}"
|
||||
}
|
||||
|
||||
notarytool_submit() {
|
||||
local path="$1"
|
||||
xcrun notarytool submit "${path}" \
|
||||
--keychain-profile "${NOTARYTOOL_KEYCHAIN_PROFILE}" \
|
||||
--wait
|
||||
local path="$1"
|
||||
xcrun notarytool submit "${path}" \
|
||||
--keychain-profile "${NOTARYTOOL_KEYCHAIN_PROFILE}" \
|
||||
--wait
|
||||
}
|
||||
|
||||
# pkgbuild records protected com.apple.provenance xattrs as empty
|
||||
# AppleDouble (._*) Bom entries. Rewrite the Bom with mkbom so the
|
||||
# installer does not lay down those stubs next to real files.
|
||||
rewrite_component_bom() {
|
||||
local pkg="$1"
|
||||
local expand_dir root_dir flat_pkg
|
||||
local pkg="$1"
|
||||
local expand_dir root_dir flat_pkg
|
||||
|
||||
expand_dir="${STAGE}/component-expand"
|
||||
root_dir="${STAGE}/component-root"
|
||||
flat_pkg="${STAGE}/probo-agent-component-clean.pkg"
|
||||
rm -rf "${expand_dir}" "${root_dir}" "${flat_pkg}"
|
||||
# pkgutil --expand creates the destination directory itself.
|
||||
pkgutil --expand "${pkg}" "${expand_dir}"
|
||||
find "${expand_dir}/Scripts" -name '._*' -delete 2>/dev/null || true
|
||||
expand_dir="${STAGE}/component-expand"
|
||||
root_dir="${STAGE}/component-root"
|
||||
flat_pkg="${STAGE}/probo-agent-component-clean.pkg"
|
||||
rm -rf "${expand_dir}" "${root_dir}" "${flat_pkg}"
|
||||
# pkgutil --expand creates the destination directory itself.
|
||||
pkgutil --expand "${pkg}" "${expand_dir}"
|
||||
find "${expand_dir}/Scripts" -name '._*' -delete 2>/dev/null || true
|
||||
|
||||
mkdir -p "${root_dir}"
|
||||
(
|
||||
cd "${root_dir}"
|
||||
gzip -dc "${expand_dir}/Payload" | cpio -idmu 2>/dev/null
|
||||
)
|
||||
find "${root_dir}" -name '._*' -delete 2>/dev/null || true
|
||||
mkbom "${root_dir}" "${expand_dir}/Bom"
|
||||
if lsbom "${expand_dir}/Bom" | grep -q '/\._'; then
|
||||
echo "error: rewritten Bom still contains AppleDouble entries" >&2
|
||||
return 1
|
||||
fi
|
||||
pkgutil --flatten "${expand_dir}" "${flat_pkg}"
|
||||
mv "${flat_pkg}" "${pkg}"
|
||||
mkdir -p "${root_dir}"
|
||||
(
|
||||
cd "${root_dir}"
|
||||
gzip -dc "${expand_dir}/Payload" | cpio -idmu 2>/dev/null
|
||||
)
|
||||
find "${root_dir}" -name '._*' -delete 2>/dev/null || true
|
||||
mkbom "${root_dir}" "${expand_dir}/Bom"
|
||||
if lsbom "${expand_dir}/Bom" | grep -q '/\._'; then
|
||||
echo "error: rewritten Bom still contains AppleDouble entries" >&2
|
||||
return 1
|
||||
fi
|
||||
pkgutil --flatten "${expand_dir}" "${flat_pkg}"
|
||||
mv "${flat_pkg}" "${pkg}"
|
||||
}
|
||||
|
||||
STAGE="$(mktemp -d -t probo-agent-pkg)"
|
||||
@@ -379,18 +398,18 @@ build_probo_agent_app "${PAYLOAD}/Applications"
|
||||
APP_PATH="${PAYLOAD}/Applications/${APP_NAME}"
|
||||
|
||||
if [ "${notarize_enabled}" = true ]; then
|
||||
# Password appears on argv only for this short-lived store. Submits
|
||||
# use --keychain-profile so concurrent processes cannot read it.
|
||||
xcrun notarytool store-credentials "${NOTARYTOOL_KEYCHAIN_PROFILE}" \
|
||||
--apple-id "${APPLE_ID}" \
|
||||
--password "${APPLE_ID_PASSWORD}" \
|
||||
--team-id "${APPLE_TEAM_ID}"
|
||||
echo "Notarizing Probo Agent.app before packaging..."
|
||||
zip_path="${STAGE}/probo-agent-app.zip"
|
||||
ditto -c -k --keepParent "${APP_PATH}" "${zip_path}"
|
||||
notarytool_submit "${zip_path}"
|
||||
rm -f "${zip_path}"
|
||||
xcrun stapler staple "${APP_PATH}"
|
||||
# Password appears on argv only for this short-lived store. Submits
|
||||
# use --keychain-profile so concurrent processes cannot read it.
|
||||
xcrun notarytool store-credentials "${NOTARYTOOL_KEYCHAIN_PROFILE}" \
|
||||
--apple-id "${APPLE_ID}" \
|
||||
--password "${APPLE_ID_PASSWORD}" \
|
||||
--team-id "${APPLE_TEAM_ID}"
|
||||
echo "Notarizing Probo Agent.app before packaging..."
|
||||
zip_path="${STAGE}/probo-agent-app.zip"
|
||||
ditto -c -k --keepParent "${APP_PATH}" "${zip_path}"
|
||||
notarytool_submit "${zip_path}"
|
||||
rm -f "${zip_path}"
|
||||
xcrun stapler staple "${APP_PATH}"
|
||||
fi
|
||||
|
||||
# AppleDouble / xattr hygiene: COPYFILE_DISABLE + ditto --norsrc/--noextattr
|
||||
@@ -401,53 +420,53 @@ export COPYFILE_DISABLE=1
|
||||
ditto --norsrc --noextattr "${SCRIPT_DIR}/scripts/preinstall" "${SCRIPTS}/preinstall"
|
||||
ditto --norsrc --noextattr "${SCRIPT_DIR}/scripts/postinstall" "${SCRIPTS}/postinstall"
|
||||
ditto --norsrc --noextattr \
|
||||
"${REPO_ROOT}/pkg/deviceagent/tray/launchagent.plist.tmpl" \
|
||||
"${SCRIPTS}/launchagent.plist.tmpl"
|
||||
"${REPO_ROOT}/pkg/deviceagent/tray/launchagent.plist.tmpl" \
|
||||
"${SCRIPTS}/launchagent.plist.tmpl"
|
||||
chmod 0755 "${SCRIPTS}/preinstall" "${SCRIPTS}/postinstall"
|
||||
chmod 0644 "${SCRIPTS}/launchagent.plist.tmpl"
|
||||
|
||||
ditto --norsrc --noextattr "${SCRIPT_DIR}/Resources/welcome.html" "${RESOURCES}/welcome.html"
|
||||
ditto --norsrc --noextattr "${SCRIPT_DIR}/Resources/welcome.html" "${RESOURCES}/welcome.html"
|
||||
ditto --norsrc --noextattr "${SCRIPT_DIR}/Resources/conclusion.html" "${RESOURCES}/conclusion.html"
|
||||
ditto --norsrc --noextattr "${REPO_ROOT}/LICENSE" "${RESOURCES}/license.txt"
|
||||
ditto --norsrc --noextattr "${REPO_ROOT}/LICENSE" "${RESOURCES}/license.txt"
|
||||
|
||||
xattr -cr "${PAYLOAD}" "${SCRIPTS}" "${RESOURCES}" 2>/dev/null || true
|
||||
find "${PAYLOAD}" "${SCRIPTS}" "${RESOURCES}" -name '._*' -delete 2>/dev/null || true
|
||||
|
||||
COMPONENT_PKG="${STAGE}/probo-agent-component.pkg"
|
||||
pkgbuild \
|
||||
--root "${PAYLOAD}" \
|
||||
--scripts "${SCRIPTS}" \
|
||||
--identifier "${IDENTIFIER}" \
|
||||
--version "${VERSION}" \
|
||||
--install-location "/" \
|
||||
"${COMPONENT_PKG}"
|
||||
--root "${PAYLOAD}" \
|
||||
--scripts "${SCRIPTS}" \
|
||||
--identifier "${IDENTIFIER}" \
|
||||
--version "${VERSION}" \
|
||||
--install-location "/" \
|
||||
"${COMPONENT_PKG}"
|
||||
|
||||
rewrite_component_bom "${COMPONENT_PKG}"
|
||||
|
||||
DISTRIBUTION="${STAGE}/Distribution.xml"
|
||||
sed \
|
||||
-e "s|@@VERSION@@|${VERSION}|g" \
|
||||
-e "s|@@IDENTIFIER@@|${IDENTIFIER}|g" \
|
||||
"${SCRIPT_DIR}/Distribution.xml.tmpl" > "${DISTRIBUTION}"
|
||||
-e "s|@@VERSION@@|${VERSION}|g" \
|
||||
-e "s|@@IDENTIFIER@@|${IDENTIFIER}|g" \
|
||||
"${SCRIPT_DIR}/Distribution.xml.tmpl" >"${DISTRIBUTION}"
|
||||
|
||||
mkdir -p "$(dirname "${OUTPUT}")"
|
||||
|
||||
PRODUCTBUILD_ARGS=(
|
||||
--distribution "${DISTRIBUTION}"
|
||||
--package-path "${STAGE}"
|
||||
--resources "${RESOURCES}"
|
||||
--distribution "${DISTRIBUTION}"
|
||||
--package-path "${STAGE}"
|
||||
--resources "${RESOURCES}"
|
||||
)
|
||||
if [ -n "${INSTALLER_IDENTITY}" ]; then
|
||||
PRODUCTBUILD_ARGS+=(--sign "${INSTALLER_IDENTITY}")
|
||||
PRODUCTBUILD_ARGS+=(--sign "${INSTALLER_IDENTITY}")
|
||||
fi
|
||||
PRODUCTBUILD_ARGS+=("${OUTPUT}")
|
||||
|
||||
productbuild "${PRODUCTBUILD_ARGS[@]}"
|
||||
|
||||
if [ "${notarize_enabled}" = true ]; then
|
||||
echo "Notarizing ${OUTPUT}..."
|
||||
notarytool_submit "${OUTPUT}"
|
||||
xcrun stapler staple "${OUTPUT}"
|
||||
echo "Notarizing ${OUTPUT}..."
|
||||
notarytool_submit "${OUTPUT}"
|
||||
xcrun stapler staple "${OUTPUT}"
|
||||
fi
|
||||
|
||||
echo "Built ${OUTPUT}"
|
||||
|
||||
@@ -11,13 +11,13 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PKG="${1:-}"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "error: must run as root (try: sudo make -C cmd/probo-agent install)" >&2
|
||||
exit 1
|
||||
echo "error: must run as root (try: sudo make -C cmd/probo-agent install)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${PKG}" ] || [ ! -f "${PKG}" ]; then
|
||||
echo "error: usage: $0 /path/to/probo-agent_*.pkg" >&2
|
||||
exit 2
|
||||
echo "error: usage: $0 /path/to/probo-agent_*.pkg" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
"${SCRIPT_DIR}/uninstall.sh"
|
||||
|
||||
@@ -26,94 +26,93 @@ PKG_ID="com.probo.agent"
|
||||
LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
|
||||
|
||||
log() {
|
||||
printf '%s\n' "$*"
|
||||
printf '%s\n' "$*"
|
||||
}
|
||||
|
||||
die() {
|
||||
printf 'error: %s\n' "$*" >&2
|
||||
exit 1
|
||||
printf 'error: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_root() {
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
die "must run as root (try: sudo make -C cmd/probo-agent uninstall)"
|
||||
fi
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
die "must run as root (try: sudo make -C cmd/probo-agent uninstall)"
|
||||
fi
|
||||
}
|
||||
|
||||
bootout_system_plist() {
|
||||
local plist="$1"
|
||||
if [ -f "${plist}" ]; then
|
||||
launchctl bootout system "${plist}" 2>/dev/null || true
|
||||
log "Booted out ${plist}"
|
||||
fi
|
||||
local plist="$1"
|
||||
if [ -f "${plist}" ]; then
|
||||
launchctl bootout system "${plist}" 2>/dev/null || true
|
||||
log "Booted out ${plist}"
|
||||
fi
|
||||
}
|
||||
|
||||
bootout_tray_for_user() {
|
||||
local username="$1"
|
||||
local user_uid
|
||||
local username="$1"
|
||||
local user_uid
|
||||
|
||||
if [ -z "${username}" ] || \
|
||||
[ "${username}" = "root" ] || \
|
||||
[ "${username}" = "loginwindow" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ -z "${username}" ] \
|
||||
|| [ "${username}" = "root" ] \
|
||||
|| [ "${username}" = "loginwindow" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
user_uid="$(id -u "${username}" 2>/dev/null || true)"
|
||||
if [ -z "${user_uid}" ]; then
|
||||
return 0
|
||||
fi
|
||||
user_uid="$(id -u "${username}" 2>/dev/null || true)"
|
||||
if [ -z "${user_uid}" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
launchctl bootout "gui/${user_uid}/${TRAY_LABEL}" 2>/dev/null || true
|
||||
launchctl bootout "gui/${user_uid}/${TRAY_LABEL}" 2>/dev/null || true
|
||||
}
|
||||
|
||||
unregister_apps() {
|
||||
local path
|
||||
for path in \
|
||||
"/Applications/Probo Agent.app" \
|
||||
"/Applications/Probo Agent.localized/Probo Agent.app"
|
||||
do
|
||||
if [ -d "${path}" ] && [ -x "${LSREGISTER}" ]; then
|
||||
"${LSREGISTER}" -u "${path}" 2>/dev/null || true
|
||||
log "Unregistered Launch Services entry for ${path}"
|
||||
fi
|
||||
done
|
||||
local path
|
||||
for path in \
|
||||
"/Applications/Probo Agent.app" \
|
||||
"/Applications/Probo Agent.localized/Probo Agent.app"; do
|
||||
if [ -d "${path}" ] && [ -x "${LSREGISTER}" ]; then
|
||||
"${LSREGISTER}" -u "${path}" 2>/dev/null || true
|
||||
log "Unregistered Launch Services entry for ${path}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
kill_leftovers() {
|
||||
# Best-effort; deleted-but-running binaries otherwise keep claiming probo://.
|
||||
pkill -x probo-agent-url-handler 2>/dev/null || true
|
||||
pkill -f '/usr/local/bin/probo-agent tray' 2>/dev/null || true
|
||||
pkill -f '/Library/PrivilegedHelperTools/com.probo.agent.helper' 2>/dev/null || true
|
||||
# Agent daemon may still be running after plist bootout races.
|
||||
pkill -x probo-agent 2>/dev/null || true
|
||||
# Best-effort; deleted-but-running binaries otherwise keep claiming probo://.
|
||||
pkill -x probo-agent-url-handler 2>/dev/null || true
|
||||
pkill -f '/usr/local/bin/probo-agent tray' 2>/dev/null || true
|
||||
pkill -f '/Library/PrivilegedHelperTools/com.probo.agent.helper' 2>/dev/null || true
|
||||
# Agent daemon may still be running after plist bootout races.
|
||||
pkill -x probo-agent 2>/dev/null || true
|
||||
}
|
||||
|
||||
require_root
|
||||
|
||||
if [ "$(uname -s)" != "Darwin" ]; then
|
||||
die "this uninstall script is macOS-only"
|
||||
die "this uninstall script is macOS-only"
|
||||
fi
|
||||
|
||||
log "=== probo-agent macOS uninstall $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="
|
||||
|
||||
# Prefer the agent's own uninstall for service/tray/state when present.
|
||||
if [ -x "${BINARY}" ]; then
|
||||
if "${BINARY}" uninstall; then
|
||||
log "Ran: ${BINARY} uninstall"
|
||||
else
|
||||
log "warning: ${BINARY} uninstall failed; continuing with manual cleanup"
|
||||
fi
|
||||
if "${BINARY}" uninstall; then
|
||||
log "Ran: ${BINARY} uninstall"
|
||||
else
|
||||
log "warning: ${BINARY} uninstall failed; continuing with manual cleanup"
|
||||
fi
|
||||
else
|
||||
log "Binary not found at ${BINARY}; skipping probo-agent uninstall"
|
||||
log "Binary not found at ${BINARY}; skipping probo-agent uninstall"
|
||||
fi
|
||||
|
||||
seen_users=" "
|
||||
for username in $(users 2>/dev/null || true); do
|
||||
case "${seen_users}" in
|
||||
*" ${username} "*) continue ;;
|
||||
esac
|
||||
seen_users="${seen_users}${username} "
|
||||
bootout_tray_for_user "${username}"
|
||||
case "${seen_users}" in
|
||||
*" ${username} "*) continue ;;
|
||||
esac
|
||||
seen_users="${seen_users}${username} "
|
||||
bootout_tray_for_user "${username}"
|
||||
done
|
||||
bootout_tray_for_user "$(stat -f "%Su" /dev/console 2>/dev/null || true)"
|
||||
|
||||
@@ -127,23 +126,23 @@ log "Removed LaunchDaemon / LaunchAgent / helper files (if present)"
|
||||
|
||||
unregister_apps
|
||||
rm -rf \
|
||||
"/Applications/Probo Agent.app" \
|
||||
"/Applications/Probo Agent.localized"
|
||||
"/Applications/Probo Agent.app" \
|
||||
"/Applications/Probo Agent.localized"
|
||||
log "Removed Probo Agent.app (if present)"
|
||||
|
||||
rm -f "${BINARY}"
|
||||
rm -rf "${STATE_DIR}" "${RUN_DIR}"
|
||||
rm -f \
|
||||
/var/log/probo-agent.log \
|
||||
/var/log/probo-agent-install.log \
|
||||
/tmp/probo-agent.conf
|
||||
/var/log/probo-agent.log \
|
||||
/var/log/probo-agent-install.log \
|
||||
/tmp/probo-agent.conf
|
||||
log "Removed binary, state, run dir, logs, and staged conf (if present)"
|
||||
|
||||
if pkgutil --pkg-info "${PKG_ID}" >/dev/null 2>&1; then
|
||||
if ! pkgutil --forget "${PKG_ID}" >/dev/null; then
|
||||
die "failed to forget PKG receipt ${PKG_ID}"
|
||||
fi
|
||||
log "Forgot PKG receipt ${PKG_ID}"
|
||||
if ! pkgutil --forget "${PKG_ID}" >/dev/null; then
|
||||
die "failed to forget PKG receipt ${PKG_ID}"
|
||||
fi
|
||||
log "Forgot PKG receipt ${PKG_ID}"
|
||||
fi
|
||||
|
||||
log "=== uninstall done ==="
|
||||
|
||||
Reference in New Issue
Block a user