From 0a536f08fd4df847b3777a658aea75b788f8fa4a Mon Sep 17 00:00:00 2001 From: Giorgio Azzinnaro Date: Sat, 12 Nov 2022 03:35:51 +0100 Subject: [PATCH] feat: build of shared/static libraries (#3511) This PR improves the handling of shared or static libraries by GoReleaser. It uses the default behaviour of the Go compiler by appending the right extension to libraries. * `.so` and `.a` for Linux shared libraries and static libraries respectively * `.dylib` and `.a.` on Darwin * `.dll` and `.lib` on Windows (pre-existent) It does not add any configuration option to `.goreleaser.yml`, since it leverages the existing `buildmode` flag. Additionally, this PR takes care of adding the generated header file into the archive. Personally I would leverage this change to release some software both as a CLI and as a shared library. I believe others who use CGo or need interoperability with Go from other languages could benefit from this. This was previously discussed in #3497. I couldn't quite think of a proper way to add some tests to the header archiving feature. Any recommendation? --- internal/artifact/artifact.go | 12 ++++++ internal/builders/golang/build.go | 72 +++++++++++++++++++++++++++---- internal/pipe/archive/archive.go | 3 ++ internal/pipe/build/build.go | 41 ++++++++++++------ internal/pipe/build/build_test.go | 45 ++++++++++++++----- pkg/config/config.go | 13 +++--- www/docs/customization/build.md | 31 +++++++++++++ 7 files changed, 178 insertions(+), 39 deletions(-) diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go index 06434e02c4c..c917c13c228 100644 --- a/internal/artifact/artifact.go +++ b/internal/artifact/artifact.go @@ -66,6 +66,12 @@ const ( ScoopManifest // SBOM is a Software Bill of Materials file. SBOM + // Header is a C header file, generated for CGo library builds. + Header + // CArchive is a C static library, generated via a CGo build with buildmode=c-archive. + CArchive + // CShared is a C shared library, generated via a CGo build with buildmode=c-shared. + CShared ) func (t Type) String() string { @@ -104,6 +110,12 @@ func (t Type) String() string { return "PKGBUILD" case SrcInfo: return "SRCINFO" + case Header: + return "C Header" + case CArchive: + return "C Archive Library" + case CShared: + return "C Shared Library" default: return "unknown" } diff --git a/internal/builders/golang/build.go b/internal/builders/golang/build.go index 7d76e135329..0d3c6366d7c 100644 --- a/internal/builders/golang/build.go +++ b/internal/builders/golang/build.go @@ -135,7 +135,7 @@ func (*Builder) Build(ctx *context.Context, build config.Build, options api.Opti return err } - artifact := &artifact.Artifact{ + a := &artifact.Artifact{ Type: artifact.Binary, Path: options.Path, Name: options.Name, @@ -151,6 +151,15 @@ func (*Builder) Build(ctx *context.Context, build config.Build, options api.Opti }, } + if build.Buildmode == "c-archive" { + a.Type = artifact.CArchive + ctx.Artifacts.Add(getHeaderArtifactForLibrary(build, options)) + } + if build.Buildmode == "c-shared" { + a.Type = artifact.CShared + ctx.Artifacts.Add(getHeaderArtifactForLibrary(build, options)) + } + details, err := withOverrides(ctx, build, options) if err != nil { return err @@ -167,7 +176,7 @@ func (*Builder) Build(ctx *context.Context, build config.Build, options api.Opti "GOAMD64="+options.Goamd64, ) - cmd, err := buildGoBuildLine(ctx, build, details, options, artifact, env) + cmd, err := buildGoBuildLine(ctx, build, details, options, a, env) if err != nil { return err } @@ -177,7 +186,7 @@ func (*Builder) Build(ctx *context.Context, build config.Build, options api.Opti } if build.ModTimestamp != "" { - modTimestamp, err := tmpl.New(ctx).WithEnvS(env).WithArtifact(artifact, map[string]string{}).Apply(build.ModTimestamp) + modTimestamp, err := tmpl.New(ctx).WithEnvS(env).WithArtifact(a, map[string]string{}).Apply(build.ModTimestamp) if err != nil { return err } @@ -192,7 +201,7 @@ func (*Builder) Build(ctx *context.Context, build config.Build, options api.Opti } } - ctx.Artifacts.Add(artifact) + ctx.Artifacts.Add(a) return nil } @@ -206,11 +215,12 @@ func withOverrides(ctx *context.Context, build config.Build, options api.Options if optsTarget == overrideTarget { dets := config.BuildDetails{ - Ldflags: build.BuildDetails.Ldflags, - Tags: build.BuildDetails.Tags, - Flags: build.BuildDetails.Flags, - Asmflags: build.BuildDetails.Asmflags, - Gcflags: build.BuildDetails.Gcflags, + Buildmode: build.BuildDetails.Buildmode, + Ldflags: build.BuildDetails.Ldflags, + Tags: build.BuildDetails.Tags, + Flags: build.BuildDetails.Flags, + Asmflags: build.BuildDetails.Asmflags, + Gcflags: build.BuildDetails.Gcflags, } if err := mergo.Merge(&dets, o.BuildDetails, mergo.WithOverride); err != nil { return build.BuildDetails, err @@ -228,6 +238,9 @@ func withOverrides(ctx *context.Context, build config.Build, options api.Options func buildGoBuildLine(ctx *context.Context, build config.Build, details config.BuildDetails, options api.Options, artifact *artifact.Artifact, env []string) ([]string, error) { cmd := []string{build.GoBinary, build.Command} + // tags, ldflags, and buildmode, should only appear once, warning only to avoid a breaking change + validateUniqueFlags(details) + flags, err := processFlags(ctx, artifact, env, details.Flags, "") if err != nil { return cmd, err @@ -266,10 +279,28 @@ func buildGoBuildLine(ctx *context.Context, build config.Build, details config.B cmd = append(cmd, "-ldflags="+strings.Join(ldflags, " ")) } + if details.Buildmode != "" { + cmd = append(cmd, "-buildmode="+details.Buildmode) + } + cmd = append(cmd, "-o", options.Path, build.Main) return cmd, nil } +func validateUniqueFlags(details config.BuildDetails) { + for _, flag := range details.Flags { + if strings.HasPrefix(flag, "-tags") && len(details.Tags) > 0 { + log.WithField("flag", flag).WithField("tags", details.Tags).Warn("tags is defined twice") + } + if strings.HasPrefix(flag, "-ldflags") && len(details.Ldflags) > 0 { + log.WithField("flag", flag).WithField("ldflags", details.Ldflags).Warn("ldflags is defined twice") + } + if strings.HasPrefix(flag, "-buildmode") && details.Buildmode != "" { + log.WithField("flag", flag).WithField("buildmode", details.Buildmode).Warn("buildmode is defined twice") + } + } +} + func processFlags(ctx *context.Context, a *artifact.Artifact, env, flags []string, flagPrefix string) ([]string, error) { processed := make([]string, 0, len(flags)) for _, rawFlag := range flags { @@ -366,3 +397,26 @@ func hasMain(file *ast.File) bool { } return false } + +func getHeaderArtifactForLibrary(build config.Build, options api.Options) *artifact.Artifact { + fullPathWithoutExt := strings.TrimSuffix(options.Path, options.Ext) + basePath := filepath.Base(fullPathWithoutExt) + fullPath := fullPathWithoutExt + ".h" + headerName := basePath + ".h" + + return &artifact.Artifact{ + Type: artifact.Header, + Path: fullPath, + Name: headerName, + Goos: options.Goos, + Goarch: options.Goarch, + Goamd64: options.Goamd64, + Goarm: options.Goarm, + Gomips: options.Gomips, + Extra: map[string]interface{}{ + artifact.ExtraBinary: headerName, + artifact.ExtraExt: ".h", + artifact.ExtraID: build.ID, + }, + } +} diff --git a/internal/pipe/archive/archive.go b/internal/pipe/archive/archive.go index e5219ed4b59..4b20472e638 100644 --- a/internal/pipe/archive/archive.go +++ b/internal/pipe/archive/archive.go @@ -91,6 +91,9 @@ func (Pipe) Run(ctx *context.Context) error { filter := []artifact.Filter{artifact.Or( artifact.ByType(artifact.Binary), artifact.ByType(artifact.UniversalBinary), + artifact.ByType(artifact.Header), + artifact.ByType(artifact.CArchive), + artifact.ByType(artifact.CShared), )} if len(archive.Builds) > 0 { filter = append(filter, artifact.ByIDs(archive.Builds...)) diff --git a/internal/pipe/build/build.go b/internal/pipe/build/build.go index a05f42d2ed0..003c715ed15 100644 --- a/internal/pipe/build/build.go +++ b/internal/pipe/build/build.go @@ -156,7 +156,7 @@ func doBuild(ctx *context.Context, build config.Build, opts builders.Options) er } func buildOptionsForTarget(ctx *context.Context, build config.Build, target string) (*builders.Options, error) { - ext := extFor(target, build.Flags) + ext := extFor(target, build.BuildDetails) parts := strings.Split(target, "_") if len(parts) < 2 { return nil, fmt.Errorf("%s is not a valid build target", target) @@ -211,20 +211,35 @@ func buildOptionsForTarget(ctx *context.Context, build config.Build, target stri return &buildOpts, nil } -func extFor(target string, flags config.FlagArray) string { - if strings.Contains(target, "windows") { - for _, s := range flags { - if s == "-buildmode=c-shared" { - return ".dll" - } - if s == "-buildmode=c-archive" { - return ".lib" - } - } - return ".exe" - } +func extFor(target string, build config.BuildDetails) string { if target == "js_wasm" { return ".wasm" } + + // Configure the extensions for shared and static libraries - by default .so and .a respectively - + // with overrides for Windows (.dll for shared and .lib for static) and .dylib for macOS. + buildmode := build.Buildmode + + if buildmode == "c-shared" { + if strings.Contains(target, "darwin") { + return ".dylib" + } + if strings.Contains(target, "windows") { + return ".dll" + } + return ".so" + } + + if buildmode == "c-archive" { + if strings.Contains(target, "windows") { + return ".lib" + } + return ".a" + } + + if strings.Contains(target, "windows") { + return ".exe" + } + return "" } diff --git a/internal/pipe/build/build_test.go b/internal/pipe/build/build_test.go index 7155f8fd9f1..596b0e0740a 100644 --- a/internal/pipe/build/build_test.go +++ b/internal/pipe/build/build_test.go @@ -426,24 +426,47 @@ func TestSkipBuild(t *testing.T) { require.Len(t, ctx.Artifacts.List(), 0) } +func TestExtDarwin(t *testing.T) { + require.Equal(t, "", extFor("darwin_amd64", config.BuildDetails{})) + require.Equal(t, "", extFor("darwin_arm64", config.BuildDetails{})) + require.Equal(t, "", extFor("darwin_amd64", config.BuildDetails{})) + require.Equal(t, ".dylib", extFor("darwin_amd64", config.BuildDetails{Buildmode: "c-shared"})) + require.Equal(t, ".dylib", extFor("darwin_arm64", config.BuildDetails{Buildmode: "c-shared"})) + require.Equal(t, ".a", extFor("darwin_amd64", config.BuildDetails{Buildmode: "c-archive"})) + require.Equal(t, ".a", extFor("darwin_arm64", config.BuildDetails{Buildmode: "c-archive"})) +} + +func TestExtLinux(t *testing.T) { + require.Equal(t, "", extFor("linux_amd64", config.BuildDetails{})) + require.Equal(t, "", extFor("linux_386", config.BuildDetails{})) + require.Equal(t, "", extFor("linux_amd64", config.BuildDetails{})) + require.Equal(t, ".so", extFor("linux_amd64", config.BuildDetails{Buildmode: "c-shared"})) + require.Equal(t, ".so", extFor("linux_386", config.BuildDetails{Buildmode: "c-shared"})) + require.Equal(t, ".a", extFor("linux_amd64", config.BuildDetails{Buildmode: "c-archive"})) + require.Equal(t, ".a", extFor("linux_386", config.BuildDetails{Buildmode: "c-archive"})) +} + func TestExtWindows(t *testing.T) { - require.Equal(t, ".exe", extFor("windows_amd64", config.FlagArray{})) - require.Equal(t, ".exe", extFor("windows_386", config.FlagArray{})) - require.Equal(t, ".exe", extFor("windows_amd64", config.FlagArray{"-tags=dev", "-v"})) - require.Equal(t, ".dll", extFor("windows_amd64", config.FlagArray{"-tags=dev", "-v", "-buildmode=c-shared"})) - require.Equal(t, ".dll", extFor("windows_386", config.FlagArray{"-buildmode=c-shared"})) - require.Equal(t, ".lib", extFor("windows_amd64", config.FlagArray{"-buildmode=c-archive"})) - require.Equal(t, ".lib", extFor("windows_386", config.FlagArray{"-tags=dev", "-v", "-buildmode=c-archive"})) + require.Equal(t, ".exe", extFor("windows_amd64", config.BuildDetails{})) + require.Equal(t, ".exe", extFor("windows_386", config.BuildDetails{})) + require.Equal(t, ".exe", extFor("windows_amd64", config.BuildDetails{})) + require.Equal(t, ".dll", extFor("windows_amd64", config.BuildDetails{Buildmode: "c-shared"})) + require.Equal(t, ".dll", extFor("windows_386", config.BuildDetails{Buildmode: "c-shared"})) + require.Equal(t, ".lib", extFor("windows_amd64", config.BuildDetails{Buildmode: "c-archive"})) + require.Equal(t, ".lib", extFor("windows_386", config.BuildDetails{Buildmode: "c-archive"})) } func TestExtWasm(t *testing.T) { - require.Equal(t, ".wasm", extFor("js_wasm", config.FlagArray{})) + require.Equal(t, ".wasm", extFor("js_wasm", config.BuildDetails{})) } func TestExtOthers(t *testing.T) { - require.Empty(t, "", extFor("linux_amd64", config.FlagArray{})) - require.Empty(t, "", extFor("linuxwin_386", config.FlagArray{})) - require.Empty(t, "", extFor("winasdasd_sad", config.FlagArray{})) + require.Equal(t, "", extFor("linux_amd64", config.BuildDetails{})) + require.Equal(t, "", extFor("linuxwin_386", config.BuildDetails{})) + require.Equal(t, "", extFor("winasdasd_sad", config.BuildDetails{})) + require.Equal(t, ".so", extFor("aix_amd64", config.BuildDetails{Buildmode: "c-shared"})) + require.Equal(t, ".a", extFor("android_386", config.BuildDetails{Buildmode: "c-archive"})) + require.Equal(t, ".so", extFor("winasdasd_sad", config.BuildDetails{Buildmode: "c-shared"})) } func TestTemplate(t *testing.T) { diff --git a/pkg/config/config.go b/pkg/config/config.go index 6060202c23b..29a803b9f56 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -333,12 +333,13 @@ type BuildDetailsOverride struct { } type BuildDetails struct { - Ldflags StringArray `yaml:"ldflags,omitempty" json:"ldflags,omitempty"` - Tags FlagArray `yaml:"tags,omitempty" json:"tags,omitempty"` - Flags FlagArray `yaml:"flags,omitempty" json:"flags,omitempty"` - Asmflags StringArray `yaml:"asmflags,omitempty" json:"asmflags,omitempty"` - Gcflags StringArray `yaml:"gcflags,omitempty" json:"gcflags,omitempty"` - Env []string `yaml:"env,omitempty" json:"env,omitempty"` + Buildmode string `yaml:"buildmode,omitempty" json:"buildmode,omitempty"` + Ldflags StringArray `yaml:"ldflags,omitempty" json:"ldflags,omitempty"` + Tags FlagArray `yaml:"tags,omitempty" json:"tags,omitempty"` + Flags FlagArray `yaml:"flags,omitempty" json:"flags,omitempty"` + Asmflags StringArray `yaml:"asmflags,omitempty" json:"asmflags,omitempty"` + Gcflags StringArray `yaml:"gcflags,omitempty" json:"gcflags,omitempty"` + Env []string `yaml:"env,omitempty" json:"env,omitempty"` } type BuildHookConfig struct { diff --git a/www/docs/customization/build.md b/www/docs/customization/build.md index e260dcde1c7..d8e5a9d2ce2 100644 --- a/www/docs/customization/build.md +++ b/www/docs/customization/build.md @@ -51,6 +51,11 @@ builds: - -s -w -X main.build={{.Version}} - ./usemsan=-msan + # Custom Go build mode. + # `c-shared` and `c-archive` configure the publishing of the header and set the correct extension. + # Default is empty. + buildmode: c-shared + # Custom build tags templates. # Default is empty. tags: @@ -517,3 +522,29 @@ will evaluate to the list of first class ports as defined in the Go wiki. You can read more about it [here](https://github.com/golang/go/wiki/PortingPolicy#first-class-ports). + +## Building shared or static libraries + +GoReleaser supports compiling and releasing C shared or static libraries, +by configuring the [Go build mode](https://pkg.go.dev/cmd/go#hdr-Build_modes). + +This can be set with `buildmode` in your build. It currently supports `c-shared` and `c-archive`. +Other values will transparently be applied to the build line (via the `-buildmode` flag), +but GoReleaser will not attempt to configure any additional logic. + +As of today, a template may not be applied to this field. + +GoReleaser will: + +* set the correct file extension for the target OS. +* package the generated header file (`.h`) in the release bundle. + +```yaml +# .goreleaser.yaml +builds: + - + id: "my-library" + + # Configure the buildmode flag to output a shared library + buildmode: "c-shared" # or "c-archive" for a static library +```