add design decision include path - #3196
Conversation
|
Documentation preview for this pull request is available at: |
aschemmel-tech
left a comment
There was a problem hiding this comment.
Option B conflicts with DR-003-proc (as in #3194).
LittleHuba
left a comment
There was a problem hiding this comment.
I find this proposal heavily skewed towards a pitchfork layout (option B) (even clearly stating pitchfork layout in the evaluation criteria. Based on that, I find it hard to base a discussion on this, since it feels like the arguments were written based on the decision instead of the other way round.
From my comments you can probably guess that I'm clearly leaning towards option A. And this review felt like a uphill battle from the start. Looking just at the advantages/disadvantages of options A and B, the evaluation should not be leaning so heavily towards option B.
Two sources for reference why I heavily prefer option A:
- https://bazel.build/tutorials/cpp-use-cases where the pitch fork layout is clearly only stated as a legacy adoption use case. This means that Bazel is not built for this project structure and that you will feel lots of friction trying to use it.
- https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1204r0.html#src-dir has a full section just talking about why pitch fork layout is bad and should not be used
Side note: I appreciate the accepted alternative that will safe communication and baselibs from having to rewrite 90% of their code. I still do not feel comfortable agreeing with the final decision because of the consequences for the overall project.
| to learn which headers are public. | ||
| - **Accidental API exposure:** A private header may be listed in `hdrs` by mistake, | ||
| or a consumer may reach for an internal header. | ||
| - **Poor scalability:** As the library grows the folder becomes cluttered. |
There was a problem hiding this comment.
Disagree on this. You can create subdirectories.
There was a problem hiding this comment.
Agreed and changed, A flat package can be organized with subdirectories and per-package targets, so "clutter" was overstated. This disadvantage has been removed; Option A is now the recommended layout and its write-up no longer claims poor scalability.
| - **Zero Bazel path overhead:** No `strip_include_prefix` / `includes` juggling. The | ||
| header's on-disk path *is* its include path, so there is one obvious way to write | ||
| an `#include` and no divergence between what the file tree shows and what the | ||
| compiler sees. |
There was a problem hiding this comment.
This is actually quite a crucial advantage. Using strip_include_prefix messes with external tools. Relying on this option would make packaging tremendously hard since you would have to rewrite source to actually achieve a compilation with any tool that is not aware of bazel.
There was a problem hiding this comment.
Agreed. It is now called out as an advantage and in the consumer-view table ("works with non-Bazel tools: yes").
| - **Easier local navigation:** Declarations sit next to their implementations; no | ||
| jumping between distant folders in the IDE. Refactoring that splits or renames a | ||
| unit touches a single directory, and "go to file" / fuzzy-open lands on the pair | ||
| immediately. |
There was a problem hiding this comment.
This is not just about navigation. This is also about maintainability. By vicinity of code in the source tree you also know likely affected code.
There was a problem hiding this comment.
Agreed. Option A now lists the maintainability/vicinity benefit ("the source tree makes likely-affected code visible by vicinity — a maintainability property, not just a navigation convenience"), and it's a rationale point.
| - **High encapsulation:** The public contract is physically isolated. It is | ||
| immediately clear to a consumer what may be used. |
There was a problem hiding this comment.
Not really true. E.g. we include private headers in public headers for templated APIs. There we also leak private symbols which could be used by users. This is a false promise.
See https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1204r0.html#src-dir
Also consider that Bazel prohibits you refering to files in other packages. So your Bazel rules would become very awkward if you would not put all headers into includes that are referenced in at least one public header.
There was a problem hiding this comment.
Also Agreed. This is now treated as the decisive argument. Option A solves it with impl/ + restricted visibility, which holds even for templated public headers.
| - **Clean BUILD files:** `hdrs = glob(["include/**/*.h"])` and | ||
| `srcs = glob(["src/**"])` without risk of accidentally exposing private files. |
There was a problem hiding this comment.
How can you raise globbing here as a advantage when you state it in option A as an advantage that it is avoided?
There was a problem hiding this comment.
Sorry that was contradictory. Both glob-related claims were removed.
| Public headers go into `hdrs` and are exposed through `strip_include_prefix` so that | ||
| the on-disk `include/` prefix is removed from the include path consumers see: |
There was a problem hiding this comment.
Doing this, completely breaks other tools because headers are not where source files expect them.
There was a problem hiding this comment.
Agreed — this is now explicitly documented as an Option B/C disadvantage and an Option A advantage (include path == on-disk location), including in the consumer-view table.
|
|
||
| | Option | Producer `BUILD.bazel` (public headers) | Consumer `#include` | | ||
| |--------|------------------------------------------|---------------------| | ||
| | **A: Flat** | `hdrs = glob(["*.h"])` — no prefix handling | `#include "libs/my_lib/my_lib.h"` — leaks repo layout, not namespaced | |
There was a problem hiding this comment.
Where does the libs come from? We do not have this today.
There was a problem hiding this comment.
Fixed — all examples now use real S-CORE paths (score/mw/my_component/…) instead of the invented libs/my_lib/.
| - **General open-source convention (dedicated `include/`):** The | ||
| [Pitchfork Layout (PFL)](https://github.com/vector-of-bool/pitchfork) and the | ||
| [WG21 Canonical Project Structure (P1204R0)](https://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1204r0.html) | ||
| both place public headers under a project-named `include/<lib>/` root — matching | ||
| **Options B/C**. |
There was a problem hiding this comment.
This is just wrong.
- There is no general open-source convention. There are many project examples for both.
- Canonical Project Structure explicitly mentions pitch forking as a bad practice that should not be followed - see https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1204r0.html#src-dir
There was a problem hiding this comment.
The "general open-source convention (dedicated include/)" claim is removed; the guidance now notes both styles exist.
| - **Option A (flat, repo-root includes):** collision-safe *as long as* headers are | ||
| included by their full repo-root path. The danger is resetting the prefix | ||
| (`strip_include_prefix = "."` as in `utils/base64`, or `includes = ["."]`), which | ||
| collapses the path to `#include "error.h"` and re-introduces the ambiguity globally. |
There was a problem hiding this comment.
This is why we are strict about full include paths in our source in score_communication.
The same logic applies for option B. It is just enforced because without providing the full path you simply don't find the header. The result in your source files is the same.
There was a problem hiding this comment.
Agreed. Option A adopts exactly that rule — full project-prefixed repo-root include paths — and the DR notes that the resulting #include in source is effectively the same discipline communication already enforces, without needing strip_include_prefix.
| ## Header Name Collisions Across Modules | ||
|
|
||
| A common worry is what happens when several modules expose an identically named | ||
| header — for example `error.h`. The important point is that collisions are decided by | ||
| the **include-path string**, not the file name. Two `error.h` files coexist without | ||
| issue as long as their include paths differ: | ||
|
|
||
| ```cpp | ||
| #include "score/filesystem/error.h" // baselibs | ||
| #include "score/concurrency/future/error.h" // baselibs | ||
| ``` | ||
|
|
||
| Both exist side by side in `baselibs` today with no conflict, because the package | ||
| prefix makes them unique. A problem only arises when the path is shortened to the bare | ||
| file name and two dependencies provide it: | ||
|
|
||
| ```cpp | ||
| #include "error.h" // provided by module A AND module B → ambiguous | ||
| ``` | ||
|
|
||
| If a target depends on both libraries, `-I`/`-isystem` ordering decides which file | ||
| wins — the wrong header may be included, silently violating the One Definition Rule. | ||
| The **Bazel module name does not protect against this**: `@module_a` / `@module_b` do | ||
| not appear in the C++ include path by default; the path is determined solely by the | ||
| package location and by `strip_include_prefix` / `include_prefix` / `includes`. | ||
|
|
||
| Consequences per option: | ||
|
|
||
| - **Option A (flat, repo-root includes):** collision-safe *as long as* headers are | ||
| included by their full repo-root path. The danger is resetting the prefix | ||
| (`strip_include_prefix = "."` as in `utils/base64`, or `includes = ["."]`), which | ||
| collapses the path to `#include "error.h"` and re-introduces the ambiguity globally. | ||
| - **Option B (`include/<component>/`):** collision-safe, but the protection comes from | ||
| the **component-name nesting**, not from the `include/` directory itself. A flat | ||
| `include/error.h` (without the `<component>` subdirectory) still collides. | ||
| - **Option C (Hybrid):** same as B — public headers stay unique via | ||
| `include/<component>/`. | ||
| - **Flat + `impl/` + visibility:** same as A — the repo-root path is preserved, and | ||
| the restricted `impl/` visibility additionally shrinks the set of externally | ||
| reachable headers. | ||
|
|
||
| **Rule:** Uniqueness must be guaranteed by the include-path **prefix** (the project or | ||
| component name) — via the repo package path under Option A, or via the | ||
| `include/<component>/` nesting under Options B/C. A bare `include/` without a | ||
| component-named subdirectory does **not** solve the problem. | ||
|
|
||
| --- | ||
|
|
||
| ## Consequences | ||
|
|
||
| ### Positive | ||
|
|
||
| - The public API of every C++ component is visible, isolated, and self-documenting. | ||
| - Bazel `hdrs`/`srcs` boundaries align with the physical layout, reducing accidental | ||
| API leakage. | ||
| - Consumers get stable, collision-free include paths regardless of how the dependency | ||
| is resolved (in-repo, override, or registry module). | ||
| - Public SDK packaging for non-Bazel consumers is a folder copy. | ||
| - The layout is consistent with widely used community conventions (PFL, P1204). | ||
|
|
||
| ### Negative / Costs | ||
|
|
||
| - A modest, one-time increase in Bazel boilerplate (`strip_include_prefix`) and the | ||
| `include/<component>/` nesting. | ||
| - Minor day-to-day navigation overhead from the `include/` ↔ `src/` split. | ||
| - Existing flat components must be migrated to gain the benefits (can be incremental). | ||
|
|
||
| ### Follow-Up Actions | ||
|
|
||
| - Provide a component template / scaffolding (directory skeleton + `BUILD.bazel`) that | ||
| encodes the `include/<component>/` + `src/` layout and `strip_include_prefix`. | ||
| - Document the convention in the S-CORE contribution guidelines and C++ coding | ||
| guidelines, including the private-vs-public include-path rules. | ||
| - Define a migration path for existing flat components (opportunistic, per module). | ||
| - Consider a lightweight CI/lint check that flags private headers appearing in `hdrs` | ||
| or public headers being included via non-canonical paths. |
There was a problem hiding this comment.
As stated previously, this discussion is not related to the main topic of this DR. It should be handled separately to avoid confusion.
There was a problem hiding this comment.
Agreed. Header-name collisions are orthogonal to the public-header layout decision — they're governed by the include-path prefix, which every option can get right or wrong independently. Keeping a full analysis here blurs the actual decision. It's not part of the discussion of the option discussion anymore. But an general discussion. Shall I remove that from the PR or move it somewhere else ?
There was a problem hiding this comment.
IMO we need a separate DR for the topic of include path collision and symbol collision. AFAIK there were already some initial talks in some meetings in S-CORE.
aschemmel-tech
left a comment
There was a problem hiding this comment.
Thanks for the rework, option A selected matches with DR-003-proc
add design decision include path