Skip to content

Commit e963bd1

Browse files
[TO REVIEW][nrf noup][ZAP] Imporove west zap commands
* Now we can obtain the recommended zap version from the `scripts/setup/zap.version` file instead of reading it from the `zap_execution.py` file. Sometimes the nightly version is recommended whereas in the python file there is no `-nightly` postfix. * If the sandbox is not configured on a Linux PC, an annoying issue with the sandbox occurs after updating to the newest ZAP tool. To resolve it, ask the user if they want to add permissions automatically. * `update_zcl_in_zap` function must be called only while the specific clusters are known. * Added the new zap-update command to synchronize the .zap and zcl.json files with the new Matter Data Model from SDK.
1 parent 460d4c4 commit e963bd1

5 files changed

Lines changed: 174 additions & 44 deletions

File tree

scripts/west/zap_append.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,20 @@
1212
from zap_common import DEFAULT_MATTER_PATH, DEFAULT_MATTER_TYPES_RELATIVE_PATH, DEFAULT_ZCL_JSON_RELATIVE_PATH
1313

1414

15-
def add_custom_attributes_from_xml(xml_file: Path, zcl_data: dict):
15+
def add_custom_attributes_from_xml(xml_file: Path, zcl_data: dict, matter_path: Path = DEFAULT_MATTER_PATH):
1616
"""
1717
Parse the cluster XML file and add attributes with custom types to
1818
attributeAccessInterfaceAttributes in zcl_data.
1919
2020
Args:
2121
cluster_xml_path: Path to the cluster XML file
2222
zcl_data: The loaded zcl.json data dictionary
23+
matter_path: Path to the Matter directory
2324
"""
2425

2526
# Step 1: Load all type names from chip-types.xml into a list
2627
types = []
27-
tree = ET.parse(DEFAULT_MATTER_PATH / DEFAULT_MATTER_TYPES_RELATIVE_PATH)
28+
tree = ET.parse(matter_path / DEFAULT_MATTER_TYPES_RELATIVE_PATH)
2829
root = tree.getroot()
2930

3031
for type_element in root.findall('.//type'):
@@ -82,7 +83,7 @@ def add_custom_attributes_from_xml(xml_file: Path, zcl_data: dict):
8283
return modified
8384

8485

85-
def add_cluster_to_zcl(zcl_base: Path, cluster_xml_paths: list, output: Path):
86+
def add_cluster_to_zcl(zcl_base: Path, cluster_xml_paths: list, output: Path, matter_path: Path = DEFAULT_MATTER_PATH):
8687
"""
8788
Add the cluster to the ZCL file.
8889
"""
@@ -135,7 +136,7 @@ def add_cluster_to_zcl(zcl_base: Path, cluster_xml_paths: list, output: Path):
135136
log.dbg(f"Successfully added {file}")
136137

137138
# Add custom attributes from the XML file to the ZCL file
138-
add_custom_attributes_from_xml(Path(cluster), zcl_json)
139+
add_custom_attributes_from_xml(Path(cluster), zcl_json, matter_path)
139140

140141
# If output file is not provided, we will edit the existing ZCL file
141142
file_to_write = output if output else zcl_base
@@ -163,7 +164,7 @@ def do_add_parser(self, parser_adder) -> argparse.ArgumentParser:
163164
help=f"An absolute path to the Matter directory. If not set the path with be set to the {DEFAULT_MATTER_PATH}")
164165
parser.add_argument("-o", "--output", type=Path,
165166
help=f"Output path to store the generated zcl.json file. If not provided the path will be set to the base zcl.json file (MATTER/{DEFAULT_ZCL_JSON_RELATIVE_PATH}).")
166-
parser.add_argument("new_clusters", nargs='+',
167+
parser.add_argument("--clusters", nargs='+',
167168
help="Paths to the XML files that contain the custom cluster definitions")
168169
return parser
169170

@@ -173,9 +174,9 @@ def do_run(self, args, unknown_args) -> None:
173174
if not args.output:
174175
args.output = args.matter.joinpath(DEFAULT_ZCL_JSON_RELATIVE_PATH)
175176

176-
for cluster in args.new_clusters:
177+
for cluster in args.clusters:
177178
if not Path(cluster).exists():
178179
log.err(f"No such cluster file: {cluster}")
179180
return
180181

181-
add_cluster_to_zcl(args.base.absolute(), args.new_clusters, args.output.absolute())
182+
add_cluster_to_zcl(args.base.absolute(), args.clusters, args.output.absolute())

scripts/west/zap_common.py

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def post_process_generated_files(output_path: Path):
159159
continue
160160

161161

162-
def synchronize_zcl_with_base(zcl_json: Path):
162+
def synchronize_zcl_with_base(zcl_json: Path, matter_path: Path = DEFAULT_MATTER_PATH):
163163
"""
164164
Synchronizes a zcl.json file with the base/default zcl.json from Matter SDK.
165165
@@ -170,15 +170,15 @@ def synchronize_zcl_with_base(zcl_json: Path):
170170

171171
print(f"Synchronizing {zcl_json} with base zcl.json")
172172

173-
base_zcl_path = DEFAULT_MATTER_PATH / DEFAULT_ZCL_JSON_RELATIVE_PATH
173+
base_zcl_path = matter_path / DEFAULT_ZCL_JSON_RELATIVE_PATH
174174
with open(base_zcl_path, 'r') as f:
175175
base_zcl_data = json.load(f)
176176

177177
with open(zcl_json, 'r') as f:
178178
target_zcl_data = json.load(f)
179179

180180
modified = False
181-
fields_to_skip = {'xmlRoot'} # Fields to skip in comparison
181+
fields_to_skip = {'xmlRoot', 'manufacturersXml'} # Fields to skip in comparison
182182

183183
for key, value in base_zcl_data.items():
184184
if key in fields_to_skip:
@@ -188,6 +188,10 @@ def synchronize_zcl_with_base(zcl_json: Path):
188188
target_zcl_data[key] = value
189189
modified = True
190190
print(f"Added missing field: '{key}'")
191+
if value != target_zcl_data[key]:
192+
target_zcl_data[key] = value
193+
modified = True
194+
print(f"Updated field: '{key}'")
191195

192196
if modified:
193197
with open(zcl_json, 'w') as f:
@@ -199,9 +203,23 @@ def synchronize_zcl_with_base(zcl_json: Path):
199203
print("Done")
200204

201205

206+
def fix_sandbox_permissions(e: subprocess.CalledProcessError) -> None:
207+
"""
208+
Fix sandbox permissions if needed.
209+
"""
210+
if e.returncode == -5:
211+
log.inf("\nThe wrong sandbox permissions are used. Do you wanto to add the permissions to the sandbox?")
212+
answer = input("y/n: ")
213+
if answer == "y":
214+
subprocess.check_call(['sudo', 'chown', 'root', str(
215+
self.get_install_path() / 'chrome-sandbox')])
216+
subprocess.check_call(['sudo', 'chmod', '4755', str(self.get_install_path() / 'chrome-sandbox')])
217+
log.inf("Permissions added to the sandbox")
218+
219+
202220
class ZapInstaller:
203221
INSTALL_DIR = Path('.zap-install')
204-
ZAP_URL_PATTERN = 'https://github.com/project-chip/zap/releases/download/v%04d.%02d.%02d/%s.zip'
222+
ZAP_URL_PATTERN = 'https://github.com/project-chip/zap/releases/download/%s/%s.zip'
205223

206224
def __init__(self, matter_path: Path):
207225
self.matter_path = matter_path
@@ -261,22 +279,20 @@ def get_zap_cli_path(self) -> Path:
261279
"""
262280
return self.install_path / self.zap_cli_exe
263281

264-
def get_recommended_version(self) -> Tuple[int, int, int]:
282+
def get_recommended_version(self) -> str:
265283
"""
266284
Returns ZAP package recommended version as a tuple of integers.
267285
268-
Parses zap_execution.py script from Matter SDK to determine the minimum
269-
required ZAP package version.
286+
Reads the version from zap.version file in Matter SDK.
287+
Expected format: v{YEAR}.{MONTH}.{DAY}[-suffix]
288+
Example: v2025.09.23-nightly
270289
"""
271-
RE_MIN_ZAP_VERSION = r'MIN_ZAP_VERSION\s*=\s*\'(\d+)\.(\d+)\.(\d+)'
272-
zap_execution_path = self.matter_path / 'scripts/tools/zap/zap_execution.py'
290+
zap_version_path = self.matter_path / 'scripts/setup/zap.version'
273291

274-
with open(zap_execution_path, 'r') as f:
275-
if match := re.search(RE_MIN_ZAP_VERSION, f.read()):
276-
return tuple(int(group) for group in match.groups())
277-
raise RuntimeError(f'Failed to find MIN_ZAP_VERSION in {zap_execution_path}')
292+
with open(zap_version_path, 'r') as f:
293+
return f.read().strip()
278294

279-
def get_current_version(self) -> Tuple[int, int, int]:
295+
def get_current_version(self) -> str:
280296
"""
281297
Returns ZAP package current version as a tuple of integers.
282298
@@ -292,7 +308,7 @@ def get_current_version(self) -> Tuple[int, int, int]:
292308

293309
RE_VERSION = r'Version:\s*(\d+)\.(\d+)\.(\d+)'
294310
if match := re.search(RE_VERSION, output):
295-
return tuple(int(group) for group in match.groups())
311+
return match.group(1) + '.' + match.group(2) + '.' + match.group(3)
296312

297313
raise RuntimeError("Failed to find version in ZAP output")
298314

@@ -301,7 +317,7 @@ def install_zap(self, version: Tuple[int, int, int]) -> None:
301317
Downloads and unpacks selected ZAP package version.
302318
"""
303319
with tempfile.TemporaryDirectory() as temp_dir:
304-
url = ZapInstaller.ZAP_URL_PATTERN % (*version, self.package)
320+
url = ZapInstaller.ZAP_URL_PATTERN % (version, self.package)
305321
log.inf(f'Downloading {url}...')
306322
zip_file_path = str(Path(temp_dir).joinpath(f'{self.package}.zip'))
307323

@@ -343,15 +359,24 @@ def update_zap_if_needed(self) -> None:
343359
recommended_version = self.get_recommended_version()
344360
current_version = self.get_current_version()
345361

346-
log.inf(f'ZAP installation directory: {self.install_path}')
362+
# Extract version without prefix and suffix for comparison
363+
# recommended_version format: v2025.09.23-nightly or v2025.09.23
364+
# current_version format: 2025.9.23
365+
recommended_version_clean = recommended_version
366+
if match := re.search(r'v?(\d+\.\d+\.\d+)', recommended_version):
367+
year, month, day = match.group(1).split('.')
368+
recommended_version_clean = f"{int(year)}.{int(month)}.{int(day)}"
369+
370+
log.inf(f'ZAP installation directory: {self.install_path}')
371+
log.inf(f"Current ZAP version: {current_version}")
347372

348-
if current_version:
349-
verdict = 'up to date' if current_version == recommended_version else 'outdated'
350-
log.inf('Found ZAP {}.{}.{} ({})'.format(*current_version, verdict))
373+
if current_version:
374+
verdict = 'up to date' if current_version == recommended_version_clean else 'outdated'
375+
log.inf('Found ZAP {} ({})'.format(current_version, verdict))
351376

352-
if current_version != recommended_version:
353-
log.inf('Installing ZAP {}.{}.{}'.format(*recommended_version))
354-
self.install_zap(recommended_version)
377+
if current_version != recommended_version_clean:
378+
log.inf('Installing ZAP {}'.format(recommended_version))
379+
self.install_zap(recommended_version)
355380

356381
@staticmethod
357382
def set_exec_permission(path: Path) -> None:

scripts/west/zap_generate.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from west.commands import CommandError, WestCommand
1616
from zap_common import (DEFAULT_MATTER_PATH, ZapInstaller, existing_dir_path, existing_file_path, find_zap,
1717
post_process_generated_files, synchronize_zcl_with_base, update_zcl_in_zap)
18+
from zap_update import ZapUpdate
1819

1920
# fmt: off
2021
scripts_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -106,6 +107,23 @@ def do_run(self, args, unknown_args):
106107
if zcl_file and not zcl_file.exists():
107108
raise CommandError(f"ZCL file {zcl_file} does not exist")
108109
full = zap.get('full', False)
110+
clusters = None
111+
112+
if full:
113+
clusters = zap.get('clusters', [])
114+
if not clusters:
115+
raise CommandError("Clusters are not set in the yaml file for the full build")
116+
117+
# Prepare arguments for ZapUpdate
118+
zap_update_args = argparse.Namespace(
119+
zap_file=zap_file,
120+
zcl_json=zcl_file,
121+
clusters=clusters,
122+
matter_path=args.matter_path
123+
)
124+
125+
# Run zap_update to update and synchronize zap/zcl with clusters using the current Matter SDK
126+
ZapUpdate().do_run(zap_update_args, [])
109127

110128
zap_entry = ZapFile(name=name, zap_file=zap_file, full=full, zcl_file=zcl_file)
111129
zap_files.append(zap_entry)
@@ -161,10 +179,6 @@ def do_run(self, args, unknown_args):
161179
# Generate .matter file
162180
self.check_call(self.build_command(zap.zap_file, output_path))
163181

164-
# Update the zcl in zap file if needed
165-
# We need to do this in case the zap gui was not called before.
166-
update_zcl_in_zap(zap.zap_file, zcl_file, app_templates_path)
167-
168182
if args.full:
169183
# Full build is about generating an apropertiate Matter data model files in a specific directory layout.
170184
# Currently, we must align to the following directory layout:
@@ -200,10 +214,9 @@ def do_run(self, args, unknown_args):
200214
zap_output_dir = output_path / 'app-common' / 'zap-generated'
201215
codegen_output_dir = output_path / 'clusters'
202216

203-
# Synchronize the zcl.json file with the base zcl.json file
204-
# We need to do this to update the zcl.json file with the new clusters and attributes.
205-
# It may be helpful if the Matter SDK was updated.
206-
synchronize_zcl_with_base(zcl_file)
217+
# Update the zcl in zap file if needed
218+
# We need to do this in case the zap gui was not called before.
219+
update_zcl_in_zap(zap.zap_file, zcl_file, app_templates_path)
207220

208221
# Temporarily change directory to matter_path so JinjaCodegenTarget and ZAPGenerateTarget can find their scripts
209222
original_cwd = os.getcwd()

scripts/west/zap_gui.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@
33
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
44

55
import argparse
6+
import os
7+
import pwd
8+
import grp
69
from pathlib import Path
10+
import subprocess
711

812
from textwrap import dedent
913

1014
from west.commands import WestCommand
1115
from west import log
1216

13-
from zap_common import existing_file_path, existing_dir_path, find_zap, ZapInstaller, DEFAULT_MATTER_PATH, DEFAULT_ZCL_JSON_RELATIVE_PATH, DEFAULT_APP_TEMPLATES_RELATIVE_PATH, update_zcl_in_zap
17+
from zap_common import existing_file_path, existing_dir_path, find_zap, ZapInstaller, DEFAULT_MATTER_PATH, DEFAULT_ZCL_JSON_RELATIVE_PATH, DEFAULT_APP_TEMPLATES_RELATIVE_PATH, update_zcl_in_zap, synchronize_zcl_with_base, fix_sandbox_permissions
1418
from zap_append import add_cluster_to_zcl
1519

1620

@@ -46,9 +50,8 @@ def do_add_parser(self, parser_adder):
4650

4751
def do_run(self, args, unknown_args):
4852
default_zcl_path = args.matter_path.joinpath(DEFAULT_ZCL_JSON_RELATIVE_PATH)
49-
50-
zap_file_path = args.zap_file or find_zap()
5153
zcl_json_path = Path(args.zcl_json).absolute() if args.zcl_json else default_zcl_path
54+
zap_file_path = args.zap_file or find_zap()
5255

5356
if zap_file_path is None:
5457
log.err("ZAP file not found!")
@@ -58,8 +61,10 @@ def do_run(self, args, unknown_args):
5861
# If the user provided the clusters and the zcl.json file provided by -j argument does not exist
5962
# we will create a new zcl.json file according to the base zcl.json file in default_zcl_path.
6063
# If the provided zcl.json file exists, we will use it as a base and update with a new cluster.
61-
base_zcl = zcl_json_path if zcl_json_path.exists() else default_zcl_path
62-
add_cluster_to_zcl(base_zcl, args.clusters, zcl_json_path)
64+
65+
# Add the new cluster to the zcl.json file
66+
add_cluster_to_zcl(zcl_json_path, args.clusters, zcl_json_path, args.matter_path)
67+
6368
elif not zcl_json_path.exists():
6469
# If clusters are not provided, but user provided a zcl.json file we need to check whether the file exists.
6570
log.err(f"ZCL file not found: {zcl_json_path}")
@@ -89,4 +94,8 @@ def do_run(self, args, unknown_args):
8994
cmd += ["--stateDirectory", args.cache.absolute()]
9095
else:
9196
cmd += ["--tempState"]
92-
self.check_call([str(x) for x in cmd])
97+
98+
try:
99+
self.check_call([str(x) for x in cmd])
100+
except subprocess.CalledProcessError as e:
101+
fix_sandbox_permissions(e)

scripts/west/zap_update.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Copyright (c) 2025 Nordic Semiconductor ASA
2+
#
3+
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
4+
5+
import argparse
6+
import os
7+
import pwd
8+
import grp
9+
from pathlib import Path
10+
import subprocess
11+
12+
from textwrap import dedent
13+
14+
from west.commands import WestCommand
15+
from west import log
16+
17+
from zap_common import existing_file_path, existing_dir_path, find_zap, DEFAULT_MATTER_PATH, DEFAULT_ZCL_JSON_RELATIVE_PATH, DEFAULT_APP_TEMPLATES_RELATIVE_PATH, synchronize_zcl_with_base, ZapInstaller, fix_sandbox_permissions
18+
from zap_append import add_cluster_to_zcl
19+
20+
21+
class ZapUpdate(WestCommand):
22+
def __init__(self):
23+
super().__init__(
24+
'zap-update',
25+
'Update the ZAP file with the new clusters',
26+
dedent('''
27+
Update the ZAP file with the new clusters'''))
28+
29+
def do_add_parser(self, parser_adder):
30+
parser = parser_adder.add_parser(self.name,
31+
help=self.help,
32+
formatter_class=argparse.RawDescriptionHelpFormatter,
33+
description=self.description)
34+
parser.add_argument('-z', '--zap-file', type=existing_file_path,
35+
help='Path to data model configuration file (*.zap)')
36+
parser.add_argument('-j', '--zcl-json', type=str,
37+
help='Path to data model definition file (zcl.json). If new clusters are added using --clusters, the new zcl.json file will be created and used.')
38+
parser.add_argument('-c', '--clusters', nargs='+',
39+
help="Paths to the XML files that contain the external cluster definitions")
40+
parser.add_argument('-m', '--matter-path', type=existing_dir_path,
41+
default=DEFAULT_MATTER_PATH, help=f'Path to Matter SDK. Default is set to {DEFAULT_MATTER_PATH}')
42+
return parser
43+
44+
def do_run(self, args, unknown_args):
45+
46+
zap_file_path = args.zap_file or find_zap()
47+
48+
if zap_file_path is None:
49+
log.err("ZAP file not found!")
50+
return
51+
52+
zcl_file_path = args.zcl_json or args.matter_path.joinpath(DEFAULT_ZCL_JSON_RELATIVE_PATH).absolute()
53+
if not zcl_file_path.exists():
54+
raise CommandError(f"ZCL file not found: {zcl_file_path}")
55+
56+
app_templates_path = args.matter_path.joinpath(DEFAULT_APP_TEMPLATES_RELATIVE_PATH)
57+
58+
zap_installer = ZapInstaller(args.matter_path)
59+
zap_installer.update_zap_if_needed()
60+
61+
# Update the .zap file with with the all changes from the Matter SDK
62+
cmd = [zap_installer.get_zap_cli_path()]
63+
cmd += ["convert"]
64+
cmd += [zap_file_path]
65+
cmd += ["--zcl", zcl_file_path]
66+
cmd += ["--gen", app_templates_path]
67+
cmd += ["--out", zap_file_path]
68+
cmd += ["--tempState"]
69+
70+
try:
71+
self.check_call([str(x) for x in cmd])
72+
except subprocess.CalledProcessError as e:
73+
fix_sandbox_permissions(e)
74+
75+
# zcl.json file was provided, so synchronize it with the Matter SDK
76+
if args.zcl_json:
77+
if not args.clusters:
78+
raise CommandError("Clusters are not provided, so the zcl.json file cannot be synchronized")
79+
synchronize_zcl_with_base(zcl_file_path, args.matter_path)
80+
81+
# Add the new clusters again to the zcl.json file
82+
add_cluster_to_zcl(zcl_file_path, args.clusters, zcl_file_path, args.matter_path)

0 commit comments

Comments
 (0)