diff mbox series

[v3,3/4] parse: add support for 'addfragments' directive

Message ID 20241118162610.1423384-3-alex.kanavin@gmail.com
State Accepted, archived
Commit f687746703e7b096c5480668fd4f49bd4951182b
Headers show
Series [v3,1/4] bitbake-layers: ensure tinfoil.shutdown() gets executed when tinfoil.prepare() fails | expand

Commit Message

Alexander Kanavin Nov. 18, 2024, 4:26 p.m. UTC
From: Alexander Kanavin <alex@linutronix.de>

It takes two parameters:
- location prefix for fragments
- name of variable that holds the list of enabled fragments, each of them prefixed
by layer id

Implementation of this directive essentially expands the fragment list
obtained from the variable into absolute fragment paths and hands them to the
implementation of 'require'.

Signed-off-by: Alexander Kanavin <alex@linutronix.de>
---
 lib/bb/parse/ast.py                  | 33 ++++++++++++++++++++++++++++
 lib/bb/parse/parse_py/ConfHandler.py |  6 +++++
 2 files changed, 39 insertions(+)

Comments

Richard Purdie Nov. 28, 2024, 12:54 p.m. UTC | #1
Hi Alex,

Thanks for working on this. It is close but there is one small issue
needing to be resolved below.

On Mon, 2024-11-18 at 17:26 +0100, Alexander Kanavin via lists.openembedded.org wrote:
> From: Alexander Kanavin <alex@linutronix.de>
> 
> It takes two parameters:
> - location prefix for fragments
> - name of variable that holds the list of enabled fragments, each of them prefixed
> by layer id
> 
> Implementation of this directive essentially expands the fragment list
> obtained from the variable into absolute fragment paths and hands them to the
> implementation of 'require'.
> 
> Signed-off-by: Alexander Kanavin <alex@linutronix.de>
> ---
>  lib/bb/parse/ast.py                  | 33 ++++++++++++++++++++++++++++
>  lib/bb/parse/parse_py/ConfHandler.py |  6 +++++
>  2 files changed, 39 insertions(+)
> 
> diff --git a/lib/bb/parse/ast.py b/lib/bb/parse/ast.py
> index 001ba8d289a..d3b7fef9895 100644
> --- a/lib/bb/parse/ast.py
> +++ b/lib/bb/parse/ast.py
> @@ -326,6 +326,34 @@ class InheritDeferredNode(AstNode):
>          inherits.append(self.inherit)
>          data.setVar('__BBDEFINHERITS', inherits)
>  
> +class AddFragmentsNode(AstNode):
> +    def __init__(self, filename, lineno, fragments_path_prefix, fragments_variable):
> +        AstNode.__init__(self, filename, lineno)
> +        self.path = fragments_path_prefix
> +        self.variable = fragments_variable

"path" in bitbake context becomes easily confusing. I think I'd prefer
something like frag_pathprefix or something which makes it clear both
that it isn't a full path and what kind of path it is.

> +
> +    def eval(self, data):
> +        def find_fragment(layers, layerid, full_fragment_name):
> +           for layerpath in layers.split():
> +               candidate_fragment_path = os.path.join(layerpath, full_fragment_name)
> +               if os.path.exists(candidate_fragment_path) and bb.utils.get_file_layer(candidate_fragment_path, data) == layerid:
> +                   return candidate_fragment_path
> +           return None
> +
> +        fragments = data.getVar(self.variable)
> +        layers = data.getVar('BBLAYERS')
> +
> +        if not fragments:
> +            return
> +        for f in fragments.split():
> +            layerid, fragment_name = f.split('/', 1)
> +            full_fragment_name = data.expand("{}/{}.conf".format(self.path, fragment_name))
> +            fragment_path = find_fragment(layers, layerid, full_fragment_name)
> +            if fragment_path:
> +                bb.parse.ConfHandler.include(self.filename, fragment_path, self.lineno, data, "include fragment")
> +            else:
> +                bb.error("Could not find fragment {} in enabled layers: {}".format(f, layers))
> +

Whilst checking if the file exists and only including if present sounds
like the right thing to do, it needs to be a bit more complex. The
issue is that bitbake needs to track both the files it accesses but
also the files it looked for but didn't find. If those files are then
created, we need to reparse.

There are two ways to handle this, one is to trust that the
bb.parse.ConfHandler code does already know how to do this and just
call unconditionally until it matches a file. The apis might not let
you easily know if it found a file.

The other way is to call bb.parse.mark_dependency() and just mark the
accessed files that way.


Otherwise I think this looks good to me, thanks!

Cheers,

Richard
Alexander Kanavin Dec. 5, 2024, 3:12 p.m. UTC | #2
On Thu, 28 Nov 2024 at 13:54, Richard Purdie
<richard.purdie@linuxfoundation.org> wrote:
> Whilst checking if the file exists and only including if present sounds
> like the right thing to do, it needs to be a bit more complex. The
> issue is that bitbake needs to track both the files it accesses but
> also the files it looked for but didn't find. If those files are then
> created, we need to reparse.
>
> There are two ways to handle this, one is to trust that the
> bb.parse.ConfHandler code does already know how to do this and just
> call unconditionally until it matches a file. The apis might not let
> you easily know if it found a file.
>
> The other way is to call bb.parse.mark_dependency() and just mark the
> accessed files that way.

After reading through usage of mark_dependency() in existing code, I
need a bit more clarification there, because I'm not 100% sure you are
correct, or alternatively I don't have complete understanding of how
things work :)

The original 'include some/file' directive iterates over BBPATH, and
appends some/file to each of those, regardless of whether they exist,
and marks them as dependency in case they appear later. This is fine.

The fragments code takes a fragment name (which has a layer id as
prefix), unambiguously converts that to a full file path that is
checked for existence and hands it over to ConfHandler.include()
(which calls mark_dependency() internally). If such conversion to full
path fails, it calls bb.error, which immediately fails the whole
thing. Would we want to still call mark_dependency() on a full path
that does not exist in a case of such failure, and why? If the error
is addressed and file is created, then bitbake will parse it anyway,
no?

Alex
Richard Purdie Dec. 5, 2024, 3:28 p.m. UTC | #3
On Thu, 2024-12-05 at 16:12 +0100, Alexander Kanavin wrote:
> On Thu, 28 Nov 2024 at 13:54, Richard Purdie
> <richard.purdie@linuxfoundation.org> wrote:
> > Whilst checking if the file exists and only including if present sounds
> > like the right thing to do, it needs to be a bit more complex. The
> > issue is that bitbake needs to track both the files it accesses but
> > also the files it looked for but didn't find. If those files are then
> > created, we need to reparse.
> > 
> > There are two ways to handle this, one is to trust that the
> > bb.parse.ConfHandler code does already know how to do this and just
> > call unconditionally until it matches a file. The apis might not let
> > you easily know if it found a file.
> > 
> > The other way is to call bb.parse.mark_dependency() and just mark the
> > accessed files that way.
> 
> After reading through usage of mark_dependency() in existing code, I
> need a bit more clarification there, because I'm not 100% sure you are
> correct, or alternatively I don't have complete understanding of how
> things work :)
> 
> The original 'include some/file' directive iterates over BBPATH, and
> appends some/file to each of those, regardless of whether they exist,
> and marks them as dependency in case they appear later. This is fine.
> 
> The fragments code takes a fragment name (which has a layer id as
> prefix), unambiguously converts that to a full file path that is
> checked for existence and hands it over to ConfHandler.include()
> (which calls mark_dependency() internally). If such conversion to full
> path fails, it calls bb.error, which immediately fails the whole
> thing. Would we want to still call mark_dependency() on a full path
> that does not exist in a case of such failure, and why? If the error
> is addressed and file is created, then bitbake will parse it anyway,
> no?

You're right, the loop in the fragment code is a little subtle. The
code in question:

+        def find_fragment(layers, layerid, full_fragment_name):
+           for layerpath in layers.split():
+               candidate_fragment_path = os.path.join(layerpath, full_fragment_name)
+               if os.path.exists(candidate_fragment_path) and bb.utils.get_file_layer(candidate_fragment_path, data) == layerid:
+                   return candidate_fragment_path
+           return None

looks like it loops checking for file existence but it is guarded by
the layername matching check too. It shouldn't be possible for two
layers to have the same layer name. I'm not sure how rigorous our
checks are for that.

For some reason I was thinking that fragments with and without a layer
prefix were possible but that isn't the case.

The patch is therefore probably ok but I am a bit worried about this
potentially breaking in the future if someone else (or I!) miss the
subtly in the code.

Cheers,

Richard
Alexander Kanavin Dec. 5, 2024, 3:47 p.m. UTC | #4
On Thu, 5 Dec 2024 at 16:28, Richard Purdie
<richard.purdie@linuxfoundation.org> wrote:

> The patch is therefore probably ok but I am a bit worried about this
> potentially breaking in the future if someone else (or I!) miss the
> subtly in the code.

I still need to improve variable naming. I'll do that and send v2, but
I'm not sure at the moment what can be done about this subtlety.

Alex
Richard Purdie Dec. 5, 2024, 5:06 p.m. UTC | #5
On Thu, 2024-12-05 at 16:47 +0100, Alexander Kanavin wrote:
> On Thu, 5 Dec 2024 at 16:28, Richard Purdie
> <richard.purdie@linuxfoundation.org> wrote:
> 
> > The patch is therefore probably ok but I am a bit worried about
> > this
> > potentially breaking in the future if someone else (or I!) miss the
> > subtly in the code.
> 
> I still need to improve variable naming. I'll do that and send v2,
> but I'm not sure at the moment what can be done about this subtlety.

Probably a comment:

# No need to use mark_dependency since we would only match a fragment 
# from a specific layer and there can only be a single layer with a 
# given namespace.

Cheers,

Richard
Alexander Kanavin Dec. 5, 2024, 5:14 p.m. UTC | #6
On Thu, 5 Dec 2024 at 18:06, Richard Purdie
<richard.purdie@linuxfoundation.org> wrote:
> > I still need to improve variable naming. I'll do that and send v2,
> > but I'm not sure at the moment what can be done about this subtlety.
>
> Probably a comment:
>
> # No need to use mark_dependency since we would only match a fragment
> # from a specific layer and there can only be a single layer with a
> # given namespace.

Variables renamed in v4. This comment added in v5 :)

Alex
diff mbox series

Patch

diff --git a/lib/bb/parse/ast.py b/lib/bb/parse/ast.py
index 001ba8d289a..d3b7fef9895 100644
--- a/lib/bb/parse/ast.py
+++ b/lib/bb/parse/ast.py
@@ -326,6 +326,34 @@  class InheritDeferredNode(AstNode):
         inherits.append(self.inherit)
         data.setVar('__BBDEFINHERITS', inherits)
 
+class AddFragmentsNode(AstNode):
+    def __init__(self, filename, lineno, fragments_path_prefix, fragments_variable):
+        AstNode.__init__(self, filename, lineno)
+        self.path = fragments_path_prefix
+        self.variable = fragments_variable
+
+    def eval(self, data):
+        def find_fragment(layers, layerid, full_fragment_name):
+           for layerpath in layers.split():
+               candidate_fragment_path = os.path.join(layerpath, full_fragment_name)
+               if os.path.exists(candidate_fragment_path) and bb.utils.get_file_layer(candidate_fragment_path, data) == layerid:
+                   return candidate_fragment_path
+           return None
+
+        fragments = data.getVar(self.variable)
+        layers = data.getVar('BBLAYERS')
+
+        if not fragments:
+            return
+        for f in fragments.split():
+            layerid, fragment_name = f.split('/', 1)
+            full_fragment_name = data.expand("{}/{}.conf".format(self.path, fragment_name))
+            fragment_path = find_fragment(layers, layerid, full_fragment_name)
+            if fragment_path:
+                bb.parse.ConfHandler.include(self.filename, fragment_path, self.lineno, data, "include fragment")
+            else:
+                bb.error("Could not find fragment {} in enabled layers: {}".format(f, layers))
+
 def handleInclude(statements, filename, lineno, m, force):
     statements.append(IncludeNode(filename, lineno, m.group(1), force))
 
@@ -370,6 +398,11 @@  def handleInheritDeferred(statements, filename, lineno, m):
     classes = m.group(1)
     statements.append(InheritDeferredNode(filename, lineno, classes))
 
+def handleAddFragments(statements, filename, lineno, m):
+    fragments_path_prefix = m.group(1)
+    fragments_variable = m.group(2)
+    statements.append(AddFragmentsNode(filename, lineno, fragments_path_prefix, fragments_variable))
+
 def runAnonFuncs(d):
     code = []
     for funcname in d.getVar("__BBANONFUNCS", False) or []:
diff --git a/lib/bb/parse/parse_py/ConfHandler.py b/lib/bb/parse/parse_py/ConfHandler.py
index 27665443dd4..35321dacfe1 100644
--- a/lib/bb/parse/parse_py/ConfHandler.py
+++ b/lib/bb/parse/parse_py/ConfHandler.py
@@ -47,6 +47,7 @@  __export_regexp__ = re.compile( r"export\s+([a-zA-Z0-9\-_+.${}/~]+)$" )
 __unset_regexp__ = re.compile( r"unset\s+([a-zA-Z0-9\-_+.${}/~]+)$" )
 __unset_flag_regexp__ = re.compile( r"unset\s+([a-zA-Z0-9\-_+.${}/~]+)\[([a-zA-Z0-9\-_+.][a-zA-Z0-9\-_+.@]+)\]$" )
 __addpylib_regexp__      = re.compile(r"addpylib\s+(.+)\s+(.+)" )
+__addfragments_regexp__  = re.compile(r"addfragments\s+(.+)\s+(.+)" )
 
 def init(data):
     return
@@ -197,6 +198,11 @@  def feeder(lineno, s, fn, statements, baseconfig=False, conffile=True):
         ast.handlePyLib(statements, fn, lineno, m)
         return
 
+    m = __addfragments_regexp__.match(s)
+    if m:
+        ast.handleAddFragments(statements, fn, lineno, m)
+        return
+
     raise ParseError("unparsed line: '%s'" % s, fn, lineno);
 
 # Add us to the handlers list