Skip to content

Implicit deps resolution: add #embed and fix #include followed by a comment - #4889

Open
vassilit wants to merge 4 commits into
SCons:masterfrom
vassilit:embed
Open

vassilit wants to merge 4 commits into
SCons:masterfrom
vassilit:embed

Conversation

@vassilit

@vassilit vassilit commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
  • Implement #embed C23 (and c++26) preprocessor feature. (see: here for an explanation of what this is
  • Fix issue with any text following #include

Both a small bug-fix and a new feature.

Tested manually on rmlint:
Before this PR:
#include "header.h" in code.c, modify header.h, code.c get rebuilt.
#include "header.h" /* useful header */ in code.c, modify header.h, code.c do not get rebuilt.
#embed "py.py" in code.c, modify py.py, code.c do not get rebuilt.

After:
#include "header.h" in code.c, modify header.h, code.c get rebuilt.
#include "header.h" /* useful header */ in code.c, modify header.h, code.c do get rebuilt.
#embed "py.py" in code.c, modify py.py, code.c do get rebuilt.

Contributor Checklist:

  • I have created a new test or updated the unit tests to cover the new/changed functionality.
  • I have updated CHANGES.txt and RELEASE.txt (and read the README.rst).
  • I have updated the appropriate documentation

The C scanners recognised only #include and C++ #import, so a change to
a resource named by #embed did not mark the embedding source out of date.

Both scanners now track the resource. A quoted name is looked up relative
to the including file, a bracketed name in $CPPPATH:

- CScanner's regex captures the directive keyword, through a new private
  _ClassicCPPEmbed subclass which drops the keyword again when computing
  the sort key, so result ordering is unchanged. ClassicCPP itself keeps
  its documented three-group contract, which IDL, SWIG and RC rely on.
- SCons/cpp.py, which backs the opt-in CConditionalScanner, gains the
  keyword in its directive table plus a do_embed handler, and honours
  #embed in the conditional-branch wiring of start_handling_includes and
  stop_handling_includes.

The resource itself is deliberately not scanned: its contents are
embedded as data (possibly binary) rather than preprocessed. Resolved
resources are tagged on the node and filtered out by recurse_nodes().

Also fixes resolve_include() assuming the closing delimiter is the last
character of the line, which turned `#embed "d.bin" limit(4)` into the
filename `d.bin" limit(4`, and likewise mangled an #include carrying a
trailing comment.

Signed-off-by: Vassili Tchersky <vt+git@vbcy.org>
Assisted-by: Claude Opus 5
@mwichmann

Copy link
Copy Markdown
Collaborator

Thanks for your attention. There are some issues already on missing support for features introduced in newer C++ standards (and C standards too, for that matter)... see for example #4517 and #4518. Aren't there feature test macros for embed too? We can use all the help we can get sorting out the state of these newer features.

@vassilit

vassilit commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Aren't there feature test macros for embed too?

Are you referring to __has_embed ?

I use it as a feature test as well, like here.

@mwichmann

Copy link
Copy Markdown
Collaborator

Aren't there feature test macros for embed too?

Are you referring to __has_embed ?

I use it as a feature test as well, like here.

Yes. What happens if you use __has_embed or __has_include on a compiler that doesn't yet have support for the standard each appeared in?

@vassilit

Copy link
Copy Markdown
Contributor Author

Aren't there feature test macros for embed too?

Are you referring to __has_embed ?
I use it as a feature test as well, like here.

Yes. What happens if you use __has_embed or __has_include on a compiler that doesn't yet have support for the standard each appeared in?

It fails, with missing binary operator before token "(", that's why we use #if !defined(__has_embed). If the compiler supports C89, it does not fail. For even older compilers (K&R, etc), maybe #ifdef __has_embed works, but I'm not sure.

Comment thread SCons/Scanner/C.py Outdated
not be scanned for dependencies of its own. :func:`_scannable` drops
tagged nodes when a scanner recurses into what it found.
"""
node.attributes.embedded_resource = True

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Tagging the resource "globally" like this seems to preclude the file in question ever being used another way in the project (say, via #include). Maybe that's fine, and nobody would ever do that, but maybe we should leave a note about that somewhere if that's an intentional limitation.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If this nit matters, I think just dropping the embedded_resource attribute once it's seen ("consumed") in _scannable ought to do the trick. I'll let wiser folk decide if I'm just being silly.

@bdbaddog bdbaddog Sep 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@vassilit -I think this is unnecessary?
If the file is xyz.bmp, there's no scanner for .bmp so it won't be scanned.
If the file is xyz.h, there's a scanner for that, and it will be scanned. (although as far as I understand the use model for #embed it wouldn't make sense to us that for a source file?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Tagging the resource "globally" like this seems to preclude the file in question ever being used another way in the project (say, via #include). Maybe that's fine, and nobody would ever do that, but maybe we should leave a note about that somewhere if that's an intentional limitation.

I'm not that sure that nobody would ever do that, but those who do may use explicit dependencies instead.

@bdbaddog

bdbaddog commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Is there a downside to having this when #embed isn't handled by the compiler?
It would still introduce a new dependency which doesn't map to reality?

@vassilit

vassilit commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Is there a downside to having this when #embed isn't handled by the compiler?
It would still introduce a new dependency which doesn't map to reality?

It would, but for some versions of the standard, this seems acceptable, e.g. C17/C18 section 6.10 paragraph 9 says:

The execution of a non-directive preprocessing directive results in undefined behavior.

@bdbaddog

bdbaddog commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Is there a downside to having this when #embed isn't handled by the compiler?
It would still introduce a new dependency which doesn't map to reality?

It would, but for some versions of the standard, this seems acceptable, e.g. C17/C18 section 6.10 paragraph 9 says:

The execution of a non-directive preprocessing directive results in undefined behavior.

That's the acceptable compiler behavior.
That's not the same as accurately having SCons create a correct dependency graph.
(Which is always our goal).

Seems like if we knew we were dealing with a c23 or c++26 then we could enable this, otherwise disable it would provide a more accurate picture of the actual dependencies.

What file suffixes are likely to be pulled in via embed?
(.bin, .dat, .bmp, .png, .jpg, .gif, .ico, .wasm, …), etc?

@vassilit

vassilit commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Is there a downside to having this when #embed isn't handled by the compiler?

The execution of a non-directive preprocessing directive results in undefined behavior.

That's the acceptable compiler behavior. That's not the same as accurately having SCons create a correct dependency graph. (Which is always our goal).

What I meant is if #embed is found unguarded in a source file is likely destined to be compiled with a compiler that supports it. If guarded by CPP, then yes, there is a risk of dependancies wanted only when compiled with a specific compiler and not wanted with an older one. But usually, the data is needed by the program, #embeded directly or not.

What file suffixes are likely to be pulled in via embed? (.bin, .dat, .bmp, .png, .jpg, .gif, .ico, .wasm, …), etc?

In our SCons-built project, we embed source code (.sh, .py, .go). But every file suffix is likely to be embeded, it's just data.
I've embeded .yaml and .wav in other projects (using go:embed, but the use-case would be the same in C).

There is another case, #embed with limit() can depend on only part of the file and we have no simple way of knowing which offsets of the file changed, so that can lead to uneeded rebuilds.

@bdbaddog

bdbaddog commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

One issue with your current solution, if the file is also a source, a "regular" implicit dependency, is it could be scanned recursively before it's #embed'd and then you'd end up with the recursive dependencies thereof.

Given the types of files you've stated could be embedded, does it really make sense to exclude the recursive dependency?

Did you run into such causing an issue with your build, or were you being preemptively cautious?

@bdbaddog

bdbaddog commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Just to explain the level of caution I'm taking with this PR.

  1. Not building when a dependency updates is VERY bad for a build system. Which is possible (though probably unlikely) with the current code.
  2. Extraneous build when it's not needed is annoying (maybe costly), but not VERY bad...

@vassilit

vassilit commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Given the types of files you've stated could be embedded, does it really make sense to exclude the recursive dependency?

They can be embeded but to be used as pure binary data exclusively. Recursively parsing their content would be wrong, if of course they are not included or compiled on their own elsewhere anyway. Tagging it embedded_resource is rarely, but possibly problematic indeed.
Yes, .c and .h files could be embeded, but the only time I've done this was to provide a way to compile plugins at runtime (which was presented as a "script" but actually compiled), not the standard include anyway.

Not merging this PR is OK, as the goal was to avoid manual env.Depends() (and forgeting to add new deps), not critical.

Not building when a dependency updates is VERY bad for a build system. Which is possible (though probably unlikely) with the current code.

However, fixing the trailing comments on an include line is actually addressing this point.
Embeded files are dependancies also, but if SCons states that it does not support it, it's clear. There are already a lot of dependancies that influences the build that are probably not checked for .. if the compiler version is updated, etc.

@bdbaddog

bdbaddog commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Did you run into any filetypes which led you to block the recursive scanning? Sounds like all/most of the file's you've embedded wouldn't have scanners registered and so there'd be no need to add logic to do it?

@bdbaddog

bdbaddog commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

To be clear, I'd like to merge all the functionality of this PR, minus the blocking recursive scanning bit.
Because unless we can address the issue of it blocking recursive scanning should the file be included elsewhere with #include for example, then we could be creating a bigger problem than blocking it is serving..

@bdbaddog

bdbaddog commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

@vassilit - btw unless you're setting env['IMPLICIT_COMMAND_DEPENDENCIES']=0, then assuming the compiler is the first item in the command line, it is included in the dependencies and if that changes will cause a rebuild. That also goes for almost everything on the compile command line changing will trigger rebuilds.

@bdbaddog

bdbaddog commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Did you run into a situation with your build which required precluding the embed'd file from further scanning? Or was that code just pre-emptive caution?

@vassilit

vassilit commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Did you run into a situation with your build which required precluding the embed'd file from further scanning? Or was that code just pre-emptive caution?

It was pre-emptive caution, because #embed open the door to include absolutely everything that used to be linked as an object and/or using .incbin assembly tricks. One could #embed the whole Linux kernel tree and scanning it would be pointless. However, you can always do IMPLICIT_COMMAND_DEPENDENCIES to the environement for such very special cases.

I haven't studied mwichmann's trick of dropping embedded_resource once seen, but it seems elegant.
I will try that and if not conclusive, will drop embedded_resource entirely.

@bdbaddog

bdbaddog commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Did you run into a situation with your build which required precluding the embed'd file from further scanning? Or was that code just pre-emptive caution?

It was pre-emptive caution, because #embed open the door to include absolutely everything that used to be linked as an object and/or using .incbin assembly tricks. One could #embed the whole Linux kernel tree and scanning it would be pointless. However, you can always do IMPLICIT_COMMAND_DEPENDENCIES to the environement for such very special cases.

I haven't studied mwichmann's trick of dropping embedded_resource once seen, but it seems elegant. I will try that and if not conclusive, will drop embedded_resource entirely.

IMPLICIT_COMMAND_DEPENDENCIES doesn't do that.
It's only a setting to decide what in the command line is added to implicit dependencies. The default is to ONLY add the first argument on the command line, which is almost always the compiler. Other items on the command line are not added as file implicit dependencies. However the command line itself is compared against previous, so if it changes that triggers a rebuild.

Unless the file(s) which are embedded have registered scanners in SCons, they will never be recursively scanned and added as implicit dependencies.

Given all of this, does it still seem like theres good cause to block the recursion?
You'd have to embed a header file or source file for some language you've loaded the tools for ( or were loaded by default).

@bdbaddog

bdbaddog commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Did you run into a situation with your build which required precluding the embed'd file from further scanning? Or was that code just pre-emptive caution?

It was pre-emptive caution, because #embed open the door to include absolutely everything that used to be linked as an object and/or using .incbin assembly tricks. One could #embed the whole Linux kernel tree and scanning it would be pointless. However, you can always do IMPLICIT_COMMAND_DEPENDENCIES to the environement for such very special cases.

I haven't studied mwichmann's trick of dropping embedded_resource once seen, but it seems elegant. I will try that and if not conclusive, will drop embedded_resource entirely.

Because the scan is multi-threaded, that solution (dropping the attribute) won't solve the issue, or solve the issue where it has no effect if it's first included by #include, and recursively scanned. IMPLICIT_COMMAND_DEPENDENCIES won't need to be touched for your issue. It's unrelated.

I only brought it up because you though that the compiler changing wouldn't cause a rebuild.

As I said before, unless the file type being #embed has a registered scanner, it won't be recursively scanned. I'd put money on this being very unlikely to happen in a way which wouldn't have been rebuilt when the dependency tree was modified anyway.

@vassilit

vassilit commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Also, the fix for #include "test.h" /* comment */ might be not necessary because it is specific to the conditionnal CPP scanner, which is commented out in CScanner() by default.

The #embeded resource can also be an #included resource elsewhere.
Be safe and recursively scan for implicit dependencies.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants