summaryrefslogtreecommitdiff
path: root/tools/ci/membrowse_filter_targets.py
blob: 67afa71f78a036ccb4efefe404a43d13bc5517a1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#!/usr/bin/env python3
#
# Copyright (c) 2026, RT-Thread Development Team
#
# SPDX-License-Identifier: Apache-2.0
#

import argparse
import json
import os


GLOBAL_FILES = {
    ".github/membrowse-targets.json",
    ".github/workflows/membrowse-report.yml",
    "Kconfig",
}

GLOBAL_PREFIXES = (
    "components/",
    "include/",
    "libcpu/",
    "src/",
    "tools/",
)

def normalize_path(path):
    return path.replace("\\", "/").strip().lstrip("./")


def is_ignored_change(path):
    name = path.rsplit("/", 1)[-1]
    return name.lower().startswith("readme") or (path.startswith("bsp/") and "/docs/" in path)


def is_under(path, prefix):
    path = normalize_path(path)
    prefix = normalize_path(prefix).rstrip("/")
    return path == prefix or path.startswith(prefix + "/")


def read_changed_files(path):
    with open(path, "r", encoding="utf-8") as file:
        return [
            changed_file
            for changed_file in (normalize_path(line) for line in file if line.strip())
            if not is_ignored_change(changed_file)
        ]


def is_global_change(path):
    return path in GLOBAL_FILES or any(path.startswith(prefix) for prefix in GLOBAL_PREFIXES)


def target_related_prefixes(target):
    return [
        normalize_path(prefix)
        for prefix in target.get("watch_paths", [target["bsp_path"]])
    ]


def select_targets(targets, changed_files):
    if not changed_files:
        return []

    if any(is_global_change(path) for path in changed_files):
        return targets

    selected = []
    for target in targets:
        related_prefixes = target_related_prefixes(target)
        if any(
            is_under(changed_file, related_prefix)
            for changed_file in changed_files
            for related_prefix in related_prefixes
        ):
            selected.append(target)

    return selected


def skipped_targets(targets, selected_targets):
    selected_names = {target["target_name"] for target in selected_targets}
    return [target for target in targets if target["target_name"] not in selected_names]


def write_github_output(selected, skipped, output_path):
    if not output_path:
        return

    with open(output_path, "a", encoding="utf-8") as output:
        output.write("matrix={}\n".format(json.dumps(selected, separators=(",", ":"))))
        output.write("target_count={}\n".format(len(selected)))
        output.write("skipped_matrix={}\n".format(json.dumps(skipped, separators=(",", ":"))))
        output.write("skipped_count={}\n".format(len(skipped)))


def main():
    parser = argparse.ArgumentParser(description="Filter MemBrowse targets by changed files.")
    parser.add_argument("--targets", required=True, help="Path to membrowse-targets.json.")
    parser.add_argument("--changed-files", required=True, help="Path to a newline-delimited changed file list.")
    args = parser.parse_args()

    with open(args.targets, "r", encoding="utf-8") as file:
        targets = json.load(file)

    changed_files = read_changed_files(args.changed_files)
    selected_targets = select_targets(targets, changed_files)
    # Targets not affected by this change are built by nobody, but their
    # binaries are unchanged, so they are reported as identical (metadata-only)
    # to keep every target's per-commit chain unbroken. See membrowse-report.yml.
    unselected_targets = skipped_targets(targets, selected_targets)

    print("Changed files:")
    for changed_file in changed_files:
        print("  {}".format(changed_file))

    print("Selected MemBrowse targets:")
    for target in selected_targets:
        print("  {}".format(target["target_name"]))

    print("Skipped MemBrowse targets (reported as identical):")
    for target in unselected_targets:
        print("  {}".format(target["target_name"]))

    print("Selected {}/{} MemBrowse targets".format(len(selected_targets), len(targets)))
    write_github_output(selected_targets, unselected_targets, os.getenv("GITHUB_OUTPUT"))


if __name__ == "__main__":
    main()