1# Copyright 2022 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Creates a new Pigweed module.""" 15 16import abc 17import argparse 18import dataclasses 19from dataclasses import dataclass 20import datetime 21import logging 22import os 23from pathlib import Path 24import re 25import sys 26 27from typing import Any, Dict, Iterable, List, Optional, Type, Union 28 29from pw_build import generate_modules_lists 30 31_LOG = logging.getLogger(__name__) 32 33_PIGWEED_LICENSE = f""" 34# Copyright {datetime.datetime.now().year} The Pigweed Authors 35# 36# Licensed under the Apache License, Version 2.0 (the "License"); you may not 37# use this file except in compliance with the License. You may obtain a copy of 38# the License at 39# 40# https://www.apache.org/licenses/LICENSE-2.0 41# 42# Unless required by applicable law or agreed to in writing, software 43# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 44# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 45# License for the specific language governing permissions and limitations under 46# the License.""".lstrip() 47 48_PIGWEED_LICENSE_CC = _PIGWEED_LICENSE.replace('#', '//') 49 50 51# TODO(frolv): Adapted from pw_protobuf. Consolidate them. 52class _OutputFile: 53 DEFAULT_INDENT_WIDTH = 2 54 55 def __init__(self, file: Path, indent_width: int = DEFAULT_INDENT_WIDTH): 56 self._file = file 57 self._content: List[str] = [] 58 self._indent_width: int = indent_width 59 self._indentation = 0 60 61 def line(self, line: str = '') -> None: 62 if line: 63 self._content.append(' ' * self._indentation) 64 self._content.append(line) 65 self._content.append('\n') 66 67 def indent( 68 self, 69 width: Optional[int] = None, 70 ) -> '_OutputFile._IndentationContext': 71 """Increases the indentation level of the output.""" 72 return self._IndentationContext( 73 self, width if width is not None else self._indent_width 74 ) 75 76 @property 77 def path(self) -> Path: 78 return self._file 79 80 @property 81 def content(self) -> str: 82 return ''.join(self._content) 83 84 def write(self) -> None: 85 print(' create ' + str(self._file.relative_to(Path.cwd()))) 86 self._file.write_text(self.content) 87 88 class _IndentationContext: 89 """Context that increases the output's indentation when it is active.""" 90 91 def __init__(self, output: '_OutputFile', width: int): 92 self._output = output 93 self._width: int = width 94 95 def __enter__(self): 96 self._output._indentation += self._width 97 98 def __exit__(self, typ, value, traceback): 99 self._output._indentation -= self._width 100 101 102class _ModuleName: 103 _MODULE_NAME_REGEX = '(^[a-zA-Z]{2,})((_[a-zA-Z0-9]+)+)$' 104 105 def __init__(self, prefix: str, main: str) -> None: 106 self._prefix = prefix 107 self._main = main 108 109 @property 110 def full(self) -> str: 111 return f'{self._prefix}_{self._main}' 112 113 @property 114 def prefix(self) -> str: 115 return self._prefix 116 117 @property 118 def main(self) -> str: 119 return self._main 120 121 @property 122 def default_namespace(self) -> str: 123 return f'{self._prefix}::{self._main}' 124 125 def upper_camel_case(self) -> str: 126 return ''.join(s.capitalize() for s in self._main.split('_')) 127 128 def __str__(self) -> str: 129 return self.full 130 131 def __repr__(self) -> str: 132 return self.full 133 134 @classmethod 135 def parse(cls, name: str) -> Optional['_ModuleName']: 136 match = re.fullmatch(_ModuleName._MODULE_NAME_REGEX, name) 137 if not match: 138 return None 139 140 return cls(match.group(1), match.group(2)[1:]) 141 142 143@dataclass 144class _ModuleContext: 145 name: _ModuleName 146 dir: Path 147 root_build_files: List['_BuildFile'] 148 sub_build_files: List['_BuildFile'] 149 build_systems: List[str] 150 is_upstream: bool 151 152 def build_files(self) -> Iterable['_BuildFile']: 153 yield from self.root_build_files 154 yield from self.sub_build_files 155 156 def add_docs_file(self, file: Path): 157 for build_file in self.root_build_files: 158 build_file.add_docs_source(str(file.relative_to(self.dir))) 159 160 def add_cc_target(self, target: '_BuildFile.CcTarget') -> None: 161 for build_file in self.root_build_files: 162 build_file.add_cc_target(target) 163 164 def add_cc_test(self, target: '_BuildFile.CcTarget') -> None: 165 for build_file in self.root_build_files: 166 build_file.add_cc_test(target) 167 168 169class _BuildFile: 170 """Abstract representation of a build file for a module.""" 171 172 @dataclass 173 class Target: 174 name: str 175 176 # TODO(frolv): Shouldn't be a string list as that's build system 177 # specific. Figure out a way to resolve dependencies from targets. 178 deps: List[str] = dataclasses.field(default_factory=list) 179 180 @dataclass 181 class CcTarget(Target): 182 sources: List[Path] = dataclasses.field(default_factory=list) 183 headers: List[Path] = dataclasses.field(default_factory=list) 184 185 def rebased_sources(self, rebase_path: Path) -> Iterable[str]: 186 return (str(src.relative_to(rebase_path)) for src in self.sources) 187 188 def rebased_headers(self, rebase_path: Path) -> Iterable[str]: 189 return (str(hdr.relative_to(rebase_path)) for hdr in self.headers) 190 191 def __init__(self, path: Path, ctx: _ModuleContext): 192 self._path = path 193 self._ctx = ctx 194 195 self._docs_sources: List[str] = [] 196 self._cc_targets: List[_BuildFile.CcTarget] = [] 197 self._cc_tests: List[_BuildFile.CcTarget] = [] 198 199 @property 200 def path(self) -> Path: 201 return self._path 202 203 @property 204 def dir(self) -> Path: 205 return self._path.parent 206 207 def add_docs_source(self, filename: str) -> None: 208 self._docs_sources.append(filename) 209 210 def add_cc_target(self, target: CcTarget) -> None: 211 self._cc_targets.append(target) 212 213 def add_cc_test(self, target: CcTarget) -> None: 214 self._cc_tests.append(target) 215 216 def write(self) -> None: 217 """Writes the contents of the build file to disk.""" 218 file = _OutputFile(self._path, self._indent_width()) 219 220 if self._ctx.is_upstream: 221 file.line(_PIGWEED_LICENSE) 222 file.line() 223 224 self._write_preamble(file) 225 226 for target in self._cc_targets: 227 file.line() 228 self._write_cc_target(file, target) 229 230 for target in self._cc_tests: 231 file.line() 232 self._write_cc_test(file, target) 233 234 if self._docs_sources: 235 file.line() 236 self._write_docs_target(file, self._docs_sources) 237 238 file.write() 239 240 @abc.abstractmethod 241 def _indent_width(self) -> int: 242 """Returns the default indent width for the build file's code style.""" 243 244 @abc.abstractmethod 245 def _write_preamble(self, file: _OutputFile) -> None: 246 """Formats""" 247 248 @abc.abstractmethod 249 def _write_cc_target( 250 self, 251 file: _OutputFile, 252 target: '_BuildFile.CcTarget', 253 ) -> None: 254 """Defines a C++ library target within the build file.""" 255 256 @abc.abstractmethod 257 def _write_cc_test( 258 self, 259 file: _OutputFile, 260 target: '_BuildFile.CcTarget', 261 ) -> None: 262 """Defines a C++ unit test target within the build file.""" 263 264 @abc.abstractmethod 265 def _write_docs_target( 266 self, 267 file: _OutputFile, 268 docs_sources: List[str], 269 ) -> None: 270 """Defines a documentation target within the build file.""" 271 272 273# TODO(frolv): The Dict here should be Dict[str, '_GnVal'] (i.e. _GnScope), 274# but mypy does not yet support recursive types: 275# https://github.com/python/mypy/issues/731 276_GnVal = Union[bool, int, str, List[str], Dict[str, Any]] 277_GnScope = Dict[str, _GnVal] 278 279 280class _GnBuildFile(_BuildFile): 281 _DEFAULT_FILENAME = 'BUILD.gn' 282 _INCLUDE_CONFIG_TARGET = 'public_include_path' 283 284 def __init__( 285 self, 286 directory: Path, 287 ctx: _ModuleContext, 288 filename: str = _DEFAULT_FILENAME, 289 ): 290 super().__init__(directory / filename, ctx) 291 292 def _indent_width(self) -> int: 293 return 2 294 295 def _write_preamble(self, file: _OutputFile) -> None: 296 # Upstream modules always require a tests target, even if it's empty. 297 has_tests = len(self._cc_tests) > 0 or self._ctx.is_upstream 298 299 imports = [] 300 301 if self._cc_targets: 302 imports.append('$dir_pw_build/target_types.gni') 303 304 if has_tests: 305 imports.append('$dir_pw_unit_test/test.gni') 306 307 if self._docs_sources: 308 imports.append('$dir_pw_docgen/docs.gni') 309 310 file.line('import("//build_overrides/pigweed.gni")\n') 311 for imp in sorted(imports): 312 file.line(f'import("{imp}")') 313 314 if self._cc_targets: 315 file.line() 316 _GnBuildFile._target( 317 file, 318 'config', 319 _GnBuildFile._INCLUDE_CONFIG_TARGET, 320 { 321 'include_dirs': ['public'], 322 'visibility': [':*'], 323 }, 324 ) 325 326 if has_tests: 327 file.line() 328 _GnBuildFile._target( 329 file, 330 'pw_test_group', 331 'tests', 332 { 333 'tests': list(f':{test.name}' for test in self._cc_tests), 334 }, 335 ) 336 337 def _write_cc_target( 338 self, 339 file: _OutputFile, 340 target: _BuildFile.CcTarget, 341 ) -> None: 342 """Defines a GN source_set for a C++ target.""" 343 344 target_vars: _GnScope = {} 345 346 if target.headers: 347 target_vars['public_configs'] = [ 348 f':{_GnBuildFile._INCLUDE_CONFIG_TARGET}' 349 ] 350 target_vars['public'] = list(target.rebased_headers(self.dir)) 351 352 if target.sources: 353 target_vars['sources'] = list(target.rebased_sources(self.dir)) 354 355 if target.deps: 356 target_vars['deps'] = target.deps 357 358 _GnBuildFile._target(file, 'pw_source_set', target.name, target_vars) 359 360 def _write_cc_test( 361 self, 362 file: _OutputFile, 363 target: '_BuildFile.CcTarget', 364 ) -> None: 365 _GnBuildFile._target( 366 file, 367 'pw_test', 368 target.name, 369 { 370 'sources': list(target.rebased_sources(self.dir)), 371 'deps': target.deps, 372 }, 373 ) 374 375 def _write_docs_target( 376 self, 377 file: _OutputFile, 378 docs_sources: List[str], 379 ) -> None: 380 """Defines a pw_doc_group for module documentation.""" 381 _GnBuildFile._target( 382 file, 383 'pw_doc_group', 384 'docs', 385 { 386 'sources': docs_sources, 387 }, 388 ) 389 390 @staticmethod 391 def _target( 392 file: _OutputFile, 393 target_type: str, 394 name: str, 395 args: _GnScope, 396 ) -> None: 397 """Formats a GN target.""" 398 399 file.line(f'{target_type}("{name}") {{') 400 401 with file.indent(): 402 _GnBuildFile._format_gn_scope(file, args) 403 404 file.line('}') 405 406 @staticmethod 407 def _format_gn_scope(file: _OutputFile, scope: _GnScope) -> None: 408 """Formats all of the variables within a GN scope to a file. 409 410 This function does not write the enclosing braces of the outer scope to 411 support use from multiple formatting contexts. 412 """ 413 for key, val in scope.items(): 414 if isinstance(val, int): 415 file.line(f'{key} = {val}') 416 continue 417 418 if isinstance(val, str): 419 file.line(f'{key} = {_GnBuildFile._gn_string(val)}') 420 continue 421 422 if isinstance(val, bool): 423 file.line(f'{key} = {str(val).lower()}') 424 continue 425 426 if isinstance(val, dict): 427 file.line(f'{key} = {{') 428 with file.indent(): 429 _GnBuildFile._format_gn_scope(file, val) 430 file.line('}') 431 continue 432 433 # Format a list of strings. 434 # TODO(frolv): Lists of other types? 435 assert isinstance(val, list) 436 437 if not val: 438 file.line(f'{key} = []') 439 continue 440 441 if len(val) == 1: 442 file.line(f'{key} = [ {_GnBuildFile._gn_string(val[0])} ]') 443 continue 444 445 file.line(f'{key} = [') 446 with file.indent(): 447 for string in sorted(val): 448 file.line(f'{_GnBuildFile._gn_string(string)},') 449 file.line(']') 450 451 @staticmethod 452 def _gn_string(string: str) -> str: 453 """Converts a Python string into a string literal within a GN file. 454 455 Accounts for the possibility of variable interpolation within GN, 456 removing quotes if unnecessary: 457 458 "string" -> "string" 459 "string" -> "string" 460 "$var" -> var 461 "$var2" -> var2 462 "$3var" -> "$3var" 463 "$dir_pw_foo" -> dir_pw_foo 464 "$dir_pw_foo:bar" -> "$dir_pw_foo:bar" 465 "$dir_pw_foo/baz" -> "$dir_pw_foo/baz" 466 "${dir_pw_foo}" -> dir_pw_foo 467 468 """ 469 470 # Check if the entire string refers to a interpolated variable. 471 # 472 # Simple case: '$' followed a single word, e.g. "$my_variable". 473 # Note that identifiers can't start with a number. 474 if re.fullmatch(r'^\$[a-zA-Z_]\w*$', string): 475 return string[1:] 476 477 # GN permits wrapping an interpolated variable in braces. 478 # Check for strings of the format "${my_variable}". 479 if re.fullmatch(r'^\$\{[a-zA-Z_]\w*\}$', string): 480 return string[2:-1] 481 482 return f'"{string}"' 483 484 485class _BazelBuildFile(_BuildFile): 486 _DEFAULT_FILENAME = 'BUILD.bazel' 487 488 def __init__( 489 self, 490 directory: Path, 491 ctx: _ModuleContext, 492 filename: str = _DEFAULT_FILENAME, 493 ): 494 super().__init__(directory / filename, ctx) 495 496 def _indent_width(self) -> int: 497 return 4 498 499 def _write_preamble(self, file: _OutputFile) -> None: 500 imports = ['//pw_build:pigweed.bzl'] 501 if self._cc_targets: 502 imports.append('pw_cc_library') 503 504 if self._cc_tests: 505 imports.append('pw_cc_test') 506 507 file.line('load(') 508 with file.indent(): 509 for imp in sorted(imports): 510 file.line(f'"{imp}",') 511 file.line(')\n') 512 513 file.line('package(default_visibility = ["//visibility:public"])\n') 514 file.line('licenses(["notice"])') 515 516 def _write_cc_target( 517 self, 518 file: _OutputFile, 519 target: _BuildFile.CcTarget, 520 ) -> None: 521 _BazelBuildFile._target( 522 file, 523 'pw_cc_library', 524 target.name, 525 { 526 'srcs': list(target.rebased_sources(self.dir)), 527 'hdrs': list(target.rebased_headers(self.dir)), 528 'includes': ['public'], 529 }, 530 ) 531 532 def _write_cc_test( 533 self, 534 file: _OutputFile, 535 target: '_BuildFile.CcTarget', 536 ) -> None: 537 _BazelBuildFile._target( 538 file, 539 'pw_cc_test', 540 target.name, 541 { 542 'srcs': list(target.rebased_sources(self.dir)), 543 'deps': target.deps, 544 }, 545 ) 546 547 def _write_docs_target( 548 self, 549 file: _OutputFile, 550 docs_sources: List[str], 551 ) -> None: 552 file.line('# Bazel does not yet support building docs.') 553 _BazelBuildFile._target( 554 file, 'filegroup', 'docs', {'srcs': docs_sources} 555 ) 556 557 @staticmethod 558 def _target( 559 file: _OutputFile, 560 target_type: str, 561 name: str, 562 keys: Dict[str, List[str]], 563 ) -> None: 564 file.line(f'{target_type}(') 565 566 with file.indent(): 567 file.line(f'name = "{name}",') 568 569 for k, vals in keys.items(): 570 if len(vals) == 1: 571 file.line(f'{k} = ["{vals[0]}"],') 572 continue 573 574 file.line(f'{k} = [') 575 with file.indent(): 576 for val in sorted(vals): 577 file.line(f'"{val}",') 578 file.line('],') 579 580 file.line(')') 581 582 583class _CmakeBuildFile(_BuildFile): 584 _DEFAULT_FILENAME = 'CMakeLists.txt' 585 586 def __init__( 587 self, 588 directory: Path, 589 ctx: _ModuleContext, 590 filename: str = _DEFAULT_FILENAME, 591 ): 592 super().__init__(directory / filename, ctx) 593 594 def _indent_width(self) -> int: 595 return 2 596 597 def _write_preamble(self, file: _OutputFile) -> None: 598 file.line('include($ENV{PW_ROOT}/pw_build/pigweed.cmake)') 599 600 def _write_cc_target( 601 self, 602 file: _OutputFile, 603 target: _BuildFile.CcTarget, 604 ) -> None: 605 if target.name == self._ctx.name.full: 606 target_name = target.name 607 else: 608 target_name = f'{self._ctx.name.full}.{target.name}' 609 610 _CmakeBuildFile._target( 611 file, 612 'pw_add_module_library', 613 target_name, 614 { 615 'sources': list(target.rebased_sources(self.dir)), 616 'headers': list(target.rebased_headers(self.dir)), 617 'public_includes': ['public'], 618 }, 619 ) 620 621 def _write_cc_test( 622 self, 623 file: _OutputFile, 624 target: '_BuildFile.CcTarget', 625 ) -> None: 626 _CmakeBuildFile._target( 627 file, 628 'pw_auto_add_module_tests', 629 self._ctx.name.full, 630 {'private_deps': []}, 631 ) 632 633 def _write_docs_target( 634 self, 635 file: _OutputFile, 636 docs_sources: List[str], 637 ) -> None: 638 file.line('# CMake does not yet support building docs.') 639 640 @staticmethod 641 def _target( 642 file: _OutputFile, 643 target_type: str, 644 name: str, 645 keys: Dict[str, List[str]], 646 ) -> None: 647 file.line(f'{target_type}({name}') 648 649 with file.indent(): 650 for k, vals in keys.items(): 651 file.line(k.upper()) 652 with file.indent(): 653 for val in sorted(vals): 654 file.line(val) 655 656 file.line(')') 657 658 659class _LanguageGenerator: 660 """Generates files for a programming language in a new Pigweed module.""" 661 662 def __init__(self, ctx: _ModuleContext) -> None: 663 self._ctx = ctx 664 665 @abc.abstractmethod 666 def create_source_files(self) -> None: 667 """Creates the boilerplate source files required by the language.""" 668 669 670class _CcLanguageGenerator(_LanguageGenerator): 671 """Generates boilerplate source files for a C++ module.""" 672 673 def __init__(self, ctx: _ModuleContext) -> None: 674 super().__init__(ctx) 675 676 self._public_dir = ctx.dir / 'public' 677 self._headers_dir = self._public_dir / ctx.name.full 678 679 def create_source_files(self) -> None: 680 self._headers_dir.mkdir(parents=True) 681 682 main_header = self._new_header(self._ctx.name.main) 683 main_source = self._new_source(self._ctx.name.main) 684 test_source = self._new_source(f'{self._ctx.name.main}_test') 685 686 # TODO(frolv): This could be configurable. 687 namespace = self._ctx.name.default_namespace 688 689 main_source.line( 690 f'#include "{main_header.path.relative_to(self._public_dir)}"\n' 691 ) 692 main_source.line(f'namespace {namespace} {{\n') 693 main_source.line('int magic = 42;\n') 694 main_source.line(f'}} // namespace {namespace}') 695 696 main_header.line(f'namespace {namespace} {{\n') 697 main_header.line('extern int magic;\n') 698 main_header.line(f'}} // namespace {namespace}') 699 700 test_source.line( 701 f'#include "{main_header.path.relative_to(self._public_dir)}"\n' 702 ) 703 test_source.line('#include "gtest/gtest.h"\n') 704 test_source.line(f'namespace {namespace} {{') 705 test_source.line('namespace {\n') 706 test_source.line( 707 f'TEST({self._ctx.name.upper_camel_case()}, GeneratesCorrectly) {{' 708 ) 709 with test_source.indent(): 710 test_source.line('EXPECT_EQ(magic, 42);') 711 test_source.line('}\n') 712 test_source.line('} // namespace') 713 test_source.line(f'}} // namespace {namespace}') 714 715 self._ctx.add_cc_target( 716 _BuildFile.CcTarget( 717 name=self._ctx.name.full, 718 sources=[main_source.path], 719 headers=[main_header.path], 720 ) 721 ) 722 723 self._ctx.add_cc_test( 724 _BuildFile.CcTarget( 725 name=f'{self._ctx.name.main}_test', 726 deps=[f':{self._ctx.name.full}'], 727 sources=[test_source.path], 728 ) 729 ) 730 731 main_header.write() 732 main_source.write() 733 test_source.write() 734 735 def _new_source(self, name: str) -> _OutputFile: 736 file = _OutputFile(self._ctx.dir / f'{name}.cc') 737 738 if self._ctx.is_upstream: 739 file.line(_PIGWEED_LICENSE_CC) 740 file.line() 741 742 return file 743 744 def _new_header(self, name: str) -> _OutputFile: 745 file = _OutputFile(self._headers_dir / f'{name}.h') 746 747 if self._ctx.is_upstream: 748 file.line(_PIGWEED_LICENSE_CC) 749 750 file.line('#pragma once\n') 751 return file 752 753 754_BUILD_FILES: Dict[str, Type[_BuildFile]] = { 755 'bazel': _BazelBuildFile, 756 'cmake': _CmakeBuildFile, 757 'gn': _GnBuildFile, 758} 759 760_LANGUAGE_GENERATORS: Dict[str, Type[_LanguageGenerator]] = { 761 'cc': _CcLanguageGenerator, 762} 763 764 765def _check_module_name( 766 module: str, 767 is_upstream: bool, 768) -> Optional[_ModuleName]: 769 """Checks whether a module name is valid.""" 770 771 name = _ModuleName.parse(module) 772 if not name: 773 _LOG.error( 774 '"%s" does not conform to the Pigweed module name format', module 775 ) 776 return None 777 778 if is_upstream and name.prefix != 'pw': 779 _LOG.error('Modules within Pigweed itself must start with "pw_"') 780 return None 781 782 return name 783 784 785def _create_main_docs_file(ctx: _ModuleContext) -> None: 786 """Populates the top-level docs.rst file within a new module.""" 787 788 docs_file = _OutputFile(ctx.dir / 'docs.rst') 789 docs_file.line(f'.. _module-{ctx.name}:\n') 790 791 title = '=' * len(ctx.name.full) 792 docs_file.line(title) 793 docs_file.line(ctx.name.full) 794 docs_file.line(title) 795 docs_file.line(f'This is the main documentation file for {ctx.name}.') 796 797 ctx.add_docs_file(docs_file.path) 798 799 docs_file.write() 800 801 802def _basic_module_setup( 803 module_name: _ModuleName, 804 module_dir: Path, 805 build_systems: Iterable[str], 806 is_upstream: bool, 807) -> _ModuleContext: 808 """Creates the basic layout of a Pigweed module.""" 809 module_dir.mkdir() 810 811 ctx = _ModuleContext( 812 name=module_name, 813 dir=module_dir, 814 root_build_files=[], 815 sub_build_files=[], 816 build_systems=list(build_systems), 817 is_upstream=is_upstream, 818 ) 819 820 ctx.root_build_files.extend( 821 _BUILD_FILES[build](module_dir, ctx) for build in ctx.build_systems 822 ) 823 824 _create_main_docs_file(ctx) 825 826 return ctx 827 828 829def _create_module( 830 module: str, languages: Iterable[str], build_systems: Iterable[str] 831) -> None: 832 project_root = Path(os.environ.get('PW_PROJECT_ROOT', '')) 833 assert project_root.is_dir() 834 835 is_upstream = os.environ.get('PW_ROOT') == str(project_root) 836 837 module_name = _check_module_name(module, is_upstream) 838 if not module_name: 839 sys.exit(1) 840 841 if not is_upstream: 842 _LOG.error( 843 '`pw module create` is experimental and does ' 844 'not yet support downstream projects.' 845 ) 846 sys.exit(1) 847 848 module_dir = project_root / module 849 850 if module_dir.is_dir(): 851 _LOG.error('Module %s already exists', module) 852 sys.exit(1) 853 854 if module_dir.is_file(): 855 _LOG.error( 856 'Cannot create module %s as a file of that name already exists', 857 module, 858 ) 859 sys.exit(1) 860 861 ctx = _basic_module_setup( 862 module_name, module_dir, build_systems, is_upstream 863 ) 864 865 try: 866 generators = list(_LANGUAGE_GENERATORS[lang](ctx) for lang in languages) 867 except KeyError as key: 868 _LOG.error('Unsupported language: %s', key) 869 sys.exit(1) 870 871 for generator in generators: 872 generator.create_source_files() 873 874 for build_file in ctx.build_files(): 875 build_file.write() 876 877 if is_upstream: 878 modules_file = project_root / 'PIGWEED_MODULES' 879 if not modules_file.exists(): 880 _LOG.error( 881 'Could not locate PIGWEED_MODULES file; ' 882 'your repository may be in a bad state.' 883 ) 884 return 885 886 modules_gni_file = ( 887 project_root / 'pw_build' / 'generated_pigweed_modules_lists.gni' 888 ) 889 890 # Cut off the extra newline at the end of the file. 891 modules_list = modules_file.read_text().split('\n')[:-1] 892 modules_list.append(module_name.full) 893 modules_list.sort() 894 modules_list.append('') 895 modules_file.write_text('\n'.join(modules_list)) 896 print(' modify ' + str(modules_file.relative_to(Path.cwd()))) 897 898 generate_modules_lists.main( 899 root=project_root, 900 modules_list=modules_file, 901 modules_gni_file=modules_gni_file, 902 mode=generate_modules_lists.Mode.UPDATE, 903 ) 904 print(' modify ' + str(modules_gni_file.relative_to(Path.cwd()))) 905 906 print() 907 _LOG.info( 908 'Module %s created at %s', 909 module_name, 910 module_dir.relative_to(Path.cwd()), 911 ) 912 913 914def register_subcommand(parser: argparse.ArgumentParser) -> None: 915 csv = lambda s: s.split(',') 916 917 parser.add_argument( 918 '--build-systems', 919 help=( 920 'Comma-separated list of build systems the module supports. ' 921 f'Options: {", ".join(_BUILD_FILES.keys())}' 922 ), 923 type=csv, 924 default=_BUILD_FILES.keys(), 925 metavar='BUILD[,BUILD,...]', 926 ) 927 parser.add_argument( 928 '--languages', 929 help=( 930 'Comma-separated list of languages the module will use. ' 931 f'Options: {", ".join(_LANGUAGE_GENERATORS.keys())}' 932 ), 933 type=csv, 934 default=[], 935 metavar='LANG[,LANG,...]', 936 ) 937 parser.add_argument( 938 'module', help='Name of the module to create.', metavar='MODULE_NAME' 939 ) 940 parser.set_defaults(func=_create_module) 941