#!/usr/bin/env bash # shellcheck disable=SC2016 set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" resolve_php_bin() { if [[ -n "${PHP_BIN:-}" ]]; then echo "$PHP_BIN" return fi if command -v keyhelp-php84 >/dev/null 2>&1; then echo "keyhelp-php84" return fi if command -v php >/dev/null 2>&1; then echo "php" return fi echo "php" } resolve_configured_php_bin() { local configured="${1:-}" local current="${2:-php}" local trimmed="$configured" trimmed="${trimmed#"${trimmed%%[![:space:]]*}"}" trimmed="${trimmed%"${trimmed##*[![:space:]]}"}" if [[ -z "$trimmed" ]]; then echo "$current" return fi if [[ "$trimmed" == "keyhelp-php-domain" ]]; then if command -v keyhelp-php-domain >/dev/null 2>&1; then echo "keyhelp-php-domain" return fi if [[ -x "/usr/bin/keyhelp-php-domain" ]]; then echo "/usr/bin/keyhelp-php-domain" return fi if [[ -x "/usr/local/bin/keyhelp-php-domain" ]]; then echo "/usr/local/bin/keyhelp-php-domain" return fi echo "Configured PHP binary 'keyhelp-php-domain' was not found." >&2 echo "Set ACP -> System -> CLI to a working custom binary (e.g. keyhelp-php84)." >&2 exit 1 fi if command -v "$trimmed" >/dev/null 2>&1; then echo "$trimmed" return fi if [[ "$trimmed" == */* && -x "$trimmed" ]]; then echo "$trimmed" return fi echo "Configured PHP binary '$trimmed' is not executable/resolvable." >&2 echo "Set ACP -> System -> CLI to a valid command or absolute executable path." >&2 exit 1 } read_setting_php_bin() { if [[ ! -f artisan ]]; then echo "" return 0 fi echo "Using bootstrap PHP binary to read system.php_binary: $PHP_BIN" >&2 "$PHP_BIN" -r ' require "vendor/autoload.php"; $app = require "bootstrap/app.php"; $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); $value = (string) \App\Models\Setting::where("key", "system.php_binary")->value("value"); echo trim($value); ' } enforce_php_requirement() { local bin="${1:-php}" echo "Validating PHP requirement from composer.json with binary: $bin" "$bin" -r ' $composer = json_decode((string) file_get_contents("composer.json"), true); $constraint = (string) ($composer["require"]["php"] ?? ""); $current = PHP_VERSION; if ($constraint === "") { fwrite(STDOUT, "No PHP requirement found in composer.json; skipping check.\n"); exit(0); } $normalize = static function (string $value): ?array { if (!preg_match("/(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?/", $value, $m)) { return null; } return [(int) $m[1], (int) ($m[2] ?? 0), (int) ($m[3] ?? 0)]; }; $cmp = static function (array $a, array $b): int { for ($i = 0; $i < 3; $i++) { if ($a[$i] > $b[$i]) { return 1; } if ($a[$i] < $b[$i]) { return -1; } } return 0; }; $partMin = static function (string $part) use ($normalize): ?array { $tokens = preg_split("/\\s+/", trim($part)) ?: []; $tokens = array_values(array_filter($tokens, static fn ($t) => $t !== "")); foreach ($tokens as $token) { if (str_starts_with($token, ">=")) { $parsed = $normalize(substr($token, 2)); if ($parsed) { return $parsed; } } } foreach ($tokens as $token) { if (str_starts_with($token, "^")) { $parsed = $normalize(substr($token, 1)); if ($parsed) { return $parsed; } } if (str_starts_with($token, "~")) { $parsed = $normalize(substr($token, 1)); if ($parsed) { return $parsed; } } } return isset($tokens[0]) ? $normalize($tokens[0]) : null; }; $parts = array_values(array_filter(array_map("trim", explode("||", $constraint)), static fn ($p) => $p !== "")); $mins = []; foreach ($parts as $part) { $min = $partMin($part); if ($min) { $mins[] = $min; } } if (!$mins) { fwrite(STDOUT, "Could not parse PHP requirement \"$constraint\"; skipping strict check.\n"); exit(0); } $required = array_reduce($mins, static function ($carry, $item) use ($cmp) { if ($carry === null) { return $item; } return $cmp($item, $carry) < 0 ? $item : $carry; }); $currentParts = $normalize($current); if (!$currentParts) { fwrite(STDERR, "Unable to parse current PHP version: $current\n"); exit(1); } $requiredString = implode(".", $required); if ($cmp($currentParts, $required) < 0) { fwrite(STDERR, "PHP requirement check failed: composer.json requires \"$constraint\" (>= $requiredString), current is $current.\n"); exit(1); } fwrite(STDOUT, "PHP requirement check passed: composer.json requires \"$constraint\" (>= $requiredString), current is $current.\n"); ' || return 1 } main() { cd "$ROOT_DIR" git restore -q bootstrap/cache/packages.php bootstrap/cache/services.php 2>/dev/null || true DIRTY="$(git status --porcelain)" DIRTY_FILTERED="$(echo "$DIRTY" | grep -vE '^( M|M ) (bootstrap/cache/(packages|services)\.php|package-lock\.json)$' || true)" if [[ -n "$DIRTY_FILTERED" ]]; then echo "Working tree is dirty. Please commit or stash changes before updating." echo "$DIRTY_FILTERED" exit 1 fi if echo "$DIRTY" | grep -qE 'package-lock\.json'; then echo "Warning: package-lock.json is modified. Continuing anyway." fi echo "Fetching latest refs..." git fetch --prune --tags echo "Checking out stable branch..." git checkout stable echo "Pulling latest stable..." git pull --ff-only PHP_BIN="$(resolve_php_bin)" echo "Initial fallback PHP binary: $PHP_BIN" if command -v "$PHP_BIN" >/dev/null 2>&1; then echo "PHP version ($PHP_BIN): $($PHP_BIN -v | head -n 1)" else echo "PHP binary '$PHP_BIN' not found in PATH." fi echo "Installing PHP dependencies..." COMPOSER_BIN="$(command -v composer || true)" if [[ -z "$COMPOSER_BIN" ]]; then echo "Composer not found in PATH." exit 1 fi echo "Running with PHP binary: $PHP_BIN $COMPOSER_BIN install --no-dev --optimize-autoloader" "$PHP_BIN" "$COMPOSER_BIN" install --no-dev --optimize-autoloader if ! CONFIGURED_PHP="$(read_setting_php_bin)"; then echo "Failed to read configured PHP binary from settings." >&2 echo "Aborting to avoid running update with the wrong PHP binary." >&2 exit 1 fi echo "Configured PHP binary from settings: ${CONFIGURED_PHP:-}" PHP_BIN="$(resolve_configured_php_bin "$CONFIGURED_PHP" "$PHP_BIN")" echo "Final PHP binary: $PHP_BIN" if command -v "$PHP_BIN" >/dev/null 2>&1; then echo "Final PHP version ($PHP_BIN): $($PHP_BIN -v | head -n 1)" fi if ! enforce_php_requirement "$PHP_BIN"; then echo "Aborting update because selected PHP binary does not satisfy composer.json requirements." >&2 exit 1 fi echo "Installing JS dependencies..." npm install echo "Building assets..." npm run build echo "Running migrations..." echo "Running with PHP binary: $PHP_BIN artisan migrate --force" "$PHP_BIN" artisan migrate --force echo "Syncing version/build to settings..." echo "Running with PHP binary: $PHP_BIN -r " VERSION="$("$PHP_BIN" -r '$c=json_decode(file_get_contents("composer.json"), true); echo $c["version"] ?? "";')" echo "Running with PHP binary: $PHP_BIN -r " BUILD="$("$PHP_BIN" -r '$c=json_decode(file_get_contents("composer.json"), true); echo $c["build"] ?? "";')" echo "Computed from composer.json: VERSION=$VERSION, BUILD=$BUILD" if [[ -n "$VERSION" || -n "$BUILD" ]]; then echo "Updating settings version/build (VERSION=$VERSION, BUILD=$BUILD)..." echo "Running with PHP binary: $PHP_BIN -r " SPEEDBB_VERSION="$VERSION" SPEEDBB_BUILD="$BUILD" "$PHP_BIN" -r ' require "vendor/autoload.php"; $app = require "bootstrap/app.php"; $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); $version = getenv("SPEEDBB_VERSION"); $build = getenv("SPEEDBB_BUILD"); if ($version !== false && $version !== "") { \Illuminate\Support\Facades\DB::table("settings")->upsert( [[ "key" => "version", "value" => $version, "created_at" => now(), "updated_at" => now(), ]], ["key"], ["value", "updated_at"] ); echo "Upserted version setting.\n"; } if ($build !== false && $build !== "") { \Illuminate\Support\Facades\DB::table("settings")->upsert( [[ "key" => "build", "value" => $build, "created_at" => now(), "updated_at" => now(), ]], ["key"], ["value", "updated_at"] ); echo "Upserted build setting.\n"; } ' \ && echo "Running with PHP binary: $PHP_BIN -r " \ && "$PHP_BIN" -r ' require "vendor/autoload.php"; $app = require "bootstrap/app.php"; $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); $version = \App\Models\Setting::where("key", "version")->value("value"); $build = \App\Models\Setting::where("key", "build")->value("value"); echo "Settings now: version={$version}, build={$build}\n"; ' fi echo "Update complete." } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then main "$@" fi