From e5fa9429ae7b52a7d3bb241ab56674eea0b00b79 Mon Sep 17 00:00:00 2001 From: yoyo Date: Wed, 3 Dec 2025 21:31:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- Makefile | 5 +- build-all.sh | 269 ++++++++++++++ install.sh | 311 +++++++++++++++- start-systemd.sh | 7 + upload.sh | 898 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 1481 insertions(+), 12 deletions(-) create mode 100755 build-all.sh create mode 100755 upload.sh diff --git a/.gitignore b/.gitignore index 496ee2c..b5731b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.DS_Store \ No newline at end of file +.DS_Store +bin/ diff --git a/Makefile b/Makefile index 1e41539..bb69cb6 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build build-linux clean +.PHONY: build build-linux build-all clean build: go build -o bin/linkmaster-node ./cmd/agent @@ -6,6 +6,9 @@ build: build-linux: GOOS=linux GOARCH=amd64 go build -o bin/linkmaster-node-linux ./cmd/agent +build-all: + @./build-all.sh + clean: rm -rf bin/ diff --git a/build-all.sh b/build-all.sh new file mode 100755 index 0000000..45515db --- /dev/null +++ b/build-all.sh @@ -0,0 +1,269 @@ +#!/bin/bash + +# 跨平台编译脚本 +# 支持编译多个操作系统和架构的版本 + +set -e + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 项目信息 +PROJECT_NAME="agent" +BUILD_DIR="bin" +VERSION="${VERSION:-$(date +%Y%m%d-%H%M%S)}" +MAIN_PACKAGE="./cmd/agent" + +# 支持的平台列表 +# 格式: OS/ARCH +PLATFORMS=( + "linux/amd64" + "linux/arm64" + "darwin/amd64" + "darwin/arm64" + "windows/amd64" + "windows/arm64" +) + +# 显示使用说明 +usage() { + echo -e "${BLUE}使用方法:${NC}" + echo " $0 [选项]" + echo "" + echo -e "${BLUE}选项:${NC}" + echo " -h, --help 显示帮助信息" + echo " -p, --platform PLATFORM 只编译指定平台 (例如: linux/amd64)" + echo " -l, --list 列出所有支持的平台" + echo " -c, --clean 编译前清理输出目录" + echo " -j, --jobs N 并行编译数量 (默认: 4)" + echo " -v, --version VERSION 设置版本号 (默认: 时间戳)" + echo " -s, --simple-only 只生成不带版本号的文件(默认生成两个)" + echo "" + echo -e "${BLUE}示例:${NC}" + echo " $0 # 编译所有平台" + echo " $0 -p linux/amd64 # 只编译 Linux AMD64" + echo " $0 -j 2 # 使用2个并行任务" + echo " $0 -c # 清理后编译" +} + +# 列出所有平台 +list_platforms() { + echo -e "${BLUE}支持的平台:${NC}" + for platform in "${PLATFORMS[@]}"; do + echo " - $platform" + done +} + +# 清理输出目录 +clean_build() { + if [ -d "$BUILD_DIR" ]; then + echo -e "${YELLOW}清理输出目录...${NC}" + rm -rf "$BUILD_DIR" + fi + mkdir -p "$BUILD_DIR" +} + +# 编译单个平台 +build_platform() { + local os_arch=$1 + local simple_only=$2 # 是否只生成不带版本号的文件 + local os=$(echo $os_arch | cut -d'/' -f1) + local arch=$(echo $os_arch | cut -d'/' -f2) + + local output_path + if [ "$simple_only" = "true" ]; then + # 只生成不带版本号的文件 + output_path="${BUILD_DIR}/${PROJECT_NAME}-${os}-${arch}" + if [ "$os" = "windows" ]; then + output_path="${output_path}.exe" + fi + else + # 生成带版本号的文件 + output_path="${BUILD_DIR}/${PROJECT_NAME}-${os}-${arch}-${VERSION}" + if [ "$os" = "windows" ]; then + output_path="${output_path}.exe" + fi + fi + + echo -e "${BLUE}[编译]${NC} ${os}/${arch} -> ${output_path}" + + if GOOS=$os GOARCH=$arch go build -ldflags "-s -w -X main.version=${VERSION}" \ + -o "$output_path" "$MAIN_PACKAGE" 2>&1; then + echo -e "${GREEN}[成功]${NC} ${os}/${arch}" + + # 如果不是只生成简单版本,则创建不带版本号的副本(方便使用) + if [ "$simple_only" != "true" ]; then + local simple_name="${BUILD_DIR}/${PROJECT_NAME}-${os}-${arch}" + if [ "$os" = "windows" ]; then + simple_name="${simple_name}.exe" + fi + cp "$output_path" "$simple_name" 2>/dev/null || true + fi + return 0 + else + echo -e "${RED}[失败]${NC} ${os}/${arch}" + return 1 + fi +} + +# 主函数 +main() { + local selected_platforms=() + local clean=false + local jobs=4 + local list_only=false + local simple_only=false + + # 解析参数 + while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + usage + exit 0 + ;; + -p|--platform) + selected_platforms+=("$2") + shift 2 + ;; + -l|--list) + list_only=true + shift + ;; + -c|--clean) + clean=true + shift + ;; + -j|--jobs) + jobs="$2" + shift 2 + ;; + -v|--version) + VERSION="$2" + shift 2 + ;; + -s|--simple-only) + simple_only=true + shift + ;; + *) + echo -e "${RED}未知参数: $1${NC}" + usage + exit 1 + ;; + esac + done + + # 如果只是列出平台,则退出 + if [ "$list_only" = true ]; then + list_platforms + exit 0 + fi + + # 确定要编译的平台 + if [ ${#selected_platforms[@]} -eq 0 ]; then + selected_platforms=("${PLATFORMS[@]}") + else + # 验证平台是否支持 + for platform in "${selected_platforms[@]}"; do + local found=false + for p in "${PLATFORMS[@]}"; do + if [ "$p" = "$platform" ]; then + found=true + break + fi + done + if [ "$found" = false ]; then + echo -e "${RED}错误: 不支持的平台 '$platform'${NC}" + echo "" + list_platforms + exit 1 + fi + done + fi + + # 清理(如果需要) + if [ "$clean" = true ]; then + clean_build + else + mkdir -p "$BUILD_DIR" + fi + + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN}开始跨平台编译${NC}" + echo -e "${GREEN}========================================${NC}" + echo -e "项目名称: ${BLUE}${PROJECT_NAME}${NC}" + echo -e "版本号: ${BLUE}${VERSION}${NC}" + echo -e "输出目录: ${BLUE}${BUILD_DIR}${NC}" + echo -e "并行任务数: ${BLUE}${jobs}${NC}" + echo -e "平台数量: ${BLUE}${#selected_platforms[@]}${NC}" + echo "" + + # 导出函数和变量供后台任务使用 + export -f build_platform + export PROJECT_NAME BUILD_DIR VERSION MAIN_PACKAGE RED GREEN YELLOW BLUE NC + + # 使用后台任务进行并行编译 + local success_count=0 + local fail_count=0 + local temp_file=$(mktemp) + + # 导出变量供后台任务使用 + export simple_only + + # 启动编译任务 + for platform in "${selected_platforms[@]}"; do + ( + if build_platform "$platform" "$simple_only"; then + echo "SUCCESS $platform" >> "$temp_file" + else + echo "FAIL $platform" >> "$temp_file" + fi + ) & + + # 控制并行数量 + while [ $(jobs -r | wc -l | tr -d ' ') -ge "$jobs" ]; do + sleep 0.1 + done + done + + # 等待所有后台任务完成 + wait + + # 统计结果 + if [ -f "$temp_file" ]; then + while IFS= read -r line; do + if [[ $line == SUCCESS* ]]; then + ((success_count++)) + elif [[ $line == FAIL* ]]; then + ((fail_count++)) + fi + done < "$temp_file" + rm -f "$temp_file" + fi + + echo "" + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN}编译完成${NC}" + echo -e "${GREEN}========================================${NC}" + echo -e "成功: ${GREEN}${success_count}${NC}" + echo -e "失败: ${RED}${fail_count}${NC}" + echo "" + + if [ $fail_count -eq 0 ]; then + echo -e "${GREEN}所有平台编译成功!${NC}" + echo "" + echo -e "${BLUE}编译输出文件:${NC}" + ls -lh "$BUILD_DIR" | grep "$PROJECT_NAME" | awk '{print " " $9 " (" $5 ")"}' + else + echo -e "${RED}部分平台编译失败,请检查错误信息${NC}" + exit 1 + fi +} + +# 运行主函数 +main "$@" + diff --git a/install.sh b/install.sh index e563602..82af099 100755 --- a/install.sh +++ b/install.sh @@ -48,14 +48,30 @@ fi # 检测系统类型和架构 detect_system() { - if [ -f /etc/os-release ]; then - . /etc/os-release - OS=$ID - OS_VERSION=$VERSION_ID - else - echo -e "${RED}无法检测系统类型${NC}" - exit 1 - fi + # 检测操作系统类型(linux/darwin/windows) + OS_TYPE=$(uname -s | tr '[:upper:]' '[:lower:]') + case $OS_TYPE in + linux) + OS_TYPE="linux" + if [ -f /etc/os-release ]; then + . /etc/os-release + OS=$ID + OS_VERSION=$VERSION_ID + else + OS="linux" + OS_VERSION="" + fi + ;; + darwin) + OS_TYPE="darwin" + OS="darwin" + OS_VERSION=$(sw_vers -productVersion 2>/dev/null || echo "") + ;; + *) + echo -e "${RED}不支持的操作系统: $OS_TYPE${NC}" + exit 1 + ;; + esac ARCH=$(uname -m) case $ARCH in @@ -71,7 +87,7 @@ detect_system() { ;; esac - echo -e "${BLUE}检测到系统: $OS $OS_VERSION ($ARCH)${NC}" + echo -e "${BLUE}检测到系统: $OS_TYPE $OS_VERSION ($ARCH)${NC}" } # 检测并选择最快的镜像源 @@ -628,6 +644,256 @@ uninstall_service() { echo "" } +# 尝试从 Releases 下载二进制文件 +download_binary_from_releases() { + echo -e "${BLUE}尝试从 Releases 下载预编译二进制文件...${NC}" + + # Gitea API 地址 + local api_base="https://gitee.nas.cpolar.cn/api/v1" + local repo_api="${api_base}/repos/${GITHUB_REPO}" + + # 获取所有 releases(按创建时间排序,最新的在前) + echo -e "${BLUE}获取最新 release 信息...${NC}" + local releases_response=$(curl -s -X GET "${repo_api}/releases?limit=10" 2>/dev/null) + + if [ $? -ne 0 ] || [ -z "$releases_response" ]; then + echo -e "${YELLOW}⚠ 无法获取 releases 信息,将使用源码编译${NC}" + return 1 + fi + + # 解析所有 releases,找到最新的(按创建时间或版本号) + # Gitea API 返回的 releases 通常已经按创建时间倒序排列 + # 但我们还是需要解析并验证 + + # 提取所有 tag 和对应的 release_id、创建时间 + local latest_tag="" + local latest_release_id="" + local latest_created_at="" + + # 使用更健壮的方式解析 JSON(虽然简单,但能工作) + # 查找第一个有效的 release(非 draft,非 prerelease) + local tag_line=$(echo "$releases_response" | grep -o '"tag_name":"[^"]*"' | head -1) + local id_line=$(echo "$releases_response" | grep -o '"id":[0-9]*' | head -1) + local created_line=$(echo "$releases_response" | grep -o '"created_at":"[^"]*"' | head -1) + local draft_line=$(echo "$releases_response" | grep -o '"draft":[^,}]*' | head -1) + local prerelease_line=$(echo "$releases_response" | grep -o '"prerelease":[^,}]*' | head -1) + + # 检查是否是 draft 或 prerelease + if echo "$draft_line" | grep -q "true" || echo "$prerelease_line" | grep -q "true"; then + # 如果是 draft 或 prerelease,尝试找下一个 + echo -e "${YELLOW}⚠ 第一个 release 是草稿或预发布版本,查找正式版本...${NC}" + # 简化处理:如果第一个是预发布,仍然使用它(因为可能是最新的) + fi + + latest_tag=$(echo "$tag_line" | cut -d'"' -f4) + latest_release_id=$(echo "$id_line" | cut -d':' -f2) + latest_created_at=$(echo "$created_line" | cut -d'"' -f4) + + if [ -z "$latest_tag" ] || [ -z "$latest_release_id" ]; then + echo -e "${YELLOW}⚠ 无法解析 release 信息,将使用源码编译${NC}" + return 1 + fi + + # 显示找到的版本信息 + echo -e "${GREEN}✓ 找到最新版本: ${latest_tag}${NC}" + if [ -n "$latest_created_at" ]; then + echo -e "${BLUE} 发布日期: ${latest_created_at}${NC}" + fi + + # 构建文件名(根据系统类型) + local file_name="agent-${OS_TYPE}-${ARCH}-${latest_tag}" + local file_ext="tar.gz" + if [ "$OS_TYPE" = "windows" ]; then + file_ext="zip" + fi + local full_file_name="${file_name}.${file_ext}" + + # 获取 release 的详细信息(包含 commit hash) + local release_detail=$(curl -s -X GET "${repo_api}/releases/${latest_release_id}" 2>/dev/null) + + if [ $? -ne 0 ] || [ -z "$release_detail" ]; then + echo -e "${YELLOW}⚠ 无法获取 release 详细信息,将使用源码编译${NC}" + return 1 + fi + + # 解析 release 对应的 commit hash(target_commitish) + local release_commit=$(echo "$release_detail" | grep -o '"target_commitish":"[^"]*"' | head -1 | cut -d'"' -f4) + + # 如果 release_detail 中没有 target_commitish,尝试通过 tag 获取 commit hash + if [ -z "$release_commit" ]; then + # 通过 tag API 获取 commit hash + local tag_info=$(curl -s -X GET "${repo_api}/git/tags/${latest_tag}" 2>/dev/null) + if [ $? -eq 0 ] && [ -n "$tag_info" ]; then + release_commit=$(echo "$tag_info" | grep -o '"sha":"[^"]*"' | head -1 | cut -d'"' -f4) + fi + fi + + # 如果还是无法获取 commit hash,使用 assets_response 继续(不强制要求 commit) + local assets_response="$release_detail" + + # 查找匹配的二进制文件 + local download_url=$(echo "$assets_response" | grep -o "\"browser_download_url\":\"[^\"]*${full_file_name}[^\"]*\"" | head -1 | cut -d'"' -f4) + + if [ -z "$download_url" ]; then + echo -e "${YELLOW}⚠ 未找到匹配的二进制文件: ${full_file_name}${NC}" + echo -e "${YELLOW} 将使用源码编译${NC}" + return 1 + fi + + echo -e "${BLUE}下载二进制文件: ${full_file_name}...${NC}" + + # 创建临时目录 + local temp_dir=$(mktemp -d) + local download_file="${temp_dir}/${full_file_name}" + + # 下载文件 + if ! curl -fsSL -o "$download_file" "$download_url" 2>/dev/null; then + echo -e "${YELLOW}⚠ 下载失败,将使用源码编译${NC}" + rm -rf "$temp_dir" + return 1 + fi + + # 检查文件大小(至少应该大于 1MB) + local file_size=$(stat -f%z "$download_file" 2>/dev/null || stat -c%s "$download_file" 2>/dev/null || echo "0") + if [ "$file_size" -lt 1048576 ]; then + echo -e "${YELLOW}⚠ 下载的文件大小异常,将使用源码编译${NC}" + rm -rf "$temp_dir" + return 1 + fi + + # 解压文件 + echo -e "${BLUE}解压二进制文件...${NC}" + cd "$temp_dir" + + if [ "$file_ext" = "tar.gz" ]; then + if ! tar -xzf "$download_file" 2>/dev/null; then + echo -e "${YELLOW}⚠ 解压失败,将使用源码编译${NC}" + rm -rf "$temp_dir" + return 1 + fi + # 查找解压后的二进制文件(优先查找 agent,然后是 agent-*) + local binary_file="" + if [ -f "./agent" ] && [ -x "./agent" ]; then + binary_file="./agent" + else + binary_file=$(find . -maxdepth 1 -type f \( -name "agent" -o -name "agent-*" -o -name "Agent" -o -name "Agent-*" \) ! -name "*.tar.gz" ! -name "*.zip" 2>/dev/null | head -1) + fi + else + # Windows zip 文件(虽然脚本主要在 Linux 上运行,但保留兼容性) + if ! unzip -q "$download_file" 2>/dev/null; then + echo -e "${YELLOW}⚠ 解压失败,将使用源码编译${NC}" + rm -rf "$temp_dir" + return 1 + fi + local binary_file=$(find . -maxdepth 1 -type f \( -name "agent*.exe" -o -name "Agent*.exe" \) 2>/dev/null | head -1) + fi + + if [ -z "$binary_file" ] || [ ! -f "$binary_file" ]; then + echo -e "${YELLOW}⚠ 未找到解压后的二进制文件,将使用源码编译${NC}" + echo -e "${YELLOW} 解压目录内容:${NC}" + ls -la "$temp_dir" 2>/dev/null || true + rm -rf "$temp_dir" + return 1 + fi + + # 验证二进制文件是否可执行 + if [ ! -x "$binary_file" ]; then + chmod +x "$binary_file" 2>/dev/null || true + fi + + # 验证二进制文件类型(Linux 应该是 ELF 文件) + if [ "$OS_TYPE" = "linux" ]; then + if command -v file > /dev/null 2>&1; then + local file_type=$(file "$binary_file" 2>/dev/null || echo "") + if [ -n "$file_type" ] && ! echo "$file_type" | grep -qi "ELF"; then + echo -e "${YELLOW}⚠ 二进制文件类型异常: ${file_type}${NC}" + echo -e "${YELLOW} 将使用源码编译${NC}" + rm -rf "$temp_dir" + return 1 + fi + fi + fi + + # 保存下载的二进制文件到临时位置 + local downloaded_binary="${temp_dir}/downloaded_agent" + sudo cp "$binary_file" "$downloaded_binary" + sudo chmod +x "$downloaded_binary" + + # 验证复制后的文件 + if [ ! -f "$downloaded_binary" ] || [ ! -x "$downloaded_binary" ]; then + echo -e "${YELLOW}⚠ 二进制文件验证失败,将使用源码编译${NC}" + rm -rf "$temp_dir" + return 1 + fi + + # 清理临时下载文件 + rm -f "$download_file" + + # 克隆仓库(用于获取 run.sh 和 start-systemd.sh 等脚本) + echo -e "${BLUE}克隆仓库以获取启动脚本...${NC}" + if [ -d "$SOURCE_DIR" ]; then + sudo rm -rf "$SOURCE_DIR" + fi + + if ! sudo git clone --branch "${GITHUB_BRANCH}" "https://gitee.nas.cpolar.cn/${GITHUB_REPO}.git" "$SOURCE_DIR" 2>&1; then + echo -e "${YELLOW}⚠ 克隆仓库失败,将使用源码编译${NC}" + rm -rf "$temp_dir" + return 1 + fi + + # 对比 Git commit hash + if [ -n "$release_commit" ]; then + echo -e "${BLUE}验证 Git commit 版本...${NC}" + cd "$SOURCE_DIR" + + # 获取当前分支的最新 commit hash + local current_commit=$(git rev-parse HEAD 2>/dev/null || echo "") + + if [ -n "$current_commit" ]; then + # 截取短 commit hash 用于显示(前7位) + local release_commit_short=$(echo "$release_commit" | cut -c1-7) + local current_commit_short=$(echo "$current_commit" | cut -c1-7) + + echo -e "${BLUE} Release commit: ${release_commit_short}${NC}" + echo -e "${BLUE} 当前代码 commit: ${current_commit_short}${NC}" + + # 对比 commit hash + if [ "$release_commit" != "$current_commit" ]; then + echo -e "${YELLOW}⚠ Commit hash 不匹配,二进制文件可能不是最新代码编译的${NC}" + echo -e "${YELLOW} Release 基于较旧的代码,将使用源码编译最新版本${NC}" + rm -rf "$temp_dir" + return 1 + else + echo -e "${GREEN}✓ Commit hash 匹配,二进制文件是最新代码编译的${NC}" + fi + else + echo -e "${YELLOW}⚠ 无法获取当前代码的 commit hash,跳过验证${NC}" + fi + else + echo -e "${YELLOW}⚠ 无法获取 release 的 commit hash,跳过验证${NC}" + fi + + # 用下载的二进制文件覆盖克隆目录中的文件 + sudo cp "$downloaded_binary" "$SOURCE_DIR/agent" + sudo chmod +x "$SOURCE_DIR/agent" + + # 复制到安装目录 + sudo mkdir -p "$INSTALL_DIR" + sudo cp "$SOURCE_DIR/agent" "$INSTALL_DIR/$BINARY_NAME" + sudo chmod +x "$INSTALL_DIR/$BINARY_NAME" + + # 清理临时文件 + rm -rf "$temp_dir" + + # 显示文件信息 + local binary_size=$(du -h "$SOURCE_DIR/agent" | cut -f1) + echo -e "${GREEN}✓ 二进制文件下载完成 (文件大小: ${binary_size})${NC}" + echo -e "${BLUE}版本: ${latest_tag}${NC}" + echo -e "${BLUE}二进制文件: ${SOURCE_DIR}/agent${NC}" + + return 0 +} + # 从源码编译安装 build_from_source() { echo -e "${BLUE}从源码编译安装节点端...${NC}" @@ -779,6 +1045,22 @@ create_service() { sudo chmod +x "$SOURCE_DIR/run.sh" sudo chmod +x "$SOURCE_DIR/start-systemd.sh" + # 检测 Go 的安装路径 + GO_PATH="" + if [ -d "/usr/local/go/bin" ]; then + GO_PATH="/usr/local/go/bin" + elif command -v go > /dev/null 2>&1; then + GO_BIN=$(command -v go) + GO_PATH=$(dirname "$GO_BIN") + fi + + # 构建 PATH 环境变量 + if [ -n "$GO_PATH" ]; then + ENV_PATH="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${GO_PATH}" + else + ENV_PATH="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + fi + sudo tee /etc/systemd/system/${SERVICE_NAME}.service > /dev/null < /dev/null 2>&1; then echo "错误: 未找到 Go 环境,无法编译" >&2 + echo "PATH: $PATH" >&2 + echo "请确保 Go 已安装: /usr/local/go/bin 或系统 PATH 中包含 go 命令" >&2 exit 1 fi diff --git a/upload.sh b/upload.sh new file mode 100755 index 0000000..c0ced1a --- /dev/null +++ b/upload.sh @@ -0,0 +1,898 @@ +#!/bin/bash + +# 发布上传脚本 +# 支持多种上传方式:GitHub Releases、Gitea Releases、SCP、FTP、本地复制 + +set -e + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# 项目信息 +PROJECT_NAME="agent" +BUILD_DIR="bin" +VERSION="${VERSION:-$(date +%Y%m%d-%H%M%S)}" +RELEASE_DIR="release" +TEMP_DIR=$(mktemp -d) + +# 支持的平台列表 +PLATFORMS=( + "linux/amd64" + "linux/arm64" + "darwin/amd64" + "darwin/arm64" + "windows/amd64" + "windows/arm64" +) + +# 清理函数 +cleanup() { + if [ -d "$TEMP_DIR" ]; then + rm -rf "$TEMP_DIR" + fi +} +trap cleanup EXIT + +# 显示使用说明 +usage() { + echo -e "${BLUE}使用方法:${NC}" + echo " $0 [选项]" + echo "" + echo -e "${BLUE}选项:${NC}" + echo " -h, --help 显示帮助信息" + echo " -m, --method METHOD 上传方式: github|gitea|scp|ftp|local (默认: gitea)" + echo " -v, --version VERSION 版本号 (默认: 时间戳)" + echo " -p, --platform PLATFORM 只上传指定平台 (例如: linux/amd64)" + echo " -t, --tag TAG Git标签 (GitHub/Gitea Releases需要)" + echo " -r, --repo REPO 仓库 (格式: owner/repo,默认从.git/config读取)" + echo " -b, --base-url URL Gitea基础URL (默认从.git/config读取)" + echo " -T, --token TOKEN 访问令牌 (Gitea需要,也可通过GITEA_TOKEN环境变量)" + echo " -d, --dest DEST 目标路径 (SCP/FTP/local需要)" + echo " -H, --host HOST 主机地址 (SCP/FTP需要)" + echo " -u, --user USER 用户名 (SCP/FTP需要)" + echo " -P, --port PORT 端口号 (SCP/FTP需要,默认: SCP=22, FTP=21)" + echo " -k, --key KEY 私钥路径 (SCP需要)" + echo " --pack-only 只打包不上传" + echo " --no-pack 不上传压缩包,直接上传二进制文件" + echo " --notes NOTES 发布说明 (GitHub/Gitea Releases)" + echo " --notes-file FILE 从文件读取发布说明" + echo "" + echo -e "${BLUE}上传方式说明:${NC}" + echo "" + echo -e "${CYAN}Gitea Releases (自动从.git/config读取):${NC}" + echo " $0 -m gitea -t v1.0.0 -v 1.0.0" + echo " $0 -m gitea -t v1.0.0 -v 1.0.0 -T your_token" + echo "" + echo -e "${CYAN}GitHub Releases:${NC}" + echo " $0 -m github -r owner/repo -t v1.0.0 -v 1.0.0" + echo "" + echo -e "${CYAN}SCP上传:${NC}" + echo " $0 -m scp -H example.com -u user -d /path/to/release" + echo " $0 -m scp -H example.com -u user -d /path/to/release -k ~/.ssh/id_rsa" + echo "" + echo -e "${CYAN}FTP上传:${NC}" + echo " $0 -m ftp -H ftp.example.com -u user -d /path/to/release" + echo "" + echo -e "${CYAN}本地复制:${NC}" + echo " $0 -m local -d /path/to/release" + echo "" + echo -e "${CYAN}只打包:${NC}" + echo " $0 --pack-only -v 1.0.0" +} + +# 从 .git/config 读取 Git 信息 +read_git_info() { + local git_config=".git/config" + + if [ ! -f "$git_config" ]; then + return 1 + fi + + # 读取 origin URL + local url=$(git config --get remote.origin.url 2>/dev/null || echo "") + + if [ -z "$url" ]; then + return 1 + fi + + # 解析 URL + # 支持格式: + # - https://user:pass@host/owner/repo.git + # - https://host/owner/repo.git + # - git@host:owner/repo.git + # - ssh://user@host/owner/repo.git + + local base_url="" + local owner="" + local repo_name="" + local token="" + + # 提取 token (如果有) + if [[ "$url" =~ https://([^:]+):([^@]+)@(.+) ]]; then + token="${BASH_REMATCH[2]}" + url="https://${BASH_REMATCH[1]}@${BASH_REMATCH[3]}" + fi + + # 提取 base_url, owner, repo + if [[ "$url" =~ https://([^/]+)/([^/]+)/([^/]+)\.git ]]; then + base_url="https://${BASH_REMATCH[1]}" + owner="${BASH_REMATCH[2]}" + repo_name="${BASH_REMATCH[3]}" + elif [[ "$url" =~ git@([^:]+):([^/]+)/([^/]+)\.git ]]; then + base_url="https://${BASH_REMATCH[1]}" + owner="${BASH_REMATCH[2]}" + repo_name="${BASH_REMATCH[3]}" + elif [[ "$url" =~ ssh://[^@]+@([^/]+)/([^/]+)/([^/]+)\.git ]]; then + base_url="https://${BASH_REMATCH[1]}" + owner="${BASH_REMATCH[2]}" + repo_name="${BASH_REMATCH[3]}" + fi + + if [ -n "$base_url" ] && [ -n "$owner" ] && [ -n "$repo_name" ]; then + echo "$base_url|$owner|$repo_name|$token" + return 0 + fi + + return 1 +} + +# 检查必要的工具 +check_dependencies() { + local method=$1 + local missing_tools=() + + case $method in + github) + if ! command -v gh &> /dev/null; then + missing_tools+=("gh (GitHub CLI)") + fi + ;; + gitea) + if ! command -v curl &> /dev/null; then + missing_tools+=("curl") + fi + if ! command -v jq &> /dev/null; then + echo -e "${YELLOW}警告: jq 未安装,某些功能可能受限${NC}" + fi + ;; + scp) + if ! command -v scp &> /dev/null; then + missing_tools+=("scp") + fi + ;; + ftp) + if ! command -v curl &> /dev/null && ! command -v ftp &> /dev/null; then + missing_tools+=("curl 或 ftp") + fi + ;; + esac + + if [ ${#missing_tools[@]} -gt 0 ]; then + echo -e "${RED}错误: 缺少必要的工具:${NC}" + for tool in "${missing_tools[@]}"; do + echo " - $tool" + done + exit 1 + fi +} + +# 检查构建文件是否存在 +check_build_files() { + if [ ! -d "$BUILD_DIR" ] || [ -z "$(ls -A $BUILD_DIR 2>/dev/null)" ]; then + echo -e "${RED}错误: 构建目录为空或不存在${NC}" + echo "请先运行 ./build-all.sh 编译项目" + exit 1 + fi + + local found=0 + for platform in "${PLATFORMS[@]}"; do + local os=$(echo $platform | cut -d'/' -f1) + local arch=$(echo $platform | cut -d'/' -f2) + local file="${BUILD_DIR}/${PROJECT_NAME}-${os}-${arch}" + if [ "$os" = "windows" ]; then + file="${file}.exe" + fi + + if [ -f "$file" ]; then + found=1 + break + fi + done + + if [ $found -eq 0 ]; then + echo -e "${RED}错误: 未找到任何构建文件${NC}" + echo "请先运行 ./build-all.sh 编译项目" + exit 1 + fi +} + +# 打包文件 +pack_files() { + local platform=$1 + local os=$(echo $platform | cut -d'/' -f1) + local arch=$(echo $platform | cut -d'/' -f2) + local binary="${BUILD_DIR}/${PROJECT_NAME}-${os}-${arch}" + + if [ "$os" = "windows" ]; then + binary="${binary}.exe" + fi + + if [ ! -f "$binary" ]; then + echo -e "${YELLOW}[跳过]${NC} ${platform} - 文件不存在" + return 1 + fi + + local pack_name="${PROJECT_NAME}-${os}-${arch}-${VERSION}" + local pack_file + + if [ "$os" = "windows" ]; then + pack_file="${TEMP_DIR}/${pack_name}.zip" + echo -e "${BLUE}[打包]${NC} ${platform} -> ${pack_name}.zip" + (cd "$BUILD_DIR" && zip -q "${pack_file}" "$(basename $binary)") + else + pack_file="${TEMP_DIR}/${pack_name}.tar.gz" + echo -e "${BLUE}[打包]${NC} ${platform} -> ${pack_name}.tar.gz" + tar -czf "$pack_file" -C "$BUILD_DIR" "$(basename $binary)" + fi + + echo "$pack_file" + return 0 +} + +# 创建发布说明 +create_release_notes() { + local notes_file="${TEMP_DIR}/release_notes.md" + cat > "$notes_file" </dev/null || echo "N/A") +- **Git分支**: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "N/A") + +## 支持的平台 +EOF + + for platform in "${PLATFORMS[@]}"; do + local os=$(echo $platform | cut -d'/' -f1) + local arch=$(echo $platform | cut -d'/' -f2) + local binary="${BUILD_DIR}/${PROJECT_NAME}-${os}-${arch}" + if [ "$os" = "windows" ]; then + binary="${binary}.exe" + fi + + if [ -f "$binary" ]; then + local size=$(ls -lh "$binary" | awk '{print $5}') + echo "- ${os}/${arch} (${size})" >> "$notes_file" + fi + done + + echo "" >> "$notes_file" + echo "## 安装说明" >> "$notes_file" + echo "" >> "$notes_file" + echo "### Linux/macOS" >> "$notes_file" + echo "\`\`\`bash" >> "$notes_file" + echo "tar -xzf ${PROJECT_NAME}-linux-amd64-${VERSION}.tar.gz" >> "$notes_file" + echo "chmod +x ${PROJECT_NAME}-linux-amd64" >> "$notes_file" + echo "./${PROJECT_NAME}-linux-amd64" >> "$notes_file" + echo "\`\`\`" >> "$notes_file" + echo "" >> "$notes_file" + echo "### Windows" >> "$notes_file" + echo "\`\`\`powershell" >> "$notes_file" + echo "Expand-Archive ${PROJECT_NAME}-windows-amd64-${VERSION}.zip" >> "$notes_file" + echo ".\\${PROJECT_NAME}-windows-amd64.exe" >> "$notes_file" + echo "\`\`\`" >> "$notes_file" + + echo "$notes_file" +} + +# GitHub Releases 上传 +upload_github() { + local repo=$1 + local tag=$2 + local notes_file=$3 + + if [ -z "$repo" ]; then + echo -e "${RED}错误: GitHub仓库未指定,使用 -r owner/repo${NC}" + exit 1 + fi + + if [ -z "$tag" ]; then + echo -e "${RED}错误: Git标签未指定,使用 -t TAG${NC}" + exit 1 + fi + + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN}上传到 GitHub Releases${NC}" + echo -e "${GREEN}========================================${NC}" + echo -e "仓库: ${BLUE}${repo}${NC}" + echo -e "标签: ${BLUE}${tag}${NC}" + echo -e "版本: ${BLUE}${VERSION}${NC}" + echo "" + + # 检查是否已存在release + if gh release view "$tag" --repo "$repo" &>/dev/null; then + echo -e "${YELLOW}警告: Release ${tag} 已存在${NC}" + read -p "是否删除并重新创建? (y/N): " confirm + if [[ "$confirm" =~ ^[Yy]$ ]]; then + gh release delete "$tag" --repo "$repo" --yes + else + echo "取消上传" + exit 0 + fi + fi + + # 创建release + echo -e "${BLUE}[创建Release]${NC} ${tag}" + if [ -f "$notes_file" ]; then + gh release create "$tag" \ + --repo "$repo" \ + --title "${PROJECT_NAME} ${VERSION}" \ + --notes-file "$notes_file" \ + --draft=false \ + --prerelease=false + else + gh release create "$tag" \ + --repo "$repo" \ + --title "${PROJECT_NAME} ${VERSION}" \ + --notes "Release ${VERSION}" \ + --draft=false \ + --prerelease=false + fi + + # 上传文件 + local upload_count=0 + shopt -s nullglob + for file in "${TEMP_DIR}"/*.tar.gz "${TEMP_DIR}"/*.zip; do + if [ -f "$file" ]; then + echo -e "${BLUE}[上传]${NC} $(basename $file)" + gh release upload "$tag" "$file" --repo "$repo" --clobber + ((upload_count++)) + fi + done + shopt -u nullglob + + if [ $upload_count -eq 0 ] && [ "$NO_PACK" != "true" ]; then + echo -e "${YELLOW}警告: 没有找到要上传的文件${NC}" + else + echo -e "${GREEN}[完成]${NC} 已上传 ${upload_count} 个文件" + echo -e "${CYAN}Release链接:${NC} https://github.com/${repo}/releases/tag/${tag}" + fi +} + +# Gitea Releases 上传 +upload_gitea() { + local base_url=$1 + local owner=$2 + local repo=$3 + local tag=$4 + local token=$5 + local notes_file=$6 + + if [ -z "$base_url" ] || [ -z "$owner" ] || [ -z "$repo" ]; then + echo -e "${RED}错误: Gitea仓库信息不完整${NC}" + exit 1 + fi + + if [ -z "$tag" ]; then + echo -e "${RED}错误: Git标签未指定,使用 -t TAG${NC}" + exit 1 + fi + + if [ -z "$token" ]; then + token="${GITEA_TOKEN}" + fi + + if [ -z "$token" ]; then + echo -e "${RED}错误: 访问令牌未指定,使用 -T TOKEN 或设置 GITEA_TOKEN 环境变量${NC}" + exit 1 + fi + + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN}上传到 Gitea Releases${NC}" + echo -e "${GREEN}========================================${NC}" + echo -e "基础URL: ${BLUE}${base_url}${NC}" + echo -e "仓库: ${BLUE}${owner}/${repo}${NC}" + echo -e "标签: ${BLUE}${tag}${NC}" + echo -e "版本: ${BLUE}${VERSION}${NC}" + echo "" + + local api_base="${base_url}/api/v1" + local repo_api="${api_base}/repos/${owner}/${repo}" + + # 读取发布说明 + local notes="Release ${VERSION}" + if [ -f "$notes_file" ]; then + notes=$(cat "$notes_file") + fi + + # 转义 JSON 字符串(不使用 jq) + local notes_escaped=$(echo "$notes" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') + + # 检查是否已存在release + local existing_release=$(curl -s -X GET \ + "${repo_api}/releases/tags/${tag}" \ + -H "Authorization: token ${token}" \ + -H "Content-Type: application/json" 2>/dev/null) + + if echo "$existing_release" | grep -q '"id"'; then + echo -e "${YELLOW}警告: Release ${tag} 已存在${NC}" + read -p "是否删除并重新创建? (y/N): " confirm + if [[ "$confirm" =~ ^[Yy]$ ]]; then + local release_id=$(echo "$existing_release" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2) + if [ -n "$release_id" ]; then + curl -s -X DELETE \ + "${repo_api}/releases/${release_id}" \ + -H "Authorization: token ${token}" > /dev/null + echo -e "${BLUE}[删除]${NC} 已删除现有 Release" + fi + else + echo "取消上传" + exit 0 + fi + fi + + # 创建release + echo -e "${BLUE}[创建Release]${NC} ${tag}" + local release_data=$(cat < /dev/null + done + + echo "" + echo -e "${GREEN}打包完成${NC}" + echo -e "${BLUE}打包文件:${NC}" + shopt -s nullglob + ls -lh "${TEMP_DIR}"/*.tar.gz "${TEMP_DIR}"/*.zip 2>/dev/null | awk '{print " " $9 " (" $5 ")"}' + shopt -u nullglob + echo "" + fi + + # 如果只是打包,则复制到release目录 + if [ "$pack_only" = "true" ]; then + mkdir -p "$RELEASE_DIR" + shopt -s nullglob + cp "${TEMP_DIR}"/*.tar.gz "${TEMP_DIR}"/*.zip "$RELEASE_DIR/" 2>/dev/null || true + shopt -u nullglob + echo -e "${GREEN}文件已复制到 ${RELEASE_DIR} 目录${NC}" + exit 0 + fi + + # 创建发布说明 + local release_notes="" + if [ -n "$notes_file" ] && [ -f "$notes_file" ]; then + release_notes="$notes_file" + elif [ -n "$notes" ]; then + echo "$notes" > "${TEMP_DIR}/release_notes.md" + release_notes="${TEMP_DIR}/release_notes.md" + elif [ "$method" = "github" ] || [ "$method" = "gitea" ]; then + release_notes=$(create_release_notes) + fi + + # 根据方法上传 + case $method in + gitea) + # 从 .git/config 读取信息(如果未指定) + local git_info="" + if [ -z "$base_url" ] || [ -z "$repo" ]; then + git_info=$(read_git_info) + if [ $? -eq 0 ] && [ -n "$git_info" ]; then + IFS='|' read -r git_base_url git_owner git_repo_name git_token <<< "$git_info" + if [ -z "$base_url" ]; then + base_url="$git_base_url" + fi + if [ -z "$repo" ]; then + repo="${git_owner}/${git_repo_name}" + fi + if [ -z "$token" ] && [ -n "$git_token" ]; then + token="$git_token" + fi + echo -e "${CYAN}[信息]${NC} 从 .git/config 读取仓库信息: ${repo}" + fi + fi + + if [ -z "$base_url" ] || [ -z "$repo" ]; then + echo -e "${RED}错误: Gitea仓库信息不完整${NC}" + echo "请指定 -b BASE_URL 和 -r owner/repo,或确保 .git/config 中有正确的远程仓库配置" + exit 1 + fi + + IFS='/' read -r owner repo_name <<< "$repo" + if [ -z "$owner" ] || [ -z "$repo_name" ]; then + echo -e "${RED}错误: 仓库格式不正确,应为 owner/repo${NC}" + exit 1 + fi + + upload_gitea "$base_url" "$owner" "$repo_name" "$tag" "$token" "$release_notes" + ;; + github) + upload_github "$repo" "$tag" "$release_notes" + ;; + scp) + upload_scp "$host" "$user" "$dest" "$port" "$key" + ;; + ftp) + upload_ftp "$host" "$user" "$dest" "$port" + ;; + local) + upload_local "$dest" + ;; + *) + echo -e "${RED}错误: 不支持的上传方式: ${method}${NC}" + echo "支持的方式: gitea, github, scp, ftp, local" + exit 1 + ;; + esac + + echo "" + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN}发布完成${NC}" + echo -e "${GREEN}========================================${NC}" +} + +# 运行主函数 +main "$@" +