476 lines
15 KiB
Plaintext
476 lines
15 KiB
Plaintext
![]() |
#!/usr/bin/env nu
|
|||
|
|
|||
|
# Docker Cross-Compilation Manager for Nushell Plugins
|
|||
|
# Manages Docker-based cross-compilation environment and builds
|
|||
|
|
|||
|
# Load build targets configuration
|
|||
|
def load_targets_config [] {
|
|||
|
if not ("etc/build_targets.toml" | path exists) {
|
|||
|
error make {msg: "Build targets configuration not found: etc/build_targets.toml"}
|
|||
|
}
|
|||
|
open etc/build_targets.toml
|
|||
|
}
|
|||
|
|
|||
|
# Check if Docker is available and running
|
|||
|
def check_docker [] {
|
|||
|
try {
|
|||
|
let version = (docker --version | complete)
|
|||
|
if $version.exit_code != 0 {
|
|||
|
return {available: false, reason: "Docker command failed"}
|
|||
|
}
|
|||
|
|
|||
|
let info = (docker info | complete)
|
|||
|
if $info.exit_code != 0 {
|
|||
|
return {available: false, reason: "Docker daemon not running"}
|
|||
|
}
|
|||
|
|
|||
|
{available: true, reason: null}
|
|||
|
} catch {|err|
|
|||
|
{available: false, reason: $"Docker not found: ($err.msg)"}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
# Build the Docker cross-compilation image
|
|||
|
def build_docker_image [
|
|||
|
--force (-f) # Force rebuild of image
|
|||
|
--no-cache # Don't use Docker build cache
|
|||
|
--verbose (-v) # Verbose output
|
|||
|
] {
|
|||
|
let config = load_targets_config
|
|||
|
let docker_config = $config.docker
|
|||
|
let dockerfile = $docker_config.dockerfile
|
|||
|
let image_tag = $docker_config.image_tag
|
|||
|
|
|||
|
print $"🐳 Building Docker cross-compilation image..."
|
|||
|
print $"📄 Dockerfile: ($dockerfile)"
|
|||
|
print $"🏷️ Tag: ($image_tag)"
|
|||
|
|
|||
|
# Check if Dockerfile exists
|
|||
|
if not ($dockerfile | path exists) {
|
|||
|
error make {msg: $"Dockerfile not found: ($dockerfile)"}
|
|||
|
}
|
|||
|
|
|||
|
# Check if image already exists
|
|||
|
let existing_image = (docker images $image_tag --format "{{.Repository}}:{{.Tag}}" | complete)
|
|||
|
if $existing_image.exit_code == 0 and ($existing_image.stdout | str trim | str length) > 0 and not $force {
|
|||
|
print $"✅ Docker image already exists: ($image_tag)"
|
|||
|
print "💡 Use --force to rebuild or --no-cache for fresh build"
|
|||
|
return {success: true, image: $image_tag, rebuilt: false}
|
|||
|
}
|
|||
|
|
|||
|
let start_time = date now
|
|||
|
|
|||
|
try {
|
|||
|
mut build_args = ["docker", "build", "-f", $dockerfile, "-t", $image_tag]
|
|||
|
|
|||
|
if $no_cache {
|
|||
|
$build_args = ($build_args | append "--no-cache")
|
|||
|
}
|
|||
|
|
|||
|
# Add cache-from if specified
|
|||
|
if ($docker_config.cache_from? | default [] | length) > 0 {
|
|||
|
for cache in $docker_config.cache_from {
|
|||
|
$build_args = ($build_args | append ["--cache-from", $cache])
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
# Add build args if specified
|
|||
|
if ($docker_config.build_args? | default [] | length) > 0 {
|
|||
|
for arg in $docker_config.build_args {
|
|||
|
$build_args = ($build_args | append ["--build-arg", $arg])
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$build_args = ($build_args | append ".")
|
|||
|
|
|||
|
if $verbose {
|
|||
|
print $"🔨 Build command: ($build_args | str join ' ')"
|
|||
|
}
|
|||
|
|
|||
|
print "🔨 Building Docker image (this may take several minutes)..."
|
|||
|
let result = (run-external "docker" ...($build_args | skip 1))
|
|||
|
|
|||
|
let end_time = date now
|
|||
|
let duration = $end_time - $start_time
|
|||
|
|
|||
|
print $"✅ Docker image built successfully: ($image_tag)"
|
|||
|
print $"⏱️ Build time: ($duration)"
|
|||
|
|
|||
|
# Show image info
|
|||
|
let image_info = (docker images $image_tag --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}")
|
|||
|
print $"📊 Image info:"
|
|||
|
print $image_info
|
|||
|
|
|||
|
{success: true, image: $image_tag, rebuilt: true, duration: $duration}
|
|||
|
} catch {|err|
|
|||
|
print $"❌ Failed to build Docker image: ($err.msg)"
|
|||
|
{success: false, image: null, error: $err.msg}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
# Get all plugin directories
|
|||
|
def get_plugin_directories [] {
|
|||
|
glob "nu_plugin_*" | where ($it | path type) == "dir"
|
|||
|
}
|
|||
|
|
|||
|
# Build a single plugin in Docker for a specific target
|
|||
|
def build_plugin_in_docker [
|
|||
|
plugin_dir: string,
|
|||
|
target_name: string,
|
|||
|
image_tag: string,
|
|||
|
verbose: bool = false
|
|||
|
] {
|
|||
|
let config = load_targets_config
|
|||
|
let target_config = $config.targets | get $target_name
|
|||
|
|
|||
|
print $"🐳 Building ($plugin_dir) for ($target_name) in Docker..."
|
|||
|
|
|||
|
let start_time = date now
|
|||
|
let work_dir = "/workspace"
|
|||
|
let source_dir = $"/workspace/($plugin_dir)"
|
|||
|
|
|||
|
# Prepare Docker run command
|
|||
|
mut docker_args = [
|
|||
|
"docker", "run", "--rm",
|
|||
|
"-v", $"(pwd):($work_dir)",
|
|||
|
"-w", $source_dir,
|
|||
|
$image_tag
|
|||
|
]
|
|||
|
|
|||
|
# Set environment variables for the target
|
|||
|
let env_vars = $config.environment? | default {}
|
|||
|
let target_env = $env_vars | get $target_name? | default {}
|
|||
|
|
|||
|
for var in ($target_env | transpose key value) {
|
|||
|
$docker_args = ($docker_args | append ["-e", $"($var.key)=($var.value)"])
|
|||
|
}
|
|||
|
|
|||
|
# Add common environment variables
|
|||
|
$docker_args = ($docker_args | append [
|
|||
|
"-e", "RUST_BACKTRACE=1",
|
|||
|
"-e", "CARGO_INCREMENTAL=0"
|
|||
|
])
|
|||
|
|
|||
|
# Build command
|
|||
|
let build_cmd = ["cargo", "build", "--release", "--target", $target_config.rust_target]
|
|||
|
$docker_args = ($docker_args | append $build_cmd)
|
|||
|
|
|||
|
try {
|
|||
|
if $verbose {
|
|||
|
print $" 🔨 Docker command: ($docker_args | str join ' ')"
|
|||
|
}
|
|||
|
|
|||
|
let result = (run-external "docker" ...($docker_args | skip 1))
|
|||
|
|
|||
|
let end_time = date now
|
|||
|
let duration = $end_time - $start_time
|
|||
|
|
|||
|
# Check if binary was created
|
|||
|
let binary_name = if ($target_config | get binary_extension? | default "") != "" {
|
|||
|
$"($plugin_dir)($target_config.binary_extension)"
|
|||
|
} else {
|
|||
|
$plugin_dir
|
|||
|
}
|
|||
|
|
|||
|
let binary_path = $"($plugin_dir)/target/($target_config.rust_target)/release/($binary_name)"
|
|||
|
|
|||
|
if ($binary_path | path exists) {
|
|||
|
let size = (ls $binary_path | get 0.size)
|
|||
|
print $"✅ Built ($plugin_dir) for ($target_name) in ($duration) → ($size)"
|
|||
|
|
|||
|
{
|
|||
|
plugin: $plugin_dir,
|
|||
|
target: $target_name,
|
|||
|
status: "success",
|
|||
|
duration: $duration,
|
|||
|
binary_path: $binary_path,
|
|||
|
size: $size,
|
|||
|
error: null
|
|||
|
}
|
|||
|
} else {
|
|||
|
error make {msg: $"Binary not found at expected path: ($binary_path)"}
|
|||
|
}
|
|||
|
} catch {|err|
|
|||
|
let end_time = date now
|
|||
|
let duration = $end_time - $start_time
|
|||
|
|
|||
|
print $"❌ Error building ($plugin_dir) for ($target_name): ($err.msg)"
|
|||
|
|
|||
|
{
|
|||
|
plugin: $plugin_dir,
|
|||
|
target: $target_name,
|
|||
|
status: "error",
|
|||
|
duration: $duration,
|
|||
|
binary_path: null,
|
|||
|
size: null,
|
|||
|
error: $err.msg
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
# Clean up Docker build artifacts
|
|||
|
def cleanup_docker [
|
|||
|
--images (-i) # Remove Docker images
|
|||
|
--containers (-c) # Remove stopped containers
|
|||
|
--volumes (-v) # Remove unused volumes
|
|||
|
--all (-a) # Clean everything
|
|||
|
] {
|
|||
|
print "🧹 Cleaning up Docker artifacts..."
|
|||
|
|
|||
|
if $all or $containers {
|
|||
|
print "🗑️ Removing stopped containers..."
|
|||
|
try {
|
|||
|
docker container prune -f
|
|||
|
} catch {
|
|||
|
print "⚠️ No containers to remove"
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if $all or $volumes {
|
|||
|
print "🗑️ Removing unused volumes..."
|
|||
|
try {
|
|||
|
docker volume prune -f
|
|||
|
} catch {
|
|||
|
print "⚠️ No volumes to remove"
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if $all or $images {
|
|||
|
print "🗑️ Removing unused images..."
|
|||
|
try {
|
|||
|
docker image prune -f
|
|||
|
} catch {
|
|||
|
print "⚠️ No images to remove"
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
print "✅ Docker cleanup completed"
|
|||
|
}
|
|||
|
|
|||
|
# Show Docker environment information
|
|||
|
def show_docker_info [] {
|
|||
|
let docker_status = check_docker
|
|||
|
|
|||
|
print "🐳 Docker Environment Information"
|
|||
|
print $"Status: (if $docker_status.available { '✅ Available' } else { '❌ Not available' })"
|
|||
|
|
|||
|
if not $docker_status.available {
|
|||
|
print $"Reason: ($docker_status.reason)"
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
try {
|
|||
|
print "\n📊 Docker Version:"
|
|||
|
docker --version
|
|||
|
|
|||
|
print "\n💾 Docker System Info:"
|
|||
|
docker system df
|
|||
|
|
|||
|
print "\n🖼️ Available Images:"
|
|||
|
docker images --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}"
|
|||
|
|
|||
|
let config = load_targets_config
|
|||
|
let image_tag = $config.docker.image_tag
|
|||
|
|
|||
|
print $"\n🎯 Cross-compilation Image Status:"
|
|||
|
let image_exists = (docker images $image_tag --format "{{.Repository}}:{{.Tag}}" | complete)
|
|||
|
if $image_exists.exit_code == 0 and ($image_exists.stdout | str trim | str length) > 0 {
|
|||
|
print $"✅ Image exists: ($image_tag)"
|
|||
|
let image_info = (docker images $image_tag --format "table {{.Size}}\t{{.CreatedAt}}")
|
|||
|
print $image_info
|
|||
|
} else {
|
|||
|
print $"❌ Image not found: ($image_tag)"
|
|||
|
print "💡 Run with --build-image to create it"
|
|||
|
}
|
|||
|
} catch {|err|
|
|||
|
print $"❌ Error getting Docker info: ($err.msg)"
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
# Main function
|
|||
|
def main [
|
|||
|
--target (-t): string = "" # Specific target to build for
|
|||
|
--plugin (-p): string = "" # Specific plugin to build
|
|||
|
--build-image (-b) # Build Docker image before building
|
|||
|
--force-rebuild (-f) # Force rebuild of Docker image
|
|||
|
--no-cache # Don't use Docker build cache
|
|||
|
--parallel # Build plugins in parallel
|
|||
|
--cleanup # Clean up Docker artifacts after build
|
|||
|
--info (-i) # Show Docker environment info
|
|||
|
--verbose (-v) # Verbose output
|
|||
|
--dry-run # Show what would be built
|
|||
|
] {
|
|||
|
# Ensure we're in the repository root
|
|||
|
if not ("nu_plugin_clipboard" | path exists) {
|
|||
|
error make {msg: "Please run this script from the nushell-plugins repository root directory"}
|
|||
|
}
|
|||
|
|
|||
|
print "🐳 Docker Cross-Compilation Manager for Nushell Plugins"
|
|||
|
|
|||
|
# Check Docker availability
|
|||
|
let docker_status = check_docker
|
|||
|
if not $docker_status.available {
|
|||
|
print $"❌ Docker not available: ($docker_status.reason)"
|
|||
|
print "💡 Please install and start Docker to use cross-compilation"
|
|||
|
exit 1
|
|||
|
}
|
|||
|
|
|||
|
# Info mode
|
|||
|
if $info {
|
|||
|
show_docker_info
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
let config = load_targets_config
|
|||
|
let image_tag = $config.docker.image_tag
|
|||
|
|
|||
|
# Build Docker image if requested or if it doesn't exist
|
|||
|
let image_exists = (docker images $image_tag --format "{{.Repository}}:{{.Tag}}" | complete)
|
|||
|
let should_build_image = $build_image or $force_rebuild or (
|
|||
|
$image_exists.exit_code != 0 or ($image_exists.stdout | str trim | str length) == 0
|
|||
|
)
|
|||
|
|
|||
|
if $should_build_image {
|
|||
|
let build_result = build_docker_image --force=$force_rebuild --no-cache=$no_cache --verbose=$verbose
|
|||
|
if not $build_result.success {
|
|||
|
print "❌ Failed to build Docker image, cannot proceed"
|
|||
|
exit 1
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
# Determine targets and plugins
|
|||
|
let available_targets = $config.targets | transpose name config | where ($it.config | get enabled? | default true)
|
|||
|
let docker_targets = $available_targets | where $it.config.docker_required
|
|||
|
|
|||
|
let targets_to_build = if ($target | str length) > 0 {
|
|||
|
let selected = $docker_targets | where $it.name == $target
|
|||
|
if ($selected | length) == 0 {
|
|||
|
print $"❌ Target not found or doesn't require Docker: ($target)"
|
|||
|
print $"💡 Available Docker targets: ($docker_targets | get name | str join ', ')"
|
|||
|
exit 1
|
|||
|
}
|
|||
|
$selected
|
|||
|
} else {
|
|||
|
$docker_targets
|
|||
|
}
|
|||
|
|
|||
|
let all_plugins = get_plugin_directories
|
|||
|
let plugins_to_build = if ($plugin | str length) > 0 {
|
|||
|
let selected = $all_plugins | where $it == $plugin
|
|||
|
if ($selected | length) == 0 {
|
|||
|
print $"❌ Plugin not found: ($plugin)"
|
|||
|
print $"💡 Available plugins: ($all_plugins | str join ', ')"
|
|||
|
exit 1
|
|||
|
}
|
|||
|
$selected
|
|||
|
} else {
|
|||
|
$all_plugins
|
|||
|
}
|
|||
|
|
|||
|
if ($targets_to_build | length) == 0 {
|
|||
|
print "❓ No Docker targets to build"
|
|||
|
print $"💡 Available Docker targets: ($docker_targets | get name | str join ', ')"
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
if ($plugins_to_build | length) == 0 {
|
|||
|
print "❓ No plugins to build"
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
print $"\n📦 Plugins to build: ($plugins_to_build | length)"
|
|||
|
for plugin in $plugins_to_build {
|
|||
|
print $" - ($plugin)"
|
|||
|
}
|
|||
|
|
|||
|
print $"\n🎯 Docker targets: ($targets_to_build | length)"
|
|||
|
for target in $targets_to_build {
|
|||
|
print $" - ($target.name): ($target.config.description)"
|
|||
|
}
|
|||
|
|
|||
|
# Dry run mode
|
|||
|
if $dry_run {
|
|||
|
print "\n🔍 Dry run - showing build matrix:"
|
|||
|
for plugin in $plugins_to_build {
|
|||
|
for target in $targets_to_build {
|
|||
|
print $" 🐳 ($plugin) × ($target.name)"
|
|||
|
}
|
|||
|
}
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
# Build plugins
|
|||
|
print $"\n🔨 Starting Docker builds..."
|
|||
|
let start_time = date now
|
|||
|
mut all_results = []
|
|||
|
|
|||
|
if $parallel {
|
|||
|
print "⚡ Building in parallel mode..."
|
|||
|
let build_jobs = []
|
|||
|
for plugin in $plugins_to_build {
|
|||
|
for target in $targets_to_build {
|
|||
|
$build_jobs = ($build_jobs | append {
|
|||
|
plugin: $plugin,
|
|||
|
target: $target.name
|
|||
|
})
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$all_results = ($build_jobs | par-each {|job|
|
|||
|
build_plugin_in_docker $job.plugin $job.target $image_tag $verbose
|
|||
|
})
|
|||
|
} else {
|
|||
|
# Sequential builds
|
|||
|
for plugin in $plugins_to_build {
|
|||
|
for target in $targets_to_build {
|
|||
|
let result = build_plugin_in_docker $plugin $target.name $image_tag $verbose
|
|||
|
$all_results = ($all_results | append $result)
|
|||
|
print "---"
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
let end_time = date now
|
|||
|
let total_duration = $end_time - $start_time
|
|||
|
|
|||
|
# Summary
|
|||
|
print "\n📊 Docker Build Summary:"
|
|||
|
let successful = $all_results | where status == "success"
|
|||
|
let failed = $all_results | where status == "error"
|
|||
|
|
|||
|
print $"✅ Successful builds: ($successful | length)"
|
|||
|
print $"❌ Failed builds: ($failed | length)"
|
|||
|
print $"⏱️ Total time: ($total_duration)"
|
|||
|
|
|||
|
if ($successful | length) > 0 {
|
|||
|
print "\n✅ Built binaries:"
|
|||
|
for success in $successful {
|
|||
|
print $" - ($success.plugin) → ($success.target): ($success.size)"
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if ($failed | length) > 0 {
|
|||
|
print "\n❌ Failed builds:"
|
|||
|
for failure in $failed {
|
|||
|
print $" - ($failure.plugin) → ($failure.target): ($failure.error)"
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
# Cleanup if requested
|
|||
|
if $cleanup {
|
|||
|
print ""
|
|||
|
cleanup_docker --containers --volumes
|
|||
|
}
|
|||
|
|
|||
|
# Exit with error if any builds failed
|
|||
|
if ($failed | length) > 0 {
|
|||
|
exit 1
|
|||
|
} else {
|
|||
|
print "\n🎉 All Docker builds completed successfully!"
|
|||
|
print "💡 Binaries are available in plugin/target/RUST_TARGET/release/ directories"
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if ($env.NUSHELL_EXECUTION_CONTEXT? | default "" | str contains "run") {
|
|||
|
main
|
|||
|
}
|