joshuar-go-hass-agent/build/magefiles/helpers.go

223 lines
5.8 KiB
Go

// Copyright (c) 2024 Joshua Rich <joshua.rich@gmail.com>
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
package main
import (
"fmt"
"log/slog"
"os"
"os/exec"
"runtime"
"slices"
"strings"
"syscall"
"github.com/magefile/mage/sh"
)
// isCI checks whether we are currently running as part of a CI pipeline (i.e.
// in a GitHub runner).
func isCI() bool {
return os.Getenv("CI") != ""
}
// isRoot checks whether we are running as the root user or with elevated
// privileges.
func isRoot() bool {
euid := syscall.Geteuid()
uid := syscall.Getuid()
egid := syscall.Getegid()
gid := syscall.Getgid()
if uid != euid || gid != egid || uid == 0 {
return true
}
return false
}
// sudoWrap will "wrap" the given command with sudo if needed.
func sudoWrap(cmd string, args ...string) error {
if isRoot() {
if err := sh.RunV(cmd, args...); err != nil {
return fmt.Errorf("could not run command: %w", err)
}
} else {
if err := sh.RunV("sudo", slices.Concat([]string{cmd}, args)...); err != nil {
return fmt.Errorf("could not run command: %w", err)
}
}
return nil
}
// foundOrInstalled checks for existence then installs a file if it's not there.
func foundOrInstalled(executableName, installURL string) error {
_, missing := exec.LookPath(executableName)
if missing != nil {
slog.Info("Installing tool.",
slog.String("tool", executableName),
slog.String("url", installURL))
err := sh.Run("go", "install", installURL)
if err != nil {
return fmt.Errorf("installation failed: %w", err)
}
}
return nil
}
// getFlags gets all the compile flags to set the version and stuff.
func getFlags() (string, error) {
// pkgPath is where flags are derived from.
pkgPath := "github.com/joshuar/go-hass-agent/internal/preferences"
var version, hash, date string
var err error
if version, err = getVersion(); err != nil {
return "", fmt.Errorf("failed to retrieve version from git: %w", err)
}
if hash, err = getGitHash(); err != nil {
return "", fmt.Errorf("failed to retrieve hash from git: %w", err)
}
if date, err = getBuildDate(); err != nil {
return "", fmt.Errorf("failed to retrieve build date from git: %w", err)
}
var flags strings.Builder
flags.WriteString("-X " + pkgPath + ".gitVersion=" + version)
flags.WriteString(" ")
flags.WriteString("-X " + pkgPath + ".gitCommit=" + hash)
flags.WriteString(" ")
flags.WriteString("-X " + pkgPath + ".buildDate=" + date)
return flags.String(), nil
}
// getVersion returns a string that can be used as a version string.
func getVersion() (string, error) {
// Use the version already set in the environment (i.e., by the CI run).
if version, ok := os.LookupEnv("APPVERSION"); ok {
return version, nil
}
// Else, derive a version from git.
version, err := sh.Output("git", "describe", "--tags", "--always", "--dirty")
if err != nil {
return "", fmt.Errorf("could not get version from git: %w", err)
}
return version, nil
}
// hash returns the git hash for the current repo or "" if none.
func getGitHash() (string, error) {
hash, err := sh.Output("git", "rev-parse", "--short", "HEAD")
if err != nil {
return "", fmt.Errorf("could not get git hash: %w", err)
}
return hash, nil
}
// getBuildDate returns the build date.
func getBuildDate() (string, error) {
date, err := sh.Output("git", "log", "--date=iso8601-strict", "-1", "--pretty=%ct")
if err != nil {
return "", fmt.Errorf("could not get build date from git: %w", err)
}
return date, nil
}
// generateEnv will create a map[string]string containing environment variables
// and their values necessary for building the package on the given
// architecture.
func generateEnv() (map[string]string, error) {
envMap := make(map[string]string)
// CGO_ENABLED is required.
envMap["CGO_ENABLED"] = "1"
version, err := getVersion()
if err != nil {
return nil, fmt.Errorf("could not generate environment: %w", err)
}
// Set APPVERSION to current version.
envMap["APPVERSION"] = version
// Set NFPM_ARCH so that nfpm knows how to package for this arch.
envMap["NFPM_ARCH"] = runtime.GOARCH
// Get the value of BUILDPLATFORM (if set) from the environment, which
// indicates cross-compilation has been requested.
_, arch, ver := parseBuildPlatform()
if arch != "" && arch != runtime.GOARCH {
slog.Info("Cross compilation requested.",
slog.String("host", runtime.GOARCH),
slog.String("target", arch))
// Update NFPM_ARCH to the target arch.
envMap["NFPM_ARCH"] = arch + ver
// Set additional build-related variables based on the target arch.
switch arch {
case "arm":
envMap["CC"] = "arm-linux-gnueabihf-gcc"
envMap["PKG_CONFIG_PATH"] = "/usr/lib/arm-linux-gnueabihf/pkgconfig"
envMap["GOARCH"] = arch
envMap["GOARM"] = ver
envMap["PLATFORMPAIR"] = arch + ver
case "arm64":
envMap["CC"] = "aarch64-linux-gnu-gcc"
envMap["PKG_CONFIG_PATH"] = "/usr/lib/aarch64-linux-gnu/pkgconfig"
envMap["GOARCH"] = arch
envMap["PLATFORMPAIR"] = arch
default:
return nil, ErrUnsupportedArch
}
} else {
envMap["GOARCH"] = runtime.GOARCH
envMap["PLATFORMPAIR"] = runtime.GOARCH
}
return envMap, nil
}
// parseBuildPlatform reads the TARGETPLATFORM environment variable, which should
// always be set, and extracts the value into appropriate GOOS, GOARCH and GOARM
// (if applicable) variables.
//
//nolint:mnd
func parseBuildPlatform() (operatingsystem, architecture, version string) {
var buildPlatform string
var ok bool
if buildPlatform, ok = os.LookupEnv(platformENV); !ok {
return runtime.GOOS, runtime.GOARCH, ""
}
buildComponents := strings.Split(buildPlatform, "/")
operatingsystem = buildComponents[0]
if len(buildComponents) > 1 {
architecture = buildComponents[1]
}
if len(buildComponents) > 2 {
version = strings.TrimPrefix(buildComponents[2], "v")
}
return operatingsystem, architecture, version
}