From e749c2afaf0c5b32d5309a57627b38fbbb0ac808 Mon Sep 17 00:00:00 2001 From: mrlee <470349233@qq.com> Date: Fri, 2 Oct 2026 14:37:50 +0800 Subject: [PATCH] feat: bundle multi-arch offline images into fnOS package --- README.md | 76 ++++++++------------ build.ps1 | 96 +++++++++++++++++++++++++- havline/app/docker/docker-compose.yaml | 3 +- havline/cmd/install_callback | 5 ++ havline/cmd/offline_image.sh | 49 +++++++++++++ havline/cmd/upgrade_callback | 5 ++ havline/config/privilege | 3 +- havline/manifest | 2 +- 8 files changed, 186 insertions(+), 53 deletions(-) create mode 100644 havline/cmd/offline_image.sh diff --git a/README.md b/README.md index 313d782..b99d45f 100644 --- a/README.md +++ b/README.md @@ -19,66 +19,36 @@ 仍需完成: -- 修复 Gitea Registry 的 HTTP token realm,使 Docker 能正常 push/pull; -- 确认飞牛设备是否可以匿名拉取,或在安装前配置 Registry 登录; -- 在飞牛测试设备验证安装、启动、入口、升级和卸载; -- 验证 amd64 与 arm64 镜像均可在真实设备上拉取。 +- 在飞牛 x86_64 与 ARM 设备验证离线安装、启动、入口、升级和卸载; +- 验证应用用户加入 `docker` 组后的 `docker load`、`docker inspect` 权限。 -## 当前阻塞 +## 离线镜像包 -`jayhub.top/mrlee/havline:1.9.0` 已经包含 amd64 与 arm64 多架构索引, -但当前 Gitea 返回的认证地址仍是 HTTP: +当前应用已经切换为离线镜像包模式,不依赖外部 Docker Registry。 + +包内包含: ```text -WWW-Authenticate: Bearer realm="http://jayhub.top/v2/token", ... +target/docker/havline--amd64.tar.gz +target/docker/havline--arm64.tar.gz ``` -Docker 会在 HTTP 到 HTTPS 的跳转过程中丢失换取 token 所需的认证信息, -因此 `docker pull` 和 `docker push` 都会报: - -```text -failed to fetch oauth token: -authorization server did not include a token in the response -``` - -在 Gitea `ROOT_URL` 改成 `https://jayhub.top/` 并确认反向代理传递 -`X-Forwarded-Proto: https` 之前,飞牛设备无法稳定拉取该镜像。 -详细修复步骤见主程序仓库: - -```text -docs/gitea-registry-fix.md -``` - -## 镜像地址 - -当前应用使用: - -```text -jayhub.top/mrlee/havline:${TRIM_APPVER} -``` - -对应标签需要包含: - -```text -linux/amd64 -linux/arm64 -``` - -新地址已经使用 HTTPS,不需要再配置 `insecure-registries`。 - -验证镜像: +安装或升级回调会执行: ```bash -docker pull jayhub.top/mrlee/havline:1.9.0 +docker load -i target/docker/havline--.tar.gz ``` -如果仓库不是公开可拉取,先登录: +Compose 使用本地镜像: -```bash -docker login jayhub.top +```yaml +image: havline:${TRIM_APPVER} +pull_policy: never ``` -拉取成功后重新安装 `havline-1.9.0.fpk`。 +因此安装时不需要访问 `jayhub.top`,也不需要配置 Registry 登录。 + +当前 1.9.0 双架构 `.fpk` 约 `150.6 MB`。 ## 打包 @@ -95,6 +65,13 @@ cd F:\Fonu-main\fnos fnos/dist/havline-.fpk ``` +默认会同时导出 amd64 与 arm64 镜像归档。若本次只修改了打包元数据, +且已有对应版本的镜像归档,可以使用: + +```powershell +.\build.ps1 -SkipImageExport +``` + 如果 `fnpack` 已在 PATH 中,也可以传入路径: ```powershell @@ -110,7 +87,7 @@ appcenter-cli install-fpk havline.fpk 安装后检查: 1. 应用中心显示 Havline; -2. Docker 镜像可以拉取; +2. 安装回调成功加载离线镜像; 3. Havline 容器能够启动; 4. 桌面入口可以打开管理界面; 5. 数据写入 `${TRIM_PKGVAR}`; @@ -126,7 +103,8 @@ appcenter-cli install-fpk havline.fpk ## 首发边界 -- 不挂载 Docker Socket。 +- 容器本身不挂载 Docker Socket。 +- 生命周期脚本通过应用用户的 `docker` 附加组执行 `docker load` 和状态检查。 - 不启动 Havline 自带 updater。 - 不使用 host 网络。 - 不共享数据库、证书、密钥、Nginx、FRP 和 cloudflared 目录。 diff --git a/build.ps1 b/build.ps1 index b5b9bbb..6fa15f1 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,5 +1,6 @@ param( - [string]$FnpackPath = "" + [string]$FnpackPath = "", + [switch]$SkipImageExport ) $ErrorActionPreference = "Stop" @@ -7,6 +8,79 @@ $root = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path $project = Join-Path $root "fnos\havline" $version = (Get-Content -Raw (Join-Path $root "havline\VERSION")).Trim() +function Resolve-DockerCli { + if ($env:DOCKER_BIN) { return $env:DOCKER_BIN } + $command = Get-Command docker -ErrorAction SilentlyContinue + if ($command) { return $command.Source } + $default = "C:\Program Files\Docker\Docker\resources\bin\docker.exe" + if (Test-Path -LiteralPath $default) { return $default } + throw "找不到 docker 命令" +} + +function Export-OfflineImage { + param( + [string]$DockerBin, + [string]$BuildContext, + [string]$Platform, + [string]$Arch, + [string]$Version, + [string]$OutputDir + ) + + $tarName = "havline-$Version-$Arch.tar" + $tar = Join-Path $OutputDir $tarName + $gz = Join-Path $OutputDir "havline-$Version-$Arch.tar.gz" + + Push-Location $BuildContext + try { + & $DockerBin buildx build ` + --platform $Platform ` + --build-arg VERSION_FILE=VERSION ` + --provenance=false ` + --sbom=false ` + --output "type=docker,name=havline:$Version,dest=$tar" ` + . + if ($LASTEXITCODE -ne 0) { + throw "导出 $Arch 镜像失败,退出码:$LASTEXITCODE" + } + } + finally { + Pop-Location + } + + Compress-GzipFile -Source $tar -Destination $gz + + Remove-Item -LiteralPath $tar -Force + Write-Output "镜像归档:$gz" +} + +function Compress-GzipFile { + param( + [string]$Source, + [string]$Destination + ) + + $sourceStream = [System.IO.File]::OpenRead($Source) + try { + $targetStream = [System.IO.File]::Create($Destination) + try { + $gzip = New-Object System.IO.Compression.GZipStream($targetStream, [System.IO.Compression.CompressionLevel]::Optimal) + try { + $sourceStream.CopyTo($gzip) + } + finally { + $gzip.Dispose() + } + } + finally { + $targetStream.Dispose() + } + } + finally { + $sourceStream.Dispose() + } +} + if (-not (Test-Path -LiteralPath $project)) { throw "找不到飞牛应用工程:$project" } @@ -24,6 +98,24 @@ if ($LASTEXITCODE -ge 8) { throw "复制飞牛应用工程失败,robocopy 退出码:$LASTEXITCODE" } +$imageDir = Join-Path $stage "app\docker" +New-Item -ItemType Directory -Force $imageDir | Out-Null + +if (-not $SkipImageExport) { + $dockerBin = Resolve-DockerCli + $buildContext = Join-Path $root "havline" + Export-OfflineImage -DockerBin $dockerBin -BuildContext $buildContext -Platform "linux/amd64" -Arch "amd64" -Version $version -OutputDir $imageDir + Export-OfflineImage -DockerBin $dockerBin -BuildContext $buildContext -Platform "linux/arm64" -Arch "arm64" -Version $version -OutputDir $imageDir +} +else { + foreach ($arch in @("amd64", "arm64")) { + $required = Join-Path $imageDir "havline-$version-$arch.tar.gz" + if (-not (Test-Path -LiteralPath $required)) { + throw "缺少离线镜像:$required" + } + } +} + Push-Location $PSScriptRoot try { & $FnpackPath build -d $stage @@ -35,6 +127,8 @@ try { New-Item -ItemType Directory -Force $dist | Out-Null Copy-Item -LiteralPath $output -Destination (Join-Path $dist "havline-$version.fpk") -Force Write-Output "FPK: $(Join-Path $dist "havline-$version.fpk")" + $sizeMB = [math]::Round((Get-Item (Join-Path $dist "havline-$version.fpk")).Length / 1MB, 1) + Write-Output "FPK 大小:$sizeMB MB" } finally { Pop-Location diff --git a/havline/app/docker/docker-compose.yaml b/havline/app/docker/docker-compose.yaml index a3349c1..8ad4b0c 100644 --- a/havline/app/docker/docker-compose.yaml +++ b/havline/app/docker/docker-compose.yaml @@ -1,6 +1,7 @@ services: havline: - image: jayhub.top/mrlee/havline:${TRIM_APPVER}-amd64-docker + image: havline:${TRIM_APPVER} + pull_policy: never container_name: havline restart: unless-stopped ports: diff --git a/havline/cmd/install_callback b/havline/cmd/install_callback index e380a62..2e4531a 100755 --- a/havline/cmd/install_callback +++ b/havline/cmd/install_callback @@ -1,8 +1,13 @@ #!/bin/bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/offline_image.sh" + mkdir -p "$TRIM_PKGVAR" "$TRIM_PKGVAR/backups" chown -R "$TRIM_USERNAME:$TRIM_GROUPNAME" "$TRIM_PKGVAR" +load_offline_image + echo "Havline 数据目录已准备:$TRIM_PKGVAR" exit 0 diff --git a/havline/cmd/offline_image.sh b/havline/cmd/offline_image.sh new file mode 100644 index 0000000..f866a76 --- /dev/null +++ b/havline/cmd/offline_image.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -euo pipefail + +image_arch() { + case "$(uname -m)" in + x86_64|amd64) echo "amd64" ;; + aarch64|arm64) echo "arm64" ;; + *) return 1 ;; + esac +} + +image_path() { + local arch="$1" + local primary="$TRIM_APPDEST/docker/havline-${TRIM_APPVER}-${arch}.tar.gz" + + if [ -f "$primary" ]; then + echo "$primary" + return 0 + fi + + find "$TRIM_APPDEST" -name "havline-${TRIM_APPVER}-${arch}.tar.gz" 2>/dev/null | head -n 1 +} + +load_offline_image() { + local arch path + + arch="$(image_arch)" || { + echo "不支持的设备架构:$(uname -m)" > "$TRIM_TEMP_LOGFILE" + exit 1 + } + + path="$(image_path "$arch")" + if [ -z "$path" ] || [ ! -f "$path" ]; then + echo "找不到离线镜像:havline-${TRIM_APPVER}-${arch}.tar.gz" > "$TRIM_TEMP_LOGFILE" + exit 1 + fi + + if ! docker load -i "$path" >/dev/null 2>&1; then + echo "加载离线镜像失败:$path" > "$TRIM_TEMP_LOGFILE" + exit 1 + fi + + if ! docker image inspect "havline:${TRIM_APPVER}" >/dev/null 2>&1; then + echo "离线镜像加载后未找到 havline:${TRIM_APPVER}" > "$TRIM_TEMP_LOGFILE" + exit 1 + fi + + echo "离线镜像已加载:$path" +} diff --git a/havline/cmd/upgrade_callback b/havline/cmd/upgrade_callback index c22a518..f1fe3f7 100755 --- a/havline/cmd/upgrade_callback +++ b/havline/cmd/upgrade_callback @@ -1,6 +1,9 @@ #!/bin/bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/offline_image.sh" + if [ -f "$TRIM_PKGVAR/havline.db" ] && [ ! -r "$TRIM_PKGVAR/havline.db" ]; then echo "数据库不可读:$TRIM_PKGVAR/havline.db" > "$TRIM_TEMP_LOGFILE" exit 1 @@ -11,4 +14,6 @@ if [ -f "$TRIM_PKGVAR/session_secret" ] && [ ! -r "$TRIM_PKGVAR/session_secret" exit 1 fi +load_offline_image + exit 0 diff --git a/havline/config/privilege b/havline/config/privilege index cff6a3a..991e604 100644 --- a/havline/config/privilege +++ b/havline/config/privilege @@ -3,5 +3,6 @@ "run-as": "package" }, "username": "havline", - "groupname": "havline" + "groupname": "havline", + "join-groups": ["docker"] } diff --git a/havline/manifest b/havline/manifest index dc0c845..5f2f588 100644 --- a/havline/manifest +++ b/havline/manifest @@ -11,4 +11,4 @@ desktop_uidir = ui desktop_applaunchname = havline.main checkport = false ctl_stop = true -changelog = 首发飞牛应用:支持安装向导、端口配置、数据持久化和应用中心升级 +changelog = 离线镜像安装:不依赖外部仓库,同时支持 amd64 与 arm64