Mastering Build Automation with Mage: A Modern Make Alternative for Go
Mage is a powerful, Go-based build tool that combines the flexibility of traditional make
with the type safety and ecosystem of Go. This guide explores how to leverage Mage for Go projects, covering cross-compilation, dependency management, advanced workflows, and deployment strategies.
Introduction to Mage
Mage replaces Makefiles with Go scripts (magefile.go
), offering:
- Go Syntax: Write build logic in a familiar language
- Cross-Platform: Works natively on Windows, Linux, and macOS
- Dependency Tracking: Automatic tracking of file changes
- Concurrent Execution: Parallel task execution with Go routines
Installation
go install github.com/magefile/mage@latest
Basic Mage Setup
Create a magefile.go
in your project root:
// +build mage
package main
import (
"fmt"
"github.com/magefile/mage/mg"
)
// Build the project
func Build() error {
fmt.Println("Building...")
return goBuild()
}
func goBuild() error {
return sh.Run("go", "build", "-o", "golang-tech-stack", ".")
}
Run tasks:
mage build
Cross-Platform Builds with Mage
Create platform-specific targets using Go’s conditional compilation:
// +build mage
package main
import (
"fmt"
"github.com/magefile/mage/sh"
"os"
"runtime"
)
type Build mg.Namespace
// BuildAll builds binaries for all platforms
func (Build) All() error {
targets := []struct {
OS string
Arch string
}{
{"linux", "amd64"},
{"windows", "amd64"},
{"darwin", "arm64"},
}
for _, t := range targets {
err := buildBinary(t.OS, t.Arch)
if err != nil {
return err
}
}
return nil
}
func buildBinary(goos, goarch string) error {
return sh.RunWith(
map[string]string{"GOOS": goos, "GOARCH": goarch},
"go", "build", "-o", fmt.Sprintf("build/%s_%s/myapp", goos, goarch), ".",
)
}
Run with:
mage build:all
Advanced Dependency Management
Versioned Builds
var Version = "dev"
func Build() error {
ldflags := fmt.Sprintf("-X main.version=%s -X main.buildTime=%s",
Version, time.Now().Format(time.RFC3339))
return sh.Run("go", "build", "-ldflags", ldflags, "-o", "myapp", ".")
}
Vendor Support
func Build() error {
mg.Deps(func() error {
return sh.Run("go", "mod", "vendor")
})
return sh.Run("go", "build", "-mod=vendor", "-o", "myapp", ".")
}
Conditional Builds and Tags
Implement complex build logic with Mage’s task dependencies:
// +build mage
package main
import (
"github.com/magefile/mage/mg"
"github.com/magefile/mage/sh"
)
type Build mg.Namespace
var buildTags string
func (Build) Prod() {
buildTags = "prod,release"
mg.Deps(Build.All)
}
func (Build) Dev() {
buildTags = "dev,debug"
mg.Deps(Build.All)
}
func (Build) All() error {
return sh.Run("go", "build", "-tags", buildTags, "-o", "myapp", ".")
}
Run with:
mage build:prod
mage build:dev
Error Handling and Debugging
Mage provides rich error handling through Go’s native error system:
func Lint() error {
if err := sh.Run("golangci-lint", "run"); err != nil {
return fmt.Errorf("linting failed: %w", err)
}
return nil
}
func Test() error {
mg.Deps(Lint)
return sh.Run("go", "test", "-race", "./...")
}
Use verbose output:
mage -v test
Deployment Automation
Docker Deployment Pipeline
func Docker() error {
mg.Deps(Build.Prod)
if err := sh.Run("docker", "build", "-t", "myapp:latest", "."); err != nil {
return err
}
return sh.Run("docker", "push", "myapp:latest")
}
Multi-Stage Deployment
type Deploy mg.Namespace
func (Deploy) Staging() error {
mg.Deps(Build.Prod)
return sh.Run("rsync", "-az", "myapp", "user@staging:/opt/myapp")
}
func (Deploy) Production() error {
mg.Deps(Build.Prod)
return sh.Run("ansible-playbook", "-i", "prod", "deploy.yml")
}
Performance Optimization
Cached Builds
var buildCache = "./.buildcache"
func Build() error {
if !isDir(buildCache) {
if err := os.Mkdir(buildCache, 0755); err != nil {
return err
}
}
return sh.Run("go", "build", "-o", filepath.Join(buildCache, "myapp"), ".")
}
Parallel Tasks
func CI() error {
mg.Deps(mg.F(Lint, Test, Build), GenerateDocs)
return nil
}
func GenerateDocs() error {
mg.Deps(Build)
// Documentation generation logic
return nil
}
Best Practices
- Task Namespacing: Use
mg.Namespace
for complex projects - Dependency Management: Use
mg.Deps
for task ordering - Configuration: Store build parameters in
mage_config.go
- CI/CD Integration:
# GitHub Actions Example
- name: Run Mage
run: mage ci
Why Choose Mage Over Native Go Build?
Example Advanced Magefile:
// +build mage
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"time"
"github.com/magefile/mage/mg"
"github.com/magefile/mage/sh"
)
var (
buildTime = time.Now().Format(time.RFC3339)
gitCommit string
)
type Build mg.Namespace
func init() {
if b, err := sh.Output("git", "rev-parse", "--short", "HEAD"); err == nil {
gitCommit = b
}
}
// Full CI pipeline
func CI() {
mg.SerialDeps(Build.All, Test, Lint, Docker.Build)
}
// Build production binary
func (Build) Prod() error {
return buildBinary("prod", runtime.GOOS, runtime.GOARCH)
}
// Build for all platforms
func (Build) All() error {
mg.Deps(func() error {
return os.MkdirAll("dist", 0755)
})
// Build for multiple targets
targets := []struct{ OS, Arch string }{
{"linux", "amd64"},
{"windows", "amd64"},
{"darwin", "arm64"},
}
for _, t := range targets {
if err := buildBinary("prod", t.OS, t.Arch); err != nil {
return err
}
}
return nil
}
func buildBinary(env, goos, goarch string) error {
output := filepath.Join("dist", fmt.Sprintf("myapp-%s-%s-%s", env, goos, goarch))
if goos == "windows" {
output += ".exe"
}
ldflags := fmt.Sprintf("-X main.version=%s -X main.commit=%s -X main.buildTime=%s",
env, gitCommit, buildTime)
return sh.RunWith(
map[string]string{"GOOS": goos, "GOARCH": goarch},
"go", "build", "-trimpath", "-ldflags", ldflags, "-o", output, ".",
)
}
type Docker mg.Namespace
func (Docker) Build(ctx context.Context) error {
mg.CtxDeps(ctx, Build.Prod)
return sh.Run("docker", "build", "-t", "myapp:latest", ".")
}