diff --git a/.gitea/workflows/build-opencode.yaml b/.gitea/workflows/build-opencode.yaml index 055adba..4039586 100644 --- a/.gitea/workflows/build-opencode.yaml +++ b/.gitea/workflows/build-opencode.yaml @@ -23,8 +23,10 @@ jobs: with: builder: multiplatform-builder context: "{{defaultContext}}:opencode" + ssh: default=/var/lib/gitea-runner/.ssh/id_ed25519 build-args: | OPENCODE_VERSION=1.18.4 + BUN_VERSION=1.3.14 platforms: | linux/amd64 push: true @@ -32,4 +34,3 @@ jobs: tags: | "gitea.woggioni.net/woggioni/opencode:1.18.4" "gitea.woggioni.net/woggioni/opencode:latest" - diff --git a/opencode/Dockerfile b/opencode/Dockerfile index 02e78b3..cbc860f 100644 --- a/opencode/Dockerfile +++ b/opencode/Dockerfile @@ -1,8 +1,30 @@ +ARG BUN_VERSION +FROM oven/bun:${BUN_VERSION}-slim AS build + +ARG OPENCODE_VERSION +# RUN --mount=type=cache,target=/var/cache/apk apk update +# RUN --mount=type=cache,target=/var/cache/apk apk add python3 make g++ npm zip git +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt update &&\ + apt install -y python3 make g++ npm zip git +#RUN adduser -D -h /home/builder -u 1126 builder +RUN useradd -m -u 1126 builder +USER builder +WORKDIR /home/builder +ADD --chown=builder:builder --keep-git-dir=true git@github.com:anomalyco/opencode.git#v${OPENCODE_VERSION} opencode +WORKDIR /home/builder/opencode + +RUN --mount=type=cache,target=/home/builder/.bun/install/cache,uid=1126 bun install +ADD --chown=builder:builder ./build.ts ./packages/opencode/script/build.ts +RUN --mount=type=cache,target=/home/builder/.bun/install/cache,uid=1126 ./packages/opencode/script/build.ts + + + + FROM archlinux:latest AS release ARG OPENCODE_VERSION - COPY mirrorlist /etc/pacman.d/mirrolist - RUN pacman-key --init #RUN pacman-key --refresh-keys @@ -27,9 +49,7 @@ USER opencode RUN pipx install pyright RUN pipx install pip-tools USER root -ADD https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-x64.tar.gz ./opencode.tgz -RUN tar -xvf ./opencode.tgz -C /usr/bin -RUN rm opencode.tgz +COPY --from=build --chown=root:root /home/builder/opencode/packages/opencode/dist/opencode-linux-x64/bin/opencode /usr/bin/opencode ADD --chown=opencode:opencode ./opencode.json /etc/opencode/opencode.json RUN mkdir -p /usr/lib/opencode/addons ADD --chmod=644 https://repo1.maven.org/maven2/org/projectlombok/lombok/1.18.46/lombok-1.18.46.jar /usr/lib/opencode/addons/lombok.jar diff --git a/opencode/build.ts b/opencode/build.ts new file mode 100755 index 0000000..8e24cfb --- /dev/null +++ b/opencode/build.ts @@ -0,0 +1,220 @@ +#!/usr/bin/env bun + +import { $ } from "bun" +import path from "path" +import { fileURLToPath } from "url" +import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const dir = path.resolve(__dirname, "..") + +process.chdir(dir) + +const generated = await import("./generate.ts") + +import { Script } from "@opencode-ai/script" +import pkg from "../package.json" + +const singleFlag = process.argv.includes("--single") +const baselineFlag = process.argv.includes("--baseline") +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 createEmbeddedWebUIBundle = async () => { + console.log(`Building Web UI to embed in the binary`) + const appDir = path.join(import.meta.dirname, "../../app") + const dist = path.join(appDir, "dist") + await $`OPENCODE_CHANNEL=${Script.channel} bun run --cwd ${appDir} build` + const files = (await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: dist }))) + .map((file) => file.replaceAll("\\", "/")) + .filter((file) => !file.endsWith(".map")) + .sort() + const imports = files.map((file, i) => { + const spec = path.relative(dir, path.join(dist, file)).replaceAll("\\", "/") + return `import file_${i} from ${JSON.stringify(spec.startsWith(".") ? spec : `./${spec}`)} with { type: "file" };` + }) + const entries = files.map((file, i) => ` ${JSON.stringify(file)}: file_${i},`) + return [ + `// Import all files as file_$i with type: "file"`, + ...imports, + `// Export with original mappings`, + `export default {`, + ...entries, + `}`, + ].join("\n") +} + +const embeddedFileMap = skipEmbedWebUi ? null : await createEmbeddedWebUIBundle() +const treeSitterWorker = await Bun.file(fileURLToPath(import.meta.resolve("@opentui/core/parser.worker"))).text() + +const allTargets: { + os: string + arch: "arm64" | "x64" + abi?: "musl" + avx2?: false +}[] = [ + { + os: "linux", + arch: "arm64", + }, + { + os: "linux", + arch: "x64", + }, + { + os: "linux", + arch: "x64", + avx2: false, + }, + { + os: "linux", + arch: "arm64", + abi: "musl", + }, + { + os: "linux", + arch: "x64", + abi: "musl", + }, + { + os: "linux", + arch: "x64", + abi: "musl", + avx2: false, + } +] + +const targets = singleFlag + ? allTargets.filter((item) => { + if (item.os !== process.platform || item.arch !== process.arch) { + return false + } + + // When building for the current platform, prefer a single native binary by default. + // Baseline binaries require additional Bun artifacts and can be flaky to download. + if (item.avx2 === false) { + return baselineFlag + } + + // also skip abi-specific builds for the same reason + if (item.abi !== undefined) { + return false + } + + return true + }) + : allTargets + +await $`rm -rf dist` + +const binaries: Record = {} +if (!skipInstall) { + await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}` + 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"]}` +} +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("-") + console.log(`building ${name}`) + await $`mkdir -p dist/${name}/bin` + + const workerPath = "./src/cli/tui/worker.ts" + const treeSitterWorkerPath = "opentui-tree-sitter-worker.js" + const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/" + + await Bun.build({ + conditions: ["bun", "node"], + tsconfig: "./tsconfig.json", + plugins: [plugin], + external: ["node-gyp"], + format: "esm", + minify: true, + bytecode: true, + sourcemap: sourcemapsFlag ? "linked" : "none", + splitting: true, + compile: { + autoloadBunfig: false, + autoloadDotenv: false, + autoloadTsconfig: true, + autoloadPackageJson: true, + target: name.replace(pkg.name, "bun") as any, + outfile: `dist/${name}/bin/opencode`, + execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"], + windows: {}, + }, + files: { + [treeSitterWorkerPath]: treeSitterWorker, + ...(embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}), + }, + entrypoints: [ + "./src/index.ts", + workerPath, + treeSitterWorkerPath, + ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : []), + ], + define: { + FFF_LIBC: JSON.stringify(item.abi === "musl" ? "musl" : "gnu"), + OPENCODE_VERSION: `'${Script.version}'`, + OPENCODE_MODELS_DEV: generated.modelsData, + OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + treeSitterWorkerPath, + OPENCODE_WORKER_PATH: workerPath, + OPENCODE_CHANNEL: `'${Script.channel}'`, + OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "", + ...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}), + }, + }) + + // 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` + console.log(`Running smoke test: ${binaryPath} --version`) + try { + const versionOutput = await $`${binaryPath} --version`.text() + console.log(`Smoke test passed: ${versionOutput.trim()}`) + } catch (e) { + console.error(`Smoke test failed for ${name}:`, e) + process.exit(1) + } + } + + await $`rm -rf ./dist/${name}/bin/tui` + await Bun.file(`dist/${name}/package.json`).write( + JSON.stringify( + { + name, + version: Script.version, + preferUnplugged: true, + os: [item.os], + cpu: [item.arch], + ...(item.abi ? { libc: [item.abi] } : {}), + }, + null, + 2, + ), + ) + binaries[name] = Script.version +} + +if (Script.release) { + for (const key of Object.keys(binaries)) { + if (key.includes("linux")) { + await $`tar -czf ../../${key}.tar.gz *`.cwd(`dist/${key}/bin`) + } else { + await $`zip -r ../../${key}.zip *`.cwd(`dist/${key}/bin`) + } + } +} + +export { binaries }