Files
PX4-Autopilot/Tools/setup/macos.sh
Jacob Dahl fbfc1386ee ci(macos): install from pinned Homebrew commits
The job ran brew update and then installed whatever Homebrew was publishing, except Gazebo, which stayed on one commit. Those were built against different protobufs, so a release upstream failed the build with no commit here. Check the package repos out at commits recorded in the tree and do not update them. Run on macos-15 only, so the OS label cannot move either.

Assisted-by: Grok:grok-4.7
Signed-off-by: Jacob Dahl <dahl.jakejacob@gmail.com>
2026-09-21 16:55:31 -07:00

304 lines
12 KiB
Bash
Executable File

#! /usr/bin/env bash
## Basch script to setup the PX4 development environment on macOS
## Works for Intel and Arm based Apple hardware
##
## Installs:
## - Common dependencies and tools for building PX4
## - Cross compilers for building hardware targets using NuttX
## - With --sim-tools: Gazebo Harmonic and jMAVSim simulation stack
##
## --sim-tools pins the osrf/simulation tap to gz-tap-pin.txt so Gazebo
## installs from bottles even while OSRF has them pulled, and protobuf to
## protobuf-pin.txt so those bottles' headers still compile.
##
## Homebrew 4.5+ no longer auto-resolves cross-tap dependencies, so
## every tap and package is listed explicitly here rather than hidden
## behind meta-formulae. See PX4/homebrew-px4#104 for background.
##
# Abort on the first failing command.
set -e
# script directory
DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
# Reinstall if --reinstall set
REINSTALL_FORMULAS=""
# Install simulation tools?
INSTALL_SIM=""
# Parse arguments
for arg in "$@"
do
if [[ $arg == "--reinstall" ]]; then
REINSTALL_FORMULAS=$arg
elif [[ $arg == "--sim-tools" ]]; then
INSTALL_SIM=$arg
fi
done
# Leave a checkout that is already there. `brew tap` on one would try to
# unshallow it, and CI has already checked these repos out at a commit.
brew_tap() {
local name="$1"
local user="${name%%/*}"
local repo="${name#*/}"
local path
path="$(brew --repo)/Library/Taps/${user}/homebrew-${repo}"
if [[ -d "${path}/.git" ]]; then
return 0
fi
brew tap "$name"
}
echo "[macos.sh] Installing the development dependencies for the PX4 Autopilot"
if ! command -v brew &> /dev/null
then
# install Homebrew if not installed yet
echo "[macos.sh] Installing Homebrew"
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
fi
# discoteq/discoteq used to be the only source of flock (required by the
# NuttX apps archive step), but homebrew/core now carries the identical
# formula (same upstream, same version). Drop the old tap so `flock`
# resolves unambiguously from homebrew/core instead of erroring with
# "installed from the discoteq/discoteq tap but you are trying to install
# it from homebrew/core" on machines that still have it tapped.
if brew tap | grep -q '^discoteq/discoteq$'; then
brew uninstall flock 2>/dev/null || true
brew untap discoteq/discoteq
fi
# Required taps. Homebrew 4.5+ no longer auto-resolves cross-tap
# dependencies, so every tap that a package lives in must be added
# explicitly here before `brew install`.
#
# - osx-cross/arm: arm-gcc-bin@13 (ARM cross-compiler)
# - PX4/px4: fastdds, genromfs, kconfig-frontends (PX4-specific)
#
# Homebrew 6.0+ refuses to load formulae from third-party taps unless they
# are explicitly trusted ("Refusing to load formula ... from untrusted tap"),
# and recent versions validate every formula of a tap while tapping it. An
# untrusted tap therefore fails with "Cannot tap ...: invalid syntax in tap!",
# so the taps must be trusted *before* they are tapped. `brew trust` works on
# a tap that is not installed yet. Without the taps, `brew install` aborts
# on the first PX4/px4 formula before pouring any package (including ccache).
# `brew trust` only exists on Homebrew 6.0+; guard it so older versions,
# which don't gate untrusted taps, skip it silently.
if brew trust --help &> /dev/null; then
brew trust osx-cross/arm
brew trust PX4/px4
fi
brew_tap osx-cross/arm
brew_tap PX4/px4
# Package list. This replaces the px4-dev meta-formula, which is kept
# as a deprecated no-op upstream. See PX4/homebrew-px4 for history.
PX4_BREW_PACKAGES=(
ant
astyle
bash-completion
ccache
cmake
fastdds
genromfs
kconfig-frontends
ncurses
ninja
osx-cross/arm/arm-gcc-bin@13
flock
python
python-tk
)
if [[ $REINSTALL_FORMULAS == "--reinstall" ]]; then
echo "[macos.sh] Re-installing PX4 toolchain dependencies"
brew doctor || true # warnings are informational here
brew reinstall "${PX4_BREW_PACKAGES[@]}"
else
echo "[macos.sh] Installing PX4 toolchain dependencies"
brew install "${PX4_BREW_PACKAGES[@]}"
fi
brew link --overwrite --force arm-gcc-bin@13
# Python dependencies
echo "[macos.sh] Installing Python3 dependencies"
# Resolve to git repo root based on script location (handles submodules and subdirectory invocation)
ROOT_DIR="$(git -C "$DIR" rev-parse --show-toplevel 2>/dev/null || echo "$DIR")"
VENV_DIR="$ROOT_DIR/.venv"
# Create virtual environment if it doesn't exist
if [ ! -d "$VENV_DIR" ]; then
echo "[macos.sh] Creating Python virtual environment at $VENV_DIR"
python3 -m venv "$VENV_DIR"
fi
# We need to have future to install pymavlink later.
"$VENV_DIR/bin/pip" install future
"$VENV_DIR/bin/pip" install -r "${DIR}/requirements.txt"
# Optional, but recommended additional simulation tools:
if [[ $INSTALL_SIM == "--sim-tools" ]]; then
# Simulation packages. This replaces the px4-sim / px4-sim-gazebo
# meta-formulae, which declared cross-tap dependencies that
# Homebrew 4.5+ no longer auto-resolves. Same migration pattern as
# the toolchain block above. See PX4/homebrew-px4#104 for the
# px4-dev precedent.
#
# osrf/simulation: gz-harmonic (Gazebo Harmonic meta-formula)
#
# Trust before tapping, same as the toolchain taps above. Tapping an
# untrusted tap fails, which leaves no tap clone to pin below; the
# later `brew install osrf/simulation/gz-harmonic` then taps it
# implicitly at HEAD and the pin is silently skipped.
if brew trust --help &> /dev/null; then
brew trust osrf/simulation
fi
brew_tap osrf/simulation
# OSRF drops the gz bottle blocks within minutes of a breaking
# homebrew-core dependency bump and rebuilds them days later, so an
# unpinned tap compiles Gazebo from source for a large part of the
# year. Pin unconditionally so dev machines get the same fast, binary
# install as CI. See gz-tap-pin.txt.
GZ_TAP_PIN=$(grep -v '^#' "${DIR}/gz-tap-pin.txt" | tr -d '[:space:]')
if [[ -n $GZ_TAP_PIN ]]; then
GZ_TAP_DIR=$(brew --repo osrf/simulation)
echo "[macos.sh] Pinning osrf/simulation to ${GZ_TAP_PIN}"
# brew taps are shallow clones, so the pinned commit has to be
# fetched by SHA before it can be checked out.
git -C "$GZ_TAP_DIR" fetch --quiet origin "$GZ_TAP_PIN" 2>/dev/null || true
if git -C "$GZ_TAP_DIR" checkout --quiet "$GZ_TAP_PIN"; then
# `brew update` walks local taps and would reset the pin.
# homebrew-core resolves through the JSON API, not this
# clone, so nothing else goes stale.
export HOMEBREW_NO_AUTO_UPDATE=1
else
echo "[macos.sh] WARNING: could not pin osrf/simulation to ${GZ_TAP_PIN}," \
"continuing on tap HEAD (gz may build from source)"
fi
fi
# opencv@4: the unversioned formula is OpenCV 5, which PX4-OpticalFlow
# does not build against.
PX4_SIM_BREW_PACKAGES=(
exiftool
glog
graphviz
gstreamer
opencv@4
osrf/simulation/gz-harmonic
protobuf
)
if [[ $REINSTALL_FORMULAS == "--reinstall" ]]; then
echo "[macos.sh] Re-installing PX4 simulation dependencies"
brew reinstall "${PX4_SIM_BREW_PACKAGES[@]}"
else
echo "[macos.sh] Installing PX4 simulation dependencies"
brew install "${PX4_SIM_BREW_PACKAGES[@]}"
fi
# Gazebo's generated headers only compile against the exact protobuf
# their gencode came from, so protobuf has to come from the
# homebrew-core revision that was current when the pinned gz bottles
# were built rather than from whatever homebrew-core ships today. See
# protobuf-pin.txt.
#
# This runs after the installs above rather than before them because
# opencv@4 depends on protobuf as well and drags in the current one:
# brew resolves a dependency against the versions recorded in the
# dependent's bottle, so an opencv bottle rebuilt against a newer
# protobuf pulls that protobuf in no matter what is installed.
# Pinning the formula instead of reinstalling it here is not an
# option either: brew refuses to install anything whose pinned
# dependency is not the current one ("You must `brew unpin
# protobuf`").
PROTOBUF_FORMULA="Formula/p/protobuf.rb"
PROTOBUF_PIN_LINE=$(grep -v -e '^#' -e '^[[:space:]]*$' "${DIR}/protobuf-pin.txt" | head -n 1)
read -r PROTOBUF_PIN PROTOBUF_PIN_VERSION <<< "$PROTOBUF_PIN_LINE" || true
if [[ -n $PROTOBUF_PIN ]]; then
CORE_TAP_DIR=$(brew --repo homebrew/core)
INSTALLED_PROTOBUF=$(brew list --versions protobuf 2> /dev/null | awk '{print $2}')
if [[ $INSTALLED_PROTOBUF == "$PROTOBUF_PIN_VERSION" ]]; then
echo "[macos.sh] protobuf ${PROTOBUF_PIN_VERSION} is what the gz bottles need, leaving it"
elif ! git -C "$CORE_TAP_DIR" rev-parse --git-dir &> /dev/null; then
# homebrew-core resolves through the JSON API by default, and a
# formula as it was at some commit can only be read from a clone.
echo "[macos.sh] WARNING: homebrew-core is not cloned here, so protobuf cannot be held" \
"at ${PROTOBUF_PIN_VERSION} and Gazebo's headers may fail to compile." \
"Run 'brew tap homebrew/core' once to enable the pin."
else
echo "[macos.sh] Installing protobuf ${PROTOBUF_PIN_VERSION} from homebrew-core ${PROTOBUF_PIN}"
PROTOBUF_TMP=$(mktemp -d)
# A clone that has not been updated since the pin was made does
# not have the commit yet.
git -C "$CORE_TAP_DIR" fetch --quiet origin "$PROTOBUF_PIN" 2> /dev/null || true
if git -C "$CORE_TAP_DIR" show "${PROTOBUF_PIN}:${PROTOBUF_FORMULA}" \
> "${PROTOBUF_TMP}/pinned.rb" 2> /dev/null; then
# Swap the formula in place rather than checking it out, so
# the clone's git state is never touched and the file comes
# back byte for byte whether or not the install works.
cp "${CORE_TAP_DIR}/${PROTOBUF_FORMULA}" "${PROTOBUF_TMP}/current.rb"
cp "${PROTOBUF_TMP}/pinned.rb" "${CORE_TAP_DIR}/${PROTOBUF_FORMULA}"
# Whatever is installed is the wrong version, and only
# reinstall replaces it; install alone would be a no-op.
if [[ -n $INSTALLED_PROTOBUF ]]; then
PROTOBUF_INSTALL="reinstall"
else
PROTOBUF_INSTALL="install"
fi
# brew reads this clone instead of the JSON API only with
# HOMEBREW_NO_INSTALL_FROM_API, and brew update would put
# the swapped formula back before the install. Without
# HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK brew would then
# see everything linked against the protobuf it just
# replaced as broken and reinstall it, which pulls the
# newer protobuf straight back in through opencv@4.
if ! HOMEBREW_NO_INSTALL_FROM_API=1 HOMEBREW_NO_AUTO_UPDATE=1 \
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 \
brew "$PROTOBUF_INSTALL" protobuf; then
echo "[macos.sh] WARNING: could not install protobuf ${PROTOBUF_PIN_VERSION}," \
"continuing on the current one (Gazebo headers may fail to compile)"
fi
cp "${PROTOBUF_TMP}/current.rb" "${CORE_TAP_DIR}/${PROTOBUF_FORMULA}"
else
echo "[macos.sh] WARNING: homebrew-core commit ${PROTOBUF_PIN} is not available," \
"continuing on the current protobuf (Gazebo headers may fail to compile)"
fi
rm -rf "$PROTOBUF_TMP"
fi
fi
# XQuartz is required for Gazebo GUI display on macOS.
if ! brew list --cask xquartz &> /dev/null; then
echo "[macos.sh] Installing XQuartz (required for Gazebo display)"
# XQuartz is not in the pinned package repos.
env -u HOMEBREW_NO_INSTALL_FROM_API brew install --cask xquartz
fi
# jMAVSim requires a JDK (Java 17 LTS recommended)
if ! brew ls --versions openjdk@17 > /dev/null; then
echo "[macos.sh] Installing OpenJDK 17 (required for jMAVSim)"
brew install openjdk@17
sudo ln -sfn $(brew --prefix openjdk@17)/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-17.jdk
fi
fi
echo ""
echo "[macos.sh] All set! The PX4 Autopilot toolchain was installed."
echo ""
echo "Python dependencies were installed into a virtual environment at:"
echo " $VENV_DIR"
echo ""
echo "Activate it before building (run in each new terminal session):"
echo " source $VENV_DIR/bin/activate"
echo ""