411 lines
12 KiB
Plaintext
411 lines
12 KiB
Plaintext
|
|
#!/usr/bin/env nu
|
||
|
|
|
||
|
|
# Core bundle tool - bundles Nushell core libraries and CLI for distribution
|
||
|
|
#
|
||
|
|
# Bundles:
|
||
|
|
# - Nushell provisioning CLI wrapper
|
||
|
|
# - Core Nushell libraries (lib_provisioning)
|
||
|
|
# - Configuration system
|
||
|
|
# - Template system
|
||
|
|
# - Extensions and plugins
|
||
|
|
|
||
|
|
use std log
|
||
|
|
|
||
|
|
def main [
|
||
|
|
--output-dir: string = "dist/core" # Output directory for core bundle
|
||
|
|
--config-dir: string = "dist/config" # Configuration directory
|
||
|
|
--validate: bool = false # Validate Nushell syntax
|
||
|
|
--compress: bool = false # Compress bundle with gzip
|
||
|
|
--exclude-dev: bool = true # Exclude development files
|
||
|
|
--verbose: bool = false # Enable verbose logging
|
||
|
|
] -> record {
|
||
|
|
|
||
|
|
let repo_root = ($env.PWD | path dirname | path dirname | path dirname)
|
||
|
|
let bundle_config = {
|
||
|
|
output_dir: ($output_dir | path expand)
|
||
|
|
config_dir: ($config_dir | path expand)
|
||
|
|
validate: $validate
|
||
|
|
compress: $compress
|
||
|
|
exclude_dev: $exclude_dev
|
||
|
|
verbose: $verbose
|
||
|
|
}
|
||
|
|
|
||
|
|
log info $"Starting core bundle creation with config: ($bundle_config)"
|
||
|
|
|
||
|
|
# Ensure output directories exist
|
||
|
|
mkdir ($bundle_config.output_dir)
|
||
|
|
mkdir ($bundle_config.config_dir)
|
||
|
|
|
||
|
|
# Define core components to bundle
|
||
|
|
let core_components = [
|
||
|
|
{
|
||
|
|
name: "provisioning-cli"
|
||
|
|
source: ($repo_root | path join "provisioning" "core" "nulib" "provisioning")
|
||
|
|
target: ($bundle_config.output_dir | path join "bin" "provisioning")
|
||
|
|
type: "executable"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "core-libraries"
|
||
|
|
source: ($repo_root | path join "provisioning" "core" "nulib" "lib_provisioning")
|
||
|
|
target: ($bundle_config.output_dir | path join "lib" "lib_provisioning")
|
||
|
|
type: "directory"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "workflows"
|
||
|
|
source: ($repo_root | path join "provisioning" "core" "nulib" "workflows")
|
||
|
|
target: ($bundle_config.output_dir | path join "lib" "workflows")
|
||
|
|
type: "directory"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "servers"
|
||
|
|
source: ($repo_root | path join "provisioning" "core" "nulib" "servers")
|
||
|
|
target: ($bundle_config.output_dir | path join "lib" "servers")
|
||
|
|
type: "directory"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "extensions"
|
||
|
|
source: ($repo_root | path join "provisioning" "extensions")
|
||
|
|
target: ($bundle_config.output_dir | path join "extensions")
|
||
|
|
type: "directory"
|
||
|
|
}
|
||
|
|
]
|
||
|
|
|
||
|
|
# Define configuration files
|
||
|
|
let config_files = [
|
||
|
|
{
|
||
|
|
name: "default-config"
|
||
|
|
source: ($repo_root | path join "provisioning" "config.defaults.toml")
|
||
|
|
target: ($bundle_config.config_dir | path join "config.defaults.toml")
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "config-examples"
|
||
|
|
source: ($repo_root | path join "provisioning" "config")
|
||
|
|
target: ($bundle_config.config_dir | path join "examples")
|
||
|
|
}
|
||
|
|
]
|
||
|
|
|
||
|
|
let results = []
|
||
|
|
|
||
|
|
# Bundle core components
|
||
|
|
let component_results = $core_components | each {|component|
|
||
|
|
bundle_component $component $bundle_config $repo_root
|
||
|
|
}
|
||
|
|
|
||
|
|
let results = ($results | append $component_results)
|
||
|
|
|
||
|
|
# Bundle configuration files
|
||
|
|
let config_results = $config_files | each {|config|
|
||
|
|
bundle_config_file $config $bundle_config
|
||
|
|
}
|
||
|
|
|
||
|
|
let results = ($results | append $config_results)
|
||
|
|
|
||
|
|
# Validate Nushell syntax if requested
|
||
|
|
let validation_result = if $bundle_config.validate {
|
||
|
|
validate_nushell_syntax ($bundle_config.output_dir)
|
||
|
|
} else {
|
||
|
|
{ status: "skipped", validated_files: 0, errors: [] }
|
||
|
|
}
|
||
|
|
|
||
|
|
# Create bundle metadata
|
||
|
|
create_bundle_metadata $bundle_config $repo_root $results
|
||
|
|
|
||
|
|
# Compress if requested
|
||
|
|
let compression_result = if $bundle_config.compress {
|
||
|
|
compress_bundle ($bundle_config.output_dir)
|
||
|
|
} else {
|
||
|
|
{ status: "skipped" }
|
||
|
|
}
|
||
|
|
|
||
|
|
let summary = {
|
||
|
|
total_components: ($results | length)
|
||
|
|
successful: ($results | where status == "success" | length)
|
||
|
|
failed: ($results | where status == "failed" | length)
|
||
|
|
validation: $validation_result
|
||
|
|
compression: $compression_result
|
||
|
|
bundle_config: $bundle_config
|
||
|
|
results: $results
|
||
|
|
}
|
||
|
|
|
||
|
|
if $summary.failed > 0 {
|
||
|
|
log error $"Core bundle creation completed with ($summary.failed) failures"
|
||
|
|
exit 1
|
||
|
|
} else {
|
||
|
|
log info $"Core bundle creation completed successfully"
|
||
|
|
}
|
||
|
|
|
||
|
|
return $summary
|
||
|
|
}
|
||
|
|
|
||
|
|
# Bundle a single core component
|
||
|
|
def bundle_component [
|
||
|
|
component: record
|
||
|
|
bundle_config: record
|
||
|
|
repo_root: string
|
||
|
|
] -> record {
|
||
|
|
log info $"Bundling ($component.name)..."
|
||
|
|
|
||
|
|
if not ($component.source | path exists) {
|
||
|
|
log warning $"Source path does not exist: ($component.source)"
|
||
|
|
return {
|
||
|
|
component: $component.name
|
||
|
|
status: "skipped"
|
||
|
|
reason: "source not found"
|
||
|
|
target: $component.target
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
# Ensure target directory exists
|
||
|
|
let target_parent = ($component.target | path dirname)
|
||
|
|
mkdir $target_parent
|
||
|
|
|
||
|
|
if $component.type == "executable" {
|
||
|
|
# Copy executable file and make it executable
|
||
|
|
cp $component.source $component.target
|
||
|
|
chmod +x $component.target
|
||
|
|
} else if $component.type == "directory" {
|
||
|
|
# Copy directory recursively, excluding development files if requested
|
||
|
|
if $bundle_config.exclude_dev {
|
||
|
|
copy_directory_filtered $component.source $component.target
|
||
|
|
} else {
|
||
|
|
cp -r $component.source $component.target
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
log info $"Successfully bundled ($component.name) -> ($component.target)"
|
||
|
|
|
||
|
|
{
|
||
|
|
component: $component.name
|
||
|
|
status: "success"
|
||
|
|
source: $component.source
|
||
|
|
target: $component.target
|
||
|
|
size: (get_directory_size $component.target)
|
||
|
|
}
|
||
|
|
|
||
|
|
} catch {|err|
|
||
|
|
log error $"Failed to bundle ($component.name): ($err.msg)"
|
||
|
|
{
|
||
|
|
component: $component.name
|
||
|
|
status: "failed"
|
||
|
|
reason: $err.msg
|
||
|
|
target: $component.target
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Bundle a configuration file
|
||
|
|
def bundle_config_file [
|
||
|
|
config: record
|
||
|
|
bundle_config: record
|
||
|
|
] -> record {
|
||
|
|
log info $"Bundling config ($config.name)..."
|
||
|
|
|
||
|
|
if not ($config.source | path exists) {
|
||
|
|
log warning $"Config source does not exist: ($config.source)"
|
||
|
|
return {
|
||
|
|
component: $config.name
|
||
|
|
status: "skipped"
|
||
|
|
reason: "source not found"
|
||
|
|
target: $config.target
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
# Ensure target directory exists
|
||
|
|
let target_parent = ($config.target | path dirname)
|
||
|
|
mkdir $target_parent
|
||
|
|
|
||
|
|
# Copy config file or directory
|
||
|
|
cp -r $config.source $config.target
|
||
|
|
|
||
|
|
log info $"Successfully bundled config ($config.name) -> ($config.target)"
|
||
|
|
|
||
|
|
{
|
||
|
|
component: $config.name
|
||
|
|
status: "success"
|
||
|
|
source: $config.source
|
||
|
|
target: $config.target
|
||
|
|
}
|
||
|
|
|
||
|
|
} catch {|err|
|
||
|
|
log error $"Failed to bundle config ($config.name): ($err.msg)"
|
||
|
|
{
|
||
|
|
component: $config.name
|
||
|
|
status: "failed"
|
||
|
|
reason: $err.msg
|
||
|
|
target: $config.target
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Copy directory with filtering for development files
|
||
|
|
def copy_directory_filtered [source: string, target: string] {
|
||
|
|
let exclude_patterns = [
|
||
|
|
"*.tmp"
|
||
|
|
"*.bak"
|
||
|
|
"*~"
|
||
|
|
"*.swp"
|
||
|
|
".git*"
|
||
|
|
"test_*"
|
||
|
|
"*_test.nu"
|
||
|
|
"dev_*"
|
||
|
|
"debug_*"
|
||
|
|
]
|
||
|
|
|
||
|
|
# Create target directory
|
||
|
|
mkdir $target
|
||
|
|
|
||
|
|
# Get all files recursively, excluding patterns
|
||
|
|
let files = (find $source -type f | where {|file|
|
||
|
|
let exclude = $exclude_patterns | any {|pattern|
|
||
|
|
$file =~ $pattern
|
||
|
|
}
|
||
|
|
not $exclude
|
||
|
|
})
|
||
|
|
|
||
|
|
# Copy each file, preserving directory structure
|
||
|
|
$files | each {|file|
|
||
|
|
let relative_path = ($file | str replace $source "" | str trim-left "/")
|
||
|
|
let target_path = ($target | path join $relative_path)
|
||
|
|
let target_dir = ($target_path | path dirname)
|
||
|
|
|
||
|
|
mkdir $target_dir
|
||
|
|
cp $file $target_path
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Validate Nushell syntax in bundled files
|
||
|
|
def validate_nushell_syntax [bundle_dir: string] -> record {
|
||
|
|
log info "Validating Nushell syntax..."
|
||
|
|
|
||
|
|
let nu_files = (find $bundle_dir -name "*.nu" -type f)
|
||
|
|
let mut validation_errors = []
|
||
|
|
let mut validated_count = 0
|
||
|
|
|
||
|
|
for file in $nu_files {
|
||
|
|
try {
|
||
|
|
# Use nu --check to validate syntax
|
||
|
|
nu --check $file
|
||
|
|
$validated_count = $validated_count + 1
|
||
|
|
} catch {|err|
|
||
|
|
$validation_errors = ($validation_errors | append {
|
||
|
|
file: $file
|
||
|
|
error: $err.msg
|
||
|
|
})
|
||
|
|
log error $"Syntax error in ($file): ($err.msg)"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($validation_errors | length) > 0 {
|
||
|
|
log error $"Found ($validation_errors | length) syntax errors"
|
||
|
|
{
|
||
|
|
status: "failed"
|
||
|
|
validated_files: $validated_count
|
||
|
|
total_files: ($nu_files | length)
|
||
|
|
errors: $validation_errors
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
log info $"All ($validated_count) Nushell files passed syntax validation"
|
||
|
|
{
|
||
|
|
status: "success"
|
||
|
|
validated_files: $validated_count
|
||
|
|
total_files: ($nu_files | length)
|
||
|
|
errors: []
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Create bundle metadata
|
||
|
|
def create_bundle_metadata [bundle_config: record, repo_root: string, results: list] {
|
||
|
|
let metadata = {
|
||
|
|
bundle_version: "2.1.0"
|
||
|
|
created_at: (date now | format date "%Y-%m-%d %H:%M:%S")
|
||
|
|
created_by: "provisioning-build-system"
|
||
|
|
source_commit: (cd $repo_root; git rev-parse HEAD)
|
||
|
|
source_branch: (cd $repo_root; git branch --show-current)
|
||
|
|
bundle_config: $bundle_config
|
||
|
|
components: $results
|
||
|
|
total_size: (get_directory_size $bundle_config.output_dir)
|
||
|
|
}
|
||
|
|
|
||
|
|
let metadata_file = ($bundle_config.output_dir | path join "bundle-metadata.json")
|
||
|
|
$metadata | to json | save $metadata_file
|
||
|
|
|
||
|
|
log info $"Created bundle metadata: ($metadata_file)"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Compress bundle directory
|
||
|
|
def compress_bundle [bundle_dir: string] -> record {
|
||
|
|
log info "Compressing bundle..."
|
||
|
|
|
||
|
|
try {
|
||
|
|
let bundle_name = ($bundle_dir | path basename)
|
||
|
|
let parent_dir = ($bundle_dir | path dirname)
|
||
|
|
let archive_name = $"($bundle_name).tar.gz"
|
||
|
|
let archive_path = ($parent_dir | path join $archive_name)
|
||
|
|
|
||
|
|
cd $parent_dir
|
||
|
|
tar -czf $archive_name $bundle_name
|
||
|
|
|
||
|
|
let original_size = (get_directory_size $bundle_dir)
|
||
|
|
let compressed_size = (ls $archive_path | get 0.size)
|
||
|
|
let compression_ratio = (($compressed_size | into float) / ($original_size | into float) * 100)
|
||
|
|
|
||
|
|
log info $"Bundle compressed: ($original_size) -> ($compressed_size) (($compression_ratio | math round)% of original)"
|
||
|
|
|
||
|
|
{
|
||
|
|
status: "success"
|
||
|
|
archive_path: $archive_path
|
||
|
|
original_size: $original_size
|
||
|
|
compressed_size: $compressed_size
|
||
|
|
compression_ratio: $compression_ratio
|
||
|
|
}
|
||
|
|
|
||
|
|
} catch {|err|
|
||
|
|
log error $"Failed to compress bundle: ($err.msg)"
|
||
|
|
{
|
||
|
|
status: "failed"
|
||
|
|
reason: $err.msg
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Get directory size recursively
|
||
|
|
def get_directory_size [dir: string] -> int {
|
||
|
|
if not ($dir | path exists) {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($dir | path type) == "file" {
|
||
|
|
return (ls $dir | get 0.size)
|
||
|
|
}
|
||
|
|
|
||
|
|
let total_size = (find $dir -type f | each {|file|
|
||
|
|
ls $file | get 0.size
|
||
|
|
} | math sum)
|
||
|
|
|
||
|
|
return ($total_size | if $in == null { 0 } else { $in })
|
||
|
|
}
|
||
|
|
|
||
|
|
# Show bundle info
|
||
|
|
def "main info" [bundle_dir: string = "dist/core"] {
|
||
|
|
let bundle_dir = ($bundle_dir | path expand)
|
||
|
|
|
||
|
|
if not ($bundle_dir | path exists) {
|
||
|
|
log error $"Bundle directory does not exist: ($bundle_dir)"
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
|
||
|
|
let metadata_file = ($bundle_dir | path join "bundle-metadata.json")
|
||
|
|
if ($metadata_file | path exists) {
|
||
|
|
open $metadata_file
|
||
|
|
} else {
|
||
|
|
{
|
||
|
|
directory: $bundle_dir
|
||
|
|
size: (get_directory_size $bundle_dir)
|
||
|
|
files: (find $bundle_dir -type f | length)
|
||
|
|
nu_files: (find $bundle_dir -name "*.nu" -type f | length)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|