Skip to content

Commit 5e6eb57

Browse files
committed
app: Update Forall command to allow multiple concurrent processes
Demonstrate asynchronous behavior for the Forall command and add an argument to select the number of jobs. Signed-off-by: Pieter De Gendt <[email protected]>
1 parent f3cdb3f commit 5e6eb57

File tree

1 file changed

+41
-14
lines changed

1 file changed

+41
-14
lines changed

src/west/app/project.py

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
'''West project commands'''
77

88
import argparse
9+
import asyncio
910
import logging
1011
import os
1112
import shlex
@@ -1809,16 +1810,15 @@ def do_add_parser(self, parser_adder):
18091810
parser.add_argument('projects', metavar='PROJECT', nargs='*',
18101811
help='''projects (by name or path) to operate on;
18111812
defaults to active cloned projects''')
1813+
parser.add_argument('-j', '--jobs', nargs='?', const=-1,
1814+
default=1, type=int, action='store',
1815+
help='''Use multiple jobs to parallelize commands.
1816+
Pass no number or -1 to run commands on all cores.''')
18121817
return parser
18131818

1814-
def do_run(self, args, user_args):
1815-
failed = []
1816-
group_set = set(args.groups)
1817-
env = os.environ.copy()
1818-
for project in self._cloned_projects(args, only_active=not args.all):
1819-
if group_set and not group_set.intersection(set(project.groups)):
1820-
continue
1821-
1819+
async def run_for_project(self, project, args, semaphore):
1820+
async with semaphore:
1821+
env = os.environ.copy()
18221822
env["WEST_PROJECT_NAME"] = project.name
18231823
env["WEST_PROJECT_PATH"] = project.path
18241824
env["WEST_PROJECT_ABSPATH"] = project.abspath if project.abspath else ''
@@ -1828,12 +1828,39 @@ def do_run(self, args, user_args):
18281828

18291829
cwd = args.cwd if args.cwd else project.abspath
18301830

1831-
self.banner(
1832-
f'running "{args.subcommand}" in {project.name_and_path}:')
1833-
rc = subprocess.Popen(args.subcommand, shell=True, env=env,
1834-
cwd=cwd).wait()
1835-
if rc:
1836-
failed.append(project)
1831+
self.banner(f'running "{args.subcommand}" in {project.name_and_path}:',
1832+
end=('\r' if self.jobs > 1 else '\n'))
1833+
proc = await asyncio.create_subprocess_shell(
1834+
args.subcommand,
1835+
cwd=cwd, env=env, shell=True,
1836+
stdout=asyncio.subprocess.PIPE if self.jobs > 1 else None,
1837+
stderr=asyncio.subprocess.PIPE if self.jobs > 1 else None)
1838+
1839+
if self.jobs > 1:
1840+
(out, err) = await proc.communicate()
1841+
1842+
self.banner(f'finished "{args.subcommand}" in {project.name_and_path}:')
1843+
sys.stdout.write(out.decode())
1844+
sys.stderr.write(err.decode())
1845+
1846+
return proc.returncode
1847+
1848+
return await proc.wait()
1849+
1850+
def do_run(self, args, unknown):
1851+
group_set = set(args.groups)
1852+
projects = [p for p in self._cloned_projects(args, only_active=not args.all)
1853+
if not group_set or group_set.intersection(set(p.groups))]
1854+
1855+
asyncio.run(self.do_run_async(args, projects))
1856+
1857+
async def do_run_async(self, args, projects):
1858+
self.jobs = args.jobs if args.jobs > 0 else os.cpu_count() or sys.maxsize
1859+
sem = asyncio.Semaphore(self.jobs)
1860+
1861+
rcs = await asyncio.gather(*[self.run_for_project(p, args, sem) for p in projects])
1862+
1863+
failed = [p for (p, rc) in zip(projects, rcs) if rc]
18371864
self._handle_failed(args, failed)
18381865

18391866
GREP_EPILOG = '''

0 commit comments

Comments
 (0)