Files
mbedtls-framework/scripts/mbedtls_framework/generate_files_helper.py
T
Gilles Peskine d85217d8f5 Support test data generators in generate_files_helper.py
Support check and always-update mode. Update-if-needed mode falls back to
always-update mode because test_data_generation.py doesn't support
update-if-needed.

Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
2026-02-19 18:24:08 +01:00

183 lines
6.8 KiB
Python

"""Utilities for intermediate files that are generated, but platform-independent
and configuration-independent.
"""
# Copyright The Mbed TLS Contributors
# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
import argparse
import os
import subprocess
import sys
from typing import Dict, Iterable, List, Sequence, Set
class Generator:
"""An abstract base class for generators of intermediate files."""
def generator_name(self) -> str:
"""A name for this generator.
Generator names must be unique and should not be identical to
the name of any target.
"""
raise NotImplementedError
def target_files(self) -> List[str]:
"""The list of files targeted by this generator.
File names are relative to the project root.
"""
raise NotImplementedError
def outdated_files(self) -> Iterable[str]:
"""Return the list of targets that are out of date.
This is empty after running update().
Missing targets are considered out of date.
"""
raise NotImplementedError
def update(self, always: bool) -> None:
"""Update the target(s) of this generator.
If always is false, avoid changing the output file if it already has
the desired content. If always is true, make sure to update the
time stamp on the output file even if it already has the desired content.
"""
raise NotImplementedError
class TestDataGenerator(Generator):
"""A test data generator script.
Even though the test data generator scripts are written in Python, we
run them as a separate process, because their output depends on the
program name (they write sys.argv[0] in a comment in the .data file).
"""
def __init__(self, script: str) -> None:
"""Run the specified test generator to generate files.
Assume that the script is written in Python and has the command line
interface of test_data_generation.py.
"""
self.script = script
def generator_name(self) -> str:
return os.path.basename(self.script)
def target_files(self) -> List[str]:
output = subprocess.check_output([sys.executable, self.script, '--list'],
encoding='utf-8')
return output.splitlines()
def outdated_files(self) -> List[str]:
output = subprocess.check_output([sys.executable, self.script, '--list-outdated'],
encoding='utf-8')
return output.splitlines()
def update(self, _always) -> None:
subprocess.check_call([sys.executable, self.script])
def assemble(available: Iterable[Generator]) -> Dict[str, Generator]:
"""Assemble the generators into a dictionary with both names and targets as keys."""
by_ident = {} #type: Dict[str, Generator]
for generator in available:
ident = generator.generator_name()
if ident in by_ident:
raise Exception(f'Generator conflict: name "{ident}" of {generator} '
f'already recorded for {by_ident[ident]}')
by_ident[ident] = generator
for ident in generator.target_files():
if ident in by_ident:
raise Exception(f'Generator conflict: target "{ident}" of {generator} '
f'already recorded for {by_ident[ident]}')
by_ident[ident] = generator
return by_ident
def list_names(available: Iterable[Generator]) -> List[str]:
"""Return the list of generator names."""
return sorted(generator.generator_name() for generator in available)
def list_targets(available: Iterable[Generator]) -> List[str]:
"""Return the list of generator targets."""
return sorted(target
for generator in available
for target in generator.target_files())
def select(available: Dict[str, Generator],
wanted: Iterable[str]) -> List[Generator]:
"""Select generators by name or target."""
wanted_names = set() #type: Set[str]
for ident in wanted:
if ident not in available:
raise Exception(f'No generator found for {ident}')
wanted_names.add(ident)
return [available[name] for name in sorted(wanted_names)]
def main(generators: Sequence[Generator],
description: str) -> None:
#pylint: disable=too-many-branches
"""Command line entry point.
"""
parser = argparse.ArgumentParser(description=description)
parser.add_argument('--always-update', '-U',
action='store_true',
help=('Update target files unconditionally '
'(overrides --update)'))
parser.add_argument('--list',
action='store_true',
help='List generator names and targets and exit')
parser.add_argument('--list-names',
action='store_true',
help='List generator names and exit')
parser.add_argument('--list-targets',
action='store_true',
help='List generator targets and exit')
parser.add_argument('--update', '-u',
action='store_true',
help='Update target files if needed')
parser.add_argument('--verbose', '-v',
action='store_true',
help='Be more verbose')
parser.add_argument('idents', nargs='*', metavar='NAME|TARGET',
help='List of generator names or targets (all targets if empty)')
args = parser.parse_args()
if args.list:
args.list_names = True
args.list_targets = True
if args.list_names:
for name in list_names(generators):
print(name)
if args.list_targets:
for target in list_targets(generators):
print(target)
if args.list_names or args.list_targets:
return
if args.idents:
available = assemble(generators)
wanted = select(available, args.idents) #type: Sequence[Generator]
else:
wanted = generators
if args.update or args.always_update:
for generator in wanted:
if args.verbose:
sys.stderr.write(f'Running generator {generator.generator_name()}...\n')
generator.update(args.always_update)
else:
outdated = [] #type: List[str]
for generator in wanted:
if args.verbose:
sys.stderr.write(f'Checking targets of generator {generator.generator_name()}...\n')
outdated += generator.outdated_files()
if outdated:
sys.stderr.write(f'Some targets are missing or out of date.\n')
for target in outdated:
print(target)
sys.stderr.write(f'Run {sys.argv[0]} -u and commit the result.')
sys.exit(1)