blob: bd1207521648950a35010b6c1a2e1ff182afa3fc [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright (c) 2021 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import argparse
import json
import os
from pathlib import Path
import tempfile
import subprocess
import sys
import urllib.request
from dataclasses import dataclass
@dataclass
class CmdLineArgs:
zapFile: str
zclFile: str
templateFile: str
outputDir: str
runBootstrap: bool
parallel: bool = True
CHIP_ROOT_DIR = os.path.realpath(
os.path.join(os.path.dirname(__file__), '../../..'))
def checkPythonVersion():
if sys.version_info[0] < 3:
print('Must use Python 3. Current version is ' +
str(sys.version_info[0]))
exit(1)
def checkFileExists(path):
if not os.path.isfile(path):
print('Error: ' + path + ' does not exists or is not a file.')
exit(1)
def checkDirExists(path):
if not os.path.isdir(path):
print('Error: ' + path + ' does not exists or is not a directory.')
exit(1)
def getFilePath(name):
fullpath = os.path.join(CHIP_ROOT_DIR, name)
checkFileExists(fullpath)
return fullpath
def getDirPath(name):
fullpath = os.path.join(CHIP_ROOT_DIR, name)
checkDirExists(fullpath)
return fullpath
def detectZclFile(zapFile):
print(f"Searching for zcl file from {zapFile}")
path = 'src/app/zap-templates/zcl/zcl.json'
data = json.load(open(zapFile))
for package in data["package"]:
if package["type"] != "zcl-properties":
continue
# found the right path, try to figure out the actual path
if package["pathRelativity"] == "relativeToZap":
path = os.path.abspath(os.path.join(os.path.dirname(zapFile), package["path"]))
else:
path = package["path"]
return getFilePath(path)
def runArgumentsParser() -> CmdLineArgs:
default_templates = 'src/app/zap-templates/app-templates.json'
default_output_dir = 'zap-generated/'
parser = argparse.ArgumentParser(
description='Generate artifacts from .zapt templates')
parser.add_argument('zap', help='Path to the application .zap file')
parser.add_argument('-t', '--templates', default=default_templates,
help='Path to the .zapt templates records to use for generating artifacts (default: "' + default_templates + '")')
parser.add_argument('-z', '--zcl',
help='Path to the zcl templates records to use for generating artifacts (default: autodetect read from zap file)')
parser.add_argument('-o', '--output-dir', default=None,
help='Output directory for the generated files (default: automatically selected)')
parser.add_argument('--run-bootstrap', default=None, action='store_true',
help='Automatically run ZAP bootstrap. By default the bootstrap is not triggered')
parser.add_argument('--parallel', action='store_true')
parser.add_argument('--no-parallel', action='store_false', dest='parallel')
parser.set_defaults(parallel=True)
args = parser.parse_args()
# By default, this script assumes that the global CHIP template is used with
# a default 'zap-generated/' output folder relative to APP_ROOT_DIR.
# If needed, the user may specify a specific template as a second argument. In
# this case the output folder is relative to CHIP_ROOT_DIR.
if args.output_dir:
output_dir = args.output_dir
elif args.templates == default_templates:
output_dir = os.path.join(Path(args.zap).parent, default_output_dir)
else:
output_dir = ''
zap_file = getFilePath(args.zap)
if args.zcl:
zcl_file = getFilePath(args.zcl)
else:
zcl_file = detectZclFile(zap_file)
templates_file = getFilePath(args.templates)
output_dir = getDirPath(output_dir)
return CmdLineArgs(zap_file, zcl_file, templates_file, output_dir, args.run_bootstrap)
def extractGeneratedIdl(output_dir, zap_config_path):
"""Find a file Clusters.matter in the output directory and
place it along with the input zap file.
Intent is to make the "zap content" more humanly understandable.
"""
idl_path = os.path.join(output_dir, "Clusters.matter")
if not os.path.exists(idl_path):
return
target_path = zap_config_path.replace(".zap", ".matter")
if not target_path.endswith(".matter"):
# We expect "something.zap" and don't handle corner cases of
# multiple extensions. This is to work with existing codebase only
raise Error("Unexpected input zap file %s" % self.zap_config)
os.rename(idl_path, target_path)
def runGeneration(zap_file, zcl_file, templates_file, output_dir):
# Accepted environment variables, in order:
#
# ZAP_DEVELOPMENT_PATH - the path to a zap development environment. This is
# a zap checkout, used for local development
# ZAP_INSTALL_PATH - the path where zap-cli exists. This is if zap-cli
# is NOT in the current path
if 'ZAP_DEVELOPMENT_PATH' in os.environ:
generate_cmd = ['node', 'src-script/zap-start.js', 'generate']
working_directory = os.environ['ZAP_DEVELOPMENT_PATH']
elif 'ZAP_INSTALL_PATH' in os.environ:
generate_cmd = [os.path.join(os.environ['ZAP_INSTALL_PATH'], 'zap-cli'), 'generate']
working_directory = None
else:
generate_cmd = ['zap-cli', 'generate']
working_directory = None
try:
subprocess.check_call(generate_cmd + ['-z', zcl_file, '-g', templates_file,
'-i', zap_file, '-o', output_dir], cwd=working_directory)
except FileNotFoundError as e:
print(f'FAILED TO EXECUTE ZAP GENERATION: {e.strerror} - "{e.filename}"')
print('*'*80)
print('* You may need to install zap. Please ensure one of these applies:')
print('* - `zap-cli` is in $PATH. Install from https://github.com/project-chip/zap/releases')
print('* see docs/guides/BUILDING.md for details')
print('* - `zap-cli` is in $ZAP_INSTALL_PATH. Use this option if you')
print('* installed zap but do not want to update $PATH')
print('* - Point $ZAP_DEVELOPMENT_PATH to your local copy of zap that you')
print('* develop on (to use a developer build of zap)')
print('*'*80)
sys.exit(1)
extractGeneratedIdl(output_dir, zap_file)
def runClangPrettifier(templates_file, output_dir):
listOfSupportedFileExtensions = [
'.js', '.h', '.c', '.hpp', '.cpp', '.m', '.mm']
try:
jsonData = json.loads(Path(templates_file).read_text())
outputs = [(os.path.join(output_dir, template['output']))
for template in jsonData['templates']]
clangOutputs = list(filter(lambda filepath: os.path.splitext(
filepath)[1] in listOfSupportedFileExtensions, outputs))
if len(clangOutputs) > 0:
# The "clang-format" pigweed comes with is now version 14, which
# changed behavior from version 13 and earlier regarding some
# whitespace formatting. Unfortunately, all the CI bits run
# clang-format 13 or earlier, so we get styling mismatches.
#
# Try some older clang-format versions just in case they are
# installed. In particular, clang-format-13 is available on various
# Linux distributions and clang-format-11 is available via homebrew
# on Mac. If all else fails, fall back to clang-format.
clang_formats = ['clang-format-13', 'clang-format-12', 'clang-format-11', 'clang-format']
for clang_format in clang_formats:
args = [clang_format, '-i']
args.extend(clangOutputs)
try:
subprocess.check_call(args)
err = None
break
except Exception as thrown:
err = thrown
# Press on to the next binary name
if err is not None:
raise err
except Exception as err:
print('clang-format error:', err)
def runJavaPrettifier(templates_file, output_dir):
try:
jsonData = json.loads(Path(templates_file).read_text())
outputs = [(os.path.join(output_dir, template['output']))
for template in jsonData['templates']]
javaOutputs = list(
filter(lambda filepath: os.path.splitext(filepath)[1] == ".java", outputs))
if len(javaOutputs) > 0:
# Keep this version in sync with what restyler uses (https://github.com/project-chip/connectedhomeip/blob/master/.restyled.yaml).
google_java_format_version = "1.6"
google_java_format_url = 'https://github.com/google/google-java-format/releases/download/google-java-format-' + \
google_java_format_version + '/'
google_java_format_jar = 'google-java-format-' + \
google_java_format_version + '-all-deps.jar'
jar_url = google_java_format_url + google_java_format_jar
home = str(Path.home())
path, http_message = urllib.request.urlretrieve(
jar_url, home + '/' + google_java_format_jar)
args = ['java', '-jar', path, '--replace']
args.extend(javaOutputs)
subprocess.check_call(args)
except Exception as err:
print('google-java-format error:', err)
def main():
checkPythonVersion()
cmdLineArgs = runArgumentsParser()
if cmdLineArgs.runBootstrap:
subprocess.check_call(getFilePath("scripts/tools/zap/zap_bootstrap.sh"), shell=True)
# The maximum memory usage is over 4GB (#15620)
os.environ["NODE_OPTIONS"] = "--max-old-space-size=8192"
if cmdLineArgs.parallel:
# Parallel-compatible runs will need separate state
os.environ["ZAP_TEMPSTATE"] = "1"
# `zap-cli` may extract things into a temporary directory. ensure extraction
# does not conflict.
with tempfile.TemporaryDirectory(prefix='zap') as temp_dir:
old_temp = os.environ['TEMP'] if 'TEMP' in os.environ else None
os.environ['TEMP'] = temp_dir
runGeneration(cmdLineArgs.zapFile, cmdLineArgs.zclFile, cmdLineArgs.templateFile, cmdLineArgs.outputDir)
if old_temp:
os.environ['TEMP'] = old_temp
else:
del os.environ['TEMP']
prettifiers = [
runClangPrettifier,
runJavaPrettifier,
]
for prettifier in prettifiers:
prettifier(cmdLineArgs.templateFile, cmdLineArgs.outputDir)
if __name__ == '__main__':
main()