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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/opencode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions packages/opencode/portable/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
embedded
target
Dockerfile
2 changes: 2 additions & 0 deletions packages/opencode/portable/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target
output
162 changes: 162 additions & 0 deletions packages/opencode/portable/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions packages/opencode/portable/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "launcher"
version = "0.1.0"
edition = "2021"

[dependencies]
tempfile = "3.24"

[build-dependencies]
45 changes: 45 additions & 0 deletions packages/opencode/portable/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
121 changes: 121 additions & 0 deletions packages/opencode/portable/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<ExitCode, Box<dyn std::error::Error>> {
// 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<String> = 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<PathBuf, Box<dyn std::error::Error>> {
let path = dir.join(name);
let mut file = File::create(&path)?;
file.write_all(data)?;
Ok(path)
}

fn set_executable(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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())
}
Loading