|
| 1 | +# Copyright (c) 2024 Nordic Semiconductor ASA |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause |
| 4 | + |
| 5 | +import argparse |
| 6 | +import os |
| 7 | +import platform |
| 8 | +import re |
| 9 | +import shutil |
| 10 | +import stat |
| 11 | +import subprocess |
| 12 | +import tempfile |
| 13 | +import wget |
| 14 | + |
| 15 | +from pathlib import Path |
| 16 | +from typing import Tuple |
| 17 | +from zipfile import ZipFile |
| 18 | + |
| 19 | +from west import log |
| 20 | + |
| 21 | +MATTER_PATH = Path(__file__).parents[2] |
| 22 | + |
| 23 | + |
| 24 | +def find_zap(root: Path = Path.cwd(), max_depth: int = 1): |
| 25 | + """ |
| 26 | + Find *.zap file in the given directory or its subdirectories. |
| 27 | + """ |
| 28 | + subdirs = [] |
| 29 | + for name in root.iterdir(): |
| 30 | + if name.is_file() and (name.suffix.lower() == '.zap'): |
| 31 | + return root / name |
| 32 | + if name.is_dir() and (max_depth > 0): |
| 33 | + subdirs.append(name) |
| 34 | + for subdir in subdirs: |
| 35 | + if zap := find_zap(root / subdir, max_depth - 1): |
| 36 | + return zap |
| 37 | + return None |
| 38 | + |
| 39 | + |
| 40 | +def existing_file_path(arg: str) -> Path: |
| 41 | + """ |
| 42 | + Helper function to validate file path argument. |
| 43 | + """ |
| 44 | + p = Path(arg) |
| 45 | + if p.is_file(): |
| 46 | + return p |
| 47 | + raise argparse.ArgumentTypeError(f'invalid file path: \'{arg}\'') |
| 48 | + |
| 49 | + |
| 50 | +class ZapInstaller: |
| 51 | + INSTALL_DIR = Path('.zap-install') |
| 52 | + ZAP_URL_PATTERN = 'https://github.com/project-chip/zap/releases/download/v%04d.%02d.%02d-nightly/%s.zip' |
| 53 | + |
| 54 | + def __init__(self, matter_path: Path): |
| 55 | + self.matter_path = matter_path |
| 56 | + self.install_path = matter_path / ZapInstaller.INSTALL_DIR |
| 57 | + |
| 58 | + def unzip_darwin(zip: Path, out: Path): |
| 59 | + subprocess.check_call(['unzip', zip, '-d', out]) |
| 60 | + |
| 61 | + def unzip(zip: Path, out: Path): |
| 62 | + f = ZipFile(zip) |
| 63 | + f.extractall(out) |
| 64 | + f.close() |
| 65 | + |
| 66 | + current_os = platform.system() |
| 67 | + if current_os == 'Linux': |
| 68 | + self.package = 'zap-linux-x64' |
| 69 | + self.zap_exe = 'zap' |
| 70 | + self.zap_cli_exe = 'zap-cli' |
| 71 | + self.unzip = unzip |
| 72 | + elif current_os == 'Windows': |
| 73 | + self.package = 'zap-win-x64' |
| 74 | + self.zap_exe = 'zap.exe' |
| 75 | + self.zap_cli_exe = 'zap-cli.exe' |
| 76 | + self.unzip = unzip |
| 77 | + elif current_os == 'Darwin': |
| 78 | + self.package = 'zap-mac-x64' |
| 79 | + self.zap_exe = 'zap.app/Contents/MacOS/zap' |
| 80 | + self.zap_cli_exe = 'zap-cli' |
| 81 | + self.unzip = unzip_darwin |
| 82 | + else: |
| 83 | + raise RuntimeError(f"Unsupported platform: {current_os}") |
| 84 | + |
| 85 | + def get_install_path(self) -> Path: |
| 86 | + """ |
| 87 | + Returns ZAP package installation directory. |
| 88 | + """ |
| 89 | + return self.install_path |
| 90 | + |
| 91 | + def get_zap_path(self) -> Path: |
| 92 | + """ |
| 93 | + Returns path to ZAP GUI. |
| 94 | + """ |
| 95 | + return self.install_path / self.zap_exe |
| 96 | + |
| 97 | + def get_zap_cli_path(self) -> Path: |
| 98 | + """ |
| 99 | + Returns path to ZAP CLI. |
| 100 | + """ |
| 101 | + return self.install_path / self.zap_cli_exe |
| 102 | + |
| 103 | + def get_recommended_version(self) -> Tuple[int, int, int]: |
| 104 | + """ |
| 105 | + Returns ZAP package recommended version as a tuple of integers. |
| 106 | +
|
| 107 | + Parses zap_execution.py script from Matter SDK to determine the minimum |
| 108 | + required ZAP package version. |
| 109 | + """ |
| 110 | + RE_MIN_ZAP_VERSION = r'MIN_ZAP_VERSION\s*=\s*\'(\d+)\.(\d+)\.(\d+)' |
| 111 | + zap_execution_path = self.matter_path / 'scripts/tools/zap/zap_execution.py' |
| 112 | + |
| 113 | + with open(zap_execution_path, 'r') as f: |
| 114 | + if match := re.search(RE_MIN_ZAP_VERSION, f.read()): |
| 115 | + return tuple(int(group) for group in match.groups()) |
| 116 | + raise RuntimeError(f'Failed to find MIN_ZAP_VERSION in {zap_execution_path}') |
| 117 | + |
| 118 | + def get_current_version(self) -> Tuple[int, int, int]: |
| 119 | + """ |
| 120 | + Returns ZAP package current version as a tuple of integers. |
| 121 | +
|
| 122 | + Parses the output of `zap --version` to determine the current ZAP |
| 123 | + package version. If the ZAP package has not been installed yet, |
| 124 | + the method returns None. |
| 125 | + """ |
| 126 | + try: |
| 127 | + output = subprocess.check_output( |
| 128 | + [self.get_zap_path(), '--version']).decode('ascii').strip() |
| 129 | + except Exception: |
| 130 | + return None |
| 131 | + |
| 132 | + RE_VERSION = r'Version:\s*(\d+)\.(\d+)\.(\d+)' |
| 133 | + if match := re.search(RE_VERSION, output): |
| 134 | + return tuple(int(group) for group in match.groups()) |
| 135 | + |
| 136 | + raise RuntimeError("Failed to find version in ZAP output") |
| 137 | + |
| 138 | + def install_zap(self, version: Tuple[int, int, int]) -> None: |
| 139 | + """ |
| 140 | + Downloads and unpacks selected ZAP package version. |
| 141 | + """ |
| 142 | + with tempfile.TemporaryDirectory() as temp_dir: |
| 143 | + url = ZapInstaller.ZAP_URL_PATTERN % (*version, self.package) |
| 144 | + log.inf(f'Downloading {url}...') |
| 145 | + |
| 146 | + try: |
| 147 | + wget.download(url, out=temp_dir) |
| 148 | + except Exception as e: |
| 149 | + raise RuntimeError(f'Failed to download ZAP package from {url}: {e}') |
| 150 | + |
| 151 | + shutil.rmtree(self.install_path, ignore_errors=True) |
| 152 | + |
| 153 | + log.inf('') # Fix console after displaying wget progress bar |
| 154 | + log.inf(f'Unzipping ZAP package to {self.install_path}...') |
| 155 | + |
| 156 | + try: |
| 157 | + self.unzip(Path(temp_dir) / f'{self.package}.zip', self.install_path) |
| 158 | + except Exception as e: |
| 159 | + raise RuntimeError(f'Failed to unzip ZAP package: {e}') |
| 160 | + |
| 161 | + ZapInstaller.set_exec_permission(self.get_zap_path()) |
| 162 | + ZapInstaller.set_exec_permission(self.get_zap_cli_path()) |
| 163 | + |
| 164 | + def update_zap_if_needed(self) -> None: |
| 165 | + """ |
| 166 | + Installs ZAP package if not up to date. |
| 167 | +
|
| 168 | + Installs or overrides the previous ZAP package installation if the |
| 169 | + current version does not match the recommended version. |
| 170 | + """ |
| 171 | + recommended_version = self.get_recommended_version() |
| 172 | + current_version = self.get_current_version() |
| 173 | + |
| 174 | + if current_version == recommended_version: |
| 175 | + log.inf('ZAP is up to date: {0}.{1}.{2}'.format(*recommended_version)) |
| 176 | + return |
| 177 | + |
| 178 | + if current_version: |
| 179 | + log.inf('Found ZAP version: {0}.{1}.{2}'.format(*current_version)) |
| 180 | + |
| 181 | + log.inf('Installing ZAP version: {0}.{1}.{2}'.format(*recommended_version)) |
| 182 | + self.install_zap(recommended_version) |
| 183 | + |
| 184 | + @staticmethod |
| 185 | + def set_exec_permission(path: Path) -> None: |
| 186 | + os.chmod(path, os.stat(path).st_mode | stat.S_IEXEC) |
0 commit comments