Skip to content
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

Merged
merged 14 commits into from
Jan 9, 2025
Merged

Conversation

pamaury
Copy link
Contributor

@pamaury pamaury commented Dec 10, 2024

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:

# In hw/ip/uart/BUILD:
# This target exports an OpenTitanIpInfo provider
opentitan_ip(
  name = "uart",
  hjson = "//path/to/uart.hjson"
)

# In hw/top_earlgrey/BUILD:
# This target exports an OpenTitanTopInfo provider
opentitan_top(
  name = "top_earlgrey_desc",
  hjson = "//path/to/top_earlgrey.gen.hjson",
  ips = [
    "//hw/ip/uart",
   ...
  ]
)

The information is then used to e.g. build headers:

# In hw/top:
opentitan_ip_c_headers(
  name = "uart_c_regs",
  top = "//hw/top_earlgrey:top_earlgrey_desc",
  ip = "uart",
)

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:

# In hw/top
alias(
  name = "top_desc",
  actual = select({
    "//hw/top:is_earlgrey": "//hw/top_earlgrey:top_earlgrey_desc",
    "//hw/top:is_darjeeling": "//hw/top_earlgrey:top_darjeeling_desc",
  })
)

opentitan_ip_c_headers(
  name = "uart_c_regs",
  top = "//hw/top:top_desc",   # <-- NOTE: not pointing to earlgrey anymore
  ip = "uart",
)

This works fine because both Earlgrey and Darjeeling have a UART. Now consider an IP block that only exists in Earlgrey or Darjeeling:

opentitan_ip_c_headers(
  name = "dma_c_regs",
  top = "//hw/top:top_desc", 
  ip = "dma",
)

opentitan_ip_c_headers(
  name = "usb_c_regs",
  top = "//hw/top:top_desc", 
  ip = "usb",
)

We run into an issue: the rule opentitan_ip_c_headers cannot work for ip = 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:

opentitan_ip_c_headers(
  name = "dma_c_regs",
  top = "//hw/top:top_desc", 
  ip = "dma",
  target_compatible_with = opentitan_require_top("darjeeling") # Express that we want darjeeling
)

opentitan_ip_c_headers(
  name = "usb_c_regs",
  top = "//hw/top:top_desc", 
  ip = "usb",
  target_compatible_with = opentitan_require_top("earlgrey") # Express that we want earlgrey
)

Where opentitan_require_top is some unspecified-as-of-yet macro, for example:

def opentitan_require_top(topname):
  return select({
    "//hw/top:is_{}".format(topname): [],
    "//conditions:default": ["@platforms//:incompatible"],
  })

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:

opentitan_ip_c_headers(
  name = "dma_c_regs",
  top = "//hw/top:top_desc", 
  ip = "dma",
  target_compatible_with = opentitan_require_ip("dma") # Express that we only accept tops with the DMA IP
)

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:

  • other macros defined in .bzl files
  • variables defines in .bzl files

It 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 and opentitan_top become macros returning struct. Those need to be placed in newly created .bzl files to be usable in the loading phase:

# In hw/ip/uart/defs.bzl:  (not a BUILD file)

UART = opentitan_ip(
  name = "uart",
  hjson = "//path/to/uart.hjson"
)

# In hw/top_earlgrey/defs.bzl:  (not a BUILD file)
load("//hw/ip/uart:defz.bzl", "UART")
EARLGREY = opentitan_top(
  name = "top_earlgrey_desc",
  hjson = "//path/to/top_earlgrey.gen.hjson",
  ips = [
    UART,
   ...
  ]
)

# In hw/top/defs.bzl:
load("//hw/top_earlgrey:defz.bzl", "EARLGREY")
load("//hw/top_darjeeling:defz.bzl", "DARJEELING")

ALL_TOPS = [EARLGREY, DARJEELING]

A simple implementation of those macros is to simply wrap the information in a struct:

def opentitan_ip(name, hjson):
    return struct(
        name = name,
        hjson = hjson,
    )

def opentitan_top(name, hjson, ips):
    return struct(
        name = name,
        hjson = hjson,
        top_lib = top_lib,
        top_ld = top_ld,
        ips = ips,
    )

This allows use to implement opentitan_require_ip simply as follows:

def opentitan_require_ip(ip):
    compatible_tops = []
    for top in ALL_TOPS:
        for _ip in top.ips:
            if _ip.name == ip:
                compatible_tops.append(top.name)
                break

    return select({
        "//hw/top:is_{}".format(top): []
        for top in compatible_tops
    } | {
        "//conditions:default": ["@platform//:incompatible"],
    })

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:

# In hw/top/BUILD:

# Create one description per top
[
    describe_top(
        name = "top_{}_desc".format(top),
        all_tops = ALL_TOPS,
        top = top,
    )
    for top in ALL_TOP_NAMES
]

# In hw/top/defs.bzl:
def _describe_top(ctx):
    return [
        OpenTitanTopInfo(
            name = ctx.attr.topname,
            hjson = ctx.file.hjson,
            ip_hjson = ctx.attr.ip_hjson, # simplified: ctx.attr.ip_hjson has the wrong "type" and needs a bit of massaging...
        ),
    ]

describe_top_rule = rule(
    implementation = _describe_top,
    doc = """Create a target that provides the description of a top in the form of an OpenTitanTopInfo provider.""",
    attrs = {
        "hjson": attr.label(mandatory = True, allow_single_file = True, doc = "toplevel hjson file generated by topgen"),
        "ip_hjson": attr.label_keyed_string_dict(allow_files = True, doc = "mapping from hjson files to tops"),
        "topname": attr.string(mandatory = True, doc = "Name of the top"),
    },
)


def describe_top(name, all_tops, top):
    """
    Create a target that provides an OpenTitanTopInfo corresponding to the
    requested top.

    - all_tops: list of tops (created by opentitan_top).
    - top: name of the top to use.
    """

    # Details omitted: go through the all_tops struct and extract the relevant information

    describe_top_rule(
        name = name,
        hjson = top_hjson,
        ip_hjson = all_hjson,
        topname = top,
    )

@pamaury pamaury force-pushed the multitop_hw_desc branch 11 times, most recently from 6a33b1c to 3ad7c24 Compare December 16, 2024 14:48
@pamaury pamaury changed the title [WIP][DO NOT MERGE][DO NOT COMMENT] HW rules reconstruction [multitop] Implement HW rules RFC Dec 16, 2024
@pamaury pamaury marked this pull request as ready for review December 16, 2024 18:00
@pamaury pamaury requested review from msfschaffner, vogelpi, cfrantz and a team as code owners December 16, 2024 18:00
@pamaury pamaury requested review from HU90m, Razer6, a-will, nbdd0121 and jwnrt and removed request for a team December 16, 2024 18:00
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")
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

else:
assert False, "unknown step"


Copy link
Contributor

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()

Copy link
Contributor Author

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
Comment on lines 19 to 22
names = {}
for top in ALL_TOPS:
for ip in top.ips:
names[ip.name] = {}
Copy link
Contributor

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}

Copy link
Contributor

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.

Comment on lines +26 to +27
# WARNING This is a horrible hack: when we transition to host, we pretend
# that this is earlgrey so opentitantool can compile...
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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).

@a-will
Copy link
Contributor

a-will commented Dec 16, 2024

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?

@pamaury pamaury force-pushed the multitop_hw_desc branch 2 times, most recently from 2dbfffa to c08ef1a Compare December 23, 2024 10:40
pamaury added a commit to pamaury/opentitan that referenced this pull request Jan 6, 2025
Signed-off-by: Amaury Pouly <[email protected]>
pamaury added a commit to pamaury/opentitan that referenced this pull request Jan 7, 2025
Signed-off-by: Amaury Pouly <[email protected]>
Copy link
Member

@Razer6 Razer6 left a 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.

pamaury added a commit to pamaury/opentitan that referenced this pull request Jan 8, 2025
Signed-off-by: Amaury Pouly <[email protected]>
pamaury added a commit to pamaury/opentitan that referenced this pull request Jan 8, 2025
Signed-off-by: Amaury Pouly <[email protected]>
pamaury added 14 commits January 9, 2025 16:20
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]>
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]>
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]>
@pamaury
Copy link
Contributor Author

pamaury commented Jan 9, 2025

The CI failures are unrelated (flaky tests, flaky CW305 bitstream build).

@pamaury pamaury merged commit e5c633d into lowRISC:master Jan 9, 2025
36 of 38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants