Repository navigation
Expand file tree
/
Copy pathdev
More file actions
executable file
·1120 lines (1055 loc) · 55.4 KB
/
Copy pathdev
File metadata and controls
executable file
·1120 lines (1055 loc) · 55.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
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
# Dev command wrapper for the devenv-based developer environment.
#
# The daily loop runs natively under devenv (devenv.sh) — no Docker dev
# container. Each git worktree derives its own non-colliding ports and isolated
# state (see devenv.nix). This wrapper is a thin shim over devenv that preserves
# the historical `./dev` command vocabulary.
#
# Unchanged, intentionally NOT devenv-backed:
# - relay <build|push|restart|deploy|logs> (Docker + kubectl; logs is curl)
# - player android <build|run|tv-run|shell> (nix devShell .#android /
# .#android-tv, native Nix)
# - player macos <build|run> (host Xcode/CocoaPods/rustup toolchain)
# - player flatpak <build|install|run> (host flatpak + flatpak-builder)
# - player e2e / e2e-build (Docker E2E stack, compose.player-e2e.yml)
# e2e pulls prebuilt images by default; --build forces local builds
set -e
# Colors for output. ANSI-C quoting ($'...') stores real ESC bytes so the codes
# render correctly both in `echo -e` and in the `cat <<EOF` help block.
RED=$'\033[0;31m'
GREEN=$'\033[0;32m'
YELLOW=$'\033[1;33m'
NC=$'\033[0m' # No Color
# Resolve project root (directory containing this script) so the wrapper works
# from any cwd within the worktree.
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT_DIR"
# Ensure devenv is available, with an actionable hint otherwise (R12 error path).
if ! command -v devenv &> /dev/null; then
echo -e "${RED}Error: devenv is not installed or not on PATH${NC}"
echo "This project uses devenv for local development. Set it up with:"
echo " 1. Install Nix: https://nixos.org/download"
echo " 2. Install devenv: nix profile install nixpkgs#devenv"
echo " 3. Install direnv: https://direnv.net (then 'direnv allow' in this worktree)"
echo "See docs/contributing/setup.md for the full first-run guide."
exit 1
fi
if [ ! -f "$ROOT_DIR/devenv.nix" ]; then
echo -e "${RED}Error: devenv.nix not found in $ROOT_DIR${NC}"
echo "Please run this script from the project root directory."
exit 1
fi
# Run a command inside the devenv shell (loads the env if not already active).
# Usage: in_shell mix deps.get
in_shell() {
devenv shell -- "$@"
}
# Run a shell snippet inside the devenv shell (for chained/`cd` commands with
# NO user-supplied arguments — never interpolate "$@"/"$*" into the snippet, as
# that loses quoting and lets shell metacharacters through).
# Usage: in_shell_sh "cd player && flutter pub get"
in_shell_sh() {
devenv shell -- bash -c "$1"
}
# Run a command (argv-safe) inside the devenv shell from the player/ directory.
# Args are passed positionally, so quoting/whitespace and metacharacters are
# preserved exactly. Usage: in_player flutter pub get
in_player() {
devenv shell -- bash -c 'cd player && exec "$@"' _ "$@"
}
# Create and migrate the development database.
#
# Under Postgres this asks the process daemon to start the server FIRST, so the
# daemon stays the single owner of the postmaster. Letting devenv start Postgres
# as a task dependency instead is what produced "lock file postmaster.pid
# already exists" alongside a running `./dev up -d`.
#
# `devenv processes start` talks to the process daemon over its native socket,
# which does not exist until some daemon is already running — so on a cold
# worktree (no `./dev up -d` yet) it errors immediately. Fall back to
# `devenv up -d postgres`, which starts just that one process detached with no
# daemon required. Both forms are tolerated failing: if Postgres genuinely
# cannot start, the mydia:ecto task's own readiness poll reports it with a
# better message than devenv does, and it names the host and port.
ensure_dev_db() {
case "${DATABASE_TYPE:-}" in
postgres|postgresql)
devenv processes start postgres 2>/dev/null \
|| devenv up -d postgres \
|| true
;;
esac
devenv tasks run mydia:ecto
}
# Show help if no arguments provided
if [ $# -eq 0 ]; then
cat <<EOF
${GREEN}Mydia Development Helper (devenv)${NC}
Usage: ./dev <command> [arguments...]
Each git worktree runs its own stack on deterministic, non-colliding ports
(derived from the worktree path; override in devenv.local.nix). Enter a worktree
with direnv (auto) or run any command below.
${YELLOW}Process Lifecycle (devenv processes):${NC}
up [-d] Start the stack (phoenix + flutter codegen, +postgres if
DATABASE_TYPE=postgres). Add -d for background.
down Stop background processes
stop Stop processes
start Start processes in the background
restart Restart the background stack
logs <process> Show recent logs for a process (e.g. 'logs phoenix';
no live-follow — use foreground 'up' to stream live)
ps List managed processes and their status
killall Force-kill ALL devenv process trees on every worktree
(daemons, phx.server BEAMs, dart codegen, watchers,
devenv-managed Postgres). Docker services and the
metadata_relay release are left untouched.
${YELLOW}Shells & Mix:${NC}
shell Open an interactive devenv shell
bash Open a bash shell in the devenv environment
iex Start IEx (iex -S mix)
mix <args> Run a mix command
mix precommit Run precommit checks natively (compact summary)
${YELLOW}Development Commands:${NC}
test [args] Run tests (mix test) natively, with test-DB setup
feature-test [args] Run Wallaby browser tests (uses in-shell chromedriver)
format Format code (mix format)
dialyzer [args] Run Dialyzer (mix dialyzer, MIX_ENV=test)
deps.get Install dependencies
db.setup Create and migrate the development database
ecto.migrate Run database migrations
ecto.reset Reset database
phx.server Start the Phoenix server (foreground)
${YELLOW}User Management:${NC}
user list [--role=R] List all users (optionally filter by role)
user add <email> <username> [opts]
Add a new user (--password, --role, --display-name)
user delete <id> Delete a user by email or username
user reset-password <id> [--password=P]
Reset a user's password
${YELLOW}Player (Flutter):${NC}
flutter <args> Run any flutter command (in player/)
player setup Install deps and run code generation
player build Build + deploy web assets to priv/static/player
player icons [--check]
Regenerate web icons from assets/*.svg (--check verifies
the committed set is still current)
player shell Open a shell in the player directory
player logs Show recent build_runner (codegen) process logs
player restart Restart the flutter-codegen process
player e2e [opts] Run player E2E integration tests (Docker)
player e2e-build Pre-build E2E images (Docker)
${YELLOW}Player Android (native, uses the .#android nix devShell):${NC}
player android build Build Android APK (release)
player android run Build and run on connected Android device
player android tv-run [args]
Provision an Android TV emulator and run the debug app
player android shell Open the .#android nix develop shell
${YELLOW}Player macOS (native, uses the host Xcode/CocoaPods/rustup toolchain):${NC}
player macos build Build the macOS app (release)
player macos run Build and run the macOS app (debug, hot reload)
Both accept --skip-codegen to reuse existing build_runner
output; extra args after 'run' pass through to flutter run.
${YELLOW}Player Flatpak (native, uses host flatpak + flatpak-builder):${NC}
player flatpak build Build the Flatpak and export it to the staged repo
player flatpak install
Install (or reinstall) the staged build for this user
player flatpak run Launch the installed Flatpak (args pass through)
player flatpak smoke Run the Xvfb smoke test against the installed build
player flatpak clean Remove build-dir/, staged-repo/ and .flatpak-builder/
build/install accept --branch=<name> (default: stable).
${YELLOW}Metadata Relay (Docker + kubectl):${NC}
relay deploy Build, push :dev image, and redeploy to k8s
relay build Build :dev Docker image only
relay push Push :dev image to ghcr.io
relay restart Restart deployment (pull latest image)
relay logs <device|LOG-code> [--since 1h] [--until T] [--level warn]
[--tag X] [--session S] [--grep text]
Print a player's uploaded logs (MYDIA_RELAY_DASHBOARD_USER,
MYDIA_RELAY_DASHBOARD_PASSWORD, optional MYDIA_RELAY_URL)
${YELLOW}Examples:${NC}
./dev up -d # Start the stack in the background
./dev logs phoenix # Show Phoenix logs
./dev mix test # Run tests
./dev mix ecto.migrate # Run migrations
./dev user list # List all users
./dev player logs # Show recent build_runner (codegen) logs
EOF
exit 0
fi
# ── Native precommit: reproduce the historical 6-step compact summary ─────────
# (Mirrors the prior Docker run_precommit; now native against the active adapter.
# Default SQLite serializes async tests; set DATABASE_TYPE=postgres to run the
# warm-Postgres parallel path. This adapter choice is the deferred tuning item
# from the migration plan's Open Questions.)
print_test_rerun_hints() {
echo ""
echo -e "${YELLOW}── Re-run failed tests ──${NC}"
echo " ./dev test --failed Re-run previously failed tests"
echo " ./dev test path/to_test.exs:42 Re-run a specific test"
echo " ./dev test path/to_test.exs Re-run a specific file"
}
run_precommit() {
# Run all eight steps inside a SINGLE devenv shell so the environment (and the
# enterShell setup-task guards) load once and _build/test stays warm across
# steps — no per-step shell re-entry, no recompilation between steps. Runs
# against the active adapter (SQLite by default; DATABASE_TYPE=postgres keeps
# async tests parallel — the deferred KTD6 tuning knob).
#
# KEEP IN SYNC with the `precommit` alias in mix.exs. This function is a
# reimplementation, not a wrapper, so a step added there is NOT picked up
# here. Credo was missing for exactly that reason and a --strict regression
# reached CI green-locally, red-in-CI.
devenv shell -- bash <<'PRECOMMIT'
set +e
GREEN='\033[0;32m'; RED='\033[0;31m'; NC='\033[0m'
export MIX_ENV=test
declare -a FAILED_STEPS FAILED_OUTPUTS
any_failed=0
run_step() {
local label="$1" cmd="$2" output exit_code start elapsed
printf " %-30s" "$label"
start=$SECONDS
output=$(eval "$cmd" 2>&1); exit_code=$?
elapsed=$(( SECONDS - start ))
if [ $exit_code -eq 0 ]; then
local summary=""
[ "$label" = "Tests" ] && summary=$(echo "$output" | grep -oE '[0-9]+ tests?, [0-9]+ failures?' | tail -1)
if [ -n "$summary" ]; then echo -e "${GREEN}✓${NC} $summary (${elapsed}s)"; else echo -e "${GREEN}✓${NC} ${elapsed}s"; fi
else
echo -e "${RED}✗${NC} ${elapsed}s"
local f; f=$(mktemp /tmp/mydia-precommit-XXXXXX.log); echo "$output" > "$f"
local filtered; filtered=$(echo "$output" | sed 's/\x1b\[[0-9;]*m//g' | sed -n '/^ [0-9]\+) /,/^$/p; /^Finished in/p; /^[0-9]\+ tests\?/p')
[ -z "$filtered" ] && filtered=$(echo "$output" | tail -20)
FAILED_STEPS+=("$label"); FAILED_OUTPUTS+=("$(printf '%s\n\nFull output: %s' "$filtered" "$f")")
any_failed=1
fi
}
echo "Precommit"
run_step "Dependencies" "mix deps.get >/dev/null 2>&1 || mix deps.get"
run_step "Compile" "mix compile --warnings-as-errors"
run_step "Unused deps" "mix deps.unlock --unused"
run_step "Format" "mix format --check-formatted"
run_step "Credo" "mix credo --strict"
run_step "Ratchet" "mix mydia.ratchet"
run_step "Database" "mix ecto.create --quiet && mix ecto.migrate --quiet"
run_step "Tests" "mix test --warnings-as-errors --color"
echo ""
if [ ${#FAILED_OUTPUTS[@]} -gt 0 ]; then
for i in "${!FAILED_STEPS[@]}"; do
echo -e "${RED}── ${FAILED_STEPS[$i]} ──${NC}"
echo "${FAILED_OUTPUTS[$i]}"
echo ""
done
fi
if [ $any_failed -ne 0 ]; then echo -e "${RED}✗ Precommit failed${NC}"; exit 1; else echo -e "${GREEN}✓ Precommit passed${NC}"; exit 0; fi
PRECOMMIT
}
# ── killall: reclaim CPU/RAM from stale devenv processes ───────────────────────
# devenv's `processes down` only touches the current worktree. Stale worktrees
# (and agent-spawned ones) leave their process daemons and children running
# until reboot. This sweeps them all, plus orphaned dev processes whose devenv
# parent already exited.
#
# Only devenv's OWN signatures are matched directly — `daemon-processes` (the
# wrapped process manager), `devenv-processes-{phoenix,postgres,flutter-codegen}`
# (the wrapper scripts), and `.devenv/state/postgres` (devenv's Postgres data
# dir). These are unambiguous, so an unrelated process can never match.
# Everything else — the phx.server BEAMs, dart codegen, inotify/esbuild/tailwind
# watchers, and Postgres backends — is reached only as a DESCENDANT of those
# roots, so an unrelated project's dev server or watcher is never touched. This
# also covers Postgres orphan cleanup: a killed postmaster's backends are
# already in the target set as its descendants, so they are reaped without ever
# matching an arbitrary `postgres:` process by name.
#
# Docker services (mydia_metadata_relay, mydia_prowlarr) and the metadata_relay
# release BEAM are left untouched.
kill_all_dev_processes() {
local snap pid ppid args roots="" pids="" count
snap="$(ps -eo pid=,ppid=,args=)"
# Pass 1: devenv roots.
while IFS= read -r line; do
read -r pid ppid args <<< "$line"
case "$args" in
*daemon-processes*|*devenv-processes*|*.devenv/state/postgres*)
roots="$roots $pid" ;;
esac
done <<< "$snap"
if [ -z "$roots" ]; then
echo -e "${GREEN}No devenv/mydia dev processes found.${NC}"
return 0
fi
# Pass 2: descendants of the roots via the snapshot's ppid links.
local -A children
while IFS= read -r line; do
read -r pid ppid args <<< "$line"
children[$ppid]+=" $pid"
done <<< "$snap"
local -a stack
local p child
stack=($roots)
pids="$roots"
while [ ${#stack[@]} -gt 0 ]; do
p="${stack[0]}"
stack=("${stack[@]:1}")
for child in ${children[$p]}; do
pids="$pids $child"
stack+=("$child")
done
done
# Deduplicate (a root may also be another root's descendant) and count.
pids="$(printf '%s\n' $pids | sort -un | tr '\n' ' ')"
count="$(echo -n "$pids" | wc -w)"
echo -e "${GREEN}Killing $count devenv/mydia dev process(es) across all worktrees…${NC}"
# shellcheck disable=SC2086
kill -TERM $pids 2>/dev/null || true
sleep 1
local still=""
for p in $pids; do
kill -0 "$p" 2>/dev/null && still="$still $p"
done
if [ -n "$still" ]; then
echo -e "${YELLOW}Force-killing $(echo "$still" | wc -w) straggler(s)…${NC}"
# shellcheck disable=SC2086
kill -KILL $still 2>/dev/null || true
fi
echo -e "${GREEN}Done.${NC}"
}
# Parse command
COMMAND=$1
shift
case "$COMMAND" in
# ── Process lifecycle ────────────────────────────────────────────────────
up)
echo -e "${GREEN}Starting devenv processes...${NC}"
devenv up "$@"
;;
down)
echo -e "${GREEN}Stopping devenv processes...${NC}"
devenv processes down
;;
stop)
echo -e "${GREEN}Stopping devenv processes...${NC}"
devenv processes stop "$@"
;;
start)
echo -e "${GREEN}Starting devenv processes in the background...${NC}"
devenv up -d "$@"
;;
restart)
echo -e "${GREEN}Restarting devenv processes...${NC}"
devenv processes down >/dev/null 2>&1 || true
devenv up -d "$@"
;;
logs)
# `devenv processes logs` shows recent lines for ONE named process and
# does not follow (no -f). For a live stream of everything, run `./dev up`
# in the foreground. Translate a historical -f/--follow into a hint
# rather than forwarding an unsupported flag.
follow=false
log_args=()
for a in "$@"; do
case "$a" in
-f|--follow) follow=true ;;
*) log_args+=("$a") ;;
esac
done
if [ "$follow" = true ]; then
echo -e "${YELLOW}Note: 'logs' shows recent lines only — devenv has no live-follow.${NC}"
echo -e "${YELLOW} Run './dev up' (foreground) to stream all processes live.${NC}"
fi
if [ ${#log_args[@]} -eq 0 ]; then
echo -e "${YELLOW}Specify a process, e.g.:${NC} ./dev logs phoenix"
echo "Managed processes: phoenix, flutter-codegen (plus postgres when DATABASE_TYPE=postgres)."
echo "Use './dev ps' to list them, or './dev up' (foreground) to stream all live."
exit 0
fi
devenv processes logs "${log_args[@]}"
;;
ps)
devenv processes list
;;
killall)
kill_all_dev_processes
;;
# ── Shells & mix ─────────────────────────────────────────────────────────
shell|sh)
echo -e "${GREEN}Entering devenv shell...${NC}"
devenv shell
;;
bash)
echo -e "${GREEN}Opening bash in the devenv environment...${NC}"
in_shell bash
;;
iex)
echo -e "${GREEN}Starting IEx...${NC}"
# An IEx session against an unmigrated dev database is useless, and this
# is the common command that needs one without going through `./dev up`.
ensure_dev_db
if [ $# -gt 0 ]; then
in_shell iex "$@"
else
in_shell iex -S mix
fi
;;
mix)
if [[ "$1" == "precommit" ]]; then
run_precommit
elif [[ "$1" == "test" ]]; then
echo -e "${GREEN}Running tests...${NC}"
test_exit_code=0
in_shell mix test --color "${@:2}" || test_exit_code=$?
[ $test_exit_code -ne 0 ] && print_test_rerun_hints
exit $test_exit_code
else
in_shell mix "$@"
fi
;;
# ── Development shortcuts ────────────────────────────────────────────────
test)
echo -e "${GREEN}Running tests...${NC}"
test_exit_code=0
in_shell mix test --color "$@" || test_exit_code=$?
[ $test_exit_code -ne 0 ] && print_test_rerun_hints
exit $test_exit_code
;;
feature-test)
echo -e "${GREEN}Running feature tests (Wallaby browser tests)...${NC}"
in_shell env MIX_ENV=test ./scripts/run-feature-tests.sh "$@"
;;
format)
echo -e "${GREEN}Formatting code...${NC}"
in_shell mix format
;;
dialyzer)
echo -e "${GREEN}Running Dialyzer...${NC}"
in_shell mix dialyzer "$@"
;;
deps.get)
echo -e "${GREEN}Installing dependencies...${NC}"
in_shell mix deps.get
;;
ecto.migrate)
echo -e "${GREEN}Running migrations...${NC}"
in_shell mix ecto.migrate
;;
ecto.reset)
echo -e "${GREEN}Resetting database...${NC}"
in_shell mix ecto.reset
;;
db.setup)
echo -e "${GREEN}Setting up the development database...${NC}"
ensure_dev_db
;;
phx.server)
echo -e "${GREEN}Starting Phoenix server...${NC}"
# `./dev up` gets this ordering from processes.phoenix.after; the
# foreground shortcut bypasses the process graph, so do it by hand.
ensure_dev_db
in_shell mix phx.server
;;
# ── User management ──────────────────────────────────────────────────────
user)
if [ $# -eq 0 ]; then
echo -e "${RED}Error: user command requires a subcommand${NC}"
echo "Usage: ./dev user <list|add|delete|reset-password> [args...]"
exit 1
fi
# mix mydia.user boots the full supervision tree (Mix.Task.run("app.start")),
# which dies in ClientHealth.init/1 against an unmigrated database. This is
# the documented dev user-management path, so it needs the same guard as
# iex/phx.server rather than assuming `./dev up` already ran.
ensure_dev_db
in_shell mix mydia.user "$@"
;;
# ── Flutter / player ─────────────────────────────────────────────────────
flutter)
echo -e "${GREEN}Running flutter command...${NC}"
in_player flutter "$@"
;;
player)
case "$1" in
logs)
echo -e "${GREEN}Showing recent build_runner (codegen) process logs...${NC}"
devenv processes logs flutter-codegen
;;
build)
echo -e "${GREEN}Building player web assets...${NC}"
# --pwa-strategy=none: a scope holds exactly one service worker
# registration, and web/sw.js needs the app's own scope to
# intercept media requests. Leaving Flutter's worker on would
# mean the two replacing each other on every load and playback
# cycle. Flutter's own build prints that its service worker is
# deprecated and slated for removal.
in_shell_sh "cd player && flutter build web --release --base-href /player/ --tree-shake-icons --pwa-strategy=none"
echo -e "${GREEN}Copying build output to priv/static/player...${NC}"
in_shell_sh "rm -rf priv/static/player && cp -r player/build/web priv/static/player"
in_shell_sh "[ -f player/web/manifest.json ] && cp player/web/manifest.json priv/static/player/ || true"
echo -e "${GREEN}Build deployed successfully!${NC}"
;;
icons)
shift
if [ $# -gt 0 ]; then
# Every argument path needs only coreutils, so skip the
# ~2min devenv startup and let the script validate.
exec player/tool/gen-web-icons.sh "$@"
fi
in_shell_sh "player/tool/gen-web-icons.sh"
;;
setup)
echo -e "${GREEN}Setting up player project (pub get + build_runner)...${NC}"
in_shell_sh "cd player && flutter pub get && flutter pub run build_runner build"
echo -e "${GREEN}Player project setup complete!${NC}"
;;
shell)
echo -e "${GREEN}Opening shell in player directory...${NC}"
in_shell_sh "cd player && exec bash"
;;
restart)
echo -e "${GREEN}Restarting flutter-codegen process...${NC}"
devenv processes restart flutter-codegen
;;
e2e)
shift
rebuild_images=false
test_target="integration_test/all_tests.dart"
test_args=""
while [ $# -gt 0 ]; do
case "$1" in
--build) rebuild_images=true ;;
--target)
if [ -z "$2" ]; then
echo -e "${RED}Error: --target requires a test file path${NC}"
exit 1
fi
test_target="$2"; shift ;;
--)
shift
[ $# -gt 0 ] && test_args="$*"
break ;;
*) test_args="${test_args:+$test_args }$1" ;;
esac
shift
done
echo -e "${GREEN}Running player E2E integration tests (Docker)...${NC}"
echo "Test target: $test_target"
if [ "$rebuild_images" = true ]; then
echo -e "${GREEN}Building E2E images locally...${NC}"
DOCKER_BUILDKIT=1 docker build -f Dockerfile \
--build-arg DATABASE_TYPE=sqlite -t mydia-e2e:local .
DOCKER_BUILDKIT=1 docker build -f player/Dockerfile.test \
-t player-e2e-toolbox:local .
DOCKER_BUILDKIT=1 docker build -f metadata-relay/Dockerfile \
-t metadata-relay:local .
export MYDIA_E2E_IMAGE=mydia-e2e:local
export PLAYER_E2E_TOOLBOX_IMAGE=player-e2e-toolbox:local
export RELAY_E2E_IMAGE=metadata-relay:local
else
docker compose -f compose.player-e2e.yml pull --quiet || \
echo -e "${YELLOW}Warning: could not pull all images; compose will use whatever is cached locally${NC}"
fi
exit_code=0
E2E_TEST_TARGET="$test_target" E2E_TEST_ARGS="$test_args" \
docker compose -f compose.player-e2e.yml up --abort-on-container-exit --exit-code-from test \
|| exit_code=$?
docker compose -f compose.player-e2e.yml down -v
exit $exit_code
;;
e2e-build)
echo -e "${GREEN}Pre-building player E2E images (Docker)...${NC}"
DOCKER_BUILDKIT=1 docker build -f Dockerfile \
--build-arg DATABASE_TYPE=sqlite -t mydia-e2e:local .
DOCKER_BUILDKIT=1 docker build -f player/Dockerfile.test \
-t player-e2e-toolbox:local .
DOCKER_BUILDKIT=1 docker build -f metadata-relay/Dockerfile \
-t metadata-relay:local .
echo -e "${GREEN}E2E images pre-built successfully!${NC}"
# --build is required here: without it, 'player e2e' takes the
# pull branch, and :local tags can never be pulled from a
# registry, so the copy-pasted command would always print a
# spurious "could not pull" warning. --build re-runs the same
# docker build commands above, which is a fast no-op against
# Docker's layer cache when nothing changed since this run.
echo "Run with: ./dev player e2e --build"
;;
android)
# Android builds use the player's flake.nix (native, not devenv)
if ! command -v nix &> /dev/null; then
echo -e "${RED}Error: nix is not installed${NC}"
echo "Android builds require Nix. Install from: https://nixos.org/download.html"
exit 1
fi
case "$2" in
build)
echo -e "${GREEN}Building Android APK using the .#android nix devShell...${NC}"
# build_runner is required, not optional: every generated
# Dart file is gitignored, so a fresh checkout has none and
# the build fails with hundreds of unrelated-looking Dart
# errors. Idempotent, so running it every time is safe.
nix develop .#android --command bash -c "cd player && flutter pub get && flutter pub run build_runner build && flutter build apk --release --target-platform android-arm,android-arm64"
if [ $? -eq 0 ]; then
echo -e "${GREEN}APK built successfully!${NC}"
echo "Output: player/build/app/outputs/flutter-apk/app-release.apk"
fi
;;
run)
echo -e "${GREEN}Building and running on Android device...${NC}"
extra_args="${@:3}"
nix develop .#android --command bash -c "cd player && flutter pub get && flutter pub run build_runner build && flutter run $extra_args"
;;
tv-run)
# The runner owns the whole sequence (AVD creation,
# emulator start/ownership, boot wait, flutter run), so
# this branch only enters the TV shell and passes the
# remaining arguments as argv — never interpolated into
# a `bash -c` string, which would lose their quoting.
echo -e "${GREEN}Starting Mydia on the Nix-managed Android TV emulator...${NC}"
nix develop .#android-tv --command \
bash player/tool/run_android_tv.sh "${@:3}"
;;
shell)
echo -e "${GREEN}Opening the .#android nix develop shell...${NC}"
nix develop .#android
;;
*)
echo -e "${RED}Error: Unknown android command: $2${NC}"
echo "Usage: ./dev player android <build|run|tv-run|shell>"
exit 1
;;
esac
;;
macos)
# macOS app builds use the HOST toolchain — not devenv, and not
# the .#android devShell either. Xcode and CocoaPods are Apple-licensed
# SDKs Nix cannot provide, and cargokit shells out to `rustup` to
# compile the Rust p2p core into the app bundle. Same escape hatch
# as `player android`, different reason.
if [ "$(uname -s)" != "Darwin" ]; then
echo -e "${RED}Error: macOS builds require a macOS host${NC}"
echo "Current host: $(uname -s). Use 'player android' or 'player build' (web) instead."
exit 1
fi
macos_cmd="${2:-}"
case "$macos_cmd" in
build|run) ;;
*)
echo -e "${RED}Error: Unknown macos command: ${macos_cmd:-<none>}${NC}"
echo "Usage: ./dev player macos <build|run> [--skip-codegen] [flutter args...]"
exit 1
;;
esac
shift 2
# Escape the devenv/Nix environment. direnv loads devenv on cd
# into the worktree, so by the time this branch runs the shell
# already exports a Nix clang/cctools PATH, DEVELOPER_DIR and
# SDKROOT pointing at nixpkgs' apple-sdk, NIX_CFLAGS_COMPILE,
# and a Nix-wrapped Flutter. That last one is fatal, not merely
# wrong: its engine artifacts live read-only in /nix/store, so
# Flutter copies FlutterMacOS.framework out as dr-xr-xr-x and
# `lipo` dies with "can't create temporary output file
# ... (Permission denied)" during debug_unpack_macos. Xcode must
# see the host toolchain, so strip the Nix layer back off.
if [ -n "${IN_NIX_SHELL:-}${DEVENV_ROOT:-}" ]; then
PATH="$(printf '%s' "$PATH" | tr ':' '\n' | grep -v '^/nix/store/' | paste -sd: -)"
export PATH
# NIX_SSL_CERT_FILE stays: rustup and CocoaPods fetch over
# TLS and Nix-installed curl has no other CA bundle.
for nix_var in $(compgen -v | grep '^NIX_'); do
[ "$nix_var" = "NIX_SSL_CERT_FILE" ] || unset "$nix_var"
done
unset DEVELOPER_DIR SDKROOT MACOSX_DEPLOYMENT_TARGET IN_NIX_SHELL \
LD_DYLD_PATH LD_FOR_BUILD PKG_CONFIG_PATH CC CXX AR LD \
CFLAGS LDFLAGS CPATH LIBRARY_PATH
fi
# Preflight the host toolchain so a missing prerequisite fails
# here with a fix, not 200 lines into an Xcode build log.
missing_tools=""
for tool in flutter pod rustup; do
command -v "$tool" &> /dev/null || missing_tools="${missing_tools:+$missing_tools }$tool"
done
if [ -n "$missing_tools" ]; then
echo -e "${RED}Error: missing host tools: $missing_tools${NC}"
echo "macOS builds need the host toolchain (devenv does not provide these):"
echo " flutter https://docs.flutter.dev/get-started/install/macos"
echo " pod sudo gem install cocoapods (or: brew install cocoapods)"
echo " rustup https://rustup.rs (cargokit builds the Rust p2p core via rustup)"
exit 1
fi
# Full Xcode, NOT just the Command Line Tools: /usr/bin/xcodebuild
# is a shim that exists either way, so `command -v` passes with CLT
# only and the build then dies deep inside SPM resolution with
# "unable to find utility xcodebuild". Ask xcrun to resolve it.
if ! xcrun --find xcodebuild &> /dev/null; then
echo -e "${RED}Error: full Xcode is required to build the macOS app${NC}"
echo "Active developer directory: $(xcode-select -p 2>/dev/null || echo '<unset>')"
echo "The Command Line Tools alone cannot build macOS app bundles. Fix with:"
echo " 1. Install Xcode from the App Store (or https://developer.apple.com/xcode)"
echo " 2. sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer"
echo " 3. sudo xcodebuild -runFirstLaunch"
exit 1
fi
# Codegen mirrors CI (ci-player.yml) — desktop builds need the
# build_runner output. --skip-codegen reuses it for fast iteration;
# everything else passes through to flutter.
run_codegen=true
flutter_args=()
for arg in "$@"; do
if [ "$arg" = "--skip-codegen" ]; then
run_codegen=false
else
flutter_args+=("$arg")
fi
done
(
cd player
echo -e "${GREEN}Installing player dependencies...${NC}"
flutter pub get
if [ "$run_codegen" = true ]; then
echo -e "${GREEN}Running code generation...${NC}"
flutter pub run build_runner build
fi
case "$macos_cmd" in
build)
echo -e "${GREEN}Building macOS app (release)...${NC}"
flutter build macos --release
;;
run)
echo -e "${GREEN}Running macOS app...${NC}"
flutter run -d macos "${flutter_args[@]}"
;;
esac
)
if [ "$macos_cmd" = "build" ]; then
echo -e "${GREEN}macOS app built successfully!${NC}"
echo "Output: player/build/macos/Build/Products/Release/Mydia Player.app"
echo "Open it with: open 'player/build/macos/Build/Products/Release/Mydia Player.app'"
fi
;;
flatpak)
# Linux desktop packaging. Native like `player android` and
# `player macos`, for a third reason: flatpak-builder drives
# bubblewrap and needs the host's flatpak installation, its
# runtimes and its per-user repo state. devenv provides none of
# that and could not without shipping a second flatpak.
if [ "$(uname -s)" != "Linux" ]; then
echo -e "${RED}Error: Flatpak builds require a Linux host${NC}"
echo "Current host: $(uname -s). Use 'player macos' instead."
exit 1
fi
flatpak_cmd="${2:-}"
case "$flatpak_cmd" in
build|install|run|smoke|clean) ;;
*)
echo -e "${RED}Error: Unknown flatpak command: ${flatpak_cmd:-<none>}${NC}"
echo "Usage: ./dev player flatpak <build|install|run|smoke|clean>"
exit 1
;;
esac
shift 2
FLATPAK_APP_ID="dev.mydia.player"
FLATPAK_MANIFEST="$ROOT_DIR/player/flatpak/dev.mydia.player.yml"
# Build state lives in the MAIN checkout, not the current
# worktree. Three things depend on that: the ~130M
# .flatpak-builder cache is shared instead of rebuilt per
# worktree; the exported repo stays at one path, so the `staged`
# remote keeps resolving after a worktree is deleted; and the
# 80M build-dir never lands inside a worktree, where the
# dart-analyze pre-commit hook would walk it (it does not read
# .gitignore) and block every commit. `--git-common-dir` is the
# main checkout's .git in a worktree and plain `.git` otherwise,
# so this collapses to ROOT_DIR for a normal clone and in CI.
#
# Both tests belong in the `if` condition: a bare `[ -n ... ] &&`
# as the last statement of the body would return 1 on an empty
# result and, under this script's `set -e`, exit right here.
flatpak_state_root="$ROOT_DIR"
if git_common="$(git -C "$ROOT_DIR" rev-parse --path-format=absolute --git-common-dir 2> /dev/null)" \
&& [ -n "$git_common" ]; then
flatpak_state_root="$(dirname "$git_common")"
fi
FLATPAK_BUILD_DIR="${MYDIA_FLATPAK_BUILD_DIR:-$flatpak_state_root/build-dir}"
FLATPAK_STATE_DIR="${MYDIA_FLATPAK_STATE_DIR:-$flatpak_state_root/.flatpak-builder}"
FLATPAK_REPO="${MYDIA_FLATPAK_REPO:-$flatpak_state_root/staged-repo}"
# The manifest's `dir` source is `path: ../..`, relative to the
# manifest file, so the current worktree's code is what gets
# built even though the state above is shared.
flatpak_branch="stable"
flatpak_passthru=()
for arg in "$@"; do
case "$arg" in
--branch=*) flatpak_branch="${arg#--branch=}" ;;
*) flatpak_passthru+=("$arg") ;;
esac
done
# `clean` is three `rm -rf`s and needs none of the tooling
# below. Dispatched before the checks so that removing build
# state still works on a host where flatpak has since been
# uninstalled — otherwise the one command whose whole job is
# reclaiming ~270M is the one that refuses to run.
if [ "$flatpak_cmd" = "clean" ]; then
echo -e "${GREEN}Removing Flatpak build state...${NC}"
rm -rf "$FLATPAK_BUILD_DIR" "$FLATPAK_STATE_DIR" "$FLATPAK_REPO"
echo "Removed: $FLATPAK_BUILD_DIR"
echo " $FLATPAK_STATE_DIR"
echo " $FLATPAK_REPO"
exit 0
fi
if ! command -v flatpak &> /dev/null; then
echo -e "${RED}Error: flatpak is not installed${NC}"
echo "Install it from your distribution, then re-run. See https://flatpak.org/setup/"
exit 1
fi
# flatpak-builder shells out to appstreamcli on the HOST during
# export. Missing appstreamcli fails the build at the very last
# step, after mpv, libplacebo, libass, Flutter and the Rust
# cdylib have all compiled — so both are resolved together here,
# up front. On NixOS neither is normally installed; the flake
# registry form is used because `nix-shell -p` needs a NIX_PATH
# that a flakes-based system does not set.
#
# Only `build` runs flatpak-builder. `install`, `run` and
# `smoke` drive the plain `flatpak` CLI, so resolving the
# builder for them meant a spurious nix resolution (and its
# warning) on every install.
flatpak_builder_cmd=()
if [ "$flatpak_cmd" = "build" ]; then
if command -v flatpak-builder &> /dev/null && command -v appstreamcli &> /dev/null; then
flatpak_builder_cmd=(flatpak-builder)
elif command -v nix &> /dev/null; then
echo -e "${YELLOW}flatpak-builder/appstreamcli not on PATH; resolving both through nix${NC}"
flatpak_builder_cmd=(nix shell nixpkgs#flatpak-builder nixpkgs#appstream --command flatpak-builder)
else
echo -e "${RED}Error: need flatpak-builder and appstreamcli (or nix to provide them)${NC}"
echo "Install both: appstreamcli is what flatpak-builder calls during export,"
echo "and without it the build fails only after everything has compiled."
exit 1
fi
fi
case "$flatpak_cmd" in
build)
# Runtimes are a hard prerequisite of the manifest, and a
# missing one fails immediately rather than an hour in,
# so installing them is cheap insurance.
flatpak remote-add --user --if-not-exists flathub \
https://dl.flathub.org/repo/flathub.flatpakrepo
echo -e "${GREEN}Ensuring runtimes are installed...${NC}"
flatpak install --user -y --noninteractive flathub \
org.gnome.Platform//50 org.gnome.Sdk//50 \
org.freedesktop.Sdk.Extension.llvm21//25.08 || true
# Inside the state dir, which is already gitignored;
# beside build-dir it would show up as an untracked file.
mkdir -p "$FLATPAK_STATE_DIR"
flatpak_log="$FLATPAK_STATE_DIR/build.log"
echo -e "${GREEN}Building ${FLATPAK_APP_ID}//${flatpak_branch}...${NC}"
echo " manifest: $FLATPAK_MANIFEST"
echo " build dir: $FLATPAK_BUILD_DIR"
echo " state dir: $FLATPAK_STATE_DIR"
echo " repo: $FLATPAK_REPO"
echo " log: $flatpak_log"
echo "Expect 30-90 minutes cold (mpv, libplacebo, libass, Flutter, Rust)."
# --force-clean because flatpak-builder refuses a
# non-empty build dir on a re-run; the expensive compiled
# stages live in the state dir and are reused regardless.
# --disable-rofiles-fuse because the rofiles overlay is
# unreliable on several host filesystems.
#
# tee, and never a bare pipe into tail: a pipeline
# reports the LAST command's status, so a failed build
# would look like exit 0. PIPESTATUS carries the real one.
set +e
"${flatpak_builder_cmd[@]}" \
--user --force-clean --disable-rofiles-fuse \
--state-dir="$FLATPAK_STATE_DIR" \
--repo="$FLATPAK_REPO" \
--default-branch="$flatpak_branch" \
"$FLATPAK_BUILD_DIR" "$FLATPAK_MANIFEST" 2>&1 | tee "$flatpak_log"
flatpak_status=${PIPESTATUS[0]}
set -e
if [ "$flatpak_status" -ne 0 ]; then
echo -e "${RED}Flatpak build failed (exit $flatpak_status)${NC}"
echo "Full log: $flatpak_log"
exit "$flatpak_status"
fi
echo -e "${GREEN}Flatpak built and exported to $FLATPAK_REPO${NC}"
echo "Install it with: ./dev player flatpak install"
;;
install)
if [ ! -d "$FLATPAK_REPO" ]; then
echo -e "${RED}Error: no staged repo at $FLATPAK_REPO${NC}"
echo "Run './dev player flatpak build' first."
exit 1
fi
# Point `staged` at this repo unconditionally. --if-not-exists
# alone would leave an existing remote aimed at whichever
# checkout last built one, and the install would then
# silently deploy that build instead of this one.
flatpak remote-add --user --no-gpg-verify --if-not-exists staged "$FLATPAK_REPO"
flatpak remote-modify --user --no-gpg-verify --url="file://$FLATPAK_REPO" staged
echo -e "${GREEN}Installing ${FLATPAK_APP_ID}//${flatpak_branch} from staged...${NC}"
flatpak install --user -y --noninteractive --reinstall \
staged "${FLATPAK_APP_ID}//${flatpak_branch}"
echo -e "${GREEN}Installed.${NC} Run it with: ./dev player flatpak run"
;;
run)
# A shell with no session (ssh, a background job, cron)
# exports neither WAYLAND_DISPLAY nor DISPLAY, and the
# app then dies on "Unable to init server". Adopt the
# logged-in session's socket when one is sitting in
# XDG_RUNTIME_DIR rather than failing on an empty env.
if [ -z "${WAYLAND_DISPLAY:-}" ] && [ -z "${DISPLAY:-}" ]; then
if [ -S "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/wayland-0" ]; then
export WAYLAND_DISPLAY=wayland-0
echo -e "${YELLOW}No display in the environment; using WAYLAND_DISPLAY=wayland-0${NC}"
elif [ -S /tmp/.X11-unix/X0 ]; then
export DISPLAY=:0
echo -e "${YELLOW}No display in the environment; using DISPLAY=:0${NC}"
fi
fi
echo -e "${GREEN}Launching ${FLATPAK_APP_ID}//${flatpak_branch}...${NC}"
exec flatpak run --branch="$flatpak_branch" "$FLATPAK_APP_ID" "${flatpak_passthru[@]}"
;;
smoke)
# smoke-test.sh is written for CI, where xvfb-run comes
# from apt. Resolve it the same way as flatpak-builder
# when the host does not carry it.
smoke_cmd=("$ROOT_DIR/player/flatpak/smoke-test.sh" "$flatpak_branch")
if ! command -v xvfb-run &> /dev/null; then
if ! command -v nix &> /dev/null; then
echo -e "${RED}Error: the smoke test needs xvfb-run${NC}"
echo "Install xvfb (Debian/Ubuntu: xvfb; Fedora: xorg-x11-server-Xvfb)."
exit 1
fi
echo -e "${YELLOW}xvfb-run not on PATH; resolving through nix${NC}"
smoke_cmd=(nix shell nixpkgs#xvfb-run --command "${smoke_cmd[@]}")
fi
set +e
"${smoke_cmd[@]}"
smoke_status=$?
set -e
# The test's SIGTERM hits the xvfb-run wrapper, not the
# flatpak, so on a developer machine the app and its
# Xvfb both survive a passing run — and the app keeps a
# live p2p node registered with the relay. CI throws the
# runner away; here the leftovers have to go explicitly.
flatpak kill "$FLATPAK_APP_ID" 2> /dev/null || true
pgrep -f 'Xvfb :99' 2> /dev/null | xargs -r kill 2> /dev/null || true
exit "$smoke_status"
;;