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"
"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
)
// LinuxKitBuilder builds LinuxKit images.
type LinuxKitBuilder struct { }
// NewLinuxKitBuilder creates a new LinuxKit builder.
func NewLinuxKitBuilder ( ) * LinuxKitBuilder {
return & LinuxKitBuilder { }
}
// Name returns the builder's identifier.
func ( b * LinuxKitBuilder ) Name ( ) string {
return "linuxkit"
}
// Detect checks if a linuxkit.yml or .yml config exists in the directory.
func ( b * LinuxKitBuilder ) Detect ( fs io . Medium , dir string ) ( bool , error ) {
// Check for linuxkit.yml
if fs . IsFile ( filepath . Join ( dir , "linuxkit.yml" ) ) {
return true , nil
}
// Check for .core/linuxkit/
lkDir := filepath . Join ( dir , ".core" , "linuxkit" )
if fs . IsDir ( lkDir ) {
entries , err := fs . List ( lkDir )
if err == nil {
for _ , entry := range entries {
if ! entry . IsDir ( ) && strings . HasSuffix ( entry . Name ( ) , ".yml" ) {
return true , nil
}
}
}
}
return false , nil
}
// Build builds LinuxKit images for the specified targets.
func ( b * LinuxKitBuilder ) Build ( ctx context . Context , cfg * build . Config , targets [ ] build . Target ) ( [ ] build . Artifact , error ) {
// Validate linuxkit CLI is available
if err := b . validateLinuxKitCli ( ) ; err != nil {
return nil , err
}
// Determine config file path
configPath := cfg . LinuxKitConfig
if configPath == "" {
// Auto-detect
if cfg . FS . IsFile ( filepath . Join ( cfg . ProjectDir , "linuxkit.yml" ) ) {
configPath = filepath . Join ( cfg . ProjectDir , "linuxkit.yml" )
} else {
// Look in .core/linuxkit/
lkDir := filepath . Join ( cfg . ProjectDir , ".core" , "linuxkit" )
if cfg . FS . IsDir ( lkDir ) {
entries , err := cfg . FS . List ( lkDir )
if err == nil {
for _ , entry := range entries {
if ! entry . IsDir ( ) && strings . HasSuffix ( entry . Name ( ) , ".yml" ) {
configPath = filepath . Join ( lkDir , entry . Name ( ) )
break
}
}
}
}
}
}
if configPath == "" {
2026-03-16 21:03:21 +00:00
return nil , coreerr . E ( "LinuxKitBuilder.Build" , "no LinuxKit config file found. Specify with --config or create linuxkit.yml" , 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 config file exists
if ! cfg . FS . IsFile ( configPath ) {
2026-03-16 21:03:21 +00:00
return nil , coreerr . E ( "LinuxKitBuilder.Build" , "config file not found: " + configPath , 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
}
// Determine output formats
formats := cfg . Formats
if len ( formats ) == 0 {
formats = [ ] string { "qcow2-bios" } // Default to QEMU-compatible format
}
// Create output directory
outputDir := cfg . OutputDir
if outputDir == "" {
outputDir = filepath . Join ( cfg . ProjectDir , "dist" )
}
if err := cfg . FS . EnsureDir ( outputDir ) ; err != nil {
2026-03-16 21:03:21 +00:00
return nil , coreerr . E ( "LinuxKitBuilder.Build" , "failed to create output 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
}
// Determine base name from config file or project name
baseName := cfg . Name
if baseName == "" {
baseName = strings . TrimSuffix ( filepath . Base ( configPath ) , ".yml" )
}
// If no targets, default to linux/amd64
if len ( targets ) == 0 {
targets = [ ] build . Target { { OS : "linux" , Arch : "amd64" } }
}
var artifacts [ ] build . Artifact
// Build for each target and format
for _ , target := range targets {
// LinuxKit only supports Linux
if target . OS != "linux" {
fmt . Printf ( "Skipping %s/%s (LinuxKit only supports Linux)\n" , target . OS , target . Arch )
continue
}
for _ , format := range formats {
outputName := fmt . Sprintf ( "%s-%s" , baseName , target . Arch )
args := b . buildLinuxKitArgs ( configPath , format , outputName , outputDir , target . Arch )
cmd := exec . CommandContext ( ctx , "linuxkit" , args ... )
cmd . Dir = cfg . ProjectDir
cmd . Stdout = os . Stdout
cmd . Stderr = os . Stderr
fmt . Printf ( "Building LinuxKit image: %s (%s, %s)\n" , outputName , format , target . Arch )
if err := cmd . Run ( ) ; err != nil {
2026-03-16 21:03:21 +00:00
return nil , coreerr . E ( "LinuxKitBuilder.Build" , "build failed for " + target . Arch + "/" + format , 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
}
// Determine the actual output file path
artifactPath := b . getArtifactPath ( outputDir , outputName , format )
// Verify the artifact was created
if ! cfg . FS . Exists ( artifactPath ) {
// Try alternate naming conventions
artifactPath = b . findArtifact ( cfg . FS , outputDir , outputName , format )
if artifactPath == "" {
2026-03-16 21:03:21 +00:00
return nil , coreerr . E ( "LinuxKitBuilder.Build" , "artifact not found after build: expected " + b . getArtifactPath ( outputDir , outputName , format ) , 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
}
}
artifacts = append ( artifacts , build . Artifact {
Path : artifactPath ,
OS : target . OS ,
Arch : target . Arch ,
} )
}
}
return artifacts , nil
}
// buildLinuxKitArgs builds the arguments for linuxkit build command.
func ( b * LinuxKitBuilder ) buildLinuxKitArgs ( configPath , format , outputName , outputDir , arch string ) [ ] string {
args := [ ] string { "build" }
// Output format
args = append ( args , "--format" , format )
// Output name
args = append ( args , "--name" , outputName )
// Output directory
args = append ( args , "--dir" , outputDir )
// Architecture (if not amd64)
if arch != "amd64" {
args = append ( args , "--arch" , arch )
}
// Config file
args = append ( args , configPath )
return args
}
// getArtifactPath returns the expected path of the built artifact.
func ( b * LinuxKitBuilder ) getArtifactPath ( outputDir , outputName , format string ) string {
ext := b . getFormatExtension ( format )
return filepath . Join ( outputDir , outputName + ext )
}
// findArtifact searches for the built artifact with various naming conventions.
func ( b * LinuxKitBuilder ) findArtifact ( fs io . Medium , outputDir , outputName , format string ) string {
// LinuxKit can create files with different suffixes
extensions := [ ] string {
b . getFormatExtension ( format ) ,
"-bios" + b . getFormatExtension ( format ) ,
"-efi" + b . getFormatExtension ( format ) ,
}
for _ , ext := range extensions {
path := filepath . Join ( outputDir , outputName + ext )
if fs . Exists ( path ) {
return path
}
}
// Try to find any file matching the output name
entries , err := fs . List ( outputDir )
if err == nil {
for _ , entry := range entries {
if strings . HasPrefix ( entry . Name ( ) , outputName ) {
match := filepath . Join ( outputDir , entry . Name ( ) )
// Return first match that looks like an image
ext := filepath . Ext ( match )
if ext == ".iso" || ext == ".qcow2" || ext == ".raw" || ext == ".vmdk" || ext == ".vhd" {
return match
}
}
}
}
return ""
}
// getFormatExtension returns the file extension for a LinuxKit output format.
func ( b * LinuxKitBuilder ) getFormatExtension ( format string ) string {
switch format {
case "iso" , "iso-bios" , "iso-efi" :
return ".iso"
case "raw" , "raw-bios" , "raw-efi" :
return ".raw"
case "qcow2" , "qcow2-bios" , "qcow2-efi" :
return ".qcow2"
case "vmdk" :
return ".vmdk"
case "vhd" :
return ".vhd"
case "gcp" :
return ".img.tar.gz"
case "aws" :
return ".raw"
default :
return "." + strings . TrimSuffix ( format , "-bios" )
}
}
// validateLinuxKitCli checks if the linuxkit CLI is available.
func ( b * LinuxKitBuilder ) validateLinuxKitCli ( ) error {
// Check PATH first
if _ , err := exec . LookPath ( "linuxkit" ) ; err == nil {
return nil
}
// Check common locations
paths := [ ] string {
"/usr/local/bin/linuxkit" ,
"/opt/homebrew/bin/linuxkit" ,
}
for _ , p := range paths {
if _ , err := os . Stat ( p ) ; err == nil {
return nil
}
}
2026-03-16 21:03:21 +00:00
return coreerr . E ( "LinuxKitBuilder.validateLinuxKitCli" , "linuxkit CLI not found. Install with: brew install linuxkit (macOS) or see https://github.com/linuxkit/linuxkit" , 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
}