provisioning/tools/package/create-tarball.nu
2025-10-07 11:12:02 +01:00

800 lines
24 KiB
Plaintext

#!/usr/bin/env nu
# Tarball creation tool - creates distribution archives for different platforms
#
# Creates:
# - Source distributions (.tar.gz, .zip)
# - Binary distributions by platform
# - Complete distributions with all components
# - Customized distributions based on feature sets
use std log
def main [
--dist-dir: string = "dist" # Distribution directory to package
--output-dir: string = "packages" # Output directory for archives
--format: string = "tar.gz" # Archive format: tar.gz, zip, both
--platform: string = "all" # Target platforms: linux, macos, windows, all
--variant: string = "complete" # Distribution variant: complete, minimal, custom
--version: string = "" # Version string (auto-detected if empty)
--compression-level: int = 6 # Compression level 1-9
--exclude: string = "" # Comma-separated patterns to exclude
--verbose: bool = false # Enable verbose logging
--checksum: bool = true # Generate checksums for archives
] -> record {
let repo_root = ($env.PWD | path dirname | path dirname | path dirname)
let dist_root = ($dist_dir | path expand)
let output_root = ($output_dir | path expand)
# Detect version if not provided
let detected_version = if $version == "" {
detect_version $repo_root
} else {
$version
}
let package_config = {
dist_dir: $dist_root
output_dir: $output_root
formats: ($format | if $in == "both" { ["tar.gz", "zip"] } else { [$format] })
platforms: (if $platform == "all" { ["linux", "macos", "windows"] } else { [$platform] })
variant: $variant
version: $detected_version
compression_level: $compression_level
exclude_patterns: ($exclude | if $in == "" { [] } else { $in | split row "," | each { str trim } })
verbose: $verbose
checksum: $checksum
}
log info $"Starting tarball creation with config: ($package_config)"
# Validate distribution directory
if not ($dist_root | path exists) {
log error $"Distribution directory does not exist: ($dist_root)"
exit 1
}
# Ensure output directory exists
mkdir $output_root
let package_results = []
# Create packages for each platform and format combination
for platform in $package_config.platforms {
for format in $package_config.formats {
let package_result = create_platform_package $platform $format $package_config $repo_root
let package_results = ($package_results | append $package_result)
}
}
# Generate checksums if requested
let checksum_result = if $package_config.checksum {
generate_checksums $package_results $package_config
} else {
{ status: "skipped", checksums: [] }
}
# Create package manifest
create_package_manifest $package_results $checksum_result $package_config $repo_root
let summary = {
total_packages: ($package_results | length)
successful_packages: ($package_results | where status == "success" | length)
failed_packages: ($package_results | where status == "failed" | length)
total_size: ($package_results | where status == "success" | get archive_size | math sum)
checksum_result: $checksum_result
package_config: $package_config
results: $package_results
}
if $summary.failed_packages > 0 {
log error $"Package creation completed with ($summary.failed_packages) failures"
exit 1
} else {
log info $"Package creation completed successfully - ($summary.successful_packages) packages created"
}
return $summary
}
# Detect version from git or other sources
def detect_version [repo_root: string] -> string {
try {
cd $repo_root
# Try git describe first
let git_version = (git describe --tags --always --dirty 2>/dev/null | complete)
if $git_version.exit_code == 0 and ($git_version.stdout | str trim) != "" {
return ($git_version.stdout | str trim)
}
# Try git rev-parse for short hash
let git_hash = (git rev-parse --short HEAD 2>/dev/null | complete)
if $git_hash.exit_code == 0 {
return $"dev-($git_hash.stdout | str trim)"
}
# Fallback to date-based version
return $"dev-(date now | format date "%Y%m%d")"
} catch {
return "dev-unknown"
}
}
# Create package for a specific platform and format
def create_platform_package [
platform: string
format: string
package_config: record
repo_root: string
] -> record {
log info $"Creating ($format) package for ($platform)..."
let start_time = (date now)
let package_name = $"provisioning-($package_config.version)-($platform)-($package_config.variant)"
try {
# Prepare package directory
let package_dir = prepare_package_directory $platform $package_config $package_name
# Create archive
let archive_result = match $format {
"tar.gz" => { create_tar_archive $package_dir $package_config $package_name }
"zip" => { create_zip_archive $package_dir $package_config $package_name }
_ => {
log error $"Unsupported format: ($format)"
{ status: "failed", reason: $"unsupported format: ($format)" }
}
}
if $archive_result.status == "success" {
log info $"Successfully created ($format) package for ($platform): ($archive_result.archive_path)"
{
platform: $platform
format: $format
package_name: $package_name
status: "success"
archive_path: $archive_result.archive_path
archive_size: $archive_result.archive_size
original_size: $archive_result.original_size
compression_ratio: $archive_result.compression_ratio
duration: ((date now) - $start_time)
}
} else {
{
platform: $platform
format: $format
package_name: $package_name
status: "failed"
reason: $archive_result.reason
duration: ((date now) - $start_time)
}
}
} catch {|err|
log error $"Failed to create package for ($platform) ($format): ($err.msg)"
{
platform: $platform
format: $format
package_name: $package_name
status: "failed"
reason: $err.msg
duration: ((date now) - $start_time)
}
}
}
# Prepare package directory structure
def prepare_package_directory [
platform: string
package_config: record
package_name: string
] -> string {
let temp_package_dir = ($package_config.output_dir | path join "tmp" $package_name)
# Clean and create package directory
if ($temp_package_dir | path exists) {
rm -rf $temp_package_dir
}
mkdir $temp_package_dir
# Copy distribution files based on variant and platform
match $package_config.variant {
"complete" => {
copy_complete_distribution $package_config.dist_dir $temp_package_dir $platform $package_config
}
"minimal" => {
copy_minimal_distribution $package_config.dist_dir $temp_package_dir $platform $package_config
}
"custom" => {
copy_custom_distribution $package_config.dist_dir $temp_package_dir $platform $package_config
}
_ => {
log error $"Unknown variant: ($package_config.variant)"
exit 1
}
}
return $temp_package_dir
}
# Copy complete distribution
def copy_complete_distribution [
dist_dir: string
package_dir: string
platform: string
package_config: record
] {
# Copy all distribution components
let components = ["platform", "core", "config", "kcl", "extensions", "templates", "docs"]
for component in $components {
let source_path = ($dist_dir | path join $component)
let target_path = ($package_dir | path join $component)
if ($source_path | path exists) {
copy_filtered $source_path $target_path $package_config.exclude_patterns
}
}
# Filter platform-specific binaries
filter_platform_binaries $package_dir $platform
# Add installation scripts
add_installation_scripts $package_dir $platform $package_config
}
# Copy minimal distribution
def copy_minimal_distribution [
dist_dir: string
package_dir: string
platform: string
package_config: record
] {
# Copy only essential components
let essential_components = ["platform", "core", "config"]
for component in $essential_components {
let source_path = ($dist_dir | path join $component)
let target_path = ($package_dir | path join $component)
if ($source_path | path exists) {
copy_filtered $source_path $target_path $package_config.exclude_patterns
}
}
# Filter platform-specific binaries
filter_platform_binaries $package_dir $platform
# Add minimal installation scripts
add_minimal_installation_scripts $package_dir $platform $package_config
}
# Copy custom distribution
def copy_custom_distribution [
dist_dir: string
package_dir: string
platform: string
package_config: record
] {
# Custom distribution would be configurable - for now, same as complete
copy_complete_distribution $dist_dir $package_dir $platform $package_config
}
# Copy with exclusion filtering
def copy_filtered [source: string, target: string, exclude_patterns: list] {
if ($source | path type) == "file" {
if not (should_exclude $source $exclude_patterns) {
mkdir ($target | path dirname)
cp $source $target
}
} else {
mkdir $target
let items = (ls $source | where type == file)
for item in $items {
if not (should_exclude $item.name $exclude_patterns) {
cp $item.name ($target | path join ($item.name | path basename))
}
}
let dirs = (ls $source | where type == dir)
for dir in $dirs {
if not (should_exclude $dir.name $exclude_patterns) {
copy_filtered $dir.name ($target | path join ($dir.name | path basename)) $exclude_patterns
}
}
}
}
# Filter platform-specific binaries
def filter_platform_binaries [package_dir: string, platform: string] {
let platform_dir = ($package_dir | path join "platform")
if not ($platform_dir | path exists) {
return
}
# Get all binaries
let binaries = (ls $platform_dir | where type == file | get name)
# Filter based on platform
for binary in $binaries {
let binary_name = ($binary | path basename)
let should_keep = match $platform {
"linux" => { $binary_name =~ "linux" or not ($binary_name =~ "(macos|windows|darwin|win|exe)") }
"macos" => { $binary_name =~ "(macos|darwin)" or not ($binary_name =~ "(linux|windows|win|exe)") }
"windows" => { $binary_name =~ "(windows|win|exe)" or not ($binary_name =~ "(linux|macos|darwin)") }
_ => true
}
if not $should_keep {
rm $binary
}
}
}
# Add installation scripts
def add_installation_scripts [package_dir: string, platform: string, package_config: record] {
let install_script = match $platform {
"linux" => { create_linux_install_script $package_config }
"macos" => { create_macos_install_script $package_config }
"windows" => { create_windows_install_script $package_config }
_ => { create_generic_install_script $package_config }
}
let script_name = match $platform {
"windows" => "install.bat"
_ => "install.sh"
}
$install_script | save ($package_dir | path join $script_name)
# Make executable on Unix-like systems
if $platform in ["linux", "macos"] {
chmod +x ($package_dir | path join $script_name)
}
# Add README
create_package_readme $package_config | save ($package_dir | path join "README.md")
}
# Add minimal installation scripts
def add_minimal_installation_scripts [package_dir: string, platform: string, package_config: record] {
# Create simple installation instructions
let instructions = match $platform {
"linux" | "macos" => $"#!/bin/bash
# Provisioning System Installation
# Version: ($package_config.version)
echo \"Installing provisioning system...\"
# Copy binaries to /usr/local/bin
sudo cp platform/* /usr/local/bin/
sudo chmod +x /usr/local/bin/provisioning-orchestrator
# Copy core libraries
sudo mkdir -p /usr/local/lib/provisioning
sudo cp -r core/* /usr/local/lib/provisioning/
# Copy configuration
sudo mkdir -p /etc/provisioning
sudo cp config/* /etc/provisioning/
echo \"Installation complete!\"
echo \"Run 'provisioning help' to get started.\"
"
"windows" => $"@echo off
REM Provisioning System Installation
REM Version: ($package_config.version)
echo Installing provisioning system...
REM Create directories
mkdir \"C:\\Program Files\\Provisioning\" 2>NUL
mkdir \"C:\\Program Files\\Provisioning\\bin\" 2>NUL
mkdir \"C:\\Program Files\\Provisioning\\lib\" 2>NUL
mkdir \"C:\\ProgramData\\Provisioning\" 2>NUL
REM Copy files
xcopy platform\\* \"C:\\Program Files\\Provisioning\\bin\\\" /Y /Q
xcopy core\\* \"C:\\Program Files\\Provisioning\\lib\\\" /Y /Q /S
xcopy config\\* \"C:\\ProgramData\\Provisioning\\\" /Y /Q
echo Installation complete!
echo Add C:\\Program Files\\Provisioning\\bin to your PATH
echo Run 'provisioning-orchestrator --help' to get started.
"
_ => "# Installation instructions not available for this platform"
}
let script_name = match $platform {
"windows" => "install.bat"
_ => "install.sh"
}
$instructions | save ($package_dir | path join $script_name)
}
# Create tar.gz archive
def create_tar_archive [
package_dir: string
package_config: record
package_name: string
] -> record {
let archive_name = $"($package_name).tar.gz"
let archive_path = ($package_config.output_dir | path join $archive_name)
let package_parent = ($package_dir | path dirname)
let package_basename = ($package_dir | path basename)
try {
cd $package_parent
# Create tar with specified compression level
let compression_flag = $"--gzip --compression-level=($package_config.compression_level)"
tar --create --file $archive_path $compression_flag $package_basename
# Clean up temporary directory
rm -rf $package_dir
# Get size information
let archive_size = (ls $archive_path | get 0.size)
let original_size = get_directory_size $package_dir
let compression_ratio = if $original_size > 0 {
(($archive_size | into float) / ($original_size | into float) * 100)
} else {
0.0
}
{
status: "success"
archive_path: $archive_path
archive_size: $archive_size
original_size: $original_size
compression_ratio: $compression_ratio
}
} catch {|err|
{
status: "failed"
reason: $err.msg
}
}
}
# Create zip archive
def create_zip_archive [
package_dir: string
package_config: record
package_name: string
] -> record {
let archive_name = $"($package_name).zip"
let archive_path = ($package_config.output_dir | path join $archive_name)
let package_parent = ($package_dir | path dirname)
let package_basename = ($package_dir | path basename)
try {
cd $package_parent
# Create zip archive
zip -r -$package_config.compression_level $archive_path $package_basename
# Clean up temporary directory
rm -rf $package_dir
# Get size information
let archive_size = (ls $archive_path | get 0.size)
let original_size = get_directory_size $package_dir
let compression_ratio = if $original_size > 0 {
(($archive_size | into float) / ($original_size | into float) * 100)
} else {
0.0
}
{
status: "success"
archive_path: $archive_path
archive_size: $archive_size
original_size: $original_size
compression_ratio: $compression_ratio
}
} catch {|err|
{
status: "failed"
reason: $err.msg
}
}
}
# Generate checksums for all packages
def generate_checksums [
package_results: list
package_config: record
] -> record {
log info "Generating checksums..."
let successful_packages = ($package_results | where status == "success")
let mut checksums = []
for package in $successful_packages {
try {
let sha256_hash = (shasum -a 256 $package.archive_path | awk '{print $1}')
let md5_hash = (md5sum $package.archive_path | awk '{print $1}')
$checksums = ($checksums | append {
file: ($package.archive_path | path basename)
sha256: $sha256_hash
md5: $md5_hash
size: $package.archive_size
})
} catch {|err|
log warning $"Failed to generate checksum for ($package.archive_path): ($err.msg)"
}
}
# Save checksums to file
let checksum_file = ($package_config.output_dir | path join "checksums.txt")
let checksum_content = ($checksums | each {|cs|
$"($cs.sha256) ($cs.file)\n($cs.md5) ($cs.file)"
} | str join "\n")
$checksum_content | save $checksum_file
log info $"Generated checksums for ($checksums | length) packages: ($checksum_file)"
{
status: "success"
checksum_file: $checksum_file
checksums: $checksums
}
}
# Create package manifest
def create_package_manifest [
package_results: list
checksum_result: record
package_config: record
repo_root: string
] {
let manifest = {
version: $package_config.version
created_at: (date now | format date "%Y-%m-%d %H:%M:%S")
created_by: "provisioning-package-system"
source_commit: (try { cd $repo_root; git rev-parse HEAD } catch { "unknown" })
source_branch: (try { cd $repo_root; git branch --show-current } catch { "unknown" })
package_config: $package_config
packages: $package_results
checksums: $checksum_result.checksums
total_packages: ($package_results | where status == "success" | length)
total_size: ($package_results | where status == "success" | get archive_size | math sum)
}
let manifest_file = ($package_config.output_dir | path join "manifest.json")
$manifest | to json | save $manifest_file
log info $"Created package manifest: ($manifest_file)"
}
# Utility functions
def should_exclude [path: string, patterns: list] -> bool {
if ($patterns | length) == 0 { return false }
return ($patterns | any {|pattern| $path =~ $pattern })
}
def get_directory_size [dir: string] -> int {
if not ($dir | path exists) { return 0 }
try {
find $dir -type f | each {|file| ls $file | get 0.size } | math sum | if $in == null { 0 } else { $in }
} catch { 0 }
}
# Create installation script templates
def create_linux_install_script [config: record] -> string {
$"#!/bin/bash
# Provisioning System Installation Script
# Version: ($config.version)
# Platform: Linux
set -e
INSTALL_DIR=\"/usr/local\"
CONFIG_DIR=\"/etc/provisioning\"
LIB_DIR=\"$INSTALL_DIR/lib/provisioning\"
echo \"Installing Provisioning System ($config.version) for Linux...\"
# Check for root privileges
if [[ $EUID -ne 0 ]]; then
echo \"This script must be run as root (use sudo)\"
exit 1
fi
# Install binaries
echo \"Installing binaries...\"
mkdir -p \"$INSTALL_DIR/bin\"
cp platform/* \"$INSTALL_DIR/bin/\"
chmod +x \"$INSTALL_DIR/bin/\"*
# Install libraries
echo \"Installing libraries...\"
mkdir -p \"$LIB_DIR\"
cp -r core/* \"$LIB_DIR/\"
# Install configuration
echo \"Installing configuration...\"
mkdir -p \"$CONFIG_DIR\"
cp config/* \"$CONFIG_DIR/\"
# Install KCL schemas
if [ -d \"kcl\" ]; then
echo \"Installing KCL schemas...\"
mkdir -p \"$LIB_DIR/kcl\"
cp -r kcl/* \"$LIB_DIR/kcl/\"
fi
echo \"Installation complete!\"
echo \"Run 'provisioning help' to get started.\"
"
}
def create_macos_install_script [config: record] -> string {
$"#!/bin/bash
# Provisioning System Installation Script
# Version: ($config.version)
# Platform: macOS
set -e
INSTALL_DIR=\"/usr/local\"
CONFIG_DIR=\"/usr/local/etc/provisioning\"
LIB_DIR=\"$INSTALL_DIR/lib/provisioning\"
echo \"Installing Provisioning System ($config.version) for macOS...\"
# Install binaries
echo \"Installing binaries...\"
mkdir -p \"$INSTALL_DIR/bin\"
cp platform/* \"$INSTALL_DIR/bin/\"
chmod +x \"$INSTALL_DIR/bin/\"*
# Install libraries
echo \"Installing libraries...\"
mkdir -p \"$LIB_DIR\"
cp -r core/* \"$LIB_DIR/\"
# Install configuration
echo \"Installing configuration...\"
mkdir -p \"$CONFIG_DIR\"
cp config/* \"$CONFIG_DIR/\"
# Install KCL schemas
if [ -d \"kcl\" ]; then
echo \"Installing KCL schemas...\"
mkdir -p \"$LIB_DIR/kcl\"
cp -r kcl/* \"$LIB_DIR/kcl/\"
fi
echo \"Installation complete!\"
echo \"Run 'provisioning help' to get started.\"
echo \"Note: You may need to add /usr/local/bin to your PATH\"
"
}
def create_windows_install_script [config: record] -> string {
$"@echo off
REM Provisioning System Installation Script
REM Version: ($config.version)
REM Platform: Windows
echo Installing Provisioning System ($config.version) for Windows...
REM Create directories
mkdir \"C:\\Program Files\\Provisioning\\bin\" 2>NUL
mkdir \"C:\\Program Files\\Provisioning\\lib\" 2>NUL
mkdir \"C:\\ProgramData\\Provisioning\" 2>NUL
REM Install binaries
echo Installing binaries...
xcopy platform\\* \"C:\\Program Files\\Provisioning\\bin\\\" /Y /Q
REM Install libraries
echo Installing libraries...
xcopy core\\* \"C:\\Program Files\\Provisioning\\lib\\\" /Y /Q /S
REM Install configuration
echo Installing configuration...
xcopy config\\* \"C:\\ProgramData\\Provisioning\\\" /Y /Q
REM Install KCL schemas
if exist kcl\\ (
echo Installing KCL schemas...
mkdir \"C:\\Program Files\\Provisioning\\lib\\kcl\" 2>NUL
xcopy kcl\\* \"C:\\Program Files\\Provisioning\\lib\\kcl\\\" /Y /Q /S
)
echo Installation complete!
echo Add \"C:\\Program Files\\Provisioning\\bin\" to your PATH
echo Run 'provisioning-orchestrator --help' to get started.
pause
"
}
def create_generic_install_script [config: record] -> string {
$"#!/bin/sh
# Generic Installation Instructions
# Version: ($config.version)
echo \"Manual installation required for this platform\"
echo \"Please follow the instructions in README.md\"
"
}
def create_package_readme [config: record] -> string {
$"# Provisioning System ($config.version)
Cloud-native infrastructure provisioning and management system.
## Installation
### Automated Installation
Run the platform-specific installation script:
- **Linux/macOS**: `sudo ./install.sh`
- **Windows**: Run `install.bat` as Administrator
### Manual Installation
1. Copy platform binaries to your system PATH
2. Copy core libraries to a standard location
3. Copy configuration files to appropriate system directory
4. Set up environment variables if needed
## Getting Started
After installation, run:
```bash
provisioning help
```
For detailed documentation, visit: https://github.com/your-org/provisioning
## Version Information
- **Version**: ($config.version)
- **Variant**: ($config.variant)
- **Build Date**: (date now | format date "%Y-%m-%d")
## Support
For support and issues, please visit the project repository.
"
}
# Show package information
def "main info" [packages_dir: string = "packages"] {
let packages_root = ($packages_dir | path expand)
if not ($packages_root | path exists) {
return { error: "packages directory not found", directory: $packages_root }
}
let manifest_file = ($packages_root | path join "manifest.json")
if ($manifest_file | path exists) {
open $manifest_file
} else {
let packages = (ls $packages_root | where name =~ "\.(tar\.gz|zip)$")
{
directory: $packages_root
packages: ($packages | length)
total_size: ($packages | get size | math sum)
files: ($packages | get name | each { path basename })
}
}
}