
- 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>
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
|
||
} |