From 4065177a2edd3686a6bf2a5b8e3e59f2d9096ebf Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 5 Aug 2024 12:00:57 +0100 Subject: [PATCH 1/3] feat: embed version info into binary --- .github/workflows/ci.yaml | 6 +--- buildinfo/version.go | 71 +++++++++++++++++++++++++++++++++++++ envbuilder.go | 5 +-- scripts/build.sh | 12 +++++-- scripts/lib.sh | 36 +++++++++++++++++++ scripts/version.sh | 73 +++++++++++++++++++++++++++++++++++++-- 6 files changed, 190 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 215ccc48..c593a8fb 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 %s"), 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 %s"), buildinfo.Version()) cleanupDockerConfigJSON, err := initDockerConfigJSON(opts.DockerConfigBase64) if err != nil { diff --git a/scripts/build.sh b/scripts/build.sh index e186dc02..54e4950e 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 @@ -67,5 +73,5 @@ docker buildx build --builder $BUILDER_NAME "${args[@]}" -t "${base}:${tag}" -t # 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..a5a821e1 --- /dev/null +++ b/scripts/lib.sh @@ -0,0 +1,36 @@ +#!/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'" +} diff --git a/scripts/version.sh b/scripts/version.sh index 31968d27..7fdaf05d 100755 --- a/scripts/version.sh +++ b/scripts/version.sh @@ -1,10 +1,77 @@ #!/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 2.0.0 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="v2.0.0" +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 "-devel+" 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 + version+="-dev-$(git rev-parse --short HEAD)" +fi -last_tag="$(git describe --tags --abbrev=0)" -version="$last_tag" +# 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 # Remove the "v" prefix. echo "${version#v}" From 8bb879299c4d1e06910c2d994f19c801cdd32054 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 5 Aug 2024 12:13:28 +0100 Subject: [PATCH 2/3] fix linter error --- envbuilder.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/envbuilder.go b/envbuilder.go index c593a8fb..7e05f323 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -90,7 +90,7 @@ func Run(ctx context.Context, opts options.Options) error { } } - opts.Logger(log.LevelInfo, "%s %s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder %s"), buildinfo.Version()) + 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 { @@ -864,7 +864,7 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error) } } - opts.Logger(log.LevelInfo, "%s %s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder %s"), buildinfo.Version()) + 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 { From 9f819a58c2ffdb87e3c9c1d4587b0ec2e2c2bd2e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 5 Aug 2024 12:41:12 +0100 Subject: [PATCH 3/3] address PR comments --- scripts/build.sh | 2 ++ scripts/lib.sh | 5 +++++ scripts/version.sh | 19 ++++++++++--------- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index 54e4950e..40545199 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -68,6 +68,8 @@ 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! diff --git a/scripts/lib.sh b/scripts/lib.sh index a5a821e1..3fbcd979 100644 --- a/scripts/lib.sh +++ b/scripts/lib.sh @@ -34,3 +34,8 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR" && realpath "$(project_root)")" 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 7fdaf05d..17c8f727 100755 --- a/scripts/version.sh +++ b/scripts/version.sh @@ -27,12 +27,12 @@ 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 2.0.0 for this build." + 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="v2.0.0" + last_tag="v0.0.1" else current_commit=$(git rev-parse HEAD) # Try to find the last tag that contains the current commit @@ -48,7 +48,7 @@ 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 "-devel+" suffix with a trailing commit short +# 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 @@ -64,14 +64,15 @@ if [[ "${ENVBUILDER_RELEASE:-}" == *t* ]]; then error "version.sh: the current commit is not tagged with an annotated tag" fi else - version+="-dev-$(git rev-parse --short HEAD)" + 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 -# 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 # Remove the "v" prefix. echo "${version#v}"