Files
cf391d21f1 [builder] Add .exe to output binary names when building for Windows (#14658)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue.
Ex. Adding a feature - Explain what this achieves.-->
#### Description
This PR fixes builder output naming on Windows by appending .exe to the
compiled binary name when ocb runs on Windows.
<!-- Issue number if applicable -->
#### Link to tracking issue
Fixes #12591

<!--Describe what testing was performed and which tests were added.-->
#### Testing
Added unit test for Windows/non-Windows output naming

---------

Signed-off-by: Shaanveer Singh <[email protected]>
Co-authored-by: Damien Mathieu <[email protected]>
2026-03-09 10:40:09 +00:00

272 lines
8.1 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package builder // import "go.opentelemetry.io/collector/cmd/builder/internal/builder"
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"text/template"
"time"
"go.uber.org/zap"
"golang.org/x/mod/modfile"
"golang.org/x/mod/semver"
)
var (
// ErrGoNotFound is returned when a Go binary hasn't been found
ErrGoNotFound = errors.New("go binary not found")
ErrDepNotFound = errors.New("dependency not found in go mod file")
ErrVersionMismatch = errors.New("mismatch in go.mod and builder configuration versions")
errDownloadFailed = errors.New("failed to download go modules")
errCompileFailed = errors.New("failed to compile the OpenTelemetry Collector distribution")
skipStrictMsg = "Use --skip-strict-versioning to temporarily disable this check. This flag will be removed in a future minor version"
)
const otelcolPath = "go.opentelemetry.io/collector/otelcol"
func runGoCommand(cfg *Config, args ...string) ([]byte, error) {
if cfg.Verbose {
cfg.Logger.Info("Running go subcommand.", zap.Any("arguments", args))
}
//nolint:gosec // #nosec G204 -- cfg.Distribution.Go is trusted to be a safe path and the caller is assumed to have carried out necessary input validation
cmd := exec.Command(cfg.Distribution.Go, args...)
cmd.Dir = cfg.Distribution.OutputPath
cmd.Env = os.Environ()
if cfg.Distribution.CGoEnabled {
cmd.Env = append(cmd.Env, "CGO_ENABLED=1")
} else {
cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
}
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("go subcommand failed with args '%v': %w, error message: %s", args, err, stderr.String())
}
if cfg.Verbose && stderr.Len() != 0 {
cfg.Logger.Info("go subcommand error", zap.String("message", stderr.String()))
}
return stdout.Bytes(), nil
}
// GenerateAndCompile will generate the source files based on the given configuration, update go mod, and will compile into a binary
func GenerateAndCompile(cfg *Config) error {
if err := Generate(cfg); err != nil {
return err
}
// run go get to update go.mod and go.sum files
if err := GetModules(cfg); err != nil {
return err
}
return Compile(cfg)
}
// Generate assembles a new distribution based on the given configuration
func Generate(cfg *Config) error {
if cfg.SkipGenerate {
cfg.Logger.Info("Skipping generating source codes.")
return nil
}
// if the file does not exist, try to create it
if _, err := os.Stat(cfg.Distribution.OutputPath); os.IsNotExist(err) {
if err = os.Mkdir(cfg.Distribution.OutputPath, 0o750); err != nil {
return fmt.Errorf("failed to create output path: %w", err)
}
} else if err != nil {
return fmt.Errorf("failed to create output path: %w", err)
}
for _, tmpl := range []*template.Template{
mainTemplate,
mainOthersTemplate,
mainWindowsTemplate,
componentsTemplate,
goModTemplate,
} {
if err := processAndWrite(cfg, tmpl, tmpl.Name(), cfg); err != nil {
return fmt.Errorf("failed to generate source file %q: %w", tmpl.Name(), err)
}
}
cfg.Logger.Info("Sources created", zap.String("path", cfg.Distribution.OutputPath))
return nil
}
// Compile generates a binary from the sources based on the configuration
func Compile(cfg *Config) error {
if cfg.SkipCompilation {
cfg.Logger.Info("Generating source codes only, the distribution will not be compiled.")
return nil
}
cfg.Logger.Info("Compiling")
ldflags := "-s -w" // we strip the symbols by default for smaller binaries
gcflags := ""
binaryName := outputBinaryName(cfg.Distribution.Name)
args := []string{"build", "-trimpath", "-o", binaryName}
if cfg.Distribution.DebugCompilation {
cfg.Logger.Info("Debug compilation is enabled, the debug symbols will be left on the resulting binary")
ldflags = cfg.LDFlags
gcflags = "all=-N -l"
} else {
if cfg.LDSet {
cfg.Logger.Info("Using custom ldflags", zap.String("ldflags", cfg.LDFlags))
ldflags = cfg.LDFlags
}
if cfg.GCSet {
cfg.Logger.Info("Using custom gcflags", zap.String("gcflags", cfg.GCFlags))
gcflags = cfg.GCFlags
}
}
if cfg.Distribution.CGoEnabled {
cfg.Logger.Info("Building with cgo enabled")
}
args = append(args, "-ldflags="+ldflags, "-gcflags="+gcflags)
if cfg.Distribution.BuildTags != "" {
args = append(args, "-tags", cfg.Distribution.BuildTags)
}
if _, err := runGoCommand(cfg, args...); err != nil {
return fmt.Errorf("%w: %s", errCompileFailed, err.Error())
}
cfg.Logger.Info("Compiled", zap.String("binary", fmt.Sprintf("%s/%s", cfg.Distribution.OutputPath, binaryName)))
return nil
}
func outputBinaryName(name string) string {
goos, ok := os.LookupEnv("GOOS")
if !ok || goos == "" {
goos = runtime.GOOS
}
if !strings.EqualFold(goos, "windows") {
return name
}
if strings.EqualFold(filepath.Ext(name), ".exe") {
return name
}
return name + ".exe"
}
// GetModules retrieves the go modules, updating go.mod and go.sum in the process
func GetModules(cfg *Config) error {
if cfg.SkipGetModules {
cfg.Logger.Info("Generating source codes only, will not update go.mod and retrieve Go modules.")
return nil
}
if _, err := runGoCommand(cfg, "mod", "tidy", "-compat=1.25"); err != nil {
return fmt.Errorf("failed to update go.mod: %w", err)
}
if cfg.SkipStrictVersioning {
return downloadModules(cfg)
}
// Perform strict version checking. For each component listed and the
// otelcol core dependency, check that the enclosing go module matches.
modulePath, dependencyVersions, err := readGoModFile(cfg)
if err != nil {
return err
}
coreDepVersion, ok := dependencyVersions[otelcolPath]
betaVersion := semver.MajorMinor(DefaultBetaOtelColVersion)
if !ok {
return fmt.Errorf("core collector %w: '%s'. %s", ErrDepNotFound, otelcolPath, skipStrictMsg)
}
if semver.MajorMinor(coreDepVersion) != betaVersion {
return fmt.Errorf(
"%w: core collector version calculated by component dependencies %q does not match configured version %q. %s",
ErrVersionMismatch, coreDepVersion, betaVersion, skipStrictMsg)
}
for _, mod := range cfg.allComponents() {
module, version, _ := strings.Cut(mod.GoMod, " ")
if module == modulePath {
// No need to check the version of components that are part of the
// module we're building from.
continue
}
moduleDepVersion, ok := dependencyVersions[module]
if !ok {
return fmt.Errorf("component %w: '%s'. %s", ErrDepNotFound, module, skipStrictMsg)
}
if semver.MajorMinor(moduleDepVersion) != semver.MajorMinor(version) {
return fmt.Errorf(
"%w: component %q version calculated by dependencies %q does not match configured version %q. %s",
ErrVersionMismatch, module, moduleDepVersion, version, skipStrictMsg)
}
}
return downloadModules(cfg)
}
func downloadModules(cfg *Config) error {
cfg.Logger.Info("Getting go modules")
failReason := "unknown"
for i := 1; i <= cfg.downloadModules.numRetries; i++ {
if _, err := runGoCommand(cfg, "mod", "download"); err != nil {
failReason = err.Error()
cfg.Logger.Info("Failed modules download", zap.String("retry", fmt.Sprintf("%d/%d", i, cfg.downloadModules.numRetries)))
time.Sleep(cfg.downloadModules.wait)
continue
}
return nil
}
return fmt.Errorf("%w: %s", errDownloadFailed, failReason)
}
func processAndWrite(cfg *Config, tmpl *template.Template, outFile string, tmplParams any) error {
out, err := os.Create(filepath.Clean(filepath.Join(cfg.Distribution.OutputPath, outFile)))
if err != nil {
return err
}
defer out.Close()
return tmpl.Execute(out, tmplParams)
}
func readGoModFile(cfg *Config) (string, map[string]string, error) {
var modPath string
stdout, err := runGoCommand(cfg, "mod", "edit", "-print")
if err != nil {
return modPath, nil, err
}
parsedFile, err := modfile.Parse("go.mod", stdout, nil)
if err != nil {
return modPath, nil, err
}
if parsedFile.Module != nil {
modPath = parsedFile.Module.Mod.Path
}
dependencies := map[string]string{}
for _, req := range parsedFile.Require {
if req == nil {
continue
}
dependencies[req.Mod.Path] = req.Mod.Version
}
return modPath, dependencies, nil
}