-
Notifications
You must be signed in to change notification settings - Fork 795
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[multitop] Implement HW rules RFC #25580
Conversation
6a33b1c
to
3ad7c24
Compare
util/rewrite_all_hw.py
Outdated
is_template = info.get("is_template", False) | ||
|
||
def_file_path = ippath / ("defs.bzl.tpl" if is_template else "defs.bzl") | ||
# build_file_path = ippath / ("BUILD.tpl" if is_template else "BUILD") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we also templating BUILD files?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes some (all?) ipgen IP template BUILD
file. Well technically they are not templates because their content does not depend on the template but we can't name them just BUILD
because then bazel considers them as a package and this can cause all sorts of weird issues potentially. Therefore calling BUILD.tpl
seems like an easy way to avoid this issue. Unless you have a better idea on how to handle this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will we be able to dispense with the "all_files" target? That may make these BUILD and BUILD.tpl files unnecessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, there was quite a lot of pushback against touching the "all_files" targets so I have given up on touching them for the time being.
util/rewrite_all_hw.py
Outdated
else: | ||
assert False, "unknown step" | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
if __name__ == '__main__':
main()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, although I should point out that this script will disappear at the end of the PR (I just forgot to add it yet). Still good practice though.
hw/top/defs.bzl
Outdated
names = {} | ||
for top in ALL_TOPS: | ||
for ip in top.ips: | ||
names[ip.name] = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this be better as a comprehension? (also, maybe use an integer as the value, rather than empty dict?)
names = {ip.name: 1 for ip in top.ips for top in ALL_TOPS}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Too bad the set is only in bazel nightly, hehe.
# WARNING This is a horrible hack: when we transition to host, we pretend | ||
# that this is earlgrey so opentitantool can compile... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. Longer term, we probably need to distinguish between chip-specific constants in opentitantool, stick them into something like a hashmap by chip, and add a --chip=...
cmdline argument to select which set of constants we care about.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that's a big issue. I would like to raise it in the SW WG tomorrow, there is clearly some work to be done here. I think the multitop SW PR is a good starting point to generate such a data structure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One route: I could revive my devicetree generator, and opentitantool could consume its output for a description of the system. That would also leave open support for out-of-tree tops (instead of hard-coding entire known chips).
Out of curiosity, did you already consider a bazel module extension to create a repo that fills out the information from the hjson files? From the description, this starts to sound rather similar to a "foreign build system" or "foreign packaging" sort of problem. The hjson files seem to contain critical information about the build graph, such that it exists outside of bazel's BUILD files. It looks like this PR goes the route of requiring the user to generate the graph with a separate call, but could we do it on the fly (in a bazel module extension) with what we already have with topgen, reggen, and the hjson files? |
2dbfffa
to
c08ef1a
Compare
c08ef1a
to
9baa683
Compare
Signed-off-by: Amaury Pouly <[email protected]>
Signed-off-by: Amaury Pouly <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me. Thanks.
Signed-off-by: Amaury Pouly <[email protected]>
9baa683
to
42f720d
Compare
Signed-off-by: Amaury Pouly <[email protected]>
Those macros record the IPs/top description in a struct. This struct can then be used to implement various build system rules and macros that need access to the top description. The main reason why these are macros and not rules is that some of this information needs to be available during bazel's loading phase, where only macro and variable expansion are available. Signed-off-by: Amaury Pouly <[email protected]>
This commit was generated by the following script: from pathlib import Path import subprocess all_tops = ["darjeeling", "earlgrey"] all_ips = { "hw/ip/adc_ctrl": {}, "hw/ip/aes": {}, "hw/ip/aon_timer": {}, "hw/ip/csrng": {}, "hw/ip/dma": {}, "hw/ip/edn": {}, "hw/ip/entropy_src": {}, "hw/ip/gpio": {}, "hw/ip/hmac": {}, "hw/ip/i2c": {}, "hw/ip/keymgr": {}, "hw/ip/keymgr_dpe": {}, "hw/ip/kmac": {}, "hw/ip/lc_ctrl": {}, "hw/ip/otbn": {}, "hw/ip/otp_ctrl": {}, "hw/ip/mbx": {}, "hw/ip/pattgen": {}, "hw/ip/pwm": {}, "hw/ip/rom_ctrl": {}, "hw/ip/rv_core_ibex": {}, "hw/ip/rv_dm": {}, "hw/ip/rv_timer": {}, "hw/ip/soc_dbg_ctrl": {}, "hw/ip/spi_device": {}, "hw/ip/spi_host": {}, "hw/ip/sram_ctrl": {}, "hw/ip/sysrst_ctrl": {}, "hw/ip/uart": {}, "hw/ip/usbdev": {}, # templates "hw/ip_templates/alert_handler": {"is_template": True, "tops": all_tops}, "hw/ip_templates/clkmgr": {"is_template": True, "tops": all_tops}, "hw/ip_templates/rstmgr": {"is_template": True, "tops": all_tops}, "hw/ip_templates/pwrmgr": {"is_template": True, "tops": all_tops}, "hw/ip_templates/rv_plic": {"is_template": True, "tops": all_tops}, "hw/ip_templates/flash_ctrl": {"is_template": True, "tops": ["earlgrey"]}, "hw/ip_templates/pinmux": {"is_template": True, "tops": all_tops}, # top_earlgrey 'hw/top_earlgrey/ip/ast': {}, 'hw/top_earlgrey/ip/sensor_ctrl': {}, # top_darjeeling 'hw/top_darjeeling/ip/ast': {}, 'hw/top_darjeeling/ip/sensor_ctrl': {}, 'hw/top_darjeeling/ip/soc_proxy': {}, } project_root = Path(__file__).parents[1].resolve() def run_buildifier(project_root): subprocess.run( ["./bazelisk.sh", "run", "//quality:buildifier_fix"], check=True, cwd = project_root ) def run_topgen(project_root): for top in all_tops: subprocess.run( ["./util/topgen.py", "-t", f"hw/top_{top}/data/top_{top}.hjson"], check=True, cwd = project_root, ) subprocess.run( ["make", "-C", "hw", "cmdgen"], check=False, cwd = project_root, ) def step1(project_root): new_files = [] for (_ippath, info) in all_ips.items(): ippath = project_root / Path(_ippath) ip_name = ippath.name is_template = info.get("is_template", False) def_file_path = ippath / ("defs.bzl.tpl" if is_template else "defs.bzl") # build_file_path = ippath / ("BUILD.tpl" if is_template else "BUILD") # If file does not exist, create one. if def_file_path.exists(): print(f"File {def_file_path} already exists, will overwrite") new_files.append(def_file_path) if is_template: for top in info["tops"]: new_files.append(project_root / f"hw/top_{top}/ip_autogen" / ip_name / "defs.bzl") if is_template: hjson_bazel_target = f"//hw/top_${{topname}}/ip_autogen/{ip_name}:data/{ip_name}.hjson" # noqa: E231, E501 else: hjson_bazel_target = f"//{_ippath}/data:{ip_name}.hjson" # noqa: E231 print(hjson_bazel_target) def_file = [ '# Copyright lowRISC contributors (OpenTitan project).\n', '# Licensed under the Apache License, Version 2.0, see LICENSE for details.\n', '# SPDX-License-Identifier: Apache-2.0\n', 'load("//rules/opentitan:hw.bzl", "opentitan_ip")\n', '\n', '{} = opentitan_ip(\n'.format(ip_name.upper()), ' name = "{}",\n'.format(ip_name), ' hjson = "{}",\n'.format(hjson_bazel_target), ')\n', ] def_file_path.write_text(''.join(def_file)) # Run buildifier. run_buildifier(project_root) run_topgen(project_root) subprocess.run( ["git", "add"] + new_files, check = True, cwd = project_root, ) subprocess.run( [ "git", "commit", "-vas", "-m", "[bazel] Use new rules to describe IPs", # noqa: E231 "-m", "This commit was generated by the following script:", "-m", Path(__file__).read_text(), ], check=True, cwd = project_root ) step1(project_root) Signed-off-by: Amaury Pouly <[email protected]>
Signed-off-by: Amaury Pouly <[email protected]>
For now this package contains nothing but a definition file that collects all tops. Signed-off-by: Amaury Pouly <[email protected]>
Now that the top description is available from the opentitan_top macro, we can create rules to extract it in a form that is more convenient for the build system. This commit introduces three rules: - two to extract the top's C library and linker files - one to construct a top description in the form of a provider This rules essentially reformats the opentitan_top's struct content as a provider. The reason for this indirection is that for rules that will depend on the top description in the analysis phase (and not the loading phase), getting the information via a provider is much cleaner and useful that via a struct. Signed-off-by: Amaury Pouly <[email protected]>
Signed-off-by: Amaury Pouly <[email protected]>
The new rule takes an input a target created by opentitan_top and will eventually replace the old one that directly used an hjson file. Signed-off-by: Amaury Pouly <[email protected]>
This commit add a new ALL_IP_NAMES variable that collects the names of all IPs in all tops listed in ALL_TOPS. It also introduces two new macros opentitan_if_ip and opentitan_require_ip that can be used to conditional include starlark expression based on the availability of a particular IP for the selected top, or express compatibility requirements for targets. Signed-off-by: Amaury Pouly <[email protected]>
These new targets are multitop aware and will replace the existing ones. Note that those targets are marked with compatibility requirements on the top. For example, //hw/top:mbx_c_regs cannot be compiled with --//hw/top=earlgrey. This guarantees that by transitivity, targets that depends on, .e.g mbx headers, can only be compiled on relevant targets. Signed-off-by: Amaury Pouly <[email protected]>
This commit was generated by the following script: from pathlib import Path import subprocess import os all_tops = ["darjeeling", "earlgrey"] all_ips = { "hw/ip/adc_ctrl": {}, "hw/ip/aes": {}, "hw/ip/aon_timer": {}, "hw/ip/csrng": {}, "hw/ip/dma": {}, "hw/ip/edn": {}, "hw/ip/entropy_src": {}, "hw/ip/gpio": {}, "hw/ip/hmac": {}, "hw/ip/i2c": {}, "hw/ip/keymgr": {}, "hw/ip/keymgr_dpe": {}, "hw/ip/kmac": {}, "hw/ip/lc_ctrl": {}, "hw/ip/otbn": {}, "hw/ip/otp_ctrl": {}, "hw/ip/mbx": {}, "hw/ip/pattgen": {}, "hw/ip/pwm": {}, "hw/ip/rom_ctrl": {}, "hw/ip/rv_core_ibex": {}, "hw/ip/rv_dm": {}, "hw/ip/rv_timer": {}, "hw/ip/soc_dbg_ctrl": {}, "hw/ip/spi_device": {}, "hw/ip/spi_host": {}, "hw/ip/sram_ctrl": {}, "hw/ip/sysrst_ctrl": {}, "hw/ip/uart": {}, "hw/ip/usbdev": {}, # templates "hw/ip_templates/alert_handler": {"is_template": True, "tops": all_tops}, "hw/ip_templates/clkmgr": {"is_template": True, "tops": all_tops}, "hw/ip_templates/rstmgr": {"is_template": True, "tops": all_tops}, "hw/ip_templates/pwrmgr": {"is_template": True, "tops": all_tops}, "hw/ip_templates/rv_plic": {"is_template": True, "tops": all_tops}, "hw/ip_templates/flash_ctrl": {"is_template": True, "tops": ["earlgrey"]}, "hw/ip_templates/pinmux": {"is_template": True, "tops": all_tops}, # top_earlgrey 'hw/top_earlgrey/ip/ast': {}, 'hw/top_earlgrey/ip/sensor_ctrl': {}, # top_darjeeling 'hw/top_darjeeling/ip/ast': {}, 'hw/top_darjeeling/ip/sensor_ctrl': {}, 'hw/top_darjeeling/ip/soc_proxy': {}, } project_root = Path(__file__).parents[1].resolve() def find_all_files(project_root, search): # Use ripgrep to find all matching files res = subprocess.run( ["rg", "-l", search], capture_output = True, cwd = project_root, ) # ripgrep returns 1 if there are no matches, 2 on error if res.returncode == 1: return [] assert res.returncode == 0, "ripgrep command failed" return [Path(os.fsdecode(path)) for path in res.stdout.splitlines()] def global_replace(project_root, search, replace, verbose): print(f'global replace "{search}" by "{replace}"') for path in find_all_files(project_root, search): path = project_root / path if verbose: print(f"Patching {path}") # Read, patch, write f = path.read_text() f = f.replace(search, replace) path.write_text(f) def run_buildifier(project_root): subprocess.run( ["./bazelisk.sh", "run", "//quality:buildifier_fix"], check=True, cwd = project_root ) def run_topgen(project_root): subprocess.run( ["make", "-C", "hw"], check=False, cwd = project_root, ) def step2(project_root): for (_ippath, info) in all_ips.items(): ippath = project_root / Path(_ippath) ip_name = ippath.name is_template = info.get("is_template", False) replacements = {} for typ in ["c", "rust"]: new_target = f"//hw/top:{ip_name}_{typ}_regs" # noqa: E231 if is_template: for top in info["tops"]: # Some exceptions if ip_name in ["rv_plic", "alert_handler"]: replacements[f"//hw/top_{top}:{ip_name}_{typ}_regs"] = new_target # noqa: E231, E501 else: replacements[f"//hw/top_{top}/ip_autogen/{ip_name}:{ip_name}_{typ}_regs"] = new_target # noqa: E231, E501 else: replacements[f"//{_ippath}/data:{ip_name}_{typ}_regs"] = new_target # noqa: E231 for (old, new) in replacements.items(): global_replace(project_root, old, new, verbose=False) # Run buildifier. run_buildifier(project_root) run_topgen(project_root) subprocess.run( [ "git", "commit", "-vas", "-m", "Replace old header targets by new ones", # noqa: E231 "-m", "This commit was generated by the following script:", "-m", Path(__file__).read_text(), ], check=True, cwd = project_root ) step2(project_root) Signed-off-by: Amaury Pouly <[email protected]>
This commit was generated by the following script: from pathlib import Path import subprocess import os all_tops = ["darjeeling", "earlgrey"] project_root = Path(__file__).parents[1].resolve() def find_all_files(project_root, search): # Use ripgrep to find all matching files res = subprocess.run( ["rg", "-l", search], capture_output = True, cwd = project_root, ) # ripgrep returns 1 if there are no matches, 2 on error if res.returncode == 1: return [] assert res.returncode == 0, "ripgrep command failed" return [Path(os.fsdecode(path)) for path in res.stdout.splitlines()] def run_buildifier(project_root): subprocess.run( ["./bazelisk.sh", "run", "//quality:buildifier_fix"], check=True, cwd = project_root ) def run_topgen(project_root): for top in all_tops: subprocess.run( ["./util/topgen.py", "-t", f"hw/top_{top}/data/top_{top}.hjson"], check=True, cwd = project_root, ) subprocess.run( ["make", "-C", "hw", "cmdgen"], check=False, cwd = project_root, ) def delete_rule(lines, rule_name, target_name, file_name): try: start_idx = 0 while start_idx < len(lines): start_idx = lines.index(f'{rule_name}(\n', start_idx) if target_name is None or lines[start_idx + 1] == f' {target_name},\n': # noqa: E231 break start_idx = start_idx + 1 except ValueError: assert False, \ f"did not find beginning of rule {rule_name} (target name {target_name}) in {file_name}" try: end_idx = lines.index(')\n', start_idx + 1) except ValueError: assert False, \ f"did not find end of rule {rule_name} (target name {target_name}) in {file_name}" if start_idx > 0 and lines[start_idx - 1] == '\n' and \ end_idx + 1 < len(lines) and lines[end_idx + 1] == '\n': end_idx += 1 return lines[:start_idx] + lines[end_idx + 1:], start_idx, lines[start_idx:end_idx + 1] def delete_all_rules(lines, rule_name, target_name, file_name): while True: try: lines, _, _ = delete_rule(lines, rule_name, target_name, file_name) except: # noqa: E722 break return lines def step3(project_root): files = list(set( find_all_files(project_root, "autogen_hjson_c_header\\(") + find_all_files(project_root, "autogen_hjson_rust_header\\(") )) print(files) for path in files: build_file_path = project_root / path build_file = build_file_path.read_text().splitlines(keepends=True) # Remove load to //rules:autogen.bzl build_file, _, _ = delete_rule( build_file, 'load', '"//rules:autogen.bzl"', build_file_path ) # Remove autogen_hjson_c_header and autogen_hjson_rust_header build_file = delete_all_rules( build_file, 'autogen_hjson_c_header', None, build_file_path ) build_file = delete_all_rules( build_file, 'autogen_hjson_rust_header', None, build_file_path ) build_file_path.write_text(''.join(build_file)) # Run buildifier. run_buildifier(project_root) run_topgen(project_root) subprocess.run( [ "git", "commit", "-vas", "-m", "Remove old header targets", # noqa: E231 "-m", "This commit was generated by the following script:", "-m", Path(__file__).read_text(), ], check=True, cwd = project_root ) step3(project_root) Signed-off-by: Amaury Pouly <[email protected]>
Those rules are now unused Signed-off-by: Amaury Pouly <[email protected]>
With the new rule, a very easy way to get the list of all Hjson files for IPs is to look at the `ip_hjson` attribute of the definition of //hw/top:top_<name>_desc. Since this script kind of assumes earlgrey anyway, explicitely point to the earlgrey description of now. Signed-off-by: Amaury Pouly <[email protected]>
Unfortunately, opentitantool is far from compiling with darjeeling due to many explicit dependencies on earlgrey. At the same time, opentitantool provides many operations which are not earlgrey-specific at all such as flash image generation, signing, modid checks, etc This commit introduces a necessary hack: in the host transition for the golden toolchain, always set the top to earlgrey. This way we can ensure that opentitantool will compile. For now this does not introduce any problem but this is not a proper fix. Signed-off-by: Amaury Pouly <[email protected]>
42f720d
to
8d438b8
Compare
The CI failures are unrelated (flaky tests, flaky CW305 bitstream build). |
This PR implements the Multitop HW desc rule RFC. It replaces #24791 which is the original implementation.
There is one notable difference between this implementation and the one suggested in the RFC.
RFC suggested implementation
The RFC suggested to build the top description entire out of rules and providers:
The information is then used to e.g. build headers:
Problem with this approach
While this approach works fine, we need to consider what happens when we mix this multitop. Suppose that we have a way to selecting a top though a bazel config and we use that to point to the relevant top description:
This works fine because both Earlgrey and Darjeeling have a UART. Now consider an IP block that only exists in Earlgrey or Darjeeling:
We run into an issue: the rule
opentitan_ip_c_headers
cannot work forip = dma
if run on earlgrey because Earlgrey does not have a DMA and therefore does not even know where the DMA hjson file is. One solution could be to output an empty header file but this would just push the problem onto the users: if they try to use it and depend on DMA register offsets, they will fail to compile. Therefore, the proper definition should be:Where
opentitan_require_top
is some unspecified-as-of-yet macro, for example:But now run into another issue: we have to manually specify for each IP the list of tops that it is compatible with! This means that adding a top require adding 35+ conditions everywhere! Worse, if we change the definition of a top (e.g. Darjeeling which is supposed to be easily tweakable) then we have to change this as well. This defeats the entire point of have a top description generated by bazel.
Formalizing the problem
Thinking back, what we really would like to write is this:
where
opentitan_require_ip
is a magical macro that somehow expands to the list of tops with the DMA block. In order to write such a macro, we need to remember that macros are expanded during Bazel's loading phase, i.e. before the build graph is even constructed. During the loading phase, the only "symbols" available to macros are:.bzl
files.bzl
filesIt is clear that in order to work,
opentitan_require_ip
needs access to a (possibly scaled-down) description of every top. Therefore the conclusion is that the top descriptions needs to be available in the loading phase. With the rule+provider approach from the RFC, it is only available in the analysis phase.Solution
The solution proposed in the RFC is therefore to still use the idea of the RFC but at the loading phase. Here
opentitan_ip
andopentitan_top
become macros returningstruct
. Those need to be placed in newly created.bzl
files to be usable in the loading phase:A simple implementation of those macros is to simply wrap the information in a
struct
:This allows use to implement
opentitan_require_ip
simply as follows:Of course, we do not want to carry those struct around for the analysis phase so we still want to create a target that exports an
OpenTitanTopInfo
provider as before. This is implemented in the PR and a simplified version of the code is the following: