#!/usr/bin/env sh set -eu SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) APP_DIR="$SCRIPT_DIR" PKG_ROOT_NAME=${PKG_ROOT_NAME:-vmess} OUT_TAR=${OUT_TAR:-"$APP_DIR/vmess.tar.gz"} TMP_DIR=${TMP_DIR:-"/tmp/vmess_pkg.$$"} ROUTER_CONF=${ROUTER_CONF:-} if [ -z "$ROUTER_CONF" ]; then if [ -f "$APP_DIR/configs/config_router.conf" ]; then ROUTER_CONF="$APP_DIR/configs/config_router.conf" else ROUTER_CONF="$APP_DIR/config_router.conf" fi elif [ ! -f "$ROUTER_CONF" ] && [ -f "$APP_DIR/configs/$ROUTER_CONF" ]; then ROUTER_CONF="$APP_DIR/configs/$ROUTER_CONF" fi HTTP_SCRIPT=${HTTP_SCRIPT:-"$APP_DIR/scripts/router/router_local_http.sh"} UPDATE_SCRIPT=${UPDATE_SCRIPT:-"$APP_DIR/scripts/router/router_local_update.sh"} # Defaults aligned with current repo layout. BUSYBOX_SRC=${BUSYBOX_SRC:-"$APP_DIR/busybox/busybox-armv7l"} CFST_SRC_DIR=${CFST_SRC_DIR:-"$APP_DIR/cfst_linux_armv5"} VERBOSE=${VERBOSE:-1} SYNC_FROM_CONFIG=${SYNC_FROM_CONFIG:-} WRITE_BACK=${WRITE_BACK:-0} while [ $# -gt 0 ]; do case "$1" in --sync-from=*|--sync-from-config=*|--sync-from-json=*) SYNC_FROM_CONFIG="${1#*=}" shift ;; --sync-from|--sync-from-config|--sync-from-json) [ $# -ge 2 ] || { printf '[error] %s requires a file path argument\n' "$1" >&2; exit 1; } SYNC_FROM_CONFIG="$2" shift 2 ;; --write-back) WRITE_BACK=1 shift ;; *) shift ;; esac done log() { if [ "${VERBOSE}" = "1" ]; then printf '%s\n' "$*" fi } warn() { printf '[warn] %s\n' "$*" >&2 } err() { printf '[error] %s\n' "$*" >&2 exit 1 } require_file() { p="$1" [ -f "$p" ] || err "required file missing: $p" } require_dir() { p="$1" [ -d "$p" ] || err "required directory missing: $p" } parse_conf_var() { conf="$1" key="$2" if [ ! -r "$conf" ]; then return 1 fi sed -n "s|^${key}=\"\\(.*\\)\"$|\\1|p" "$conf" | head -n 1 } resolve_from_conf_dir() { conf="$1" val="$2" conf_dir=$(CDPATH= cd -- "$(dirname -- "$conf")" && pwd) case "$val" in "") printf '%s\n' "" ;; /*) printf '%s\n' "$val" ;; *) printf '%s/%s\n' "$conf_dir" "$val" ;; esac } check_busybox_httpd() { bb="$1" if [ ! -f "$bb" ]; then warn "BusyBox binary missing: $bb" return 1 fi if [ -x "$bb" ] && "$bb" --list 2>/dev/null | grep '^httpd$' >/dev/null 2>&1; then return 0 fi # 跨平台构建(例如在 macOS 上打包 Linux ARM ELF 二进制),回退到静态字符串检查 if strings "$bb" 2>/dev/null | grep -q 'httpd'; then return 0 fi warn "BusyBox has no httpd applet: $bb" return 1 } log "[pkg] app dir: $APP_DIR" log "[pkg] output: $OUT_TAR" require_file "$ROUTER_CONF" require_file "$HTTP_SCRIPT" require_file "$UPDATE_SCRIPT" require_dir "$CFST_SRC_DIR" require_file "$BUSYBOX_SRC" # 基础文件校验通过后再安全初始化临时目录与 trap case "$TMP_DIR" in ""|"/"|"/tmp"|"/var"|"$APP_DIR"|"$HOME") err "Unsafe TMP_DIR: '$TMP_DIR'" ;; esac cleanup() { rm -rf "$TMP_DIR" } trap cleanup EXIT INT TERM bb_from_conf_raw=$(parse_conf_var "$ROUTER_CONF" "BUSYBOX_BIN" || true) if [ -n "${bb_from_conf_raw:-}" ]; then bb_from_conf=$(resolve_from_conf_dir "$ROUTER_CONF" "$bb_from_conf_raw") if [ "$bb_from_conf" != "$BUSYBOX_SRC" ]; then warn "BUSYBOX_BIN in conf points to: $bb_from_conf_raw -> $bb_from_conf" warn "packaging source busybox is: $BUSYBOX_SRC" warn "if needed, set BUSYBOX_SRC to keep packaging and runtime consistent" fi fi sync_conf_from_json() { src_conf="$1" json_file="$2" dst_conf="$3" if ! command -v python3 >/dev/null 2>&1; then warn "python3 not found, skip syncing router config from JSON" cp "$src_conf" "$dst_conf" return 0 fi python3 - "$src_conf" "$json_file" "$dst_conf" <<'EOF' import json import re import sys src_conf_path, json_path, dst_conf_path = sys.argv[1], sys.argv[2], sys.argv[3] with open(json_path, "r", encoding="utf-8") as f: cfg = json.load(f) updates = {} out_cfg = cfg.get("output", {}) if "selected_value_file" in out_cfg: updates["VALUE_TEXT_FILE"] = str(out_cfg["selected_value_file"]) if "selected_value_json" in out_cfg: updates["VALUE_JSON_FILE"] = str(out_cfg["selected_value_json"]) if "selected_value_json_key" in out_cfg: updates["VALUE_JSON_KEY"] = str(out_cfg["selected_value_json_key"]) if "state_last_good_key" in out_cfg: updates["STATE_LAST_GOOD_KEY"] = str(out_cfg["state_last_good_key"]) if "export_vars_file" in out_cfg: updates["EXPORT_VARS_FILE"] = str(out_cfg["export_vars_file"]) if "substore_value_key" in out_cfg: updates["EXPORT_VALUE_KEY"] = str(out_cfg["substore_value_key"]) sel_cfg = cfg.get("selection", {}) if "top_n" in sel_cfg: updates["TOP_N"] = str(sel_cfg["top_n"]) cfst_cfg = cfg.get("cfst_local", {}) run_args = cfst_cfg.get("run_args", []) if isinstance(run_args, list): arg_map = { "-p": "CFST_DISPLAY_COUNT", "-f": "CFST_IP_FILE", "-n": "CFST_THREADS", "-t": "CFST_TEST_COUNT", "-dn": "CFST_DOWNLOAD_COUNT", "-dt": "CFST_DOWNLOAD_TIME", "-tp": "CFST_PORT", "-url": "CFST_URL", "-cfcolo": "CFST_CFCOLO", "-tl": "CFST_LATENCY_LIMIT", "-tll": "CFST_LATENCY_LOWER", "-sl": "CFST_LOSS_LIMIT", "-speedlimit": "CFST_SPEED_LIMIT", } i = 0 while i < len(run_args): flag = str(run_args[i]).strip() if flag in arg_map and i + 1 < len(run_args): updates[arg_map[flag]] = str(run_args[i + 1]).strip() i += 2 elif flag == "-httping": updates["CFST_HTTPING"] = "1" i += 1 elif flag == "-dd": updates["CFST_DISABLE_DOWNLOAD"] = "1" i += 1 elif flag == "-allip": updates["CFST_ALL_IP"] = "1" i += 1 else: i += 1 with open(src_conf_path, "r", encoding="utf-8") as f: conf_lines = f.readlines() new_lines = [] modified_keys = set() for line in conf_lines: m = re.match(r'^([A-Za-z0-9_]+)="(.*)"', line.strip()) if m: key = m.group(1) if key in updates: new_lines.append(f'{key}="{updates[key]}"\n') modified_keys.add(key) continue new_lines.append(line) for key, val in updates.items(): if key not in modified_keys: new_lines.append(f'{key}="{val}"\n') with open(dst_conf_path, "w", encoding="utf-8") as f: f.writelines(new_lines) EOF } if ! check_busybox_httpd "$BUSYBOX_SRC"; then warn "continuing packaging, but router HTTP may fail later" fi mkdir -p "$TMP_DIR/$PKG_ROOT_NAME/scripts" "$TMP_DIR/$PKG_ROOT_NAME/cfst" if [ -n "$SYNC_FROM_CONFIG" ]; then if [ ! -f "$SYNC_FROM_CONFIG" ] && [ -f "$APP_DIR/configs/$SYNC_FROM_CONFIG" ]; then SYNC_FROM_CONFIG="$APP_DIR/configs/$SYNC_FROM_CONFIG" fi require_file "$SYNC_FROM_CONFIG" log "[pkg] syncing router config from $SYNC_FROM_CONFIG" sync_conf_from_json "$ROUTER_CONF" "$SYNC_FROM_CONFIG" "$TMP_DIR/$PKG_ROOT_NAME/config_router.conf" if [ "$WRITE_BACK" = "1" ]; then cp "$TMP_DIR/$PKG_ROOT_NAME/config_router.conf" "$ROUTER_CONF" log "[pkg] written synced config back to $ROUTER_CONF" fi else cp "$ROUTER_CONF" "$TMP_DIR/$PKG_ROOT_NAME/config_router.conf" fi cp "$HTTP_SCRIPT" "$TMP_DIR/$PKG_ROOT_NAME/scripts/router_local_http.sh" cp "$UPDATE_SCRIPT" "$TMP_DIR/$PKG_ROOT_NAME/scripts/router_local_update.sh" cp "$BUSYBOX_SRC" "$TMP_DIR/$PKG_ROOT_NAME/busybox_armv7l" cp -R "$CFST_SRC_DIR"/. "$TMP_DIR/$PKG_ROOT_NAME/cfst/" chmod +x \ "$TMP_DIR/$PKG_ROOT_NAME/busybox_armv7l" \ "$TMP_DIR/$PKG_ROOT_NAME/scripts/router_local_http.sh" \ "$TMP_DIR/$PKG_ROOT_NAME/scripts/router_local_update.sh" || true rm -f "$OUT_TAR" COPYFILE_DISABLE=1 tar --exclude='.DS_Store' --exclude='._*' -czf "$OUT_TAR" -C "$TMP_DIR" "$PKG_ROOT_NAME" log "[pkg] tar created: $OUT_TAR" log "[pkg] tar size: $(ls -lh "$OUT_TAR" | awk '{print $5}')" log "[pkg] tar content preview:" tar -tzf "$OUT_TAR" | sed -n '1,120p' if ! tar -tzf "$OUT_TAR" | grep "^$PKG_ROOT_NAME/busybox_armv7l$" >/dev/null; then err "package validation failed: busybox_armv7l missing" fi if ! tar -tzf "$OUT_TAR" | grep "^$PKG_ROOT_NAME/scripts/router_local_http.sh$" >/dev/null; then err "package validation failed: router_local_http.sh missing" fi if ! tar -tzf "$OUT_TAR" | grep "^$PKG_ROOT_NAME/cfst/cfst$" >/dev/null; then err "package validation failed: cfst binary missing" fi log "[pkg] validation passed"