provisioning/tools/distribution/prepare-platform-dist.nu
2025-10-07 11:12:02 +01:00

899 lines
28 KiB
Plaintext

#!/usr/bin/env nu
# Platform distribution preparation tool - prepares platform services for distribution
#
# Prepares:
# - Rust platform binaries (orchestrator, control-center, etc.)
# - Container images and deployment configurations
# - Service definitions and systemd units
# - Platform-specific installation packages
# - Cross-compilation for multiple architectures
use std log
def main [
--source-root: string = "" # Source root directory (auto-detected if empty)
--output-dir: string = "dist/platform" # Output directory for platform distribution
--target-platforms: string = "linux-amd64,macos-amd64,windows-amd64" # Target platforms
--build-mode: string = "release" # Build mode: debug, release, optimized
--strip-symbols: bool = true # Strip debug symbols from release builds
--upx-compress: bool = false # Compress binaries with UPX
--create-containers: bool = false # Create container images
--generate-services: bool = true # Generate service definitions
--sign-binaries: bool = false # Sign binaries (requires signing keys)
--parallel-builds: bool = true # Build platforms in parallel
--verbose: bool = false # Enable verbose logging
] -> record {
let repo_root = if $source_root == "" {
($env.PWD | path dirname | path dirname | path dirname)
} else {
($source_root | path expand)
}
let target_platforms_list = ($target_platforms | split row "," | each { str trim })
let platform_config = {
source_root: $repo_root
output_dir: ($output_dir | path expand)
target_platforms: $target_platforms_list
build_mode: $build_mode
strip_symbols: $strip_symbols
upx_compress: $upx_compress
create_containers: $create_containers
generate_services: $generate_services
sign_binaries: $sign_binaries
parallel_builds: $parallel_builds
verbose: $verbose
}
log info $"Starting platform distribution preparation with config: ($platform_config)"
# Ensure output directory exists
mkdir ($platform_config.output_dir)
let preparation_results = []
try {
# Phase 1: Discover platform components
let discovery_result = discover_platform_components $platform_config
let preparation_results = ($preparation_results | append { phase: "discovery", result: $discovery_result })
if $discovery_result.status != "success" {
log error $"Platform component discovery failed: ($discovery_result.reason)"
exit 1
}
# Phase 2: Build platform binaries
let build_result = build_platform_binaries $platform_config $discovery_result
let preparation_results = ($preparation_results | append { phase: "build", result: $build_result })
if $build_result.status != "success" and $build_result.status != "partial" {
log error $"Platform binary build failed: ($build_result.reason)"
exit 1
}
# Phase 3: Post-process binaries
let processing_result = post_process_binaries $platform_config $build_result
let preparation_results = ($preparation_results | append { phase: "processing", result: $processing_result })
# Phase 4: Generate service definitions
let services_result = if $platform_config.generate_services {
generate_service_definitions $platform_config $build_result
} else {
{ status: "skipped", reason: "service generation disabled" }
}
let preparation_results = ($preparation_results | append { phase: "services", result: $services_result })
# Phase 5: Create container images
let containers_result = if $platform_config.create_containers {
create_container_images $platform_config $build_result
} else {
{ status: "skipped", reason: "container creation disabled" }
}
let preparation_results = ($preparation_results | append { phase: "containers", result: $containers_result })
# Phase 6: Generate platform metadata
let metadata_result = generate_platform_metadata $platform_config $preparation_results
let preparation_results = ($preparation_results | append { phase: "metadata", result: $metadata_result })
let summary = {
source_root: $platform_config.source_root
output_directory: $platform_config.output_dir
target_platforms: ($platform_config.target_platforms | length)
successful_phases: ($preparation_results | where {|r| $r.result.status == "success"} | length)
total_phases: ($preparation_results | length)
binaries_built: ($build_result.successful_builds)
total_size: (get_directory_size $platform_config.output_dir)
platform_config: $platform_config
phases: $preparation_results
}
log info $"Platform distribution preparation completed - ($summary.binaries_built) binaries built for ($summary.target_platforms) platforms"
return $summary
} catch {|err|
log error $"Platform distribution preparation failed: ($err.msg)"
exit 1
}
}
# Discover platform components in the source tree
def discover_platform_components [platform_config: record] -> record {
log info "Discovering platform components..."
let start_time = (date now)
try {
# Define platform project locations
let rust_projects = [
{
name: "orchestrator"
path: ($platform_config.source_root | path join "orchestrator")
binary: "provisioning-orchestrator"
description: "Main orchestration service"
required: true
},
{
name: "control-center"
path: ($platform_config.source_root | path join "control-center")
binary: "control-center"
description: "Control center API service"
required: true
},
{
name: "control-center-ui"
path: ($platform_config.source_root | path join "control-center-ui")
binary: "control-center-ui"
description: "Control center web UI"
required: false
},
{
name: "mcp-server"
path: ($platform_config.source_root | path join "provisioning" "mcp-server-rust")
binary: "mcp-server-rust"
description: "MCP integration server"
required: false
}
]
# Validate project existence and Cargo.toml files
let mut validated_projects = []
let mut missing_projects = []
for project in $rust_projects {
let cargo_file = ($project.path | path join "Cargo.toml")
if ($project.path | path exists) and ($cargo_file | path exists) {
# Parse Cargo.toml to get project information
let cargo_data = (open $cargo_file)
let project_info = ($project | insert version $cargo_data.package.version | insert cargo_data $cargo_data)
$validated_projects = ($validated_projects | append $project_info)
} else {
if $project.required {
$missing_projects = ($missing_projects | append $project.name)
}
}
}
if ($missing_projects | length) > 0 {
return {
status: "failed"
reason: $"Missing required projects: ($missing_projects | str join ', ')"
duration: ((date now) - $start_time)
}
}
# Check build environment
let build_env = validate_build_environment $platform_config
{
status: "success"
rust_projects: $validated_projects
missing_projects: $missing_projects
total_projects: ($validated_projects | length)
build_environment: $build_env
duration: ((date now) - $start_time)
}
} catch {|err|
{
status: "failed"
reason: $err.msg
duration: ((date now) - $start_time)
}
}
}
# Validate build environment
def validate_build_environment [platform_config: record] -> record {
let mut build_tools = {}
# Check Rust toolchain
let rust_version = try {
rustc --version | str trim
} catch {
"not available"
}
let cargo_version = try {
cargo --version | str trim
} catch {
"not available"
}
$build_tools = ($build_tools | insert rust $rust_version | insert cargo $cargo_version)
# Check for target platforms
let mut target_availability = {}
for platform in $platform_config.target_platforms {
let target_triple = get_rust_target_triple $platform
let target_installed = try {
rustup target list --installed | lines | any {|line| $line | str contains $target_triple}
} catch {
false
}
$target_availability = ($target_availability | insert $platform $target_installed)
}
# Check optional tools
let upx_available = try {
upx --version | complete | get exit_code | $in == 0
} catch {
false
}
let strip_available = try {
strip --version | complete | get exit_code | $in == 0
} catch {
false
}
let docker_available = try {
docker --version | complete | get exit_code | $in == 0
} catch {
false
}
{
build_tools: $build_tools
target_availability: $target_availability
optional_tools: {
upx: $upx_available
strip: $strip_available
docker: $docker_available
}
rust_ready: ($rust_version != "not available" and $cargo_version != "not available")
}
}
# Build platform binaries for all target platforms
def build_platform_binaries [
platform_config: record
discovery_result: record
] -> record {
log info "Building platform binaries..."
let start_time = (date now)
try {
# Build binaries for each platform
let build_results = if $platform_config.parallel_builds {
build_platforms_parallel $platform_config $discovery_result
} else {
build_platforms_sequential $platform_config $discovery_result
}
let successful_builds = ($build_results | where status == "success" | length)
let total_builds = ($build_results | length)
let failed_builds = ($build_results | where status == "failed")
let status = if $successful_builds == 0 {
"failed"
} else if $successful_builds < $total_builds {
"partial"
} else {
"success"
}
{
status: $status
successful_builds: $successful_builds
total_builds: $total_builds
failed_builds: ($failed_builds | length)
build_results: $build_results
duration: ((date now) - $start_time)
}
} catch {|err|
{
status: "failed"
reason: $err.msg
duration: ((date now) - $start_time)
}
}
}
# Build platforms in parallel (simplified sequential for now)
def build_platforms_parallel [
platform_config: record
discovery_result: record
] -> list {
build_platforms_sequential $platform_config $discovery_result
}
# Build platforms sequentially
def build_platforms_sequential [
platform_config: record
discovery_result: record
] -> list {
let mut all_build_results = []
for platform in $platform_config.target_platforms {
for project in $discovery_result.rust_projects {
let build_result = build_single_binary $platform $project $platform_config
$all_build_results = ($all_build_results | append $build_result)
}
}
return $all_build_results
}
# Build a single binary for a specific platform
def build_single_binary [
platform: string
project: record
platform_config: record
] -> record {
log info $"Building ($project.name) for ($platform)..."
let start_time = (date now)
let target_triple = get_rust_target_triple $platform
try {
cd $project.path
# Build cargo command
let mut cargo_cmd = ["cargo", "build"]
# Add build mode
if $platform_config.build_mode == "release" or $platform_config.build_mode == "optimized" {
$cargo_cmd = ($cargo_cmd | append "--release")
}
# Add target platform
$cargo_cmd = ($cargo_cmd | append ["--target", $target_triple])
# Add optimization flags for optimized builds
if $platform_config.build_mode == "optimized" {
$env.RUSTFLAGS = "-C target-cpu=native -C opt-level=3 -C lto=fat"
}
# Execute build
if $platform_config.verbose {
log info $"Running: ($cargo_cmd | str join ' ')"
}
let build_result = (run-external --redirect-combine $cargo_cmd.0 ...$cargo_cmd.1.. | complete)
if $build_result.exit_code == 0 {
# Determine binary path
let profile = if $platform_config.build_mode == "debug" { "debug" } else { "release" }
let binary_path = ($project.path | path join "target" $target_triple $profile $project.binary)
# Add .exe extension for Windows
let final_binary_path = if ($platform | str contains "windows") {
$binary_path + ".exe"
} else {
$binary_path
}
if ($final_binary_path | path exists) {
# Copy to output directory with platform suffix
let output_name = $"($project.binary)-($platform)"
let output_path = ($platform_config.output_dir | path join $output_name)
cp $final_binary_path $output_path
let binary_size = (ls $output_path | get 0.size)
{
project: $project.name
platform: $platform
target: $target_triple
status: "success"
binary_path: $output_path
binary_size: $binary_size
duration: ((date now) - $start_time)
}
} else {
{
project: $project.name
platform: $platform
target: $target_triple
status: "failed"
reason: $"binary not found at ($final_binary_path)"
duration: ((date now) - $start_time)
}
}
} else {
{
project: $project.name
platform: $platform
target: $target_triple
status: "failed"
reason: $build_result.stderr
duration: ((date now) - $start_time)
}
}
} catch {|err|
{
project: $project.name
platform: $platform
target: $target_triple
status: "failed"
reason: $err.msg
duration: ((date now) - $start_time)
}
}
}
# Get Rust target triple for platform
def get_rust_target_triple [platform: string] -> string {
match $platform {
"linux-amd64" => "x86_64-unknown-linux-gnu"
"linux-arm64" => "aarch64-unknown-linux-gnu"
"macos-amd64" => "x86_64-apple-darwin"
"macos-arm64" => "aarch64-apple-darwin"
"windows-amd64" => "x86_64-pc-windows-gnu"
"windows-arm64" => "aarch64-pc-windows-msvc"
_ => $platform # Assume it's already a target triple
}
}
# Post-process binaries (strip, compress, sign)
def post_process_binaries [
platform_config: record
build_result: record
] -> record {
log info "Post-processing binaries..."
let start_time = (date now)
try {
let successful_builds = ($build_result.build_results | where status == "success")
let mut processing_results = []
for build in $successful_builds {
let processing_result = process_single_binary $build $platform_config
$processing_results = ($processing_results | append $processing_result)
}
let successful_processing = ($processing_results | where status == "success" | length)
let total_processing = ($processing_results | length)
{
status: (if $successful_processing == $total_processing { "success" } else { "partial" })
processed_binaries: $successful_processing
total_binaries: $total_processing
processing_results: $processing_results
duration: ((date now) - $start_time)
}
} catch {|err|
{
status: "failed"
reason: $err.msg
duration: ((date now) - $start_time)
}
}
}
# Process a single binary
def process_single_binary [
build: record
platform_config: record
] -> record {
let mut processing_steps = []
let binary_path = $build.binary_path
try {
let original_size = $build.binary_size
# Strip debug symbols if requested
if $platform_config.strip_symbols and not ($build.platform | str contains "windows") {
let strip_result = strip_binary_symbols $binary_path $platform_config
$processing_steps = ($processing_steps | append { step: "strip", result: $strip_result })
}
# UPX compress if requested
let mut final_size = $original_size
if $platform_config.upx_compress {
let upx_result = upx_compress_binary $binary_path $platform_config
$processing_steps = ($processing_steps | append { step: "upx", result: $upx_result })
if $upx_result.status == "success" {
$final_size = $upx_result.compressed_size
}
}
# Sign binary if requested
if $platform_config.sign_binaries {
let sign_result = sign_binary $binary_path $platform_config
$processing_steps = ($processing_steps | append { step: "sign", result: $sign_result })
}
# Update final size
let current_size = (ls $binary_path | get 0.size)
let compression_ratio = (($current_size | into float) / ($original_size | into float) * 100)
{
project: $build.project
platform: $build.platform
binary_path: $binary_path
status: "success"
original_size: $original_size
final_size: $current_size
compression_ratio: $compression_ratio
processing_steps: $processing_steps
}
} catch {|err|
{
project: $build.project
platform: $build.platform
binary_path: $binary_path
status: "failed"
reason: $err.msg
processing_steps: $processing_steps
}
}
}
# Strip debug symbols from binary
def strip_binary_symbols [binary_path: string, platform_config: record] -> record {
try {
if $platform_config.verbose {
log info $"Stripping symbols from: ($binary_path)"
}
let strip_result = (strip $binary_path | complete)
if $strip_result.exit_code == 0 {
{ status: "success", stripped: true }
} else {
{ status: "failed", reason: $strip_result.stderr }
}
} catch {|err|
{ status: "failed", reason: $err.msg }
}
}
# Compress binary with UPX
def upx_compress_binary [binary_path: string, platform_config: record] -> record {
try {
if $platform_config.verbose {
log info $"UPX compressing: ($binary_path)"
}
let original_size = (ls $binary_path | get 0.size)
let upx_result = (upx --best $binary_path | complete)
if $upx_result.exit_code == 0 {
let compressed_size = (ls $binary_path | get 0.size)
let ratio = (($compressed_size | into float) / ($original_size | into float) * 100)
{
status: "success"
compressed: true
original_size: $original_size
compressed_size: $compressed_size
compression_ratio: $ratio
}
} else {
{ status: "failed", reason: $upx_result.stderr }
}
} catch {|err|
{ status: "failed", reason: $err.msg }
}
}
# Sign binary (placeholder - would need actual signing implementation)
def sign_binary [binary_path: string, platform_config: record] -> record {
log warning "Binary signing not implemented - skipping"
{ status: "skipped", reason: "signing not implemented" }
}
# Generate service definitions
def generate_service_definitions [
platform_config: record
build_result: record
] -> record {
log info "Generating service definitions..."
let start_time = (date now)
try {
let services_dir = ($platform_config.output_dir | path join "services")
mkdir $services_dir
let successful_builds = ($build_result.build_results | where status == "success")
let mut generated_services = []
# Generate systemd service files
for build in $successful_builds {
if $build.platform | str starts-with "linux" {
let systemd_service = generate_systemd_service $build $platform_config
let service_file = ($services_dir | path join $"($build.project).service")
$systemd_service | save $service_file
$generated_services = ($generated_services | append {
project: $build.project
platform: $build.platform
service_type: "systemd"
service_file: $service_file
})
}
}
# Generate Docker Compose definitions
let docker_compose = generate_docker_compose $successful_builds $platform_config
let compose_file = ($services_dir | path join "docker-compose.yml")
$docker_compose | save $compose_file
{
status: "success"
services_generated: ($generated_services | length)
services_directory: $services_dir
generated_services: $generated_services
docker_compose: $compose_file
duration: ((date now) - $start_time)
}
} catch {|err|
{
status: "failed"
reason: $err.msg
duration: ((date now) - $start_time)
}
}
}
# Generate systemd service file
def generate_systemd_service [build: record, platform_config: record] -> string {
$"[Unit]
Description=Provisioning ($build.project | str title-case) Service
After=network.target
Wants=network.target
[Service]
Type=simple
User=provisioning
Group=provisioning
ExecStart=/usr/local/bin/($build.project | str replace "_" "-")
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=provisioning-($build.project)
# Security settings
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/provisioning /var/log/provisioning
[Install]
WantedBy=multi-user.target
"
}
# Generate Docker Compose file
def generate_docker_compose [builds: list, platform_config: record] -> string {
let mut services = []
for build in $builds {
let service_def = $" ($build.project | str replace "_" "-"):
image: provisioning/($build.project):latest
restart: unless-stopped
ports:
- \"8080:8080\" # Adjust port as needed
volumes:
- provisioning-data:/data
- ./config:/etc/provisioning:ro
environment:
- PROVISIONING_ENV=production
healthcheck:
test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8080/health\"]
interval: 30s
timeout: 10s
retries: 3"
$services = ($services | append $service_def)
}
let compose_content = $"version: '3.8'
services:
($services | str join "\n\n")
volumes:
provisioning-data:
driver: local
networks:
default:
name: provisioning
"
return $compose_content
}
# Create container images
def create_container_images [
platform_config: record
build_result: record
] -> record {
log info "Creating container images..."
let start_time = (date now)
# Container creation would use the build-containers.nu tool
let containers_result = try {
nu ($platform_config.source_root | path join "src" "tools" "package" "build-containers.nu")
--dist-dir ($platform_config.output_dir | path dirname)
--tag-prefix "provisioning"
--platforms "linux/amd64"
--verbose:$platform_config.verbose
} catch {|err|
{
status: "failed"
reason: $err.msg
}
}
{
status: $containers_result.status
containers_created: (if "successful_builds" in ($containers_result | columns) { $containers_result.successful_builds } else { 0 })
container_results: $containers_result
duration: ((date now) - $start_time)
}
}
# Generate platform metadata
def generate_platform_metadata [
platform_config: record
preparation_results: list
] -> record {
log info "Generating platform metadata..."
let start_time = (date now)
try {
let metadata = {
name: "provisioning-platform"
version: (detect_version $platform_config.source_root)
type: "platform-distribution"
created_at: (date now)
created_by: "platform-distribution-tool"
build_configuration: $platform_config
target_platforms: $platform_config.target_platforms
preparation_phases: ($preparation_results | each {|r|
{
phase: $r.phase
status: $r.result.status
duration: (if "duration" in ($r.result | columns) { $r.result.duration } else { 0 })
}
})
build_statistics: {
total_phases: ($preparation_results | length)
successful_phases: ($preparation_results | where {|r| $r.result.status == "success"} | length)
distribution_size: (get_directory_size $platform_config.output_dir)
}
}
let metadata_file = ($platform_config.output_dir | path join "platform-metadata.json")
$metadata | to json | save $metadata_file
{
status: "success"
metadata_file: $metadata_file
metadata: $metadata
duration: ((date now) - $start_time)
}
} catch {|err|
{
status: "failed"
reason: $err.msg
duration: ((date now) - $start_time)
}
}
}
# Detect version from git or other sources
def detect_version [repo_root: string] -> string {
cd $repo_root
try {
let git_version = (git describe --tags --always --dirty 2>/dev/null | str trim)
if $git_version != "" {
return ($git_version | str replace "^v" "")
}
return $"dev-(date now | format date '%Y%m%d')"
} catch {
return "unknown"
}
}
# Get directory size helper
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
}
}
# Show platform distribution status
def "main status" [] {
let repo_root = ($env.PWD | path dirname | path dirname | path dirname)
let version = (detect_version $repo_root)
# Check for platform components
let orchestrator_exists = (($repo_root | path join "orchestrator" "Cargo.toml") | path exists)
let control_center_exists = (($repo_root | path join "control-center" "Cargo.toml") | path exists)
let build_env = validate_build_environment {
target_platforms: ["linux-amd64", "macos-amd64", "windows-amd64"]
}
{
repository: $repo_root
version: $version
platform_components: {
orchestrator: $orchestrator_exists
control_center: $control_center_exists
}
build_environment: $build_env
ready_for_build: ($build_env.rust_ready and $orchestrator_exists and $control_center_exists)
supported_platforms: ["linux-amd64", "linux-arm64", "macos-amd64", "macos-arm64", "windows-amd64"]
}
}
# Quick platform build for single target
def "main quick" [
--platform: string = "linux-amd64" # Single platform to build
--output-dir: string = "dist/platform" # Output directory
] {
main --target-platforms $platform --output-dir $output_dir --parallel-builds:false
}