diff --git a/Bins/versions.json b/Bins/versions.json new file mode 100644 index 0000000..63d50d7 --- /dev/null +++ b/Bins/versions.json @@ -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" + } + } + } +} diff --git a/Commands/Bin/mod.just b/Commands/Bin/mod.just new file mode 100644 index 0000000..c83e74a --- /dev/null +++ b/Commands/Bin/mod.just @@ -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' diff --git a/Commands/Lang/mod.just b/Commands/Lang/mod.just new file mode 100644 index 0000000..abbac8e --- /dev/null +++ b/Commands/Lang/mod.just @@ -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' diff --git a/Commands/Setup/mod.just b/Commands/Setup/mod.just new file mode 100644 index 0000000..1823590 --- /dev/null +++ b/Commands/Setup/mod.just @@ -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" diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..c4e8cff --- /dev/null +++ b/Justfile @@ -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 diff --git a/Makefile b/Makefile index 685f0db..a609f61 100644 --- a/Makefile +++ b/Makefile @@ -1,93 +1,10 @@ # Makefile -REBOOT_MARKER := .setup-reboot-required +SCRIPTS_DIR := ./Scripts # Default target -all: stow +all: setup -# Full Setup +# Bootstrap entrypoint for first-run setup. setup: - @set -e; \ - 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" + bash $(SCRIPTS_DIR)/setup.sh diff --git a/scripts/base.sh b/Scripts/base.sh old mode 100755 new mode 100644 similarity index 97% rename from scripts/base.sh rename to Scripts/base.sh index fb1c438..acc5a0c --- a/scripts/base.sh +++ b/Scripts/base.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Path: scripts/base.sh +# Path: Scripts/base.sh 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 fi -# Moving Pre-Built bin to .local/bin -mkdir -p "$HOME/.local/bin" -cp -f "$REPO_ROOT/bin/"* "$HOME/.local/bin/" -chmod +x "$HOME/.local/bin/"* +# Installing pinned repo-managed CLI binaries +bash "$REPO_ROOT/Scripts/bin/install.sh" --all if ! command -v rclone &> /dev/null; then echo -e "${BLUE} LOG:${YELLOW} Installing Rclone CLI...${NC}" @@ -250,7 +248,8 @@ fi # 6. Cleanup & Secrets 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 TARGET_SHELL="$(command -v zsh)" diff --git a/Scripts/bin/install.sh b/Scripts/bin/install.sh new file mode 100644 index 0000000..9f85c5f --- /dev/null +++ b/Scripts/bin/install.sh @@ -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 "$@" diff --git a/Scripts/bin/update.sh b/Scripts/bin/update.sh new file mode 100644 index 0000000..95eb99e --- /dev/null +++ b/Scripts/bin/update.sh @@ -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 "$@" diff --git a/scripts/cpp.sh b/Scripts/cpp.sh similarity index 97% rename from scripts/cpp.sh rename to Scripts/cpp.sh index 521e798..3da255e 100644 --- a/scripts/cpp.sh +++ b/Scripts/cpp.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Path: scripts/cpp.sh +# Path: Scripts/cpp.sh set -e diff --git a/scripts/go.sh b/Scripts/go.sh similarity index 98% rename from scripts/go.sh rename to Scripts/go.sh index 6653ebb..1ce871c 100644 --- a/scripts/go.sh +++ b/Scripts/go.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Path: scripts/go.sh +# Path: Scripts/go.sh set -e diff --git a/scripts/lib/distro.sh b/Scripts/lib/distro.sh similarity index 100% rename from scripts/lib/distro.sh rename to Scripts/lib/distro.sh diff --git a/scripts/node.sh b/Scripts/node.sh similarity index 97% rename from scripts/node.sh rename to Scripts/node.sh index cfd25c3..9629874 100644 --- a/scripts/node.sh +++ b/Scripts/node.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Path: scripts/node.sh +# Path: Scripts/node.sh set -e diff --git a/scripts/provision.sh b/Scripts/provision.sh old mode 100755 new mode 100644 similarity index 99% rename from scripts/provision.sh rename to Scripts/provision.sh index ce4e93c..d642791 --- a/scripts/provision.sh +++ b/Scripts/provision.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Path: scripts/provision.sh +# Path: Scripts/provision.sh set -e diff --git a/scripts/python.sh b/Scripts/python.sh similarity index 99% rename from scripts/python.sh rename to Scripts/python.sh index 2532cd2..47bddf6 100644 --- a/scripts/python.sh +++ b/Scripts/python.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Path: scripts/python.sh +# Path: Scripts/python.sh set -e BLUE='\033[1;34m' diff --git a/scripts/r.sh b/Scripts/r.sh similarity index 75% rename from scripts/r.sh rename to Scripts/r.sh index 8492fff..0bd366a 100644 --- a/scripts/r.sh +++ b/Scripts/r.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Path: scripts/r.sh +# Path: Scripts/r.sh set -e BLUE='\033[1;34m' @@ -12,6 +12,27 @@ NC='\033[0m' SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 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" < /dev/null; then else echo -e "${RED} ERROR: Cargo not found in PATH. Check CARGO_HOME.${NC}" exit 1 -fi \ No newline at end of file +fi diff --git a/Scripts/setup.sh b/Scripts/setup.sh new file mode 100644 index 0000000..9f2b65d --- /dev/null +++ b/Scripts/setup.sh @@ -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 "$@" diff --git a/.zsh_aliases b/Zsh/.zsh_aliases similarity index 99% rename from .zsh_aliases rename to Zsh/.zsh_aliases index 2202ecf..8e9cc2b 100644 --- a/.zsh_aliases +++ b/Zsh/.zsh_aliases @@ -91,4 +91,4 @@ proxy_on() { proxy_off() { unset ALL_PROXY HTTP_PROXY HTTPS_PROXY echo "Proxy OFF (direct)" -} \ No newline at end of file +} diff --git a/.zsh_secrets.example b/Zsh/.zsh_secrets.example similarity index 100% rename from .zsh_secrets.example rename to Zsh/.zsh_secrets.example diff --git a/.zshenv b/Zsh/.zshenv similarity index 93% rename from .zshenv rename to Zsh/.zshenv index 0cdebd1..9f5e377 100644 --- a/.zshenv +++ b/Zsh/.zshenv @@ -4,6 +4,8 @@ [ -f "$HOME/.zsh_secrets" ] && source "$HOME/.zsh_secrets" export GOGC=500 +export R_ROOT="$HOME/.programming/r" +export R_LIBS_USER="$R_ROOT/library" # CodeGraphContext defaults export CGC_RUNTIME_DB_TYPE=kuzudb diff --git a/.zshrc b/Zsh/.zshrc similarity index 100% rename from .zshrc rename to Zsh/.zshrc diff --git a/bin/eza b/bin/eza deleted file mode 100644 index 84a5edf..0000000 Binary files a/bin/eza and /dev/null differ diff --git a/bin/nvim b/bin/nvim deleted file mode 100644 index 2887bbc..0000000 Binary files a/bin/nvim and /dev/null differ