← Files ModRetro Chromatic PluginARCHIVED FILE
scripts/setup-compiler.sh
20.8 KB · Oct 3, 2026 · 06:38 UTC
#!/usr/bin/env bash
set -euo pipefail
# Do not let an unrelated project inject shell startup files, Node preloads,
# Python imports, or uv configuration into the trusted toolchain bootstrap.
unset BASH_ENV ENV NODE_OPTIONS NODE_PATH \
UV_PROJECT UV_WORKING_DIR UV_CONFIG_FILE UV_ENV_FILE VIRTUAL_ENV \
PYTHONPATH PYTHONHOME PYTHONSTARTUP PYTHONUSERBASE PYTHONWARNINGS
setup_gbs_version="4.3.2"
setup_gbs_commit="ccb891b2670134ba8237416772eea4ed09d34e1e"
setup_gbvm_commit="bd6f41cc5e05cbe6601dcc7f8e2db89bed527fe3"
setup_gbdk_version="4.5.0"
setup_with_cli=false
setup_with_desktop=false
setup_desktop_requested=false
setup_headless=false
setup_use_existing=false
setup_dry_run=false
setup_help=false
usage() {
printf 'Usage: %s [--headless] [--with-cli] [--with-desktop] [--use-existing] [--dry-run]\n' "$0"
printf ' macOS defaults to the verified desktop editor and GBDK.\n'
printf ' Linux defaults to headless official project compilation and GBDK.\n'
printf ' --use-existing verifies an installed official CLI and GBDK without modifying them.\n'
printf ' --dry-run prints an unverified plan without running tools.\n'
printf ' For installed plugins, use packaged setup.mjs (or setup.sh/setup.ps1) with an external dependency root.\n'
}
for setup_argument in "$@"; do
case "$setup_argument" in
--headless) setup_headless=true; setup_with_cli=true ;;
--with-cli) setup_with_cli=true ;;
--with-desktop) setup_with_desktop=true; setup_desktop_requested=true ;;
--use-existing) setup_use_existing=true ;;
--dry-run) setup_dry_run=true ;;
--help|-h) setup_help=true ;;
*) printf 'Unknown ModRetro Chromatic setup option: %s\n' "$setup_argument" >&2; usage >&2; exit 2 ;;
esac
done
if [[ "$setup_headless" == true && "$setup_with_desktop" == true ]]; then
printf '%s\n' '--headless and --with-desktop cannot be combined.' >&2
exit 2
fi
if [[ "$setup_help" == true ]]; then usage; exit 0; fi
if [[ "$setup_dry_run" == true ]]; then
setup_plan_script_directory="${BASH_SOURCE[0]%/*}"
if [[ "$setup_plan_script_directory" == "${BASH_SOURCE[0]}" ]]; then setup_plan_script_directory=.; fi
setup_plan_root="${GB_STUDIO_TOOLCHAIN_ROOT:-$setup_plan_script_directory/..}"
setup_plan_action='verify/reuse or prepare'
if [[ "$setup_use_existing" == true ]]; then setup_plan_action='verify existing'; fi
printf '%s\n' '[dry-run] Plan only; no tools were run and dependency health is unverified.'
printf '[dry-run] Dependency destination (lexical, not verified): %s/.local\n' "$setup_plan_root"
printf '[dry-run] Would %s GBDK %s.\n' \
"$setup_plan_action" "$setup_gbdk_version"
if [[ "$setup_with_cli" == true || "$OSTYPE" == linux* || "$setup_use_existing" == true ]]; then
printf '[dry-run] Official GB Studio %s CLI; setup may need Git, Node.js and Corepack.\n' "$setup_gbs_version"
fi
if [[ "$setup_with_desktop" == true || ( "$OSTYPE" == darwin* && "$setup_headless" == false && "$setup_use_existing" == false ) ]]; then
printf '[dry-run] Optional GB Studio %s desktop editor.\n' "$setup_gbs_version"
fi
printf '%s\n' '[dry-run] Installed plugins should use packaged setup.mjs (or setup.sh/setup.ps1) with an external dependency root.'
exit 0
fi
setup_script_directory="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
setup_plugin_root="$(cd -P -- "${setup_script_directory}/.." && pwd -P)"
assert_mutable_toolchain() {
local setup_candidate="$1"
while [[ -n "$setup_candidate" ]]; do
if [[ -e "$setup_candidate/.codex-plugin/local-payload.json" || -L "$setup_candidate/.codex-plugin/local-payload.json" ]] ||
[[ -f "$setup_candidate/.codex-plugin/plugin.json" && "$(<"$setup_candidate/.codex-plugin/plugin.json")" == *+codex.payload.* ]]; then
printf '%s\n' 'Legacy setup cannot write into an immutable plugin payload or cache. Use packaged setup.mjs (or setup.sh/setup.ps1) with an external dependency root.' >&2
exit 1
fi
[[ "$setup_candidate" != / ]] || break
setup_candidate="${setup_candidate%/*}"
done
}
setup_system="$(uname -s)"
setup_architecture="$(uname -m)"
case "${setup_system}:${setup_architecture}" in
Darwin:arm64|Darwin:aarch64)
setup_platform="darwin-arm64"
setup_gbs_asset="gb-studio-mac-apple-silicon.zip"
setup_gbs_sha="1b5e2c4402350223cf2810bd917a98025d85b350a48e449d1da3bb4486ff91eb"
setup_gbdk_asset="gbdk-macos-arm64.tar.gz"
setup_gbdk_sha="289ee60e46c5a2785a21e35533f84a5131ed4a063b21b0dbdedc9a10af15bf78"
;;
Darwin:x86_64|Darwin:amd64)
setup_platform="darwin-x64"
setup_gbs_asset="gb-studio-mac-intel.zip"
setup_gbs_sha="d56a850c7585f22c99b09d9c03af231fd76604a402b1f6c556979e72aefdba7c"
setup_gbdk_asset="gbdk-macos.tar.gz"
setup_gbdk_sha="1aa549d12032d8f6509d11923bb28b1a453098f42597feb378e9a42541f8fd89"
;;
Linux:aarch64|Linux:arm64)
setup_platform="linux-arm64"
setup_gbs_asset="gb-studio-linux-arm64.AppImage"
setup_gbs_sha="7249d19b9c1cc4c145f7abe08b6c1dd9518f35a983168356e1258172b713fa7d"
setup_gbdk_asset="gbdk-linux-arm64.tar.gz"
setup_gbdk_sha="31eb2235f0fdb60163d0b1e9574a022098d6069cd56606a1daca4478a46e0439"
;;
Linux:x86_64|Linux:amd64)
setup_platform="linux-x64"
setup_gbs_asset="gb-studio-linux.AppImage"
setup_gbs_sha="12f23e878974fbfcb97ac690131567a42c50f267d090a39e2c398e88b86958e0"
setup_gbdk_asset="gbdk-linux64.tar.gz"
setup_gbdk_sha="d7857a5f6d135ee4c249043ca26aad9f2ec8ab5d4106d97720d404114f42605c"
;;
*)
printf 'Unsupported ModRetro Chromatic setup platform: %s %s. Supported platforms are macOS and Linux on x64 or arm64.\n' \
"$setup_system" "$setup_architecture" >&2
exit 1
;;
esac
if [[ "$setup_system" == Darwin && "$setup_headless" == false ]]; then
setup_with_desktop=true
elif [[ "$setup_system" == Linux ]]; then
setup_with_cli=true
fi
setup_toolchain_root="${GB_STUDIO_TOOLCHAIN_ROOT:-$setup_plugin_root}"
if [[ ! -d "$setup_toolchain_root" ]]; then
printf 'The selected game toolchain root must be an existing directory: %s\n' \
"$setup_toolchain_root" >&2
exit 1
fi
setup_toolchain_root="$(cd -P -- "$setup_toolchain_root" && pwd -P)"
if [[ "$setup_use_existing" == false ]]; then assert_mutable_toolchain "$setup_toolchain_root"; fi
# Preserve relative toolchain-root arguments, then run probes and installers
# from the trusted plugin instead of a selected, potentially hostile project.
cd -P -- "$setup_script_directory"
setup_local="$setup_toolchain_root/.local"
if [[ -L "$setup_local" ]]; then
if [[ ! -d "$setup_local" ]]; then
printf 'The game toolchain .local link is broken: %s\n' "$setup_local" >&2
exit 1
fi
setup_local="$(cd -P -- "$setup_local" && pwd -P)"
if [[ "$(basename -- "$setup_local")" != ".local" ]]; then
printf 'A linked game toolchain must resolve to an actual .local directory: %s\n' \
"$setup_local" >&2
exit 1
fi
elif [[ -e "$setup_local" && ! -d "$setup_local" ]]; then
printf 'The game toolchain .local path is not a directory: %s\n' "$setup_local" >&2
exit 1
fi
if [[ "$setup_use_existing" == false ]]; then assert_mutable_toolchain "$setup_local"; fi
setup_downloads="$setup_local/downloads"
setup_apps="$setup_local/apps"
setup_vendor="$setup_local/vendor/gb-studio"
setup_cli="$setup_vendor/out/cli/gb-studio-cli.js"
setup_gbdk_compiler="$setup_local/gbdk/bin/lcc"
setup_gbdk_version_header="$setup_local/gbdk/include/gbdk/version.h"
setup_gbs_archive="$setup_downloads/$setup_gbs_asset"
setup_gbdk_archive="$setup_downloads/$setup_gbdk_asset"
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
printf 'ModRetro Chromatic setup requires %s; install it and rerun setup.\n' "$1" >&2
exit 1
fi
}
verify_cli() {
if [[ ! -f "$setup_cli" || -L "$setup_cli" ]]; then
printf 'The official GB Studio CLI is unavailable at %s\n' "$setup_cli" >&2
return 1
fi
require_command node
local reported_version
if ! reported_version="$(node "$setup_cli" --version)"; then
printf 'The official GB Studio CLI failed its version check: %s\n' "$setup_cli" >&2
return 1
fi
if [[ "$reported_version" != "$setup_gbs_version" && "$reported_version" != "v${setup_gbs_version}" ]]; then
printf 'The official GB Studio CLI reported %s; expected %s.\n' \
"$reported_version" "$setup_gbs_version" >&2
return 1
fi
}
verify_gbdk() {
if [[ ! -f "$setup_gbdk_compiler" || ! -x "$setup_gbdk_compiler" ]]; then
printf 'GBDK %s requires an existing executable compiler: %s\n' \
"$setup_gbdk_version" "$setup_gbdk_compiler" >&2
return 1
fi
if [[ ! -f "$setup_gbdk_version_header" || -L "$setup_gbdk_version_header" ]]; then
printf 'The existing GBDK installation lacks its trusted version header: %s\n' \
"$setup_gbdk_version_header" >&2
return 1
fi
if ! grep -Eq '^[[:space:]]*#[[:space:]]*define[[:space:]]+__GBDK_VERSION[[:space:]]+450([[:space:]]|$)' \
"$setup_gbdk_version_header"; then
printf 'The existing GBDK installation does not report the required %s release: %s\n' \
"$setup_gbdk_version" "$setup_gbdk_version_header" >&2
return 1
fi
if ! "$setup_gbdk_compiler" -v >/dev/null 2>&1; then
printf 'The existing GBDK compiler could not run its version probe: %s\n' \
"$setup_gbdk_compiler" >&2
return 1
fi
}
verify_existing() {
verify_gbdk
verify_cli
}
if [[ "$setup_use_existing" == true ]]; then
if [[ "$setup_desktop_requested" == true && "$setup_system" == Linux ]]; then
if [[ ! -f "$setup_apps/$setup_gbs_asset" || ! -x "$setup_apps/$setup_gbs_asset" ]]; then
printf 'The existing optional Linux desktop editor is unavailable: %s\n' \
"$setup_apps/$setup_gbs_asset" >&2
exit 1
fi
elif [[ "$setup_desktop_requested" == true && ! -d "$setup_apps/GB Studio.app" ]]; then
printf 'The existing optional macOS desktop editor is unavailable: %s\n' \
"$setup_apps/GB Studio.app" >&2
exit 1
fi
verify_existing
printf 'Verified existing official GB Studio %s CLI: %s\n' "$setup_gbs_version" "$setup_cli"
printf 'Verified existing GBDK %s compiler: %s\n' "$setup_gbdk_version" "$setup_gbdk_compiler"
exit 0
fi
if [[ -e "$setup_gbdk_compiler" && ! -x "$setup_gbdk_compiler" ]]; then
printf 'The existing GBDK compiler is not executable; repair it manually: %s\n' \
"$setup_gbdk_compiler" >&2
exit 1
fi
for setup_managed_directory in \
"$setup_downloads" "$setup_apps" "$setup_local/vendor" "$setup_vendor" \
"$setup_local/gbdk" "$setup_local/.yarn" "$setup_local/.yarn/global" \
"$setup_local/.yarn/cache" "$setup_vendor/node_modules"; do
if [[ -L "$setup_managed_directory" ]]; then
printf 'A managed game toolchain directory must not be a symbolic link: %s\n' \
"$setup_managed_directory" >&2
exit 1
fi
if [[ -e "$setup_managed_directory" && ! -d "$setup_managed_directory" ]]; then
printf 'A managed game toolchain path is not a directory: %s\n' \
"$setup_managed_directory" >&2
exit 1
fi
done
download_and_verify() (
local archive_path="$1"
local expected_sha="$2"
local archive_url="$3"
local temporary_directory=""
local quarantine_directory
local checksum_command
# Only the unique, mode-0700 directory created by this invocation is removed.
# Bad old cache files are retained separately for diagnosis, never installed.
trap 'if [[ -n "$temporary_directory" ]]; then rm -rf -- "$temporary_directory"; fi' EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
if [[ -L "$setup_downloads" || ! -d "$setup_downloads" || ! -O "$setup_downloads" ]]; then
printf 'Release downloads require an owned real directory: %s\n' "$setup_downloads" >&2
return 1
fi
if [[ -L "$archive_path" ]]; then
printf 'Refusing a symbolic-link release archive: %s\n' "$archive_path" >&2
return 1
fi
if [[ -e "$archive_path" && ! -f "$archive_path" ]]; then
printf 'The cached release archive must be a regular file: %s\n' "$archive_path" >&2
return 1
fi
if [[ -e "$archive_path" && ! -O "$archive_path" ]]; then
printf 'Refusing a release archive owned by another user: %s\n' "$archive_path" >&2
return 1
fi
if command -v sha256sum >/dev/null 2>&1; then
checksum_command=sha256sum
elif command -v shasum >/dev/null 2>&1; then
checksum_command=shasum
else
printf '%s\n' 'ModRetro Chromatic setup requires sha256sum or shasum to authenticate official releases.' >&2
return 1
fi
verify_archive() {
local candidate="$1"
local archive_bytes
if [[ -L "$candidate" || ! -f "$candidate" || ! -O "$candidate" ]]; then return 1; fi
archive_bytes="$(wc -c < "$candidate")"
if (( archive_bytes > 536870912 )); then return 1; fi
if [[ "$checksum_command" == sha256sum ]]; then
printf '%s %s\n' "$expected_sha" "$candidate" | sha256sum --check --status -
else
printf '%s %s\n' "$expected_sha" "$candidate" | shasum -a 256 -c - >/dev/null
fi
}
if [[ -f "$archive_path" ]]; then
if verify_archive "$archive_path"; then return 0; fi
quarantine_directory="$(mktemp -d "$setup_downloads/.rejected.XXXXXX")"
mv -- "$archive_path" "$quarantine_directory/"
printf 'Quarantined an invalid cached release archive at %s; retrying a fresh verified download.\n' \
"$quarantine_directory/${archive_path##*/}" >&2
fi
require_command curl
temporary_directory="$(mktemp -d "$setup_downloads/.download.XXXXXX")"
local temporary_archive="$temporary_directory/${archive_path##*/}"
if ! curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' --tlsv1.2 \
--retry 3 --connect-timeout 20 --max-time 1800 --max-filesize 536870912 \
--output "$temporary_archive" "$archive_url"; then
printf 'Official release download failed; no partial archive was cached or installed: %s\n' "$archive_url" >&2
return 1
fi
if ! verify_archive "$temporary_archive"; then
printf 'Official release SHA-256 verification failed; archive was not installed: %s\n' "$archive_path" >&2
return 1
fi
if [[ -L "$setup_downloads" || ! -d "$setup_downloads" || ! -O "$setup_downloads" ]]; then
printf 'The owned release download directory changed during download: %s\n' "$setup_downloads" >&2
return 1
fi
# Linking into the known directory publishes the verified basename atomically
# without overwriting an entry (including a link) from a concurrent invocation.
if ! ln -- "$temporary_archive" "$setup_downloads/"; then
printf 'The release cache destination changed during download; nothing was overwritten: %s\n' "$archive_path" >&2
return 1
fi
)
if [[ "$setup_with_desktop" == true ]]; then
if [[ "$setup_system" == Darwin && ! -d "$setup_apps/GB Studio.app" ]]; then
mkdir -p "$setup_downloads" "$setup_apps"
download_and_verify "$setup_gbs_archive" "$setup_gbs_sha" \
"https://github.com/chrismaltby/gb-studio/releases/download/v${setup_gbs_version}/${setup_gbs_asset}"
require_command ditto
ditto -x -k "$setup_gbs_archive" "$setup_apps"
if [[ ! -d "$setup_apps/GB Studio.app" ]]; then
setup_nested_archive="$setup_apps/gb-studio-main-${setup_platform/-/_}.zip"
if [[ ! -f "$setup_nested_archive" || -L "$setup_nested_archive" ]]; then
printf 'The official GB Studio release did not contain the expected macOS application.\n' >&2
exit 1
fi
ditto -x -k "$setup_nested_archive" "$setup_apps"
fi
if [[ ! -d "$setup_apps/GB Studio.app" ]]; then
printf 'The official GB Studio macOS application could not be extracted.\n' >&2
exit 1
fi
elif [[ "$setup_system" == Linux && ! -f "$setup_apps/$setup_gbs_asset" ]]; then
mkdir -p "$setup_downloads" "$setup_apps"
download_and_verify "$setup_gbs_archive" "$setup_gbs_sha" \
"https://github.com/chrismaltby/gb-studio/releases/download/v${setup_gbs_version}/${setup_gbs_asset}"
cp "$setup_gbs_archive" "$setup_apps/$setup_gbs_asset"
chmod +x "$setup_apps/$setup_gbs_asset"
fi
fi
if [[ ! -x "$setup_gbdk_compiler" ]]; then
mkdir -p "$setup_downloads"
download_and_verify "$setup_gbdk_archive" "$setup_gbdk_sha" \
"https://github.com/gbdk-2020/gbdk-2020/releases/download/${setup_gbdk_version}/${setup_gbdk_asset}"
require_command tar
tar -xzf "$setup_gbdk_archive" -C "$setup_local"
if [[ ! -f "$setup_gbdk_compiler" || ! -x "$setup_gbdk_compiler" ]]; then
printf 'The verified GBDK release did not contain an executable compiler: %s\n' \
"$setup_gbdk_compiler" >&2
exit 1
fi
fi
verify_gbdk
if [[ "$setup_with_cli" == true ]]; then
if [[ ! -f "$setup_cli" ]]; then
require_command git
require_command node
require_command corepack
mkdir -p "$setup_local/vendor"
if [[ -L "$setup_vendor/.git" ]]; then
printf 'The official GB Studio checkout must contain a real git directory: %s\n' \
"$setup_vendor/.git" >&2
exit 1
elif [[ ! -d "$setup_vendor/.git" ]]; then
if [[ -e "$setup_vendor" ]]; then
printf 'The existing official GB Studio checkout is invalid; repair it manually: %s\n' \
"$setup_vendor" >&2
exit 1
fi
git clone --depth 1 --branch "v${setup_gbs_version}" \
https://github.com/chrismaltby/gb-studio.git "$setup_vendor"
fi
(
cd "$setup_vendor"
setup_actual_commit="$(git rev-parse HEAD)"
if [[ "$setup_actual_commit" != "$setup_gbs_commit" ]]; then
printf 'The official GB Studio checkout has commit %s; expected verified v%s commit %s.\n' \
"$setup_actual_commit" "$setup_gbs_version" "$setup_gbs_commit" >&2
exit 1
fi
git submodule update --init --depth 1 appData/engine/gbvm
setup_actual_gbvm_commit="$(git -C appData/engine/gbvm rev-parse HEAD)"
if [[ "$setup_actual_gbvm_commit" != "$setup_gbvm_commit" ]]; then
printf 'The official GBVM source has commit %s; expected verified commit %s.\n' \
"$setup_actual_gbvm_commit" "$setup_gbvm_commit" >&2
exit 1
fi
setup_build_tools="buildTools/$setup_platform/gbdk"
if [[ -L buildTools || -L "buildTools/$setup_platform" ]]; then
printf 'The official GB Studio build-tools directories must not be symbolic links: %s\n' \
"buildTools/$setup_platform" >&2
exit 1
elif [[ ! -d "buildTools/$setup_platform" ]]; then
printf 'The official GB Studio checkout lacks build tools for %s.\n' "$setup_platform" >&2
exit 1
fi
if [[ -L "$setup_build_tools" ]]; then
printf 'GB Studio build tools require a real GBDK directory, not a symbolic link: %s\n' \
"$setup_build_tools" >&2
exit 1
elif [[ -e "$setup_build_tools" && ! -x "$setup_build_tools/bin/lcc" ]]; then
printf 'The existing GB Studio build tools are incomplete; repair them manually: %s\n' \
"$setup_build_tools" >&2
exit 1
elif [[ ! -e "$setup_build_tools" ]]; then
# Upstream's build-tools copier reads symlinks as ordinary files.
cp -R "$setup_local/gbdk" "$setup_build_tools"
fi
# Use one archive cache inside the owned root; do not inherit a second cache.
env -u YARN_NO_PROXY -u YARN_NPM_MINIMAL_AGE_GATE -u YARN_CACHE_FOLDER \
YARN_ENABLE_GLOBAL_CACHE=true \
YARN_GLOBAL_FOLDER="$setup_local/.yarn/global" \
ELECTRON_SKIP_BINARY_DOWNLOAD=1 \
corepack yarn install --immutable
if [[ ! -d node_modules/electron || -L node_modules/electron ]]; then
printf '%s\n' 'Official GB Studio dependencies did not provide a real Electron package.' >&2
exit 1
fi
# The project CLI runs in Node. Electron's package only requires this
# metadata string; its desktop binary is never launched or required.
if [[ -L node_modules/electron/path.txt ]]; then
printf '%s\n' 'Official GB Studio Electron metadata must be a real file, not a symbolic link.' >&2
exit 1
elif [[ -e node_modules/electron/path.txt && ! -f node_modules/electron/path.txt ]]; then
printf '%s\n' 'Official GB Studio Electron metadata must be a regular file.' >&2
exit 1
elif [[ ! -f node_modules/electron/path.txt ]]; then
printf 'electron' > node_modules/electron/path.txt
fi
NO_TYPE_CHECKING=1 node node_modules/webpack/bin/webpack.js \
--config ./src/apps/gb-studio-cli/webpack.cli.config.js
)
fi
verify_cli
fi
if [[ "$setup_with_desktop" == true && "$setup_system" == Darwin ]]; then
printf 'GB Studio %s desktop editor: %s\n' "$setup_gbs_version" "$setup_apps/GB Studio.app"
elif [[ "$setup_with_desktop" == true ]]; then
printf 'GB Studio %s desktop editor: %s\n' "$setup_gbs_version" "$setup_apps/$setup_gbs_asset"
else
printf '%s\n' 'GB Studio desktop editor: optional; headless project compilation requires no GUI.'
fi
printf 'GBDK %s compiler: %s\n' "$setup_gbdk_version" "$setup_gbdk_compiler"
if [[ -f "$setup_cli" ]]; then
printf 'Official GB Studio %s CLI: %s\n' "$setup_gbs_version" "$setup_cli"
else
printf '%s\n' 'Official GB Studio CLI: optional; rerun setup with --with-cli to compile .gbsproj projects.'
fi
SHA-256: 38499946eb785428f8213e46c6f31f5e9c7b3e3aa5376c4c1000173bb29c5c7e