Created
September 3, 2026 02:08
-
-
Save 20plays/69d905f728a2333665c6cc42e5accbdb to your computer and use it in GitHub Desktop.
BeamMP Installer for Linux
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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