Skip to content

Commit 7f81be2

Browse files
committed
feat(opencode): added portable static builds
- Portable builds are multi arch, supporting linux-x64 and linux-aarch64 - `build-portable.ts` is replacing the existing musl binaries with their static variants - Files in dist/ retain their name "opencode-linux-*-musl" - `install` script updated to parse the system's glibc version in order to select the static musl variant even on non-alpine distros - build using: `bun run build && bun run build:portable` - build requires `docker` to be installed
1 parent 0bc4a43 commit 7f81be2

File tree

9 files changed

+432
-0
lines changed

9 files changed

+432
-0
lines changed

install

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ RED='\033[0;31m'
77
ORANGE='\033[38;5;214m'
88
NC='\033[0m' # No Color
99

10+
# Minimum glibc version may change with Bun upgrades.
11+
# Update using: `readelf --version-info dist/opencode-linux-x64/bin/opencode | rg -o "GLIBC_[0-9.]+" | sort -V | tail -1`
12+
GLIBC_REQUIRED_VERSION=2.25
13+
1014
usage() {
1115
cat <<EOF
1216
OpenCode Installer
@@ -125,6 +129,23 @@ else
125129
is_musl=true
126130
fi
127131
fi
132+
133+
glibc_version=""
134+
if [ "$is_musl" = "false" ] && command -v getconf >/dev/null 2>&1; then
135+
glibc_version=$(getconf GNU_LIBC_VERSION 2>/dev/null | awk '{print $2}')
136+
fi
137+
138+
if [ "$is_musl" = "false" ] && [ -z "$glibc_version" ] && command -v ldd >/dev/null 2>&1; then
139+
glibc_version=$(ldd --version 2>&1 | head -n1 | sed -n 's/.* \([0-9]\+\.[0-9]\+\).*/\1/p')
140+
fi
141+
142+
if [ "$is_musl" = "false" ] && [ -n "$glibc_version" ]; then
143+
# Sort the versions to get the min(GLIBC_REQUIRED_VERSION, glibc_version)
144+
glibc_min=$(printf '%s\n' "$GLIBC_REQUIRED_VERSION" "$glibc_version" | sort -V | head -n1)
145+
if [ "$glibc_min" != "$GLIBC_REQUIRED_VERSION" ]; then
146+
is_musl=true
147+
fi
148+
fi
128149
fi
129150

130151
needs_baseline=false

packages/opencode/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"typecheck": "tsgo --noEmit",
1010
"test": "bun test",
1111
"build": "bun run script/build.ts",
12+
"build:portable": "bun run script/build-portable.ts",
1213
"dev": "bun run --conditions=browser ./src/index.ts",
1314
"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'",
1415
"clean": "echo 'Cleaning up...' && rm -rf node_modules dist",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
embedded
2+
target
3+
Dockerfile
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
target
2+
output

packages/opencode/portable/Cargo.lock

Lines changed: 162 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "launcher"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
tempfile = "3.24"
8+
9+
[build-dependencies]
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# syntax=docker/dockerfile:1
2+
ARG BUN_VERSION="1.3.5"
3+
ARG TARGETARCH
4+
5+
FROM oven/bun:${BUN_VERSION}-alpine AS build-step
6+
ARG TARGETARCH
7+
ARG RUST_TARGET
8+
ARG OPENCODE_ARTIFACT
9+
ARG LD_NAME
10+
ARG LIBC_NAME
11+
ARG CPU
12+
13+
WORKDIR /code
14+
RUN mkdir -p ./embedded /output
15+
16+
# Install deps
17+
RUN apk add patchelf rustup gcc libstdc++
18+
RUN rustup-init -t ${RUST_TARGET} -y
19+
ENV PATH="/root/.cargo/bin:$PATH"
20+
21+
# These are the required shared libraries for bun and opencode to run
22+
# They are copied from this base image to ensure compatibility
23+
# The `patchelf` tool is used in runtime to set the interpreter after unpacking the binary
24+
RUN cp --dereference \
25+
/usr/bin/patchelf \
26+
/usr/lib/libstdc++.so.6 \
27+
/usr/lib/libgcc_s.so.1 \
28+
/lib/${LIBC_NAME} \
29+
./embedded && \
30+
patchelf --set-rpath '$ORIGIN' ./embedded/patchelf && \
31+
ln -sf ${LIBC_NAME} ./embedded/${LD_NAME}
32+
33+
COPY --from=dist ${OPENCODE_ARTIFACT}/bin/opencode ./embedded/opencode
34+
RUN patchelf --set-rpath '$ORIGIN' ./embedded/opencode
35+
36+
# Pull the code and build the launcher
37+
# It will bundle the files we put in ./embedded above
38+
ADD . /code
39+
RUN cargo build --release --target ${RUST_TARGET} && \
40+
cp /code/target/${RUST_TARGET}/release/launcher /output/opencode
41+
42+
# This step is used together with buildx `--output=type=local,dest=$PATH` to extract the built binary
43+
# Otherwise we need to create a temporary container to copy the binary out
44+
FROM scratch AS output-step
45+
COPY --from=build-step /output/opencode opencode
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use std::fs::{self, File};
2+
use std::io::Write;
3+
use std::os::unix::fs::{symlink, PermissionsExt};
4+
use std::path::{Path, PathBuf};
5+
use std::process::{Command, ExitCode};
6+
use tempfile::TempDir;
7+
8+
const EXE: &str = "opencode";
9+
const PATCHELF: &str = "patchelf";
10+
#[cfg(target_arch = "x86_64")]
11+
const LIBC_MUSL: &str = "libc.musl-x86_64.so.1";
12+
#[cfg(target_arch = "aarch64")]
13+
const LIBC_MUSL: &str = "libc.musl-aarch64.so.1";
14+
#[cfg(target_arch = "x86_64")]
15+
const LD_MUSL: &str = "ld-musl-x86_64.so.1";
16+
#[cfg(target_arch = "aarch64")]
17+
const LD_MUSL: &str = "ld-musl-aarch64.so.1";
18+
const LIBSTDCPP: &str = "libstdc++.so.6";
19+
const LIBGCC: &str = "libgcc_s.so.1";
20+
21+
// Embed the files at compile time
22+
static EMBEDDED_EXE: &[u8] = include_bytes!(concat!("../embedded/", "opencode"));
23+
static EMBEDDED_PATCHELF: &[u8] = include_bytes!(concat!("../embedded/", "patchelf"));
24+
#[cfg(target_arch = "x86_64")]
25+
static EMBEDDED_LIBC_MUSL: &[u8] = include_bytes!(concat!("../embedded/", "libc.musl-x86_64.so.1"));
26+
#[cfg(target_arch = "aarch64")]
27+
static EMBEDDED_LIBC_MUSL: &[u8] = include_bytes!(concat!("../embedded/", "libc.musl-aarch64.so.1"));
28+
static EMBEDDED_LIBSTDCPP: &[u8] = include_bytes!(concat!("../embedded/", "libstdc++.so.6"));
29+
static EMBEDDED_LIBGCC: &[u8] = include_bytes!(concat!("../embedded/", "libgcc_s.so.1"));
30+
31+
fn main() -> std::process::ExitCode {
32+
// Create temporary directory with automatic cleanup
33+
let tmp_dir = match TempDir::with_prefix("opencode-tmp-") {
34+
Ok(dir) => dir,
35+
Err(e) => {
36+
eprintln!("Error: {}", e);
37+
return ExitCode::FAILURE;
38+
}
39+
};
40+
41+
let result = (|| -> Result<ExitCode, Box<dyn std::error::Error>> {
42+
// Extract embedded files
43+
let exe_path = extract_file(tmp_dir.path(), EXE, EMBEDDED_EXE)?;
44+
let patchelf_path = extract_file(tmp_dir.path(), PATCHELF, EMBEDDED_PATCHELF)?;
45+
extract_file(tmp_dir.path(), LIBC_MUSL, EMBEDDED_LIBC_MUSL)?;
46+
extract_file(tmp_dir.path(), LIBSTDCPP, EMBEDDED_LIBSTDCPP)?;
47+
extract_file(tmp_dir.path(), LIBGCC, EMBEDDED_LIBGCC)?;
48+
49+
// Create symlink for ld-musl (same as libc)
50+
let ld_path = tmp_dir.path().join(LD_MUSL);
51+
symlink(LIBC_MUSL, &ld_path)?;
52+
53+
// Make executables executable (libraries don't need +x)
54+
set_executable(&exe_path)?;
55+
set_executable(&ld_path)?;
56+
set_executable(&patchelf_path)?;
57+
58+
// Patch the ELF binary to use our embedded interpreter
59+
patch_elf(
60+
patchelf_path.to_str().ok_or("Invalid patchelf path")?,
61+
exe_path.to_str().ok_or("Invalid exe path")?,
62+
ld_path.to_str().ok_or("Invalid ld path")?,
63+
)?;
64+
65+
// Collect command-line arguments (skip argv[0] which is this launcher)
66+
let args: Vec<String> = std::env::args().skip(1).collect();
67+
68+
// Launch the embedded exe
69+
let mut child = Command::new(&exe_path).args(&args).spawn()?;
70+
71+
// Wait for the child process to complete
72+
let status = child.wait()?;
73+
74+
Ok(ExitCode::from(status.code().unwrap_or(1) as u8))
75+
})();
76+
77+
match result {
78+
Ok(code) => code,
79+
Err(e) => {
80+
eprintln!("Error: {}", e);
81+
ExitCode::FAILURE
82+
}
83+
}
84+
}
85+
86+
fn extract_file(
87+
dir: &Path,
88+
name: &str,
89+
data: &[u8],
90+
) -> Result<PathBuf, Box<dyn std::error::Error>> {
91+
let path = dir.join(name);
92+
let mut file = File::create(&path)?;
93+
file.write_all(data)?;
94+
Ok(path)
95+
}
96+
97+
fn set_executable(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
98+
let mut perms = fs::metadata(path)?.permissions();
99+
perms.set_mode(0o755);
100+
fs::set_permissions(path, perms)?;
101+
Ok(())
102+
}
103+
104+
fn patch_elf(
105+
patchelf_path: &str,
106+
elf_path: &str,
107+
ld_path: &str,
108+
) -> Result<(), Box<dyn std::error::Error>> {
109+
let status = Command::new(ld_path)
110+
.arg(patchelf_path)
111+
.arg("--set-interpreter")
112+
.arg(ld_path)
113+
.arg(elf_path)
114+
.status()?;
115+
116+
if status.success() {
117+
return Ok(());
118+
}
119+
120+
Err("patchelf failed".into())
121+
}

0 commit comments

Comments
 (0)