From e2c2c862f83a8a277c530600bb3edc7d6ed1704b Mon Sep 17 00:00:00 2001 From: Dmitry Kolesnikov Date: Sat, 13 Dec 2025 15:01:34 +0200 Subject: [PATCH] enable toolchain config (ldflags, ccflags, etc) --- README.md | 57 ++++++++++++++++++++++++++++++++++++++++---- container.go | 18 ++++---------- doc/scud-logo-v2.svg | 51 +++++++++++++++++++++++++++++++++++++++ gocc.go | 56 +++++++++++++++++++++++++++---------------- handler.go | 18 ++++---------- scud_test.go | 24 +++++++++++-------- 6 files changed, 163 insertions(+), 61 deletions(-) create mode 100644 doc/scud-logo-v2.svg diff --git a/README.md b/README.md index eeeb2a5..ffdf326 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

scud

serverless Golang made simple, secure, and fast

@@ -47,6 +47,7 @@ With SCUD, you can deliver fast, secure, and maintainable serverless APIs withou - [Linker Flags and Version Injection](#linker-flags-and-version-injection) - [Lambda Environment Variables](#lambda-environment-variables) - [Architecture: Graviton vs x86\_64](#architecture-graviton-vs-x86_64) + - [CGO / C Libraries](#cgo--c-libraries) - [Container images](#container-images) - [Universal Function](#universal-function) - [Custom Go Environment](#custom-go-environment) @@ -133,9 +134,11 @@ scud.NewFunctionGo(stack, jsii.String("Handler"), SourceCodeModule: "github.com/fogfish/scud", SourceCodeLambda: "test/lambda/go", SourceCodeVersion: "v1.2.3", // Injects -X main.version=v1.2.3 - GoVar: map[string]string{ - "main.buildTime": "2024-01-01", // Injects -X main.buildTime=2024-01-01 - "main.commit": "abc123", // Injects -X main.commit=abc123 + Toolchain: &scud.Toolchain{ + LDVars:: map[string]string{ + "main.buildTime": "2024-01-01", // Injects -X main.buildTime=2024-01-01 + "main.commit": "abc123", // Injects -X main.commit=abc123 + }, }, }, ) @@ -185,11 +188,55 @@ scud.NewFunctionGo(scope, jsii.String("test"), &scud.FunctionGoProps{ SourceCodeModule: "github.com/fogfish/scud", SourceCodeLambda: "test/lambda/go", - GoEnv: map[string]string{"GOARCH": "amd64"}, + Toolchain: &scud.Toolchain{ + GoEnv: map[string]string{"GOARCH": "amd64"}, + }, + }, +) +``` + +### CGO / C Libraries + +CGO is disabled by default. Use standard Golang environment variable `"CGO_ENABLED"` to enable. + +```go +scud.NewFunctionGo(scope, jsii.String("test"), + &scud.FunctionGoProps{ + SourceCodeModule: "github.com/fogfish/scud", + SourceCodeLambda: "test/lambda/go", + Toolchain: &scud.Toolchain{ + GoEnv: map[string]string{"CGO_ENABLED": "1"}, + }, + }, +) +``` + +When developing on macOS, building Go binaries that depend on CGO or C libraries may fail or produce incompatible artifacts. Use a Linux cross-compiler (e.g. musl toolchain). + +```bash +brew install FiloSottile/musl-cross/musl-cross +``` + +```go +scud.NewFunctionGo(scope, jsii.String("test"), + &scud.FunctionGoProps{ + SourceCodeModule: "github.com/fogfish/scud", + SourceCodeLambda: "test/lambda/go", + Toolchain: &scud.Toolchain{ + GoEnv: map[string]string{ + "CGO_ENABLED": "1", + "CC": "aarch64-linux-musl-gcc", + }, + LDVars: []string{"-linkmode external", `-extldflags "-static"`}, + }, }, ) ``` +Known limitations +* Some C libraries cannot be statically linked +* Libraries that require glibc may not build correctly on macOS +* In such cases, builds must be performed on a Linux system ### Container images diff --git a/container.go b/container.go index a2f8135..11a13b4 100644 --- a/container.go +++ b/container.go @@ -40,15 +40,8 @@ type ContainerGoProps struct { // -ldflags '-X main.version=...' SourceCodeVersion string - // Variables and its values passed as linker flags - // -ldflags '-X key1=val1 -X key2=val2 ...' - GoVar map[string]string - - // Go environment, default includes - // GOOS=linux - // GOARCH=arm64 - // CGO_ENABLED=0 - GoEnv map[string]string + // Toolchain configuration for building Go Lambda function + Toolchain *Toolchain // Static files included into container, the path is relative to module StaticAssets []string @@ -92,8 +85,8 @@ func NewContainerGo(scope constructs.Construct, id *string, spec *ContainerGoPro platContainer := "linux/arm64" platCode := awsecrassets.Platform_LINUX_ARM64() props.Architecture = awslambda.Architecture_ARM_64() - if spec.GoEnv != nil { - switch spec.GoEnv["GOARCH"] { + if spec.Toolchain != nil && spec.Toolchain.GoEnv != nil { + switch spec.Toolchain.GoEnv["GOARCH"] { case "amd64": platContainer = "linux/amd64" props.Architecture = awslambda.Architecture_X86_64() @@ -109,8 +102,7 @@ func NewContainerGo(scope constructs.Construct, id *string, spec *ContainerGoPro spec.SourceCodeModule, spec.SourceCodeLambda, spec.SourceCodeVersion, - spec.GoVar, - spec.GoEnv, + spec.Toolchain, ) path := filepath.Join(os.TempDir(), spec.SourceCodeModule, spec.SourceCodeLambda) diff --git a/doc/scud-logo-v2.svg b/doc/scud-logo-v2.svg new file mode 100644 index 0000000..27ebf95 --- /dev/null +++ b/doc/scud-logo-v2.svg @@ -0,0 +1,51 @@ + + + + diff --git a/gocc.go b/gocc.go index 3626798..985b9e5 100644 --- a/gocc.go +++ b/gocc.go @@ -14,13 +14,28 @@ import ( "github.com/aws/jsii-runtime-go" ) +// Configure Go compiler and Linter +type Toolchain struct { + // Go environment, default includes + // GOOS=linux + // GOARCH=arm64 + // CGO_ENABLED=0 + GoEnv map[string]string + + // Linker flags passed to the Go linker (-ldflags). + LDFlags []string + + // Go variables injected at link time using -X key=value. + // Example: -ldflags "-X main.version=1.0.0" + LDVars map[string]string +} + type GoCompiler struct { sourceCode string sourceCodePackage string sourceCodeLambda string sourceCodeVersion string - govar map[string]string - goenv map[string]string + config *Toolchain } const goBinary = "bootstrap" @@ -29,15 +44,14 @@ func NewGoCompiler( sourceCodePackage string, sourceCodeLambda string, sourceCodeVersion string, - govar map[string]string, - goenv map[string]string, + config *Toolchain, ) *GoCompiler { - if goenv == nil { - goenv = map[string]string{} - } - - if govar == nil { - govar = map[string]string{} + if config == nil { + config = &Toolchain{ + GoEnv: map[string]string{}, + LDVars: map[string]string{}, + LDFlags: []string{}, + } } return &GoCompiler{ @@ -45,8 +59,7 @@ func NewGoCompiler( sourceCodePackage: sourceCodePackage, sourceCodeLambda: sourceCodeLambda, sourceCodeVersion: sourceCodeVersion, - govar: govar, - goenv: goenv, + config: config, } } @@ -61,11 +74,14 @@ func (g *GoCompiler) TryBundle(outputDir *string, options *awscdk.BundlingOption goflags := []string{"build", "-tags", "lambda.norpc"} ldflags := []string{"-s", "-w"} + if len(g.config.LDFlags) > 0 { + ldflags = append(ldflags, g.config.LDFlags...) + } if g.sourceCodeVersion != "" { ldflags = append(ldflags, fmt.Sprintf("-X main.version=%s", g.sourceCodeVersion)) } - for name, value := range g.govar { + for name, value := range g.config.LDVars { ldflags = append(ldflags, fmt.Sprintf("-X %s=%s", name, value)) } goflags = append(goflags, "-ldflags", strings.Join(ldflags, " ")) @@ -117,8 +133,8 @@ func (g *GoCompiler) cmdEnv() []string { "GOROOT", "GOMODCACHE", } { - if _, exists := g.goenv[envvar]; !exists { - g.goenv[envvar] = os.Getenv(envvar) + if _, exists := g.config.GoEnv[envvar]; !exists { + g.config.GoEnv[envvar] = os.Getenv(envvar) } } @@ -127,21 +143,21 @@ func (g *GoCompiler) cmdEnv() []string { "GOARCH": "arm64", "CGO_ENABLED": "0", } { - if _, exists := g.goenv[envvar]; !exists { - g.goenv[envvar] = defval + if _, exists := g.config.GoEnv[envvar]; !exists { + g.config.GoEnv[envvar] = defval } } for envvar, gen := range map[string]func() string{ "GOCACHE": g.goCache, } { - if _, exists := g.goenv[envvar]; !exists { - g.goenv[envvar] = gen() + if _, exists := g.config.GoEnv[envvar]; !exists { + g.config.GoEnv[envvar] = gen() } } env := make([]string, 0) - for key, val := range g.goenv { + for key, val := range g.config.GoEnv { env = append(env, key+"="+val) } return env diff --git a/handler.go b/handler.go index d331bef..dcb45ce 100644 --- a/handler.go +++ b/handler.go @@ -35,15 +35,8 @@ type FunctionGoProps struct { // -ldflags '-X main.version=...' SourceCodeVersion string - // Variables and its values passed as linker flags - // -ldflags '-X key1=val1 -X key2=val2 ...' - GoVar map[string]string - - // Go environment, default includes - // GOOS=linux - // GOARCH=arm64 - // CGO_ENABLED=0 - GoEnv map[string]string + // Toolchain configuration for building Go Lambda function + Toolchain *Toolchain } func (*FunctionGoProps) HKT1(awslambda.Function) {} @@ -80,8 +73,8 @@ func NewFunctionGo(scope constructs.Construct, id *string, spec *FunctionGoProps // arm64 is default deployment props.Architecture = awslambda.Architecture_ARM_64() - if spec.GoEnv != nil { - switch spec.GoEnv["GOARCH"] { + if spec.Toolchain != nil && spec.Toolchain.GoEnv != nil { + switch spec.Toolchain.GoEnv["GOARCH"] { case "amd64": props.Architecture = awslambda.Architecture_X86_64() case "arm64": @@ -93,8 +86,7 @@ func NewFunctionGo(scope constructs.Construct, id *string, spec *FunctionGoProps spec.SourceCodeModule, spec.SourceCodeLambda, spec.SourceCodeVersion, - spec.GoVar, - spec.GoEnv, + spec.Toolchain, ) props.Code = AssetCodeGo(gocc) props.Handler = jsii.String(goBinary) diff --git a/scud_test.go b/scud_test.go index 540ca1f..f823d56 100644 --- a/scud_test.go +++ b/scud_test.go @@ -57,11 +57,13 @@ func TestFunctionGoArch(t *testing.T) { SourceCodeModule: "github.com/fogfish/scud", SourceCodeLambda: "test/lambda/go", SourceCodeVersion: "v1.2.3", - GoEnv: map[string]string{ - "GOARCH": arch, - }, - GoVar: map[string]string{ - "main.some": "1.2.3", + Toolchain: &scud.Toolchain{ + GoEnv: map[string]string{ + "GOARCH": arch, + }, + LDVars: map[string]string{ + "main.some": "1.2.3", + }, }, }, ) @@ -185,11 +187,13 @@ func TestFunctionGoContainerArch(t *testing.T) { SourceCodeModule: "github.com/fogfish/scud", SourceCodeLambda: "test/lambda/go", SourceCodeVersion: "v1.2.3", - GoEnv: map[string]string{ - "GOARCH": arch, - }, - GoVar: map[string]string{ - "main.some": "1.2.3", + Toolchain: &scud.Toolchain{ + GoEnv: map[string]string{ + "GOARCH": arch, + }, + LDVars: map[string]string{ + "main.some": "1.2.3", + }, }, }, )