diff --git a/.woodpecker/dev-image.yml b/.woodpecker/dev-image.yml index f6c2f72..559417b 100644 --- a/.woodpecker/dev-image.yml +++ b/.woodpecker/dev-image.yml @@ -36,18 +36,6 @@ steps: - python3 ci/run-with-timeout.py 1800 bash ci/build-dev-image.sh backend depends_on: [] - - name: build-musl-amd64 - image: bash - environment: - GITEA_BASE: http://127.0.0.1:3100 - GITEA_TOKEN: - from_secret: gitea_token - RELEASE_SIGNING_KEY: - from_secret: release_signing_key - commands: - - python3 ci/run-with-timeout.py 3600 bash ci/build-linux-agent.sh linux-musl-x64 - depends_on: [] - - name: build-musl-arm64 image: bash environment: @@ -100,7 +88,7 @@ steps: commands: - python3 ci/run-with-timeout.py 900 bash ci/release-node-images.sh depends_on: - - build-musl-amd64 + - build-glibc-amd64 - build-musl-arm64 - name: backend-e2e-tests diff --git a/.woodpecker/linux-agent.yml b/.woodpecker/linux-agent.yml index fdcfb72..844d74c 100644 --- a/.woodpecker/linux-agent.yml +++ b/.woodpecker/linux-agent.yml @@ -16,18 +16,6 @@ when: concurrency: 1 steps: - - name: build-musl-amd64 - image: bash - environment: - GITEA_BASE: http://127.0.0.1:3100 - GITEA_TOKEN: - from_secret: gitea_token - RELEASE_SIGNING_KEY: - from_secret: release_signing_key - commands: - - python3 ci/run-with-timeout.py 3600 bash ci/build-linux-agent.sh linux-musl-x64 - depends_on: [] - - name: build-musl-arm64 image: bash environment: diff --git a/AGENTS.md b/AGENTS.md index 880677c..467a1be 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,7 +83,7 @@ Triggers, tags and image paths are defined in `.woodpecker/*.yml` and [`docs/ops - A commit message containing `[CI]` builds and pushes development images. `[CI SKIP]` skips the pipeline; `[SKIP TESTS]` skips only the Backend tests. Slow end-to-end tests run after image publication and do not block it. - A tag `vMAJOR.MINOR.PATCH` runs the full cross-platform release pipeline. No version-bump commit is needed. - Versions are produced by `ci/version.sh`. The `` in `backend/Directory.Build.props` and the `version` in `web` `package.json` files are placeholders (`0.0.0-dev`) and must not be edited. -- Linux Agent builds use `ci/build-linux-agent.sh` for musl/glibc on amd64/arm64 in both dev and tag workflows. OCI and OpenWrt installers reuse these native outputs; Windows runners build only Windows/macOS. +- Linux Agent builds use `ci/build-linux-agent.sh` for glibc on amd64/arm64 and musl on arm64 in both dev and tag workflows. OCI and OpenWrt installers reuse these native outputs; Windows runners build only Windows/macOS. - Build stores keep NuGet cache mounts, retain image layers for 7 days, and clean temporary outputs under store locks (shared for native builds, exclusive for cleanup). Do not use `buildah prune` on these stores: it removes restore caches too. - Development images use the `moshiro-dev/` repository path and release images use `moshiro/`. A development build must never overwrite a release tag such as `latest`. - Release manifests are registered at `/api/release/offer`, which requires no token because the panel verifies the signature. diff --git a/backend/README.md b/backend/README.md index f3ebbe6..7a6cb5f 100644 --- a/backend/README.md +++ b/backend/README.md @@ -38,7 +38,7 @@ dotnet publish src/MoShiro.Backend -c Release -r -p:PublishAot=true 例如 `` 可为 `win-x64`、`linux-x64` 或 `linux-arm64`。NativeAOT 发布需要对应平台的 C/C++ 工具链。仓库根目录的 `scripts/publish.sh backend ` 可构建单个平台的 Backend 可执行文件;容器发布由 Woodpecker tag 流水线完成,详见 [`../docs/ops/deployment.md`](../docs/ops/deployment.md)。 -正式发布通过 Woodpecker tag 流水线生成 Linux/amd64 与 Linux/arm64 OCI 镜像,容器内监听 `8080`,数据写入挂载的 `/data` 卷。Agent 的多平台二进制包单独发布,不包含在 Backend 镜像中;Linux runner 构建四个 Linux RID,Windows runner 构建 macOS 和 Windows。 +正式发布通过 Woodpecker tag 流水线生成 Linux/amd64 与 Linux/arm64 OCI 镜像,容器内监听 `8080`,数据写入挂载的 `/data` 卷。Agent 的多平台二进制包单独发布,不包含在 Backend 镜像中;Linux runner 构建三个 Linux RID,Windows runner 构建 macOS 和 Windows。 ## 本地运行 diff --git a/ci/Bundle-QuicRuntime.ps1 b/ci/Bundle-QuicRuntime.ps1 index 69f7042..f02df60 100644 --- a/ci/Bundle-QuicRuntime.ps1 +++ b/ci/Bundle-QuicRuntime.ps1 @@ -1,6 +1,6 @@ param( [Parameter(Mandatory)] - [ValidateSet('linux-x64', 'linux-arm64', 'linux-musl-x64', 'linux-musl-arm64')] + [ValidateSet('linux-x64', 'linux-arm64', 'linux-musl-arm64')] [string]$RuntimeIdentifier, [Parameter(Mandatory)] [string]$Destination diff --git a/ci/build-linux-agent.sh b/ci/build-linux-agent.sh index 5922e8c..d79f7ee 100644 --- a/ci/build-linux-agent.sh +++ b/ci/build-linux-agent.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash # One native build/package path for glibc and musl, used by development and tags. set -euo pipefail -rid="${1:?usage: build-linux-agent.sh }" +rid="${1:?usage: build-linux-agent.sh }" case "$rid" in linux-x64|linux-arm64) static_msquic=false ;; - linux-musl-x64|linux-musl-arm64) static_msquic=true ;; + linux-musl-arm64) static_msquic=true ;; *) echo "unsupported Linux RID: $rid" >&2; exit 2 ;; esac root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -35,14 +35,20 @@ trap cleanup EXIT context="$work/context" # Serialize toolchain import/build; the native jobs use its resolved immutable ID. exec 7> "$base_home/.toolchain.lock" +echo "[$rid] waiting for the shared NativeAOT toolchain lock" flock -w 1800 7 +echo "[$rid] acquired the toolchain lock; checking the local cache" builder_image="localhost/moshiro-dotnet10-nativeaot-aotanywhere:ci" source "$root/ci/nativeaot-toolchain.sh" moshiro_nativeaot_settings "$root" builder_cache="${MOSHIRO_RUNNER_IMAGE_CACHE:-/home/justme/woodpecker/cache/runner-image}" cached_key="$(cat "$builder_cache/nativeaot-builder.key" 2>/dev/null || true)" builder_id="$(cat "$builder_cache/nativeaot-builder.id" 2>/dev/null || true)" -if [[ "$cached_key" == "$MOSHIRO_NATIVEAOT_BUILDER_KEY" && "$builder_id" =~ ^[0-9a-f]{64}$ && -f "$builder_cache/nativeaot-builder-$builder_id.oci" ]]; then +local_key="$(cat "$base_home/nativeaot-builder.key" 2>/dev/null || true)" +local_builder_id="$(buildah images --noheading --no-trunc --format '{{.ID}}' "$builder_image" 2>/dev/null || true)" +if [[ "$local_key" == "$MOSHIRO_NATIVEAOT_BUILDER_KEY" && "${local_builder_id#sha256:}" =~ ^[0-9a-f]{64}$ ]]; then + echo "[$rid] reusing the toolchain already built in this store" +elif [[ "$cached_key" == "$MOSHIRO_NATIVEAOT_BUILDER_KEY" && "$builder_id" =~ ^[0-9a-f]{64}$ && -f "$builder_cache/nativeaot-builder-$builder_id.oci" ]]; then local_builder_id="$(buildah images --noheading --no-trunc --format '{{.ID}}' "$builder_image" 2>/dev/null || true)" if [[ "${local_builder_id#sha256:}" != "$builder_id" ]]; then python3 "$root/ci/run-with-timeout.py" 600 buildah pull "oci-archive:$builder_cache/nativeaot-builder-$builder_id.oci" @@ -53,13 +59,17 @@ else builder_context="$work/nativeaot-builder" mkdir -p "$builder_context" tr -d '\r' < "$root/global.json" > "$builder_context/global.json" - python3 "$root/ci/run-with-timeout.py" 1800 buildah bud --layers --network host --tls-verify=true --arch amd64 \ + touch -t 200001010000 "$builder_context/global.json" + echo "[$rid] preparing the shared toolchain (other targets wait for this build)" + python3 "$root/ci/run-with-timeout.py" 1800 buildah bud --layers --force-rm --network host --tls-verify=true --arch amd64 \ --build-arg "AOTANYWHERE_VERSION=$MOSHIRO_AOTANYWHERE_VERSION" \ --build-arg "ZIG_TOOLSET_VERSION=$MOSHIRO_ZIG_TOOLSET_VERSION" \ -f "$root/packaging/container/NativeAot.Builder.Containerfile" \ -t "$builder_image" "$builder_context" fi builder_image="$(buildah images --noheading --no-trunc --format '{{.ID}}' "$builder_image")" +printf '%s\n' "$MOSHIRO_NATIVEAOT_BUILDER_KEY" > "$base_home/nativeaot-builder.key" +echo "[$rid] toolchain ready: $builder_image; starting the native build" flock -u 7 exec 7>&- diff --git a/ci/cleanup-build-cache.sh b/ci/cleanup-build-cache.sh index 4e37c8a..d5cec18 100644 --- a/ci/cleanup-build-cache.sh +++ b/ci/cleanup-build-cache.sh @@ -2,6 +2,10 @@ # Caller holds its store lock. Keep recent image layers and persistent RUN caches. set -euo pipefail : "${CONTAINERS_STORAGE_CONF:?an explicit, locked CI Buildah store is required}" +# No build is active while the caller holds the exclusive store lock. Remove +# working containers left by canceled builds before considering old image layers. +buildah unshare buildah rm --all >/dev/null 2>&1 || true +buildah rmi localhost/moshiro-agent-binaries:linux-musl-x64 >/dev/null 2>&1 || true # prune also deletes RUN cache mounts; remove only expired intermediate layers and versioned application images. mapfile -t expired < <(buildah images --all --no-trunc --format '{{.ID}} {{.Created}} {{.Name}}:{{.Tag}}' | python3 -c ' import re,sys,time diff --git a/ci/package-linux-agent.sh b/ci/package-linux-agent.sh index 5289aed..348e179 100644 --- a/ci/package-linux-agent.sh +++ b/ci/package-linux-agent.sh @@ -6,7 +6,6 @@ binaries="${2:?}"; destination="${3:?}" case "$rid" in linux-x64) suffix=amd64 ;; linux-arm64) suffix=arm64 ;; - linux-musl-x64) suffix=musl-amd64 ;; linux-musl-arm64) suffix=musl-arm64 ;; *) echo "unsupported Linux RID: $rid" >&2; exit 2 ;; esac diff --git a/ci/publish-gitea-release.sh b/ci/publish-gitea-release.sh index d1194a6..4d4141d 100644 --- a/ci/publish-gitea-release.sh +++ b/ci/publish-gitea-release.sh @@ -29,7 +29,7 @@ trap 'rm -rf "$tmp"' EXIT root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" install_sh="$(sha256sum "$root_dir/packaging/install/install.sh" | cut -d" " -f1)" install_ps1="$(sha256sum "$root_dir/packaging/install/install.ps1" | cut -d" " -f1)" -release_body="Install script SHA-256 (compare with the panel before you run an install command): install.sh $install_sh, install.ps1 $install_ps1. Agent builds: Linux glibc and musl (x64, arm64), macOS (x64, arm64), and Windows (x64, arm64). Linux musl Agent OCI image: registry.oeo.one/moshiro/moshiro-agent:$version (linux/amd64 and linux/arm64). Backend OCI image: registry.oeo.one/moshiro/moshiro-backend:$version (linux/amd64 and linux/arm64). Compose files for Agent and Backend: moshiro-compose-.yaml (images use :latest, or pin MOSHIRO_VERSION in .env). macOS binaries are not Developer ID signed or notarized." +release_body="Install script SHA-256 (compare with the panel before you run an install command): install.sh $install_sh, install.ps1 $install_ps1. Agent builds: Linux glibc (x64, arm64) and musl (arm64), macOS (x64, arm64), and Windows (x64, arm64). Linux Agent OCI image: registry.oeo.one/moshiro/moshiro-agent:$version (linux/amd64 and linux/arm64). Backend OCI image: registry.oeo.one/moshiro/moshiro-backend:$version (linux/amd64 and linux/arm64). Compose files for Agent and Backend: moshiro-compose-.yaml (images use :latest, or pin MOSHIRO_VERSION in .env). macOS binaries are not Developer ID signed or notarized." payload="$(printf '{"tag_name":"%s","target_commitish":"%s","name":"MoShiro %s","body":"%s","draft":false,"prerelease":false}' \ "$tag" "${CI_COMMIT_SHA:-}" "$tag" "$release_body")" release_status="$(curl --silent --show-error --retry 3 --netrc-file "$netrc" \ @@ -65,7 +65,6 @@ curl --fail-with-body --silent --show-error --retry 3 --netrc-file "$netrc" \ assets=( "moshiro-agent-v$version-linux-amd64.tar.gz" "moshiro-agent-v$version-linux-arm64.tar.gz" - "moshiro-agent-v$version-linux-musl-amd64.tar.gz" "moshiro-agent-v$version-linux-musl-arm64.tar.gz" "moshiro-agent-v$version-macos-x64.tar.gz" "moshiro-agent-v$version-macos-arm64.tar.gz" diff --git a/ci/publish-openwrt.sh b/ci/publish-openwrt.sh index 80dfeb0..8427926 100644 --- a/ci/publish-openwrt.sh +++ b/ci/publish-openwrt.sh @@ -5,7 +5,7 @@ set -euo pipefail channel="${1:?usage: publish-openwrt.sh }" rid="${2:?}"; source_dir="${3:?}" [[ "$channel" == dev || "$channel" == release ]] || exit 2 -[[ "$rid" == linux-musl-arm64 || "$rid" == linux-musl-x64 ]] || exit 2 +[[ "$rid" == linux-musl-arm64 ]] || exit 2 if [[ "$channel" == release ]]; then [[ "${CI_COMMIT_TAG:-}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo 'release feed requires a stable version tag' >&2; exit 2; } else diff --git a/ci/release-cross-platform.ps1 b/ci/release-cross-platform.ps1 index 9fbc1c3..dd3cc3e 100644 --- a/ci/release-cross-platform.ps1 +++ b/ci/release-cross-platform.ps1 @@ -2,7 +2,7 @@ param( [Parameter(Mandatory)] [ValidateSet( 'linux-x64', 'linux-arm64', - 'linux-musl-x64', 'linux-musl-arm64', + 'linux-musl-arm64', 'osx-x64', 'osx-arm64', 'win-x64', 'win-arm64' )] [string]$RuntimeIdentifier, diff --git a/ci/release-node-images.sh b/ci/release-node-images.sh index ed7aae4..519591c 100644 --- a/ci/release-node-images.sh +++ b/ci/release-node-images.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Publish musl OCI images for the Linux Agent on amd64 and arm64. +# Publish the Agent's glibc amd64 and musl arm64 OCI images. set -euo pipefail tag="${CI_COMMIT_TAG:-}" @@ -64,22 +64,29 @@ build_arch_image() { local component="$1" arch="$2" suffix="$3" local filename="moshiro-$component-v$version-linux-$suffix.tar.gz" local context="$work/$component-$arch" + local extracted="$work/extracted-$component-$arch" local archive="$work/$filename" local containerfile="$root/packaging/container/${component^}.Containerfile" local image="$image_root/moshiro-$component" local local_image="localhost/moshiro-$component:ci-$channel-$arch" + local runtime_base=mcr.microsoft.com/dotnet/runtime-deps:10.0 + [[ "$suffix" != musl-* ]] || runtime_base=mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine curl --fail-with-body --silent --show-error --retry 3 --netrc-file "$netrc" \ "$gitea_base/api/packages/justme/generic/moshiro-node/$version/$filename" -o "$archive" mkdir -p "$context/app" "$context/config" - tar -xzf "$archive" -C "$work" - install -m 0755 "$work/fleet-enroll" "$context/app/fleet-enroll" - install -m 0755 "$work/moshiro-$component" "$context/app/moshiro-$component" + mkdir -p "$extracted" + tar -xzf "$archive" -C "$extracted" + install -m 0755 "$extracted/fleet-enroll" "$context/app/fleet-enroll" + install -m 0755 "$extracted/moshiro-$component" "$context/app/moshiro-$component" + for library in "$extracted"/lib*.so.*; do + [[ ! -f "$library" ]] || install -m 0644 "$library" "$context/app/" + done : > "$context/config/.keep" install -m 0755 "$root/packaging/container/entrypoint.sh" "$context/entrypoint.sh" buildah bud --tls-verify=true --arch "$arch" \ - --build-arg "APP_VERSION=$version" \ + --build-arg "APP_VERSION=$version" --build-arg "RUNTIME_BASE=$runtime_base" \ -f "$containerfile" -t "$local_image" "$context" >/dev/null local pushed=false @@ -116,7 +123,7 @@ publish_index() { } for component in agent; do - build_arch_image "$component" amd64 musl-amd64 + build_arch_image "$component" amd64 amd64 build_arch_image "$component" arm64 musl-arm64 publish_index "$component" # 发布清单:给多架构镜像的 digest 签名(私钥只在 CI 密钥里),放进镜像仓库(<仓库>-manifest:release),面板在线时再登记一份加速。 diff --git a/ci/stage-node-image.py b/ci/stage-node-image.py index 84b1d70..71443ee 100644 --- a/ci/stage-node-image.py +++ b/ci/stage-node-image.py @@ -134,7 +134,7 @@ def normalize_mtimes(directory: Path) -> None: def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("component", choices=tuple(CONTAINERFILES)) - parser.add_argument("runtime_identifier", choices=("linux-x64", "linux-arm64", "linux-musl-x64", "linux-musl-arm64")) + parser.add_argument("runtime_identifier", choices=("linux-x64", "linux-arm64", "linux-musl-arm64")) parser.add_argument("destination", type=Path) args = parser.parse_args() stage(args.component, args.runtime_identifier, args.destination) diff --git a/docs/dev/project-layout.md b/docs/dev/project-layout.md index ffc0e50..4554557 100644 --- a/docs/dev/project-layout.md +++ b/docs/dev/project-layout.md @@ -232,12 +232,12 @@ Backend 允许的包:ASP.NET Core 共享框架、`Microsoft.Data.Sqlite`、`Mi | --- | --- | --- | | Agent | `linux-x64`、`linux-arm64` | VPS | | 拨号方 | `linux-x64`、`linux-arm64`、`win-x64` | 普通 PC 或服务器 | -| 拨号方(OpenWrt) | `linux-musl-x64`、`linux-musl-arm64` | 正式支持;`linux-musl-arm`(armv7)视工具链验证结果决定是否支持 | +| 拨号方(OpenWrt) | `linux-musl-arm64` | 正式支持;`linux-musl-arm`(armv7)视工具链验证结果决定是否支持 | | fleet-enroll | 与所在节点相同 | | | Backend | `linux-x64`、`linux-arm64`、`win-x64` | | - MIPS 不支持(NativeAOT 没有 MIPS 目标)。使用 MIPS 路由器(例如 MT7621)时,把 拨号方 运行在局域网内的其他设备上,比如 NAS、树莓派或 PC,测量结果同样代表这条家宽。 -- NativeAOT 需要目标平台的原生工具链:musl 目标在 Alpine 容器中构建,arm64 使用交叉编译镜像。CI 矩阵为 Windows、Linux glibc、Linux musl。 +- NativeAOT 需要目标平台的原生工具链:Linux amd64/arm64 发布 glibc,OpenWrt ARM64 发布静态 musl;arm64 使用交叉编译镜像。CI 矩阵为 Windows、macOS、Linux glibc amd64/arm64 和 Linux musl arm64。 - `scripts/publish.sh ` 输出到 `backend/artifacts/publish///`。 - 容器镜像由 CI 构建并对镜像 digest 签名,节点的宿主机更新器验签后更新,见 [deployment.md](../ops/deployment.md);纯二进制节点的更新见 [路线图](../roadmap.md)。 - Linux 上 TLS 依赖系统 OpenSSL(`libssl.so.3`)。 diff --git a/docs/ops/deployment.md b/docs/ops/deployment.md index ef674ff..dc3531a 100644 --- a/docs/ops/deployment.md +++ b/docs/ops/deployment.md @@ -1,6 +1,6 @@ # 部署 -节点(Agent)使用 NativeAOT。Linux 的开发版和正式版共用 `ci/build-linux-agent.sh`:musl amd64/arm64 静态链接 MsQuic、OpenSSL 和 .NET 加密 shim;glibc amd64/arm64 附带 MsQuic、NUMA、zstd 和 zlib 共享库,主机仍需提供 OpenSSL 3 和对应 libc。Agent OCI 镜像以 Alpine 为基底,直接使用同版本的 musl 原生包。OpenWrt 更新包也使用这份二进制,安装与签名清单沿用设备端现有协议。 +节点(Agent)使用 NativeAOT。Linux 的开发版和正式版共用 `ci/build-linux-agent.sh`:musl arm64 静态链接 MsQuic、OpenSSL 和 .NET 加密 shim;glibc amd64/arm64 附带 MsQuic、NUMA、zstd 和 zlib 共享库,主机仍需提供 OpenSSL 3 和对应 libc。Agent OCI 镜像复用同版本的原生包:amd64 使用 glibc 与 Debian 基底,arm64 使用静态 musl 与 Alpine 基底。OpenWrt 更新包也使用这份二进制,安装与签名清单沿用设备端现有协议。 ## 1. 构建与打包 @@ -42,13 +42,13 @@ scripts/package.sh → backend/artifacts | rid | 目标 | 构建方式 | | --- | --- | --- | | `linux-x64`、`linux-arm64` | Debian/Ubuntu glibc 服务器 | Linux amd64 runner 通过 AotAnywhere 交叉编译 | -| `linux-musl-x64`、`linux-musl-arm64` | Alpine、amd64 OpenWrt、aarch64 OpenWrt 路由器 | Linux amd64 runner 通过 AotAnywhere 交叉编译 | +| `linux-musl-arm64` | ARM64 Alpine、aarch64 OpenWrt 路由器 | Linux amd64 runner 通过 AotAnywhere 交叉编译 | | `osx-x64`、`osx-arm64` | Intel Mac、Apple Silicon | Windows CI 主机通过 AotAnywhere 交叉编译;发布包未做 Developer ID 签名或 Apple 公证 | | `win-x64`、`win-arm64` | Windows x64、ARM64 | Windows CI 主机上的 Visual Studio C++ 工具链 | macOS 的 Apple Silicon 产物带临时 ad-hoc 签名,Intel 产物未签名。通过浏览器下载时,macOS Gatekeeper 可能阻止启动;面向普通用户分发前需要 Developer ID 签名和 Apple 公证。MIPS 不在当前目标列表中(见 `project-layout.md`)。 -仓库内的 `scripts/publish.sh` 面向本机目标平台;Woodpecker 的开发版和 tag 流水线在 Linux runner 上构建 Agent 与 `fleet-enroll` 的四个 Linux RID;Windows runner 负责 macOS 和 Windows。Backend OCI 镜像在 Linux runner 上构建。 +仓库内的 `scripts/publish.sh` 面向本机目标平台;Woodpecker 的开发版和 tag 流水线在 Linux runner 上构建 Agent 与 `fleet-enroll` 的三个 Linux RID;Windows runner 负责 macOS 和 Windows。Backend OCI 镜像在 Linux runner 上构建。 ## Backend 容器 @@ -84,25 +84,25 @@ Backend 的 NativeAOT 发布默认启用 `SqliteStaticLink`,支持 Linux glibc ## 节点二进制发布 -Woodpecker 在 `vMAJOR.MINOR.PATCH` tag 上,由 Windows runner 使用 AotAnywhere 并行构建 Agent 的 Linux glibc、Linux musl、macOS 和 Windows 产物。Agent 包还包含一次性注册工具 `fleet-enroll`。Linux 归档带有安装脚本和 systemd/OpenWrt 单元;Windows 产物为 `.zip`,Linux/macOS 产物为 `.tar.gz`。glibc sidecar 选用 Debian 12 兼容构建,musl sidecar 来自 Alpine v3.22。Linux musl 的 Agent 二进制还会打包为 OCI 镜像,基底为 `mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine`,支持 `linux/amd64` 和 `linux/arm64`;镜像直接带有归档中的 MsQuic、NUMA 和 LTTng 共享库,并通过 `LD_LIBRARY_PATH` 加载。Linux 原生库包按 RID 缓存在 CI runner 的 `.nuget/moshiro-quic-runtime`,索引最多缓存 24 小时,已下载的版本包会复用。所有平台产物和 OCI 镜像发布成功后,CI 会创建对应的 Gitea Release,并附上压缩包和 SHA-256 校验文件。macOS 产物目前未签名/公证。安装说明见 [Agent](../../backend/agent/README.md) 文档。 +Woodpecker 在 `vMAJOR.MINOR.PATCH` tag 上构建 Agent 的 Linux、macOS 和 Windows 产物。Linux amd64/arm64 使用 glibc,OpenWrt ARM64 使用静态 musl;Agent 包还包含一次性注册工具 `fleet-enroll`。Linux 归档带有安装脚本和 systemd/OpenWrt 单元;Windows 产物为 `.zip`,Linux/macOS 产物为 `.tar.gz`。glibc sidecar 选用 Debian 12 兼容构建,musl sidecar 来自 Alpine v3.22。Agent OCI 镜像使用 amd64 glibc/Debian 和 arm64 musl/Alpine,直接携带相应归档中的共享库,并通过 `LD_LIBRARY_PATH` 加载。Linux 原生库包按 RID 缓存在 CI runner 的 `.nuget/moshiro-quic-runtime`,索引最多缓存 24 小时,已下载的版本包会复用。所有平台产物和 OCI 镜像发布成功后,CI 会创建对应的 Gitea Release,并附上压缩包和 SHA-256 校验文件。macOS 产物目前未签名/公证。安装说明见 [Agent](../../backend/agent/README.md) 文档。 ## CI 工作流 ### Linux runner 的运行环境 -Linux workflow 使用 `ci=domain-linux-native`、`platform=linux/amd64` 标签和 Woodpecker `local` backend;步骤里的 `image: bash` 是执行程序,构建工具来自 runner 环境。测试、前端和 Backend 编译使用 .NET 10、Node/npm、Python、clang 和 Zig;Agent 的 Linux 原生包通过 Buildah 的 NativeAOT builder 编译,OCI 镜像从已发布的 musl 包组装。 +Linux workflow 使用 `ci=domain-linux-native`、`platform=linux/amd64` 标签和 Woodpecker `local` backend;步骤里的 `image: bash` 是执行程序,构建工具来自 runner 环境。测试、前端和 Backend 编译使用 .NET 10、Node/npm、Python、clang 和 Zig;Agent 的 Linux 原生包通过 Buildah 的 NativeAOT builder 编译,OCI 镜像从已发布的 glibc amd64、musl arm64 包组装。 -WSL 的 agent 通过 rootless Podman 运行,systemd 模板见 `ci/woodpecker-agent-podman.service`。工具全部在 `packaging/container/Woodpecker.Agent.Containerfile` 中安装,包括 .NET 10 SDK、Node/npm、Buildah、netavark/aardvark-dns、Git、Python、clang、Woodpecker agent/clone 插件和测试用 MsQuic 库;AotAnywhere SDK、Zig 与四个 Linux RID 的 NativeAOT 包由 `NativeAot.Builder.Containerfile` 预热。容器 PID 上限是 8192,避免多个并行 AotAnywhere/Zig 链接时撞到默认 2048 的限制。容器只把 `/home/justme` 作为可写工作区和缓存,不挂载宿主工具目录。连接信息与 7897 代理配置放在宿主 `/home/justme/woodpecker/agent-podman.env`,不要提交密钥。容器允许 `unshare`、提供 `SYS_ADMIN` 和 `/dev/fuse`,以便 Buildah 挂载镜像层;不要通过 `chroot /host` 启动 agent,因为 Linux 会拒绝 chroot 进程创建新的用户命名空间。 +WSL 的 agent 通过 rootless Podman 运行,systemd 模板见 `ci/woodpecker-agent-podman.service`。工具全部在 `packaging/container/Woodpecker.Agent.Containerfile` 中安装,包括 .NET 10 SDK、Node/npm、Buildah、netavark/aardvark-dns、Git、Python、clang、Woodpecker agent/clone 插件和测试用 MsQuic 库;AotAnywhere SDK、Zig 与三个 Linux RID 的 NativeAOT 包由 `NativeAot.Builder.Containerfile` 预热。容器 PID 上限是 8192,避免多个并行 AotAnywhere/Zig 链接时撞到默认 2048 的限制。容器只把 `/home/justme` 作为可写工作区和缓存,不挂载宿主工具目录。连接信息与 7897 代理配置放在宿主 `/home/justme/woodpecker/agent-podman.env`,不要提交密钥。容器允许 `unshare`、提供 `SYS_ADMIN` 和 `/dev/fuse`,以便 Buildah 挂载镜像层;不要通过 `chroot /host` 启动 agent,因为 Linux 会拒绝 chroot 进程创建新的用户命名空间。 以 WSL 的 `justme` 用户运行 `bash ci/build-runner-image.sh` 构建工具镜像,验证后安装 systemd 模板并重启 agent。该脚本使用 Podman 的持久镜像层缓存和 `--pull=missing`,服务启动使用 `--pull=never`;流水线和服务重启都不会执行工具安装。NativeAOT builder 只在 SDK/工具版本或 Containerfile 改变时更新,不受每次开发构建的版本号影响。它按镜像 ID 导出到 `woodpecker/cache/runner-image`,Linux Agent 的共享 Buildah store 从本地归档导入一次,之后直接复用镜像。只有所需归档不存在或工具配置改变时,构建脚本才回退到 Containerfile 构建。应用的 NuGet restore 缓存独立持久化在 `woodpecker/cache/nuget/packages`,启动时从镜像复制已有工具包作为初始缓存。 -Agent 构建按 `build-musl-amd64`、`build-musl-arm64`、`build-glibc-amd64`、`build-glibc-arm64` 拆分。`ci/stage-node-image.py` 只把 Agent、`fleet-enroll` 的依赖工程和父目录构建配置放进还原层,文件时间固定;C# 源码进入后续编译层。四个 Linux RID 共用一个 Buildah store、工具链镜像和 `moshiro-nuget` 缓存挂载,首次从工具镜像的包目录初始化。restore 独占写入,后续编译和发布只读;镜像层命中时跳过 restore,依赖变化时仍复用已下载包。 +Agent 构建按 `build-musl-arm64`、`build-glibc-amd64`、`build-glibc-arm64` 拆分。`ci/stage-node-image.py` 只把 Agent、`fleet-enroll` 的依赖工程和父目录构建配置放进还原层,文件时间固定;C# 源码进入后续编译层。三个 Linux RID 共用一个 Buildah store、工具链镜像和 `moshiro-nuget` 缓存挂载,首次从工具镜像的包目录初始化。restore 独占写入,后续编译和发布只读;镜像层命中时跳过 restore,依赖变化时仍复用已下载包。 `Agent.Native.proj` 在一个 MSBuild 图内编译两份程序和公共依赖。随后两个独立 stage 用 `publish --no-build --no-restore` 完成 NativeAOT,Buildah `--jobs 2` 并行执行;中间目录随 stage 隔离。最终 scratch stage 只保留两份可执行文件,`ci/extract-node-binaries.sh` 在同一个 `buildah unshare` 会话内挂载、复制和卸载。`ci/package-linux-agent.sh` 生成原生安装归档,`ci/release-node-images.sh` 复用已发布归档生成 OCI 镜像。 任务结束时清理本次上下文、导出目录和临时包,失败退出也清理临时容器。构建持有共享 store 锁和当前 RID 的锁;清理需要独占 store 锁;`ci/cleanup-build-cache.sh` 清理超过 7 天的中间镜像与旧版本应用镜像,保留具名工具镜像和当前构建镜像,删除时不强制移除仍被引用的层。已退役的 Probe、独立 Agent 架构及索引 store,在存储元数据连续 7 天未更新且可取得独占锁时整目录清理;近期使用、锁定或没有可识别元数据的目录保留。NuGet 缓存挂载保留,不使用会同时删除挂载缓存的 `buildah prune`。`ci/build-runner-image.sh` 在持锁期间保留当前工具链和最近的上一份 OCI 导出归档。 -所有镜像构建、索引及签名清单发布脚本通过 `ci/buildah-env.sh` 显式配置 Buildah 的 `graphroot` 和 `runroot`。Agent 原生构建的四个 RID 共用 `buildah-home/linux-agent`,Backend 和 OCI 发布使用各自的 store,运行状态留在当前 runner 的临时目录;容器内 UID 0 不再使各步骤落到同一个 `/var/lib/containers/storage`。`RUN` 默认使用 `chroot` isolation,避免额外的 OCI runtime/cgroup 嵌套;这与上面禁止的 agent 启动方式是不同阶段。 +所有镜像构建、索引及签名清单发布脚本通过 `ci/buildah-env.sh` 显式配置 Buildah 的 `graphroot` 和 `runroot`。Agent 原生构建的三个 RID 共用 `buildah-home/linux-agent`,Backend 和 OCI 发布使用各自的 store,运行状态留在当前 runner 的临时目录;容器内 UID 0 不再使各步骤落到同一个 `/var/lib/containers/storage`。`RUN` 默认使用 `chroot` isolation,避免额外的 OCI runtime/cgroup 嵌套;这与上面禁止的 agent 启动方式是不同阶段。 WSL 的 systemd 服务不会单独阻止发行版空闲退出。Windows 登录用户运行 `powershell -ExecutionPolicy Bypass -File ci/start-wsl-runner.ps1`,注册并启动隐藏的登录任务,持续运行 Debian 的前台 `sleep infinity`,让 CI server/agent 保持在线。任务使用当前用户的普通权限;需要暂停 WSL 时先停止任务 `MoShiro WSL runner (Debian)`。agent 单元等待 `user@1000.service` 并设置用户 D-Bus 地址,让 rootless Podman 使用已就绪的用户会话。 @@ -138,7 +138,7 @@ Pull request 和 `main` 的 push 会跑三个 workflow: | Backend | `registry.oeo.one/moshiro-dev/moshiro-backend:latest` | `registry.oeo.one/moshiro-dev/moshiro-backend:<版本>`(如 `0.4.1-dev.12-044ce0c`) | | Agent | `registry.oeo.one/moshiro-dev/moshiro-agent:latest` | `registry.oeo.one/moshiro-dev/moshiro-agent:<版本>` | -Agent 也发布架构固定标签 `:<版本>-amd64` 与 `:<版本>-arm64`。原生构建在 amd64 的 `sdk:10.0-noble-aot` 衍生 builder 中使用 AotAnywhere 编译四个 Linux RID,ARM64 无需 QEMU;builder 预热项目固定版本的 SDK、Zig 和运行时包,NuGet 使用共享的持久缓存挂载,restore 独占写入、编译与发布只读。运行基底为 `runtime-deps:10.0-alpine`。开发镜像不创建 Gitea Release,也不修改正式版 `moshiro/` 仓库中的标签。普通 main push 不会构建镜像。节点和 Backend 判断自己是不是开发构建,看构建信息里的 tag 含 `-dev.`(旧的 `dev-`、`main-` 也认)。 +Agent 也发布架构固定标签 `:<版本>-amd64` 与 `:<版本>-arm64`。原生构建在 amd64 的 `sdk:10.0-noble-aot` 衍生 builder 中使用 AotAnywhere 编译三个 Linux RID,ARM64 无需 QEMU;builder 预热项目固定版本的 SDK、Zig 和运行时包,NuGet 使用共享的持久缓存挂载,restore 独占写入、编译与发布只读。amd64 镜像基底为 `runtime-deps:10.0`,arm64 为 `runtime-deps:10.0-alpine`。开发镜像不创建 Gitea Release,也不修改正式版 `moshiro/` 仓库中的标签。普通 main push 不会构建镜像。节点和 Backend 判断自己是不是开发构建,看构建信息里的 tag 含 `-dev.`(旧的 `dev-`、`main-` 也认)。 ### 正式版发布 @@ -192,7 +192,7 @@ Linux 产物比 Windows 大,是因为 .NET 的加密、压缩、网络本机 #### 原生一键安装与自动更新 -管理端「添加 拨号方」提供 OpenWrt · 原生二进制 命令,以 root 执行。支持 ARM64/aarch64 和 x86_64,不需要容器或 .NET;自动安装依赖、验签下载、固定属主并注册、配置 procd。身份在 `/etc/moshiro/`,更新策略在 `/etc/moshiro-native.conf`(`CHANNEL=dev|release`、`POLICY=auto|manual`、`INTERVAL=30`)。动态 musl 正式包仍需 `libopenssl3`,实际设备兼容性需实机验证。 +管理端「添加 拨号方」提供 OpenWrt · 原生二进制 命令,以 root 执行。支持 ARM64/aarch64,不需要容器或 .NET;自动安装依赖、验签下载、固定属主并注册、配置 procd。身份在 `/etc/moshiro/`,更新策略在 `/etc/moshiro-native.conf`(`CHANNEL=dev|release`、`POLICY=auto|manual`、`INTERVAL=30`)。原生包使用静态 musl 可执行文件,实际设备兼容性需实机验证。 带 `[CI]` 的 main push 发布 dev,稳定 tag 发布 release,沿用 `gitea_token` 和 `release_signing_key`;Gitea generic package `moshiro-openwrt` 需允许匿名下载。制品发布后,在线节点收到通知就唤醒更新器,另有 30 秒轮询兜底,不等待 10 分钟。更新器验签、防重放、原子切换完整版本,健康检查失败回滚;这些步骤由独立的更新器完成,不在 拨号方 进程内执行。 diff --git a/packaging/container/Agent.Containerfile b/packaging/container/Agent.Containerfile index 8ddbc36..39fd09c 100644 --- a/packaging/container/Agent.Containerfile +++ b/packaging/container/Agent.Containerfile @@ -1,4 +1,5 @@ -FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-alpine +ARG RUNTIME_BASE=mcr.microsoft.com/dotnet/runtime-deps:10.0 +FROM ${RUNTIME_BASE} ARG APP_VERSION=unknown LABEL org.opencontainers.image.title="MoShiro Agent" \ @@ -6,8 +7,7 @@ LABEL org.opencontainers.image.title="MoShiro Agent" \ org.opencontainers.image.licenses="Apache-2.0" \ org.opencontainers.image.version="$APP_VERSION" -COPY --chown=1654:1654 app/moshiro-agent /usr/local/bin/moshiro-agent -COPY --chown=1654:1654 app/fleet-enroll /usr/local/bin/fleet-enroll +COPY --chown=1654:1654 app/ /usr/local/bin/ COPY --chown=1654:1654 config/ /etc/moshiro/ COPY --chown=1654:1654 --chmod=0755 entrypoint.sh /usr/local/bin/moshiro-entrypoint diff --git a/packaging/container/Agent.NativeBuild.Containerfile b/packaging/container/Agent.NativeBuild.Containerfile index ca7db0d..c149cdf 100644 --- a/packaging/container/Agent.NativeBuild.Containerfile +++ b/packaging/container/Agent.NativeBuild.Containerfile @@ -4,7 +4,7 @@ FROM --platform=linux/amd64 ${NATIVEAOT_BUILDER_IMAGE} AS toolchain FROM toolchain AS restore WORKDIR /src/backend COPY restore/backend/ ./ -ARG NODE_RID=linux-musl-x64 +ARG NODE_RID=linux-musl-arm64 ARG STATIC_MSQUIC=true ENV NUGET_PACKAGES=/root/.nuget/packages # Layer hits skip restore. A dependency change still reuses downloaded packages. diff --git a/packaging/container/NativeAot.Builder.Containerfile b/packaging/container/NativeAot.Builder.Containerfile index c7e3dd2..3a2bb4f 100644 --- a/packaging/container/NativeAot.Builder.Containerfile +++ b/packaging/container/NativeAot.Builder.Containerfile @@ -28,7 +28,6 @@ RUN printf '%s\n' \ " " \ ' ' \ '' > Warmup.csproj \ - && dotnet restore Warmup.csproj -p:RuntimeFrameworkVersion=10.0.12 -p:PublishAot=true -p:UseAotAnywhere=true -r linux-musl-x64 \ && dotnet restore Warmup.csproj -p:RuntimeFrameworkVersion=10.0.12 -p:PublishAot=true -p:UseAotAnywhere=true -r linux-musl-arm64 \ && dotnet restore Warmup.csproj -p:RuntimeFrameworkVersion=10.0.12 -p:PublishAot=true -p:UseAotAnywhere=true -r linux-x64 \ && dotnet restore Warmup.csproj -p:RuntimeFrameworkVersion=10.0.12 -p:PublishAot=true -p:UseAotAnywhere=true -r linux-arm64 \ diff --git a/packaging/openwrt/install.sh b/packaging/openwrt/install.sh index cb57284..ce8b9d4 100644 --- a/packaging/openwrt/install.sh +++ b/packaging/openwrt/install.sh @@ -84,7 +84,7 @@ load_config() { . "$CONF" case "$CHANNEL" in release|dev) ;; *) die "invalid channel" ;; esac case "$POLICY" in auto|manual) ;; *) die "invalid policy" ;; esac - case "$RID" in linux-musl-arm64|linux-musl-x64) ;; *) die "unsupported architecture" ;; esac + case "$RID" in linux-musl-arm64) ;; *) die "unsupported architecture" ;; esac case "$FEED" in https://*) ;; *) die "feed must use HTTPS" ;; esac case "$FEED" in *[!A-Za-z0-9:/._-]*) die "invalid feed URL" ;; esac case "$INTERVAL" in ''|*[!0-9]*) die "invalid interval" ;; esac @@ -270,7 +270,7 @@ install() { load_config restoring=true fi - case "$(uname -m)" in aarch64|arm64) detected_rid=linux-musl-arm64 ;; x86_64) detected_rid=linux-musl-x64 ;; *) die "only ARM64 and x86_64 are supported" ;; esac + case "$(uname -m)" in aarch64|arm64) detected_rid=linux-musl-arm64 ;; *) die "only ARM64 is supported" ;; esac if [ "$restoring" = true ]; then [ "$RID" = "$detected_rid" ] || die "saved updater configuration belongs to a different architecture" fi diff --git a/scripts/publish.sh b/scripts/publish.sh index 39c88f6..6b76103 100644 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # 发布一个节点程序:scripts/publish.sh -# 输出到 backend/artifacts/publish///。linux-musl-x64(Alpine)和 linux-x64(Ubuntu noble,glibc)在 NativeAOT SDK 容器里构建(任何主机都可以,包括 Windows), +# 输出到 backend/artifacts/publish///。linux-x64(Ubuntu noble,glibc)在 NativeAOT SDK 容器里构建(任何主机都可以,包括 Windows), # 其他 rid 用本机的 dotnet(NativeAOT 不能交叉编译到别的操作系统,所以 win-x64 要在 Windows 上、linux-x64 要在 glibc 的 Linux 上构建)。 set -euo pipefail @@ -29,11 +29,6 @@ fi args=(publish "$csproj" -c Release -r "$rid" -p:PublishAot=true -p:DebugType=none -p:DebugSymbols=false "${publish_options[@]}" -o "/src/$out" --artifacts-path /artifacts -nologo -v q) case "$rid" in - linux-musl-x64) - # SDK 镜像的版本可以比 global.json 新(rollForward = latestFeature) - docker run --rm "${build_env[@]}" -v "$root:/src" -v moshiro-nuget:/root/.nuget -v "moshiro-artifacts-$rid:/artifacts" -w /src \ - mcr.microsoft.com/dotnet/sdk:10.0-alpine-aot dotnet "${args[@]}" - ;; linux-x64) docker run --rm "${build_env[@]}" -v "$root:/src" -v moshiro-nuget:/root/.nuget -v "moshiro-artifacts-$rid:/artifacts" -w /src mcr.microsoft.com/dotnet/sdk:10.0-noble-aot dotnet "${args[@]}" ;; diff --git a/scripts/test-linux-agent-build.py b/scripts/test-linux-agent-build.py index 8864806..9c59864 100644 --- a/scripts/test-linux-agent-build.py +++ b/scripts/test-linux-agent-build.py @@ -80,7 +80,7 @@ esac OLD_TIME=str(int(time.time()) - 8 * 86400), RECENT_TIME=str(int(time.time())), CONTAINERS_STORAGE_CONF=str(base / 'moshiro-storage.conf')) subprocess.run(['bash', str(ROOT / 'ci/cleanup-build-cache.sh')], env=env, check=True, capture_output=True) - self.assertEqual((base / 'removed').read_text(), 'rmi old\nrmi version\n') + self.assertEqual((base / 'removed').read_text(), 'rmi localhost/moshiro-agent-binaries:linux-musl-x64\nrmi old\nrmi version\n') def test_export_requires_one_unshare_session_and_cleans_up_on_copy_failure(self): with tempfile.TemporaryDirectory() as directory: @@ -122,7 +122,7 @@ esac self.assertEqual((base / 'output/moshiro-agent').read_bytes(), b'agent') self.assertEqual((base / 'output/fleet-enroll').read_bytes(), b'enroll') - def test_four_linux_packages_preserve_installers_and_shared_library_payloads(self): + def test_linux_packages_preserve_installers_and_shared_library_payloads(self): with tempfile.TemporaryDirectory() as directory: base = Path(directory) binaries = base / 'binaries' @@ -130,7 +130,7 @@ esac for name in ('moshiro-agent', 'fleet-enroll', 'libmsquic.so.2'): (binaries / name).write_bytes(name.encode()) for rid, suffix in [('linux-x64', 'amd64'), ('linux-arm64', 'arm64'), - ('linux-musl-x64', 'musl-amd64'), ('linux-musl-arm64', 'musl-arm64')]: + ('linux-musl-arm64', 'musl-arm64')]: with self.subTest(rid=rid): output = base / rid env = dict(os.environ, CI_COMMIT_TAG='v1.2.3') diff --git a/web/admin/src/pages/JoinTokenCreated.tsx b/web/admin/src/pages/JoinTokenCreated.tsx index 305c724..ecb8d67 100644 --- a/web/admin/src/pages/JoinTokenCreated.tsx +++ b/web/admin/src/pages/JoinTokenCreated.tsx @@ -34,7 +34,7 @@ export function JoinTokenCreated({ created, onClose }: { created: CreatedJoinTok {script.openwrt && (