Refactored justfile for more reliability

This commit is contained in:
2026-04-07 12:53:21 +01:00
parent ea35fe339b
commit c4d74f74b8
4 changed files with 330 additions and 242 deletions
+217
View File
@@ -0,0 +1,217 @@
#!/usr/bin/env nix-shell
#!nix-shell -i python3 -p "python3.withPackages(ps: with ps; [ typer rich ])"
import os
import sys
import subprocess
import socket
import getpass
import shutil
REPO_URL = "https://github.com/Cian-H/My_NixOS_Config"
TARGET_DIR = os.path.expanduser("~/.config/nix")
def command_exists(cmd: str) -> bool:
"""Check if a command is available on the system."""
return shutil.which(cmd) is not None
def check_preconditions(target_dir: str):
"""Ensure the system is in a valid state to begin bootstrapping."""
if not command_exists("git"):
print(
"Error: 'git' is not installed. Please install git before bootstrapping.",
file=sys.stderr,
)
sys.exit(1)
if os.path.exists(target_dir):
print(
f"Error: {target_dir} already exists. Aborting bootstrap.", file=sys.stderr
)
sys.exit(1)
def clone_repository(repo_url: str, target_dir: str):
"""Clone the configuration repository and enter the directory."""
print(f">> Cloning {repo_url} into {target_dir}...")
try:
subprocess.run(["git", "clone", repo_url, target_dir], check=True)
except subprocess.CalledProcessError:
print("Error: Failed to clone repository.", file=sys.stderr)
sys.exit(1)
os.chdir(target_dir)
def detect_nixos() -> bool:
"""Check if the current host system is NixOS."""
if os.path.exists("/etc/os-release"):
with open("/etc/os-release", "r") as f:
if "ID=nixos" in f.read():
return True
return False
def has_config(sys_name: str, is_nixos: bool) -> bool:
"""Determine if system configuration directories exist for the given hostname."""
has_nixos_dir = os.path.isdir(f"nixos/{sys_name}")
has_hm_dir = os.path.isdir(f"home-manager/{sys_name}")
return (has_nixos_dir and has_hm_dir) if is_nixos else has_hm_dir
def get_available_systems() -> list:
"""Discover existing system configurations in the repository."""
opts = set()
if os.path.exists("nixos"):
opts.update([d for d in os.listdir("nixos") if os.path.isdir(f"nixos/{d}")])
if os.path.exists("home-manager"):
opts.update(
[
d
for d in os.listdir("home-manager")
if os.path.isdir(f"home-manager/{d}")
]
)
# Filter out base/core directories and hidden folders, then sort for consistent output
return sorted([o for o in opts if o not in ["core"] and not o.startswith(".")])
def setup_new_config(target_sys: str, target_dir: str, is_nixos: bool):
"""Create boilerplate directories for a missing system and open an editor."""
print(">> Creating directories...")
if is_nixos:
os.makedirs(f"nixos/{target_sys}", exist_ok=True)
os.makedirs(f"home-manager/{target_sys}", exist_ok=True)
if command_exists("yazi"):
print(">> Opening yazi. Please add your configs and exit yazi when finished.")
subprocess.run(["yazi", target_dir])
else:
print(
f">> Yazi not found. Please manually configure your files in {target_dir} in another terminal."
)
input("Press Enter when you have finished adding your configurations...")
def prompt_for_existing_config(current_sys: str, current_user: str) -> tuple[str, str]:
"""Prompt the user to select an existing configuration from the repo."""
opts = get_available_systems()
if not opts:
print(
"Error: No existing configurations found. Cannot proceed.", file=sys.stderr
)
sys.exit(1)
print("\nAvailable systems:")
for o in opts:
print(f" - {o}")
chosen_sys = input(f"\nEnter system to use [default: {current_sys}]: ").strip()
if chosen_sys:
if chosen_sys not in opts:
print(f"Error: '{chosen_sys}' is not a valid option.")
sys.exit(1)
target_sys = chosen_sys
else:
target_sys = current_sys
# Failsafe: if they press enter, but the default hostname isn't actually a valid config
if target_sys not in opts:
print(
f"Error: Default '{target_sys}' is not in available systems. You must type a valid option."
)
sys.exit(1)
chosen_user = input(
f"Enter user configuration to use [default: {current_user}]: "
).strip()
target_user = chosen_user if chosen_user else current_user
return target_sys, target_user
def resolve_configuration(
target_sys: str, target_user: str, target_dir: str, is_nixos: bool
) -> tuple[str, str]:
"""Ensure a valid configuration target exists, prompting the user for resolution if necessary."""
if has_config(target_sys, is_nixos):
return target_sys, target_user
print(
f"\n>> Configuration for system '{target_sys}' (user: '{target_user}') not found."
)
ans = (
input("Do you want to create the missing directories now? [y/N]: ")
.strip()
.lower()
)
if ans == "y":
setup_new_config(target_sys, target_dir, is_nixos)
return target_sys, target_user
ans = (
input("Do you want to switch to one of the existing configs instead? [y/N]: ")
.strip()
.lower()
)
if ans == "y":
return prompt_for_existing_config(target_sys, target_user)
print(
"Error: Cannot proceed without a valid configuration. Aborting.",
file=sys.stderr,
)
sys.exit(1)
def execute_installation(target_sys: str, target_user: str, is_nixos: bool):
"""Execute the final NixOS or Home Manager commands to build the system."""
print(
f"\n>> Proceeding with installation for user '{target_user}' on system '{target_sys}'..."
)
try:
if is_nixos:
print(
">> [NixOS detected] Performing full system and home-manager install..."
)
subprocess.run(
["sudo", "nixos-rebuild", "switch", "--flake", f".#{target_sys}"],
check=True,
)
subprocess.run(
["home-manager", "switch", "--flake", f".#{target_user}@{target_sys}"],
check=True,
)
else:
print(">> [Non-NixOS detected] Performing home-manager install only...")
subprocess.run(
["home-manager", "switch", "--flake", f".#{target_user}@{target_sys}"],
check=True,
)
print("\n>> Bootstrap complete! Welcome to your new environment.")
except subprocess.CalledProcessError as e:
print(f"\nError: Command '{' '.join(e.cmd)}' failed with exit status {e.returncode}.", file=sys.stderr)
sys.exit(e.returncode)
def main():
"""Main orchestration loop."""
check_preconditions(TARGET_DIR)
clone_repository(REPO_URL, TARGET_DIR)
is_nixos = detect_nixos()
hostname = socket.gethostname()
username = getpass.getuser()
# The returned tuple contains the final resolved system/user strings
final_sys, final_user = resolve_configuration(
hostname, username, TARGET_DIR, is_nixos
)
execute_installation(final_sys, final_user, is_nixos)
if __name__ == "__main__":
main()
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env nix-shell
#!nix-shell -i python3 -p "python3.withPackages(ps: with ps; [ typer rich ])"
import typer
import subprocess
from pathlib import Path
import mimetypes
def is_text_file(filepath):
mime_type, _ = mimetypes.guess_type(filepath)
if mime_type:
return mime_type.startswith("text/")
try:
with open(filepath, "tr") as f:
f.read(1024)
return True
except (UnicodeDecodeError, IsADirectoryError):
return False
app = typer.Typer()
@app.command()
def edit(target: Path = typer.Argument(..., help="File or directory to edit")):
if not target.exists():
subprocess.run(["nvim", str(target)])
return
if target.is_dir():
subprocess.run(["yazi", str(target)])
return
if is_text_file(str(target)):
subprocess.run(["nvim", str(target)])
else:
subprocess.run(["heh", str(target)])
if __name__ == "__main__":
app()
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bb -p babashka
(require '[babashka.cli :as cli]
'[babashka.process :refer [shell]]
'[clojure.string :as str])
(defn get-hostname []
(let [env-host (System/getenv "HOSTNAME")
etc-host (try (str/trim (slurp "/etc/hostname")) (catch Exception _ nil))
java-host (try (.getHostName (java.net.InetAddress/getLocalHost)) (catch Exception _ nil))]
(first (remove str/blank? [env-host etc-host java-host "default-system"]))))
(def cli-spec
{:spec {:home {:coerce :boolean :desc "Edit home-manager config instead of NixOS config"}
:sys {:desc "Specify system (defaults to current hostname)"} ; Removed :default from spec
:user {:default (System/getProperty "user.name")
:desc "Specify user (defaults to current user)"}
:update {:coerce :boolean :default true
:desc "Run auto-update after editing"}
:help {:alias :h :coerce :boolean :desc "Show this help message"}}})
(let [parsed (cli/parse-opts *command-line-args* cli-spec)
opts (:opts parsed)]
(when (:help opts)
(println "Usage: just packages [OPTIONS]")
(println (cli/format-opts cli-spec))
(System/exit 0))
(let [
sys (let [s (:sys opts)]
(if (str/blank? s) (get-hostname) s))
target (if (:home opts)
(str "home-manager/" sys "/packages.nix")
(str "nixos/" sys "/packages.nix"))]
(println ">> Editing" target "...")
(shell "just" "edit" target)
(if (:update opts)
(do
(println ">> Applying updates...")
(shell "just" "quick-update"))
(println ">> Skipping update (no-update provided)."))))