diff --git a/documentation/guile_scripting_solution.md b/documentation/guile_scripting_solution.md deleted file mode 100644 index 87538ef..0000000 --- a/documentation/guile_scripting_solution.md +++ /dev/null @@ -1,333 +0,0 @@ -# Replacing Bash with Guile Scheme for Home Lab Tools - -This document outlines a proposal to migrate the `home-lab-tools` script from Bash to GNU Guile Scheme. This change aims to address the increasing complexity of the script and leverage the benefits of a more powerful programming language. - -## 1. Introduction: Why Guile Scheme? - -GNU Guile is the official extension language for the GNU Project. It is an implementation of the Scheme programming language, a dialect of Lisp. Using Guile for scripting offers several advantages over Bash, especially as scripts grow in size and complexity. - -Key reasons for considering Guile: - -* **Expressiveness and Power:** Scheme is a full-fledged programming language with features like first-class functions, macros, and a rich standard library. This allows for more elegant and maintainable solutions to complex problems. -* **Better Error Handling:** Guile provides robust error handling mechanisms (conditions and handlers) that are more sophisticated than Bash's `set -e` and trap. -* **Modularity:** Guile supports modules, making it easier to organize code into reusable components. -* **Data Manipulation:** Scheme excels at handling structured data, which can be beneficial for managing configurations or parsing output from commands. -* **Readability (for Lisp programmers):** While Lisp syntax can be initially unfamiliar, it can lead to very clear and concise code once learned. -* **Interoperability:** Guile can easily call external programs and libraries, and can be extended with C code if needed. - -## 2. Advantages over Bash for `home-lab-tools` - -Migrating `home-lab-tools` from Bash to Guile offers specific benefits: - -* **Improved Logic Handling:** Complex conditional logic, loops, and function definitions are more naturally expressed in Guile. The current Bash script uses case statements and string comparisons extensively, which can become unwieldy. -* **Structured Data Management:** Machine definitions, deployment modes, and status information could be represented as Scheme data structures (lists, association lists, records), making them easier to manage and query. -* **Enhanced Error Reporting:** More descriptive error messages and better control over script termination in case of failures. -* **Code Reusability:** Functions for common tasks (e.g., SSHing to a machine, running `nixos-rebuild`) can be more cleanly defined and reused. -* **Easier Testing:** Guile's nature as a programming language makes it more amenable to unit testing individual functions or modules. -* **Future Extensibility:** Adding new commands, machines, or features will be simpler and less error-prone in a more structured language. - -## 3. Setting up Guile - -Guile is often available through system package managers. On NixOS, it can be added to your environment or system configuration. - -```nix -# Example: Adding Guile to a Nix shell -nix-shell -p guile -``` - -A Guile script typically starts with a shebang line: - -```scheme -#!/usr/bin/env guile -!# -``` - -The `!#` at the end is a Guile-specific convention that allows the script to be both executable and loadable into a Guile REPL. - -## 4. Basic Guile Scripting Concepts - -* **S-expressions:** Code is written using S-expressions (Symbolic Expressions), which are lists enclosed in parentheses, e.g., `(function arg1 arg2)`. -* **Definitions:** `(define variable value)` and `(define (function-name arg1 arg2) ...body...)`. -* **Procedures (Functions):** Core of Guile programming. -* **Control Flow:** `(if condition then-expr else-expr)`, `(cond (test1 expr1) (test2 expr2) ... (else else-expr))`, `(case ...)` -* **Modules:** `(use-modules (ice-9 popen))` for using libraries. - -## 5. Interacting with the System - -Guile provides modules for system interaction: - -* **(ice-9 popen):** For running external commands and capturing their output (similar to backticks or `$(...)` in Bash). - * `open-pipe* command mode`: Opens a pipe to a command. - * `get-string-all port`: Reads all output from a port. -* **(ice-9 rdelim):** For reading lines from ports. -* **(ice-9 filesys):** For file system operations (checking existence, deleting, etc.). - * `file-exists? path` - * `delete-file path` -* **(srfi srfi-1):** List processing utilities. -* **(srfi srfi-26):** `cut` for partial application, useful for creating specialized functions. -* **Environment Variables:** `(getenv "VAR_NAME")`, `(setenv "VAR_NAME" "value")`. - -**Example: Running a command** - -```scheme -(use-modules (ice-9 popen)) - -(define (run-command . args) - (let* ((cmd (string-join args " ")) - (port (open-pipe* cmd OPEN_READ))) - (let ((output (get-string-all port))) - (close-pipe port) - output))) - -(display (run-command "echo" "Hello from Guile")) -(newline) -``` - -## 6. Error Handling - -Guile uses a condition system for error handling. - -* `catch`: Allows you to catch specific types of errors. -* `throw`: Raises an error. - -```scheme -(use-modules (ice-9 exceptions)) - -(catch #t - (lambda () - (display "Trying something that might fail... -") - ;; Example: Force an error - (if #t (error "Something went wrong!")) - (display "This won't be printed if an error occurs above. -")) - (lambda (key . args) - (format (current-error-port) "Caught an error: ~a - Args: ~a -" key args) - #f)) ; Return value indicating an error was caught -``` - -For `home-lab-tools`, this means we can provide more specific feedback when a deployment fails or a machine is unreachable. - -## 7. Modularity and Code Organization - -Guile's module system allows splitting the code into logical units. For `home-lab-tools`, we could have modules for: - -* `lab-config`: Machine definitions, paths. -* `lab-deploy`: Functions related to deploying configurations. -* `lab-ssh`: SSH interaction utilities. -* `lab-status`: Functions for checking machine status. -* `lab-utils`: General helper functions, logging. - -**Example module structure:** - -```scheme -;; file: lab-utils.scm -(define-module (lab utils) - #:export (log success warn error)) - -(define blue "") -(define nc "") - -(define (log msg) - (format #t "~a[lab]~a ~a -" blue nc msg)) -;; ... other logging functions -``` - -```scheme -;; file: main-lab-script.scm -#!/usr/bin/env guile -!# -(use-modules (lab utils) (ice-9 popen)) - -(log "Starting lab script...") -;; ... rest of the script -``` - -## 8. Example: Rewriting a Small Part of `home-lab-tools.nix` (Conceptual) - -Let's consider the `log` function and a simplified `deploy_machine` for local deployment. - -**Current Bash:** - -```bash -BLUE='' -NC='' # No Color - -log() { - echo -e "''${BLUE}[lab]''${NC} $1" -} - -deploy_machine() { - local machine="$1" - # ... - if [[ "$machine" == "congenital-optimist" ]]; then - log "Deploying $machine (mode: $mode) locally" - sudo nixos-rebuild $mode --flake "$HOMELAB_ROOT#$machine" - fi - # ... -} -``` - -**Conceptual Guile Scheme:** - -```scheme -;; main-lab-script.scm -#!/usr/bin/env guile -!# - -(use-modules (ice-9 popen) - (ice-9 rdelim) - (ice-9 pretty-print) - (ice-9 exceptions) - (srfi srfi-1)) ;; For list utilities like `string-join` - -;; Configuration (could be in a separate module) -(define homelab-root "/home/geir/Home-lab") - -;; Color Definitions -(define RED "") -(define GREEN "") -(define YELLOW "") -(define BLUE "") -(define NC "") - -;; Logging functions -(define (log level-color level-name message) - (format #t "~a[~a]~a ~a -" level-color level-name NC message)) - -(define (info . messages) - (log BLUE "lab" (apply string-append (map (lambda (m) (if (string? m) m (format #f "~s" m))) messages)))) - -(define (success . messages) - (log GREEN "lab" (apply string-append (map (lambda (m) (if (string? m) m (format #f "~s" m))) messages)))) - -(define (warn . messages) - (log YELLOW "lab" (apply string-append (map (lambda (m) (if (string? m) m (format #f "~s" m))) messages)))) - -(define (err . messages) - (log RED "lab" (apply string-append (map (lambda (m) (if (string? m) m (format #f "~s" m))) messages))) - (exit 1)) ;; Exit on error - -;; Function to run shell commands and handle output/errors -(define (run-shell-command . command-parts) - (let ((command-string (string-join command-parts " "))) - (info "Executing: " command-string) - (let ((pipe (open-pipe* command-string OPEN_READ))) - (let loop ((lines '())) - (let ((line (read-line pipe))) - (if (eof-object? line) - (begin - (close-pipe pipe) - (reverse lines)) ;; Return lines in order - (begin - (display line) (newline) ;; Display live output - (loop (cons line lines))))))) - ;; TODO: Add proper error checking based on exit status of the command - ;; For now, we assume success if open-pipe* doesn't fail. - ;; A more robust solution would check `close-pipe` status or use `system*`. - )) - -;; Simplified deploy_machine -(define (deploy-machine machine mode) - (info "Deploying " machine " (mode: " mode ")") - (cond - ((string=? machine "congenital-optimist") - (info "Deploying " machine " locally") - (catch #t - (lambda () - (run-shell-command "sudo" "nixos-rebuild" mode "--flake" (string-append homelab-root "#" machine)) - (success "Successfully deployed " machine)) - (lambda (key . args) - (err "Failed to deploy " machine ". Error: " key " Args: " args)))) - ;; Add other machines here - (else - (err "Unknown machine: " machine)))) - -;; Main script logic (parsing arguments, calling functions) -(define (main args) - (if (< (length args) 3) - (begin - (err "Usage: