Преглед на файлове

Fix npm CLI binary installation (#27801)

Dax преди 3 месеца
родител
ревизия
09549661e1
променени са 4 файла, в които са добавени 170 реда и са изтрити 71 реда
  1. 1 0
      .github/workflows/publish.yml
  2. 1 0
      packages/opencode/script/build.ts
  3. 155 68
      packages/opencode/script/postinstall.mjs
  4. 13 3
      packages/opencode/script/publish.ts

+ 1 - 0
.github/workflows/publish.yml

@@ -7,6 +7,7 @@ on:
       - ci
       - dev
       - beta
+      - fix/npm-native-binary-install
       - snapshot-*
   workflow_dispatch:
     inputs:

+ 1 - 0
packages/opencode/script/build.ts

@@ -244,6 +244,7 @@ for (const item of targets) {
       {
         name,
         version: Script.version,
+        preferUnplugged: true,
         os: [item.os],
         cpu: [item.arch],
       },

+ 155 - 68
packages/opencode/script/postinstall.mjs

@@ -1,102 +1,189 @@
 #!/usr/bin/env node
 
+import childProcess from "child_process"
 import fs from "fs"
-import path from "path"
 import os from "os"
-import { fileURLToPath } from "url"
+import path from "path"
 import { createRequire } from "module"
+import { fileURLToPath } from "url"
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url))
 const require = createRequire(import.meta.url)
+const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8"))
+
+const platformMap = {
+  darwin: "darwin",
+  linux: "linux",
+  win32: "windows",
+}
+const archMap = {
+  x64: "x64",
+  arm64: "arm64",
+  arm: "arm",
+}
+
+const platform = platformMap[os.platform()] ?? os.platform()
+const arch = archMap[os.arch()] ?? os.arch()
+const base = `opencode-${platform}-${arch}`
+const sourceBinary = platform === "windows" ? "opencode.exe" : "opencode"
+const targetBinary = path.join(__dirname, "bin", "opencode.exe")
+
+function supportsAvx2() {
+  if (arch !== "x64") return false
+
+  if (platform === "linux") {
+    try {
+      return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
+    } catch {
+      return false
+    }
+  }
 
-function detectPlatformAndArch() {
-  // Map platform names
-  let platform
-  switch (os.platform()) {
-    case "darwin":
-      platform = "darwin"
-      break
-    case "linux":
-      platform = "linux"
-      break
-    case "win32":
-      platform = "windows"
-      break
-    default:
-      platform = os.platform()
-      break
+  if (platform === "darwin") {
+    try {
+      const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
+        encoding: "utf8",
+        timeout: 1500,
+      })
+      if (result.status !== 0) return false
+      return (result.stdout || "").trim() === "1"
+    } catch {
+      return false
+    }
   }
 
-  // Map architecture names
-  let arch
-  switch (os.arch()) {
-    case "x64":
-      arch = "x64"
-      break
-    case "arm64":
-      arch = "arm64"
-      break
-    case "arm":
-      arch = "arm"
-      break
-    default:
-      arch = os.arch()
-      break
+  if (platform === "windows") {
+    const command =
+      '(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
+
+    for (const executable of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
+      try {
+        const result = childProcess.spawnSync(executable, ["-NoProfile", "-NonInteractive", "-Command", command], {
+          encoding: "utf8",
+          timeout: 3000,
+          windowsHide: true,
+        })
+        if (result.status !== 0) continue
+        const output = (result.stdout || "").trim().toLowerCase()
+        if (output === "true" || output === "1") return true
+        if (output === "false" || output === "0") return false
+      } catch {
+        continue
+      }
+    }
   }
 
-  return { platform, arch }
+  return false
 }
 
-function findBinary() {
-  const { platform, arch } = detectPlatformAndArch()
-  const packageName = `opencode-${platform}-${arch}`
-  const binaryName = platform === "windows" ? "opencode.exe" : "opencode"
+function isMusl() {
+  if (platform !== "linux") return false
 
   try {
-    // Use require.resolve to find the package
-    const packageJsonPath = require.resolve(`${packageName}/package.json`)
-    const packageDir = path.dirname(packageJsonPath)
-    const binaryPath = path.join(packageDir, "bin", binaryName)
+    if (fs.existsSync("/etc/alpine-release")) return true
+  } catch {
+    // Ignore filesystem probes that are blocked by the host.
+  }
 
-    if (!fs.existsSync(binaryPath)) {
-      throw new Error(`Binary not found at ${binaryPath}`)
+  try {
+    const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
+    return `${result.stdout || ""}${result.stderr || ""}`.toLowerCase().includes("musl")
+  } catch {
+    return false
+  }
+}
+
+function packageNames() {
+  const baseline = arch === "x64" && !supportsAvx2()
+
+  if (platform === "linux") {
+    if (isMusl()) {
+      if (arch === "x64")
+        return baseline
+          ? [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
+          : [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
+      return [`${base}-musl`, base]
     }
 
-    return { binaryPath, binaryName }
-  } catch (error) {
-    throw new Error(`Could not find package ${packageName}: ${error.message}`, { cause: error })
+    if (arch === "x64")
+      return baseline
+        ? [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
+        : [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
+    return [base, `${base}-musl`]
   }
+
+  if (arch === "x64") return baseline ? [`${base}-baseline`, base] : [base, `${base}-baseline`]
+  return [base]
+}
+
+function resolveBinary(name) {
+  const packageJsonPath = require.resolve(`${name}/package.json`)
+  const binaryPath = path.join(path.dirname(packageJsonPath), "bin", sourceBinary)
+  if (!fs.existsSync(binaryPath)) throw new Error(`Binary not found at ${binaryPath}`)
+  return binaryPath
 }
 
-async function main() {
+function installPackage(name) {
+  const version = packageJson.optionalDependencies?.[name]
+  if (!version) return
+
+  const temp = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-install-"))
   try {
-    if (os.platform() === "win32") {
-      // On Windows, the .exe is already included in the package and bin field points to it
-      // No postinstall setup needed
-      console.log("Windows detected: binary setup not needed (using packaged .exe)")
-      return
-    }
+    const result = childProcess.spawnSync(
+      "npm",
+      ["install", "--ignore-scripts", "--no-save", "--loglevel=error", "--prefix", temp, `${name}@${version}`],
+      { stdio: "inherit", windowsHide: true },
+    )
+    if (result.status !== 0) return
+    const packageDir = path.join(temp, "node_modules", name)
+    copyBinary(path.join(packageDir, "bin", sourceBinary), targetBinary)
+    return true
+  } finally {
+    fs.rmSync(temp, { recursive: true, force: true })
+  }
+}
 
-    // On non-Windows platforms, just verify the binary package exists
-    // Don't replace the wrapper script - it handles binary execution
-    const { binaryPath } = findBinary()
-    const target = path.join(__dirname, "bin", ".opencode")
-    if (fs.existsSync(target)) fs.unlinkSync(target)
+function copyBinary(source, target) {
+  if (!fs.existsSync(source)) throw new Error(`Binary not found at ${source}`)
+  fs.mkdirSync(path.dirname(target), { recursive: true })
+  if (fs.existsSync(target)) fs.unlinkSync(target)
+  try {
+    fs.linkSync(source, target)
+  } catch {
+    fs.copyFileSync(source, target)
+  }
+  fs.chmodSync(target, 0o755)
+}
+
+function verifyBinary() {
+  const result = childProcess.spawnSync(targetBinary, ["--version"], {
+    encoding: "utf8",
+    stdio: "ignore",
+    windowsHide: true,
+  })
+  return result.status === 0
+}
+
+function main() {
+  for (const name of packageNames()) {
     try {
-      fs.linkSync(binaryPath, target)
+      copyBinary(resolveBinary(name), targetBinary)
+      if (verifyBinary()) return
     } catch {
-      fs.copyFileSync(binaryPath, target)
+      if (installPackage(name) && verifyBinary()) return
     }
-    fs.chmodSync(target, 0o755)
-  } catch (error) {
-    console.error("Failed to setup opencode binary:", error.message)
-    process.exit(1)
   }
+
+  throw new Error(
+    `It seems your package manager failed to install the right opencode CLI package. Try manually installing ${packageNames()
+      .map((name) => JSON.stringify(name))
+      .join(" or ")}.`,
+  )
 }
 
 try {
-  void main()
+  main()
 } catch (error) {
-  console.error("Postinstall script error:", error.message)
-  process.exit(0)
+  console.error(error.message)
+  process.exit(1)
 }

+ 13 - 3
packages/opencode/script/publish.ts

@@ -32,22 +32,32 @@ console.log("binaries", binaries)
 const version = Object.values(binaries)[0]
 
 await $`mkdir -p ./dist/${pkg.name}`
-await $`cp -r ./bin ./dist/${pkg.name}/bin`
+await $`mkdir -p ./dist/${pkg.name}/bin`
 await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`
 await Bun.file(`./dist/${pkg.name}/LICENSE`).write(await Bun.file("../../LICENSE").text())
+await Bun.file(`./dist/${pkg.name}/bin/${pkg.name}.exe`).write(
+  [
+    "#!/usr/bin/env node",
+    "console.error('The opencode native binary was not installed. Run `node postinstall.mjs` from the opencode-ai package directory to finish setup.')",
+    "process.exit(1)",
+    "",
+  ].join("\n"),
+)
 
 await Bun.file(`./dist/${pkg.name}/package.json`).write(
   JSON.stringify(
     {
       name: pkg.name + "-ai",
       bin: {
-        [pkg.name]: `./bin/${pkg.name}`,
+        [pkg.name]: `./bin/${pkg.name}.exe`,
       },
       scripts: {
-        postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs",
+        postinstall: "node ./postinstall.mjs",
       },
       version: version,
       license: pkg.license,
+      os: ["darwin", "linux", "win32"],
+      cpu: ["arm64", "x64"],
       optionalDependencies: binaries,
     },
     null,