add port-specific modules to support matrix

This commit is contained in:
Dan Halbert 2023-03-14 16:03:42 -04:00
parent 9083ae02de
commit 6a9d69d743

View File

@ -31,7 +31,17 @@ import functools
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
SUPPORTED_PORTS = ['atmel-samd', 'broadcom', 'cxd56', 'espressif', 'litex', 'mimxrt10xx', 'nrf', 'raspberrypi', 'stm'] SUPPORTED_PORTS = [
"atmel-samd",
"broadcom",
"cxd56",
"espressif",
"litex",
"mimxrt10xx",
"nrf",
"raspberrypi",
"stm",
]
ALIASES_BY_BOARD = { ALIASES_BY_BOARD = {
"circuitplayground_express": [ "circuitplayground_express": [
@ -44,16 +54,11 @@ ALIASES_BY_BOARD = {
} }
ALIASES_BRAND_NAMES = { ALIASES_BRAND_NAMES = {
"circuitplayground_express_4h": "circuitplayground_express_4h": "Adafruit Circuit Playground Express 4-H",
"Adafruit Circuit Playground Express 4-H", "circuitplayground_express_digikey_pycon2019": "Circuit Playground Express Digi-Key PyCon 2019",
"circuitplayground_express_digikey_pycon2019": "edgebadge": "Adafruit EdgeBadge",
"Circuit Playground Express Digi-Key PyCon 2019", "pyportal_pynt": "Adafruit PyPortal Pynt",
"edgebadge": "gemma_m0_pycon2018": "Adafruit Gemma M0 PyCon 2018",
"Adafruit EdgeBadge",
"pyportal_pynt":
"Adafruit PyPortal Pynt",
"gemma_m0_pycon2018":
"Adafruit Gemma M0 PyCon 2018",
} }
ADDITIONAL_MODULES = { ADDITIONAL_MODULES = {
@ -72,7 +77,19 @@ ADDITIONAL_MODULES = {
"usb": "CIRCUITPY_USB_HOST", "usb": "CIRCUITPY_USB_HOST",
} }
MODULES_NOT_IN_SHARED_BINDINGS = ["_asyncio", "array", "binascii", "builtins", "collections", "errno", "json", "re", "select", "sys", "ulab"] MODULES_NOT_IN_BINDINGS = [
"_asyncio",
"array",
"binascii",
"builtins",
"collections",
"errno",
"json",
"re",
"select",
"sys",
"ulab",
]
FROZEN_EXCLUDES = ["examples", "docs", "tests", "utils", "conf.py", "setup.py"] FROZEN_EXCLUDES = ["examples", "docs", "tests", "utils", "conf.py", "setup.py"]
"""Files and dirs at the root of a frozen directory that should be ignored. """Files and dirs at the root of a frozen directory that should be ignored.
@ -83,16 +100,23 @@ repository_urls = {}
root_dir = pathlib.Path(__file__).resolve().parent.parent root_dir = pathlib.Path(__file__).resolve().parent.parent
def get_circuitpython_root_dir(): def get_circuitpython_root_dir():
""" The path to the root './circuitpython' directory. """The path to the root './circuitpython' directory."""
"""
return root_dir return root_dir
def get_shared_bindings():
""" Get a list of modules in shared-bindings based on folder names. def get_bindings():
""" """Get a list of modules in shared-bindings and ports/*/bindings based on folder names."""
shared_bindings_dir = get_circuitpython_root_dir() / "shared-bindings" shared_bindings_modules = [
return [item.name for item in shared_bindings_dir.iterdir()] + MODULES_NOT_IN_SHARED_BINDINGS module.name
for module in (get_circuitpython_root_dir() / "shared-bindings").iterdir()
if module.is_dir()
]
bindings_modules = []
for d in get_circuitpython_root_dir().glob("ports/*/bindings"):
bindings_modules.extend(module.name for module in d.iterdir() if d.is_dir())
return shared_bindings_modules + bindings_modules + MODULES_NOT_IN_BINDINGS
def get_board_mapping(): def get_board_mapping():
@ -124,8 +148,7 @@ def get_board_mapping():
def read_mpconfig(): def read_mpconfig():
""" Open 'circuitpy_mpconfig.mk' and return the contents. """Open 'circuitpy_mpconfig.mk' and return the contents."""
"""
configs = [] configs = []
cpy_mpcfg = get_circuitpython_root_dir() / "py" / "circuitpy_mpconfig.mk" cpy_mpcfg = get_circuitpython_root_dir() / "py" / "circuitpy_mpconfig.mk"
with open(cpy_mpcfg) as mpconfig: with open(cpy_mpcfg) as mpconfig:
@ -135,14 +158,14 @@ def read_mpconfig():
def build_module_map(): def build_module_map():
""" Establish the base of the JSON file, based on the contents from """Establish the base of the JSON file, based on the contents from
`configs`. Base will contain module names, if they're part of `configs`. Base will contain module names, if they're part of
the `FULL_BUILD`, or their default value (0, 1, or a list of the `FULL_BUILD`, or their default value (0, 1, or a list of
modules that determine default [see audiocore, audiomixer, etc.]). modules that determine default [see audiocore, audiomixer, etc.]).
""" """
base = dict() base = dict()
modules = get_shared_bindings() modules = get_bindings()
configs = read_mpconfig() configs = read_mpconfig()
full_build = False full_build = False
for module in modules: for module in modules:
@ -150,7 +173,7 @@ def build_module_map():
if module in ADDITIONAL_MODULES: if module in ADDITIONAL_MODULES:
search_identifier = ADDITIONAL_MODULES[module] search_identifier = ADDITIONAL_MODULES[module]
else: else:
search_identifier = 'CIRCUITPY_'+module.lstrip("_").upper() search_identifier = "CIRCUITPY_" + module.lstrip("_").upper()
re_pattern = f"{re.escape(search_identifier)}\s*\??=\s*(.+)" re_pattern = f"{re.escape(search_identifier)}\s*\??=\s*(.+)"
find_config = re.findall(re_pattern, configs) find_config = re.findall(re_pattern, configs)
if not find_config: if not find_config:
@ -173,21 +196,22 @@ def build_module_map():
return base return base
def get_settings_from_makefile(port_dir, board_name):
""" Invoke make in a mode which prints the database, then parse it for
settings.
This means that the effect of all Makefile directives is taken def get_settings_from_makefile(port_dir, board_name):
into account, without having to re-encode the logic that sets them """Invoke make in a mode which prints the database, then parse it for
in this script, something that has proved error-prone settings.
This means that the effect of all Makefile directives is taken
into account, without having to re-encode the logic that sets them
in this script, something that has proved error-prone
""" """
contents = subprocess.run( contents = subprocess.run(
["make", "-C", port_dir, f"BOARD={board_name}", "-qp", "print-CC"], ["make", "-C", port_dir, f"BOARD={board_name}", "-qp", "print-CC"],
encoding="utf-8", encoding="utf-8",
errors="replace", errors="replace",
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE stderr=subprocess.PIPE,
) )
# Make signals errors with exit status 2; 0 and 1 are "non-error" statuses # Make signals errors with exit status 2; 0 and 1 are "non-error" statuses
if contents.returncode not in (0, 1): if contents.returncode not in (0, 1):
error_msg = ( error_msg = (
@ -197,22 +221,23 @@ def get_settings_from_makefile(port_dir, board_name):
raise RuntimeError(error_msg) raise RuntimeError(error_msg)
settings = {} settings = {}
for line in contents.stdout.split('\n'): for line in contents.stdout.split("\n"):
# Handle both = and := definitions. # Handle both = and := definitions.
m = re.match(r'^([A-Z][A-Z0-9_]*) :?= (.*)$', line) m = re.match(r"^([A-Z][A-Z0-9_]*) :?= (.*)$", line)
if m: if m:
settings[m.group(1)] = m.group(2) settings[m.group(1)] = m.group(2)
return settings return settings
def get_repository_url(directory): def get_repository_url(directory):
if directory in repository_urls: if directory in repository_urls:
return repository_urls[directory] return repository_urls[directory]
readme = None readme = None
for readme_path in ( for readme_path in (
os.path.join(directory, "README.rst"), os.path.join(directory, "README.rst"),
os.path.join(os.path.dirname(directory), "README.rst") os.path.join(os.path.dirname(directory), "README.rst"),
): ):
if os.path.exists(readme_path): if os.path.exists(readme_path):
readme = readme_path readme = readme_path
break break
@ -220,7 +245,10 @@ def get_repository_url(directory):
if readme: if readme:
with open(readme, "r") as fp: with open(readme, "r") as fp:
for line in fp.readlines(): for line in fp.readlines():
if m := re.match("\s+:target:\s+(http\S+(docs.circuitpython|readthedocs)\S+)\s*", line): if m := re.match(
"\s+:target:\s+(http\S+(docs.circuitpython|readthedocs)\S+)\s*",
line,
):
path = m.group(1) path = m.group(1)
break break
if m := re.search("<(http[^>]+)>", line): if m := re.search("<(http[^>]+)>", line):
@ -233,12 +261,13 @@ def get_repository_url(directory):
errors="replace", errors="replace",
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
cwd=directory cwd=directory,
) )
path = contents.stdout.strip() path = contents.stdout.strip()
repository_urls[directory] = path repository_urls[directory] = path
return path return path
def frozen_modules_from_dirs(frozen_mpy_dirs, withurl): def frozen_modules_from_dirs(frozen_mpy_dirs, withurl):
""" """
Go through the list of frozen directories and extract the python modules. Go through the list of frozen directories and extract the python modules.
@ -261,34 +290,36 @@ def frozen_modules_from_dirs(frozen_mpy_dirs, withurl):
else: else:
frozen_modules.append(sub.name[:-3]) frozen_modules.append(sub.name[:-3])
continue continue
if next(sub.glob("**/*.py"), None): # tests if not empty if next(sub.glob("**/*.py"), None): # tests if not empty
if withurl: if withurl:
frozen_modules.append((sub.name, url_repository)) frozen_modules.append((sub.name, url_repository))
else: else:
frozen_modules.append(sub.name) frozen_modules.append(sub.name)
return frozen_modules return frozen_modules
def lookup_setting(settings, key, default=''):
def lookup_setting(settings, key, default=""):
while True: while True:
value = settings.get(key, default) value = settings.get(key, default)
if not value.startswith('$'): if not value.startswith("$"):
break break
key = value[2:-1] key = value[2:-1]
return value return value
@functools.cache @functools.cache
def all_ports_all_boards(ports=SUPPORTED_PORTS): def all_ports_all_boards(ports=SUPPORTED_PORTS):
for port in ports: for port in ports:
port_dir = get_circuitpython_root_dir() / "ports" / port port_dir = get_circuitpython_root_dir() / "ports" / port
for entry in (port_dir / "boards").iterdir(): for entry in (port_dir / "boards").iterdir():
if not entry.is_dir(): if not entry.is_dir():
continue continue
yield (port, entry) yield (port, entry)
def support_matrix_by_board(use_branded_name=True, withurl=True): def support_matrix_by_board(use_branded_name=True, withurl=True):
""" Compiles a list of the available core modules available for each """Compiles a list of the available core modules available for each
board. board.
""" """
base = build_module_map() base = build_module_map()
@ -300,8 +331,9 @@ def support_matrix_by_board(use_branded_name=True, withurl=True):
if use_branded_name: if use_branded_name:
with open(entry / "mpconfigboard.h") as get_name: with open(entry / "mpconfigboard.h") as get_name:
board_contents = get_name.read() board_contents = get_name.read()
board_name_re = re.search(r"(?<=MICROPY_HW_BOARD_NAME)\s+(.+)", board_name_re = re.search(
board_contents) r"(?<=MICROPY_HW_BOARD_NAME)\s+(.+)", board_contents
)
if board_name_re: if board_name_re:
board_name = board_name_re.group(1).strip('"') board_name = board_name_re.group(1).strip('"')
else: else:
@ -309,56 +341,69 @@ def support_matrix_by_board(use_branded_name=True, withurl=True):
board_modules = [] board_modules = []
for module in base: for module in base:
key = base[module]['key'] key = base[module]["key"]
if int(lookup_setting(settings, key, '0')): if int(lookup_setting(settings, key, "0")):
board_modules.append(base[module]['name']) board_modules.append(base[module]["name"])
board_modules.sort() board_modules.sort()
if "CIRCUITPY_BUILD_EXTENSIONS" in settings: if "CIRCUITPY_BUILD_EXTENSIONS" in settings:
board_extensions = [ board_extensions = [
extension.strip() for extension in extension.strip()
settings["CIRCUITPY_BUILD_EXTENSIONS"].split(",") for extension in settings["CIRCUITPY_BUILD_EXTENSIONS"].split(",")
] ]
else: else:
raise OSError(f"Board extensions undefined: {board_name}.") raise OSError(f"Board extensions undefined: {board_name}.")
frozen_modules = [] frozen_modules = []
if "FROZEN_MPY_DIRS" in settings: if "FROZEN_MPY_DIRS" in settings:
frozen_modules = frozen_modules_from_dirs(settings["FROZEN_MPY_DIRS"], withurl) frozen_modules = frozen_modules_from_dirs(
settings["FROZEN_MPY_DIRS"], withurl
)
if frozen_modules: if frozen_modules:
frozen_modules.sort() frozen_modules.sort()
# generate alias boards too # generate alias boards too
board_matrix = [( board_matrix = [
board_name, { (
"modules": board_modules, board_name,
"frozen_libraries": frozen_modules, {
"extensions": board_extensions, "modules": board_modules,
} "frozen_libraries": frozen_modules,
)] "extensions": board_extensions,
},
)
]
if entry.name in ALIASES_BY_BOARD: if entry.name in ALIASES_BY_BOARD:
for alias in ALIASES_BY_BOARD[entry.name]: for alias in ALIASES_BY_BOARD[entry.name]:
if use_branded_name: if use_branded_name:
if alias in ALIASES_BRAND_NAMES: if alias in ALIASES_BRAND_NAMES:
alias = ALIASES_BRAND_NAMES[alias] alias = ALIASES_BRAND_NAMES[alias]
else: else:
alias = alias.replace("_"," ").title() alias = alias.replace("_", " ").title()
board_matrix.append(( board_matrix.append(
alias, { (
"modules": board_modules, alias,
"frozen_libraries": frozen_modules, {
"extensions": board_extensions, "modules": board_modules,
}, "frozen_libraries": frozen_modules,
)) "extensions": board_extensions,
},
)
)
return board_matrix # this is now a list of (board,modules) return board_matrix # this is now a list of (board,modules)
executor = ThreadPoolExecutor(max_workers=os.cpu_count()) executor = ThreadPoolExecutor(max_workers=os.cpu_count())
mapped_exec = executor.map(support_matrix, all_ports_all_boards()) mapped_exec = executor.map(support_matrix, all_ports_all_boards())
# flatmap with comprehensions # flatmap with comprehensions
boards = dict(sorted([board for matrix in mapped_exec for board in matrix], key=lambda x: x[0])) boards = dict(
sorted(
[board for matrix in mapped_exec for board in matrix], key=lambda x: x[0]
)
)
return boards return boards
if __name__ == '__main__':
if __name__ == "__main__":
print(json.dumps(support_matrix_by_board(), indent=2)) print(json.dumps(support_matrix_by_board(), indent=2))