|
1 | | -#!/usr/bin/env bash |
| 1 | +#!/bin/sh |
2 | 2 | # |
3 | | -# BrowserCode installer — bytecode / serve-only build. |
| 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. |
4 | 6 | # |
5 | | -# Hosted at https://bcode.sh/bytecode, alongside (not replacing) |
6 | | -# https://bcode.sh/install: |
| 7 | +# curl -fsSL https://bcode.sh/bytecode | sh -s -- --no-modify-path --version 0.1.19 |
7 | 8 | # |
8 | | -# curl -fsSL https://bcode.sh/bytecode | bash -s -- --no-modify-path --version 0.0.3 |
| 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. |
9 | 12 | # |
10 | | -# Installs the `bcode-linux-<arch>-serve` asset, which provides ONLY |
11 | | -# `bcode serve` — `run`, `tui`, `web`, `github` and the rest are absent and |
12 | | -# exit 1. It exists for headless containers; anyone installing bcode on a |
13 | | -# laptop wants https://bcode.sh/install. |
14 | | -# |
15 | | -# `install.sh` is the published path docs point at and stays untouched. This is |
16 | | -# additive: a second script, a second URL, a second release asset. |
17 | | -# |
18 | | -# Linux only. Deliberately much shorter than install.sh: one build per arch (no |
19 | | -# baseline/AVX2 detection), no shell-rc editing (containers set PATH in the |
20 | | -# Dockerfile), no uv hint. Takes install.sh's flags so switching is a one-word |
21 | | -# change to the URL in a Dockerfile. |
22 | | -set -euo pipefail |
23 | | - |
24 | | -APP=bcode |
25 | | -VARIANT=serve |
26 | | -REPO=browser-use/browsercode |
| 13 | +# POSIX sh, not bash: Alpine is a supported target here and ships no bash. |
| 14 | +set -eu |
27 | 15 |
|
28 | | -MUTED='\033[0;2m' |
| 16 | +REPO=browser-use/browsercode |
| 17 | +DIM='\033[0;2m' |
29 | 18 | RED='\033[0;31m' |
30 | | -ORANGE='\033[38;5;214m' |
31 | | -NC='\033[0m' |
32 | | - |
33 | | -usage() { |
34 | | - cat <<EOF |
35 | | -BrowserCode Installer (bytecode / serve-only build) |
| 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 | +} |
36 | 28 |
|
37 | | -Usage: install-bytecode.sh [options] |
| 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 | +} |
38 | 36 |
|
39 | | -Installs a bcode binary that provides ONLY 'bcode serve'. For the full CLI use |
40 | | -https://bcode.sh/install instead. |
| 37 | +usage() { |
| 38 | + cat >&2 <<EOF |
| 39 | +BrowserCode installer (serve-only build) |
41 | 40 |
|
42 | | -Options: |
43 | | - -h, --help Display this help message |
44 | | - -v, --version <version> Install a specific version (e.g. 0.0.3) |
45 | | - --install-dir <dir> Install to <dir> (default: \$HOME/.bcode/bin) |
46 | | - --no-modify-path Accepted for parity with install.sh; this script |
47 | | - never edits shell config files. |
| 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 |
48 | 46 |
|
49 | | -Examples: |
50 | | - curl -fsSL https://bcode.sh/bytecode | bash |
51 | | - curl -fsSL https://bcode.sh/bytecode | bash -s -- --no-modify-path --version 0.0.3 |
| 47 | +Installs a bcode that provides ONLY 'bcode serve'. |
| 48 | +For the full CLI: curl -fsSL https://bcode.sh/install | bash |
52 | 49 | EOF |
53 | 50 | } |
54 | 51 |
|
55 | | -requested_version=${VERSION:-} |
56 | | -install_dir=${BCODE_INSTALL_DIR:-$HOME/.bcode/bin} |
57 | | - |
58 | | -while [[ $# -gt 0 ]]; do |
59 | | - case "$1" in |
60 | | - -h|--help) |
61 | | - usage |
62 | | - exit 0 |
63 | | - ;; |
64 | | - -v|--version) |
65 | | - if [[ -n "${2:-}" ]]; then |
66 | | - requested_version="$2" |
67 | | - shift 2 |
68 | | - else |
69 | | - echo -e "${RED}Error: --version requires a version argument${NC}" >&2 |
70 | | - exit 1 |
71 | | - fi |
72 | | - ;; |
73 | | - --install-dir) |
74 | | - if [[ -n "${2:-}" ]]; then |
75 | | - install_dir="$2" |
76 | | - shift 2 |
77 | | - else |
78 | | - echo -e "${RED}Error: --install-dir requires a path argument${NC}" >&2 |
79 | | - exit 1 |
80 | | - fi |
81 | | - ;; |
82 | | - --no-modify-path) |
83 | | - # No-op: this script never touches shell config. Accepted so an |
84 | | - # existing install.sh invocation works verbatim against this URL. |
85 | | - shift |
86 | | - ;; |
87 | | - *) |
88 | | - echo -e "${ORANGE}Warning: Unknown option '$1'${NC}" >&2 |
89 | | - shift |
90 | | - ;; |
91 | | - esac |
| 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 |
92 | 70 | done |
93 | 71 |
|
94 | | -raw_os=$(uname -s) |
95 | | -case "$raw_os" in |
96 | | - Linux*) os="linux" ;; |
97 | | - *) |
98 | | - echo -e "${RED}Error: the ${VARIANT} build is published for linux only (detected: ${raw_os}).${NC}" >&2 |
99 | | - echo -e "${MUTED}Use https://bcode.sh/install for the standard cross-platform binary.${NC}" >&2 |
100 | | - exit 1 |
101 | | - ;; |
102 | | -esac |
103 | | - |
104 | | -arch=$(uname -m) |
105 | | -case "$arch" in |
106 | | - aarch64|arm64) arch="arm64" ;; |
107 | | - x86_64|amd64) arch="x64" ;; |
108 | | - *) |
109 | | - echo -e "${RED}Error: unsupported architecture '${arch}'. Supported: arm64, x64.${NC}" >&2 |
110 | | - exit 1 |
111 | | - ;; |
112 | | -esac |
113 | | - |
114 | 72 | for tool in curl tar; do |
115 | | - if ! command -v "$tool" >/dev/null 2>&1; then |
116 | | - echo -e "${RED}Error: '${tool}' is required but not installed.${NC}" >&2 |
117 | | - exit 1 |
118 | | - fi |
| 73 | + command -v "$tool" >/dev/null 2>&1 || die "'$tool' is required but not installed." |
119 | 74 | done |
120 | 75 |
|
121 | | -# A glibc binary will not start on musl, so pick the matching build rather than |
122 | | -# installing something that dies at exec. |
123 | | -is_musl=false |
124 | | -if [ -f /etc/alpine-release ]; then |
125 | | - is_musl=true |
126 | | -elif command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; then |
127 | | - is_musl=true |
| 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." |
128 | 93 | fi |
129 | 94 |
|
130 | | -# Non-baseline x64 builds require AVX2. This variant publishes no baseline |
131 | | -# asset, so say that rather than installing a binary that SIGILLs on first run. |
132 | | -# (Adding it is one entry in packages/bcode-serve/script/build.ts if ever needed.) |
133 | | -# Only trip when /proc/cpuinfo is actually readable — unknown is not "absent". |
134 | | -if [ "$arch" = "x64" ] && [ -r /proc/cpuinfo ] && ! grep -qwi avx2 /proc/cpuinfo; then |
135 | | - echo -e "${RED}Error: this CPU lacks AVX2, and no baseline ${VARIANT} build is published.${NC}" >&2 |
136 | | - echo -e "${MUTED}Use https://bcode.sh/install, which ships a baseline binary.${NC}" >&2 |
137 | | - exit 1 |
| 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 |
138 | 100 | fi |
| 101 | +asset=bcode-$target-serve.tar.gz |
139 | 102 |
|
140 | | -target="${os}-${arch}" |
141 | | -[ "$is_musl" = true ] && target="${target}-musl" |
142 | | -filename="${APP}-${target}-${VARIANT}.tar.gz" |
143 | | - |
144 | | -if [ -z "$requested_version" ]; then |
145 | | - url="https://github.com/${REPO}/releases/latest/download/${filename}" |
146 | | - specific_version=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null \ |
147 | | - | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p' || true) |
148 | | - if [ -z "$specific_version" ]; then |
149 | | - echo -e "${RED}Failed to resolve the latest version.${NC}" >&2 |
150 | | - echo -e "${MUTED}If the repo is private, pin a version: --version <ver>${NC}" >&2 |
151 | | - exit 1 |
152 | | - fi |
| 103 | +if [ -n "$version" ]; then |
| 104 | + version=${version#v} |
153 | 105 | else |
154 | | - specific_version="${requested_version#v}" |
155 | | - url="https://github.com/${REPO}/releases/download/v${specific_version}/${filename}" |
| 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>." |
156 | 109 | fi |
| 110 | +url=https://github.com/$REPO/releases/download/v$version/$asset |
157 | 111 |
|
158 | | -echo -e "${MUTED}Installing ${NC}${APP} ${MUTED}(${VARIANT} build) version: ${NC}${specific_version}" |
| 112 | +say "Installing bcode $version ($target, serve build)" |
159 | 113 |
|
160 | | -tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/bcode_bytecode_install.XXXXXX") |
161 | | -staged="" |
| 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. |
162 | 118 | cleanup() { |
163 | | - rm -rf "$tmp_dir" |
164 | | - [ -n "${staged:-}" ] && rm -f "$staged" |
165 | | - return 0 |
| 119 | + rm -rf "$tmp" |
| 120 | + [ -n "$staged" ] && rm -f "$staged" |
| 121 | + return 0 |
166 | 122 | } |
167 | 123 | trap cleanup EXIT |
168 | | - |
169 | | -# Fail loudly rather than unpacking a 404 body. Releases predating this variant |
170 | | -# legitimately lack the asset, so say that instead of erroring out of tar. |
171 | | -if ! curl -fsSL -o "${tmp_dir}/${filename}" "$url"; then |
172 | | - echo -e "${RED}Error: could not download ${filename} for v${specific_version}.${NC}" >&2 |
173 | | - echo -e "${MUTED}URL: ${url}${NC}" >&2 |
174 | | - echo -e "${MUTED}Releases predating the ${VARIANT} variant do not publish this asset.${NC}" >&2 |
175 | | - echo -e "${MUTED}Check https://github.com/${REPO}/releases for one that does, or use https://bcode.sh/install.${NC}" >&2 |
176 | | - exit 1 |
177 | | -fi |
178 | | - |
179 | | -tar -xzf "${tmp_dir}/${filename}" -C "$tmp_dir" |
180 | | - |
181 | | -if [ ! -f "${tmp_dir}/${APP}" ]; then |
182 | | - echo -e "${RED}Error: archive did not contain a '${APP}' binary.${NC}" >&2 |
183 | | - exit 1 |
| 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." |
184 | 149 | fi |
185 | | - |
186 | | -# Stage inside the install dir, validate, then swap into place. |
187 | | -# |
188 | | -# Validating before the swap keeps a corrupt or wrong-libc download from |
189 | | -# replacing a working bcode. Staging in the install dir rather than the temp dir |
190 | | -# keeps that safety without requiring an exec-capable /tmp — `noexec` there is |
191 | | -# common hardening, and the install dir has to allow exec regardless. It also |
192 | | -# makes the final step a same-filesystem `mv`, so the swap is atomic and no |
193 | | -# reader can observe a half-written binary. |
194 | | -mkdir -p "$install_dir" |
195 | | -staged=$(mktemp "${install_dir}/.${APP}.XXXXXX") |
196 | | -mv "${tmp_dir}/${APP}" "$staged" |
| 150 | +staged=$(mktemp "$install_dir/.bcode.XXXXXX") |
| 151 | +mv "$tmp/bcode" "$staged" |
197 | 152 | chmod 755 "$staged" |
198 | 153 |
|
199 | | -if ! installed_version=$("$staged" --version 2>/dev/null); then |
200 | | - echo -e "${RED}Error: downloaded binary failed to run; leaving any existing install untouched.${NC}" >&2 |
201 | | - echo -e "${MUTED}If ${install_dir} is mounted noexec, install elsewhere with --install-dir.${NC}" >&2 |
202 | | - exit 1 |
| 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." |
203 | 160 | fi |
204 | 161 |
|
205 | | -mv "$staged" "${install_dir}/${APP}" |
206 | | -staged="" |
| 162 | +mv "$staged" "$install_dir/bcode" |
| 163 | +staged= |
207 | 164 |
|
208 | | -echo -e "${MUTED}Installed ${NC}${install_dir}/${APP}${MUTED} (${installed_version})${NC}" |
209 | | -echo -e "${MUTED}This build provides '${APP} serve' only.${NC}" |
| 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