zephyr/scripts/snippets.py

330 lines
12 KiB
Python
Raw Normal View History

snippets: initial snippet.yml support Add a new script, snippets.py, which is responsible for searching SNIPPET_ROOT for snippet definitions, validating them, and informing the build system about what needs doing as a result. Use this script in snippets.cmake to: - validate any discovered snippet.yml files - error out on undefined snippets - add a 'snippets' build system target that prints all snippet names (analogous to 'boards' and 'shields' targets) - handle any specific build system settings properly, by include()-ing a file it generates With this patch, you can define or extend a snippet in a snippet.yml file anywhere underneath a directory in SNIPPET_ROOT. The snippet.yml file format has a schema whose initial definition is in a new file, snippet-schema.yml. This initial snippet.yml file format supports adding .overlay and .conf files, like this: name: foo append: DTC_OVERLAY_FILE: foo.overlay OVERLAY_CONFIG: foo.conf boards: myboard: append: DTC_OVERLAY_FILE: myboard.overlay OVERLAY_CONFIG: myboard.conf /my-regular-expression-over-board-names/: append: DTC_OVERLAY_FILE: myregexp.overlay OVERLAY_CONFIG: myregexp.conf (Note that since the snippet feature is intended to be extensible, the same snippet name may appear in multiple files throughout any directory in SNIPPET_ROOT, with each addition augmenting prior ones.) This initial syntax aligns with the following snippet design goals: - extensible: you can add board-specific support for an existing snippet in another module - able to combine multiple types of configuration: we can now apply a .overlay and .conf at the same time - specializable: this allows you to define settings that only apply to a selectable set of boards (including with regular expression support for matching against multiple similar boards that follow a naming convention) - DRY: you can use regular expressions to apply the same snippet settings to multiple boards like this: /(board1|board2|...)/ This patch is not trying to design and implement everything up front. Additional features can and will be added to the snippet.yml format over time; using YAML as a format allows us to make backwards-compatible extensions as needed. Signed-off-by: Marti Bolivar <marti.bolivar@nordicsemi.no>
2023-01-08 00:27:07 +01:00
#!/usr/bin/env python3
#
# Copyright (c) 2022, Nordic Semiconductor ASA
#
# SPDX-License-Identifier: Apache-2.0
'''Internal snippets tool.
This is part of the build system's support for snippets.
It is not meant for use outside of the build system.
Output CMake variables:
- SNIPPET_NAMES: CMake list of discovered snippet names
- SNIPPET_FOUND_{snippet}: one per discovered snippet
'''
from collections import defaultdict, UserDict
from dataclasses import dataclass, field
from pathlib import Path, PurePosixPath
snippets: initial snippet.yml support Add a new script, snippets.py, which is responsible for searching SNIPPET_ROOT for snippet definitions, validating them, and informing the build system about what needs doing as a result. Use this script in snippets.cmake to: - validate any discovered snippet.yml files - error out on undefined snippets - add a 'snippets' build system target that prints all snippet names (analogous to 'boards' and 'shields' targets) - handle any specific build system settings properly, by include()-ing a file it generates With this patch, you can define or extend a snippet in a snippet.yml file anywhere underneath a directory in SNIPPET_ROOT. The snippet.yml file format has a schema whose initial definition is in a new file, snippet-schema.yml. This initial snippet.yml file format supports adding .overlay and .conf files, like this: name: foo append: DTC_OVERLAY_FILE: foo.overlay OVERLAY_CONFIG: foo.conf boards: myboard: append: DTC_OVERLAY_FILE: myboard.overlay OVERLAY_CONFIG: myboard.conf /my-regular-expression-over-board-names/: append: DTC_OVERLAY_FILE: myregexp.overlay OVERLAY_CONFIG: myregexp.conf (Note that since the snippet feature is intended to be extensible, the same snippet name may appear in multiple files throughout any directory in SNIPPET_ROOT, with each addition augmenting prior ones.) This initial syntax aligns with the following snippet design goals: - extensible: you can add board-specific support for an existing snippet in another module - able to combine multiple types of configuration: we can now apply a .overlay and .conf at the same time - specializable: this allows you to define settings that only apply to a selectable set of boards (including with regular expression support for matching against multiple similar boards that follow a naming convention) - DRY: you can use regular expressions to apply the same snippet settings to multiple boards like this: /(board1|board2|...)/ This patch is not trying to design and implement everything up front. Additional features can and will be added to the snippet.yml format over time; using YAML as a format allows us to make backwards-compatible extensions as needed. Signed-off-by: Marti Bolivar <marti.bolivar@nordicsemi.no>
2023-01-08 00:27:07 +01:00
from typing import Dict, Iterable, List, Set
import argparse
import logging
import os
import pykwalify.core
import pykwalify.errors
import re
import sys
import textwrap
import yaml
import platform
snippets: initial snippet.yml support Add a new script, snippets.py, which is responsible for searching SNIPPET_ROOT for snippet definitions, validating them, and informing the build system about what needs doing as a result. Use this script in snippets.cmake to: - validate any discovered snippet.yml files - error out on undefined snippets - add a 'snippets' build system target that prints all snippet names (analogous to 'boards' and 'shields' targets) - handle any specific build system settings properly, by include()-ing a file it generates With this patch, you can define or extend a snippet in a snippet.yml file anywhere underneath a directory in SNIPPET_ROOT. The snippet.yml file format has a schema whose initial definition is in a new file, snippet-schema.yml. This initial snippet.yml file format supports adding .overlay and .conf files, like this: name: foo append: DTC_OVERLAY_FILE: foo.overlay OVERLAY_CONFIG: foo.conf boards: myboard: append: DTC_OVERLAY_FILE: myboard.overlay OVERLAY_CONFIG: myboard.conf /my-regular-expression-over-board-names/: append: DTC_OVERLAY_FILE: myregexp.overlay OVERLAY_CONFIG: myregexp.conf (Note that since the snippet feature is intended to be extensible, the same snippet name may appear in multiple files throughout any directory in SNIPPET_ROOT, with each addition augmenting prior ones.) This initial syntax aligns with the following snippet design goals: - extensible: you can add board-specific support for an existing snippet in another module - able to combine multiple types of configuration: we can now apply a .overlay and .conf at the same time - specializable: this allows you to define settings that only apply to a selectable set of boards (including with regular expression support for matching against multiple similar boards that follow a naming convention) - DRY: you can use regular expressions to apply the same snippet settings to multiple boards like this: /(board1|board2|...)/ This patch is not trying to design and implement everything up front. Additional features can and will be added to the snippet.yml format over time; using YAML as a format allows us to make backwards-compatible extensions as needed. Signed-off-by: Marti Bolivar <marti.bolivar@nordicsemi.no>
2023-01-08 00:27:07 +01:00
# Marker type for an 'append:' configuration. Maps variables
# to the list of values to append to them.
Appends = Dict[str, List[str]]
def _new_append():
return defaultdict(list)
def _new_board2appends():
return defaultdict(_new_append)
@dataclass
class Snippet:
'''Class for keeping track of all the settings discovered for an
individual snippet.'''
name: str
appends: Appends = field(default_factory=_new_append)
board2appends: Dict[str, Appends] = field(default_factory=_new_board2appends)
def process_data(self, pathobj: Path, snippet_data: dict):
'''Process the data in a snippet.yml file, after it is loaded into a
python object and validated by pykwalify.'''
def append_value(variable, value):
if variable in ('EXTRA_DTC_OVERLAY_FILE', 'EXTRA_CONF_FILE'):
snippets: initial snippet.yml support Add a new script, snippets.py, which is responsible for searching SNIPPET_ROOT for snippet definitions, validating them, and informing the build system about what needs doing as a result. Use this script in snippets.cmake to: - validate any discovered snippet.yml files - error out on undefined snippets - add a 'snippets' build system target that prints all snippet names (analogous to 'boards' and 'shields' targets) - handle any specific build system settings properly, by include()-ing a file it generates With this patch, you can define or extend a snippet in a snippet.yml file anywhere underneath a directory in SNIPPET_ROOT. The snippet.yml file format has a schema whose initial definition is in a new file, snippet-schema.yml. This initial snippet.yml file format supports adding .overlay and .conf files, like this: name: foo append: DTC_OVERLAY_FILE: foo.overlay OVERLAY_CONFIG: foo.conf boards: myboard: append: DTC_OVERLAY_FILE: myboard.overlay OVERLAY_CONFIG: myboard.conf /my-regular-expression-over-board-names/: append: DTC_OVERLAY_FILE: myregexp.overlay OVERLAY_CONFIG: myregexp.conf (Note that since the snippet feature is intended to be extensible, the same snippet name may appear in multiple files throughout any directory in SNIPPET_ROOT, with each addition augmenting prior ones.) This initial syntax aligns with the following snippet design goals: - extensible: you can add board-specific support for an existing snippet in another module - able to combine multiple types of configuration: we can now apply a .overlay and .conf at the same time - specializable: this allows you to define settings that only apply to a selectable set of boards (including with regular expression support for matching against multiple similar boards that follow a naming convention) - DRY: you can use regular expressions to apply the same snippet settings to multiple boards like this: /(board1|board2|...)/ This patch is not trying to design and implement everything up front. Additional features can and will be added to the snippet.yml format over time; using YAML as a format allows us to make backwards-compatible extensions as needed. Signed-off-by: Marti Bolivar <marti.bolivar@nordicsemi.no>
2023-01-08 00:27:07 +01:00
path = pathobj.parent / value
if not path.is_file():
_err(f'snippet file {pathobj}: {variable}: file not found: {path}')
return f'"{path}"'
_err(f'unknown append variable: {variable}')
for variable, value in snippet_data.get('append', {}).items():
self.appends[variable].append(append_value(variable, value))
for board, settings in snippet_data.get('boards', {}).items():
if board.startswith('/') and not board.endswith('/'):
_err(f"snippet file {pathobj}: board {board} starts with '/', so "
"it must end with '/' to use a regular expression")
for variable, value in settings.get('append', {}).items():
self.board2appends[board][variable].append(
append_value(variable, value))
class Snippets(UserDict):
'''Type for all the information we have discovered about all snippets.
As a dict, this maps a snippet's name onto the Snippet object.
Any additional global attributes about all snippets go here as
instance attributes.'''
def __init__(self, requested: Iterable[str] = None):
super().__init__()
self.paths: Set[Path] = set()
self.requested: List[str] = list(requested or [])
snippets: initial snippet.yml support Add a new script, snippets.py, which is responsible for searching SNIPPET_ROOT for snippet definitions, validating them, and informing the build system about what needs doing as a result. Use this script in snippets.cmake to: - validate any discovered snippet.yml files - error out on undefined snippets - add a 'snippets' build system target that prints all snippet names (analogous to 'boards' and 'shields' targets) - handle any specific build system settings properly, by include()-ing a file it generates With this patch, you can define or extend a snippet in a snippet.yml file anywhere underneath a directory in SNIPPET_ROOT. The snippet.yml file format has a schema whose initial definition is in a new file, snippet-schema.yml. This initial snippet.yml file format supports adding .overlay and .conf files, like this: name: foo append: DTC_OVERLAY_FILE: foo.overlay OVERLAY_CONFIG: foo.conf boards: myboard: append: DTC_OVERLAY_FILE: myboard.overlay OVERLAY_CONFIG: myboard.conf /my-regular-expression-over-board-names/: append: DTC_OVERLAY_FILE: myregexp.overlay OVERLAY_CONFIG: myregexp.conf (Note that since the snippet feature is intended to be extensible, the same snippet name may appear in multiple files throughout any directory in SNIPPET_ROOT, with each addition augmenting prior ones.) This initial syntax aligns with the following snippet design goals: - extensible: you can add board-specific support for an existing snippet in another module - able to combine multiple types of configuration: we can now apply a .overlay and .conf at the same time - specializable: this allows you to define settings that only apply to a selectable set of boards (including with regular expression support for matching against multiple similar boards that follow a naming convention) - DRY: you can use regular expressions to apply the same snippet settings to multiple boards like this: /(board1|board2|...)/ This patch is not trying to design and implement everything up front. Additional features can and will be added to the snippet.yml format over time; using YAML as a format allows us to make backwards-compatible extensions as needed. Signed-off-by: Marti Bolivar <marti.bolivar@nordicsemi.no>
2023-01-08 00:27:07 +01:00
class SnippetsError(Exception):
'''Class for signalling expected errors'''
def __init__(self, msg):
self.msg = msg
class SnippetToCMakePrinter:
'''Helper class for printing a Snippets's semantics to a .cmake
include file for use by snippets.cmake.'''
def __init__(self, snippets: Snippets, out_file):
self.snippets = snippets
self.out_file = out_file
self.section = '#' * 79
def print_cmake(self):
'''Print to the output file provided to the constructor.'''
# TODO: add source file info
snippets = self.snippets
snippet_names = sorted(snippets.keys())
if platform.system() == "Windows":
# Change to linux-style paths for windows to avoid cmake escape character code issues
snippets.paths = set(map(lambda x: str(PurePosixPath(x)), snippets.paths))
for this_snippet in snippets:
for snippet_append in (snippets[this_snippet].appends):
snippets[this_snippet].appends[snippet_append] = \
set(map(lambda x: str(x.replace("\\", "/")), \
snippets[this_snippet].appends[snippet_append]))
snippets: initial snippet.yml support Add a new script, snippets.py, which is responsible for searching SNIPPET_ROOT for snippet definitions, validating them, and informing the build system about what needs doing as a result. Use this script in snippets.cmake to: - validate any discovered snippet.yml files - error out on undefined snippets - add a 'snippets' build system target that prints all snippet names (analogous to 'boards' and 'shields' targets) - handle any specific build system settings properly, by include()-ing a file it generates With this patch, you can define or extend a snippet in a snippet.yml file anywhere underneath a directory in SNIPPET_ROOT. The snippet.yml file format has a schema whose initial definition is in a new file, snippet-schema.yml. This initial snippet.yml file format supports adding .overlay and .conf files, like this: name: foo append: DTC_OVERLAY_FILE: foo.overlay OVERLAY_CONFIG: foo.conf boards: myboard: append: DTC_OVERLAY_FILE: myboard.overlay OVERLAY_CONFIG: myboard.conf /my-regular-expression-over-board-names/: append: DTC_OVERLAY_FILE: myregexp.overlay OVERLAY_CONFIG: myregexp.conf (Note that since the snippet feature is intended to be extensible, the same snippet name may appear in multiple files throughout any directory in SNIPPET_ROOT, with each addition augmenting prior ones.) This initial syntax aligns with the following snippet design goals: - extensible: you can add board-specific support for an existing snippet in another module - able to combine multiple types of configuration: we can now apply a .overlay and .conf at the same time - specializable: this allows you to define settings that only apply to a selectable set of boards (including with regular expression support for matching against multiple similar boards that follow a naming convention) - DRY: you can use regular expressions to apply the same snippet settings to multiple boards like this: /(board1|board2|...)/ This patch is not trying to design and implement everything up front. Additional features can and will be added to the snippet.yml format over time; using YAML as a format allows us to make backwards-compatible extensions as needed. Signed-off-by: Marti Bolivar <marti.bolivar@nordicsemi.no>
2023-01-08 00:27:07 +01:00
snippet_path_list = " ".join(
sorted(f'"{path}"' for path in snippets.paths))
self.print('''\
# WARNING. THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!
#
# This file contains build system settings derived from your snippets.
# Its contents are an implementation detail that should not be used outside
# of Zephyr's snippets CMake module.
#
# See the Snippets guide in the Zephyr documentation for more information.
''')
self.print(f'''\
{self.section}
# Global information about all snippets.
# The name of every snippet that was discovered.
set(SNIPPET_NAMES {' '.join(f'"{name}"' for name in snippet_names)})
# The paths to all the snippet.yml files. One snippet
# can have multiple snippet.yml files.
set(SNIPPET_PATHS {snippet_path_list})
# Create variable scope for snippets build variables
zephyr_create_scope(snippets)
snippets: initial snippet.yml support Add a new script, snippets.py, which is responsible for searching SNIPPET_ROOT for snippet definitions, validating them, and informing the build system about what needs doing as a result. Use this script in snippets.cmake to: - validate any discovered snippet.yml files - error out on undefined snippets - add a 'snippets' build system target that prints all snippet names (analogous to 'boards' and 'shields' targets) - handle any specific build system settings properly, by include()-ing a file it generates With this patch, you can define or extend a snippet in a snippet.yml file anywhere underneath a directory in SNIPPET_ROOT. The snippet.yml file format has a schema whose initial definition is in a new file, snippet-schema.yml. This initial snippet.yml file format supports adding .overlay and .conf files, like this: name: foo append: DTC_OVERLAY_FILE: foo.overlay OVERLAY_CONFIG: foo.conf boards: myboard: append: DTC_OVERLAY_FILE: myboard.overlay OVERLAY_CONFIG: myboard.conf /my-regular-expression-over-board-names/: append: DTC_OVERLAY_FILE: myregexp.overlay OVERLAY_CONFIG: myregexp.conf (Note that since the snippet feature is intended to be extensible, the same snippet name may appear in multiple files throughout any directory in SNIPPET_ROOT, with each addition augmenting prior ones.) This initial syntax aligns with the following snippet design goals: - extensible: you can add board-specific support for an existing snippet in another module - able to combine multiple types of configuration: we can now apply a .overlay and .conf at the same time - specializable: this allows you to define settings that only apply to a selectable set of boards (including with regular expression support for matching against multiple similar boards that follow a naming convention) - DRY: you can use regular expressions to apply the same snippet settings to multiple boards like this: /(board1|board2|...)/ This patch is not trying to design and implement everything up front. Additional features can and will be added to the snippet.yml format over time; using YAML as a format allows us to make backwards-compatible extensions as needed. Signed-off-by: Marti Bolivar <marti.bolivar@nordicsemi.no>
2023-01-08 00:27:07 +01:00
''')
for snippet_name in snippets.requested:
snippets: initial snippet.yml support Add a new script, snippets.py, which is responsible for searching SNIPPET_ROOT for snippet definitions, validating them, and informing the build system about what needs doing as a result. Use this script in snippets.cmake to: - validate any discovered snippet.yml files - error out on undefined snippets - add a 'snippets' build system target that prints all snippet names (analogous to 'boards' and 'shields' targets) - handle any specific build system settings properly, by include()-ing a file it generates With this patch, you can define or extend a snippet in a snippet.yml file anywhere underneath a directory in SNIPPET_ROOT. The snippet.yml file format has a schema whose initial definition is in a new file, snippet-schema.yml. This initial snippet.yml file format supports adding .overlay and .conf files, like this: name: foo append: DTC_OVERLAY_FILE: foo.overlay OVERLAY_CONFIG: foo.conf boards: myboard: append: DTC_OVERLAY_FILE: myboard.overlay OVERLAY_CONFIG: myboard.conf /my-regular-expression-over-board-names/: append: DTC_OVERLAY_FILE: myregexp.overlay OVERLAY_CONFIG: myregexp.conf (Note that since the snippet feature is intended to be extensible, the same snippet name may appear in multiple files throughout any directory in SNIPPET_ROOT, with each addition augmenting prior ones.) This initial syntax aligns with the following snippet design goals: - extensible: you can add board-specific support for an existing snippet in another module - able to combine multiple types of configuration: we can now apply a .overlay and .conf at the same time - specializable: this allows you to define settings that only apply to a selectable set of boards (including with regular expression support for matching against multiple similar boards that follow a naming convention) - DRY: you can use regular expressions to apply the same snippet settings to multiple boards like this: /(board1|board2|...)/ This patch is not trying to design and implement everything up front. Additional features can and will be added to the snippet.yml format over time; using YAML as a format allows us to make backwards-compatible extensions as needed. Signed-off-by: Marti Bolivar <marti.bolivar@nordicsemi.no>
2023-01-08 00:27:07 +01:00
self.print_cmake_for(snippets[snippet_name])
self.print()
def print_cmake_for(self, snippet: Snippet):
self.print(f'''\
{self.section}
# Snippet '{snippet.name}'
# Common variable appends.''')
self.print_appends(snippet.appends, 0)
for board, appends in snippet.board2appends.items():
self.print_appends_for_board(board, appends)
def print_appends_for_board(self, board: str, appends: Appends):
if board.startswith('/'):
board_re = board[1:-1]
self.print(f'''\
# Appends for board regular expression '{board_re}'
if("${{BOARD}}" MATCHES "^{board_re}$")''')
else:
self.print(f'''\
# Appends for board '{board}'
if("${{BOARD}}" STREQUAL "{board}")''')
self.print_appends(appends, 1)
self.print('endif()')
def print_appends(self, appends: Appends, indent: int):
space = ' ' * indent
for name, values in appends.items():
for value in values:
self.print(f'{space}zephyr_set({name} {value} SCOPE snippets APPEND)')
snippets: initial snippet.yml support Add a new script, snippets.py, which is responsible for searching SNIPPET_ROOT for snippet definitions, validating them, and informing the build system about what needs doing as a result. Use this script in snippets.cmake to: - validate any discovered snippet.yml files - error out on undefined snippets - add a 'snippets' build system target that prints all snippet names (analogous to 'boards' and 'shields' targets) - handle any specific build system settings properly, by include()-ing a file it generates With this patch, you can define or extend a snippet in a snippet.yml file anywhere underneath a directory in SNIPPET_ROOT. The snippet.yml file format has a schema whose initial definition is in a new file, snippet-schema.yml. This initial snippet.yml file format supports adding .overlay and .conf files, like this: name: foo append: DTC_OVERLAY_FILE: foo.overlay OVERLAY_CONFIG: foo.conf boards: myboard: append: DTC_OVERLAY_FILE: myboard.overlay OVERLAY_CONFIG: myboard.conf /my-regular-expression-over-board-names/: append: DTC_OVERLAY_FILE: myregexp.overlay OVERLAY_CONFIG: myregexp.conf (Note that since the snippet feature is intended to be extensible, the same snippet name may appear in multiple files throughout any directory in SNIPPET_ROOT, with each addition augmenting prior ones.) This initial syntax aligns with the following snippet design goals: - extensible: you can add board-specific support for an existing snippet in another module - able to combine multiple types of configuration: we can now apply a .overlay and .conf at the same time - specializable: this allows you to define settings that only apply to a selectable set of boards (including with regular expression support for matching against multiple similar boards that follow a naming convention) - DRY: you can use regular expressions to apply the same snippet settings to multiple boards like this: /(board1|board2|...)/ This patch is not trying to design and implement everything up front. Additional features can and will be added to the snippet.yml format over time; using YAML as a format allows us to make backwards-compatible extensions as needed. Signed-off-by: Marti Bolivar <marti.bolivar@nordicsemi.no>
2023-01-08 00:27:07 +01:00
def print(self, *args, **kwargs):
kwargs['file'] = self.out_file
print(*args, **kwargs)
# Name of the file containing the pykwalify schema for snippet.yml
# files.
SCHEMA_PATH = str(Path(__file__).parent / 'schemas' / 'snippet-schema.yml')
with open(SCHEMA_PATH, 'r') as f:
SNIPPET_SCHEMA = yaml.safe_load(f.read())
# The name of the file which contains metadata about the snippets
# being defined in a directory.
SNIPPET_YML = 'snippet.yml'
# Regular expression for validating snippet names. Snippet names must
# begin with an alphanumeric character, and may contain alphanumeric
# characters or underscores. This is intentionally very restrictive to
# keep things consistent and easy to type and remember. We can relax
# this a bit later if needed.
SNIPPET_NAME_RE = re.compile('[A-Za-z0-9][A-Za-z0-9_-]*')
# Logger for this module.
LOG = logging.getLogger('snippets')
def _err(msg):
raise SnippetsError(f'error: {msg}')
def parse_args():
parser = argparse.ArgumentParser(description='snippets helper',
allow_abbrev=False)
parser.add_argument('--snippet-root', default=[], action='append', type=Path,
help='''a SNIPPET_ROOT element; may be given
multiple times''')
parser.add_argument('--snippet', dest='snippets', default=[], action='append',
help='''a SNIPPET element; may be given
multiple times''')
parser.add_argument('--cmake-out', type=Path,
help='''file to write cmake output to; include()
this file after calling this script''')
return parser.parse_args()
def setup_logging():
# Silence validation errors from pykwalify, which are logged at
# logging.ERROR level. We want to handle those ourselves as
# needed.
logging.getLogger('pykwalify').setLevel(logging.CRITICAL)
logging.basicConfig(level=logging.INFO,
format=' %(name)s: %(message)s')
def process_snippets(args: argparse.Namespace) -> Snippets:
'''Process snippet.yml files under each *snippet_root*
by recursive search. Return a Snippets object describing
the results of the search.
'''
# This will contain information about all the snippets
# we discover in each snippet_root element.
snippets = Snippets(requested=args.snippets)
# Process each path in snippet_root in order, adjusting
# snippets as needed for each one.
for root in args.snippet_root:
process_snippets_in(root, snippets)
return snippets
def process_snippets_in(root_dir: Path, snippets: Snippets) -> None:
'''Process snippet.yml files in *root_dir*,
updating *snippets* as needed.'''
if not root_dir.is_dir():
LOG.warning(f'SNIPPET_ROOT {root_dir} '
'is not a directory; ignoring it')
return
snippets_dir = root_dir / 'snippets'
if not snippets_dir.is_dir():
return
for dirpath, _, filenames in os.walk(snippets_dir):
if SNIPPET_YML not in filenames:
continue
snippet_yml = Path(dirpath) / SNIPPET_YML
snippet_data = load_snippet_yml(snippet_yml)
name = snippet_data['name']
if name not in snippets:
snippets[name] = Snippet(name=name)
snippets[name].process_data(snippet_yml, snippet_data)
snippets.paths.add(snippet_yml)
def load_snippet_yml(snippet_yml: Path) -> dict:
'''Load a snippet.yml file *snippet_yml*, validate the contents
against the schema, and do other basic checks. Return the dict
of the resulting YAML data.'''
with open(snippet_yml, 'r') as f:
try:
snippet_data = yaml.safe_load(f.read())
except yaml.scanner.ScannerError:
_err(f'snippets file {snippet_yml} is invalid YAML')
def pykwalify_err(e):
return f'''\
invalid {SNIPPET_YML} file: {snippet_yml}
{textwrap.indent(e.msg, ' ')}
'''
try:
pykwalify.core.Core(source_data=snippet_data,
schema_data=SNIPPET_SCHEMA).validate()
except pykwalify.errors.PyKwalifyException as e:
_err(pykwalify_err(e))
name = snippet_data['name']
if not SNIPPET_NAME_RE.fullmatch(name):
_err(f"snippet file {snippet_yml}: invalid snippet name '{name}'; "
'snippet names must begin with a letter '
'or number, and may only contain letters, numbers, '
'dashes (-), and underscores (_)')
return snippet_data
def check_for_errors(snippets: Snippets) -> None:
unknown_snippets = sorted(snippet for snippet in snippets.requested
if snippet not in snippets)
if unknown_snippets:
all_snippets = '\n '.join(sorted(snippets))
_err(f'''\
snippets not found: {', '.join(unknown_snippets)}
Please choose from among the following snippets:
{all_snippets}''')
def write_cmake_out(snippets: Snippets, cmake_out: Path) -> None:
'''Write a cmake include file to *cmake_out* which
reflects the information in *snippets*.
The contents of this file should be considered an implementation
detail and are not meant to be used outside of snippets.cmake.'''
if not cmake_out.parent.exists():
cmake_out.parent.mkdir()
with open(cmake_out, 'w') as f:
SnippetToCMakePrinter(snippets, f).print_cmake()
def main():
args = parse_args()
setup_logging()
try:
snippets = process_snippets(args)
check_for_errors(snippets)
except SnippetsError as e:
LOG.critical(e.msg)
sys.exit(1)
write_cmake_out(snippets, args.cmake_out)
if __name__ == "__main__":
main()