Skip to content

Commit

Permalink
feat: build of shared/static libraries (goreleaser#3511)
Browse files Browse the repository at this point in the history
<!--

Hi, thanks for contributing!

Please make sure you read our CONTRIBUTING guide.

Also, add tests and the respective documentation changes as well.

-->


<!-- If applied, this commit will... -->

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.

<!-- Why is this change being made? -->

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.

<!-- # Provide links to any relevant tickets, URLs or other resources
-->

This was previously discussed in goreleaser#3497.

I couldn't quite think of a proper way to add some tests to the header
archiving feature. Any recommendation?
  • Loading branch information
borgoat authored and gal-legit committed Nov 13, 2022
1 parent 52ed38c commit bb983ad
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 39 deletions.
12 changes: 12 additions & 0 deletions internal/artifact/artifact.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -106,6 +112,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"
}
Expand Down
72 changes: 63 additions & 9 deletions internal/builders/golang/build.go
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
},
}
}
3 changes: 3 additions & 0 deletions internal/pipe/archive/archive.go
Expand Up @@ -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...))
Expand Down
41 changes: 28 additions & 13 deletions internal/pipe/build/build.go
Expand Up @@ -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)
Expand Down Expand Up @@ -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 ""
}
45 changes: 34 additions & 11 deletions internal/pipe/build/build_test.go
Expand Up @@ -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) {
Expand Down
13 changes: 7 additions & 6 deletions pkg/config/config.go
Expand Up @@ -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 {
Expand Down
31 changes: 31 additions & 0 deletions www/docs/customization/build.md
Expand Up @@ -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:
Expand Down Expand Up @@ -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
```

0 comments on commit bb983ad

Please sign in to comment.