Skip to content

libraries: guard TinyUSB include under PlatformIO so SAMD builds compile - #396

Open
tyeth-ai-assisted wants to merge 4 commits into
adafruit:masterfrom
tyeth-ai-assisted:platformio-tinyusb-include-guards
Open

tyeth-ai-assisted wants to merge 4 commits into
adafruit:masterfrom
tyeth-ai-assisted:platformio-tinyusb-include-guards

Conversation

@tyeth-ai-assisted

@tyeth-ai-assisted tyeth-ai-assisted commented Aug 28, 2026

Copy link
Copy Markdown

Problem

Every PlatformIO SAMD build with USE_TINYUSB fails to compile:

libraries/Servo/src/samd/Servo.cpp:26:10: fatal error: Adafruit_TinyUSB.h: No such file or directory
   26 | #include <Adafruit_TinyUSB.h>

arduino-cli is unaffected, which is why this has gone unnoticed.

Why

Adafruit_TinyUSB.h lives in the library's src/, and only src/arduino is ever put on the include path — by both toolchains:

  • platform.txt (line 80) for arduino-cli:
    "-I{runtime.platform.path}/libraries/Adafruit_TinyUSB_Arduino/src/arduino"
  • PlatformIO's atmelsam platform appends that same .../Adafruit_TinyUSB_Arduino/src/arduino to the global CPPPATH for the Adafruit vendor core (builder/frameworks/arduino/arduino-samd.py).

So Adafruit_USBD_CDC.h resolves everywhere, while <Adafruit_TinyUSB.h> resolves from neither.

Under arduino-cli it works anyway, because that unresolved include is itself what drives the Arduino builder's library discovery: it finds Adafruit_TinyUSB_Arduino, adds its src/ to the include path, and links the library — the side effect the existing comment refers to. PlatformIO has no equivalent step for framework-bundled libraries (they never receive lib_deps include paths), so there the include is simply a hard error.

Fix

Skip the include under PlatformIO when the header is unreachable, and leave it plain and unconditional everywhere else:

#ifdef USE_TINYUSB
// For Serial when selecting TinyUSB (also causes the Arduino builder to link
// the TinyUSB library). Outside PlatformIO this include must stay plain and
// unconditional: the Arduino builder discovers the library from it, and its
// dependency-detection pass cannot parse a __has_include() expression.
#ifdef PLATFORMIO
// PlatformIO does not give framework-bundled libraries the lib_deps include
// paths, so the header can be unreachable here; skip it instead of failing.
#if !defined(__has_include) || __has_include(<Adafruit_TinyUSB.h>)
#include <Adafruit_TinyUSB.h>
#endif
#else
#include <Adafruit_TinyUSB.h>
#endif
#endif

The nesting is load-bearing, and this repo's CI is what proved it. A flat guard — #if !defined(__has_include) || __has_include(<Adafruit_TinyUSB.h>) around the include, with or without a !defined(PLATFORMIO) clause — turns both usbstack=tinyusb legs red with undefined reference to 'Serial' and Adafruit_USBD_CDC::begin(unsigned long) in the Wire and SAMD_AnalogCorrection examples. The failures are link-time only, never compile errors: the Arduino builder's dependency-detection pass cannot parse a __has_include() expression, so it finds no dependency and TinyUSB is never linked, while the real compile (GCC 9, which does support __has_include) goes through fine. Short-circuiting does not help, because the expression still has to be parsed. Nesting it inside #ifdef PLATFORMIO puts it in a branch arduino-cli skips without evaluating, leaving that toolchain untouched.

Servo.cpp is the only file that actually fails in the configuration I tested; the other four carry the identical unguarded include and the identical hazard, so they get the same treatment rather than surfacing one at a time.

Testing

PlatformIOAdafruit WipperSnapper (TinyUSB + SPI + Wire + ~60 sensor libraries), env adafruit_pyportal_m4, PlatformIO 6, USE_TINYUSB=1. Each tree was built from the installed platformio/framework-arduino-samd-adafruit@1.10716.0 package (= this repo at 1.7.16) consumed via platform_packages = framework-arduino-samd-adafruit@symlink://<tree>, so these five files are the only variable. They are byte-identical between master and 1.7.16, so the result applies directly to this branch.

framework tree PlatformIO
unmodified FAILEDServo.cpp:26: fatal error: Adafruit_TinyUSB.h: No such file or directory
this PR SUCCESS — Flash 44.7% (468,924 B), RAM 5.9% (15,400 B)

arduino-cli — this repo's own matrix, run three times on this PR:

branch state tinyusb legs other 10 legs
baseline (master tree, guards not applied) pass pass
flat __has_include guard fail (undefined Serial) pass
nested guard (this PR) pass pass

The PlatformIO firmware is byte-for-byte the same size across every guard variant, confirming the guard only ever removes an include PlatformIO could not resolve in the first place.

Note

This supersedes a Copilot-authored attempt (tyeth/adafruit_ArduinoCore-samd#1) that rerouted the Adafruit_USBD_CDC.h include to "arduino/Adafruit_USBD_CDC.h" under PLATFORMIO. That rested on the inverted premise that PlatformIO exposes src but not src/arduino; it breaks every translation unit that includes Arduino.h, and it was never compiled. Closed.

@tyeth

tyeth commented Aug 28, 2026

Copy link
Copy Markdown
Member

Hey @hathach could you have a review of this please?
Testing this PR (https://github.com/adafruit/Adafruit_Wippersnapper_Arduino/tree/fix-ws-v2-global-to-pointer adafruit/Adafruit_Wippersnapper_Arduino#974) it was found that Servo.cpp failed to compile due to platformIO LDF being awkward compared to arduino-cli.

This rectifies the include error that occurs otherwise.

@tyeth
tyeth requested a review from hathach August 28, 2026 12:52
@tyeth

tyeth commented Aug 28, 2026

Copy link
Copy Markdown
Member

Oops, failing CI on untested path, will fix

@tyeth-ai-assisted
tyeth-ai-assisted force-pushed the platformio-tinyusb-include-guards branch from 8904a30 to a9a2c20 Compare August 28, 2026 12:56
@tyeth-ai-assisted tyeth-ai-assisted changed the title libraries: guard TinyUSB include with __has_include so PlatformIO SAMD builds compile libraries: guard TinyUSB include under PlatformIO so SAMD builds compile Aug 28, 2026
@tyeth-ai-assisted
tyeth-ai-assisted force-pushed the platformio-tinyusb-include-guards branch from a9a2c20 to 26c39c0 Compare August 28, 2026 13:06
@tyeth-ai-assisted
tyeth-ai-assisted force-pushed the platformio-tinyusb-include-guards branch 2 times, most recently from 00da71d to 06da1e0 Compare August 28, 2026 13:16
Under PlatformIO, framework-bundled libraries do not receive lib_deps
include paths, so the unguarded #include <Adafruit_TinyUSB.h> hard-fails
every SAMD build with USE_TINYUSB. Adafruit_TinyUSB.h lives in the
library's src/, and only src/arduino is ever added to the include path
(platform.txt does this for arduino-cli; PlatformIO's atmelsam builder
does the same), so the header is unreachable from these five sources.

Skip the include under PlatformIO when it is unreachable, keeping it
plain and unconditional everywhere else. The nesting is deliberate: the
include is what the Arduino builder discovers the TinyUSB library from,
and its dependency-detection pass cannot parse a __has_include()
expression -- it then finds no dependency, TinyUSB is never linked, and
the usbstack=tinyusb examples fail with undefined references to Serial
and Adafruit_USBD_CDC::begin. Putting __has_include() inside #ifdef
PLATFORMIO leaves it in a branch arduino-cli skips without evaluating,
so arduino-cli behaviour is unchanged. Verified both ways in CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJa4WYfEHBFUhnJVRLja7A
@tyeth-ai-assisted
tyeth-ai-assisted force-pushed the platformio-tinyusb-include-guards branch from 06da1e0 to c63ec86 Compare August 28, 2026 13:25

@tyeth tyeth left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments are a bit long, worth a replacement possibly.

@hathach hathach left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

look good, sorry for the delay. I think we can simplify this

// PlatformIO does not give framework-bundled libraries the lib_deps include
// paths, so the header can be unreachable there; skip it instead of failing.
#if !defined(PLATFORMIO) || !defined(__has_include) || __has_include(<Adafruit_TinyUSB.h>)
#include <Adafruit_TinyUSB.h>
#endif

tyeth and others added 2 commits September 18, 2026 15:24
Apply hathach's review suggestion from adafruit#396:
one #if covering PLATFORMIO and __has_include, replacing the nested
#ifdef PLATFORMIO form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ng for it

Replaces the __has_include probe with a plain #ifndef PLATFORMIO. The
include exists only so the Arduino builder discovers and links
Adafruit_TinyUSB_Arduino; none of these five translation units references
Serial or any TinyUSB symbol, so under PlatformIO -- where the header is
unreachable and the discovery side effect does not exist -- it can simply
be left out.

Background
----------

Only src/arduino of Adafruit_TinyUSB_Arduino is ever placed on the include
path: platform.txt line 80 does it for arduino-cli, and PlatformIO's
atmelsam platform appends the same path to CPPPATH for the Adafruit vendor
core. So Adafruit_USBD_CDC.h resolves everywhere while
<Adafruit_TinyUSB.h> resolves from neither. arduino-cli survives that
because the unresolved include is itself what drives library discovery;
PlatformIO gives framework-bundled libraries no equivalent step, so every
SAMD build with USE_TINYUSB dies with

  libraries/Servo/src/samd/Servo.cpp:26:10: fatal error:
  Adafruit_TinyUSB.h: No such file or directory

Why not __has_include
---------------------

Both previous attempts used __has_include, and the flat form has now been
measured red four times on this repo's matrix -- three runs before opening
the PR, and run 35356063732 on the previous commit, which applied the
review suggestion

  #if !defined(PLATFORMIO) || !defined(__has_include) || __has_include(<Adafruit_TinyUSB.h>)

verbatim. Both usbstack=tinyusb legs failed while the other ten passed:

  CorrectADCResponse.ino:43: undefined reference to
    `Adafruit_USBD_CDC::begin(unsigned long)'
  CorrectADCResponse.ino:144: undefined reference to `Serial'
  collect2: error: ld returned 1 exit status

The failures are link-time only, never compile errors: GCC 9 handles
__has_include fine, but the Arduino builder's dependency-detection pass
cannot, so it finds no dependency and TinyUSB is never linked. Placing
!defined(PLATFORMIO) first does not help, because __has_include is
resolved while the #if line is expanded, before its operands are
evaluated -- short-circuiting never gets the chance to hide it. The
earlier nested form worked only by burying the expression inside an
#ifdef PLATFORMIO branch that arduino-cli skips without reading.

#ifndef PLATFORMIO avoids the construct altogether: arduino-cli's branch
holds a byte-identical unconditional include, and PlatformIO takes no
branch at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tyeth-ai-assisted

Copy link
Copy Markdown
Author

Update: the __has_include probe is gone; the guard is now just #ifndef PLATFORMIO

#ifdef USE_TINYUSB
// For Serial when selecting TinyUSB; the Arduino builder discovers and links
// the library from this include. PlatformIO never resolves it (framework-
// bundled libraries get no lib_deps include paths), so skip it there.
#ifndef PLATFORMIO
#include <Adafruit_TinyUSB.h>
#endif
#endif

Three lines, no __has_include anywhere, and arduino-cli sees a byte-identical unconditional include. Full matrix green: run 35357540160 — 12/12, both usbstack=tinyusb legs included. (One pyportal_m4 leg in that run first failed at 10s on a runner blip; it passed on re-run, different job id, same tree.)

What actually breaks, per scenario

guard arduino-cli (usbstack=tinyusb) PlatformIO (USE_TINYUSB)
none (master) passes compile errorServo.cpp:26:10: fatal error: Adafruit_TinyUSB.h: No such file or directory
flat __has_include, with or without a leading !defined(PLATFORMIO) link errorundefined reference to 'Serial', undefined reference to 'Adafruit_USBD_CDC::begin(unsigned long)' passes
__has_include nested inside #ifdef PLATFORMIO passes passes
#ifndef PLATFORMIO (current) passes passes

Why each cell is what it is:

PlatformIO's compile error. Only Adafruit_TinyUSB_Arduino/src/arduino is ever put on the include path — platform.txt line 80 for arduino-cli, and the atmelsam platform appends the same directory to the global CPPPATH for this core. Adafruit_USBD_CDC.h lives there and resolves everywhere; Adafruit_TinyUSB.h lives one level up in src/ and resolves from neither. PlatformIO gives framework-bundled libraries no lib_deps include paths, so there is no second chance and the include is a hard error.

Why arduino-cli survives that same unresolved include. The unresolved include is the discovery mechanism: the Arduino builder uses it to find Adafruit_TinyUSB_Arduino, add its src/ to the include path, and link the library. Remove or hide the include and nothing links.

The link errors, and why the flat form can't work. The failures are link-time only — never compile errors. GCC 9 handles __has_include fine, so the real compile is clean; it is the builder's dependency-detection pass that cannot parse the expression, finds no dependency, and never links TinyUSB. Ordering the clauses so !defined(PLATFORMIO) short-circuits first does not rescue it, because __has_include is resolved while the #if line is being expanded — before its operands are evaluated — so short-circuiting never gets the chance to hide it. Measured red four times: three runs before this PR was opened, and run 35356063732 on the flat form, where both tinyusb legs failed in the Wire and SAMD_AnalogCorrection examples while the other ten passed. Nesting it inside #ifdef PLATFORMIO worked only because arduino-cli skips that branch without reading it.

Why dropping the include outright is safe. None of the five files — I2S.cpp, SAMD_AnalogCorrection.cpp, SPI.cpp, Servo.cpp, Wire.cpp — references Serial or any TinyUSB symbol. The include exists purely for the arduino-cli discovery side effect, so where that side effect doesn't exist there is nothing to preserve. That is what lets the guard be a plain #ifndef rather than a probe, and it makes the nesting unnecessary rather than merely tidier.

PlatformIO side, unchanged from the original report: WipperSnapper, env adafruit_pyportal_m4, USE_TINYUSB=1, built against the installed framework-arduino-samd-adafruit@1.10716.0 with only these five files varied — fails unpatched, builds patched, identical firmware size across every working guard variant, confirming the guard only ever removes an include PlatformIO could not resolve.

Tyeth will squash the three commits on this branch into one before merge and release.

@tyeth

tyeth commented Sep 18, 2026

Copy link
Copy Markdown
Member

@hathach thanks for the review and suggestion. I had a try and unfortunately still fails so this simpler version should be sufficient. I'll squash the PR before merge if you're happy with that.

@tyeth
tyeth requested a review from hathach September 18, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants