// Package build provides the micro build command for building service binaries package build import ( "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "github.com/urfave/cli/v2" "go-micro.dev/v6/cmd" "go-micro.dev/v6/cmd/micro/run/config" ) // Build builds Go binaries for services func Build(c *cli.Context) error { dir := c.Args().Get(0) if dir == "" { dir = "." } absDir, err := filepath.Abs(dir) if err != nil { return fmt.Errorf("failed to get absolute path: %w", err) } // Load config cfg, err := config.Load(absDir) if err != nil { return fmt.Errorf("failed to load config: %w", err) } // Output directory outDir := c.String("output") if outDir == "" { outDir = filepath.Join(absDir, "bin") } if err := os.MkdirAll(outDir, 0755); err != nil { return fmt.Errorf("failed to create output dir: %w", err) } // Target OS/ARCH targetOS := c.String("os") targetArch := c.String("arch") if targetOS == "" { targetOS = runtime.GOOS } if targetArch == "" { targetArch = runtime.GOARCH } if cfg != nil && len(cfg.Services) > 0 { // Build each service from config sorted, err := cfg.TopologicalSort() if err != nil { return err } for _, svc := range sorted { svcDir := filepath.Join(absDir, svc.Path) if err := buildService(svc.Name, svcDir, outDir, targetOS, targetArch); err != nil { return fmt.Errorf("failed to build %s: %w", svc.Name, err) } } } else { // Build single service from current directory name := filepath.Base(absDir) if err := buildService(name, absDir, outDir, targetOS, targetArch); err != nil { return err } } fmt.Printf("\n \033[32m✓\033[0m Built to \033[36m%s\033[0m\n", outDir) return nil } func buildService(name, dir, outDir, targetOS, targetArch string) error { binName := name if targetOS == "windows" { binName += ".exe" } outPath := filepath.Join(outDir, binName) fmt.Printf(" Building \033[36m%s (%s/%s)...\n", name, targetOS, targetArch) // Build command buildCmd := exec.Command("go", "build", "-o", outPath, ".") buildCmd.Dir = dir buildCmd.Env = append(os.Environ(), "GOOS="+targetOS, "GOARCH="+targetArch, "CGO_ENABLED=0", ) buildCmd.Stdout = os.Stdout buildCmd.Stderr = os.Stderr if err := buildCmd.Run(); err != nil { return fmt.Errorf("go build failed: %w", err) } fmt.Printf(" \033[32m✓\033[0m %s\n", outPath) return nil } // Docker builds container images (optional) func Docker(c *cli.Context) error { dir := c.Args().Get(0) if dir == "" { dir = "." } absDir, err := filepath.Abs(dir) if err != nil { return fmt.Errorf("failed to get absolute path: %w", err) } cfg, err := config.Load(absDir) if err != nil { return fmt.Errorf("failed to load config: %w", err) } tag := c.String("tag") if tag == "" { tag = "latest" } registry := c.String("registry") push := c.Bool("push") if cfg != nil && len(cfg.Services) > 0 { for name, svc := range cfg.Services { svcDir := filepath.Join(absDir, svc.Path) if err := buildDockerImage(name, svcDir, svc.Port, tag, registry, push); err != nil { return fmt.Errorf("failed to build %s: %w", name, err) } } } else { name := filepath.Base(absDir) if err := buildDockerImage(name, absDir, 8080, tag, registry, push); err != nil { return err } } return nil } const dockerfileTemplate = `FROM golang:1.22-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /service . FROM alpine:latest RUN apk --no-cache add ca-certificates COPY --from=builder /service /service EXPOSE %d CMD ["/service"] ` func buildDockerImage(name, dir string, port int, tag, registry string, push bool) error { if port == 0 { port = 8080 } // Generate Dockerfile if not exists dockerfilePath := filepath.Join(dir, "Dockerfile") if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) { fmt.Printf("Generating Dockerfile for %s...\n", name) dockerfile := fmt.Sprintf(dockerfileTemplate, port) if err := os.WriteFile(dockerfilePath, []byte(dockerfile), 0644); err != nil { return fmt.Errorf("failed to write Dockerfile: %w", err) } } imageName := name + ":" + tag if registry != "" { imageName = registry + "/" + imageName } fmt.Printf(" Building \033[36m%s...\n", imageName) buildCmd := exec.Command("docker", "build", "-t", imageName, dir) buildCmd.Stdout = os.Stdout buildCmd.Stderr = os.Stderr if err := buildCmd.Run(); err != nil { return fmt.Errorf("docker build failed: %w", err) } fmt.Printf(" \033[32m✓\033[0m Built %s\n", imageName) if push { fmt.Printf("Pushing %s...\n", imageName) pushCmd := exec.Command("docker", "push", imageName) pushCmd.Stdout = os.Stdout pushCmd.Stderr = os.Stderr if err := pushCmd.Run(); err != nil { return fmt.Errorf("docker push failed: %w", err) } fmt.Printf(" \033[32m✓\033[0m Pushed %s\n", imageName) } return nil } // Compose generates docker-compose.yml (optional) func Compose(c *cli.Context) error { dir := c.Args().Get(0) if dir == "" { dir = "." } absDir, err := filepath.Abs(dir) if err != nil { return fmt.Errorf("failed to get absolute path: %w", err) } cfg, err := config.Load(absDir) if err != nil { return fmt.Errorf("failed to load config: %w", err) } if cfg == nil || len(cfg.Services) == 0 { return fmt.Errorf("no services found in micro.mu or micro.json") } registry := c.String("registry") tag := c.String("tag") if tag == "" { tag = "latest" } var sb strings.Builder sb.WriteString("# Generated by micro build --compose\n") sb.WriteString("version: '3.8'\n\nservices:\n") sorted, err := cfg.TopologicalSort() if err != nil { return err } for _, svc := range sorted { imageName := svc.Name + ":" + tag if registry != "" { imageName = registry + "/" + imageName } fmt.Fprintf(&sb, " %s:\n", svc.Name) fmt.Fprintf(&sb, " image: %s\n", imageName) if svc.Port > 0 { fmt.Fprintf(&sb, " ports:\n - \"%d:%d\"\n", svc.Port, svc.Port) } if len(svc.Depends) > 0 { sb.WriteString(" depends_on:\n") for _, dep := range svc.Depends { fmt.Fprintf(&sb, " - %s\n", dep) } } sb.WriteString(" environment:\n - MICRO_REGISTRY=mdns\n\n") } output := filepath.Join(absDir, "docker-compose.yml") if err := os.WriteFile(output, []byte(sb.String()), 0644); err != nil { return fmt.Errorf("failed to write docker-compose.yml: %w", err) } fmt.Printf(" \033[32m✓\033[0m Generated %s\n", output) return nil } func init() { cmd.Register(&cli.Command{ Name: "build", Usage: "Build Go binaries for services", Description: `Build compiles Go binaries for your services. With a micro.mu config, builds all services. Without, builds the current directory. Output goes to ./bin/ by default. Examples: micro build # Build for current OS/arch micro build --os linux # Cross-compile for Linux micro build --os linux --arch arm64 # For ARM64 micro build --output ./dist # Custom output directory Docker (optional): micro build --docker # Build container images micro build --docker --push # Build and push micro build --compose # Generate docker-compose.yml`, Action: func(c *cli.Context) error { if c.Bool("docker") { return Docker(c) } if c.Bool("compose") { return Compose(c) } return Build(c) }, Flags: []cli.Flag{ &cli.StringFlag{ Name: "output", Aliases: []string{"o"}, Usage: "Output directory (default: ./bin)", }, &cli.StringFlag{ Name: "os", Usage: "Target OS (linux, darwin, windows)", }, &cli.StringFlag{ Name: "arch", Usage: "Target architecture (amd64, arm64)", }, // Docker options (optional) &cli.BoolFlag{ Name: "docker", Usage: "Build Docker container images instead", }, &cli.StringFlag{ Name: "tag", Aliases: []string{"t"}, Usage: "Docker image tag (default: latest)", Value: "latest", }, &cli.StringFlag{ Name: "registry", Aliases: []string{"r"}, Usage: "Docker registry (e.g., docker.io/myuser)", }, &cli.BoolFlag{ Name: "push", Usage: "Push Docker images after building", }, &cli.BoolFlag{ Name: "compose", Usage: "Generate docker-compose.yml", }, }, }) }