From adbcb3063f590001778a43080186aa269388af25 Mon Sep 17 00:00:00 2001 From: kira1928 Date: Sun, 22 Feb 2026 02:12:26 +0900 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96CI=20=E6=9E=84=E5=BB=BA=20doc?= =?UTF-8?q?ker=20image=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yaml | 138 ++++++++++++++++++++++++++--- .gitignore | 6 ++ Dockerfile | 85 ++++++++++++------ docker-context/tools/.gitkeep | 0 scripts/docker-prepare-tools.sh | 150 ++++++++++++++++++++++++++++++++ src/cmd/bililive/bililive.go | 13 +++ 6 files changed, 356 insertions(+), 36 deletions(-) create mode 100644 docker-context/tools/.gitkeep create mode 100644 scripts/docker-prepare-tools.sh diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index bb4b4fc..04e4b62 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -81,30 +81,148 @@ jobs: dist/**/*.7z prerelease: ${{ contains(env.GIT_TAG, 'rc') }} + # Docker 镜像构建:按平台分矩阵,arm64 使用原生 ARM runner + # 优化策略: + # 1. amd64/arm64 使用原生 runner,避免 QEMU 模拟 + # 2. 内置工具在 CI 原生环境中预下载,COPY 进镜像(避免在 QEMU 下解压 xz) + # 3. bgo 二进制直接从 artifact 获取,不再从 GitHub Releases curl 下载 release-docker-images: - runs-on: ubuntu-latest - needs: - - publish-release + needs: [publish-release] + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + arch: amd64 + - platform: linux/arm64/v8 + runner: ubuntu-24.04-arm + arch: arm64 + - platform: linux/arm/v7 + runner: ubuntu-24.04-arm # arm/v7 仍需 QEMU,但 ARM runner 上模拟 armv7 比 x86 模拟快 + arch: arm + runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v4 - run: echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV - - run: if ! echo $GIT_TAG | grep "rc" >/dev/null; then DOCKER_TAGS=chigusa/bililive-go:$GIT_TAG,chigusa/bililive-go:latest; else DOCKER_TAGS=chigusa/bililive-go:$GIT_TAG; fi; echo "DOCKER_TAGS=$DOCKER_TAGS" >> $GITHUB_ENV + + # 下载编译好的二进制 artifact(从 release-bins-sharded 产物中提取) + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + path: dist + + # 准备 Docker 构建上下文:提取对应架构的二进制 + - name: Prepare bgo binary for Docker + run: | + mkdir -p docker-context/bin + # 从 shard artifacts 中查找对应架构的 tar.gz + TARBALL=$(find dist -name "bililive-linux-${{ matrix.arch }}.tar.gz" -print -quit) + if [ -z "$TARBALL" ]; then + echo "::error::找不到 bililive-linux-${{ matrix.arch }}.tar.gz" + exit 1 + fi + echo "找到二进制包: $TARBALL" + tar xzf "$TARBALL" -C docker-context/bin "bililive-linux-${{ matrix.arch }}" + mv "docker-context/bin/bililive-linux-${{ matrix.arch }}" docker-context/bin/bililive-go + chmod +x docker-context/bin/bililive-go + ls -la docker-context/bin/ + + # 在原生环境中预下载内置工具(避免在 QEMU 下解压 xz 的巨大开销) + - name: Pre-download built-in tools + run: | + chmod +x scripts/docker-prepare-tools.sh + scripts/docker-prepare-tools.sh "${{ matrix.arch }}" docker-context/tools + + # 仅 arm/v7 需要 QEMU(用于 apt-get 安装 ffmpeg 等) - name: Set up QEMU + if: matrix.arch == 'arm' uses: docker/setup-qemu-action@v3 + - name: Docker Setup Buildx uses: docker/setup-buildx-action@v3 + - name: Log in to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - - name: Build the Docker image + + # 构建并推送单平台镜像(by digest,不关联 tag) + - name: Build and push single-platform image + id: build uses: docker/build-push-action@v5 with: + context: . file: Dockerfile - build-args: tag=${{ env.GIT_TAG }} - platforms: linux/amd64,linux/arm64/v8,linux/arm/v7 + platforms: ${{ matrix.platform }} push: true - tags: ${{ env.DOCKER_TAGS }} - cache-from: type=gha,scope=bililive-go - cache-to: type=gha,mode=max,scope=bililive-go + tags: "" + outputs: type=image,name=chigusa/bililive-go,push-by-digest=true,name-canonical=true + cache-from: type=gha,scope=docker-${{ matrix.arch }} + cache-to: type=gha,mode=max,scope=docker-${{ matrix.arch }} + + # 导出 digest 供后续合并 + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: docker-digest-${{ matrix.arch }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + # 合并各平台镜像为 multi-arch manifest + merge-docker-manifest: + runs-on: ubuntu-latest + needs: [release-docker-images] + steps: + - uses: actions/checkout@v4 + - run: echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV + - run: | + if ! echo $GIT_TAG | grep "rc" >/dev/null; then + DOCKER_TAGS="chigusa/bililive-go:$GIT_TAG,chigusa/bililive-go:latest" + else + DOCKER_TAGS="chigusa/bililive-go:$GIT_TAG" + fi + echo "DOCKER_TAGS=$DOCKER_TAGS" >> $GITHUB_ENV + + - name: Download all digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: docker-digest-* + merge-multiple: true + + - name: Docker Setup Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Create and push multi-arch manifest + working-directory: /tmp/digests + run: | + # 构建 digest 参数列表 + DIGEST_ARGS="" + for dgst in *; do + DIGEST_ARGS="$DIGEST_ARGS chigusa/bililive-go@sha256:$dgst" + done + + # 为每个 tag 创建 manifest + IFS=',' read -ra TAGS <<< "$DOCKER_TAGS" + for tag in "${TAGS[@]}"; do + echo "创建 manifest: $tag" + docker buildx imagetools create -t "$tag" $DIGEST_ARGS + done + + - name: Inspect final image + run: docker buildx imagetools inspect "chigusa/bililive-go:$GIT_TAG" diff --git a/.gitignore b/.gitignore index d786d7f..3fe54a8 100644 --- a/.gitignore +++ b/.gitignore @@ -192,3 +192,9 @@ test/launcher-config-local.json # npm 锁文件(项目统一使用 yarn) package-lock.json + +# CI Docker 构建临时目录(保留 .gitkeep 确保目录结构存在) +docker-context/bin/* +!docker-context/bin/.gitkeep +docker-context/tools/* +!docker-context/tools/.gitkeep diff --git a/Dockerfile b/Dockerfile index 2f766a2..acdb072 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,12 @@ # syntax=docker/dockerfile:1.7 +# +# 支持两种构建模式: +# CI 模式: docker-context/ 已由 CI 脚本预下载好二进制和工具,直接 COPY(快) +# 本地模式: 传入 --build-arg tag=vX.Y.Z,容器内自动下载二进制并运行 sync-built-in-tools(兼容 Windows/Mac) +# +# 本地构建示例: +# docker build --build-arg tag=v0.8.0-rc.3 -t bililive-go . +# docker build --build-arg tag=v0.8.0-rc.3 --platform linux/arm64 -t bililive-go:arm64 . FROM ubuntu:22.04 ARG tag @@ -17,6 +25,7 @@ ENV PUID=0 PGID=0 UMASK=022 RUN mkdir -p $OUTPUT_DIR && \ mkdir -p $CONF_DIR && \ + mkdir -p /opt/bililive/tools && \ apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ curl \ @@ -38,35 +47,59 @@ RUN mkdir -p $OUTPUT_DIR && \ cp -r -f /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ apt-get clean && rm -rf /var/lib/apt/lists/* -RUN set -x; \ - case $(arch) in \ - aarch64) go_arch=arm64 ;; \ - arm*) go_arch=arm ;; \ - i386|i686) go_arch=386 ;; \ - x86_64) go_arch=amd64 ;; \ - *) echo "Unsupported arch: $(arch)"; exit 1 ;; \ - esac; \ - echo "Downloading release ${tag} for arch ${go_arch}"; \ - cd /tmp && curl -sSLO "https://github.com/bililive-go/bililive-go/releases/download/${tag}/bililive-linux-${go_arch}.tar.gz" && \ - tar zxvf "bililive-linux-${go_arch}.tar.gz" "bililive-linux-${go_arch}" && \ - chmod +x "bililive-linux-${go_arch}" && \ - mv "./bililive-linux-${go_arch}" /usr/bin/bililive-go && \ - rm "./bililive-linux-${go_arch}.tar.gz"; \ - if [ "${tag}" != "$(/usr/bin/bililive-go --version 2>&1 | tr -d '\n')" ]; then exit 1; fi - -# For local testing: copy pre-built binary from build context instead of downloading -# COPY bin/bililive-linux-amd64 /usr/bin/bililive-go -# RUN chmod +x /usr/bin/bililive-go +# =========================================================================== +# 二进制安装(双模式) +# CI 模式: docker-context/bin/ 已包含预编译的 bililive-go → 直接 COPY +# 本地模式: 传入 --build-arg tag=vX.Y.Z → 从 GitHub Releases 下载 +# =========================================================================== +COPY docker-context/bin/ /tmp/prebuilt-bin/ +RUN set -eux; \ + if [ -f /tmp/prebuilt-bin/bililive-go ]; then \ + echo "=== CI 模式: 使用预编译二进制 ==="; \ + mv /tmp/prebuilt-bin/bililive-go /usr/bin/bililive-go; \ + elif [ -n "${tag}" ]; then \ + echo "=== 本地模式: 从 GitHub Releases 下载 (tag=${tag}) ==="; \ + case $(arch) in \ + aarch64) go_arch=arm64 ;; \ + arm*) go_arch=arm ;; \ + i386|i686) go_arch=386 ;; \ + x86_64) go_arch=amd64 ;; \ + *) echo "Unsupported arch: $(arch)"; exit 1 ;; \ + esac; \ + cd /tmp && \ + curl -sSLO "https://github.com/bililive-go/bililive-go/releases/download/${tag}/bililive-linux-${go_arch}.tar.gz" && \ + tar zxvf "bililive-linux-${go_arch}.tar.gz" "bililive-linux-${go_arch}" && \ + chmod +x "bililive-linux-${go_arch}" && \ + mv "./bililive-linux-${go_arch}" /usr/bin/bililive-go && \ + rm "./bililive-linux-${go_arch}.tar.gz"; \ + if [ "${tag}" != "$(/usr/bin/bililive-go --version 2>&1 | tr -d '\n')" ]; then \ + echo "版本验证失败: 期望 ${tag},实际 $(/usr/bin/bililive-go --version 2>&1 | tr -d '\n')"; \ + exit 1; \ + fi; \ + else \ + echo "错误: 必须提供预编译二进制 (docker-context/bin/) 或 --build-arg tag=vX.Y.Z"; \ + exit 1; \ + fi; \ + chmod +x /usr/bin/bililive-go; \ + rm -rf /tmp/prebuilt-bin COPY config.docker.yml $CONF_DIR/config.yml -RUN --mount=type=cache,id=bililive-tools-$TARGETOS-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/cache/bililive/tools \ - set -eux; \ - echo "Preparing bililive tools cache for bililive-tools-$TARGETOS-$TARGETARCH$TARGETVARIANT..."; \ - mkdir -p /opt/bililive/tools /cache/bililive/tools; \ - ls -lR /cache/bililive/tools || true; \ - /usr/bin/bililive-go --sync-built-in-tools-to-path /cache/bililive/tools || true; \ - cp -a /cache/bililive/tools/. /opt/bililive/tools/ +# =========================================================================== +# 内置工具安装(双模式) +# CI 模式: docker-context/tools/ 已包含预下载的工具 → 直接 COPY +# 本地模式: 使用 bililive-go --sync-built-in-tools-to-path 下载 +# =========================================================================== +COPY docker-context/tools/ /opt/bililive/tools/ +RUN set -eux; \ + TOOL_COUNT=$(find /opt/bililive/tools -mindepth 1 -not -name '.gitkeep' -print -quit | wc -l); \ + if [ "$TOOL_COUNT" -gt 0 ]; then \ + echo "=== CI 模式: 使用预下载的内置工具 ==="; \ + find /opt/bililive/tools -name '.gitkeep' -delete 2>/dev/null || true; \ + else \ + echo "=== 本地模式: 通过 sync-built-in-tools 下载 ==="; \ + /usr/bin/bililive-go --sync-built-in-tools-to-path /opt/bililive/tools || true; \ + fi COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh diff --git a/docker-context/tools/.gitkeep b/docker-context/tools/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/docker-prepare-tools.sh b/scripts/docker-prepare-tools.sh new file mode 100644 index 0000000..fc45e43 --- /dev/null +++ b/scripts/docker-prepare-tools.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# docker-prepare-tools.sh +# 在原生环境中为 Docker 构建预下载内置工具 +# 用法: ./scripts/docker-prepare-tools.sh +# 示例: ./scripts/docker-prepare-tools.sh arm64 docker-context/tools +# +# 支持的架构: amd64, arm64, arm (armv7) +# 此脚本在 CI 的原生 x86/ARM runner 上运行,避免在 QEMU 中解压大文件 +# +# 重要: 解压逻辑必须与 remotetools 的 extractDownloadedFile() 行为一致: +# 1. 先解压到临时目录 +# 2. 如果解压后顶层只有一个目录,则自动提升该子目录(strip 顶层) +# 3. 最终移动到目标目录 +set -euo pipefail + +ARCH="${1:?用法: $0 }" +OUTPUT_DIR="${2:?用法: $0 }" + +mkdir -p "$OUTPUT_DIR" + +# =========================================================================== +# 辅助函数 +# 模拟 remotetools 的 extractDownloadedFile() 行为: +# - 解压到临时目录 +# - 如果顶层只有一个子目录,自动 strip 该目录层 +# =========================================================================== +download_and_extract() { + local url="$1" + local dest="$2" # 最终目标目录(例如 tools/ffmpeg/n8.0-latest) + local filename + filename=$(basename "$url") + + # 确保目标目录的父目录存在 + mkdir -p "$(dirname "$dest")" + + # 使用临时目录解压(与 remotetools 行为一致) + local tmp_dir="${dest}.tmp_extract" + rm -rf "$tmp_dir" + mkdir -p "$tmp_dir" + + echo ">>> 下载: $url" + curl -sSL -o "/tmp/$filename" "$url" + + echo " 解压到临时目录..." + case "$filename" in + *.tar.xz) + tar xf "/tmp/$filename" -C "$tmp_dir" + ;; + *.tar.gz) + tar xzf "/tmp/$filename" -C "$tmp_dir" + ;; + *.zip) + unzip -qo "/tmp/$filename" -d "$tmp_dir" + ;; + *) + echo " 错误: 不支持的格式: $filename" + rm -rf "$tmp_dir" + exit 1 + ;; + esac + rm -f "/tmp/$filename" + + # 模拟 remotetools 的 "单目录提升" 逻辑: + # 如果解压后顶层只有一个子目录,将其内容提升为目标目录 + local entries + entries=$(ls -1 "$tmp_dir" | wc -l) + local first_entry + first_entry=$(ls -1 "$tmp_dir" | head -1) + + if [ "$entries" -eq 1 ] && [ -d "$tmp_dir/$first_entry" ]; then + echo " 检测到单一顶层目录 '$first_entry',自动提升" + rm -rf "$dest" + mv "$tmp_dir/$first_entry" "$dest" + rm -rf "$tmp_dir" + else + echo " 直接使用解压内容" + rm -rf "$dest" + mv "$tmp_dir" "$dest" + fi + + echo " 完成: $dest" +} + +echo "=== 为 linux/$ARCH 预下载 Docker 内置工具 ===" + +# --- ffmpeg --- +# 注意: arm/armv7 使用 apt 安装的 ffmpeg,不需要下载 +if [ "$ARCH" = "amd64" ]; then + download_and_extract \ + "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n8.0-latest-linux64-gpl-8.0.tar.xz" \ + "$OUTPUT_DIR/ffmpeg/n8.0-latest" +elif [ "$ARCH" = "arm64" ]; then + download_and_extract \ + "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n8.0-latest-linuxarm64-gpl-8.0.tar.xz" \ + "$OUTPUT_DIR/ffmpeg/n8.0-latest" +fi + +# --- dotnet --- +if [ "$ARCH" = "amd64" ]; then + download_and_extract \ + "https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/8.0.20/aspnetcore-runtime-8.0.20-linux-x64.tar.gz" \ + "$OUTPUT_DIR/dotnet/8.0.20" +elif [ "$ARCH" = "arm64" ]; then + download_and_extract \ + "https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/8.0.20/aspnetcore-runtime-8.0.20-linux-arm64.tar.gz" \ + "$OUTPUT_DIR/dotnet/8.0.20" +elif [ "$ARCH" = "arm" ]; then + download_and_extract \ + "https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/8.0.20/aspnetcore-runtime-8.0.20-linux-arm.tar.gz" \ + "$OUTPUT_DIR/dotnet/8.0.20" +fi + +# --- bililive-recorder --- +# 跨平台 .NET 程序,所有架构相同 +download_and_extract \ + "https://github.com/BililiveRecorder/BililiveRecorder/releases/download/v2.17.3/BililiveRecorder-CLI-any.zip" \ + "$OUTPUT_DIR/bililive-recorder/v2.17.3" + +# --- node --- +if [ "$ARCH" = "amd64" ]; then + download_and_extract \ + "https://nodejs.org/dist/v20.10.0/node-v20.10.0-linux-x64.tar.xz" \ + "$OUTPUT_DIR/node/v20.10.0" +elif [ "$ARCH" = "arm64" ]; then + download_and_extract \ + "https://nodejs.org/dist/v20.10.0/node-v20.10.0-linux-arm64.tar.xz" \ + "$OUTPUT_DIR/node/v20.10.0" +elif [ "$ARCH" = "arm" ]; then + download_and_extract \ + "https://nodejs.org/dist/v20.10.0/node-v20.10.0-linux-armv7l.tar.xz" \ + "$OUTPUT_DIR/node/v20.10.0" +fi + +# --- biliLive-tools --- +if [ "$ARCH" = "amd64" ]; then + download_and_extract \ + "https://github.com/kira1928/biliLive-tools/releases/download/3.1.2-bgo.2/bililive-cli-lib-ubuntu-x64.zip" \ + "$OUTPUT_DIR/biliLive-tools/3.1.2-bgo.2" +elif [ "$ARCH" = "arm64" ]; then + download_and_extract \ + "https://github.com/kira1928/biliLive-tools/releases/download/3.1.2-bgo.2/bililive-cli-lib-ubuntu-arm64.zip" \ + "$OUTPUT_DIR/biliLive-tools/3.1.2-bgo.2" +elif [ "$ARCH" = "arm" ]; then + download_and_extract \ + "https://github.com/kira1928/biliLive-tools/releases/download/3.1.2-bgo.2/bililive-cli-lib-ubuntu-armv7.zip" \ + "$OUTPUT_DIR/biliLive-tools/3.1.2-bgo.2" +fi + +echo "=== 所有工具下载完成 ===" +du -sh "$OUTPUT_DIR"/* 2>/dev/null || true diff --git a/src/cmd/bililive/bililive.go b/src/cmd/bililive/bililive.go index 490acde..03720da 100644 --- a/src/cmd/bililive/bililive.go +++ b/src/cmd/bililive/bililive.go @@ -70,6 +70,19 @@ func getConfigBesidesExecutable() (*configs.Config, error) { if err != nil { return nil, err } + + // 如果是由 Launcher 启动的子进程(新版本 exe 位于 .appdata/versions/{version}/), + // 优先从 Launcher(入口程序)的 exe 旁边查找用户的真实 config.yml。 + // 因为 release 压缩包解压后,新版本 exe 旁边总会有一个默认的 config.yml, + // 必须优先使用用户原始目录的配置文件。 + if launcherExe := os.Getenv("BILILIVE_LAUNCHER_EXE"); launcherExe != "" { + launcherConfigPath := filepath.Join(filepath.Dir(launcherExe), "config.yml") + if config, err := configs.NewConfigWithFile(launcherConfigPath); err == nil { + return config, nil + } + } + + // 回退:在当前 exe 旁边查找(用户直接双击运行的场景) configPath := filepath.Join(filepath.Dir(exePath), "config.yml") config, err := configs.NewConfigWithFile(configPath) if err != nil {