Add strict offline asset bundles

This commit is contained in:
Arvin
2026-07-30 17:59:35 +08:00
parent 3be0915a71
commit 2de0456ef3
8 changed files with 873 additions and 123 deletions
+9
View File
@@ -8,6 +8,15 @@
# Runtime
runtime/
# Optional offline assets
resources/bin/mihomo/
resources/bin/clash/
resources/bin/yq/
resources/bin/subconverter/
resources/geo/
resources/asset-manifest.env
resources/SHA256SUMS
# Local dashboard unpacked files
resources/dashboard/dist/
+41 -6
View File
@@ -312,13 +312,14 @@ MIXED_PORT=7890
EXTERNAL_CONTROLLER=0.0.0.0:9090
CLASH_CONTROLLER_SECRET=your-secret
CLASH_SUBSCRIPTION_URL=https://example.com/sub
MIHOMO_VERSION=latest
CLASH_VERSION=latest
YQ_VERSION=v4.44.3
MIHOMO_VERSION=v1.19.23
CLASH_VERSION=v1.18.0
YQ_VERSION=v4.52.4
SUBCONVERTER_VERSION=v0.9.9
MIHOMO_DOWNLOAD_BASE=https://github.com/MetaCubeX/mihomo/releases/download
CLASH_DOWNLOAD_BASE=https://github.com/WindSpiritSR/clash/releases/download
CLASH_BUNDLED_ASSET_ENABLED=true
CLASH_OFFLINE=false
CLASH_SHELL_AUTO_RESTORE_PROXY=true
CLASH_PREDOWNLOAD_GEO=true
```
@@ -349,9 +350,7 @@ CLASH_GH_PROXY=https://ghfast.top
当前正式支持的架构为 `amd64`、`arm64`、`armv7`。超出这三种架构时会明确失败,不会伪装成已支持。
如果安装环境访问 GitHub 很慢,可以把 Mihomo、yq、subconverter 的刚需文件跟随项目一起分发。安装和 `clash upgrade` 会优先读取 `resources/bin` 中与当前版本、架构对应的精确文件名;本地没有对应文件时,会回退到原来的远程下载逻辑,不影响后续升级内核。
Clash 仅作为兼容内核处理,固定走远程下载,不会命中 `resources/bin` 中的本地资源。
如果安装环境访问 GitHub 很慢,可以把 Mihomo/Clash、yq、subconverter 的刚需文件跟随项目一起分发。安装和 `clash upgrade` 会优先读取 `resources/bin` 中与当前版本、架构对应的精确文件名;普通在线模式下,本地没有对应文件时会回退到远程下载。
推荐路径直接放在分类目录下:
@@ -359,6 +358,9 @@ Clash 仅作为兼容内核处理,固定走远程下载,不会命中 `resour
resources/bin/mihomo/mihomo-linux-amd64-compatible-v1.19.23.gz
resources/bin/mihomo/mihomo-linux-arm64-v1.19.23.gz
resources/bin/mihomo/mihomo-linux-armv7-v1.19.23.gz
resources/bin/clash/clash-linux-amd64-v1.18.0.gz
resources/bin/clash/clash-linux-arm64-v1.18.0.gz
resources/bin/clash/clash-linux-armv7-v1.18.0.gz
resources/bin/yq/yq_linux_amd64.tar.gz
resources/bin/yq/yq_linux_arm64.tar.gz
resources/bin/yq/yq_linux_arm.tar.gz
@@ -372,6 +374,39 @@ resources/geo/Country.mmdb
也可以设置 `CLASH_BUNDLED_ASSET_ENABLED=false` 强制跳过内置文件,或用 `CLASH_BUNDLED_ASSET_DIR=/path/to/assets` 指向项目外的资源目录。Mihomo、yq、subconverter 兼容旧路径 `resources/bin/<category>/<version>/<file>`。
#### 离线资源包
推荐在有代理或网络较好的设备上,按目标主机架构生成资源包:
```bash
# 查看将要下载的文件,不产生网络请求
bash scripts/prefetch-assets.sh --arch amd64 --dry-run
# 生成 amd64 + Mihomo 资源包
bash scripts/prefetch-assets.sh \
--arch amd64 \
--kernel mihomo \
--output clash-assets-mihomo-amd64.tar.gz
```
支持的目标架构为 `amd64`、`arm64`、`armv7`。脚本默认包含内核、yq、subconverter 和五个 GEO 文件,只打包 `resources/` 静态资源,不会把本机的订阅、日志、密钥或其他 `runtime/` 状态带入资源包。压缩包同时包含版本/架构清单与 `SHA256SUMS`。
把项目和资源包复制到目标主机,在项目根目录执行:
```bash
tar -xzf clash-assets-mihomo-amd64.tar.gz
bash install.sh --offline
```
`--offline` 等价于本次安装设置 `CLASH_OFFLINE=true`。安装脚本会先集中检查架构、版本、校验和及所有必需文件;有任何缺失都会明确列出并停止,不会静默回退到 GitHub。也可以和安装范围一起使用:
```bash
bash install.sh system --offline
bash install.sh user --offline
```
严格离线安装时,订阅也不能使用远程 URL。可暂时不配置订阅,或者提前准备 Clash YAML 并通过 `file:///绝对路径/config.yaml` 导入。使用 `--no-geo` 生成精简资源包时,目标项目还需要设置 `CLASH_PREDOWNLOAD_GEO=false`;若最终配置使用 `GEOIP`,仍须提供 `Country.mmdb`。
### `runtime/mixin.yaml`(兼容 `config/mixin.yaml`)
用于对最终运行配置做补丁:
+13 -2
View File
@@ -12,10 +12,20 @@ source "$PROJECT_DIR/scripts/init/systemd-user.sh"
source "$PROJECT_DIR/scripts/init/script.sh"
init_project_context "$PROJECT_DIR"
guard_unsafe_sudo_auto_install "${1:-}"
parse_install_options "$@"
if [ "$INSTALL_SHOW_HELP" = "true" ]; then
print_install_usage
exit 0
fi
guard_unsafe_sudo_auto_install "$INSTALL_REQUESTED_SCOPE"
load_env_if_exists
if [ "$INSTALL_OFFLINE_REQUESTED" = "true" ]; then
export CLASH_OFFLINE="true"
fi
migrate_env_legacy_compat_fields "$PROJECT_DIR/.env"
detect_install_scope "${1:-auto}"
detect_install_scope "$INSTALL_REQUESTED_SCOPE"
ensure_project_not_wsl_windows_mount
ensure_openwrt_install_supported
@@ -23,6 +33,7 @@ ensure_required_commands
init_layout
ensure_dashboard_deploy_prerequisites
preflight_offline_assets
resolve_geo_assets
+13 -3
View File
@@ -1,8 +1,6 @@
# 内置运行依赖
如果安装时 GitHub 下载太慢,可以把 Mihomo、yq、subconverter 提前放到本目录。安装和 `clash upgrade` 会优先读取这里的文件;没有匹配文件时,仍会回退到原来的下载逻辑。
Clash 是兼容内核,固定走远程下载,不使用本目录中的本地资源。
如果安装时 GitHub 下载太慢,可以把 Mihomo/Clash、yq、subconverter 提前放到本目录。安装和 `clash upgrade` 会优先读取这里的文件;普通在线模式下,没有匹配文件时仍会回退到原来的下载逻辑。
默认目录结构:
@@ -12,6 +10,10 @@ resources/bin/
mihomo-linux-amd64-compatible-v1.19.23.gz
mihomo-linux-arm64-v1.19.23.gz
mihomo-linux-armv7-v1.19.23.gz
clash/
clash-linux-amd64-v1.18.0.gz
clash-linux-arm64-v1.18.0.gz
clash-linux-armv7-v1.18.0.gz
yq/
yq_linux_amd64.tar.gz
yq_linux_arm64.tar.gz
@@ -34,3 +36,11 @@ export CLASH_BUNDLED_ASSET_DIR=/path/to/assets
```
`Country.mmdb` 仍放在 `resources/geo/Country.mmdb` 或 `resources/geo/country.mmdb`。
推荐通过项目提供的脚本生成完整资源包:
```bash
bash scripts/prefetch-assets.sh --arch amd64 --kernel mihomo
```
将生成的压缩包复制到目标主机并解压到项目根目录后,可执行 `bash install.sh --offline`。严格离线模式会在安装前检查全部资源,缺失时直接失败,不会回退联网下载。资源包只包含 `resources/`,不会包含可能带有订阅、日志或密钥的 `runtime/`。
+147 -10
View File
@@ -428,16 +428,75 @@ bundled_asset_root() {
echo "$RESOURCE_DIR/bin"
}
copy_bundled_asset() {
bundled_asset_metadata_root() {
if [ -n "${CLASH_BUNDLED_ASSET_DIR:-}" ]; then
echo "$CLASH_BUNDLED_ASSET_DIR"
return 0
fi
[ -n "${RESOURCE_DIR:-}" ] || return 1
echo "$RESOURCE_DIR"
}
bundled_asset_manifest_file() {
local root
root="$(bundled_asset_metadata_root 2>/dev/null || true)"
[ -n "${root:-}" ] || return 1
echo "$root/asset-manifest.env"
}
bundled_asset_checksums_file() {
local root
root="$(bundled_asset_metadata_root 2>/dev/null || true)"
[ -n "${root:-}" ] || return 1
echo "$root/SHA256SUMS"
}
read_bundled_asset_manifest_value() {
local key="$1"
local file
file="$(bundled_asset_manifest_file 2>/dev/null || true)"
[ -n "${file:-}" ] && [ -f "$file" ] || return 1
sed -nE "s/^[[:space:]]*${key}=['\"]?([^'\"]*)['\"]?$/\1/p" "$file" | head -n 1
}
bundled_asset_manifest_compatible() {
local category="$1"
local version="$2"
local file bundle_arch bundle_version version_key
file="$(bundled_asset_manifest_file 2>/dev/null || true)"
[ -n "${file:-}" ] && [ -f "$file" ] || return 0
bundle_arch="$(read_bundled_asset_manifest_value "BUNDLE_ARCH" 2>/dev/null || true)"
if [ -n "${bundle_arch:-}" ] && [ "$bundle_arch" != "$(get_arch)" ]; then
return 1
fi
case "$category" in
mihomo) version_key="MIHOMO_VERSION" ;;
clash) version_key="CLASH_VERSION" ;;
yq) version_key="YQ_VERSION" ;;
subconverter) version_key="SUBCONVERTER_VERSION" ;;
*) return 0 ;;
esac
bundle_version="$(read_bundled_asset_manifest_value "$version_key" 2>/dev/null || true)"
[ -z "${bundle_version:-}" ] || [ "$bundle_version" = "$version" ]
}
find_bundled_asset() {
local category="$1"
local version="$2"
local file="$3"
local out="$4"
local asset_name="${5:-$file}"
local root candidate
[ "$category" != "clash" ] || return 1
bundled_asset_enabled || return 1
bundled_asset_manifest_compatible "$category" "$version" || return 1
root="$(bundled_asset_root 2>/dev/null || true)"
[ -n "${root:-}" ] || return 1
@@ -448,18 +507,48 @@ copy_bundled_asset() {
"$root/$file" \
"$RESOURCE_DIR/$category/$file"; do
[ -s "$candidate" ] || continue
mkdir -p "$(dirname "$out")"
if [ "$candidate" != "$out" ]; then
cp -f "$candidate" "$out"
fi
success "${asset_name} 已使用内置资源:$candidate"
echo "$candidate"
return 0
done
return 1
}
copy_bundled_asset() {
local category="$1"
local version="$2"
local file="$3"
local out="$4"
local asset_name="${5:-$file}"
local candidate
candidate="$(find_bundled_asset "$category" "$version" "$file" 2>/dev/null || true)"
[ -n "${candidate:-}" ] || return 1
mkdir -p "$(dirname "$out")"
if [ "$candidate" != "$out" ]; then
cp -f "$candidate" "$out"
fi
success "${asset_name} 已使用内置资源:$candidate"
}
offline_mode_enabled() {
case "${CLASH_OFFLINE:-false}" in
true|1|yes|on|TRUE|YES|ON) return 0 ;;
*) return 1 ;;
esac
}
offline_download_blocked() {
local asset_name="$1"
local url="${2:-}"
local url_hint=""
[ -n "${url:-}" ] && url_hint=";远程地址:$url"
die_state "离线模式禁止下载:${asset_name}${url_hint}" \
"请补齐离线资源包后重试,或移除 --offline / CLASH_OFFLINE=true 允许联网下载"
}
download_connect_timeout() {
echo "${CLASH_DOWNLOAD_CONNECT_TIMEOUT:-8}"
}
@@ -934,6 +1023,10 @@ download_file() {
local tried_urls=""
local fetch_tmp
if offline_mode_enabled; then
offline_download_blocked "$asset_name" "$url"
fi
mkdir -p "$(dirname "$out")"
rm -f "$out" 2>/dev/null || true
@@ -1031,6 +1124,10 @@ download_http_fetch_to_file() {
local max_time="${5:-300}"
local progress_arg="--progress-bar"
if offline_mode_enabled; then
offline_download_blocked "$(basename "${url%%\?*}")" "$url"
fi
curl_download \
"$progress_arg" \
--show-error \
@@ -1171,6 +1268,46 @@ guard_unsafe_sudo_auto_install() {
fi
}
parse_install_options() {
local arg
INSTALL_REQUESTED_SCOPE="auto"
INSTALL_OFFLINE_REQUESTED="false"
INSTALL_SHOW_HELP="false"
for arg in "$@"; do
case "$arg" in
system|user|auto)
if [ "$INSTALL_REQUESTED_SCOPE" != "auto" ] && [ "$INSTALL_REQUESTED_SCOPE" != "$arg" ]; then
die_usage "安装范围不能同时指定为 $INSTALL_REQUESTED_SCOPE 和 $arg" \
"用法:bash install.sh [system|user|auto] [--offline]"
fi
INSTALL_REQUESTED_SCOPE="$arg"
;;
--offline)
INSTALL_OFFLINE_REQUESTED="true"
;;
-h|--help)
INSTALL_SHOW_HELP="true"
;;
*)
die_usage "未知安装参数:$arg" \
"用法:bash install.sh [system|user|auto] [--offline]"
;;
esac
done
}
print_install_usage() {
cat <<'EOF'
用法:
bash install.sh [system|user|auto] [--offline]
选项:
--offline 严格离线安装;缺少本地资源时直接失败,不回退联网下载
EOF
}
detect_install_scope() {
local requested="${1:-auto}"
+261 -47
View File
@@ -13,10 +13,256 @@ GEO_ASSET_DOWNLOADS=(
"resources/geo/GeoSite.dat https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat"
)
resolve_geo_assets() {
geo_predownload_enabled() {
case "${CLASH_PREDOWNLOAD_GEO:-true}" in
0|false|no|off|disable|disabled|FALSE|NO|OFF) return 0 ;;
0|false|no|off|disable|disabled|FALSE|NO|OFF) return 1 ;;
*) return 0 ;;
esac
}
yq_asset_file() {
case "$1" in
amd64) echo "yq_linux_amd64.tar.gz" ;;
arm64) echo "yq_linux_arm64.tar.gz" ;;
armv7) echo "yq_linux_arm.tar.gz" ;;
*) die "暂不支持的 yq 架构:$1" ;;
esac
}
mihomo_asset_file() {
local arch="$1"
local version="$2"
case "$arch" in
amd64) echo "mihomo-linux-amd64-compatible-${version}.gz" ;;
arm64) echo "mihomo-linux-arm64-${version}.gz" ;;
armv7) echo "mihomo-linux-armv7-${version}.gz" ;;
*) die "暂不支持的 Mihomo 架构:$arch" ;;
esac
}
subconverter_asset_file() {
case "$1" in
amd64) echo "subconverter_linux64.tar.gz" ;;
arm64) echo "subconverter_aarch64.tar.gz" ;;
armv7) echo "subconverter_armv7.tar.gz" ;;
*) die "暂不支持的 subconverter 架构:$1" ;;
esac
}
clash_asset_candidates() {
local arch="$1"
local version="$2"
local version_no_v="${version#v}"
case "$arch" in
amd64)
printf '%s\n' \
"clash-linux-amd64-${version}.gz" \
"clash-linux-amd64-v${version_no_v}.gz"
;;
arm64)
printf '%s\n' \
"clash-linux-arm64-${version}.gz" \
"clash-linux-arm64-v${version_no_v}.gz" \
"clash-linux-armv8-${version}.gz" \
"clash-linux-armv8-v${version_no_v}.gz"
;;
armv7)
printf '%s\n' \
"clash-linux-armv7-${version}.gz" \
"clash-linux-armv7-v${version_no_v}.gz"
;;
*)
die "暂不支持的 Clash 架构:$arch"
;;
esac
}
verify_offline_asset_bundle_metadata() {
local manifest checksums metadata_root actual expected kernel
local failed="false"
manifest="$(bundled_asset_manifest_file 2>/dev/null || true)"
checksums="$(bundled_asset_checksums_file 2>/dev/null || true)"
metadata_root="$(bundled_asset_metadata_root 2>/dev/null || true)"
if [ -n "${manifest:-}" ] && [ -f "$manifest" ]; then
actual="$(read_bundled_asset_manifest_value "BUNDLE_ARCH" 2>/dev/null || true)"
expected="$(get_arch)"
if [ -n "${actual:-}" ] && [ "$actual" != "$expected" ]; then
error "离线资源包架构不匹配:资源包=$actual,目标=$expected"
failed="true"
fi
actual="$(read_bundled_asset_manifest_value "YQ_VERSION" 2>/dev/null || true)"
expected="${YQ_VERSION:-$DEFAULT_YQ_VERSION}"
if [ -n "${actual:-}" ] && [ "$actual" != "$expected" ]; then
error "离线资源包 yq 版本不匹配:资源包=$actual,目标=$expected"
failed="true"
fi
actual="$(read_bundled_asset_manifest_value "SUBCONVERTER_VERSION" 2>/dev/null || true)"
expected="${SUBCONVERTER_VERSION:-$DEFAULT_SUBCONVERTER_VERSION}"
if [ -n "${actual:-}" ] && [ "$actual" != "$expected" ]; then
error "离线资源包 subconverter 版本不匹配:资源包=$actual,目标=$expected"
failed="true"
fi
kernel="$(runtime_kernel_type)"
case "$kernel" in
mihomo)
actual="$(read_bundled_asset_manifest_value "MIHOMO_VERSION" 2>/dev/null || true)"
expected="${MIHOMO_VERSION:-$DEFAULT_MIHOMO_VERSION}"
;;
clash)
actual="$(read_bundled_asset_manifest_value "CLASH_VERSION" 2>/dev/null || true)"
expected="${CLASH_VERSION:-$DEFAULT_CLASH_VERSION}"
;;
*)
actual=""
expected=""
;;
esac
if [ -n "${actual:-}" ] && [ "$actual" != "$expected" ]; then
error "离线资源包 $kernel 版本不匹配:资源包=$actual,目标=$expected"
failed="true"
fi
fi
if [ -n "${checksums:-}" ] && [ -f "$checksums" ]; then
if command -v sha256sum >/dev/null 2>&1; then
if ! (cd "$metadata_root" && sha256sum -c "$(basename "$checksums")" >/dev/null); then
error "离线资源包 SHA256 校验失败:$checksums"
failed="true"
fi
elif command -v shasum >/dev/null 2>&1; then
if ! (cd "$metadata_root" && shasum -a 256 -c "$(basename "$checksums")" >/dev/null); then
error "离线资源包 SHA256 校验失败:$checksums"
failed="true"
fi
else
warn "系统缺少 sha256sum/shasum,已跳过离线资源包完整性校验"
fi
fi
[ "$failed" = "false" ]
}
offline_asset_missing_text() {
local category="$1"
local version="$2"
local file="$3"
local label="$4"
if [ -n "${CLASH_BUNDLED_ASSET_DIR:-}" ]; then
printf '%s:%s/%s/%s(版本 %s)' \
"$label" "${CLASH_BUNDLED_ASSET_DIR%/}" "$category" "$file" "$version"
return 0
fi
case "$category" in
geo)
printf '%s:resources/geo/%s' "$label" "$file"
;;
*)
printf '%s:resources/bin/%s/%s(版本 %s)' "$label" "$category" "$file" "$version"
;;
esac
}
preflight_offline_assets() {
offline_mode_enabled || return 0
local arch kernel version file item path
local missing=()
local candidate_found="false"
arch="$(get_arch)"
kernel="$(runtime_kernel_type)"
ui_section "离线资源预检"
ui_kv "🧩" "目标架构" "$arch"
ui_kv "🚀" "目标内核" "$kernel"
if ! verify_offline_asset_bundle_metadata; then
die_state "离线资源包元数据或完整性校验失败" \
"请重新生成与当前架构、版本匹配的资源包"
fi
case "$kernel" in
mihomo)
version="${MIHOMO_VERSION:-$DEFAULT_MIHOMO_VERSION}"
file="$(mihomo_asset_file "$arch" "$version")"
if [ ! -x "$(mihomo_bin)" ] \
&& ! find_bundled_asset "mihomo" "$version" "$file" >/dev/null 2>&1; then
missing+=("$(offline_asset_missing_text "mihomo" "$version" "$file" "Mihomo")")
fi
;;
clash)
version="${CLASH_VERSION:-$DEFAULT_CLASH_VERSION}"
if [ ! -x "$(clash_bin)" ]; then
candidate_found="false"
while IFS= read -r file; do
[ -n "${file:-}" ] || continue
if find_bundled_asset "clash" "$version" "$file" >/dev/null 2>&1; then
candidate_found="true"
break
fi
done <<EOF
$(clash_asset_candidates "$arch" "$version")
EOF
if [ "$candidate_found" != "true" ]; then
file="$(clash_asset_candidates "$arch" "$version" | head -n 1)"
missing+=("$(offline_asset_missing_text "clash" "$version" "$file" "Clash")")
fi
fi
;;
*)
missing+=("未知内核类型:$kernel")
;;
esac
version="${YQ_VERSION:-$DEFAULT_YQ_VERSION}"
file="$(yq_asset_file "$arch")"
if [ ! -x "$(yq_bin)" ] \
&& ! find_bundled_asset "yq" "$version" "$file" >/dev/null 2>&1; then
missing+=("$(offline_asset_missing_text "yq" "$version" "$file" "yq")")
fi
version="${SUBCONVERTER_VERSION:-$DEFAULT_SUBCONVERTER_VERSION}"
file="$(subconverter_asset_file "$arch")"
if ! { [ -x "$(subconverter_bin)" ] \
&& [ "$(subconverter_version_file_value)" = "$version" ] \
&& subconverter_runtime_layout_ready "$(subconverter_home)"; } \
&& ! find_bundled_asset "subconverter" "$version" "$file" >/dev/null 2>&1; then
missing+=("$(offline_asset_missing_text "subconverter" "$version" "$file" "subconverter")")
fi
if geo_predownload_enabled; then
for item in "${GEO_ASSET_DOWNLOADS[@]}"; do
path="${item%% *}"
file="$(basename "$path")"
if [ ! -s "$RUNTIME_DIR/$file" ] \
&& ! find_bundled_asset "geo" "latest" "$file" >/dev/null 2>&1; then
missing+=("$(offline_asset_missing_text "geo" "latest" "$file" "$file")")
fi
done
fi
if [ "${#missing[@]}" -gt 0 ]; then
error "离线安装缺少以下资源:"
printf ' - %s\n' "${missing[@]}" >&2
die_state "离线资源预检失败,共缺少 ${#missing[@]} 项" \
"在联网设备执行 scripts/prefetch-assets.sh --arch $arch,再把资源包复制到目标主机并解压到项目根目录"
fi
success "离线资源预检通过"
}
resolve_geo_assets() {
geo_predownload_enabled || return 0
[ -n "${PROJECT_DIR:-}" ] || return 0
@@ -44,12 +290,7 @@ resolve_yq() {
return 0
fi
case "$arch" in
amd64) file="yq_linux_amd64.tar.gz" ;;
arm64) file="yq_linux_arm64.tar.gz" ;;
armv7) file="yq_linux_arm.tar.gz" ;;
*) die "暂不支持的 yq 架构:$arch" ;;
esac
file="$(yq_asset_file "$arch")"
url="https://github.com/mikefarah/yq/releases/download/${version}/${file}"
tmp_dir="$(mktemp -d)"
@@ -86,12 +327,7 @@ resolve_mihomo() {
return 0
fi
case "$arch" in
amd64) file="mihomo-linux-amd64-compatible-${version}.gz" ;;
arm64) file="mihomo-linux-arm64-${version}.gz" ;;
armv7) file="mihomo-linux-armv7-${version}.gz" ;;
*) die "暂不支持的 Mihomo 架构:$arch" ;;
esac
file="$(mihomo_asset_file "$arch" "$version")"
if [ -n "${custom_url:-}" ]; then
url="$custom_url"
@@ -109,12 +345,11 @@ resolve_mihomo() {
}
resolve_clash() {
local arch version version_no_v url_base tmp_file url downloaded="false"
local arch version url_base tmp_file url downloaded="false"
local candidates file
arch="$(get_arch)"
version="${CLASH_VERSION:-$DEFAULT_CLASH_VERSION}"
version_no_v="${version#v}"
url_base="${CLASH_DOWNLOAD_BASE:-https://github.com/WindSpiritSR/clash/releases/download}"
if [ -x "$(clash_bin)" ] \
@@ -123,36 +358,20 @@ resolve_clash() {
return 0
fi
case "$arch" in
amd64)
candidates="
clash-linux-amd64-${version}.gz
clash-linux-amd64-v${version_no_v}.gz
"
;;
arm64)
candidates="
clash-linux-arm64-${version}.gz
clash-linux-arm64-v${version_no_v}.gz
clash-linux-armv8-${version}.gz
clash-linux-armv8-v${version_no_v}.gz
"
;;
armv7)
candidates="
clash-linux-armv7-${version}.gz
clash-linux-armv7-v${version_no_v}.gz
"
;;
*)
die "暂不支持的 Clash 架构:$arch"
;;
esac
candidates="$(clash_asset_candidates "$arch" "$version")"
tmp_file="$(mktemp)"
rm -f "$tmp_file"
for file in $candidates; do
if copy_bundled_asset "clash" "$version" "$file" "$tmp_file" "clash"; then
downloaded="true"
break
fi
done
for file in $candidates; do
[ "$downloaded" = "false" ] || break
url="${url_base}/${version}/${file}"
if download_file "$url" "$tmp_file" "clash"; then
@@ -273,12 +492,7 @@ resolve_subconverter() {
fi
fi
case "$arch" in
amd64) file="subconverter_linux64.tar.gz" ;;
arm64) file="subconverter_aarch64.tar.gz" ;;
armv7) file="subconverter_armv7.tar.gz" ;;
*) die "暂不支持的 subconverter 架构:$arch" ;;
esac
file="$(subconverter_asset_file "$arch")"
url="https://github.com/asdlokj1qpi233/subconverter/releases/download/${version}/${file}"
tmp_dir="$(mktemp -d)"
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
# shellcheck source=scripts/core/runtime.sh
source "$PROJECT_DIR/scripts/core/runtime.sh"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
fixture_project="$tmp_dir/project"
RUNTIME_DIR="$fixture_project/runtime"
RESOURCE_DIR="$fixture_project/resources"
BIN_DIR="$RUNTIME_DIR/bin"
LOG_DIR="$RUNTIME_DIR/logs"
mkdir -p \
"$RESOURCE_DIR/bin/mihomo" \
"$RESOURCE_DIR/bin/yq" \
"$RESOURCE_DIR/bin/subconverter" \
"$RESOURCE_DIR/geo" \
"$BIN_DIR" \
"$LOG_DIR"
export CLASH_TEST_UNAME_M="amd64"
export KERNEL_TYPE="mihomo"
export CLASH_OFFLINE="true"
export CLASH_PREDOWNLOAD_GEO="true"
unset CLASH_BUNDLED_ASSET_DIR
unset CLASH_BUNDLED_ASSET_ENABLED
mihomo_version="${MIHOMO_VERSION:-$DEFAULT_MIHOMO_VERSION}"
yq_version="${YQ_VERSION:-$DEFAULT_YQ_VERSION}"
subconverter_version="${SUBCONVERTER_VERSION:-$DEFAULT_SUBCONVERTER_VERSION}"
mihomo_file="$(mihomo_asset_file amd64 "$mihomo_version")"
yq_file="$(yq_asset_file amd64)"
subconverter_file="$(subconverter_asset_file amd64)"
printf 'mock\n' > "$RESOURCE_DIR/bin/mihomo/$mihomo_file"
printf 'mock\n' > "$RESOURCE_DIR/bin/yq/$yq_file"
printf 'mock\n' > "$RESOURCE_DIR/bin/subconverter/$subconverter_file"
for item in "${GEO_ASSET_DOWNLOADS[@]}"; do
geo_path="${item%% *}"
printf 'mock\n' > "$RESOURCE_DIR/geo/$(basename "$geo_path")"
done
preflight_offline_assets >/dev/null
echo "ok - complete offline asset set passes preflight"
rm -f "$RESOURCE_DIR/bin/mihomo/$mihomo_file"
missing_output="$tmp_dir/missing-output"
if (preflight_offline_assets) >"$missing_output" 2>&1; then
echo "not ok - missing Mihomo asset unexpectedly passed preflight" >&2
exit 1
fi
if ! grep -Fq "Mihomo" "$missing_output"; then
echo "not ok - missing asset output did not identify Mihomo" >&2
cat "$missing_output" >&2
exit 1
fi
echo "ok - missing offline asset fails with an actionable name"
blocked_output="$tmp_dir/blocked-output"
if (download_file "https://example.com/file" "$tmp_dir/downloaded" "fixture") >"$blocked_output" 2>&1; then
echo "not ok - offline download unexpectedly succeeded" >&2
exit 1
fi
if [ -e "$tmp_dir/downloaded" ]; then
echo "not ok - offline download created an output file" >&2
exit 1
fi
if ! grep -Fq "离线模式禁止下载" "$blocked_output"; then
echo "not ok - offline download failure was not explicit" >&2
cat "$blocked_output" >&2
exit 1
fi
echo "ok - offline mode blocks remote fallback before curl"
parse_install_options user --offline
if [ "$INSTALL_REQUESTED_SCOPE" != "user" ] \
|| [ "$INSTALL_OFFLINE_REQUESTED" != "true" ]; then
echo "not ok - install option parser lost scope or offline flag" >&2
exit 1
fi
echo "ok - install options accept scope and --offline together"
dry_run_output="$tmp_dir/dry-run-output"
bash "$PROJECT_DIR/scripts/prefetch-assets.sh" \
--arch arm64 \
--kernel mihomo \
--dry-run >"$dry_run_output"
if ! grep -Fq "mihomo-linux-arm64-" "$dry_run_output" \
|| ! grep -Fq "subconverter_aarch64.tar.gz" "$dry_run_output" \
|| ! grep -Fq "未生成资源包" "$dry_run_output"; then
echo "not ok - prefetch dry run did not describe the arm64 bundle" >&2
cat "$dry_run_output" >&2
exit 1
fi
echo "ok - prefetch dry run resolves target architecture without downloading"
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# shellcheck source=scripts/core/runtime.sh
source "$PROJECT_DIR/scripts/core/runtime.sh"
prefetch_usage() {
cat <<'EOF'
用法:
bash scripts/prefetch-assets.sh [选项]
选项:
--arch amd64|arm64|armv7 目标主机架构;默认使用当前主机架构
--kernel mihomo|clash 要打包的代理内核;默认读取 .env 或使用 mihomo
--output FILE 输出文件;默认 clash-assets-<kernel>-<arch>.tar.gz
--no-geo 不包含安装期 GEO 资源
--dry-run 只显示下载计划,不下载或生成压缩包
-h, --help 显示帮助
资源包复制到目标主机后,在项目根目录执行:
tar -xzf clash-assets-<kernel>-<arch>.tar.gz
bash install.sh --offline
EOF
}
normalize_prefetch_arch() {
case "$1" in
x86_64|amd64) echo "amd64" ;;
aarch64|arm64) echo "arm64" ;;
armv7l|armv7) echo "armv7" ;;
*) die_usage "不支持的目标架构:$1" "可选值:amd64、arm64、armv7" ;;
esac
}
prefetch_sha256_line() {
local file="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$file"
return 0
fi
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$file"
return 0
fi
die "生成资源包需要 sha256sum 或 shasum"
}
prefetch_download() {
local url="$1"
local out="$2"
local label="$3"
mkdir -p "$(dirname "$out")"
download_file "$url" "$out" "$label"
}
target_arch=""
target_kernel=""
output_file=""
include_geo="true"
dry_run="false"
while [ "$#" -gt 0 ]; do
case "$1" in
--arch)
[ "$#" -ge 2 ] || die_usage "--arch 缺少参数" "可选值:amd64、arm64、armv7"
target_arch="$2"
shift 2
;;
--kernel)
[ "$#" -ge 2 ] || die_usage "--kernel 缺少参数" "可选值:mihomo、clash"
target_kernel="$2"
shift 2
;;
--output)
[ "$#" -ge 2 ] || die_usage "--output 缺少文件路径" "例如:--output /tmp/clash-assets-amd64.tar.gz"
output_file="$2"
shift 2
;;
--no-geo)
include_geo="false"
shift
;;
--dry-run)
dry_run="true"
shift
;;
-h|--help)
prefetch_usage
exit 0
;;
*)
die_usage "未知参数:$1" "执行 bash scripts/prefetch-assets.sh --help 查看用法"
;;
esac
done
init_project_context "$PROJECT_DIR"
load_env_if_exists
target_arch="$(normalize_prefetch_arch "${target_arch:-$(get_arch)}")"
target_kernel="$(normalize_kernel_type "${target_kernel:-${KERNEL_TYPE:-mihomo}}")"
case "$target_kernel" in
mihomo|clash) ;;
*) die_usage "不支持的目标内核:$target_kernel" "可选值:mihomo、clash" ;;
esac
export CLASH_TEST_UNAME_M="$target_arch"
export CLASH_OFFLINE="false"
mihomo_version="${MIHOMO_VERSION:-$DEFAULT_MIHOMO_VERSION}"
clash_version="${CLASH_VERSION:-$DEFAULT_CLASH_VERSION}"
yq_version="${YQ_VERSION:-$DEFAULT_YQ_VERSION}"
subconverter_version="${SUBCONVERTER_VERSION:-$DEFAULT_SUBCONVERTER_VERSION}"
yq_file="$(yq_asset_file "$target_arch")"
subconverter_file="$(subconverter_asset_file "$target_arch")"
yq_url="https://github.com/mikefarah/yq/releases/download/${yq_version}/${yq_file}"
subconverter_url="https://github.com/asdlokj1qpi233/subconverter/releases/download/${subconverter_version}/${subconverter_file}"
case "$target_kernel" in
mihomo)
kernel_file="$(mihomo_asset_file "$target_arch" "$mihomo_version")"
if [ -n "${MIHOMO_DOWNLOAD_URL:-}" ]; then
kernel_url="$MIHOMO_DOWNLOAD_URL"
else
kernel_url="${MIHOMO_DOWNLOAD_BASE:-https://github.com/MetaCubeX/mihomo/releases/download}"
kernel_url="${kernel_url%/}/${mihomo_version}/${kernel_file}"
fi
;;
clash)
kernel_file="$(clash_asset_candidates "$target_arch" "$clash_version" | head -n 1)"
kernel_url="${CLASH_DOWNLOAD_BASE:-https://github.com/WindSpiritSR/clash/releases/download}"
kernel_url="${kernel_url%/}/${clash_version}/${kernel_file}"
;;
esac
ui_section "资源包计划"
ui_kv "🧩" "目标架构" "$target_arch"
ui_kv "🚀" "目标内核" "$target_kernel"
printf ' - %s\n' "$kernel_url"
printf ' - %s\n' "$yq_url"
printf ' - %s\n' "$subconverter_url"
if [ "$include_geo" = "true" ]; then
for item in "${GEO_ASSET_DOWNLOADS[@]}"; do
printf ' - %s\n' "${item#* }"
done
fi
if [ "$dry_run" = "true" ]; then
success "仅显示下载计划,未生成资源包"
exit 0
fi
command -v curl >/dev/null 2>&1 || die "当前系统缺少 curl"
command -v tar >/dev/null 2>&1 || die "当前系统缺少 tar"
stage_dir="$(mktemp -d)"
trap 'rm -rf "$stage_dir"' EXIT
RESOURCE_DIR="$stage_dir/resources"
RUNTIME_DIR="$stage_dir/runtime"
BIN_DIR="$RUNTIME_DIR/bin"
LOG_DIR="$RUNTIME_DIR/logs"
mkdir -p "$RESOURCE_DIR/bin" "$RESOURCE_DIR/geo" "$RUNTIME_DIR"
prefetch_download "$kernel_url" "$RESOURCE_DIR/bin/$target_kernel/$kernel_file" "$target_kernel"
prefetch_download "$yq_url" "$RESOURCE_DIR/bin/yq/$yq_file" "yq"
prefetch_download \
"$subconverter_url" \
"$RESOURCE_DIR/bin/subconverter/$subconverter_file" \
"subconverter"
if [ "$include_geo" = "true" ]; then
for item in "${GEO_ASSET_DOWNLOADS[@]}"; do
geo_path="${item%% *}"
geo_url="${item#* }"
geo_file="$(basename "$geo_path")"
prefetch_download "$geo_url" "$RESOURCE_DIR/geo/$geo_file" "$geo_file"
done
fi
source_commit="$(git -C "$PROJECT_DIR" rev-parse --verify HEAD 2>/dev/null || echo unknown)"
cat > "$RESOURCE_DIR/asset-manifest.env" <<EOF
BUNDLE_FORMAT="1"
BUNDLE_ARCH="$target_arch"
KERNEL_TYPE="$target_kernel"
MIHOMO_VERSION="$mihomo_version"
CLASH_VERSION="$clash_version"
YQ_VERSION="$yq_version"
SUBCONVERTER_VERSION="$subconverter_version"
GEO_INCLUDED="$include_geo"
SOURCE_COMMIT="$source_commit"
GENERATED_AT="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
EOF
(
cd "$RESOURCE_DIR"
while IFS= read -r asset_file; do
prefetch_sha256_line "$asset_file"
done < <(
find bin geo -type f -print
printf '%s\n' "asset-manifest.env"
) | LC_ALL=C sort -k2
) > "$RESOURCE_DIR/SHA256SUMS"
if [ -z "${output_file:-}" ]; then
output_file="$PWD/clash-assets-${target_kernel}-${target_arch}.tar.gz"
else
output_dir="$(dirname "$output_file")"
output_name="$(basename "$output_file")"
mkdir -p "$output_dir"
output_file="$(cd "$output_dir" && pwd)/$output_name"
fi
tar -czf "$output_file" -C "$stage_dir" resources
success "资源包已生成:$output_file"
ui_next "复制到目标主机的项目目录,解压后执行:bash install.sh --offline"