From 2b2e0fc2cc01eb77f10fe4f2520c424aa12f6f03 Mon Sep 17 00:00:00 2001 From: Sam Salisbury Date: Fri, 13 Jan 2023 09:45:53 +0000 Subject: [PATCH] build release binaries for all platforms (#24) --- CHANGELOG.md | 3 + Makefile | 150 ++++++++++++++--------- action-setup | 8 ++ dev/build | 271 ++++++++++++++++++++++++++++++++++++++++++ dev/changes/v0.1.8.md | 3 + dev/source-id | 30 +++++ dev/update-source-id | 12 ++ pkg/build/build.go | 5 +- 8 files changed, 424 insertions(+), 58 deletions(-) create mode 100755 dev/build create mode 100755 dev/source-id create mode 100755 dev/update-source-id diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e98ee8e..7c9f6170 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ Instead, edit the files in dev/changes/, then run 'make docs' to update this fil - New inspect flag: `-worktree` which reports on the dirty/clean status of the worktree. - Development documentation docs/development.md +- Build system: + - `make build` - build dev (maybe dirty) CLI binaries & zips for all supported platforms. + - `make release` - build release (clean) CLI binaries & zipe for all supported platforms. ### Fixed: diff --git a/Makefile b/Makefile index 90270899..e8f0651c 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ SHELL := /usr/bin/env bash -euo pipefail -c +MAKEFLAGS := --jobs=10 + PRODUCT_NAME := actions-go-build DESTDIR ?= /usr/local/bin @@ -12,10 +14,17 @@ else CLEAR := endif +SOURCE_ID := .git/source-id +SOURCE_ID_VALUE := $(shell SOURCE_ID=$(SOURCE_ID) ./dev/update-source-id) + +# Uncomment to show the source ID. +# $(info SOURCE_ID=$(SOURCE_ID_VALUE)) + default: run ifeq ($(TMPDIR),) TMPDIR := $(RUNNER_TEMP) +export TMPDIR endif ifeq ($(TMPDIR),) $(error Neither TMPDIR nor RUNNER_TEMP are set.) @@ -52,50 +61,51 @@ CLINAME := $(PRODUCT_NAME) # Release versions of the CLI are built in three phases: # -# 1) TMP_BUILD - No build metadata. +# 1) INITIAL_BUILD - No build metadata. # 2) INTERMEDIATE_BUILD - Some build metadata. -# 3) RELEASE_BUILD - All build metadata. +# 3) BOOTSTRAPPED_BUILD - All build metadata. # # See comments below for more explanation. -# TMP_BUILD is a build of the CLI done using just `go build ...`. This is used to bootstrap -# compiling the CLI using itself, for dogfooding purposes. The TMP_BUILD contains none of the +TMP_BASE := $(TMPDIR)/actions-go-build.builds/$(SOURCE_ID_VALUE) + +# INITIAL_BUILD is a build of the CLI done using just `go build ...`. This is used to bootstrap +# compiling the CLI using itself, for dogfooding purposes. The INITIAL_BUILD contains none of the # automatically generated metadata like the version or revision. It is used to build the # intermediate build... -TMP_BUILD := $(TMPDIR)/temp-build/$(CLINAME) +INITIAL_BUILD := $(TMP_BASE)/initial/$(CLINAME) -# INTERMEDIATE_BUILD is a build of the CLI done using the TMP_BUILD build. Because it used -# TMP_BUILD (i.e. the code in this repo) to build itself, it contains automatically generated +# INTERMEDIATE_BUILD is a build of the CLI done using the INITIAL_BUILD build. Because it used +# INITIAL_BUILD (i.e. the code in this repo) to build itself, it contains automatically generated # metadata like the version and revision. However, it does not contain the metadata about the -# version of actions-go-build that built it because TMP_BUILD doesn't have that metadata +# version of actions-go-build that built it because INITIAL_BUILD doesn't have that metadata # available to inject. -INTERMEDIATE_BUILD := $(TMPDIR)/intermediate-build/$(CLINAME) +INTERMEDIATE_BUILD := $(TMP_BASE)/intermediate/$(CLINAME) -# RELEASE_BUILD is the final build of the CLI, done using the INTERMEDIATE_BUILD. Because +# BOOTSTRAPPED_BUILD is the final build of the CLI, done using the INTERMEDIATE_BUILD. Because # INTERMEDIATE_BUILD contains build metadata (e.g. version and revision), it is able to inject # that information, into this final build as "tool metadata". Thus we can track the provanance of # this binary just like we are able to with any product binaries also built using this tool. -RELEASE_BUILD := dist/$(CLINAME) +BOOTSTRAPPED_BUILD := $(TMP_BASE)/bootstrapped/$(CLINAME) # HOST_PLATFORM_TARGETS are targets that must always produce output compatible with # the current host platform. We therefore unset the GOOS and GOARCH variable to allow # the defaults to shine through. -HOST_PLATFORM_TARGETS := $(TMP_BUILD) $(INTERMEDIATE_BUILD) test/go +HOST_PLATFORM_TARGETS := $(INITIAL_BUILD) $(INTERMEDIATE_BUILD) test/go $(HOST_PLATFORM_TARGETS): export GOOS := $(HOST_PLATFORM_TARGETS): export GOARCH := +HOST_PLATFORM_TARGET_ENV := GOOS= GOARCH= OS= ARCH= + # # Targets # -build: - go build ./... - test: test/go .PHONY: test/go test/go: compile - go test $(GO_TEST_FLAGS) ./... + @$(HOST_PLATFORM_TARGET_ENV) go test $(GO_TEST_FLAGS) ./... cover: GO_TEST_FLAGS := -coverprofile=coverage.profile cover: test/go @@ -120,41 +130,27 @@ env: @echo " PRODUCT_REVISION=$$PRODUCT_REVISION" @echo " PRODUCT_REVISION_TIME=$$PRODUCT_REVISION_TIME" -# When building the binary, we first do a plain 'go build' to build a temporary -# binary that contains no version info. Then we use that version of the binary -# to build this product with all the version info added automatically from the -# build context. -# -# We then use _that_ binary to build yet another binary, this time with the -# correct tool version injected into the build. -# -# Thus, each version of actions-go-build is built using itself. +# The targets below invoke the ./dev/build script. Please see that script for more docs +# on how actions-go-build is built using itself. -.PHONY: $(TMP_BUILD) -$(TMP_BUILD): +$(INITIAL_BUILD): $(SOURCE_ID) @echo "# Running tests..." 1>&2 @$(RUN_TESTS_QUIET) - @echo "# Creating temporary build..." 1>&2 - @rm -f "$(TMP_BUILD)" - @mkdir -p "$(dir $(TMP_BUILD))" - @go build -o "$(TMP_BUILD)" - -.PHONY: $(INTERMEDIATE_BUILD) -$(INTERMEDIATE_BUILD): export TARGET_DIR := $(dir $(INTERMEDIATE_BUILD)) -$(INTERMEDIATE_BUILD): $(TMP_BUILD) - @echo "# Creating intermediate build..." 1>&2 - @$(TMP_BUILD) build -rebuild - -.PHONY: $(RELEASE_BUILD) -$(RELEASE_BUILD): $(INTERMEDIATE_BUILD) - @echo "# Creating final build." 1>&2 - @$(INTERMEDIATE_BUILD) build -rebuild - @echo "# Verifying reproducibility of self..." 1>&2 - @./$@ verify - -cli: $(RELEASE_BUILD) + @BIN_PATH="$@" ./dev/build initial > /dev/null + +RUN_QUIET = OUT="$$($(1) 2>&1)" || { \ + echo "Command Failed: $(notdir $(1))"; echo "$$OUT"; exit 1; \ + } + +$(INTERMEDIATE_BUILD): $(INITIAL_BUILD) + @BIN_PATH="$@" ./dev/build intermediate "$<" > /dev/null + +$(BOOTSTRAPPED_BUILD): $(INTERMEDIATE_BUILD) + @BIN_PATH="$@" ./dev/build bootstrapped "$<" > /dev/null + +cli: $(BOOTSTRAPPED_BUILD) @echo "Build successful." - $(RELEASE_BUILD) --version + $(BOOTSTRAPPED_BUILD) --version .PHONY: install # Ensure install always targets the host platform. @@ -163,13 +159,13 @@ install: export GOARCH := ifneq ($(GITHUB_PATH),) # install for GitHub Actions. -install: $(RELEASE_BUILD) - @echo "$(dir $(CURDIR)/$(RELEASE_BUILD))" >> "$(GITHUB_PATH)" - @echo "Command '$(CLINAME)' installed to GITHUB_PATH" - @PATH="$$(cat $(GITHUB_PATH))" $(CLINAME) --version +install: $(BOOTSTRAPPED_BUILD) + @echo "$(dir $(BOOTSTRAPPED_BUILD))" >> "$(GITHUB_PATH)" + @echo "Command '$(CLINAME)' installed to GITHUB_PATH ($(GITHUB_PATH))" + @export PATH="$$(cat $(GITHUB_PATH))" && $(CLINAME) --version else # install for local use. -install: $(RELEASE_BUILD) +install: $(BOOTSTRAPPED_BUILD) @mv "$<" "$(DESTDIR)" @V="$$($(CLINAME) version -short)" && \ echo "# $(CLINAME) v$$V installed to $(DESTDIR)" @@ -184,10 +180,10 @@ mod/framework/update: # which is usful for quickly seeing its output whilst developing. .PHONY: run -run: $(TMP_BUILD) +run: $(INITIAL_BUILD) @$${QUIET:-false} || $(CLEAR) @$${QUIET:-false} || echo "\$$ $(notdir $<) $(RUN)" - @$(TMP_BUILD) $(RUN) + @$(INITIAL_BUILD) $(RUN) .PHONY: docs docs: readme changelog @@ -235,9 +231,49 @@ ifneq ($(GH_AUTHED),true) endif endif -.PHONY: release -release: - @./dev/release/create +# +# Release build targets +# +# FINAL_BUILD_TARGETS defines all the targets for platform-specific "final" builds. +# It's a macro so that we get a consistent set of targets for each platform. +define FINAL_BUILD_TARGETS + +# Inside this define, $(1) is a platform string, like "linux/amd64" or "darwin/arm64" + +# build/ does not require a clean worktree and results in a "Development" build. +build/$(1): $$(BOOTSTRAPPED_BUILD) + @./dev/build dev "$$<" "$1" > /dev/null + +DEV_BUILDS := $$(DEV_BUILDS) build/$(1) + +dist/$1/actions-go-build \ +out/actions-go-build_$(CURR_VERSION)_$(subst /,_,$1).zip \ +zip/$(1) \ +release/build/$(1): $$(BOOTSTRAPPED_BUILD) + @./dev/build release "$$<" "$1" > /dev/null +RELEASE_BUILDS := $$(RELEASE_BUILDS) release/build/$(1) +RELEASE_ZIPS := $$(RELEASE_ZIPS) out/actions-go-build_$(CURR_VERSION)_$(subst /,_,$1).zip + +endef + +# Get the list of supported platforms defined in the build script. +TARGET_PLATFORMS := $(shell ./dev/build platforms) + +# For each supported platform, call FINAL_BUILD_TARGETS to create the needed targets. +$(eval $(foreach P,$(TARGET_PLATFORMS),$(call FINAL_BUILD_TARGETS,$(P)))) + +.PHONY: $(RELEASE_BUILDS) +.PHONY: $(DEV_BUILDS) + +# build builds a dev build for each platform. +build: $(DEV_BUILDS) + +# release/build builds a release build for each platform. +release/build: $(RELEASE_BUILDS) + +# release builds zip-packaged builds for each platform. +release: $(RELEASE_ZIPS) + @for Z in $^; do echo $$Z; done version: version/check @LATEST="$(shell $(GH) release list -L 1 --exclude-drafts | grep Latest | cut -f1)"; \ diff --git a/action-setup b/action-setup index 7b84c3a6..cd66acce 100755 --- a/action-setup +++ b/action-setup @@ -9,6 +9,14 @@ die() { echo "$*"; exit 1; } # It defaults to "." meaning the repo root itself. SUB_ACTION="${1:-.}" +if [[ -z "${TMPDIR:-}" ]]; then + if [[ -z "$RUNNER_TEMP" ]]; then + die "Neither TMPDIR nor RUNNER_TEMP are set." + fi + TMPDIR="$RUNNER_TEMP" + export TMPDIR +fi + # This script exports some environment variables for later steps, # and clones this entire repository into the action path. # diff --git a/dev/build b/dev/build new file mode 100755 index 00000000..45ffe849 --- /dev/null +++ b/dev/build @@ -0,0 +1,271 @@ +#!/usr/bin/env bash + +# shellcheck disable=SC2030,SC2031 # We modify exported vars in subshells on purpose in this script. + +# build +# +# This script encodes building a releasable actions-go-build CLI. +# +# Relased versions of actions-go-build are always built using themselves. +# The build happens in four stages: +# +# 1. Initial - this build is created using a standard 'go build' invocation. +# 2. Intermediate - built using the initial build. It contains its own version metadata. +# 3. Bootstrapped - built using the intermediate build. It additionally contains version metadata +# about the version of actions-go-build that built it, just like product binaries are able to. +# 4. Final - this build happens using the fully bootstrapped build. Final builds can be for any +# platform, whereas all initial, intermediate, and bootstrapped builds are always for the +# host platform. + +# Enable "safe mode" +set -Eeuo pipefail + +# Logging functions. +# shellcheck disable=SC2059 +log() { local F="$1"; shift; printf "$F\n" "$@" 1>&2; } +die() { local F="FATAL: ${1:-$_emptylog}"; shift; log "$F" "$@"; exit 1; } +err() { local F="ERROR: ${1:-$_emptylog}"; shift; log "$F" "$@"; echo 1; return 1; } +enabled() { [[ "${!1:-,,}" =~ (1|y|yes|true|enabled) ]]; } +is_debug() { enabled DEBUG; } +is_verbose() { enabled VERBOSE || enabled DEBUG; } +debug() { is_debug || return 0; local F="DEBUG: $1"; shift; log "$F" "$@"; } +verbose() { is_verbose || return 0; local F="INFO: $1"; shift; log "$F" "$@"; } +run() { + # shellcheck disable=SC2001 # using sed not bash find/replace. + if is_verbose; then + log "\$ $(sed "s,$TMP_ROOT,\$TMP_ROOT,g" <<< "$*")" + fi + /usr/bin/env "$@" +} + +# Header that enables you to run specific functions in this file. +# +# Set exit trap if this file was directly invoked rather than sourced. +# https://stackoverflow.com/questions/2683279/how-to-detect-if-a-script-is-being-sourced +(return 0 2>/dev/null) || trap 'Makefile.funcs.main "$@"' EXIT +Makefile.funcs.func_exists() { declare -F | cut -d' ' -f3 | grep -qE "^$1\$"; } +Makefile.funcs.main() { + local CODE=$?; trap - EXIT + [[ $CODE == 0 ]] || die "Script exited with code $CODE before main func could run." + [[ ${#@} != 0 ]] || { + Makefile.funcs.func_exists default || die "No arguments passed and no default function." + default "$@"; return + } + local FUNC="$1"; shift + Makefile.funcs.func_exists "$FUNC" || die "Function '%s' not defined." "$FUNC" + rm -rf "${TMP_ROOT:?}" + "$FUNC" "$@" +} + +# +# End Header + +# Define constants. + +BIN_NAME=actions-go-build + +TMP_ROOT="${TMP_ROOT:-$TMPDIR/$BIN_NAME/build}" + +SUPPORTED_PLATFORMS=( + linux/amd64 + linux/arm64 + darwin/amd64 + darwin/arm64 +) + +# +# Define defaults for this system. +# + +HOST_PLATFORM="$(go env GOHOSTOS)/$(go env GOHOSTARCH)" + +# +# Define top-level functions. +# + +# the default function is what's called when the script is invoked with no args. +default() { from_scratch "$@"; } + +# list supported platforms +platforms() { + for P in "${SUPPORTED_PLATFORMS[@]}"; do echo "$P"; done +} + +all_from_scratch() { + export FINAL_HOST_RESULT RESULT + BOOTSTRAPPED="$(bootstrap_default_path)" || return 1 + log "==> Building final distribution binaries..." + for P in "${SUPPORTED_PLATFORMS[@]}"; do + FINAL="$(final "$BOOTSTRAPPED" "$P")" + done +} + +# from_scratch performs a complete build: +# +# source code -> bootstrap -> intermediate -> final -> verification +# +from_scratch() ( local PLATFORM="${1:-$HOST_PLATFORM}" + export FINAL_HOST_RESULT RESULT + RESULT="$(tmpfile "final/$PLATFORM/result.json")" + BOOTSTRAPPED="$(bootstrap_default_path)" || return 1 + if [[ "$PLATFORM" == "$HOST_PLATFORM" ]]; then + log "==> Target platform is the same as host platform, all done." + FINAL="$BOOTSTRAPPED" + else + log "==> Building for target plaform: $PLATFORM" + FINAL="$(final "$BOOTSTRAPPED" "$PLATFORM" "$RESULT")" || return 1 + fi + + log "Success; binary written to $FINAL" +) + +# bootstrap produces a bootstrapped build from source for the host platform, ready to build +# actions-go-build binaries for all the other platforms. +bootstrap() ( export BIN_PATH="${1:-}" + log "==> Building local bootstrap build chain for host platform: $HOST_PLATFORM" + # Always use default target paths for the intial and intermediate builds. + INTERMEDIATE="$( + set -Eeuo pipefail + export BIN_PATH="" TARGET_DIR="" PLATFORM="$HOST_PLATFORM" + INITIAL="$(initial)" + INTERMEDIATE="$(intermediate "$INITIAL")" + verify "$INTERMEDIATE" > /dev/null || exit 1 + echo "$INTERMEDIATE" + )" + FINAL_HOST_RESULT="$(tmpfile "final/$HOST_PLATFORM/result.json")" + FINAL_HOST="$(bootstrapped "$INTERMEDIATE" "$HOST_PLATFORM" "$FINAL_HOST_RESULT")" || exit 1 + # Verify the final build for the host platform. + # Print the path to the bootstrapped binary... + echo "$FINAL_HOST" +) + +bootstrap_default_path() { + bootstrap "$(get_default_bin_path bootstrap "$HOST_PLATFORM")" +} + +# initial produces an initial build using just 'go build'. +initial() { + _build initial "$HOST_PLATFORM" _go_build +} + +# intermediate produces an intermediate build given a bootstrap build. +intermediate() { local INITIAL="$1" RESULT="${2:-/dev/null}" + _build intermediate "$HOST_PLATFORM" _self_build "$INITIAL" build "$RESULT" +} + +# bootstrapped produces the bootstrapped build given an intermediate build. +bootstrapped() ( local INTERMEDIATE="$1" PLATFORM="${2:-$HOST_PLATFORM}" RESULT="${3:-/dev/null}" + export TARGET_DIR="${TARGET_DIR:-dist/$PLATFORM}" + export BIN_PATH="${BIN_PATH:-}" + _build bootstrapped "$PLATFORM" _self_build "$INTERMEDIATE" build "$RESULT" +) + +# final produces a final build given a bootstraped build. +final() ( local BOOTSTRAPPED="$1" PLATFORM="${2:-$HOST_PLATFORM}" RESULT="${3:-/dev/null}" + export TARGET_DIR="${TARGET_DIR:-dist/$PLATFORM}" + export BIN_PATH="${BIN_PATH:-}" + _build final "$PLATFORM" _self_build "$BOOTSTRAPPED" build "$RESULT" +) + +# dev produces a final dev build given a bootstraped build. +dev() ( local BOOTSTRAPPED="$1" PLATFORM="${2:-$HOST_PLATFORM}" RESULT="${3:-/dev/null}" + final "$@" +) + +# release produces a final release build given a bootstrapped build. +# The difference from dev is that final fails if the worktree is not clean. +release() ( local BOOTSTRAPPED="$1" PLATFORM="${2:-$HOST_PLATFORM}" RESULT="${3:-/dev/null}" + export REQUIRE_CLEAN_WORKTREE=true + final "$@" +) + +verify() { local FINAL="$1" RESULT_OUT="${2:-/dev/null}" + _build verification "$HOST_PLATFORM" _self_build "$FINAL" verify "$RESULT_OUT" +} + +verify_remote() { local FINAL="$1" RESULT_IN="$2" RESULT_OUT="${3:-/dev/null}" + _build verification "$HOST_PLATFORM" _self_build "$FINAL" verify "$RESULT_OUT" "$RESULT_IN" +} + +# +# Define utility functions. +# + +# _build performs a build and outputs the absolute path to the built binary. +_build() { local TYPE="$1" PLATFORM="$2"; shift 2 + verbose "TMP_ROOT=$TMP_ROOT" + assert_supported_platform "$PLATFORM" + OS="$(cut -d'/' -f1 <<< "$PLATFORM")" || return 1 + ARCH="$(cut -d'/' -f2 <<< "$PLATFORM")" || return 1 + if [[ -z "${BIN_PATH:-}" ]]; then + BIN_PATH="$(get_bin_path "$TYPE" "$PLATFORM")" || return 1 + fi + export BIN_PATH TARGET_DIR + log "Building %s %s/%s binary..." "$TYPE" "$OS" "$ARCH" + OS="$OS" ARCH="$ARCH" BIN_PATH="$BIN_PATH" "$@" 1>&2 || return 1 + echo "$BIN_PATH" +} + +# _self_build performs a build of actions-go-build using actions-go-build. +# USING is the path to the actions-go-build binary to use for this build. +_self_build() { local USING="$1" SUBCOMMAND="$2" RESULT_PATH="${3:-/dev/null}"; shift 3 + if [[ -z "$USING" ]]; then + return "$(err "USING is empty")" + fi + # if USING is a relative path, make it invokable by prefixing with ./ + if [[ "${USING:0:1}" != / ]]; then + USING="./$USING" + fi + local FLAGS=(-rebuild -json) + if ${REQUIRE_CLEAN_WORKTREE:-false}; then + FLAGS+=(-clean) + fi + if is_debug; then + FLAGS+=(-debug) + elif is_verbose; then + FLAGS+=(-v) + else + FLAGS+=(-q) + fi + TARGET_DIR="$(dirname "$BIN_PATH")" + BIN_NAME="$(basename "$BIN_PATH")" + export TARGET_DIR BIN_NAME + run "$USING" "$SUBCOMMAND" "${FLAGS[@]}" "$@" > "$RESULT_PATH" +} + +_go_build() { PACKAGE="${1:-.}" + GOOS="$OS" GOARCH="$ARCH" run go build -o "$BIN_PATH" "$PACKAGE" +} + +# tmpdir creates a temp directory and prints its path. +tmpdir() { local D="$TMP_ROOT/$1" && mkdir -p "$D" && echo "$D"; } + +tmpfile() { local D R F + D="$(dirname "$1")" + R="$(tmpdir "$D")" + F="$R/$(basename "$1")" + rm -rf "$F" + echo "$F" +} + +get_default_bin_path() { local KIND="$1" PLATFORM="${2:-$HOST_PLATFORM}" + tmpfile "$KIND/$PLATFORM/dist/$BIN_NAME" +} + +get_bin_path() { local KIND="$1" PLATFORM="$2" + [[ -z "${BIN_PATH:-}" ]] || { echo "$BIN_PATH" && return 0; } + [[ -z "${TARGET_DIR:-}" ]] || { echo "$TARGET_DIR/$BIN_NAME" && return 0; } + get_default_bin_path "$KIND" "$PLATFORM" +} + +is_supported_platform() { local P="$1" + for S in "${SUPPORTED_PLATFORMS[@]}"; do + [[ "$S" != "$P" ]] || return 0 + done + return 1 +} + +assert_supported_platform() { local P="$1" + is_supported_platform "$P" || \ + die "Platform '%s' not supported. Pick from: %s" "$P" "${SUPPORTED_PLATFORMS[*]}" +} diff --git a/dev/changes/v0.1.8.md b/dev/changes/v0.1.8.md index 3e53b209..da83a8b4 100644 --- a/dev/changes/v0.1.8.md +++ b/dev/changes/v0.1.8.md @@ -13,6 +13,9 @@ - New inspect flag: `-worktree` which reports on the dirty/clean status of the worktree. - Development documentation docs/development.md +- Build system: + - `make build` - build dev (maybe dirty) CLI binaries & zips for all supported platforms. + - `make release` - build release (clean) CLI binaries & zipe for all supported platforms. ### Fixed: diff --git a/dev/source-id b/dev/source-id new file mode 100755 index 00000000..4931aa7d --- /dev/null +++ b/dev/source-id @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# Calculate an ID for the current state of the work tree. +# +# Take the hashes of git diff and git diff --staged and the HEAD +# commit ID, then take the hash of them all together. + +set -Eeuo pipefail + +sha() { + local SHA256SUM=sha256sum + if [[ "$(uname)" == "Darwin" ]]; then + SHA256SUM="shasum -a 256" + fi + cat - | $SHA256SUM | cut -d' ' -f1 +} + +nonempty_diff_sha() { local NAME="git diff $*" + SHA="" + D="$(git diff --exit-code "$@")" || { + SHA="$(sha <<< "$D")" + } + echo "$SHA" +} + +{ + git rev-parse HEAD + nonempty_diff_sha + nonempty_diff_sha --staged +} | sha diff --git a/dev/update-source-id b/dev/update-source-id new file mode 100755 index 00000000..ca910dfd --- /dev/null +++ b/dev/update-source-id @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +# Update the source ID + +SOURCE_ID="${SOURCE_ID:-.git/source-id}" + +NEW="$(./dev/source-id)" +CURR="$(cat "$SOURCE_ID" 2>/dev/null || echo none)" +[[ "$NEW" == "$CURR" ]] || echo "$NEW" > "$SOURCE_ID" +echo "$NEW" diff --git a/pkg/build/build.go b/pkg/build/build.go index cb7ffc19..cd7ea350 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -146,7 +146,10 @@ func (b *core) Steps() []Step { func (b *core) createDirectories() error { c := b.config - return fs.Mkdirs(c.Paths.TargetDir(), c.Paths.ZipDir(), c.Paths.MetaDir) + if err := fs.MkdirEmpty(c.Paths.TargetDir()); err != nil { + return err + } + return fs.Mkdirs(c.Paths.ZipDir(), c.Paths.MetaDir) } func (b *core) assertExecutableWritten() error {