feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
// Package builders provides build implementations for different project types.
|
|
|
|
|
package builders
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"runtime"
|
|
|
|
|
"strings"
|
|
|
|
|
|
2026-03-13 09:30:02 +00:00
|
|
|
"forge.lthn.ai/core/go-build/pkg/build"
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
"forge.lthn.ai/core/go-io"
|
2026-03-16 21:03:21 +00:00
|
|
|
coreerr "forge.lthn.ai/core/go-log"
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// CPPBuilder implements the Builder interface for C++ projects using CMake + Conan.
|
|
|
|
|
// It wraps the Makefile-based build system from the .core/build submodule.
|
|
|
|
|
type CPPBuilder struct{}
|
|
|
|
|
|
|
|
|
|
// NewCPPBuilder creates a new CPPBuilder instance.
|
|
|
|
|
func NewCPPBuilder() *CPPBuilder {
|
|
|
|
|
return &CPPBuilder{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Name returns the builder's identifier.
|
|
|
|
|
func (b *CPPBuilder) Name() string {
|
|
|
|
|
return "cpp"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Detect checks if this builder can handle the project in the given directory.
|
|
|
|
|
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.
|
|
|
|
|
// Cross-compilation is handled via Conan profiles specified in .core/build.yaml.
|
|
|
|
|
func (b *CPPBuilder) Build(ctx context.Context, cfg *build.Config, targets []build.Target) ([]build.Artifact, error) {
|
|
|
|
|
if cfg == nil {
|
2026-03-16 21:03:21 +00:00
|
|
|
return nil, coreerr.E("CPPBuilder.Build", "config is nil", nil)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2026-03-16 21:03:21 +00:00
|
|
|
return artifacts, coreerr.E("CPPBuilder.Build", "build failed", err)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
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) {
|
|
|
|
|
fmt.Printf("Building C++ project for %s/%s (host)\n", target.OS, target.Arch)
|
|
|
|
|
|
|
|
|
|
// Step 1: Configure (runs conan install + cmake configure)
|
|
|
|
|
if err := b.runMake(ctx, cfg.ProjectDir, "configure"); err != nil {
|
2026-03-16 21:03:21 +00:00
|
|
|
return nil, coreerr.E("CPPBuilder.buildHost", "configure failed", err)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 2: Build
|
|
|
|
|
if err := b.runMake(ctx, cfg.ProjectDir, "build"); err != nil {
|
2026-03-16 21:03:21 +00:00
|
|
|
return nil, coreerr.E("CPPBuilder.buildHost", "build failed", err)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 3: Package
|
|
|
|
|
if err := b.runMake(ctx, cfg.ProjectDir, "package"); err != nil {
|
2026-03-16 21:03:21 +00:00
|
|
|
return nil, coreerr.E("CPPBuilder.buildHost", "package failed", err)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 == "" {
|
2026-03-16 21:03:21 +00:00
|
|
|
return nil, coreerr.E("CPPBuilder.buildCross", "no Conan profile mapped for target "+target.OS+"/"+target.Arch, nil)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("Building C++ project for %s/%s (cross: %s)\n", target.OS, target.Arch, profile)
|
|
|
|
|
|
|
|
|
|
// The Makefile exposes each profile as a top-level target
|
|
|
|
|
if err := b.runMake(ctx, cfg.ProjectDir, profile); err != nil {
|
2026-03-16 21:03:21 +00:00
|
|
|
return nil, coreerr.E("CPPBuilder.buildCross", "cross-compile for "+profile+" failed", err)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return b.findArtifacts(cfg.FS, cfg.ProjectDir, target)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// runMake executes a make target in the project directory.
|
|
|
|
|
func (b *CPPBuilder) runMake(ctx context.Context, projectDir string, target string) error {
|
|
|
|
|
cmd := exec.CommandContext(ctx, "make", target)
|
|
|
|
|
cmd.Dir = projectDir
|
|
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
|
cmd.Stderr = os.Stderr
|
|
|
|
|
cmd.Env = os.Environ()
|
|
|
|
|
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
2026-03-16 21:03:21 +00:00
|
|
|
return coreerr.E("CPPBuilder.runMake", "make "+target+" failed", err)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
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 := filepath.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 {
|
2026-03-16 21:03:21 +00:00
|
|
|
return nil, coreerr.E("CPPBuilder.findArtifacts", "failed to list packages directory", err)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var artifacts []build.Artifact
|
|
|
|
|
for _, entry := range entries {
|
|
|
|
|
if entry.IsDir() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
name := entry.Name()
|
|
|
|
|
// Skip checksum files and hidden files
|
|
|
|
|
if strings.HasSuffix(name, ".sha256") || strings.HasPrefix(name, ".") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
artifacts = append(artifacts, build.Artifact{
|
|
|
|
|
Path: filepath.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 := filepath.Join(projectDir, "build", "release", "src")
|
|
|
|
|
|
|
|
|
|
if !fs.IsDir(binDir) {
|
2026-03-16 21:03:21 +00:00
|
|
|
return nil, coreerr.E("CPPBuilder.findBinaries", "no build output found in "+binDir, nil)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entries, err := fs.List(binDir)
|
|
|
|
|
if err != nil {
|
2026-03-16 21:03:21 +00:00
|
|
|
return nil, coreerr.E("CPPBuilder.findBinaries", "failed to list build directory", err)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var artifacts []build.Artifact
|
|
|
|
|
for _, entry := range entries {
|
|
|
|
|
if entry.IsDir() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
name := entry.Name()
|
|
|
|
|
// Skip non-executable files (libraries, cmake files, etc.)
|
|
|
|
|
if strings.HasSuffix(name, ".a") || strings.HasSuffix(name, ".o") ||
|
|
|
|
|
strings.HasSuffix(name, ".cmake") || strings.HasPrefix(name, ".") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fullPath := filepath.Join(binDir, name)
|
|
|
|
|
|
|
|
|
|
// On Unix, check if file is executable
|
|
|
|
|
if target.OS != "windows" {
|
|
|
|
|
info, err := os.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 {
|
|
|
|
|
if _, err := exec.LookPath("make"); err != nil {
|
2026-03-16 21:03:21 +00:00
|
|
|
return coreerr.E("CPPBuilder.validateMake", "make not found. Install build-essential (Linux) or Xcode Command Line Tools (macOS)", nil)
|
feat: extract build/, release/, sdk/ from go-devops
Build system (8 builders, signing, archiving), release pipeline
(7 publishers, versioning, changelog), and SDK generation
(OpenAPI diff, code gen). 18K LOC, all tests pass except Go
builder workspace isolation (pre-existing).
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-09 12:37:36 +00:00
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure CPPBuilder implements the Builder interface.
|
|
|
|
|
var _ build.Builder = (*CPPBuilder)(nil)
|