Files
noconnorandClaude Opus 5 72d85eb9ec vscode: install VS Code into ~/.local/opt from upstream's tarball
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) <noreply@anthropic.com>
2026-09-28 13:08:49 +00:00

217 lines
9.4 KiB
Bash
Executable File

#!/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" <<DESKTOP
[Desktop Entry]
Type=Application
Name=Visual Studio Code
GenericName=Text Editor
Comment=Code Editing. Redefined.
Exec=$PREFIX/code %F
Icon=$PREFIX/resources/app/resources/linux/code.png
Terminal=false
StartupNotify=true
StartupWMClass=Code
Categories=TextEditor;Development;IDE;
MimeType=text/plain;inode/directory;
Keywords=vscode;editor;
Actions=new-window;
[Desktop Action new-window]
Name=New Empty Window
Exec=$PREFIX/code --new-window %F
Icon=$PREFIX/resources/app/resources/linux/code.png
DESKTOP
say "vscode: wrote $DESKTOP_DIR/code.desktop"
# Refresh the per-user menu caches so the entry appears without a re-login.
if command -v update-desktop-database >/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."