#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" SOURCE_DIR="$REPO_ROOT/out/linux/bin" CONFIG_FILE="" OUTPUT_DIR="$REPO_ROOT/dist/UpdateClient-linux" ARCHIVE_FILE="$REPO_ROOT/dist/UpdateClient-linux.tar.gz" SKIP_MANIFEST_CHECK=0 usage() { cat <<'EOF' Usage: package-client.sh --config-file FILE [options] Options: --source-dir DIR Release/install root to package. Default: ./out/linux/bin --config-file FILE app_config.json used by this client package. Required. --output-dir DIR Output directory. Default: ./dist/UpdateClient-linux --archive FILE Output tar.gz. Default: ./dist/UpdateClient-linux.tar.gz --skip-manifest-check Skip current-version manifest cache check. -h, --help Show this help. EOF } normalize_relative_path() { local value="${1:-}" value="${value//\\//}" value="${value#/}" value="${value%/}" printf '%s' "$value" } join_relative_path() { local base local child base="$(normalize_relative_path "${1:-}")" child="$(normalize_relative_path "${2:-}")" if [[ -z "$base" ]]; then printf '%s' "$child"; return; fi if [[ -z "$child" ]]; then printf '%s' "$base"; return; fi printf '%s/%s' "$base" "$child" } json_value() { python3 - "$CONFIG_FILE" "$1" <<'PY' import json import sys with open(sys.argv[1], "r", encoding="utf-8-sig") as f: data = json.load(f) value = data.get(sys.argv[2], "") print("" if value is None else value) PY } relative_to_source() { local full="$1" local fallback="$2" python3 - "$SOURCE_DIR" "$full" "$fallback" <<'PY' import os import sys source = os.path.realpath(sys.argv[1]) full = os.path.realpath(sys.argv[2]) fallback = sys.argv[3] try: rel = os.path.relpath(full, source) except ValueError: rel = fallback if rel.startswith(".."): rel = fallback print(rel.replace(os.sep, "/").strip("/")) PY } while [[ $# -gt 0 ]]; do case "$1" in --source-dir) SOURCE_DIR="$2"; shift 2 ;; --config-file) CONFIG_FILE="$2"; shift 2 ;; --output-dir) OUTPUT_DIR="$2"; shift 2 ;; --archive|--tar-file|--zip-file) ARCHIVE_FILE="$2"; shift 2 ;; --skip-manifest-check) SKIP_MANIFEST_CHECK=1; shift ;; -h|--help) usage; exit 0 ;; *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; esac done if [[ -z "$CONFIG_FILE" ]]; then echo "--config-file is required." >&2 usage >&2 exit 2 fi SOURCE_DIR="$(realpath "$SOURCE_DIR")" CONFIG_FILE="$(realpath "$CONFIG_FILE")" OUTPUT_DIR="$(realpath -m "$OUTPUT_DIR")" ARCHIVE_FILE="$(realpath -m "$ARCHIVE_FILE")" for field in app_id channel api_base_url current_version client_token launch_token license_key main_executable launcher_executable updater_executable bootstrap_executable; do if [[ -z "$(json_value "$field")" ]]; then echo "Config file is missing required field: $field" >&2 exit 1 fi done CONFIG_RELATIVE_PATH="$(relative_to_source "$CONFIG_FILE" "config/app_config.json")" CONFIG_RELATIVE_PARENT="$(dirname "$CONFIG_RELATIVE_PATH")" [[ "$CONFIG_RELATIVE_PARENT" == "." ]] && CONFIG_RELATIVE_PARENT="" RUNTIME_DIR_RELATIVE="$(normalize_relative_path "$(dirname "$CONFIG_RELATIVE_PARENT")")" [[ "$RUNTIME_DIR_RELATIVE" == "." ]] && RUNTIME_DIR_RELATIVE="" MAIN_EXECUTABLE="$(normalize_relative_path "$(json_value main_executable)")" LAUNCHER_EXECUTABLE="$(normalize_relative_path "$(json_value launcher_executable)")" UPDATER_EXECUTABLE="$(normalize_relative_path "$(json_value updater_executable)")" BOOTSTRAP_EXECUTABLE="$(normalize_relative_path "$(json_value bootstrap_executable)")" CURRENT_VERSION="$(json_value current_version)" for required in \ "$(join_relative_path "$RUNTIME_DIR_RELATIVE" "$MAIN_EXECUTABLE")" \ "$(join_relative_path "$RUNTIME_DIR_RELATIVE" "$LAUNCHER_EXECUTABLE")" \ "$(join_relative_path "$RUNTIME_DIR_RELATIVE" "$UPDATER_EXECUTABLE")" \ "$(join_relative_path "$RUNTIME_DIR_RELATIVE" "$BOOTSTRAP_EXECUTABLE")"; do if [[ ! -f "$SOURCE_DIR/$required" ]]; then echo "Source directory is missing required file: $required" >&2 exit 1 fi done DEBUG_ARTIFACT="$(find "$SOURCE_DIR" -type f \( -name '*.pdb' -o -name '*.ilk' -o -name '*d.dll' \) -print -quit)" if [[ -n "$DEBUG_ARTIFACT" ]]; then echo "Source directory contains Debug artifacts. Use a clean Release root directory." >&2 echo "Example: $DEBUG_ARTIFACT" >&2 exit 1 fi EXPECTED_MAIN_PATH="$(join_relative_path "$RUNTIME_DIR_RELATIVE" "$MAIN_EXECUTABLE")" MAIN_LEAF="$(basename "$MAIN_EXECUTABLE")" DUPLICATE_MAIN="$({ find "$SOURCE_DIR" -type f -name "$MAIN_LEAF" | while read -r item; do rel="$(python3 - "$SOURCE_DIR" "$item" <<'PY' import os, sys print(os.path.relpath(os.path.realpath(sys.argv[2]), os.path.realpath(sys.argv[1])).replace(os.sep, "/")) PY )" [[ "$rel" != "$EXPECTED_MAIN_PATH" ]] && { echo "$item"; break; } done; } || true)" if [[ -n "$DUPLICATE_MAIN" ]]; then echo "Source directory contains a duplicate main executable outside $EXPECTED_MAIN_PATH: $DUPLICATE_MAIN" >&2 exit 1 fi MANIFEST_NAME="manifest_${CURRENT_VERSION}.json" SOURCE_MANIFEST="$SOURCE_DIR/$(join_relative_path "$RUNTIME_DIR_RELATIVE" "update/manifest_cache/$MANIFEST_NAME")" if [[ ! -f "$SOURCE_MANIFEST" && -f "$SOURCE_DIR/update/manifest_cache/$MANIFEST_NAME" ]]; then SOURCE_MANIFEST="$SOURCE_DIR/update/manifest_cache/$MANIFEST_NAME" fi if [[ "$SKIP_MANIFEST_CHECK" -eq 0 && ! -f "$SOURCE_MANIFEST" ]]; then echo "Missing signed Manifest cache for current version: $SOURCE_MANIFEST. Complete online update/verification for this version before packaging." >&2 exit 1 fi rm -rf "$OUTPUT_DIR" mkdir -p "$OUTPUT_DIR" shopt -s dotglob nullglob for item in "$SOURCE_DIR"/*; do base="$(basename "$item")" case "$base" in update|update_temp) ;; *) cp -a "$item" "$OUTPUT_DIR/" ;; esac done shopt -u dotglob nullglob rm -rf "$OUTPUT_DIR/$(join_relative_path "$RUNTIME_DIR_RELATIVE" "update")" rm -rf "$OUTPUT_DIR/$(join_relative_path "$RUNTIME_DIR_RELATIVE" "update_temp")" OUTPUT_CONFIG_PATH="$OUTPUT_DIR/$CONFIG_RELATIVE_PATH" mkdir -p "$(dirname "$OUTPUT_CONFIG_PATH")" cp "$CONFIG_FILE" "$OUTPUT_CONFIG_PATH" rm -f "$(dirname "$OUTPUT_CONFIG_PATH")/client_identity.dat" \ "$(dirname "$OUTPUT_CONFIG_PATH")/local_state.json" \ "$(dirname "$OUTPUT_CONFIG_PATH")/version_policy.dat" if [[ -f "$SOURCE_MANIFEST" ]]; then MANIFEST_DIR="$OUTPUT_DIR/$(join_relative_path "$RUNTIME_DIR_RELATIVE" "update/manifest_cache")" mkdir -p "$MANIFEST_DIR" cp "$SOURCE_MANIFEST" "$MANIFEST_DIR/$MANIFEST_NAME" fi chmod +x "$OUTPUT_DIR/$(join_relative_path "$RUNTIME_DIR_RELATIVE" "$LAUNCHER_EXECUTABLE")" \ "$OUTPUT_DIR/$(join_relative_path "$RUNTIME_DIR_RELATIVE" "$UPDATER_EXECUTABLE")" \ "$OUTPUT_DIR/$(join_relative_path "$RUNTIME_DIR_RELATIVE" "$BOOTSTRAP_EXECUTABLE")" \ "$OUTPUT_DIR/$(join_relative_path "$RUNTIME_DIR_RELATIVE" "$MAIN_EXECUTABLE")" 2>/dev/null || true mkdir -p "$(dirname "$ARCHIVE_FILE")" rm -f "$ARCHIVE_FILE" tar -C "$OUTPUT_DIR" -czf "$ARCHIVE_FILE" . echo "Package directory: $OUTPUT_DIR" echo "Archive file: $ARCHIVE_FILE"