- deploy-from-mac.sh 增加 --skip-regctl,并在 regctl 被安全软件拦截或隧道失败时 自动改用 docker 直传(docker save + scp + docker load/push),与 Windows 版一致 - 修正若干在 set -e 下会被误判为失败的短路条件语句 - AGENTS.md 与《开发与部署SOP》补充 macOS 发布命令、钥匙串保存登录信息, 以及 --allow-migrations、--skip-regctl、--allow-non-main 的对应参数
340 lines
12 KiB
Bash
Executable File
340 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
#
|
||
# macOS 版前端构建:与 scripts/build-frontend.ps1 保持相同的输入指纹、缓存与产物布局。
|
||
# 生成物写入 .caiwu-build/public,供 deploy-from-mac.sh 作为镜像构建上下文使用。
|
||
#
|
||
# 用法:bash scripts/build-frontend.sh [--publish-local] [--node-version 20.20.2]
|
||
#
|
||
set -euo pipefail
|
||
|
||
NODE_VERSION="${CAIWU_NODE_VERSION:-20.20.2}"
|
||
MAX_OLD_SPACE_SIZE_MB="${CAIWU_MAX_OLD_SPACE_MB:-4096}"
|
||
TERSER_WORKERS="${CAIWU_TERSER_WORKERS:-2}"
|
||
PUBLISH_LOCAL=0
|
||
|
||
while [ "$#" -gt 0 ]; do
|
||
case "$1" in
|
||
--publish-local) PUBLISH_LOCAL=1 ;;
|
||
--node-version) NODE_VERSION="$2"; shift ;;
|
||
--max-old-space-size-mb) MAX_OLD_SPACE_SIZE_MB="$2"; shift ;;
|
||
--terser-workers) TERSER_WORKERS="$2"; shift ;;
|
||
-h|--help) sed -n '2,8p' "$0"; exit 0 ;;
|
||
*) echo "未知参数:$1" >&2; exit 2 ;;
|
||
esac
|
||
shift
|
||
done
|
||
|
||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
repo_root="$(cd "$script_dir/.." && pwd)"
|
||
tool_root="$HOME/.cache/CaiWuXiTong/tools"
|
||
|
||
case "$(uname -m)" in
|
||
arm64) node_platform='darwin-arm64' ;;
|
||
x86_64) node_platform='darwin-x64' ;;
|
||
*) echo "不支持的 macOS 架构:$(uname -m)" >&2; exit 1 ;;
|
||
esac
|
||
|
||
node_folder="node-v$NODE_VERSION-$node_platform"
|
||
node_root="$tool_root/$node_folder"
|
||
node_exe="$node_root/bin/node"
|
||
npm_cmd="$node_root/bin/npm"
|
||
node_archive="$tool_root/$node_folder.tar.gz"
|
||
node_checksums="$tool_root/SHASUMS256.txt"
|
||
|
||
tracked_css="$repo_root/public/css/app.css"
|
||
public_js="$repo_root/public/js"
|
||
public_manifest="$repo_root/public/mix-manifest.json"
|
||
cache_root="$repo_root/.caiwu-build"
|
||
output_root="$cache_root/public"
|
||
vendor_root="$output_root/vendor"
|
||
js_fingerprint_file="$cache_root/frontend-js.sha"
|
||
css_fingerprint_file="$cache_root/frontend-css.sha"
|
||
dependency_fingerprint_file="$cache_root/node-modules.sha"
|
||
js_manifest_cache="$cache_root/manifest-js.json"
|
||
css_manifest_cache="$cache_root/manifest-css.json"
|
||
output_fingerprint_file="$cache_root/frontend-output.sha"
|
||
|
||
sha256_file() {
|
||
if command -v shasum >/dev/null 2>&1; then
|
||
shasum -a 256 "$1" | awk '{print $1}'
|
||
else
|
||
sha256sum "$1" | awk '{print $1}'
|
||
fi
|
||
}
|
||
|
||
sha256_stdin() {
|
||
if command -v shasum >/dev/null 2>&1; then
|
||
shasum -a 256 | awk '{print $1}'
|
||
else
|
||
sha256sum | awk '{print $1}'
|
||
fi
|
||
}
|
||
|
||
path_fingerprint() {
|
||
local list_file content path
|
||
list_file="$(mktemp)"
|
||
|
||
for path in "$@"; do
|
||
if [ -f "$repo_root/$path" ]; then
|
||
printf '%s\n' "$repo_root/$path" >> "$list_file"
|
||
elif [ -d "$repo_root/$path" ]; then
|
||
find "$repo_root/$path" -type f >> "$list_file"
|
||
else
|
||
rm -f "$list_file"
|
||
echo "前端指纹路径不存在:$path" >&2
|
||
return 1
|
||
fi
|
||
done
|
||
|
||
LC_ALL=C sort -o "$list_file" "$list_file"
|
||
content="$(while IFS= read -r file; do
|
||
printf '%s|%s\n' "${file#"$repo_root"/}" "$(sha256_file "$file")"
|
||
done < "$list_file")"
|
||
rm -f "$list_file"
|
||
printf '%s' "$content" | sha256_stdin
|
||
}
|
||
|
||
cache_hit() {
|
||
local fingerprint_file="$1" expected="$2"
|
||
shift 2
|
||
|
||
[ -f "$fingerprint_file" ] || return 1
|
||
for output in "$@"; do
|
||
[ -e "$output" ] || return 1
|
||
done
|
||
[ "$(cat "$fingerprint_file")" = "$expected" ]
|
||
}
|
||
|
||
write_file() {
|
||
printf '%s' "$2" > "$1"
|
||
}
|
||
|
||
download_file() {
|
||
local url="$1" destination="$2" attempt=1
|
||
|
||
while [ "$attempt" -le 4 ]; do
|
||
if curl -fL --retry 3 --retry-delay 2 --retry-all-errors \
|
||
--speed-limit 20000 --speed-time 20 \
|
||
-C - -o "$destination" "$url"; then
|
||
return 0
|
||
fi
|
||
attempt=$((attempt + 1))
|
||
sleep 2
|
||
done
|
||
|
||
return 1
|
||
}
|
||
|
||
publish_frontend_cache() {
|
||
for required in "$output_root/js" "$output_root/css/app.css" "$output_root/mix-manifest.json"; do
|
||
[ -e "$required" ] || { echo "前端缓存产物缺失:$required" >&2; exit 1; }
|
||
done
|
||
|
||
rm -rf "$public_js"
|
||
cp -R "$output_root/js" "$public_js"
|
||
cp "$output_root/css/app.css" "$tracked_css"
|
||
cp "$output_root/mix-manifest.json" "$public_manifest"
|
||
}
|
||
|
||
package_lock_fingerprint="$(path_fingerprint package-lock.json)"
|
||
js_fingerprint="$NODE_VERSION|$TERSER_WORKERS|$(path_fingerprint resources/assets/js package.json package-lock.json scripts/build-frontend.ps1 scripts/build-frontend.sh webpack.mix.js)"
|
||
css_fingerprint="$NODE_VERSION|$(path_fingerprint resources/assets/sass resources/views resources/lang package.json package-lock.json webpack.mix.js tailwind.config.js presets.js safelist.txt composer.lock)"
|
||
dependency_fingerprint="$NODE_VERSION|$package_lock_fingerprint"
|
||
|
||
output_cache_valid=0
|
||
if [ -f "$output_fingerprint_file" ] && [ -d "$output_root" ] \
|
||
&& [ "$(cat "$output_fingerprint_file")" = "$(path_fingerprint .caiwu-build/public)" ]; then
|
||
output_cache_valid=1
|
||
fi
|
||
|
||
js_cache_valid=0
|
||
if cache_hit "$js_fingerprint_file" "$js_fingerprint" \
|
||
"$output_root/js/auth/common.min.js" \
|
||
"$output_root/js/business/pages.min.js" \
|
||
"$output_root/js/business/marketing-account-bills.min.js" \
|
||
"$output_root/js/manifest.js" \
|
||
"$output_root/js/vendor.js" \
|
||
"$vendor_root/apexcharts/apexcharts.min.js" \
|
||
"$vendor_root/js-cookie/js.cookie.js" \
|
||
"$vendor_root/quicksand/css/quicksand.css" \
|
||
"$vendor_root/quicksand/files/quicksand-latin-400-normal.woff2" \
|
||
"$js_manifest_cache"; then
|
||
js_cache_valid=1
|
||
fi
|
||
|
||
css_cache_valid=0
|
||
if cache_hit "$css_fingerprint_file" "$css_fingerprint" "$output_root/css/app.css" "$css_manifest_cache"; then
|
||
css_cache_valid=1
|
||
fi
|
||
|
||
if [ "$output_cache_valid" -eq 0 ]; then
|
||
js_cache_valid=0
|
||
css_cache_valid=0
|
||
echo '前端产物完整性缺失或已变化,重新构建资源。'
|
||
fi
|
||
|
||
if [ "$js_cache_valid" -eq 1 ] && [ "$css_cache_valid" -eq 1 ]; then
|
||
echo '前端输入未变化,复用 JavaScript、CSS 与依赖缓存。'
|
||
if [ "$PUBLISH_LOCAL" -eq 1 ]; then
|
||
publish_frontend_cache
|
||
echo '已把缓存中的前端资源发布到本地 public 目录。'
|
||
fi
|
||
exit 0
|
||
fi
|
||
|
||
if [ ! -x "$node_exe" ]; then
|
||
mkdir -p "$tool_root"
|
||
download_root="https://nodejs.org/dist/v$NODE_VERSION"
|
||
if [ -n "${CAIWU_NODE_MIRROR:-}" ]; then
|
||
download_sources="$CAIWU_NODE_MIRROR"
|
||
else
|
||
download_sources="$download_root https://registry.npmmirror.com/-/binary/node/v$NODE_VERSION"
|
||
fi
|
||
|
||
echo "下载项目专用 Node ${NODE_VERSION}(${node_platform})。"
|
||
node_ready=0
|
||
for download_source in $download_sources; do
|
||
if download_file "$download_source/$node_folder.tar.gz" "$node_archive" \
|
||
&& download_file "$download_source/SHASUMS256.txt" "$node_checksums"; then
|
||
node_ready=1
|
||
break
|
||
fi
|
||
echo "下载源 ${download_source} 中断,换下一个源重试。"
|
||
rm -f "$node_archive" "$node_checksums"
|
||
done
|
||
|
||
if [ "$node_ready" -eq 0 ]; then
|
||
echo '下载项目专用 Node 失败,请设置 CAIWU_NODE_MIRROR 指向可用的 Node 镜像后重试。' >&2
|
||
exit 1
|
||
fi
|
||
|
||
expected_hash="$(awk -v name="$node_folder.tar.gz" '$2 == name { print $1; exit }' "$node_checksums")"
|
||
if [ -z "$expected_hash" ]; then
|
||
rm -f "$node_archive" "$node_checksums"
|
||
echo 'Node 压缩包缺少官方 SHA-256 校验值。' >&2
|
||
exit 1
|
||
fi
|
||
|
||
actual_hash="$(sha256_file "$node_archive")"
|
||
if [ "$actual_hash" != "$expected_hash" ]; then
|
||
rm -f "$node_archive" "$node_checksums"
|
||
echo 'Node 压缩包 SHA-256 校验失败。' >&2
|
||
exit 1
|
||
fi
|
||
|
||
tar -xzf "$node_archive" -C "$tool_root"
|
||
rm -f "$node_archive" "$node_checksums"
|
||
fi
|
||
|
||
if [ ! -x "$node_exe" ] || [ ! -x "$npm_cmd" ]; then
|
||
echo "项目专用 Node $NODE_VERSION 安装不完整。" >&2
|
||
exit 1
|
||
fi
|
||
|
||
original_path="$PATH"
|
||
tracked_css_backup=""
|
||
if [ -f "$tracked_css" ]; then
|
||
tracked_css_backup="$(mktemp)"
|
||
cp "$tracked_css" "$tracked_css_backup"
|
||
fi
|
||
|
||
restore_tracked_css() {
|
||
if [ "$PUBLISH_LOCAL" -eq 0 ] && [ -n "$tracked_css_backup" ] && [ -f "$tracked_css_backup" ]; then
|
||
cp "$tracked_css_backup" "$tracked_css"
|
||
fi
|
||
if [ -n "$tracked_css_backup" ]; then
|
||
rm -f "$tracked_css_backup"
|
||
fi
|
||
}
|
||
trap restore_tracked_css EXIT
|
||
|
||
cd "$repo_root"
|
||
export PATH="$node_root/bin:$original_path"
|
||
export NODE_OPTIONS="--max-old-space-size=$MAX_OLD_SPACE_SIZE_MB"
|
||
export CAIWU_TERSER_WORKERS="$TERSER_WORKERS"
|
||
|
||
"$node_exe" --version >/dev/null
|
||
|
||
dependency_cache_valid=0
|
||
if [ -f "$repo_root/node_modules/.package-lock.json" ] && [ -f "$dependency_fingerprint_file" ] \
|
||
&& [ "$(cat "$dependency_fingerprint_file")" = "$dependency_fingerprint" ]; then
|
||
dependency_cache_valid=1
|
||
fi
|
||
|
||
if [ "$dependency_cache_valid" -eq 1 ]; then
|
||
echo 'package-lock.json 未变化,复用 node_modules。'
|
||
else
|
||
"$npm_cmd" ci --prefer-offline --no-audit --no-fund
|
||
mkdir -p "$cache_root"
|
||
write_file "$dependency_fingerprint_file" "$dependency_fingerprint"
|
||
fi
|
||
|
||
if [ "$js_cache_valid" -eq 0 ]; then
|
||
echo "构建 JavaScript(Node 堆 ${MAX_OLD_SPACE_SIZE_MB}MB,压缩线程 ${TERSER_WORKERS})。"
|
||
rm -rf "$public_js"
|
||
CAIWU_BUILD_TARGET=js "$npm_cmd" run production
|
||
|
||
for output in \
|
||
public/js/auth/common.min.js \
|
||
public/js/business/marketing-account-bills.min.js \
|
||
public/js/manifest.js \
|
||
public/js/vendor.js \
|
||
public/mix-manifest.json; do
|
||
[ -f "$repo_root/$output" ] || { echo "JavaScript 构建产物缺失:$output" >&2; exit 1; }
|
||
done
|
||
|
||
rm -rf "$output_root/js" "$vendor_root"
|
||
mkdir -p "$output_root" "$vendor_root/apexcharts" "$vendor_root/js-cookie" "$vendor_root/quicksand/css" "$vendor_root/quicksand/files"
|
||
cp -R "$public_js" "$output_root/js"
|
||
cp "$repo_root/node_modules/apexcharts/dist/apexcharts.min.js" "$vendor_root/apexcharts/apexcharts.min.js"
|
||
cp "$repo_root/node_modules/js-cookie/dist/js.cookie.min.js" "$vendor_root/js-cookie/js.cookie.js"
|
||
cp -R "$repo_root/node_modules/@fontsource/quicksand/files/." "$vendor_root/quicksand/files/"
|
||
|
||
: > "$vendor_root/quicksand/css/quicksand.css"
|
||
for weight in 400 500 600 700; do
|
||
sed 's#url(\./files/#url(../files/#g' "$repo_root/node_modules/@fontsource/quicksand/$weight.css" >> "$vendor_root/quicksand/css/quicksand.css"
|
||
printf '\n' >> "$vendor_root/quicksand/css/quicksand.css"
|
||
done
|
||
|
||
cp "$public_manifest" "$js_manifest_cache"
|
||
write_file "$js_fingerprint_file" "$js_fingerprint"
|
||
fi
|
||
|
||
if [ "$css_cache_valid" -eq 0 ]; then
|
||
echo "构建 CSS(Node 堆 ${MAX_OLD_SPACE_SIZE_MB}MB)。"
|
||
CAIWU_BUILD_TARGET=css "$npm_cmd" run production
|
||
|
||
for output in public/css/app.css public/mix-manifest.json; do
|
||
[ -f "$repo_root/$output" ] || { echo "CSS 构建产物缺失:$output" >&2; exit 1; }
|
||
done
|
||
|
||
mkdir -p "$output_root/css"
|
||
cp "$tracked_css" "$output_root/css/app.css"
|
||
cp "$public_manifest" "$css_manifest_cache"
|
||
write_file "$css_fingerprint_file" "$css_fingerprint"
|
||
fi
|
||
|
||
"$node_exe" -e '
|
||
const fs = require("fs");
|
||
const [jsPath, cssPath, outputPath, publicPath] = process.argv.slice(1);
|
||
const merged = {};
|
||
for (const [key, value] of Object.entries(JSON.parse(fs.readFileSync(jsPath, "utf8")))) {
|
||
if (key.endsWith(".js")) merged[key] = value;
|
||
}
|
||
for (const [key, value] of Object.entries(JSON.parse(fs.readFileSync(cssPath, "utf8")))) {
|
||
if (key.endsWith(".css")) merged[key] = value;
|
||
}
|
||
const json = JSON.stringify(merged, null, 4);
|
||
fs.writeFileSync(outputPath, json);
|
||
fs.writeFileSync(publicPath, json);
|
||
' "$js_manifest_cache" "$css_manifest_cache" "$output_root/mix-manifest.json" "$public_manifest"
|
||
|
||
if [ "$js_fingerprint" != "$NODE_VERSION|$TERSER_WORKERS|$(path_fingerprint resources/assets/js package.json package-lock.json scripts/build-frontend.ps1 scripts/build-frontend.sh webpack.mix.js)" ] \
|
||
|| [ "$css_fingerprint" != "$NODE_VERSION|$(path_fingerprint resources/assets/sass resources/views resources/lang package.json package-lock.json webpack.mix.js tailwind.config.js presets.js safelist.txt composer.lock)" ]; then
|
||
echo '构建过程中前端源码发生变化,请在稳定的提交上重试。' >&2
|
||
exit 1
|
||
fi
|
||
|
||
write_file "$output_fingerprint_file" "$(path_fingerprint .caiwu-build/public)"
|
||
echo '前端构建完成,产物位于 .caiwu-build/public。'
|