Refactor setup workflow

This commit is contained in:
MangoPig
2026-05-31 23:24:18 +01:00
parent 5cefa4019b
commit 58531bf579
24 changed files with 478 additions and 103 deletions

32
Bins/versions.json Normal file
View File

@@ -0,0 +1,32 @@
{
"nvim": {
"owner": "neovim",
"repo": "neovim",
"version": "v0.11.5",
"linux": {
"x86_64": {
"asset": "nvim-linux-x86_64.tar.gz",
"binary": "nvim-linux-x86_64/bin/nvim"
},
"aarch64": {
"asset": "nvim-linux-arm64.tar.gz",
"binary": "nvim-linux-arm64/bin/nvim"
}
}
},
"eza": {
"owner": "eza-community",
"repo": "eza",
"version": "v0.23.4",
"linux": {
"x86_64": {
"asset": "eza_x86_64-unknown-linux-gnu.tar.gz",
"binary": "eza"
},
"aarch64": {
"asset": "eza_aarch64-unknown-linux-gnu.tar.gz",
"binary": "eza"
}
}
}
}

22
Commands/Bin/mod.just Normal file
View File

@@ -0,0 +1,22 @@
project_root := justfile_directory()
bin_script_dir := project_root + "/Scripts/bin"
# Install all pinned repo-managed binaries into ~/.local/bin.
install-all:
bash '{{bin_script_dir}}/install.sh' --all
# Install one pinned repo-managed binary into ~/.local/bin.
install tool:
bash '{{bin_script_dir}}/install.sh' '{{tool}}'
# Update the pinned version for a binary. If no version is given, open an fzf selector when available.
update tool version='':
bash '{{bin_script_dir}}/update.sh' '{{tool}}' '{{version}}'
# List available releases for a supported binary.
list tool:
bash '{{bin_script_dir}}/update.sh' --list '{{tool}}'
# Print the currently pinned versions.
show:
cat '{{project_root}}/Bins/versions.json'

20
Commands/Lang/mod.just Normal file
View File

@@ -0,0 +1,20 @@
project_root := justfile_directory()
scripts_dir := project_root + "/Scripts"
node:
bash '{{scripts_dir}}/node.sh'
go:
bash '{{scripts_dir}}/go.sh'
rust:
bash '{{scripts_dir}}/rust.sh'
python:
bash '{{scripts_dir}}/python.sh'
r:
bash '{{scripts_dir}}/r.sh'
cpp:
bash '{{scripts_dir}}/cpp.sh'

43
Commands/Setup/mod.just Normal file
View File

@@ -0,0 +1,43 @@
project_root := justfile_directory()
scripts_dir := project_root + "/Scripts"
# Run the full setup flow.
all:
bash '{{scripts_dir}}/setup.sh'
# Install the base system layer only.
base:
bash '{{scripts_dir}}/base.sh'
# Stow shell files into $HOME.
stow:
stow --dir='{{project_root}}' Zsh --target="$HOME"
# Remove stowed shell files from $HOME.
clean:
stow --dir='{{project_root}}' -D Zsh --target="$HOME"
# Pull latest changes and rerun setup.
update:
git -C '{{project_root}}' pull origin main
just --justfile '{{project_root}}/Justfile' setup all
# Run distro test containers.
test-ubuntu:
echo "Ubuntu Test"
docker run -it --rm -e TERM=xterm-256color -v '{{project_root}}':/root/dotfiles ubuntu:latest \
bash -c "export DEBIAN_FRONTEND=noninteractive && \
apt-get update && \
apt-get install -y sudo git make curl && \
cd /root/dotfiles && \
make setup"
test-arch:
echo "Spawning Arch Container..."
docker run -it --rm -e TERM=xterm-256color -v '{{project_root}}':/root/dotfiles archlinux:latest \
bash -c "pacman -Sy --noconfirm base-devel git make sudo && cd /root/dotfiles && make setup"
test-fedora:
echo "Spawning Fedora Container..."
docker run -it --rm -e TERM=xterm-256color -v '{{project_root}}':/root/dotfiles fedora:latest \
bash -c "dnf install -y git make sudo curl which passwd procps-ng && cd /root/dotfiles && make setup"

9
Justfile Normal file
View File

@@ -0,0 +1,9 @@
set shell := ["bash", "-cu"]
mod bin "Commands/Bin"
mod lang "Commands/Lang"
mod setup "Commands/Setup"
[default]
help:
just --list --list-submodules

View File

@@ -1,93 +1,10 @@
# Makefile # Makefile
REBOOT_MARKER := .setup-reboot-required SCRIPTS_DIR := ./Scripts
# Default target # Default target
all: stow all: setup
# Full Setup # Bootstrap entrypoint for first-run setup.
setup: setup:
@set -e; \ bash $(SCRIPTS_DIR)/setup.sh
rm -f $(REBOOT_MARKER); \
bash ./scripts/base.sh; \
if [ -f $(REBOOT_MARKER) ]; then \
rm -f $(REBOOT_MARKER); \
echo "Package layering finished. Reboot, then rerun make setup."; \
exit 0; \
fi; \
bash ./scripts/node.sh; \
bash ./scripts/go.sh; \
bash ./scripts/rust.sh; \
bash ./scripts/python.sh; \
if [ -f $(REBOOT_MARKER) ]; then \
rm -f $(REBOOT_MARKER); \
echo "Package layering finished. Reboot, then rerun make setup."; \
exit 0; \
fi; \
bash ./scripts/r.sh; \
if [ -f $(REBOOT_MARKER) ]; then \
rm -f $(REBOOT_MARKER); \
echo "Package layering finished. Reboot, then rerun make setup."; \
exit 0; \
fi; \
$(MAKE) clean; \
$(MAKE) stow; \
echo "Full setup completed."
base:
bash ./scripts/base.sh
@echo "Base setup completed."
# Just stow the dotfiles
stow:
stow . --target=$$HOME --ignore=".git" --ignore=".gitignore" --ignore="README.md" --ignore=".zsh_secrets" --ignore=".zsh_secrets.example" --ignore="LICENSE" --ignore="Makefile" --ignore="bin" --ignore="scripts"
@echo "Dotfiles linked."
# Clean old files and links
clean:
stow -D . --target=$$HOME
@echo "Links removed."
# Pull Git Updates
update:
git pull origin main
$(MAKE) setup
# Language Setups
node:
bash ./scripts/node.sh
go:
bash ./scripts/go.sh
rust:
bash ./scripts/rust.sh
python:
bash ./scripts/python.sh
r:
bash ./scripts/r.sh
cpp:
bash ./scripts/cpp.sh
# Docker Tests
test-ubuntu:
@echo "Ubuntu Test"
docker run -it --rm -e TERM=xterm-256color -v $(PWD):/root/dotfiles ubuntu:latest \
bash -c "export DEBIAN_FRONTEND=noninteractive && \
apt-get update && \
apt-get install -y sudo git make curl && \
cd /root/dotfiles && \
make setup"
test-arch:
@echo "Spawning Arch Container..."
docker run -it --rm -e TERM=xterm-256color -v $(PWD):/root/dotfiles archlinux:latest \
bash -c "pacman -Sy --noconfirm base-devel git make sudo && cd /root/dotfiles && make setup"
test-fedora:
@echo "Spawning Fedora Container..."
docker run -it --rm -e TERM=xterm-256color -v $(PWD):/root/dotfiles fedora:latest \
bash -c "dnf install -y git make sudo curl which passwd procps-ng && cd /root/dotfiles && make setup"

11
scripts/base.sh → Scripts/base.sh Executable file → Normal file
View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# Path: scripts/base.sh # Path: Scripts/base.sh
set -e set -e
@@ -154,10 +154,8 @@ if is_debian_family || is_fedora_family; then
[ -f /usr/bin/batcat ] && sudo ln -sf /usr/bin/batcat /usr/local/bin/bat [ -f /usr/bin/batcat ] && sudo ln -sf /usr/bin/batcat /usr/local/bin/bat
fi fi
# Moving Pre-Built bin to .local/bin # Installing pinned repo-managed CLI binaries
mkdir -p "$HOME/.local/bin" bash "$REPO_ROOT/Scripts/bin/install.sh" --all
cp -f "$REPO_ROOT/bin/"* "$HOME/.local/bin/"
chmod +x "$HOME/.local/bin/"*
if ! command -v rclone &> /dev/null; then if ! command -v rclone &> /dev/null; then
echo -e "${BLUE} LOG:${YELLOW} Installing Rclone CLI...${NC}" echo -e "${BLUE} LOG:${YELLOW} Installing Rclone CLI...${NC}"
@@ -250,7 +248,8 @@ fi
# 6. Cleanup & Secrets # 6. Cleanup & Secrets
rm -f "$HOME/.zshrc" "$HOME/.zsh_aliases" rm -f "$HOME/.zshrc" "$HOME/.zsh_aliases"
touch "$HOME/.zsh_secrets" mkdir -p "$REPO_ROOT/Zsh"
touch "$REPO_ROOT/Zsh/.zsh_secrets"
# 7. Set Shell # 7. Set Shell
TARGET_SHELL="$(command -v zsh)" TARGET_SHELL="$(command -v zsh)"

99
Scripts/bin/install.sh Normal file
View File

@@ -0,0 +1,99 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
MANIFEST="$REPO_ROOT/Bins/versions.json"
TARGET_DIR="$HOME/.local/bin"
ensure_deps() {
local missing=()
for cmd in jq curl tar; do
if ! command -v "$cmd" >/dev/null 2>&1; then
missing+=("$cmd")
fi
done
if [ "${#missing[@]}" -gt 0 ]; then
printf 'Missing required commands: %s\n' "${missing[*]}" >&2
exit 1
fi
}
detect_arch() {
case "$(uname -m)" in
x86_64) printf 'x86_64\n' ;;
aarch64|arm64) printf 'aarch64\n' ;;
*)
printf 'Unsupported architecture: %s\n' "$(uname -m)" >&2
exit 1
;;
esac
}
install_tool() {
local tool="$1"
local arch version owner repo asset binary_rel url tmp_dir archive_path extracted_path target_path
arch="$(detect_arch)"
if ! jq -e --arg tool "$tool" '.[$tool]' "$MANIFEST" >/dev/null; then
printf 'Unsupported tool: %s\n' "$tool" >&2
exit 1
fi
version="$(jq -r --arg tool "$tool" '.[$tool].version' "$MANIFEST")"
owner="$(jq -r --arg tool "$tool" '.[$tool].owner' "$MANIFEST")"
repo="$(jq -r --arg tool "$tool" '.[$tool].repo' "$MANIFEST")"
asset="$(jq -r --arg tool "$tool" --arg arch "$arch" '.[$tool].linux[$arch].asset' "$MANIFEST")"
binary_rel="$(jq -r --arg tool "$tool" --arg arch "$arch" '.[$tool].linux[$arch].binary' "$MANIFEST")"
if [ -z "$asset" ] || [ "$asset" = "null" ] || [ -z "$binary_rel" ] || [ "$binary_rel" = "null" ]; then
printf 'No Linux asset mapping for %s on %s\n' "$tool" "$arch" >&2
exit 1
fi
mkdir -p "$TARGET_DIR"
tmp_dir="$(mktemp -d)"
archive_path="$tmp_dir/$asset"
url="https://github.com/$owner/$repo/releases/download/$version/$asset"
printf 'Installing %s %s\n' "$tool" "$version"
curl -fL "$url" -o "$archive_path"
tar -xzf "$archive_path" -C "$tmp_dir"
extracted_path="$tmp_dir/$binary_rel"
if [ ! -f "$extracted_path" ]; then
printf 'Expected binary not found after extraction: %s\n' "$binary_rel" >&2
rm -rf "$tmp_dir"
exit 1
fi
target_path="$TARGET_DIR/$tool"
install -m 755 "$extracted_path" "$target_path"
rm -rf "$tmp_dir"
printf 'Installed %s -> %s\n' "$tool" "$target_path"
}
main() {
ensure_deps
if [ "${1:-}" = "--all" ]; then
while IFS= read -r tool; do
install_tool "$tool"
done < <(jq -r 'keys[]' "$MANIFEST")
exit 0
fi
if [ -z "${1:-}" ]; then
printf 'Usage: %s [--all|tool]\n' "$0" >&2
exit 1
fi
install_tool "$1"
}
main "$@"

135
Scripts/bin/update.sh Normal file
View File

@@ -0,0 +1,135 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
MANIFEST="$REPO_ROOT/Bins/versions.json"
ensure_deps() {
local missing=()
for cmd in jq curl python3; do
if ! command -v "$cmd" >/dev/null 2>&1; then
missing+=("$cmd")
fi
done
if [ "${#missing[@]}" -gt 0 ]; then
printf 'Missing required commands: %s\n' "${missing[*]}" >&2
exit 1
fi
}
github_api() {
local path="$1"
local auth_args=()
if [ -n "${GITHUB_TOKEN:-}" ]; then
auth_args=(-H "Authorization: Bearer $GITHUB_TOKEN")
fi
curl -fsSL \
-H "Accept: application/vnd.github+json" \
"${auth_args[@]}" \
"https://api.github.com${path}"
}
list_versions() {
local tool="$1" owner repo
if ! jq -e --arg tool "$tool" '.[$tool]' "$MANIFEST" >/dev/null; then
printf 'Unsupported tool: %s\n' "$tool" >&2
exit 1
fi
owner="$(jq -r --arg tool "$tool" '.[$tool].owner' "$MANIFEST")"
repo="$(jq -r --arg tool "$tool" '.[$tool].repo' "$MANIFEST")"
github_api "/repos/$owner/$repo/releases?per_page=100" | jq -r '.[].tag_name'
}
select_version() {
local tool="$1"
local versions
versions="$(list_versions "$tool")"
if [ -z "$versions" ]; then
printf 'No releases found for %s\n' "$tool" >&2
exit 1
fi
if command -v fzf >/dev/null 2>&1; then
printf '%s\n' "$versions" | fzf --prompt="Select ${tool} version > " --height=20 --reverse
else
printf '%s\n' "$versions" | sed -n '1,20p' >&2
printf 'fzf is not installed, so pass a version explicitly.\n' >&2
exit 1
fi
}
update_version() {
local tool="$1" version="$2"
local tmp_file
if [ -z "$version" ]; then
printf 'No version selected for %s\n' "$tool" >&2
exit 1
fi
tmp_file="$(mktemp)"
python3 - "$MANIFEST" "$tool" "$version" "$tmp_file" <<'PY'
import json
import pathlib
import sys
manifest_path = pathlib.Path(sys.argv[1])
tool = sys.argv[2]
version = sys.argv[3]
tmp_path = pathlib.Path(sys.argv[4])
data = json.loads(manifest_path.read_text())
if tool not in data:
raise SystemExit(f"Unsupported tool: {tool}")
data[tool]["version"] = version
tmp_path.write_text(json.dumps(data, indent=2) + "\n")
PY
mv "$tmp_file" "$MANIFEST"
printf 'Pinned %s to %s\n' "$tool" "$version"
}
main() {
local mode="update" tool version
ensure_deps
if [ "${1:-}" = "--list" ]; then
mode="list"
shift
fi
tool="${1:-}"
version="${2:-}"
if [ -z "$tool" ]; then
printf 'Usage: %s [--list] tool [version]\n' "$0" >&2
exit 1
fi
case "$mode" in
list)
list_versions "$tool"
;;
update)
if [ -z "$version" ]; then
version="$(select_version "$tool")"
fi
update_version "$tool" "$version"
;;
esac
}
main "$@"

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# Path: scripts/cpp.sh # Path: Scripts/cpp.sh
set -e set -e

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# Path: scripts/go.sh # Path: Scripts/go.sh
set -e set -e

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# Path: scripts/node.sh # Path: Scripts/node.sh
set -e set -e

2
scripts/provision.sh → Scripts/provision.sh Executable file → Normal file
View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# Path: scripts/provision.sh # Path: Scripts/provision.sh
set -e set -e

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# Path: scripts/python.sh # Path: Scripts/python.sh
set -e set -e
BLUE='\033[1;34m' BLUE='\033[1;34m'

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# Path: scripts/r.sh # Path: Scripts/r.sh
set -e set -e
BLUE='\033[1;34m' BLUE='\033[1;34m'
@@ -12,6 +12,27 @@ NC='\033[0m'
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
source "$SCRIPT_DIR/lib/distro.sh" source "$SCRIPT_DIR/lib/distro.sh"
PROG_DIR="$HOME/.programming"
R_ROOT="$PROG_DIR/r"
R_BIN_DIR="$R_ROOT/bin"
R_LIB_DIR="$R_ROOT/library"
mkdir -p "$R_BIN_DIR" "$R_LIB_DIR"
write_r_wrapper() {
local target_name="$1"
local command_body="$2"
cat > "$R_BIN_DIR/$target_name" <<EOF
#!/bin/bash
set -e
export R_LIBS_USER="$R_LIB_DIR"
$command_body "\$@"
EOF
chmod +x "$R_BIN_DIR/$target_name"
}
echo -e "${BLUE} LOG:${YELLOW} Detecting R installation strategy for $OS...${NC}" echo -e "${BLUE} LOG:${YELLOW} Detecting R installation strategy for $OS...${NC}"
if is_arch_family; then if is_arch_family; then
@@ -30,6 +51,8 @@ if is_arch_family; then
echo -e "${GREEN} SUCCESS:${NC} R installed via Pacman." echo -e "${GREEN} SUCCESS:${NC} R installed via Pacman."
R_BIN="R" R_BIN="R"
write_r_wrapper "R" 'exec "$(command -v R)"'
write_r_wrapper "Rscript" 'exec "$(command -v Rscript)"'
elif is_debian_family; then elif is_debian_family; then
@@ -77,6 +100,8 @@ elif is_debian_family; then
fi fi
R_BIN="rig run" R_BIN="rig run"
write_r_wrapper "R" 'exec rig run'
write_r_wrapper "Rscript" 'exec rig run Rscript'
elif is_fedora_family; then elif is_fedora_family; then
@@ -90,14 +115,18 @@ elif is_fedora_family; then
fi fi
R_BIN="R" R_BIN="R"
write_r_wrapper "R" 'exec "$(command -v R)"'
write_r_wrapper "Rscript" 'exec "$(command -v Rscript)"'
else else
echo -e "${RED} ERROR:${NC} Unsupported OS: $OS" echo -e "${RED} ERROR:${NC} Unsupported OS: $OS"
exit 1 exit 1
fi fi
export R_LIBS_USER="$R_LIB_DIR"
echo -e "${BLUE} LOG:${YELLOW} Installing 'renv' package in the user R library...${NC}" echo -e "${BLUE} LOG:${YELLOW} Installing 'renv' package in the user R library...${NC}"
$R_BIN -e 'user_lib <- path.expand(Sys.getenv("R_LIBS_USER", unset = "~/R/library")); dir.create(user_lib, recursive = TRUE, showWarnings = FALSE); .libPaths(c(user_lib, .libPaths())); if (!require("renv", quietly=TRUE)) install.packages("renv", lib = user_lib, repos = "https://cloud.r-project.org")' $R_BIN -e 'user_lib <- path.expand(Sys.getenv("R_LIBS_USER", unset = "~/.programming/r/library")); dir.create(user_lib, recursive = TRUE, showWarnings = FALSE); .libPaths(c(user_lib, .libPaths())); if (!require("renv", quietly=TRUE)) install.packages("renv", lib = user_lib, repos = "https://cloud.r-project.org")'
echo -e "${GREEN} SUCCESS:${NC} R setup completed." echo -e "${GREEN} SUCCESS:${NC} R setup completed."

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# Path: scripts/rust.sh # Path: Scripts/rust.sh
set -e set -e
@@ -38,4 +38,4 @@ if command -v cargo &> /dev/null; then
else else
echo -e "${RED} ERROR: Cargo not found in PATH. Check CARGO_HOME.${NC}" echo -e "${RED} ERROR: Cargo not found in PATH. Check CARGO_HOME.${NC}"
exit 1 exit 1
fi fi

68
Scripts/setup.sh Normal file
View File

@@ -0,0 +1,68 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
REBOOT_MARKER="$REPO_ROOT/.setup-reboot-required"
REPO_SECRET_FILE="$REPO_ROOT/Zsh/.zsh_secrets"
HOME_SECRET_FILE="$HOME/.zsh_secrets"
handle_reboot_marker() {
if [ -f "$REBOOT_MARKER" ]; then
rm -f "$REBOOT_MARKER"
echo "Package layering finished. Reboot, then rerun the same command."
exit 0
fi
}
sync_repo_managed_secret_file() {
mkdir -p "$(dirname "$REPO_SECRET_FILE")"
if [ -L "$HOME_SECRET_FILE" ]; then
return 0
fi
if [ -f "$HOME_SECRET_FILE" ]; then
if [ ! -f "$REPO_SECRET_FILE" ] || [ ! -s "$REPO_SECRET_FILE" ]; then
mv "$HOME_SECRET_FILE" "$REPO_SECRET_FILE"
return 0
fi
if cmp -s "$HOME_SECRET_FILE" "$REPO_SECRET_FILE"; then
rm -f "$HOME_SECRET_FILE"
return 0
fi
echo "Conflict: both $HOME_SECRET_FILE and $REPO_SECRET_FILE exist with different contents."
echo "Please merge them manually, then rerun the command."
exit 1
fi
touch "$REPO_SECRET_FILE"
}
main() {
rm -f "$REBOOT_MARKER"
bash "$SCRIPT_DIR/base.sh"
handle_reboot_marker
bash "$SCRIPT_DIR/node.sh"
bash "$SCRIPT_DIR/go.sh"
bash "$SCRIPT_DIR/rust.sh"
bash "$SCRIPT_DIR/python.sh"
handle_reboot_marker
bash "$SCRIPT_DIR/r.sh"
handle_reboot_marker
sync_repo_managed_secret_file
stow --dir="$REPO_ROOT" -D Zsh --target="$HOME" 2>/dev/null || true
stow --dir="$REPO_ROOT" Zsh --target="$HOME"
echo "Full setup completed."
}
main "$@"

View File

@@ -91,4 +91,4 @@ proxy_on() {
proxy_off() { proxy_off() {
unset ALL_PROXY HTTP_PROXY HTTPS_PROXY unset ALL_PROXY HTTP_PROXY HTTPS_PROXY
echo "Proxy OFF (direct)" echo "Proxy OFF (direct)"
} }

View File

@@ -4,6 +4,8 @@
[ -f "$HOME/.zsh_secrets" ] && source "$HOME/.zsh_secrets" [ -f "$HOME/.zsh_secrets" ] && source "$HOME/.zsh_secrets"
export GOGC=500 export GOGC=500
export R_ROOT="$HOME/.programming/r"
export R_LIBS_USER="$R_ROOT/library"
# CodeGraphContext defaults # CodeGraphContext defaults
export CGC_RUNTIME_DB_TYPE=kuzudb export CGC_RUNTIME_DB_TYPE=kuzudb

View File

BIN
bin/eza

Binary file not shown.

BIN
bin/nvim

Binary file not shown.