1414"""
1515from argparse import ArgumentParser
1616import copy
17+ import concurrent .futures
1718from os import popen , system , environ
1819import shutil
1920from pathlib import Path
2021from dataclasses import dataclass
21- from sys import executable
22+ from sys import executable , stderr
2223from tarfile import open as tar_open , TarInfo
2324import platform as host_platform
2425from enum import IntEnum
2526import json
27+ import os
28+ import tempfile
2629import subprocess
2730
2831from typing import Any , Dict , Union , List , Tuple , Optional
@@ -877,6 +880,22 @@ def build_initialiser(
877880 dest .chmod (0o744 )
878881
879882
883+ # Taken and modified from Ninja's example jobserver_pool, under the Apache License
884+ # Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
885+ # This code does not form part of the distribution of Microkit.
886+ # https://github.com/ninja-build/ninja/blob/v1.13.2/misc/jobserver_pool.py#L162-L177
887+ def create_jobserver_fifo (path : str , jobs_count : int ) -> str :
888+ """Create and fill Posix FIFO."""
889+ os .mkfifo (path )
890+
891+ # Unused, but necessary as otherwise opening O_WRONLY will fail with ENXIO
892+ read_fd = os .open (path , os .O_RDONLY | os .O_NONBLOCK )
893+ write_fd = os .open (path , os .O_WRONLY | os .O_NONBLOCK )
894+ assert jobs_count > 0 , f"Token count must be strictly positive"
895+ os .write (write_fd , (jobs_count - 1 ) * b"x" )
896+ return f" -j{ jobs_count } --jobserver-auth=fifo:" + path
897+
898+
880899def main () -> None :
881900 parser = ArgumentParser ()
882901 parser .add_argument ("--sel4" , type = Path , required = True )
@@ -890,6 +909,7 @@ def main() -> None:
890909 parser .add_argument ("--skip-initialiser" , action = "store_true" , help = "Initialiser will not be built" )
891910 parser .add_argument ("--skip-docs" , action = "store_true" , help = "Docs will not be built" )
892911 parser .add_argument ("--skip-tar" , action = "store_true" , help = "SDK and source tarballs will not be built" )
912+ parser .add_argument ("--jobs" , type = int , default = os .cpu_count ())
893913 parser .add_argument ("--release-packaging" , action = "store_true" , help = "All SDKs for distribution will be produced" )
894914 # Read from the version file as unless someone has specified
895915 # a version, that is the source of truth
@@ -994,20 +1014,43 @@ def main() -> None:
9941014
9951015 if not args .skip_run_time :
9961016 build_dir = Path ("build" )
997- for (board , configs ) in build_goals :
998- for config in configs :
999- if not args .skip_sel4 :
1000- build_sel4 (sel4_dir , tool_dir , sdk_dir , build_dir , board , config , args .llvm )
1001- loader_printing = 1 if config .name == "debug" else 0
1002- loader_defines = []
1003- if not board .arch .is_x86 ():
1004- loader_defines .append (("LINK_ADDRESS" , hex (board .loader_link_address )))
1005- build_elf_component ("loader" , sdk_dir , build_dir , board , config , args .llvm , loader_defines )
1006-
1007- build_elf_component ("monitor" , sdk_dir , build_dir , board , config , args .llvm , [])
1008- build_lib_component ("libmicrokit" , sdk_dir , build_dir , board , config , args .llvm )
1009- if not args .skip_initialiser :
1010- build_initialiser ("initialiser" , sdk_dir , build_dir , board , config )
1017+
1018+ def build_one_goal (board : str , config : str ):
1019+ if not args .skip_sel4 :
1020+ build_sel4 (sel4_dir , tool_dir , sdk_dir , build_dir , board , config , args .llvm )
1021+ loader_printing = 1 if config .name == "debug" else 0
1022+ loader_defines = []
1023+ if not board .arch .is_x86 ():
1024+ loader_defines .append (("LINK_ADDRESS" , hex (board .loader_link_address )))
1025+ build_elf_component ("loader" , sdk_dir , build_dir , board , config , args .llvm , loader_defines )
1026+
1027+ build_elf_component ("monitor" , sdk_dir , build_dir , board , config , args .llvm , [])
1028+ build_lib_component ("libmicrokit" , sdk_dir , build_dir , board , config , args .llvm )
1029+ if not args .skip_initialiser :
1030+ build_initialiser ("initialiser" , sdk_dir , build_dir , board , config )
1031+
1032+ # FIXME: Possible improvement here is that our ThreadPool does not know
1033+ # about the current state of the jobserver, so may spawn threads which
1034+ # will make the number of jobs exceed the capacity of the jobserver
1035+ # (since the protocol assumes that if you are started under a jobserver
1036+ # you have "1" job available to you implicitly)
1037+ # So we can sometimes over-allocate a bit.
1038+ with concurrent .futures .ThreadPoolExecutor (max_workers = args .jobs ) as executor , \
1039+ tempfile .TemporaryDirectory (prefix = "microkit_" ) as fifo_dir :
1040+
1041+ if not os .environ .get ("MAKEFLAGS" ):
1042+ os .environ ["MAKEFLAGS" ] = create_jobserver_fifo (fifo_dir + "/fifo" , args .jobs )
1043+
1044+ goals = [(board , config ) for (board , configs ) in build_goals for config in configs ]
1045+ tasks_map = {executor .submit (build_one_goal , board , config ): (board , config ) for (board , config ) in goals }
1046+
1047+ for task in concurrent .futures .as_completed (tasks_map ):
1048+ board , config = tasks_map [task ]
1049+ try :
1050+ task .result ()
1051+ except Exception as exc :
1052+ print (f"Build goal { board } { config } failed" , file = stderr )
1053+
10111054
10121055 # Setup the examples
10131056 for example , example_path in EXAMPLES .items ():
0 commit comments