build/mage/golang/build.go (132 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package golang
import (
"errors"
"fmt"
"os"
"runtime"
"strings"
"time"
"github.com/magefile/mage/mg"
"github.com/magefile/mage/sh"
"github.com/elastic/harp/build/mage/git"
)
type buildOpts struct {
binaryName string
packageName string
cgoEnabled bool
pieEnabled bool
goOS string
goArch string
goArm string
}
// BuildOption is used to define function option pattern.
type BuildOption func(*buildOpts)
// -----------------------------------------------------------------------------
// WithCGO enables CGO compilation
func WithCGO() BuildOption {
return func(opts *buildOpts) {
opts.cgoEnabled = true
}
}
// WithPIE enables Position Independent Executable compilation
func WithPIE() BuildOption {
return func(opts *buildOpts) {
opts.pieEnabled = true
}
}
// GOOS sets the GOOS value during build
func GOOS(value string) BuildOption {
return func(opts *buildOpts) {
opts.goOS = value
}
}
// GOARCH sets the GOARCH value during build
func GOARCH(value string) BuildOption {
return func(opts *buildOpts) {
opts.goArch = value
}
}
// GOARM sets the GOARM value during build
func GOARM(value string) BuildOption {
return func(opts *buildOpts) {
opts.goArm = value
}
}
// -----------------------------------------------------------------------------
// Build the given binary using the given package.
//
//nolint:funlen // to refactor
func Build(name, packageName, version string, opts ...BuildOption) func() error {
const (
defaultCgoEnabled = false
defaultGoOs = runtime.GOOS
defaultGoArch = runtime.GOARCH
defaultGoArm = ""
)
// Default build options
defaultOpts := &buildOpts{
binaryName: name,
packageName: packageName,
cgoEnabled: defaultCgoEnabled,
goOS: defaultGoOs,
goArch: defaultGoArch,
goArm: defaultGoArm,
}
// Apply options
for _, o := range opts {
o(defaultOpts)
}
return func() error {
// Retrieve git info first
mg.SerialDeps(git.CollectInfo)
// Generate artifact name
artifactName := fmt.Sprintf("%s-%s-%s%s", name, defaultOpts.goOS, defaultOpts.goArch, defaultOpts.goArm)
// Compilation flags
compilationFlags := []string{}
// Check if fips is enabled
buildTags := "-tags=!fips"
if os.Getenv("HARP_BUILD_FIPS_MODE") == "1" {
artifactName = fmt.Sprintf("%s-fips", artifactName)
compilationFlags = append(compilationFlags, "fips")
buildTags = "-tags=fips"
}
// Check if CGO is enabled
if defaultOpts.cgoEnabled {
artifactName = fmt.Sprintf("%s-cgo", artifactName)
compilationFlags = append(compilationFlags, "cgo")
}
// Enable PIE if requested
buildMode := "-buildmode=exe"
if defaultOpts.pieEnabled {
buildMode = "-buildmode=pie"
artifactName = fmt.Sprintf("%s-pie", artifactName)
compilationFlags = append(compilationFlags, "pie")
}
// Check compilation flags
strCompilationFlags := "defaults"
if len(compilationFlags) > 0 {
strCompilationFlags = strings.Join(compilationFlags, ",")
}
// Inject version information
varsSetByLinker := map[string]string{
"github.com/elastic/harp/build/version.Name": name,
"github.com/elastic/harp/build/version.AppName": packageName,
"github.com/elastic/harp/build/version.Version": version,
"github.com/elastic/harp/build/version.Commit": git.Revision,
"github.com/elastic/harp/build/version.Branch": git.Branch,
"github.com/elastic/harp/build/version.BuildDate": time.Now().Format(time.RFC3339),
"github.com/elastic/harp/build/version.BuildTags": strCompilationFlags,
}
var linkerArgs []string
for name, value := range varsSetByLinker {
linkerArgs = append(linkerArgs, "-X", fmt.Sprintf("'%s=%s'", name, value))
}
// Strip and remove DWARF
linkerArgs = append(linkerArgs, "-s", "-w")
// Assemble ldflags
ldflagsValue := strings.Join(linkerArgs, " ")
// Build environment
env := map[string]string{
"GOOS": defaultOpts.goOS,
"GOARCH": defaultOpts.goArch,
"CGO_ENABLED": "0",
}
if defaultOpts.cgoEnabled {
env["CGO_ENABLED"] = "1"
}
if defaultOpts.goArm != "" {
env["GOARM"] = defaultOpts.goArm
}
// Create output directory
if errMkDir := os.Mkdir("bin", 0o744); errMkDir != nil {
if !errors.Is(errMkDir, os.ErrExist) {
return fmt.Errorf("unable to create output directory: %w", errMkDir)
}
}
// Generate output filename
filename := fmt.Sprintf("bin/%s", artifactName)
if defaultOpts.goOS == "windows" {
filename = fmt.Sprintf("%s.exe", filename)
}
fmt.Fprintf(os.Stdout, " > Generating SBOM %s [%s] [os:%s arch:%s%s flags:%v tag:%v]\n", defaultOpts.binaryName, defaultOpts.packageName, defaultOpts.goOS, defaultOpts.goArch, defaultOpts.goArm, strCompilationFlags, version)
// Generate SBOM
if err := sh.RunWith(env, "cyclonedx-gomod", "app", "-json", "-output", fmt.Sprintf("%s.sbom.json", filename), "-files", "-licenses", "-main", fmt.Sprintf("cmd/%s", defaultOpts.binaryName), "-packages"); err != nil {
return fmt.Errorf("unable to generate SBOM for artifact: %w", err)
}
fmt.Fprintf(os.Stdout, " > Building %s [%s] [os:%s arch:%s%s flags:%v tag:%v]\n", defaultOpts.binaryName, defaultOpts.packageName, defaultOpts.goOS, defaultOpts.goArch, defaultOpts.goArm, strCompilationFlags, version)
// Compile
return sh.RunWith(env, "go", "build", buildMode, buildTags, "-trimpath", "-mod=readonly", "-ldflags", ldflagsValue, "-o", filename, packageName)
}
}