diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 914fcf5..ee56813 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,6 +8,45 @@ on: pull_request: jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v6 + with: + go-version-file: 'go.mod' + + - run: go version + + - name: Build + run: make + + - run: bin/gamon3 -v + + - name: Print embedded module version information + run: go version -m bin/gamon3 + + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v6 + with: + go-version-file: 'go.mod' + + - run: go version + + - name: Run tests + run: make test + lint: runs-on: ubuntu-latest @@ -18,14 +57,16 @@ jobs: - uses: actions/setup-go@v6 with: - go-version: '1.25' + go-version-file: 'go.mod' + + - run: go version - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: version: v2.6 - test: + check-release-build-parity: runs-on: ubuntu-latest steps: @@ -35,7 +76,18 @@ jobs: - uses: actions/setup-go@v6 with: - go-version: '1.25' + go-version-file: 'go.mod' - - name: Run tests - run: make test + - run: go version + + - name: Install GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + version: "~> v2" + install-only: true + + - name: Show GoReleaser version + run: goreleaser -v + + - name: Compare Makefile builds to GoReleaser builds + run: scripts/check-reproducible-builds diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e080819..6367972 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -14,13 +14,15 @@ jobs: pull-requests: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: actions/setup-go@v6 with: - go-version: '1.25' + go-version-file: 'go.mod' + + - run: go version - name: Install GoReleaser uses: goreleaser/goreleaser-action@v6 diff --git a/.gitignore b/.gitignore index 71ea8a0..ee0f618 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,6 @@ go.work.sum # Added by goreleaser init: dist/ -build/ +bin/ node_modules/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 2c43208..04d59ec 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,12 +9,23 @@ before: hooks: - go mod tidy +gomod: + proxy: true + builds: - env: - CGO_ENABLED=0 + - GAMON3_VERSION={{ if index .Env "GAMON3_VERSION" }}{{ .Env.GAMON3_VERSION }}{{ else }}{{ .Version }}{{ end }} goos: - darwin - linux + flags: + - -v + - -buildvcs=auto + - -trimpath + ldflags: + - -s -w + - -X github.com/peter-bread/gamon3/v2/internal/build.Version={{ .Env.GAMON3_VERSION }} archives: - formats: [tar.gz] diff --git a/Makefile b/Makefile index 3387fcd..d526773 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,37 @@ .PHONY: build test cover clean install goreleaser -BUILD_DIR = build +override VALID_BUILD_MODES := release debug -VERSION ?= $(shell git describe --tags --dirty --always) -LDFLAGS ?= -X main.version=$(VERSION) +# Release builds by default, must explicitly set BUILD_MODE=debug for dev builds. +BUILD_MODE ?= release + +ifneq ($(filter $(BUILD_MODE),$(VALID_BUILD_MODES)),$(BUILD_MODE)) +$(error Invalid BUILD_MODE '$(BUILD_MODE)'; must be one of: [$(VALID_BUILD_MODES)]) +endif + +# This is the default value. In contexts where VCS information is unavaliable, +# this needs to be set manually. +# +# For example, in a Homebrew formula, this set to 'version.to_s'. +GAMON3_VERSION ?= $(shell git describe --tags --dirty --always) + +BIN := bin +CGO_ENABLED := 0 + +LDFLAGS_COMMON := -X github.com/peter-bread/gamon3/v2/internal/build.Version=$(GAMON3_VERSION) +GOFLAGS_COMMON := -v -buildvcs=auto + +ifeq ($(BUILD_MODE), release) +LDFLAGS := -s -w $(LDFLAGS_COMMON) +GOFLAGS := -trimpath $(GOFLAGS_COMMON) +else ifeq ($(BUILD_MODE), debug) +LDFLAGS := $(LDFLAGS_COMMON) +GOFLAGS := $(GOFLAGS_COMMON) +endif build: - mkdir -p $(BUILD_DIR) - go build -o $(BUILD_DIR) -v -ldflags "$(LDFLAGS)" + mkdir -p $(BIN) + CGO_ENABLED=$(CGO_ENABLED) go build $(GOFLAGS) -o "$(BIN)" -ldflags "$(LDFLAGS)" test: go test -v ./... @@ -17,17 +41,20 @@ cover: clean: go clean - rm -rf $(BUILD_DIR) + rm -rf $(BIN) PREFIX ?= /usr/local install: build install -d $(PREFIX)/bin - install $(BUILD_DIR)/gamon3 $(PREFIX)/bin + install $(BIN)/gamon3 $(PREFIX)/bin ################################################################################ # This will use '.goreleaser.yaml' and build in 'dist/'. +# GAMON3_VERSION is passed to ensure version output is the same as using 'make'. +# This is just for checking that GoReleaser and Makefile builds are the same. goreleaser: - goreleaser release --snapshot --clean + GAMON3_VERSION=$(GAMON3_VERSION) goreleaser release --snapshot --clean + # goreleaser release --snapshot --clean diff --git a/README.md b/README.md index 304b685..457637c 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,8 @@ go install github.com/peter-bread/gamon3/v2@latest ### Build From Source -To build and install Gamon3 under the default prefix (`/usr/local`), run: +To build and install a release version Gamon3 under the default prefix +(`/usr/local`), run: ```bash git clone https://github.com/peter-bread/gamon3 @@ -83,6 +84,12 @@ To install under a custom prefix, e.g. `~/.local`, run: make install PREFIX=~/.local ``` +If you wish to build with debug information, use + +```bash +make BUILD_MODE=debug +``` + ## Usage ### Authenticate with GH CLI diff --git a/cmd/root.go b/cmd/root.go index ee11208..0af41f2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -24,6 +24,7 @@ THE SOFTWARE. package cmd import ( + "fmt" "os" "github.com/peter-bread/gamon3/v2/cmd/hook" @@ -52,9 +53,8 @@ func Execute() { } } -func SetVersion(version, commit, date string) { - // TODO: Use `commit` and `date` in version output (MAYBE). - rootCmd.Version = version +func SetVersion(version, os, arch string) { + rootCmd.Version = fmt.Sprintf("%s %s-%s", version, os, arch) } func init() { diff --git a/go.mod b/go.mod index a061d57..40bb64a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/peter-bread/gamon3/v2 -go 1.25.0 +go 1.25 require ( github.com/goccy/go-yaml v1.18.0 diff --git a/internal/build/build.go b/internal/build/build.go new file mode 100644 index 0000000..81f56dc --- /dev/null +++ b/internal/build/build.go @@ -0,0 +1,46 @@ +/* +Copyright © 2025 Peter Sheehan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// Package build sets build information for the binary. +package build + +import ( + "runtime" + "runtime/debug" +) + +var ( + Version = "dev" + Os = "unknown" + Arch = "unknown" +) + +func init() { + if Version == "dev" { + if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "(devel)" { + Version = info.Main.Version + } + } + + Os = runtime.GOOS + Arch = runtime.GOARCH +} diff --git a/main.go b/main.go index c0fbb13..b3c726f 100644 --- a/main.go +++ b/main.go @@ -24,15 +24,13 @@ package main import ( "github.com/peter-bread/gamon3/v2/cmd" + "github.com/peter-bread/gamon3/v2/internal/build" ) -var ( - version = "dev" - commit = "none" - date = "unknown" -) +func init() { + cmd.SetVersion(build.Version, build.Os, build.Arch) +} func main() { - cmd.SetVersion(version, commit, date) cmd.Execute() } diff --git a/scripts/check-reproducible-builds b/scripts/check-reproducible-builds new file mode 100755 index 0000000..78dbde7 --- /dev/null +++ b/scripts/check-reproducible-builds @@ -0,0 +1,118 @@ +#!/usr/bin/env bash + +# This script ensures that release builds from the Makefile are identical to +# release builds from GoReleaser. + +set -e + +# Logging options. +: "${ENABLE_COLOR:=1}" # 1 = colored output, 0 = plain +DATE_FORMAT="%Y-%m-%d %H:%M:%S" + +# ANSI color codes. +RESET="\e[0m" +RED="\e[31m" +GREEN="\e[32m" +YELLOW="\e[33m" +BLUE="\e[34m" + +# Internal logging function. +_log() { + local level="$1" + local msg="$2" + local color="$3" + local stream=${4:-1} + local timestamp + timestamp=$(date +"$DATE_FORMAT") + + if [[ $ENABLE_COLOR -eq 1 && -n "$color" ]]; then + printf "%s ${color}[%-7s]${RESET} %s\n" "$timestamp" "$level" "$msg" >&"$stream" + else + printf "%s [%-7s] %s\n" "$timestamp" "$level" "$msg" >&"$stream" + fi +} + +# Public logging functions. +info() { _log "INFO" "$1" "$BLUE"; } +warn() { _log "WARN" "$1" "$YELLOW" 2; } +error() { _log "ERROR" "$1" "$RED" 2; } +success() { _log "SUCCESS" "$1" "$GREEN"; } + +available() { [[ -n "$1" ]] && command -v -- "$1" &>/dev/null; } + +unavailable() { ! available "$1"; } + +check_files() { + local file1="$1" + local file2="$2" + + local hash1 + local hash2 + hash1=$(sha256sum "$file1" | awk '{print $1}') + hash2=$(sha256sum "$file2" | awk '{print $1}') + + if [ "$hash1" = "$hash2" ]; then + return 0 + else + # NOTE: Using '-trimpath' at build time hides '-ldflags' in 'go version -m' + # output. + warn "Makefile hash: $hash1" + warn "GoReleaser hash: $hash2" + info "Printing diff between go version -m outputs" + diff <(go version -m "$file1") <(go version -m "$file2") + return 1 + fi +} + +goreleaser_path() { + local version + case $GOARCH in + arm64) version="8.0" ;; + amd64) version="1" ;; + *) exit 1 ;; + esac + + echo "./dist/gamon3_${GOOS}_${GOARCH}_v${version}/gamon3" +} + +verify() { + local goos=$1 + local goarch=$2 + + ( + export GOOS=$goos GOARCH=$goarch + info "Building for $GOOS-$GOARCH" + make BIN="$TMP" + info "Verifying release build checksums for $GOOS-$GOARCH" + if ! check_files "$TMP"/gamon3 "$(goreleaser_path)"; then + error "Release build checksums did not match for $GOOS-$GOARCH" + return 1 + fi + success "Release build checksums matched for $GOOS-$GOARCH" + ) +} + +info "Verifying Makefile release builds are the same as GoReleaser builds" + +if unavailable sha256sum; then + error "sha256sum is required" + exit 1 +fi + +info "Building GoReleaser snapshot" +make goreleaser + +info "Building from Makefile and checking" + +TMP=$(mktemp -d) +cleanup() { rm -rf "$TMP"; } +trap cleanup EXIT + +export TMP + +verify darwin arm64 +verify darwin amd64 +verify linux arm64 +verify linux amd64 + +success "All release build checksums match"