Files
tursom f508ecc1b9
Some checks failed
Go / build (.exe, 386, windows, windows-386) (push) Has been cancelled
Go / build (.exe, amd64, windows, windows-amd64) (push) Has been cancelled
Go / build (.exe, arm64, windows, windows-arm64) (push) Has been cancelled
Go / build (386, freebsd, freebsd-386) (push) Has been cancelled
Go / build (386, linux, linux-386) (push) Has been cancelled
Go / build (386, netbsd, netbsd-386) (push) Has been cancelled
Go / build (386, openbsd, openbsd-386) (push) Has been cancelled
Go / build (386, plan9, plan9-386) (push) Has been cancelled
Go / build (amd64, darwin, darwin-amd64) (push) Has been cancelled
Go / build (amd64, dragonfly, dragonfly-amd64) (push) Has been cancelled
Go / build (amd64, freebsd, freebsd-amd64) (push) Has been cancelled
Go / build (amd64, illumos, illumos-amd64) (push) Has been cancelled
Go / build (amd64, linux, linux-amd64) (push) Has been cancelled
Go / build (amd64, netbsd, netbsd-amd64) (push) Has been cancelled
Go / build (amd64, openbsd, openbsd-amd64) (push) Has been cancelled
Go / build (amd64, plan9, plan9-amd64) (push) Has been cancelled
Go / build (amd64, solaris, solaris-amd64) (push) Has been cancelled
Go / build (arm, 6, linux, linux-armv6) (push) Has been cancelled
Go / build (arm, 7, linux, linux-armv7) (push) Has been cancelled
Go / build (arm, freebsd, freebsd-arm) (push) Has been cancelled
Go / build (arm, netbsd, netbsd-arm) (push) Has been cancelled
Go / build (arm, openbsd, openbsd-arm) (push) Has been cancelled
Go / build (arm, plan9, plan9-arm) (push) Has been cancelled
Go / build (arm64, darwin, darwin-arm64) (push) Has been cancelled
Go / build (arm64, freebsd, freebsd-arm64) (push) Has been cancelled
Go / build (arm64, linux, linux-arm64) (push) Has been cancelled
Go / build (arm64, netbsd, netbsd-arm64) (push) Has been cancelled
Go / build (arm64, openbsd, openbsd-arm64) (push) Has been cancelled
Go / build (loong64, linux, linux-loong64) (push) Has been cancelled
Go / build (mips, linux, linux-mips) (push) Has been cancelled
Go / build (mips64, linux, linux-mips64) (push) Has been cancelled
Go / build (mips64le, linux, linux-mips64le) (push) Has been cancelled
Go / build (mipsle, linux, linux-mipsle) (push) Has been cancelled
Go / build (ppc64, aix, aix-ppc64) (push) Has been cancelled
Go / build (ppc64, linux, linux-ppc64) (push) Has been cancelled
Go / build (ppc64, openbsd, openbsd-ppc64) (push) Has been cancelled
Go / build (ppc64le, linux, linux-ppc64le) (push) Has been cancelled
Go / build (riscv64, freebsd, freebsd-riscv64) (push) Has been cancelled
Go / build (riscv64, linux, linux-riscv64) (push) Has been cancelled
Go / build (riscv64, openbsd, openbsd-riscv64) (push) Has been cancelled
Go / build (s390x, linux, linux-s390x) (push) Has been cancelled
Docker Image / docker (push) Has been cancelled
Go / merge-artifacts (push) Has been cancelled
docs: 补充中文代码注释
2026-06-27 20:15:29 +08:00

332 lines
9.8 KiB
Go

// internal/pluginmanager/builder.go 把源码插件包构建为网关可加载制品,并记录构建元数据。
package pluginmanager
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
)
type SourceBuilder interface {
Build(ctx context.Context, source ArtifactRecord, req BuildRequest, build BuildRecord) (BuildResult, error)
}
type BuildResult struct {
Manifest Manifest
ArtifactBytes []byte
ArtifactSHA256 string
GoVersion string
ModuleSummary string
GoVersionM string
ABIFingerprint string
LogSummary string
Metadata map[string]any
}
type LocalProcessBuilder struct {
StoreRoot string
}
func (b LocalProcessBuilder) Build(ctx context.Context, source ArtifactRecord, req BuildRequest, build BuildRecord) (BuildResult, error) {
if source.ArtifactType != ArtifactTypeSource {
return BuildResult{}, fmt.Errorf("artifact %s is %q, want source", source.ID, source.ArtifactType)
}
var manifest Manifest
if err := json.Unmarshal([]byte(source.MetadataJSON), &manifest); err != nil {
return BuildResult{}, fmt.Errorf("decode source manifest: %w", err)
}
if err := validateManifest(manifest); err != nil {
return BuildResult{}, err
}
if manifest.Build.Type != "" && manifest.Build.Type != BuildTypeGo {
return BuildResult{}, fmt.Errorf("unsupported build.type %q", manifest.Build.Type)
}
if manifest.Build.Output != "" && manifest.Build.Output != RuntimeEntry {
return BuildResult{}, fmt.Errorf("unsupported build.output %q", manifest.Build.Output)
}
if req.VendorRequired {
if _, err := os.Stat(filepath.Join(source.FilePath, "vendor")); err != nil {
return BuildResult{}, errors.New("vendor is required but source package has no vendor directory")
}
}
goVersion, goVersionOutput, err := commandOutput(ctx, source.FilePath, nil, "go", "version")
if err != nil {
return BuildResult{LogSummary: sanitizeLog(goVersionOutput)}, err
}
if manifest.GoVersion != "" && manifest.GoVersion != goVersion {
return BuildResult{GoVersion: goVersion, LogSummary: sanitizeLog(goVersionOutput)}, fmt.Errorf("source go_version %q does not match builder %q", manifest.GoVersion, goVersion)
}
manifest.GoVersion = goVersion
manifest.GOOS = req.GOOS
manifest.GOARCH = req.GOARCH
manifest.ArtifactType = ArtifactTypeBinary
manifest.Runtime.Entry = RuntimeEntry
outDir, err := os.MkdirTemp("", "mc-gateway-plugin-build-*")
if err != nil {
return BuildResult{}, err
}
defer os.RemoveAll(outDir)
outPath := filepath.Join(outDir, RuntimeEntry)
env := buildEnvironment(req)
args := []string{"build", "-buildmode=plugin", "-trimpath", "-buildvcs=false", "-o", outPath}
if req.BuildTags != "" {
args = append(args, "-tags", req.BuildTags)
}
buildEntry := sourceBuildEntry(manifest)
args = append(args, buildEntry)
_, buildLog, buildErr := commandOutput(ctx, source.FilePath, env, "go", args...)
if buildErr != nil {
return BuildResult{GoVersion: goVersion, LogSummary: sanitizeLog(buildLog)}, buildErr
}
if _, nmLog, err := commandOutput(ctx, source.FilePath, env, "go", "tool", "nm", outPath); err != nil {
return BuildResult{GoVersion: goVersion, LogSummary: sanitizeLog(buildLog + "\n" + nmLog)}, fmt.Errorf("inspect built plugin symbols: %w", err)
} else if err := validateBuiltSymbols(manifest, nmLog); err != nil {
return BuildResult{GoVersion: goVersion, LogSummary: sanitizeLog(buildLog + "\n" + nmLog)}, err
}
artifactBytes, err := os.ReadFile(outPath)
if err != nil {
return BuildResult{}, err
}
artifactSum := sha256.Sum256(artifactBytes)
artifactSHA := hex.EncodeToString(artifactSum[:])
moduleSummary := "[]"
if _, moduleLog, err := commandOutput(ctx, source.FilePath, env, "go", "list", "-m", "-json", "all"); err == nil {
moduleSummary = summarizeGoModules(moduleLog)
} else {
buildLog += "\n" + moduleLog
}
goVersionM := "{}"
if _, versionMLog, err := commandOutput(ctx, source.FilePath, env, "go", "version", "-m", outPath); err == nil {
goVersionM = summarizeGoVersionM(versionMLog)
} else {
buildLog += "\n" + versionMLog
}
abi := abiFingerprint(manifest, goVersion)
metadata := map[string]any{
"source_sha256": source.SHA256,
"artifact_sha256": artifactSHA,
"builder_type": BuilderTypeLocalProcess,
"builder_version": build.BuilderVersion,
"go_version": goVersion,
"go_os": req.GOOS,
"go_arch": req.GOARCH,
"go_amd64": req.GOAMD64,
"go_arm64": req.GOARM64,
"cgo_enabled": req.CGOEnabled,
"build_tags": req.BuildTags,
"vendor_required": req.VendorRequired,
"abi_fingerprint": abi,
}
return BuildResult{
Manifest: manifest,
ArtifactBytes: artifactBytes,
ArtifactSHA256: artifactSHA,
GoVersion: goVersion,
ModuleSummary: moduleSummary,
GoVersionM: goVersionM,
ABIFingerprint: abi,
LogSummary: sanitizeLog(buildLog),
Metadata: metadata,
}, nil
}
type ContainerBuilder struct{}
func (ContainerBuilder) Build(context.Context, ArtifactRecord, BuildRequest, BuildRecord) (BuildResult, error) {
return BuildResult{}, errors.New("container builder is not configured in this phase")
}
func buildEnvironment(req BuildRequest) []string {
env := os.Environ()
env = append(env,
"GOOS="+req.GOOS,
"GOARCH="+req.GOARCH,
"CGO_ENABLED="+req.CGOEnabled,
)
if req.GOAMD64 != "" {
env = append(env, "GOAMD64="+req.GOAMD64)
}
if req.GOARM64 != "" {
env = append(env, "GOARM64="+req.GOARM64)
}
if req.GOPROXY != "" {
env = append(env, "GOPROXY="+req.GOPROXY)
}
if req.GONOSUMDB != "" {
env = append(env, "GONOSUMDB="+req.GONOSUMDB)
}
if req.GOPRIVATE != "" {
env = append(env, "GOPRIVATE="+req.GOPRIVATE)
}
return env
}
func commandOutput(ctx context.Context, dir string, env []string, name string, args ...string) (string, string, error) {
cmd := exec.CommandContext(ctx, name, args...)
cmd.Dir = dir
if env != nil {
cmd.Env = env
}
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
err := cmd.Run()
output := out.String()
if name == "go" && len(args) == 1 && args[0] == "version" && err == nil {
fields := strings.Fields(output)
if len(fields) >= 3 {
return fields[2], output, nil
}
}
return strings.TrimSpace(output), output, err
}
func sanitizeLog(log string) string {
replacers := []string{
os.Getenv("HOME"), "$HOME",
os.TempDir(), "$TMPDIR",
}
sanitized := log
for i := 0; i+1 < len(replacers); i += 2 {
if replacers[i] != "" {
sanitized = strings.ReplaceAll(sanitized, replacers[i], replacers[i+1])
}
}
for _, key := range []string{"TOKEN", "SECRET", "PASSWORD", "PRIVATE"} {
for _, part := range strings.Fields(sanitized) {
if strings.Contains(strings.ToUpper(part), key+"=") {
sanitized = strings.ReplaceAll(sanitized, part, key+"=<redacted>")
}
}
}
if len(sanitized) > DefaultBuildLogMaxBytes {
sanitized = sanitized[len(sanitized)-DefaultBuildLogMaxBytes:]
}
lines := strings.Split(sanitized, "\n")
if len(lines) > 80 {
lines = lines[len(lines)-80:]
}
return strings.TrimSpace(strings.Join(lines, "\n"))
}
func summarizeGoModules(raw string) string {
decoder := json.NewDecoder(strings.NewReader(raw))
var modules []map[string]any
for decoder.More() {
var module struct {
Path string `json:"Path"`
Version string `json:"Version"`
Main bool `json:"Main"`
Replace *struct {
Path string `json:"Path"`
Version string `json:"Version"`
} `json:"Replace"`
}
if err := decoder.Decode(&module); err != nil {
break
}
item := map[string]any{"path": module.Path, "version": module.Version, "main": module.Main}
if module.Replace != nil {
item["replace"] = map[string]string{"path": module.Replace.Path, "version": module.Replace.Version}
}
modules = append(modules, item)
}
data, err := json.Marshal(modules)
if err != nil || len(data) == 0 {
return "[]"
}
return string(data)
}
func summarizeGoVersionM(raw string) string {
summary := map[string]any{"raw_summary": sanitizeLog(raw)}
data, err := json.Marshal(summary)
if err != nil {
return "{}"
}
return string(data)
}
func abiFingerprint(manifest Manifest, goVersion string) string {
payload := strings.Join([]string{
manifest.ID,
manifest.APIVersion,
manifest.SDKModule,
manifest.SDKModuleVersion,
goVersion,
runtime.GOOS,
runtime.GOARCH,
extensionPointsFingerprint(manifest),
}, "\x00")
sum := sha256.Sum256([]byte(payload))
return hex.EncodeToString(sum[:])
}
func extensionPointsFingerprint(manifest Manifest) string {
keys := extensionPointKeys(manifest)
data, _ := json.Marshal(keys)
return string(data)
}
func validateBuiltSymbols(manifest Manifest, nmLog string) error {
entrySymbol := manifest.Runtime.EntrySymbol
if entrySymbol == "" {
entrySymbol = "Plugin"
}
if !strings.Contains(nmLog, entrySymbol) {
return fmt.Errorf("built plugin is missing entry symbol %q", entrySymbol)
}
return nil
}
func defaultBuildRequest(req BuildRequest, source ArtifactRecord) BuildRequest {
var manifest Manifest
_ = json.Unmarshal([]byte(source.MetadataJSON), &manifest)
if req.BuilderType == "" {
req.BuilderType = BuilderTypeLocalProcess
}
if req.BuilderVersion == "" {
req.BuilderVersion = "local-process/go-buildmode-plugin"
}
if req.GOOS == "" {
req.GOOS = runtime.GOOS
}
if req.GOARCH == "" {
req.GOARCH = runtime.GOARCH
}
if req.CGOEnabled == "" {
if manifest.Build.CGOEnabled != nil && !*manifest.Build.CGOEnabled {
req.CGOEnabled = "0"
} else {
req.CGOEnabled = "1"
}
}
if req.BuildTags == "" && len(manifest.Build.Tags) > 0 {
req.BuildTags = strings.Join(manifest.Build.Tags, ",")
}
if manifest.Build.VendorRequired {
req.VendorRequired = true
}
if req.SDKModule == "" {
req.SDKModule = manifest.SDKModule
req.SDKVersion = manifest.SDKModuleVersion
}
return req
}
func buildDurationMS(start time.Time) int64 {
return time.Since(start).Milliseconds()
}