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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Bison Xu 2026-06-02 21:57:31 +08:00 committed by GitHub
parent 712d562691
commit 89effb6b22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 92 additions and 27 deletions

View file

@ -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

View file

@ -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

View file

@ -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**

View file

@ -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
```
**从源码构建**

View file

@ -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");

View file

@ -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)

View file

@ -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 {

View file

@ -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 {