Skip to content

Commit d1f0c55

Browse files
authored
Move codegen.py invoke logic into a gni template (#23154)
* Move codegen logic into a standalone gni template * Remove extra hadcoded path in codegen.gni * Remove extra hadcoded path in codegen.gni * Use common codegen instruction for bridge as well * Make sure that the codegen include is a public target, so that includes work for generated code * Restyle * Enforce that outputs in the codegen are known rather than dynamic * Apply some code review comments * rephrased a comment based on code review * Restyle * Stop using exec_script in gn (faster gn) * Fix path rebasing to get valid paths * Restyle * make sure that generated expected outputs is one of the expected input files for the codegen script * Add documentation on how to get the list of expected files * Add missing init call in the test storage impl
1 parent d520b39 commit d1f0c55

File tree

6 files changed

+294
-52
lines changed

6 files changed

+294
-52
lines changed

build/chip/chip_codegen.gni

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Copyright (c) 2022 Project CHIP Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import("//build_overrides/build.gni")
16+
import("//build_overrides/chip.gni")
17+
import("//build_overrides/pigweed.gni")
18+
19+
import("$dir_pw_build/python.gni")
20+
21+
# Defines a target that runs code generation based on
22+
# scripts/codegen.py
23+
#
24+
# Arguments:
25+
# input
26+
# The ".matter" file to use to start the code generation
27+
#
28+
# generator
29+
# Name of the generator to use (e.g. java, cpp-app)
30+
#
31+
# outputs
32+
# Explicit names of the expected outputs. Enforced to validate that
33+
# expected outputs are generated when processing input files.
34+
#
35+
# NOTE: content of "outputs" is verified to match the output of codegen.py
36+
# exactly. It is not inferred on purpose, to make build-rules explicit
37+
# and verifiable (even though codege.py can at runtime report its outputs)
38+
#
39+
# To find the list of generated files, you can run codegen.py with the
40+
# "--name-only" argument
41+
#
42+
# Example usage:
43+
#
44+
# chip_codegen("java-jni-generate") {
45+
# input = "controller-clusters.matter"
46+
# generator = "java"
47+
#
48+
# outputs = [
49+
# "jni/IdentifyClient-ReadImpl.cpp",
50+
# "jni/IdentifyClient-InvokeSubscribeImpl.cpp",
51+
# # ... more to follow
52+
# ]
53+
# }
54+
#
55+
template("chip_codegen") {
56+
_name = target_name
57+
_generator = invoker.generator
58+
59+
config("${_name}_config") {
60+
include_dirs = [ target_gen_dir ]
61+
}
62+
63+
pw_python_action(_name) {
64+
script = "${chip_root}/scripts/codegen.py"
65+
66+
_idl_file = invoker.input
67+
_expected_outputs = "${target_gen_dir}/${_name}.expected.outputs"
68+
69+
write_file(_expected_outputs, invoker.outputs, "list lines")
70+
71+
args = [
72+
"--generator",
73+
_generator,
74+
"--output-dir",
75+
rebase_path(target_gen_dir, root_build_dir),
76+
"--expected-outputs",
77+
rebase_path(_expected_outputs, root_build_dir),
78+
rebase_path(_idl_file, root_build_dir),
79+
]
80+
81+
deps = [ "${chip_root}/scripts/idl" ]
82+
public_configs = [ ":${_name}_config" ]
83+
84+
inputs = [
85+
_idl_file,
86+
_expected_outputs,
87+
]
88+
sources = [ _idl_file ]
89+
90+
outputs = []
91+
foreach(name, invoker.outputs) {
92+
outputs += [ "${target_gen_dir}/${name}" ]
93+
}
94+
}
95+
}

examples/dynamic-bridge-app/linux/BUILD.gn

+13-17
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import("//build_overrides/build.gni")
1616
import("//build_overrides/chip.gni")
1717

18+
import("${chip_root}/build/chip/chip_codegen.gni")
1819
import("${chip_root}/build/chip/tools.gni")
1920
import("${chip_root}/src/app/common_flags.gni")
2021

@@ -27,24 +28,19 @@ if (chip_enable_pw_rpc) {
2728

2829
assert(chip_build_tools)
2930

30-
action("chip-bridge-codegen") {
31-
script = "${chip_root}/scripts/codegen.py"
32-
sources =
33-
[ "${chip_root}/examples/bridge-app/bridge-common/bridge-app.matter" ]
34-
35-
# Also several other files, but this is sufficient for dependency purposes.
36-
outputs = [ "$target_gen_dir/bridge/BridgeClustersImpl.h" ]
37-
38-
args = [
39-
"--generator",
40-
"bridge",
41-
"--output-dir",
42-
rebase_path(target_gen_dir, root_build_dir),
43-
rebase_path(
44-
"${chip_root}/examples/dynamic-bridge-app/bridge-common/bridge-app.matter",
45-
root_build_dir),
31+
chip_codegen("chip-bridge-codegen") {
32+
input = "${chip_root}/examples/bridge-app/bridge-common/bridge-app.matter"
33+
generator = "bridge"
34+
35+
outputs = [
36+
"bridge/OnOff.h",
37+
"bridge/LevelControl.h",
38+
"bridge/Descriptor.h",
39+
"bridge/Switch.h",
40+
"bridge/TemperatureMeasurement.h",
41+
"bridge/BridgeClustersImpl.h",
42+
"bridge/BridgeGlobalStructs.h",
4643
]
47-
deps = [ "${chip_root}/scripts/idl" ]
4844
}
4945

5046
executable("dynamic-chip-bridge-app") {

scripts/codegen.py

+33-2
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
import logging
1818
import coloredlogs
1919
import enum
20+
import sys
2021

2122
try:
2223
from idl.matter_idl_parser import CreateParser
2324
except:
2425
import os
25-
import sys
2626
sys.path.append(os.path.abspath(os.path.dirname(__file__)))
2727
from idl.matter_idl_parser import CreateParser
2828

@@ -54,6 +54,9 @@ class ListGeneratedFilesStorage(GeneratorStorage):
5454
A storage that prints out file names that would have content in them.
5555
"""
5656

57+
def __init__(self):
58+
super().__init__()
59+
5760
def get_existing_data(self, relative_path: str):
5861
return None # stdout has no pre-existing data
5962

@@ -102,10 +105,15 @@ def write_new_data(self, relative_path: str, content: str):
102105
default=False,
103106
is_flag=True,
104107
help='Output just a list of file names that would be generated')
108+
@click.option(
109+
'--expected-outputs',
110+
type=click.Path(exists=True),
111+
default=None,
112+
help='A file containing all expected outputs. Script will fail if outputs do not match')
105113
@click.argument(
106114
'idl_path',
107115
type=click.Path(exists=True))
108-
def main(log_level, generator, output_dir, dry_run, name_only, idl_path):
116+
def main(log_level, generator, output_dir, dry_run, name_only, expected_outputs, idl_path):
109117
"""
110118
Parses MATTER IDL files (.matter) and performs SDK code generation
111119
as set up by the program arguments.
@@ -124,6 +132,29 @@ def main(log_level, generator, output_dir, dry_run, name_only, idl_path):
124132
generator = __GENERATORS__[
125133
generator].CreateGenerator(storage, idl=idl_tree)
126134
generator.render(dry_run)
135+
136+
if expected_outputs:
137+
with open(expected_outputs, 'rt') as fin:
138+
expected = set()
139+
for l in fin.readlines():
140+
l = l.strip()
141+
if l:
142+
expected.add(l)
143+
144+
if expected != storage.generated_paths:
145+
logging.fatal("expected and generated files do not match.")
146+
147+
extra = storage.generated_paths - expected
148+
missing = expected - storage.generated_paths
149+
150+
for name in extra:
151+
logging.fatal(" '%s' was generated but not expected" % name)
152+
153+
for name in missing:
154+
logging.fatal(" '%s' was expected but not generated" % name)
155+
156+
sys.exit(1)
157+
127158
logging.info("Done")
128159

129160

scripts/idl/generators/__init__.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,22 @@
2525

2626
class GeneratorStorage:
2727
"""
28-
Handles file operations for generator output. Specificall can create
28+
Handles file operations for generator output. Specifically can create
2929
required files for output.
3030
3131
Is overriden for unit tests.
3232
"""
3333

34+
def __init__(self):
35+
self._generated_paths = set()
36+
37+
@property
38+
def generated_paths(self):
39+
return self._generated_paths
40+
41+
def report_output_file(self, relative_path: str):
42+
self._generated_paths.add(relative_path)
43+
3444
def get_existing_data(self, relative_path: str):
3545
"""Gets the existing data at the given path.
3646
If such data does not exist, will return None.
@@ -49,6 +59,7 @@ class FileSystemGeneratorStorage(GeneratorStorage):
4959
"""
5060

5161
def __init__(self, output_dir: str):
62+
super().__init__()
5263
self.output_dir = output_dir
5364

5465
def get_existing_data(self, relative_path: str):
@@ -148,6 +159,11 @@ def internal_render_one_output(self, template_path: str, output_file_name: str,
148159

149160
rendered = self.jinja_env.get_template(template_path).render(vars)
150161

162+
# Report regardless if it has changed or not. This is because even if
163+
# files are unchanged, validation of what the correct output is should
164+
# still be done.
165+
self.storage.report_output_file(output_file_name)
166+
151167
if rendered == self.storage.get_existing_data(output_file_name):
152168
logging.info("File content not changed")
153169
else:

scripts/idl/test_generators.py

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def add_outputs(self, yaml_outputs_dict):
5858

5959
class TestCaseStorage(GeneratorStorage):
6060
def __init__(self, test_case: GeneratorTestCase, checker: unittest.TestCase):
61+
super().__init__()
6162
self.test_case = test_case
6263
self.checker = checker
6364
self.checked_files = set()

0 commit comments

Comments
 (0)