Compare commits

...
Author SHA1 Message Date
Claude da80fe81bc Fix five Debian arm64 install failures
- dotnet-sdk-10.0: add setup_dotnet_repo() to register the Microsoft apt
  feed (packages.microsoft.com) before attempting the install
- lua: map to lua5.4 in apt-get overrides (Debian has no unversioned lua pkg)
- pulumi: add setup_pulumi_repo() to register apt.releases.pulumi.com before
  attempting the install; add dnf/yum variant too
- qemu: map to qemu-system in apt-get overrides (Debian meta-package name)
- python3 -m ensurepip: Debian intentionally strips ensurepip from the system
  Python package; fall back to apt-get install python3-pip automatically

Also add libnsl2 → libnsl-dev mapping for Debian (bonus pyenv build fix).

https://claude.ai/code/session_017KuwqSq7nCp2iWiTeaNivn
2026-05-22 18:05:05 +00:00
Jason Ross 2e9cbb0cab Merge pull request #7 from JMR-dev/claude/headless-default-gui-flag-KCpH8
Default to headless install; add --gui flag; add lazygit and pulumi
2026-05-22 12:57:26 -05:00
Claude 18182aee58 Default to headless install; add --gui flag; add lazygit and pulumi
- Inverts the GUI opt-out model: GUI packages and Flatpak are now skipped
  by default (headless-friendly), and --gui opts in to installing them.
  Removes --no-gui entirely.
- Adds lazygit and pulumi to the default system package list.

https://claude.ai/code/session_01CsTAu2SNhE5ZAQm3RnVwp3
2026-05-22 17:56:27 +00:00
Jason Ross 5b184d81ff Merge pull request #6 from JMR-dev/claude/custom-packages-checksum-yf1cS
Fix custom package checksum verification for Mac and Linux arm64
2026-05-22 12:52:08 -05:00
Claude 8668c12f7d Fix custom package checksum verification for Mac and Linux arm64
Three root causes were causing failures on non-x86_64-Linux platforms:

1. Single SHA256 per package: the pinned fallback checksum in
   formatted_packages.py was a single value computed for linux-x86_64 only.
   Downloads on linux-aarch64, macos-x86_64, and macos-aarch64 produced
   different binaries with different digests, so verification always failed
   on those platforms when the fetch_latest resolver was unavailable.

   Fix: replace the single `sha256` field with a `sha256_map` dict keyed by
   "{os}-{arch}" (e.g. "linux-aarch64", "macos-arm64"). Added the correct
   pinned digests for all four supported platforms for both Go 1.26.3 and
   Firecracker 1.15.1, fetched from official sources.

2. Case-sensitive SHA256 comparison: _sha256_of() always returns lowercase
   hex, but checksums returned by external APIs could be uppercase.
   _verify() compared them without normalising case, causing false mismatches.

   Fix: new resolved_sha256 property always returns a lowercased digest;
   _resolve_latest() also lowercases the dynamically-fetched sha before
   storing it.

3. Firecracker not skipped on macOS: _resolve_latest_firecracker() returns
   None on macOS (firecracker is Linux-only), causing a fallback to the
   pinned linux binary URL with a linux-only checksum map entry. The download
   and verification would both fail misleadingly.

   Fix: explicit early-continue guard in install_custom_packages() when
   name == "firecracker" and IS_MACOS.
2026-05-22 17:49:58 +00:00
Jason Ross 539833344b Merge pull request #5 from JMR-dev/claude/add-python-build-deps-u1Gg5
Add missing Python build deps and fix apt-get name overrides
2026-05-18 16:44:49 -05:00
2 changed files with 113 additions and 25 deletions
+101 -23
View File
@@ -5,7 +5,7 @@ Bootstrap packages declared in formatted_packages.py.
Sections handled:
System Packages — installed via dnf, apt-get, or brew (macOS)
Flatpak Packages — installed via flatpak from Flathub (Linux only;
skipped with --no-gui and skipped entirely on macOS)
skipped by default and skipped entirely on macOS; use --gui to enable)
Custom Packages — downloaded, verified, extracted
macOS firecracker VM — provisions a Fedora cloud image under a
hypervisor that supports nested virtualization,
@@ -23,8 +23,8 @@ Xcode Command Line Tools and Homebrew, which is then used as the system
package manager.
Usage:
Linux: sudo python3 bootstrap_environment.py [--only system|flatpak|custom] [--no-gui]
macOS: python3 bootstrap_environment.py [--only system|custom] [--no-gui] [--no-vm]
Linux: sudo python3 bootstrap_environment.py [--only system|flatpak|custom] [--gui]
macOS: python3 bootstrap_environment.py [--only system|custom] [--gui] [--no-vm]
(do NOT use sudo on macOS — Homebrew refuses to run as root)
"""
@@ -47,7 +47,7 @@ import urllib.error
import urllib.request
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
from typing import Optional, Union
import formatted_packages
@@ -389,11 +389,14 @@ _OVERRIDES: dict[str, dict[str, Optional[list[str]]]] = {
},
"apt-get": {
"ffmpeg-free": ["ffmpeg"], # Fedora-specific name
"lua": ["lua5.4"], # Debian ships versioned packages only
"qemu": ["qemu-system"], # Debian meta-package name
"rg": ["ripgrep"],
# Python build deps — Fedora/RHEL naming differs from Debian/Ubuntu
"bzip2-devel": ["libbz2-dev"],
"gdbm-libs": ["libgdbm-dev"],
"libffi-devel": ["libffi-dev"],
"libnsl2": ["libnsl-dev"], # Debian package name
"libuuid-devel": ["uuid-dev"],
"libxml2-devel": ["libxml2-dev"],
"libzstd-devel": ["libzstd-dev"],
@@ -630,6 +633,48 @@ def setup_temurin_repo() -> None:
"sudo apt-get update"
)
def setup_dotnet_repo() -> None:
# .NET is in Fedora repos directly — no extra repo needed.
if PKG_MGR != "apt-get":
return
if _repo_file_exists(
"/etc/apt/sources.list.d/microsoft-prod.list",
"/etc/apt/sources.list.d/dotnet.list",
):
return
distro_id = _os_release_field("ID").strip('"')
version_id = _os_release_field("VERSION_ID").strip('"')
deb_url = (
f"https://packages.microsoft.com/config/{distro_id}/{version_id}"
"/packages-microsoft-prod.deb"
)
shell(
f"curl -fsSL {deb_url} -o /tmp/packages-microsoft-prod.deb && "
"sudo dpkg -i /tmp/packages-microsoft-prod.deb && "
"sudo apt-get update"
)
def setup_pulumi_repo() -> None:
if PKG_MGR == "dnf":
if _repo_file_exists("/etc/yum.repos.d/pulumi.repo"):
return
_write_dnf_repo(
"pulumi", "Pulumi",
"https://yum.releases.pulumi.com/",
"https://api.pulumi.com/releases/sdk/rpm-keyring.gpg",
)
elif PKG_MGR == "apt-get":
if _repo_file_exists("/etc/apt/sources.list.d/pulumi-releases.list"):
return
shell(
"curl -fsSL https://api.pulumi.com/releases/sdk/apt-keyring.gpg | "
"sudo gpg --dearmor -o /usr/share/keyrings/pulumi-releases-keyring.gpg && "
"echo 'deb [signed-by=/usr/share/keyrings/pulumi-releases-keyring.gpg] "
"https://apt.releases.pulumi.com/ stable main' | "
"sudo tee /etc/apt/sources.list.d/pulumi-releases.list > /dev/null && "
"sudo apt-get update"
)
_REPO_GROUPS: list[tuple[set[str], callable]] = [
(
{"containerd.io", "docker-buildx-plugin", "docker-ce-cli",
@@ -640,6 +685,8 @@ _REPO_GROUPS: list[tuple[set[str], callable]] = [
({"google-chrome-stable"}, setup_chrome_repo),
({"vivaldi-stable"}, setup_vivaldi_repo),
({"temurin-25-jdk"}, setup_temurin_repo),
({"dotnet-sdk-10.0"}, setup_dotnet_repo),
({"pulumi"}, setup_pulumi_repo),
]
# ── special package installers ────────────────────────────────────────────────
@@ -649,7 +696,7 @@ _SPECIAL_PKGS: set[str] = (
else {"github-desktop", "zoom", "obsidian", "minikube", "bashtop", "pipx", "poetry"}
)
# GUI apps — skipped when --no-gui is passed (headless environments).
# GUI apps — skipped by default (headless); included only when --gui is passed.
_GUI_SYSTEM_PKGS = {
"github-desktop",
"google-chrome-stable",
@@ -890,7 +937,8 @@ class CustomPackage:
name: str
version: Optional[str] = None # pinned fallback version
url_template: Optional[str] = None # uses {version}, {arch}, {arch_go}
sha256: Optional[str] = None # hex digest of the pinned archive
sha256: Optional[str] = None # single-arch hex digest (set by _resolve_latest)
sha256_map: Optional[dict] = None # per-platform pinned digests: {"os-arch": hex}
sha256_url_template: Optional[str] = None # template for a .minisig URL
minisign_key: Optional[str] = None # base64 public key for minisign verification
fetch_latest: Optional[str] = None # latest-version resolver hint
@@ -907,6 +955,22 @@ class CustomPackage:
if self.sha256_url_template else None
)
@property
def resolved_sha256(self) -> Optional[str]:
"""Return the SHA256 hex for the current OS+arch, lowercased.
sha256 (set dynamically by _resolve_latest) takes priority over sha256_map
so that a freshly fetched checksum always wins over the pinned fallback.
"""
if self.sha256:
return self.sha256.lower()
if self.sha256_map:
key = f"{OS}-{ARCH}"
val = self.sha256_map.get(key)
if val:
return val.lower()
return None
@property
def display_name(self) -> str:
return f"{self.name}-{self.version}" if self.version else self.name
@@ -974,10 +1038,11 @@ def _sha256_of(path: Path) -> str:
def _verify(archive: Path, pkg: CustomPackage) -> bool:
"""Returns True if verification passed (or nothing to verify), False on failure."""
if pkg.sha256:
expected = pkg.resolved_sha256
if expected:
actual = _sha256_of(archive)
if actual != pkg.sha256:
err(f"SHA256 mismatch for {pkg.name}: expected {pkg.sha256}, got {actual}")
if actual != expected:
err(f"SHA256 mismatch for {pkg.name}: expected {expected}, got {actual}")
return False
print(" SHA256 OK")
elif pkg.sha256_url:
@@ -1067,6 +1132,8 @@ def _install_pip() -> None:
Unlike the other custom packages, pip ships inside CPython itself and is
bootstrapped from the wheel in the standard library rather than downloaded.
Debian intentionally disables ensurepip in the system Python package, so we
fall back to the distro's python3-pip package before giving up.
"""
if not has_cmd("python3"):
err("python3 is not installed — cannot install pip")
@@ -1077,8 +1144,15 @@ def _install_pip() -> None:
as_sudo=True, check=False,
)
if bootstrap.returncode != 0:
err("python3 -m ensurepip failed (system Python may need a distro 'python3-pip' package)")
return
if PKG_MGR == "apt-get":
warn("ensurepip unavailable in system Python — installing python3-pip via apt-get")
apt_result = run(["apt-get", "install", "-y", "python3-pip"], as_sudo=True, check=False)
if apt_result.returncode != 0:
err("python3-pip failed to install via apt-get — skipping pip bootstrap")
return
else:
err("python3 -m ensurepip failed (system Python may need a distro 'python3-pip' package)")
return
print(" Upgrading pip to the latest version ...")
upgrade = run(
["python3", "-m", "pip", "install", "--upgrade", "pip"],
@@ -1469,7 +1543,7 @@ def _resolve_latest(pkg: CustomPackage) -> None:
return
print(f" Latest is {latest_version} (pinned was {pkg.version}); using latest.")
pkg.version = latest_version
pkg.sha256 = latest_sha
pkg.sha256 = latest_sha.lower()
pkg.sha256_url_template = None # prefer the freshly resolved sha256
@@ -1483,6 +1557,11 @@ def install_custom_packages(to_install: list[CustomPackage]) -> None:
if check_path is None and name_lower != "pip":
warn(f"{pkg.name}: no known install path — script will not detect future installs")
# Packages that are OS-specific
if name_lower == "firecracker" and IS_MACOS:
warn(f"{pkg.name}: Linux-only — skipping on macOS")
continue
# Handlers that manage their own download/install
if name_lower == "nvm":
_install_nvm()
@@ -2203,10 +2282,10 @@ def main() -> None:
)
ap.add_argument("--only", choices=["system", "flatpak", "custom"],
help="Install only the named section")
ap.add_argument("--no-gui", action="store_true",
help="Skip GUI applications (suitable for headless environments). "
"Excludes GUI system packages and skips the entire Flatpak "
"section, including installing flatpak itself.")
ap.add_argument("--gui", action="store_true",
help="Include GUI applications (headed environments). "
"Adds GUI system packages and enables the Flatpak "
"section. By default GUI apps and Flatpak are skipped.")
ap.add_argument("--no-vm", action="store_true",
help="macOS only: skip provisioning the Fedora-on-QEMU VM that "
"backs the firecracker() zsh wrapper.")
@@ -2222,8 +2301,8 @@ def main() -> None:
print(f"OS: {OS}")
print(f"Architecture: {ARCH}")
print(f"Package manager: {PKG_MGR}")
if args.no_gui:
print("Mode: headless (--no-gui) — skipping GUI apps and Flatpak")
if not args.gui:
print("Mode: headless (default) — skipping GUI apps and Flatpak")
if IS_MACOS:
# Refuse to run as root before doing anything (brew won't run as root).
@@ -2233,19 +2312,18 @@ def main() -> None:
print("Checking installed packages ...")
if args.no_gui:
if not args.gui:
skipped_gui = [p for p in system_pkgs if p in _GUI_SYSTEM_PKGS]
system_pkgs = [p for p in system_pkgs if p not in _GUI_SYSTEM_PKGS]
if skipped_gui:
print(f" [NO-GUI] Skipping GUI system packages: {_fmt(skipped_gui)}")
print(f" [HEADLESS] Skipping GUI system packages: {_fmt(skipped_gui)}")
flatpak_pkgs = []
# Flatpak is Linux-only — macOS has no Flatpak section regardless of flags.
# --no-gui also suppresses the Flatpak section entirely (both `flatpak`
# itself and the Flathub apps), even when --only=flatpak is requested.
# GUI apps and Flatpak are skipped by default; --gui re-enables them.
do_flatpak = (
args.only in (None, "flatpak")
and not args.no_gui
and args.gui
and not IS_MACOS
)
+12 -2
View File
@@ -36,6 +36,7 @@ SYSTEM_PACKAGES: list[str] = [
"git",
"github-desktop",
"google-chrome-stable",
"lazygit",
"lua",
"minisign",
"minikube",
@@ -43,6 +44,7 @@ SYSTEM_PACKAGES: list[str] = [
"obsidian",
"pipx",
"poetry",
"pulumi",
"podman",
"qemu",
"restic",
@@ -94,7 +96,12 @@ CUSTOM_PACKAGES: list[dict] = [
"name": "go",
"version": "1.26.3",
"url_template": "https://go.dev/dl/go{version}.{os_go}-{arch_go}.tar.gz",
"sha256": "2b2cfc7148493da5e73981bffbf3353af381d5f93e789c82c79aff64962eb556",
"sha256_map": {
"linux-x86_64": "2b2cfc7148493da5e73981bffbf3353af381d5f93e789c82c79aff64962eb556",
"linux-aarch64": "9d89a3ea57d141c2b22d70083f2c8459ba3890f2d9e818e7e933b75614936565",
"macos-x86_64": "278d580b32e299fe4a9c990fcf2d02acfe538c7e551a6ee18f9c7164573d2c63",
"macos-aarch64": "875cf54a15311eee2c99b9dd67c68c4a49351d489ab622bf2cfd28c8f2078d3c",
},
"fetch_latest": "go",
},
{"name": "neovim"},
@@ -105,7 +112,10 @@ CUSTOM_PACKAGES: list[dict] = [
"https://github.com/firecracker-microvm/firecracker/releases/download/"
"v{version}/firecracker-v{version}-{arch}.tgz"
),
"sha256": "d4a32ab2322d887ca1bc4a4e7afa9cc35393e6362dfc2b3becb389d362e4275a",
"sha256_map": {
"linux-x86_64": "d4a32ab2322d887ca1bc4a4e7afa9cc35393e6362dfc2b3becb389d362e4275a",
"linux-aarch64": "00654ac1e702a22744121ea9f10a4f792ebd7c3a744cba587dfac9fcb79b41a5",
},
"fetch_latest": "firecracker",
},
{