Repository navigation
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·370 lines (354 loc) · 16.5 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·370 lines (354 loc) · 16.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
#!/bin/sh
# Lens in one line:
#
# curl -fsSL https://raw.githubusercontent.com/adeelahmad/lense/main/install.sh | sh
#
# Gets the code (or updates it), writes the secrets once, builds and starts the stack with Docker (or in a microsandbox
# microVM, if chosen), waits until the web app answers, then opens it on the setup page with the setup code already
# filled in. Run it again to update: the secrets, the database and the archive are kept.
#
# Settings, all optional (put them before `sh`: `... | LENS_DIR=/srv/lens sh`):
# LENS_DIR where Lens goes (default: ~/lens)
# LENS_REF the branch or tag to run (default: main)
# LENS_PORT the web app's port (default: 3000, or the next free one)
# LENS_PUBLIC 1: reachable from other machines on the network, 0: this machine only (default: 1 on a server
# without a desktop, else 0)
# LENS_TARGET full (reads Office files, web pages and emails too) or lean (smaller) (default: full)
# LENS_BACKEND docker or microsandbox (one microVM, no Docker: docs/microsandbox.md). Asked on a first install
# when microsandbox can run here (Linux with KVM, or a Mac with Apple silicon); default: docker
# GITHUB_TOKEN a GitHub token to use when downloading (not needed; avoids GitHub's rate limits)
# LENS_NO_OPEN 1: don't open a browser
# LENS_YES 1: install what Lens needs without asking (it only asks at a terminal)
set -eu
# All in a function, so nothing runs until the whole script has arrived (curl | sh streams it).
main() {
REPO="${LENS_REPO:-adeelahmad/lense}"
DIR="${LENS_DIR:-$HOME/lens}"
REF="${LENS_REF:-main}"
TARGET="${LENS_TARGET:-full}"
say() { printf '\033[1m%s\033[0m\n' "$*"; }
fail() {
printf '\033[31m%s\033[0m\n' "$*" >&2
exit 1
}
have() { com
8000
mand -v "$1" >/dev/null 2>&1; }
secret() {
if have openssl; then openssl rand -hex 32; else od -An -N32 -tx1 /dev/urandom | tr -d ' \n'; fi
}
code() { LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 12; }
os="$(uname -s)"
sudo_ok() { [ "$(id -u)" = 0 ] && echo "" || echo sudo; }
# --- What runs it: Docker, or a microsandbox microVM ----------------------------------------------------------------
# microsandbox needs hardware virtualization: KVM on 64-bit Linux (or a CPU that has it, whose module isn't loaded
# yet), Apple silicon on a Mac
msb_ok() {
case "$os/$(uname -m)" in
Linux/x86_64) [ -e /dev/kvm ] || grep -Eqw 'vmx|svm' /proc/cpuinfo 2>/dev/null ;;
Linux/aarch64) [ -e /dev/kvm ] ;;
Darwin/arm64) true ;;
*) false ;;
esac
}
tty_ok() { [ "${LENS_YES:-0}" != 1 ] && (: </dev/tty) 2>/dev/null; }
BACKEND="${LENS_BACKEND:-}"
if [ -z "$BACKEND" ] && [ -f "$DIR/.env" ]; then # an existing install keeps what it runs on (its data is there)
BACKEND="$(grep '^LENS_BACKEND=' "$DIR/.env" | cut -d= -f2- || true)"
[ -n "$BACKEND" ] || BACKEND=docker
fi
if [ -z "$BACKEND" ]; then
BACKEND=docker
# asked only when there's someone to ask: piped into sh, the answer comes from the terminal, not stdin
if msb_ok && tty_ok; then
printf '\n\033[1mHow should Lens run?\033[0m\n'
echo " 1) Docker (default): a container for each part, the usual way"
echo " 2) microsandbox: one lightweight virtual machine, no Docker needed"
printf 'Choose [1]: '
read -r answer </dev/tty || answer=
case "$answer" in 2 | m*) BACKEND=microsandbox ;; esac
fi
fi
case "$BACKEND" in
docker) ;;
microsandbox) msb_ok || fail "microsandbox needs KVM on 64-bit Linux or a Mac with Apple silicon. Use LENS_BACKEND=docker." ;;
*) fail "LENS_BACKEND is docker or microsandbox, not $BACKEND." ;;
esac
# --- What's missing: listed, and installed once you say yes ---------------------------------------------------------
# Asked only at a terminal; piped in by a script or CI (no terminal, or LENS_YES=1) it goes ahead, as before.
export PATH="$HOME/.local/bin:$HOME/.microsandbox/bin:$PATH"
plan=""
need() { plan="$plan
- $*"; }
if [ "$BACKEND" = microsandbox ]; then
have msb || need "microsandbox (from install.microsandbox.dev, into ~/.microsandbox)"
if [ "$os" = Linux ]; then
[ -e /dev/kvm ] || need "the KVM kernel module (modprobe, with sudo)"
{ [ -r /dev/kvm ] && [ -w /dev/kvm ]; } || need "access to KVM for $(id -un) (the kvm group, with sudo)"
fi
else
if ! have docker; then
case "$os" in
Linux) need "Docker Engine with Compose (from get.docker.com, with sudo)" ;;
Darwin)
have brew || need "Homebrew (from brew.sh, asks for your password)"
need "OrbStack, which runs Docker on a Mac (with Homebrew)"
;;
esac
elif ! docker compose version >/dev/null 2>&1 &&
! have docker-compose && [ "$os" = Linux ]; then
need "the Docker Compose plugin (with your package manager, with sudo)"
fi
fi
if [ -n "$plan" ]; then
printf '\n\033[1mLens needs these, and will install them:\033[0m%s\n' "$plan"
if tty_ok; then
printf 'Install them now? [Y/n] '
read -r answer </dev/tty || answer=
case "$answer" in
n* | N*) fail "Nothing was installed. Install them yourself, or run this again and answer yes." ;;
esac
fi
fi
if [ "$BACKEND" = microsandbox ]; then
# --- microsandbox -----------------------------------------------------------------------------------------------
if ! have msb; then
say "Installing microsandbox (install.microsandbox.dev)..."
have curl || fail "curl is needed to install microsandbox."
curl -fsSL https://install.microsandbox.dev | sh
have msb || fail "microsandbox didn't install. See https://docs.microsandbox.dev, then run this again."
fi
if [ "$os" = Linux ] && [ ! -e /dev/kvm ]; then
say "Loading KVM..."
$(sudo_ok) modprobe kvm_intel 2>/dev/null || $(sudo_ok) modprobe kvm_amd 2>/dev/null || $(sudo_ok) modprobe kvm 2>/dev/null || true
[ -e /dev/kvm ] || fail "KVM didn't load. Turn on virtualization (VT-x or AMD-V) in the firmware settings, or use LENS_BACKEND=docker."
fi
if [ "$os" = Linux ] && ! { [ -r /dev/kvm ] && [ -w /dev/kvm ]; }; then
say "Letting $(id -un) use KVM (the kvm group)..."
$(sudo_ok) usermod -aG kvm "$(id -un)" 2>/dev/null || true
# the group counts from the next sign-in; for this run, access for this user alone
have setfacl && $(sudo_ok) setfacl -m "u:$(id -un):rw" /dev/kvm 2>/dev/null || true
[ -r /dev/kvm ] && [ -w /dev/kvm ] || fail "Can't use /dev/kvm. Sign out and in again (or run: msb doctor), then run this again."
fi
else
# --- Docker -------------------------------------------------------------------------------------------------------
if ! have docker; then
case "$os" in
Linux)
say "Installing Docker (get.docker.com)..."
have curl || fail "curl is needed to install Docker."
curl -fsSL https://get.docker.com | $(sudo_ok) sh
$(sudo_ok) systemctl enable --now docker 2>/dev/null || true
;;
Darwin)
if ! have brew && tty_ok; then
say "Installing Homebrew (brew.sh)..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" </dev/tty
for b in /opt/homebrew/bin/brew /usr/local/bin/brew; do [ -x "$b" ] && eval "$("$b" shellenv)" && break; done
fi
if have brew; then
say "Installing OrbStack, which runs Docker on a Mac (brew)..."
<
8000
div>
brew install --cask orbstack
open -a OrbStack
else
fail "Lens runs in Docker. Install OrbStack (https://orbstack.dev) or Docker Desktop, then run this again."
fi
;;
*) fail "Lens runs in Docker. Install Docker for your system, then run this again." ;;
esac
fi
DOCKER=docker
if ! docker info >/dev/null 2>&1; then
if [ "$os" = Linux ] && $(sudo_ok) docker info >/dev/null 2>&1; then
DOCKER="$(sudo_ok) docker" # not in the docker group yet: works for this run
else
say "Waiting for Docker to start..."
i=0
until docker info >/dev/null 2>&1; do
i=$((i + 1))
[ $i -gt 60 ] && fail "Docker isn't running. Start it, then run this again."
sleep 2
done
fi
fi
if $DOCKER compose version >/dev/null 2>&1; then
COMPOSE="$DOCKER compose"
elif have docker-compose; then
COMPOSE="docker-compose"
elif [ "$os" = Linux ] && {
if have apt-get; then $(sudo_ok) apt-get install -y -q docker-compose-plugin
elif have dnf; then $(sudo_ok) dnf install -y -q docker-compose-plugin
else false; fi
} >/dev/null 2>&1 && $DOCKER compose version >/dev/null 2>&1; then
COMPOSE="$DOCKER compose"
else
fail "Docker Compose is missing. Install the Compose plugin (docker-compose-plugin), then run this again."
fi
fi
# --- The code -----------------------------------------------------------------------------------------------------
auth_url="https://github.com/$REPO.git"
[ -n "${GITHUB_TOKEN:-}" ] && auth_url="https://x-access-token:$GITHUB_TOKEN@github.com/$REPO.git"
if [ -d "$DIR/.git" ]; then
say "Updating Lens in $DIR..."
git -C "$DIR" fetch -q --depth 1 "$auth_url" "$REF" && git -C "$DIR" checkout -q -B "$REF" FETCH_HEAD
elif have git; then
say "Getting Lens into $DIR..."
git clone -q --depth 1 --branch "$REF" "$auth_url" "$DIR" ||
fail "Couldn't get the code from GitHub ($REF). Check the network and LENS_REF, and try again."
git -C "$DIR" remote set-url origin "https://github.com/$REPO.git" # the token isn't kept on disk
else
say "Getting Lens into $DIR..."
mkdir -p "$DIR"
set -- -fsSL
[ -n "${GITHUB_TOKEN:-}" ] && set -- "$@" -H "Authorization: Bearer $GITHUB_TOKEN"
curl "$@" "https://api.github.com/repos/$REPO/tarball/$REF" | tar -xz -C "$DIR" --strip-components 1 ||
fail "Couldn't get the code from GitHub ($REF). Check the network and LENS_REF, and try again."
fi
cd "$DIR"
# --- The port: the one asked for, else the one this install already uses, else the first free one from 3000 -------
free() { # nothing listening: curl can't connect (exit 7)
curl -s -o /dev/null --max-time 2 "http://127.0.0.1:$1/" 2>/dev/null
[ $? -eq 7 ]
}
PORT="${LENS_PORT:-$( [ -f .env ] && grep '^LENS_PORT=' .env | cut -d= -f2- || true)}"
if [ -z "$PORT" ]; then
PORT=3000
until free "$PORT"; do
PORT=$((PORT + 1))
[ "$PORT" -gt 3100 ] && fail "No free port between 3000 and 3100. Set LENS_PORT to one that is."
done
[ "$PORT" = 3000 ] || say "Port 3000 is taken, so Lens uses $PORT."
fi
# --- Where it's reached -------------------------------------------------------------------------------------------
if [ -z "${LENS_PUBLIC:-}" ] && [ -f .env ] && grep -q '^LENS_BIND=' .env; then # as the last run left it
LENS_PUBLIC=0
grep -q '^LENS_BIND=0.0.0.0' .env && LENS_PUBLIC=1
fi
if [ -z "${LENS_PUBLIC:-}" ]; then
LENS_PUBLIC=0
if [ "$os" = Linux ] && [ -z "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]; then LENS_PUBLIC=1; fi # a server: used from elsewhere
fi
host=localhost
bind=127.0.0.1
if [ "$LENS_PUBLIC" = 1 ]; then
bind=0.0.0.0
ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
[ -z "$ip" ] && ip="$(ipconfig getifaddr en0 2>/dev/null || true)"
[ -n "$ip" ] && host="$ip"
fi
# --- Secrets, once: changing them later would sign everyone out and make stored credentials unreadable ------------
setenv() { # key value: replace the line, or add it
if grep -q "^$1=" .env; then
sed "s|^$1=.*|$1=$2|" .env >.env.tmp && mv .env.tmp .env
else
printf '%s=%s\n' "$1" "$2" >>.env
fi
}
if [ ! -f .env ]; then
umask 077
printf '# Written by install.sh. Keep it with the volumes.\n' >.env
setenv ACCESS_SECRET_KEY "$(secret)"
setenv ARCHIVE_SECRET_KEY "$(secret)"
setenv AUTH_SECRET "$(secret)"
setenv SURREAL_PASS "$(secret)"
fi
grep -q '^LENS_SETUP_CODE=' .env || setenv LENS_SETUP_CODE "$(code)" # only used until the first admin exists
setenv LENS_BACKEND "$BACKEND"
setenv LENS_TARGET "$TARGET"
setenv LENS_BIND "$bind"
setenv LENS_PORT "$PORT"
setenv FRONTEND_URL "http://$host:$PORT"
setup_code="$(grep '^LENS_SETUP_CODE=' .env | cut -d= -f2-)"
# --- Build and start ----------------------------------------------------------------------------------------------
if [ "$BACKEND" = microsandbox ]; then
# the code it was built from: the sandbox builds again when this changes
{ git rev-parse HEAD 2>/dev/null || date +%s; } >.lens-build
# Memory: three quarters of this machi
94D9
ne's, 2 to 8 GB (building the web app needs about 2)
if [ "$os" = Darwin ]; then mem=$(($(sysctl -n hw.memsize) / 1048576)); else mem=$(($(awk '/MemTotal/ {print $2}' /proc/meminfo) / 1024)); fi
mem=$((mem * 3 / 4))
[ "$mem" -lt 2048 ] && mem=2048
[ "$mem" -gt 8192 ] && mem=8192
cpus="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 2)"
sensors="${LENS_SENSOR_BIND:-$bind}"
# The stock Python image, from mirrors that need no sign-in first: Docker Hub can refuse it ("Not authorized") when
# this machine has Docker Hub credentials that microsandbox can't use, and limits anonymous pulls.
say "Getting the base image..."
image=""
for ref in mirror.gcr.io/library/python:3.12-slim-bookworm public.ecr.aws/docker/library/python:3.12-slim-bookworm \
docker.io/library/python:3.12-slim-bookworm; do
if out="$(msb pull -q "$ref" 2>&1)"; then
image="$ref"
break
fi
done
[ -n "$image" ] || fail "Couldn't download the base image (python:3.12-slim-bookworm): $out"
# The internet, this network (mail servers, model servers) and this machine (Ollama, LM Studio); only the web app
# and the sensor ports are published, as with Docker.
set -- run -d -q --name lens --replace -c "$cpus" -m "${mem}M" --root-disk 16G --net public,private,host \
-p "$bind:$PORT:3000" \
-p "$sensors:${LENS_MQTT_PORT:-1883}:1883" \
-p "$sensors:${LENS_SYSLOG_PORT:-5514}:5514/udp" -p "$sensors:${LENS_SYSLOG_PORT:-5514}:5514/tcp" \
-v "$PWD:/lens:ro" --mount-named lens-data:/data \
--entrypoint /bin/sh "$image" -- /lens/microsandbox/run.sh
say "Starting Lens in a microsandbox (the first start installs and builds it, which takes a while)..."
# The command that starts it, kept for a restart of the machine: a sandbox started again (msb start) boots without
# running Lens, so it is always replaced; its packages come from the cache in the volume, its build stays there.
{
echo "#!/bin/sh"
echo "# Starts Lens in its microsandbox (written by install.sh)"
printf 'exec %s' "'$(command -v msb)'"
for a in "$@"; do printf " '%s'" "$a"; done
echo
} >.lens-sandbox
chmod +x .lens-sandbox
./.lens-sandbox >/dev/null
LOGS="msb logs -f lens"
STOP="msb stop lens"
WAIT=900 # 30 minutes: the first start installs the system packages and builds everything
# back after a restart of the machine, as Docker's restart policy does
if [ "$os" = Linux ] && have systemctl && [ -d /run/systemd/system ]; then
unit=/etc/systemd/system/lens-microsandbox.service
printf '%s\n' "[Unit]" "Description=Lens (microsandbox)" "After=network-online.target" "Wants=network-online.target" "" \
"[Service]" "Type=oneshot" "RemainAfterExit=yes" "User=$(id -un)" "ExecStart=$PWD/.lens-sandbox" \
"ExecStop=$(command -v msb) stop lens" "" "[Install]" "WantedBy=multi-user.target" |
$(sudo_ok) tee "$unit" >/dev/null && $(sudo_ok) systemctl enable lens-microsandbox.service >/dev/null 2>&1 || true
elif [ "$os" = Darwin ]; then
plist="$HOME/Library/LaunchAgents/dev.lens.microsandbox.plist"
mkdir -p "$(dirname "$plist")"
cat >"$plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>dev.lens.microsandbox</string>
<key>ProgramArguments</key><array><string>$PWD/.lens-sandbox</string></array>
<key>RunAtLoad</key><true/>
</dict></plist>
PLIST
fi
else
say "Building and starting Lens (the first build takes a few minutes)..."
$COMPOSE -f docker-compose.prod.yml up -d --build
LOGS="$COMPOSE -f docker-compose.prod.yml logs"
STOP="$COMPOSE -f docker-compose.prod.yml down"
WAIT=300
fi
say "Waiting for the web app..."
i=0
# the page and the API behind it: the setup page needs both, and the API starts a few seconds after the page
until curl -fsS -o /dev/null "http://127.0.0.1:$PORT/login" 2>/dev/null &&
curl -fsS -o /dev/null "http://127.0.0.1:$PORT/api/v1/auth/status" 2>/dev/null; do
i=$((i + 1))
[ $i -gt $WAIT ] && fail "Lens didn't answer within $((WAIT * 2 / 60)) minutes. See what happened: cd $DIR && $LOGS"
sleep 2
done
url="http://$host:$PORT/setup?code=$setup_code"
say "Lens is running: $url"
echo " (once the admin account exists, the link opens the sign-in page)"
echo " Stop: cd $DIR && $STOP Update: run the same line again"
if [ "${LENS_NO_OPEN:-0}" != 1 ]; then
if [ "$os" = Darwin ]; then
open "$url" 2>/dev/null || true
elif [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ] && have xdg-open; then
xdg-open "$url" >/dev/null 2>&1 || true
fi
fi
}
main "$@"