OILS / soil / host-shim.sh View on Github | oils.pub

440 lines, 251 significant
1#!/usr/bin/env bash
2#
3# Shell functions run on the host machine, OUTSIDE the container.
4#
5# Usage:
6# soil/host-shim.sh <function name>
7#
8# Examples:
9# soil/host-shim.sh local-test-uke cpp-spec # run a job
10# soil/host-shim.sh local-test-uke cpp-spec '' T # get shell
11
12set -o nounset
13set -o pipefail
14set -o errexit
15
16REPO_ROOT=$(cd "$(dirname $0)/.."; pwd)
17
18source soil/common.sh
19source test/tsv-lib.sh
20
21readonly REGISTRY='ghcr.io'
22
23live-image-tag() {
24 ### image ID -> Docker tag name
25 local image_id=$1
26
27 # ghcr.io migration
28 echo 'v-2025-12-21'
29
30 return
31
32 #
33 # UNUSED
34 #
35 case $image_id in
36 app-tests)
37 # wedges 2025
38 echo 'v-2025-10-28'
39 ;;
40 wild)
41 # wedges 2025
42 echo 'v-2025-10-28'
43 ;;
44 bloaty)
45 # wedges 2025
46 echo 'v-2025-10-28'
47 ;;
48 benchmarks)
49 # wedges 2025
50 echo 'v-2025-10-28'
51 ;;
52 benchmarks2)
53 # wedges 2025
54 # rebuild cmark, NOT uftrace
55 echo 'v-2025-10-28'
56 ;;
57 cpp-spec)
58 # wedges 2025
59 echo 'v-2025-10-28'
60 ;;
61 pea)
62 # wedges 2025
63 echo 'v-2025-10-28'
64 ;;
65 cpp-small)
66 # wedges 2025
67 echo 'v-2025-10-28'
68 ;;
69 clang)
70 # wedges 2025
71 echo 'v-2025-10-28'
72 ;;
73 ovm-tarball)
74 # wedges 2025
75 echo 'v-2025-10-28'
76 ;;
77 other-tests)
78 # wedges 2025
79 echo 'v-2025-10-28'
80 ;;
81 dev-minimal)
82 # wedges 2025
83 echo 'v-2025-10-28'
84 ;;
85 dummy)
86 # wedges 2025
87 echo 'v-2025-10-28'
88 ;;
89 *)
90 die "Invalid image $image"
91 ;;
92 esac
93}
94
95make-soil-dir() {
96 log-context 'make-soil-dir'
97
98 mkdir --verbose -p _tmp/soil
99 ls -l -d . _tmp _tmp/soil
100
101 # Match what mount-perms does
102 chmod --changes 777 _tmp _tmp/soil
103 ls -l -d . _tmp _tmp/soil
104}
105
106show-disk-info() {
107 # Debug 'no space left on device' issue
108 echo 'DISKS'
109 df -h
110 echo
111
112 # Useful but many permissions errors
113 if false; then
114 echo 'SPACE FOR IMAGES?'
115 du --si -s ~/.local/share/ || true
116 echo
117 fi
118}
119
120podman-prune() {
121 ### Should this work on Debian?
122
123 if ! command -v podman; then
124 echo 'no podman'
125 return
126 fi
127
128 echo 'IMAGES'
129 podman images --all
130 echo
131
132 if false; then
133 # This causes an interactive prompt
134 echo 'PRUNE'
135 podman system prune || true
136 echo
137
138 show-disk-info
139
140 echo 'PRUNE AS ROOT'
141 sudo podman system prune || true
142 echo
143
144 show-disk-info
145 fi
146}
147
148mount-perms() {
149 ### Ensure that the guest can write to bind mount
150
151 local repo_root=$1
152
153 #show-disk-info
154
155 log-context 'mount-perms'
156
157 # We have to chmod all dirs because 'build/py.sh all' creates
158 # build/temp.linux-*, for example. Also can't exclude .git/ because
159 # submodules need it.
160 time find "$repo_root" -type d -a -print \
161 | xargs -d $'\n' -- chmod --changes 777 \
162 | wc -l
163 echo
164}
165
166job-reset() {
167 ### Called between jobs
168
169 #show-disk-info
170
171 log-context 'job-reset'
172
173 # The VM runs as the 'build' user on sourcehut. The podman container runs as
174 # 'uke' user, which apparently gets UID 100999.
175 #
176 # Running as 'build', we can't remove files created by the guest, so use
177 # 'sudo'.
178 #
179 # It's really these three dirs.
180 # ls -l -d _tmp/soil _tmp/soil/logs _devbuild/bin || true
181
182 sudo $0 mount-perms $PWD
183 echo
184
185 git status .
186 echo
187
188 # Similar to functions in 'build/clean.sh'
189 local -a dirs=(_tmp _bin _build _devbuild _test)
190 #local -a dirs=(_tmp)
191
192 log 'Removing temp dirs'
193 log ''
194
195 du --si -s "${dirs[@]}" || true
196 rm -r -f "${dirs[@]}"
197 echo
198
199 show-disk-info
200}
201
202image-layers-tsv() {
203 local docker=${1:-podman}
204 local image=${2:-oils-for-unix/soil-dummy}
205 local tag=${3:-latest}
206
207 # --human=0 gives us raw bytes and ISO timestamps
208 # --no-trunc shows the full command line
209
210 # Removed .CreatedAt because of this issue
211 # https://github.com/containers/podman/issues/17738
212 #echo $'num_bytes\tcreated_at\tcreated_by'
213
214 echo $'num_bytes\tcreated_by'
215
216 $docker history \
217 --no-trunc --human=0 \
218 --format json \
219 $REGISTRY/$image:$tag | python3 -c '
220import sys, json
221array = json.load(sys.stdin)
222for row in array:
223 print("%s\t%s" % (row["size"], row["CreatedBy"]))
224'
225}
226
227save-image-stats() {
228 local soil_dir=${1:-_tmp/soil}
229 local docker=${2:-podman}
230 local image=${3:-oils-for-unix/soil-dummy}
231 local tag=${4:-latest}
232
233 log "save-image-stats with $(which $docker)"
234 $docker --version
235 echo
236
237 mkdir -p $soil_dir
238
239 # NOTE: Works on my dev machine, but produces an empty table on CI?
240 $docker images "$REGISTRY/$image:v-*" > $soil_dir/images-tagged.txt
241 log "Wrote $soil_dir/images-tagged.txt"
242
243 $docker history $REGISTRY/$image:$tag > $soil_dir/image-layers.txt
244 log "Wrote $soil_dir/image-layers.txt"
245
246 image-layers-tsv $docker $image $tag > $soil_dir/image-layers.tsv
247 log "Wrote $soil_dir/image-layers.tsv"
248
249 # TODO: sum into image-layers.json
250 # - total size
251 # - earliest and layer date?
252
253 here-schema-tsv >$soil_dir/image-layers.schema.tsv <<EOF
254column_name type
255num_bytes integer
256created_at string
257created_by string
258EOF
259
260 log "Wrote $soil_dir/image-layers.schema.tsv"
261}
262
263# for both ASAN/LeakSanitizer and GDB
264readonly PTRACE_FLAGS=( --cap-add=SYS_PTRACE --security-opt seccomp=unconfined )
265
266run-job-uke() {
267 local docker=$1 # docker or podman
268 local repo_root=$2
269 local job_name=$3 # e.g. dev-minimal
270 local debug_shell=${4:-}
271
272 log-context 'run-job-uke'
273
274 # Do this on the HOST because we write the pull time into it as well. It's
275 # shared between guest and host.
276 make-soil-dir
277 local soil_dir=$repo_root/_tmp/soil
278
279 local image_id=$job_name
280 # Some jobs don't have their own image, and some need docker -t
281 case $job_name in
282 app-tests)
283 ;;
284 cpp-coverage)
285 image_id='clang'
286 ;;
287 cpp-tarball)
288 image_id='cpp-small'
289 ;;
290 benchmarks3)
291 # for test/syscall
292 #image_id='ovm-tarball'
293
294 # TODO: use wait-for-tarball
295 #image_id='benchmarks2'
296
297 # Ninja build
298 image_id='benchmarks'
299 ;;
300 interactive)
301 # Reuse for now
302 image_id='benchmarks'
303 ;;
304 esac
305
306 local -a flags=()
307 case $job_name in
308 app-tests|interactive)
309 # app-tests: to run ble.sh tests
310 # interactive: to run 'interactive-osh' with job control enabled
311 flags+=( -t )
312 ;;
313 esac
314
315 case $job_name in
316 cpp-small|cpp-spec|cpp-tarball|ovm-tarball)
317 # podman requires an additional flag for ASAN, so it can use ptrace()
318 # Otherwise we get:
319 # ==1194==LeakSanitizer has encountered a fatal error.
320 # ==1194==HINT: LeakSanitizer does not work under ptrace (strace, gdb, etc)
321 #
322 # Note: somehow this isn't necessary locally: on podamn 4.3.1 on Debian 12
323 if test $docker = podman; then
324 flags+=( "${PTRACE_FLAGS[@]}" )
325 fi
326 esac
327
328 local image="oils-for-unix/soil-$image_id"
329
330 local tag=$(live-image-tag $image_id)
331
332 local pull_status
333 # Use external time command in POSIX format, so it's consistent between hosts
334 set -o errexit
335 command time -p -o $soil_dir/image-pull-time.txt \
336 $docker pull $REGISTRY/$image:$tag
337 pull_status=$?
338 set +o errexit
339
340 if test $pull_status -ne 0; then
341 log "$docker pull failed with status $pull_status"
342
343 # Save status for a check later
344 mkdir -p _soil-jobs
345 echo "$pull_status" > _soil-jobs/$job_name.status.txt
346
347 # Return success
348 return
349 fi
350
351 save-image-stats $soil_dir $docker $image $tag
352
353 show-disk-info
354
355 podman-prune
356
357 local -a args
358 if test -n "$debug_shell"; then
359 # launch interactive shell
360 flags+=( -i -t )
361
362 # So we can run GDB
363 # https://stackoverflow.com/questions/35860527/warning-error-disabling-address-space-randomization-operation-not-permitted
364 flags+=( "${PTRACE_FLAGS[@]}" )
365
366 args=(bash)
367 else
368 args=(sh -c "cd /home/uke/oil; soil/worker.sh JOB-$job_name")
369 fi
370
371 set -x
372 $docker run "${flags[@]}" \
373 --mount "type=bind,source=$repo_root,target=/home/uke/oil" \
374 $REGISTRY/$image:$tag \
375 "${args[@]}"
376 set +x
377}
378
379did-all-succeed() {
380 ### Check if the given jobs succeeded
381
382 local max_status=0
383 for job_name in "$@"; do
384 local status
385 read status unused_job_id < "_soil-jobs/$job_name.status.txt"
386
387 echo "$job_name status: $status"
388 if test $status -gt $max_status; then
389 max_status=$status
390 fi
391 done
392
393 log ''
394 log "Exiting with max job status $max_status"
395
396 return "$max_status"
397}
398
399local-test-uke() {
400 ### Something I can run locally. This is fast.
401
402 # Simulate sourcehut with 'local-test-uke dummy dummy'
403 local job_name=${1:-dummy}
404 local job2=${2:-}
405 local debug_shell=${3:-} # add 'bash' to change it to a debug shell
406 local docker=${4:-podman}
407
408 local branch=$(git rev-parse --abbrev-ref HEAD)
409
410 local fresh_clone=/tmp/soil-$job_name
411 rm -r -f -v $fresh_clone
412
413 local this_repo=$PWD
414 git clone $this_repo $fresh_clone
415 cd $fresh_clone
416 git checkout $branch
417
418 sudo $0 mount-perms $fresh_clone
419 sudo $0 run-job-uke "$docker" $fresh_clone $job_name "$debug_shell"
420
421 # Run another job in the same container, to test interactions
422
423 if test -n "$job2"; then
424 $0 job-reset
425 sudo $0 run-job-uke "$docker" $fresh_clone $job2
426 fi
427}
428
429local-shell() {
430 local job_name=${1:-cpp}
431
432 # no job 2
433 local-test-uke $job_name '' bash
434}
435
436cleanup() {
437 sudo rm -r -f -v _tmp/soil /tmp/soil-*
438}
439
440"$@"