Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: build of shared/static libraries #3511

Merged
merged 5 commits into from Nov 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -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"
}
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)
caarlos0 marked this conversation as resolved.
Show resolved Hide resolved
}

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
```