Files
drjones 3678b199d0
Some checks failed
Test / test (push) Has been cancelled
Initial commit: AetherForge Linux (forge-mesh) v0.1.0-dev
2026-07-04 09:31:23 +00:00

187 lines
4.1 KiB
Go

package forge
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"time"
"forge-mesh/internal/api/types"
"github.com/google/uuid"
)
var targets = []struct {
OS string
Arch string
}{
{"linux", "amd64"},
{"linux", "arm64"},
}
// Pipeline builds and optionally signs agent binaries.
type Pipeline struct {
db *sql.DB
artifactsDir string
signingKey *KeyPair
agentMainPath string
version string
}
func NewPipeline(db *sql.DB, artifactsDir, signingKeyPath, agentMainPath, version string) (*Pipeline, error) {
key, err := LoadOrCreateKey(signingKeyPath)
if err != nil {
return nil, err
}
return &Pipeline{
db: db,
artifactsDir: artifactsDir,
signingKey: key,
agentMainPath: agentMainPath,
version: version,
}, nil
}
// BuildAll cross-compiles agent for linux amd64/arm64, signs, and stores artifacts.
func (p *Pipeline) BuildAll(public bool) ([]types.Build, error) {
var builds []types.Build
for _, tgt := range targets {
b, err := p.buildOne(tgt.OS, tgt.Arch, public)
if err != nil {
return nil, err
}
builds = append(builds, *b)
}
return builds, nil
}
func (p *Pipeline) buildOne(osName, arch string, public bool) (*types.Build, error) {
outName := fmt.Sprintf("forge-mesh-agent-%s-%s", osName, arch)
outPath := filepath.Join(p.artifactsDir, outName)
cmd := exec.Command("go", "build", "-trimpath", "-ldflags=-s -w",
"-o", outPath,
p.agentMainPath,
)
cmd.Env = append(os.Environ(),
"GOOS="+osName,
"GOARCH="+arch,
"CGO_ENABLED=0",
)
if out, err := cmd.CombinedOutput(); err != nil {
return nil, fmt.Errorf("build %s/%s: %w\n%s", osName, arch, err, out)
}
data, err := os.ReadFile(outPath)
if err != nil {
return nil, err
}
sum := sha256.Sum256(data)
checksum := hex.EncodeToString(sum[:])
sig := p.signingKey.Sign(data)
build := types.Build{
ID: uuid.NewString(),
OS: osName,
Arch: arch,
Version: p.version,
Checksum: checksum,
Signature: sig,
Public: public,
Path: outPath,
CreatedAt: time.Now().UTC(),
}
if err := p.saveBuild(&build); err != nil {
return nil, err
}
return &build, nil
}
func (p *Pipeline) saveBuild(b *types.Build) error {
pub := 0
if b.Public {
pub = 1
}
_, err := p.db.Exec(`
INSERT INTO builds (id, os, arch, version, checksum, signature, public, path, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, b.ID, b.OS, b.Arch, b.Version, b.Checksum, b.Signature, pub, b.Path, b.CreatedAt.Format(time.RFC3339))
return err
}
// LatestPublic returns the newest public build for os/arch.
func LatestPublic(db *sql.DB, osName, arch string) (*types.Build, error) {
row := db.QueryRow(`
SELECT id, os, arch, version, checksum, signature, public, path, created_at
FROM builds
WHERE public = 1 AND os = ? AND arch = ?
ORDER BY created_at DESC
LIMIT 1
`, osName, arch)
var b types.Build
var pub int
var path sql.NullString
var createdAt string
err := row.Scan(&b.ID, &b.OS, &b.Arch, &b.Version, &b.Checksum, &b.Signature, &pub, &path, &createdAt)
if err != nil {
return nil, err
}
b.Public = pub == 1
if path.Valid {
b.Path = path.String
}
b.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
return &b, nil
}
// GetBuild loads a build by ID.
func GetBuild(db *sql.DB, id string) (*types.Build, error) {
row := db.QueryRow(`
SELECT id, os, arch, version, checksum, signature, public, path, created_at
FROM builds WHERE id = ?
`, id)
var b types.Build
var pub int
var path sql.NullString
var createdAt string
err := row.Scan(&b.ID, &b.OS, &b.Arch, &b.Version, &b.Checksum, &b.Signature, &pub, &path, &createdAt)
if err != nil {
return nil, err
}
b.Public = pub == 1
if path.Valid {
b.Path = path.String
}
b.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
return &b, nil
}
// PublicKey returns the pipeline signing public key hex.
func (p *Pipeline) PublicKey() string {
return p.signingKey.PublicKeyHex()
}
// CopyArtifact streams a build artifact to w.
func CopyArtifact(path string, w io.Writer) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(w, f)
return err
}