manifest: model west-commands as per-entry records#966
Conversation
661bd0b to
02fb69f
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #966 +/- ##
==========================================
+ Coverage 86.40% 86.45% +0.04%
==========================================
Files 11 11
Lines 3532 3566 +34
==========================================
+ Hits 3052 3083 +31
- Misses 480 483 +3
|
ea2092e to
ec491ab
Compare
ec491ab to
c35e889
Compare
|
I think the tests in #962 should got first because they record/"document" the existing behavior, as it was just changed. Then, this PR can adjust the tests if needed which will again document any behavior change (or none if none) |
Bump the schema version and relax the west-commands type from str to any. The parsing of the west-commands is offloaded to the manifest parser to allow for maps with a file/base_dit keyset. Signed-off-by: Pieter De Gendt <pieter.degendt@basalte.be>
da49e22 to
8660875
Compare
There was a problem hiding this comment.
I'm afraid "entry" sounds pretty vague.
We have a massive and self-inflicted name collision between:
- The
west-commands:field in the west.yml manifest - The "west-commands.yml" file(s) it points at
- The OTHER
west-commands:header line in those files! - The "west commands" descriptors in those files. "Descriptor" is an exception: theonly specific name around here? I wish we had more of these.
- The "west commands" .py files that actually implement the commands
- The mypy
WestCommandsType - The WestCommand python class
- A new "entry" class added by this PR
- project.west_commands
- others?
EVERYTHING is a "West command"! Unless it's an "extension", which does not help at all.
This mass collision starts in the user interface and documentation at https://docs.zephyrproject.org/latest/develop/west/extensions.html and unsurprisingly gets worse in the code. That relevant code has already given me many headaches and this one PR will obviously increase the count. I would like those collisions to ideally stop or at least start be mitigated.
Naming is hard, but this PR is definitely the right time to formalize some better terminology. I'm not implying a mass search/replace, at least not yet. But at least finalizing some "correct" names to accelerate communication, bug tracking and reviews.
To be fair, some of the above are unique enough not to need a specific name. For instance, there is only one Python file (per command), so a "west command python file" is specific enough. Not concise but good enough. On the other hand, the stuff in YAML files definitely needs more specific names. Even more so considering it's the user interface, even more so considering we have multiple YAML files.
Let me think about some terminology, just a heads-up.
There was a problem hiding this comment.
I'm confused for now, sorry. I don't understand why this requires a schema change. Maybe I need more time to better understand the problem statement.
I think I understand the value of having some base_dir in the internal implementation but I don't understand yet why this has to be exposed in the (changed) user interface. Why can't base_dir simply be added as a prefix in the output of west manifest --resolve?
Adding that base_dir does not seem to add any value to the user interface, it looks like a pure hack to make the output of west manifest --resolve work again.
Also, do we really need to add yet another level of lists? We already have two. That bit really looks like a new feature, not a bug fix.
Let me think about some terminology, just a heads-up.
-
west-commands.yml is a DESCRIPTOR LIST (e.g:
cmd_descs_listor longer). LIST sounds vague but I think we have no other, top-level list in this context and we can always extend it to "(command) descriptor list" in any context where it could be ambiguous. Hopefully, making a difference between the list, its file and the pointer to it from the manifest is obvious enough from the context when needed and they don't need different names. -
Each DESCRIPTOR has a single pointer to one command PYTHON FILE.
-
Each PYTHON FILE has a list of command CLASSES. No collision there.
|
|
||
| ret: list[WestCommandsEntry] = [] | ||
| for item in raw: | ||
| if isinstance(item, WestCommandsEntry): |
There was a problem hiding this comment.
In general, prefer the newer case and pattern matching. But don't go and change this before we close the high-level discussions.
|
|
||
| @dataclass(frozen=True) | ||
| class WestCommandsEntry: | ||
| '''A single west-commands spec declared in a project. |
There was a problem hiding this comment.
The term "project" is also problematic :-(
For west, a project (not my choice...) is a git repo:
https://docs.zephyrproject.org/latest/develop/west/manifest.html#projects
But in most IDEs and even Zephyr sometimes, a "project" can be the workspace:
- https://code.visualstudio.com/docs/editing/userinterface#_basic-layout
- https://docs.zephyrproject.org/latest/develop/getting_started/index.html#get-zephyr-and-install-python-dependencies
In the following instructions the name "zephyrproject" is used for the workspace,...
EDIT: one possible reason for this confusion is: many development "projects" are single-repo. That is: they don't use west or anything like it.
I think in the case the fix is easy: use either "workspace topdir" or "git top dir".
There was a problem hiding this comment.
I think for West, it's clear what a project is? Maybe should just name it like that.
| '''A single west-commands spec declared in a project. | |
| '''A single west-commands spec declared in a West project. |
| wc1: list[WestCommandsEntry], wc2: list[WestCommandsEntry] | ||
| ) -> list[WestCommandsEntry]: | ||
| # Merge two west_commands lists, dropping any entry from wc2 | ||
| # whose path is already in wc1 (first wins). |
There was a problem hiding this comment.
Such an override should print at least a DEBUG.
There was a problem hiding this comment.
Sure, the existing code also did not print though.
| ``base_dir`` needed to resolve Python files inside the spec when | ||
| the entry was promoted from a subdirectory submanifest. | ||
| ''' | ||
| warnings.warn( |
There was a problem hiding this comment.
Why not just fail? This wasn't part of any documented API AFAICT
There was a problem hiding this comment.
I was under the impression that all fields that do not start with an underscore are considered public? Anyway we don't need to break users of this.
There was a problem hiding this comment.
That is a typical Python convention but in west I believe the supported API is only the functions that have a pydoc - which I believe is leveraged to produce https://docs.zephyrproject.org/latest/develop/west/west-apis.html
There was a problem hiding this comment.
west_commands is documented public API, though. In the Zephyr docs (doc/develop/west/west-apis.rst) the Project autoclass documents it explicitly, including a version directive:
.. autoclass:: west.manifest.Project
.. (note: attributes are part of the class docstring)
...
.. versionchanged:: 0.8.0
The *west_commands* attribute is now always a list. In previous
releases, it could be a string or ``None``.It's rendered on https://docs.zephyrproject.org/latest/develop/west/west-apis.html (attributes come from the Project class docstring, which autoclass picks up).
We should make it deprecated IMO.
pdgendt
left a comment
There was a problem hiding this comment.
I'm confused for now, sorry. I don't understand why this requires a schema change. Maybe I need more time to better understand the problem statement.
I think I understand the value of having some
base_dirin the internal implementation but I don't understand yet why this has to be exposed in the (changed) user interface. Why can'tbase_dirsimply be added as a prefix in the output ofwest manifest --resolve?
The problem is that west manifest --resolve outputs a single yaml file, aggregated from the imported ones. The west-commands.yml descriptor files stay in their projects untouched.
We can't simply add the prefix, that conflates two genuinely different directories:
path= location of thewest-commands.ymlspec file, relative to project root (e.g.mf_subdir/scripts/winsubA/west-commands.yml)base_dir= the root that thefile:entries inside that spec resolve against (e.g.mf_subdir)
The Python file resolves as base_dir / desc['file'], i.e. relative to mf_subdir, not relative to the spec file's own directory mf_subdir/scripts/winsubA/. So you can't fold base_dir into the path prefix; they point at different places.
Adding that
base_dirdoes not seem to add any value to the user interface, it looks like a pure hack to make the output ofwest manifest --resolvework again.
True, base-dir is meaningless for a hand-authored manifest, but it is needed for a resolved manifest to work. I wouldn't call it a hack, but it is an internal detail surfacing in the user-facing schema.
Also, do we really need to add yet another level of lists? We already have two.
There's no level added? Currently we have the following "levels":
- the manifest
west-commands:field -> pointer(s) to spec file(s). Wasstr | list[str]. - inside each
west-commands.yml, the top-levelwest-commands:key -> list of descriptors. - inside each descriptor,
commands:-> list of command classes.
This PR changes level 1 only, and it widens the element type of that existing list from str to str | {file, base-dir}.
That bit really looks like a new feature, not a bug fix.
We're not adding new features? Users can't express anything new. It is genuinely a bug fix IMO.
Let me think about some terminology, just a heads-up.
- west-commands.yml is a DESCRIPTOR LIST (e.g:
cmd_descs_listor longer). LIST sounds vague but I think we have no other, top-level list in this context and we can always extend it to "(command) descriptor list" in any context where it could be ambiguous. Hopefully, making a difference between the list, its file and the pointer to it from the manifest is obvious enough from the context when needed and they don't need different names.- Each DESCRIPTOR has a single pointer to one commands PYTHON FILE.
- Each PYTHON FILE has a list of command CLASSES. No collision there.
I like the idea of using descriptor.
|
|
||
| @dataclass(frozen=True) | ||
| class WestCommandsEntry: | ||
| '''A single west-commands spec declared in a project. |
There was a problem hiding this comment.
I think for West, it's clear what a project is? Maybe should just name it like that.
| '''A single west-commands spec declared in a project. | |
| '''A single west-commands spec declared in a West project. |
|
|
||
| ret: list[WestCommandsEntry] = [] | ||
| for item in raw: | ||
| if isinstance(item, WestCommandsEntry): |
| wc1: list[WestCommandsEntry], wc2: list[WestCommandsEntry] | ||
| ) -> list[WestCommandsEntry]: | ||
| # Merge two west_commands lists, dropping any entry from wc2 | ||
| # whose path is already in wc1 (first wins). |
There was a problem hiding this comment.
Sure, the existing code also did not print though.
| ``base_dir`` needed to resolve Python files inside the spec when | ||
| the entry was promoted from a subdirectory submanifest. | ||
| ''' | ||
| warnings.warn( |
There was a problem hiding this comment.
I was under the impression that all fields that do not start with an underscore are considered public? Anyway we don't need to break users of this.
To come back to this a bit, I very much agree that this is a massive source of confusion and likely a reason why the related bits are hard to reason about. I propose "West Commands Index" be used for the west-commands.yml file in which west-command-python-files are listed along with the west extension commands that they define. We can then call "WestCommandsEntry" "WestCommandsIndexPath" or something, we can refer to those defined within a west manifest as its west commands indices... I think picking a proper name for that particular file actually resolves a lot of the unclarity simply by providing a good mental reference point from which other naming can branch off of. |
Great summary, thanks. I've been thinking about this... "inconsistency" a little bit (not as much as I wanted to yet), and maybe
Thanks for concurring, that means a lot from the person who did 96ad4f3 and friends :-) |
Not particularly thrilled about this. |
Not thrilled about this either but I think it's a small enough egg to break. I think we should look at what are the practical use cases for In an informative, read-only, "pure SBOM" use case, west extensions don't matter. This is probably the most common use case and why we never heard of this bug before and just discovered it "by chance". So in this particular context we must focus on the more "active" use case where people actually try to feed the resolved manifest back to So I think that leaves only the case of people actually trying to use the resolved manifest to both:
So yes: the (small minority of?) people bent on doing all that would have to change 2. and copy one extra file, which is still not the end of the world. Unless I missed some use cases?
Maybe we could have a middle-ground with a smaller
So |
If we must make a This could be used by
Interestingly, NONE of the solutions mentioned so far achieves "perfect" reproducibility for the simple reason that:
That will always be a major difference no matter what. It's the "original sin" and why we are stuck between a rock and a hard place.
That "simplicity" (quotes mine) is deceiving. |
I don't see the backwards incompatible change that you're seeing. From what I can see, the new schema should be backwards compatible with existing schemas? Personally I would be in favor of adding base-dir to the schema as a solution. It feels like the alternative suggested approach would be much more complex, and has a much higher chance of followup issues appearing. I think a schema change is necessary anyway to allow round trips, because we (as far as I can tell) currently do not support lists of west-command-index. Consider: where both of those import a west-command-index via |
My bad, I meant "incompatible schema change" and I added "backwards" by force of habit.
How would some generated |
|
I just remembered that Imagine you want to copy Before rsync v2.6.7, none of these did it:
You could of course always So, rsync v2.6.7 added the following "dot separator":
This creates Would such a "dot separator" work in this case too? I think we can assume that no one used any dot separator in their EDIT: any other separator should work too. Maybe a separator that filesystems do NOT parse successfully would be more explicit - and then it would absolutely not require any schema version bump. I suspect |
I suppose it could, it just feels to me like it would become even more of a hacky solution, especially considering that there might be some edge case where someone does want west-commands-index to be a list.
To me this feels like it would be an even hackier way of doing things. Especially if we wanted to make a separator that filesystems do not parse successfully, then to my eyes this wouldn't be much different to having If we were starting from scratch, the correct way of handling it would likely be to just state that the west-command-python-file paths specified in a west-commands-index file must be relative to the project root, rather than relative to the west manifest. Perhaps we can do something where we deprecate the current path handling and set the schema type as |
Replace the WestCommandsType alias and the parallel
_west_commands_manifest_dirs dict with a frozen WestCommandsEntry
dataclass carrying (path, base_dir). Project.west_commands_entries is
the new canonical attribute; the old west_commands becomes a deprecated
read-only property. Schema accepts str / list[str] / list[{file,
base-dir}] so resolved manifests roundtrip through as_yaml() without
losing the base_dir needed to resolve `file:` paths inside specs
imported from a project subdirectory.
Signed-off-by: Pieter De Gendt <pieter.degendt@basalte.be>
8660875 to
00a5f5a
Compare
Replace the
WestCommandsTypealias and the parallel_west_commands_manifest_dirsdict with a frozenWestCommandsEntrydataclass carrying (path,base_dir).Project.west_commands_entriesis the new canonical attribute; the oldwest_commandsbecomes a deprecated read-only property. Schema acceptsstr/list[str]/list[{file, base-dir}]so resolved manifests roundtrip throughas_yaml()without losing thebase_dirneeded to resolvefile:paths inside specs imported from a project subdirectory.Fixes #959
Supersedes #965