Skip to content

Commit db84746

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 09d6233 commit db84746

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
@@ -1710,16 +1711,15 @@ def do_add_parser(self, parser_adder):
17101711
parser.add_argument('projects', metavar='PROJECT', nargs='*',
17111712
help='''projects (by name or path) to operate on;
17121713
defaults to active cloned projects''')
1714+
parser.add_argument('-j', '--jobs', nargs='?', const=-1,
1715+
default=1, type=int, action='store',
1716+
help='''Use multiple jobs to parallelize commands.
1717+
Pass no number or -1 to run commands on all cores.''')
17131718
return parser
17141719

1715-
def do_run(self, args, user_args):
1716-
failed = []
1717-
group_set = set(args.groups)
1718-
env = os.environ.copy()
1719-
for project in self._cloned_projects(args, only_active=not args.all):
1720-
if group_set and not group_set.intersection(set(project.groups)):
1721-
continue
1722-
1720+
async def run_for_project(self, project, args, semaphore):
1721+
async with semaphore:
1722+
env = os.environ.copy()
17231723
env["WEST_PROJECT_NAME"] = project.name
17241724
env["WEST_PROJECT_PATH"] = project.path
17251725
env["WEST_PROJECT_ABSPATH"] = project.abspath if project.abspath else ''
@@ -1729,12 +1729,39 @@ def do_run(self, args, user_args):
17291729

17301730
cwd = args.cwd if args.cwd else project.abspath
17311731

1732-
self.banner(
1733-
f'running "{args.subcommand}" in {project.name_and_path}:')
1734-
rc = subprocess.Popen(args.subcommand, shell=True, env=env,
1735-
cwd=cwd).wait()
1736-
if rc:
1737-
failed.append(project)
1732+
self.banner(f'running "{args.subcommand}" in {project.name_and_path}:',
1733+
end=('\r' if self.jobs > 1 else '\n'))
1734+
proc = await asyncio.create_subprocess_shell(
1735+
args.subcommand,
1736+
cwd=cwd, env=env, shell=True,
1737+
stdout=asyncio.subprocess.PIPE if self.jobs > 1 else None,
1738+
stderr=asyncio.subprocess.PIPE if self.jobs > 1 else None)
1739+
1740+
if self.jobs > 1:
1741+
(out, err) = await proc.communicate()
1742+
1743+
self.banner(f'finished "{args.subcommand}" in {project.name_and_path}:')
1744+
sys.stdout.write(out.decode())
1745+
sys.stderr.write(err.decode())
1746+
1747+
return proc.returncode
1748+
1749+
return await proc.wait()
1750+
1751+
def do_run(self, args, unknown):
1752+
group_set = set(args.groups)
1753+
projects = [p for p in self._cloned_projects(args, only_active=not args.all)
1754+
if not group_set or group_set.intersection(set(p.groups))]
1755+
1756+
asyncio.run(self.do_run_async(args, projects))
1757+
1758+
async def do_run_async(self, args, projects):
1759+
self.jobs = args.jobs if args.jobs > 0 else os.cpu_count() or sys.maxsize
1760+
sem = asyncio.Semaphore(self.jobs)
1761+
1762+
rcs = await asyncio.gather(*[self.run_for_project(p, args, sem) for p in projects])
1763+
1764+
failed = [p for (p, rc) in zip(projects, rcs) if rc]
17381765
self._handle_failed(args, failed)
17391766

17401767
GREP_EPILOG = '''

0 commit comments

Comments
 (0)