go-build/pkg/build/builders/cpp.go

277 lines
8.2 KiB
Go

// Package builders provides build implementations for different project types.
package builders
import (
"context"
"runtime"
"dappco.re/go/core"
"dappco.re/go/core/build/internal/ax"
"dappco.re/go/core/build/pkg/build"
"dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
)
// CPPBuilder implements the Builder interface for C++ projects using CMake + Conan.
// It wraps the Makefile-based build system from the .core/build submodule.
//
// b := builders.NewCPPBuilder()
type CPPBuilder struct{}
// NewCPPBuilder creates a new CPPBuilder instance.
//
// b := builders.NewCPPBuilder()
func NewCPPBuilder() *CPPBuilder {
return &CPPBuilder{}
}
// Name returns the builder's identifier.
//
// name := b.Name() // → "cpp"
func (b *CPPBuilder) Name() string {
return "cpp"
}
// Detect checks if this builder can handle the project (checks for CMakeLists.txt).
//
// ok, err := b.Detect(io.Local, ".")
func (b *CPPBuilder) Detect(fs io.Medium, dir string) (bool, error) {
return build.IsCPPProject(fs, dir), nil
}
// Build compiles the C++ project using Make targets.
// The build flow is: make configure → make build → make package.
//
// artifacts, err := b.Build(ctx, cfg, []build.Target{{OS: "linux", Arch: "amd64"}})
func (b *CPPBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) ([]build.Artifact, error) {
if cfg == nil {
return nil, coreerr.E("CPPBuilder.Build", "config is nil", nil)
}
// Validate make is available
if err := b.validateMake(); err != nil {
return nil, err
}
// For C++ projects, the Makefile handles everything.
// We don't iterate per-target like Go — the Makefile's configure + build
// produces binaries for the host platform, and cross-compilation uses
// named Conan profiles (e.g., make gcc-linux-armv8).
if len(targets) == 0 {
// Default to host platform
targets = []build.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}
}
var artifacts []build.Artifact
for _, target := range targets {
built, err := b.buildTarget(ctx, cfg, target)
if err != nil {
return artifacts, coreerr.E("CPPBuilder.Build", "build failed", err)
}
artifacts = append(artifacts, built...)
}
return artifacts, nil
}
// buildTarget compiles for a single target platform.
func (b *CPPBuilder) buildTarget(ctx context.Context, cfg *build.Config, target build.Target) ([]build.Artifact, error) {
// Determine if this is a cross-compile or host build
isHostBuild := target.OS == runtime.GOOS && target.Arch == runtime.GOARCH
if isHostBuild {
return b.buildHost(ctx, cfg, target)
}
return b.buildCross(ctx, cfg, target)
}
// buildHost runs the standard make configure → make build → make package flow.
func (b *CPPBuilder) buildHost(ctx context.Context, cfg *build.Config, target build.Target) ([]build.Artifact, error) {
core.Print(nil, "Building C++ project for %s/%s (host)", target.OS, target.Arch)
// Step 1: Configure (runs conan install + cmake configure)
if err := b.runMake(ctx, cfg, "configure"); err != nil {
return nil, coreerr.E("CPPBuilder.buildHost", "configure failed", err)
}
// Step 2: Build
if err := b.runMake(ctx, cfg, "build"); err != nil {
return nil, coreerr.E("CPPBuilder.buildHost", "build failed", err)
}
// Step 3: Package
if err := b.runMake(ctx, cfg, "package"); err != nil {
return nil, coreerr.E("CPPBuilder.buildHost", "package failed", err)
}
// Discover artifacts from build/packages/
return b.findArtifacts(cfg.FS, cfg.ProjectDir, target)
}
// buildCross runs a cross-compilation using a Conan profile name.
// The Makefile supports profile targets like: make gcc-linux-armv8
func (b *CPPBuilder) buildCross(ctx context.Context, cfg *build.Config, target build.Target) ([]build.Artifact, error) {
// Map target to a Conan profile name
profile := b.targetToProfile(target)
if profile == "" {
return nil, coreerr.E("CPPBuilder.buildCross", "no Conan profile mapped for target "+target.OS+"/"+target.Arch, nil)
}
core.Print(nil, "Building C++ project for %s/%s (cross: %s)", target.OS, target.Arch, profile)
// The Makefile exposes each profile as a top-level target
if err := b.runMake(ctx, cfg, profile); err != nil {
return nil, coreerr.E("CPPBuilder.buildCross", "cross-compile for "+profile+" failed", err)
}
return b.findArtifacts(cfg.FS, cfg.ProjectDir, target)
}
// runMake executes a make target in the project directory.
func (b *CPPBuilder) runMake(ctx context.Context, cfg *build.Config, target string) error {
makeCommand, err := b.resolveMakeCli()
if err != nil {
return err
}
if err := ax.ExecWithEnv(ctx, cfg.ProjectDir, cfg.Env, makeCommand, target); err != nil {
return coreerr.E("CPPBuilder.runMake", "make "+target+" failed", err)
}
return nil
}
// findArtifacts searches for built packages in build/packages/.
func (b *CPPBuilder) findArtifacts(fs io.Medium, projectDir string, target build.Target) ([]build.Artifact, error) {
packagesDir := ax.Join(projectDir, "build", "packages")
if !fs.IsDir(packagesDir) {
// Fall back to searching build/release/src/ for raw binaries
return b.findBinaries(fs, projectDir, target)
}
entries, err := fs.List(packagesDir)
if err != nil {
return nil, coreerr.E("CPPBuilder.findArtifacts", "failed to list packages directory", err)
}
var artifacts []build.Artifact
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
// Skip checksum files and hidden files
if core.HasSuffix(name, ".sha256") || core.HasPrefix(name, ".") {
continue
}
artifacts = append(artifacts, build.Artifact{
Path: ax.Join(packagesDir, name),
OS: target.OS,
Arch: target.Arch,
})
}
return artifacts, nil
}
// findBinaries searches for compiled binaries in build/release/src/.
func (b *CPPBuilder) findBinaries(fs io.Medium, projectDir string, target build.Target) ([]build.Artifact, error) {
binDir := ax.Join(projectDir, "build", "release", "src")
if !fs.IsDir(binDir) {
return nil, coreerr.E("CPPBuilder.findBinaries", "no build output found in "+binDir, nil)
}
entries, err := fs.List(binDir)
if err != nil {
return nil, coreerr.E("CPPBuilder.findBinaries", "failed to list build directory", err)
}
var artifacts []build.Artifact
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
// Skip non-executable files (libraries, cmake files, etc.)
if core.HasSuffix(name, ".a") || core.HasSuffix(name, ".o") ||
core.HasSuffix(name, ".cmake") || core.HasPrefix(name, ".") {
continue
}
fullPath := ax.Join(binDir, name)
// On Unix, check if file is executable
if target.OS != "windows" {
info, err := fs.Stat(fullPath)
if err != nil {
continue
}
if info.Mode()&0111 == 0 {
continue
}
}
artifacts = append(artifacts, build.Artifact{
Path: fullPath,
OS: target.OS,
Arch: target.Arch,
})
}
return artifacts, nil
}
// targetToProfile maps a build target to a Conan cross-compilation profile name.
// Profile names match those in .core/build/cmake/profiles/.
func (b *CPPBuilder) targetToProfile(target build.Target) string {
key := target.OS + "/" + target.Arch
profiles := map[string]string{
"linux/amd64": "gcc-linux-x86_64",
"linux/x86_64": "gcc-linux-x86_64",
"linux/arm64": "gcc-linux-armv8",
"linux/armv8": "gcc-linux-armv8",
"darwin/arm64": "apple-clang-armv8",
"darwin/armv8": "apple-clang-armv8",
"darwin/amd64": "apple-clang-x86_64",
"darwin/x86_64": "apple-clang-x86_64",
"windows/amd64": "msvc-194-x86_64",
"windows/x86_64": "msvc-194-x86_64",
}
return profiles[key]
}
// validateMake checks if make is available.
func (b *CPPBuilder) validateMake() error {
_, err := b.resolveMakeCli()
return err
}
// resolveMakeCli returns the executable path for make or gmake.
func (b *CPPBuilder) resolveMakeCli(paths ...string) (string, error) {
if len(paths) == 0 {
paths = []string{
"/usr/bin/make",
"/usr/local/bin/make",
"/opt/homebrew/bin/make",
"/usr/local/bin/gmake",
"/opt/homebrew/bin/gmake",
}
}
command, err := ax.ResolveCommand("make", paths...)
if err != nil {
return "", coreerr.E("CPPBuilder.resolveMakeCli", "make not found. Install build-essential (Linux) or Xcode Command Line Tools (macOS)", err)
}
return command, nil
}
// Ensure CPPBuilder implements the Builder interface.
var _ build.Builder = (*CPPBuilder)(nil)