mirror of
https://github.com/hanzoai/telemetry.git
synced 2026-10-30 20:01:49 +00:00
<!--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]>
272 lines
8.1 KiB
Go
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
|
|
}
|