diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 5b993a4d7c1..52bedcf14bf 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -9,6 +9,7 @@ "typecheck": "tsgo --noEmit", "test": "bun test", "build": "bun run script/build.ts", + "build:portable": "bun run script/build-portable.ts", "dev": "bun run --conditions=browser ./src/index.ts", "random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'", "clean": "echo 'Cleaning up...' && rm -rf node_modules dist", diff --git a/packages/opencode/portable/.dockerignore b/packages/opencode/portable/.dockerignore new file mode 100644 index 00000000000..69fb9f73157 --- /dev/null +++ b/packages/opencode/portable/.dockerignore @@ -0,0 +1,3 @@ +embedded +target +Dockerfile diff --git a/packages/opencode/portable/.gitignore b/packages/opencode/portable/.gitignore new file mode 100644 index 00000000000..d002386a807 --- /dev/null +++ b/packages/opencode/portable/.gitignore @@ -0,0 +1,2 @@ +target +output diff --git a/packages/opencode/portable/Cargo.lock b/packages/opencode/portable/Cargo.lock new file mode 100644 index 00000000000..34d4ff88581 --- /dev/null +++ b/packages/opencode/portable/Cargo.lock @@ -0,0 +1,162 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cc" +version = "1.2.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "embedded-launcher" +version = "0.1.0" +dependencies = [ + "patchelf", + "tempfile", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "patchelf" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad9ad0be261cb118cb6ee66675910ce718da9e600746b0427a81521e5a91524" +dependencies = [ + "cc", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" diff --git a/packages/opencode/portable/Cargo.toml b/packages/opencode/portable/Cargo.toml new file mode 100644 index 00000000000..95c20775f90 --- /dev/null +++ b/packages/opencode/portable/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "launcher" +version = "0.1.0" +edition = "2021" + +[dependencies] +tempfile = "3.24" + +[build-dependencies] diff --git a/packages/opencode/portable/Dockerfile b/packages/opencode/portable/Dockerfile new file mode 100644 index 00000000000..be60d2da544 --- /dev/null +++ b/packages/opencode/portable/Dockerfile @@ -0,0 +1,45 @@ +# syntax=docker/dockerfile:1 +ARG BUN_VERSION="1.3.5" +ARG TARGETARCH + +FROM oven/bun:${BUN_VERSION}-alpine AS build-step +ARG TARGETARCH +ARG RUST_TARGET +ARG OPENCODE_ARTIFACT +ARG LD_NAME +ARG LIBC_NAME +ARG CPU + +WORKDIR /code +RUN mkdir -p ./embedded /output + +# Install deps +RUN apk add patchelf rustup gcc libstdc++ +RUN rustup-init -t ${RUST_TARGET} -y +ENV PATH="/root/.cargo/bin:$PATH" + +# These are the required shared libraries for bun and opencode to run +# They are copied from this base image to ensure compatibility +# The `patchelf` tool is used in runtime to set the interpreter after unpacking the binary +RUN cp --dereference \ + /usr/bin/patchelf \ + /usr/lib/libstdc++.so.6 \ + /usr/lib/libgcc_s.so.1 \ + /lib/${LIBC_NAME} \ + ./embedded && \ + patchelf --set-rpath '$ORIGIN' ./embedded/patchelf && \ + ln -sf ${LIBC_NAME} ./embedded/${LD_NAME} + +COPY --from=dist ${OPENCODE_ARTIFACT}/bin/opencode ./embedded/opencode +RUN patchelf --set-rpath '$ORIGIN' ./embedded/opencode + +# Pull the code and build the launcher +# It will bundle the files we put in ./embedded above +ADD . /code +RUN cargo build --release --target ${RUST_TARGET} && \ + cp /code/target/${RUST_TARGET}/release/launcher /output/opencode + +# This step is used together with buildx `--output=type=local,dest=$PATH` to extract the built binary +# Otherwise we need to create a temporary container to copy the binary out +FROM scratch AS output-step +COPY --from=build-step /output/opencode opencode diff --git a/packages/opencode/portable/src/main.rs b/packages/opencode/portable/src/main.rs new file mode 100644 index 00000000000..85896ca26ca --- /dev/null +++ b/packages/opencode/portable/src/main.rs @@ -0,0 +1,121 @@ +use std::fs::{self, File}; +use std::io::Write; +use std::os::unix::fs::{symlink, PermissionsExt}; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitCode}; +use tempfile::TempDir; + +const EXE: &str = "opencode"; +const PATCHELF: &str = "patchelf"; +#[cfg(target_arch = "x86_64")] +const LIBC_MUSL: &str = "libc.musl-x86_64.so.1"; +#[cfg(target_arch = "aarch64")] +const LIBC_MUSL: &str = "libc.musl-aarch64.so.1"; +#[cfg(target_arch = "x86_64")] +const LD_MUSL: &str = "ld-musl-x86_64.so.1"; +#[cfg(target_arch = "aarch64")] +const LD_MUSL: &str = "ld-musl-aarch64.so.1"; +const LIBSTDCPP: &str = "libstdc++.so.6"; +const LIBGCC: &str = "libgcc_s.so.1"; + +// Embed the files at compile time +static EMBEDDED_EXE: &[u8] = include_bytes!(concat!("../embedded/", "opencode")); +static EMBEDDED_PATCHELF: &[u8] = include_bytes!(concat!("../embedded/", "patchelf")); +#[cfg(target_arch = "x86_64")] +static EMBEDDED_LIBC_MUSL: &[u8] = include_bytes!(concat!("../embedded/", "libc.musl-x86_64.so.1")); +#[cfg(target_arch = "aarch64")] +static EMBEDDED_LIBC_MUSL: &[u8] = include_bytes!(concat!("../embedded/", "libc.musl-aarch64.so.1")); +static EMBEDDED_LIBSTDCPP: &[u8] = include_bytes!(concat!("../embedded/", "libstdc++.so.6")); +static EMBEDDED_LIBGCC: &[u8] = include_bytes!(concat!("../embedded/", "libgcc_s.so.1")); + +fn main() -> std::process::ExitCode { + // Create temporary directory with automatic cleanup + let tmp_dir = match TempDir::with_prefix("opencode-tmp-") { + Ok(dir) => dir, + Err(e) => { + eprintln!("Error: {}", e); + return ExitCode::FAILURE; + } + }; + + let result = (|| -> Result> { + // Extract embedded files + let exe_path = extract_file(tmp_dir.path(), EXE, EMBEDDED_EXE)?; + let patchelf_path = extract_file(tmp_dir.path(), PATCHELF, EMBEDDED_PATCHELF)?; + extract_file(tmp_dir.path(), LIBC_MUSL, EMBEDDED_LIBC_MUSL)?; + extract_file(tmp_dir.path(), LIBSTDCPP, EMBEDDED_LIBSTDCPP)?; + extract_file(tmp_dir.path(), LIBGCC, EMBEDDED_LIBGCC)?; + + // Create symlink for ld-musl (same as libc) + let ld_path = tmp_dir.path().join(LD_MUSL); + symlink(LIBC_MUSL, &ld_path)?; + + // Make executables executable (libraries don't need +x) + set_executable(&exe_path)?; + set_executable(&ld_path)?; + set_executable(&patchelf_path)?; + + // Patch the ELF binary to use our embedded interpreter + patch_elf( + patchelf_path.to_str().ok_or("Invalid patchelf path")?, + exe_path.to_str().ok_or("Invalid exe path")?, + ld_path.to_str().ok_or("Invalid ld path")?, + )?; + + // Collect command-line arguments (skip argv[0] which is this launcher) + let args: Vec = std::env::args().skip(1).collect(); + + // Launch the embedded exe + let mut child = Command::new(&exe_path).args(&args).spawn()?; + + // Wait for the child process to complete + let status = child.wait()?; + + Ok(ExitCode::from(status.code().unwrap_or(1) as u8)) + })(); + + match result { + Ok(code) => code, + Err(e) => { + eprintln!("Error: {}", e); + ExitCode::FAILURE + } + } +} + +fn extract_file( + dir: &Path, + name: &str, + data: &[u8], +) -> Result> { + let path = dir.join(name); + let mut file = File::create(&path)?; + file.write_all(data)?; + Ok(path) +} + +fn set_executable(path: &Path) -> Result<(), Box> { + let mut perms = fs::metadata(path)?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(path, perms)?; + Ok(()) +} + +fn patch_elf( + patchelf_path: &str, + elf_path: &str, + ld_path: &str, +) -> Result<(), Box> { + let status = Command::new(ld_path) + .arg(patchelf_path) + .arg("--set-interpreter") + .arg(ld_path) + .arg(elf_path) + .status()?; + + if status.success() { + return Ok(()); + } + + Err("patchelf failed".into()) +} diff --git a/packages/opencode/script/build-portable.ts b/packages/opencode/script/build-portable.ts new file mode 100644 index 00000000000..a7ac18f842d --- /dev/null +++ b/packages/opencode/script/build-portable.ts @@ -0,0 +1,50 @@ +#!/usr/bin/env bun +import { $ } from "bun" +import { fileURLToPath } from "url" + +const dir = fileURLToPath(new URL("..", import.meta.url)) +process.chdir(dir) + +const bunVersion = process.env.BUN_VERSION ?? Bun.version + +const builds = [ + { + name: "opencode-linux-x64-musl-portable", + platform: "linux/amd64", + rust: "x86_64-unknown-linux-musl", + artifact: "opencode-linux-x64-musl", + ld: "ld-musl-x86_64.so.1", + libc: "libc.musl-x86_64.so.1", + cpu: "x64", + }, + { + name: "opencode-linux-arm64-musl-portable", + platform: "linux/arm64", + rust: "aarch64-unknown-linux-musl", + artifact: "opencode-linux-arm64-musl", + ld: "ld-musl-aarch64.so.1", + libc: "libc.musl-aarch64.so.1", + cpu: "arm64", + }, +] + +for (const item of builds) { + console.log(`building ${item.name}`) + await $`mkdir -p dist/${item.name}/bin` + + await $`docker build \ + --platform=${item.platform} \ + --build-context dist=dist \ + --output=type=local,dest=dist/${item.name}/bin \ + --target output-step \ + --build-arg BUN_VERSION=${bunVersion} \ + --build-arg RUST_TARGET=${item.rust} \ + --build-arg OPENCODE_ARTIFACT=${item.artifact} \ + --build-arg LD_NAME=${item.ld} \ + --build-arg LIBC_NAME=${item.libc} \ + --build-arg CPU=${item.cpu} \ + portable`.quiet(true).catch((e) => { + console.error(`Failed to build ${item.name}:`, e.stderr.toString()) + process.exit(e.exitCode) + }) +}