blob: 39c468e127d96de318f005c34bd15d40f37f975d [file]
# Copyright (c) 2023 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.
# This file contains private utilities for use by the `configure` script.
# It is self-contained and depends only on the Python 3 standard library.
#
# Some unit tests that can be run manually are in configure.impl.test.py.
import collections
import json
import os
import re
import sys
import urllib.request
import zipfile
def download(url, dest):
urllib.request.urlretrieve(url, dest)
def download_and_extract_zip(url, dest_dir, *member_names):
file, *_ = urllib.request.urlretrieve(url)
with zipfile.ZipFile(file) as zip_file:
for member in zip_file.infolist():
if member.filename in member_names:
zip_file.extract(member, dest_dir)
def process_project_args(gn_args_json_file, *params):
processor = ProjectArgProcessor(gn_args_json_file)
processor.process_env()
processor.process_parameters(params)
for arg, value in processor.args.items():
info(" - %s = %s" % (arg, value))
print("%s = %s" % (arg, value))
def describe_args(gn_args_json_file):
processor = ProjectArgProcessor(gn_args_json_file)
# Header / Built-ins handled by process_triplet_parameter
print("Available configure options and default values:\n"
"\n"
"### Target Options\n"
"\n"
"--build=...\n"
" # host_cpu, host_os: GN built-in\n"
" # The system on which the software is being built, as cpu-os or cpu-vendor-os[-abi]\n"
"\n"
"--host=...\n"
"--target=...\n"
" # target_cpu, target_os: GN built-in\n"
" # The system on which the software will run, as cpu-os or cpu-vendor-os[-abi]\n"
)
def describe_group(group, heading):
if not group:
return
print(f"### {heading}\n")
for arg in sorted(group, key=lambda a: a.alias):
if arg.type == 'b':
print(f"--enable-{arg.alias}{'=no' if arg.default == 'false' else ''}")
else:
# Abbreviate long list / structs
default = arg.default if len(arg.default) <= 40 \
else '[...]' if arg.type == '[' \
else '{...}' if arg.type == '{' \
else arg.default
print(f"--{arg.alias}={default}")
if arg.source:
print(f" # {arg.name}: {arg.source}")
if arg.comment:
print(" #", arg.comment.replace("\n", "\n # "))
print()
project, sdk, third_party, overrides = [], [], [], []
for arg in processor.gn_args.values():
if (source := arg.source) and arg.alias:
group = third_party if source.startswith('//third_party/connectedhomeip/third_party/') \
else sdk if source.startswith('//third_party/connectedhomeip/') \
else third_party if source.startswith('//third_party/') \
else overrides if source.startswith('//build_overrides/') \
else project
group.append(arg)
describe_group(project, "Project Options")
describe_group(sdk, "Matter SDK Options")
describe_group(overrides, "Build Override Paths")
describe_group(third_party, "Third-Party Options")
class ProjectArgProcessor:
class GnArg:
# Prefixes for mapping between configure parameters and GN arguments (longer prefixes first)
BOOL_ARG_PREFIXES = ('is_', 'enable_', 'use_',
'chip_enable_', 'chip_use_', 'chip_config_', 'chip_',
'matter_enable_', 'matter_use_', 'matter_config_', 'matter_', '')
GENERIC_ARG_PREFIXES = ('chip_config_', 'chip_', 'matter_config_', 'matter_', '')
ARG_TYPE_TRANS = str.maketrans('"tf0123456789-', 'sbbiiiiiiiiiii')
DASH_TRANS = str.maketrans('_', '-')
def __init__(self, data):
self.name = str(data['name'])
self.alias = None # will be set below
self.default = str(data['default']['value'])
self.type = self.default[0].translate(self.ARG_TYPE_TRANS) # 's'tring, 'b'ool, 'i'integer, '[' list, '{' struct
file = data['default'].get('file', None)
self.source = '%s:%d' % (file, data['default']['line']) if file else None
self.comment = str(data.get('comment', '')).strip()
def alias_candidates(self):
prefixes = self.BOOL_ARG_PREFIXES if self.type == 'b' else self.GENERIC_ARG_PREFIXES
return (self.name.removeprefix(p).lower().translate(self.DASH_TRANS) for p in prefixes if self.name.startswith(p))
def __str__(self):
return self.name
args: collections.OrderedDict # collected arguments
gn_args: dict[str, GnArg] # by GN name
options: dict[str, GnArg] # by all unique option aliases
def __init__(self, gn_args_json_file):
self.args = collections.OrderedDict()
# Parse `gn args --list --json` output into gn_args
with open(gn_args_json_file) as fh:
self.gn_args = {arg.name: arg for arg in (self.GnArg(rec) for rec in json.load(fh))}
# Count collisions first then index options by all unique aliases
counts = collections.defaultdict(int)
for arg in self.gn_args.values():
for alias in arg.alias_candidates():
counts[alias] += 1
self.options = {}
for arg in self.gn_args.values():
for alias in arg.alias_candidates():
if counts[alias] <= 1:
self.options[alias] = arg
arg.alias = arg.alias or alias # canonical (shortest unique) alias
def process_env(self):
self.add_env_arg('chip_code_pre_generated_directory', 'CHIP_PREGEN_DIR')
self.add_arg('custom_toolchain', 'custom', required=False)
self.add_env_arg('target_cc', 'CC', 'cc')
self.add_env_arg('target_cxx', 'CXX', 'cxx')
self.add_env_arg('target_ar', 'AR', 'ar')
self.add_env_arg('target_cflags', 'CPPFLAGS', is_list=True)
self.add_env_arg('target_cflags_c', 'CFLAGS', is_list=True)
self.add_env_arg('target_cflags_cc', 'CXXFLAGS', is_list=True)
self.add_env_arg('target_cflags_objc', 'OBJCFLAGS', is_list=True)
self.add_env_arg('target_ldflags', 'LDFLAGS', is_list=True)
def add_arg(self, nameOrArg, value, required=True):
if arg := self.gn_args.get(nameOrArg, None) if isinstance(nameOrArg, str) else nameOrArg:
# format strings and booleans as JSON, otherwise pass through as is
self.args[arg.name] = (json.dumps(value) if arg.type in 'sb' else value)
elif required:
fail("Project has no build arg '%s'" % nameOrArg)
def add_env_arg(self, name, envvar, default=None, is_list=False):
"""Add an argument from an environment variable"""
if not (value := os.environ.get(envvar, default)):
return
if not (arg := self.gn_args.get(name, None)):
info("Warning: Not propagating %s, project has no build arg '%s'" % (envvar, name))
return
# bypass add_arg() to handle list splitting / formatting directly
self.args[arg.name] = json.dumps(value if arg.type != '[' else value.split() if is_list else [value])
def process_triplet_parameter(self, name, value):
if value is None:
fail("Project option --%s requires an argument" % name)
triplet = value.split('-')
if len(triplet) not in (2, 3, 4):
fail("Project option --%s argument must be a cpu-os pair or cpu-vendor-os[-abi] triplet" % name)
prefix = 'host_' if name == 'build' else 'target_' # "host" has different meanings in GNU and GN!
self.add_arg(prefix + 'cpu', triplet[0])
self.add_arg(prefix + 'os', triplet[1 if len(triplet) == 2 else 2])
def option(self, option):
"""Returns the GnArg for the given option name, or None"""
if not (arg := self.options.get(option, None)):
info("Warning: Project has no build option '%s'" % option)
return arg
def process_enable_parameter(self, option, value):
if not (arg := self.option(option.removeprefix('enable-'))):
return
if arg.type != 'b':
fail("Project build arg '%s' is not a boolean" % arg)
if value != 'no' and value is not None:
fail("Invalid argument for --%s, must be absent or 'no'" % option)
self.add_arg(arg, value is None)
def process_generic_parameter(self, option, value):
if not (arg := self.option(option)):
return
if arg.type == 'b':
fail("Project build arg '%s' is a boolean, use --enable-..." % arg)
if value is None:
fail("Project option --%s requires an argument" % option)
self.add_arg(arg, value)
def process_parameter(self, option, value):
if option in ('build', 'host', 'target'):
self.process_triplet_parameter(option, value)
elif option.startswith('enable-'):
self.process_enable_parameter(option, value)
else:
self.process_generic_parameter(option, value)
def process_parameters(self, params):
"""Process GNU-style configure command line parameters"""
for param in params:
match = re.fullmatch(r'--([a-z][a-z0-9-]*)(?:=(.*))?', param)
if not match:
fail("Invalid argument: '%s'" % param)
self.process_parameter(match.group(1), match.group(2))
def info(message):
print(message, file=sys.stderr)
def fail(message):
info("Error: " + message)
sys.exit(1)
if __name__ == "__main__":
# `configure` invokes the top-level functions in this file by
# passing the function name and arguments on the command line.
[_, func, *args] = sys.argv
globals()[func](*args)