nushell-plugins/scripts/build_docker_cross.nu
Jesús Pérez c5b510b939 feat: modularize justfile and fix collection/packaging scripts
- Modularize justfile system with dedicated modules:
  * alias.just: Command aliases (h, b, c, s)
  * build.just: Build and cross-compilation commands
  * distro.just: Collection and packaging commands
  * help.just: Comprehensive help system with areas
  * qa.just: Testing and quality assurance
  * tools.just: Development tools and utilities
  * upstream.just: Repository tracking and sync

- Fix platform detection in collect script:
  * Use actual platform names (darwin-arm64) instead of generic "host"
  * Support both "host" argument and auto-detection
  * Filter out .d dependency files from distribution

- Fix packaging script issues:
  * Correct uname command syntax (^uname -m)
  * Fix string interpolation and environment parsing
  * Include plugin binaries in archives (was only packaging metadata)
  * Use proper path join instead of string interpolation
  * Add --force flag to avoid interactive prompts

- Fix justfile absolute paths:
  * Replace relative paths with {{justfile_directory()}} function
  * Enable commands to work from any directory

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-20 19:04:08 +01:00

476 lines
15 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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
}