Please purchase the course to watch this video.

Full Course
Creating a CLI application to facilitate the building of projects for various operating systems and architectures is an essential skill for developers working with Go. This task involves several stages, starting from a simple implementation and progressing to more complex features. The application should utilize the go build
command with the -o
flag to generate binaries, handle different output directories, and support specific naming conventions, especially for Windows binaries that require the .exe
extension. Key strategies include using the exec command to run builds and parsing the Go distribution list, preferably in JSON format, to manage OS and architecture pairs efficiently. By completing this exercise, developers will enhance their command-line interface skills and gain practical experience in cross-platform building processes, setting the stage for further exploration of advanced topics like CGO integration.
This lesson is going to be a little different from the other ones we've done up to this point. Whereas instead of showing you how to build an application, I'm actually going to show you a built application (an application that I built), and I'm going to ask you to recreate it as homework.
The application I'm talking about is basically what we did in the last lesson when we were looking at how we can build our application for different operating system and architecture pairs.
The Challenge: Build a CLI Application Builder
Your task is to build a CLI application that will help you build other projects for all of the different operating systems and architectures that Go supports.
What You'll Build
A CLI tool called builder
that:
- Takes a Go project directory
- Builds binaries for all supported OS/architecture combinations
- Outputs properly named binaries in an organized structure
- Provides various configuration options via CLI flags
Example Usage
# Build current directory for all platforms
builder .
# Build specific project
builder /path/to/my/project
# Build with custom output directory
builder --output release .
# Build with custom binary name
builder --name myapp .
# Build for specific targets only
builder --target darwin,linux/amd64 .
Demonstration: How the Final Tool Works
Let me show you how the completed builder
application works:
Installation and Basic Usage
# Install the builder tool
go install github.com/dreamsofcode-io/builder@latest
# Navigate to a Go project (e.g., our pwned CLI)
cd /path/to/pwned
# Build for all platforms
builder .
Expected Output Structure
After running builder .
, you'll get:
build/
├── pwned_darwin_amd64
├── pwned_darwin_arm64
├── pwned_linux_386
├── pwned_linux_amd64
├── pwned_linux_arm
├── pwned_linux_arm64
├── pwned_windows_386.exe
├── pwned_windows_amd64.exe
├── pwned_windows_arm64.exe
├── pwned_freebsd_amd64
├── pwned_openbsd_amd64
└── ... (all supported platforms)
Key Features Demonstrated
- Automatic Platform Detection - Builds for all
go tool dist list
platforms - Smart Naming -
{project}_{os}_{arch}[.exe]
format - Windows Handling - Automatically adds
.exe
extension - Organized Output - All builds in
build/
directory - Error Handling - Graceful handling of unsupported combinations
Project Specifications
You need to implement 5 progressive stages, each building upon the previous:
Stage 1: Basic Multi-Platform Building ⭐
Goal: Build a tool that compiles the current directory for all supported Go platforms.
Requirements:
- Use
go tool dist list
to get platform combinations - Build using
go build
with appropriateGOOS
andGOARCH
- Output to
build/
directory - Use naming convention:
{project}_{os}_{arch}[.exe]
- Handle Windows
.exe
extension automatically
Key Implementation Hints:
# Get platform list in JSON format (recommended)
go tool dist list -json
# Example output structure
[
{
"GOOS": "darwin",
"GOARCH": "amd64",
"CgoSupported": true,
"FirstClass": true
},
...
]
Stage 2: Custom Project Directory ⭐⭐
Goal: Allow building projects in different directories.
Requirements:
- Accept directory path as command-line argument
- Determine project name from directory name
- Build binaries with correct project name
- Handle relative and absolute paths
Usage:
builder /path/to/other/project
builder ../my-other-app
Stage 3: Custom Output Directory ⭐⭐⭐
Goal: Add CLI flag to specify output directory.
Requirements:
- Add
--output
or-o
flag - Create output directory if it doesn't exist
- Maintain same naming convention within custom directory
Usage:
builder --output release .
builder -o dist /path/to/project
Stage 4: Custom Binary Name ⭐⭐⭐⭐
Goal: Add CLI flag to override the binary name.
Requirements:
- Add
--name
or-n
flag - Use custom name instead of directory name
- Still append OS/architecture suffix
Usage:
builder --name myapp .
# Produces: myapp_darwin_amd64, myapp_linux_amd64.exe, etc.
builder --name foobar --output release .
# Produces binaries in release/ with foobar prefix
Stage 5: Target Platform Selection ⭐⭐⭐⭐⭐
Goal: Add CLI flag to build only specific platforms.
Requirements:
- Add
--target
or-t
flag - Support multiple target formats:
- OS only:
darwin
,linux
,windows
- OS/Arch pairs:
linux/amd64
,darwin/arm64
- Multiple targets:
darwin,linux/amd64,windows
- OS only:
- Build only specified targets
Usage:
# Build only for Darwin (all architectures)
builder --target darwin .
# Build for specific OS/arch combination
builder --target linux/amd64 .
# Build for multiple targets
builder --target darwin,linux/amd64,windows/amd64 .
Implementation Guide
Required Go Packages
import (
"encoding/json"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
Core Data Structures
Platform Information (from go tool dist list -json
):
type GoDist struct {
GOOS string `json:"GOOS"`
GOARCH string `json:"GOARCH"`
CgoSupported bool `json:"CgoSupported"`
FirstClass bool `json:"FirstClass"`
}
Build Configuration:
type BuildConfig struct {
ProjectDir string
OutputDir string
BinaryName string
Targets []GoDist
}
Key Implementation Steps
Step 1: Get Available Platforms
func getAvailablePlatforms() ([]GoDist, error) {
cmd := exec.Command("go", "tool", "dist", "list", "-json")
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to get platform list: %w", err)
}
var platforms []GoDist
if err := json.Unmarshal(output, &platforms); err != nil {
return nil, fmt.Errorf("failed to parse platform list: %w", err)
}
return platforms, nil
}
Step 2: Build for Single Platform
func buildForPlatform(config BuildConfig, platform GoDist) error {
// Determine output filename
filename := fmt.Sprintf("%s_%s_%s", config.BinaryName, platform.GOOS, platform.GOARCH)
if platform.GOOS == "windows" {
filename += ".exe"
}
outputPath := filepath.Join(config.OutputDir, filename)
// Build command
cmd := exec.Command("go", "build", "-o", outputPath)
cmd.Dir = config.ProjectDir
cmd.Env = append(os.Environ(),
"GOOS="+platform.GOOS,
"GOARCH="+platform.GOARCH,
)
return cmd.Run()
}
Step 3: Handle CLI Flags
func main() {
var (
outputDir = flag.String("output", "build", "Output directory for binaries")
binaryName = flag.String("name", "", "Binary name (defaults to project directory name)")
targets = flag.String("target", "", "Comma-separated list of target platforms")
)
flag.Parse()
// Parse project directory from args
projectDir := "."
if flag.NArg() > 0 {
projectDir = flag.Arg(0)
}
// Implementation continues...
}
Step 4: Parse Target Selection
func parseTargets(targetStr string, allPlatforms []GoDist) []GoDist {
if targetStr == "" {
return allPlatforms // Build all if none specified
}
var selectedPlatforms []GoDist
targets := strings.Split(targetStr, ",")
for _, target := range targets {
target = strings.TrimSpace(target)
if strings.Contains(target, "/") {
// Specific OS/Arch pair
parts := strings.Split(target, "/")
if len(parts) == 2 {
for _, platform := range allPlatforms {
if platform.GOOS == parts[0] && platform.GOARCH == parts[1] {
selectedPlatforms = append(selectedPlatforms, platform)
break
}
}
}
} else {
// OS only - include all architectures
for _, platform := range allPlatforms {
if platform.GOOS == target {
selectedPlatforms = append(selectedPlatforms, platform)
}
}
}
}
return selectedPlatforms
}
Error Handling Considerations
// Handle CGO-related build failures gracefully
func buildForPlatform(config BuildConfig, platform GoDist) error {
cmd := exec.Command("go", "build", "-o", outputPath)
cmd.Dir = config.ProjectDir
cmd.Env = append(os.Environ(),
"GOOS="+platform.GOOS,
"GOARCH="+platform.GOARCH,
"CGO_ENABLED=0", // Disable CGO for cross-compilation
)
if err := cmd.Run(); err != nil {
// Log error but continue with other platforms
fmt.Printf("Failed to build %s/%s: %v\n", platform.GOOS, platform.GOARCH, err)
return err
}
fmt.Printf("Built %s_%s_%s\n", config.BinaryName, platform.GOOS, platform.GOARCH)
return nil
}
Testing Your Implementation
Test Cases
Create test scenarios to verify your implementation:
Test 1: Basic Functionality
cd /tmp
mkdir test-project
cd test-project
go mod init test-app
echo 'package main; import "fmt"; func main() { fmt.Println("Hello World") }' > main.go
# Test your builder
your-builder .
# Verify output
ls build/
# Should contain: test-app_darwin_amd64, test-app_linux_amd64, etc.
Test 2: Custom Output Directory
your-builder --output release .
ls release/
# Should contain binaries in release/ directory
Test 3: Custom Binary Name
your-builder --name hello-world .
ls build/
# Should contain: hello-world_darwin_amd64, hello-world_linux_amd64, etc.
Test 4: Target Selection
# Test OS-only targeting
your-builder --target linux .
ls build/
# Should only contain linux_* binaries
# Test specific OS/Arch targeting
your-builder --target linux/amd64 .
ls build/
# Should only contain test-app_linux_amd64
Verification Script
Create a test script to verify your implementation:
#!/bin/bash
# test-builder.sh
echo "Testing builder implementation..."
# Create test project
mkdir -p /tmp/builder-test
cd /tmp/builder-test
go mod init builder-test
echo 'package main; func main() { println("test") }' > main.go
echo "✓ Created test project"
# Test 1: Basic build
./your-builder .
if [ -d "build" ] && [ "$(ls build/ | wc -l)" -gt 10 ]; then
echo "✓ Basic build works"
else
echo "✗ Basic build failed"
fi
# Test 2: Custom output
./your-builder --output release .
if [ -d "release" ]; then
echo "✓ Custom output directory works"
else
echo "✗ Custom output directory failed"
fi
# Test 3: Custom name
./your-builder --name mytest .
if ls build/mytest_* >/dev/null 2>&1; then
echo "✓ Custom binary name works"
else
echo "✗ Custom binary name failed"
fi
# Test 4: Target selection
./your-builder --target linux .
if ls build/*linux* >/dev/null 2>&1 && ! ls build/*darwin* >/dev/null 2>&1; then
echo "✓ Target selection works"
else
echo "✗ Target selection failed"
fi
echo "Testing complete!"
Common Pitfalls and Solutions
1. CGO Cross-Compilation Issues
Problem: CGO doesn't work with cross-compilation by default.
Solution: Set CGO_ENABLED=0
for all builds.
2. Windows Executable Extension
Problem: Forgetting .exe
extension for Windows binaries.
Solution: Check GOOS == "windows"
and append .exe
.
3. Path Handling
Problem: Incorrect path handling across different operating systems.
Solution: Use filepath
package functions consistently.
4. Empty Target Lists
Problem: Parsing empty or invalid target specifications.
Solution: Validate targets against available platforms list.
5. Directory Creation
Problem: Output directory doesn't exist.
Solution: Use os.MkdirAll()
to create directories recursively.
Advanced Features (Bonus Challenges)
If you complete all 5 stages, consider adding these advanced features:
Parallel Building
// Build platforms concurrently
func buildAllPlatforms(config BuildConfig, platforms []GoDist) {
var wg sync.WaitGroup
semaphore := make(chan struct{}, runtime.NumCPU())
for _, platform := range platforms {
wg.Add(1)
go func(p GoDist) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
buildForPlatform(config, p)
}(platform)
}
wg.Wait()
}
Progress Indication
// Add progress bar using your UI package from earlier lessons
func buildWithProgress(config BuildConfig, platforms []GoDist) {
bar := ui.NewProgressBar()
bar.Start()
for i, platform := range platforms {
buildForPlatform(config, platform)
progress := float64(i+1) / float64(len(platforms))
bar.SetProgress(progress)
}
bar.Stop()
}
Configuration Files
// Support build configuration files
type BuildManifest struct {
Name string `yaml:"name"`
Targets []string `yaml:"targets"`
Output string `yaml:"output"`
LdFlags string `yaml:"ldflags"`
BuildTags string `yaml:"tags"`
}
// Load from builder.yaml or builder.json
Build Metadata
// Include build information in binaries
func buildWithMetadata(config BuildConfig, platform GoDist) error {
version := getGitVersion()
buildTime := time.Now().UTC().Format(time.RFC3339)
ldflags := fmt.Sprintf("-X main.version=%s -X main.buildTime=%s", version, buildTime)
cmd := exec.Command("go", "build", "-ldflags", ldflags, "-o", outputPath)
// ... rest of build command
}
Real-World Applications
Your builder tool has practical applications:
CI/CD Integration
# GitHub Actions workflow
- name: Build binaries
run: |
go install github.com/your-username/builder@latest
builder --output dist .
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: binaries
path: dist/
Release Automation
#!/bin/bash
# release.sh
VERSION=$(git describe --tags --abbrev=0)
builder --output "release/$VERSION" --name "myapp-$VERSION" .
Development Workflow
# Quick test builds for specific platforms
builder --target "$(go env GOOS)/$(go env GOARCH)" .
Summary
This project combines many concepts from throughout the module:
Skills Applied:
- Command Execution - Using
exec.Command
to run Go build tools - JSON Processing - Parsing
go tool dist list -json
output - CLI Design - Creating intuitive command-line interfaces
- File Operations - Managing output directories and file names
- Error Handling - Graceful failure handling for unsupported platforms
- Cross-Platform Development - Understanding GOOS/GOARCH combinations
Real-World Value:
- Automation - Eliminates manual cross-compilation steps
- Consistency - Standardized binary naming and organization
- Efficiency - Builds all platforms with a single command
- Flexibility - Configurable for different use cases
Professional Development:
- Tool Building - Creating tools that solve real problems
- API Design - Thoughtful command-line interface design
- Testing - Comprehensive testing across different scenarios
Next Steps
Once you complete this project:
- Submit your implementation for review
- Test with different Go projects to ensure robustness
- Consider publishing your builder tool for others to use
- Move to the next lesson on CGO and the
-ldflags
we'll explore
Remember: This is a substantial project that brings together everything you've learned. Take your time, test thoroughly, and don't hesitate to reference the provided code if you get stuck!
In the next lesson, we'll explore CGO and the CGO_ENABLED
flag, which you may have noticed in some of the build errors during development.
Additional Resources
- Reference Implementation - Complete builder source code (check after attempting)
- Go Build Documentation - Official documentation for build process
- Cross-Compilation Guide - Detailed Go cross-compilation techniques
- CLI Design Patterns - Best practices for command-line tool design
Good luck building your application builder! 🛠️
Build a CLI Application Builder Tool
This lesson is different - instead of showing you how to build an application, you need to recreate the demonstrated application as homework.
Build a CLI tool called builder
that helps you build Go projects for all supported OS/architecture combinations.
Project Overview
Your task is to implement 5 progressive stages, each building upon the previous:
Stage 1: Basic Multi-Platform Building ⭐
Goal: Build a tool that compiles the current directory for all supported Go platforms.
Requirements:
- Use
go tool dist list
to get platform combinations (recommend-json
flag) - Build using
go build
with appropriateGOOS
andGOARCH
- Output to
build/
directory - Use naming convention:
{project}_{os}_{arch}[.exe]
- Handle Windows
.exe
extension automatically
Key Implementation Hints:
# Get platform list in JSON format (recommended)
go tool dist list -json
Expected Output Structure:
build/
├── pwned_darwin_amd64
├── pwned_darwin_arm64
├── pwned_linux_386
├── pwned_linux_amd64
├── pwned_windows_amd64.exe
└── ... (all supported platforms)
Stage 2: Custom Project Directory ⭐⭐
Goal: Allow building projects in different directories.
Requirements:
- Accept directory path as command-line argument
- Determine project name from directory name
- Build binaries with correct project name
- Handle relative and absolute paths
Usage:
builder /path/to/other/project
builder ../my-other-app
Stage 3: Custom Output Directory ⭐⭐⭐
Goal: Add CLI flag to specify output directory.
Requirements:
- Add
--output
or-o
flag - Create output directory if it doesn't exist
- Maintain same naming convention within custom directory
Usage:
builder --output release .
builder -o dist /path/to/project
Stage 4: Custom Binary Name ⭐⭐⭐⭐
Goal: Add CLI flag to override the binary name.
Requirements:
- Add
--name
or-n
flag - Use custom name instead of directory name
- Still append OS/architecture suffix
Usage:
builder --name myapp .
# Produces: myapp_darwin_amd64, myapp_linux_amd64.exe, etc.
builder --name foobar --output release .
# Produces binaries in release/ with foobar prefix
Stage 5: Target Platform Selection ⭐⭐⭐⭐⭐
Goal: Add CLI flag to build only specific platforms.
Requirements:
- Add
--target
or-t
flag - Support multiple target formats:
- OS only:
darwin
,linux
,windows
- OS/Arch pairs:
linux/amd64
,darwin/arm64
- Multiple targets:
darwin,linux/amd64,windows
- OS only:
- Build only specified targets
Usage:
# Build only for Darwin (all architectures)
builder --target darwin .
# Build for specific OS/arch combination
builder --target linux/amd64 .
# Build for multiple targets
builder --target darwin,linux/amd64,windows/amd64 .
Implementation Guide
Required Go Packages
import (
"encoding/json"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
Core Data Structures
Platform Information (from go tool dist list -json
):
type GoDist struct {
GOOS string `json:"GOOS"`
GOARCH string `json:"GOARCH"`
CgoSupported bool `json:"CgoSupported"`
FirstClass bool `json:"FirstClass"`
}
Build Configuration:
type BuildConfig struct {
ProjectDir string
OutputDir string
BinaryName string
Targets []GoDist
}
Key Implementation Steps
Step 1: Get Available Platforms
func getAvailablePlatforms() ([]GoDist, error) {
cmd := exec.Command("go", "tool", "dist", "list", "-json")
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to get platform list: %w", err)
}
var platforms []GoDist
if err := json.Unmarshal(output, &platforms); err != nil {
return nil, fmt.Errorf("failed to parse platform list: %w", err)
}
return platforms, nil
}
Step 2: Build for Single Platform
func buildForPlatform(config BuildConfig, platform GoDist) error {
// Determine output filename
filename := fmt.Sprintf("%s_%s_%s", config.BinaryName, platform.GOOS, platform.GOARCH)
if platform.GOOS == "windows" {
filename += ".exe"
}
outputPath := filepath.Join(config.OutputDir, filename)
// Build command
cmd := exec.Command("go", "build", "-o", outputPath)
cmd.Dir = config.ProjectDir
cmd.Env = append(os.Environ(),
"GOOS="+platform.GOOS,
"GOARCH="+platform.GOARCH,
"CGO_ENABLED=0", // Disable CGO for cross-compilation
)
return cmd.Run()
}
Testing Your Implementation
Test Verification Script
#!/bin/bash
# test-builder.sh
echo "Testing builder implementation..."
# Create test project
mkdir -p /tmp/builder-test
cd /tmp/builder-test
go mod init builder-test
echo 'package main; func main() { println("test") }' > main.go
echo "✓ Created test project"
# Test 1: Basic build
./your-builder .
if [ -d "build" ] && [ "$(ls build/ | wc -l)" -gt 10 ]; then
echo "✓ Basic build works"
else
echo "✗ Basic build failed"
fi
# Test 2: Custom output
./your-builder --output release .
if [ -d "release" ]; then
echo "✓ Custom output directory works"
else
echo "✗ Custom output directory failed"
fi
# Test 3: Custom name
./your-builder --name mytest .
if ls build/mytest_* >/dev/null 2>&1; then
echo "✓ Custom binary name works"
else
echo "✗ Custom binary name failed"
fi
# Test 4: Target selection
./your-builder --target linux .
if ls build/*linux* >/dev/null 2>&1 && ! ls build/*darwin* >/dev/null 2>&1; then
echo "✓ Target selection works"
else
echo "✗ Target selection failed"
fi
echo "Testing complete!"
Common Pitfalls and Solutions
- CGO Cross-Compilation Issues - Set
CGO_ENABLED=0
for all builds - Windows Executable Extension - Check
GOOS == "windows"
and append.exe
- Path Handling - Use
filepath
package functions consistently - Empty Target Lists - Validate targets against available platforms list
- Directory Creation - Use
os.MkdirAll()
to create directories recursively
Bonus Challenges (Advanced Features)
If you complete all 5 stages, consider adding:
- Parallel Building - Build platforms concurrently
- Progress Indication - Show build progress using your UI package
- Configuration Files - Support
builder.yaml
configuration - Build Metadata - Include version and build time in binaries
Final Goal Usage Examples
# Basic usage
builder .
# Build specific project with custom output
builder --output release /path/to/project
# Custom binary name with target selection
builder --name myapp --target darwin,linux/amd64 .
# All flags combined
builder --name myapp --output dist --target windows,linux .
Remember: This project combines many concepts from the entire module. Take your time, test thoroughly, and implement the stages progressively!