nushell-plugins/scripts/create_distribution_packages.nu
Jesús Pérez 2f6089caaf # Summary
feat: bootstrap installer with robust archive extraction, version mismatch handling, and improved PATH messaging

## Detailed Description

This commit implements a production-ready bootstrap installer with comprehensive error handling, version-agnostic archive extraction, and clear user messaging. All improvements follow DRY principles using symlink-based architecture for single-source-of-truth maintenance.

## Major Changes (Session 2025-10-19)

### 0. Bootstrap Installer & Distribution Improvements (NEW)

#### 0a. Install Script Architecture - DRY Design

**Issue**: Code duplication across installation paths
- `./install.sh`, `./scripts/templates/install.sh`, `installers/bootstrap/install.sh` were separate copies
- Changes required updating multiple files
- Risk of divergence and inconsistency

**Solution**: Implemented symlink-based DRY architecture
- Single source of truth: `installers/bootstrap/install.sh` (1,247 lines)
- Symlinks created:
  - `./install.sh` → `installers/bootstrap/install.sh`
  - `./scripts/templates/install.sh` → `installers/bootstrap/install.sh`
- All changes automatically propagate through symlinks
- No code duplication

**Impact**:
-  Single maintenance point for all install scripts
-  Consistent behavior across all paths
-  Reduced maintenance burden
-  No divergence risk

#### 0b. Archive Extraction - Version-Agnostic Binary Detection

**Issue**: "No Nushell binary found in extracted archive"
- Script failed when extracting from `nushell-full-0.108.0-darwin-arm64.tar.gz`
- Error occurred even though binaries were present at `nushell-full-0.108.0/bin/nu`

**Root Cause**: `find` command returning parent directory itself in results
```bash
#  OLD - find returns parent directory first
local extracted=$(find "$extract_dir" -maxdepth 1 -type d -name "nushell-*" | head -1)
# Returns: /tmp/nushell-install-22919/nushell-extract (parent!)
# Should return: /tmp/nushell-install-22919/nushell-extract/nushell-full-0.108.0 (subdirectory)
```

**Solution**: Added `-not -path` to exclude starting directory
```bash
#  NEW - exclude parent directory
local extracted=$(find "$extract_dir" -maxdepth 1 -type d -name "nushell-*" -not -path "$extract_dir" | head -1)
# Now correctly returns the subdirectory
```

**Additional Improvements**:
- 4-level fallback detection strategy:
  1. Check `$extracted/bin/nu` (subdirectory structure)
  2. Check `$extracted/nu` (flat structure)
  3. Fallback search for any `nushell-*` subdirectory with `bin/nu`
  4. Last resort recursive search for any executable named `nu`
- Validates binaries exist before using them
- Clear error reporting with all search locations

**Version-Agnostic**:
- Uses `nushell-*` pattern (not hardcoded version numbers)
- Works with any Nushell version: 0.107, 0.108, 0.109, etc.
- Supports both `.tar.gz` and `.zip` archive formats

**Impact**:
-  Archive extraction works reliably
-  Works with any Nushell version
-  Clear error messages guide users
-  Multiple archive structure support

#### 0c. Plugin Registration - Version Mismatch Handling

**Issue**: Plugin registration failed with version incompatibility errors
```
Error: nu:🐚:plugin_failed_to_load
× Plugin `polars` is compiled for nushell version 0.107.1, which is not compatible with version 0.108.0
```

**Solution**: Implemented intelligent error classification
- Captures both stdout and stderr from plugin add commands
- Detects version incompatibility: "is not compatible with version" or "is compiled for nushell version"
- Classifies errors into three categories:
  1. **Success**: Plugin registered successfully 
  2. **Incompatible**: Version mismatch (skipped gracefully) ⚠️
  3. **Failed**: Other registration failures 
- Reports summary with counts of each category
- Installation continues successfully even with version mismatches

**New Error Reporting**:
```
 Registered nu_plugin_auth
⚠️  Skipping nu_plugin_polars: Version mismatch (built for different Nushell version)
 Successfully registered 13 plugins
⚠️  Skipped 1 incompatible plugins (version mismatch):
  - nu_plugin_polars
```

**Impact**:
-  No installation failures due to version mismatches
-  Users informed of incompatible plugins
-  Clear distinction between error types
-  Installation completes successfully

#### 0d. Shell Configuration PATH Update - Clear Messaging

**Issue**: Confusing PATH update messages
- User sees: "PATH already updated" for all files
- Then sees: "No shell configuration files were updated" warning
- Then sees: "Please manually add to your PATH" error
- **Problem**: Contradictory messages when PATH is already configured everywhere

**Root Cause**: Script conflated two states
- State 1: "Was PATH found in files?" (skips updating if found)
- State 2: "Did we add PATH to any file?" (used for messaging)
- Both states ignored means no update was made, but PATH might already exist

**Solution**: Track two separate states
```bash
local updated=false        # Was PATH ADDED to any file?
local path_found=false     # Was PATH FOUND in any file?

# In loop:
if grep -q "$install_dir" "$config_file"; then
    path_found=true        # Found it! Mark as true
    continue
fi

# After loop:
if [ "$updated" = "true" ]; then
    log_success "Shell configuration updated"
elif [ "$path_found" = "true" ]; then
    log_success "PATH is already configured in your shell configuration files"
else
    log_warn "Could not find or update shell configuration"
fi
```

**New Clear Messages**:
-  "PATH is already configured in your shell configuration files" (when found everywhere)
-  "Shell configuration updated" (when just added)
- ⚠️ "Could not find or update shell configuration" (when file missing)

**Impact**:
-  Non-contradictory messages
-  Users understand what happened
-  No false warnings when PATH already configured
-  Clear guidance when manual action needed

#### 0e. Installation Features

**`--source-path` Option** (Local Installation):
- Install from local archive: `./install.sh --source-path archive.tar.gz`
- Install from local directory: `./install.sh --source-path /path/to/binaries`
- Default behavior: `./install.sh --source-path` uses `./bin_archives`
- Works with custom `--install-dir` paths
- No download needed, offline installation support

**`--uninstall` with Configuration Management**:
- Prompts user: "Remove ~/.config/nushell? [y/N]"
- Removes all installed binaries and plugins
- Preserves user choice for configuration
- Clean uninstall before fresh reinstall

#### 0f. Documentation Updates

**CLAUDE.md**:
- Added "Install Script Architecture (DRY Design)" section
- Documents source of truth and symlink structure
- Explains `--source-path` feature
- Shows version-agnostic archive detection
- Lists DRY architecture benefits

**README.md**:
- Added "Install Script Architecture (DRY Design)" subsection
- Shows symlink structure with arrows
- Provides `--source-path` usage examples
- Explains version-agnostic detection
- "How DRY Works" 3-step explanation

#### 0g. Files Modified

Core Changes:
- `installers/bootstrap/install.sh` (+3 lines for PATH messaging fix)
  - Archive extraction fix with `-not -path`
  - Plugin registration error classification
  - Clear PATH update messaging
  - Total: 1,247 lines (unified)

Auto-Updated via Symlinks:
- `./install.sh` - Auto-updated (1,247 lines)
- `./scripts/templates/install.sh` - Auto-updated (1,247 lines)

Documentation:
- `CLAUDE.md` - Added install architecture section
- `README.md` - Added install architecture subsection
- `CHANGELOG.md` - Added comprehensive entry (+100 lines)

#### 0h. Testing & Verification

All scenarios tested and verified:
- [x] Archive extraction works with version-agnostic detection
- [x] Installation to `~/.local` successful (16 binaries)
- [x] Installation to `~/.local/bin` successful (21 plugins loaded)
- [x] Plugin registration handles version mismatches gracefully
- [x] PATH messaging is clear and non-contradictory
- [x] Clean uninstall followed by fresh reinstall works perfectly

#### 0i. Impact

User-Facing Benefits:
-  Users can install from any version of nushell-full archive
-  Version mismatch plugins skipped without breaking installation
-  Clear, honest error messages
-  Non-confusing PATH update messages
-  Offline installation support via `--source-path`
-  Clean uninstall/reinstall workflow

Developer Benefits:
-  Single source of truth eliminates code duplication
-  Changes propagate automatically through symlinks
-  Reduced maintenance burden
-  Consistent behavior across all paths
-  Production-ready installation process

---

### 1. Help System Integration (New Feature)

**Issue**: Version-update module recipes were not discoverable
- Not shown in `just help modules`
- Not referenced in `just help`
- Not included in help navigation system
- Users had to manually run `just --list` to find update commands

**Solution**:
- Added version-update module to all help outputs
- Updated `justfiles/help.just` to document all 30+ version-update recipes
- Created new `just commands` recipe as discoverable alias for `just --list`
- Integrated version-update into help-all workflow

**Impact**:
- Version-update commands now fully discoverable via help system
- Users can find update commands with: `just help modules`, `just help`, `just commands`
- Improved overall help system navigation

**Files Modified**:
- `justfiles/help.just` (+23 lines)
  - Added version-update module to help sections
  - Added to modules list
  - Added to help-all workflow
  - New `commands` recipe showing all recipes by group

### 2. Build Process Fixes (Phase 3: Bin Archives)

#### 2a. Plugin Archive Collection Bug

**Issue**: "No plugins found to package" warning in Phase 3
- Collected 26 plugin binaries but reported 0
- Archive creation skipped because count was wrong

**Root Cause**: `each` command returns null, so `| length` returned 0
```nushell
#  OLD - each returns null
let plugin_count = (ls nu_plugin_*/target/release/nu_plugin_* | each {|p|
    cp $p.name $"($temp_dir)/"
} | length)  # Returns 0!
```

**Solution**: Separated counting from copying with proper filtering
```nushell
#  NEW - count before operations
let plugins_to_copy = (ls nu_plugin_*/target/release/nu_plugin_* | where type == "file")
let plugin_count = ($plugins_to_copy | length)
```

**Impact**:
- Now correctly collects and reports 26 plugins
- Filters out .d dependency files automatically
- Warning eliminated

#### 2b. Tar Archive Path Handling

**Issue**: Tar command failing silently with relative paths in subshell
- `cd $temp_dir` changes context unpredictably
- Relative path `../$archive_name` fails in subshell
- Archive file not created despite exit code 0

**Root Cause**: Shell context and relative path issues in Nushell `do` block

**Solution**: Used `tar -C` with absolute paths instead of `cd`
```nushell
#  OLD - unreliable context switching
do {
    cd $temp_dir
    tar -czf ../$archive_name .
}

#  NEW - absolute paths, no context switching
tar -C $temp_dir -czf $archive_path .
```

**Additional Improvements**:
- Absolute path construction using `pwd | path join`
- Better error diagnostics with exit code and stderr output
- File verification after creation

**Impact**:
- Tar archives now created successfully
- Robust path handling across platforms
- Clear error messages for debugging

#### 2c. File Size Calculation Type Error

**Issue**: Runtime error when calculating archive size
```
Error: The '/' operator does not work on values of type 'list<filesize>'
```

**Root Cause**: `ls` returns list of records, so `.size` was a list
```nushell
#  OLD - returns list<filesize>
(ls $archive_path).size / 1024 / 1024

#  NEW - returns filesize
(ls $archive_path | get 0.size) / 1024 / 1024
```

**Impact**:
- Proper file size calculation in MB
- No more type errors

**Files Modified**:
- `scripts/create_full_distribution.nu` (+58 lines, refactored plugin collection)
  - Fixed plugin counting logic
  - Improved path handling with absolute paths
  - Enhanced error diagnostics

### 3. Plugin Rebuild Optimization

**Issue**: All plugins marked for rebuild even when dependencies unchanged
- Step 4 (`update_all_plugins.nu`) touched all Cargo.toml files at 01:00:32
- Step 5 saw all files as "newer" than binaries
- Marked ALL plugins for rebuild, though cargo only rebuilt changed ones

**Root Cause**: Script always saved files, even when no changes made
```nushell
#  OLD - always saves, touching file timestamp
$updated_content | to toml | save -f $cargo_toml
```

**Solution**: Only save if content actually changed
```nushell
#  NEW - compare before writing
let original_toml = $content | to toml
let new_toml = $updated_content | to toml

if $original_toml != $new_toml {
    $updated_content | to toml | save -f $cargo_toml
}
```

**Impact**:
- Unchanged files preserve original timestamps
- Only plugins with actual dependency changes are rebuilt
- Efficient rebuild process with accurate file modification detection

**Files Modified**:
- `scripts/update_all_plugins.nu` (+12 lines, added content comparison)
  - Only touches files with real changes
  - Preserves timestamps for efficiency
  - Clearer logic and comments

### 4. Documentation

**Files Modified**:
- `CHANGELOG.md` (+56 lines)
  - Added comprehensive 2025-10-19 entry
  - Documented all fixes with root causes
  - Listed files modified and impact summary

## Technical Details

### Nushell Patterns Used

1. **Proper List Handling**:
   - `ls` returns list of records, access with `| get 0.size`
   - Filter with `where type == "file"` to exclude metadata

2. **Absolute Path Construction**:
   - `pwd | append "path" | path join` for cross-platform paths
   - Safer than string concatenation with `/`

3. **Content Comparison**:
   - Compare TOML string representation before saving
   - Preserves file timestamps for efficiency

4. **Error Diagnostics**:
   - Capture `stderr` from commands
   - Report exit codes and error messages separately

## Testing

- [x] Help system shows version-update module
- [x] `just commands` displays all recipes by group
- [x] Phase 3 bin archive creation works
- [x] Plugin collection reports correct count (26)
- [x] Tar archives created successfully
- [x] File size calculated correctly
- [x] Plugin rebuild only touches changed files
- [x] CHANGELOG updated with all changes

## Files Changed

```
38 files changed, 2721 insertions(+), 2548 deletions(-)

Core Changes:
- justfiles/help.just                  (+23)  Help system integration
- scripts/create_full_distribution.nu  (+58)  Build process fixes
- scripts/update_all_plugins.nu        (+12)  Rebuild optimization
- CHANGELOG.md                         (+56)  Documentation

Dependency Updates:
- All plugin Cargo.toml and Cargo.lock files (version consistency)
```

## Breaking Changes

None. These are bug fixes and optimizations that maintain backward compatibility.

## Migration Notes

No migration needed. Improvements are transparent to users.

## Related Issues

- Help system discoverability
- Build process Phase 3 failures
- Unnecessary plugin rebuilds
- Build process reliability

## Checklist

- [x] Changes follow Rust/Nushell idioms
- [x] Code is well-commented
- [x] Error handling is comprehensive
- [x] Documentation is updated
- [x] All changes tested
- [x] No breaking changes introduced
2025-10-19 02:39:31 +01:00

605 lines
20 KiB
Plaintext
Executable File

#!/usr/bin/env nu
# Create Distribution Packages Script
# Creates platform-specific distribution packages with nushell binary, plugins, and installers
use lib/common_lib.nu [
log_info, log_error, log_success, log_warn, log_debug,
validate_nushell_version, get_current_platform, get_supported_platforms,
get_binary_extension, get_archive_extension, ensure_dir, remove_dir,
copy_file, create_checksums, get_workspace_version, create_manifest
]
def main [
--platform (-p): string = "" # Target platform (e.g., linux-x86_64, darwin-arm64)
--output (-o): string = "bin_archives" # Output directory for packages
--force (-f) # Force overwrite existing packages
--list (-l) # List what would be packaged
--all-platforms # Create packages for all platforms
--checksums # Generate checksums for packages
--bootstrap # Include bootstrap installers
--include-docs # Include documentation
--version (-v): string = "" # Version override
] {
# Validate nushell version consistency first
validate_nushell_version
# Get version
let version = if ($version | str length) > 0 {
$version
} else {
get_workspace_version
}
if $list {
list_packageable_components $platform $version
return
}
if $all_platforms {
create_all_platform_packages $output $force $checksums $bootstrap $include_docs $version
return
}
# Create package for specific or current platform
let target_platform = if ($platform | str length) > 0 {
$platform
} else {
get_current_platform
}
create_platform_package $target_platform $output $force $checksums $bootstrap $include_docs $version
}
# List components that would be packaged
def list_packageable_components [platform: string, version: string] {
let target_platform = if ($platform | str length) > 0 {
$platform
} else {
get_current_platform
}
log_info $"📋 Components for platform: ($target_platform)"
log_info "================================================"
# Check nushell binary
let nushell_components = get_nushell_components $target_platform $version
log_info "🚀 Nushell Components:"
$nushell_components.binaries | each {|binary|
let status = if ($binary.path | path exists) { "✅" } else { "❌" }
log_info $" ($status) ($binary.name) - ($binary.path)"
}
# Check plugins
let plugin_components = get_plugin_components $target_platform $version
log_info ""
log_info "🔌 Plugin Components:"
$plugin_components.binaries | each {|binary|
let status = if ($binary.path | path exists) { "✅" } else { "❌" }
log_info $" ($status) ($binary.name) - ($binary.path)"
}
# Check configuration templates
let config_components = get_config_components $version
log_info ""
log_info "⚙️ Configuration Components:"
$config_components | each {|config|
let status = if ($config.path | path exists) { "✅" } else { "❌" }
log_info $" ($status) ($config.name) - ($config.path)"
}
# Check installers
let installer_components = get_installer_components $target_platform
log_info ""
log_info "📥 Installer Components:"
$installer_components | each {|installer|
let status = if ($installer.path | path exists) { "✅" } else { "❌" }
log_info $" ($status) ($installer.name) - ($installer.path)"
}
}
# Create packages for all supported platforms
def create_all_platform_packages [
output: string,
force: bool,
checksums: bool,
bootstrap: bool,
include_docs: bool,
version: string
] {
log_info "🌍 Creating packages for all supported platforms..."
let platforms = get_supported_platforms
mut successful_packages = []
mut failed_packages = []
for platform in $platforms {
log_info $""
log_info $"📦 Processing platform: ($platform)"
log_info "================================"
let result = try {
create_platform_package $platform $output $force $checksums $bootstrap $include_docs $version
"success"
} catch {|err|
log_error $"Failed to create package for ($platform): ($err.msg)"
"failed"
}
if $result == "success" {
$successful_packages = ($successful_packages | append $platform)
} else {
$failed_packages = ($failed_packages | append $platform)
}
}
# Summary
log_info ""
log_info "📊 Package Creation Summary"
log_info "========================="
log_success $"✅ Successful: ($successful_packages | length) platforms"
if ($successful_packages | length) > 0 {
$successful_packages | each {|platform| log_success $" - ($platform)"}
}
if ($failed_packages | length) > 0 {
log_error $"❌ Failed: ($failed_packages | length) platforms"
$failed_packages | each {|platform| log_error $" - ($platform)"}
}
# Create cross-platform checksums if requested
if $checksums != null {
create_cross_platform_checksums $output
}
}
# Create package for a specific platform
def create_platform_package [
platform: string,
output: string,
force: bool,
checksums: bool,
bootstrap: bool,
include_docs: bool,
version: string
] {
log_info $"📦 Creating package for platform: ($platform)"
# Validate platform
if not ($platform in (get_supported_platforms)) {
log_error $"Unsupported platform: ($platform). Supported: (get_supported_platforms | str join ', ')"
return
}
# Setup package directory structure
# Archive name includes platform, but directory structure is platform-agnostic
let archive_name = $"nushell-full-($version)-($platform)"
let package_name = $"nushell-full-($version)" # Directory name without platform suffix
let package_dir = $"($output)/($package_name)"
let archive_extension = get_archive_extension $platform
# Check if package already exists
let archive_path = $"($output)/($archive_name)($archive_extension)"
if ($archive_path | path exists) and ($force == null) {
log_warn $"Package already exists: ($archive_path). Use --force to overwrite."
return
}
# Clean and create package directory
ensure_dir $output
remove_dir $package_dir
ensure_dir $package_dir
# Create package structure
ensure_dir $"($package_dir)/bin"
ensure_dir $"($package_dir)/config"
ensure_dir $"($package_dir)/scripts"
if $include_docs != null {
ensure_dir $"($package_dir)/docs"
}
# Package components
mut components = {}
# Package nushell binary
let nushell_components = package_nushell_components $platform $package_dir $version
$components = ($components | merge {nushell: $nushell_components})
# Package plugins
let plugin_components = package_plugin_components $platform $package_dir $version
$components = ($components | merge {plugins: $plugin_components})
# Package configuration
let config_components = package_config_components $package_dir $version
$components = ($components | merge {configs: $config_components})
# Package installers
let installer_components = package_installer_components $platform $package_dir $bootstrap
$components = ($components | merge {installers: $installer_components})
# Package documentation if requested
if $include_docs != null {
let docs_components = package_docs_components $package_dir
$components = ($components | merge {docs: $docs_components})
}
# Create manifest
create_manifest $version $platform $components $"($package_dir)/manifest.json"
# Create README for package
create_package_readme $platform $package_dir $version
# Create archive
create_package_archive $package_dir $archive_path $platform
# Create checksums if requested
if $checksums != null {
create_checksums [$archive_path] $output
}
# Clean up temporary directory
remove_dir $package_dir
log_success $"✅ Package created: ($archive_path)"
}
# Package nushell components
def package_nushell_components [platform: string, package_dir: string, version: string] {
log_info "📦 Packaging nushell components..."
let components = get_nushell_components $platform $version
mut packaged = []
for binary in $components.binaries {
if ($binary.path | path exists) {
let dest_path = $"($package_dir)/bin/($binary.name)"
if (copy_file $binary.path $dest_path) {
$packaged = ($packaged | append $binary.name)
}
} else {
log_warn $"Nushell binary not found: ($binary.path)"
}
}
{
binaries: $packaged,
version: $version,
platform: $platform
}
}
# Package plugin components
def package_plugin_components [platform: string, package_dir: string, version: string] {
log_info "📦 Packaging plugin components..."
let components = get_plugin_components $platform $version
mut packaged = []
for binary in $components.binaries {
if ($binary.path | path exists) {
let dest_path = $"($package_dir)/bin/($binary.name)"
if (copy_file $binary.path $dest_path) {
$packaged = ($packaged | append $binary.name)
}
} else {
log_warn $"Plugin binary not found: ($binary.path)"
}
}
{
binaries: $packaged,
count: ($packaged | length)
}
}
# Package configuration components
def package_config_components [package_dir: string, version: string] {
log_info "📦 Packaging configuration components..."
let components = get_config_components $version
mut packaged = []
for config in $components {
if ($config.path | path exists) {
let dest_path = $"($package_dir)/config/($config.name)"
if (copy_file $config.path $dest_path) {
$packaged = ($packaged | append $config.name)
}
} else {
log_warn $"Configuration file not found: ($config.path)"
}
}
# Create distribution-specific config
create_distribution_config $package_dir $version
$packaged = ($packaged | append "distribution_config.toml")
{
files: $packaged,
count: ($packaged | length)
}
}
# Package installer components
def package_installer_components [platform: string, package_dir: string, bootstrap: bool] {
log_info "📦 Packaging installer components..."
let components = get_installer_components $platform
mut packaged = []
for installer in $components {
if ($installer.path | path exists) {
let dest_path = $"($package_dir)/($installer.name)"
if (copy_file $installer.path $dest_path) {
# Make installer executable on Unix-like systems
if not ($platform | str starts-with "windows") {
try { chmod +x $dest_path }
}
$packaged = ($packaged | append $installer.name)
}
} else {
log_warn $"Installer not found: ($installer.path)"
}
}
# Create uninstaller
create_uninstaller $platform $package_dir
$packaged = ($packaged | append "uninstall.sh")
{
files: $packaged,
bootstrap: $bootstrap
}
}
# Package documentation components
def package_docs_components [package_dir: string] {
log_info "📦 Packaging documentation components..."
let docs = [
{name: "README.md", path: "./README.md"},
{name: "LICENSE", path: "./LICENSE"}
]
mut packaged = []
for doc in $docs {
if ($doc.path | path exists) {
let dest_path = $"($package_dir)/docs/($doc.name)"
if (copy_file $doc.path $dest_path) {
$packaged = ($packaged | append $doc.name)
}
}
}
{
files: $packaged
}
}
# Get nushell components for platform
def get_nushell_components [platform: string, version: string] {
let extension = get_binary_extension $platform
let profile_dir = if ($platform | str contains "release") or true { "release" } else { "debug" }
# Check if platform-specific build exists in distribution
let dist_binary = $"./distribution/($platform)/nu($extension)"
let workspace_binary = $"./nushell/target/($profile_dir)/nu($extension)"
let nushell_path = if ($dist_binary | path exists) {
$dist_binary
} else if ($workspace_binary | path exists) {
$workspace_binary
} else {
$workspace_binary # Will be reported as missing in list
}
{
binaries: [
{
name: $"nu($extension)",
path: $nushell_path,
component: "nushell"
}
]
}
}
# Get plugin components for platform
def get_plugin_components [platform: string, version: string] {
let extension = get_binary_extension $platform
# Get plugins from individual plugin target/release directories
# (Never from distribution dir - that's the staging output, not source)
let plugin_binaries = (
glob "nu_plugin_*"
| where ($it | path type) == "dir"
| each {|plugin_dir|
let plugin_name = ($plugin_dir | path basename)
let binary_name = $"($plugin_name)($extension)"
mut binary_path = $"($plugin_dir)/target/release/($binary_name)"
if not ($binary_path | path exists) {
# Try debug build as fallback
$binary_path = $"($plugin_dir)/target/debug/($binary_name)"
}
# Only return if binary actually exists
if ($binary_path | path exists) {
{
name: $binary_name,
path: $binary_path,
component: "plugin"
}
}
}
| compact # Remove null/empty values
)
{
binaries: $plugin_binaries
}
}
# Get configuration components
def get_config_components [version: string] {
[
{name: "default_config.nu", path: "./scripts/templates/default_config.nu"},
{name: "default_env.nu", path: "./scripts/templates/default_env.nu"}
]
}
# Get installer components
def get_installer_components [platform: string] {
if ($platform | str starts-with "windows") {
[
{name: "install.ps1", path: "./scripts/templates/install.ps1"}
]
} else {
[
{name: "install.sh", path: "./scripts/templates/install.sh"}
]
}
}
# Create distribution-specific configuration
def create_distribution_config [package_dir: string, version: string] {
let config = {
distribution: {
version: $version,
created_at: (date now | format date "%Y-%m-%d %H:%M:%S UTC"),
full_distribution: true
}
}
$config | to toml | save -f $"($package_dir)/config/distribution_config.toml"
}
# Create package README
def create_package_readme [platform: string, package_dir: string, version: string] {
let readme_content = [
$"# Nushell Full Distribution v($version)",
"",
"This package contains a complete Nushell distribution with all plugins and configurations.",
"",
"## Platform",
$"- Target: ($platform)",
$"- Created: (date now | format date '%Y-%m-%d %H:%M:%S UTC')",
"",
"## Contents",
"",
"### Binaries (bin/)",
"- nu - Nushell shell binary",
"- nu_plugin_* - Plugin binaries",
"",
"### Configuration (config/)",
"- default_config.nu - Default nushell configuration",
"- default_env.nu - Default environment setup",
"- distribution_config.toml - Distribution metadata",
"",
"### Installation",
"Run the installer for your platform:",
"- Unix/Linux/macOS: ./install.sh",
"- Windows: ./install.ps1",
"",
"### Manual Installation",
"1. Copy binaries from bin/ to a directory in your PATH",
"2. Copy configuration files from config/ to your nushell config directory",
"3. Register plugins: nu -c 'plugin add path/to/plugin'",
"",
"### Verification",
"After installation, verify with:",
"nu --version",
"",
"### Uninstallation",
"Run: ./uninstall.sh (or ./uninstall.ps1 on Windows)",
"",
"## Documentation",
"See docs/ directory for additional documentation.",
"",
"## Support",
"For issues and support, visit: https://github.com/nushell/nushell"
] | str join "\n"
$readme_content | save -f $"($package_dir)/README.md"
}
# Create uninstaller script
def create_uninstaller [platform: string, package_dir: string] {
let uninstaller_path = if ($platform | str starts-with "windows") {
"./scripts/templates/uninstall.ps1"
} else {
"./scripts/templates/uninstall.sh"
}
if ($uninstaller_path | path exists) {
let dest = if ($platform | str starts-with "windows") {
$"($package_dir)/uninstall.ps1"
} else {
$"($package_dir)/uninstall.sh"
}
copy_file $uninstaller_path $dest
}
}
# Create package archive
def create_package_archive [package_dir: string, archive_path: string, platform: string] {
log_info $"📦 Creating archive: ($archive_path)"
let package_name = ($package_dir | path basename)
let archive_name = ($archive_path | path basename)
let work_dir = ($package_dir | path dirname)
# Get absolute paths before changing directory
let abs_archive_path = ($archive_path | path expand)
let abs_work_dir = ($work_dir | path expand)
if ($platform | str starts-with "windows") {
# Create ZIP archive for Windows
cd $abs_work_dir
try {
run-external "zip" "-r" $archive_name $package_name
} catch {
# Fallback to tar if zip not available
run-external "tar" "-czf" $archive_name $package_name
}
} else {
# Create tar.gz archive for Unix-like systems
cd $abs_work_dir
run-external "tar" "-czf" $archive_name $package_name
}
# The archive is created in work_dir, so check there
let temp_archive = $"($abs_work_dir)/($archive_name)"
# Check if archive was successfully created
if ($temp_archive | path exists) {
# Move archive to final location if different
if $temp_archive != $abs_archive_path {
mv $temp_archive $abs_archive_path
}
# Verify final archive and report size
if ($abs_archive_path | path exists) {
let size = (ls $abs_archive_path | get 0.size)
log_success $"✅ Archive created: ($archive_path) (($size))"
} else {
log_error $"❌ Failed to move archive to: ($archive_path)"
}
} else {
log_error $"❌ Failed to create archive: ($archive_path)"
}
}
# Create cross-platform checksums
def create_cross_platform_checksums [output: string] {
log_info "🔐 Creating cross-platform checksums..."
let archives = glob $"($output)/*.tar.gz" | append (glob $"($output)/*.zip")
if ($archives | length) > 0 {
create_checksums $archives $output
log_success $"✅ Cross-platform checksums created in ($output)/checksums.txt"
} else {
log_warn "No archives found for checksum generation"
}
}