diff options
author | 2023-02-01 10:27:04 -0800 | |
---|---|---|
committer | 2023-02-01 10:28:01 -0800 | |
commit | 73d6c888b9d4a9ac94452253d924f7ccea069429 (patch) | |
tree | da5c67a32b9547a057b50dabd0e155e35dcfa03d | |
parent | 661fca9cde8bd82fa848944e0c1e0d6f6f12d403 (diff) | |
download | bun-73d6c888b9d4a9ac94452253d924f7ccea069429.tar.gz bun-73d6c888b9d4a9ac94452253d924f7ccea069429.tar.zst bun-73d6c888b9d4a9ac94452253d924f7ccea069429.zip |
Add bun-release package
-rw-r--r-- | .github/workflows/bun-linux-build.yml | 35 | ||||
-rw-r--r-- | .github/workflows/bun-mac-aarch64.yml | 35 | ||||
-rw-r--r-- | .github/workflows/bun-mac-x64-baseline.yml | 35 | ||||
-rw-r--r-- | .github/workflows/bun-mac-x64.yml | 35 | ||||
-rw-r--r-- | .github/workflows/bun-release.yml | 6 | ||||
-rw-r--r-- | .scripts/sign-release.ts | 93 | ||||
-rwxr-xr-x | packages/bun-npm/bun.lockb | bin | 14600 -> 0 bytes | |||
-rw-r--r-- | packages/bun-npm/scripts/npm-build.ts | 256 | ||||
-rw-r--r-- | packages/bun-npm/src/util.ts | 212 | ||||
-rw-r--r-- | packages/bun-release/.gitignore (renamed from packages/bun-npm/.gitignore) | 0 | ||||
-rw-r--r-- | packages/bun-release/.npmrc (renamed from packages/bun-npm/.npmrc) | 0 | ||||
-rw-r--r-- | packages/bun-release/README.md (renamed from packages/bun-npm/README.md) | 4 | ||||
-rwxr-xr-x | packages/bun-release/bun.lockb | bin | 0 -> 22796 bytes | |||
-rw-r--r-- | packages/bun-release/npm/@oven/bun-darwin-aarch64/README.md (renamed from packages/bun-npm/npm/@oven/bun-darwin-aarch64/README.md) | 0 | ||||
-rw-r--r-- | packages/bun-release/npm/@oven/bun-darwin-aarch64/package.json (renamed from packages/bun-npm/npm/@oven/bun-darwin-aarch64/package.json) | 2 | ||||
-rw-r--r-- | packages/bun-release/npm/@oven/bun-darwin-x64-baseline/README.md (renamed from packages/bun-npm/npm/@oven/bun-darwin-x64-baseline/README.md) | 0 | ||||
-rw-r--r-- | packages/bun-release/npm/@oven/bun-darwin-x64-baseline/package.json (renamed from packages/bun-npm/npm/@oven/bun-darwin-x64-baseline/package.json) | 2 | ||||
-rw-r--r-- | packages/bun-release/npm/@oven/bun-darwin-x64/README.md (renamed from packages/bun-npm/npm/@oven/bun-darwin-x64/README.md) | 0 | ||||
-rw-r--r-- | packages/bun-release/npm/@oven/bun-darwin-x64/package.json (renamed from packages/bun-npm/npm/@oven/bun-darwin-x64/package.json) | 2 | ||||
-rw-r--r-- | packages/bun-release/npm/@oven/bun-linux-aarch64/README.md (renamed from packages/bun-npm/npm/@oven/bun-linux-aarch64/README.md) | 0 | ||||
-rw-r--r-- | packages/bun-release/npm/@oven/bun-linux-aarch64/package.json (renamed from packages/bun-npm/npm/@oven/bun-linux-aarch64/package.json) | 2 | ||||
-rw-r--r-- | packages/bun-release/npm/@oven/bun-linux-x64-baseline/README.md (renamed from packages/bun-npm/npm/@oven/bun-linux-x64-baseline/README.md) | 0 | ||||
-rw-r--r-- | packages/bun-release/npm/@oven/bun-linux-x64-baseline/package.json (renamed from packages/bun-npm/npm/@oven/bun-linux-x64-baseline/package.json) | 2 | ||||
-rw-r--r-- | packages/bun-release/npm/@oven/bun-linux-x64/README.md (renamed from packages/bun-npm/npm/@oven/bun-linux-x64/README.md) | 0 | ||||
-rw-r--r-- | packages/bun-release/npm/@oven/bun-linux-x64/package.json (renamed from packages/bun-npm/npm/@oven/bun-linux-x64/package.json) | 2 | ||||
-rw-r--r-- | packages/bun-release/npm/bun/README.md (renamed from packages/bun-npm/npm/bun/README.md) | 0 | ||||
-rw-r--r-- | packages/bun-release/npm/bun/package.json (renamed from packages/bun-npm/npm/bun/package.json) | 14 | ||||
-rw-r--r-- | packages/bun-release/package.json (renamed from packages/bun-npm/package.json) | 13 | ||||
-rw-r--r-- | packages/bun-release/scripts/npm-exec.ts (renamed from packages/bun-npm/scripts/npm-exec.ts) | 2 | ||||
-rw-r--r-- | packages/bun-release/scripts/npm-postinstall.ts (renamed from packages/bun-npm/scripts/npm-postinstall.ts) | 2 | ||||
-rw-r--r-- | packages/bun-release/scripts/upload-assets.ts | 113 | ||||
-rw-r--r-- | packages/bun-release/scripts/upload-npm.ts | 191 | ||||
-rw-r--r-- | packages/bun-release/src/console.ts | 77 | ||||
-rw-r--r-- | packages/bun-release/src/fetch.ts | 73 | ||||
-rw-r--r-- | packages/bun-release/src/fs.ts | 166 | ||||
-rw-r--r-- | packages/bun-release/src/github.ts | 113 | ||||
-rw-r--r-- | packages/bun-release/src/npm/install.ts (renamed from packages/bun-npm/src/install.ts) | 66 | ||||
-rw-r--r-- | packages/bun-release/src/platform.ts (renamed from packages/bun-npm/src/platform.ts) | 10 | ||||
-rw-r--r-- | packages/bun-release/src/spawn.ts | 24 | ||||
-rw-r--r-- | packages/bun-release/tsconfig.json (renamed from packages/bun-npm/tsconfig.json) | 6 |
40 files changed, 826 insertions, 767 deletions
diff --git a/.github/workflows/bun-linux-build.yml b/.github/workflows/bun-linux-build.yml index cfe79ef02..39382f22b 100644 --- a/.github/workflows/bun-linux-build.yml +++ b/.github/workflows/bun-linux-build.yml @@ -146,38 +146,3 @@ jobs: name: "Canary (${{github.sha}})" tag: "canary" artifacts: "${{runner.temp}}/release/bun-${{matrix.tag}}.zip,${{runner.temp}}/release/bun-${{matrix.tag}}-profile.zip" - - id: setup-bun - name: Setup Bun - uses: oven-sh/setup-bun@v0.1.8 - if: | - github.repository_owner == 'oven-sh' - && github.ref == 'refs/heads/main' - with: - bun-version: canary - github-token: ${{ secrets.GITHUB_TOKEN }} - # - name: Sign Release - # id: sign-release - # if: | - # github.repository_owner == 'oven-sh' - # && github.ref == 'refs/heads/main' - # env: - # GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - # run: | - # echo "$GPG_PASSPHRASE" | bun run .scripts/sign-release.ts - # - name: Release Checksum - # id: release-checksum - # uses: ncipollo/release-action@v1 - # if: | - # github.repository_owner == 'oven-sh' - # && github.ref == 'refs/heads/main' - # with: - # prerelease: true - # body: "This canary release of Bun corresponds to the commit [${{ github.sha }}]" - # allowUpdates: true - # replacesArtifacts: true - # generateReleaseNotes: true - # artifactErrorsFailBuild: true - # token: ${{ secrets.GITHUB_TOKEN }} - # name: "Canary (${{github.sha}})" - # tag: "canary" - # artifacts: "SHASUMS256.txt,SHASUMS256.txt.asc" diff --git a/.github/workflows/bun-mac-aarch64.yml b/.github/workflows/bun-mac-aarch64.yml index 3b2ce89ca..923f30a5c 100644 --- a/.github/workflows/bun-mac-aarch64.yml +++ b/.github/workflows/bun-mac-aarch64.yml @@ -384,38 +384,3 @@ jobs: name: "Canary (${{github.sha}})" tag: "canary" artifacts: "${{runner.temp}}/release/${{matrix.tag}}.zip,${{runner.temp}}/release/${{matrix.tag}}-profile.zip" - - id: setup-bun - name: Setup Bun - uses: oven-sh/setup-bun@v0.1.8 - if: | - github.repository_owner == 'oven-sh' - && github.ref == 'refs/heads/main' - with: - bun-version: canary - github-token: ${{ secrets.GITHUB_TOKEN }} - # - name: Sign Release - # id: sign-release - # if: | - # github.repository_owner == 'oven-sh' - # && github.ref == 'refs/heads/main' - # env: - # GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - # run: | - # echo "$GPG_PASSPHRASE" | bun run .scripts/sign-release.ts - # - name: Release Checksum - # id: release-checksum - # uses: ncipollo/release-action@v1 - # if: | - # github.repository_owner == 'oven-sh' - # && github.ref == 'refs/heads/main' - # with: - # prerelease: true - # body: "This canary release of Bun corresponds to the commit [${{ github.sha }}]" - # allowUpdates: true - # replacesArtifacts: true - # generateReleaseNotes: true - # artifactErrorsFailBuild: true - # token: ${{ secrets.GITHUB_TOKEN }} - # name: "Canary (${{github.sha}})" - # tag: "canary" - # artifacts: "SHASUMS256.txt,SHASUMS256.txt.asc" diff --git a/.github/workflows/bun-mac-x64-baseline.yml b/.github/workflows/bun-mac-x64-baseline.yml index b7261b7d7..f28d10947 100644 --- a/.github/workflows/bun-mac-x64-baseline.yml +++ b/.github/workflows/bun-mac-x64-baseline.yml @@ -388,38 +388,3 @@ jobs: name: "Canary (${{github.sha}})" tag: "canary" artifacts: "${{runner.temp}}/release/${{matrix.tag}}.zip,${{runner.temp}}/release/${{matrix.tag}}-profile.zip" - - id: setup-bun - name: Setup Bun - uses: oven-sh/setup-bun@v0.1.8 - if: | - github.repository_owner == 'oven-sh' - && github.ref == 'refs/heads/main' - with: - bun-version: canary - github-token: ${{ secrets.GITHUB_TOKEN }} - # - name: Sign Release - # id: sign-release - # if: | - # github.repository_owner == 'oven-sh' - # && github.ref == 'refs/heads/main' - # env: - # GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - # run: | - # echo "$GPG_PASSPHRASE" | bun run .scripts/sign-release.ts - # - name: Release Checksum - # id: release-checksum - # uses: ncipollo/release-action@v1 - # if: | - # github.repository_owner == 'oven-sh' - # && github.ref == 'refs/heads/main' - # with: - # prerelease: true - # body: "This canary release of Bun corresponds to the commit [${{ github.sha }}]" - # allowUpdates: true - # replacesArtifacts: true - # generateReleaseNotes: true - # artifactErrorsFailBuild: true - # token: ${{ secrets.GITHUB_TOKEN }} - # name: "Canary (${{github.sha}})" - # tag: "canary" - # artifacts: "SHASUMS256.txt,SHASUMS256.txt.asc" diff --git a/.github/workflows/bun-mac-x64.yml b/.github/workflows/bun-mac-x64.yml index f094c7b1e..67e07ffe0 100644 --- a/.github/workflows/bun-mac-x64.yml +++ b/.github/workflows/bun-mac-x64.yml @@ -390,38 +390,3 @@ jobs: name: "Canary (${{github.sha}})" tag: "canary" artifacts: "${{runner.temp}}/release/${{matrix.tag}}.zip,${{runner.temp}}/release/${{matrix.tag}}-profile.zip" - - id: setup-bun - name: Setup Bun - uses: oven-sh/setup-bun@v0.1.8 - if: | - github.repository_owner == 'oven-sh' - && github.ref == 'refs/heads/main' - with: - bun-version: canary - github-token: ${{ secrets.GITHUB_TOKEN }} - # - name: Sign Release - # id: sign-release - # if: | - # github.repository_owner == 'oven-sh' - # && github.ref == 'refs/heads/main' - # env: - # GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - # run: | - # echo "$GPG_PASSPHRASE" | bun run .scripts/sign-release.ts - # - name: Release Checksum - # id: release-checksum - # uses: ncipollo/release-action@v1 - # if: | - # github.repository_owner == 'oven-sh' - # && github.ref == 'refs/heads/main' - # with: - # prerelease: true - # body: "This canary release of Bun corresponds to the commit [${{ github.sha }}]" - # allowUpdates: true - # replacesArtifacts: true - # generateReleaseNotes: true - # artifactErrorsFailBuild: true - # token: ${{ secrets.GITHUB_TOKEN }} - # name: "Canary (${{github.sha}})" - # tag: "canary" - # artifacts: "SHASUMS256.txt,SHASUMS256.txt.asc" diff --git a/.github/workflows/bun-release.yml b/.github/workflows/bun-release.yml index 141725379..b25c0d17b 100644 --- a/.github/workflows/bun-release.yml +++ b/.github/workflows/bun-release.yml @@ -4,7 +4,6 @@ on: release: types: - published - - edited # canary only workflow_dispatch: inputs: tag: @@ -17,7 +16,7 @@ jobs: runs-on: ubuntu-latest defaults: run: - working-directory: packages/bun-npm + working-directory: packages/bun-release steps: - id: checkout name: Checkout @@ -41,7 +40,7 @@ jobs: run: bun install - id: bun-run name: Release - run: bun run npm -- "${{ env.TAG }}" publish + run: bun upload-npm -- "${{ env.TAG }}" publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} @@ -73,7 +72,6 @@ jobs: uses: docker/metadata-action@v4 with: images: oven/bun - flavor: latest=false # TODO tags: | type=match,pattern=(bun-v)?(canary|\d.\d.\d),group=2,value=${{ env.TAG }} type=match,pattern=(bun-v)?(canary|\d.\d.\d),group=2,value=${{ env.TAG }} diff --git a/.scripts/sign-release.ts b/.scripts/sign-release.ts deleted file mode 100644 index 9ff189659..000000000 --- a/.scripts/sign-release.ts +++ /dev/null @@ -1,93 +0,0 @@ -/// <reference types="bun-types" /> -import { SHA256, which, write, spawnSync } from "bun"; -import { isatty } from "node:tty"; -import { createInterface } from "node:readline"; - -const tag = process.argv[2]; -const url = tag - ? `https://api.github.com/repos/oven-sh/bun/releases/tags/${tag}` - : "https://api.github.com/repos/oven-sh/bun/releases/latest"; -const response = await fetch(url); -if (response.status === 404) { - throw new Error(`Release not found: ${tag}`); -} -if (!response.ok) { - throw new Error(`Failed to find release: ${tag} [status: ${response.status}]`); -} -const release: any = await response.json(); -if (release.assets.find(({ name }) => name === "SHA256SUMS.txt.asc")) { - throw new Error(`Release already signed: ${tag}`); -} -const sha256s = await Promise.all( - release.assets.map(async ({ name, browser_download_url }) => { - return `${await sha256(browser_download_url)} ${name}`; - }), -); -await write("SHASUMS256.txt", sha256s.join("\n")); -await sign("SHASUMS256.txt"); - -async function sha256(url: string): Promise<string> { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to find asset: ${url} [status: ${response.status}]`); - } - const body = await response.arrayBuffer(); - const sha256 = SHA256.hash(body); - return Buffer.from(sha256).toString("hex"); -} - -async function sign(path: string): Promise<void> { - // https://www.gnupg.org/gph/en/manual/x135.html - if (!which("gpg")) { - throw new Error("Command not found: gpg"); - } - const { stdout } = spawnSync( - [ - "gpg", - "--list-secret-keys", - "--keyid-format", - "long" - ], - { - stdout: "pipe", - stderr: "pipe", - } - ); - if (!stdout.includes("F3DCC08A8572C0749B3E18888EAB4D40A7B22B59")) { - console.warn("Signature is likely wrong, key not found: robobun@oven.sh"); - } - const passphrase = await prompt("Passphrase:"); - spawnSync( - [ - "gpg", - "--batch", - "--yes", - "--clearsign", - "--output", - `${path}.asc`, - path - ], - { - stdin: new TextEncoder().encode(passphrase), - stdout: "inherit", - stderr: "inherit", - } - ); -} - -async function prompt(question: string): Promise<string> { - if (isatty(process.stdout.fd)) { - return globalThis.prompt(question) || ""; - } - const reader = createInterface({ - input: process.stdin, - terminal: false - }); - let buffer = ""; - reader.on("line", (line) => { - buffer += line; - }); - return new Promise((resolve) => { - reader.once("close", () => resolve(buffer)); - }); -} diff --git a/packages/bun-npm/bun.lockb b/packages/bun-npm/bun.lockb Binary files differdeleted file mode 100755 index 41a60a1ec..000000000 --- a/packages/bun-npm/bun.lockb +++ /dev/null diff --git a/packages/bun-npm/scripts/npm-build.ts b/packages/bun-npm/scripts/npm-build.ts deleted file mode 100644 index 9f3547565..000000000 --- a/packages/bun-npm/scripts/npm-build.ts +++ /dev/null @@ -1,256 +0,0 @@ -import type { Endpoints } from "@octokit/types"; -import { copy, exists, fetch, spawn } from "../src/util"; -import type { JSZipObject } from "jszip"; -import { loadAsync } from "jszip"; -import { join } from "node:path"; -import { chmod, read, write } from "../src/util"; -import type { BuildOptions } from "esbuild"; -import { buildSync, formatMessagesSync } from "esbuild"; -import type { Platform } from "../src/platform"; -import { platforms } from "../src/platform"; - -type Release = - Endpoints["GET /repos/{owner}/{repo}/releases/latest"]["response"]["data"]; - -const npmPackage = "bun"; -const npmOwner = "@oven"; -let npmVersion: string; - -const [tag, action] = process.argv.slice(2); - -await build(tag); -if (action === "publish") { - await publish(); -} else if (action === "dry-run") { - await publish(true); -} else if (action) { - throw new Error(`Unknown action: ${action}`); -} - -async function build(version: string): Promise<void> { - const release = await getRelease(version); - if (release.tag_name === "canary") { - npmVersion = await getCanaryVersion(); - } else { - npmVersion = release.tag_name.replace("bun-v", ""); - } - await buildBasePackage(); - for (const platform of platforms) { - await buildPackage(release, platform); - } -} - -async function publish(dryRun?: boolean): Promise<void> { - const npmPackages = platforms.map(({ bin }) => `${npmOwner}/${bin}`); - npmPackages.push(npmPackage); - for (const npmPackage of npmPackages) { - publishPackage(npmPackage, dryRun); - } -} - -async function buildBasePackage() { - const done = log("Building:", `${npmPackage}@${npmVersion}`); - const cwd = join("npm", npmPackage); - const define = { - npmVersion: `"${npmVersion}"`, - npmPackage: `"${npmPackage}"`, - npmOwner: `"${npmOwner}"`, - }; - buildJs(join("scripts", "npm-postinstall.ts"), join(cwd, "install.js"), { - define, - }); - buildJs(join("scripts", "npm-exec.ts"), join(cwd, "bin", "bun"), { - define, - banner: { - js: "#!/usr/bin/env node", - }, - }); - const os = [...new Set(platforms.map(({ os }) => os))]; - const cpu = [...new Set(platforms.map(({ arch }) => arch))]; - patchJson(join(cwd, "package.json"), { - name: npmPackage, - version: npmVersion, - scripts: { - postinstall: "node install.js", - }, - optionalDependencies: Object.fromEntries( - platforms.map(({ bin }) => [`${npmOwner}/${bin}`, npmVersion]), - ), - bin: { - bun: "bin/bun", - bunx: "bin/bun", - }, - os, - cpu, - }); - if (exists(".npmrc")) { - copy(".npmrc", join(cwd, ".npmrc")); - } - done(); -} - -async function buildPackage( - release: Release, - { bin, exe, os, arch }: Platform, -): Promise<void> { - const npmPackage = `${npmOwner}/${bin}`; - const done = log("Building:", `${npmPackage}@${npmVersion}`); - const asset = release.assets.find(({ name }) => name === `${bin}.zip`); - if (!asset) { - console.warn(`No asset found: ${bin}`); - return; - } - const bun = await extractFromZip(asset.browser_download_url, `${bin}/bun`); - const cwd = join("npm", npmPackage); - write(join(cwd, exe), await bun.async("arraybuffer")); - chmod(join(cwd, exe), 0o755); - patchJson(join(cwd, "package.json"), { - name: npmPackage, - version: npmVersion, - preferUnplugged: true, - os: [os], - cpu: [arch], - }); - if (exists(".npmrc")) { - copy(".npmrc", join(cwd, ".npmrc")); - } - done(); -} - -function publishPackage(name: string, dryRun?: boolean): void { - const done = log( - dryRun ? "Dry-run Publishing:" : "Publishing:", - `${name}@${npmVersion}`, - ); - const { exitCode, stdout, stderr } = spawn( - "npm", - [ - "publish", - "--access", - "public", - "--tag", - npmVersion.includes("canary") ? "canary" : "latest", - ...(dryRun ? ["--dry-run"] : []), - ], - { - cwd: join("npm", name), - }, - ); - if (exitCode === 0) { - done(); - return; - } - console.warn(stdout || stderr); -} - -async function extractFromZip( - url: string, - filename: string, -): Promise<JSZipObject> { - const response = await fetch(url); - const buffer = await response.arrayBuffer(); - const zip = await loadAsync(buffer); - for (const [name, file] of Object.entries(zip.files)) { - if (!file.dir && name.startsWith(filename)) { - return file; - } - } - console.warn("Found files:", Object.keys(zip.files)); - throw new Error(`File not found: ${filename}`); -} - -async function getRelease(version?: string | null): Promise<Release> { - const response = await fetchGithub( - version ? `releases/tags/${formatTag(version)}` : `releases/latest`, - ); - return response.json(); -} - -async function getSha(version: string): Promise<string> { - const response = await fetchGithub(`git/ref/tags/${formatTag(version)}`); - const { - object, - }: Endpoints["GET /repos/{owner}/{repo}/git/ref/{ref}"]["response"]["data"] = - await response.json(); - return object.sha.substring(0, 7); -} - -async function fetchGithub(path: string) { - const headers = new Headers(); - const token = process.env.GITHUB_TOKEN; - if (token) { - headers.set("Authorization", `Bearer ${token}`); - } - const url = new URL(path, "https://api.github.com/repos/oven-sh/bun/"); - return fetch(url.toString()); -} - -async function getCanaryVersion(): Promise<string> { - const date = new Date().toISOString().split("T")[0].replace(/-/g, ""); - const semver = `${Bun.version}-canary.${date}`; - try { - const sha = await getSha("canary"); - const response = await fetch( - `https://registry.npmjs.org/-/package/${npmPackage}/dist-tags`, - ); - const { canary }: { canary: string } = await response.json(); - if (canary.startsWith(semver)) { - const match = /canary.[0-9]{8}\.([0-9]+)+?/.exec(canary); - const build = 1 + (match ? parseInt(match[1]) : 0); - return `${semver}.${build}+${sha}`; - } - return `${semver}.1+${sha}`; - } catch (error) { - console.warn("Failed to calculate canary version", error); - } - return `${semver}.1`; -} - -function formatTag(version: string): string { - if (version.includes("canary") || version.startsWith("bun-v")) { - return version; - } - return `bun-v${version}`; -} - -function patchJson(path: string, patch: object): void { - let value; - try { - const existing = JSON.parse(read(path)); - value = { - ...existing, - ...patch, - }; - } catch { - value = patch; - } - write(path, `${JSON.stringify(value, undefined, 2)}\n`); -} - -function buildJs(src: string, dst: string, options: BuildOptions = {}): void { - const { errors } = buildSync({ - bundle: true, - treeShaking: true, - keepNames: true, - minifySyntax: true, - pure: ["console.debug"], - platform: "node", - target: "es6", - format: "cjs", - entryPoints: [src], - outfile: dst, - ...options, - }); - if (errors?.length) { - const messages = formatMessagesSync(errors, { kind: "error" }); - throw new Error(messages.join("\n")); - } -} - -function log(...args: any[]): () => void { - console.write(Bun.inspect(...args)); - const start = Date.now(); - return () => { - console.write(` [${(Date.now() - start).toFixed()} ms]\n`); - }; -} diff --git a/packages/bun-npm/src/util.ts b/packages/bun-npm/src/util.ts deleted file mode 100644 index e2cf19ee0..000000000 --- a/packages/bun-npm/src/util.ts +++ /dev/null @@ -1,212 +0,0 @@ -import fs from "fs"; -import path, { dirname } from "path"; -import { tmpdir } from "os"; -import child_process from "child_process"; - -if (process.env["DEBUG"] !== "1") { - console.debug = () => {}; -} - -export function join(...paths: (string | string[])[]): string { - return path.join(...paths.flat(2)); -} - -export function tmp(): string { - const path = fs.mkdtempSync(join(tmpdir(), "bun-")); - console.debug("tmp", path); - return path; -} - -export function rm(path: string): void { - console.debug("rm", path); - try { - fs.rmSync(path, { recursive: true }); - return; - } catch (error) { - console.debug("rmSync failed", error); - // Did not exist before Node.js v14. - // Attempt again with older, slower implementation. - } - let stats: fs.Stats; - try { - stats = fs.lstatSync(path); - } catch (error) { - console.debug("lstatSync failed", error); - // The file was likely deleted, so return early. - return; - } - if (!stats.isDirectory()) { - fs.unlinkSync(path); - return; - } - try { - fs.rmdirSync(path, { recursive: true }); - return; - } catch (error) { - console.debug("rmdirSync failed", error); - // Recursive flag did not exist before Node.js X. - // Attempt again with older, slower implementation. - } - for (const filename of fs.readdirSync(path)) { - rm(join(path, filename)); - } - fs.rmdirSync(path); -} - -export function rename(path: string, newPath: string): void { - console.debug("rename", path, newPath); - try { - fs.renameSync(path, newPath); - return; - } catch (error) { - console.debug("renameSync failed", error); - // If there is an error, delete the new path and try again. - } - try { - rm(newPath); - } catch (error) { - console.debug("rm failed", error); - // The path could have been deleted already. - } - fs.renameSync(path, newPath); -} - -export function write( - path: string, - content: string | ArrayBuffer | ArrayBufferView, -): void { - console.debug("write", path); - try { - fs.writeFileSync(path, content); - return; - } catch (error) { - console.debug("writeFileSync failed", error); - // If there is an error, ensure the parent directory - // exists and try again. - try { - fs.mkdirSync(dirname(path), { recursive: true }); - } catch (error) { - console.debug("mkdirSync failed", error); - // The directory could have been created already. - } - fs.writeFileSync(path, content); - } -} - -export function read(path: string): string { - console.debug("read", path); - return fs.readFileSync(path, "utf-8"); -} - -export function chmod(path: string, mode: fs.Mode): void { - console.debug("chmod", path, mode); - fs.chmodSync(path, mode); -} - -export function copy(path: string, newPath: string): void { - console.debug("copy", path, newPath); - try { - fs.copyFileSync(path, newPath); - return; - } catch (error) { - console.debug("copyFileSync failed", error); - } - write(newPath, read(path)); -} - -export function exists(path: string): boolean { - console.debug("exists", path); - try { - return fs.existsSync(path); - } catch (error) { - console.debug("existsSync failed", error); - } - return false; -} - -export function spawn( - cmd: string, - args: string[], - options: child_process.SpawnOptions = {}, -): { - exitCode: number; - stdout: string; - stderr: string; -} { - console.debug("spawn", [cmd, ...args].join(" ")); - const { status, stdout, stderr } = child_process.spawnSync(cmd, args, { - stdio: "pipe", - encoding: "utf-8", - ...options, - }); - return { - exitCode: status ?? 1, - stdout, - stderr, - }; -} - -export type Response = { - readonly status: number; - arrayBuffer(): Promise<ArrayBuffer>; - json<T>(): Promise<T>; -}; - -export const fetch = "fetch" in globalThis ? webFetch : nodeFetch; - -async function webFetch(url: string, assert?: boolean): Promise<Response> { - const response = await globalThis.fetch(url); - console.debug("fetch", url, response.status); - if (assert !== false && !isOk(response.status)) { - throw new Error(`${response.status}: ${url}`); - } - return response; -} - -async function nodeFetch(url: string, assert?: boolean): Promise<Response> { - const { get } = await import("node:http"); - return new Promise((resolve, reject) => { - get(url, (response) => { - console.debug("get", url, response.statusCode); - const status = response.statusCode ?? 501; - if (response.headers.location && isRedirect(status)) { - return nodeFetch(url).then(resolve, reject); - } - if (assert !== false && !isOk(status)) { - return reject(new Error(`${status}: ${url}`)); - } - const body: Buffer[] = []; - response.on("data", (chunk) => { - body.push(chunk); - }); - response.on("end", () => { - resolve({ - status, - async arrayBuffer() { - return Buffer.concat(body).buffer as ArrayBuffer; - }, - async json() { - const text = Buffer.concat(body).toString("utf-8"); - return JSON.parse(text); - }, - }); - }); - }).on("error", reject); - }); -} - -function isOk(status: number): boolean { - return status === 200; -} - -function isRedirect(status: number): boolean { - switch (status) { - case 301: // Moved Permanently - case 308: // Permanent Redirect - case 302: // Found - case 307: // Temporary Redirect - case 303: // See Other - return true; - } - return false; -} diff --git a/packages/bun-npm/.gitignore b/packages/bun-release/.gitignore index 4c021cb09..4c021cb09 100644 --- a/packages/bun-npm/.gitignore +++ b/packages/bun-release/.gitignore diff --git a/packages/bun-npm/.npmrc b/packages/bun-release/.npmrc index aa0ab1f07..aa0ab1f07 100644 --- a/packages/bun-npm/.npmrc +++ b/packages/bun-release/.npmrc diff --git a/packages/bun-npm/README.md b/packages/bun-release/README.md index 4556f0f4d..9a01bdbfd 100644 --- a/packages/bun-npm/README.md +++ b/packages/bun-release/README.md @@ -1,6 +1,6 @@ -# bun-npm +# bun-release -Scripts that allow Bun to be installed with `npm install`. +Scripts that release Bun to npm, Dockerhub, Homebrew, etc. ### Running diff --git a/packages/bun-release/bun.lockb b/packages/bun-release/bun.lockb Binary files differnew file mode 100755 index 000000000..df506ae60 --- /dev/null +++ b/packages/bun-release/bun.lockb diff --git a/packages/bun-npm/npm/@oven/bun-darwin-aarch64/README.md b/packages/bun-release/npm/@oven/bun-darwin-aarch64/README.md index 3b03fdd9f..3b03fdd9f 100644 --- a/packages/bun-npm/npm/@oven/bun-darwin-aarch64/README.md +++ b/packages/bun-release/npm/@oven/bun-darwin-aarch64/README.md diff --git a/packages/bun-npm/npm/@oven/bun-darwin-aarch64/package.json b/packages/bun-release/npm/@oven/bun-darwin-aarch64/package.json index 46da9b168..701f4c92b 100644 --- a/packages/bun-npm/npm/@oven/bun-darwin-aarch64/package.json +++ b/packages/bun-release/npm/@oven/bun-darwin-aarch64/package.json @@ -1,6 +1,6 @@ { "name": "@oven/bun-darwin-aarch64", - "version": "0.5.1", + "version": "0.5.3", "description": "This is the macOS arm64 binary for Bun, a fast all-in-one JavaScript runtime.", "homepage": "https://bun.sh", "bugs": "https://github.com/oven-sh/issues", diff --git a/packages/bun-npm/npm/@oven/bun-darwin-x64-baseline/README.md b/packages/bun-release/npm/@oven/bun-darwin-x64-baseline/README.md index e451cccfe..e451cccfe 100644 --- a/packages/bun-npm/npm/@oven/bun-darwin-x64-baseline/README.md +++ b/packages/bun-release/npm/@oven/bun-darwin-x64-baseline/README.md diff --git a/packages/bun-npm/npm/@oven/bun-darwin-x64-baseline/package.json b/packages/bun-release/npm/@oven/bun-darwin-x64-baseline/package.json index a9a330f42..b14492c1c 100644 --- a/packages/bun-npm/npm/@oven/bun-darwin-x64-baseline/package.json +++ b/packages/bun-release/npm/@oven/bun-darwin-x64-baseline/package.json @@ -1,6 +1,6 @@ { "name": "@oven/bun-darwin-x64-baseline", - "version": "0.5.1", + "version": "0.5.3", "description": "This is the macOS x64 binary for Bun, a fast all-in-one JavaScript runtime.", "homepage": "https://bun.sh", "bugs": "https://github.com/oven-sh/issues", diff --git a/packages/bun-npm/npm/@oven/bun-darwin-x64/README.md b/packages/bun-release/npm/@oven/bun-darwin-x64/README.md index be73fd7a2..be73fd7a2 100644 --- a/packages/bun-npm/npm/@oven/bun-darwin-x64/README.md +++ b/packages/bun-release/npm/@oven/bun-darwin-x64/README.md diff --git a/packages/bun-npm/npm/@oven/bun-darwin-x64/package.json b/packages/bun-release/npm/@oven/bun-darwin-x64/package.json index d83283425..82934da0a 100644 --- a/packages/bun-npm/npm/@oven/bun-darwin-x64/package.json +++ b/packages/bun-release/npm/@oven/bun-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@oven/bun-darwin-x64", - "version": "0.5.1", + "version": "0.5.3", "description": "This is the macOS x64 binary for Bun, a fast all-in-one JavaScript runtime.", "homepage": "https://bun.sh", "bugs": "https://github.com/oven-sh/issues", diff --git a/packages/bun-npm/npm/@oven/bun-linux-aarch64/README.md b/packages/bun-release/npm/@oven/bun-linux-aarch64/README.md index e727d8008..e727d8008 100644 --- a/packages/bun-npm/npm/@oven/bun-linux-aarch64/README.md +++ b/packages/bun-release/npm/@oven/bun-linux-aarch64/README.md diff --git a/packages/bun-npm/npm/@oven/bun-linux-aarch64/package.json b/packages/bun-release/npm/@oven/bun-linux-aarch64/package.json index c2feaf708..4f68fec41 100644 --- a/packages/bun-npm/npm/@oven/bun-linux-aarch64/package.json +++ b/packages/bun-release/npm/@oven/bun-linux-aarch64/package.json @@ -1,6 +1,6 @@ { "name": "@oven/bun-linux-aarch64", - "version": "0.5.1", + "version": "0.5.3", "description": "This is the Linux arm64 binary for Bun, a fast all-in-one JavaScript runtime.", "homepage": "https://bun.sh", "bugs": "https://github.com/oven-sh/issues", diff --git a/packages/bun-npm/npm/@oven/bun-linux-x64-baseline/README.md b/packages/bun-release/npm/@oven/bun-linux-x64-baseline/README.md index 6b2ce0fcf..6b2ce0fcf 100644 --- a/packages/bun-npm/npm/@oven/bun-linux-x64-baseline/README.md +++ b/packages/bun-release/npm/@oven/bun-linux-x64-baseline/README.md diff --git a/packages/bun-npm/npm/@oven/bun-linux-x64-baseline/package.json b/packages/bun-release/npm/@oven/bun-linux-x64-baseline/package.json index e3333b134..1767be0dc 100644 --- a/packages/bun-npm/npm/@oven/bun-linux-x64-baseline/package.json +++ b/packages/bun-release/npm/@oven/bun-linux-x64-baseline/package.json @@ -1,6 +1,6 @@ { "name": "@oven/bun-linux-x64-baseline", - "version": "0.5.1", + "version": "0.5.3", "description": "This is the Linux x64 binary for Bun, a fast all-in-one JavaScript runtime.", "homepage": "https://bun.sh", "bugs": "https://github.com/oven-sh/issues", diff --git a/packages/bun-npm/npm/@oven/bun-linux-x64/README.md b/packages/bun-release/npm/@oven/bun-linux-x64/README.md index e0fac1347..e0fac1347 100644 --- a/packages/bun-npm/npm/@oven/bun-linux-x64/README.md +++ b/packages/bun-release/npm/@oven/bun-linux-x64/README.md diff --git a/packages/bun-npm/npm/@oven/bun-linux-x64/package.json b/packages/bun-release/npm/@oven/bun-linux-x64/package.json index 71228ad14..16f9309f5 100644 --- a/packages/bun-npm/npm/@oven/bun-linux-x64/package.json +++ b/packages/bun-release/npm/@oven/bun-linux-x64/package.json @@ -1,6 +1,6 @@ { "name": "@oven/bun-linux-x64", - "version": "0.5.1", + "version": "0.5.3", "description": "This is the Linux x64 binary for Bun, a fast all-in-one JavaScript runtime.", "homepage": "https://bun.sh", "bugs": "https://github.com/oven-sh/issues", diff --git a/packages/bun-npm/npm/bun/README.md b/packages/bun-release/npm/bun/README.md index f344b258e..f344b258e 100644 --- a/packages/bun-npm/npm/bun/README.md +++ b/packages/bun-release/npm/bun/README.md diff --git a/packages/bun-npm/npm/bun/package.json b/packages/bun-release/npm/bun/package.json index caf298447..374518b85 100644 --- a/packages/bun-npm/npm/bun/package.json +++ b/packages/bun-release/npm/bun/package.json @@ -1,6 +1,6 @@ { "name": "bun", - "version": "0.5.1", + "version": "0.5.3", "description": "Bun is a fast all-in-one JavaScript runtime.", "keywords": [ "bun", @@ -24,12 +24,12 @@ "postinstall": "node install.js" }, "optionalDependencies": { - "@oven/bun-darwin-aarch64": "0.5.1", - "@oven/bun-darwin-x64": "0.5.1", - "@oven/bun-darwin-x64-baseline": "0.5.1", - "@oven/bun-linux-aarch64": "0.5.1", - "@oven/bun-linux-x64": "0.5.1", - "@oven/bun-linux-x64-baseline": "0.5.1" + "@oven/bun-darwin-aarch64": "0.5.3", + "@oven/bun-darwin-x64": "0.5.3", + "@oven/bun-darwin-x64-baseline": "0.5.3", + "@oven/bun-linux-aarch64": "0.5.3", + "@oven/bun-linux-x64": "0.5.3", + "@oven/bun-linux-x64-baseline": "0.5.3" }, "os": [ "darwin", diff --git a/packages/bun-npm/package.json b/packages/bun-release/package.json index 33de39c3c..6c28a229f 100644 --- a/packages/bun-npm/package.json +++ b/packages/bun-release/package.json @@ -1,15 +1,18 @@ { "private": true, - "dependencies": {}, + "dependencies": { + "@octokit/rest": "^19.0.7", + "esbuild": "^0.17.3", + "jszip": "^3.10.1" + }, "devDependencies": { "@octokit/types": "^8.1.1", "bun-types": "^0.4.0", - "prettier": "^2.8.2", - "esbuild": "^0.17.3", - "jszip": "^3.10.1" + "prettier": "^2.8.2" }, "scripts": { "format": "prettier --write src scripts", - "npm": "bun scripts/npm-build.ts" + "upload-npm": "bun scripts/upload-npm.ts", + "upload-assets": "bun scripts/upload-assets.ts" } } diff --git a/packages/bun-npm/scripts/npm-exec.ts b/packages/bun-release/scripts/npm-exec.ts index 3937247cb..5b054ac86 100644 --- a/packages/bun-npm/scripts/npm-exec.ts +++ b/packages/bun-release/scripts/npm-exec.ts @@ -1,4 +1,4 @@ -import { importBun } from "../src/install"; +import { importBun } from "../src/npm/install"; import { execFileSync } from "child_process"; importBun() diff --git a/packages/bun-npm/scripts/npm-postinstall.ts b/packages/bun-release/scripts/npm-postinstall.ts index c22f2c669..bde044300 100644 --- a/packages/bun-npm/scripts/npm-postinstall.ts +++ b/packages/bun-release/scripts/npm-postinstall.ts @@ -1,4 +1,4 @@ -import { importBun, optimizeBun } from "../src/install"; +import { importBun, optimizeBun } from "../src/npm/install"; importBun() .then((path) => { diff --git a/packages/bun-release/scripts/upload-assets.ts b/packages/bun-release/scripts/upload-assets.ts new file mode 100644 index 000000000..27b7ba01a --- /dev/null +++ b/packages/bun-release/scripts/upload-assets.ts @@ -0,0 +1,113 @@ +import { getRelease, uploadAsset } from "../src/github"; +import { fetch } from "../src/fetch"; +import { spawn } from "../src/spawn"; +import { confirm, exit, log, stdin, warn } from "../src/console"; +import { hash, join, rm, tmp, write, basename, blob } from "../src/fs"; + +const [tag, ...paths] = process.argv.slice(2); + +if (!tag) { + exit("Invalid arguments: [tag] [...assets]"); +} + +const { tag_name, assets } = await getRelease(tag); +log("Release:", tag_name, "\n"); +log("Existing assets:\n", ...assets.map(({ name }) => `- ${name}\n`)); +log("Updating assets:\n", ...paths.map((path) => `+ ${basename(path)}\n`)); +await confirm(); + +log("Hashing assets...\n"); +const existing: Map<string, string> = new Map(); +for (const { name, browser_download_url } of assets) { + if (name.startsWith("SHASUMS256.txt")) { + continue; + } + const response = await fetch(browser_download_url); + const buffer = Buffer.from(await response.arrayBuffer()); + existing.set(name, await hash(buffer)); +} +const updated: Map<string, string> = new Map(); +for (const path of paths) { + const name = basename(path); + updated.set(name, await hash(path)); +} +log( + "Unchanged hashes:\n", + ...Array.from(existing.entries()) + .filter(([name]) => !updated.has(name)) + .map(([name, sha256]) => ` - ${sha256} => ${name}\n`), +); +log( + "Changed hashes:\n", + ...Array.from(updated.entries()).map( + ([name, sha256]) => ` + ${sha256} => ${name}\n`, + ), +); +await confirm(); + +log("Signing assets...\n"); +const cwd = tmp(); +const path = join(cwd, "SHASUMS256.txt"); +const signedPath = `${path}.asc`; +write( + path, + [ + ...Array.from(updated.entries()), + ...Array.from(existing.entries()).filter(([name]) => !updated.has(name)), + ] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([name, sha256]) => `${sha256} ${name}`) + .join("\n"), +); +const { stdout: keys } = spawn("gpg", [ + "--list-secret-keys", + "--keyid-format", + "long", +]); +const verifiedKeys = [ + "F3DCC08A8572C0749B3E18888EAB4D40A7B22B59", // robobun@oven.sh +]; +if (!verifiedKeys.find((key) => keys.includes(key))) { + warn("Signature is probably wrong, key not found: robobun@oven.sh"); +} +const passphrase = await stdin("Passphrase:"); +log(); +const { exitCode, stdout, stderr } = spawn( + "gpg", + [ + "--pinentry-mode", + "loopback", + "--passphrase-fd", + "0", + "--clearsign", + "--output", + signedPath, + path, + ], + { + // @ts-ignore + input: passphrase, + stdout: "inherit", + stderr: "inherit", + }, +); +if (exitCode !== 0) { + exit(stdout || stderr); +} + +const uploads = [...paths, path, signedPath]; +log("Uploading assets:\n", ...uploads.map((path) => ` + ${basename(path)}\n`)); +await confirm(); + +for (const path of uploads) { + const name = basename(path); + await uploadAsset(tag_name, name, blob(path)); +} +try { + rm(cwd); +} catch { + warn("Failed to cleanup:", cwd, "\n"); +} +log("Done"); + +process.exit(0); // FIXME diff --git a/packages/bun-release/scripts/upload-npm.ts b/packages/bun-release/scripts/upload-npm.ts new file mode 100644 index 000000000..8309d7e57 --- /dev/null +++ b/packages/bun-release/scripts/upload-npm.ts @@ -0,0 +1,191 @@ +import { join, copy, exists, chmod, write, writeJson } from "../src/fs"; +import { fetch } from "../src/fetch"; +import { spawn } from "../src/spawn"; +import type { Platform } from "../src/platform"; +import { platforms } from "../src/platform"; +import { getSemver } from "../src/github"; +import { getRelease } from "../src/github"; +import type { BuildOptions } from "esbuild"; +import { buildSync, formatMessagesSync } from "esbuild"; +import type { JSZipObject } from "jszip"; +import { loadAsync } from "jszip"; +import { debug, log, error } from "../src/console"; + +const module = "bun"; +const owner = "@oven"; +let version: string; + +const [tag, action] = process.argv.slice(2); + +await build(tag); +if (action === "publish") { + await publish(); +} else if (action === "dry-run") { + await publish(true); +} else if (action) { + throw new Error(`Unknown action: ${action}`); +} +process.exit(0); // HACK + +async function build(tag?: string): Promise<void> { + const release = await getRelease(tag); + if (release.tag_name === "canary") { + version = await getCanarySemver(); + } else { + version = release.tag_name.replace("bun-v", ""); + } + await buildRootModule(); + for (const platform of platforms) { + await buildModule(release, platform); + } +} + +async function publish(dryRun?: boolean): Promise<void> { + const modules = platforms.map(({ bin }) => `${owner}/${bin}`); + modules.push(module); + for (const module of modules) { + publishModule(module, dryRun); + } +} + +async function buildRootModule() { + log("Building:", `${module}@${version}`); + const cwd = join("npm", module); + const define = { + version: `"${version}"`, + module: `"${module}"`, + owner: `"${owner}"`, + }; + bundle(join("scripts", "npm-postinstall.ts"), join(cwd, "install.js"), { + define, + }); + bundle(join("scripts", "npm-exec.ts"), join(cwd, "bin", "bun"), { + define, + banner: { + js: "#!/usr/bin/env node", + }, + }); + const os = [...new Set(platforms.map(({ os }) => os))]; + const cpu = [...new Set(platforms.map(({ arch }) => arch))]; + writeJson(join(cwd, "package.json"), { + name: module, + version: version, + scripts: { + postinstall: "node install.js", + }, + optionalDependencies: Object.fromEntries( + platforms.map(({ bin }) => [`${owner}/${bin}`, version]), + ), + bin: { + bun: "bin/bun", + bunx: "bin/bun", + }, + os, + cpu, + }); + if (exists(".npmrc")) { + copy(".npmrc", join(cwd, ".npmrc")); + } +} + +async function buildModule( + release: Awaited<ReturnType<typeof getRelease>>, + { bin, exe, os, arch }: Platform, +): Promise<void> { + const module = `${owner}/${bin}`; + log("Building:", `${module}@${version}`); + const asset = release.assets.find(({ name }) => name === `${bin}.zip`); + if (!asset) { + error(`No asset found: ${bin}`); + return; + } + const bun = await extractFromZip(asset.browser_download_url, `${bin}/bun`); + const cwd = join("npm", module); + write(join(cwd, exe), await bun.async("arraybuffer")); + chmod(join(cwd, exe), 0o755); + writeJson(join(cwd, "package.json"), { + name: module, + version: version, + preferUnplugged: true, + os: [os], + cpu: [arch], + }); + if (exists(".npmrc")) { + copy(".npmrc", join(cwd, ".npmrc")); + } +} + +function publishModule(name: string, dryRun?: boolean): void { + log(dryRun ? "Dry-run Publishing:" : "Publishing:", `${name}@${version}`); + const { exitCode, stdout, stderr } = spawn( + "npm", + [ + "publish", + "--access", + "public", + "--tag", + version.includes("canary") ? "canary" : "latest", + ...(dryRun ? ["--dry-run"] : []), + ], + { + cwd: join("npm", name), + }, + ); + if (exitCode === 0) { + error(stderr || stdout); + } +} + +async function extractFromZip( + url: string, + filename: string, +): Promise<JSZipObject> { + const response = await fetch(url); + const buffer = await response.arrayBuffer(); + const zip = await loadAsync(buffer); + for (const [name, file] of Object.entries(zip.files)) { + if (!file.dir && name.startsWith(filename)) { + return file; + } + } + debug("Found files:", Object.keys(zip.files)); + throw new Error(`File not found: ${filename}`); +} + +async function getCanarySemver(): Promise<string> { + const date = new Date().toISOString().split("T")[0].replace(/-/g, ""); + try { + const response = await fetch( + `https://registry.npmjs.org/-/package/${module}/dist-tags`, + ); + const { canary }: { canary: string } = await response.json(); + if (canary.includes(date)) { + const match = /canary.[0-9]{8}\.([0-9]+)+?/.exec(canary); + const build = 1 + (match ? parseInt(match[1]) : 0); + return getSemver("canary", build); + } + } catch (error) { + debug("getCanarySemver failed", error); + } + return getSemver("canary"); +} + +function bundle(src: string, dst: string, options: BuildOptions = {}): void { + const { errors } = buildSync({ + bundle: true, + treeShaking: true, + keepNames: true, + minifySyntax: true, + pure: ["console.debug"], + platform: "node", + target: "es6", + format: "cjs", + entryPoints: [src], + outfile: dst, + ...options, + }); + if (errors?.length) { + const messages = formatMessagesSync(errors, { kind: "error" }); + throw new Error(messages.join("\n")); + } +} diff --git a/packages/bun-release/src/console.ts b/packages/bun-release/src/console.ts new file mode 100644 index 000000000..69e0911d6 --- /dev/null +++ b/packages/bun-release/src/console.ts @@ -0,0 +1,77 @@ +import { isatty } from "tty"; +import { createInterface } from "readline"; + +export const isAction = !!process.env["GITHUB_ACTION"]; + +export const isDebug = + process.env["DEBUG"] === "1" || + process.env["LOG_LEVEL"] === "debug" || + process.env["RUNNER_DEBUG"] === "1"; + +export function debug(...message: any[]): void { + if (isAction) { + console.debug("::debug::", ...message); + } else if (isDebug) { + console.debug(...message); + } +} + +export function log(...message: any[]): void { + console.log(...message); +} + +export function warn(...message: any[]): void { + if (isAction) { + console.warn("::warning::", ...message); + } else { + console.warn(...message); + } +} + +export function error(...message: any[]): void { + if (isAction) { + console.error("::error::", ...message); + } else { + console.error(...message); + } +} + +export function exit(...message: any[]): never { + error(...message); + process.exit(1); +} + +export function isTty(): boolean { + return isatty(process.stdout.fd); +} + +export async function stdin(question: string): Promise<string> { + if (isTty()) { + return prompt(question) || ""; + } + const reader = createInterface({ + input: process.stdin, + terminal: false, + }); + let buffer = ""; + reader.on("line", (line) => { + buffer += line; + }); + return new Promise((resolve) => { + reader.once("close", () => resolve(buffer)); + }); +} + +export async function confirm(message?: string): Promise<void> { + if (!isTty()) { + return; + } + const question = message ?? "Confirm?"; + switch (prompt(`${question} [y/n]`)) { + case "y": + case "Y": + log(); + return; + } + exit(); +} diff --git a/packages/bun-release/src/fetch.ts b/packages/bun-release/src/fetch.ts new file mode 100644 index 000000000..d9c85a5f3 --- /dev/null +++ b/packages/bun-release/src/fetch.ts @@ -0,0 +1,73 @@ +import { debug, isDebug } from "./console"; + +export const fetch = "fetch" in globalThis ? webFetch : nodeFetch; + +type Options = RequestInit & { assert?: boolean }; + +async function webFetch(url: string, options: Options = {}): Promise<Response> { + debug("fetch request", url, options); + const response = await globalThis.fetch(url, options, { verbose: isDebug }); + debug("fetch response", response); + if (options?.assert !== false && !isOk(response.status)) { + try { + debug(await response.text()); + } catch {} + throw new Error(`${response.status}: ${url}`); + } + return response; +} + +async function nodeFetch( + url: string, + options: Options = {}, +): Promise<Response> { + const { get } = await import("node:http"); + return new Promise((resolve, reject) => { + get(url, (response) => { + debug("http.get", url, response.statusCode); + const status = response.statusCode ?? 501; + if (response.headers.location && isRedirect(status)) { + return nodeFetch(url).then(resolve, reject); + } + if (options?.assert !== false && !isOk(status)) { + return reject(new Error(`${status}: ${url}`)); + } + const body: Buffer[] = []; + response.on("data", (chunk) => { + body.push(chunk); + }); + response.on("end", () => { + resolve({ + ok: isOk(status), + status, + async arrayBuffer() { + return Buffer.concat(body).buffer as ArrayBuffer; + }, + async text() { + return Buffer.concat(body).toString("utf-8"); + }, + async json() { + const text = Buffer.concat(body).toString("utf-8"); + return JSON.parse(text); + }, + } as Response); + }); + }).on("error", reject); + }); +} + +function isOk(status: number): boolean { + return status >= 200 && status <= 204; +} + +function isRedirect(status: number): boolean { + switch (status) { + case 301: // Moved Permanently + case 308: // Permanent Redirect + case 302: // Found + case 307: // Temporary Redirect + case 303: // See Other + return true; + } + return false; +} diff --git a/packages/bun-release/src/fs.ts b/packages/bun-release/src/fs.ts new file mode 100644 index 000000000..ef20c224a --- /dev/null +++ b/packages/bun-release/src/fs.ts @@ -0,0 +1,166 @@ +import path from "path"; +import fs from "fs"; +import os from "os"; +import crypto from "crypto"; +import { debug } from "./console"; + +export function join(...paths: (string | string[])[]): string { + return path.join(...paths.flat(2)); +} + +export function basename(...paths: (string | string[])[]): string { + return path.basename(join(...paths)); +} + +export function tmp(): string { + const tmpdir = process.env["RUNNER_TEMP"] ?? os.tmpdir(); + const dir = fs.mkdtempSync(join(tmpdir, "bun-")); + debug("tmp", dir); + return dir; +} + +export function rm(path: string): void { + debug("rm", path); + try { + fs.rmSync(path, { recursive: true }); + return; + } catch (error) { + debug("fs.rmSync failed", error); + // Did not exist before Node.js v14. + // Attempt again with older, slower implementation. + } + let stats: fs.Stats; + try { + stats = fs.lstatSync(path); + } catch (error) { + debug("fs.lstatSync failed", error); + // The file was likely deleted, so return early. + return; + } + if (!stats.isDirectory()) { + fs.unlinkSync(path); + return; + } + try { + fs.rmdirSync(path, { recursive: true }); + return; + } catch (error) { + debug("fs.rmdirSync failed", error); + // Recursive flag did not exist before Node.js X. + // Attempt again with older, slower implementation. + } + for (const filename of fs.readdirSync(path)) { + rm(join(path, filename)); + } + fs.rmdirSync(path); +} + +export function rename(path: string, newPath: string): void { + debug("rename", path, newPath); + try { + fs.renameSync(path, newPath); + return; + } catch (error) { + debug("fs.renameSync failed", error); + // If there is an error, delete the new path and try again. + } + try { + rm(newPath); + } catch (error) { + debug("rm failed", error); + // The path could have been deleted already. + } + fs.renameSync(path, newPath); +} + +export function write( + dst: string, + content: string | ArrayBuffer | ArrayBufferView, +): void { + debug("write", dst); + try { + fs.writeFileSync(dst, content); + return; + } catch (error) { + debug("fs.writeFileSync failed", error); + // If there is an error, ensure the parent directory + // exists and try again. + try { + fs.mkdirSync(path.dirname(dst), { recursive: true }); + } catch (error) { + debug("fs.mkdirSync failed", error); + // The directory could have been created already. + } + fs.writeFileSync(dst, content); + } +} + +export function writeJson(path: string, json: object, force?: boolean): void { + let value = json; + if (!force && exists(path)) { + try { + const existing = JSON.parse(read(path)); + value = { + ...existing, + ...json, + }; + } catch { + value = json; + } + } + write(path, `${JSON.stringify(value, undefined, 2)}\n`); +} + +export function read(path: string): string { + debug("read", path); + return fs.readFileSync(path, "utf-8"); +} + +export function blob(path: string): Blob { + debug("blob", path); + if ("Bun" in globalThis) { + return Bun.file(path); + } + const buffer = fs.readFileSync(path); + return new Blob([buffer], { + type: path.endsWith(".zip") + ? "application/zip" + : path.endsWith(".txt") + ? "text/plain" + : "application/octet-stream", + }); +} + +export function hash(content: string | crypto.BinaryLike): string { + debug("hash", content); + return crypto + .createHash("sha256") + .update(typeof content === "string" ? fs.readFileSync(content) : content) + .digest("hex"); +} + +export function chmod(path: string, mode: fs.Mode): void { + debug("chmod", path, mode); + fs.chmodSync(path, mode); +} + +export function copy(path: string, newPath: string): void { + debug("copy", path, newPath); + try { + fs.copyFileSync(path, newPath); + return; + } catch (error) { + debug("fs.copyFileSync failed", error); + } + write(newPath, read(path)); +} + +export function exists(path: string): boolean { + debug("exists", path); + try { + return fs.existsSync(path); + } catch (error) { + debug("fs.existsSync failed", error); + } + return false; +} diff --git a/packages/bun-release/src/github.ts b/packages/bun-release/src/github.ts new file mode 100644 index 000000000..fbbbbf267 --- /dev/null +++ b/packages/bun-release/src/github.ts @@ -0,0 +1,113 @@ +import type { Endpoints, RequestParameters, Route } from "@octokit/types"; +import { Octokit } from "octokit"; +import { fetch } from "./fetch"; +import { debug, log, warn, error } from "./console"; + +const [owner, repo] = process.env["GITHUB_REPOSITORY"]?.split("/") ?? [ + "oven-sh", + "bun", +]; + +const octokit = new Octokit({ + auth: process.env["GITHUB_TOKEN"], + request: { + fetch, + }, + log: { + debug, + info: log, + warn, + error, + }, +}); + +export async function github<R extends Route>( + url: R | keyof Endpoints, + options?: Omit< + R extends keyof Endpoints + ? Endpoints[R]["parameters"] & RequestParameters + : RequestParameters, + "owner" | "repo" + >, +): Promise< + R extends keyof Endpoints ? Endpoints[R]["response"]["data"] : unknown +> { + // @ts-ignore + const { data } = await octokit.request(url, { + owner, + repo, + ...options, + }); + return data; +} + +export async function getRelease(tag?: string) { + if (!tag) { + return github("GET /repos/{owner}/{repo}/releases/latest"); + } + return github("GET /repos/{owner}/{repo}/releases/tags/{tag}", { + tag: formatTag(tag), + }); +} + +export async function uploadAsset(tag: string, name: string, blob: Blob) { + const release = await getRelease(tag); + const asset = release.assets.find((asset) => asset.name === name); + // Github requires that existing assets are deleted before uploading + // a new asset, but does not provide a rename or re-upload API?!? + if (asset) { + await github("DELETE /repos/{owner}/{repo}/releases/assets/{asset_id}", { + asset_id: asset.id, + }); + } + return github( + "POST {origin}/repos/{owner}/{repo}/releases/{release_id}/assets{?name,label}", + { + baseUrl: "https://uploads.github.com", + release_id: release.id, + name, + headers: { + "content-type": blob.type, + "content-length": blob.size, + }, + data: Buffer.from(await blob.arrayBuffer()), + }, + ); +} + +export async function downloadAsset(tag: string, name: string): Promise<Blob> { + const release = await getRelease(tag); + const asset = release.assets.find((asset) => asset.name === name); + if (!asset) { + throw new Error(`Asset not found: ${name}`); + } + const response = await fetch(asset.browser_download_url); + return response.blob(); +} + +export async function getSha(tag: string, format?: "short" | "long") { + const { sha } = await github("GET /repos/{owner}/{repo}/git/tags/{tag_sha}", { + tag_sha: formatTag(tag), + }); + return format === "short" ? sha.substring(0, 7) : sha; +} + +export async function getSemver(tag?: string, build?: number): Promise<string> { + const { tag_name } = await getRelease(tag); + if (tag_name !== "canary") { + return tag_name.replace("bun-v", ""); + } + const sha = await getSha(tag_name, "short"); + const date = new Date().toISOString().split("T")[0].replace(/-/g, ""); + return `${Bun.version}-canary.${date}.${build ?? 1}+${sha}`; +} + +export function formatTag(tag: string): string { + if (tag === "canary" || tag.startsWith("bun-v")) { + return tag; + } + if (tag.startsWith("v")) { + return tag.slice(1); + } + return `bun-v${tag}`; +} diff --git a/packages/bun-npm/src/install.ts b/packages/bun-release/src/npm/install.ts index 9eabd2c41..ac6f2e670 100644 --- a/packages/bun-npm/src/install.ts +++ b/packages/bun-release/src/npm/install.ts @@ -1,11 +1,14 @@ -import { fetch, chmod, join, rename, rm, tmp, write, spawn } from "./util"; +import { fetch } from "../fetch"; +import { spawn } from "../spawn"; +import { chmod, join, rename, rm, tmp, write } from "../fs"; import { unzipSync } from "zlib"; -import type { Platform } from "./platform"; -import { os, arch, supportedPlatforms } from "./platform"; +import type { Platform } from "../platform"; +import { os, arch, supportedPlatforms } from "../platform"; +import { debug, error } from "../console"; -declare const npmVersion: string; -declare const npmPackage: string; -declare const npmOwner: string; +declare const version: string; +declare const module: string; +declare const owner: string; export async function importBun(): Promise<string> { if (!supportedPlatforms.length) { @@ -15,16 +18,16 @@ export async function importBun(): Promise<string> { try { return await requireBun(platform); } catch (error) { - console.debug("requireBun failed", error); + debug("requireBun failed", error); } } - throw new Error(`Failed to install package "${npmPackage}"`); + throw new Error(`Failed to install package "${module}"`); } async function requireBun(platform: Platform): Promise<string> { - const npmPackage = `${npmOwner}/${platform.bin}`; + const module = `${owner}/${platform.bin}`; function resolveBun() { - const exe = require.resolve(join(npmPackage, platform.exe)); + const exe = require.resolve(join(module, platform.exe)); const { exitCode, stderr, stdout } = spawn(exe, ["--version"]); if (exitCode === 0) { return exe; @@ -33,29 +36,26 @@ async function requireBun(platform: Platform): Promise<string> { } try { return resolveBun(); - } catch (error) { - console.debug("resolveBun failed", error); - console.error( - `Failed to find package "${npmPackage}".`, + } catch (cause) { + debug("resolveBun failed", cause); + error( + `Failed to find package "${module}".`, `You may have used the "--no-optional" flag when running "npm install".`, ); } - const cwd = join("node_modules", npmPackage); + const cwd = join("node_modules", module); try { installBun(platform, cwd); - } catch (error) { - console.debug("installBun failed", error); - console.error( - `Failed to install package "${npmPackage}" using "npm install".`, - error, - ); + } catch (cause) { + debug("installBun failed", cause); + error(`Failed to install package "${module}" using "npm install".`, cause); try { await downloadBun(platform, cwd); - } catch (error) { - console.debug("downloadBun failed", error); - console.error( - `Failed to download package "${npmPackage}" from "registry.npmjs.org".`, - error, + } catch (cause) { + debug("downloadBun failed", cause); + error( + `Failed to download package "${module}" from "registry.npmjs.org".`, + cause, ); } } @@ -63,7 +63,7 @@ async function requireBun(platform: Platform): Promise<string> { } function installBun(platform: Platform, dst: string): void { - const npmPackage = `${npmOwner}/${platform.bin}`; + const module = `${owner}/${platform.bin}`; const cwd = tmp(); try { write(join(cwd, "package.json"), "{}"); @@ -75,7 +75,7 @@ function installBun(platform: Platform, dst: string): void { "--prefer-offline", "--no-audit", "--progress=false", - `${npmPackage}@${npmVersion}`, + `${module}@${version}`, ], { cwd, @@ -87,13 +87,13 @@ function installBun(platform: Platform, dst: string): void { }, ); if (exitCode === 0) { - rename(join(cwd, "node_modules", npmPackage), dst); + rename(join(cwd, "node_modules", module), dst); } } finally { try { rm(cwd); } catch (error) { - console.debug("rm failed", error); + debug("rm failed", error); // There is nothing to do if the directory cannot be cleaned up. } } @@ -101,7 +101,7 @@ function installBun(platform: Platform, dst: string): void { async function downloadBun(platform: Platform, dst: string): Promise<void> { const response = await fetch( - `https://registry.npmjs.org/${npmOwner}/${platform.bin}/-/${platform.bin}-${npmVersion}.tgz`, + `https://registry.npmjs.org/${owner}/${platform.bin}/-/${platform.bin}-${version}.tgz`, ); const tgz = await response.arrayBuffer(); let buffer: Buffer; @@ -127,7 +127,7 @@ async function downloadBun(platform: Platform, dst: string): Promise<void> { try { chmod(join(dst, name), 0o755); } catch (error) { - console.debug("chmod failed", error); + debug("chmod failed", error); } } offset += (size + 511) & ~511; @@ -151,7 +151,7 @@ export function optimizeBun(path: string): void { rename(path, join(__dirname, "bin", "bun")); return; } catch (error) { - console.debug("optimizeBun failed", error); + debug("optimizeBun failed", error); } throw new Error( "Your package manager doesn't seem to support bun. To use bun, install using the following command: curl -fsSL https://bun.sh/install | bash", diff --git a/packages/bun-npm/src/platform.ts b/packages/bun-release/src/platform.ts index a01cc3ddc..fb438d824 100644 --- a/packages/bun-npm/src/platform.ts +++ b/packages/bun-release/src/platform.ts @@ -1,4 +1,6 @@ -import { read, spawn } from "./util"; +import { spawn } from "./spawn"; +import { read } from "./fs"; +import { debug } from "./console"; export const os = process.platform; @@ -71,7 +73,7 @@ function isLinuxAVX2(): boolean { try { return read("/proc/cpuinfo").includes("avx2"); } catch (error) { - console.debug("isLinuxAVX2 failed", error); + debug("isLinuxAVX2 failed", error); return false; } } @@ -81,7 +83,7 @@ function isDarwinAVX2(): boolean { const { exitCode, stdout } = spawn("sysctl", ["-n", "machdep.cpu"]); return exitCode === 0 && stdout.includes("AVX2"); } catch (error) { - console.debug("isDarwinAVX2 failed", error); + debug("isDarwinAVX2 failed", error); return false; } } @@ -94,7 +96,7 @@ function isRosetta2(): boolean { ]); return exitCode === 0 && stdout.includes("1"); } catch (error) { - console.debug("isRosetta2 failed", error); + debug("isRosetta2 failed", error); return false; } } diff --git a/packages/bun-release/src/spawn.ts b/packages/bun-release/src/spawn.ts new file mode 100644 index 000000000..7a04dc3f2 --- /dev/null +++ b/packages/bun-release/src/spawn.ts @@ -0,0 +1,24 @@ +import child_process from "child_process"; +import { debug } from "./console"; + +export function spawn( + cmd: string, + args: string[], + options: child_process.SpawnOptions = {}, +): { + exitCode: number; + stdout: string; + stderr: string; +} { + debug("spawn", [cmd, ...args].join(" ")); + const { status, stdout, stderr } = child_process.spawnSync(cmd, args, { + stdio: "pipe", + encoding: "utf-8", + ...options, + }); + return { + exitCode: status ?? 1, + stdout, + stderr, + }; +} diff --git a/packages/bun-npm/tsconfig.json b/packages/bun-release/tsconfig.json index 9272b9920..b7194ece3 100644 --- a/packages/bun-npm/tsconfig.json +++ b/packages/bun-release/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "lib": ["esnext"], - "module": "esnext", - "target": "esnext", + "lib": ["ESNext"], + "module": "ESNext", + "target": "ESNext", "moduleResolution": "node", "types": ["bun-types"], "esModuleInterop": true, |