|
1 | 1 | #!/usr/bin/env node |
2 | 2 |
|
| 3 | +import childProcess from "child_process" |
3 | 4 | import fs from "fs" |
4 | | -import path from "path" |
5 | 5 | import os from "os" |
6 | | -import { fileURLToPath } from "url" |
| 6 | +import path from "path" |
7 | 7 | import { createRequire } from "module" |
| 8 | +import { fileURLToPath } from "url" |
8 | 9 |
|
9 | 10 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) |
10 | 11 | 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 | + } |
11 | 41 |
|
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 | + } |
28 | 53 | } |
29 | 54 |
|
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 | + } |
45 | 74 | } |
46 | 75 |
|
47 | | - return { platform, arch } |
| 76 | + return false |
48 | 77 | } |
49 | 78 |
|
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 |
54 | 81 |
|
55 | 82 | 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 | + } |
60 | 87 |
|
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] |
63 | 106 | } |
64 | 107 |
|
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`] |
68 | 113 | } |
| 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 |
69 | 124 | } |
70 | 125 |
|
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-")) |
72 | 131 | 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 | +} |
79 | 145 |
|
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()) { |
85 | 169 | try { |
86 | | - fs.linkSync(binaryPath, target) |
| 170 | + copyBinary(resolveBinary(name), targetBinary) |
| 171 | + if (verifyBinary()) return |
87 | 172 | } catch { |
88 | | - fs.copyFileSync(binaryPath, target) |
| 173 | + if (installPackage(name) && verifyBinary()) return |
89 | 174 | } |
90 | | - fs.chmodSync(target, 0o755) |
91 | | - } catch (error) { |
92 | | - console.error("Failed to setup opencode binary:", error.message) |
93 | | - process.exit(1) |
94 | 175 | } |
| 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 | + ) |
95 | 182 | } |
96 | 183 |
|
97 | 184 | try { |
98 | | - void main() |
| 185 | + main() |
99 | 186 | } catch (error) { |
100 | | - console.error("Postinstall script error:", error.message) |
101 | | - process.exit(0) |
| 187 | + console.error(error.message) |
| 188 | + process.exit(1) |
102 | 189 | } |
0 commit comments