Created
April 30, 2026 23:50
-
-
Save techjanitor/d15ae63ec9faf84d584641e57d83d1d8 to your computer and use it in GitHub Desktop.
Steam ARM64 native installer for Apple Silicon Macs (verifies Valve's code signature before installing)
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
| #!/bin/bash | |
| # | |
| # Steam ARM64 Native Installer for Apple Silicon Macs | |
| # | |
| # Downloads the latest macOS Steam bootstrapper from Valve's CDN, verifies | |
| # its Apple code signature, and installs it to /Applications. After install, | |
| # Steam self-updates to the native ARM64 client (no Rosetta). | |
| # | |
| # Trust model: | |
| # The integrity guarantee is the Apple code signature on Steam.app, which | |
| # must be issued by Valve's developer Team ID (MXGJJ98X76). HTTPS to | |
| # client-update.steamstatic.com and the SHA1 baked into the CDN filename | |
| # are defense-in-depth: they catch corruption and accidental tampering, | |
| # not adversarial substitution (SHA1 is collision-broken). The codesign | |
| # check is the only thing that proves the bytes came from Valve. | |
| # | |
| # What it modifies: | |
| # /Applications/Steam.app (replaced) | |
| # /Applications/Steam.app.backup.<timestamp> (any prior install) | |
| # ~/Library/Application Support/Steam/package/beta (set to publicbeta | |
| # unless already set | |
| # to a different value) | |
| set -euo pipefail | |
| readonly RED=$'\033[0;31m' | |
| readonly GREEN=$'\033[0;32m' | |
| readonly YELLOW=$'\033[1;33m' | |
| readonly NC=$'\033[0m' | |
| # Valve's Apple developer Team ID. The bootstrapper's code signature must | |
| # match this; otherwise we refuse to install. | |
| readonly EXPECTED_TEAM_ID="MXGJJ98X76" | |
| # Set by main flow; consumed by cleanup() trap. | |
| TMP_DIR="" | |
| BACKUP="" | |
| err() { printf '%sError: %s%s\n' "${RED}" "$*" "${NC}" >&2; } | |
| warn() { printf '%sWarning: %s%s\n' "${YELLOW}" "$*" "${NC}"; } | |
| info() { printf '%s%s%s\n' "${YELLOW}" "$*" "${NC}"; } | |
| ok() { printf '%s%s%s\n' "${GREEN}" "$*" "${NC}"; } | |
| cleanup() { | |
| local exit_code=$? | |
| if [[ -n "${TMP_DIR}" && -d "${TMP_DIR}" ]]; then | |
| rm -rf "${TMP_DIR}" | |
| fi | |
| # If the install failed after we moved the existing Steam.app aside but | |
| # before a working replacement landed, restore the user's previous install. | |
| if [[ ${exit_code} -ne 0 \ | |
| && -n "${BACKUP}" \ | |
| && -e "${BACKUP}" \ | |
| && ! -e /Applications/Steam.app ]]; then | |
| warn "Install did not complete; restoring previous install from ${BACKUP}" | |
| mv "${BACKUP}" /Applications/Steam.app \ | |
| || err "Could not restore backup from ${BACKUP}; manual recovery required." | |
| fi | |
| } | |
| trap cleanup EXIT INT TERM | |
| echo "" | |
| echo "Steam ARM64 Native Installer for Apple Silicon Macs" | |
| echo "====================================================" | |
| echo "" | |
| # --- Pre-flight checks ---------------------------------------------------- | |
| ARCH=$(uname -m) | |
| if [[ "${ARCH}" != "arm64" ]]; then | |
| err "This script is for Apple Silicon Macs only (detected: ${ARCH})." | |
| exit 1 | |
| fi | |
| if pgrep -x "steam_osx" > /dev/null || pgrep -x "Steam" > /dev/null; then | |
| err "Steam is currently running. Please quit Steam fully and try again." | |
| exit 1 | |
| fi | |
| for cmd in curl unzip tar lipo codesign shasum xattr ditto; do | |
| if ! command -v "${cmd}" >/dev/null 2>&1; then | |
| err "required command not found: ${cmd}" | |
| exit 1 | |
| fi | |
| done | |
| # --- Step 1: Fetch manifest ----------------------------------------------- | |
| info "Step 1/8: Fetching latest package info from Valve's CDN..." | |
| MANIFEST=$(curl -sf "https://client-update.steamstatic.com/steam_client_osx") || { | |
| err "Could not reach Valve's CDN. Check your internet connection." | |
| exit 1 | |
| } | |
| if [[ -z "${MANIFEST}" ]]; then | |
| err "Manifest from CDN was empty." | |
| exit 1 | |
| fi | |
| # Pull the appdmg_osx block (skipping the steamchina mirror) and grab the | |
| # first "file" entry from it. | |
| APPDMG_BLOCK=$(printf '%s\n' "${MANIFEST}" | grep -A20 '"appdmg_osx"' | grep -v steamchina) | |
| FILE=$(printf '%s\n' "${APPDMG_BLOCK}" | grep '"file"' | awk -F'"' '{print $4}' | head -n 1) | |
| if [[ -z "${FILE}" ]]; then | |
| err "Could not parse manifest filename. Valve may have changed CDN format." | |
| exit 1 | |
| fi | |
| # Steam's CDN uses content-addressable filenames: the trailing 40 hex chars | |
| # are the SHA1 of the file. We use this to sanity-check the byte stream. | |
| EXPECTED_SHA1=$(printf '%s\n' "${FILE}" | grep -oE '[a-f0-9]{40}$' || true) | |
| echo " Bootstrapper: ${FILE}" | |
| if [[ -z "${EXPECTED_SHA1}" ]]; then | |
| err "Filename has no SHA1 suffix; CDN format has changed. Refusing to proceed." | |
| err "Update this script to match Valve's new manifest layout." | |
| exit 1 | |
| fi | |
| echo " Expected SHA1: ${EXPECTED_SHA1}" | |
| # --- Step 2: Download ------------------------------------------------------ | |
| info "Step 2/8: Downloading universal bootstrapper..." | |
| TMP_DIR=$(mktemp -d) | |
| curl -fL "https://client-update.steamstatic.com/${FILE}" -o "${TMP_DIR}/appdmg_osx.zip" | |
| # --- Step 3: Verify download integrity ------------------------------------- | |
| info "Step 3/8: Verifying SHA1 of download against filename..." | |
| ACTUAL_SHA1=$(shasum -a 1 "${TMP_DIR}/appdmg_osx.zip" | awk '{print $1}') | |
| if [[ "${ACTUAL_SHA1}" != "${EXPECTED_SHA1}" ]]; then | |
| err "SHA1 mismatch." | |
| echo " Expected: ${EXPECTED_SHA1}" | |
| echo " Actual: ${ACTUAL_SHA1}" | |
| exit 1 | |
| fi | |
| echo " Hash verified." | |
| # --- Step 4: Extract ------------------------------------------------------- | |
| info "Step 4/8: Extracting..." | |
| unzip -q "${TMP_DIR}/appdmg_osx.zip" -d "${TMP_DIR}" | |
| tar xzf "${TMP_DIR}/SteamMacBootstrapper.tar.gz" -C "${TMP_DIR}" | |
| if [[ ! -d "${TMP_DIR}/Steam.app" ]]; then | |
| err "extracted archive does not contain Steam.app." | |
| exit 1 | |
| fi | |
| # --- Step 5: Verify arm64 slice ------------------------------------------- | |
| info "Step 5/8: Verifying ARM64 support..." | |
| ARCHS=$(lipo -info "${TMP_DIR}/Steam.app/Contents/MacOS/steam_osx" 2>&1) | |
| if ! printf '%s\n' "${ARCHS}" | grep -q "arm64"; then | |
| err "downloaded binary does not contain arm64 slice. Aborting." | |
| exit 1 | |
| fi | |
| echo " ${ARCHS}" | |
| # --- Step 6: Verify Valve code signature on staged copy ------------------- | |
| info "Step 6/8: Verifying Valve code signature (pre-install)..." | |
| if ! codesign --verify --strict --deep "${TMP_DIR}/Steam.app" 2>/dev/null; then | |
| err "code signature is invalid or has been tampered with." | |
| exit 1 | |
| fi | |
| TEAM=$(codesign -dv "${TMP_DIR}/Steam.app" 2>&1 | awk -F= '/TeamIdentifier/ {print $2}') | |
| if [[ "${TEAM}" != "${EXPECTED_TEAM_ID}" ]]; then | |
| err "signature is not from Valve." | |
| echo " Expected Team ID: ${EXPECTED_TEAM_ID}" | |
| echo " Got Team ID: ${TEAM:-<none>}" | |
| exit 1 | |
| fi | |
| ok " Signature valid. Signed by Valve (Team ID: ${TEAM})." | |
| # Strip quarantine on the staged copy now, so the bundle that lands in | |
| # /Applications is already clean. The signature was just verified above, | |
| # so there is nothing to gate on first launch. | |
| xattr -dr com.apple.quarantine "${TMP_DIR}/Steam.app" 2>/dev/null || true | |
| # --- Step 7: Install ------------------------------------------------------- | |
| info "Step 7/8: Installing to /Applications..." | |
| if [[ -e /Applications/Steam.app ]]; then | |
| BACKUP="/Applications/Steam.app.backup.$(date +%Y%m%d-%H%M%S)" | |
| echo " Existing Steam.app found; moving to ${BACKUP}" | |
| mv /Applications/Steam.app "${BACKUP}" | |
| fi | |
| # ditto preserves all xattrs/ACLs/flags, so the codesign seal stays intact | |
| # across the copy. cp -R does not. | |
| ditto "${TMP_DIR}/Steam.app" /Applications/Steam.app | |
| # --- Step 8: Re-verify the installed bundle ------------------------------- | |
| info "Step 8/8: Re-verifying signature on installed bundle..." | |
| if ! codesign --verify --strict --deep /Applications/Steam.app 2>/dev/null; then | |
| err "Post-install signature check failed. The installed copy may be corrupt." | |
| err "Removing the broken install; previous version will be restored." | |
| rm -rf /Applications/Steam.app | |
| exit 1 | |
| fi | |
| ok " Installed bundle verified." | |
| # Beta channel — required for the native ARM64 client today. Don't trample | |
| # a deliberately-different value the user has set. | |
| STEAM_PKG_DIR="${HOME}/Library/Application Support/Steam/package" | |
| mkdir -p "${STEAM_PKG_DIR}" | |
| BETA_FILE="${STEAM_PKG_DIR}/beta" | |
| CURRENT_BETA="" | |
| if [[ -s "${BETA_FILE}" ]]; then | |
| CURRENT_BETA=$(<"${BETA_FILE}") | |
| fi | |
| if [[ -z "${CURRENT_BETA}" || "${CURRENT_BETA}" == "publicbeta" ]]; then | |
| echo "publicbeta" > "${BETA_FILE}" | |
| echo " Beta channel set to: publicbeta" | |
| else | |
| warn "Existing beta channel '${CURRENT_BETA}' preserved." | |
| warn "If you want the native ARM64 client, set ${BETA_FILE} to 'publicbeta'." | |
| fi | |
| echo "" | |
| ok "Done! Steam is now installed as a universal binary." | |
| echo "" | |
| echo "What happens next:" | |
| echo " 1. Launch Steam from /Applications" | |
| echo " 2. Steam will self-update to the full native ARM64 client" | |
| echo " 3. You will NOT be prompted to install Rosetta" | |
| echo "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment