diff --git a/.github/termux-release.json b/.github/termux-release.json new file mode 100644 index 000000000000..51c32d03391a --- /dev/null +++ b/.github/termux-release.json @@ -0,0 +1,15 @@ +{ + "upstream_repo": "anomalyco/opencode", + "upstream_tag": "v1.17.15", + "upstream_name": "v1.17.15", + "upstream_html_url": "https://github.com/anomalyco/opencode/releases/tag/v1.17.15", + "upstream_target": "adf178a6b95c61506ddaadaf4dd062badb4a8fda", + "upstream_release_id": "350399617", + "upstream_prerelease": false, + "release_train": "1.17.15", + "release_branch": "release-1.17.15", + "work_branch": "termux-1.17.15", + "patch_branch": "termux-target", + "patch_source_sha": "ffca5f8f2e00621977c43a232f95d55b58d3f0c5", + "termux_tag": "v1.17.15-termux" +} diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index e93d5fbdb260..2e49924a46d6 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -7,7 +7,7 @@ on: jobs: sync: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'anomalyco/opencode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} permissions: contents: write pull-requests: write @@ -21,6 +21,7 @@ jobs: uses: ./.github/actions/setup-bun - name: Setup Git Committer + if: github.repository == 'anomalyco/opencode' id: setup-git-committer uses: ./.github/actions/setup-git-committer with: @@ -31,6 +32,7 @@ jobs: run: bun i -g opencode-ai - name: Sync beta branch + if: github.repository == 'anomalyco/opencode' env: GH_TOKEN: ${{ steps.setup-git-committer.outputs.token }} OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml index 15bf0783160e..ba168be4068c 100644 --- a/.github/workflows/containers.yml +++ b/.github/workflows/containers.yml @@ -16,7 +16,7 @@ permissions: jobs: build: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'anomalyco/opencode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} env: REGISTRY: ghcr.io/${{ github.repository_owner }} TAG: "24.04" @@ -32,6 +32,7 @@ jobs: uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Login to GHCR + if: github.repository == 'anomalyco/opencode' uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: ghcr.io diff --git a/.github/workflows/docs-locale-sync.yml b/.github/workflows/docs-locale-sync.yml index 5f921e8bb717..fb09c2c3a1d3 100644 --- a/.github/workflows/docs-locale-sync.yml +++ b/.github/workflows/docs-locale-sync.yml @@ -11,7 +11,7 @@ jobs: sync-locales: if: false #if: github.actor != 'opencode-agent[bot]' - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'anomalyco/opencode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} permissions: contents: write steps: @@ -26,6 +26,7 @@ jobs: uses: ./.github/actions/setup-bun - name: Setup git committer + if: github.repository == 'anomalyco/opencode' id: committer uses: ./.github/actions/setup-git-committer with: @@ -53,7 +54,7 @@ jobs: run: curl -fsSL https://opencode.ai/install | bash - name: Sync locale docs with OpenCode - if: steps.changes.outputs.has_changes == 'true' + if: github.repository == 'anomalyco/opencode' && (steps.changes.outputs.has_changes == 'true') env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} OPENCODE_CONFIG_CONTENT: | diff --git a/.github/workflows/duplicate-issues.yml b/.github/workflows/duplicate-issues.yml index 3972247dafc6..e36beb438a8a 100644 --- a/.github/workflows/duplicate-issues.yml +++ b/.github/workflows/duplicate-issues.yml @@ -7,7 +7,7 @@ on: jobs: check-duplicates: if: github.event.action == 'opened' - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'anomalyco/opencode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} permissions: contents: read issues: write @@ -41,7 +41,7 @@ jobs: run: curl -fsSL https://opencode.ai/install | bash - name: Check duplicates and compliance - if: steps.author.outputs.skip != 'true' + if: github.repository == 'anomalyco/opencode' && (steps.author.outputs.skip != 'true') env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -141,7 +141,7 @@ jobs: recheck-compliance: if: github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'needs:compliance') - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'anomalyco/opencode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} permissions: contents: read issues: write @@ -175,7 +175,7 @@ jobs: run: curl -fsSL https://opencode.ai/install | bash - name: Recheck compliance - if: steps.author.outputs.skip != 'true' + if: github.repository == 'anomalyco/opencode' && (steps.author.outputs.skip != 'true') env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml index 324cfec02001..e84bda5197ba 100644 --- a/.github/workflows/generate.yml +++ b/.github/workflows/generate.yml @@ -7,7 +7,7 @@ on: jobs: generate: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'anomalyco/opencode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} permissions: contents: write pull-requests: write @@ -19,6 +19,7 @@ jobs: uses: ./.github/actions/setup-bun - name: Setup git committer + if: github.repository == 'anomalyco/opencode' id: committer uses: ./.github/actions/setup-git-committer with: diff --git a/.github/workflows/nix-eval.yml b/.github/workflows/nix-eval.yml index 75332695a1ad..28b9781ac7ce 100644 --- a/.github/workflows/nix-eval.yml +++ b/.github/workflows/nix-eval.yml @@ -16,7 +16,7 @@ permissions: jobs: nix-eval: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'anomalyco/opencode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 15 steps: - name: Checkout repository diff --git a/.github/workflows/nix-hashes.yml b/.github/workflows/nix-hashes.yml index ce1d9237fde5..3f7d83c49613 100644 --- a/.github/workflows/nix-hashes.yml +++ b/.github/workflows/nix-hashes.yml @@ -37,7 +37,7 @@ jobs: runner: macos-15-intel - system: aarch64-darwin runner: macos-latest - runs-on: ${{ matrix.runner }} + runs-on: ${{ github.repository == 'anomalyco/opencode' && (matrix.runner) || 'ubuntu-latest' }} steps: - name: Checkout repository diff --git a/.github/workflows/notify-discord.yml b/.github/workflows/notify-discord.yml index 0b2b1cde051b..98f8978cd7e6 100644 --- a/.github/workflows/notify-discord.yml +++ b/.github/workflows/notify-discord.yml @@ -6,9 +6,10 @@ on: jobs: notify: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'anomalyco/opencode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} steps: - name: Send nicely-formatted embed to Discord + if: github.repository == 'anomalyco/opencode' uses: SethCohen/github-releases-to-discord@24d166886aee4646d448c8a389ff9e1ebcab3682 # v1.20.0 with: webhook_url: ${{ secrets.DISCORD_WEBHOOK }} diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index 3469c21917f8..d4c325f6b373 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -13,7 +13,7 @@ jobs: startsWith(github.event.comment.body, '/oc') || contains(github.event.comment.body, ' /opencode') || startsWith(github.event.comment.body, '/opencode') - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'anomalyco/opencode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} permissions: id-token: write contents: read @@ -26,6 +26,7 @@ jobs: - uses: ./.github/actions/setup-bun - name: Run opencode + if: github.repository == 'anomalyco/opencode' uses: anomalyco/opencode/github@2c14fc5586fe0b88e5c04732d2e846769cc35671 # latest env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} diff --git a/.github/workflows/pr-management.yml b/.github/workflows/pr-management.yml index b6aa4e589d89..fc14d255bfb5 100644 --- a/.github/workflows/pr-management.yml +++ b/.github/workflows/pr-management.yml @@ -6,7 +6,7 @@ on: jobs: check-duplicates: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'anomalyco/opencode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} permissions: contents: read pull-requests: write @@ -57,7 +57,7 @@ jobs: } > pr_info.txt - name: Check for duplicate PRs - if: steps.team-check.outputs.is_team != 'true' + if: github.repository == 'anomalyco/opencode' && (steps.team-check.outputs.is_team != 'true') env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish-github-action.yml b/.github/workflows/publish-github-action.yml index e5ca91b5618a..97fe6565dda4 100644 --- a/.github/workflows/publish-github-action.yml +++ b/.github/workflows/publish-github-action.yml @@ -14,7 +14,7 @@ permissions: jobs: publish: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'anomalyco/opencode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: diff --git a/.github/workflows/publish-vscode.yml b/.github/workflows/publish-vscode.yml index 00c7e260482e..0ae4e5080af1 100644 --- a/.github/workflows/publish-vscode.yml +++ b/.github/workflows/publish-vscode.yml @@ -13,7 +13,7 @@ permissions: jobs: publish: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'anomalyco/opencode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: @@ -29,6 +29,7 @@ jobs: working-directory: ./sdks/vscode - name: Publish + if: github.repository == 'anomalyco/opencode' run: | ./script/publish working-directory: ./sdks/vscode diff --git a/.github/workflows/release-github-action.yml b/.github/workflows/release-github-action.yml index 4a1d7218bb2c..7bc5b50128c3 100644 --- a/.github/workflows/release-github-action.yml +++ b/.github/workflows/release-github-action.yml @@ -14,7 +14,7 @@ permissions: jobs: release: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'anomalyco/opencode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml index 00a4fba8ca13..3a173dec2ed3 100644 --- a/.github/workflows/review.yml +++ b/.github/workflows/review.yml @@ -10,7 +10,7 @@ jobs: github.event.issue.pull_request && startsWith(github.event.comment.body, '/review') && contains(fromJson('["OWNER","MEMBER"]'), github.event.comment.author_association) - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'anomalyco/opencode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} permissions: contents: read pull-requests: write @@ -44,6 +44,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Check PR guidelines compliance + if: github.repository == 'anomalyco/opencode' env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml index be2e099d0ed9..9ba6088429a7 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/storybook.yml @@ -28,7 +28,7 @@ concurrency: jobs: build: name: storybook build - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'anomalyco/opencode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c69de1d93b0d..edad3c2fb67c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: host: blacksmith-4vcpu-ubuntu-2404 - name: windows host: blacksmith-4vcpu-windows-2025 - runs-on: ${{ matrix.settings.host }} + runs-on: ${{ github.repository == 'anomalyco/opencode' && (matrix.settings.host) || 'ubuntu-latest' }} defaults: run: shell: bash @@ -89,7 +89,7 @@ jobs: host: blacksmith-4vcpu-ubuntu-2404 - name: windows host: blacksmith-4vcpu-windows-2025 - runs-on: ${{ matrix.settings.host }} + runs-on: ${{ github.repository == 'anomalyco/opencode' && (matrix.settings.host) || 'ubuntu-latest' }} env: PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers defaults: diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index 0350e43877c4..29fa33a463cb 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -6,7 +6,7 @@ on: jobs: triage: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'anomalyco/opencode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} permissions: contents: read issues: write @@ -41,7 +41,7 @@ jobs: run: curl -fsSL https://opencode.ai/install | bash - name: Triage issue - if: steps.author.outputs.skip != 'true' + if: github.repository == 'anomalyco/opencode' && (steps.author.outputs.skip != 'true') env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index fc9a52797c1d..0ef6327e7ce7 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -9,7 +9,7 @@ on: jobs: typecheck: - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'anomalyco/opencode' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 diff --git a/bun.lock b/bun.lock index 6af38c1f784a..52ceedc20c03 100644 --- a/bun.lock +++ b/bun.lock @@ -356,6 +356,7 @@ }, "devDependencies": { "@opencode-ai/http-recorder": "workspace:*", + "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", diff --git a/install b/install index b0716d532082..158bbfaed45c 100755 --- a/install +++ b/install @@ -92,6 +92,12 @@ else arch="x64" fi + if [ "$os" = "linux" ] && [ "$arch" = "arm64" ]; then + if [ -n "${TERMUX_VERSION:-}" ] || [ "${PREFIX:-}" = "/data/data/com.termux/files/usr" ] || [ -d /data/data/com.termux/files/usr ]; then + os="android" + fi + fi + if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0) if [ "$rosetta_flag" = "1" ]; then @@ -101,7 +107,7 @@ else combo="$os-$arch" case "$combo" in - linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64) + android-arm64|linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64) ;; *) echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}" @@ -110,7 +116,7 @@ else esac archive_ext=".zip" - if [ "$os" = "linux" ]; then + if [ "$os" = "linux" ] || [ "$os" = "android" ]; then archive_ext=".tar.gz" fi @@ -168,7 +174,7 @@ else filename="$APP-$target$archive_ext" - if [ "$os" = "linux" ]; then + if [ "$os" = "linux" ] || [ "$os" = "android" ]; then if ! command -v tar >/dev/null 2>&1; then echo -e "${RED}Error: 'tar' is required but not installed.${NC}" exit 1 @@ -274,7 +280,9 @@ download_with_progress() { exec 4>/dev/null fi - local tmp_dir=${TMPDIR:-/tmp} + local tmp_dir="${TMPDIR:-${PREFIX:+${PREFIX}/tmp}}" + tmp_dir="${tmp_dir:-/tmp}" + mkdir -p "$tmp_dir" local basename="${tmp_dir}/opencode_install_$$" local tracefile="${basename}.trace" @@ -326,7 +334,9 @@ download_with_progress() { download_and_install() { print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version" - local tmp_dir="${TMPDIR:-/tmp}/opencode_install_$$" + local tmp_root="${TMPDIR:-${PREFIX:+${PREFIX}/tmp}}" + tmp_root="${tmp_root:-/tmp}" + local tmp_dir="${tmp_root}/opencode_install_$$" mkdir -p "$tmp_dir" if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$tmp_dir/$filename"; then @@ -334,7 +344,7 @@ download_and_install() { curl -# -L -o "$tmp_dir/$filename" "$url" fi - if [ "$os" = "linux" ]; then + if [ "$os" = "linux" ] || [ "$os" = "android" ]; then tar -xzf "$tmp_dir/$filename" -C "$tmp_dir" else unzip -q "$tmp_dir/$filename" -d "$tmp_dir" diff --git a/packages/core/package.json b/packages/core/package.json index c0927930af68..d95ef678e483 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -34,6 +34,7 @@ "default": "./src/pty/pty.bun.ts" }, "#fff": { + "android": "./src/filesystem/fff.node.ts", "bun": "./src/filesystem/fff.bun.ts", "node": "./src/filesystem/fff.node.ts", "default": "./src/filesystem/fff.bun.ts" @@ -49,6 +50,7 @@ "@types/semver": "catalog:", "@types/turndown": "5.0.5", "@types/which": "3.0.4", + "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", diff --git a/packages/core/src/filesystem/watcher.ts b/packages/core/src/filesystem/watcher.ts index b0f568a56661..4bde31cd0883 100644 --- a/packages/core/src/filesystem/watcher.ts +++ b/packages/core/src/filesystem/watcher.ts @@ -36,6 +36,7 @@ const watcher = lazy((): typeof import("@parcel/watcher") | undefined => { }) function getBackend() { + if (process.platform === "android") return "inotify" if (process.platform === "win32") return "windows" if (process.platform === "darwin") return "fs-events" if (process.platform === "linux") return "inotify" diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index a0eb78a13e2a..591455818c5d 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -31,7 +31,8 @@ export const Flag = { OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"], OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"], OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"], - OPENCODE_DISABLE_FFF: fff === undefined ? process.platform === "win32" : truthy("OPENCODE_DISABLE_FFF"), + OPENCODE_DISABLE_FFF: + fff === undefined ? process.platform === "android" || process.platform === "win32" : truthy("OPENCODE_DISABLE_FFF"), // Experimental OPENCODE_EXPERIMENTAL_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_FILEWATCHER").pipe( diff --git a/packages/desktop/scripts/utils.ts b/packages/desktop/scripts/utils.ts index 19b96b0a161f..7172743707bd 100644 --- a/packages/desktop/scripts/utils.ts +++ b/packages/desktop/scripts/utils.ts @@ -39,6 +39,11 @@ export const SIDECAR_BINARIES: Array<{ rustTarget: string; ocBinary: string; ass ocBinary: "opencode-linux-arm64", assetExt: "tar.gz", }, + { + rustTarget: "aarch64-linux-android", + ocBinary: "opencode-android-arm64", + assetExt: "tar.gz", + }, ] export const RUST_TARGET = Bun.env.RUST_TARGET diff --git a/packages/opencode/bin/opencode b/packages/opencode/bin/opencode index a7101f42b0fe..880607c434ff 100755 --- a/packages/opencode/bin/opencode +++ b/packages/opencode/bin/opencode @@ -52,6 +52,7 @@ const scriptDir = path.dirname(scriptPath) const cached = path.join(scriptDir, ".opencode") const platformMap = { + android: "android", darwin: "darwin", linux: "linux", win32: "windows", @@ -127,6 +128,8 @@ const names = (() => { const avx2 = supportsAvx2() const baseline = arch === "x64" && !avx2 + if (platform === "android") return [base] + if (platform === "linux") { const musl = (() => { try { diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 236838dbdee7..6767b6ef594f 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -23,6 +23,7 @@ const skipInstall = process.argv.includes("--skip-install") const sourcemapsFlag = process.argv.includes("--sourcemaps") const plugin = createSolidTransformPlugin() const skipEmbedWebUi = process.argv.includes("--skip-embed-web-ui") +const targetFlag = process.argv.find((item) => item.startsWith("--target="))?.slice("--target=".length) const createEmbeddedWebUIBundle = async () => { console.log(`Building Web UI to embed in the binary`) @@ -51,11 +52,15 @@ const createEmbeddedWebUIBundle = async () => { const embeddedFileMap = skipEmbedWebUi ? null : await createEmbeddedWebUIBundle() const allTargets: { - os: string + os: "android" | "darwin" | "linux" | "win32" arch: "arm64" | "x64" abi?: "musl" avx2?: false }[] = [ + { + os: "android", + arch: "arm64", + }, { os: "linux", arch: "arm64", @@ -113,6 +118,19 @@ const allTargets: { }, ] +function targetName(item: (typeof allTargets)[number]) { + return [ + pkg.name, + // changing to win32 flags npm for some reason + item.os === "win32" ? "windows" : item.os, + item.arch, + item.avx2 === false ? "baseline" : undefined, + item.abi === undefined ? undefined : item.abi, + ] + .filter(Boolean) + .join("-") +} + const targets = singleFlag ? allTargets.filter((item) => { if (item.os !== process.platform || item.arch !== process.arch) { @@ -132,8 +150,17 @@ const targets = singleFlag return true }) + : targetFlag + ? allTargets.filter((item) => { + const name = targetName(item) + return targetFlag === name || targetFlag === name.replace(`${pkg.name}-`, "") + }) : allTargets +if (targetFlag && targets.length === 0) { + throw new Error(`Unknown build target: ${targetFlag}`) +} + await $`rm -rf dist` const binaries: Record = {} @@ -142,17 +169,11 @@ if (!skipInstall) { await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}` await $`bun install --os="*" --cpu="*" @ff-labs/fff-bun@${pkg.dependencies["@ff-labs/fff-bun"]}` } +if (targets.some((item) => item.os === "android")) { + await import("./opentui-android.ts") +} for (const item of targets) { - const name = [ - pkg.name, - // changing to win32 flags npm for some reason - item.os === "win32" ? "windows" : item.os, - item.arch, - item.avx2 === false ? "baseline" : undefined, - item.abi === undefined ? undefined : item.abi, - ] - .filter(Boolean) - .join("-") + const name = targetName(item) console.log(`building ${name}`) await $`mkdir -p dist/${name}/bin` @@ -166,7 +187,7 @@ for (const item of targets) { const workerRelativePath = path.relative(dir, parserWorker).replaceAll("\\", "/") await Bun.build({ - conditions: ["bun", "node"], + conditions: item.os === "android" ? ["android", "bun", "node"] : ["bun", "node"], tsconfig: "./tsconfig.json", plugins: [plugin], external: ["node-gyp"], @@ -180,6 +201,7 @@ for (const item of targets) { autoloadTsconfig: true, autoloadPackageJson: true, target: name.replace(pkg.name, "bun") as any, + executablePath: item.os === "android" ? process.env.OPENCODE_ANDROID_BUN : undefined, outfile: `dist/${name}/bin/opencode`, execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"], windows: {}, @@ -198,6 +220,11 @@ for (const item of targets) { }, }) + if (item.os === "android") { + const androidElf = await import("./patch-android-elf.ts") + await androidElf.patchAndroidElf(`dist/${name}/bin/opencode`) + } + // Smoke test: only run if binary is for current platform if (item.os === process.platform && item.arch === process.arch && !item.abi) { const binaryPath = `dist/${name}/bin/opencode` @@ -231,7 +258,7 @@ for (const item of targets) { if (Script.release) { for (const key of Object.keys(binaries)) { - if (key.includes("linux")) { + if (key.includes("linux") || key.includes("android")) { await $`tar -czf ../../${key}.tar.gz *`.cwd(`dist/${key}/bin`) } else { await $`zip -r ../../${key}.zip *`.cwd(`dist/${key}/bin`) diff --git a/packages/opencode/script/opentui-android.ts b/packages/opencode/script/opentui-android.ts new file mode 100644 index 000000000000..47edfdd195d2 --- /dev/null +++ b/packages/opencode/script/opentui-android.ts @@ -0,0 +1,262 @@ +#!/usr/bin/env bun + +import { $ } from "bun" +import fs from "fs" +import os from "os" +import path from "path" +import pkg from "../package.json" +import top from "../../../package.json" + +const dir = path.resolve(import.meta.dirname, "..") +const root = path.resolve(dir, "../..") +const dep = pkg.dependencies["@opentui/core"] +const ver = dep === "catalog:" ? top.workspaces.catalog["@opentui/core"] : dep + +if (!ver || ver.includes(":")) { + throw new Error(`Cannot resolve @opentui/core version from ${dep}`) +} + +const core = [path.join(dir, "node_modules/@opentui/core"), path.join(root, "node_modules/@opentui/core")].find((item) => + fs.existsSync(item), +) + +if (!core) { + throw new Error("Cannot find installed @opentui/core") +} + +const name = "@opentui/core-android-arm64" +const native = path.join(core, "android-arm64") +const head = JSON.parse((await $`npm view @opentui/core@${ver} gitHead --json`.text()).trim()) + +if (!head) { + throw new Error(`Cannot resolve @opentui/core@${ver} gitHead`) +} + +const tmp = process.env.RUNNER_TEMP ?? process.env.TMPDIR ?? os.tmpdir() +const src = path.join(tmp, `opentui-${ver}-${head.slice(0, 12)}`) +const ndk = androidNdk() +const libc = path.join(tmp, `android-ndk-${ndk.api}.libc`) + +await Bun.write( + libc, + [ + `include_dir=${path.join(ndk.sysroot, "usr/include")}`, + `sys_include_dir=${ndk.sysInclude}`, + `crt_dir=${ndk.libDir}`, + "msvc_lib_dir=", + "kernel32_lib_dir=", + "gcc_dir=", + "", + ].join("\n"), +) + +if (!fs.existsSync(path.join(src, "packages/core/src/zig/build.zig"))) { + fs.mkdirSync(src, { recursive: true }) + await $`git init`.cwd(src) + await $`git remote add origin https://github.com/anomalyco/opentui`.cwd(src) + await $`git fetch --depth 1 origin ${head}`.cwd(src) + await $`git checkout --detach FETCH_HEAD`.cwd(src) +} + +patchOpenTUIBuild(path.join(src, "packages/core/src/zig/build.zig")) + +await $`OPENCODE_ANDROID_NDK_LIBDIR=${ndk.libDir} zig build -Dtarget=aarch64-linux-android -Doptimize=ReleaseFast --libc ${libc}`.cwd( + path.join(src, "packages/core/src/zig"), +) + +const lib = path.join(src, "packages/core/src/zig/lib/aarch64-linux-android/libopentui.so") + +if (!fs.existsSync(lib)) { + throw new Error(`OpenTUI Android build did not produce ${lib}`) +} + +fs.rmSync(native, { recursive: true, force: true }) +fs.mkdirSync(native, { recursive: true }) +fs.copyFileSync(lib, path.join(native, "libopentui.so")) +fs.chmodSync(path.join(native, "libopentui.so"), 0o755) + +await Bun.write( + path.join(native, "index.js"), + `import { fileURLToPath } from "node:url" + +export default fileURLToPath(new URL("./libopentui.so", import.meta.url)) +`, +) +await Bun.write( + path.join(native, "index.bun.js"), + `const module = await import("./libopentui.so", { with: { type: "file" } }) + +export default module.default +`, +) +await Bun.write(path.join(native, "index.d.ts"), "declare const path: string\nexport default path\n") +await Bun.write( + path.join(native, "package.json"), + JSON.stringify( + { + name, + version: ver, + description: `Prebuilt android-arm64 binaries for @opentui/core`, + type: "module", + main: "index.js", + module: "index.js", + types: "index.d.ts", + license: "MIT", + repository: { + type: "git", + url: "https://github.com/anomalyco/opentui", + directory: "packages/core", + }, + keywords: ["prebuild", "prebuilt"], + exports: { + ".": { + bun: "./index.bun.js", + import: "./index.js", + types: "./index.d.ts", + }, + }, + os: ["android"], + cpu: ["arm64"], + }, + null, + 2, + ) + "\n", +) +await Bun.write(path.join(native, "README.md"), `## ${name}\n\n> Prebuilt android-arm64 binaries for \`@opentui/core\`.\n`) + +const license = path.join(src, "LICENSE") +if (fs.existsSync(license)) { + fs.copyFileSync(license, path.join(native, "LICENSE")) +} + +const marker = "@opentui/core-android-arm64" +const block = ` // ${marker} + if (process.platform === "android") { + if (process.arch === "arm64") return await import("./android-arm64/index.bun.js") + } + +` +let patched = 0 + +for (const file of fs.readdirSync(core)) { + if (!file.endsWith(".js")) continue + + const target = path.join(core, file) + const text = fs.readFileSync(target, "utf8") + + if (text.includes(marker) || text.includes('process.platform === "android"')) continue + if (!text.includes('@opentui/core-linux-arm64')) continue + + const next = text.replace(' if (process.platform === "linux") {\n', block + ' if (process.platform === "linux") {\n') + if (next === text) continue + + fs.writeFileSync(target, next) + patched++ +} + +if (patched === 0) { + throw new Error("Did not patch @opentui/core native loader for Android") +} + +function androidNdk() { + const roots = [ + process.env.ANDROID_NDK_ROOT, + process.env.ANDROID_NDK_HOME, + process.env.NDK_ROOT, + process.env.ANDROID_HOME ? path.join(process.env.ANDROID_HOME, "ndk") : undefined, + process.env.ANDROID_SDK_ROOT ? path.join(process.env.ANDROID_SDK_ROOT, "ndk") : undefined, + "/usr/local/lib/android/sdk/ndk", + ].filter((item): item is string => item !== undefined && item.length > 0) + + const ndkRoots = roots.flatMap((item) => { + if (!fs.existsSync(item)) return [] + if (fs.existsSync(path.join(item, "toolchains/llvm/prebuilt"))) return [item] + return fs + .readdirSync(item, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => path.join(item, entry.name)) + .sort() + .reverse() + }) + const sysroot = ndkRoots + .flatMap((item) => { + const prebuilt = path.join(item, "toolchains/llvm/prebuilt") + if (!fs.existsSync(prebuilt)) return [] + return fs + .readdirSync(prebuilt, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => path.join(prebuilt, entry.name, "sysroot")) + }) + .find( + (item) => + fs.existsSync(path.join(item, "usr/include")) && + fs.existsSync(path.join(item, "usr/lib/aarch64-linux-android")), + ) + + if (!sysroot) { + throw new Error("Cannot find Android NDK sysroot. Set ANDROID_NDK_ROOT or ANDROID_NDK_HOME.") + } + + const libRoot = path.join(sysroot, "usr/lib/aarch64-linux-android") + const api = androidApi(libRoot) + const libDir = path.join(libRoot, api) + const include = path.join(sysroot, "usr/include") + const archInclude = path.join(include, "aarch64-linux-android") + + return { + api, + libDir, + sysroot, + sysInclude: fs.existsSync(archInclude) ? archInclude : include, + } +} + +function androidApi(libRoot: string) { + const requested = process.env.ANDROID_API_LEVEL ?? process.env.ANDROID_NDK_API ?? "24" + + if (fs.existsSync(path.join(libRoot, requested, "libdl.so"))) { + return requested + } + + const fallback = fs + .readdirSync(libRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && /^\d+$/.test(entry.name)) + .map((entry) => entry.name) + .sort((a, b) => Number(a) - Number(b)) + .findLast((item) => fs.existsSync(path.join(libRoot, item, "libdl.so"))) + + if (!fallback) { + throw new Error(`Cannot find Android NDK libraries under ${libRoot}`) + } + + return fallback +} + +function patchOpenTUIBuild(file: string) { + const marker = "OPENCODE_ANDROID_NDK_LIBDIR" + const text = fs.readFileSync(file, "utf8") + + if (text.includes(marker)) return + + const before = ` .linux => { + artifact.linkSystemLibrary("dl"); + artifact.linkSystemLibrary("pthread"); + },` + const after = ` .linux => { + if (target.result.abi == .android) { + if (b.graph.env_map.get("${marker}")) |lib_path| { + artifact.addLibraryPath(.{ .cwd_relative = lib_path }); + } + artifact.linkSystemLibrary("dl"); + } else { + artifact.linkSystemLibrary("dl"); + artifact.linkSystemLibrary("pthread"); + } + },` + + if (!text.includes(before)) { + throw new Error("Cannot patch OpenTUI Android system library handling") + } + + fs.writeFileSync(file, text.replace(before, after)) +} diff --git a/packages/opencode/script/patch-android-elf.ts b/packages/opencode/script/patch-android-elf.ts new file mode 100644 index 000000000000..29665e5bab49 --- /dev/null +++ b/packages/opencode/script/patch-android-elf.ts @@ -0,0 +1,421 @@ +#!/usr/bin/env bun + +import { readFile, writeFile } from "node:fs/promises" + +const ET_DYN = 3 +const EM_AARCH64 = 183 +const PT_LOAD = 1 +const PT_DYNAMIC = 2 +const PF_W = 2 +const SHT_NOBITS = 8 +const SHF_ALLOC = 2 +const DT_NULL = 0 +const DT_RELA = 7 +const DT_RELASZ = 8 +const DT_RELAENT = 9 +const R_AARCH64_RELATIVE = 1027 + +type LoadSegment = { + offset: number + vaddr: number + filesz: number + flags: number +} + +type DynamicSegment = { + offset: number + filesz: number +} + +type DynamicEntry = { + tag: number + value: number + valueOffset: number +} + +type Section = { + name: string + type: number + flags: number + addr: number + offset: number + size: number +} + +type Elf = { + view: DataView + loadSegments: LoadSegment[] + dynamicSegment: DynamicSegment + sections: Section[] +} + +type PointerSlot = { + offset: number + vaddr: number +} + +export async function patchAndroidElf(file: string) { + const data = new Uint8Array(await readFile(file)) + const elf = parseElf(data) + const dynamic = parseDynamic(elf) + const rela = requireDynamic(dynamic, DT_RELA, "DT_RELA") + const relasz = requireDynamic(dynamic, DT_RELASZ, "DT_RELASZ") + const relaent = dynamic.find((item) => item.tag === DT_RELAENT)?.value ?? 24 + + if (relaent < 24) { + throw new Error(`Unsupported DT_RELAENT ${relaent}`) + } + if (relasz.value % relaent !== 0) { + throw new Error(`DT_RELASZ ${relasz.value} is not divisible by DT_RELAENT ${relaent}`) + } + + const relaOffset = vaddrToOffset(elf.loadSegments, rela.value, relasz.value) + const bun = requireBunSection(elf.sections) + const slot = findBunPointerSlot(data, elf.loadSegments, bun, { offset: relaOffset, size: relasz.value }) + + if (hasPayloadRelocation(elf.view, relaOffset, relasz.value, relaent, slot.vaddr, bun.addr)) { + console.log(`Android ELF already patches Bun standalone payload pointer in ${file}`) + return + } + + const nextSize = relasz.value + relaent + const nextRelaOffset = requireWritablePadding(data, elf, slot.offset, nextSize, { offset: relaOffset, size: relasz.value }) + const nextRelaVaddr = offsetToVaddr(elf.loadSegments, nextRelaOffset, nextSize) + data.set(data.slice(relaOffset, relaOffset + relasz.value), nextRelaOffset) + + const nextEntry = nextRelaOffset + relasz.value + writeU64(elf.view, nextEntry, slot.vaddr) + writeU64(elf.view, nextEntry + 8, R_AARCH64_RELATIVE) + writeI64(elf.view, nextEntry + 16, bun.addr) + writeU64(elf.view, rela.valueOffset, nextRelaVaddr) + writeU64(elf.view, relasz.valueOffset, nextSize) + + await writeFile(file, data) + console.log( + `Patched Android ELF ${file}: relocated Bun payload pointer ${hex(slot.vaddr)} -> ${hex(bun.addr)}`, + ) +} + +function parseElf(data: Uint8Array): Elf { + const view = new DataView(data.buffer, data.byteOffset, data.byteLength) + if ( + data.byteLength < 64 || + data[0] !== 0x7f || + data[1] !== 0x45 || + data[2] !== 0x4c || + data[3] !== 0x46 + ) { + throw new Error("Expected an ELF file") + } + if (data[4] !== 2 || data[5] !== 1) { + throw new Error("Expected a 64-bit little-endian ELF file") + } + if (readU16(view, 0x10) !== ET_DYN || readU16(view, 0x12) !== EM_AARCH64) { + throw new Error("Expected an AArch64 PIE ELF file") + } + + const programHeaderOffset = readU64(view, 0x20, "e_phoff") + const sectionHeaderOffset = readU64(view, 0x28, "e_shoff") + const programHeaderEntrySize = readU16(view, 0x36) + const programHeaderCount = readU16(view, 0x38) + const sectionHeaderEntrySize = readU16(view, 0x3a) + const sectionHeaderCount = readU16(view, 0x3c) + const sectionNameIndex = readU16(view, 0x3e) + + const loadSegments: LoadSegment[] = [] + let dynamicSegment: DynamicSegment | undefined + + for (let i = 0; i < programHeaderCount; i += 1) { + const offset = programHeaderOffset + i * programHeaderEntrySize + const type = readU32(view, offset) + const flags = readU32(view, offset + 4) + if (type === PT_LOAD) { + loadSegments.push({ + flags, + offset: readU64(view, offset + 8, "p_offset"), + vaddr: readU64(view, offset + 16, "p_vaddr"), + filesz: readU64(view, offset + 32, "p_filesz"), + }) + } + if (type === PT_DYNAMIC) { + dynamicSegment = { + offset: readU64(view, offset + 8, "p_offset"), + filesz: readU64(view, offset + 32, "p_filesz"), + } + } + } + + if (!dynamicSegment) { + throw new Error("ELF file has no PT_DYNAMIC segment") + } + + return { + view, + loadSegments, + dynamicSegment, + sections: parseSections(data, view, sectionHeaderOffset, sectionHeaderEntrySize, sectionHeaderCount, sectionNameIndex), + } +} + +function parseSections( + data: Uint8Array, + view: DataView, + sectionHeaderOffset: number, + sectionHeaderEntrySize: number, + sectionHeaderCount: number, + sectionNameIndex: number, +) { + if (!sectionHeaderOffset || !sectionHeaderCount) { + throw new Error("ELF file has no section headers; cannot find Bun payload section") + } + if (sectionNameIndex >= sectionHeaderCount) { + throw new Error(`Invalid section string table index ${sectionNameIndex}`) + } + + const stringHeader = sectionHeaderOffset + sectionNameIndex * sectionHeaderEntrySize + const stringOffset = readU64(view, stringHeader + 24, "sh_offset") + const sections: Section[] = [] + + for (let i = 0; i < sectionHeaderCount; i += 1) { + const offset = sectionHeaderOffset + i * sectionHeaderEntrySize + sections.push({ + name: readString(data, stringOffset + readU32(view, offset)), + type: readU32(view, offset + 4), + flags: readU64(view, offset + 8, "sh_flags"), + addr: readU64(view, offset + 16, "sh_addr"), + offset: readU64(view, offset + 24, "sh_offset"), + size: readU64(view, offset + 32, "sh_size"), + }) + } + + return sections +} + +function parseDynamic(elf: Elf) { + const result: DynamicEntry[] = [] + for (let offset = elf.dynamicSegment.offset; offset < elf.dynamicSegment.offset + elf.dynamicSegment.filesz; offset += 16) { + const tag = readU64(elf.view, offset, "d_tag") + result.push({ + tag, + value: readU64(elf.view, offset + 8, "d_val"), + valueOffset: offset + 8, + }) + if (tag === DT_NULL) break + } + return result +} + +function requireDynamic(dynamic: DynamicEntry[], tag: number, name: string) { + const entry = dynamic.find((item) => item.tag === tag) + if (!entry) { + throw new Error(`ELF dynamic section has no ${name}`) + } + return entry +} + +function requireBunSection(sections: Section[]) { + const matches = sections.filter((item) => item.name === ".bun" && item.type !== SHT_NOBITS && item.size > 0) + if (matches.length !== 1) { + throw new Error(`Expected exactly one .bun section, found ${matches.length}`) + } + return matches[0] +} + +function findBunPointerSlot( + data: Uint8Array, + loadSegments: LoadSegment[], + bun: Section, + rela: { offset: number; size: number }, +): PointerSlot { + const needle = u64Bytes(bun.addr) + const excluded = [ + { offset: bun.offset, size: bun.size }, + rela, + ] + const candidates = loadSegments + .filter((item) => (item.flags & PF_W) !== 0) + .flatMap((segment) => { + const result: PointerSlot[] = [] + const end = segment.offset + segment.filesz - needle.length + for (let offset = align(segment.offset, 8); offset <= end; offset += 8) { + if (excluded.some((item) => contains(item.offset, item.size, offset, needle.length))) continue + if (!matches(data, offset, needle)) continue + result.push({ + offset, + vaddr: offsetToVaddr(loadSegments, offset, needle.length), + }) + } + return result + }) + + if (candidates.length !== 1) { + throw new Error( + `Expected exactly one writable Bun payload pointer slot for ${hex(bun.addr)}, found ${candidates.length}`, + ) + } + return candidates[0] +} + +function hasPayloadRelocation( + view: DataView, + relaOffset: number, + relasz: number, + relaent: number, + slotVaddr: number, + payloadVaddr: number, +) { + for (let offset = relaOffset; offset < relaOffset + relasz; offset += relaent) { + if ( + readU64(view, offset, "r_offset") === slotVaddr && + readU64(view, offset + 8, "r_info") % 0x100000000 === R_AARCH64_RELATIVE && + readI64(view, offset + 16, "r_addend") === payloadVaddr + ) { + return true + } + } + return false +} + +function requireWritablePadding( + data: Uint8Array, + elf: Elf, + slotOffset: number, + size: number, + rela: { offset: number; size: number }, +) { + const occupied = [ + ...elf.sections + .filter((item) => (item.flags & SHF_ALLOC) !== 0 && item.type !== SHT_NOBITS && item.size > 0) + .map((item) => ({ offset: item.offset, size: item.size })), + { offset: slotOffset, size: 8 }, + rela, + ] + + const offset = findWritablePadding(data, elf.loadSegments, occupied, size, slotOffset + 8) + if (offset !== undefined) return offset + + const fallback = findWritablePadding(data, elf.loadSegments, occupied, size, 0) + if (fallback !== undefined) return fallback + + throw new Error(`Could not find ${size} bytes of writable zero padding for patched RELA table`) +} + +function findWritablePadding( + data: Uint8Array, + loadSegments: LoadSegment[], + occupied: Array<{ offset: number; size: number }>, + size: number, + minimumOffset: number, +) { + for (const segment of loadSegments.filter((item) => (item.flags & PF_W) !== 0)) { + const segmentStart = Math.max(segment.offset, minimumOffset) + const segmentEnd = segment.offset + segment.filesz + const ranges = occupied + .map((item) => ({ start: item.offset, end: item.offset + item.size })) + .filter((item) => item.start < segmentEnd && item.end > segmentStart) + .sort((a, b) => a.start - b.start) + let cursor = segmentStart + + for (const range of ranges) { + const offset = align(cursor, 8) + if (offset + size <= range.start && isZero(data, offset, size)) return offset + cursor = Math.max(cursor, range.end) + } + + const offset = align(cursor, 8) + if (offset + size <= segmentEnd && isZero(data, offset, size)) return offset + } +} + +function vaddrToOffset(loadSegments: LoadSegment[], vaddr: number, size: number) { + const segment = loadSegments.find((item) => vaddr >= item.vaddr && vaddr + size <= item.vaddr + item.filesz) + if (!segment) { + throw new Error(`Could not map virtual address ${hex(vaddr)} to a file offset`) + } + return segment.offset + vaddr - segment.vaddr +} + +function offsetToVaddr(loadSegments: LoadSegment[], offset: number, size: number) { + const segment = loadSegments.find((item) => offset >= item.offset && offset + size <= item.offset + item.filesz) + if (!segment) { + throw new Error(`Could not map file offset ${hex(offset)} to a virtual address`) + } + return segment.vaddr + offset - segment.offset +} + +function readU16(view: DataView, offset: number) { + return view.getUint16(offset, true) +} + +function readU32(view: DataView, offset: number) { + return view.getUint32(offset, true) +} + +function readU64(view: DataView, offset: number, label: string) { + const value = view.getBigUint64(offset, true) + if (value > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new Error(`${label} ${value} is too large to handle safely`) + } + return Number(value) +} + +function readI64(view: DataView, offset: number, label: string) { + const value = view.getBigInt64(offset, true) + if (value > BigInt(Number.MAX_SAFE_INTEGER) || value < BigInt(Number.MIN_SAFE_INTEGER)) { + throw new Error(`${label} ${value} is too large to handle safely`) + } + return Number(value) +} + +function writeU64(view: DataView, offset: number, value: number) { + view.setBigUint64(offset, BigInt(value), true) +} + +function writeI64(view: DataView, offset: number, value: number) { + view.setBigInt64(offset, BigInt(value), true) +} + +function readString(data: Uint8Array, offset: number) { + const end = data.indexOf(0, offset) + return new TextDecoder().decode(data.slice(offset, end === -1 ? data.length : end)) +} + +function u64Bytes(value: number) { + const data = new Uint8Array(8) + writeU64(new DataView(data.buffer), 0, value) + return data +} + +function align(value: number, amount: number) { + return Math.ceil(value / amount) * amount +} + +function matches(data: Uint8Array, offset: number, needle: Uint8Array) { + for (let i = 0; i < needle.length; i += 1) { + if (data[offset + i] !== needle[i]) return false + } + return true +} + +function contains(offset: number, size: number, targetOffset: number, targetSize: number) { + return targetOffset < offset + size && targetOffset + targetSize > offset +} + +function isZero(data: Uint8Array, offset: number, size: number) { + for (let i = 0; i < size; i += 1) { + if (data[offset + i] !== 0) return false + } + return true +} + +function hex(value: number) { + return `0x${value.toString(16)}` +} + +if (import.meta.main) { + const file = Bun.argv[2] + if (!file) { + throw new Error("Usage: patch-android-elf.ts ") + } + await patchAndroidElf(file) +} diff --git a/packages/opencode/script/postinstall.mjs b/packages/opencode/script/postinstall.mjs index fa303b746b8c..34a9d5aa0125 100644 --- a/packages/opencode/script/postinstall.mjs +++ b/packages/opencode/script/postinstall.mjs @@ -12,6 +12,7 @@ const require = createRequire(import.meta.url) const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8")) const platformMap = { + android: "android", darwin: "darwin", linux: "linux", win32: "windows", @@ -96,6 +97,8 @@ function isMusl() { function packageNames() { const baseline = arch === "x64" && !supportsAvx2() + if (platform === "android") return [base] + if (platform === "linux") { if (isMusl()) { if (arch === "x64") diff --git a/packages/opencode/script/publish.ts b/packages/opencode/script/publish.ts index ac1cafff56f4..7fa73189cb57 100755 --- a/packages/opencode/script/publish.ts +++ b/packages/opencode/script/publish.ts @@ -63,7 +63,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write( }, version: version, license: pkg.license, - os: ["darwin", "linux", "win32"], + os: ["android", "darwin", "linux", "win32"], cpu: ["arm64", "x64"], optionalDependencies: binaries, },