diff --git a/builder/frameworks/arduino.py b/builder/frameworks/arduino.py index b0b55a6..09df797 100644 --- a/builder/frameworks/arduino.py +++ b/builder/frameworks/arduino.py @@ -23,6 +23,8 @@ """ import os +import platform as host_platform +import subprocess from SCons.Script import DefaultEnvironment @@ -34,6 +36,77 @@ FRAMEWORK_DIR = platform.get_package_dir("framework-portduino") assert os.path.isdir(FRAMEWORK_DIR) +# On macOS, APFS is case-insensitive by default. The framework's +# `cores/arduino/api/String.h` shadows libc's `` when other code +# does `#include `, which breaks both libc++'s machinery +# and `extern "C" { #include }` blocks in third-party libraries +# (e.g., SparkFun ICM_20948_C.h) — under `extern "C"`, Arduino's String class +# trips C-linkage rules and clang reports its `friend operator+` overloads +# as "conflicting types". +# +# Trying to outrank `cores/arduino/api/` via `-I/path/to/sdk/...` doesn't +# work because clang dedupes against its implicit search paths. +# Instead, we materialize a tiny shim directory containing a *symlink* named +# `string.h` -> libc++'s real `` shim, and prepend that directory +# to the include search path. Since the shim path is novel (not already +# implicit), clang doesn't dedupe; `` lookups find it first; the +# shim's own `#include_next ` chains correctly to libc; and +# Arduino's `String.h` is never reached via case-shadow. +MACOS_STRING_SHIM_DIR = None +if host_platform.system() == "Darwin": + try: + sdk_path = subprocess.check_output( + ["xcrun", "--show-sdk-path"], text=True + ).strip() + libc_string_h = os.path.join(sdk_path, "usr", "include", "string.h") + if os.path.isfile(libc_string_h): + shim_dir = os.path.join( + env.subst("$PROJECT_BUILD_DIR"), + "framework-portduino-darwin-shim", + ) + os.makedirs(shim_dir, exist_ok=True) + shim_path = os.path.join(shim_dir, "string.h") + # Generate the shim content. We deliberately avoid `#include_next` + # because clang's `#include_next` searches relative to where the + # current file was *found* on the include path, not where it lives + # on disk — so if we routed through libc++'s shim via symlink, + # `#include_next` would continue searching from /our/shim/ and + # land on `cores/arduino/api/String.h` again. Instead, we + # explicitly: + # 1) define _LIBCPP_STRING_H so libc++'s sees its + # `` shim "as if" it had been processed, and + # 2) absolute-include libc's real `` to declare + # memcpy/strlen/etc. at C linkage (which is also what every + # `extern "C" { #include }` block expects). + shim_content = ( + "// Auto-generated by platform-native/builder/frameworks/" + "arduino.py.\n" + "// Routes away from framework-portduino's " + "case-clashing\n" + "// `cores/arduino/api/String.h` on macOS APFS.\n" + "#pragma once\n" + "#ifndef _LIBCPP_STRING_H\n" + "#define _LIBCPP_STRING_H\n" + "#endif\n" + "#include \"%s\"\n" % libc_string_h + ) + try: + existing = "" + if os.path.isfile(shim_path): + with open(shim_path, "r") as f: + existing = f.read() + if existing != shim_content: + if os.path.lexists(shim_path): + os.remove(shim_path) + with open(shim_path, "w") as f: + f.write(shim_content) + except OSError: + pass + if os.path.isfile(shim_path): + MACOS_STRING_SHIM_DIR = shim_dir + except (subprocess.CalledProcessError, FileNotFoundError): + pass + env.Append( CPPDEFINES=[ ("ARDUINO", 4403), # FIXME, find how these numbers are assigned! @@ -46,7 +119,13 @@ CCFLAGS=[ "-w", "-Wall", - "-ggdb" + "-ggdb", + # Apple Clang 16+ promotes -Wenum-constexpr-conversion to a hard + # default error. The firmware uses 0xFF as an "unset" sentinel for + # several pb-generated enums whose declared range is smaller (e.g. + # MODEM_PRESET_END in mesh/MeshRadio.h). GCC has no equivalent + # warning, so this flag is harmless on Linux and necessary on macOS. + "-Wno-enum-constexpr-conversion", ], CXXFLAGS=[ @@ -78,6 +157,12 @@ ] ) +# Darwin-only: prepend the macOS String.h shim directory so `` +# lookups find libc++'s shim first instead of case-shadowing onto Arduino's +# `String.h` (see comment block at top). +if MACOS_STRING_SHIM_DIR: + env.Prepend(CPPPATH=[MACOS_STRING_SHIM_DIR]) + # # Target: Build Core Library # @@ -88,15 +173,26 @@ variants_dir = os.path.join( "$PROJECT_DIR", board.get("build.variants_dir")) if board.get( "build.variants_dir", "") else os.path.join(FRAMEWORK_DIR, "variants") - env.Append( - CPPPATH=[ - os.path.join(variants_dir, board.get("build.variant")) - ] - ) - libs.append(env.BuildLibrary( - os.path.join("$BUILD_DIR", "FrameworkArduinoVariant"), - os.path.join(variants_dir, board.get("build.variant")) - )) + variant_path = os.path.join(variants_dir, board.get("build.variant")) + env.Append(CPPPATH=[variant_path]) + # Skip BuildLibrary if the variant directory has no .c/.cpp/.S sources. + # macOS `ar` rejects archives with zero members ("ar: no archive members + # specified"); GNU ar on Linux silently produces an empty archive. The + # framework's bundled `variants/` is just a sentinel `.gitignore` for many + # boards, so we'd hit this on any host where `ar` is strict. + resolved_variant = env.subst(variant_path) + has_variant_sources = False + if os.path.isdir(resolved_variant): + for _, _, files in os.walk(resolved_variant): + if any(f.endswith((".c", ".cpp", ".cc", ".cxx", ".S", ".s")) + for f in files): + has_variant_sources = True + break + if has_variant_sources: + libs.append(env.BuildLibrary( + os.path.join("$BUILD_DIR", "FrameworkArduinoVariant"), + variant_path + )) libs.append(env.BuildLibrary( os.path.join("$BUILD_DIR", "FrameworkArduino"),