From 253180326101417a95d6769ab270af3f6822c7a7 Mon Sep 17 00:00:00 2001 From: LYJW131 Date: Mon, 13 Jul 2026 09:55:01 +0800 Subject: [PATCH] ci: publish amd64 manager image Restore automatic installation of the matching x86_64 wrapper release. Co-authored-by: Codex Signed-off-by: LYJW131 --- .github/workflows/wrapper-manager-image.yml | 93 +++++++++--------- Dockerfile | 29 ++++-- main.go | 5 +- wrapper.go | 98 ++++++++++++++++--- wrapper_download_test.go | 103 ++++++++++++++++++++ 5 files changed, 258 insertions(+), 70 deletions(-) create mode 100644 wrapper_download_test.go diff --git a/.github/workflows/wrapper-manager-image.yml b/.github/workflows/wrapper-manager-image.yml index 845921f..7a041cd 100644 --- a/.github/workflows/wrapper-manager-image.yml +++ b/.github/workflows/wrapper-manager-image.yml @@ -1,52 +1,57 @@ -name: wrapper-manager-image-build +name: Build amd64 Docker image on: + workflow_dispatch: push: - branches: [ "main" ] + branches: [main] pull_request: - branches: [ "main" ] - workflow_dispatch: + branches: [main] -jobs: +permissions: + contents: read + packages: write + +concurrency: + group: wrapper-manager-amd64-${{ github.ref }} + cancel-in-progress: true - build: +env: + IMAGE_NAME: ghcr.io/amdl-web/wrapper-manager + +jobs: + docker: runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.23' - - - name: Build - run: go build . - - - name: Download wrapper-manager basic image - run: wget https://github.com/WorldObservationLog/wrapper-manager/releases/download/wrapper-manager-image/wrapper-manager.qcow2 - - - name: Mount image - run: | - sudo apt-get update - sudo apt-get install -y qemu-utils - sudo modprobe nbd max_part=8 - sudo qemu-nbd --connect=/dev/nbd0 wrapper-manager.qcow2 - sudo mkdir /mnt/wrapper-manager - sudo mount /dev/nbd0p3 /mnt/wrapper-manager/ - - - name: Prepare wrapper-manager - run: | - sudo mkdir /mnt/wrapper-manager/root/wrapper-manager - sudo mv wrapper-manager /mnt/wrapper-manager/root/wrapper-manager - sudo bash -c 'cd /mnt/wrapper-manager/root/wrapper-manager && ./wrapper-manager -prepare' - - - name: Unmount image - run: | - sudo umount /mnt/wrapper-manager/ - sudo qemu-nbd --disconnect /dev/nbd0 - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: wrapper-manager-image - path: wrapper-manager.qcow2 + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha + + - name: Build and publish amd64 image + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index 2cda190..869867d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,28 @@ -FROM golang:1.23 as builder +FROM --platform=$BUILDPLATFORM golang:1.23 AS builder + +ARG TARGETOS +ARG TARGETARCH WORKDIR /app +COPY go.mod go.sum ./ +COPY proto/go.mod proto/go.sum ./proto/ +RUN go mod download + COPY . . -# RUN go env -w GO111MODULE=on && go env -w GOPROXY=https://goproxy.cn,direct -RUN go mod tidy -RUN GOOS=linux go build -o wrapper-manager +RUN test "$TARGETARCH" = "amd64" && \ + CGO_ENABLED=0 GOOS="$TARGETOS" GOARCH="$TARGETARCH" \ + go build -trimpath -ldflags="-s -w" -o /out/wrapper-manager . + +FROM ubuntu:24.04 -FROM ubuntu:latest +WORKDIR /root -WORKDIR /root/ +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates && \ + rm -rf /var/lib/apt/lists/* -COPY --from=builder /app/wrapper-manager . -RUN apt-get update && apt-get install -y ca-certificates -RUN chmod +x ./wrapper-manager +COPY --from=builder /out/wrapper-manager ./wrapper-manager ENTRYPOINT ["./wrapper-manager"] -EXPOSE 8080 \ No newline at end of file +EXPOSE 8080 diff --git a/main.go b/main.go index c6d4bfd..e8bb66d 100644 --- a/main.go +++ b/main.go @@ -497,7 +497,6 @@ func main() { flag.StringVar(&PROXY, "proxy", "", "proxy for wrapper and manager") flag.StringVar(&DeviceInfo, "device-info", "Music/5.0.2/Android/10/Pixel 10/7663314/en-US/en-US/dc28071e371c439e", "device info for wrapper") flag.Parse() - _ = mirror log.SetOutput(os.Stdout) if *debug { @@ -514,8 +513,8 @@ func main() { log.Panicln("root permission required") } - if _, err := os.Stat("data/wrapper/wrapper"); errors.Is(err, os.ErrNotExist) { - log.Panicln("wrapper binary not found in data/wrapper/wrapper. Please build and place it there first.") + if err := PrepareWrapper(*mirror); err != nil { + log.Panicf("prepare x86_64 wrapper: %v", err) } if _, err := os.Stat("data/storefront_ids.json"); errors.Is(err, os.ErrNotExist) { diff --git a/wrapper.go b/wrapper.go index ae3cc9d..3d55127 100644 --- a/wrapper.go +++ b/wrapper.go @@ -16,6 +16,13 @@ import ( "strings" ) +const ( + wrapperReleaseTag = "wrapper.x86_64.latest" + wrapperAssetName = "Wrapper.x86_64.latest.zip" +) + +var wrapperReleaseAPIURL = "https://api.github.com/repos/AMDL-Web/wrapper/releases/tags/" + wrapperReleaseTag + func parseStorefrontID(id string) string { sfID, err := strconv.Atoi(strings.Split(id, "-")[0]) if err != nil { @@ -43,26 +50,31 @@ func parseStorefrontID(id string) string { return "" } -func PrepareWrapper(mirror bool) { - var wrapperZipPath string - if runtime.GOARCH == "amd64" { - wrapperZipPath = "data/wrapper-x86_64.zip" - } else if runtime.GOARCH == "arm64" { - wrapperZipPath = "data/wrapper-arm64.zip" +func PrepareWrapper(mirror bool) error { + return prepareWrapper(mirror, runtime.GOARCH) +} + +func prepareWrapper(mirror bool, arch string) error { + if arch != "amd64" { + return fmt.Errorf("wrapper auto-install only supports x86_64, current architecture is %s", arch) } + wrapperZipPath := "data/wrapper-x86_64.zip" if _, err := os.Stat("data/wrapper/wrapper"); os.IsNotExist(err) { if _, err := os.Stat(wrapperZipPath); os.IsNotExist(err) { - DownloadWrapperRelease(mirror) + if err := DownloadWrapperRelease(mirror); err != nil { + return err + } } - err = unzip.New(wrapperZipPath, "data/wrapper").Extract() + err := unzip.New(wrapperZipPath, "data/wrapper").Extract() if err != nil { - panic(err) + return fmt.Errorf("extract wrapper release: %w", err) } err = os.Chmod("data/wrapper/wrapper", 0777) if err != nil { - panic(err) + return fmt.Errorf("make wrapper executable: %w", err) } } + return nil } func WrapperInitial(account string, password string) { @@ -234,7 +246,7 @@ func KillWrapper(id string) error { } func provide2FACode(id string, code string) { - path := "data/wrapper/rootfs/data/instances/"+id+"/2fa.txt" + path := "data/wrapper/rootfs/data/instances/" + id + "/2fa.txt" err := os.WriteFile(path, []byte(code), 0777) if err != nil { log.Warnf("failed to write 2fa.txt: %v", err) @@ -248,8 +260,68 @@ func RemoveWrapperData(id string) { } } -func DownloadWrapperRelease(mirror bool) { - panic("Downloading wrapper binary is disabled. Please provide the binary manually in data/wrapper/wrapper.") +func DownloadWrapperRelease(mirror bool) error { + resp, err := GetHttpClient().Get(wrapperReleaseAPIURL) + if err != nil { + return fmt.Errorf("request wrapper release: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("request wrapper release: %s", resp.Status) + } + + var release struct { + Assets []struct { + Name string `json:"name"` + BrowserDownloadURL string `json:"browser_download_url"` + } `json:"assets"` + } + if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + return fmt.Errorf("decode wrapper release: %w", err) + } + + var downloadURL string + for _, asset := range release.Assets { + if asset.Name == wrapperAssetName { + downloadURL = asset.BrowserDownloadURL + break + } + } + if downloadURL == "" { + return fmt.Errorf("wrapper release %s has no %s asset", wrapperReleaseTag, wrapperAssetName) + } + if mirror { + downloadURL = strings.Replace(downloadURL, "github.com", "gh-proxy.com/github.com", 1) + } + + wrapperResp, err := GetHttpClient().Get(downloadURL) + if err != nil { + return fmt.Errorf("download wrapper release: %w", err) + } + defer wrapperResp.Body.Close() + if wrapperResp.StatusCode < 200 || wrapperResp.StatusCode >= 300 { + return fmt.Errorf("download wrapper release: %s", wrapperResp.Status) + } + if err := os.MkdirAll("data", 0o755); err != nil { + return fmt.Errorf("create data directory: %w", err) + } + temp, err := os.CreateTemp("data", "wrapper-x86_64-*.zip") + if err != nil { + return fmt.Errorf("create wrapper download: %w", err) + } + tempPath := temp.Name() + defer os.Remove(tempPath) + if _, err := io.Copy(temp, wrapperResp.Body); err != nil { + temp.Close() + return fmt.Errorf("save wrapper release: %w", err) + } + if err := temp.Close(); err != nil { + return fmt.Errorf("close wrapper release: %w", err) + } + if err := os.Rename(tempPath, "data/wrapper-x86_64.zip"); err != nil { + return fmt.Errorf("finalize wrapper release: %w", err) + } + return nil } func DownloadStorefrontIds() { diff --git a/wrapper_download_test.go b/wrapper_download_test.go new file mode 100644 index 0000000..30a3173 --- /dev/null +++ b/wrapper_download_test.go @@ -0,0 +1,103 @@ +package main + +import ( + "archive/zip" + "bytes" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" +) + +func wrapperZip(t *testing.T) []byte { + t.Helper() + var buffer bytes.Buffer + writer := zip.NewWriter(&buffer) + file, err := writer.Create("wrapper") + if err != nil { + t.Fatal(err) + } + if _, err := file.Write([]byte("x86-wrapper")); err != nil { + t.Fatal(err) + } + if err := writer.Close(); err != nil { + t.Fatal(err) + } + return buffer.Bytes() +} + +func TestPrepareWrapperDownloadsExactX86Asset(t *testing.T) { + archive := wrapperZip(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/release": + fmt.Fprintf(w, `{"assets":[{"name":"Wrapper.arm64.latest.zip","browser_download_url":"%s/arm64"},{"name":"%s","browser_download_url":"%s/x86"}]}`, serverURL(r), wrapperAssetName, serverURL(r)) + case "/x86": + _, _ = w.Write(archive) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + oldURL := wrapperReleaseAPIURL + wrapperReleaseAPIURL = server.URL + "/release" + defer func() { wrapperReleaseAPIURL = oldURL }() + + oldWorkingDirectory, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(t.TempDir()); err != nil { + t.Fatal(err) + } + defer os.Chdir(oldWorkingDirectory) + + if err := prepareWrapper(false, "amd64"); err != nil { + t.Fatal(err) + } + contents, err := os.ReadFile(filepath.Join("data", "wrapper", "wrapper")) + if err != nil { + t.Fatal(err) + } + if string(contents) != "x86-wrapper" { + t.Fatalf("unexpected wrapper contents: %q", contents) + } + info, err := os.Stat(filepath.Join("data", "wrapper", "wrapper")) + if err != nil { + t.Fatal(err) + } + if info.Mode().Perm()&0o111 == 0 { + t.Fatal("downloaded wrapper is not executable") + } +} + +func TestDownloadWrapperReleaseRequiresExactAsset(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(`{"assets":[{"name":"Wrapper.arm64.latest.zip","browser_download_url":"https://example.invalid/arm64"}]}`)) + })) + defer server.Close() + + oldURL := wrapperReleaseAPIURL + wrapperReleaseAPIURL = server.URL + defer func() { wrapperReleaseAPIURL = oldURL }() + + err := DownloadWrapperRelease(false) + if err == nil || !strings.Contains(err.Error(), wrapperAssetName) { + t.Fatalf("expected missing x86 asset error, got %v", err) + } +} + +func TestPrepareWrapperRejectsNonX86(t *testing.T) { + err := prepareWrapper(false, "arm64") + if err == nil || !strings.Contains(err.Error(), "only supports x86_64") { + t.Fatalf("expected unsupported architecture error, got %v", err) + } +} + +func serverURL(r *http.Request) string { + return "http://" + r.Host +}