Skip to content

Commit 6d5f489

Browse files
authored
feat: embed version info into binary (#298)
1 parent c1f9917 commit 6d5f489

File tree

6 files changed

+198
-13
lines changed

6 files changed

+198
-13
lines changed

.github/workflows/ci.yaml

+1-5
Original file line numberDiff line numberDiff line change
@@ -99,24 +99,20 @@ jobs:
9999
- name: Build
100100
if: github.event_name == 'pull_request'
101101
run: |
102-
VERSION=$(./scripts/version.sh)-dev-$(git rev-parse --short HEAD)
103102
BASE=ghcr.io/coder/envbuilder-preview
104103
105104
./scripts/build.sh \
106105
--arch=amd64 \
107-
--base=$BASE \
108-
--tag=$VERSION
106+
--base=$BASE
109107
110108
- name: Build and Push
111109
if: github.ref == 'refs/heads/main'
112110
run: |
113-
VERSION=$(./scripts/version.sh)-dev-$(git rev-parse --short HEAD)
114111
BASE=ghcr.io/coder/envbuilder-preview
115112
116113
./scripts/build.sh \
117114
--arch=amd64 \
118115
--arch=arm64 \
119116
--arch=arm \
120117
--base=$BASE \
121-
--tag=$VERSION \
122118
--push

buildinfo/version.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package buildinfo
2+
3+
import (
4+
"fmt"
5+
"runtime/debug"
6+
"sync"
7+
8+
"golang.org/x/mod/semver"
9+
)
10+
11+
const (
12+
noVersion = "v0.0.0"
13+
develPreRelease = "devel"
14+
)
15+
16+
var (
17+
buildInfo *debug.BuildInfo
18+
buildInfoValid bool
19+
readBuildInfo sync.Once
20+
21+
version string
22+
readVersion sync.Once
23+
24+
// Injected with ldflags at build time
25+
tag string
26+
)
27+
28+
func revision() (string, bool) {
29+
return find("vcs.revision")
30+
}
31+
32+
func find(key string) (string, bool) {
33+
readBuildInfo.Do(func() {
34+
buildInfo, buildInfoValid = debug.ReadBuildInfo()
35+
})
36+
if !buildInfoValid {
37+
panic("could not read build info")
38+
}
39+
for _, setting := range buildInfo.Settings {
40+
if setting.Key != key {
41+
continue
42+
}
43+
return setting.Value, true
44+
}
45+
return "", false
46+
}
47+
48+
// Version returns the semantic version of the build.
49+
// Use golang.org/x/mod/semver to compare versions.
50+
func Version() string {
51+
readVersion.Do(func() {
52+
revision, valid := revision()
53+
if valid {
54+
revision = "+" + revision[:7]
55+
}
56+
if tag == "" {
57+
// This occurs when the tag hasn't been injected,
58+
// like when using "go run".
59+
// <version>-<pre-release>+<revision>
60+
version = fmt.Sprintf("%s-%s%s", noVersion, develPreRelease, revision)
61+
return
62+
}
63+
version = "v" + tag
64+
// The tag must be prefixed with "v" otherwise the
65+
// semver library will return an empty string.
66+
if semver.Build(version) == "" {
67+
version += revision
68+
}
69+
})
70+
return version
71+
}

envbuilder.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"syscall"
2525
"time"
2626

27+
"github.com/coder/envbuilder/buildinfo"
2728
"github.com/coder/envbuilder/constants"
2829
"github.com/coder/envbuilder/git"
2930
"github.com/coder/envbuilder/options"
@@ -89,7 +90,7 @@ func Run(ctx context.Context, opts options.Options) error {
8990
}
9091
}
9192

92-
opts.Logger(log.LevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder"))
93+
opts.Logger(log.LevelInfo, "%s %s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder"), buildinfo.Version())
9394

9495
cleanupDockerConfigJSON, err := initDockerConfigJSON(opts.DockerConfigBase64)
9596
if err != nil {
@@ -863,7 +864,7 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error)
863864
}
864865
}
865866

866-
opts.Logger(log.LevelInfo, "%s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder"))
867+
opts.Logger(log.LevelInfo, "%s %s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder"), buildinfo.Version())
867868

868869
cleanupDockerConfigJSON, err := initDockerConfigJSON(opts.DockerConfigBase64)
869870
if err != nil {

scripts/build.sh

+11-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ set -euo pipefail
66
archs=()
77
push=false
88
base="envbuilder"
9-
tag="latest"
9+
tag=""
1010

1111
for arg in "$@"; do
1212
if [[ $arg == --arch=* ]]; then
@@ -30,6 +30,10 @@ if [ ${#archs[@]} -eq 0 ]; then
3030
archs=( "$current" )
3131
fi
3232

33+
if [[ -z "${tag}" ]]; then
34+
tag=$(./version.sh)
35+
fi
36+
3337
# We have to use docker buildx to tag multiple images with
3438
# platforms tragically, so we have to create a builder.
3539
BUILDER_NAME="envbuilder"
@@ -46,9 +50,11 @@ fi
4650
# Ensure the builder is bootstrapped and ready to use
4751
docker buildx inspect --bootstrap &> /dev/null
4852

53+
ldflags=(-X "'github.com/coder/envbuilder/buildinfo.tag=$tag'")
54+
4955
for arch in "${archs[@]}"; do
5056
echo "Building for $arch..."
51-
GOARCH=$arch CGO_ENABLED=0 go build -o "./envbuilder-${arch}" ../cmd/envbuilder &
57+
GOARCH=$arch CGO_ENABLED=0 go build -ldflags="${ldflags[*]}" -o "./envbuilder-${arch}" ../cmd/envbuilder &
5258
done
5359
wait
5460

@@ -62,10 +68,12 @@ else
6268
args+=( --load )
6369
fi
6470

71+
# coerce semver build tags into something docker won't complain about
72+
tag="${tag//\+/-}"
6573
docker buildx build --builder $BUILDER_NAME "${args[@]}" -t "${base}:${tag}" -t "${base}:latest" -f Dockerfile .
6674

6775
# Check if archs contains the current. If so, then output a message!
6876
if [[ -z "${CI:-}" ]] && [[ " ${archs[*]} " =~ ${current} ]]; then
6977
docker tag "${base}:${tag}" envbuilder:latest
70-
echo "Tagged $current as envbuilder:latest!"
78+
echo "Tagged $current as ${base}:${tag} ${base}:latest!"
7179
fi

scripts/lib.sh

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env bash
2+
3+
# This script is meant to be sourced by other scripts. To source this script:
4+
# # shellcheck source=scripts/lib.sh
5+
# source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
6+
7+
set -euo pipefail
8+
9+
# Avoid sourcing this script multiple times to guard against when lib.sh
10+
# is used by another sourced script, it can lead to confusing results.
11+
if [[ ${SCRIPTS_LIB_IS_SOURCED:-0} == 1 ]]; then
12+
return
13+
fi
14+
# Do not export to avoid this value being inherited by non-sourced
15+
# scripts.
16+
SCRIPTS_LIB_IS_SOURCED=1
17+
18+
# We have to define realpath before these otherwise it fails on Mac's bash.
19+
SCRIPT="${BASH_SOURCE[1]:-${BASH_SOURCE[0]}}"
20+
SCRIPT_DIR="$(realpath "$(dirname "$SCRIPT")")"
21+
22+
function project_root {
23+
# Nix sets $src in derivations!
24+
[[ -n "${src:-}" ]] && echo "$src" && return
25+
26+
# Try to use `git rev-parse --show-toplevel` to find the project root.
27+
# If this directory is not a git repository, this command will fail.
28+
git rev-parse --show-toplevel 2>/dev/null && return
29+
}
30+
31+
PROJECT_ROOT="$(cd "$SCRIPT_DIR" && realpath "$(project_root)")"
32+
33+
# cdroot changes directory to the root of the repository.
34+
cdroot() {
35+
cd "$PROJECT_ROOT" || error "Could not change directory to '$PROJECT_ROOT'"
36+
}
37+
38+
# log prints a message to stderr
39+
log() {
40+
echo "$*" 1>&2
41+
}

scripts/version.sh

+71-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,78 @@
11
#!/usr/bin/env bash
22

3+
# This script generates the version string used by Envbuilder, including for dev
4+
# versions. Note: the version returned by this script will NOT include the "v"
5+
# prefix that is included in the Git tag.
6+
#
7+
# If $ENVBUILDER_RELEASE is set to "true", the returned version will equal the
8+
# current git tag. If the current commit is not tagged, this will fail.
9+
#
10+
# If $ENVBUILDER_RELEASE is not set, the returned version will always be a dev
11+
# version.
12+
313
set -euo pipefail
4-
cd "$(dirname "${BASH_SOURCE[0]}")"
14+
# shellcheck source=scripts/lib.sh
15+
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
16+
cdroot
17+
18+
if [[ -n "${ENVBUILDER_FORCE_VERSION:-}" ]]; then
19+
echo "${ENVBUILDER_FORCE_VERSION}"
20+
exit 0
21+
fi
22+
23+
# To make contributing easier, if there are no tags, we'll use a default
24+
# version.
25+
tag_list=$(git tag)
26+
if [[ -z ${tag_list} ]]; then
27+
log
28+
log "INFO(version.sh): It appears you've checked out a fork or shallow clone of Envbuilder."
29+
log "INFO(version.sh): By default GitHub does not include tags when forking."
30+
log "INFO(version.sh): We will use the default version 0.0.1 for this build."
31+
log "INFO(version.sh): To pull tags from upstream, use the following commands:"
32+
log "INFO(version.sh): - git remote add upstream https://github.com/coder/envbuilder.git"
33+
log "INFO(version.sh): - git fetch upstream"
34+
log
35+
last_tag="v0.0.1"
36+
else
37+
current_commit=$(git rev-parse HEAD)
38+
# Try to find the last tag that contains the current commit
39+
last_tag=$(git tag --contains "$current_commit" --sort=version:refname | head -n 1)
40+
# If there is no tag that contains the current commit,
41+
# get the latest tag sorted by semver.
42+
if [[ -z "${last_tag}" ]]; then
43+
last_tag=$(git tag --sort=version:refname | tail -n 1)
44+
fi
45+
fi
46+
47+
version="${last_tag}"
48+
49+
# If the HEAD has extra commits since the last tag then we are in a dev version.
50+
#
51+
# Dev versions are denoted by the "-dev+" suffix with a trailing commit short
52+
# SHA.
53+
if [[ "${ENVBUILDER_RELEASE:-}" == *t* ]]; then
54+
# $last_tag will equal `git describe --always` if we currently have the tag
55+
# checked out.
56+
if [[ "${last_tag}" != "$(git describe --always)" ]]; then
57+
# make won't exit on $(shell cmd) failures, so we have to kill it :(
58+
if [[ "$(ps -o comm= "${PPID}" || true)" == *make* ]]; then
59+
log "ERROR: version.sh: the current commit is not tagged with an annotated tag"
60+
kill "${PPID}" || true
61+
exit 1
62+
fi
63+
64+
error "version.sh: the current commit is not tagged with an annotated tag"
65+
fi
66+
else
67+
rev=$(git rev-parse --short HEAD)
68+
version="0.0.0+dev-${rev}"
69+
# If the git repo has uncommitted changes, mark the version string as 'dirty'.
70+
dirty_files=$(git ls-files --other --modified --exclude-standard)
71+
if [[ -n "${dirty_files}" ]]; then
72+
version+="-dirty"
73+
fi
74+
fi
575

6-
last_tag="$(git describe --tags --abbrev=0)"
7-
version="$last_tag"
876

977
# Remove the "v" prefix.
1078
echo "${version#v}"

0 commit comments

Comments
 (0)