From e7acbad4064b54243c57f2579c8341535132affe Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 5 Aug 2024 12:53:46 +0100 Subject: [PATCH] feat: embed version info into binary (#298) (cherry picked from commit 6d5f48990f3fee9bb650166864b876ce6d29d17c) --- .github/workflows/ci.yaml | 6 +--- buildinfo/version.go | 71 +++++++++++++++++++++++++++++++++++++ envbuilder.go | 5 +-- scripts/build.sh | 14 ++++++-- scripts/lib.sh | 41 ++++++++++++++++++++++ scripts/version.sh | 74 +++++++++++++++++++++++++++++++++++++-- 6 files changed, 198 insertions(+), 13 deletions(-) create mode 100644 buildinfo/version.go create mode 100644 scripts/lib.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4e0d51cc..ecb23ed1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -99,18 +99,15 @@ jobs: - name: Build if: github.event_name == 'pull_request' run: | - VERSION=$(./scripts/version.sh)-dev-$(git rev-parse --short HEAD) BASE=ghcr.io/coder/envbuilder-preview ./scripts/build.sh \ --arch=amd64 \ - --base=$BASE \ - --tag=$VERSION + --base=$BASE - name: Build and Push if: github.ref == 'refs/heads/main' run: | - VERSION=$(./scripts/version.sh)-dev-$(git rev-parse --short HEAD) BASE=ghcr.io/coder/envbuilder-preview ./scripts/build.sh \ @@ -118,5 +115,4 @@ jobs: --arch=arm64 \ --arch=arm \ --base=$BASE \ - --tag=$VERSION \ --push diff --git a/buildinfo/version.go b/buildinfo/version.go new file mode 100644 index 00000000..86f35348 --- /dev/null +++ b/buildinfo/version.go @@ -0,0 +1,71 @@ +package buildinfo + +import ( + "fmt" + "runtime/debug" + "sync" + + "golang.org/x/mod/semver" +) + +const ( + noVersion = "v0.0.0" + develPreRelease = "devel" +) + +var ( + buildInfo *debug.BuildInfo + buildInfoValid bool + readBuildInfo sync.Once + + version string + readVersion sync.Once + + // Injected with ldflags at build time + tag string +) + +func revision() (string, bool) { + return find("vcs.revision") +} + +func find(key string) (string, bool) { + readBuildInfo.Do(func() { + buildInfo, buildInfoValid = debug.ReadBuildInfo() + }) + if !buildInfoValid { + panic("could not read build info") + } + for _, setting := range buildInfo.Settings { + if setting.Key != key { + continue + } + return setting.Value, true + } + return "", false +} + +// Version returns the semantic version of the build. +// Use golang.org/x/mod/semver to compare versions. +func Version() string { + readVersion.Do(func() { + revision, valid := revision() + if valid { + revision = "+" + revision[:7] + } + if tag == "" { + // This occurs when the tag hasn't been injected, + // like when using "go run". + // -+ + version = fmt.Sprintf("%s-%s%s", noVersion, develPreRelease, revision) + return + } + version = "v" + tag + // The tag must be prefixed with "v" otherwise the + // semver library will return an empty string. + if semver.Build(version) == "" { + version += revision + } + }) + return version +} diff --git a/envbuilder.go b/envbuilder.go index a16f2fb4..7a61159e 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -24,6 +24,7 @@ import ( "syscall" "time" + "github.com/coder/envbuilder/buildinfo" "github.com/coder/envbuilder/constants" "github.com/coder/envbuilder/git" "github.com/coder/envbuilder/options" @@ -89,7 +90,7 @@ func Run(ctx context.Context, opts options.Options) error { } } - opts.Logger(log.LevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder")) + opts.Logger(log.LevelInfo, "%s %s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder"), buildinfo.Version()) cleanupDockerConfigJSON, err := initDockerConfigJSON(opts.DockerConfigBase64) if err != nil { @@ -863,7 +864,7 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error) } } - opts.Logger(log.LevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder")) + opts.Logger(log.LevelInfo, "%s %s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder"), buildinfo.Version()) cleanupDockerConfigJSON, err := initDockerConfigJSON(opts.DockerConfigBase64) if err != nil { diff --git a/scripts/build.sh b/scripts/build.sh index e186dc02..40545199 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -6,7 +6,7 @@ set -euo pipefail archs=() push=false base="envbuilder" -tag="latest" +tag="" for arg in "$@"; do if [[ $arg == --arch=* ]]; then @@ -30,6 +30,10 @@ if [ ${#archs[@]} -eq 0 ]; then archs=( "$current" ) fi +if [[ -z "${tag}" ]]; then + tag=$(./version.sh) +fi + # We have to use docker buildx to tag multiple images with # platforms tragically, so we have to create a builder. BUILDER_NAME="envbuilder" @@ -46,9 +50,11 @@ fi # Ensure the builder is bootstrapped and ready to use docker buildx inspect --bootstrap &> /dev/null +ldflags=(-X "'github.com/coder/envbuilder/buildinfo.tag=$tag'") + for arch in "${archs[@]}"; do echo "Building for $arch..." - GOARCH=$arch CGO_ENABLED=0 go build -o "./envbuilder-${arch}" ../cmd/envbuilder & + GOARCH=$arch CGO_ENABLED=0 go build -ldflags="${ldflags[*]}" -o "./envbuilder-${arch}" ../cmd/envbuilder & done wait @@ -62,10 +68,12 @@ else args+=( --load ) fi +# coerce semver build tags into something docker won't complain about +tag="${tag//\+/-}" docker buildx build --builder $BUILDER_NAME "${args[@]}" -t "${base}:${tag}" -t "${base}:latest" -f Dockerfile . # Check if archs contains the current. If so, then output a message! if [[ -z "${CI:-}" ]] && [[ " ${archs[*]} " =~ ${current} ]]; then docker tag "${base}:${tag}" envbuilder:latest - echo "Tagged $current as envbuilder:latest!" + echo "Tagged $current as ${base}:${tag} ${base}:latest!" fi diff --git a/scripts/lib.sh b/scripts/lib.sh new file mode 100644 index 00000000..3fbcd979 --- /dev/null +++ b/scripts/lib.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# This script is meant to be sourced by other scripts. To source this script: +# # shellcheck source=scripts/lib.sh +# source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +set -euo pipefail + +# Avoid sourcing this script multiple times to guard against when lib.sh +# is used by another sourced script, it can lead to confusing results. +if [[ ${SCRIPTS_LIB_IS_SOURCED:-0} == 1 ]]; then + return +fi +# Do not export to avoid this value being inherited by non-sourced +# scripts. +SCRIPTS_LIB_IS_SOURCED=1 + +# We have to define realpath before these otherwise it fails on Mac's bash. +SCRIPT="${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}" +SCRIPT_DIR="$(realpath "$(dirname "$SCRIPT")")" + +function project_root { + # Nix sets $src in derivations! + [[ -n "${src:-}" ]] && echo "$src" && return + + # Try to use `git rev-parse --show-toplevel` to find the project root. + # If this directory is not a git repository, this command will fail. + git rev-parse --show-toplevel 2>/dev/null && return +} + +PROJECT_ROOT="$(cd "$SCRIPT_DIR" && realpath "$(project_root)")" + +# cdroot changes directory to the root of the repository. +cdroot() { + cd "$PROJECT_ROOT" || error "Could not change directory to '$PROJECT_ROOT'" +} + +# log prints a message to stderr +log() { + echo "$*" 1>&2 +} diff --git a/scripts/version.sh b/scripts/version.sh index 31968d27..17c8f727 100755 --- a/scripts/version.sh +++ b/scripts/version.sh @@ -1,10 +1,78 @@ #!/usr/bin/env bash +# This script generates the version string used by Envbuilder, including for dev +# versions. Note: the version returned by this script will NOT include the "v" +# prefix that is included in the Git tag. +# +# If $ENVBUILDER_RELEASE is set to "true", the returned version will equal the +# current git tag. If the current commit is not tagged, this will fail. +# +# If $ENVBUILDER_RELEASE is not set, the returned version will always be a dev +# version. + set -euo pipefail -cd "$(dirname "${BASH_SOURCE[0]}")" +# shellcheck source=scripts/lib.sh +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +cdroot + +if [[ -n "${ENVBUILDER_FORCE_VERSION:-}" ]]; then + echo "${ENVBUILDER_FORCE_VERSION}" + exit 0 +fi + +# To make contributing easier, if there are no tags, we'll use a default +# version. +tag_list=$(git tag) +if [[ -z ${tag_list} ]]; then + log + log "INFO(version.sh): It appears you've checked out a fork or shallow clone of Envbuilder." + log "INFO(version.sh): By default GitHub does not include tags when forking." + log "INFO(version.sh): We will use the default version 0.0.1 for this build." + log "INFO(version.sh): To pull tags from upstream, use the following commands:" + log "INFO(version.sh): - git remote add upstream https://github.com/coder/envbuilder.git" + log "INFO(version.sh): - git fetch upstream" + log + last_tag="v0.0.1" +else + current_commit=$(git rev-parse HEAD) + # Try to find the last tag that contains the current commit + last_tag=$(git tag --contains "$current_commit" --sort=version:refname | head -n 1) + # If there is no tag that contains the current commit, + # get the latest tag sorted by semver. + if [[ -z "${last_tag}" ]]; then + last_tag=$(git tag --sort=version:refname | tail -n 1) + fi +fi + +version="${last_tag}" + +# If the HEAD has extra commits since the last tag then we are in a dev version. +# +# Dev versions are denoted by the "-dev+" suffix with a trailing commit short +# SHA. +if [[ "${ENVBUILDER_RELEASE:-}" == *t* ]]; then + # $last_tag will equal `git describe --always` if we currently have the tag + # checked out. + if [[ "${last_tag}" != "$(git describe --always)" ]]; then + # make won't exit on $(shell cmd) failures, so we have to kill it :( + if [[ "$(ps -o comm= "${PPID}" || true)" == *make* ]]; then + log "ERROR: version.sh: the current commit is not tagged with an annotated tag" + kill "${PPID}" || true + exit 1 + fi + + error "version.sh: the current commit is not tagged with an annotated tag" + fi +else + rev=$(git rev-parse --short HEAD) + version="0.0.0+dev-${rev}" + # If the git repo has uncommitted changes, mark the version string as 'dirty'. + dirty_files=$(git ls-files --other --modified --exclude-standard) + if [[ -n "${dirty_files}" ]]; then + version+="-dirty" + fi +fi -last_tag="$(git describe --tags --abbrev=0)" -version="$last_tag" # Remove the "v" prefix. echo "${version#v}"