Something went wrong. Try again.
Backend environment for match hosting for lance.blue
Something went wrong. Try again.
Shell
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655#!/usr/bin/env bash# Which locally built image is in play.## ./scripts/image.sh list every arena image on this machine# ./scripts/image.sh resolve the tag versions.env names# ./scripts/image.sh resolve newest the most recent build# ./scripts/image.sh resolve c836f26 the build made from that commit# ./scripts/image.sh verify newest resolve it, and say what it is# ./scripts/image.sh diff 4e5403f c836f26 what changed between two builds## A machine that has built this repo a few times has a dozen tags on it, spanning# more than one Suramadu version, and the tag string is the only thing telling# them apart. Reading a tag is guesswork; this script answers instead.## `resolve` prints one reference on stdout and nothing else, so anything that# needs an image can ask here rather than composing IMAGE_NAME and IMAGE_TAG for# itself:## IMAGE="$(scripts/image.sh resolve "$SELECTOR")"## Selectors:## (nothing), current the tag versions.env names for this commit# newest the most recently built image# <tag> an exact tag, as `list` prints it# <sha> a git sha, short or long; matches the sha in the tag# <name>:<tag> a full reference, taken as given## Every command is read-only. Nothing here builds, pushes or removes an image -# what to delete is your call, so `list` reports what is on disk and stops there.set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"# shellcheck source=../versions.env. "$ROOT/versions.env"
say() { printf 'image: %s\n' "$*" >&2; }die() { say "$*"; exit 1; }
usage() { cat <<'USAGE'image.sh - which locally built image is in play
image.sh list every arena image on this machine image.sh resolve [<selector>] print one reference, and nothing else image.sh verify [<selector>] resolve it, and say what it is image.sh diff [--deep] <a> <b> what changed between two builds
selectors
(nothing), current the tag versions.env names for this commit newest the most recently built image <tag> an exact tag, as `list` prints it <sha> a git sha, short or long; matches the sha in the tag <name>:<tag> a full reference, taken as given
--deep runs one throwaway container per image to read versions.env and thecontainer scripts out of them. Everything else is `docker image inspect`.USAGE}
# One temp directory per run, cleaned up on the way out. A RETURN trap per# function would be tidier to read but traps are not scoped to a function, so# the second one to be set wins and the first directory is never removed.## It sets $TMP rather than printing it: a command substitution runs in a# subshell, so the EXIT trap would fire there and delete the directory before# the caller had written anything into it.TMP=""tmpdir() { if [ -z "$TMP" ]; then TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT fi}
# --- facts a tag carries -----------------------------------------------------
# versions.env builds the tag as mm<megamek>-sur<suramadu>-<ref>, where <ref> is# the branch, a dash, and a 12-character sha - with the branch dropped on a# detached HEAD and "-dirty" appended when the tree was not clean.## So the commit is the LAST dash-separated field, never the third: branch names# contain dashes, and this repo's branches are all `claude/some-long-name`. Tags# from before the ref was part of the name (mm0.51.0-ws26.4.5) end in a version# rather than hex, and get no commit at all, which is the right answer for them.tag_commit() { local tag="${1%-dirty}" last last="${tag##*-}" case "$last" in "" | *[!0-9a-f]*) return 0 ;; esac # Seven is git's own floor for an abbreviated sha. Below it, a short lowercase # field like "abc" is far more likely to be part of a branch name. [ "${#last}" -ge 7 ] || return 0 printf '%s' "$last"}
# The commit an image came from: the tag first, then the revision label the build# baked in. They agree on everything built since the tag started carrying a ref;# the label is what answers for the tags from before that, which is most of what# is on a machine that has been building this repo for a week.image_commit() { local sha sha="$(tag_commit "$1")" [ -n "$sha" ] || sha="$(tag_commit "${2:-}")" printf '%s' "$sha"}
tag_is_dirty() { case "$1" in *-dirty) return 0 ;; esac return 1}
# A tag naming a commit nobody can `git show` is a build nobody can reproduce.# scripts/deploy.sh refuses to ship one; here it is only reported, because an# unreachable commit on a local image is normal - it is usually a rebased branch.commit_in_repo() { [ -n "$1" ] || return 1 git -C "$ROOT" cat-file -e "${1}^{commit}" 2>/dev/null}
commit_subject() { git -C "$ROOT" log -1 --format=%s "$1" 2>/dev/null || true}
# A selector and a tag's commit match when either is a prefix of the other: the# tag carries 12 characters, `git log --oneline` prints 7, and people paste all# 40. Four is the shortest prefix worth trusting to mean one commit.sha_matches() { local sel="$1" have="$2" [ -n "$have" ] || return 1 [ "${#sel}" -ge 4 ] || return 1 case "$have" in "$sel"*) return 0 ;; esac case "$sel" in "$have"*) return 0 ;; esac return 1}
# --- formatting --------------------------------------------------------------
# Integer arithmetic only, so this needs nothing beyond bash. KiB is here for# `diff`: two builds off the same base differ by a metadata layer, and "0.0 MiB# larger" is not an answer.human_bytes() { local b="$1" unit scaled if [ "$b" -lt 1048576 ]; then scaled=$(( (b * 10 + 512) / 1024 )); unit=KiB elif [ "$b" -lt 1073741824 ]; then scaled=$(( (b * 10 + 524288) / 1048576 )); unit=MiB else scaled=$(( (b * 10 + 536870912) / 1073741824 )); unit=GiB fi printf '%d.%d %s' $(( scaled / 10 )) $(( scaled % 10 )) "$unit"}
# 2026-08-07T17:44:54.895042045-04:00 -> 2026-08-07 17:44short_time() { printf '%s %s' "${1:0:10}" "${1:11:5}"; }
short_id() { local id="${1#sha256:}"; printf '%s' "${id:0:12}"; }
# --- reading the daemon ------------------------------------------------------
need_docker() { command -v docker >/dev/null || die "docker is not installed"}
# id|size|created|revision, for one reference. The revision label is what# build.sh baked in from BUILD_REF, so it is the image's own copy of the commit -# worth having next to the one parsed out of the tag.inspect_fields() { docker image inspect --format \ '{{.Id}}|{{.Size}}|{{.Created}}|{{with index .Config.Labels "org.opencontainers.image.revision"}}{{.}}{{end}}' \ "$1"}
image_exists() { docker image inspect "$1" >/dev/null 2>&1; }
# scripts/run.sh has a one-line version of this check. The difference is what it# can say afterwards: the usual cause of a miss is that versions.env names a tag# for THIS commit and no build has produced it yet, which reads as "not built"# when what changed was the commit.require_image() { local ref="$1" sel="${2:-}" if image_exists "$ref"; then return 0; fi say "no such image on this machine: $ref" if [ -z "$sel" ] || [ "$sel" = current ]; then say "that is the tag versions.env names for this commit - build it:" say " ./scripts/build.sh" fi say "./scripts/image.sh list shows what is on disk" exit 1}
# Every tag of IMAGE_NAME, grouped by image id, newest first.## Grouping matters for the totals: build.sh tags each build `latest` as well as# its immutable tag, so two rows share one set of layers. A row per tag would# imply disk usage that is not there.## The sort is ours rather than `docker image ls`'s, because timestamps here are# not unique - three builds of this repo carry the same Created value down to the# nanosecond, and docker orders those ties differently from one run to the next,# so the same command printed a different "newest" each time. Sorting on the# second and then on the tag makes the output the same twice in a row; which of# a tied set is really the newest build is a question for git, and `newest`# answers it below.IMG_LOADED=0G_TAG=() # the tag that best names the groupG_ALIAS=() # its other tags, space separatedG_ID=()G_SIZE=()G_CREATED=()G_REV=()
img_load() { if [ "$IMG_LOADED" = 1 ]; then return 0; fi IMG_LOADED=1 need_docker
local tags=() mapfile -t tags < <(docker image ls "$IMAGE_NAME" --format '{{.Tag}}' | grep -vx '<none>' || true) if [ "${#tags[@]}" -eq 0 ]; then return 0; fi
local refs=() t for t in "${tags[@]}"; do refs+=("$IMAGE_NAME:$t"); done
# One inspect for the whole list. Its output is one line per argument, in # order, which is the only thing tying a line back to its tag - so if the # counts disagree, something removed an image while this was running and the # rows would be attached to the wrong tags. local rows=() mapfile -t rows < <(docker image inspect --format \ '{{.Id}}|{{.Size}}|{{.Created}}|{{with index .Config.Labels "org.opencontainers.image.revision"}}{{.}}{{end}}' \ "${refs[@]}") if [ "${#rows[@]}" -ne "${#tags[@]}" ]; then die "the image list changed while it was being read; run this again" fi
# Sort on the whole second only. Docker prints RFC 3339 with the trailing # zeros trimmed, so the fractional part is not a fixed width and comparing it # as text is not comparing time. local order=() when mapfile -t order < <( for i in "${!tags[@]}"; do when="${rows[$i]#*|}"; when="${when#*|}" printf '%s\t%s\t%s\n' "${when:0:19}" "${tags[$i]}" "$i" done | sort -t$'\t' -k1,1r -k2,2 )
local row i id size created rev g found for row in "${order[@]}"; do i="${row##*$'\t'}" IFS='|' read -r id size created rev <<<"${rows[$i]}" found=-1 for g in "${!G_ID[@]}"; do if [ "${G_ID[$g]}" = "$id" ]; then found="$g"; break; fi done if [ "$found" -lt 0 ]; then G_TAG+=("${tags[$i]}"); G_ALIAS+=(""); G_ID+=("$id") G_SIZE+=("$size"); G_CREATED+=("$created"); G_REV+=("$rev") continue fi # `latest` moves with every build and cannot say which one it is, so a tag # that carries a commit takes the row and the moving one becomes the alias. if [ -z "$(tag_commit "${G_TAG[$found]}")" ] && [ -n "$(tag_commit "${tags[$i]}")" ]; then G_ALIAS[$found]="${G_ALIAS[$found]:+${G_ALIAS[$found]} }${G_TAG[$found]}" G_TAG[$found]="${tags[$i]}" else G_ALIAS[$found]="${G_ALIAS[$found]:+${G_ALIAS[$found]} }${tags[$i]}" fi done}
# The newest build, as a group index.## Timestamps tie, so the first row is only a candidate. Among everything built in# the same second, the newest build is the one whose commit has all the others# behind it in git - which is a question the timestamps cannot answer and git# always can. If they are not on one line of history there is no newest, and# saying so beats picking whichever the daemon happened to list first.newest_group() { img_load [ "${#G_TAG[@]}" -gt 0 ] || die "no $IMAGE_NAME image on this machine; run ./scripts/build.sh"
local tied=() g for g in "${!G_TAG[@]}"; do if [ "${G_CREATED[$g]:0:19}" = "${G_CREATED[0]:0:19}" ]; then tied+=("$g"); fi done if [ "${#tied[@]}" -eq 1 ]; then printf '%s' "${tied[0]}"; return 0; fi
# How many of the others each candidate has behind it. The tip has all of them. local best=-1 bestscore=-1 score other ca cb ties=0 for g in "${tied[@]}"; do ca="$(image_commit "${G_TAG[$g]}" "${G_REV[$g]}")" score=0 for other in "${tied[@]}"; do [ "$other" != "$g" ] || continue cb="$(image_commit "${G_TAG[$other]}" "${G_REV[$other]}")" if [ -n "$ca" ] && [ -n "$cb" ] \ && git -C "$ROOT" merge-base --is-ancestor "$cb" "$ca" 2>/dev/null; then score=$(( score + 1 )) fi done if [ "$score" -gt "$bestscore" ]; then best="$g"; bestscore="$score"; ties=0 elif [ "$score" -eq "$bestscore" ]; then ties=$(( ties + 1 )) fi done
if [ "$ties" -gt 0 ]; then say "${#tied[@]} images were built in the same second and git cannot order them:" for g in "${tied[@]}"; do say " ${G_TAG[$g]}"; done die "name one of them instead of 'newest'" fi printf '%s' "$best"}
# Index of the group holding a tag, or nothing.group_of_tag() { local want="$1" g t for g in "${!G_TAG[@]}"; do if [ "${G_TAG[$g]}" = "$want" ]; then printf '%s' "$g"; return 0; fi for t in ${G_ALIAS[$g]}; do if [ "$t" = "$want" ]; then printf '%s' "$g"; return 0; fi done done}
# --- resolve -----------------------------------------------------------------
resolve_selector() { local sel="${1:-}" g hits=() case "$sel" in "" | current) printf '%s:%s' "$IMAGE_NAME" "$IMAGE_TAG" return 0 ;; *:*) # Already a full reference. Nothing to derive, and second-guessing a # repository someone typed in full is not this script's business. printf '%s' "$sel" return 0 ;; newest) # Loaded here rather than only inside newest_group: that call is a command # substitution, and a subshell cannot hand the group arrays back. img_load g="$(newest_group)" printf '%s:%s' "$IMAGE_NAME" "${G_TAG[$g]}" return 0 ;; esac
img_load
# An exact tag wins over a sha. It is unambiguous, and it is the string `list` # printed, so what you copied is what you get. g="$(group_of_tag "$sel")" if [ -n "$g" ]; then printf '%s:%s' "$IMAGE_NAME" "$sel" return 0 fi
for g in "${!G_TAG[@]}"; do if sha_matches "$sel" "$(image_commit "${G_TAG[$g]}" "${G_REV[$g]}")"; then hits+=("$g"); fi done
case "${#hits[@]}" in 0) die "no tag and no build matches '$sel'; ./scripts/image.sh list shows what is on disk" ;; 1) printf '%s:%s' "$IMAGE_NAME" "${G_TAG[${hits[0]}]}"; return 0 ;; esac
say "'$sel' matches more than one build:" for g in "${hits[@]}"; do say " ${G_TAG[$g]}"; done die "name one of them"}
# --- commands ----------------------------------------------------------------
cmd_resolve() { [ "$#" -le 1 ] || die "usage: image.sh resolve [<selector>]" local ref ref="$(resolve_selector "${1:-}")" require_image "$ref" "${1:-}" printf '%s\n' "$ref"}
cmd_verify() { [ "$#" -le 1 ] || die "usage: image.sh verify [<selector>]" local ref sel="${1:-}" ref="$(resolve_selector "$sel")" require_image "$ref" "$sel"
local id size created rev sha subject g IFS='|' read -r id size created rev < <(inspect_fields "$ref") sha="$(image_commit "${ref##*:}" "$rev")"
printf '%s\n' "$ref" printf ' id %s\n' "$(short_id "$id")" printf ' size %s (docker image inspect .Size)\n' "$(human_bytes "$size")" printf ' built %s\n' "$(short_time "$created")" printf ' ref %s (baked into the image as a label)\n' "${rev:--}"
if [ -z "$sha" ]; then printf ' commit none in the tag, and no revision label either\n' elif commit_in_repo "$sha"; then subject="$(commit_subject "$sha")" printf ' commit %s %s\n' "$sha" "$subject" else printf ' commit %s NOT in this repo - nothing can rebuild this image\n' "$sha" fi if tag_is_dirty "${ref##*:}"; then printf ' built from an uncommitted tree; the commit above is not what it contains\n' fi
img_load g="$(group_of_tag "${ref##*:}")" if [ -n "$g" ] && [ -n "${G_ALIAS[$g]}" ]; then printf ' also %s\n' "${G_ALIAS[$g]}" fi}
cmd_list() { [ "$#" -eq 0 ] || die "usage: image.sh list" img_load if [ "${#G_TAG[@]}" -eq 0 ]; then say "no $IMAGE_NAME image on this machine; run ./scripts/build.sh" return 0 fi
local g w=3 t total=0 orphans=0 mark sha state for t in "${G_TAG[@]}"; do if [ "${#t}" -gt "$w" ]; then w="${#t}"; fi done
# By group rather than by tag: the tag versions.env names can be an alias of # the row, and the row is still the image it means. local current; current="$(group_of_tag "$IMAGE_TAG")"
printf '%-2s %-*s %-12s %10s %-16s %-12s %s\n' \ '' "$w" TAG ID SIZE BUILT COMMIT 'IN REPO' for g in "${!G_TAG[@]}"; do sha="$(image_commit "${G_TAG[$g]}" "${G_REV[$g]}")" if [ -z "$sha" ]; then state='-' elif commit_in_repo "$sha"; then state=yes else state=no orphans=$(( orphans + 1 )) fi mark=' ' if [ "$g" = "$current" ]; then mark='*'; fi printf '%-2s %-*s %-12s %10s %-16s %-12s %s\n' \ "$mark" "$w" "${G_TAG[$g]}" "$(short_id "${G_ID[$g]}")" \ "$(human_bytes "${G_SIZE[$g]}")" "$(short_time "${G_CREATED[$g]}")" \ "${sha:--}" "$state" if [ -n "${G_ALIAS[$g]}" ]; then printf ' also tagged: %s\n' "${G_ALIAS[$g]}" fi total=$(( total + G_SIZE[g] )) done
printf '\n%d image(s), %s total, counted once per image id\n' \ "${#G_TAG[@]}" "$(human_bytes "$total")" if [ -n "$current" ]; then printf '* %s is what versions.env names for this commit\n' "$IMAGE_TAG" else printf '* nothing here is %s, which is what versions.env names for this commit\n' "$IMAGE_TAG" fi if [ "$orphans" -gt 0 ]; then printf '%d image(s) name a commit this repo does not have; nothing can rebuild them\n' "$orphans" fi report_dangling}
# Untagged images are not necessarily arena's - a rebuild of anything on this# machine leaves one behind. Reported rather than removed: which of them is worth# keeping is not a decision a script gets to make.report_dangling() { local ids=() sizes=() n s total=0 mapfile -t ids < <(docker image ls -f dangling=true --format '{{.ID}}' || true) n="${#ids[@]}" if [ "$n" -eq 0 ]; then return 0; fi mapfile -t sizes < <(docker image inspect --format '{{.Size}}' "${ids[@]}" 2>/dev/null || true) for s in "${sizes[@]}"; do total=$(( total + s )); done printf '%d untagged image(s) hold %s, from any project on this machine:\n' \ "$n" "$(human_bytes "$total")" printf ' docker image ls -f dangling=true\n'}
# --- diff --------------------------------------------------------------------
# The same trick scripts/run.sh uses to read a scenario out of an image: one# throwaway container, no entrypoint, one command that reads files and exits.# versions.env is copied into the image, so this is the build's own record of# what it pinned rather than an inference from the tag.## Capped on purpose. This cats a file and hashes a handful of scripts; if it ever# needs more than a core and half a gigabyte, something is wrong with it.DEEP_READ='echo "=== /opt/arena/versions.env"cat /opt/arena/versions.env 2>/dev/null || echo "(this image has no versions.env)"echo "=== container scripts (sha256)"find /opt/arena/container -name "*.sh" 2>/dev/null | sort \ | xargs -r sha256sum 2>/dev/null | sed "s|/opt/arena/||" \ || echo "(no container scripts)"'
deep_read() { docker run --rm --cpus 1 --memory 512m --entrypoint /bin/sh "$1" -c "$DEEP_READ"}
cmd_diff() { local deep=0 if [ "${1:-}" = --deep ]; then deep=1; shift; fi [ "$#" -eq 2 ] || die "usage: image.sh diff [--deep] <selector> <selector>"
local a b a="$(resolve_selector "$1")"; require_image "$a" "$1" b="$(resolve_selector "$2")"; require_image "$b" "$2"
local aid asize acreated arev bid bsize bcreated brev IFS='|' read -r aid asize acreated arev < <(inspect_fields "$a") IFS='|' read -r bid bsize bcreated brev < <(inspect_fields "$b")
printf 'a %s\n %s %s %s\n' "$a" "$(short_id "$aid")" \ "$(human_bytes "$asize")" "$(short_time "$acreated")" printf 'b %s\n %s %s %s\n\n' "$b" "$(short_id "$bid")" \ "$(human_bytes "$bsize")" "$(short_time "$bcreated")"
if [ "$aid" = "$bid" ]; then printf 'Both selectors name the same image.\n' return 0 fi
local delta=$(( bsize - asize )) word=larger if [ "$delta" -lt 0 ]; then delta=$(( -delta )); word=smaller; fi if [ "$delta" -eq 0 ]; then printf 'size the same, %s\n' "$(human_bytes "$asize")" else printf 'size b is %s %s (%d bytes)\n' "$(human_bytes "$delta")" "$word" "$delta" fi
diff_labels "$a" "$b" diff_commits "$(image_commit "${a##*:}" "$arev")" "$(image_commit "${b##*:}" "$brev")" diff_layers "$a" "$b"
if [ "$deep" = 1 ]; then diff_deep "$a" "$b"; fi}
# Go's template iterates a map in key order, so both sides come out sorted and a# plain line comparison is enough. The key names moved when Webswing Lite became# Suramadu, which is exactly the kind of difference a fixed list of keys hides.diff_labels() { tmpdir; local tmp="$TMP" local fmt='{{range $k, $v := .Config.Labels}}{{$k}}={{$v}}{{"\n"}}{{end}}' docker image inspect --format "$fmt" "$1" >"$tmp/labels-a" docker image inspect --format "$fmt" "$2" >"$tmp/labels-b" if diff -q "$tmp/labels-a" "$tmp/labels-b" >/dev/null; then printf 'labels identical\n' return 0 fi printf 'labels\n' diff --unchanged-line-format= --old-line-format=' - %L' --new-line-format=' + %L' \ "$tmp/labels-a" "$tmp/labels-b" || true}
# The revision label is the build's own copy of BUILD_REF, so the range below is# between the commits the images were actually built from.diff_commits() { local ca="$1" cb="$2" n if [ -z "$ca" ] || [ -z "$cb" ]; then printf 'commits at least one image does not name a commit\n' return 0 fi if ! commit_in_repo "$ca" || ! commit_in_repo "$cb"; then printf 'commits %s -> %s, and this repo does not have both\n' "$ca" "$cb" return 0 fi n="$(git -C "$ROOT" rev-list --count "$ca..$cb" 2>/dev/null || echo 0)" printf 'commits %s -> %s, %s commit(s) forward\n' "$ca" "$cb" "$n" git -C "$ROOT" log --oneline --max-count=20 "$ca..$cb" 2>/dev/null | sed 's/^/ /' || true}
# Two builds off the same base share every layer up to the first thing that# changed, and where that is says whether the diff is a rebuild of the world or a# metadata bump. .RootFS.Layers is the cheap answer; docker history names the# step.diff_layers() { tmpdir; local tmp="$TMP" local fmt='{{range .RootFS.Layers}}{{.}}{{"\n"}}{{end}}' docker image inspect --format "$fmt" "$1" >"$tmp/layers-a" docker image inspect --format "$fmt" "$2" >"$tmp/layers-b"
local la=() lb=() common=0 i mapfile -t la <"$tmp/layers-a" mapfile -t lb <"$tmp/layers-b" for i in "${!la[@]}"; do if [ "$i" -ge "${#lb[@]}" ] || [ "${la[$i]}" != "${lb[$i]}" ]; then break; fi common=$(( common + 1 )) done printf 'layers %d shared, then %d in a and %d in b\n' \ "$common" "$(( ${#la[@]} - common ))" "$(( ${#lb[@]} - common ))"
# docker history prints newest first; reversed, step 1 is the base image and # the numbering lines up with reading the Dockerfile top to bottom. local ha=() hb=() mapfile -t ha < <(docker history --no-trunc --format '{{.CreatedBy}}' "$1" | tac) mapfile -t hb < <(docker history --no-trunc --format '{{.CreatedBy}}' "$2" | tac) for i in "${!ha[@]}"; do if [ "$i" -ge "${#hb[@]}" ] || [ "${ha[$i]}" != "${hb[$i]}" ]; then printf 'history first difference at step %d of %d\n' "$(( i + 1 ))" "${#ha[@]}" printf ' a %.100s\n' "${ha[$i]}" if [ "$i" -lt "${#hb[@]}" ]; then printf ' b %.100s\n' "${hb[$i]}"; fi return 0 fi done printf 'history identical for all %d step(s)\n' "${#ha[@]}"}
diff_deep() { tmpdir; local tmp="$TMP" printf '\ninside the images\n' # One at a time, and each exits as soon as it has read what it came for. deep_read "$1" >"$tmp/deep-a" deep_read "$2" >"$tmp/deep-b" if diff -q "$tmp/deep-a" "$tmp/deep-b" >/dev/null; then printf ' versions.env and the container scripts are identical\n' return 0 fi diff -u --label a --label b "$tmp/deep-a" "$tmp/deep-b" | sed 's/^/ /' || true}
# --- entry -------------------------------------------------------------------
main() { local cmd=list if [ "$#" -gt 0 ]; then cmd="$1"; shift; fi case "$cmd" in list) cmd_list "$@" ;; resolve) cmd_resolve "$@" ;; verify) cmd_verify "$@" ;; diff) cmd_diff "$@" ;; -h | --help | help) usage ;; *) die "unknown command '$cmd'; there is list, resolve, verify and diff" ;; esac}
# Sourcing this file defines the helpers and runs nothing, which is how# tests/shell/test-image.sh exercises the parsing where there is no docker# daemon - the image's test stage, for one.if [ "${BASH_SOURCE[0]}" = "${0}" ]; then main "$@"fi