Skip to content

Commit b948d1b

Browse files
1 parent be3a6d0 commit b948d1b

File tree

1 file changed

+354
-0
lines changed

1 file changed

+354
-0
lines changed

patches/869.patch

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
From 272c585f4901b75ea04796ef65feb06c34b2415b Mon Sep 17 00:00:00 2001
2+
From: "Klein, Thorsten" <[email protected]>
3+
Date: Thu, 16 Oct 2025 14:37:51 +0200
4+
Subject: [PATCH 1/2] manifest: support URL modification in downstream
5+
manifests
6+
7+
Add support for a new 'import-modifications' section in the manifest.
8+
9+
Under the 'url-replace' key, users can define search-and-replace
10+
patterns that are applied to project URLs during manifest import.
11+
This allows downstream projects to modify remote URLs, e.g. when using
12+
mirrored repositories.
13+
---
14+
src/west/manifest-schema.yml | 19 +++++++++++++++
15+
src/west/manifest.py | 46 ++++++++++++++++++++++++++++++++++++
16+
2 files changed, 65 insertions(+)
17+
18+
diff --git a/src/west/manifest-schema.yml b/src/west/manifest-schema.yml
19+
index ebf82b53d..0b15c50c0 100644
20+
--- a/src/west/manifest-schema.yml
21+
+++ b/src/west/manifest-schema.yml
22+
@@ -66,6 +66,25 @@ mapping:
23+
required: true
24+
type: str
25+
26+
+ # allow some modification of imported projects by downstream projects
27+
+ import-modifications:
28+
+ required: false
29+
+ type: map
30+
+ mapping:
31+
+ # search-replace within URLs (e.g. to use mirror URLs)
32+
+ url-replace:
33+
+ required: false
34+
+ type: seq
35+
+ sequence:
36+
+ - type: map
37+
+ mapping:
38+
+ old:
39+
+ required: true
40+
+ type: str
41+
+ new:
42+
+ required: true
43+
+ type: str
44+
+
45+
# The "projects" key specifies a sequence of "projects", each of which has a
46+
# remote, and may specify additional configuration.
47+
#
48+
diff --git a/src/west/manifest.py b/src/west/manifest.py
49+
index 95afac72b..e753b32bc 100644
50+
--- a/src/west/manifest.py
51+
+++ b/src/west/manifest.py
52+
@@ -7,6 +7,7 @@
53+
Parser and abstract data types for west manifests.
54+
'''
55+
56+
+import copy
57+
import enum
58+
import errno
59+
import logging
60+
@@ -182,6 +183,34 @@ def _err(message):
61+
_logger = logging.getLogger(__name__)
62+
63+
64+
+# Representation of import-modifications
65+
+
66+
+
67+
+class ImportModifications:
68+
+ """Represents `import-modifications` within a manifest."""
69+
+
70+
+ def __init__(self, manifest_data: dict | None = None):
71+
+ """Initialize a new ImportModifications instance."""
72+
+ self.url_replaces: list[tuple[str, str]] = []
73+
+ self.append(manifest_data or {})
74+
+
75+
+ def append(self, manifest_data: dict):
76+
+ """Append values from a manifest data (dictionary) to this instance."""
77+
+ for kind, values in manifest_data.get('import-modifications', {}).items():
78+
+ if kind == 'url-replace':
79+
+ self.url_replaces += [(v['old'], v['new']) for v in values]
80+
+
81+
+ def merge(self, other):
82+
+ """Merge another ImportModifications instance into this one."""
83+
+ if not isinstance(other, ImportModifications):
84+
+ raise TypeError(f"Unsupported type'{type(other).__name__}'")
85+
+ self.url_replaces += other.url_replaces
86+
+
87+
+ def copy(self):
88+
+ """Return a deep copy of this instance."""
89+
+ return copy.deepcopy(self)
90+
+
91+
+
92+
# Type for the submodule value passed through the manifest file.
93+
class Submodule(NamedTuple):
94+
'''Represents a Git submodule within a project.'''
95+
@@ -456,6 +485,9 @@ class _import_ctx(NamedTuple):
96+
# Bit vector of flags that modify import behavior.
97+
import_flags: 'ImportFlag'
98+
99+
+ # import-modifications
100+
+ modifications: ImportModifications
101+
+
102+
103+
def _imap_filter_allows(imap_filter: ImapFilterFnType, project: 'Project') -> bool:
104+
# imap_filter(project) if imap_filter is not None; True otherwise.
105+
@@ -2052,6 +2084,7 @@ def get_option(option, default=None):
106+
current_repo_abspath=current_repo_abspath,
107+
project_importer=project_importer,
108+
import_flags=import_flags,
109+
+ modifications=ImportModifications(),
110+
)
111+
112+
def _recursive_init(self, ctx: _import_ctx):
113+
@@ -2074,6 +2107,10 @@ def _load_validated(self) -> None:
114+
115+
manifest_data = self._ctx.current_data['manifest']
116+
117+
+ # append values from resolved manifest_data to current context
118+
+ new_modifications = ImportModifications(manifest_data)
119+
+ self._ctx.modifications.merge(new_modifications)
120+
+
121+
schema_version = str(manifest_data.get('version', SCHEMA_VERSION))
122+
123+
# We want to make an ordered map from project names to
124+
@@ -2322,6 +2359,7 @@ def _import_pathobj_from_self(self, pathobj_abs: Path, pathobj: Path) -> None:
125+
current_abspath=pathobj_abs,
126+
current_relpath=pathobj,
127+
current_data=pathobj_abs.read_text(encoding=Manifest.encoding),
128+
+ modifications=self._ctx.modifications.copy(),
129+
)
130+
try:
131+
Manifest(topdir=self.topdir, internal_import_ctx=child_ctx)
132+
@@ -2452,6 +2490,13 @@ def _load_project(self, pd: dict, url_bases: dict[str, str], defaults: _defaults
133+
else:
134+
self._malformed(f'project {name} has no remote or url and no default remote is set')
135+
136+
+ # modify the url
137+
+ if url:
138+
+ url_replaces = self._ctx.modifications.url_replaces
139+
+ for url_replace in reversed(url_replaces):
140+
+ old, new = url_replace
141+
+ url = url.replace(old, new)
142+
+
143+
# The project's path needs to respect any import: path-prefix,
144+
# regardless of self._ctx.import_flags. The 'ignore' type flags
145+
# just mean ignore the imported data. The path-prefix in this
146+
@@ -2672,6 +2717,7 @@ def _import_data_from_project(
147+
# We therefore use a separate list for tracking them
148+
# from our current list.
149+
manifest_west_commands=[],
150+
+ modifications=self._ctx.modifications.copy(),
151+
)
152+
try:
153+
submanifest = Manifest(topdir=self.topdir, internal_import_ctx=child_ctx)
154+
155+
From bc81ddf8a97220b87a785b68d4ab71cca2adbf10 Mon Sep 17 00:00:00 2001
156+
From: Thorsten Klein <[email protected]>
157+
Date: Sun, 26 Oct 2025 17:00:27 +0100
158+
Subject: [PATCH 2/2] Added test for import-modifications
159+
160+
Test that import-modifications feature works to override west project
161+
urls that directly specified in west manifest or imported via
162+
submanifests.
163+
---
164+
tests/test_project.py | 177 ++++++++++++++++++++++++++++++++++++++++++
165+
1 file changed, 177 insertions(+)
166+
167+
diff --git a/tests/test_project.py b/tests/test_project.py
168+
index 58c3906a5..401158e22 100644
169+
--- a/tests/test_project.py
170+
+++ b/tests/test_project.py
171+
@@ -121,6 +121,183 @@ def test_workspace(west_update_tmpdir):
172+
assert wct.join('zephyr', 'subsys', 'bluetooth', 'code.c').check(file=1)
173+
174+
175+
+def test_workspace_modify_url_replace(tmpdir, repos_tmpdir):
176+
+ remotes_dir = repos_tmpdir / 'repos'
177+
+ workspace_dir = tmpdir / 'workspace'
178+
+ workspace_dir.mkdir()
179+
+
180+
+ # use remote zephyr
181+
+ remote_zephyr = tmpdir / 'repos' / 'zephyr'
182+
+
183+
+ # create a local base project with a west.yml
184+
+ project_base = remotes_dir / 'base'
185+
+ create_repo(project_base)
186+
+ add_commit(
187+
+ project_base,
188+
+ 'manifest commit',
189+
+ # zephyr revision is implicitly master:
190+
+ files={
191+
+ 'west.yml': textwrap.dedent('''
192+
+ manifest:
193+
+ import-modifications:
194+
+ url-replace:
195+
+ - old: xxx
196+
+ new: yyy
197+
+ remotes:
198+
+ - name: upstream
199+
+ url-base: xxx
200+
+ projects:
201+
+ - name: zephyr
202+
+ remote: upstream
203+
+ path: zephyr-rtos
204+
+ import: True
205+
+ ''')
206+
+ },
207+
+ )
208+
+
209+
+ # create another project with another west.yml (stacked on base)
210+
+ project_middle = remotes_dir / 'middle'
211+
+ create_repo(project_middle)
212+
+ add_commit(
213+
+ project_middle,
214+
+ 'manifest commit',
215+
+ # zephyr revision is implicitly master:
216+
+ files={
217+
+ 'west.yml': f'''
218+
+ manifest:
219+
+ import-modifications:
220+
+ url-replace:
221+
+ - old: yyy
222+
+ new: zzz
223+
+ projects:
224+
+ - name: base
225+
+ url: {project_base}
226+
+ import: True
227+
+ '''
228+
+ },
229+
+ )
230+
+
231+
+ # create an app that uses middle project
232+
+ project_app = workspace_dir / 'app'
233+
+ project_app.mkdir()
234+
+ with open(project_app / 'west.yml', 'w') as f:
235+
+ f.write(
236+
+ textwrap.dedent(f'''\
237+
+ manifest:
238+
+ import-modifications:
239+
+ url-replace:
240+
+ - old: zzz
241+
+ new: {os.path.dirname(remote_zephyr)}
242+
+ projects:
243+
+ - name: middle
244+
+ url: {project_middle}
245+
+ import: True
246+
+ ''')
247+
+ )
248+
+
249+
+ # init workspace in projects_dir (project_app's parent)
250+
+ cmd(['init', '-l', project_app])
251+
+
252+
+ # update workspace in projects_dir
253+
+ cmd('update', cwd=workspace_dir)
254+
+
255+
+ # zephyr projects from base are cloned
256+
+ for project_subdir in [
257+
+ Path('subdir') / 'Kconfiglib',
258+
+ 'tagged_repo',
259+
+ 'net-tools',
260+
+ 'zephyr-rtos',
261+
+ ]:
262+
+ assert (workspace_dir / project_subdir).check(dir=1)
263+
+ assert (workspace_dir / project_subdir / '.git').check(dir=1)
264+
+
265+
+
266+
+def test_workspace_modify_url_replace_with_self_import(repos_tmpdir):
267+
+ remote_zephyr = repos_tmpdir / 'repos' / 'zephyr'
268+
+ projects_dir = repos_tmpdir / 'projects'
269+
+ projects_dir.mkdir()
270+
+
271+
+ # create a local base project with a west.yml
272+
+ project_base = projects_dir / 'base'
273+
+ project_base.mkdir()
274+
+ with open(project_base / 'west.yml', 'w') as f:
275+
+ f.write(
276+
+ textwrap.dedent('''\
277+
+ manifest:
278+
+ remotes:
279+
+ - name: upstream
280+
+ url-base: nonexistent
281+
+ projects:
282+
+ - name: zephyr
283+
+ remote: upstream
284+
+ path: zephyr-rtos
285+
+ import: True
286+
+ ''')
287+
+ )
288+
+
289+
+ # create another project with another west.yml (stacked on base)
290+
+ project_middle = projects_dir / 'middle'
291+
+ project_middle.mkdir()
292+
+ with open(project_middle / 'west.yml', 'w') as f:
293+
+ f.write(
294+
+ textwrap.dedent('''\
295+
+ manifest:
296+
+ self:
297+
+ import: ../base
298+
+ ''')
299+
+ )
300+
+
301+
+ # create another project with another west.yml (stacked on base)
302+
+ project_another = projects_dir / 'another'
303+
+ project_another.mkdir()
304+
+ with open(project_another / 'west.yml', 'w') as f:
305+
+ f.write(
306+
+ textwrap.dedent('''\
307+
+ manifest:
308+
+ # this should not have any effect since there are no imports
309+
+ import-modifications:
310+
+ url-replace:
311+
+ - old: nonexistent
312+
+ new: from-another
313+
+ ''')
314+
+ )
315+
+
316+
+ # create another project with another west.yml (stacked on base)
317+
+ project_app = projects_dir / 'app'
318+
+ project_app.mkdir()
319+
+ with open(project_app / 'west.yml', 'w') as f:
320+
+ f.write(
321+
+ textwrap.dedent(f'''\
322+
+ manifest:
323+
+ import-modifications:
324+
+ url-replace:
325+
+ - old: nonexistent
326+
+ new: {os.path.dirname(remote_zephyr)}
327+
+ self:
328+
+ import:
329+
+ - ../another
330+
+ - ../middle
331+
+ ''')
332+
+ )
333+
+
334+
+ # init workspace in projects_dir (project_app's parent)
335+
+ cmd(['init', '-l', project_app])
336+
+
337+
+ # update workspace in projects_dir
338+
+ cmd('update', cwd=projects_dir)
339+
+
340+
+ ws = projects_dir
341+
+ # zephyr projects from base are cloned
342+
+ for project_subdir in [
343+
+ Path('subdir') / 'Kconfiglib',
344+
+ 'tagged_repo',
345+
+ 'net-tools',
346+
+ 'zephyr-rtos',
347+
+ ]:
348+
+ assert (ws / project_subdir).check(dir=1)
349+
+ assert (ws / project_subdir / '.git').check(dir=1)
350+
+
351+
+
352+
def test_list(west_update_tmpdir):
353+
# Projects shall be listed in the order they appear in the manifest.
354+
# Check the behavior for some format arguments of interest as well.

0 commit comments

Comments
 (0)