dev-tools/mage/build.go (216 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package mage
import (
"errors"
"fmt"
"go/build"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/josephspurrier/goversioninfo"
"github.com/magefile/mage/sh"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"github.com/elastic/elastic-agent/dev-tools/packaging"
)
// BuildArgs are the arguments used for the "build" target and they define how
// "go build" is invoked.
type BuildArgs struct {
Name string // Name of binary. (On Windows '.exe' is appended.)
InputFiles []string
OutputDir string
CGO bool
Static bool
Env map[string]string
LDFlags []string
Vars map[string]string // Vars that are passed as -X key=value with the ldflags.
ExtraFlags []string
WinMetadata bool // Add resource metadata to Windows binaries (like add the version number to the .exe properties).
}
// buildTagRE is a regexp to match strings like "-tags=abcd"
// but does not match "-tags= "
var buildTagRE = regexp.MustCompile(`-tags=([\S]+)?`)
// ParseBuildTags returns the ExtraFlags param where all flags that are go build tags are joined by a comma.
//
// For example if given -someflag=val1 -tags=buildtag1 -tags=buildtag2
// It will return -someflag=val1 -tags=buildtag1,buildtag2
func (b BuildArgs) ParseBuildTags() []string {
flags := make([]string, 0)
if len(b.ExtraFlags) == 0 {
return flags
}
buildTags := make([]string, 0)
for _, flag := range b.ExtraFlags {
if buildTagRE.MatchString(flag) {
arr := buildTagRE.FindStringSubmatch(flag)
if len(arr) != 2 || arr[1] == "" {
log.Printf("Unexpected format found for buildargs.ExtraFlags, ignoring value %q", flag)
continue
}
buildTags = append(buildTags, arr[1])
} else {
flags = append(flags, flag)
}
}
if len(buildTags) > 0 {
flags = append(flags, "-tags="+strings.Join(buildTags, ","))
}
return flags
}
// DefaultBuildArgs returns the default BuildArgs for use in builds.
func DefaultBuildArgs() BuildArgs {
args := BuildArgs{
Name: BeatName,
CGO: build.Default.CgoEnabled,
Env: map[string]string{},
Vars: map[string]string{
elasticAgentModulePath + "/version.buildTime": "{{ date }}",
elasticAgentModulePath + "/version.commit": "{{ commit }}",
},
WinMetadata: true,
}
if versionQualified {
args.Vars[elasticAgentModulePath+"/version.qualifier"] = "{{ .Qualifier }}"
}
if positionIndependentCodeSupported() {
args.ExtraFlags = append(args.ExtraFlags, "-buildmode", "pie")
}
if FIPSBuild {
fipsConfig := packaging.Settings().FIPS
for _, tag := range fipsConfig.Compile.Tags {
args.ExtraFlags = append(args.ExtraFlags, "-tags="+tag)
}
args.CGO = args.CGO || fipsConfig.Compile.CGO
for varName, value := range fipsConfig.Compile.Env {
args.Env[varName] = value
}
}
if DevBuild {
// Disable optimizations (-N) and inlining (-l) for debugging.
args.ExtraFlags = append(args.ExtraFlags, `-gcflags=all=-N -l`)
} else {
// Strip all debug symbols from binary (does not affect Go stack traces).
args.LDFlags = append(args.LDFlags, "-s")
// Remove all file system paths from the compiled executable, to improve build reproducibility
args.ExtraFlags = append(args.ExtraFlags, "-trimpath")
}
return args
}
// positionIndependentCodeSupported checks if the target platform support position independent code (or ASLR).
//
// The list of supported platforms is compiled based on the Go release notes: https://golang.org/doc/devel/release.html
// The list has been updated according to the Go version: 1.16
func positionIndependentCodeSupported() bool {
return oneOf(Platform.GOOS, "darwin") ||
(Platform.GOOS == "linux" && oneOf(Platform.GOARCH, "riscv64", "amd64", "arm", "arm64", "ppc64le", "386")) ||
(Platform.GOOS == "aix" && Platform.GOARCH == "ppc64") ||
// Windows 32bit supports ASLR, but Windows Server 2003 and earlier do not.
// According to the support matrix (https://www.elastic.co/support/matrix), these old versions
// are not supported.
(Platform.GOOS == "windows")
}
func oneOf(value string, lst ...string) bool {
for _, other := range lst {
if other == value {
return true
}
}
return false
}
// DefaultGolangCrossBuildArgs returns the default BuildArgs for use in
// cross-builds.
func DefaultGolangCrossBuildArgs() BuildArgs {
args := DefaultBuildArgs()
args.Name += "-" + Platform.GOOS + "-" + Platform.Arch
args.OutputDir = filepath.Join("build", "golang-crossbuild")
if bp, found := BuildPlatforms.Get(Platform.Name); found {
args.CGO = bp.Flags.SupportsCGO()
}
// Enable DEP (data execution protection) for Windows binaries.
if Platform.GOOS == "windows" {
args.LDFlags = append(args.LDFlags, "-extldflags=-Wl,--nxcompat")
}
return args
}
// GolangCrossBuild invokes "go build" inside of the golang-crossbuild Docker
// environment.
func GolangCrossBuild(params BuildArgs) error {
if os.Getenv("GOLANG_CROSSBUILD") != "1" {
return errors.New("Use the crossBuild target. golangCrossBuild can " +
"only be executed within the golang-crossbuild docker environment")
}
defer DockerChown(filepath.Join(params.OutputDir, params.Name+binaryExtension(GOOS)))
defer DockerChown(filepath.Join(params.OutputDir))
mountPoint, err := ElasticBeatsDir()
if err != nil {
return err
}
if err := sh.Run("git", "config", "--global", "--add", "safe.directory", mountPoint); err != nil {
return err
}
return Build(params)
}
// Build invokes "go build" to produce a binary.
func Build(params BuildArgs) error {
fmt.Println(">> build: Building", params.Name)
binaryName := params.Name + binaryExtension(GOOS)
if params.OutputDir != "" {
if err := os.MkdirAll(params.OutputDir, 0755); err != nil {
return err
}
}
// Environment
env := params.Env
if env == nil {
env = map[string]string{}
}
cgoEnabled := "0"
if params.CGO {
cgoEnabled = "1"
}
env["CGO_ENABLED"] = cgoEnabled
// Spec
args := []string{
"build",
"-o",
filepath.Join(params.OutputDir, binaryName),
}
args = append(args, params.ParseBuildTags()...)
// ldflags
ldflags := params.LDFlags
if params.Static {
ldflags = append(ldflags, `-extldflags '-static'`)
}
for k, v := range params.Vars {
ldflags = append(ldflags, fmt.Sprintf("-X %v=%v", k, v))
}
if len(ldflags) > 0 {
args = append(args, "-ldflags")
args = append(args, MustExpand(strings.Join(ldflags, " ")))
}
if len(params.InputFiles) > 0 {
args = append(args, params.InputFiles...)
}
if GOOS == "windows" && params.WinMetadata {
log.Println("Generating a .syso containing Windows file metadata.")
syso, err := MakeWindowsSysoFile()
if err != nil {
return fmt.Errorf("failed generating Windows .syso metadata file: %w", err)
}
defer os.Remove(syso)
}
log.Println("Adding build environment vars:", env)
return sh.RunWith(env, "go", args...)
}
// MakeWindowsSysoFile generates a .syso file containing metadata about the
// executable file like vendor, version, copyright. The linker automatically
// discovers the .syso file and incorporates it into the Windows exe. This
// allows users to view metadata about the exe in the Details tab of the file
// properties viewer.
func MakeWindowsSysoFile() (string, error) {
version, err := BeatQualifiedVersion()
if err != nil {
return "", err
}
commit, err := CommitHash()
if err != nil {
return "", err
}
major, minor, patch, err := ParseVersion(version)
if err != nil {
return "", err
}
fileVersion := goversioninfo.FileVersion{Major: major, Minor: minor, Patch: patch}
vi := &goversioninfo.VersionInfo{
FixedFileInfo: goversioninfo.FixedFileInfo{
FileVersion: fileVersion,
ProductVersion: fileVersion,
FileType: "01", // Application
},
StringFileInfo: goversioninfo.StringFileInfo{
CompanyName: BeatVendor,
ProductName: cases.Title(language.Und, cases.NoLower).String(BeatName),
ProductVersion: version,
FileVersion: version,
FileDescription: BeatDescription,
OriginalFilename: BeatName + ".exe",
LegalCopyright: "Copyright " + BeatVendor + ", License " + BeatLicense,
Comments: "commit=" + commit,
},
}
vi.Build()
vi.Walk()
sysoFile := BeatName + "_windows_" + GOARCH + ".syso"
if err = vi.WriteSyso(sysoFile, GOARCH); err != nil {
return "", fmt.Errorf("failed to generate syso file with Windows metadata: %w", err)
}
return sysoFile, nil
}