Skip to content

Commit

Permalink
feat: easy_setup prompts board codenames
Browse files Browse the repository at this point in the history
Using a fuzzy find after parsing boards.txt.

This has required refactoring the board parser code in a dedicated modules,
with minor impacts in the files that depended on it.
  • Loading branch information
massonal committed Aug 18, 2022
1 parent fe5794c commit 175fe4e
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 120 deletions.
40 changes: 40 additions & 0 deletions cmake/scripts/cmake_easy_setup.py
Expand Up @@ -8,6 +8,9 @@
import pathlib
import json
from jinja2 import Environment, FileSystemLoader
import difflib

from parse_boards import parse_file

parser = argparse.ArgumentParser()
parser.add_argument("--cli", "-x", type=pathlib.Path, required=False, default=shutil.which("arduino-cli"), help="path to arduino-cli")
Expand All @@ -18,6 +21,7 @@
output_args.add_argument("--sketch", "-s", type=pathlib.Path, help="output file (CMake) filled given a sketch folder")

shargs = parser.parse_args()
shargs.board = shargs.board.upper()

if shargs.sketch and not shargs.board :
print("""
Expand Down Expand Up @@ -51,9 +55,39 @@ def get_log(fname) :
for line in file :
yield json.loads(line)

def get_boards(boardstxt) :

# we "reject" everything because we don't care about the values, only the keys
families = parse_file(boardstxt, lambda x:True)
del families["menu"]

boards = set()
for fam, famcfg in families.items() :
boards.update(famcfg.menu.pnum.keys())

return boards

_, logf = arduino("lib", "list")

allboards = get_boards(pathlib.Path(__file__).parent.parent.parent/"boards.txt")


if shargs.board and shargs.board not in allboards :
print(f"Unrecognized board name: '{shargs.board}'")
print("Possible matches:")
options = difflib.get_close_matches(shargs.board, allboards, n=9, cutoff=0.0)
print("0. (keep as-is)")
for i, x in enumerate(options, start=1) :
print(f"{i}. {x}")
choice = input("Choice number: ")
while not choice.isdecimal() :
choice = input("Invalid choice *number*. Select a board: ")
choice = int(choice)
if choice != 0 :
choice -= 1
shargs.board = options[choice]


libpaths = dict()
for line in get_log(logf) :
if line["msg"] == "Adding libraries dir" :
Expand Down Expand Up @@ -109,6 +143,12 @@ def get_log(fname) :
))


print("Generated", shargs.output or shargs.sketch/"CMakeLists.txt")
print("""
Unless you are building a very simple sketch with no library (e.g., Blink),
you are advised to proofread this file to fill in any missing dependency relationship.
""")

if shargs.fire :
if not (shargs.sketch and shargs.board) :
print("There remains some placeholder in the output file; I won't build _that_.")
Expand Down
2 changes: 1 addition & 1 deletion cmake/scripts/cmake_updater_hook.py
Expand Up @@ -32,7 +32,7 @@
)
print("Updating board database...")
subprocess.run(
("python3", script_dir/"parse_boards.py",
("python3", script_dir/"update_boarddb.py",
"-b", base_dir/"boards.txt",
"-p", base_dir/"platform.txt",
"-t", templates_dir/"boards_db.cmake",
Expand Down
122 changes: 4 additions & 118 deletions cmake/scripts/parse_boards.py
@@ -1,10 +1,9 @@
#!/usr/bin/env python3

import pathlib
import argparse

from jinja2 import Environment, FileSystemLoader

"""
Utility module to parse Arduino config files
such as boards.txt/platform.txt.
"""

class Configuration(dict) :

Expand Down Expand Up @@ -51,49 +50,6 @@ def evaluate_entries(self, wrt=None) :
self[k].evaluate_entries(wrt)


def get_fpconf(config) :
fpu = (config.build.get("fpu") or "-mfpu=").rsplit("=", 1)[1]
abi = (config.build.get("float-abi") or "-mfloat-abi=").rsplit("=", 1)[1]
return f"{fpu}-{abi}"

def boardstxt_filter(key) :
# Remove menu entry labels
# In our data model, they conflict with the actual configuration they are associated to
# i.e. Nucleo_144.menu.pnum.NUCLEO_F207ZG would be both a string ("Nucleo F207ZG")
# and a dict (.build.variant_h=..., .upload.maximum_size=...)


if key[0] == "menu" :
# menu.xserial=U(S)ART support
return True
if len(key) == 4 and key[1] == "menu":
# Nucleo_144.menu.pnum.NUCLEO_F207ZG=Nucleo F207ZG
# Midatronics.menu.upload_method.MassStorage=Mass Storage
return True

# Remove upload_method also, that's out of our scope and requires more logic
if len(key) >= 3 and key[2] == "upload_method" :
return True

return False

def platformtxt_filter(key) :
# reject everything but build.**
# and also build.info (that's specific to the build system, we'll hard-code it)
# we don't need anything else from platform.txt
# +additional stuff might confuse later parts of the script
# e.g.:

# compiler.warning_flags=-w
# compiler.warning_flags.none=-w
# compiler.warning_flags.default=
# compiler.warning_flags.more=-Wall
# compiler.warning_flags.all=-Wall -Wextra

if key[0] == "build" and key[1] != "info" :
return False
return True

def parse_file(infile, reject=None) :
if reject is None :
reject = lambda x:False
Expand All @@ -118,73 +74,3 @@ def parse_file(infile, reject=None) :
else :
ptr[key[-1]] = value
return config

def regenerate_template(config, infile, outfile) :
j2_env = Environment(
loader=FileSystemLoader(str(infile.parent)), trim_blocks=True, lstrip_blocks=True
)
cmake_template = j2_env.get_template(infile.name)

with open(outfile, "w") as out :
out.write(cmake_template.render(
allcfg = config,
))


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-b", "--boards", type=pathlib.Path, required=True, help="path to boards.txt")
parser.add_argument("-p", "--platform", type=pathlib.Path, required=True, help="path to platform.txt")
parser.add_argument("-t", "--template", type=pathlib.Path, required=True, help="path to the jinja template")
parser.add_argument("-o", "--outfile", type=pathlib.Path, required=True, help="path to the cmake database to generate")

shargs = parser.parse_args()

platformtxt_cfg = parse_file(shargs.platform, reject=platformtxt_filter)
platformtxt_cfg = {"build":platformtxt_cfg["build"]} # whitelist what we need

boardstxt_cfg = parse_file(shargs.boards, reject=boardstxt_filter)
del boardstxt_cfg["menu"] # blacklist what we don't need

# these are optional features to be picked out by the user
BOARD_FEATURES = [
"enable_virtio",
"enable_usb",
"usb_speed",
"xSerial",
]

allboards = dict()

for fam, famcfg in boardstxt_cfg.items() :
famcfg.set_default_entries(platformtxt_cfg)

inherit_fam = famcfg.copy()
# shallow copy; we don't want to impact famcfg so we have to copy before edit/del
inherit_fam["menu"] = inherit_fam["menu"].copy()
# del what you iterate over (otherwise you get infinite nesting)
del inherit_fam["menu"]["pnum"]

for board, boardcfg in famcfg.menu.pnum.items() :
boardcfg.set_default_entries(inherit_fam)

inherit_board = boardcfg.copy()
del inherit_board["menu"]

board_feature_names = tuple(boardcfg["menu"].keys())

for fname in board_feature_names :
for label, labelcfg in boardcfg["menu"][fname].items() :
labelcfg.set_default_entries(inherit_board)
labelcfg.evaluate_entries()

# base config won't manage all the board features, we thus have to mask them out
for feat in BOARD_FEATURES :
boardcfg.build[feat] = ""

boardcfg.evaluate_entries()

boardcfg["_fpconf"] = get_fpconf(boardcfg)
allboards[board] = boardcfg

regenerate_template(allboards, shargs.template, shargs.outfile)
122 changes: 122 additions & 0 deletions cmake/scripts/update_boarddb.py
@@ -0,0 +1,122 @@
#!/usr/bin/env python3

import pathlib
import argparse

from jinja2 import Environment, FileSystemLoader

from parse_boards import parse_file


def get_fpconf(config) :
fpu = (config.build.get("fpu") or "-mfpu=").rsplit("=", 1)[1]
abi = (config.build.get("float-abi") or "-mfloat-abi=").rsplit("=", 1)[1]
return f"{fpu}-{abi}"

def boardstxt_filter(key) :
# Remove menu entry labels
# In our data model, they conflict with the actual configuration they are associated to
# i.e. Nucleo_144.menu.pnum.NUCLEO_F207ZG would be both a string ("Nucleo F207ZG")
# and a dict (.build.variant_h=..., .upload.maximum_size=...)


if key[0] == "menu" :
# menu.xserial=U(S)ART support
return True
if len(key) == 4 and key[1] == "menu":
# Nucleo_144.menu.pnum.NUCLEO_F207ZG=Nucleo F207ZG
# Midatronics.menu.upload_method.MassStorage=Mass Storage
return True

# Remove upload_method also, that's out of our scope and requires more logic
if len(key) >= 3 and key[2] == "upload_method" :
return True

return False

def platformtxt_filter(key) :
# reject everything but build.**
# and also build.info (that's specific to the build system, we'll hard-code it)
# we don't need anything else from platform.txt
# +additional stuff might confuse later parts of the script
# e.g.:

# compiler.warning_flags=-w
# compiler.warning_flags.none=-w
# compiler.warning_flags.default=
# compiler.warning_flags.more=-Wall
# compiler.warning_flags.all=-Wall -Wextra

if key[0] == "build" and key[1] != "info" :
return False
return True

def regenerate_template(config, infile, outfile) :
j2_env = Environment(
loader=FileSystemLoader(str(infile.parent)), trim_blocks=True, lstrip_blocks=True
)
cmake_template = j2_env.get_template(infile.name)

with open(outfile, "w") as out :
out.write(cmake_template.render(
allcfg = config,
))


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-b", "--boards", type=pathlib.Path, required=True, help="path to boards.txt")
parser.add_argument("-p", "--platform", type=pathlib.Path, required=True, help="path to platform.txt")
parser.add_argument("-t", "--template", type=pathlib.Path, required=True, help="path to the jinja template")
parser.add_argument("-o", "--outfile", type=pathlib.Path, required=True, help="path to the cmake database to generate")

shargs = parser.parse_args()

platformtxt_cfg = parse_file(shargs.platform, reject=platformtxt_filter)
platformtxt_cfg = {"build":platformtxt_cfg["build"]} # whitelist what we need

boardstxt_cfg = parse_file(shargs.boards, reject=boardstxt_filter)
del boardstxt_cfg["menu"] # blacklist what we don't need

# these are optional features to be picked out by the user
BOARD_FEATURES = [
"enable_virtio",
"enable_usb",
"usb_speed",
"xSerial",
]

allboards = dict()

for fam, famcfg in boardstxt_cfg.items() :
famcfg.set_default_entries(platformtxt_cfg)

inherit_fam = famcfg.copy()
# shallow copy; we don't want to impact famcfg so we have to copy before edit/del
inherit_fam["menu"] = inherit_fam["menu"].copy()
# del what you iterate over (otherwise you get infinite nesting)
del inherit_fam["menu"]["pnum"]

for board, boardcfg in famcfg.menu.pnum.items() :
boardcfg.set_default_entries(inherit_fam)

inherit_board = boardcfg.copy()
del inherit_board["menu"]

board_feature_names = tuple(boardcfg["menu"].keys())

for fname in board_feature_names :
for label, labelcfg in boardcfg["menu"][fname].items() :
labelcfg.set_default_entries(inherit_board)
labelcfg.evaluate_entries()

# base config won't manage all the board features, we thus have to mask them out
for feat in BOARD_FEATURES :
boardcfg.build[feat] = ""

boardcfg.evaluate_entries()

boardcfg["_fpconf"] = get_fpconf(boardcfg)
allboards[board] = boardcfg

regenerate_template(allboards, shargs.template, shargs.outfile)
2 changes: 1 addition & 1 deletion cmake/updatedb.cmake
Expand Up @@ -12,7 +12,7 @@ function(updatedb)
OR ${CMAKE_BOARDS_DB_TEMPLATE_PATH} IS_NEWER_THAN ${CMAKE_BOARDS_DB_PATH}
)
execute_process(
COMMAND ${Python3_EXECUTABLE} ${SCRIPTS_FOLDER}/parse_boards.py
COMMAND ${Python3_EXECUTABLE} ${SCRIPTS_FOLDER}/update_boarddb.py
-b ${BOARDSTXT_PATH}
-p ${PLATFORMTXT_PATH}
-t ${CMAKE_BOARDS_DB_TEMPLATE_PATH}
Expand Down

0 comments on commit 175fe4e

Please sign in to comment.