Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
1669 lines (1530 loc) · 78.7 KB
/
Copy pathMakefile
File metadata and controls
1669 lines (1530 loc) · 78.7 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
# Copyright (c) 2025 Preston Brown <pbrown@brown-house.net>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# HelixScreen UI Prototype - Main Makefile
# LVGL 9 + SDL2 simulator with modular build system
#
# ⚠️ CRITICAL: Always use 'make' - NEVER invoke gcc/g++ directly!
# The build system handles:
# - Dependency management (libhv, lvgl, SDL2)
# - Platform detection (macOS vs Linux)
# - Parallel builds (auto-fixes 'make -j' to use correct core count)
# - Patch application for multi-display support
#
# Common commands:
# make -j # Parallel incremental build (auto-detects cores)
# make build # Clean build from scratch
# make help # Show all available targets
#
# See: docs/devel/DEVELOPMENT.md for complete build instructions
# Use bash for all shell commands (needed for [[ ]] and read -n)
SHELL := /bin/bash
# GNU make >= 4.2 (the $(file ...) function). An older make parses this file
# without complaint and silently skips every $(file) call: the test-link
# response file is never written, so a link either dies on a missing @-file or
# runs against the manifest the last 4.2+ build left behind. macOS ships
# /usr/bin/make as GNU 3.81 and Homebrew installs 4.x only as `gmake`, so this
# is the loud version of a failure that is otherwise indistinguishable from a
# stale build.
ifneq ($(shell test "$(firstword $(subst ., ,$(MAKE_VERSION)))" -gt 4 -o \( "$(firstword $(subst ., ,$(MAKE_VERSION)))" -eq 4 -a "$(word 2,$(subst ., ,$(MAKE_VERSION)))" -ge 2 \) && echo ok),ok)
$(error GNU make >= 4.2 required, this is $(MAKE_VERSION). On macOS use `gmake`.)
endif
# Delete a target if its recipe fails. Without this, a failed link can leave a
# truncated/0-byte output (e.g. helix-tests) on disk with a newer mtime than its
# prerequisites — the next `make` then treats the broken artifact as up-to-date
# and never rebuilds it, so the only recovery is a manual `rm`. Deleting the
# half-written target on failure makes the next invocation retry cleanly.
.DELETE_ON_ERROR:
# Build verbosity control
# Use 'make V=1' to see full compiler commands
V ?= 0
ifeq ($(V),0)
Q := @
ECHO := @echo
else
Q :=
ECHO := @true
endif
# Color output - auto-detect terminal capabilities
# Disable with NO_COLOR=1 or when running in non-terminal environments
ifndef NO_COLOR
# Auto-detect if terminal supports colors
# Check if Make is running interactively (stdin is a TTY) AND stderr is a TTY
# This disables colors when output is piped (stdout redirect) or in non-interactive contexts
# Note: We can't check stdout directly from $(shell) because it's redirected to capture output
TERM_SUPPORTS_COLOR := $(shell \
if [ -t 0 ] && [ -t 2 ] && [ -n "$$TERM" ] && [ "$$TERM" != "dumb" ]; then \
echo 1; \
else \
echo 0; \
fi)
ifeq ($(TERM_SUPPORTS_COLOR),1)
BOLD := $(shell printf '\033[1m')
RED := $(shell printf '\033[31m')
GREEN := $(shell printf '\033[32m')
YELLOW := $(shell printf '\033[33m')
BLUE := $(shell printf '\033[34m')
MAGENTA := $(shell printf '\033[35m')
CYAN := $(shell printf '\033[36m')
RESET := $(shell printf '\033[0m')
else
# Terminal doesn't support colors - disable
BOLD :=
RED :=
GREEN :=
YELLOW :=
BLUE :=
MAGENTA :=
CYAN :=
RESET :=
endif
else
# NO_COLOR=1 explicitly set
BOLD :=
RED :=
GREEN :=
YELLOW :=
BLUE :=
MAGENTA :=
CYAN :=
RESET :=
endif
# Compilers - auto-detect or use environment variables
# Priority: environment variables > clang (if working) > gcc
#
# On some Linux distros (e.g., Arch with GCC 15), Clang may fail to find
# GCC's libstdc++ headers, causing "#include_next <stdlib.h>" errors.
# We test-compile to detect this and auto-fallback to GCC.
# Helper to test if a C++ compiler can use the standard library
# Returns "ok" on success, empty on failure
# We test #include <cstdlib> because that's where clang+libstdc++ breaks on some systems
HASH := \#
define test_cxx_stdlib
$(shell printf '$(HASH)include <cstdlib>\n' | $(1) -x c++ -std=c++17 -fsyntax-only - 2>/dev/null && echo ok)
endef
# Yocto mode: bitbake passes CC/CXX/AR/LD/STRIP/RANLIB on the make command line
# with full paths and machine flags baked in. Skip host-toolchain autodetect
# and ccache wrapping — bitbake manages its own caching upstream.
ifneq ($(PLATFORM_TARGET),yocto)
ifeq ($(origin CC),default)
ifneq ($(shell command -v clang 2>/dev/null),)
CC := clang
else ifneq ($(shell command -v gcc 2>/dev/null),)
CC := gcc
else
$(error No C compiler found. Install clang or gcc)
endif
endif
ifeq ($(origin CXX),default)
# Try clang++ first
ifneq ($(shell command -v clang++ 2>/dev/null),)
# Test if clang++ can actually compile C++ with stdlib
ifeq ($(call test_cxx_stdlib,clang++),ok)
CXX := clang++
else ifneq ($(shell command -v g++ 2>/dev/null),)
# clang selects the newest GCC installation it can find and takes
# libstdc++ from there, so a gcc-N without a matching libstdc++-N-dev
# leaves it unable to link. Usually fixable, and worth fixing: the
# sanitizer jobs are clang, so a working local clang is the only way
# to reproduce them here.
CXX := g++
CC := gcc
$(info Note: clang++ cannot link C++ here, using g++ instead)
$(info Usually a missing libstdc++-<N>-dev for the newest installed gcc.)
$(info Diagnose: clang++ -v -xc++ /dev/null -o /dev/null 2>&1 | grep "Selected GCC")
$(info Fix: install that libstdc++-<N>-dev, or pass --gcc-install-dir=/usr/lib/gcc/<triple>/<N>)
else
# No g++ available, try clang++ anyway and let it fail with a clear error
CXX := clang++
endif
else ifneq ($(shell command -v g++ 2>/dev/null),)
CXX := g++
else
$(error No C++ compiler found. Install clang++ or g++)
endif
endif
# Set RANLIB if not defined (needed for wpa_supplicant build on Linux)
ifeq ($(origin RANLIB),undefined)
RANLIB := ranlib
endif
# Ccache integration - auto-detect and use if available (10x faster rebuilds)
CCACHE := $(shell command -v ccache 2>/dev/null)
ifneq ($(CCACHE),)
# Avoid double-wrapping when CC/CXX already include ccache (common in CI env).
ifneq ($(notdir $(firstword $(CC))),ccache)
CC := ccache $(CC)
endif
ifneq ($(notdir $(firstword $(CXX))),ccache)
CXX := ccache $(CXX)
endif
endif
endif # PLATFORM_TARGET != yocto
# Dependency generation flags for proper header tracking
# -MMD: Generate .d dependency files for user headers (not system headers)
# -MP: Add phony targets for headers (prevents errors when headers are deleted)
# Note: -MF path is computed in the pattern rules to get the correct output path
DEPFLAGS = -MMD -MP
# Optimization level: -O2 by default, override with OPT=0 or OPT=1 for faster builds
# 'make dev' sets OPT=0 automatically (~2x faster compilation)
OPT ?= 2
# Debug info level. ASAN links of this tree need multiple GB at -g2; DBGLEVEL=0
# keeps the symbol table (which is what ASAN frames resolve from) without DWARF.
DBGLEVEL ?= 2
# Project source flags - warnings enabled, strict mode optional
# Use WERROR=1 to treat warnings as errors (for CI or `make strict`)
# -D_FORTIFY_SOURCE=2: compile-time + runtime bounds checking for memcpy/sprintf/etc
# -fstack-protector-strong: stack canaries on functions with local arrays/alloca
# Note: _FORTIFY_SOURCE requires optimization (-O1+), so disable it at -O0
#
# Yocto mode: bitbake's CFLAGS/CXXFLAGS already include --sysroot, -march,
# optimization, and security flags. Append our project flags with += instead
# of clobbering. -Wno-psabi silences the ARM parameter-passing ABI-change
# notes that appear on cortex-a7 hard-float cross builds.
ifeq ($(PLATFORM_TARGET),yocto)
CFLAGS += -std=c11 -Wall -Wextra -D_GNU_SOURCE -Wno-psabi
CXXFLAGS += -std=c++17 -Wall -Wextra -Wno-psabi
else
# DWARF is emitted per translation unit with no cross-TU dedup, so every TU
# that includes Catch2, LVGL, nlohmann::json or spdlog carries its own copy
# of those type descriptions and .debug_info dwarfs the code it describes.
# -gz compresses the debug sections in place, shrinking objects and binaries
# so more of a build fits in ccache. It is orthogonal to DBGLEVEL, which
# picks how much DWARF is emitted, not how it is stored.
#
# -fdebug-types-section is not used: its per-type-unit overhead grows the
# object tree by more than the dedup saves at this TU count.
#
# Probed because this block also feeds the cross toolchains (pi, ad5m, cc1),
# where an unsupported flag would break the build outright.
DEBUG_SIZE_FLAGS := $(shell printf 'int main(){return 0;}' | \
$(CXX) -x c++ -gz -c -o /dev/null - >/dev/null 2>&1 && echo -gz)
CFLAGS := -std=c11 -Wall -Wextra -O$(OPT) -g$(DBGLEVEL) $(DEBUG_SIZE_FLAGS) -D_GNU_SOURCE -fno-omit-frame-pointer -fstack-protector-strong
CXXFLAGS := -std=c++17 -Wall -Wextra -O$(OPT) -g$(DBGLEVEL) $(DEBUG_SIZE_FLAGS) -fno-omit-frame-pointer -fstack-protector-strong
ifneq ($(OPT),0)
CFLAGS += -D_FORTIFY_SOURCE=2
CXXFLAGS += -D_FORTIFY_SOURCE=2
endif
endif
# Version information (read from VERSION.txt file)
# Format: MAJOR.MINOR.PATCH following Semantic Versioning 2.0.0
# NOTE: Must use .txt extension to avoid shadowing C++20 <version> header on macOS
# (macOS filesystem is case-insensitive, so VERSION would match <version>)
HELIX_VERSION := $(shell cat VERSION.txt 2>/dev/null || echo "0.0.0")
# The numeric fields come from the version's core triple, with any prerelease or
# build suffix cut off first. Splitting the full string on "." puts "0-beta" in
# the patch field, and that reaches every translation unit as
# -DHELIX_VERSION_PATCH=0-beta, where helix_version_at_least() compares it as an
# integer and nothing compiles.
HELIX_VERSION_CORE := $(firstword $(subst +, ,$(firstword $(subst -, ,$(HELIX_VERSION)))))
HELIX_VERSION_MAJOR := $(word 1,$(subst ., ,$(HELIX_VERSION_CORE)))
HELIX_VERSION_MINOR := $(word 2,$(subst ., ,$(HELIX_VERSION_CORE)))
HELIX_VERSION_PATCH := $(word 3,$(subst ., ,$(HELIX_VERSION_CORE)))
# The short git hash is produced by scripts/gen-git-hash.sh into a generated
# header, not read here: a make variable would be a second source of the same
# value, free to drift from the one the binary actually reports.
# Installer script filename (single source of truth for Makefile packaging + C++ extraction)
INSTALLER_FILENAME := install.sh
# Add version defines to compiler flags.
#
# HELIX_GIT_HASH is deliberately NOT here. Everything in this list lands on
# every translation unit's command line, and ccache's direct mode hashes that
# command line, so a value that changes every commit invalidates the whole
# project's cache on every push. The rest only move at release, which is why
# they are safe to keep global. The hash goes through a generated header that
# reaches one object instead. See scripts/gen-git-hash.sh and $(GIT_HASH_H).
VERSION_DEFINES := -DHELIX_VERSION=\"$(HELIX_VERSION)\" \
-DHELIX_VERSION_MAJOR=$(HELIX_VERSION_MAJOR) \
-DHELIX_VERSION_MINOR=$(HELIX_VERSION_MINOR) \
-DHELIX_VERSION_PATCH=$(HELIX_VERSION_PATCH) \
-DINSTALLER_FILENAME=\"$(INSTALLER_FILENAME)\"
CFLAGS += $(VERSION_DEFINES)
CXXFLAGS += $(VERSION_DEFINES)
# Strict mode: -Werror plus additional useful warnings
ifeq ($(WERROR),1)
CFLAGS += -Werror -Wconversion -Wshadow -Wno-error=deprecated-declarations
CXXFLAGS += -Werror -Wconversion -Wshadow -Wno-error=deprecated-declarations
endif
# Address Sanitizer support for debugging heap corruption
# Usage: make SANITIZE=address (native), or `make pi-asan-docker` (cross).
# We only DEFINE the flag variable here; mk/cross.mk applies it after
# TARGET_CFLAGS and SUBMODULE_CFLAGS have been merged so LVGL, helix-xml,
# and other in-tree subprojects get instrumented too. _FORTIFY_SOURCE is
# disabled in this mode because it conflicts with ASAN's libc wrappers.
ifeq ($(SANITIZE),address)
SANITIZE_FLAGS := -fsanitize=address -fno-omit-frame-pointer
CFLAGS := $(filter-out -D_FORTIFY_SOURCE=2,$(CFLAGS))
CXXFLAGS := $(filter-out -D_FORTIFY_SOURCE=2,$(CXXFLAGS))
endif
# Thread Sanitizer, for the other half of the problem space: ASAN finds the
# corruption, TSAN finds the unsynchronized access that caused it. Added while
# chasing #1198, whose glibc abort is detected during a ThumbnailCache eviction
# that runs on both the main thread and HttpExecutor workers (#1202).
#
# Usage: make SANITIZE=thread
#
# NOT combinable with SANITIZE=address — the two runtimes are mutually
# exclusive, and asking for both silently gets you neither.
ifeq ($(SANITIZE),thread)
SANITIZE_FLAGS := -fsanitize=thread -fno-omit-frame-pointer
CFLAGS := $(filter-out -D_FORTIFY_SOURCE=2,$(CFLAGS))
CXXFLAGS := $(filter-out -D_FORTIFY_SOURCE=2,$(CXXFLAGS))
endif
# Submodule flags - suppress warnings from third-party code we don't control
# Uses -w to completely silence warnings (cleaner build output)
# Note: No DEPFLAGS for submodules - we don't track their internal dependencies
SUBMODULE_CFLAGS := -std=c11 -O2 -g$(DBGLEVEL) -D_GNU_SOURCE -w
SUBMODULE_CXXFLAGS := -std=c++17 -O2 -g$(DBGLEVEL) -w
# XML create-cost profiling in lib/helix-xml (lv_xml.c). Counts component
# creates and separates expat/SAX time from element-handler time, which is the
# only way to tell parsing apart from widget building: XML_Parse drives the
# handlers, so one timer around it measures both.
#
# Dev-build only, and OFF even there. It reads the clock on every element, which
# distorts what it is measuring, and logs every 25 creates. Turn it on for a
# measurement run, not for everyday work:
# make ENABLE_XML_PROFILE=yes
#
# make does not track compiler flags, so toggling this rebuilds nothing on its
# own — delete the object too:
# rm -f build/obj/helix-xml/src/xml/lv_xml.o
ENABLE_XML_PROFILE ?= no
ifeq ($(ENABLE_XML_PROFILE),yes)
SUBMODULE_CFLAGS += -DLV_XML_PROFILE=1
endif
# Platform detection (needed early for conditional compilation)
UNAME_S := $(shell uname -s)
# Cross-compilation support (must come early to override CC/CXX)
# Use TARGET=pi or TARGET=ad5m for cross-compilation
include mk/cross.mk
# Font tier lists (must come after cross.mk sets FONT_TIERS, before FONT_SRCS)
include mk/fonts.mk
# Remote build support (build on fast Linux host, retrieve binaries)
include mk/remote.mk
# Directories
SRC_DIR := src
INC_DIR := include
# BUILD_DIR, BIN_DIR, OBJ_DIR may be set by cross.mk for cross-compilation
# Only set defaults if not already defined
BUILD_DIR ?= build
# Native sanitizer builds need their own object tree, the same way cross.mk
# suffixes BUILD_SUBDIR with -asan/-tsan. Without this, `make SANITIZE=address`
# on an already-built tree finds every object up to date and only RELINKS them:
# the result is a binary with zero __asan symbols that passes everything,
# because it was never instrumented. That silently cost a full debugging pass
# on prestonbrown/helixscreen#960.
#
# BIN_DIR is deliberately NOT suffixed. mk/tests.mk computes TEST_ASAN_BIN from
# BIN_DIR in the PARENT make (where SANITIZE is unset) and passes only
# OBJ_DIR down, so moving BIN_DIR here would have the sub-make build into a
# directory the parent is not looking in. The app binary is therefore still
# replaced in place — but it is genuinely instrumented, which is the part that
# matters. Pass BIN_DIR=... explicitly to keep both.
ifeq ($(SANITIZE),address)
OBJ_DIR ?= $(BUILD_DIR)/obj-asan
endif
ifeq ($(SANITIZE),thread)
OBJ_DIR ?= $(BUILD_DIR)/obj-tsan
endif
# Coverage builds need their own object tree for exactly the reason above: an
# uninstrumented object that is already up to date would be relinked, not
# recompiled, and it emits no .gcda at all. The resulting report reads as "these
# lines were never executed" for every file make decided to skip.
ifeq ($(COVERAGE),1)
OBJ_DIR ?= $(BUILD_DIR)/obj-cov
endif
BIN_DIR ?= $(BUILD_DIR)/bin
OBJ_DIR ?= $(BUILD_DIR)/obj
# Objects are invalidated by a change to the flags they were built with, not only
# by a change to their sources. The path is fixed here beside OBJ_DIR because the
# rules naming it are read before CXXFLAGS is final; mk/rules.mk writes contents.
FLAGS_STAMP := $(OBJ_DIR)/.build-flags
# Created empty here, before the first rule naming it is parsed. A prerequisite
# that neither exists nor has a rule makes make discard the whole pattern rule,
# and every one of the 15 rules naming this stamp shares that one file record --
# so an object not already on disk reports "No rule to make target" instead. Only
# a build with no $(OBJ_DIR) yet is affected, which is why CI sees it and an
# incremental tree never does. mk/rules.mk fills in the flag text below.
$(shell mkdir -p $(OBJ_DIR); [ -e $(FLAGS_STAMP) ] || : > $(FLAGS_STAMP))
# LVGL
LVGL_DIR := lib/lvgl
# LVGL config discovery. Defined here (not further down) so it can travel inside
# $(LVGL_INC): every flag set that compiles LVGL-dependent code then reaches
# lv_conf.h via -I. (project root) + -DLV_CONF_INCLUDE_SIMPLE, instead of LVGL's
# fragile '#include "../../lv_conf.h"' fallback. That fallback resolves only by
# accident in a normal checkout and not at all wherever lib/lvgl does not sit two
# levels below the tree holding lv_conf.h, which takes out the splash,
# display-backend and watchdog sub-builds with it.
# Carrying it in LVGL_INC means new LVGL sub-builds inherit correct discovery
# automatically rather than each having to remember to append $(LV_CONF).
LV_CONF := -DLV_CONF_INCLUDE_SIMPLE
# Use -isystem to suppress warnings from third-party headers in strict mode
LVGL_INC := -isystem $(LVGL_DIR) -isystem $(LVGL_DIR)/src -I. $(LV_CONF)
# Add GLAD include path for desktop OpenGL ES (SDL) builds only.
# DRM+EGL builds use system EGL/GLES2 headers directly — GLAD's stub headers
# shadow them (define include guards but emit no types when LV_USE_OPENGLES=0).
ifeq ($(ENABLE_OPENGLES),yes)
ifneq ($(DISPLAY_BACKEND),drm)
LVGL_INC += -isystem $(LVGL_DIR)/src/drivers/opengles/glad/include
endif
endif
# Exclude XML and expat sources from LVGL — those are now in lib/helix-xml
LVGL_SRCS := $(filter-out $(wildcard $(LVGL_DIR)/src/xml/*.c $(LVGL_DIR)/src/xml/parsers/*.c $(LVGL_DIR)/src/libs/expat/*.c),$(shell find $(LVGL_DIR)/src -name "*.c" 2>/dev/null))
LVGL_OBJS := $(patsubst $(LVGL_DIR)/%.c,$(OBJ_DIR)/lvgl/%.o,$(LVGL_SRCS))
# When OpenGL ES is enabled, the shader asset file uses C++11 raw string literals
# (R"(...)") that can't compile as C. Filter only that file from C sources and compile
# it as C++ separately. All other opengles sources stay as C.
ifeq ($(ENABLE_OPENGLES),yes)
LVGL_OPENGLES_CXX_SRCS := $(filter $(LVGL_DIR)/src/drivers/opengles/assets/%,$(LVGL_SRCS))
LVGL_SRCS := $(filter-out $(LVGL_DIR)/src/drivers/opengles/assets/%,$(LVGL_SRCS))
LVGL_OBJS := $(patsubst $(LVGL_DIR)/%.c,$(OBJ_DIR)/lvgl/%.o,$(LVGL_SRCS))
LVGL_OPENGLES_OBJS := $(patsubst $(LVGL_DIR)/%.c,$(OBJ_DIR)/lvgl/%.o,$(LVGL_OPENGLES_CXX_SRCS))
else
LVGL_OPENGLES_OBJS :=
endif
# Helix XML engine (extracted from LVGL, with our patches baked in)
HELIX_XML_DIR := lib/helix-xml
HELIX_XML_SRCS := $(wildcard $(HELIX_XML_DIR)/src/xml/*.c $(HELIX_XML_DIR)/src/xml/parsers/*.c $(HELIX_XML_DIR)/src/libs/expat/*.c)
HELIX_XML_OBJS := $(patsubst $(HELIX_XML_DIR)/%.c,$(OBJ_DIR)/helix-xml/%.o,$(HELIX_XML_SRCS))
# lv_markdown (LVGL markdown widget)
LV_MARKDOWN_DIR := lib/lv_markdown
LV_MARKDOWN_INC := -isystem $(LV_MARKDOWN_DIR)/src -isystem $(LV_MARKDOWN_DIR)/deps/md4c
LV_MARKDOWN_SRCS := $(wildcard $(LV_MARKDOWN_DIR)/src/*.c) $(LV_MARKDOWN_DIR)/deps/md4c/md4c.c
LV_MARKDOWN_OBJS := $(patsubst $(LV_MARKDOWN_DIR)/%.c,$(OBJ_DIR)/lv_markdown/%.o,$(LV_MARKDOWN_SRCS))
# quirc (QR code decoder library)
QUIRC_DIR := lib/quirc
QUIRC_INC := -isystem $(QUIRC_DIR)/lib
QUIRC_SRCS := $(wildcard $(QUIRC_DIR)/lib/*.c)
QUIRC_OBJS := $(patsubst $(QUIRC_DIR)/%.c,$(OBJ_DIR)/quirc/%.o,$(QUIRC_SRCS))
# ThorVG sources (.cpp files for SVG support)
THORVG_SRCS := $(shell find $(LVGL_DIR)/src/libs/thorvg -name "*.cpp" 2>/dev/null)
THORVG_OBJS := $(patsubst $(LVGL_DIR)/%.cpp,$(OBJ_DIR)/lvgl/%.o,$(THORVG_SRCS))
# cpp-terminal (modern TUI library)
CPP_TERMINAL_DIR := lib/cpp-terminal
# Use -isystem to suppress warnings from third-party headers in strict mode
CPP_TERMINAL_INC := -isystem $(CPP_TERMINAL_DIR)
CPP_TERMINAL_SRCS := $(wildcard $(CPP_TERMINAL_DIR)/cpp-terminal/*.cpp) \
$(wildcard $(CPP_TERMINAL_DIR)/cpp-terminal/private/*.cpp)
CPP_TERMINAL_OBJS := $(patsubst $(CPP_TERMINAL_DIR)/%.cpp,$(OBJ_DIR)/cpp-terminal/%.o,$(CPP_TERMINAL_SRCS))
# lv_markdown (markdown viewer widget + md4c parser)
LV_MARKDOWN_DIR := lib/lv_markdown
LV_MARKDOWN_INC := -isystem $(LV_MARKDOWN_DIR)/src -isystem $(LV_MARKDOWN_DIR)/deps/md4c
LV_MARKDOWN_SRCS := $(wildcard $(LV_MARKDOWN_DIR)/src/*.c) $(LV_MARKDOWN_DIR)/deps/md4c/md4c.c
LV_MARKDOWN_OBJS := $(patsubst $(LV_MARKDOWN_DIR)/%.c,$(OBJ_DIR)/lv_markdown/%.o,$(LV_MARKDOWN_SRCS))
# LVGL Demos (separate target)
LVGL_DEMO_SRCS := $(shell find $(LVGL_DIR)/demos -name "*.c" 2>/dev/null)
LVGL_DEMO_OBJS := $(patsubst $(LVGL_DIR)/%.c,$(OBJ_DIR)/lvgl/%.o,$(LVGL_DEMO_SRCS))
# 3D G-code Rendering default (must be set before APP_SRCS filtering below)
# EGL/GLES is only available on Linux — disable on macOS
ifeq ($(UNAME_S),Darwin)
ENABLE_GLES_3D ?= no
else
ENABLE_GLES_3D ?= yes
endif
# Screensaver default (enabled on desktop/Pi, disabled on constrained targets)
ENABLE_SCREENSAVER ?= yes
# Application C sources
APP_C_SRCS := $(wildcard $(SRC_DIR)/*.c)
APP_C_OBJS := $(patsubst $(SRC_DIR)/%.c,$(OBJ_DIR)/%.o,$(APP_C_SRCS))
# Application C++ sources (exclude test binaries, splash binary, and lvgl-demo)
# Include all subdirectories: ui/, api/, rendering/, printer/, print/, system/, application/
APP_SRCS := $(filter-out $(SRC_DIR)/test_dynamic_cards.cpp $(SRC_DIR)/test_responsive_theme.cpp $(SRC_DIR)/test_gcode_geometry.cpp $(SRC_DIR)/test_gcode_analysis.cpp $(SRC_DIR)/test_sdf_reconstruction.cpp $(SRC_DIR)/test_sparse_grid.cpp $(SRC_DIR)/test_partial_extraction.cpp $(SRC_DIR)/test_render_comparison.cpp $(SRC_DIR)/test_network_tester.cpp $(SRC_DIR)/helix_splash.cpp $(SRC_DIR)/helix_watchdog.cpp $(SRC_DIR)/lvgl-demo/main.cpp,$(wildcard $(SRC_DIR)/*.cpp) $(wildcard $(SRC_DIR)/*/*.cpp) $(wildcard $(SRC_DIR)/*/*/*.cpp))
# Exclude src/tools/ — standalone build tools have their own rules in tools.mk
APP_SRCS := $(filter-out $(wildcard $(SRC_DIR)/tools/*.cpp),$(APP_SRCS))
# Exclude src/bluetooth/ — built as separate shared library (mk/bluetooth.mk)
APP_SRCS := $(filter-out $(wildcard $(SRC_DIR)/bluetooth/*.cpp),$(APP_SRCS))
# Exclude GLES renderer when not enabled
ifneq ($(ENABLE_GLES_3D),yes)
APP_SRCS := $(filter-out $(SRC_DIR)/rendering/gcode_gles_renderer.cpp,$(APP_SRCS))
endif
# Screensaver sources: every saver, the parts they share and the manager
SCREENSAVER_SRCS := $(SRC_DIR)/ui/ui_screensaver.cpp $(wildcard $(SRC_DIR)/ui/screensaver_*.cpp)
# Targets whose lv_conf.h LV_COLOR_DEPTH is 16. The savers below static_assert 32 bpp, so a
# 16 bpp target missing here fails to compile rather than shipping them.
SCREENSAVER_16BPP_TARGETS := ad5m ad5m-br cc1 mips k1 k1-dynamic ad5x k2 snapmaker-u1
# Every saver now draws at both depths, so a 16 bpp build compiles all of them. A saver that
# cannot would be listed here and marked SAVER_DEPTH_32 in SCREENSAVERS; the two must agree.
SCREENSAVER_32BPP_ONLY_SRCS :=
ifneq ($(ENABLE_SCREENSAVER),yes)
APP_SRCS := $(filter-out $(SCREENSAVER_SRCS),$(APP_SRCS))
else ifneq ($(filter $(PLATFORM_TARGET),$(SCREENSAVER_16BPP_TARGETS)),)
APP_SRCS := $(filter-out $(SCREENSAVER_32BPP_ONLY_SRCS),$(APP_SRCS))
endif
# Mock backends (the --test simulator). Dev/test scaffolding: ON for every developer
# build, OFF under HELIX_PACKAGING=1 so released binaries do not carry it. Targets
# in mk/cross.mk that set ENABLE_MOCKS := no keep it off for dev builds too.
ifeq ($(HELIX_PACKAGING),1)
ENABLE_MOCKS ?= no
else
ENABLE_MOCKS ?= yes
endif
# PWM sysfs buzzer backend — ad5m/ad5m-br probe it; the generic SBC builds
# (pi, pi32, x86, yocto, native) carry it for a sound.pwm_channel setting, a
# buzzer the user wired to any board's hardware PWM pin.
#
# The backend's own runtime probe is just "does /sys/class/pwm/pwmchip0 exist",
# which is true on boards whose PWM controller drives something else entirely: a
# CC1 has 8 channels there, no beeper on any of them, and its backlight on the
# same controller. So the probe cannot be trusted to decide this — the platform
# must, or the user naming the channel. M300 (the PRINTER's beeper, over gcode) is deliberately NOT gated and
# keeps working everywhere. Neither MIPS board exposes a sysfs pwmchip (the
# FlashForge MIPS boards' piezo goes through jz_pwm below, the K1 has no audio at all).
#
# Decided HERE, above APP_OBJS, and not down in the sound-flags section: APP_OBJS
# is computed from APP_SRCS a few lines below, so a filter-out placed after it is
# a silent no-op that still compiles and still links the backend.
ifneq (,$(filter ad5m ad5m-br pi pi-fbdev pi-both pi32 pi32-fbdev pi32-both x86 x86-fbdev x86-both yocto native,$(PLATFORM_TARGET)))
PWM_SOUND_CXXFLAGS := -DHELIX_HAS_PWM_SOUND
# Auto-export: the stock AD5M kernel ships the beeper channel unexported
# and nothing materializes pwm6, so initialize() writes the channel to
# pwmchip0/export first. ad5x is excluded: pwm6 unverified there, no test
# rig, large installed base - behavior must not change silently.
ifneq (,$(filter ad5m ad5m-br,$(PLATFORM_TARGET)))
PWM_AUTO_EXPORT_CXXFLAGS := -DHELIX_PWM_AUTO_EXPORT
endif
else
APP_SRCS := $(filter-out $(SRC_DIR)/system/pwm_sound_backend.cpp,$(APP_SRCS))
endif
# The FlashForge MIPS boards' piezo via the jz_pwm DMA engine, exec'd through fx-pwm - there is no
# sysfs pwmchip on the X2600, so the sysfs PWM backend above cannot drive it.
# Compiled into the unified MIPS build and probed at runtime (/dev/jz_pwm + the
# fx-pwm binary): the K1 carries /dev/jz_pwm but no fx-pwm, so the probe fails
# there and the ladder falls through; a host build that happens to carry the
# binary simply falls through too, and remote-UI installs keep the M300 path.
ifneq (,$(filter mips k1 ad5x,$(PLATFORM_TARGET)))
JZ_PWM_CXXFLAGS := -DHELIX_HAS_JZ_PWM
else
APP_SRCS := $(filter-out $(SRC_DIR)/system/jz_pwm_sound_backend.cpp,$(APP_SRCS))
endif
ifneq ($(ENABLE_MOCKS),yes)
APP_SRCS := $(filter-out $(wildcard $(SRC_DIR)/api/*_mock*.cpp),$(APP_SRCS))
APP_SRCS := $(filter-out $(SRC_DIR)/printer/ams_backend_mock.cpp,$(APP_SRCS))
APP_SRCS := $(filter-out $(SRC_DIR)/api/moonraker_api_mock.cpp,$(APP_SRCS))
APP_SRCS := $(filter-out $(SRC_DIR)/system/mock_performance_source.cpp,$(APP_SRCS))
endif
# Remote-control subsystem (helixctl server + socket/HTTP transport + the folded
# `ctl`/`repl` client).
#
# ON for every DEVELOPER build, native or cross. A device binary you cannot
# drive from your desk means walking to the printer for every check, and the
# cross targets are how test rigs get built. OFF only for the PRODUCTION
# packaging/release builds, so shipped devices neither build it nor pay the
# overhead — that is what HELIX_PACKAGING marks. mk/cross.mk sets it on the
# package-* targets; CI's release workflow passes it on the command line.
#
# Compiled in but left off is not a state worth having: the server only listens
# under --remote / HELIX_REMOTE_CONTROL=1, so `make deploy-*` reads the stamp
# mk/rules.mk writes beside the binary and flips the device switch to match.
#
# Override either way on the command line:
# make PLATFORM_TARGET=pi ENABLE_REMOTE_CONTROL=no
ifeq ($(HELIX_PACKAGING),1)
ENABLE_REMOTE_CONTROL ?= no
else
ENABLE_REMOTE_CONTROL ?= yes
endif
ifeq ($(ENABLE_REMOTE_CONTROL),yes)
# The helixctl client is folded into helix-screen (src/remote/remote_client.cpp,
# reached via the `ctl`/`repl` subcommands); it needs linenoise for the REPL.
REMOTE_LINENOISE_OBJ := $(OBJ_DIR)/linenoise.o
else
REMOTE_LINENOISE_OBJ :=
APP_SRCS := $(filter-out $(wildcard $(SRC_DIR)/remote/*.cpp),$(APP_SRCS))
endif
# Diagnostic-upload gate (prestonbrown/helixscreen#1410): whether this build may
# ship debug bundles / crash reports to crash.helixscreen.org, or serve the log
# ring over the `ctl log` RPC. Official packaging builds (HELIX_PACKAGING=1)
# upload — the installed fleet is the telemetry. Every other build — plain
# local make, fork cross-builds — defaults OFF; our own dev rigs are turned on
# at deploy time instead (sync-device-features stamps
# HELIX_DIAGNOSTIC_UPLOADS=1 in the device env), because nothing build-side can
# tell our rig build from a source-modified fork build. A dev build can also be
# opted in for a run with HELIX_DIAGNOSTIC_UPLOADS=1 in the environment.
#
# The define only flips the runtime DEFAULT of helix::diag::uploads_enabled()
# and the payload marker; it never compiles the pipes out, so the opt-in needs
# no rebuild.
ifeq ($(HELIX_PACKAGING),1)
ENABLE_DIAGNOSTIC_UPLOADS ?= yes
else
ENABLE_DIAGNOSTIC_UPLOADS ?= no
endif
# Developer-only showcase panel. Not reachable from the shipped navigation
# (no PanelId, no PanelFactory wiring) — it exists as a live testbed for
# icon-font coverage (glyphs_panel). Dev-only: default ON for the native dev build, OFF for
# release/cross builds. Force into a device dev image with:
# make PLATFORM_TARGET=pi ENABLE_DEV_PANELS=yes
ifeq ($(PLATFORM_TARGET),native)
ENABLE_DEV_PANELS ?= yes
else
ENABLE_DEV_PANELS ?= no
endif
ifneq ($(ENABLE_DEV_PANELS),yes)
APP_SRCS := $(filter-out $(SRC_DIR)/ui/ui_panel_glyphs.cpp,$(APP_SRCS))
endif
APP_OBJS := $(patsubst $(SRC_DIR)/%.cpp,$(OBJ_DIR)/%.o,$(APP_SRCS))
# Linenoise (bundled line-editing library) for the folded REPL. Built as C99 with
# _GNU_SOURCE so strdup/fchmod/fileno are declared — without it they are implicit
# int, truncating strdup's 64-bit pointer to 32 bits (corrupt history -> SIGSEGV).
$(OBJ_DIR)/linenoise.o: lib/linenoise/linenoise.c
$(Q)mkdir -p $(dir $@)
$(ECHO) "$(BLUE)[CC]$(RESET) $<"
$(Q)$(CC) -std=c99 -D_GNU_SOURCE -Wall -Os -c $< -o $@
# libhv dns_resolv.c — needed by safe_resolve.h on statically-linked ARM/MIPS
# targets (avoids glibc __check_pf SIGSEGV). Only compiled for cross builds.
# Uses APP_ prefix to avoid collision with DNS_RESOLV_OBJ in tests.mk.
ifneq ($(CROSS_COMPILE),)
APP_DNS_RESOLV_OBJ := $(OBJ_DIR)/dns_resolv.o
else
APP_DNS_RESOLV_OBJ :=
endif
# Objective-C++ sources (macOS only - .mm files)
# Only include on macOS, exclude on Linux to avoid linking errors
ifeq ($(UNAME_S),Darwin)
OBJCPP_SRCS := $(wildcard $(SRC_DIR)/*.mm)
OBJCPP_OBJS := $(patsubst $(SRC_DIR)/%.mm,$(OBJ_DIR)/%.o,$(OBJCPP_SRCS))
else
OBJCPP_SRCS :=
OBJCPP_OBJS :=
endif
# Font sources — assembled per-platform from mk/fonts.mk tier lists
# FONT_TIERS is set per-platform in mk/cross.mk (default: all)
FONT_SRCS := $(TIER_FONT_SRCS)
FONT_OBJS := $(patsubst %.c,$(OBJ_DIR)/%.o,$(FONT_SRCS))
# Material Design Icons - REMOVED
# Icons are now font-based using MDI font glyphs (mdi_icons_*.c)
# See include/ui_icon_codepoints.h for icon mapping
# SDL2 - Only needed for native desktop builds (not embedded targets)
# Cross-compilation targets use framebuffer/DRM instead
ifeq ($(ENABLE_SDL),yes)
SDL2_SYSTEM_AVAILABLE := $(shell command -v sdl2-config 2>/dev/null)
ifneq ($(SDL2_SYSTEM_AVAILABLE),)
# System SDL2 found - use it
SDL2_INC := $(shell sdl2-config --cflags)
SDL2_LIBS := $(shell sdl2-config --libs)
SDL2_LIB :=
else
# No system SDL2 - build from submodule. The build dir lives under
# BUILD_DIR: lib/sdl2 is one checkout shared by every worktree, and the
# desktop release and a native build configure SDL differently.
SDL2_DIR := lib/sdl2
SDL2_BUILD_DIR := $(BUILD_DIR)/sdl2
SDL2_LIB := $(SDL2_BUILD_DIR)/libSDL2.a
SDL2_INC := -I$(SDL2_DIR)/include -I$(SDL2_BUILD_DIR)/include -I$(SDL2_BUILD_DIR)/include-config-release
SDL2_LIBS := $(SDL2_LIB)
# A static libSDL2 leaves its own system dependencies to the final link;
# glibc before 2.34 keeps shm_open and clock_* in librt.
ifeq ($(UNAME_S),Linux)
SDL2_LIBS += -lrt
endif
endif
else
# Embedded target - no SDL2
SDL2_INC :=
SDL2_LIBS :=
SDL2_LIB :=
endif
# libhv (WebSocket client for Moonraker) - Use system version if available, otherwise build from submodule
# LIBHV_DIR always points to the submodule — needed for internal headers (e.g. base/dns_resolv.h)
LIBHV_DIR := lib/libhv
LIBHV_PKG_CONFIG := $(shell pkg-config --exists libhv 2>/dev/null && echo "yes")
ifeq ($(YOCTO_BUILD),yes)
# Yocto: libhv comes from DEPENDS (sysroot include/link paths baked into CC).
# Submodule path supplies internal headers (base/dns_resolv.h, cpputil/ifconfig.h)
# not shipped by the libhv public install.
LIBHV_INC := -isystem $(LIBHV_DIR)/include -isystem $(LIBHV_DIR)/cpputil -isystem $(LIBHV_DIR)
LIBHV_LIBS := -lhv
LIBHV_LIB :=
else ifeq ($(LIBHV_PKG_CONFIG),yes)
# System libhv found via pkg-config (pkg-config already returns system include paths)
# Also add submodule path for internal headers used by tests
LIBHV_INC := $(shell pkg-config --cflags libhv) -isystem $(LIBHV_DIR)
LIBHV_LIBS := $(shell pkg-config --libs libhv)
LIBHV_LIB :=
else
# No system libhv - build from submodule to $(BUILD_DIR)/lib/ for architecture isolation
# This allows concurrent native/pi/ad5m builds without conflicts
# Use -isystem to suppress warnings from third-party headers in strict mode
LIBHV_INC := -isystem $(LIBHV_DIR)/include -isystem $(LIBHV_DIR)/cpputil -isystem $(LIBHV_DIR)
LIBHV_LIB := $(BUILD_DIR)/lib/libhv.a
LIBHV_LIBS := $(LIBHV_LIB)
endif
# libhv generates include/hv headers during libhv-build. Track json.hpp so a
# stale archive cannot be reused when generated headers are missing.
ifneq ($(LIBHV_LIB),)
LIBHV_JSON_HEADER := $(LIBHV_DIR)/include/hv/json.hpp
else
LIBHV_JSON_HEADER :=
endif
# spdlog (logging library) - Use system version if available, otherwise use submodule
# Check for actual header file to avoid $(dir) path issues with directory paths
SPDLOG_SYSTEM_HEADER_PATHS := /usr/include/spdlog/spdlog.h /usr/local/include/spdlog/spdlog.h /opt/homebrew/include/spdlog/spdlog.h
SPDLOG_SYSTEM_HEADER := $(firstword $(wildcard $(SPDLOG_SYSTEM_HEADER_PATHS)))
ifneq ($(SPDLOG_SYSTEM_HEADER),)
# System spdlog found
# /usr/include and /usr/local/include are in compiler defaults - no -isystem needed
# /opt/homebrew/include is NOT in macOS compiler defaults - needs explicit -isystem
ifeq ($(SPDLOG_SYSTEM_HEADER),/opt/homebrew/include/spdlog/spdlog.h)
SPDLOG_INC := -isystem /opt/homebrew/include
else
# /usr/include or /usr/local/include - compiler already searches these
SPDLOG_INC :=
endif
else
# No system spdlog - use submodule
SPDLOG_DIR := lib/spdlog
# Use -isystem to suppress warnings from third-party headers in strict mode
SPDLOG_INC := -isystem $(SPDLOG_DIR)/include
# Compiled once from lib/spdlog/src and linked into every binary, instead of
# header-only code instantiated in every translation unit. A system spdlog
# above stays header-only.
SPDLOG_DEFINES := -DSPDLOG_COMPILED_LIB
SPDLOG_OBJS := $(patsubst $(SPDLOG_DIR)/src/%.cpp,$(OBJ_DIR)/spdlog/%.o,$(wildcard $(SPDLOG_DIR)/src/*.cpp))
endif
# fmt (formatting library required by header-only spdlog)
# For cross-compilation, we can't use host pkg-config - must detect target library directly
ifeq ($(YOCTO_BUILD),yes)
# Yocto: libfmt from DEPENDS, sysroot-aware linker resolves -lfmt.
FMT_LIBS := -lfmt
else ifneq ($(CROSS_COMPILE),)
# Cross-compiling: check if target fmt library exists (installed via libfmt-dev:arm64 etc.)
FMT_TARGET_LIB := $(shell ls /usr/lib/$(TARGET_TRIPLE)/libfmt.so 2>/dev/null || ls /usr/lib/$(TARGET_TRIPLE)/libfmt.a 2>/dev/null)
ifneq ($(FMT_TARGET_LIB),)
ifeq ($(PLATFORM_TARGET),pi)
# Pi: skip external fmt — spdlog bundles its own, and linking against
# system libfmt causes soname mismatches across Debian versions
# (Bullseye ships libfmt.so.7, Bookworm ships libfmt.so.9)
FMT_LIBS :=
else
FMT_LIBS := -lfmt
endif
else
# No target fmt - build will fail if spdlog requires it
FMT_LIBS :=
endif
else
# Native build: use pkg-config normally
FMT_PKG_CONFIG := $(shell pkg-config --exists fmt 2>/dev/null && echo "yes")
ifeq ($(FMT_PKG_CONFIG),yes)
FMT_LIBS := $(shell pkg-config --libs fmt)
else
FMT_LIBS :=
endif
endif
# libsystemd (for systemd journal logging on Linux)
# Only check on Linux (native or cross-compile), not macOS
SYSTEMD_LIBS :=
SYSTEMD_CXXFLAGS :=
ifneq ($(UNAME_S),Darwin)
ifneq ($(CROSS_COMPILE),)
# Cross-compiling: check if target libsystemd exists
SYSTEMD_TARGET_LIB := $(shell ls /usr/lib/$(TARGET_TRIPLE)/libsystemd.so 2>/dev/null || ls /usr/lib/$(TARGET_TRIPLE)/libsystemd.a 2>/dev/null)
ifneq ($(SYSTEMD_TARGET_LIB),)
SYSTEMD_CXXFLAGS := -DHELIX_HAS_SYSTEMD
SYSTEMD_LIBS := -lsystemd
endif
else ifneq ($(PLATFORM_TARGET),x86-desktop)
# Native Linux build: use pkg-config. The released desktop build stays
# off libsystemd: it runs as a window, not a service, and a desktop
# distro without systemd would otherwise refuse to load the binary.
SYSTEMD_PKG_CONFIG := $(shell pkg-config --exists libsystemd 2>/dev/null && echo "yes")
ifeq ($(SYSTEMD_PKG_CONFIG),yes)
SYSTEMD_CXXFLAGS := -DHELIX_HAS_SYSTEMD
SYSTEMD_LIBS := $(shell pkg-config --libs libsystemd)
endif
endif
endif
# 3D G-code Rendering
# ENABLE_GLES_3D: GPU-accelerated OpenGL ES 2.0 via EGL (Pi + desktop Linux)
# (default set earlier, before APP_SRCS filtering)
ifeq ($(ENABLE_GLES_3D),yes)
GLES3D_DEFINES := -DENABLE_GLES_3D
else
GLES3D_DEFINES :=
endif
# Flying Toasters screensaver (desktop/Pi only)
ifeq ($(ENABLE_SCREENSAVER),yes)
SCREENSAVER_DEFINES := -DHELIX_ENABLE_SCREENSAVER
else
SCREENSAVER_DEFINES :=
endif
# Mock backend defines
ifeq ($(ENABLE_MOCKS),yes)
MOCK_DEFINES := -DHELIX_ENABLE_MOCKS
else
MOCK_DEFINES :=
endif
# Remote-control subsystem define (see ENABLE_REMOTE_CONTROL above)
ifeq ($(ENABLE_REMOTE_CONTROL),yes)
REMOTE_CONTROL_DEFINES := -DHELIX_ENABLE_REMOTE_CONTROL
else
REMOTE_CONTROL_DEFINES :=
endif
# Diagnostic-upload define (see ENABLE_DIAGNOSTIC_UPLOADS above): runtime
# default of helix::diag::uploads_enabled() plus the payload channel marker.
ifeq ($(ENABLE_DIAGNOSTIC_UPLOADS),yes)
DIAG_UPLOAD_DEFINES := -DHELIX_ENABLE_DIAGNOSTIC_UPLOADS
else
DIAG_UPLOAD_DEFINES :=
endif
# Developer-only showcase panels define (see ENABLE_DEV_PANELS above)
ifeq ($(ENABLE_DEV_PANELS),yes)
DEV_PANELS_DEFINES := -DHELIX_ENABLE_DEV_PANELS
else
DEV_PANELS_DEFINES :=
endif
# wpa_supplicant (WiFi control via wpa_ctrl interface)
WPA_DIR := lib/wpa_supplicant
# Output to $(BUILD_DIR)/lib/ for architecture isolation (native/pi/ad5m)
WPA_CLIENT_LIB := $(BUILD_DIR)/lib/libwpa_client.a
# Use -isystem to suppress warnings from third-party headers in strict mode
WPA_INC := -isystem $(WPA_DIR)/src/common -isystem $(WPA_DIR)/src/utils
# Force-included ahead of every app and test C++ source. It supplies LVGL,
# helix-xml, spdlog and the common STL headers, and many sources compile only
# because of it. It is an ordinary header, tracked through each object's .d
# file; nothing is precompiled.
FORCED_INCLUDE := -include $(INC_DIR)/lvgl_pch.h
# Include paths
# Project includes use -I (warnings enabled), library includes use -isystem (warnings suppressed)
# This allows `make strict` to catch issues in project code while ignoring third-party header warnings
# stb_image headers (used for thumbnail processing)
STB_INC := -isystem lib/stb
INCLUDES := -I. -I$(INC_DIR) -Isrc/generated -I$(BUILD_DIR)/generated -isystem lib -isystem lib/glm $(LVGL_INC) $(LIBHV_INC) $(SPDLOG_INC) $(STB_INC) $(LV_MARKDOWN_INC) $(QUIRC_INC) $(WPA_INC) $(SDL2_INC)
# The folded helixctl client (src/remote/remote_client.cpp) includes linenoise.h.
ifeq ($(ENABLE_REMOTE_CONTROL),yes)
INCLUDES += -I lib/linenoise
endif
# Common linker flags (used by both macOS and Linux)
LDFLAGS_COMMON := $(SDL2_LIBS) $(LIBHV_LIBS) $(FMT_LIBS) -lz -lm -lpthread
# Platform-specific configuration
# Cross-compilation targets (pi, ad5m, k1) are Linux-based embedded systems
ifeq ($(YOCTO_BUILD),yes)
# Yocto/bitbake: CC already carries sysroot + mcpu/mfpu flags. All dependencies
# come from DEPENDS in the .bb recipe (libhv, openssl, spdlog, fmt, alsa-lib,
# libusb1, wpa-supplicant, libnl). Do not build any submodule library here —
# link against system libraries resolved by the sysroot-aware toolchain.
NPROC := $(shell nproc 2>/dev/null || echo 4)
LIBNL_LIBS := -lnl-genl-3 -lnl-3
# Preserve bitbake's env LDFLAGS (--hash-style=gnu, -Wl,-z,relro, etc.) —
# do not clobber. Prepending $(LDFLAGS) lets Yocto's security/packaging
# flags flow through the final link.
LDFLAGS := $(LDFLAGS) $(LIBHV_LIBS) $(FMT_LIBS) -lwpa_client $(LIBNL_LIBS) -ldl -lz -lm -lpthread
ifeq ($(ENABLE_SSL),yes)
LDFLAGS += -lssl -lcrypto
endif
LDFLAGS += $(TARGET_LDFLAGS)
# No -gz here on purpose. DEBUG_SIZE_FLAGS is defined only on the non-Yocto
# branch, and there it reaches the linker through CXXFLAGS (mk/rules.mk
# links both binaries with $(CXX) $(CXXFLAGS) ... $(LDFLAGS)), so the debug
# sections stay compressed in the linked output without a separate LDFLAGS
# entry. On this Yocto branch nothing is -gz-compressed to begin with.
PLATFORM := Linux-yocto
# No submodule wpa_client to depend on — wpa-supplicant recipe installs libwpa_client.
WPA_DEPS :=
WPA_CLIENT_LIB :=
else ifneq ($(CROSS_COMPILE)$(filter x86 x86-fbdev x86-both,$(PLATFORM_TARGET)),)
# Platform builds (cross-compilation or x86 Docker native)
NPROC := $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
# libnl libraries: cross-compiled targets use static archives from submodule,
# x86 native uses system libnl via pkg-config
ifneq (,$(filter x86 x86-fbdev x86-both,$(PLATFORM_TARGET)))
LIBNL_LIBS := -lnl-genl-3 -lnl-3
else
LIBNL_LIBS := $(BUILD_DIR)/lib/libnl-genl-3.a $(BUILD_DIR)/lib/libnl-3.a
endif
# Embedded targets link against libhv and wpa_supplicant
# No SDL2 - display handled by framebuffer/DRM
# SSL is optional - only needed if connecting to remote Moonraker over HTTPS
# Note: libnl must come AFTER wpa_client (static linking order matters)
# Note: -L path only for targets that link against host libs (Pi, AD5M);
# the toolchains below each carry their own sysroot
ifeq ($(PLATFORM_TARGET),k1-dynamic)
# K1 Dynamic: Mixed static/dynamic linking
# Project libraries linked statically, system libraries linked dynamically
# -lstdc++fs: GCC 7.5 requires separate library for <experimental/filesystem>
LDFLAGS := -Wl,-Bstatic \
$(LIBHV_LIBS) $(FMT_LIBS) $(WPA_CLIENT_LIB) $(LIBNL_LIBS) -lstdc++fs \
-Wl,-Bdynamic \
-lstdc++ -lz -lm -lpthread -lrt -ldl -latomic -lgcc_s
else ifneq ($(filter mips k1 ad5x,$(PLATFORM_TARGET)),)
# No system library path: these toolchains are self-contained.
# The unified MIPS build (mips/k1/ad5x) uses the ct-ng musl toolchain
# and links fully static, so nothing resolves from a host or chroot at runtime.
# -latomic: Required for 64-bit atomics on 32-bit MIPS (std::atomic<int64_t>)
LDFLAGS := $(LIBHV_LIBS) $(FMT_LIBS) $(WPA_CLIENT_LIB) $(LIBNL_LIBS) -latomic -ldl -lz -lm -lpthread
else ifeq ($(PLATFORM_TARGET),k2)
# K2 uses musl - fully static, no system library paths needed (same as K1)
LDFLAGS := $(LIBHV_LIBS) $(FMT_LIBS) $(WPA_CLIENT_LIB) $(LIBNL_LIBS) -ldl -lz -lm -lpthread
else
LDFLAGS := -L/usr/lib/$(TARGET_TRIPLE) $(LIBHV_LIBS) $(FMT_LIBS) $(WPA_CLIENT_LIB) $(LIBNL_LIBS) $(SYSTEMD_LIBS) -ldl -lz -lm -lpthread
# libusb only available on Pi/x86 targets (ad5m/cc1 use standalone ARM toolchains without it)
ifneq (,$(filter pi pi-fbdev pi-both pi32 pi32-fbdev pi32-both x86 x86-fbdev x86-both,$(PLATFORM_TARGET)))
LDFLAGS += -lusb-1.0
CXXFLAGS += -isystem /usr/include/libusb-1.0 -DHELIX_HAS_LIBUSB=1
endif
endif
ifeq ($(ENABLE_SSL),yes)
ifneq (,$(filter pi pi-fbdev pi-both pi32 pi32-fbdev pi32-both x86 x86-fbdev x86-both k1-dynamic,$(PLATFORM_TARGET)))
# Static-link OpenSSL to avoid soname mismatch across OS versions
# Pi: Bullseye has libssl.so.1.1, Bookworm has libssl.so.3
# K1-dynamic: K1 firmware may not have OpenSSL shared libs
LDFLAGS += -Wl,-Bstatic -lssl -lcrypto -Wl,-Bdynamic
else
LDFLAGS += -lssl -lcrypto
endif
endif
# Add target-specific linker flags (e.g., -lstdc++fs for GCC 8)
LDFLAGS += $(TARGET_LDFLAGS)
# DRM backend requires libdrm for display; libinput for LVGL input drivers.
# A DLOPEN_GPU_LIBS build loads all of them at runtime instead (mk/implib.mk).
ifeq ($(DISPLAY_BACKEND)-$(DLOPEN_GPU_LIBS),drm-)
LDFLAGS += -ldrm
ifneq ($(SNAPMAKER_SKIP_LIBINPUT),yes)
LDFLAGS += -linput
endif
# EGL/GLES/GBM for the 3D G-code renderer (src/rendering/gcode_gles_renderer.cpp)
# and any LVGL EGL display path once one is compiled.
ifneq ($(filter yes,$(ENABLE_OPENGLES) $(ENABLE_GLES_3D)),)
LDFLAGS += -lEGL -lGLESv2 -lgbm
endif
endif
PLATFORM := Linux-$(TARGET_ARCH)
WPA_DEPS := $(WPA_CLIENT_LIB)
# Strip embedded binaries for size (CI extracts symbols first, then strips)
ifeq ($(STRIP_BINARY),yes)
STRIP_CMD := $(CROSS_COMPILE)strip
NM_CMD := $(CROSS_COMPILE)nm
endif
else ifeq ($(UNAME_S),Darwin)
# macOS native build - Uses CoreWLAN framework for WiFi (with fallback to mock)
NPROC := $(shell sysctl -n hw.ncpu 2>/dev/null || echo 4)
# Set minimum macOS version (10.15 Catalina for CoreWLAN/CoreLocation modern APIs)
MACOS_MIN_VERSION := 10.15
MACOS_DEPLOYMENT_TARGET := -mmacosx-version-min=$(MACOS_MIN_VERSION)
CFLAGS += $(MACOS_DEPLOYMENT_TARGET)
CXXFLAGS += $(MACOS_DEPLOYMENT_TARGET)
# macOS has no gettid() — override libhv's hconfig.h which incorrectly assumes it
CFLAGS += -DHAVE_GETTID=0
CXXFLAGS += -DHAVE_GETTID=0
SUBMODULE_CFLAGS += $(MACOS_DEPLOYMENT_TARGET)
SUBMODULE_CXXFLAGS += $(MACOS_DEPLOYMENT_TARGET)
# libusb detection via pkg-config (Homebrew paths aren't in default search path)
LIBUSB_CFLAGS := $(shell pkg-config --cflags libusb-1.0 2>/dev/null)
LIBUSB_LIBS := $(shell pkg-config --libs libusb-1.0 2>/dev/null)
ifneq ($(LIBUSB_LIBS),)
CFLAGS += $(LIBUSB_CFLAGS)
CXXFLAGS += $(LIBUSB_CFLAGS) -DHELIX_HAS_LIBUSB=1
endif
# -Wl,-w suppresses linker warnings about macOS version mismatches between