Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 106 additions & 10 deletions builder/frameworks/arduino.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"""

import os
import platform as host_platform
import subprocess

from SCons.Script import DefaultEnvironment

Expand All @@ -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 `<string.h>` when other code
# does `#include <string.h>`, which breaks both libc++'s <cstring> machinery
# and `extern "C" { #include <string.h> }` 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 `<string.h>` shim, and prepend that directory
# to the include search path. Since the shim path is novel (not already
# implicit), clang doesn't dedupe; `<string.h>` lookups find it first; the
# shim's own `#include_next <string.h>` 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 <cstring> sees its
# `<string.h>` shim "as if" it had been processed, and
# 2) absolute-include libc's real `<string.h>` to declare
# memcpy/strlen/etc. at C linkage (which is also what every
# `extern "C" { #include <string.h> }` block expects).
shim_content = (
"// Auto-generated by platform-native/builder/frameworks/"
"arduino.py.\n"
"// Routes <string.h> 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!
Expand All @@ -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=[
Expand Down Expand Up @@ -78,6 +157,12 @@
]
)

# Darwin-only: prepend the macOS String.h shim directory so `<string.h>`
# 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
#
Expand All @@ -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"),
Expand Down
Loading