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

612 lines
19 KiB
Plaintext

#!/usr/bin/env nu
# Binary packaging tool - packages platform binaries for different architectures
#
# Packages:
# - Cross-compiled binaries for multiple platforms
# - Standalone executable packages
# - Platform-specific installers
# - Binary verification and signing
use std log
def main [
--source-dir: string = "dist/platform" # Source directory with compiled binaries
--output-dir: string = "packages/binaries" # Output directory for packaged binaries
--platforms: string = "linux-amd64,macos-amd64,windows-amd64" # Target platforms
--format: string = "archive" # Package format: archive, installer, standalone
--compress: bool = true # Compress binary packages
--sign: bool = false # Sign binaries (requires signing keys)
--verify: bool = true # Verify binary integrity
--strip: bool = true # Strip debug symbols from release binaries
--upx: bool = false # Use UPX compression
--verbose: bool = false # Enable verbose logging
] -> record {
let source_root = ($source_dir | path expand)
let output_root = ($output_dir | path expand)
let packaging_config = {
source_dir: $source_root
output_dir: $output_root
platforms: ($platforms | split row "," | each { str trim })
format: $format
compress: $compress
sign: $sign
verify: $verify
strip: $strip
upx: $upx
verbose: $verbose
}
log info $"Starting binary packaging with config: ($packaging_config)"
# Validate source directory
if not ($source_root | path exists) {
log error $"Source directory does not exist: ($source_root)"
exit 1
}
# Ensure output directory exists
mkdir $output_root
# Find available binaries
let available_binaries = find_available_binaries $source_root $packaging_config
if ($available_binaries | length) == 0 {
log warning "No binaries found to package"
return {
status: "skipped"
reason: "no binaries found"
binaries_processed: 0
}
}
log info $"Found ($available_binaries | length) binaries to package"
# Package binaries for each platform
let packaging_results = []
for platform in $packaging_config.platforms {
let platform_result = package_platform_binaries $platform $available_binaries $packaging_config
let packaging_results = ($packaging_results | append $platform_result)
}
let summary = {
total_platforms: ($packaging_config.platforms | length)
successful_platforms: ($packaging_results | where status == "success" | length)
failed_platforms: ($packaging_results | where status == "failed" | length)
total_packages: ($packaging_results | get packages_created | math sum)
total_size: ($packaging_results | get total_size | math sum)
packaging_config: $packaging_config
results: $packaging_results
}
if $summary.failed_platforms > 0 {
log error $"Binary packaging completed with ($summary.failed_platforms) platform failures"
exit 1
} else {
log info $"Binary packaging completed successfully - ($summary.total_packages) packages created for ($summary.successful_platforms) platforms"
}
return $summary
}
# Find available binaries in source directory
def find_available_binaries [
source_dir: string
packaging_config: record
] -> list {
# Find all executable files
let executables = (find $source_dir -type f -executable)
$executables | each {|binary|
let binary_info = analyze_binary $binary $packaging_config
{
path: $binary
name: ($binary | path basename)
size: (ls $binary | get 0.size)
architecture: $binary_info.architecture
platform: $binary_info.platform
format: $binary_info.format
stripped: $binary_info.stripped
}
}
}
# Analyze binary file to determine its properties
def analyze_binary [
binary_path: string
packaging_config: record
] -> record {
try {
# Use file command to get binary information
let file_info = (file $binary_path)
let architecture = if ($file_info =~ "x86-64") or ($file_info =~ "x86_64") {
"amd64"
} else if ($file_info =~ "ARM64") or ($file_info =~ "aarch64") {
"arm64"
} else if ($file_info =~ "i386") {
"i386"
} else {
"unknown"
}
let platform = if ($file_info =~ "Linux") {
"linux"
} else if ($file_info =~ "Mach-O") {
"macos"
} else if ($file_info =~ "PE32") {
"windows"
} else {
"unknown"
}
let format = if ($file_info =~ "ELF") {
"elf"
} else if ($file_info =~ "Mach-O") {
"macho"
} else if ($file_info =~ "PE32") {
"pe"
} else {
"unknown"
}
let stripped = ($file_info =~ "stripped")
{
architecture: $architecture
platform: $platform
format: $format
stripped: $stripped
}
} catch {
{
architecture: "unknown"
platform: "unknown"
format: "unknown"
stripped: false
}
}
}
# Package binaries for a specific platform
def package_platform_binaries [
platform: string
available_binaries: list
packaging_config: record
] -> record {
log info $"Packaging binaries for platform: ($platform)"
let start_time = (date now)
let platform_parts = ($platform | split row "-")
let platform_os = ($platform_parts | get 0)
let platform_arch = ($platform_parts | get 1)
# Filter binaries for this platform
let platform_binaries = ($available_binaries | where {|binary|
($binary.platform == $platform_os) and ($binary.architecture == $platform_arch)
})
if ($platform_binaries | length) == 0 {
log warning $"No binaries found for platform: ($platform)"
return {
platform: $platform
status: "skipped"
reason: "no binaries found"
packages_created: 0
total_size: 0
duration: ((date now) - $start_time)
}
}
let mut packaging_errors = []
let mut processed_binaries = []
let mut total_package_size = 0
# Process each binary
for binary in $platform_binaries {
let binary_result = process_single_binary $binary $platform $packaging_config
if $binary_result.status == "success" {
$processed_binaries = ($processed_binaries | append $binary_result)
$total_package_size = $total_package_size + $binary_result.package_size
} else {
$packaging_errors = ($packaging_errors | append $binary_result.errors)
}
}
# Create platform package
let platform_package = create_platform_package $platform $processed_binaries $packaging_config
let status = if (($packaging_errors | length) > 0) or ($platform_package.status != "success") {
"failed"
} else {
"success"
}
{
platform: $platform
status: $status
packages_created: ($processed_binaries | length)
total_size: $total_package_size
platform_package: $platform_package
processed_binaries: $processed_binaries
errors: $packaging_errors
duration: ((date now) - $start_time)
}
}
# Process a single binary
def process_single_binary [
binary: record
platform: string
packaging_config: record
] -> record {
if $packaging_config.verbose {
log info $"Processing binary: ($binary.name) for ($platform)"
}
let start_time = (date now)
let output_name = $"($binary.name)-($platform)"
let temp_binary = ($packaging_config.output_dir | path join "tmp" $output_name)
try {
# Ensure temp directory exists
mkdir ($temp_binary | path dirname)
# Copy binary to temp location
cp $binary.path $temp_binary
# Strip debug symbols if requested and not already stripped
if $packaging_config.strip and not $binary.stripped {
strip_binary $temp_binary $packaging_config
}
# Apply UPX compression if requested
if $packaging_config.upx {
upx_compress_binary $temp_binary $packaging_config
}
# Verify binary integrity
if $packaging_config.verify {
let verification_result = verify_binary_integrity $temp_binary $packaging_config
if $verification_result.status != "success" {
return {
binary: $binary.name
status: "failed"
reason: $"verification failed: ($verification_result.reason)"
errors: [$verification_result]
duration: ((date now) - $start_time)
}
}
}
# Sign binary if requested
if $packaging_config.sign {
let signing_result = sign_binary $temp_binary $packaging_config
if $signing_result.status != "success" {
return {
binary: $binary.name
status: "failed"
reason: $"signing failed: ($signing_result.reason)"
errors: [$signing_result]
duration: ((date now) - $start_time)
}
}
}
# Package binary according to format
let package_result = package_binary $temp_binary $output_name $packaging_config
if $package_result.status == "success" {
# Clean up temp file
rm $temp_binary
{
binary: $binary.name
status: "success"
output_name: $output_name
package_path: $package_result.package_path
package_size: $package_result.package_size
original_size: $binary.size
compression_ratio: $package_result.compression_ratio
duration: ((date now) - $start_time)
}
} else {
{
binary: $binary.name
status: "failed"
reason: $package_result.reason
errors: [$package_result]
duration: ((date now) - $start_time)
}
}
} catch {|err|
{
binary: $binary.name
status: "failed"
reason: $err.msg
errors: [{ error: $err.msg }]
duration: ((date now) - $start_time)
}
}
}
# Strip debug symbols from binary
def strip_binary [binary_path: string, packaging_config: record] {
if $packaging_config.verbose {
log info $"Stripping debug symbols: ($binary_path)"
}
try {
# Use strip command (available on most Unix systems)
strip $binary_path
} catch {|err|
log warning $"Failed to strip binary ($binary_path): ($err.msg)"
}
}
# Compress binary with UPX
def upx_compress_binary [binary_path: string, packaging_config: record] {
if $packaging_config.verbose {
log info $"UPX compressing: ($binary_path)"
}
try {
# Check if UPX is available
let upx_check = (which upx | complete)
if $upx_check.exit_code != 0 {
log warning "UPX not available, skipping compression"
return
}
# Apply UPX compression
upx --best $binary_path
} catch {|err|
log warning $"Failed to UPX compress binary ($binary_path): ($err.msg)"
}
}
# Verify binary integrity
def verify_binary_integrity [binary_path: string, packaging_config: record] -> record {
try {
# Check if binary is still executable
let file_info = (file $binary_path)
if not ($file_info =~ "executable") {
return {
status: "failed"
reason: "binary is not executable after processing"
}
}
# Try to run the binary with --help or --version
let help_test = (run-external --redirect-combine $binary_path --help | complete)
let version_test = (run-external --redirect-combine $binary_path --version | complete)
if ($help_test.exit_code == 0) or ($version_test.exit_code == 0) {
return {
status: "success"
verified: true
}
} else {
return {
status: "failed"
reason: "binary does not respond to --help or --version"
}
}
} catch {|err|
return {
status: "failed"
reason: $err.msg
}
}
}
# Sign binary (placeholder - would need actual signing implementation)
def sign_binary [binary_path: string, packaging_config: record] -> record {
log warning "Binary signing not implemented - skipping"
return {
status: "success"
signed: false
reason: "signing not implemented"
}
}
# Package binary according to specified format
def package_binary [
binary_path: string
output_name: string
packaging_config: record
] -> record {
match $packaging_config.format {
"archive" => { create_archive_package $binary_path $output_name $packaging_config }
"installer" => { create_installer_package $binary_path $output_name $packaging_config }
"standalone" => { create_standalone_package $binary_path $output_name $packaging_config }
_ => {
{
status: "failed"
reason: $"unsupported format: ($packaging_config.format)"
}
}
}
}
# Create archive package
def create_archive_package [
binary_path: string
output_name: string
packaging_config: record
] -> record {
let archive_name = $"($output_name).tar.gz"
let archive_path = ($packaging_config.output_dir | path join $archive_name)
try {
# Create tar.gz archive
let binary_dir = ($binary_path | path dirname)
let binary_name = ($binary_path | path basename)
cd $binary_dir
tar -czf $archive_path $binary_name
let archive_size = (ls $archive_path | get 0.size)
let original_size = (ls $binary_path | get 0.size)
let compression_ratio = (($archive_size | into float) / ($original_size | into float) * 100)
{
status: "success"
package_path: $archive_path
package_size: $archive_size
compression_ratio: $compression_ratio
}
} catch {|err|
{
status: "failed"
reason: $err.msg
}
}
}
# Create installer package
def create_installer_package [
binary_path: string
output_name: string
packaging_config: record
] -> record {
# Placeholder - would create platform-specific installers
log warning "Installer packages not implemented - using archive format"
create_archive_package $binary_path $output_name $packaging_config
}
# Create standalone package
def create_standalone_package [
binary_path: string
output_name: string
packaging_config: record
] -> record {
let standalone_path = ($packaging_config.output_dir | path join $output_name)
try {
# Just copy the binary as standalone
cp $binary_path $standalone_path
let package_size = (ls $standalone_path | get 0.size)
{
status: "success"
package_path: $standalone_path
package_size: $package_size
compression_ratio: 100.0
}
} catch {|err|
{
status: "failed"
reason: $err.msg
}
}
}
# Create platform package (combines all binaries for a platform)
def create_platform_package [
platform: string
processed_binaries: list
packaging_config: record
] -> record {
if ($processed_binaries | length) == 0 {
return {
status: "skipped"
reason: "no binaries to package"
}
}
let platform_package_name = $"provisioning-binaries-($platform).tar.gz"
let platform_package_path = ($packaging_config.output_dir | path join $platform_package_name)
try {
# Create temporary directory for platform package
let temp_platform_dir = ($packaging_config.output_dir | path join "tmp" $"platform-($platform)")
mkdir $temp_platform_dir
# Copy all processed binaries to platform directory
for binary in $processed_binaries {
let binary_name = ($binary.package_path | path basename)
cp $binary.package_path ($temp_platform_dir | path join $binary_name)
}
# Create platform archive
let temp_parent = ($temp_platform_dir | path dirname)
let temp_name = ($temp_platform_dir | path basename)
cd $temp_parent
tar -czf $platform_package_path $temp_name
# Clean up temporary directory
rm -rf $temp_platform_dir
let package_size = (ls $platform_package_path | get 0.size)
log info $"Created platform package: ($platform_package_path)"
{
status: "success"
package_path: $platform_package_path
package_size: $package_size
binaries_included: ($processed_binaries | length)
}
} catch {|err|
{
status: "failed"
reason: $err.msg
}
}
}
# Show binary information
def "main info" [source_dir: string = "dist/platform"] {
let source_root = ($source_dir | path expand)
if not ($source_root | path exists) {
return { error: "source directory not found", directory: $source_root }
}
let dummy_config = { verbose: false }
let available_binaries = find_available_binaries $source_root $dummy_config
{
source_directory: $source_root
total_binaries: ($available_binaries | length)
binaries_by_platform: ($available_binaries | group-by platform | items {|platform, binaries| { platform: $platform, count: ($binaries | length) } })
binaries_by_architecture: ($available_binaries | group-by architecture | items {|arch, binaries| { architecture: $arch, count: ($binaries | length) } })
total_size: ($available_binaries | get size | math sum)
binaries: $available_binaries
}
}
# List packaged binaries
def "main list" [packages_dir: string = "packages/binaries"] {
let packages_root = ($packages_dir | path expand)
if not ($packages_root | path exists) {
return { error: "packages directory not found", directory: $packages_root }
}
let binary_packages = (find $packages_root -name "*.tar.gz" -o -name "provisioning-*" -type f)
$binary_packages | each {|package|
let package_info = (ls $package | get 0)
{
name: ($package | path basename)
path: $package
size: $package_info.size
modified: $package_info.modified
}
}
}