From 89effb6b2231f9f2a47156c2a246382d72f61692 Mon Sep 17 00:00:00 2001 From: Bison Xu <1251604436@qq.com> Date: Tue, 2 Jun 2026 21:57:31 +0800 Subject: [PATCH] feat(build): add Windows platform support (#13) * feat(build): add Windows platform support Add windows/amd64 and windows/arm64 to CI build matrix, Makefile cross-platform targets, and handle .exe suffix in install/update scripts and binary wrapper. Skip chmod on Windows where unsupported. Co-Authored-By: Claude Opus 4.7 * docs(README): add Windows download instructions Add Windows x86_64 and ARM64 binary download commands to both English and Chinese README install sections. Co-Authored-By: Claude Opus 4.7 --------- Co-authored-by: Claude Opus 4.7 --- .github/workflows/release.yml | 13 ++++++-- Makefile | 15 ++++++++-- README.md | 6 ++++ README.zh-CN.md | 6 ++++ bin/ocr.js | 3 +- internal/release/asset_naming_test.go | 20 +++++++++---- scripts/install.js | 43 ++++++++++++++++++--------- scripts/update.js | 13 ++++++-- 8 files changed, 92 insertions(+), 27 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 36823b6..14173a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,10 @@ jobs: goarch: amd64 - goos: darwin goarch: arm64 + - goos: windows + goarch: amd64 + - goos: windows + goarch: arm64 steps: - uses: actions/checkout@v4 @@ -38,12 +42,17 @@ jobs: GIT_COMMIT=$(git rev-parse --short HEAD) BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") LD_FLAGS="-s -w -X main.Version=${VERSION} -X main.GitCommit=${GIT_COMMIT} -X main.BuildDate=${BUILD_DATE}" - go build -ldflags "${LD_FLAGS}" -o opencodereview-${{ matrix.goos }}-${{ matrix.goarch }} ./cmd/opencodereview + BIN_NAME=opencodereview-${{ matrix.goos }}-${{ matrix.goarch }} + if [ "${{ matrix.goos }}" = "windows" ]; then + BIN_NAME="${BIN_NAME}.exe" + fi + go build -ldflags "${LD_FLAGS}" -o "${BIN_NAME}" ./cmd/opencodereview + echo "bin_name=${BIN_NAME}" >> $GITHUB_ENV - uses: actions/upload-artifact@v4 with: name: binary-${{ matrix.goos }}-${{ matrix.goarch }} - path: opencodereview-${{ matrix.goos }}-${{ matrix.goarch }} + path: ${{ env.bin_name }} release: needs: build diff --git a/Makefile b/Makefile index 68067f9..0d638cc 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ .PHONY: build test clean run help \ build-all dist sha256sum version-info \ - build-linux-amd64 build-linux-arm64 build-darwin-amd64 build-darwin-arm64 + build-linux-amd64 build-linux-arm64 build-darwin-amd64 build-darwin-arm64 \ + build-windows-amd64 build-windows-arm64 BINARY_NAME := opencodereview GO := go @@ -53,7 +54,17 @@ build-darwin-amd64: build-darwin-arm64: $(call BUILD_PLATFORM,darwin,arm64) -build-all: build-linux-amd64 build-linux-arm64 build-darwin-amd64 build-darwin-arm64 +build-windows-amd64: + GOOS=windows GOARCH=amd64 CGO_ENABLED=0 $(GO) build -ldflags "$(LD_FLAGS)" \ + -o $(DIST_DIR)/$(BINARY_NAME)-windows-amd64.exe \ + ./cmd/opencodereview + +build-windows-arm64: + GOOS=windows GOARCH=arm64 CGO_ENABLED=0 $(GO) build -ldflags "$(LD_FLAGS)" \ + -o $(DIST_DIR)/$(BINARY_NAME)-windows-arm64.exe \ + ./cmd/opencodereview + +build-all: build-linux-amd64 build-linux-arm64 build-darwin-amd64 build-darwin-arm64 build-windows-amd64 build-windows-arm64 # Generate SHA256 checksums for all release binaries sha256sum: build-all diff --git a/README.md b/README.md index aa1311a..8121e3f 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,12 @@ chmod +x ocr && sudo mv ocr /usr/local/bin/ocr # Linux (ARM64) curl -Lo ocr https://github.com/alibaba/open-code-review/releases/latest/download/opencodereview-linux-arm64 chmod +x ocr && sudo mv ocr /usr/local/bin/ocr + +# Windows (x86_64) +curl -Lo ocr.exe https://github.com/alibaba/open-code-review/releases/latest/download/opencodereview-windows-amd64.exe + +# Windows (ARM64) +curl -Lo ocr.exe https://github.com/alibaba/open-code-review/releases/latest/download/opencodereview-windows-arm64.exe ``` **From Source** diff --git a/README.zh-CN.md b/README.zh-CN.md index a1dda6a..571ee75 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -89,6 +89,12 @@ chmod +x ocr && sudo mv ocr /usr/local/bin/ocr # Linux (ARM64) curl -Lo ocr https://github.com/alibaba/open-code-review/releases/latest/download/opencodereview-linux-arm64 chmod +x ocr && sudo mv ocr /usr/local/bin/ocr + +# Windows (x86_64) +curl -Lo ocr.exe https://github.com/alibaba/open-code-review/releases/latest/download/opencodereview-windows-amd64.exe + +# Windows (ARM64) +curl -Lo ocr.exe https://github.com/alibaba/open-code-review/releases/latest/download/opencodereview-windows-arm64.exe ``` **从源码构建** diff --git a/bin/ocr.js b/bin/ocr.js index ee0e268..6bc86bd 100755 --- a/bin/ocr.js +++ b/bin/ocr.js @@ -6,7 +6,8 @@ const path = require("path"); const fs = require("fs"); const os = require("os"); -const binaryPath = path.join(__dirname, "opencodereview"); +const IS_WINDOWS = process.platform === "win32"; +const binaryPath = path.join(__dirname, IS_WINDOWS ? "opencodereview.exe" : "opencodereview"); if (!process.env.OCR_NO_UPDATE) { const stateDir = path.join(os.homedir(), ".opencodereview"); diff --git a/internal/release/asset_naming_test.go b/internal/release/asset_naming_test.go index c2e0383..1d298d1 100644 --- a/internal/release/asset_naming_test.go +++ b/internal/release/asset_naming_test.go @@ -89,30 +89,40 @@ func TestReleaseYmlMatchesURLPattern(t *testing.T) { pkg := loadPackageJSON(t) yml := loadFile(t, ".github/workflows/release.yml") - // Match: -o opencodereview-${{ matrix.goos }}-${{ matrix.goarch }} - re := regexp.MustCompile(`go build[^\n]*-o\s+(opencodereview[^\n]*\}\})`) + // Match: BIN_NAME=opencodereview-${{ matrix.goos }}-${{ matrix.goarch }} + re := regexp.MustCompile(`BIN_NAME=(opencodereview[^\n]*\}\})`) m := re.FindStringSubmatch(yml) if m == nil { - t.Fatal("cannot find 'go build -o' output pattern in release.yml") + t.Fatal("cannot find BIN_NAME pattern in release.yml") } - outputTemplate := strings.TrimSpace(strings.Split(m[1], " ./")[0]) + baseTemplate := strings.TrimSpace(m[1]) + + hasWindowsExe := strings.Contains(yml, `BIN_NAME="${BIN_NAME}.exe"`) platforms := []struct{ os, arch string }{ {"linux", "amd64"}, {"linux", "arm64"}, {"darwin", "amd64"}, {"darwin", "arm64"}, + {"windows", "amd64"}, + {"windows", "arm64"}, } for _, p := range platforms { - rendered := outputTemplate + rendered := baseTemplate rendered = strings.ReplaceAll(rendered, "${{ matrix.goos }}", p.os) rendered = strings.ReplaceAll(rendered, "${{ matrix.goarch }}", p.arch) + if p.os == "windows" && hasWindowsExe { + rendered += ".exe" + } url := pkg.OcrConfig.URLPattern url = strings.ReplaceAll(url, "{version}", "1.0.7") url = strings.ReplaceAll(url, "{os}", p.os) url = strings.ReplaceAll(url, "{arch}", p.arch) expected := extractFilenameFromURL(url) + if p.os == "windows" { + expected += ".exe" + } if rendered != expected { t.Errorf("release.yml produces %q, urlPattern expects %q", rendered, expected) diff --git a/scripts/install.js b/scripts/install.js index 88f8ef8..ec76985 100644 --- a/scripts/install.js +++ b/scripts/install.js @@ -6,7 +6,8 @@ const path = require("path"); const https = require("https"); const crypto = require("crypto"); -const BINARY_NAME = "opencodereview"; +const IS_WINDOWS = process.platform === "win32"; +const BINARY_NAME = IS_WINDOWS ? "opencodereview.exe" : "opencodereview"; const packageRoot = path.join(__dirname, ".."); const binDir = path.join(packageRoot, "bin"); @@ -25,7 +26,7 @@ function error(msg) { } function detectPlatform() { - const os = process.platform; + let os = process.platform; let arch = process.arch; switch (arch) { @@ -41,10 +42,17 @@ function detectPlatform() { ); } - if (os !== "linux" && os !== "darwin") { - throw new Error( - `Unsupported operating system: ${os}. Supported: linux, darwin` - ); + switch (os) { + case "linux": + case "darwin": + break; + case "win32": + os = "windows"; + break; + default: + throw new Error( + `Unsupported operating system: ${os}. Supported: linux, darwin, win32` + ); } return { os, arch }; @@ -157,21 +165,28 @@ async function main() { fs.mkdirSync(binDir, { recursive: true }); } - const jsWrapper = path.join(binDir, "ocr.js"); - if (fs.existsSync(jsWrapper)) { - try { - fs.chmodSync(jsWrapper, 0o755); - } catch (e) { - warn(`Could not make ocr.js executable: ${e.message}`); + if (!IS_WINDOWS) { + const jsWrapper = path.join(binDir, "ocr.js"); + if (fs.existsSync(jsWrapper)) { + try { + fs.chmodSync(jsWrapper, 0o755); + } catch (e) { + warn(`Could not make ocr.js executable: ${e.message}`); + } } } const vars = { version, os, arch }; - const downloadUrl = buildUrl(config.urlPattern, vars); + let downloadUrl = buildUrl(config.urlPattern, vars); + if (IS_WINDOWS) { + downloadUrl += ".exe"; + } info(`Downloading ${downloadUrl} ...`); await downloadBinary(downloadUrl, binaryDest); - fs.chmodSync(binaryDest, 0o755); + if (!IS_WINDOWS) { + fs.chmodSync(binaryDest, 0o755); + } if (config.checksumPattern) { try { diff --git a/scripts/update.js b/scripts/update.js index 558047e..7bc3f9e 100644 --- a/scripts/update.js +++ b/scripts/update.js @@ -17,9 +17,11 @@ const { computeChecksum, } = require("./install.js"); +const IS_WINDOWS = process.platform === "win32"; +const BINARY_NAME = IS_WINDOWS ? "opencodereview.exe" : "opencodereview"; const packageRoot = path.join(__dirname, ".."); const binDir = path.join(packageRoot, "bin"); -const binaryPath = path.join(binDir, "opencodereview"); +const binaryPath = path.join(binDir, BINARY_NAME); const stateDir = path.join(os.homedir(), ".opencodereview"); const tsFile = path.join(stateDir, "last-update-check"); const lockFile = path.join(stateDir, "update.lock"); @@ -160,11 +162,16 @@ async function main() { const config = pkg.ocrConfig; const vars = { version: latestVersion, os: platform, arch }; - const downloadUrl = buildUrl(config.urlPattern, vars); + let downloadUrl = buildUrl(config.urlPattern, vars); + if (IS_WINDOWS) { + downloadUrl += ".exe"; + } const tempPath = path.join(binDir, `.opencodereview.tmp.${process.pid}`); await downloadBinary(downloadUrl, tempPath); - fs.chmodSync(tempPath, 0o755); + if (!IS_WINDOWS) { + fs.chmodSync(tempPath, 0o755); + } if (config.checksumPattern) { try {