go.mod + 142 *.go files updated. Also swept dappco.re/go/core/inference to satisfy the no-core/ verification constraint. Closes tasks.lthn.sh/view.php?id=591 Co-authored-by: Codex <noreply@openai.com>
535 lines
16 KiB
Go
535 lines
16 KiB
Go
// Package builders provides build implementations for different project types.
|
|
package builders
|
|
|
|
import (
|
|
"context"
|
|
"runtime"
|
|
|
|
"dappco.re/go/core"
|
|
"dappco.re/go/build/internal/ax"
|
|
"dappco.re/go/build/pkg/build"
|
|
"dappco.re/go/io"
|
|
coreerr "dappco.re/go/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)
|
|
}
|
|
|
|
filesystem := cfg.FS
|
|
if filesystem == nil {
|
|
filesystem = io.Local
|
|
cfg.FS = filesystem
|
|
}
|
|
if cfg.OutputDir == "" {
|
|
cfg.OutputDir = ax.Join(cfg.ProjectDir, "dist")
|
|
}
|
|
|
|
managedMake := b.hasManagedMakefile(filesystem, cfg.ProjectDir)
|
|
if managedMake {
|
|
// Managed C++ repos keep the Conan/CMake orchestration in the project Makefile.
|
|
if err := b.validateMake(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := b.validateConan(); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
if err := b.validateCMake(); err != nil {
|
|
return nil, err
|
|
}
|
|
if b.usesConan(filesystem, cfg.ProjectDir) {
|
|
if err := b.validateConan(); 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) {
|
|
if cfg == nil {
|
|
return nil, coreerr.E("CPPBuilder.buildTarget", "config is nil", nil)
|
|
}
|
|
filesystem := cfg.FS
|
|
if filesystem == nil {
|
|
filesystem = io.Local
|
|
cfg.FS = filesystem
|
|
}
|
|
if !b.hasManagedMakefile(filesystem, cfg.ProjectDir) {
|
|
return b.buildWithCMake(ctx, cfg, target)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// buildWithCMake runs a generic CMake build for plain CMakeLists.txt projects.
|
|
// Conan is used when the project declares a conanfile; otherwise the builder
|
|
// configures CMake directly.
|
|
func (b *CPPBuilder) buildWithCMake(ctx context.Context, cfg *build.Config, target build.Target) ([]build.Artifact, error) {
|
|
filesystem := cfg.FS
|
|
if filesystem == nil {
|
|
filesystem = io.Local
|
|
cfg.FS = filesystem
|
|
}
|
|
|
|
platformDir := ax.Join(cfg.OutputDir, core.Sprintf("%s_%s", target.OS, target.Arch))
|
|
if err := filesystem.EnsureDir(platformDir); err != nil {
|
|
return nil, coreerr.E("CPPBuilder.buildWithCMake", "failed to create platform output directory", err)
|
|
}
|
|
|
|
buildDir := ax.Join(cfg.ProjectDir, "build", "cmake", core.Sprintf("%s_%s", target.OS, target.Arch))
|
|
if err := filesystem.EnsureDir(buildDir); err != nil {
|
|
return nil, coreerr.E("CPPBuilder.buildWithCMake", "failed to create cmake build directory", err)
|
|
}
|
|
|
|
env := appendConfiguredEnv(cfg,
|
|
core.Sprintf("GOOS=%s", target.OS),
|
|
core.Sprintf("GOARCH=%s", target.Arch),
|
|
core.Sprintf("TARGET_OS=%s", target.OS),
|
|
core.Sprintf("TARGET_ARCH=%s", target.Arch),
|
|
core.Sprintf("OUTPUT_DIR=%s", cfg.OutputDir),
|
|
core.Sprintf("TARGET_DIR=%s", platformDir),
|
|
)
|
|
if cfg.CGO {
|
|
env = append(env, "CGO_ENABLED=1")
|
|
}
|
|
|
|
useConan := b.usesConan(filesystem, cfg.ProjectDir)
|
|
if useConan {
|
|
if err := b.runConanInstall(ctx, cfg, target, buildDir, env); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := b.runCMakeConfigure(ctx, cfg, target, buildDir, platformDir, useConan, env); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := b.runCMakeBuild(ctx, cfg, buildDir, env); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
artifacts := b.findGeneratedArtifacts(filesystem, platformDir, target)
|
|
if len(artifacts) > 0 {
|
|
return artifacts, nil
|
|
}
|
|
|
|
// Some generators ignore the explicit output directory and place binaries in
|
|
// the build tree. Fall back to scanning the cmake build directory.
|
|
artifacts = b.findGeneratedArtifacts(filesystem, buildDir, target)
|
|
if len(artifacts) > 0 {
|
|
return artifacts, nil
|
|
}
|
|
|
|
return nil, coreerr.E("CPPBuilder.buildWithCMake", "no build output found in "+platformDir+" or "+buildDir, nil)
|
|
}
|
|
|
|
// 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, build.BuildEnvironment(cfg), makeCommand, target); err != nil {
|
|
return coreerr.E("CPPBuilder.runMake", "make "+target+" failed", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *CPPBuilder) runConanInstall(ctx context.Context, cfg *build.Config, target build.Target, buildDir string, env []string) error {
|
|
conanCommand, err := b.resolveConanCli()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
args := []string{"install", ".", "--output-folder", buildDir, "--build=missing"}
|
|
if target.OS != runtime.GOOS || target.Arch != runtime.GOARCH {
|
|
profile := b.targetToProfile(target)
|
|
if profile == "" {
|
|
return coreerr.E("CPPBuilder.runConanInstall", "no Conan profile mapped for target "+target.OS+"/"+target.Arch, nil)
|
|
}
|
|
args = append(args, "--profile:host", profile)
|
|
}
|
|
|
|
output, err := ax.CombinedOutput(ctx, cfg.ProjectDir, env, conanCommand, args...)
|
|
if err != nil {
|
|
return coreerr.E("CPPBuilder.runConanInstall", "conan install failed: "+output, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *CPPBuilder) runCMakeConfigure(ctx context.Context, cfg *build.Config, target build.Target, buildDir, platformDir string, useConan bool, env []string) error {
|
|
cmakeCommand, err := b.resolveCMakeCli()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
args := []string{
|
|
"-S", cfg.ProjectDir,
|
|
"-B", buildDir,
|
|
"-DCMAKE_BUILD_TYPE=Release",
|
|
"-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=" + platformDir,
|
|
"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=" + platformDir,
|
|
"-DCMAKE_ARCHIVE_OUTPUT_DIRECTORY=" + platformDir,
|
|
}
|
|
if useConan {
|
|
args = append(args, "-DCMAKE_TOOLCHAIN_FILE="+ax.Join(buildDir, "conan_toolchain.cmake"))
|
|
}
|
|
if target.OS != runtime.GOOS || target.Arch != runtime.GOARCH {
|
|
args = append(args, "-DCORE_TARGET="+target.OS+"/"+target.Arch)
|
|
}
|
|
|
|
output, err := ax.CombinedOutput(ctx, cfg.ProjectDir, env, cmakeCommand, args...)
|
|
if err != nil {
|
|
return coreerr.E("CPPBuilder.runCMakeConfigure", "cmake configure failed: "+output, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *CPPBuilder) runCMakeBuild(ctx context.Context, cfg *build.Config, buildDir string, env []string) error {
|
|
cmakeCommand, err := b.resolveCMakeCli()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
output, err := ax.CombinedOutput(ctx, cfg.ProjectDir, env, cmakeCommand, "--build", buildDir, "--config", "Release")
|
|
if err != nil {
|
|
return coreerr.E("CPPBuilder.runCMakeBuild", "cmake build failed: "+output, 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)
|
|
}
|
|
|
|
return b.findGeneratedArtifacts(fs, binDir, target), nil
|
|
}
|
|
|
|
func (b *CPPBuilder) findGeneratedArtifacts(fs io.Medium, dir string, target build.Target) []build.Artifact {
|
|
if !fs.IsDir(dir) {
|
|
return nil
|
|
}
|
|
|
|
entries, err := fs.List(dir)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var artifacts []build.Artifact
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
if target.OS == "darwin" && core.HasSuffix(entry.Name(), ".app") {
|
|
artifacts = append(artifacts, build.Artifact{
|
|
Path: ax.Join(dir, entry.Name()),
|
|
OS: target.OS,
|
|
Arch: target.Arch,
|
|
})
|
|
}
|
|
continue
|
|
}
|
|
|
|
name := entry.Name()
|
|
// Skip common build metadata and non-runtime artefacts.
|
|
if core.HasPrefix(name, ".") ||
|
|
core.HasPrefix(name, "CMake") ||
|
|
core.HasPrefix(name, "cmake") ||
|
|
core.HasPrefix(name, "conan") ||
|
|
core.HasSuffix(name, ".a") ||
|
|
core.HasSuffix(name, ".o") ||
|
|
core.HasSuffix(name, ".cmake") ||
|
|
core.HasSuffix(name, ".ninja") ||
|
|
core.HasSuffix(name, ".txt") ||
|
|
name == "Makefile" {
|
|
continue
|
|
}
|
|
|
|
fullPath := ax.Join(dir, 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// validateConan checks if conan is available.
|
|
func (b *CPPBuilder) validateConan() error {
|
|
_, err := b.resolveConanCli()
|
|
return err
|
|
}
|
|
|
|
// validateCMake checks if cmake is available.
|
|
func (b *CPPBuilder) validateCMake() error {
|
|
_, err := b.resolveCMakeCli()
|
|
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
|
|
}
|
|
|
|
// resolveConanCli returns the executable path for conan.
|
|
func (b *CPPBuilder) resolveConanCli(paths ...string) (string, error) {
|
|
if len(paths) == 0 {
|
|
paths = []string{
|
|
"/usr/local/bin/conan",
|
|
"/opt/homebrew/bin/conan",
|
|
}
|
|
|
|
if home := core.Env("HOME"); home != "" {
|
|
paths = append(paths, ax.Join(home, ".local", "bin", "conan"))
|
|
}
|
|
}
|
|
|
|
command, err := ax.ResolveCommand("conan", paths...)
|
|
if err != nil {
|
|
return "", coreerr.E("CPPBuilder.resolveConanCli", "conan not found. Install it with: python -m pip install conan", err)
|
|
}
|
|
|
|
return command, nil
|
|
}
|
|
|
|
// resolveCMakeCli returns the executable path for cmake.
|
|
func (b *CPPBuilder) resolveCMakeCli(paths ...string) (string, error) {
|
|
if len(paths) == 0 {
|
|
paths = []string{
|
|
"/usr/bin/cmake",
|
|
"/usr/local/bin/cmake",
|
|
"/opt/homebrew/bin/cmake",
|
|
}
|
|
}
|
|
|
|
command, err := ax.ResolveCommand("cmake", paths...)
|
|
if err != nil {
|
|
return "", coreerr.E("CPPBuilder.resolveCMakeCli", "cmake not found. Install it with: brew install cmake or apt-get install cmake", err)
|
|
}
|
|
|
|
return command, nil
|
|
}
|
|
|
|
func (b *CPPBuilder) hasManagedMakefile(fs io.Medium, dir string) bool {
|
|
if fs == nil {
|
|
fs = io.Local
|
|
}
|
|
|
|
for _, name := range []string{"Makefile", "GNUmakefile", "makefile"} {
|
|
if fs.IsFile(ax.Join(dir, name)) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (b *CPPBuilder) usesConan(fs io.Medium, dir string) bool {
|
|
if fs == nil {
|
|
fs = io.Local
|
|
}
|
|
|
|
return fs.IsFile(ax.Join(dir, "conanfile.py")) || fs.IsFile(ax.Join(dir, "conanfile.txt"))
|
|
}
|
|
|
|
// Ensure CPPBuilder implements the Builder interface.
|
|
var _ build.Builder = (*CPPBuilder)(nil)
|