blob: 8d281906d633d2ed501783a0577ca7e35c8f130c [file] [log] [blame]
# Copyright (c) 2022 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
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.
# Build targets are generally of the form `target + modifiers`
# - `target` defines the platform-specific application to build. It is often
# in the form of `platform-board-app` but may be `platform-app` as well
# - `modifiers` are additional compilation options like disabling BLE, enabling
# coverage or other options that are generally passed into gn configurations
# Examples:
# - linux-x64-chip-tool: this is a 'chip-tool' build for a 64-bit linux
# - linux-x64-chip-tool-noble-coverage: a chip tool build with modifiers attached of
# "noble" and "coverage"
# - qpg-light: a 'light' app built for qpg
# - imx-thermostat-release: a 'thermostat` build for imx, with a `release` modifier applied
# Interpretation of a specific string of `a-b-c-d` is left up to the Builder/platform
# as there is no direct convention at this time that an application/variant may not contain
# a `-`. So `a-b-c-d` may for example mean any of:
# - platform 'a', application 'b', modifiers 'c' and 'd'
# - platform 'a', board 'b', application 'c-d'
# - platform 'a', application 'b', modifier 'c-d'
# - platform 'a', application 'b-c', modifier 'd'
# The only requirement of the build system is that a single string corresponds to a single
# combination of platform/board/app/modifier(s). This requirement is unenfornced in code
# but easy enough to follow when defining names for things: just don't reuse names between '-'
import itertools
import logging
import os
import re
from dataclasses import dataclass
from typing import Any, Dict, List, Iterable, Optional
from builders.builder import BuilderOptions
report_rejected_parts = True
class TargetPart:
# SubTarget/Modifier name
name: str
# The build arguments to apply to a builder if this part is active
build_arguments: Dict[str, Any]
# Part should be included if and only if the final string MATCHES the
# given regular expression
only_if_re: Optional[re.Pattern] = None
# Part should be included if and only if the final string DOES NOT match
# given regular expression
except_if_re: Optional[re.Pattern] = None
def __init__(self, name, **kargs): = name.lower()
self.build_arguments = kargs
def OnlyIfRe(self, expr: str):
self.only_if_re = re.compile(expr)
return self
def ExceptIfRe(self, expr: str):
self.except_if_re = re.compile(expr)
return self
def Accept(self, full_input: str):
if self.except_if_re:
if report_rejected_parts:
# likely nothing will match when we get such an error
logging.error(f"'{}' does not support '{full_input}' due to rule EXCEPT IF '{self.except_if_re.pattern}'")
return False
if self.only_if_re:
if not
if report_rejected_parts:
# likely nothing will match when we get such an error
logging.error(f"'{}' does not support '{full_input}' due to rule ONLY IF '{self.only_if_re.pattern}'")
return False
return True
def _HasVariantPrefix(value: str, prefix: str):
"""Checks if the given value is <prefix> or starts with "<prefix>-".
This is useful when considering '-'-delimited strings, where a specific
prefix may either be the last element in the list of items or some first element
out of several.
None if no match or the remaining value if there is a match.
_HasVariantPrefix('foo', 'foo') # -> ''
_HasVariantPrefix('foo', 'bar') # -> None
_HasVariantPrefix('foo-bar', 'foo') # -> 'bar'
_HasVariantPrefix('foo-bar', 'bar') # -> None
_HasVariantPrefix('foo-bar-baz', 'foo') # -> 'bar-baz'
_HasVariantPrefix('foo-bar-baz', 'bar') # -> None
if value == prefix:
return ''
if value.startswith(prefix + '-'):
return value[len(prefix)+1:]
def _StringIntoParts(full_input: str, remaining_input: str, fixed_targets: List[List[TargetPart]], modifiers: List[TargetPart]):
"""Given an input string, process through all the input rules and return
the underlying list of target parts for the input.
full_input: the full input string, used for validity matching (except/only_if)
remaining_input: the remaining input to parse
fixed_targets: the remaining fixed targets left to match
modifiers: the modifiers left to match
if not remaining_input:
if fixed_targets:
# String was not fully matched. Fixed thargets are required
return None
# Fully parsed
return []
if fixed_targets:
# If fixed targets remain, we MUST match one of them
for target in fixed_targets[0]:
suffix = _HasVariantPrefix(remaining_input,
if suffix is None:
# see if match should be rejected. Done AFTER variant prefix detection so we
# can log if there are issues
if not target.Accept(full_input):
result = _StringIntoParts(full_input, suffix, fixed_targets[1:], modifiers)
if result is not None:
return [target] + result
# None of the variants matched
return None
# Only modifiers left to process
# Process the modifiers one by one
for modifier in modifiers:
suffix = _HasVariantPrefix(remaining_input,
if suffix is None:
# see if match should be rejected. Done AFTER variant prefix detection so we
# can log if there are issues
if not modifier.Accept(full_input):
result = _StringIntoParts(full_input, suffix, fixed_targets[1:], [x for x in modifiers if x != modifier])
if result is not None:
return [modifier] + result
# Remaining input is not empty and we failed to match it
return None
class BuildTarget:
def __init__(self, name, builder_class, **kwargs):
""" Sets up a new build tareget starting with the given builder class
and initial arguments
""" = name.lower()
self.builder_class = builder_class
self.create_kw_args = kwargs
# a list of sub_targets for this builder
# sub-targets MUST be selected in some way. For example for esp32, we may
# have a format of esp32-{devkitc, m5stack}-{light,lock}:
# - esp32-m5stack-lock is OK
# - esp32-devkitc-light is OK
# - esp32-light is NOT ok
# - esp32-m5stack is NOT ok
self.fixed_targets: List[List[TargetPart]] = []
# a list of all available modifiers for this build target
# Modifiers can be combined in any way
self.modifiers: List[TargetPart] = []
def AppendFixedTargets(self, parts: List[TargetPart]):
"""Append a list of potential targets/variants.
target = BuildTarget('linux', LinuxBuilder)
TargetPart(name='x64', board=HostBoard.X64),
TargetPart(name='x86', board=HostBoard.X86),
TargetPart(name='arm64', board=HostBoard.ARM64),
TargetPart(name='light', app=HostApp.LIGHT),
TargetPart(name='lock', app=HostApp.LIGHT).ExceptIfRe("-arm64-"),
TargetPart(name='shell', app=HostApp.LIGHT).OnlyIfRe("-(x64|arm64)-"),
The above will accept:
def AppendModifier(self, name: str, **kargs):
"""Appends a specific modifier to a build target. For example:
target.AppendModifier(name='release', release=True)
target.AppendModifier(name='clang', use_clang=True)
target.AppendModifier(name='coverage', coverage=True).OnlyIfRe('-clang')
part = TargetPart(name, **kargs)
return part
def HumanString(self):
"""Prints out the human-readable string of the available variants and modifiers:
result =
for fixed in self.fixed_targets:
if len(fixed) > 1:
result += '-{' + ",".join(map(lambda x:, fixed)) + '}'
result += '-' + fixed[0].name
for modifier in self.modifiers:
result += f"[-{}]"
return result
def AllVariants(self) -> Iterable[str]:
"""Returns all possible accepted variants by this target.
For example name-{a,b}-{c,d}[-1][-2] could return (there may be Only/ExceptIfRe rules):
Notice that this DOES increase exponentially and is potentially a very long list
# Output is made out of 2 separate parts:
# - a valid combination of "fixed parts"
# - a combination of modifiers
fixed_indices = [0]*len(self.fixed_targets)
while True:
prefix = "-".join(map(
lambda p: self.fixed_targets[p[0]][p[1]].name, enumerate(fixed_indices)
for n in range(len(self.modifiers) + 1):
for c in itertools.combinations(self.modifiers, n):
suffix = ""
for m in c:
suffix += "-" +
option = f"{}-{prefix}{suffix}"
if self.StringIntoTargetParts(option) is not None:
yield option
# Move to the next index in fixed_indices or exit loop if we cannot move
move_idx = len(fixed_indices) - 1
while move_idx >= 0:
if fixed_indices[move_idx] + 1 < len(self.fixed_targets[move_idx]):
fixed_indices[move_idx] += 1
# need to move the previous value
fixed_indices[move_idx] = 0
move_idx -= 1
if move_idx < 0:
# done iterating through all
def StringIntoTargetParts(self, value: str):
"""Given an input string, process through all the input rules and return
the underlying list of target parts for the input.
suffix = _HasVariantPrefix(value,
if not suffix:
return None
return _StringIntoParts(value, suffix, self.fixed_targets, self.modifiers)
def Create(self, name: str, runner, repository_path: str, output_prefix: str,
builder_options: BuilderOptions):
parts = self.StringIntoTargetParts(name)
if not parts:
return None
kargs = {}
for part in parts:
kargs.update(part.build_arguments)"Preparing builder '%s'" % (name,))
builder = self.builder_class(repository_path, runner=runner, **kargs) = self
builder.identifier = name
builder.output_dir = os.path.join(output_prefix, name)
builder.chip_dir = repository_path
builder.options = builder_options
return builder