Skip to main content

max / mountaineer

4.8 KB · 155 lines History Blame Raw
1 #!/bin/sh
2 # build-apkovl.sh — produce a Mountaineer apkovl tarball from apkovl-src/.
3 #
4 # Usage: build-apkovl.sh <profile> [<output-path>]
5 #
6 # <profile> matches a [profiles.<name>] section in apkovl-src/manifest.toml.
7 # <output-path> defaults to ./out/mountaineer-<profile>-<version>.apkovl.tar.gz.
8 #
9 # Build environment: Alpine host (the canonical path) or any system with
10 # GNU tar 1.28+. macOS users: `brew install gnu-tar` and rerun.
11 #
12 # Reproducibility: best-effort. Same source tree + same builder version →
13 # same bytes. We sort filenames, pin uid/gid/mode, zero mtimes, and gzip
14 # with --no-name. Bit-identical across builder versions is a v0.2 goal.
15
16 set -eu
17
18 # --- locate GNU tar -----------------------------------------------------
19 find_gnu_tar() {
20 for candidate in gtar tar; do
21 if command -v "$candidate" >/dev/null 2>&1; then
22 if "$candidate" --version 2>&1 | grep -q 'GNU tar'; then
23 echo "$candidate"
24 return 0
25 fi
26 fi
27 done
28 return 1
29 }
30
31 if ! TAR=$(find_gnu_tar); then
32 cat >&2 <<'EOF'
33 error: GNU tar is required (deterministic --sort/--mtime/--numeric-owner).
34 Alpine: apk add tar
35 macOS: brew install gnu-tar
36 Debian: apt install tar (GNU tar by default)
37 EOF
38 exit 1
39 fi
40
41 # --- locate the source tree ---------------------------------------------
42 here() { cd "$(dirname "$0")/.." && pwd; }
43 ROOT="$(here)"
44 SRC="$ROOT/apkovl-src"
45 MANIFEST="$SRC/manifest.toml"
46
47 profile=""
48 output=""
49 extras=""
50 while [ $# -gt 0 ]; do
51 case "$1" in
52 --extra) shift; extras="$extras $1" ;;
53 --) shift; break ;;
54 -*) echo "unknown flag: $1" >&2; exit 2 ;;
55 *) if [ -z "$profile" ]; then profile="$1"
56 elif [ -z "$output" ]; then output="$1"
57 else echo "too many positional args" >&2; exit 2; fi ;;
58 esac
59 shift
60 done
61
62 if [ -z "$profile" ]; then
63 echo "usage: $0 <profile> [<output-path>] [--extra <dir>]..." >&2
64 exit 2
65 fi
66
67 if [ ! -f "$MANIFEST" ]; then
68 echo "error: manifest not found at $MANIFEST" >&2
69 exit 1
70 fi
71
72 # --- parse manifest.toml (intentionally minimal) ------------------------
73 # The manifest is small enough to grep. If it grows past what a few awks
74 # can handle, replace this block with a real parser.
75 version=$(awk -F'"' '/^version *= */ {print $2; exit}' "$MANIFEST")
76 sources=$(awk -v p="$profile" '
77 $0 ~ "^\\[profiles\\."p"\\]" {in_section=1; next}
78 in_section && /^\[/ {in_section=0}
79 in_section && /^sources *=/ {
80 sub(/.*\[/, "")
81 sub(/\].*/, "")
82 gsub(/"|[[:space:]]/, "")
83 print
84 exit
85 }
86 ' "$MANIFEST")
87
88 if [ -z "$version" ]; then
89 echo "error: could not parse version from $MANIFEST" >&2
90 exit 1
91 fi
92 if [ -z "$sources" ]; then
93 echo "error: profile '$profile' not found in $MANIFEST" >&2
94 exit 1
95 fi
96
97 [ -n "$output" ] || output="$ROOT/out/mountaineer-$profile-$version.apkovl.tar.gz"
98 mkdir -p "$(dirname "$output")"
99
100 # --- stage merged tree --------------------------------------------------
101 stage=$(mktemp -d)
102 trap 'rm -rf "$stage"' EXIT
103
104 oldifs="$IFS"
105 IFS=','
106 for src in $sources; do
107 if [ ! -d "$SRC/$src" ]; then
108 echo "error: source directory '$src' not found under apkovl-src/" >&2
109 exit 1
110 fi
111 # tar-pipe handles symlinks, modes, and recursion portably. Later
112 # sources overwrite earlier ones — declared order matters.
113 (cd "$SRC/$src" && "$TAR" cf - .) | (cd "$stage" && "$TAR" xf -)
114 done
115 IFS="$oldifs"
116
117 # --extra <dir>... — merge in dynamically-generated content after the
118 # declared sources. Used by genapkovl to embed the target apkovl and the
119 # installer into the live apkovl at build time.
120 for extra in $extras; do
121 if [ ! -d "$extra" ]; then
122 echo "error: --extra directory '$extra' not found" >&2
123 exit 1
124 fi
125 (cd "$extra" && "$TAR" cf - .) | (cd "$stage" && "$TAR" xf -)
126 done
127
128 # Drop .keep markers (they retain empty dirs in the source; the dir ships,
129 # the marker doesn't).
130 find "$stage" -name '.keep' -delete
131
132 # Zero mtimes on every staged path so tar's metadata is determined entirely
133 # by file content and sort order, not when the build ran. Skip symlinks —
134 # touch follows them by default and the runlevel symlinks (etc/runlevels/*)
135 # point at root-owned /etc/init.d/* files that the build user can't touch.
136 # tar --mtime='@0' zeroes the archive entries regardless.
137 find "$stage" ! -type l -exec touch -d '@0' {} + 2>/dev/null || \
138 find "$stage" ! -type l -exec touch -t 197001010000.00 {} +
139
140 # --- emit deterministic tarball -----------------------------------------
141 SOURCE_DATE_EPOCH=0
142 export SOURCE_DATE_EPOCH
143
144 (cd "$stage" && "$TAR" \
145 --sort=name \
146 --owner=0 \
147 --group=0 \
148 --numeric-owner \
149 --mtime='@0' \
150 --format=ustar \
151 -cf - . ) | gzip --no-name -9 > "$output"
152
153 echo "wrote $output"
154 ( cd "$(dirname "$output")" && sha256sum "$(basename "$output")" )
155