Skip to content

Instantly share code, notes, and snippets.

@20plays
Created September 3, 2026 02:08
Show Gist options
  • Select an option

  • Save 20plays/69d905f728a2333665c6cc42e5accbdb to your computer and use it in GitHub Desktop.

Select an option

Save 20plays/69d905f728a2333665c6cc42e5accbdb to your computer and use it in GitHub Desktop.
BeamMP Installer for Linux
#!/usr/bin/env bash
# Unofficial BeamMP Launcher installer/updater for Linux.
#
# Builds the upstream BeamMP Launcher from source and installs it per-user.
# Applies a narrowly scoped Linux Steam-discovery compatibility patch only
# when the matching upstream code is present.
#
# Not affiliated with BeamMP or BeamNG GmbH.
set -Eeuo pipefail
REPO="https://github.com/BeamMP/BeamMP-Launcher.git"
VCPKG_REPO="https://github.com/microsoft/vcpkg.git"
INSTALLER_PATCH_LEVEL="2"
DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
INSTALL_DIR="$DATA_HOME/BeamMP"
BIN_DIR="$HOME/.local/bin"
DESKTOP_DIR="$DATA_HOME/applications"
WORK_DIR="$CACHE_HOME/beammp-installer"
SRC_DIR="$WORK_DIR/BeamMP-Launcher"
BUILD_DIR="$WORK_DIR/build"
VCPKG_ROOT="$WORK_DIR/vcpkg"
VERSION_FILE="$INSTALL_DIR/installed-version"
PATCH_FILE="$INSTALL_DIR/installer-patch-level"
LAUNCHER="$INSTALL_DIR/BeamMP-Launcher"
JOBS="${BEAMMP_JOBS:-2}"
FORCE=0
STEAMOS_UNLOCKED=0
VCPKG_TRIPLET=""
log() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
warn() { printf '\n\033[1;33mWARNING: %s\033[0m\n' "$*" >&2; }
die() { printf '\n\033[1;31mERROR: %s\033[0m\n' "$*" >&2; exit 1; }
usage() {
cat <<'USAGE'
Usage: ./install-beammp.sh [--force]
Installs or updates the native Linux BeamMP Launcher.
Options:
--force Rebuild/reinstall even if the latest version is already installed.
-h, --help
Show this help text.
Environment:
BEAMMP_JOBS=N
Number of parallel build jobs (default: 2).
USAGE
}
for arg in "$@"; do
case "$arg" in
--force) FORCE=1 ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown option: $arg" ;;
esac
done
[[ "$(uname -s)" == "Linux" ]] || die "This installer is for Linux."
[[ "$JOBS" =~ ^[1-9][0-9]*$ ]] || die "BEAMMP_JOBS must be a positive integer."
case "$(uname -m)" in
x86_64|amd64)
VCPKG_TRIPLET="x64-linux"
;;
*)
die "Unsupported CPU architecture: $(uname -m). This installer currently targets x86_64 Linux."
;;
esac
sudo_cmd() {
if (( EUID == 0 )); then
"$@"
else
command -v sudo >/dev/null 2>&1 \
|| die "sudo is required to install missing build dependencies."
sudo "$@"
fi
}
cleanup() {
if (( STEAMOS_UNLOCKED )); then
log "Re-enabling the SteamOS read-only filesystem"
sudo_cmd steamos-readonly enable || true
fi
}
trap cleanup EXIT
have_build_deps() {
local cmd
for cmd in git cmake curl zip unzip tar python3 c++ make perl pkg-config; do
command -v "$cmd" >/dev/null 2>&1 || return 1
done
return 0
}
install_dependencies() {
[[ -r /etc/os-release ]] || die "Cannot identify this Linux distribution."
# shellcheck disable=SC1091
source /etc/os-release
local distro="${ID:-} ${ID_LIKE:-}"
if [[ "${ID:-}" == "steamos" ]]; then
log "Installing missing build dependencies on SteamOS"
sudo_cmd steamos-readonly disable
STEAMOS_UNLOCKED=1
sudo_cmd pacman -S --needed \
base-devel linux-api-headers glibc libconfig \
cmake git curl zip unzip tar python perl pkgconf
elif [[ "$distro" == *debian* || "$distro" == *ubuntu* ]] \
|| command -v apt-get >/dev/null 2>&1; then
log "Installing missing build dependencies with APT"
# Keep package-manager failures visible. A broken or untrusted APT
# repository should be fixed by the user rather than silently ignored.
sudo_cmd apt-get update \
|| die "apt-get update failed. Fix the APT error above, then rerun this installer."
sudo_cmd apt-get install -y \
build-essential cmake git curl zip unzip tar pkg-config python3 perl \
|| die "Could not install the required build dependencies."
elif [[ "$distro" == *fedora* || "$distro" == *rhel* ]] \
|| command -v dnf >/dev/null 2>&1; then
log "Installing missing build dependencies with DNF"
sudo_cmd dnf install -y \
cmake gcc gcc-c++ make git curl zip unzip tar python3 pkgconf-pkg-config \
perl perl-IPC-Cmd perl-FindBin perl-File-Compare perl-File-Copy \
kernel-headers kernel-devel
elif [[ "$distro" == *arch* ]] || command -v pacman >/dev/null 2>&1; then
log "Installing missing build dependencies with pacman"
sudo_cmd pacman -S --needed \
base-devel cmake git curl zip unzip tar python perl pkgconf
elif [[ "$distro" == *suse* || "$distro" == *opensuse* ]] \
|| command -v zypper >/dev/null 2>&1; then
log "Installing missing build dependencies with zypper"
sudo_cmd zypper install -t pattern devel_basis
sudo_cmd zypper install cmake git curl zip unzip tar python3 perl pkg-config
else
die "Unsupported distro. Install a C++ compiler, CMake, Git, curl, zip/unzip, tar, Python 3, Perl and pkg-config, then rerun this installer."
fi
}
if ! have_build_deps; then
install_dependencies
fi
have_build_deps || die "One or more required build tools are still missing."
mkdir -p "$INSTALL_DIR" "$BIN_DIR" "$DESKTOP_DIR" "$WORK_DIR"
log "Checking the latest stable BeamMP Launcher release"
LATEST_TAG="$({
git ls-remote --tags --refs "$REPO" 'v*' \
| awk -F/ '{print $3}' \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| sort -V \
| tail -n1
} || true)"
[[ -n "$LATEST_TAG" ]] \
|| die "Could not determine the latest BeamMP release tag. Check your internet connection."
echo "Latest stable release: $LATEST_TAG"
INSTALLED_TAG="$(cat "$VERSION_FILE" 2>/dev/null || true)"
INSTALLED_PATCH="$(cat "$PATCH_FILE" 2>/dev/null || true)"
NEED_BUILD=1
if (( ! FORCE )) \
&& [[ -x "$LAUNCHER" ]] \
&& [[ "$INSTALLED_TAG" == "$LATEST_TAG" ]] \
&& [[ "$INSTALLED_PATCH" == "$INSTALLER_PATCH_LEVEL" ]]; then
NEED_BUILD=0
fi
# Preflight only: find a Steam BeamNG.drive installation using the app manifest
# plus a real game file. BeamMP still performs its own checks when launched.
detect_beamng() {
python3 - <<'PY'
from pathlib import Path
import re
home = Path.home()
steam_roots = [
home / ".local/share/Steam",
home / ".steam/root",
home / ".steam/steam",
home / ".var/app/com.valvesoftware.Steam/.local/share/Steam",
home / ".var/app/com.valvesoftware.Steam/.steam/root",
home / "snap/steam/common/.local/share/Steam",
]
libraries = []
seen = set()
def add_library(path: Path):
try:
key = str(path.resolve())
except Exception:
key = str(path)
if key not in seen:
seen.add(key)
libraries.append(path)
for root in steam_roots:
if not root.exists():
continue
add_library(root)
vdf = root / "steamapps/libraryfolders.vdf"
if not vdf.is_file():
continue
try:
text = vdf.read_text(errors="replace")
except OSError:
continue
for match in re.finditer(r'"path"\s*"([^"]+)"', text):
raw = match.group(1).replace('\\\\', '\\')
add_library(Path(raw).expanduser())
for library in libraries:
manifest = library / "steamapps/appmanifest_284160.acf"
game = library / "steamapps/common/BeamNG.drive"
integrity = game / "integrity.json"
if manifest.is_file() and integrity.is_file():
print(game)
raise SystemExit(0)
raise SystemExit(1)
PY
}
if BEAMNG_DIR="$(detect_beamng 2>/dev/null)"; then
log "Detected a Steam BeamNG.drive installation"
echo "$BEAMNG_DIR"
else
warn "Could not pre-detect a Steam BeamNG.drive installation. BeamMP will still be installed, but game launching may fail until Steam has a valid appmanifest_284160.acf and BeamNG.drive/integrity.json."
fi
if (( NEED_BUILD )); then
if [[ -n "$INSTALLED_TAG" ]]; then
log "Updating BeamMP Launcher: ${INSTALLED_TAG:-unknown} -> $LATEST_TAG"
else
log "Installing BeamMP Launcher $LATEST_TAG"
fi
log "Preparing vcpkg"
if [[ -d "$VCPKG_ROOT/.git" ]]; then
git -C "$VCPKG_ROOT" fetch --depth 1 origin master
git -C "$VCPKG_ROOT" reset --hard origin/master
else
rm -rf "$VCPKG_ROOT"
git clone --depth 1 "$VCPKG_REPO" "$VCPKG_ROOT"
fi
"$VCPKG_ROOT/bootstrap-vcpkg.sh" -disableMetrics
log "Preparing BeamMP source $LATEST_TAG"
if [[ -d "$SRC_DIR/.git" ]]; then
git -C "$SRC_DIR" fetch --tags --force origin
git -C "$SRC_DIR" reset --hard
git -C "$SRC_DIR" clean -ffd
else
rm -rf "$SRC_DIR"
git clone "$REPO" "$SRC_DIR"
fi
git -C "$SRC_DIR" checkout --detach -f "$LATEST_TAG"
git -C "$SRC_DIR" submodule sync --recursive
git -C "$SRC_DIR" submodule update --init --recursive
log "Checking Linux Steam detection compatibility"
python3 - "$SRC_DIR/src/Security/BeamNG.cpp" <<'PY'
from pathlib import Path
import re
import sys
path = Path(sys.argv[1])
if not path.is_file():
raise SystemExit(f"BeamMP source file not found: {path}")
text = path.read_text()
original = text
changes = []
# Some BeamMP versions do not include the common native Steam data directory
# in their Linux Steam-app search list.
if '".local/share/Steam/steamapps"' not in text:
vector = re.compile(
r'(std::vector<std::filesystem::path>\s+steamappsCommonPaths\s*=\s*\{\s*\n)'
)
text, count = vector.subn(
r'\1 ".local/share/Steam/steamapps", // common native Steam location\n',
text,
count=1,
)
if count:
changes.append("added ~/.local/share/Steam to the Steam search paths")
# Some BeamMP versions rely on the optional per-library \"apps\" cache in
# libraryfolders.vdf. Steam's appmanifest file is a more direct installed-app
# marker and can exist even when that cache does not contain BeamNG's app ID.
gate = re.compile(
r'\(?\s*folderInfo\.second\s*->\s*childs\s*\[\s*"apps"\s*\]\s*->\s*attribs\s*\)?'
r'\s*\.contains\s*\(\s*"284160"\s*\)\s*&&\s*'
)
replacement = (
'std::filesystem::exists(folderInfo.second->attribs["path"] '
'+ "/steamapps/appmanifest_284160.acf") && '
)
text, count = gate.subn(replacement, text, count=1)
if count:
changes.append("made BeamNG detection use appmanifest_284160.acf instead of the VDF apps cache")
elif 'contains("284160")' in text and 'appmanifest_284160.acf' not in text:
raise SystemExit(
"BeamMP changed its Linux detection code and this compatibility patch no longer matches. "
"Nothing was built. Update this installer before continuing."
)
if text != original:
path.write_text(text)
if changes:
print("Applied compatibility changes:")
for change in changes:
print(f" - {change}")
else:
print("No compatibility patch was necessary for this BeamMP source.")
PY
log "Building BeamMP Launcher with $JOBS job(s)"
rm -rf "$BUILD_DIR"
cmake \
-S "$SRC_DIR" \
-B "$BUILD_DIR" \
-DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" \
-DVCPKG_TARGET_TRIPLET="$VCPKG_TRIPLET"
cmake --build "$BUILD_DIR" --parallel "$JOBS"
[[ -x "$BUILD_DIR/BeamMP-Launcher" ]] \
|| die "The build completed without producing BeamMP-Launcher."
log "Installing BeamMP Launcher $LATEST_TAG"
install -m 0755 "$BUILD_DIR/BeamMP-Launcher" "$INSTALL_DIR/.BeamMP-Launcher.new"
mv -f "$INSTALL_DIR/.BeamMP-Launcher.new" "$LAUNCHER"
printf '%s\n' "$LATEST_TAG" > "$VERSION_FILE"
printf '%s\n' "$INSTALLER_PATCH_LEVEL" > "$PATCH_FILE"
else
log "BeamMP Launcher $LATEST_TAG is already installed and up to date"
fi
# Recreate the command and desktop entry on every run, so rerunning the
# installer also repairs them if either was deleted.
cat > "$BIN_DIR/beammp" <<EOF_WRAPPER
#!/usr/bin/env bash
set -e
cd "$INSTALL_DIR"
exec "$LAUNCHER" "\$@"
EOF_WRAPPER
chmod +x "$BIN_DIR/beammp"
cat > "$DESKTOP_DIR/beammp.desktop" <<EOF_DESKTOP
[Desktop Entry]
Type=Application
Name=BeamMP
Comment=Launch BeamMP for BeamNG.drive
Exec=$BIN_DIR/beammp
Terminal=true
Categories=Game;
StartupNotify=true
EOF_DESKTOP
chmod +x "$DESKTOP_DIR/beammp.desktop"
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database "$DESKTOP_DIR" >/dev/null 2>&1 || true
fi
# Keep a local copy of this installer so future BeamMP Launcher updates are
# one command away. This updates BeamMP itself; it does not self-update the
# installer from the internet.
SELF_PATH="$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || true)"
UPDATE_SCRIPT="$INSTALL_DIR/update-beammp.sh"
if [[ -n "$SELF_PATH" && -f "$SELF_PATH" ]]; then
if [[ "$SELF_PATH" != "$UPDATE_SCRIPT" ]]; then
install -m 0755 "$SELF_PATH" "$UPDATE_SCRIPT"
else
chmod +x "$UPDATE_SCRIPT"
fi
cat > "$BIN_DIR/beammp-update" <<EOF_UPDATER
#!/usr/bin/env bash
exec "$UPDATE_SCRIPT" "\$@"
EOF_UPDATER
chmod +x "$BIN_DIR/beammp-update"
fi
log "Done"
echo "Installed version: $LATEST_TAG"
echo "Launcher: $LAUNCHER"
echo "Run BeamMP with: $BIN_DIR/beammp"
echo
if [[ -x "$BIN_DIR/beammp-update" ]]; then
echo "Update later with: $BIN_DIR/beammp-update"
else
echo "To update later, rerun this installer."
fi
if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then
echo
echo "NOTE: ~/.local/bin is not currently in PATH. You can still run:"
echo " $BIN_DIR/beammp"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment