Repository navigation
Expand file tree
/
Copy pathdevtools
More file actions
executable file
·835 lines (719 loc) · 28.4 KB
/
Copy pathdevtools
File metadata and controls
executable file
·835 lines (719 loc) · 28.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
#!/usr/bin/env bash
set -euo pipefail
print_usage() {
cat <<EOF
devtools — manage the BTC Map Android dev environment.
Usage:
./devtools emulator start Boot the AVD asynchronously
./devtools app run Run the app (desktop by default, under Compose Hot Reload;
--android for the emulator, --cold for a plain desktop run)
./devtools app deploy-beta Build and deploy beta packages for the named platforms
./devtools app prepare-release Build the release binaries for every supported platform
./devtools app bundle-data Download latest places, areas, comments and events snapshots
./devtools app bundle-map-styles Bundle MapLibre styles, sprites and glyphs (--force re-downloads)
./devtools app bundle-icons Extract the icon-name list from the bundled font
./devtools website deploy Build the Hugo site and rsync it to android.btcmap.org
EOF
}
err() {
echo "Error: $*" >&2
}
print_usage_emulator() {
cat <<EOF
Usage: ./devtools emulator <command>
Commands:
start Boot the AVD asynchronously
EOF
}
print_usage_app() {
cat <<EOF
Usage: ./devtools app <command>
Commands:
run Run the app. Runs the desktop app by default, or the
Android build on the emulator with --android. The desktop
app runs under Compose Hot Reload, so code changes reach
the running window without a restart; --cold starts a
plain run instead. Remaining arguments go to Gradle, e.g.
--no-auto to reload on demand instead of on every save.
deploy-beta Build and deploy beta packages for the named platforms:
--android (the APKs and update manifest), --linux (the
AppImage) and --windows / --macos (the Conveyor packages).
At least one platform is required.
prepare-release Build the release binaries for every supported platform
and stage them in the repository's release/ directory
bundle-data Download latest places, areas, comments and events
snapshots (always refreshes)
bundle-map-styles Bundle MapLibre styles, sprites and glyphs (--force
re-downloads)
bundle-icons Extract the icon-name list from the bundled Material
Symbols font
EOF
}
print_usage_website() {
cat <<EOF
Usage: ./devtools website <command>
Commands:
deploy Build the Hugo site and rsync it to btcmap-api:/srv/http/android.btcmap.org
EOF
}
# START emulator section
emulator_avd="btcmap"
emulator_unit="btcmap-emulator"
cmd_emulator_start() {
# Validate the AVD here: systemd-run reports success even when the emulator
# exits immediately, so an unknown name would fail out of sight in the
# journal. `grep -x` keeps `btcmap` from matching `btcmap-old`.
if ! emulator -list-avds | grep -qx "$emulator_avd"; then
err "no emulator configured: AVD $emulator_avd does not exist (list them with: emulator -list-avds)"
return 1
fi
# systemd owns the emulator: it outlives this shell, and stopping the unit
# shuts it down through SIGTERM (which saves the quickboot snapshot), with its
# output going to the journal instead of a file.
if systemctl --user is-active --quiet "$emulator_unit"; then
echo "The emulator is already running."
echo "Stop it with: systemctl --user stop $emulator_unit"
return 0
fi
systemd-run --user --unit="$emulator_unit" --collect emulator -avd "$emulator_avd" -gpu host
echo "Emulator output: journalctl --user -u $emulator_unit"
}
cmd_emulator() {
local sub="${1:-help}"
shift || true
case "$sub" in
start) cmd_emulator_start ;;
help|-h|--help) print_usage_emulator ;;
*) err "Unknown emulator command: $sub"; print_usage_emulator; return 1 ;;
esac
}
# END emulator section
# START signing section
android_sdk_dir() {
if [ -n "${ANDROID_HOME:-}" ]; then
echo "$ANDROID_HOME"
return 0
fi
if [ -n "${ANDROID_SDK_ROOT:-}" ]; then
echo "$ANDROID_SDK_ROOT"
return 0
fi
local sdk_dir
sdk_dir="$(sed -n 's/^sdk\.dir=//p' local.properties 2>/dev/null | head -n1)"
if [ -n "$sdk_dir" ]; then
echo "$sdk_dir"
return 0
fi
return 1
}
find_apksigner() {
local sdk_dir
sdk_dir="$(android_sdk_dir)" || return 1
local build_tools
build_tools="$(ls -d "$sdk_dir"/build-tools/*/ 2>/dev/null | sort -V | tail -n1)"
[ -n "$build_tools" ] && [ -x "${build_tools}apksigner" ] && echo "${build_tools}apksigner"
}
assert_apk_signed() {
local apk="$1"
if [ ! -f "$apk" ]; then
echo "Error: APK not found at $apk" >&2
return 1
fi
local apksigner
if ! apksigner="$(find_apksigner)"; then
echo "Error: could not locate apksigner; refusing to deploy an unsigned APK." >&2
return 1
fi
if ! "$apksigner" verify --print-certs "$apk" >/dev/null 2>&1; then
echo "Error: $apk is not signed. Add the release keystore properties to local.properties." >&2
return 1
fi
echo "$apk is signed."
}
# END signing section
# START website section
cmd_website_deploy() {
if ! command -v hugo >/dev/null 2>&1; then
err "hugo is not installed; install Hugo to build the site."
return 1
fi
local dest="btcmap-api:/srv/http/android.btcmap.org/"
# Hugo does not prune its output, so clear it to avoid shipping stale pages
# from earlier builds.
echo "Building the Hugo site..."
rm -rf website/public
(cd website && hugo)
echo "Deploying to $dest..."
rsync -avz --delete --chmod=D755,F644 website/public/ "$dest"
}
cmd_website() {
local target="${1:-help}"
shift || true
case "$target" in
deploy) cmd_website_deploy "$@" ;;
help|-h|--help) print_usage_website ;;
*) err "Unknown website command: $target"; print_usage_website; return 1 ;;
esac
}
# END website section
# START cdn section
# BunnyCDN API key for the static.btcmap.org pull zone. Set BUNNY_API_KEY in
# the environment or add `bunny.api.key=...` to local.properties.
bunny_api_key() {
if [ -n "${BUNNY_API_KEY:-}" ]; then
printf '%s' "$BUNNY_API_KEY"
return 0
fi
local key
key="$(sed -n 's/^bunny\.api\.key=//p' local.properties 2>/dev/null | head -n1)"
if [ -n "$key" ]; then
printf '%s' "$key"
return 0
fi
return 1
}
assert_bunny_api_key() {
if ! bunny_api_key >/dev/null; then
err "no BunnyCDN API key; refusing to deploy without purging the CDN cache."
err "Set BUNNY_API_KEY in the environment or add 'bunny.api.key=...' to local.properties."
return 1
fi
}
# Purge absolute CDN URLs from the Bunny edge cache, waiting for the purge to
# complete so a request right after the deploy cannot hit a stale object.
purge_cdn() {
local key
key="$(bunny_api_key)" || return 1
local url
for url in "$@"; do
echo "Purging $url from the CDN..."
curl -fsS -X POST -G \
--data-urlencode "url=$url" \
--data-urlencode "async=false" \
-H "AccessKey: $key" \
"https://api.bunny.net/purge" >/dev/null
done
}
# END cdn section
# START appimage section
# appimagetool only publishes a moving "continuous" build, so the URL can be
# overridden to pin one. A ready-made binary can also be pointed at directly
# with APPIMAGETOOL=/path/to/appimagetool.
appimagetool_url_default="https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
# Downloads and extracts appimagetool into a cache on first use, then prints the
# path to a runnable appimagetool. The extracted form is used so packing does
# not need a FUSE mount of the tool itself.
prepare_appimagetool() {
if [ -n "${APPIMAGETOOL:-}" ]; then
printf '%s' "$APPIMAGETOOL"
return 0
fi
local dir="${XDG_CACHE_HOME:-$HOME/.cache}/btcmap/appimagetool"
local apprun="$dir/squashfs-root/AppRun"
if [ -x "$apprun" ]; then
printf '%s' "$apprun"
return 0
fi
local url="${APPIMAGETOOL_URL:-$appimagetool_url_default}"
echo "Downloading appimagetool from $url..." >&2
mkdir -p "$dir"
local image="$dir/appimagetool-x86_64.AppImage"
curl -fsSL -o "$image" "$url"
chmod +x "$image"
rm -rf "$dir/squashfs-root"
(cd "$dir" && "$image" --appimage-extract >/dev/null)
if [ ! -x "$apprun" ]; then
err "could not extract appimagetool"
return 1
fi
printf '%s' "$apprun"
}
# Wraps the jpackage app image the Compose plugin produces (built by the
# createDistributable task) in an AppDir and packs it into a single-file
# AppImage at $1. The app image already carries its own runtime, so the result
# needs no Java on the target machine.
build_desktop_appimage() {
local out="$1"
local app_dir="desktopApp/build/compose/binaries/main/app/BTC Map"
if [ ! -x "$app_dir/bin/BTC Map" ]; then
err "desktop app image not found at $app_dir; run :desktopApp:createDistributable first."
return 1
fi
local out_dir appdir
out_dir="$(dirname "$out")"
appdir="$out_dir/BTC-Map.AppDir"
rm -rf "$out_dir"
mkdir -p "$appdir/usr"
cp -a "$app_dir/." "$appdir/usr/"
# The jpackage launcher has a space in its name; an alias without one is
# friendlier for the desktop entry (Exec=/Icon=) and for a PATH install.
cat > "$appdir/usr/bin/btc-map" <<'RUN'
#!/bin/sh
HERE="$(dirname "$(readlink -f "$0")")"
exec "$HERE/BTC Map" "$@"
RUN
chmod +x "$appdir/usr/bin/btc-map"
cat > "$appdir/AppRun" <<'RUN'
#!/bin/sh
HERE="$(dirname "$(readlink -f "$0")")"
exec "$HERE/usr/bin/btc-map" "$@"
RUN
chmod +x "$appdir/AppRun"
cat > "$appdir/btc-map.desktop" <<'DESKTOP'
[Desktop Entry]
Type=Application
Name=BTC Map
Comment=Map of Bitcoin merchants
Exec=btc-map
Icon=btc-map
Categories=Finance;Network;
Terminal=false
DESKTOP
cp fastlane/metadata/android/en-US/images/icon.png "$appdir/btc-map.png"
local appimagetool
appimagetool="$(prepare_appimagetool)"
echo "Packing $out..."
ARCH=x86_64 "$appimagetool" "$appdir" "$out"
}
# END appimage section
# START conveyor section
# Every desktop artifact lives under one directory on the CDN, one per channel:
# the Windows and macOS packages Conveyor builds and the Linux AppImage.
desktop_cdn_base="https://cdn.static.btcmap.org/desktop"
desktop_dest="btcmap-api:/srv/http/static.btcmap.org/desktop"
# Conveyor builds the Windows (MSIX, plus an installer .exe and a portable .zip)
# and macOS (a signed, Sparkle-updating .app archive) packages from this Linux
# machine. That is the whole reason it is here: jpackage, which the Compose
# plugin wraps, cannot cross-compile and silently skips the foreign-OS formats,
# so without Conveyor the desktop app could only ever ship as a Linux AppImage.
#
# It is a ~111 MB tool, cached under ${XDG_CACHE_HOME}/btcmap/conveyor on first
# use. Point CONVEYOR at a binary to use an install of your own instead, or
# CONVEYOR_URL at a different download.
conveyor_version_default="22.3"
conveyor_url_default="https://downloads.hydraulic.dev/conveyor/conveyor-${conveyor_version_default}-linux-amd64.tar.gz"
# Downloads and unpacks Conveyor into a cache on first use, then prints the path
# to its launcher.
prepare_conveyor() {
if [ -n "${CONVEYOR:-}" ]; then
printf '%s' "$CONVEYOR"
return 0
fi
local dir="${XDG_CACHE_HOME:-$HOME/.cache}/btcmap/conveyor"
local bin="$dir/conveyor-${conveyor_version_default}/bin/conveyor"
if [ -x "$bin" ]; then
printf '%s' "$bin"
return 0
fi
local url="${CONVEYOR_URL:-$conveyor_url_default}"
echo "Downloading Conveyor from $url..." >&2
mkdir -p "$dir"
local tarball="$dir/conveyor.tar.gz"
curl -fsSL -o "$tarball" "$url"
tar xzf "$tarball" -C "$dir"
rm -f "$tarball"
if [ ! -x "$bin" ]; then
err "could not unpack Conveyor"
return 1
fi
printf '%s' "$bin"
}
# The Conveyor signing root key: the one key every Windows and macOS package
# key, update feed and package identity is derived from. It is signing material,
# so it is kept beside the Android keystore and outside the repository, and
# local.properties points at it, exactly as it points at the keystore itself.
# Losing or rotating it breaks updates for every copy already installed and
# looks like a different app to Windows, so it is never created anywhere else.
conveyor_signing_key() {
sed -n 's/^conveyor\.signing\.key=//p' local.properties 2>/dev/null | head -n1
}
# Uses the root key at the configured path, generating it there on first use. If
# it has a passphrase (conveyor keys passphrase), export it as
# CONVEYOR_PASSPHRASE so the builds can unlock it.
ensure_conveyor_key() {
local conveyor="$1"
local key
key="$(conveyor_signing_key)"
if [ -z "$key" ]; then
err "no 'conveyor.signing.key' in local.properties; refusing to deploy."
err "It must point at the Conveyor signing root key file, which belongs beside the Android keystore, outside the repository."
return 1
fi
if [ "$(basename "$key")" != "defaults.conf" ]; then
err "conveyor.signing.key must name the file defaults.conf, the name Conveyor writes the root key to (got $(basename "$key"))."
return 1
fi
if [ -f "$key" ]; then
return 0
fi
echo "Generating the Conveyor signing root key (once) at $key..." >&2
mkdir -p "$(dirname "$key")"
"$conveyor" --agree-to-license=1 keys generate --no-passphrase -o "$(dirname "$key")"
cat >&2 <<EOF
Back this file up now: $key
Every Windows and macOS package key, update feed and package identity comes from
it. Without it, every copy already installed can never be updated again.
EOF
}
# Builds the Windows and macOS packages and their update site into $3 for the
# channel in $2 (beta or release), using the Conveyor in $1. $4 optionally
# narrows the build to a subset of the machines, as a HOCON list (e.g.
# [windows.amd64]); empty builds every machine the config names. The channel is
# the last path segment of the update URL baked into the packages, so installed
# copies check only their own channel; changing it later strands the copies
# already out there.
build_desktop_packages() {
local conveyor="$1"
local channel="$2"
local out="$3"
local machines="${4:-}"
rm -rf "$out"
# A debuggable build when asked for: the same packages, but with the JVM
# attach mechanism re-enabled (a signed launcher disables it, which is why a
# running Windows build cannot be inspected), a console, and a JDWP port. See
# conveyor-debug.conf. Traffic and the published channel are unchanged.
local conf="desktopApp/conveyor.conf"
if [ -n "${CONVEYOR_DEBUG:-}" ]; then
conf="desktopApp/conveyor-debug.conf"
echo "Building a DEBUG build of the desktop packages (attach enabled, console, JDWP on 127.0.0.1:5005)." >&2
fi
local args=("--agree-to-license=1")
# Conveyor will not prompt for a passphrase without a terminal, so an
# encrypted root key has to be unlocked through the environment.
if [ -n "${CONVEYOR_PASSPHRASE:-}" ]; then
args+=("--passphrase=env:CONVEYOR_PASSPHRASE")
fi
args+=("-f" "$conf" "-Kapp.site.base-url=$desktop_cdn_base/$channel")
if [ -n "$machines" ]; then
args+=("-Kapp.machines=$machines")
fi
echo "Building the desktop packages for the $channel channel..."
"$conveyor" "${args[@]}" make site --output-dir "$out"
}
# Purges every file the packaged site contains from the CDN. The names embed the
# version and are stable only across a rebuild of the same version, so they are
# read from the output directory rather than listed by hand.
purge_conveyor_site() {
local channel="$1"
local dir="$2"
local urls=()
local file
while IFS= read -r file; do
urls+=("$desktop_cdn_base/$channel/${file#"$dir"/}")
done < <(find "$dir" -type f | sort)
purge_cdn "${urls[@]}"
}
# END conveyor section
# START app section
# Downloads the latest places, areas, comments and events snapshots. The
# snapshots are committed as assets and refreshed manually; see AGENTS.md,
# "Bundled Assets".
cmd_app_bundle_data() {
python3 bundle_places.py "$@"
python3 bundle_areas.py "$@"
python3 bundle_comments.py "$@"
python3 bundle_events.py "$@"
}
# Bundles the MapLibre styles, sprites and glyphs.
cmd_app_bundle_map_styles() {
python3 bundle_map_styles.py "$@"
}
# Extracts the icon-name list from the bundled Material Symbols font into the
# generated `MaterialSymbolNames.kt`.
cmd_app_bundle_icons() {
python3 bundle_icons.py "$@"
}
cmd_app_run_android() {
local device="emulator-5554"
local package="org.btcmap.debug"
local apk_path="./app/build/outputs/apk/debug/app-x86_64-debug.apk"
if ! adb devices | grep -q "^$device[[:space:]]"; then
echo "Error: emulator $device is not running. Start it with: ./devtools emulator start" >&2
return 1
fi
echo "Assembling debug APK..."
./gradlew assembleDebug "$@"
if [ ! -f "$apk_path" ]; then
echo "Error: APK not found at $apk_path" >&2
return 1
fi
echo "Installing $apk_path on $device..."
adb -s "$device" install -r -t "$apk_path"
echo "Launching $package..."
adb -s "$device" shell monkey -p "$package" -c android.intent.category.LAUNCHER 1
echo "Process status:"
adb -s "$device" shell ps -A | grep btcmap || echo "(process not visible yet — give it a moment)"
}
cmd_app_run_desktop() {
local log="/tmp/btcmap-desktop.log"
# Two instances would open the same database under $BTCMAP_HOME, so refuse to
# start a second one.
if pgrep -f "org.btcmap.desktop.MainK[t]" >/dev/null 2>&1; then
echo "The desktop app is already running; not starting a second instance."
echo "Stop it with: pkill -f 'org.btcmap.desktop.MainK[t]'"
return 0
fi
local task=":desktopApp:hotRun"
# --auto watches the sources and reloads the running window whenever one
# changes, which is the point of running under hot reload here. It keeps the
# Gradle build alive between reloads; --no-auto (forwarded to Gradle) turns
# that off and leaves the reload to `./gradlew :desktopApp:reload`.
local args=(--auto)
if [ -z "$desktop_hot" ]; then
task=":desktopApp:run"
args=()
echo "Launching the desktop app (logs: $log)..."
else
# Hot reload runs the app on the JetBrains Runtime, not on the toolchain the
# plain run uses: only the JBR can redefine the classes it has already
# loaded. The toolchain resolver provisions it into ~/.gradle/jdks (a 560 MB
# download, once) on the first hot run.
echo "Launching the desktop app with Compose Hot Reload (logs: $log)..."
echo "Saving a source file reloads the window; the dev-tools window has a Reload UI button."
fi
# Detached: the task blocks for the life of the window, so control returns
# here. It keeps the app's output (and, under hot reload, each reload's) in
# $log, because `hotRun` runs the app attached.
(./gradlew "$task" "${args[@]}" "$@" > "$log" 2>&1 &)
}
# Chooses the app target from optional --android/--desktop flags (desktop by
# default), leaving the remaining arguments (forwarded to Gradle) in
# app_target_args. The desktop app runs under Compose Hot Reload unless --cold
# is given, because reloading the running window is how the desktop app is
# iterated on; the Android target has no equivalent and always runs plainly.
# Prints the app usage and returns 1 when asked for help.
app_target="desktop"
app_target_args=()
desktop_hot=1
parse_app_target() {
app_target="desktop"
app_target_args=()
desktop_hot=1
local arg
for arg in "$@"; do
case "$arg" in
--android) app_target="android" ;;
--desktop) app_target="desktop" ;;
--cold|--no-hot) desktop_hot="" ;;
help|-h|--help) print_usage_app; return 1 ;;
*) app_target_args+=("$arg") ;;
esac
done
}
cmd_app_run() {
parse_app_target "$@" || return 0
case "$app_target" in
android) cmd_app_run_android "${app_target_args[@]}" ;;
desktop) cmd_app_run_desktop "${app_target_args[@]}" ;;
esac
}
# The platforms `deploy-beta` can publish, each selectable on its own: the
# Android APKs and update manifest (--android), the Linux AppImage (--linux) and
# the packages Conveyor builds for Windows (--windows) and Apple Silicon macOS
# (--macos). At least one has to be named; naming several narrows neither the
# build nor the upload, it just does each.
deploy_android=""
deploy_linux=""
deploy_windows=""
deploy_macos=""
parse_deploy_platforms() {
deploy_android=""
deploy_linux=""
deploy_windows=""
deploy_macos=""
local arg
for arg in "$@"; do
case "$arg" in
--android) deploy_android=1 ;;
--linux) deploy_linux=1 ;;
--windows) deploy_windows=1 ;;
--macos) deploy_macos=1 ;;
help|-h|--help) print_usage_app; return 1 ;;
*) err "Unknown deploy-beta parameter: $arg"; print_usage_app; return 1 ;;
esac
done
if [ -z "$deploy_android$deploy_linux$deploy_windows$deploy_macos" ]; then
err "deploy-beta needs at least one platform: --android, --linux, --windows or --macos."
print_usage_app
return 1
fi
}
cmd_app_deploy_beta() {
parse_deploy_platforms "$@" || return 1
assert_bunny_api_key
# Windows and macOS are one Conveyor build, so either flag means that build.
local desktop=""
if [ -n "$deploy_windows$deploy_macos" ]; then
desktop=1
fi
# The Conveyor signing root key has to exist before Gradle runs: the generated
# config includes it, so writeConveyorConfig embeds it.
local conveyor=""
if [ -n "$desktop" ]; then
conveyor="$(prepare_conveyor)"
ensure_conveyor_key "$conveyor"
fi
# Assemble only what the selected platforms need. The APKs need the beta build
# type; the Linux AppImage comes from the jpackage app image; the Windows and
# macOS packages share the ordinary jars and the generated Conveyor config.
local gradle_tasks=()
if [ -n "$deploy_android" ]; then
gradle_tasks+=(assembleBeta)
fi
if [ -n "$deploy_linux" ]; then
gradle_tasks+=(:desktopApp:createDistributable)
fi
if [ -n "$desktop" ]; then
gradle_tasks+=(:shared:jvmJar :ui:jvmJar :desktopApp:jar :desktopApp:writeConveyorConfig)
fi
echo "Assembling: ${gradle_tasks[*]}"
# The beta build is stamped with its build time, so a rebuild at the same
# version code is still newer than the build an installed copy is running. The
# manifest written below carries the same stamp, read back from the APK's
# BuildConfig, so the two agree.
if [ -n "$deploy_android" ]; then
local build_time
build_time="$(date +%s%3N)"
./gradlew "${gradle_tasks[@]}" -PbtcmapBuildTime="$build_time"
else
./gradlew "${gradle_tasks[@]}"
fi
local appimage_out="desktopApp/build/compose/appimage/BTC-Map-x86_64.AppImage"
local packages_out="desktopApp/build/conveyor/beta"
local beta_dest="$desktop_dest/beta/"
if [ -n "$deploy_android" ]; then
local apk_dir="app/build/outputs/apk/beta"
local dest="btcmap-api:/srv/http/static.btcmap.org/android/apk/"
local manifest_dest="btcmap-api:/srv/http/static.btcmap.org/android/latest-app-beta-ver.json"
local build_config="app/build/generated/source/buildConfig/beta/org/btcmap/BuildConfig.java"
assert_apk_signed "$apk_dir/app-arm64-v8a-beta.apk"
assert_apk_signed "$apk_dir/app-universal-beta.apk"
local version_code version_name build_time_value
version_code="$(sed -n 's/.*VERSION_CODE = \([0-9]*\);.*/\1/p' "$build_config" | head -n1)"
version_name="$(sed -n 's/.*VERSION_NAME = "\(.*\)";.*/\1/p' "$build_config" | head -n1)"
build_time_value="$(sed -n 's/.*BUILD_TIME = \([0-9]*\)L;.*/\1/p' "$build_config" | head -n1)"
if [ -z "$version_code" ] || [ -z "$version_name" ] || [ -z "$build_time_value" ]; then
echo "Error: could not read version info from $build_config" >&2
return 1
fi
local manifest
manifest="$(mktemp)"
cat > "$manifest" <<JSON
{
"code": $version_code,
"name": "$version_name",
"buildTime": $build_time_value,
"url": "https://cdn.static.btcmap.org/android/apk/beta.apk"
}
JSON
chmod 644 "$manifest"
rsync -avz --progress "$apk_dir"/app-arm64-v8a-beta.apk "$dest/beta.apk"
rsync -avz --progress "$apk_dir"/app-universal-beta.apk "$dest/beta-universal.apk"
rsync -avz --progress --chmod=F644 "$manifest" "$manifest_dest"
rm -f "$manifest"
# The APKs and the manifest are overwritten in place, so the pull zone would
# keep serving its cached copy for the full max-age window unless we purge
# it.
purge_cdn \
"https://cdn.static.btcmap.org/android/apk/beta.apk" \
"https://cdn.static.btcmap.org/android/apk/beta-universal.apk" \
"https://cdn.static.btcmap.org/android/latest-app-beta-ver.json"
fi
if [ -n "$deploy_linux" ]; then
build_desktop_appimage "$appimage_out"
# Overwritten in place, so purge the cached copy as above.
rsync -avz --progress --mkpath --chmod=F755 "$appimage_out" "$beta_dest/BTC-Map-x86_64.AppImage"
purge_cdn "$desktop_cdn_base/beta/BTC-Map-x86_64.AppImage"
fi
if [ -n "$desktop" ]; then
# Conveyor builds both machines by default; narrow it to the ones asked for
# so a Windows- or macOS-only deploy does not build and upload the other.
local machines=""
if [ -n "$deploy_windows" ] && [ -n "$deploy_macos" ]; then
machines="[windows.amd64,mac.aarch64]"
elif [ -n "$deploy_windows" ]; then
machines="[windows.amd64]"
else
machines="[mac.aarch64]"
fi
build_desktop_packages "$conveyor" beta "$packages_out" "$machines"
# No --delete on the site: Conveyor keeps each version's package online so it
# can generate deltas from it for the next one, so the older files are meant
# to stay. Its own versioned file names never collide.
rsync -avz --progress --mkpath --chmod=D755,F644 "$packages_out"/ "$beta_dest"
# The part of the site whose names do not carry a version (the update
# metadata, the download page, the installer and the update checker) is
# overwritten in place, so the pull zone would keep serving its cached copy
# for the full max-age window unless we purge it.
purge_conveyor_site beta "$packages_out"
fi
}
# Builds the release binaries for every supported platform — the Android APKs,
# the Linux AppImage and the Windows and macOS packages — and stages them in a
# `release/` directory in the repository root. Nothing is uploaded; this only
# prepares the files.
cmd_app_prepare_release() {
# Resolve the signing key before Gradle runs: the generated Conveyor config
# includes it, so it has to exist by the time writeConveyorConfig writes that.
local conveyor
conveyor="$(prepare_conveyor)"
ensure_conveyor_key "$conveyor"
# The desktop app is a single build every channel shares, so its packages come
# from the ordinary jar. The generated Conveyor config has to be written before
# Conveyor reads it.
./gradlew assembleRelease :desktopApp:createDistributable \
:shared:jvmJar :ui:jvmJar :desktopApp:jar :desktopApp:writeConveyorConfig "$@"
local apk_dir="app/build/outputs/apk/release"
local packages_out="desktopApp/build/conveyor/release"
local build_config="app/build/generated/source/buildConfig/release/org/btcmap/BuildConfig.java"
assert_apk_signed "$apk_dir/app-arm64-v8a-release.apk"
assert_apk_signed "$apk_dir/app-universal-release.apk"
local version_name
version_name="$(sed -n 's/.*VERSION_NAME = "\(.*\)";.*/\1/p' "$build_config" | head -n1)"
if [ -z "$version_name" ]; then
echo "Error: could not read version info from $build_config" >&2
return 1
fi
local apk_base="btcmap-android-${version_name}"
# The release AppImage is named after the version, like the APKs, so each
# release can be told apart; the beta one keeps a stable name instead.
local appimage_out="desktopApp/build/compose/appimage/BTC-Map-${version_name}-x86_64.AppImage"
build_desktop_appimage "$appimage_out"
build_desktop_packages "$conveyor" release "$packages_out"
local out="release"
echo "Staging the release binaries in $out/..."
rm -rf "$out"
mkdir -p "$out"
cp "$apk_dir/app-arm64-v8a-release.apk" "$out/${apk_base}.apk"
cp "$apk_dir/app-universal-release.apk" "$out/${apk_base}-universal.apk"
cp "$appimage_out" "$out/"
cp -a "$packages_out"/. "$out/"
echo "Release binaries are in $out/."
}
cmd_app() {
local target="${1:-help}"
shift || true
case "$target" in
run) cmd_app_run "$@" ;;
deploy-beta) cmd_app_deploy_beta "$@" ;;
prepare-release) cmd_app_prepare_release "$@" ;;
bundle-data) cmd_app_bundle_data "$@" ;;
bundle-map-styles) cmd_app_bundle_map_styles "$@" ;;
bundle-icons) cmd_app_bundle_icons "$@" ;;
help|-h|--help) print_usage_app ;;
*) err "Unknown app command: $target"; print_usage_app; return 1 ;;
esac
}
# END app section
main() {
local cmd="${1:-help}"
shift || true
case "$cmd" in
emulator) cmd_emulator "$@" ;;
app) cmd_app "$@" ;;
website) cmd_website "$@" ;;
help|-h|--help) print_usage ;;
*) err "Unknown command: $cmd"; print_usage; return 1 ;;
esac
}
main "$@"