| # Copyright 2022 The Pigweed 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 |
| # |
| # https://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. |
| """Creates a new Pigweed module.""" |
| |
| import abc |
| import argparse |
| import dataclasses |
| from dataclasses import dataclass |
| import datetime |
| import logging |
| import os |
| from pathlib import Path |
| import re |
| import sys |
| |
| from typing import Any, Dict, Iterable, List, Optional, Type, Union |
| |
| from pw_build import generate_modules_lists |
| |
| _LOG = logging.getLogger(__name__) |
| |
| _PIGWEED_LICENSE = \ |
| f"""# Copyright {datetime.datetime.now().year} The Pigweed 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 |
| # |
| # https://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.""" |
| |
| _PIGWEED_LICENSE_CC = _PIGWEED_LICENSE.replace('#', '//') |
| |
| |
| # TODO(frolv): Adapted from pw_protobuf. Consolidate them. |
| class _OutputFile: |
| DEFAULT_INDENT_WIDTH = 2 |
| |
| def __init__(self, file: Path, indent_width: int = DEFAULT_INDENT_WIDTH): |
| self._file = file |
| self._content: List[str] = [] |
| self._indent_width: int = indent_width |
| self._indentation = 0 |
| |
| def line(self, line: str = '') -> None: |
| if line: |
| self._content.append(' ' * self._indentation) |
| self._content.append(line) |
| self._content.append('\n') |
| |
| def indent( |
| self, |
| width: int = None, |
| ) -> '_OutputFile._IndentationContext': |
| """Increases the indentation level of the output.""" |
| return self._IndentationContext( |
| self, width if width is not None else self._indent_width) |
| |
| @property |
| def path(self) -> Path: |
| return self._file |
| |
| @property |
| def content(self) -> str: |
| return ''.join(self._content) |
| |
| def write(self) -> None: |
| print(' create ' + str(self._file.relative_to(Path.cwd()))) |
| self._file.write_text(self.content) |
| |
| class _IndentationContext: |
| """Context that increases the output's indentation when it is active.""" |
| def __init__(self, output: '_OutputFile', width: int): |
| self._output = output |
| self._width: int = width |
| |
| def __enter__(self): |
| self._output._indentation += self._width |
| |
| def __exit__(self, typ, value, traceback): |
| self._output._indentation -= self._width |
| |
| |
| class _ModuleName: |
| _MODULE_NAME_REGEX = '(^[a-zA-Z]{2,})((_[a-zA-Z0-9]+)+)$' |
| |
| def __init__(self, prefix: str, main: str) -> None: |
| self._prefix = prefix |
| self._main = main |
| |
| @property |
| def full(self) -> str: |
| return f'{self._prefix}_{self._main}' |
| |
| @property |
| def prefix(self) -> str: |
| return self._prefix |
| |
| @property |
| def main(self) -> str: |
| return self._main |
| |
| @property |
| def default_namespace(self) -> str: |
| return f'{self._prefix}::{self._main}' |
| |
| def upper_camel_case(self) -> str: |
| return ''.join(s.capitalize() for s in self._main.split('_')) |
| |
| def __str__(self) -> str: |
| return self.full |
| |
| def __repr__(self) -> str: |
| return self.full |
| |
| @classmethod |
| def parse(cls, name: str) -> Optional['_ModuleName']: |
| match = re.fullmatch(_ModuleName._MODULE_NAME_REGEX, name) |
| if not match: |
| return None |
| |
| return cls(match.group(1), match.group(2)[1:]) |
| |
| |
| @dataclass |
| class _ModuleContext: |
| name: _ModuleName |
| dir: Path |
| root_build_files: List['_BuildFile'] |
| sub_build_files: List['_BuildFile'] |
| build_systems: List[str] |
| is_upstream: bool |
| |
| def build_files(self) -> Iterable['_BuildFile']: |
| yield from self.root_build_files |
| yield from self.sub_build_files |
| |
| def add_docs_file(self, file: Path): |
| for build_file in self.root_build_files: |
| build_file.add_docs_source(str(file.relative_to(self.dir))) |
| |
| def add_cc_target(self, target: '_BuildFile.CcTarget') -> None: |
| for build_file in self.root_build_files: |
| build_file.add_cc_target(target) |
| |
| def add_cc_test(self, target: '_BuildFile.CcTarget') -> None: |
| for build_file in self.root_build_files: |
| build_file.add_cc_test(target) |
| |
| |
| class _BuildFile: |
| """Abstract representation of a build file for a module.""" |
| @dataclass |
| class Target: |
| name: str |
| |
| # TODO(frolv): Shouldn't be a string list as that's build system |
| # specific. Figure out a way to resolve dependencies from targets. |
| deps: List[str] = dataclasses.field(default_factory=list) |
| |
| @dataclass |
| class CcTarget(Target): |
| sources: List[Path] = dataclasses.field(default_factory=list) |
| headers: List[Path] = dataclasses.field(default_factory=list) |
| |
| def rebased_sources(self, rebase_path: Path) -> Iterable[str]: |
| return (str(src.relative_to(rebase_path)) for src in self.sources) |
| |
| def rebased_headers(self, rebase_path: Path) -> Iterable[str]: |
| return (str(hdr.relative_to(rebase_path)) for hdr in self.headers) |
| |
| def __init__(self, path: Path, ctx: _ModuleContext): |
| self._path = path |
| self._ctx = ctx |
| |
| self._docs_sources: List[str] = [] |
| self._cc_targets: List[_BuildFile.CcTarget] = [] |
| self._cc_tests: List[_BuildFile.CcTarget] = [] |
| |
| @property |
| def path(self) -> Path: |
| return self._path |
| |
| @property |
| def dir(self) -> Path: |
| return self._path.parent |
| |
| def add_docs_source(self, filename: str) -> None: |
| self._docs_sources.append(filename) |
| |
| def add_cc_target(self, target: CcTarget) -> None: |
| self._cc_targets.append(target) |
| |
| def add_cc_test(self, target: CcTarget) -> None: |
| self._cc_tests.append(target) |
| |
| def write(self) -> None: |
| """Writes the contents of the build file to disk.""" |
| file = _OutputFile(self._path, self._indent_width()) |
| |
| if self._ctx.is_upstream: |
| file.line(_PIGWEED_LICENSE) |
| file.line() |
| |
| self._write_preamble(file) |
| |
| for target in self._cc_targets: |
| file.line() |
| self._write_cc_target(file, target) |
| |
| for target in self._cc_tests: |
| file.line() |
| self._write_cc_test(file, target) |
| |
| if self._docs_sources: |
| file.line() |
| self._write_docs_target(file, self._docs_sources) |
| |
| file.write() |
| |
| @abc.abstractmethod |
| def _indent_width(self) -> int: |
| """Returns the default indent width for the build file's code style.""" |
| |
| @abc.abstractmethod |
| def _write_preamble(self, file: _OutputFile) -> None: |
| """Formats""" |
| |
| @abc.abstractmethod |
| def _write_cc_target( |
| self, |
| file: _OutputFile, |
| target: '_BuildFile.CcTarget', |
| ) -> None: |
| """Defines a C++ library target within the build file.""" |
| |
| @abc.abstractmethod |
| def _write_cc_test( |
| self, |
| file: _OutputFile, |
| target: '_BuildFile.CcTarget', |
| ) -> None: |
| """Defines a C++ unit test target within the build file.""" |
| |
| @abc.abstractmethod |
| def _write_docs_target( |
| self, |
| file: _OutputFile, |
| docs_sources: List[str], |
| ) -> None: |
| """Defines a documentation target within the build file.""" |
| |
| |
| # TODO(frolv): The Dict here should be Dict[str, '_GnVal'] (i.e. _GnScope), |
| # but mypy does not yet support recursive types: |
| # https://github.com/python/mypy/issues/731 |
| _GnVal = Union[bool, int, str, List[str], Dict[str, Any]] |
| _GnScope = Dict[str, _GnVal] |
| |
| |
| class _GnBuildFile(_BuildFile): |
| _DEFAULT_FILENAME = 'BUILD.gn' |
| _INCLUDE_CONFIG_TARGET = 'public_include_path' |
| |
| def __init__(self, |
| directory: Path, |
| ctx: _ModuleContext, |
| filename: str = _DEFAULT_FILENAME): |
| super().__init__(directory / filename, ctx) |
| |
| def _indent_width(self) -> int: |
| return 2 |
| |
| def _write_preamble(self, file: _OutputFile) -> None: |
| # Upstream modules always require a tests target, even if it's empty. |
| has_tests = len(self._cc_tests) > 0 or self._ctx.is_upstream |
| |
| imports = [] |
| |
| if self._cc_targets: |
| imports.append('$dir_pw_build/target_types.gni') |
| |
| if has_tests: |
| imports.append('$dir_pw_unit_test/test.gni') |
| |
| if self._docs_sources: |
| imports.append('$dir_pw_docgen/docs.gni') |
| |
| file.line('import("//build_overrides/pigweed.gni")\n') |
| for imp in sorted(imports): |
| file.line(f'import("{imp}")') |
| |
| if self._cc_targets: |
| file.line() |
| _GnBuildFile._target(file, 'config', |
| _GnBuildFile._INCLUDE_CONFIG_TARGET, { |
| 'include_dirs': ['public'], |
| 'visibility': [':*'], |
| }) |
| |
| if has_tests: |
| file.line() |
| _GnBuildFile._target( |
| file, 'pw_test_group', 'tests', { |
| 'tests': list(f':{test.name}' for test in self._cc_tests), |
| }) |
| |
| def _write_cc_target( |
| self, |
| file: _OutputFile, |
| target: _BuildFile.CcTarget, |
| ) -> None: |
| """Defines a GN source_set for a C++ target.""" |
| |
| target_vars: _GnScope = {} |
| |
| if target.headers: |
| target_vars['public_configs'] = [ |
| f':{_GnBuildFile._INCLUDE_CONFIG_TARGET}' |
| ] |
| target_vars['public'] = list(target.rebased_headers(self.dir)) |
| |
| if target.sources: |
| target_vars['sources'] = list(target.rebased_sources(self.dir)) |
| |
| if target.deps: |
| target_vars['deps'] = target.deps |
| |
| _GnBuildFile._target(file, 'pw_source_set', target.name, target_vars) |
| |
| def _write_cc_test( |
| self, |
| file: _OutputFile, |
| target: '_BuildFile.CcTarget', |
| ) -> None: |
| _GnBuildFile._target( |
| file, 'pw_test', target.name, { |
| 'sources': list(target.rebased_sources(self.dir)), |
| 'deps': target.deps, |
| }) |
| |
| def _write_docs_target( |
| self, |
| file: _OutputFile, |
| docs_sources: List[str], |
| ) -> None: |
| """Defines a pw_doc_group for module documentation.""" |
| _GnBuildFile._target(file, 'pw_doc_group', 'docs', { |
| 'sources': docs_sources, |
| }) |
| |
| @staticmethod |
| def _target( |
| file: _OutputFile, |
| target_type: str, |
| name: str, |
| args: _GnScope, |
| ) -> None: |
| """Formats a GN target.""" |
| |
| file.line(f'{target_type}("{name}") {{') |
| |
| with file.indent(): |
| _GnBuildFile._format_gn_scope(file, args) |
| |
| file.line('}') |
| |
| @staticmethod |
| def _format_gn_scope(file: _OutputFile, scope: _GnScope) -> None: |
| """Formats all of the variables within a GN scope to a file. |
| |
| This function does not write the enclosing braces of the outer scope to |
| support use from multiple formatting contexts. |
| """ |
| for key, val in scope.items(): |
| if isinstance(val, int): |
| file.line(f'{key} = {val}') |
| continue |
| |
| if isinstance(val, str): |
| file.line(f'{key} = {_GnBuildFile._gn_string(val)}') |
| continue |
| |
| if isinstance(val, bool): |
| file.line(f'{key} = {str(val).lower()}') |
| continue |
| |
| if isinstance(val, dict): |
| file.line(f'{key} = {{') |
| with file.indent(): |
| _GnBuildFile._format_gn_scope(file, val) |
| file.line('}') |
| continue |
| |
| # Format a list of strings. |
| # TODO(frolv): Lists of other types? |
| assert isinstance(val, list) |
| |
| if not val: |
| file.line(f'{key} = []') |
| continue |
| |
| if len(val) == 1: |
| file.line(f'{key} = [ {_GnBuildFile._gn_string(val[0])} ]') |
| continue |
| |
| file.line(f'{key} = [') |
| with file.indent(): |
| for string in sorted(val): |
| file.line(f'{_GnBuildFile._gn_string(string)},') |
| file.line(']') |
| |
| @staticmethod |
| def _gn_string(string: str) -> str: |
| """Converts a Python string into a string literal within a GN file. |
| |
| Accounts for the possibility of variable interpolation within GN, |
| removing quotes if unnecessary: |
| |
| "string" -> "string" |
| "string" -> "string" |
| "$var" -> var |
| "$var2" -> var2 |
| "$3var" -> "$3var" |
| "$dir_pw_foo" -> dir_pw_foo |
| "$dir_pw_foo:bar" -> "$dir_pw_foo:bar" |
| "$dir_pw_foo/baz" -> "$dir_pw_foo/baz" |
| "${dir_pw_foo}" -> dir_pw_foo |
| |
| """ |
| |
| # Check if the entire string refers to a interpolated variable. |
| # |
| # Simple case: '$' followed a single word, e.g. "$my_variable". |
| # Note that identifiers can't start with a number. |
| if re.fullmatch(r'^\$[a-zA-Z_]\w*$', string): |
| return string[1:] |
| |
| # GN permits wrapping an interpolated variable in braces. |
| # Check for strings of the format "${my_variable}". |
| if re.fullmatch(r'^\$\{[a-zA-Z_]\w*\}$', string): |
| return string[2:-1] |
| |
| return f'"{string}"' |
| |
| |
| class _BazelBuildFile(_BuildFile): |
| _DEFAULT_FILENAME = 'BUILD.bazel' |
| |
| def __init__(self, |
| directory: Path, |
| ctx: _ModuleContext, |
| filename: str = _DEFAULT_FILENAME): |
| super().__init__(directory / filename, ctx) |
| |
| def _indent_width(self) -> int: |
| return 4 |
| |
| def _write_preamble(self, file: _OutputFile) -> None: |
| imports = ['//pw_build:pigweed.bzl'] |
| if self._cc_targets: |
| imports.append('pw_cc_library') |
| |
| if self._cc_tests: |
| imports.append('pw_cc_test') |
| |
| file.line('load(') |
| with file.indent(): |
| for imp in sorted(imports): |
| file.line(f'"{imp}",') |
| file.line(')\n') |
| |
| file.line('package(default_visibility = ["//visibility:public"])\n') |
| file.line('licenses(["notice"])') |
| |
| def _write_cc_target( |
| self, |
| file: _OutputFile, |
| target: _BuildFile.CcTarget, |
| ) -> None: |
| _BazelBuildFile._target( |
| file, 'pw_cc_library', target.name, { |
| 'srcs': list(target.rebased_sources(self.dir)), |
| 'hdrs': list(target.rebased_headers(self.dir)), |
| 'includes': ['public'], |
| }) |
| |
| def _write_cc_test( |
| self, |
| file: _OutputFile, |
| target: '_BuildFile.CcTarget', |
| ) -> None: |
| _BazelBuildFile._target(file, 'pw_cc_test', target.name, { |
| 'srcs': list(target.rebased_sources(self.dir)), |
| 'deps': target.deps, |
| }) |
| |
| def _write_docs_target( |
| self, |
| file: _OutputFile, |
| docs_sources: List[str], |
| ) -> None: |
| file.line('# Bazel does not yet support building docs.') |
| _BazelBuildFile._target(file, 'filegroup', 'docs', |
| {'srcs': docs_sources}) |
| |
| @staticmethod |
| def _target( |
| file: _OutputFile, |
| target_type: str, |
| name: str, |
| keys: Dict[str, List[str]], |
| ) -> None: |
| file.line(f'{target_type}(') |
| |
| with file.indent(): |
| file.line(f'name = "{name}",') |
| |
| for k, vals in keys.items(): |
| if len(vals) == 1: |
| file.line(f'{k} = ["{vals[0]}"],') |
| continue |
| |
| file.line(f'{k} = [') |
| with file.indent(): |
| for val in sorted(vals): |
| file.line(f'"{val}",') |
| file.line('],') |
| |
| file.line(')') |
| |
| |
| class _CmakeBuildFile(_BuildFile): |
| _DEFAULT_FILENAME = 'CMakeLists.txt' |
| |
| def __init__(self, |
| directory: Path, |
| ctx: _ModuleContext, |
| filename: str = _DEFAULT_FILENAME): |
| super().__init__(directory / filename, ctx) |
| |
| def _indent_width(self) -> int: |
| return 2 |
| |
| def _write_preamble(self, file: _OutputFile) -> None: |
| file.line('include($ENV{PW_ROOT}/pw_build/pigweed.cmake)') |
| |
| def _write_cc_target( |
| self, |
| file: _OutputFile, |
| target: _BuildFile.CcTarget, |
| ) -> None: |
| if target.name == self._ctx.name.full: |
| target_name = target.name |
| else: |
| target_name = f'{self._ctx.name.full}.{target.name}' |
| |
| _CmakeBuildFile._target( |
| file, |
| 'pw_add_module_library', |
| target_name, |
| { |
| 'sources': list(target.rebased_sources(self.dir)), |
| 'headers': list(target.rebased_headers(self.dir)), |
| 'public_includes': ['public'], |
| }, |
| ) |
| |
| def _write_cc_test( |
| self, |
| file: _OutputFile, |
| target: '_BuildFile.CcTarget', |
| ) -> None: |
| _CmakeBuildFile._target(file, 'pw_auto_add_module_tests', |
| self._ctx.name.full, {'private_deps': []}) |
| |
| def _write_docs_target( |
| self, |
| file: _OutputFile, |
| docs_sources: List[str], |
| ) -> None: |
| file.line('# CMake does not yet support building docs.') |
| |
| @staticmethod |
| def _target( |
| file: _OutputFile, |
| target_type: str, |
| name: str, |
| keys: Dict[str, List[str]], |
| ) -> None: |
| file.line(f'{target_type}({name}') |
| |
| with file.indent(): |
| for k, vals in keys.items(): |
| file.line(k.upper()) |
| with file.indent(): |
| for val in sorted(vals): |
| file.line(val) |
| |
| file.line(')') |
| |
| |
| class _LanguageGenerator: |
| """Generates files for a programming language in a new Pigweed module.""" |
| def __init__(self, ctx: _ModuleContext) -> None: |
| self._ctx = ctx |
| |
| @abc.abstractmethod |
| def create_source_files(self) -> None: |
| """Creates the boilerplate source files required by the language.""" |
| |
| |
| class _CcLanguageGenerator(_LanguageGenerator): |
| """Generates boilerplate source files for a C++ module.""" |
| def __init__(self, ctx: _ModuleContext) -> None: |
| super().__init__(ctx) |
| |
| self._public_dir = ctx.dir / 'public' |
| self._headers_dir = self._public_dir / ctx.name.full |
| |
| def create_source_files(self) -> None: |
| self._headers_dir.mkdir(parents=True) |
| |
| main_header = self._new_header(self._ctx.name.main) |
| main_source = self._new_source(self._ctx.name.main) |
| test_source = self._new_source(f'{self._ctx.name.main}_test') |
| |
| # TODO(frolv): This could be configurable. |
| namespace = self._ctx.name.default_namespace |
| |
| main_source.line( |
| f'#include "{main_header.path.relative_to(self._public_dir)}"\n') |
| main_source.line(f'namespace {namespace} {{\n') |
| main_source.line('int magic = 42;\n') |
| main_source.line(f'}} // namespace {namespace}') |
| |
| main_header.line(f'namespace {namespace} {{\n') |
| main_header.line('extern int magic;\n') |
| main_header.line(f'}} // namespace {namespace}') |
| |
| test_source.line( |
| f'#include "{main_header.path.relative_to(self._public_dir)}"\n') |
| test_source.line('#include "gtest/gtest.h"\n') |
| test_source.line(f'namespace {namespace} {{') |
| test_source.line('namespace {\n') |
| test_source.line( |
| f'TEST({self._ctx.name.upper_camel_case()}, GeneratesCorrectly) {{' |
| ) |
| with test_source.indent(): |
| test_source.line('EXPECT_EQ(magic, 42);') |
| test_source.line('}\n') |
| test_source.line('} // namespace') |
| test_source.line(f'}} // namespace {namespace}') |
| |
| self._ctx.add_cc_target( |
| _BuildFile.CcTarget(name=self._ctx.name.full, |
| sources=[main_source.path], |
| headers=[main_header.path])) |
| |
| self._ctx.add_cc_test( |
| _BuildFile.CcTarget(name=f'{self._ctx.name.main}_test', |
| deps=[f':{self._ctx.name.full}'], |
| sources=[test_source.path])) |
| |
| main_header.write() |
| main_source.write() |
| test_source.write() |
| |
| def _new_source(self, name: str) -> _OutputFile: |
| file = _OutputFile(self._ctx.dir / f'{name}.cc') |
| |
| if self._ctx.is_upstream: |
| file.line(_PIGWEED_LICENSE_CC) |
| file.line() |
| |
| return file |
| |
| def _new_header(self, name: str) -> _OutputFile: |
| file = _OutputFile(self._headers_dir / f'{name}.h') |
| |
| if self._ctx.is_upstream: |
| file.line(_PIGWEED_LICENSE_CC) |
| |
| file.line('#pragma once\n') |
| return file |
| |
| |
| _BUILD_FILES: Dict[str, Type[_BuildFile]] = { |
| 'bazel': _BazelBuildFile, |
| 'cmake': _CmakeBuildFile, |
| 'gn': _GnBuildFile, |
| } |
| |
| _LANGUAGE_GENERATORS: Dict[str, Type[_LanguageGenerator]] = { |
| 'cc': _CcLanguageGenerator, |
| } |
| |
| |
| def _check_module_name( |
| module: str, |
| is_upstream: bool, |
| ) -> Optional[_ModuleName]: |
| """Checks whether a module name is valid.""" |
| |
| name = _ModuleName.parse(module) |
| if not name: |
| _LOG.error('"%s" does not conform to the Pigweed module name format', |
| module) |
| return None |
| |
| if is_upstream and name.prefix != 'pw': |
| _LOG.error('Modules within Pigweed itself must start with "pw_"') |
| return None |
| |
| return name |
| |
| |
| def _create_main_docs_file(ctx: _ModuleContext) -> None: |
| """Populates the top-level docs.rst file within a new module.""" |
| |
| docs_file = _OutputFile(ctx.dir / 'docs.rst') |
| docs_file.line(f'.. _module-{ctx.name}:\n') |
| |
| title = '=' * len(ctx.name.full) |
| docs_file.line(title) |
| docs_file.line(ctx.name.full) |
| docs_file.line(title) |
| docs_file.line(f'This is the main documentation file for {ctx.name}.') |
| |
| ctx.add_docs_file(docs_file.path) |
| |
| docs_file.write() |
| |
| |
| def _basic_module_setup( |
| module_name: _ModuleName, |
| module_dir: Path, |
| build_systems: Iterable[str], |
| is_upstream: bool, |
| ) -> _ModuleContext: |
| """Creates the basic layout of a Pigweed module.""" |
| module_dir.mkdir() |
| |
| ctx = _ModuleContext(name=module_name, |
| dir=module_dir, |
| root_build_files=[], |
| sub_build_files=[], |
| build_systems=list(build_systems), |
| is_upstream=is_upstream) |
| |
| ctx.root_build_files.extend(_BUILD_FILES[build](module_dir, ctx) |
| for build in ctx.build_systems) |
| |
| _create_main_docs_file(ctx) |
| |
| return ctx |
| |
| |
| def _create_module(module: str, languages: Iterable[str], |
| build_systems: Iterable[str]) -> None: |
| project_root = Path(os.environ.get('PW_PROJECT_ROOT', '')) |
| assert project_root.is_dir() |
| |
| is_upstream = os.environ.get('PW_ROOT') == str(project_root) |
| |
| module_name = _check_module_name(module, is_upstream) |
| if not module_name: |
| sys.exit(1) |
| |
| if not is_upstream: |
| _LOG.error('`pw module create` is experimental and does ' |
| 'not yet support downstream projects.') |
| sys.exit(1) |
| |
| module_dir = project_root / module |
| |
| if module_dir.is_dir(): |
| _LOG.error('Module %s already exists', module) |
| sys.exit(1) |
| |
| if module_dir.is_file(): |
| _LOG.error( |
| 'Cannot create module %s as a file of that name already exists', |
| module) |
| sys.exit(1) |
| |
| ctx = _basic_module_setup(module_name, module_dir, build_systems, |
| is_upstream) |
| |
| try: |
| generators = list(_LANGUAGE_GENERATORS[lang](ctx) |
| for lang in languages) |
| except KeyError as key: |
| _LOG.error('Unsupported language: %s', key) |
| sys.exit(1) |
| |
| for generator in generators: |
| generator.create_source_files() |
| |
| for build_file in ctx.build_files(): |
| build_file.write() |
| |
| if is_upstream: |
| modules_file = project_root / 'PIGWEED_MODULES' |
| if not modules_file.exists(): |
| _LOG.error('Could not locate PIGWEED_MODULES file; ' |
| 'your repository may be in a bad state.') |
| return |
| |
| modules_gni_file = (project_root / 'pw_build' / |
| 'generated_pigweed_modules_lists.gni') |
| |
| # Cut off the extra newline at the end of the file. |
| modules_list = modules_file.read_text().split('\n')[:-1] |
| modules_list.append(module_name.full) |
| modules_list.sort() |
| modules_list.append('') |
| modules_file.write_text('\n'.join(modules_list)) |
| print(' modify ' + str(modules_file.relative_to(Path.cwd()))) |
| |
| generate_modules_lists.main( |
| root=project_root, |
| modules_list=modules_file, |
| modules_gni_file=modules_gni_file, |
| warn_only=None, |
| ) |
| print(' modify ' + str(modules_gni_file.relative_to(Path.cwd()))) |
| |
| print() |
| _LOG.info('Module %s created at %s', module_name, |
| module_dir.relative_to(Path.cwd())) |
| |
| |
| def register_subcommand(parser: argparse.ArgumentParser) -> None: |
| csv = lambda s: s.split(',') |
| |
| parser.add_argument( |
| '--build-systems', |
| help=('Comma-separated list of build systems the module supports. ' |
| f'Options: {", ".join(_BUILD_FILES.keys())}'), |
| type=csv, |
| default=_BUILD_FILES.keys(), |
| metavar='BUILD[,BUILD,...]') |
| parser.add_argument( |
| '--languages', |
| help=('Comma-separated list of languages the module will use. ' |
| f'Options: {", ".join(_LANGUAGE_GENERATORS.keys())}'), |
| type=csv, |
| default=[], |
| metavar='LANG[,LANG,...]') |
| parser.add_argument('module', |
| help='Name of the module to create.', |
| metavar='MODULE_NAME') |
| parser.set_defaults(func=_create_module) |