Skip to content

Commit d35e837

Browse files
manifest: support URL modification in downstream manifests
Add support for a new 'import-modifications' section in the manifest. Under the 'url-replace' key, users can define search-and-replace patterns that are applied to project URLs during manifest import. This allows downstream projects to modify remote URLs, e.g. when using mirrored repositories.
1 parent e24ae8e commit d35e837

File tree

3 files changed

+307
-0
lines changed

3 files changed

+307
-0
lines changed

src/west/manifest-schema.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,25 @@ mapping:
6666
required: true
6767
type: str
6868

69+
# allow some modification of imported projects by downstream projects
70+
import-modifications:
71+
required: false
72+
type: map
73+
mapping:
74+
# search-replace within URLs (e.g. to use mirror URLs)
75+
url-replace:
76+
required: false
77+
type: seq
78+
sequence:
79+
- type: map
80+
mapping:
81+
old:
82+
required: true
83+
type: str
84+
new:
85+
required: true
86+
type: str
87+
6988
# The "projects" key specifies a sequence of "projects", each of which has a
7089
# remote, and may specify additional configuration.
7190
#

src/west/manifest.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
Parser and abstract data types for west manifests.
88
'''
99

10+
import copy
1011
import enum
1112
import errno
1213
import logging
@@ -182,6 +183,34 @@ def _err(message):
182183
_logger = logging.getLogger(__name__)
183184

184185

186+
# Representation of import-modifications
187+
188+
189+
class ImportModifications:
190+
"""Represents `import-modifications` within a manifest."""
191+
192+
def __init__(self, manifest_data: dict | None = None):
193+
"""Initialize a new ImportModifications instance."""
194+
self.url_replaces: list[tuple[str, str]] = []
195+
self.append(manifest_data or {})
196+
197+
def append(self, manifest_data: dict):
198+
"""Append values from a manifest data (dictionary) to this instance."""
199+
for kind, values in manifest_data.get('import-modifications', {}).items():
200+
if kind == 'url-replace':
201+
self.url_replaces += [(v['old'], v['new']) for v in values]
202+
203+
def merge(self, other):
204+
"""Merge another ImportModifications instance into this one."""
205+
if not isinstance(other, ImportModifications):
206+
raise TypeError(f"Unsupported type'{type(other).__name__}'")
207+
self.url_replaces += other.url_replaces
208+
209+
def copy(self):
210+
"""Return a deep copy of this instance."""
211+
return copy.deepcopy(self)
212+
213+
185214
# Type for the submodule value passed through the manifest file.
186215
class Submodule(NamedTuple):
187216
'''Represents a Git submodule within a project.'''
@@ -456,6 +485,9 @@ class _import_ctx(NamedTuple):
456485
# Bit vector of flags that modify import behavior.
457486
import_flags: 'ImportFlag'
458487

488+
# import-modifications
489+
modifications: ImportModifications
490+
459491

460492
def _imap_filter_allows(imap_filter: ImapFilterFnType, project: 'Project') -> bool:
461493
# imap_filter(project) if imap_filter is not None; True otherwise.
@@ -2052,6 +2084,7 @@ def get_option(option, default=None):
20522084
current_repo_abspath=current_repo_abspath,
20532085
project_importer=project_importer,
20542086
import_flags=import_flags,
2087+
modifications=ImportModifications(),
20552088
)
20562089

20572090
def _recursive_init(self, ctx: _import_ctx):
@@ -2074,6 +2107,10 @@ def _load_validated(self) -> None:
20742107

20752108
manifest_data = self._ctx.current_data['manifest']
20762109

2110+
# append values from resolved manifest_data to current context
2111+
new_modifications = ImportModifications(manifest_data)
2112+
self._ctx.modifications.merge(new_modifications)
2113+
20772114
schema_version = str(manifest_data.get('version', SCHEMA_VERSION))
20782115

20792116
# We want to make an ordered map from project names to
@@ -2322,6 +2359,7 @@ def _import_pathobj_from_self(self, pathobj_abs: Path, pathobj: Path) -> None:
23222359
current_abspath=pathobj_abs,
23232360
current_relpath=pathobj,
23242361
current_data=pathobj_abs.read_text(encoding=Manifest.encoding),
2362+
modifications=self._ctx.modifications.copy(),
23252363
)
23262364
try:
23272365
Manifest(topdir=self.topdir, internal_import_ctx=child_ctx)
@@ -2452,6 +2490,13 @@ def _load_project(self, pd: dict, url_bases: dict[str, str], defaults: _defaults
24522490
else:
24532491
self._malformed(f'project {name} has no remote or url and no default remote is set')
24542492

2493+
# modify the url
2494+
if url:
2495+
url_replaces = self._ctx.modifications.url_replaces
2496+
for url_replace in reversed(url_replaces):
2497+
old, new = url_replace
2498+
url = url.replace(old, new)
2499+
24552500
# The project's path needs to respect any import: path-prefix,
24562501
# regardless of self._ctx.import_flags. The 'ignore' type flags
24572502
# just mean ignore the imported data. The path-prefix in this
@@ -2672,6 +2717,7 @@ def _import_data_from_project(
26722717
# We therefore use a separate list for tracking them
26732718
# from our current list.
26742719
manifest_west_commands=[],
2720+
modifications=self._ctx.modifications.copy(),
26752721
)
26762722
try:
26772723
submanifest = Manifest(topdir=self.topdir, internal_import_ctx=child_ctx)

tests/test_project.py

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,248 @@ def test_workspace(west_update_tmpdir):
122122
assert wct.join('zephyr', 'subsys', 'bluetooth', 'code.c').check(file=1)
123123

124124

125+
def test_workspace_stacked_imports(repos_tmpdir):
126+
remote_zephyr = repos_tmpdir / 'repos' / 'zephyr'
127+
projects_dir = repos_tmpdir / 'projects'
128+
projects_dir.mkdir()
129+
130+
# create a local base project with a west.yml
131+
project_base = projects_dir / 'base'
132+
project_base.mkdir()
133+
with open(project_base / 'west.yml', 'w') as f:
134+
f.write(
135+
textwrap.dedent(f'''\
136+
manifest:
137+
remotes:
138+
- name: upstream
139+
url-base: {os.path.dirname(remote_zephyr)}
140+
projects:
141+
- name: zephyr
142+
remote: upstream
143+
path: zephyr-rtos
144+
import: True
145+
''')
146+
)
147+
148+
# create another project with another west.yml (stacked on base)
149+
project_middle = projects_dir / 'middle'
150+
project_middle.mkdir()
151+
with open(project_middle / 'west.yml', 'w') as f:
152+
f.write(
153+
textwrap.dedent('''\
154+
manifest:
155+
self:
156+
import: ../base
157+
''')
158+
)
159+
160+
# create another project with another west.yml (stacked on base)
161+
project_app = projects_dir / 'app'
162+
project_app.mkdir()
163+
with open(project_app / 'west.yml', 'w') as f:
164+
f.write(
165+
textwrap.dedent('''\
166+
manifest:
167+
self:
168+
import: ../middle
169+
''')
170+
)
171+
172+
# init workspace in projects_dir (project_app's parent)
173+
cmd(['init', '-l', project_app])
174+
175+
# update workspace in projects_dir
176+
cmd('update', cwd=projects_dir)
177+
178+
ws = projects_dir
179+
# zephyr projects from base are cloned
180+
for project_subdir in [
181+
Path('subdir') / 'Kconfiglib',
182+
'tagged_repo',
183+
'net-tools',
184+
'zephyr-rtos',
185+
]:
186+
assert (ws / project_subdir).check(dir=1)
187+
assert (ws / project_subdir / '.git').check(dir=1)
188+
189+
190+
def test_workspace_modify_url_replace(tmpdir, repos_tmpdir):
191+
remotes_dir = repos_tmpdir / 'repos'
192+
workspace_dir = tmpdir / 'workspace'
193+
workspace_dir.mkdir()
194+
195+
# use remote zephyr
196+
remote_zephyr = tmpdir / 'repos' / 'zephyr'
197+
198+
# create a local base project with a west.yml
199+
project_base = remotes_dir / 'base'
200+
create_repo(project_base)
201+
add_commit(
202+
project_base,
203+
'manifest commit',
204+
# zephyr revision is implicitly master:
205+
files={
206+
'west.yml': textwrap.dedent('''
207+
manifest:
208+
import-modifications:
209+
url-replace:
210+
- old: xxx
211+
new: yyy
212+
remotes:
213+
- name: upstream
214+
url-base: xxx
215+
projects:
216+
- name: zephyr
217+
remote: upstream
218+
path: zephyr-rtos
219+
import: True
220+
''')
221+
},
222+
)
223+
224+
# create another project with another west.yml (stacked on base)
225+
project_middle = remotes_dir / 'middle'
226+
create_repo(project_middle)
227+
add_commit(
228+
project_middle,
229+
'manifest commit',
230+
# zephyr revision is implicitly master:
231+
files={
232+
'west.yml': f'''
233+
manifest:
234+
import-modifications:
235+
url-replace:
236+
- old: yyy
237+
new: zzz
238+
projects:
239+
- name: base
240+
url: {project_base}
241+
import: True
242+
'''
243+
},
244+
)
245+
246+
# create an app that uses middle project
247+
project_app = workspace_dir / 'app'
248+
project_app.mkdir()
249+
with open(project_app / 'west.yml', 'w') as f:
250+
f.write(
251+
textwrap.dedent(f'''\
252+
manifest:
253+
import-modifications:
254+
url-replace:
255+
- old: zzz
256+
new: {os.path.dirname(remote_zephyr)}
257+
projects:
258+
- name: middle
259+
url: {project_middle}
260+
import: True
261+
''')
262+
)
263+
264+
# init workspace in projects_dir (project_app's parent)
265+
cmd(['init', '-l', project_app])
266+
267+
# update workspace in projects_dir
268+
cmd('update', cwd=workspace_dir)
269+
270+
# zephyr projects from base are cloned
271+
for project_subdir in [
272+
Path('subdir') / 'Kconfiglib',
273+
'tagged_repo',
274+
'net-tools',
275+
'zephyr-rtos',
276+
]:
277+
assert (workspace_dir / project_subdir).check(dir=1)
278+
assert (workspace_dir / project_subdir / '.git').check(dir=1)
279+
280+
281+
def test_workspace_modify_url_replace_with_self_import(repos_tmpdir):
282+
remote_zephyr = repos_tmpdir / 'repos' / 'zephyr'
283+
projects_dir = repos_tmpdir / 'projects'
284+
projects_dir.mkdir()
285+
286+
# create a local base project with a west.yml
287+
project_base = projects_dir / 'base'
288+
project_base.mkdir()
289+
with open(project_base / 'west.yml', 'w') as f:
290+
f.write(
291+
textwrap.dedent('''\
292+
manifest:
293+
remotes:
294+
- name: upstream
295+
url-base: nonexistent
296+
projects:
297+
- name: zephyr
298+
remote: upstream
299+
path: zephyr-rtos
300+
import: True
301+
''')
302+
)
303+
304+
# create another project with another west.yml (stacked on base)
305+
project_middle = projects_dir / 'middle'
306+
project_middle.mkdir()
307+
with open(project_middle / 'west.yml', 'w') as f:
308+
f.write(
309+
textwrap.dedent('''\
310+
manifest:
311+
self:
312+
import: ../base
313+
''')
314+
)
315+
316+
# create another project with another west.yml (stacked on base)
317+
project_another = projects_dir / 'another'
318+
project_another.mkdir()
319+
with open(project_another / 'west.yml', 'w') as f:
320+
f.write(
321+
textwrap.dedent('''\
322+
manifest:
323+
# this should not have any effect since there are no imports
324+
import-modifications:
325+
url-replace:
326+
- old: nonexistent
327+
new: from-another
328+
''')
329+
)
330+
331+
# create another project with another west.yml (stacked on base)
332+
project_app = projects_dir / 'app'
333+
project_app.mkdir()
334+
with open(project_app / 'west.yml', 'w') as f:
335+
f.write(
336+
textwrap.dedent(f'''\
337+
manifest:
338+
import-modifications:
339+
url-replace:
340+
- old: nonexistent
341+
new: {os.path.dirname(remote_zephyr)}
342+
self:
343+
import:
344+
- ../another
345+
- ../middle
346+
''')
347+
)
348+
349+
# init workspace in projects_dir (project_app's parent)
350+
cmd(['init', '-l', project_app])
351+
352+
# update workspace in projects_dir
353+
cmd('update', cwd=projects_dir)
354+
355+
ws = projects_dir
356+
# zephyr projects from base are cloned
357+
for project_subdir in [
358+
Path('subdir') / 'Kconfiglib',
359+
'tagged_repo',
360+
'net-tools',
361+
'zephyr-rtos',
362+
]:
363+
assert (ws / project_subdir).check(dir=1)
364+
assert (ws / project_subdir / '.git').check(dir=1)
365+
366+
125367
def test_list(west_update_tmpdir):
126368
# Projects shall be listed in the order they appear in the manifest.
127369
# Check the behavior for some format arguments of interest as well.

0 commit comments

Comments
 (0)