Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,16 @@ import { fileURLToPath } from "node:url"

const directory = path.dirname(fileURLToPath(import.meta.url))
const require = createRequire(import.meta.url)
const packageJson = JSON.parse(fs.readFileSync(path.join(directory, "package.json"), "utf8"))
const packageJson = JSON.parse(fs.readFileSync(path.join(directory, "../package.json"), "utf8"))
const command = Object.keys(packageJson.bin ?? {})[0]
if (!command) throw new Error("Shuvcode package does not declare a binary")
if (!command) fail("Shuvcode package does not declare a binary")

const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] ?? os.platform()
const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] ?? os.arch()
const sourceBinary = platform === "windows" ? `${command}.exe` : command
const targetBinary = path.resolve(directory, packageJson.bin[command])
const dependencies = packageJson.optionalDependencies ?? {}
const base = Object.keys(dependencies).find((name) => name.endsWith(`-${platform}-${arch}`))
if (!base) throw new Error(`Shuvcode does not provide a binary for ${platform}-${arch}`)
if (!base) fail(`Shuvcode does not provide a binary for ${platform}-${arch}`)

function supportsAvx2() {
if (arch !== "x64") return false
Expand All @@ -31,33 +30,25 @@ function supportsAvx2() {
}
}
if (platform === "darwin") {
try {
const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
encoding: "utf8",
timeout: 1500,
})
return result.status === 0 && (result.stdout || "").trim() === "1"
} catch {
return false
}
const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
encoding: "utf8",
timeout: 1500,
})
return result.status === 0 && (result.stdout || "").trim() === "1"
}
if (platform === "windows") {
const script =
'(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", script], {
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
}
const result = childProcess.spawnSync(executable, ["-NoProfile", "-NonInteractive", "-Command", script], {
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
}
}
return false
Expand Down Expand Up @@ -97,75 +88,34 @@ function packageNames() {
return names.filter((name) => dependencies[name])
}

function copyBinary(source) {
if (!fs.existsSync(source)) throw new Error(`Binary not found at ${source}`)
fs.mkdirSync(path.dirname(targetBinary), { recursive: true })
if (fs.existsSync(targetBinary)) fs.unlinkSync(targetBinary)
try {
fs.linkSync(source, targetBinary)
} catch {
fs.copyFileSync(source, targetBinary)
}
fs.chmodSync(targetBinary, 0o755)
}

function resolveBinary(name) {
const packagePath = require.resolve(`${name}/package.json`)
return path.join(path.dirname(packagePath), "bin", sourceBinary)
const binary = path.join(path.dirname(packagePath), "bin", sourceBinary)
if (!fs.existsSync(binary)) throw new Error(`Binary not found at ${binary}`)
return binary
}

function installPackage(name) {
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "shuvcode-install-"))
try {
const result = childProcess.spawnSync(
"npm",
[
"install",
"--ignore-scripts",
"--no-save",
"--loglevel=error",
"--prefix",
temp,
`${name}@${dependencies[name]}`,
],
{ stdio: "inherit", windowsHide: true },
)
if (result.status !== 0) return false
copyBinary(path.join(temp, "node_modules", name, "bin", sourceBinary))
return true
} finally {
fs.rmSync(temp, { recursive: true, force: true })
}
}

function verifyBinary() {
return (
childProcess.spawnSync(targetBinary, ["--version"], {
stdio: "ignore",
windowsHide: true,
}).status === 0
)
function fail(message) {
console.error(message)
process.exit(1)
}

function main() {
const names = packageNames()
for (const name of names) {
try {
copyBinary(resolveBinary(name))
if (verifyBinary()) return
} catch {
if (installPackage(name) && verifyBinary()) return
}
const names = packageNames()
const binary = names.reduce((result, name) => {
if (result) return result
try {
return resolveBinary(name)
} catch {
return undefined
}
}, undefined)

throw new Error(
`Failed to install Shuvcode. Try manually installing ${names.map((name) => JSON.stringify(name)).join(" or ")}.`,
)
}
if (!binary) fail(`Failed to find Shuvcode binary package. Reinstall ${packageJson.name}.`)

try {
main()
} catch (error) {
console.error(error instanceof Error ? error.message : String(error))
process.exit(1)
}
const result = childProcess.spawnSync(binary, process.argv.slice(2), {
stdio: "inherit",
windowsHide: true,
})
if (result.error) fail(result.error.message)
if (result.signal) process.kill(process.pid, result.signal)
process.exit(result.status ?? 1)
16 changes: 3 additions & 13 deletions packages/cli/script/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,14 @@ async function publish(dir: string, name: string, version: string) {

async function prepareDistribution(input: ForkDistribution) {
console.log(input.name, "binaries", input.binaries)
await $`rm -rf ${input.root}/${input.name}`
await $`mkdir -p ${input.root}/${input.name}/bin`
await $`cp ./script/postinstall.mjs ${input.root}/${input.name}/postinstall.mjs`
await Bun.file(`${input.root}/${input.name}/bin/${input.binary}.exe`).write(
[
`echo "Error: ${input.name}'s postinstall script was not run." >&2`,
'echo "" >&2',
'echo "This occurs when installation scripts are disabled." >&2',
'echo "Run the package postinstall script or reinstall with scripts enabled." >&2',
"exit 1",
"",
].join("\n"),
)
await $`cp ./script/launcher.mjs ${input.root}/${input.name}/bin/launcher.mjs`
await Bun.file(`${input.root}/${input.name}/package.json`).write(
JSON.stringify(
{
name: input.name,
bin: { [input.binary]: `./bin/${input.binary}.exe` },
scripts: { postinstall: "node ./postinstall.mjs" },
bin: { [input.binary]: "./bin/launcher.mjs" },
version: input.version,
license: pkg.license,
repository: { type: "git", url: "git+https://github.com/Latitudes-Dev/shuvcode.git" },
Expand Down
48 changes: 48 additions & 0 deletions packages/cli/test/launcher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { afterEach, expect, test } from "bun:test"
import { chmod, copyFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
import os from "node:os"
import path from "node:path"

const directories: string[] = []

afterEach(async () => {
await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })))
})

test.skipIf(process.platform === "win32")("launches the installed platform binary without lifecycle scripts", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "shuvcode-launcher-"))
directories.push(root)
const platform = process.platform === "darwin" || process.platform === "linux" ? process.platform : undefined
if (!platform) throw new Error(`Unsupported test platform: ${process.platform}`)
const arch = process.arch === "x64" || process.arch === "arm64" ? process.arch : undefined
if (!arch) throw new Error(`Unsupported test architecture: ${process.arch}`)
const dependency = `shuvcode-${platform}-${arch}`
const packageRoot = path.join(root, "node_modules", dependency)

await mkdir(path.join(root, "bin"), { recursive: true })
await mkdir(path.join(packageRoot, "bin"), { recursive: true })
await copyFile(path.join(import.meta.dir, "../script/launcher.mjs"), path.join(root, "bin", "launcher.mjs"))
await writeFile(
path.join(root, "package.json"),
JSON.stringify({
name: "shuvcode",
bin: { shuvcode: "./bin/launcher.mjs" },
optionalDependencies: { [dependency]: "2.0.0-alpha-2" },
}),
)
await writeFile(path.join(packageRoot, "package.json"), JSON.stringify({ name: dependency, version: "2.0.0-alpha-2" }))
await writeFile(
path.join(packageRoot, "bin", "shuvcode"),
'#!/usr/bin/env node\nconsole.log(JSON.stringify(process.argv.slice(2)))\nprocess.exit(23)\n',
)
await chmod(path.join(packageRoot, "bin", "shuvcode"), 0o755)

const child = Bun.spawn(["node", path.join(root, "bin", "launcher.mjs"), "hello", "two words"], {
stdout: "pipe",
stderr: "pipe",
})

expect(await child.exited).toBe(23)
expect(await new Response(child.stdout).text()).toBe('["hello","two words"]\n')
expect(await new Response(child.stderr).text()).toBe("")
})
Loading