From 72d85eb9ec0ac126c2337af26fb49a6f218d6bb5 Mon Sep 17 00:00:00 2001 From: Nic O'Connor Date: Thu, 24 Sep 2026 19:24:03 +0000 Subject: [PATCH] vscode: install VS Code into ~/.local/opt from upstream's tarball MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The box this was written on had a half-finished install: vscode.repo present under /etc/yum.repos.d, `code` never installed, and nothing in this repo recording that VS Code was meant to be there. Microsoft's rpm/deb would also need root, write outside $HOME, and be wiped by a Kasm redeploy. Upstream's portable tarball is the same build with none of that, so the module keeps bin/'s contract: no sudo, nothing outside $HOME, pinned rather than floating. The Chromium sandbox survives a userland install — the bundled chrome-sandbox is not setuid, so Electron falls back to the unprivileged user-namespace sandbox. Verified by launching without --no-sandbox, so dropping root costs nothing. Adds install_tarball_tree to lib/fetch.sh: install_tarball_bin picks a single binary out of an archive, and VS Code only runs from an intact Electron tree. It stages beside the target and swaps in after extraction succeeds, so an interrupted run leaves the old version intact. Two things the verification caught rather than the design: - argv.json lives at ~/.vscode/argv.json (product.json's dataFolderName), not under the user-data-dir. A copy written anywhere else is read by nothing, so the first attempt at suppressing the keyring prompt silently did nothing. - Run as root, `code --version` prints a "super user" warning instead of the version and still exits 0, so a bare run-check never matches and would re-download 325MB on every bootstrap. Root falls back to the .versions stamp. Sets password-store=basic in argv.json so Electron's safeStorage stops waking kwalletd, which prompts for an unlock on every launch because kwallet-pam is not installed. Confirmed with dbus-monitor: no org.kde.KWallet traffic on launch. The trade-off — tokens obfuscated on disk rather than sealed behind a wallet password — and the route back to the wallet are documented in the module and the README. No-ops on headless hosts; DOTFILES_VSCODE overrides either way. Verified per verify-module: shellcheck -s sh clean, idempotent re-run on both paths, and fresh install plus re-run as root on oraclelinux:9 and debian:13. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 20 +++++ bootstrap.sh | 4 + lib/fetch.sh | 32 ++++++- vscode/install.sh | 216 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 270 insertions(+), 2 deletions(-) create mode 100755 vscode/install.sh diff --git a/README.md b/README.md index 3811e57..1550ccb 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,14 @@ Already have a clone? Just run it directly: `~/.local/bin`. Persists across redeploys; only rebuilt when the venv is missing or broken. Building it may install `python3`/`python3-venv` via the system package manager (the one place this module uses sudo). +- `vscode/` — installs Visual Studio Code from upstream's portable tarball into + `~/.local/opt/vscode`, symlinks `code` into `~/.local/bin`, and writes a + user-level `.desktop` entry so it shows up in the application menu. **No + sudo, nothing outside `$HOME`** — the same contract as `bin/`, so it survives + a Kasm redeploy. Pinned via `VSCODE_VERSION` in the module; bump it to upgrade. + Also sets `password-store: basic` in `~/.vscode/argv.json` so KDE's kwallet + does not prompt for an unlock on every launch (see below). **No-ops on + headless hosts** — it's a ~400MB GUI app — override with `DOTFILES_VSCODE`. - `docker-rootless-cgroup/` — on systemd-less hosts (e.g. Kasm), hand-delegates cgroup v2 controllers to the rootless Docker daemon so per-container cgroups work (rootless Docker otherwise comes up with @@ -111,6 +119,18 @@ item instead of `~/.zshrc.local`: don't set this by hand: run `mcp-config/install.sh` once and its shell hook exports it from `~/.config/mcp-sync/key`. Set it in `~/.zshrc.local` only on a box without mcp-sync. +- `DOTFILES_VSCODE` — controls the `vscode/` module. Unset (default) installs + VS Code only when a graphical session is detected; `1` forces the install on + a headless host (e.g. for `code tunnel`); `0` skips it everywhere. A plain + shell/env var, not a vault secret. + + On the keyring: VS Code's secret storage reaches for the OS keyring through + libsecret, which on KDE makes kwalletd prompt for an unlock before the editor + is usable — every session, since `kwallet-pam` is not installed. The module + sets `"password-store": "basic"` in `~/.vscode/argv.json` to stop that, which + means VS Code's stored tokens are obfuscated on disk rather than sealed + behind a wallet password. To use the wallet instead, install `kwallet-pam` so + it unlocks at login, then change that value to `"kwallet5"`. - `OP_SERVICE_ACCOUNT_TOKEN` — 1Password service-account token. Not a shell variable any more: `secrets/install.sh` asks for it once and stores it in `~/.config/dotfiles/op.env`. Pass it in the env only for an unattended run. diff --git a/bootstrap.sh b/bootstrap.sh index 1a43d57..c5118b0 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -124,6 +124,10 @@ else run_module hermes fi run_module ansible +# GUI editor, installed from upstream's tarball into ~/.local/opt (no sudo, same +# contract as bin/). No ordering constraints — it needs nothing from bin/, +# secrets/ or node/ — and no-ops on headless hosts. +run_module vscode # Runs last: needs rootless Docker already up (started at boot) so it can # delegate cgroups to it. No-ops where that does not apply. run_module docker-rootless-cgroup diff --git a/lib/fetch.sh b/lib/fetch.sh index c810370..a381d9d 100644 --- a/lib/fetch.sh +++ b/lib/fetch.sh @@ -8,8 +8,8 @@ # reset. # # Source this (callers already run `set -eu`), then use fetch_arch / -# fetch_arch_deb, have_version + mark_version, install_tarball_bin, and -# install_raw_bin. +# fetch_arch_deb, have_version + mark_version, install_tarball_bin, +# install_tarball_tree, and install_raw_bin. BIN_DIR="${BIN_DIR:-$HOME/.local/bin}" VERSION_DIR="$BIN_DIR/.versions" @@ -93,6 +93,34 @@ install_zip_bin() { fsay "installed $BIN_DIR/$3" } +# install_tarball_tree — download a +# .tar.gz that holds a whole application directory rather than one binary, and +# swap it into . install_tarball_bin cannot be reused here: VS Code +# is an Electron bundle of thousands of files that only runs from an intact +# tree, not a single binary that can be picked out of the archive. +# +# The new tree is staged beside the target and moved into place only once the +# download and extraction have both succeeded, so an interrupted run leaves the +# previously installed version intact instead of a half-extracted directory. +install_tarball_tree() { + _dest="$3" + _tmp="$(mktemp -d)" + _fetch_dl "$1" "$_tmp/archive.tar.gz" + tar -xzf "$_tmp/archive.tar.gz" -C "$_tmp" + if [ ! -d "$_tmp/$2" ]; then + fsay "fetch: '$2' not found in archive"; rm -rf "$_tmp"; return 1 + fi + mkdir -p "$(dirname "$_dest")" + rm -rf "$_dest.new" "$_dest.old" + mv "$_tmp/$2" "$_dest.new" + if [ -e "$_dest" ]; then + mv "$_dest" "$_dest.old" + fi + mv "$_dest.new" "$_dest" + rm -rf "$_dest.old" "$_tmp" + fsay "installed $_dest" +} + # install_raw_bin — download a single raw binary. install_raw_bin() { mkdir -p "$BIN_DIR" diff --git a/vscode/install.sh b/vscode/install.sh new file mode 100755 index 0000000..8efa679 --- /dev/null +++ b/vscode/install.sh @@ -0,0 +1,216 @@ +#!/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."