Bladeren bron

Initial commit of installer

clanker 5 dagen geleden
bovenliggende
commit
0112120c63
10 gewijzigde bestanden met toevoegingen van 1238 en 1 verwijderingen
  1. 3 1
      .gitignore
  2. 232 0
      installer/build_config.sh
  3. 212 0
      installer/compile.sh
  4. 175 0
      installer/main.sh
  5. 61 0
      installer/meson.build
  6. 75 0
      installer/pm_apt.sh
  7. 76 0
      installer/pm_base.sh
  8. 75 0
      installer/pm_dnf.sh
  9. 152 0
      installer/ui.sh
  10. 177 0
      installer/utils.sh

+ 3 - 1
.gitignore

@@ -1,2 +1,4 @@
 /build
-/build2
+/build2
+/install-builddir
+/installer/dist

+ 232 - 0
installer/build_config.sh

@@ -0,0 +1,232 @@
+#!/bin/bash
+# build_config.sh - Build and install configuration for USM and dependencies
+
+# Build order - dependencies must be built in this order
+BUILD_ORDER=("invercargill" "invercargill-json" "usm")
+
+# Component definitions
+declare -A COMPONENTS=(
+    ["invercargill"]="Invercargill|invercargill-1|https://fabrica.unitatem.net/Tilo15/Invercargill.git"
+    ["invercargill-json"]="Invercargill-Json|invercargill-json|https://git.sr.ht/~tilo15/Invercargill-Json"
+    ["usm"]="USM|usm|BUNDLED"
+)
+
+# Component properties accessor functions
+get_component_name() {
+    local key="$1"
+    echo "${COMPONENTS[$key]}" | cut -d'|' -f1
+}
+
+get_component_vapi() {
+    local key="$1"
+    echo "${COMPONENTS[$key]}" | cut -d'|' -f2
+}
+
+get_component_source() {
+    local key="$1"
+    echo "${COMPONENTS[$key]}" | cut -d'|' -f3
+}
+
+# Get output redirection based on VERBOSE flag
+get_output_redirect() {
+    if [[ "$VERBOSE" == "true" ]]; then
+        echo "/dev/stdout"
+    else
+        echo "/dev/null"
+    fi
+}
+
+# Build function for Invercargill
+build_invercargill() {
+    local src_dir="$1"
+    local prefix="$2"
+    local output_redirect=$(get_output_redirect)
+    
+    log_step "Building Invercargill..."
+    
+    pushd "$src_dir" >/dev/null || return 1
+    
+    # Configure with meson
+    if ! meson setup src builddir --prefix="$prefix" -Ddefault_library=static >"$output_redirect" 2>&1; then
+        log_error "Failed to configure Invercargill"
+        [[ "$VERBOSE" != "true" ]] && log_info "Run with --verbose for details"
+        popd >/dev/null
+        return 1
+    fi
+    
+    # Build with ninja
+    if ! ninja -C builddir >"$output_redirect" 2>&1; then
+        log_error "Failed to build Invercargill"
+        [[ "$VERBOSE" != "true" ]] && log_info "Run with --verbose for details"
+        popd >/dev/null
+        return 1
+    fi
+    
+    # Install
+    if ! ninja -C builddir install >"$output_redirect" 2>&1; then
+        log_error "Failed to install Invercargill"
+        [[ "$VERBOSE" != "true" ]] && log_info "Run with --verbose for details"
+        popd >/dev/null
+        return 1
+    fi
+    
+    popd >/dev/null
+    log_info "Invercargill built and installed successfully"
+    return 0
+}
+
+# Build function for Invercargill-Json
+build_invercargill_json() {
+    local src_dir="$1"
+    local prefix="$2"
+    local output_redirect=$(get_output_redirect)
+    
+    log_step "Building Invercargill-Json..."
+    
+    pushd "$src_dir" >/dev/null || return 1
+    
+    # Configure with meson
+    if ! meson setup src builddir --prefix="$prefix" -Ddefault_library=static >"$output_redirect" 2>&1; then
+        log_error "Failed to configure Invercargill-Json"
+        [[ "$VERBOSE" != "true" ]] && log_info "Run with --verbose for details"
+        popd >/dev/null
+        return 1
+    fi
+    
+    # Build with ninja
+    if ! ninja -C builddir >"$output_redirect" 2>&1; then
+        log_error "Failed to build Invercargill-Json"
+        [[ "$VERBOSE" != "true" ]] && log_info "Run with --verbose for details"
+        popd >/dev/null
+        return 1
+    fi
+    
+    # Install
+    if ! ninja -C builddir install >"$output_redirect" 2>&1; then
+        log_error "Failed to install Invercargill-Json"
+        [[ "$VERBOSE" != "true" ]] && log_info "Run with --verbose for details"
+        popd >/dev/null
+        return 1
+    fi
+    
+    popd >/dev/null
+    log_info "Invercargill-Json built and installed successfully"
+    return 0
+}
+
+# Build function for USM
+build_usm() {
+    local src_dir="$1"
+    local prefix="$2"
+    local output_redirect=$(get_output_redirect)
+    
+    log_step "Building USM..."
+    
+    pushd "$src_dir" >/dev/null || return 1
+    
+    # Configure with meson
+    if ! meson setup builddir --prefix="$prefix" >"$output_redirect" 2>&1; then
+        log_error "Failed to configure USM"
+        [[ "$VERBOSE" != "true" ]] && log_info "Run with --verbose for details"
+        popd >/dev/null
+        return 1
+    fi
+    
+    # Build with ninja
+    if ! ninja -C builddir >"$output_redirect" 2>&1; then
+        log_error "Failed to build USM"
+        [[ "$VERBOSE" != "true" ]] && log_info "Run with --verbose for details"
+        popd >/dev/null
+        return 1
+    fi
+    
+    # Install
+    if ! ninja -C builddir install >"$output_redirect" 2>&1; then
+        log_error "Failed to install USM"
+        [[ "$VERBOSE" != "true" ]] && log_info "Run with --verbose for details"
+        popd >/dev/null
+        return 1
+    fi
+    
+    popd >/dev/null
+    log_info "USM built and installed successfully"
+    return 0
+}
+
+# Generic build dispatcher
+build_component() {
+    local component="$1"
+    local src_dir="$2"
+    local prefix="$3"
+    
+    case "$component" in
+        "invercargill")
+            build_invercargill "$src_dir" "$prefix"
+            ;;
+        "invercargill-json")
+            build_invercargill_json "$src_dir" "$prefix"
+            ;;
+        "usm")
+            build_usm "$src_dir" "$prefix"
+            ;;
+        *)
+            log_error "Unknown component: $component"
+            return 1
+            ;;
+    esac
+}
+
+# Install shim to /usr/bin
+install_shim() {
+    local target_dir="$1"
+    local shim_path="/usr/bin/usm"
+    local sudo=""
+    
+    if ! is_root; then
+        sudo=$(get_sudo)
+    fi
+    
+    log_step "Installing shim to $shim_path..."
+    
+    local shim_content="#!/bin/bash
+exec ${target_dir}/bin/usm \"\$@\"
+"
+    
+    if [[ -n "$sudo" ]]; then
+        echo "$shim_content" | $sudo tee "$shim_path" >/dev/null
+        $sudo chmod +x "$shim_path"
+    else
+        echo "$shim_content" > "$shim_path"
+        chmod +x "$shim_path"
+    fi
+    
+    log_info "Shim installed to $shim_path"
+}
+
+# Build all components in order
+build_all() {
+    local src_base="$1"
+    local prefix="$2"
+    local total=${#BUILD_ORDER[@]}
+    local current=0
+    
+    for component in "${BUILD_ORDER[@]}"; do
+        ((current++))
+        print_progress "$current" "$total" "Building $(get_component_name "$component")..."
+        
+        local src_dir
+        
+        if [[ "$(get_component_source "$component")" == "BUNDLED" ]]; then
+            src_dir="$src_base/usm"
+        else
+            src_dir="$src_base/$(get_component_name "$component")"
+        fi
+        
+        if ! build_component "$component" "$src_dir" "$prefix"; then
+            log_error "Failed to build $(get_component_name "$component"). Installation halted."
+            return 1
+        fi
+    done
+    
+    return 0
+}

+ 212 - 0
installer/compile.sh

@@ -0,0 +1,212 @@
+#!/bin/bash
+# compile.sh - Compile installer sources into a single distributable script
+# This script is run during the build process to create the final installer
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+OUTPUT_FILE="${1:-install-usm.sh}"
+VERSION="${2:-0.1.0}"
+USM_SOURCE_DIR="${3:-$SCRIPT_DIR/../src}"
+
+# Temporary directory for building
+WORK_DIR=$(mktemp -d)
+trap "rm -rf $WORK_DIR" EXIT
+
+echo "Compiling USM installer..."
+echo "  Version: $VERSION"
+echo "  Output: $OUTPUT_FILE"
+echo "  USM Source: $USM_SOURCE_DIR"
+
+# Step 1: Clone external repositories
+echo "Cloning external repositories..."
+SOURCES_DIR="$WORK_DIR/sources"
+mkdir -p "$SOURCES_DIR"
+
+# Clone Invercargill
+if [[ ! -d "$SOURCES_DIR/Invercargill" ]]; then
+    echo "  Cloning Invercargill..."
+    git clone --depth 1 https://fabrica.unitatem.net/Tilo15/Invercargill.git "$SOURCES_DIR/Invercargill" 2>/dev/null || {
+        echo "Warning: Could not clone Invercargill, will try without --depth 1"
+        git clone https://fabrica.unitatem.net/Tilo15/Invercargill.git "$SOURCES_DIR/Invercargill" 2>/dev/null || {
+            echo "Error: Could not clone Invercargill repository"
+            exit 1
+        }
+    }
+fi
+
+# Clone Invercargill-Json
+if [[ ! -d "$SOURCES_DIR/Invercargill-Json" ]]; then
+    echo "  Cloning Invercargill-Json..."
+    git clone --depth 1 https://git.sr.ht/~tilo15/Invercargill-Json "$SOURCES_DIR/Invercargill-Json" 2>/dev/null || {
+        echo "Warning: Could not clone Invercargill-Json, will try without --depth 1"
+        git clone https://git.sr.ht/~tilo15/Invercargill-Json "$SOURCES_DIR/Invercargill-Json" 2>/dev/null || {
+            echo "Error: Could not clone Invercargill-Json repository"
+            exit 1
+        }
+    }
+fi
+
+# Copy USM source
+echo "Copying USM source..."
+mkdir -p "$SOURCES_DIR/usm"
+cp -r "$USM_SOURCE_DIR" "$SOURCES_DIR/usm/src"
+cp "$USM_SOURCE_DIR/../MANIFEST.usm" "$SOURCES_DIR/usm/" 2>/dev/null || true
+cp "$USM_SOURCE_DIR/../usm.config" "$SOURCES_DIR/usm/" 2>/dev/null || true
+
+# Step 2: Create the combined script header
+echo "Building combined script..."
+HEADER_FILE="$WORK_DIR/header.sh"
+
+cat > "$HEADER_FILE" << SCRIPT_HEADER
+#!/bin/bash
+# USM Installer - self-contained installation script
+# This script was automatically generated. Do not edit.
+# Version: $VERSION
+# Generated: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
+
+set -e
+
+SCRIPT_HEADER
+
+# Step 3: Combine all shell scripts
+# We need to rename the PM functions to avoid conflicts
+COMBINED_FILE="$WORK_DIR/combined.sh"
+
+# Function to process a bash script (minify and adapt)
+process_script() {
+    local file="$1"
+    local section="$2"
+    
+    echo "# === $section ==="
+    
+    # For PM files, we need to prefix the functions
+    case "$section" in
+        "Package Manager APT")
+            # Rename functions with apt_ prefix
+            sed -e 's/^pm_is_installed()/pm_apt_is_installed()/' \
+                -e 's/^pm_get_package_name()/pm_apt_get_package_name()/' \
+                -e 's/^pm_update()/pm_apt_update()/' \
+                -e 's/^pm_install()/pm_apt_install()/' \
+                -e 's/^get_missing_deps()/get_missing_deps_apt()/' \
+                "$file"
+            ;;
+        "Package Manager DNF")
+            # Rename functions with dnf_ prefix
+            sed -e 's/^pm_is_installed()/pm_dnf_is_installed()/' \
+                -e 's/^pm_get_package_name()/pm_dnf_get_package_name()/' \
+                -e 's/^pm_update()/pm_dnf_update()/' \
+                -e 's/^pm_install()/pm_dnf_install()/' \
+                -e 's/^get_missing_deps()/get_missing_deps_dnf()/' \
+                "$file"
+            ;;
+        *)
+            cat "$file"
+            ;;
+    esac
+}
+
+# Create the dispatcher functions
+DISPATCHER_FILE="$WORK_DIR/dispatcher.sh"
+
+cat > "$DISPATCHER_FILE" << 'DISPATCHER'
+
+# === Package Manager Dispatcher ===
+# These functions dispatch to the appropriate PM implementation
+
+pm_is_installed() {
+    case "$PM_TYPE" in
+        apt) pm_apt_is_installed "$@" ;;
+        dnf) pm_dnf_is_installed "$@" ;;
+    esac
+}
+
+pm_get_package_name() {
+    case "$PM_TYPE" in
+        apt) pm_apt_get_package_name "$@" ;;
+        dnf) pm_dnf_get_package_name "$@" ;;
+    esac
+}
+
+pm_update() {
+    case "$PM_TYPE" in
+        apt) pm_apt_update "$@" ;;
+        dnf) pm_dnf_update "$@" ;;
+    esac
+}
+
+pm_install() {
+    case "$PM_TYPE" in
+        apt) pm_apt_install "$@" ;;
+        dnf) pm_dnf_install "$@" ;;
+    esac
+}
+
+get_missing_deps() {
+    case "$PM_TYPE" in
+        apt) get_missing_deps_apt "$@" ;;
+        dnf) get_missing_deps_dnf "$@" ;;
+    esac
+}
+
+DISPATCHER
+
+# Combine scripts in order - IMPORTANT: 
+# 1. PM implementations (apt, dnf) come first with prefixed names
+# 2. Dispatcher comes next to provide the generic interface
+# 3. pm_base.sh uses the generic interface (get_missing_deps, etc.)
+# 4. main.sh and others come last
+{
+    process_script "$SCRIPT_DIR/utils.sh" "Utils"
+    echo ""
+    process_script "$SCRIPT_DIR/ui.sh" "UI"
+    echo ""
+    # PM implementations first (with prefixed function names)
+    process_script "$SCRIPT_DIR/pm_apt.sh" "Package Manager APT"
+    echo ""
+    process_script "$SCRIPT_DIR/pm_dnf.sh" "Package Manager DNF"
+    echo ""
+    # Dispatcher provides the generic interface using PM_TYPE
+    cat "$DISPATCHER_FILE"
+    echo ""
+    # Now pm_base.sh can use the generic interface
+    process_script "$SCRIPT_DIR/pm_base.sh" "Package Manager Base"
+    echo ""
+    process_script "$SCRIPT_DIR/build_config.sh" "Build Config"
+    echo ""
+    process_script "$SCRIPT_DIR/main.sh" "Main"
+    echo ""
+    # Add the main call at the very end
+    echo "# Run main function"
+    echo 'main "$@"'
+} > "$COMBINED_FILE"
+
+# Replace version placeholder in combined file
+sed -i "s/@VERSION@/$VERSION/g" "$COMBINED_FILE"
+
+# Step 4: Create the payload
+echo "Creating compressed payload..."
+PAYLOAD_FILE="$WORK_DIR/payload.tar.xz"
+
+# Use highest compression for smaller file size
+tar -C "$SOURCES_DIR" -cf - . | xz -9 -T0 > "$PAYLOAD_FILE"
+
+# Step 5: Assemble final installer
+echo "Assembling final installer..."
+{
+    cat "$HEADER_FILE"
+    cat "$COMBINED_FILE"
+    echo ""
+    echo "__PAYLOAD_START__"
+    cat "$PAYLOAD_FILE"
+} > "$OUTPUT_FILE"
+
+# Make executable
+chmod +x "$OUTPUT_FILE"
+
+# Calculate output size
+OUTPUT_SIZE=$(du -h "$OUTPUT_FILE" | cut -f1)
+echo ""
+echo "✓ Installer created successfully!"
+echo "  Output: $OUTPUT_FILE"
+echo "  Size: $OUTPUT_SIZE"

+ 175 - 0
installer/main.sh

@@ -0,0 +1,175 @@
+#!/bin/bash
+# main.sh - Main entry point for USM installer
+# This file will be the entry point of the compiled installer
+
+# Default configuration
+TARGET_DIR="/opt/usm"
+ASSUME_YES="false"
+VERBOSE="false"
+SCRIPT_VERSION="@VERSION@"
+PM_TYPE=""
+
+# Parse command line arguments
+parse_args() {
+    while [[ $# -gt 0 ]]; do
+        case "$1" in
+            --target|-t)
+                TARGET_DIR="$2"
+                shift 2
+                ;;
+            --target=*)
+                TARGET_DIR="${1#*=}"
+                shift
+                ;;
+            -y|--yes|--assume-yes)
+                ASSUME_YES="true"
+                shift
+                ;;
+            --verbose|-V)
+                VERBOSE="true"
+                shift
+                ;;
+            --help|-h)
+                show_help
+                exit 0
+                ;;
+            --version|-v)
+                echo "USM Installer v${SCRIPT_VERSION}"
+                exit 0
+                ;;
+            *)
+                echo "Unknown option: $1"
+                show_help
+                exit 1
+                ;;
+        esac
+    done
+}
+
+# Show help message
+show_help() {
+    cat << EOF
+USM Installer v${SCRIPT_VERSION}
+
+Usage: $(basename "$0") [OPTIONS]
+
+Options:
+    -t, --target DIR    Install to DIR instead of /opt/usm
+    -y, --assume-yes    Answer yes to all prompts automatically
+    -V, --verbose       Show verbose build output
+    -h, --help          Show this help message
+    -v, --version       Show version information
+
+Examples:
+    $(basename "$0")                    # Install to /opt/usm
+    $(basename "$0") -t /opt/myusm     # Install to /opt/myusm
+    $(basename "$0") -y                # Non-interactive install
+    $(basename "$0") -V                # Verbose output
+
+EOF
+}
+
+# Initialize package manager functions based on detected type
+init_package_manager() {
+    local pm="$1"
+    
+    # Set the PM type for reference
+    PM_TYPE="$pm"
+    
+    # The PM functions are already defined from the concatenated scripts
+    # We just need to verify the PM type is supported
+    case "$pm" in
+        apt|dnf)
+            # PM functions are available
+            ;;
+        *)
+            show_error_and_exit "Unsupported package manager: $pm"
+            ;;
+    esac
+}
+
+# Check for root privileges and offer to re-run with sudo
+check_root() {
+    if is_root; then
+        return 0
+    fi
+    
+    local sudo_cmd=$(get_sudo)
+    
+    if [[ -z "$sudo_cmd" ]]; then
+        show_error_and_exit "This script requires root privileges. Please run with sudo or as root."
+    fi
+    
+    log_warn "This script requires root privileges."
+    
+    if confirm "Would you like to re-run the script with $sudo_cmd?" "y"; then
+        log_info "Re-running with $sudo_cmd..."
+        exec $sudo_cmd "$0" "$@"
+    else
+        show_error_and_exit "Root privileges are required. Installation cancelled."
+    fi
+}
+
+# Main installation function
+run_installation() {
+    # Set up temporary directory
+    setup_temp_dir
+    local extract_dir="$TEMP_DIR/sources"
+    ensure_dir "$extract_dir"
+    
+    # Extract bundled sources
+    log_step "Extracting bundled sources..."
+    if ! extract_payload "$extract_dir"; then
+        show_error_and_exit "Failed to extract bundled sources"
+    fi
+    
+    # Install system dependencies
+    log_step "Checking system dependencies..."
+    if ! install_missing_deps; then
+        show_error_and_exit "Failed to install system dependencies. Installation halted."
+    fi
+    
+    # Build and install all components
+    log_step "Building and installing components..."
+    if ! build_all "$extract_dir" "$TARGET_DIR"; then
+        show_error_and_exit "Failed to build components. Installation halted."
+    fi
+    
+    # Install shim
+    install_shim "$TARGET_DIR"
+    
+    # Show completion message
+    show_completion "$TARGET_DIR"
+}
+
+# Main entry point
+main() {
+    parse_args "$@"
+    
+    # Print banner
+    print_banner
+    
+    # Check for root privileges
+    check_root
+    
+    # Detect package manager
+    local pm=$(detect_package_manager)
+    if [[ "$pm" == "unknown" ]]; then
+        show_error_and_exit "Could not detect a supported package manager (apt or dnf)"
+    fi
+    
+    # Initialize the appropriate package manager
+    init_package_manager "$pm"
+    
+    # Count missing dependencies
+    local deps_count=$(count_missing_deps)
+    
+    # Show installation summary and get confirmation
+    if ! show_installation_summary "$TARGET_DIR" "$pm" "$deps_count"; then
+        echo "Installation cancelled."
+        exit 0
+    fi
+    
+    # Run the installation
+    run_installation
+}

+ 61 - 0
installer/meson.build

@@ -0,0 +1,61 @@
+# meson.build - USM Installer build configuration
+# This is a standalone project for building the USM installer
+
+project('usm-installer', 'c',
+    version: '0.1.0',
+    default_options: ['warning_level=1']
+)
+
+installer_dir = meson.current_source_dir()
+project_root = installer_dir / '..'
+
+# Version from project
+version = meson.project_version()
+
+# Find required programs
+bash = find_program('bash', required: true)
+tar = find_program('tar', required: true)
+xz = find_program('xz', required: true)
+git = find_program('git', required: true)
+
+# Installer source files
+installer_sources = files(
+    'utils.sh',
+    'ui.sh',
+    'pm_base.sh',
+    'pm_apt.sh',
+    'pm_dnf.sh',
+    'build_config.sh',
+    'main.sh'
+)
+
+# Compile script
+compile_script = installer_dir / 'compile.sh'
+
+# Output directory and file
+output_dir = installer_dir / 'dist'
+installer_output = output_dir / 'install-usm.sh'
+
+# Ensure the output directory exists
+run_command('mkdir', '-p', output_dir, check: false)
+
+# Custom command to build the installer
+build_installer = custom_target(
+    'install-usm.sh',
+    output: 'install-usm.sh',
+    input: installer_sources,
+    command: [
+        bash, compile_script,
+        '@OUTPUT@',
+        version,
+        project_root
+    ],
+    build_by_default: true,
+    install: false,
+)
+
+# Create a custom target for distribution
+alias_target('installer', build_installer)
+
+# Message about the installer
+message('USM Installer will be built to: ' + installer_output)

+ 75 - 0
installer/pm_apt.sh

@@ -0,0 +1,75 @@
+#!/bin/bash
+# pm_apt.sh - APT package manager implementation for Debian/Ubuntu systems
+
+# Package name mappings for apt
+declare -A APT_PACKAGE_NAMES=(
+    ["vala"]="valac"
+    ["meson"]="meson"
+    ["ninja"]="ninja-build"
+    ["git"]="git"
+    ["pkgconfig"]="pkg-config"
+    ["gcc"]="gcc"
+    ["glib"]="libglib2.0-dev"
+    ["libsodium"]="libsodium-dev"
+    ["gtk"]="libgtk-3-dev"
+)
+
+# Check if a package is installed via apt/dpkg
+pm_is_installed() {
+    local pkg="$1"
+    dpkg -l "$pkg" 2>/dev/null | grep -q "^ii"
+}
+
+# Get the apt package name for a generic dependency
+pm_get_package_name() {
+    local dep="$1"
+    if [[ -n "${APT_PACKAGE_NAMES[$dep]}" ]]; then
+        echo "${APT_PACKAGE_NAMES[$dep]}"
+    else
+        echo "$dep"
+    fi
+}
+
+# Update apt cache
+pm_update() {
+    local sudo=$(get_sudo)
+    if [[ -n "$sudo" ]]; then
+        $sudo apt-get update -qq
+    else
+        apt-get update -qq
+    fi
+}
+
+# Install packages via apt
+pm_install() {
+    local -a pkgs=("$@")
+    local sudo=$(get_sudo)
+    local apt_opts="-y -qq"
+    
+    if [[ "$ASSUME_YES" == "true" ]]; then
+        apt_opts="-y -qq --allow-downgrades --allow-remove-essential --allow-change-held-packages"
+    fi
+    
+    log_step "Installing packages via apt: ${pkgs[*]}"
+    
+    if [[ -n "$sudo" ]]; then
+        DEBIAN_FRONTEND=noninteractive $sudo apt-get install $apt_opts "${pkgs[@]}"
+    else
+        DEBIAN_FRONTEND=noninteractive apt-get install $apt_opts "${pkgs[@]}"
+    fi
+}
+
+# Get list of missing dependencies (apt-specific override)
+get_missing_deps() {
+    local -a missing=()
+    local deps=("vala" "meson" "ninja" "git" "pkgconfig" "gcc" "glib" "libsodium")
+    
+    for dep in "${deps[@]}"; do
+        local pkg_name=$(pm_get_package_name "$dep")
+        if ! pm_is_installed "$pkg_name"; then
+            missing+=("$pkg_name")
+        fi
+    done
+    
+    echo "${missing[@]}"
+}

+ 76 - 0
installer/pm_base.sh

@@ -0,0 +1,76 @@
+#!/bin/bash
+# pm_base.sh - Base package manager interface for USM installer
+
+# Common dependencies required by USM and its components
+# These are package names that should be common across package managers
+declare -A COMMON_DEPS=(
+    ["vala"]="vala"
+    ["meson"]="meson"
+    ["ninja"]="ninja-build"
+    ["git"]="git"
+    ["pkgconfig"]="pkg-config"
+    ["gcc"]="gcc"
+    ["glib"]="glib2"
+)
+
+# Base function to check if a package is installed
+# Must be overridden by package manager implementation
+pm_is_installed() {
+    log_error "pm_is_installed not implemented"
+    return 1
+}
+
+# Base function to install packages
+# Must be overridden by package manager implementation
+pm_install() {
+    log_error "pm_install not implemented"
+    return 1
+}
+
+# Base function to update package cache
+# Must be overridden by package manager implementation
+pm_update() {
+    log_error "pm_update not implemented"
+    return 1
+}
+
+# Base function to get package names for dependencies
+# Must be overridden by package manager implementation
+pm_get_package_name() {
+    log_error "pm_get_package_name not implemented"
+    echo ""
+}
+
+# Install all missing dependencies
+install_missing_deps() {
+    local missing=$(get_missing_deps)
+    
+    if [[ -z "$missing" ]]; then
+        log_info "All dependencies are already installed"
+        return 0
+    fi
+    
+    log_step "Installing missing dependencies: ${missing}"
+    
+    if ! pm_update; then
+        log_warn "Failed to update package cache, continuing anyway..."
+    fi
+    
+    if ! pm_install $missing; then
+        log_error "Failed to install dependencies"
+        return 1
+    fi
+    
+    log_info "Dependencies installed successfully"
+    return 0
+}
+
+# Count missing dependencies
+count_missing_deps() {
+    local missing=$(get_missing_deps)
+    if [[ -z "$missing" ]]; then
+        echo 0
+    else
+        echo $(echo "$missing" | wc -w)
+    fi
+}

+ 75 - 0
installer/pm_dnf.sh

@@ -0,0 +1,75 @@
+#!/bin/bash
+# pm_dnf.sh - DNF package manager implementation for Fedora/RHEL systems
+
+# Package name mappings for dnf
+declare -A DNF_PACKAGE_NAMES=(
+    ["vala"]="vala"
+    ["meson"]="meson"
+    ["ninja"]="ninja-build"
+    ["git"]="git"
+    ["pkgconfig"]="pkgconf-pkg-config"
+    ["gcc"]="gcc"
+    ["glib"]="glib2-devel"
+    ["libsodium"]="libsodium-devel"
+    ["gtk"]="gtk3-devel"
+)
+
+# Check if a package is installed via dnf/rpm
+pm_is_installed() {
+    local pkg="$1"
+    rpm -q "$pkg" &>/dev/null
+}
+
+# Get the dnf package name for a generic dependency
+pm_get_package_name() {
+    local dep="$1"
+    if [[ -n "${DNF_PACKAGE_NAMES[$dep]}" ]]; then
+        echo "${DNF_PACKAGE_NAMES[$dep]}"
+    else
+        echo "$dep"
+    fi
+}
+
+# Update dnf cache
+pm_update() {
+    local sudo=$(get_sudo)
+    if [[ -n "$sudo" ]]; then
+        $sudo dnf makecache -q 2>/dev/null || $sudo dnf clean metadata
+    else
+        dnf makecache -q 2>/dev/null || dnf clean metadata
+    fi
+}
+
+# Install packages via dnf
+pm_install() {
+    local -a pkgs=("$@")
+    local sudo=$(get_sudo)
+    local dnf_opts="-y --quiet"
+    
+    if [[ "$ASSUME_YES" == "true" ]]; then
+        dnf_opts="-y --quiet --skip-broken"
+    fi
+    
+    log_step "Installing packages via dnf: ${pkgs[*]}"
+    
+    if [[ -n "$sudo" ]]; then
+        $sudo dnf install $dnf_opts "${pkgs[@]}"
+    else
+        dnf install $dnf_opts "${pkgs[@]}"
+    fi
+}
+
+# Get list of missing dependencies (dnf-specific override)
+get_missing_deps() {
+    local -a missing=()
+    local deps=("vala" "meson" "ninja" "git" "pkgconfig" "gcc" "glib" "libsodium")
+    
+    for dep in "${deps[@]}"; do
+        local pkg_name=$(pm_get_package_name "$dep")
+        if ! pm_is_installed "$pkg_name"; then
+            missing+=("$pkg_name")
+        fi
+    done
+    
+    echo "${missing[@]}"
+}

+ 152 - 0
installer/ui.sh

@@ -0,0 +1,152 @@
+#!/bin/bash
+# ui.sh - Text User Interface functions for USM installer
+
+# Print a banner/header
+print_banner() {
+    echo -e "${CYAN}${BOLD}"
+    echo "USM Installer - Universal Software Manager"
+    echo -e "Version: ${SCRIPT_VERSION}${NC}"
+    echo ""
+}
+
+# Print a section header
+print_section() {
+    local title="$1"
+    echo ""
+    echo -e "${BOLD}${BLUE}▶ $title${NC}"
+    echo ""
+}
+
+# Print a summary box
+print_summary() {
+    local -a items=("$@")
+    local max_width=0
+    
+    # Find max width
+    for item in "${items[@]}"; do
+        local len=${#item}
+        (( len > max_width )) && max_width=$len
+    done
+    
+    (( max_width < 50 )) && max_width=50
+    (( max_width > 70 )) && max_width=70
+    
+    local line=$(printf '─%.0s' $(seq 1 $((max_width + 2))))
+    
+    echo -e "${CYAN}┌${line}┐${NC}"
+    for item in "${items[@]}"; do
+        printf "${CYAN}│${NC} %-$((max_width))s ${CYAN}│${NC}\n" "$item"
+    done
+    echo -e "${CYAN}└${line}┘${NC}"
+}
+
+# Print a progress indicator
+print_progress() {
+    local current="$1"
+    local total="$2"
+    local description="$3"
+    local width=40
+    local filled=$((width * current / total))
+    local empty=$((width - filled))
+    
+    printf "\r${GREEN}["
+    printf "%$((filled))s" | tr ' ' '█'
+    printf "%$((empty))s" | tr ' ' '░'
+    printf "]${NC} ($current/$total) %s" "$description"
+    
+    if (( current == total )); then
+        echo ""
+    fi
+}
+
+# Print a list item
+print_list_item() {
+    local bullet="${1:-•}"
+    local text="$2"
+    local indent="${3:-  }"
+    echo -e "${indent}${CYAN}${bullet}${NC} ${text}"
+}
+
+# Print a status line
+print_status() {
+    local label="$1"
+    local value="$2"
+    local status="${3:-info}"
+    local status_icon
+    
+    case "$status" in
+        ok)    status_icon="${GREEN}✓${NC}" ;;
+        warn)  status_icon="${YELLOW}!${NC}" ;;
+        error) status_icon="${RED}✗${NC}" ;;
+        *)     status_icon="${BLUE}i${NC}" ;;
+    esac
+    
+    printf "  ${status_icon} %-25s %s\n" "$label" "$value"
+}
+
+# Show installation summary and get confirmation
+show_installation_summary() {
+    local target_dir="$1"
+    local pm="$2"
+    local deps_count="$3"
+    
+    print_section "Installation Summary"
+    
+    local -a summary_items=(
+        "Target Directory: ${target_dir}"
+        "Package Manager: ${pm}"
+        "Dependencies to install: ${deps_count}"
+        "Components:"
+    )
+    
+    print_summary "${summary_items[@]}"
+    
+    echo ""
+    print_list_item "1" "Invercargill (invercargill-1) - Core library"
+    print_list_item "2" "Invercargill-Json - JSON serialization"
+    print_list_item "3" "USM - Universal Software Manager"
+    
+    echo ""
+    echo -e "${YELLOW}This script will:${NC}"
+    print_list_item "•" "Install required system packages via ${pm}"
+    print_list_item "•" "Build and install Invercargill libraries"
+    print_list_item "•" "Build and install USM to ${target_dir}"
+    print_list_item "•" "Create a shim at /usr/bin/usm"
+    
+    echo ""
+    if ! confirm "Do you want to continue with the installation?" "n"; then
+        return 1
+    fi
+    
+    return 0
+}
+
+# Show completion message
+show_completion() {
+    local target_dir="$1"
+    
+    echo ""
+    echo -e "${GREEN}✓ Installation Complete!${NC}"
+    echo ""
+    echo -e "  ${CYAN}Binary:${NC}     ${target_dir}/bin/usm"
+    echo -e "  ${CYAN}Shim:${NC}       /usr/bin/usm"
+    echo ""
+    echo -e "Run ${BOLD}usm --help${NC} to get started."
+    echo ""
+}
+
+# Show error and exit
+show_error_and_exit() {
+    local message="$1"
+    local code="${2:-1}"
+    
+    echo ""
+    echo -e "${RED}╔══════════════════════════════════════════════════════════════╗${NC}"
+    echo -e "${RED}║ Installation Failed                                          ║${NC}"
+    echo -e "${RED}╚══════════════════════════════════════════════════════════════╝${NC}"
+    echo ""
+    echo -e "${RED}Error:${NC} $message"
+    echo ""
+    
+    exit "$code"
+}

+ 177 - 0
installer/utils.sh

@@ -0,0 +1,177 @@
+#!/bin/bash
+# utils.sh - Common utility functions for USM installer
+
+# Colors for terminal output
+readonly RED='\033[0;31m'
+readonly GREEN='\033[0;32m'
+readonly YELLOW='\033[1;33m'
+readonly BLUE='\033[0;34m'
+readonly CYAN='\033[0;36m'
+readonly BOLD='\033[1m'
+readonly NC='\033[0m' # No Color
+
+# Logging functions
+log_info() {
+    echo -e "${GREEN}[INFO]${NC} $1"
+}
+
+log_warn() {
+    echo -e "${YELLOW}[WARN]${NC} $1"
+}
+
+log_error() {
+    echo -e "${RED}[ERROR]${NC} $1"
+}
+
+log_step() {
+    echo -e "${CYAN}==>${NC} ${BOLD}$1${NC}"
+}
+
+# Check if running as root
+is_root() {
+    [[ $EUID -eq 0 ]]
+}
+
+# Check if a command exists
+command_exists() {
+    command -v "$1" &>/dev/null
+}
+
+# Get the system's package manager
+detect_package_manager() {
+    if command_exists apt-get; then
+        echo "apt"
+    elif command_exists dnf; then
+        echo "dnf"
+    elif command_exists yum; then
+        echo "dnf"  # Fallback to dnf implementation
+    elif command_exists pacman; then
+        echo "pacman"
+    else
+        echo "unknown"
+    fi
+}
+
+# Get the system's sudo command
+get_sudo() {
+    if is_root; then
+        echo ""
+    elif command_exists sudo; then
+        echo "sudo"
+    elif command_exists doas; then
+        echo "doas"
+    else
+        echo ""
+    fi
+}
+
+# Confirm action with user
+confirm() {
+    local prompt="$1"
+    local default="${2:-n}"
+    local response
+    
+    if [[ "$ASSUME_YES" == "true" ]]; then
+        return 0
+    fi
+    
+    if [[ "$default" == "y" ]]; then
+        prompt="$prompt [Y/n]: "
+    else
+        prompt="$prompt [y/N]: "
+    fi
+    
+    read -r -p "$prompt" response
+    response="${response:-$default}"
+    
+    [[ "$response" =~ ^[Yy]$ ]]
+}
+
+# Create directory if it doesn't exist
+ensure_dir() {
+    local dir="$1"
+    if [[ ! -d "$dir" ]]; then
+        mkdir -p "$dir"
+    fi
+}
+
+# Extract XZ compressed data from this script
+extract_payload() {
+    local output_dir="$1"
+    local script_path="$0"
+    local payload_start
+    
+    # Find the payload marker
+    payload_start=$(grep -an "^__PAYLOAD_START__$" "$script_path" | cut -d: -f1)
+    
+    if [[ -z "$payload_start" ]]; then
+        log_error "Could not find payload in installer"
+        return 1
+    fi
+    
+    # Extract and decompress the payload
+    tail -n +$((payload_start + 1)) "$script_path" | xz -d | tar -xf - -C "$output_dir"
+}
+
+# Cleanup function for traps
+cleanup() {
+    if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then
+        rm -rf "$TEMP_DIR"
+    fi
+}
+
+# Set up temporary directory
+setup_temp_dir() {
+    TEMP_DIR=$(mktemp -d)
+    trap cleanup EXIT
+}
+
+# Download a file
+download_file() {
+    local url="$1"
+    local output="$2"
+    
+    if command_exists curl; then
+        curl -fsSL "$url" -o "$output"
+    elif command_exists wget; then
+        wget -q "$url" -O "$output"
+    else
+        log_error "Neither curl nor wget is available"
+        return 1
+    fi
+}
+
+# Clone a git repository
+clone_repo() {
+    local url="$1"
+    local dest="$2"
+    local branch="${3:-main}"
+    
+    if ! command_exists git; then
+        log_error "git is not installed"
+        return 1
+    fi
+    
+    git clone --depth 1 -b "$branch" "$url" "$dest" 2>/dev/null
+}
+
+# Get total size of a directory
+get_dir_size() {
+    local dir="$1"
+    du -sb "$dir" 2>/dev/null | cut -f1
+}
+
+# Format bytes to human readable
+format_size() {
+    local bytes=$1
+    local units=('B' 'KB' 'MB' 'GB' 'TB')
+    local unit=0
+    local size=$bytes
+    
+    while (( size > 1024 && unit < 4 )); do
+        size=$((size / 1024))
+        ((unit++))
+    done
+    
+    echo "$size${units[$unit]}"
+}