Initial commit: AetherForge Linux (forge-mesh) v0.1.0-dev
Some checks failed
Test / test (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
This commit is contained in:
186
internal/forge/build.go
Normal file
186
internal/forge/build.go
Normal file
@@ -0,0 +1,186 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user