provisioning/tools/build/bundle-core.nu
2025-10-07 11:12:02 +01:00

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)
}
}
}