612 lines
19 KiB
Plaintext
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
|
|
}
|
|
}
|
|
} |