#!/bin/sh # woodls' installer, served at https://woodls.com/install.sh: # # curl -fsSL https://woodls.com/install.sh | sh # # It puts woodls in ~/.woodls (WOODLS_INSTALL names another folder), and one # line in your shell's startup file so `woodls` is on your PATH # (WOODLS_NO_MODIFY_PATH=1 leaves the file alone). Nothing runs until you start # it. Once it runs, woodls code keeps copies of your code in # ~/.local/share/woodls and package caches where your system keeps caches. # `woodls uninstall` takes the programs out, and says what stays. # WOODLS_FROM_WINDOWS=1, from woodls' installer for Windows, leaves out the # lines meant for this shell and what to do next: that installer says them. # # The download is checked against the SHA-256 in woodls' release list. With # minisign installed, the list's signature is checked too; after the first # install, `woodls update` checks every release's signature itself. set -eu BASE="${WOODLS_RELEASES:-https://woodls.com/releases}" ROOT="${WOODLS_INSTALL:-$HOME/.woodls}" CHANNEL=alpha # woodls' release key: `minisign -V -P` checks a download with it. PUBLIC_KEY="RWQxdvRUJG/T/j8YBCt+cyS67OEGgUzYBTqyoWWvKkZ0trsICgZ91Fyd" MARK="# added by woodls' installer" say() { printf '%s\n' "$*"; } fail() { printf '(x_x) %s\n' "$*" >&2 exit 1 } ROSETTA=no case "$(uname -s)-$(uname -m)" in Darwin-arm64) TARGET=aarch64-apple-darwin ;; Linux-x86_64) TARGET=x86_64-unknown-linux-gnu ;; Linux-aarch64 | Linux-arm64) TARGET=aarch64-unknown-linux-gnu ;; Darwin-x86_64) # A terminal under Rosetta says x86_64 on Apple silicon too. if [ "$(sysctl -n hw.optional.arm64 2> /dev/null)" = 1 ]; then TARGET=aarch64-apple-darwin ROSETTA=yes else fail "woodls needs a Mac with Apple silicon (M1 or later), and this Mac has an Intel processor" fi ;; *) fail "woodls isn't released for $(uname -s) on $(uname -m) yet" ;; esac # Lending's engine needs macOS 15 or later; woodls code and chat don't. MACOS="" OLD_MACOS=no if [ "$TARGET" = aarch64-apple-darwin ]; then MACOS=$(sw_vers -productVersion 2> /dev/null || true) case "${MACOS%%.*}" in '' | *[!0-9]*) ;; *) [ "${MACOS%%.*}" -lt 15 ] && OLD_MACOS=yes ;; esac fi command -v curl > /dev/null 2>&1 || fail "the installer needs curl" command -v tar > /dev/null 2>&1 || fail "the installer needs tar" if command -v sha256sum > /dev/null 2>&1; then SHA="sha256sum" elif command -v shasum > /dev/null 2>&1; then SHA="shasum -a 256" else fail "the installer needs sha256sum or shasum" fi # HTTPS only, except from a test's server on this computer: http://127.0.0.1:PORT # or http://localhost:PORT, a port of digits and nothing before the host. SECURE="--proto =https --tlsv1.2" case "$BASE" in *@*) ;; http://127.0.0.1:* | http://localhost:*) PORT=${BASE#http://*:} PORT=${PORT%%/*} case "$PORT" in '' | *[!0-9]*) ;; *) SECURE="" ;; esac ;; esac TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT INT TERM say "(o_o) reading woodls' release list" # shellcheck disable=SC2086 curl -fsSL $SECURE "$BASE/$CHANNEL.json" -o "$TMP/list.json" || fail "couldn't read $BASE/$CHANNEL.json" VERSION=$(sed -n 's/^ "version": "\([0-9][0-9.]*\)",$/\1/p' "$TMP/list.json") LINE=$(grep "^ \"$TARGET\": " "$TMP/list.json" || true) [ -n "$VERSION" ] || fail "woodls' release list can't be read" [ -n "$LINE" ] || fail "woodls $VERSION has no release for $TARGET yet" FILE=$(printf '%s' "$LINE" | sed 's/.*"url": "\([^"]*\)".*/\1/') SUM=$(printf '%s' "$LINE" | sed 's/.*"sha256": "\([0-9a-f]*\)".*/\1/') case "$FILE" in *://* | /* | *..*) fail "the release list names a file outside its folder" ;; esac CHECKED="the SHA-256 in woodls' release list" if command -v minisign > /dev/null 2>&1; then # shellcheck disable=SC2086 curl -fsSL $SECURE "$BASE/$CHANNEL.json.minisig" -o "$TMP/list.json.minisig" || fail "couldn't read the release list's signature" minisign -Vq -P "$PUBLIC_KEY" -m "$TMP/list.json" -x "$TMP/list.json.minisig" || fail "woodls' release list isn't signed by woodls" CHECKED="woodls' signature on its release list (minisign), and the SHA-256 in it" fi case "$TARGET" in *-apple-darwin) THIS=Mac ;; *) THIS=computer ;; esac say "(o_o) downloading woodls $VERSION for this $THIS" # A bar only where it can be drawn: in a file, or with TERM=dumb or NO_COLOR, each # rewrite is read out, and the line above already says it's downloading. BAR=-s if [ -t 2 ] && [ "${TERM:-}" != dumb ] && [ -z "${NO_COLOR:-}" ]; then BAR=--progress-bar; fi # shellcheck disable=SC2086 curl -fSL "$BAR" $SECURE "$BASE/$FILE" -o "$TMP/woodls.tar.gz" || fail "the download failed" GOT=$($SHA "$TMP/woodls.tar.gz" | cut -d' ' -f1) [ "$GOT" = "$SUM" ] || fail "the download isn't the file woodls' release list names (SHA-256 $GOT, not $SUM)" DEST="$ROOT/versions/$VERSION" mkdir -p "$ROOT/versions" rm -rf "$DEST.part" mkdir -p "$DEST.part" tar -xzf "$TMP/woodls.tar.gz" -C "$DEST.part" --strip-components 1 || fail "unpacking failed" # macOS checks a new program the first time it runs, and says nothing while # it does; measured at 1 to 100 s, the longest under load. if [ "$TARGET" = aarch64-apple-darwin ]; then say "(o_o) macOS is checking the new program: this can take a minute or two" fi if ! SAID=$("$DEST.part/bin/woodls" --version 2>&1); then # Built against glibc 2.28, Linux from 2018 on (cli/release/README.md). # That's the likely reason, not the only one: what it said follows. case "$TARGET" in *linux*) printf '(x_x) the new woodls doesn'"'"'t start on this computer: it may need Linux with glibc 2.28 or later (2018 on), and no musl\n' >&2 ;; *) printf '(x_x) the new woodls doesn'"'"'t start on this computer\n' >&2 ;; esac [ -z "$SAID" ] || printf ' it said: %s\n' "$SAID" >&2 printf ' more help: woodls.com/help\n' >&2 rm -rf "$DEST.part" exit 1 fi rm -rf "$DEST" mv "$DEST.part" "$DEST" ln -sfn "versions/$VERSION" "$ROOT/current" mkdir -p "$ROOT/bin" ln -sfn ../current/bin/woodls "$ROOT/bin/woodls" # Your PATH: one line in your shell's startup file, written with $HOME # when the folder is in it. case "$ROOT" in "$HOME"/*) DIR="\$HOME${ROOT#"$HOME"}/bin" ;; *) DIR="$ROOT/bin" ;; esac case ":$PATH:" in *":$ROOT/bin:"*) ON_PATH=yes ;; *) ON_PATH=no ;; esac ADDED="" if [ "$ON_PATH" = no ] && [ "${WOODLS_NO_MODIFY_PATH:-}" != 1 ]; then case "${SHELL:-}" in */zsh) RC="${ZDOTDIR:-$HOME}/.zshrc" ;; # A new .bash_profile would stop bash reading a .profile you have. */bash) if [ "$(uname -s)" != Darwin ]; then RC="$HOME/.bashrc"; elif [ ! -e "$HOME/.bash_profile" ] && [ -e "$HOME/.profile" ]; then RC="$HOME/.profile"; else RC="$HOME/.bash_profile"; fi ;; */fish) RC="$HOME/.config/fish/conf.d/woodls.fish" ;; *) RC="$HOME/.profile" ;; esac if ! grep -qs "$MARK" "$RC"; then mkdir -p "$(dirname "$RC")" # A file with no last newline: this line would join its last line. if [ -s "$RC" ] && [ -n "$(tail -c 1 "$RC")" ]; then printf '\n' >> "$RC" fi case "$RC" in *.fish) printf '%s\n' "fish_add_path \"$DIR\" $MARK" >> "$RC" ;; *) printf '%s\n' "export PATH=\"$DIR:\$PATH\" $MARK" >> "$RC" ;; esac ADDED="$RC" fi fi say "(^_^) woodls $VERSION is in $ROOT" say " checked first: $CHECKED" [ -n "$ADDED" ] && say " $ROOT/bin is on your PATH from your next terminal ($ADDED)" FROM_WINDOWS="${WOODLS_FROM_WINDOWS:-}" if [ "$ON_PATH" = no ] && [ "$FROM_WINDOWS" != 1 ]; then say " in this one: export PATH=\"$ROOT/bin:\$PATH\"" fi if [ "$ROSETTA" = yes ]; then say "(-_-) this terminal runs under Rosetta: woodls is installed for Apple silicon, and runs as that" fi if [ "$OLD_MACOS" = yes ]; then say "(-_-) lending needs macOS 15 or later, and this Mac has macOS $MACOS:" say " woodls code and chat work; Software Update, in System Settings, brings a newer macOS" fi if [ "$TARGET" != aarch64-apple-darwin ] && ! command -v bwrap > /dev/null 2>&1; then say "(-_-) woodls code's commands, and lending, run in a sandbox made with bubblewrap:" say " install it (sudo apt install bubblewrap, or your system's package) for them" fi # What next: the five doors, word for word (the plan's section 3, and # DOORS in cli/woodls-tui/src/home.rs); or nothing to set up again when this # computer has an account already, kept through an uninstall. woodls keeps # it in WOODLS_HOME, else ~/.woodls; one set up for private groups only still # has the doors. From Windows, its installer says this itself. [ "$FROM_WINDOWS" = 1 ] && exit 0 CONFIG="${WOODLS_HOME:-$HOME/.woodls}/config.json" if [ -f "$CONFIG" ] && ! grep -qs '"groves_only": *true' "$CONFIG"; then say " this computer is set up already: woodls status shows its account" say " woodls studio shows it all in a window." else say "" say " Model in Ollama or LM Studio? woodls code" say " A friend sent a link? woodls group join LINK" say " No model yet? 5.0 GB: woodls models get qwen3-8b" say " An invite code? woodls init --invite CODE" say " None of these? woodls.com/invite" say " The first three need no account. woodls studio shows it all in a window." say "" fi # The public network takes Macs with Apple silicon for now (its checks were # measured on Metal, ADR 0124); another computer hosts for private groups. if [ "$TARGET" = aarch64-apple-darwin ]; then say " woodls host lends this computer: it says which model fits, and gets it after a yes" else say " woodls host --no-public --group NAME lends this computer to your private group;" say " the public network takes Macs with Apple silicon for now" fi say " woodls uninstall takes the programs out, and says what stays" say " Using a screen reader? woodls --plain gives words, not pictures. export NO_COLOR=1 in your shell's startup file keeps it so."