2026-07-14 08:39:41 +00:00
#!/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 " ) "
2026-07-15 06:39:45 +00:00
for field in app_id channel current_version client_token launch_token license_key main_executable launcher_executable updater_executable bootstrap_executable; do
2026-07-14 08:39:41 +00:00
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 "