2025-09-20 15:18:58 +01:00
|
|
|
#!/usr/bin/env nu
|
|
|
|
|
|
|
|
# Pack Distribution Script
|
2025-09-20 19:02:28 +01:00
|
|
|
# Creates distribution archives for built plugins with multi-platform support
|
2025-09-20 15:18:58 +01:00
|
|
|
|
|
|
|
# Load environment variables from env file
|
|
|
|
def load_env_vars [] {
|
|
|
|
let env_file = "env"
|
|
|
|
if ($env_file | path exists) {
|
|
|
|
let content = open $env_file | lines
|
|
|
|
| where ($it | str trim | str length) > 0
|
|
|
|
| where not ($it | str starts-with "#")
|
|
|
|
| each {|line|
|
|
|
|
if ($line | str contains "=") {
|
2025-09-20 19:02:28 +01:00
|
|
|
let parts = ($line | split row "=")
|
|
|
|
if ($parts | length) >= 2 {
|
|
|
|
let key = ($parts | get 0 | str replace "export " "" | str trim)
|
|
|
|
let raw_value = ($parts | get 1 | str trim)
|
|
|
|
# Handle bash-style default values like ${VAR:-default}
|
|
|
|
let value = if ($raw_value | str starts-with "${") and ($raw_value | str contains ":-") {
|
|
|
|
# Extract default value from ${VAR:-default}
|
|
|
|
let parts = ($raw_value | str replace "${" "" | str replace "}" "" | split row ":-")
|
|
|
|
if ($parts | length) >= 2 {
|
|
|
|
$parts | get 1
|
|
|
|
} else {
|
|
|
|
$raw_value
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$raw_value
|
|
|
|
}
|
|
|
|
{$key: $value}
|
|
|
|
} else {
|
|
|
|
{}
|
|
|
|
}
|
2025-09-20 15:18:58 +01:00
|
|
|
} else {
|
|
|
|
{}
|
|
|
|
}
|
|
|
|
}
|
2025-09-20 19:02:28 +01:00
|
|
|
| reduce -f {} {|item, acc|
|
|
|
|
if ($item | is-empty) {
|
|
|
|
$acc
|
|
|
|
} else {
|
|
|
|
$acc | merge $item
|
|
|
|
}
|
|
|
|
}
|
2025-09-20 15:18:58 +01:00
|
|
|
|
|
|
|
$content
|
|
|
|
} else {
|
|
|
|
{
|
|
|
|
APP_NAME: "nushell-plugins",
|
|
|
|
TARGET_PATH: "distribution",
|
|
|
|
INSTALL_FILE: "install_nu_plugins.nu",
|
|
|
|
INSTALL_BIN_PATH: "/usr/local/bin",
|
|
|
|
ARCHIVE_DIR_PATH: "/tmp",
|
|
|
|
BIN_ARCHIVES_DIR_PATH: "bin_archives"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-09-20 19:02:28 +01:00
|
|
|
# Load build targets configuration
|
|
|
|
def load_targets_config [] {
|
|
|
|
if ("etc/build_targets.toml" | path exists) {
|
|
|
|
open etc/build_targets.toml
|
|
|
|
} else {
|
|
|
|
{targets: {}}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-09-20 15:18:58 +01:00
|
|
|
# Get system architecture info
|
|
|
|
def get_system_info [] {
|
2025-09-20 19:02:28 +01:00
|
|
|
let arch = match (^uname -m) {
|
2025-09-20 15:18:58 +01:00
|
|
|
"x86_64" => "amd64",
|
|
|
|
"aarch64" => "arm64",
|
|
|
|
$arch if ($arch | str starts-with "arm") => "arm64",
|
|
|
|
$other => $other
|
|
|
|
}
|
|
|
|
|
2025-09-20 19:02:28 +01:00
|
|
|
let platform = match (^uname -s | str downcase) {
|
2025-09-20 15:18:58 +01:00
|
|
|
$os if ($os | str contains "linux") => "linux",
|
|
|
|
$os if ($os | str contains "darwin") => "darwin",
|
|
|
|
$os if ($os | str contains "windows") => "windows",
|
|
|
|
$other => $other
|
|
|
|
}
|
|
|
|
|
2025-09-20 19:02:28 +01:00
|
|
|
{platform: $platform, arch: $arch, full: $"($platform)-($arch)"}
|
|
|
|
}
|
|
|
|
|
|
|
|
# Get available distribution platforms
|
|
|
|
def get_available_platforms [base_path: string] {
|
|
|
|
let config = load_targets_config
|
|
|
|
|
|
|
|
mut platforms = []
|
|
|
|
|
|
|
|
# Check for single platform in base directory
|
|
|
|
let base_plugins = glob ($base_path | path join "nu_plugin_*") | where ($it | path type) == "file"
|
|
|
|
if ($base_plugins | length) > 0 {
|
|
|
|
let host_info = get_system_info
|
|
|
|
$platforms = ($platforms | append {
|
|
|
|
name: "host",
|
|
|
|
platform_name: $host_info.full,
|
|
|
|
path: $base_path,
|
|
|
|
plugin_count: ($base_plugins | length),
|
|
|
|
description: "Host platform distribution"
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
# Check for platform-specific subdirectories
|
|
|
|
if ($config.targets | transpose name config | length) > 0 {
|
|
|
|
for target in ($config.targets | transpose name config) {
|
|
|
|
let platform_path = ($base_path | path join $target.name)
|
|
|
|
if ($platform_path | path exists) {
|
|
|
|
let platform_plugins = glob ($platform_path | path join "nu_plugin_*") | where ($it | path type) == "file"
|
|
|
|
if ($platform_plugins | length) > 0 {
|
|
|
|
$platforms = ($platforms | append {
|
|
|
|
name: $target.name,
|
|
|
|
platform_name: $target.config.platform_name,
|
|
|
|
path: $platform_path,
|
|
|
|
plugin_count: ($platform_plugins | length),
|
|
|
|
description: $target.config.description,
|
|
|
|
archive_format: $target.config.archive_format
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$platforms
|
|
|
|
}
|
|
|
|
|
|
|
|
# Generate checksums for a file
|
|
|
|
def generate_checksums [file_path: string] {
|
|
|
|
let sha256 = (open $file_path | hash sha256)
|
|
|
|
let size = (ls $file_path | get 0.size)
|
|
|
|
|
|
|
|
{
|
|
|
|
file: ($file_path | path basename),
|
|
|
|
sha256: $sha256,
|
|
|
|
size: $size
|
|
|
|
}
|
2025-09-20 15:18:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
# Check if target directory has required files
|
|
|
|
def validate_target [target_path: string] {
|
|
|
|
let required_files = ["LICENSE", "README"]
|
|
|
|
let optional_files = ["env"]
|
|
|
|
let plugin_files = (ls $target_path | where name =~ "nu_plugin_" and type == "file" | get name)
|
|
|
|
|
|
|
|
mut issues = []
|
|
|
|
|
|
|
|
if ($plugin_files | length) == 0 {
|
|
|
|
$issues = ($issues | append "No plugin binaries found")
|
|
|
|
}
|
|
|
|
|
|
|
|
for file in $required_files {
|
2025-09-20 19:02:28 +01:00
|
|
|
if not (($target_path | path join $file) | path exists) {
|
2025-09-20 15:18:58 +01:00
|
|
|
$issues = ($issues | append $"Required file missing: ($file)")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
{valid: (($issues | length) == 0), issues: $issues, plugin_count: ($plugin_files | length)}
|
|
|
|
}
|
|
|
|
|
|
|
|
# Create archive
|
|
|
|
def create_archive [target_path: string, archive_path: string, env_vars: record] {
|
|
|
|
# Copy env file to target temporarily
|
|
|
|
if ("env" | path exists) {
|
2025-09-20 19:02:28 +01:00
|
|
|
cp env ($target_path | path join "env")
|
2025-09-20 15:18:58 +01:00
|
|
|
}
|
|
|
|
|
2025-09-20 19:02:28 +01:00
|
|
|
# Get plugin files using glob
|
|
|
|
let plugin_files = glob ($target_path | path join "nu_plugin_*") | where ($it | path type) == "file"
|
|
|
|
|
|
|
|
let metadata_files = [
|
|
|
|
($target_path | path join $env_vars.INSTALL_FILE),
|
|
|
|
($target_path | path join "LICENSE"),
|
|
|
|
($target_path | path join "README"),
|
|
|
|
($target_path | path join "env")
|
2025-09-20 15:18:58 +01:00
|
|
|
]
|
|
|
|
|
2025-09-20 19:02:28 +01:00
|
|
|
# Filter metadata files that actually exist and combine with plugin files
|
|
|
|
let existing_metadata_files = $metadata_files | where ($it | path exists)
|
|
|
|
let existing_files = $plugin_files | append $existing_metadata_files
|
2025-09-20 15:18:58 +01:00
|
|
|
|
|
|
|
if ($existing_files | length) == 0 {
|
|
|
|
error make {msg: "No files found to archive"}
|
|
|
|
}
|
|
|
|
|
|
|
|
print $"📦 Creating archive with ($existing_files | length) files..."
|
|
|
|
|
|
|
|
try {
|
|
|
|
# Use tar to create the archive
|
|
|
|
tar czf $archive_path ...$existing_files
|
|
|
|
|
|
|
|
# Clean up temporary env file
|
2025-09-20 19:02:28 +01:00
|
|
|
if (($target_path | path join "env") | path exists) {
|
|
|
|
rm ($target_path | path join "env")
|
2025-09-20 15:18:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
let archive_size = ls $archive_path | get 0.size
|
|
|
|
print $"✅ Archive created: ($archive_path) (($archive_size))"
|
|
|
|
|
|
|
|
true
|
|
|
|
} catch {|err|
|
|
|
|
# Clean up on failure
|
|
|
|
if ($"($target_path)/env" | path exists) {
|
|
|
|
rm $"($target_path)/env"
|
|
|
|
}
|
|
|
|
error make {msg: $"Failed to create archive: ($err.msg)"}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# Main function
|
|
|
|
def main [
|
|
|
|
--target (-t): string = "" # Override target path
|
2025-09-20 19:02:28 +01:00
|
|
|
--platform (-p): string = "" # Specific platform to package
|
|
|
|
--all-platforms (-a) # Package all available platforms
|
2025-09-20 15:18:58 +01:00
|
|
|
--output (-o): string = "" # Override output archive path
|
2025-09-20 19:02:28 +01:00
|
|
|
--archive-dir: string = "" # Override archive directory
|
2025-09-20 15:18:58 +01:00
|
|
|
--force (-f) # Force overwrite existing archive
|
|
|
|
--list (-l) # List what would be archived
|
2025-09-20 19:02:28 +01:00
|
|
|
--list-platforms # List available platforms
|
|
|
|
--checksums # Generate checksums file
|
2025-09-20 15:18:58 +01:00
|
|
|
] {
|
|
|
|
# Ensure we're in the repository root directory
|
|
|
|
if not ("nu_plugin_clipboard" | path exists) {
|
|
|
|
error make {msg: "Please run this script from the nushell-plugins repository root directory"}
|
|
|
|
}
|
|
|
|
|
|
|
|
print "📦 Nushell Plugin Distribution Packager"
|
|
|
|
|
|
|
|
# Load environment variables
|
|
|
|
let env_vars = load_env_vars
|
2025-09-20 19:02:28 +01:00
|
|
|
let base_target_path = if ($target | str length) > 0 { $target } else { $env_vars.TARGET_PATH }
|
|
|
|
let archive_dir_path = if ($archive_dir | str length) > 0 { $archive_dir } else { $env_vars.BIN_ARCHIVES_DIR_PATH? | default "bin_archives" }
|
2025-09-20 15:18:58 +01:00
|
|
|
|
2025-09-20 19:02:28 +01:00
|
|
|
# Validate base target directory
|
|
|
|
if not ($base_target_path | path exists) {
|
|
|
|
print $"❌ Target directory not found: ($base_target_path)"
|
|
|
|
print "💡 Run 'just collect' first to collect plugins"
|
2025-09-20 15:18:58 +01:00
|
|
|
exit 1
|
|
|
|
}
|
|
|
|
|
2025-09-20 19:02:28 +01:00
|
|
|
# Get available platforms
|
|
|
|
let available_platforms = get_available_platforms $base_target_path
|
|
|
|
|
|
|
|
# List platforms mode
|
|
|
|
if $list_platforms {
|
|
|
|
if ($available_platforms | length) == 0 {
|
|
|
|
print "❓ No platforms found for packaging"
|
|
|
|
print "💡 Run 'just collect --all-platforms' first"
|
|
|
|
} else {
|
|
|
|
print "📊 Available platforms for packaging:"
|
|
|
|
for platform in $available_platforms {
|
|
|
|
let format = $platform | get archive_format? | default "tar.gz"
|
|
|
|
print $" ✅ ($platform.name) ($platform.platform_name): ($platform.plugin_count) plugins → .($format)"
|
|
|
|
print $" Path: ($platform.path)"
|
|
|
|
print $" ($platform.description)"
|
|
|
|
}
|
2025-09-20 15:18:58 +01:00
|
|
|
}
|
2025-09-20 19:02:28 +01:00
|
|
|
return
|
2025-09-20 15:18:58 +01:00
|
|
|
}
|
|
|
|
|
2025-09-20 19:02:28 +01:00
|
|
|
# Determine platforms to package
|
|
|
|
let platforms_to_package = if $all_platforms {
|
|
|
|
$available_platforms
|
|
|
|
} else if ($platform | str length) > 0 {
|
|
|
|
let selected = $available_platforms | where name == $platform
|
|
|
|
if ($selected | length) == 0 {
|
|
|
|
print $"❌ Platform not found: ($platform)"
|
|
|
|
print $"💡 Available platforms: ($available_platforms | get name | str join ', ')"
|
|
|
|
exit 1
|
|
|
|
}
|
|
|
|
$selected
|
|
|
|
} else {
|
|
|
|
# Default to host platform or first available
|
|
|
|
let host_platform = $available_platforms | where name == "host"
|
|
|
|
if ($host_platform | length) > 0 {
|
|
|
|
$host_platform
|
|
|
|
} else {
|
|
|
|
$available_platforms | first 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($platforms_to_package | length) == 0 {
|
|
|
|
print "❓ No platforms to package"
|
|
|
|
print "💡 Run 'just collect' first to collect plugins"
|
|
|
|
exit 1
|
|
|
|
}
|
2025-09-20 15:18:58 +01:00
|
|
|
|
2025-09-20 19:02:28 +01:00
|
|
|
print $"🎯 Platforms to package: ($platforms_to_package | length)"
|
|
|
|
for platform in $platforms_to_package {
|
|
|
|
print $" - ($platform.name): ($platform.plugin_count) plugins"
|
|
|
|
}
|
2025-09-20 15:18:58 +01:00
|
|
|
|
2025-09-20 19:02:28 +01:00
|
|
|
# Create archive directory if needed
|
|
|
|
if not ($archive_dir_path | path exists) {
|
|
|
|
mkdir $archive_dir_path
|
|
|
|
print $"📁 Created archive directory: ($archive_dir_path)"
|
2025-09-20 15:18:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
# List mode
|
|
|
|
if $list {
|
2025-09-20 19:02:28 +01:00
|
|
|
print "\n📋 Files that would be archived:"
|
|
|
|
for platform in $platforms_to_package {
|
|
|
|
print $"\n($platform.name) ($platform.platform_name):"
|
|
|
|
let files = ls $platform.path | where type == "file"
|
|
|
|
for file in $files {
|
|
|
|
print $" 📄 ($file.name) (($file.size))"
|
|
|
|
}
|
2025-09-20 15:18:58 +01:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-09-20 19:02:28 +01:00
|
|
|
# Package each platform
|
|
|
|
mut created_archives = []
|
|
|
|
mut total_size = 0
|
|
|
|
|
|
|
|
for platform in $platforms_to_package {
|
|
|
|
let platform_name = $platform.platform_name
|
|
|
|
let platform_path = $platform.path
|
|
|
|
let archive_format = $platform | get archive_format? | default "tar.gz"
|
|
|
|
|
|
|
|
# Validate platform directory
|
|
|
|
let validation = validate_target $platform_path
|
|
|
|
if not $validation.valid {
|
|
|
|
print $"❌ Platform directory validation failed for ($platform.name):"
|
|
|
|
for issue in $validation.issues {
|
|
|
|
print $" - ($issue)"
|
|
|
|
}
|
|
|
|
continue
|
2025-09-20 15:18:58 +01:00
|
|
|
}
|
|
|
|
|
2025-09-20 19:02:28 +01:00
|
|
|
# Create archive name
|
|
|
|
let archive_name = $"($platform_name)-($env_vars.APP_NAME).($archive_format)"
|
|
|
|
let archive_path = if ($output | str length) > 0 and ($platforms_to_package | length) == 1 {
|
|
|
|
$output
|
|
|
|
} else {
|
|
|
|
$"($archive_dir_path)/($archive_name)"
|
|
|
|
}
|
|
|
|
|
|
|
|
# Check if archive already exists
|
|
|
|
if ($archive_path | path exists) and not $force {
|
|
|
|
print $"⚠️ Archive already exists: ($archive_path)"
|
|
|
|
if ($platforms_to_package | length) == 1 {
|
|
|
|
let confirm = input "Overwrite? [y/N]: "
|
|
|
|
if ($confirm | str downcase) != "y" {
|
|
|
|
print "❌ Skipping ($platform.name)"
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
print "🔄 Overwriting existing archive (use --force to skip this check)"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
print $"\n📦 Creating archive for ($platform.name) ($platform_name)..."
|
|
|
|
print $"📁 Source: ($platform_path)"
|
|
|
|
print $"📦 Archive: ($archive_path)"
|
|
|
|
|
|
|
|
try {
|
|
|
|
create_archive $platform_path $archive_path $env_vars
|
|
|
|
let archive_size = ls $archive_path | get 0.size
|
|
|
|
$total_size = $total_size + ($archive_size | into int)
|
|
|
|
|
|
|
|
$created_archives = ($created_archives | append {
|
|
|
|
platform: $platform.name,
|
|
|
|
platform_name: $platform_name,
|
|
|
|
archive_path: $archive_path,
|
|
|
|
size: $archive_size,
|
|
|
|
plugin_count: $platform.plugin_count
|
|
|
|
})
|
|
|
|
|
|
|
|
print $"✅ Created archive for ($platform.name): ($archive_size)"
|
|
|
|
} catch {|err|
|
|
|
|
print $"❌ Failed to create archive for ($platform.name): ($err.msg)"
|
|
|
|
}
|
2025-09-20 15:18:58 +01:00
|
|
|
}
|
|
|
|
|
2025-09-20 19:02:28 +01:00
|
|
|
# Generate checksums if requested
|
|
|
|
if $checksums and ($created_archives | length) > 0 {
|
|
|
|
print "\n🔐 Generating checksums..."
|
|
|
|
let checksums_file = $"($archive_dir_path)/checksums.txt"
|
2025-09-20 15:18:58 +01:00
|
|
|
|
2025-09-20 19:02:28 +01:00
|
|
|
mut checksum_data = []
|
|
|
|
for archive in $created_archives {
|
|
|
|
let checksum_info = generate_checksums $archive.archive_path
|
|
|
|
$checksum_data = ($checksum_data | append $checksum_info)
|
|
|
|
print $" ✅ ($checksum_info.file): ($checksum_info.sha256)"
|
|
|
|
}
|
2025-09-20 15:18:58 +01:00
|
|
|
|
2025-09-20 19:02:28 +01:00
|
|
|
# Write checksums file
|
|
|
|
let checksums_content = ($checksum_data | each {|item|
|
|
|
|
$"($item.sha256) ($item.file)"
|
|
|
|
} | str join "\n")
|
|
|
|
|
|
|
|
$checksums_content | save $checksums_file
|
|
|
|
print $"💾 Checksums saved to: ($checksums_file)"
|
|
|
|
}
|
|
|
|
|
|
|
|
# Summary
|
|
|
|
print $"\n🎉 Distribution packaging completed!"
|
|
|
|
print $"📁 Archive directory: ($archive_dir_path)"
|
|
|
|
print $"📦 Archives created: ($created_archives | length)"
|
|
|
|
print $"📏 Total size: ($total_size | into filesize)"
|
|
|
|
|
|
|
|
if ($created_archives | length) > 0 {
|
|
|
|
print "\n✅ Created archives:"
|
|
|
|
for archive in $created_archives {
|
|
|
|
print $" - ($archive.platform_name): ($archive.size) ($archive.plugin_count) plugins"
|
|
|
|
print $" 📦 ($archive.archive_path)"
|
|
|
|
}
|
|
|
|
}
|
2025-09-20 15:18:58 +01:00
|
|
|
|
|
|
|
print "\n💡 Next steps:"
|
2025-09-20 19:02:28 +01:00
|
|
|
if ($created_archives | length) > 1 {
|
|
|
|
print " 1. Test archives: tar -tzf bin_archives/*.tar.gz"
|
|
|
|
print " 2. Upload to GitHub Releases or distribution server"
|
|
|
|
print " 3. Update installation documentation"
|
|
|
|
} else if ($created_archives | length) == 1 {
|
|
|
|
let archive = $created_archives.0
|
|
|
|
print $" 1. Test archive: tar -tzf ($archive.archive_path)"
|
|
|
|
print $" 2. Distribute: Upload or copy to target systems"
|
|
|
|
print $" 3. Install: Extract and run the installation script"
|
|
|
|
}
|
2025-09-20 15:18:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($env.NUSHELL_EXECUTION_CONTEXT? | default "" | str contains "run") {
|
|
|
|
main
|
|
|
|
}
|