|
| 1 | +#!/bin/sh |
| 2 | +# |
| 3 | +# BrowserCode installer — serve-only build. Hosted at https://bcode.sh/bytecode, |
| 4 | +# alongside (not replacing) https://bcode.sh/install, which stays the path for |
| 5 | +# the full CLI. |
| 6 | +# |
| 7 | +# curl -fsSL https://bcode.sh/bytecode | sh -s -- --no-modify-path --version 0.1.19 |
| 8 | +# |
| 9 | +# Installs `bcode-linux-<arch>[-musl]-serve`, which provides ONLY `bcode serve`; |
| 10 | +# run, tui, web, github and the rest are absent and exit 1. It exists for headless |
| 11 | +# containers. |
| 12 | +# |
| 13 | +# POSIX sh, not bash: Alpine is a supported target here and ships no bash. |
| 14 | +set -eu |
| 15 | + |
| 16 | +REPO=browser-use/browsercode |
| 17 | +DIM='\033[0;2m' |
| 18 | +RED='\033[0;31m' |
| 19 | +OFF='\033[0m' |
| 20 | + |
| 21 | +say() { printf "${DIM}%s${OFF}\n" "$1" >&2; } |
| 22 | +die() { |
| 23 | + printf "${RED}%s${OFF}\n" "$1" >&2 |
| 24 | + shift |
| 25 | + for line in "$@"; do [ -n "$line" ] && printf "${DIM}%s${OFF}\n" "$line" >&2; done |
| 26 | + exit 1 |
| 27 | +} |
| 28 | + |
| 29 | +# Reject empty and flag-shaped values: `--version "$UNSET"` would otherwise |
| 30 | +# silently install latest, which is exactly what pinning exists to prevent. |
| 31 | +need() { |
| 32 | + case $2 in |
| 33 | + "" | -*) die "$1 needs a value (got: ${2:-<missing>})" ;; |
| 34 | + esac |
| 35 | +} |
| 36 | + |
| 37 | +usage() { |
| 38 | + cat >&2 <<EOF |
| 39 | +BrowserCode installer (serve-only build) |
| 40 | +
|
| 41 | + -v, --version <ver> version to install (default: latest release) |
| 42 | + --install-dir <d> where to install (default: \$HOME/.bcode/bin) |
| 43 | + --no-modify-path accepted for install.sh parity; this script never edits |
| 44 | + shell config files |
| 45 | + -h, --help show this help |
| 46 | +
|
| 47 | +Installs a bcode that provides ONLY 'bcode serve'. |
| 48 | +For the full CLI: curl -fsSL https://bcode.sh/install | bash |
| 49 | +EOF |
| 50 | +} |
| 51 | + |
| 52 | +# Namespaced on purpose: a bare VERSION is a common Dockerfile ARG, and picking |
| 53 | +# it up here would silently install the wrong build. |
| 54 | +version=${BCODE_VERSION:-} |
| 55 | +# HOME is routinely unset under `--user`/runAsUser with no passwd entry. |
| 56 | +install_dir=${BCODE_INSTALL_DIR:-${HOME:-/root}/.bcode/bin} |
| 57 | + |
| 58 | +while [ $# -gt 0 ]; do |
| 59 | + case $1 in |
| 60 | + -v | --version) need "--version" "${2:-}"; version=$2; shift 2 ;; |
| 61 | + --install-dir) need "--install-dir" "${2:-}"; install_dir=$2; shift 2 ;; |
| 62 | + --no-modify-path) shift ;; |
| 63 | + # Tolerated: wrappers relaying args often forward an extra one. |
| 64 | + --) shift ;; |
| 65 | + -h | --help) usage; exit 0 ;; |
| 66 | + # Fail rather than warn: a typo'd flag would otherwise quietly install |
| 67 | + # "latest" into a build that meant to pin a version. |
| 68 | + *) die "Unknown option: $1" "Run with --help for usage." ;; |
| 69 | + esac |
| 70 | +done |
| 71 | + |
| 72 | +for tool in curl tar; do |
| 73 | + command -v "$tool" >/dev/null 2>&1 || die "'$tool' is required but not installed." |
| 74 | +done |
| 75 | + |
| 76 | +[ "$(uname -s)" = Linux ] || die \ |
| 77 | + "The serve build is published for linux only (got $(uname -s))." \ |
| 78 | + "Use https://bcode.sh/install for the standard cross-platform binary." |
| 79 | + |
| 80 | +case $(uname -m) in |
| 81 | + aarch64 | arm64) arch=arm64 ;; |
| 82 | + x86_64 | amd64) arch=x64 ;; |
| 83 | + *) die "Unsupported architecture: $(uname -m)." "Supported: arm64, x64." ;; |
| 84 | +esac |
| 85 | + |
| 86 | +# Non-baseline x64 builds need AVX2 and no baseline serve asset is published. |
| 87 | +# Only decide when /proc/cpuinfo actually carries a flags line: emulated or |
| 88 | +# redacted cpuinfo (qemu `--platform linux/amd64` on arm64, lxcfs, some |
| 89 | +# hypervisors) has none, and unknown is not the same as absent. |
| 90 | +if [ "$arch" = x64 ] && grep -qi '^flags' /proc/cpuinfo 2>/dev/null && ! grep -qwi avx2 /proc/cpuinfo; then |
| 91 | + die "This CPU has no AVX2 and no baseline serve build is published." \ |
| 92 | + "Use https://bcode.sh/install, which ships a baseline binary." |
| 93 | +fi |
| 94 | + |
| 95 | +# A glibc binary cannot exec on musl. musl's ldd prints its banner and then exits |
| 96 | +# non-zero, so match its output — the pipeline's status is grep's, not ldd's. |
| 97 | +target=linux-$arch |
| 98 | +if [ -f /etc/alpine-release ] || ldd --version 2>&1 | grep -qi musl; then |
| 99 | + target=$target-musl |
| 100 | +fi |
| 101 | +asset=bcode-$target-serve.tar.gz |
| 102 | + |
| 103 | +if [ -n "$version" ]; then |
| 104 | + version=${version#v} |
| 105 | +else |
| 106 | + version=$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null | |
| 107 | + sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p') |
| 108 | + [ -n "$version" ] || die "Could not resolve the latest version." "Pin one with --version <ver>." |
| 109 | +fi |
| 110 | +url=https://github.com/$REPO/releases/download/v$version/$asset |
| 111 | + |
| 112 | +say "Installing bcode $version ($target, serve build)" |
| 113 | + |
| 114 | +tmp=$(mktemp -d) |
| 115 | +staged= |
| 116 | +# `return 0` is load-bearing: the `&&` above it returns 1 whenever staged is |
| 117 | +# empty, which under `set -e` would turn a clean run into a non-zero exit. |
| 118 | +cleanup() { |
| 119 | + rm -rf "$tmp" |
| 120 | + [ -n "$staged" ] && rm -f "$staged" |
| 121 | + return 0 |
| 122 | +} |
| 123 | +trap cleanup EXIT |
| 124 | +# ash/dash do not run the EXIT trap on a signal; `docker build` cancellation |
| 125 | +# sends TERM. Exiting from the handler routes through the EXIT trap above. |
| 126 | +trap 'exit 130' INT |
| 127 | +trap 'exit 143' TERM |
| 128 | +trap 'exit 129' HUP |
| 129 | + |
| 130 | +curl -fsSL -o "$tmp/$asset" "$url" || die \ |
| 131 | + "Could not download $asset for v$version." \ |
| 132 | + "URL: $url" \ |
| 133 | + "Releases published before this variant existed do not carry the asset." |
| 134 | + |
| 135 | +tar -xzf "$tmp/$asset" -C "$tmp" |
| 136 | +[ -f "$tmp/bcode" ] || die "Archive did not contain a bcode binary." |
| 137 | + |
| 138 | +# Stage inside the install dir, validate, then swap. Validating first means a bad |
| 139 | +# download never replaces a working bcode; staging here rather than in /tmp avoids |
| 140 | +# requiring an exec-capable /tmp (noexec there is common hardening) and makes the |
| 141 | +# final step a same-filesystem rename, so the swap is atomic. |
| 142 | +case $install_dir in |
| 143 | + "") die "Install directory cannot be empty." ;; |
| 144 | + -*) die "Install directory must not start with '-' (got: $install_dir)." ;; |
| 145 | +esac |
| 146 | +mkdir -p -- "$install_dir" |
| 147 | +if [ -d "$install_dir/bcode" ]; then |
| 148 | + die "$install_dir/bcode is a directory; refusing to install over it." |
| 149 | +fi |
| 150 | +staged=$(mktemp "$install_dir/.bcode.XXXXXX") |
| 151 | +mv "$tmp/bcode" "$staged" |
| 152 | +chmod 755 "$staged" |
| 153 | + |
| 154 | +# Keep the binary's own stderr: on a libc mismatch it names the missing loader, |
| 155 | +# which is the actual diagnosis. |
| 156 | +if ! installed=$("$staged" --version 2>"$tmp/err"); then |
| 157 | + die "Downloaded binary failed to run; existing install left untouched." \ |
| 158 | + "$(cat "$tmp/err" 2>/dev/null)" \ |
| 159 | + "If $install_dir is mounted noexec, pass --install-dir." |
| 160 | +fi |
| 161 | + |
| 162 | +mv "$staged" "$install_dir/bcode" |
| 163 | +staged= |
| 164 | + |
| 165 | +say "Installed $install_dir/bcode ($installed) — provides 'bcode serve' only" |
| 166 | +case ":$PATH:" in |
| 167 | + *":$install_dir:"*) ;; |
| 168 | + *) say "Not on PATH. Add it with: export PATH=\"$install_dir:\$PATH\"" ;; |
| 169 | +esac |
0 commit comments