blob: 6e48793b624c5cf8dba1562397cc0ec31e25cbea [file]
# 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.
from collections import defaultdict
from collections.abc import Iterator
import contextlib
import functools
import threading
import logging
import os
import shutil
import tarfile
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import StrEnum
from typing import Callable, Concatenate, ParamSpec, TypeVar
from runner.runner import Runner
class BuildProfile(StrEnum):
"""Build profile to use for the build."""
DEFAULT = "default" # Profile defined by the build system.
DEBUG = "debug" # Default debug profile.
DEBUG_OPTIMIZED = "debug-optimized" # Debug profile with optimizations enabled.
RELEASE = "release" # Release profile optimized for performance.
RELEASE_SIZE = "release-size" # Release profile optimized for size.
log = logging.getLogger(__name__)
@dataclass
class BuilderOptions:
# Build profile
build_profile: BuildProfile = BuildProfile.DEFAULT
# Generate a link map file
enable_link_map_file: bool = False
# Enable flashbundle generation stage
enable_flashbundle: bool = False
# Allow to wrap default build command
pw_command_launcher: str | None = None
# Locations where files are pre-generated
pregen_dir: str | None = None
@dataclass
class BuilderOutput:
source: str # Source file generated by the build
target: str # Target file to be copied to
class OutDirLock:
"""Lock to provide mutual exclusion for operations on output directories."""
def __init__(self) -> None:
self._dir_locks: dict[str, threading.RLock] = defaultdict(threading.RLock)
self._dict_lock: threading.Lock = threading.Lock()
@contextlib.contextmanager
def lock_dir(self, dir_path: str | None) -> Iterator[None]:
# No directory to lock, just yield
if dir_path is None:
yield
return
with self._dict_lock:
dir_lock = self._dir_locks[dir_path]
with dir_lock:
yield
S = TypeVar('S', bound='Builder')
P = ParamSpec('P')
R = TypeVar('R')
def lock_output_dir(func: Callable[Concatenate[S, P], R | Iterator[R]]) -> Callable[Concatenate[S, P], R | Iterator[R]]:
"""Decorator to wrap build steps with output directory lock."""
@functools.wraps(func)
def wrapper(self: S, *args: P.args, **kwargs: P.kwargs) -> R | Iterator[R]:
if self.output_dir_lock is None:
return func(self, *args, **kwargs)
output_dir_lock = self.output_dir_lock
# Keep the lock held while an iterator result is consumed. This is needed for build_outputs and bundle_outputs which are
# generators yielding BuilderOutput objects.
def iterator_with_lock(result: Iterator[R]) -> Iterator[R]:
with output_dir_lock.lock_dir(self.output_dir):
yield from result
with output_dir_lock.lock_dir(self.output_dir):
return iterator_with_lock(result) if isinstance(result := func(self, *args, **kwargs), Iterator) else result
return wrapper
class Builder(ABC):
"""Generic builder base class for CHIP.
Provides ability to bootstrap and copy output artifacts and subclasses can
use a generic shell runner.
"""
def __init__(self, root: str, runner: Runner, output_dir_lock: OutDirLock | None = None):
self.root = os.path.abspath(root)
self._runner = runner
self.output_dir_lock = output_dir_lock
# Set post-init once actual build target is known
self.identifier = None
self.output_dir = None
self.options = BuilderOptions()
self.quiet = False
self.verbose = False
@abstractmethod
def generate(self):
"""Generate the build files - generally the ninja/makefiles"""
raise NotImplementedError
@abstractmethod
def _build(self):
"""Perform an actual build"""
raise NotImplementedError
def _bundle(self):
"""Perform an actual generating of flashbundle.
May do nothing (and builder can choose not to implement this) if
the app does not need special steps for generating flashbundle (e.g.
on Linux platform, the output ELF files can be used directly).
"""
pass
@abstractmethod
def build_outputs(self):
"""Return a list of relevant BuilderOutput objects after a build.
May use build output data (e.g. manifests), so this should be
invoked only after a build has succeeded.
"""
raise NotImplementedError
def bundle_outputs(self):
"""Return the BuilderOutput objects in flashbundle.
Return an empty list (and builder can choose not to implement this)
if the app does not need special files as flashbundle.
May use data from _bundle(), so this should be invoked only after
_bundle() has succeeded.
"""
return []
def outputs(self):
outputs = list(self.build_outputs())
if self.options.enable_flashbundle:
outputs.extend(self.bundle_outputs())
return outputs
def build(self):
self._build()
if self.options.enable_flashbundle:
self._bundle()
def _Execute(self, cmdarray, title=None, dedup=False):
self._runner.Run(cmdarray, title=title, dedup=dedup, quiet=self.quiet)
def CompressArtifacts(self, target_file: str):
with tarfile.open(target_file, "w:gz") as tar:
for output in self.outputs():
log.info('Adding %s into %s(%s)',
output.source, target_file, output.target)
tar.add(output.source, output.target)
def CopyArtifacts(self, target_dir: str):
for output in self.outputs():
log.info(f'Copying {output.source} into {output.target}')
target_full_name = os.path.join(target_dir, output.target)
target_dir_full_name = os.path.dirname(target_full_name)
if not os.path.exists(target_dir_full_name):
log.info('Creating subdirectory %s first',
target_dir_full_name)
os.makedirs(target_dir_full_name)
shutil.copyfile(output.source, target_full_name)
shutil.copymode(output.source, target_full_name)