bootstrap.sh — first-touch entry for a new machine¶
The repo's one-liner installer:
does just three things (the script itself is ~75 lines, mostly logging):
- Install
uvif missing (Astral's installer →~/.local/bin/uv). - Reattach stdin to
/dev/ttysoquestionaryprompts insidedotfiles_init.pycan read keystrokes (otherwise the curl pipe would steal stdin and the wrapper would silently fall back to non-interactive stubs). exec uv run --script <URL>— fetchesscripts/init/dotfiles_init.pyover HTTPS, resolves its PEP 723 inline deps (questionary,rich,tyro) into an ephemeral venv, runs it. No global pip install, nopyproject.toml.
bootstrap.sh is in .chezmoiignore.tmpl — chezmoi
apply does not deploy it to $HOME/bootstrap.sh.
"It's been 5 minutes and there's no output — is it stuck?"¶
Most likely the wrapper is downloading something silently. Three layers can each take 30 s – several minutes on a slow / throttled network with zero visible output:
| Layer | What's happening | How to confirm |
|---|---|---|
curl https://astral.sh/uv/install.sh \| sh |
Downloading the uv binary tarball from GitHub Releases |
pgrep -af 'uv\|curl\|install.sh' |
uv run --script <URL> (deps stage) |
Resolving + downloading questionary/rich/tyro wheels from PyPI; building any sdists |
lsof -p $(pgrep -f dotfiles_init) -i \| head |
chezmoi init (after prompts) |
git clone the dotfiles repo (~MBs of submodules, agent skills, etc.) over HTTPS / SSH |
pgrep -af 'git\|chezmoi' |
Diagnose without killing the run¶
In another shell on the same machine:
# 1) see the full process tree under bootstrap
echo "=== process tree ==="
pgrep -af 'bootstrap|dotfiles_init|uv|chezmoi|ansible|brew|apt|git'
# 2) what is the leaf doing right now?
LEAF=$(pgrep -f 'dotfiles_init|chezmoi|ansible' | tail -1)
[ -n "$LEAF" ] && {
ps -p "$LEAF" -o pid,ppid,etime,stat,command
lsof -p "$LEAF" 2>/dev/null -i | head -10 # network sockets
lsof -p "$LEAF" 2>/dev/null -p "$LEAF" | grep REG | tail -10 # files being read
}
# 3) which remote is slow?
for url in https://astral.sh https://raw.githubusercontent.com \
https://pypi.org https://github.com https://get.chezmoi.io; do
printf '%-40s ' "$url"
curl -fsS --max-time 5 -o /dev/null -w 'HTTP %{http_code} %{time_total}s\n' "$url" \
|| echo 'TIMEOUT/FAIL'
done
Common patterns:
curltoastral.shtaking >30 s → GFW; sethttps_proxy(see below).uvprocess exists but has no TCP socket → resolver is CPU-bound on a big sdist build (rare for these three deps; usually means a transitive pulled in something heavy). Wait it out or run withDOTFILES_BOOTSTRAP_VERBOSE=1.git-remote-https/git-remote-sshchild →chezmoi initis cloning; GFW slowness is normal here.- Python process in
select()with no children → it's waiting for keyboard input. The TTY reattach failed; kill and re-run from a real terminal (not over a non-PTY SSH session).
Verbose mode¶
Re-run with progress output everywhere:
curl -fsSL https://raw.githubusercontent.com/daviddwlee84/dotfiles/main/bootstrap.sh \
| DOTFILES_BOOTSTRAP_VERBOSE=1 bash
That sets set -x in bootstrap.sh, prints timestamped stage logs, and adds
uv run --verbose so you see every wheel download / venv operation.
Skipping bootstrap.sh entirely (recommended on slow networks)¶
Once you've cloned the repo locally, you don't need the curl-piped bootstrap
at all — call the wrapper script directly via the just recipe:
git clone https://github.com/daviddwlee84/dotfiles ~/.local/share/chezmoi
cd ~/.local/share/chezmoi
just bootstrap-local # interactive (same as bootstrap.sh)
just bootstrap-local-verbose # shows uv resolver progress
# Pass through args after `--`:
just bootstrap-local -- --yes --bundle minimal # non-interactive
just bootstrap-local -- doctor # schema parity check
just bootstrap-local -- list-bundles # see all bundles
Advantages:
- No
raw.githubusercontent.comround-trip for the wrapper script (only the deps still need PyPI, and PyPI is mirrorable; see below). - No
/dev/ttyre-exec gymnastics — you're already in a real shell. - You can
git pulland re-run to test wrapper changes without re-running the curl pipeline. - You can
git checkout <branch>and run a feature branch's wrapper before merging.
The only thing bootstrap.sh does that just bootstrap-local doesn't is
auto-installing uv. If uv is missing on the new host, install it once:
then just bootstrap-local.
Behind GFW / corporate proxy¶
Three layers each need separate proxy/mirror config. The cheapest setup is a
local SOCKS/HTTP proxy (clash, v2ray, etc.) listening on 127.0.0.1:7890:
# (1) shell layer — curl, git, uv installer
export https_proxy=http://127.0.0.1:7890
export http_proxy=$https_proxy
export all_proxy=socks5://127.0.0.1:7890
export no_proxy=localhost,127.0.0.1
# (2) git layer — chezmoi init clones via git
git config --global http.https://github.com.proxy "$https_proxy"
# (or globally: git config --global http.proxy "$https_proxy")
# (3) uv / PyPI layer — switch index instead of routing through proxy
export UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
# Tencent / Aliyun mirrors also work:
# https://mirrors.cloud.tencent.com/pypi/simple
# https://mirrors.aliyun.com/pypi/simple/
If raw.githubusercontent.com itself is the bottleneck (the very first
curl in the bootstrap pipeline), point at a mirror:
# ghproxy.com proxies *.githubusercontent.com transparently
export DOTFILES_RAW_URL="https://ghproxy.com/https://raw.githubusercontent.com/daviddwlee84/dotfiles"
export DOTFILES_REF=main
curl -fsSL "${DOTFILES_RAW_URL}/${DOTFILES_REF}/bootstrap.sh" | bash
bootstrap.sh itself respects DOTFILES_RAW_URL / DOTFILES_REF when
constructing the inner script URL, so the inner uv run --script will also
go through the mirror.
Caveat — jsdelivr / cdn.statically.io style CDNs use a different URL scheme (
/gh/owner/repo@ref/path) and will not work as a drop-inDOTFILES_RAW_URL. Stick withghproxy.com-style transparent proxies, or just clone locally and usejust bootstrap-local.
What the inner dotfiles_init.py does¶
See scripts/init/dotfiles_init.py (~836 lines) — it pre-flights chezmoi /
git / SSH, presents grouped feature-flag prompts via questionary, and
shells out to chezmoi init <repo> --apply --promptString …. Subcommands:
init(default) — the interactive flow.doctor— greps.chezmoi.toml.tmpl+Dockerfileand verifies every prompt key is mirrored in the script'sPROMPTStuple. CI-friendly; exits non-zero on drift. See AGENTS.md → Dockerfile + dotfiles_init wrapper.list-bundles— show the pre-canned feature-flag bundles (personal-mac/work-mac/server-linux/minimal).
Related¶
scripts/init/dotfiles_init.py— the wrapper..chezmoi.toml.tmpl— the prompt definitions chezmoi sees.Dockerfile— the third file that must stay in sync (CHEZMOI_* build args).- docs/this_repo/initial_setup/ — narrative walk-through of a fresh-machine install.
- docs/this_repo/fleet-apply.md — once one machine is up, push to a fleet from there.