923 lines
25 KiB
Bash
Raw Normal View History

feat: Add complete Nushell full distribution system ## Major Features Added - **Complete distribution infrastructure**: Build, package, and distribute Nushell binary alongside plugins - **Zero-prerequisite installation**: Bootstrap installers work on fresh systems without Rust/Cargo/Nushell - **Cross-platform support**: Linux, macOS, Windows (x86_64, ARM64) - **Self-contained packages**: Everything needed for complete Nushell environment ## New Components ### Build System - `scripts/build_nushell.nu` - Build nushell with all workspace plugins - `scripts/collect_full_binaries.nu` - Collect nu binary + all plugins - `justfiles/full_distro.just` - 40+ new recipes for distribution workflows ### Bootstrap Installers (Zero Prerequisites) - `installers/bootstrap/install.sh` - Universal POSIX installer (900+ lines) - `installers/bootstrap/install.ps1` - Windows PowerShell installer (800+ lines) - Complete platform detection, PATH setup, plugin registration ### Distribution System - `scripts/create_distribution_packages.nu` - Multi-platform package creator - `scripts/install_full_nushell.nu` - Advanced nu-based installer - `scripts/verify_installation.nu` - Installation verification suite - `scripts/lib/common_lib.nu` - Shared utilities and logging ### Configuration Management - `scripts/templates/default_config.nu` - Complete nushell configuration (500+ lines) - `scripts/templates/default_env.nu` - Cross-platform environment setup - `etc/distribution_config.toml` - Central distribution settings - `scripts/templates/uninstall.sh` & `uninstall.ps1` - Clean removal ## Key Workflows ```bash just build-full # Build nushell + all plugins just pack-full-all # Create packages for all platforms just verify-full # Verify installation just release-full-cross # Complete cross-platform release ``` ## Installation Experience - One-liner: `curl -sSf https://your-url/install.sh | sh` - Multiple modes: user, system, portable installation - Automatic plugin registration with verification - Professional uninstall capability ## Benefits - ✅ Solves bootstrap problem - no prerequisites needed - ✅ Production-ready distribution system - ✅ Complete cross-platform support - ✅ Professional installation experience - ✅ Integrates seamlessly with existing plugin workflows - ✅ Enterprise-grade verification and logging
2025-09-24 18:52:07 +01:00
#!/bin/bash
# Universal Nushell + Plugins Bootstrap Installer
# POSIX compliant shell script that installs Nushell and plugins without any prerequisites
#
# This script:
# - Detects platform (Linux/macOS, x86_64/arm64)
# - Downloads or builds Nushell + plugins
# - Installs to user location (~/.local/bin) or system (/usr/local/bin)
# - Updates PATH in shell configuration files
# - Creates initial Nushell configuration
# - Registers all plugins automatically
# - Verifies installation
set -e # Exit on error
# Configuration
REPO_URL="https://github.com/your-org/nushell-plugins"
BINARY_REPO_URL="$REPO_URL/releases/download"
INSTALL_DIR_USER="$HOME/.local/bin"
INSTALL_DIR_SYSTEM="/usr/local/bin"
CONFIG_DIR="$HOME/.config/nushell"
TEMP_DIR="/tmp/nushell-install-$$"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
printf "${BLUE} %s${NC}\n" "$1"
}
log_success() {
printf "${GREEN}✅ %s${NC}\n" "$1"
}
log_warn() {
printf "${YELLOW}⚠️ %s${NC}\n" "$1"
}
log_error() {
printf "${RED}❌ %s${NC}\n" "$1" >&2
}
log_header() {
printf "\n${PURPLE}🚀 %s${NC}\n" "$1"
printf "${PURPLE}%s${NC}\n" "$(printf '=%.0s' $(seq 1 ${#1}))"
}
# Usage information
usage() {
cat << 'EOF'
Nushell + Plugins Bootstrap Installer
USAGE:
curl -L install-url/install.sh | sh
# or
./install.sh [OPTIONS]
OPTIONS:
--system Install to system directory (/usr/local/bin, requires sudo)
--user Install to user directory (~/.local/bin) [default]
--no-path Don't modify shell PATH configuration
--no-config Don't create initial nushell configuration
--no-plugins Install only nushell, skip plugins
--build-from-source Build from source instead of downloading binaries
--verify Verify installation after completion
--uninstall Remove nushell and plugins
--version VERSION Install specific version (default: latest)
--help Show this help message
EXAMPLES:
# Default installation (user directory, with plugins)
curl -L install-url/install.sh | sh
# System installation
curl -L install-url/install.sh | sh -s -- --system
# Install without plugins
curl -L install-url/install.sh | sh -s -- --no-plugins
# Build from source
curl -L install-url/install.sh | sh -s -- --build-from-source
# Install specific version
curl -L install-url/install.sh | sh -s -- --version v0.107.1
EOF
}
# Platform detection
detect_platform() {
local os arch
# Detect OS
case "$(uname -s)" in
Linux) os="linux" ;;
Darwin) os="darwin" ;;
CYGWIN*|MINGW*|MSYS*) os="windows" ;;
*) log_error "Unsupported OS: $(uname -s)"; exit 1 ;;
esac
# Detect architecture
case "$(uname -m)" in
x86_64|amd64) arch="x86_64" ;;
aarch64|arm64) arch="arm64" ;;
armv7l) arch="armv7" ;;
*) log_error "Unsupported architecture: $(uname -m)"; exit 1 ;;
esac
# Special handling for Darwin arm64
if [ "$os" = "darwin" ] && [ "$arch" = "arm64" ]; then
arch="arm64"
fi
echo "${os}-${arch}"
}
# Check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Check dependencies for building from source
check_build_dependencies() {
local missing=""
if ! command_exists "git"; then
missing="$missing git"
fi
if ! command_exists "cargo"; then
missing="$missing cargo"
fi
if ! command_exists "rustc"; then
missing="$missing rust"
fi
if [ -n "$missing" ]; then
log_error "Missing build dependencies:$missing"
log_info "Please install these tools or use binary installation instead"
return 1
fi
return 0
}
# Download file with progress
download_file() {
local url="$1"
local output="$2"
local desc="${3:-file}"
log_info "Downloading $desc..."
if command_exists "curl"; then
if ! curl -L --fail --progress-bar "$url" -o "$output"; then
log_error "Failed to download $desc from $url"
return 1
fi
elif command_exists "wget"; then
if ! wget --progress=bar:force "$url" -O "$output"; then
log_error "Failed to download $desc from $url"
return 1
fi
else
log_error "Neither curl nor wget is available"
return 1
fi
log_success "Downloaded $desc"
return 0
}
# Extract archive
extract_archive() {
local archive="$1"
local destination="$2"
log_info "Extracting archive..."
case "$archive" in
*.tar.gz)
if ! tar -xzf "$archive" -C "$destination"; then
log_error "Failed to extract $archive"
return 1
fi
;;
*.zip)
if command_exists "unzip"; then
if ! unzip -q "$archive" -d "$destination"; then
log_error "Failed to extract $archive"
return 1
fi
else
log_error "unzip command not found"
return 1
fi
;;
*)
log_error "Unsupported archive format: $archive"
return 1
;;
esac
log_success "Extracted archive"
return 0
}
# Get latest release version
get_latest_version() {
local version=""
if command_exists "curl"; then
version=$(curl -s "https://api.github.com/repos/your-org/nushell-plugins/releases/latest" | \
grep '"tag_name":' | \
sed -E 's/.*"([^"]+)".*/\1/')
elif command_exists "wget"; then
version=$(wget -qO- "https://api.github.com/repos/your-org/nushell-plugins/releases/latest" | \
grep '"tag_name":' | \
sed -E 's/.*"([^"]+)".*/\1/')
fi
if [ -z "$version" ]; then
# Fallback to a reasonable default
version="v0.107.1"
log_warn "Could not detect latest version, using $version"
fi
echo "$version"
}
# Download and install binaries
install_from_binaries() {
local platform="$1"
local version="$2"
local install_dir="$3"
local include_plugins="$4"
log_header "Installing from Pre-built Binaries"
# Create temporary directory
mkdir -p "$TEMP_DIR"
cd "$TEMP_DIR"
# Determine archive name and URL
local archive_name="nushell-plugins-${platform}-${version}.tar.gz"
local download_url="${BINARY_REPO_URL}/${version}/${archive_name}"
# Download archive
if ! download_file "$download_url" "$archive_name" "Nushell distribution"; then
log_warn "Binary download failed, trying alternative..."
# Try without version prefix
archive_name="nushell-plugins-${platform}.tar.gz"
download_url="${BINARY_REPO_URL}/latest/${archive_name}"
if ! download_file "$download_url" "$archive_name" "Nushell distribution (latest)"; then
log_error "Failed to download binaries"
return 1
fi
fi
# Extract archive
if ! extract_archive "$archive_name" "."; then
return 1
fi
# Find extracted directory
local extract_dir=""
for dir in */; do
if [ -d "$dir" ]; then
extract_dir="$dir"
break
fi
done
if [ -z "$extract_dir" ]; then
log_error "No extracted directory found"
return 1
fi
log_info "Installing binaries to $install_dir..."
# Create install directory
mkdir -p "$install_dir"
# Install nushell binary
local nu_binary="nu"
if [ "$platform" = "windows-x86_64" ]; then
nu_binary="nu.exe"
fi
if [ -f "${extract_dir}${nu_binary}" ]; then
cp "${extract_dir}${nu_binary}" "$install_dir/"
chmod +x "${install_dir}/${nu_binary}"
log_success "Installed nushell binary"
else
log_error "Nushell binary not found in archive"
return 1
fi
# Install plugins if requested
if [ "$include_plugins" = "true" ]; then
local plugin_count=0
for plugin_file in "${extract_dir}"nu_plugin_*; do
if [ -f "$plugin_file" ]; then
local plugin_name=$(basename "$plugin_file")
cp "$plugin_file" "$install_dir/"
chmod +x "${install_dir}/${plugin_name}"
plugin_count=$((plugin_count + 1))
fi
done
if [ $plugin_count -gt 0 ]; then
log_success "Installed $plugin_count plugins"
else
log_warn "No plugins found in archive"
fi
fi
# Copy configuration files if they exist
if [ -d "${extract_dir}config/" ]; then
mkdir -p "$CONFIG_DIR"
cp -r "${extract_dir}config/"* "$CONFIG_DIR/"
log_success "Installed configuration files"
fi
return 0
}
# Build and install from source
install_from_source() {
local install_dir="$1"
local include_plugins="$2"
log_header "Building from Source"
# Check dependencies
if ! check_build_dependencies; then
return 1
fi
# Create temporary directory
mkdir -p "$TEMP_DIR"
cd "$TEMP_DIR"
# Clone repository
log_info "Cloning repository..."
if ! git clone --recursive "$REPO_URL" nushell-plugins; then
log_error "Failed to clone repository"
return 1
fi
cd nushell-plugins
# Build nushell
log_info "Building nushell..."
if command_exists "just"; then
if ! just build-nushell; then
log_error "Failed to build nushell with just"
return 1
fi
else
# Fallback to manual build
cd nushell
if ! cargo build --release --features "plugin,network,sqlite,trash-support,rustls-tls"; then
log_error "Failed to build nushell"
return 1
fi
cd ..
fi
# Build plugins if requested
if [ "$include_plugins" = "true" ]; then
log_info "Building plugins..."
if command_exists "just"; then
if ! just build; then
log_warn "Failed to build some plugins"
fi
else
# Build plugins manually
for plugin_dir in nu_plugin_*; do
if [ -d "$plugin_dir" ] && [ "$plugin_dir" != "nushell" ]; then
log_info "Building $plugin_dir..."
cd "$plugin_dir"
if cargo build --release; then
log_success "Built $plugin_dir"
else
log_warn "Failed to build $plugin_dir"
fi
cd ..
fi
done
fi
fi
# Install binaries
log_info "Installing binaries to $install_dir..."
mkdir -p "$install_dir"
# Install nushell
local nu_binary="nushell/target/release/nu"
if [ -f "$nu_binary" ]; then
cp "$nu_binary" "$install_dir/"
chmod +x "${install_dir}/nu"
log_success "Installed nushell binary"
else
log_error "Nushell binary not found"
return 1
fi
# Install plugins
if [ "$include_plugins" = "true" ]; then
local plugin_count=0
for plugin_dir in nu_plugin_*; do
if [ -d "$plugin_dir" ] && [ "$plugin_dir" != "nushell" ]; then
local plugin_binary="${plugin_dir}/target/release/${plugin_dir}"
if [ -f "$plugin_binary" ]; then
cp "$plugin_binary" "$install_dir/"
chmod +x "${install_dir}/${plugin_dir}"
plugin_count=$((plugin_count + 1))
fi
done
done
if [ $plugin_count -gt 0 ]; then
log_success "Installed $plugin_count plugins"
else
log_warn "No plugins were built successfully"
fi
fi
return 0
}
# Register plugins with nushell
register_plugins() {
local install_dir="$1"
local nu_binary="${install_dir}/nu"
if [ ! -f "$nu_binary" ]; then
log_error "Nushell binary not found: $nu_binary"
return 1
fi
log_header "Registering Plugins"
# Find all plugin binaries
local plugin_count=0
for plugin_file in "${install_dir}"/nu_plugin_*; do
if [ -f "$plugin_file" ] && [ -x "$plugin_file" ]; then
local plugin_name=$(basename "$plugin_file")
log_info "Registering $plugin_name..."
if "$nu_binary" -c "plugin add '$plugin_file'"; then
log_success "Registered $plugin_name"
plugin_count=$((plugin_count + 1))
else
log_warn "Failed to register $plugin_name"
fi
fi
done
if [ $plugin_count -gt 0 ]; then
log_success "Successfully registered $plugin_count plugins"
else
log_warn "No plugins were registered"
fi
return 0
}
# Update PATH in shell configuration
update_shell_path() {
local install_dir="$1"
log_header "Updating Shell Configuration"
# List of shell configuration files to update
local shell_configs=""
# Detect current shell and add its config file first
case "$SHELL" in
*/bash) shell_configs="$HOME/.bashrc $HOME/.bash_profile" ;;
*/zsh) shell_configs="$HOME/.zshrc" ;;
*/fish) shell_configs="$HOME/.config/fish/config.fish" ;;
*/nu) shell_configs="$HOME/.config/nushell/env.nu" ;;
esac
# Add common configuration files
shell_configs="$shell_configs $HOME/.profile"
local updated=false
for config_file in $shell_configs; do
if [ -f "$config_file" ] || [ "$config_file" = "$HOME/.bashrc" ] || [ "$config_file" = "$HOME/.profile" ]; then
# Check if already in PATH
if grep -q "$install_dir" "$config_file" 2>/dev/null; then
log_info "PATH already updated in $(basename "$config_file")"
continue
fi
# Create config file if it doesn't exist
if [ ! -f "$config_file" ]; then
touch "$config_file"
fi
# Add PATH update to config file
case "$config_file" in
*.fish)
echo "fish_add_path $install_dir" >> "$config_file"
;;
*/env.nu)
echo "\$env.PATH = (\$env.PATH | split row (char esep) | append \"$install_dir\" | uniq)" >> "$config_file"
;;
*)
echo "" >> "$config_file"
echo "# Added by nushell installer" >> "$config_file"
echo "export PATH=\"$install_dir:\$PATH\"" >> "$config_file"
;;
esac
log_success "Updated $(basename "$config_file")"
updated=true
fi
done
if [ "$updated" = "true" ]; then
log_info "Please restart your terminal or run 'source ~/.bashrc' to update PATH"
else
log_warn "No shell configuration files were updated"
log_info "Please manually add $install_dir to your PATH"
fi
# Update current session PATH
export PATH="$install_dir:$PATH"
log_success "Updated PATH for current session"
}
# Create initial nushell configuration
create_nushell_config() {
log_header "Creating Nushell Configuration"
# Create config directory
mkdir -p "$CONFIG_DIR"
# Create basic config.nu if it doesn't exist
local config_file="$CONFIG_DIR/config.nu"
if [ ! -f "$config_file" ]; then
cat > "$config_file" << 'EOF'
# Nushell Configuration
# Created by nushell-plugins installer
# Set up basic configuration
$env.config = {
show_banner: false
edit_mode: emacs
shell_integration: true
table: {
mode: rounded
index_mode: always
show_empty: true
padding: { left: 1, right: 1 }
}
completions: {
case_sensitive: false
quick: true
partial: true
algorithm: "prefix"
}
history: {
max_size: 10000
sync_on_enter: true
file_format: "plaintext"
}
filesize: {
metric: false
format: "auto"
}
}
# Load custom commands and aliases
# Add your custom configuration below
EOF
log_success "Created config.nu"
else
log_info "config.nu already exists, skipping"
fi
# Create basic env.nu if it doesn't exist
local env_file="$CONFIG_DIR/env.nu"
if [ ! -f "$env_file" ]; then
cat > "$env_file" << 'EOF'
# Nushell Environment Configuration
# Created by nushell-plugins installer
# Environment variables
$env.EDITOR = "nano"
$env.BROWSER = "firefox"
# Nushell specific environment
$env.NU_LIB_DIRS = [
($nu.config-path | path dirname | path join "scripts")
]
$env.NU_PLUGIN_DIRS = [
($nu.config-path | path dirname | path join "plugins")
]
# Add your custom environment variables below
EOF
log_success "Created env.nu"
else
log_info "env.nu already exists, skipping"
fi
# Create scripts directory
local scripts_dir="$CONFIG_DIR/scripts"
mkdir -p "$scripts_dir"
# Create plugins directory
local plugins_dir="$CONFIG_DIR/plugins"
mkdir -p "$plugins_dir"
}
# Verify installation
verify_installation() {
local install_dir="$1"
local nu_binary="${install_dir}/nu"
log_header "Verifying Installation"
# Check if nushell binary exists and is executable
if [ ! -f "$nu_binary" ]; then
log_error "Nushell binary not found: $nu_binary"
return 1
fi
if [ ! -x "$nu_binary" ]; then
log_error "Nushell binary is not executable: $nu_binary"
return 1
fi
# Test nushell version
log_info "Testing nushell binary..."
local version_output
if version_output=$("$nu_binary" --version 2>&1); then
log_success "Nushell version: $version_output"
else
log_error "Failed to run nushell binary"
log_error "Output: $version_output"
return 1
fi
# Test basic nushell command
log_info "Testing basic nushell functionality..."
if "$nu_binary" -c "echo 'Hello from Nushell'" >/dev/null 2>&1; then
log_success "Basic nushell functionality works"
else
log_error "Basic nushell functionality failed"
return 1
fi
# List registered plugins
log_info "Checking registered plugins..."
local plugin_output
if plugin_output=$("$nu_binary" -c "plugin list" 2>&1); then
local plugin_count=$(echo "$plugin_output" | grep -c "nu_plugin_" || true)
if [ "$plugin_count" -gt 0 ]; then
log_success "Found $plugin_count registered plugins"
else
log_warn "No plugins are registered"
fi
else
log_warn "Could not check plugin status"
fi
# Check PATH
log_info "Checking PATH configuration..."
if command -v nu >/dev/null 2>&1; then
log_success "Nushell is available in PATH"
else
log_warn "Nushell is not in PATH. You may need to restart your terminal."
fi
log_success "Installation verification complete!"
return 0
}
# Uninstall function
uninstall_nushell() {
log_header "Uninstalling Nushell"
local removed_files=0
# Remove from user directory
if [ -d "$INSTALL_DIR_USER" ]; then
for binary in nu nu_plugin_*; do
local file_path="$INSTALL_DIR_USER/$binary"
if [ -f "$file_path" ]; then
rm -f "$file_path"
log_success "Removed $binary from $INSTALL_DIR_USER"
removed_files=$((removed_files + 1))
fi
done
fi
# Remove from system directory (if accessible)
if [ -w "$INSTALL_DIR_SYSTEM" ] 2>/dev/null; then
for binary in nu nu_plugin_*; do
local file_path="$INSTALL_DIR_SYSTEM/$binary"
if [ -f "$file_path" ]; then
rm -f "$file_path"
log_success "Removed $binary from $INSTALL_DIR_SYSTEM"
removed_files=$((removed_files + 1))
fi
done
fi
# Option to remove configuration
printf "Remove nushell configuration directory ($CONFIG_DIR)? [y/N]: "
read -r response
case "$response" in
[yY]|[yY][eE][sS])
if [ -d "$CONFIG_DIR" ]; then
rm -rf "$CONFIG_DIR"
log_success "Removed configuration directory"
fi
;;
*)
log_info "Configuration directory preserved"
;;
esac
if [ $removed_files -gt 0 ]; then
log_success "Uninstallation complete ($removed_files files removed)"
log_warn "You may need to manually remove PATH entries from your shell configuration"
else
log_warn "No nushell files found to remove"
fi
}
# Main installation function
main() {
local install_mode="user"
local modify_path="true"
local create_config="true"
local include_plugins="true"
local build_from_source="false"
local verify_install="false"
local do_uninstall="false"
local version=""
# Parse command line arguments
while [ $# -gt 0 ]; do
case "$1" in
--system)
install_mode="system"
shift
;;
--user)
install_mode="user"
shift
;;
--no-path)
modify_path="false"
shift
;;
--no-config)
create_config="false"
shift
;;
--no-plugins)
include_plugins="false"
shift
;;
--build-from-source)
build_from_source="true"
shift
;;
--verify)
verify_install="true"
shift
;;
--uninstall)
do_uninstall="true"
shift
;;
--version)
version="$2"
shift 2
;;
--help)
usage
exit 0
;;
*)
log_error "Unknown option: $1"
usage
exit 1
;;
esac
done
# Handle uninstall
if [ "$do_uninstall" = "true" ]; then
uninstall_nushell
exit 0
fi
# Show header
log_header "Nushell + Plugins Installer"
log_info "Universal bootstrap installer for Nushell and plugins"
log_info ""
# Detect platform
local platform
platform=$(detect_platform)
log_info "Detected platform: $platform"
# Determine installation directory
local install_dir
if [ "$install_mode" = "system" ]; then
install_dir="$INSTALL_DIR_SYSTEM"
if [ "$(id -u)" != "0" ] && [ ! -w "$(dirname "$install_dir")" ]; then
log_error "System installation requires root privileges"
log_info "Run with sudo or use --user for user installation"
exit 1
fi
else
install_dir="$INSTALL_DIR_USER"
fi
log_info "Installing to: $install_dir"
# Get version if not specified
if [ -z "$version" ]; then
version=$(get_latest_version)
fi
log_info "Version: $version"
# Cleanup function
cleanup() {
if [ -d "$TEMP_DIR" ]; then
rm -rf "$TEMP_DIR"
fi
}
trap cleanup EXIT
# Install based on method
if [ "$build_from_source" = "true" ]; then
if ! install_from_source "$install_dir" "$include_plugins"; then
log_error "Source installation failed"
exit 1
fi
else
if ! install_from_binaries "$platform" "$version" "$install_dir" "$include_plugins"; then
log_error "Binary installation failed"
exit 1
fi
fi
# Register plugins
if [ "$include_plugins" = "true" ]; then
register_plugins "$install_dir"
fi
# Update PATH
if [ "$modify_path" = "true" ]; then
update_shell_path "$install_dir"
fi
# Create configuration
if [ "$create_config" = "true" ]; then
create_nushell_config
fi
# Verify installation
if [ "$verify_install" = "true" ]; then
if ! verify_installation "$install_dir"; then
log_error "Installation verification failed"
exit 1
fi
fi
# Final success message
log_header "Installation Complete!"
log_success "Nushell has been successfully installed to $install_dir"
if [ "$include_plugins" = "true" ]; then
log_success "Plugins have been registered with Nushell"
fi
if [ "$modify_path" = "true" ]; then
log_info "To use Nushell, restart your terminal or run:"
log_info " source ~/.bashrc # or your shell's config file"
fi
log_info ""
log_info "Try running: nu --version"
log_info "Or start Nushell with: nu"
if [ "$include_plugins" = "true" ]; then
log_info "Check plugins with: nu -c 'plugin list'"
fi
log_info ""
log_info "For more information, visit: https://nushell.sh"
log_info ""
log_success "Happy shell scripting! 🚀"
}
# Run main function with all arguments
main "$@"