Skip to content

Commit 0954966

Browse files
authored
Fix npm CLI binary installation (anomalyco#27801)
1 parent da495fd commit 0954966

4 files changed

Lines changed: 170 additions & 71 deletions

File tree

.github/workflows/publish.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77
- ci
88
- dev
99
- beta
10+
- fix/npm-native-binary-install
1011
- snapshot-*
1112
workflow_dispatch:
1213
inputs:

packages/opencode/script/build.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ for (const item of targets) {
244244
{
245245
name,
246246
version: Script.version,
247+
preferUnplugged: true,
247248
os: [item.os],
248249
cpu: [item.arch],
249250
},
Lines changed: 155 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,189 @@
11
#!/usr/bin/env node
22

3+
import childProcess from "child_process"
34
import fs from "fs"
4-
import path from "path"
55
import os from "os"
6-
import { fileURLToPath } from "url"
6+
import path from "path"
77
import { createRequire } from "module"
8+
import { fileURLToPath } from "url"
89

910
const __dirname = path.dirname(fileURLToPath(import.meta.url))
1011
const require = createRequire(import.meta.url)
12+
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8"))
13+
14+
const platformMap = {
15+
darwin: "darwin",
16+
linux: "linux",
17+
win32: "windows",
18+
}
19+
const archMap = {
20+
x64: "x64",
21+
arm64: "arm64",
22+
arm: "arm",
23+
}
24+
25+
const platform = platformMap[os.platform()] ?? os.platform()
26+
const arch = archMap[os.arch()] ?? os.arch()
27+
const base = `opencode-${platform}-${arch}`
28+
const sourceBinary = platform === "windows" ? "opencode.exe" : "opencode"
29+
const targetBinary = path.join(__dirname, "bin", "opencode.exe")
30+
31+
function supportsAvx2() {
32+
if (arch !== "x64") return false
33+
34+
if (platform === "linux") {
35+
try {
36+
return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
37+
} catch {
38+
return false
39+
}
40+
}
1141

12-
function detectPlatformAndArch() {
13-
// Map platform names
14-
let platform
15-
switch (os.platform()) {
16-
case "darwin":
17-
platform = "darwin"
18-
break
19-
case "linux":
20-
platform = "linux"
21-
break
22-
case "win32":
23-
platform = "windows"
24-
break
25-
default:
26-
platform = os.platform()
27-
break
42+
if (platform === "darwin") {
43+
try {
44+
const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
45+
encoding: "utf8",
46+
timeout: 1500,
47+
})
48+
if (result.status !== 0) return false
49+
return (result.stdout || "").trim() === "1"
50+
} catch {
51+
return false
52+
}
2853
}
2954

30-
// Map architecture names
31-
let arch
32-
switch (os.arch()) {
33-
case "x64":
34-
arch = "x64"
35-
break
36-
case "arm64":
37-
arch = "arm64"
38-
break
39-
case "arm":
40-
arch = "arm"
41-
break
42-
default:
43-
arch = os.arch()
44-
break
55+
if (platform === "windows") {
56+
const command =
57+
'(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
58+
59+
for (const executable of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
60+
try {
61+
const result = childProcess.spawnSync(executable, ["-NoProfile", "-NonInteractive", "-Command", command], {
62+
encoding: "utf8",
63+
timeout: 3000,
64+
windowsHide: true,
65+
})
66+
if (result.status !== 0) continue
67+
const output = (result.stdout || "").trim().toLowerCase()
68+
if (output === "true" || output === "1") return true
69+
if (output === "false" || output === "0") return false
70+
} catch {
71+
continue
72+
}
73+
}
4574
}
4675

47-
return { platform, arch }
76+
return false
4877
}
4978

50-
function findBinary() {
51-
const { platform, arch } = detectPlatformAndArch()
52-
const packageName = `opencode-${platform}-${arch}`
53-
const binaryName = platform === "windows" ? "opencode.exe" : "opencode"
79+
function isMusl() {
80+
if (platform !== "linux") return false
5481

5582
try {
56-
// Use require.resolve to find the package
57-
const packageJsonPath = require.resolve(`${packageName}/package.json`)
58-
const packageDir = path.dirname(packageJsonPath)
59-
const binaryPath = path.join(packageDir, "bin", binaryName)
83+
if (fs.existsSync("/etc/alpine-release")) return true
84+
} catch {
85+
// Ignore filesystem probes that are blocked by the host.
86+
}
6087

61-
if (!fs.existsSync(binaryPath)) {
62-
throw new Error(`Binary not found at ${binaryPath}`)
88+
try {
89+
const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
90+
return `${result.stdout || ""}${result.stderr || ""}`.toLowerCase().includes("musl")
91+
} catch {
92+
return false
93+
}
94+
}
95+
96+
function packageNames() {
97+
const baseline = arch === "x64" && !supportsAvx2()
98+
99+
if (platform === "linux") {
100+
if (isMusl()) {
101+
if (arch === "x64")
102+
return baseline
103+
? [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
104+
: [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
105+
return [`${base}-musl`, base]
63106
}
64107

65-
return { binaryPath, binaryName }
66-
} catch (error) {
67-
throw new Error(`Could not find package ${packageName}: ${error.message}`, { cause: error })
108+
if (arch === "x64")
109+
return baseline
110+
? [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
111+
: [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
112+
return [base, `${base}-musl`]
68113
}
114+
115+
if (arch === "x64") return baseline ? [`${base}-baseline`, base] : [base, `${base}-baseline`]
116+
return [base]
117+
}
118+
119+
function resolveBinary(name) {
120+
const packageJsonPath = require.resolve(`${name}/package.json`)
121+
const binaryPath = path.join(path.dirname(packageJsonPath), "bin", sourceBinary)
122+
if (!fs.existsSync(binaryPath)) throw new Error(`Binary not found at ${binaryPath}`)
123+
return binaryPath
69124
}
70125

71-
async function main() {
126+
function installPackage(name) {
127+
const version = packageJson.optionalDependencies?.[name]
128+
if (!version) return
129+
130+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-install-"))
72131
try {
73-
if (os.platform() === "win32") {
74-
// On Windows, the .exe is already included in the package and bin field points to it
75-
// No postinstall setup needed
76-
console.log("Windows detected: binary setup not needed (using packaged .exe)")
77-
return
78-
}
132+
const result = childProcess.spawnSync(
133+
"npm",
134+
["install", "--ignore-scripts", "--no-save", "--loglevel=error", "--prefix", temp, `${name}@${version}`],
135+
{ stdio: "inherit", windowsHide: true },
136+
)
137+
if (result.status !== 0) return
138+
const packageDir = path.join(temp, "node_modules", name)
139+
copyBinary(path.join(packageDir, "bin", sourceBinary), targetBinary)
140+
return true
141+
} finally {
142+
fs.rmSync(temp, { recursive: true, force: true })
143+
}
144+
}
79145

80-
// On non-Windows platforms, just verify the binary package exists
81-
// Don't replace the wrapper script - it handles binary execution
82-
const { binaryPath } = findBinary()
83-
const target = path.join(__dirname, "bin", ".opencode")
84-
if (fs.existsSync(target)) fs.unlinkSync(target)
146+
function copyBinary(source, target) {
147+
if (!fs.existsSync(source)) throw new Error(`Binary not found at ${source}`)
148+
fs.mkdirSync(path.dirname(target), { recursive: true })
149+
if (fs.existsSync(target)) fs.unlinkSync(target)
150+
try {
151+
fs.linkSync(source, target)
152+
} catch {
153+
fs.copyFileSync(source, target)
154+
}
155+
fs.chmodSync(target, 0o755)
156+
}
157+
158+
function verifyBinary() {
159+
const result = childProcess.spawnSync(targetBinary, ["--version"], {
160+
encoding: "utf8",
161+
stdio: "ignore",
162+
windowsHide: true,
163+
})
164+
return result.status === 0
165+
}
166+
167+
function main() {
168+
for (const name of packageNames()) {
85169
try {
86-
fs.linkSync(binaryPath, target)
170+
copyBinary(resolveBinary(name), targetBinary)
171+
if (verifyBinary()) return
87172
} catch {
88-
fs.copyFileSync(binaryPath, target)
173+
if (installPackage(name) && verifyBinary()) return
89174
}
90-
fs.chmodSync(target, 0o755)
91-
} catch (error) {
92-
console.error("Failed to setup opencode binary:", error.message)
93-
process.exit(1)
94175
}
176+
177+
throw new Error(
178+
`It seems your package manager failed to install the right opencode CLI package. Try manually installing ${packageNames()
179+
.map((name) => JSON.stringify(name))
180+
.join(" or ")}.`,
181+
)
95182
}
96183

97184
try {
98-
void main()
185+
main()
99186
} catch (error) {
100-
console.error("Postinstall script error:", error.message)
101-
process.exit(0)
187+
console.error(error.message)
188+
process.exit(1)
102189
}

packages/opencode/script/publish.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,32 @@ console.log("binaries", binaries)
3232
const version = Object.values(binaries)[0]
3333

3434
await $`mkdir -p ./dist/${pkg.name}`
35-
await $`cp -r ./bin ./dist/${pkg.name}/bin`
35+
await $`mkdir -p ./dist/${pkg.name}/bin`
3636
await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`
3737
await Bun.file(`./dist/${pkg.name}/LICENSE`).write(await Bun.file("../../LICENSE").text())
38+
await Bun.file(`./dist/${pkg.name}/bin/${pkg.name}.exe`).write(
39+
[
40+
"#!/usr/bin/env node",
41+
"console.error('The opencode native binary was not installed. Run `node postinstall.mjs` from the opencode-ai package directory to finish setup.')",
42+
"process.exit(1)",
43+
"",
44+
].join("\n"),
45+
)
3846

3947
await Bun.file(`./dist/${pkg.name}/package.json`).write(
4048
JSON.stringify(
4149
{
4250
name: pkg.name + "-ai",
4351
bin: {
44-
[pkg.name]: `./bin/${pkg.name}`,
52+
[pkg.name]: `./bin/${pkg.name}.exe`,
4553
},
4654
scripts: {
47-
postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs",
55+
postinstall: "node ./postinstall.mjs",
4856
},
4957
version: version,
5058
license: pkg.license,
59+
os: ["darwin", "linux", "win32"],
60+
cpu: ["arm64", "x64"],
5161
optionalDependencies: binaries,
5262
},
5363
null,

0 commit comments

Comments
 (0)