Skip to content

Commit 0fc3a9e

Browse files
committed
ci: add "local CI" script
Who needs Github Actions, anyway? While CI is nice, sometimes you just want to test locally. Add a script which tests locally, in podman, against our fleet of CI containers.
1 parent 7547bc6 commit 0fc3a9e

1 file changed

Lines changed: 259 additions & 0 deletions

File tree

local-ci.sh

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Mimic sameold's container-based CI jobs locally, without github.
4+
# Requires Linux and podman.
5+
6+
REGISTRY="ghcr.io/cbs228/sameold/builder/"
7+
8+
TARGETS=(
9+
x86_64-unknown-linux-gnu
10+
i686-unknown-linux-gnu
11+
armv7-unknown-linux-gnueabihf
12+
aarch64-unknown-linux-gnu
13+
)
14+
15+
CONTAINER_TAG=latest
16+
17+
# podman volumes to be removed on exit
18+
declare -a REMOVE_VOLUMES=()
19+
20+
container_runner() {
21+
# Usage: container_runner
22+
#
23+
# Automatically detect container platform command
24+
25+
if [ -x "${BUILDER:-}" ]; then
26+
echo "${BUILDER}"
27+
elif [ -n "${BUILDER:-}" ]; then
28+
command -v "${BUILDER}"
29+
else
30+
{
31+
command -v podman || \
32+
command -v docker
33+
} 2>/dev/null || {
34+
echo >&2 "FATAL: container platform tools not found"
35+
return 1;
36+
}
37+
fi
38+
}
39+
40+
is_any() {
41+
# Usage: is_any NEEDLE HAYSTACK0 HAYSTACK1 HAYSTACK2 ...
42+
#
43+
# Returns true if any of the haystacks match NEEDLE, exactly.
44+
45+
local match="$1"
46+
shift
47+
local e
48+
for e; do
49+
[[ "$e" == "$match" ]] && return 0;
50+
done
51+
return 1
52+
}
53+
54+
make_volume() {
55+
# Usage: make_temporary_volume NAME TEMPORARY
56+
#
57+
# Create a podman volume with NAME. If TEMPORARY is set, the
58+
# volume will be destroyed when this script exits
59+
60+
local vol
61+
vol="${1?expected NAME}"
62+
63+
local is_temp
64+
is_temp="${2:-}"
65+
66+
if [ -n "${is_temp:-}" ]; then
67+
"$BUILDER" >/dev/null 2>&1 volume rm "$vol" || true
68+
REMOVE_VOLUMES+=("$vol")
69+
fi
70+
71+
"$BUILDER" >/dev/null 2>&1 volume create --ignore "$vol"
72+
}
73+
74+
run() {
75+
# Usage: run CMD ARG0 ARG1 ...
76+
#
77+
# Echo and run
78+
79+
echo >&2 "$@"
80+
exec "$@"
81+
}
82+
83+
run_default_image_rw() {
84+
# Usage: run_default_image_rw CMD ARG1 ARG2
85+
#
86+
# Run the given CMD in the native-architecture image, with
87+
# caches *read/write*. The image has the cargo registry cache
88+
# and the vendored sources volumes mounted read-write.
89+
90+
run "$BUILDER" run --rm \
91+
--init \
92+
--security-opt=label:disable \
93+
--userns=keep-id:uid=1001,gid=1001 \
94+
--workdir /src \
95+
-v .:/src:ro \
96+
-v "sameold-target-$DEFAULT_TARGET":/src/target:rw \
97+
-v sameold-vendored-sources:/src/vendor:rw \
98+
-v sameold-cargo-cache:/cargo:rw \
99+
-v sameold-cargo-config:/src/.cargo:rw \
100+
"$DEFAULT_IMAGE" \
101+
"$@" &
102+
103+
wait -f "$!"
104+
}
105+
106+
run_default_image_ro() {
107+
# Usage: run_default_image_ro CMD ARG1 ARG2
108+
#
109+
# Run the given CMD in the native-architecture image, with
110+
# caches read-only. The image has the vendored sources volume
111+
# mounted read-only and the offline flag set.
112+
113+
run "$BUILDER" run --rm \
114+
--init \
115+
--security-opt=label:disable \
116+
--userns=keep-id:uid=1001,gid=1001 \
117+
--workdir /src \
118+
-e CARGO_NET_OFFLINE=true \
119+
-e RUSTFLAGS='-C strip=symbols' \
120+
-v .:/src:ro \
121+
-v "sameold-target-$DEFAULT_TARGET":/src/target:rw \
122+
-v sameold-vendored-sources:/src/vendor:ro \
123+
-v sameold-cargo-config:/src/.cargo:ro \
124+
"$DEFAULT_IMAGE" \
125+
"$@" &
126+
127+
wait -f "$!"
128+
}
129+
130+
run_all_images() {
131+
# Usage: run_all_images CMD ARG1 ARG2
132+
#
133+
# Runs the given CMD on all images, in parallel with
134+
# GNU parallel. The image has the vendored sources volume
135+
# mounted read-only and the offline flag set.
136+
137+
run parallel -i "$BUILDER" run --rm \
138+
--init \
139+
--security-opt=label:disable \
140+
--userns=keep-id:uid=1001,gid=1001 \
141+
--workdir /src \
142+
-e CARGO_NET_OFFLINE=true \
143+
-e RUSTFLAGS='-C strip=symbols' \
144+
-v .:/src:ro \
145+
-v 'sameold-target-{}:/src/target:rw' \
146+
-v sameold-vendored-sources:/src/vendor:ro \
147+
-v sameold-cargo-config:/src/.cargo:ro \
148+
"${REGISTRY}"'{}'":$CONTAINER_TAG" \
149+
"$@" \
150+
-- \
151+
"${TARGETS[@]}" &
152+
153+
wait -f "$!" || return 1
154+
155+
echo >&2
156+
echo >&2 "All targets passed:"
157+
printf >&2 ' ✔ %s\n' "${TARGETS[@]}"
158+
}
159+
160+
cleanup_exit() {
161+
# Actions executed when the script exits, either error or not
162+
trap '' ERR EXIT
163+
164+
# Kill all background jobs
165+
kill $(jobs -p) 2>/dev/null || true
166+
wait
167+
168+
# remove volumes
169+
local vol
170+
for vol in "${REMOVE_VOLUMES[@]}"; do
171+
[ -n "${vol:-}" ] || continue
172+
"$BUILDER" >/dev/null 2>&1 volume rm "$vol" || true
173+
done
174+
REMOVE_VOLUMES=()
175+
176+
echo >&2 "Cleanup finished."
177+
}
178+
179+
BUILDER="$(container_runner)"
180+
DEFAULT_TARGET="$(uname -m)-unknown-linux-gnu"
181+
182+
# compute full image names
183+
DEFAULT_IMAGE="${REGISTRY}${DEFAULT_TARGET}:${CONTAINER_TAG}"
184+
185+
# git version → source date epoch, if possible
186+
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git log -1 --pretty=%ct || date +%s)}"
187+
188+
# return if sourced
189+
(return 0 2>/dev/null) && return 0
190+
191+
set -euo pipefail
192+
193+
if ! is_any "$DEFAULT_TARGET" "${TARGETS[@]}"; then
194+
echo >&2 "error: expected image \"$DEFAULT_TARGET\" for native architecture,"
195+
echo >&2 "but none exists."
196+
exit 1
197+
fi
198+
199+
# install cleanup hooks
200+
trap cleanup_exit ERR EXIT
201+
202+
# 0. CHECK FOR WHITESPACE ERRORS
203+
git diff --check develop..
204+
205+
# make volume for cargo registry (cached across runs)
206+
make_volume sameold-cargo-cache
207+
208+
# make volume for vendored sources (cached across runs)
209+
make_volume sameold-vendored-sources
210+
211+
# make volume for cargo config (destroyed on exit)
212+
make_volume sameold-cargo-config temporary
213+
214+
# make volumes for built binaries (destroyed on exit, shared)
215+
for target in "${TARGETS[@]}"; do
216+
make_volume sameold-target-"$target" temporary
217+
done
218+
219+
# 1. VENDOR SOURCES
220+
#
221+
# Vendor sources, updating our cargo cache of crates.io
222+
# while we do it. Also audit all dependencies.
223+
224+
{ cargo_vendor=$(cat) ; } <<'SCRIPT'
225+
cargo vendor --versioned-dirs --locked >.cargo/config.toml
226+
SCRIPT
227+
228+
run_default_image_rw bash -c "$cargo_vendor"
229+
230+
231+
# 2. RUN LINTS
232+
#
233+
# Check that the code is cargo-fmt compliant, lints OK, and
234+
# builds documentation without warnings
235+
{ cargo_check=$(cat) ; } <<'SCRIPT'
236+
cargo fmt --check &&
237+
cargo check --frozen --locked --workspace &&
238+
cargo doc --frozen --locked --no-deps --workspace
239+
SCRIPT
240+
241+
run_default_image_ro bash -c "$cargo_check"
242+
243+
244+
# 3. RUN ALL BUILDS on all architectures
245+
#
246+
# Test in debug-mode
247+
# Test in release-mode
248+
# Build and install in release-mode
249+
# Run integration tests
250+
{ cargo_build=$(cat) ; } <<'SCRIPT'
251+
cargo test --offline --frozen --workspace &&
252+
cargo test --offline --frozen --release --workspace &&
253+
cargo install --offline --frozen --path=crates/samedec &&
254+
qemu-run-maybe samedec --version &&
255+
./sample/test.sh qemu-run-maybe samedec &&
256+
echo >&2 "✔ OK $CARGO_BUILD_TARGET"
257+
SCRIPT
258+
259+
run_all_images bash -c "$cargo_build"

0 commit comments

Comments
 (0)