#!/usr/bin/env sh # vscode/install.sh — install Visual Studio Code into ~/.local/opt/vscode. # # WHY: VS Code is the one GUI app this repo installs, and the obvious way to get # it — Microsoft's rpm/deb repo — needs root, writes outside $HOME, and is wiped # by a Kasm redeploy, so the editor quietly disappears on every rebuild. The box # this was written for had exactly that half-state: /etc/yum.repos.d/vscode.repo # present, `code` never installed, and nothing in this repo recording that VS # Code was meant to be there at all. Upstream's portable tarball is the same # build with none of that: everything lands under $HOME, no sudo, and it # survives a redeploy. The only thing given up is dnf/apt-driven updates, which # is what the pinned VSCODE_VERSION below replaces. # # It is not in bin/install.sh because that script installs single self-contained # binaries; VS Code is an Electron tree of thousands of files plus a menu entry # and an icon. It follows node/ and ansible/ instead: unpack under ~/.local/opt # and symlink the CLI into ~/.local/bin. # # The Chromium sandbox still works from a userland install: the bundled # chrome-sandbox helper is not setuid here, so Electron falls back to the # unprivileged user-namespace sandbox. That needs user namespaces enabled # (user.max_user_namespaces > 0, the default on every distro this repo targets). # Verified by launching without --no-sandbox. Nothing is given up for dropping # root; if a host ever has userns disabled, VS Code says so on startup. # # Idempotent and safe to re-run. To upgrade, bump VSCODE_VERSION. set -eu SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" REPO_ROOT="$(dirname -- "$SCRIPT_DIR")" # shellcheck source=../lib/fetch.sh . "$REPO_ROOT/lib/fetch.sh" say() { printf '%s\n' "$*" >&2; } # Pinned deliberately. VS Code auto-updates itself on other platforms; a tarball # install does not, which is the point — the editor runs your code and holds # your Git and extension credentials, so the version is a supply-chain decision # rather than whatever upstream served today. `code --version` prints this # string verbatim, so it must match with no leading "v". VSCODE_VERSION="1.139.0" PREFIX="$HOME/.local/opt/vscode" DESKTOP_DIR="$HOME/.local/share/applications" ARGV_JSON="$HOME/.vscode/argv.json" # has_desktop — true when this host has a graphical session to run a GUI editor # in. VS Code is ~400MB unpacked and $HOME is the thing that persists across # redeploys, so it is not worth carrying on a box that will never display it. # Desktop images ship session files under /usr/share/{x,wayland-}sessions. has_desktop() { [ -n "${XDG_CURRENT_DESKTOP:-}" ] && return 0 for d in /usr/share/xsessions /usr/share/wayland-sessions; do [ -d "$d" ] && [ -n "$(ls -A "$d" 2>/dev/null)" ] && return 0 done return 1 } # --- skip on headless hosts -------------------------------------------------- # DOTFILES_VSCODE=1 forces the install anyway (e.g. for `code tunnel` on a # server); DOTFILES_VSCODE=0 skips it even on a desktop. case "${DOTFILES_VSCODE:-auto}" in 0) say "vscode: DOTFILES_VSCODE=0; skipping."; exit 0 ;; 1) ;; *) if ! has_desktop; then say "vscode: no graphical session detected; skipping (DOTFILES_VSCODE=1 to force)." exit 0 fi ;; esac # Upstream keys its download URLs on x64/arm64 rather than amd64/arm64. case "$(fetch_arch)" in x86_64) VSCODE_ARCH=x64 ;; aarch64) VSCODE_ARCH=arm64 ;; *) say "vscode: unsupported arch $(fetch_arch)"; exit 1 ;; esac # --- install the app tree ---------------------------------------------------- # Verify it RUNS, not just that the tree exists: a Kasm redeploy can leave a # present-but-broken directory behind (the same failure node/ and ansible/ # guard against). # # The root fallback is not optional. Run as root, `code --version` refuses -- # it prints "You are trying to start Visual Studio Code as a super user" # instead of the version, and still exits 0 -- so a bare run-check never # matches and would re-download 325MB on every single bootstrap, silently. # That is the forced-install path (DOTFILES_VSCODE=1 in a container or on a # server), so for root only, fall back to the recorded version stamp. installed_version="$("$PREFIX/bin/code" --version 2>/dev/null | head -1 || true)" if [ "$installed_version" = "$VSCODE_VERSION" ]; then say "vscode: $VSCODE_VERSION already present at $PREFIX." elif [ "$(id -u)" = "0" ] && have_version code "$VSCODE_VERSION"; then say "vscode: $VSCODE_VERSION already present at $PREFIX (stamped; root cannot query --version)." else say "vscode: installing $VSCODE_VERSION into $PREFIX (this is a ~325MB download)..." install_tarball_tree \ "https://update.code.visualstudio.com/$VSCODE_VERSION/linux-$VSCODE_ARCH/stable" \ "VSCode-linux-$VSCODE_ARCH" "$PREFIX" # bin/code is upstream's CLI wrapper (`code .`, `code --install-extension`); # the Electron binary at the tree root is what the menu entry launches. mkdir -p "$BIN_DIR" ln -sf "$PREFIX/bin/code" "$BIN_DIR/code" mark_version code "$VSCODE_VERSION" say "vscode: installed $VSCODE_VERSION and linked $BIN_DIR/code." fi # --- menu entry -------------------------------------------------------------- # Icon= is an absolute path rather than a theme name: the bundled icon is a # single 1024x1024 PNG and hicolor tops out at 512x512, so there is no correct # themed directory to drop it in. The spec allows an absolute path, and it needs # no icon-cache rebuild to take effect. mkdir -p "$DESKTOP_DIR" cat >"$DESKTOP_DIR/code.desktop" </dev/null 2>&1; then update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true fi if command -v kbuildsycoca5 >/dev/null 2>&1; then kbuildsycoca5 --noincremental >/dev/null 2>&1 || true fi # --- keyring prompt suppression ---------------------------------------------- # WHY: on first launch Electron's safeStorage reaches for the OS keyring through # libsecret. On a KDE session that wakes kwalletd, which pops an unlock/create # dialog before the editor is usable -- and since kwallet-pam is not installed # on these hosts, nothing unlocks the wallet at login, so it prompts every # session. "basic" keeps VS Code's secrets in its own store and never reaches # for the wallet at all. Verified with dbus-monitor: no org.kde.KWallet traffic # on launch. # # TRADE-OFF: basic is weaker than a real keyring. Secrets VS Code stores (Git # and GitHub auth tokens, extension credentials) end up obfuscated on disk # rather than sealed behind a wallet password. To move to the wallet later, # install kwallet-pam so it unlocks at login, then change the value to # "kwallet5" and restart VS Code. # # The path comes from product.json's dataFolderName (".vscode"), NOT the # user-data-dir: argv.json lives at ~/.vscode/argv.json and a copy written # anywhere else is read by nothing and silently ignored. if [ ! -e "$ARGV_JSON" ]; then mkdir -p "$(dirname "$ARGV_JSON")" cat >"$ARGV_JSON" <<'ARGV' // Runtime arguments for VS Code. See dotfiles vscode/install.sh. { // Keep secrets in VS Code's own store rather than the OS keyring, so KDE's // kwallet does not prompt for an unlock on every launch. "password-store": "basic" } ARGV say "vscode: wrote $ARGV_JSON (password-store=basic)" elif grep -q '"password-store"' "$ARGV_JSON"; then say "vscode: $ARGV_JSON already sets password-store; leaving it alone." else # Merge rather than overwrite: this file is JSON-with-comments that VS Code # and the user both own, and it carries a crash-reporter-id that should # survive. Insert the key after the opening brace and keep the rest byte for # byte. cp -p "$ARGV_JSON" "$ARGV_JSON.bak" if awk ' BEGIN { done = 0 } { print if (!done && $0 ~ /^[[:space:]]*\{[[:space:]]*$/) { print "\t// Added by dotfiles vscode/install.sh: keep secrets in VS Code'"'"'s" print "\t// own store so kwallet does not prompt for an unlock on every launch." print "\t\"password-store\": \"basic\"," done = 1 } } END { exit(done ? 0 : 1) } ' "$ARGV_JSON.bak" >"$ARGV_JSON.new"; then mv "$ARGV_JSON.new" "$ARGV_JSON" say "vscode: set password-store=basic in $ARGV_JSON (backup: $ARGV_JSON.bak)" else rm -f "$ARGV_JSON.new" say "vscode: WARNING: could not find the object opening in $ARGV_JSON." say " Add '\"password-store\": \"basic\"' by hand to stop the keyring prompt." fi fi # A system-wide VS Code (from Microsoft's rpm/deb repo) shadows this one in the # menu and shows a duplicate entry. Flag it rather than touching it -- removing # it needs the root this module deliberately avoids. if [ -e /usr/share/applications/com.microsoft.VSCode.desktop ]; then say "" say "vscode: NOTE: a system-wide VS Code package is also installed." say " You will see two menu entries until it is removed:" say " sudo dnf remove code # or: sudo apt remove code" say "" fi say "vscode: ready."