Skip to content

feat: embed version info into binary #298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
"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 @@
}
}

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())

Check failure on line 93 in envbuilder.go

View workflow job for this annotation

GitHub Actions / test

printf: (*github.com/fatih/color.Color).Sprintf format %s reads arg #1, but call has 0 args (govet)

cleanupDockerConfigJSON, err := initDockerConfigJSON(opts.DockerConfigBase64)
if err != nil {
Expand Down Expand Up @@ -863,7 +864,7 @@
}
}

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())

Check failure on line 867 in envbuilder.go

View workflow job for this annotation

GitHub Actions / test

printf: (*github.com/fatih/color.Color).Sprintf format %s reads arg #1, but call has 0 args (govet)

cleanupDockerConfigJSON, err := initDockerConfigJSON(opts.DockerConfigBase64)
if err != nil {
Expand Down
12 changes: 9 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 @@ -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
36 changes: 36 additions & 0 deletions scripts/lib.sh
Original file line number Diff line number Diff line change
@@ -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'"
}
73 changes: 70 additions & 3 deletions scripts/version.sh
Original file line number Diff line number Diff line change
@@ -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}"
Loading