Skip to content

Commit

Permalink
feat: embed version info into binary (#298)
Browse files Browse the repository at this point in the history
(cherry picked from commit 6d5f489)
  • Loading branch information
johnstcn committed Aug 5, 2024
1 parent 2a59ce0 commit e7acbad
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 13 deletions.
6 changes: 1 addition & 5 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -99,24 +99,20 @@ 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 \
--arch=amd64 \
--arch=arm64 \
--arch=arm \
--base=$BASE \
--tag=$VERSION \
--push
71 changes: 71 additions & 0 deletions buildinfo/version.go
Original file line number Diff line number Diff line change
@@ -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>-<pre-release>+<revision>
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
}
5 changes: 3 additions & 2 deletions envbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 11 additions & 3 deletions scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ set -euo pipefail
archs=()
push=false
base="envbuilder"
tag="latest"
tag=""

for arg in "$@"; do
if [[ $arg == --arch=* ]]; then
Expand All @@ -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"
Expand All @@ -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

Expand All @@ -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
41 changes: 41 additions & 0 deletions scripts/lib.sh
Original file line number Diff line number Diff line change
@@ -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
}
74 changes: 71 additions & 3 deletions scripts/version.sh
Original file line number Diff line number Diff line change
@@ -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}"

0 comments on commit e7acbad

Please sign in to comment.