diff --git a/src/fprime/cookiecutter_templates/cookiecutter-fprime-project/{{cookiecutter.project_name}}/.gitignore b/src/fprime/cookiecutter_templates/cookiecutter-fprime-project/{{cookiecutter.project_name}}/.gitignore index 941f3179..62de17db 100644 --- a/src/fprime/cookiecutter_templates/cookiecutter-fprime-project/{{cookiecutter.project_name}}/.gitignore +++ b/src/fprime/cookiecutter_templates/cookiecutter-fprime-project/{{cookiecutter.project_name}}/.gitignore @@ -4,6 +4,8 @@ cmake-build-* build-artifacts/ build-fprime-* *-template +*.template.cpp +*.template.hpp # Misc /venv/ diff --git a/src/fprime/fbuild/target_definitions.py b/src/fprime/fbuild/target_definitions.py index 8b4c706f..b587dd1a 100644 --- a/src/fprime/fbuild/target_definitions.py +++ b/src/fprime/fbuild/target_definitions.py @@ -32,24 +32,6 @@ build_type=BuildType.BUILD_TESTING, ) - -#### Implementation targets #### -BuildSystemTarget( - "impl", - mnemonic="impl", - desc="Generate implementation template files", - scope=TargetScope.LOCAL, -) -BuildSystemTarget( - "testimpl", - mnemonic="impl", - desc="Generate unit test files", - flags={"ut"}, - build_type=BuildType.BUILD_TESTING, - scope=TargetScope.LOCAL, -) - - #### Check targets #### check = BuildSystemTarget( "check", diff --git a/src/fprime/fpp/cli.py b/src/fprime/fpp/cli.py index bcaebe31..6ad36add 100644 --- a/src/fprime/fpp/cli.py +++ b/src/fprime/fpp/cli.py @@ -74,13 +74,13 @@ def add_fpp_parsers( Tuple of dictionary mapping command name to processor, and command to parser """ check_parser = subparsers.add_parser( - "fpp-check", help="Runs fpp-check utility", parents=[common], add_help=False + "fpp-check", help="Run fpp-check utility", parents=[common], add_help=False ) check_parser.add_argument( "-u", "--unconnected", default=None, help="write unconnected ports to file" ) fpp_to_xml_parser = subparsers.add_parser( - "fpp-to-xml", help="Runs fpp-to-xml utility", parents=[common], add_help=False + "fpp-to-xml", help="Run fpp-to-xml utility", parents=[common], add_help=False ) fpp_to_xml_parser.add_argument("--directory", default=None, help="Output directory") return {"fpp-check": run_fpp_check, "fpp-to-xml": run_fpp_to_xml}, { diff --git a/src/fprime/fpp/common.py b/src/fprime/fpp/common.py index 3ddc1e4d..949b2c58 100644 --- a/src/fprime/fpp/common.py +++ b/src/fprime/fpp/common.py @@ -21,7 +21,7 @@ class FppMissingSupportFiles(FprimeException): def __init__(self, file): super().__init__( - "Current directory does not define any FPP files. Did you intend to run in the topology directory?" + f"Can not find {file}. Does current directory define any FPP files?" ) @@ -33,10 +33,14 @@ class FppUtility(ExecutableAction): in a similar order across utilities. This action executes these utilities via a subprocess using the command line format: - + [] + + Some fpp utilities distinguish between import files (--import flag) and source files. Those utilities should set + the imports_as_sources flag to False. This will cause the utility to pass the import files as --import arguments. + If imports_as_sources is True, the import files are passed as inputs just like source files. """ - def __init__(self, name): + def __init__(self, name, imports_as_sources=True): """Construct this utility with the supplied name Args: @@ -44,6 +48,7 @@ def __init__(self, name): """ super().__init__(TargetScope.LOCAL) self.utility = name + self.imports_as_sources = imports_as_sources def is_supported(self, _=None, __=None): """Returns whether this utility is supported""" @@ -67,7 +72,7 @@ def get_locations_file(builder: Build) -> Path: return locations_path @staticmethod - def get_fpp_inputs(builder: Build, context: Path) -> List[Path]: + def get_fpp_inputs(builder: Build, context: Path) -> Tuple[List[Path], List[Path]]: """Return the necessary inputs to an FPP run to forward to fpp utilities Returns two types of FPP files input into FPP utilities: the FPP files associated with the given module and the @@ -79,16 +84,24 @@ def get_fpp_inputs(builder: Build, context: Path) -> List[Path]: context: context path of module containing the FPP files Return: - list of module FPP files and included FPP files + tuple of two lists: module source FPP files and included FPP files """ cache_location = builder.get_build_cache_path(context) - input_file = cache_location / "fpp-input-list" - if not input_file.exists(): - raise FppMissingSupportFiles(input_file) - with open(input_file, "r") as file_handle: - return [ + import_file = cache_location / "fpp-import-list" + source_file = cache_location / "fpp-source-list" + if not import_file.exists(): + raise FppMissingSupportFiles(import_file) + if not source_file.exists(): + raise FppMissingSupportFiles(source_file) + with open(import_file, "r") as file_handle: + import_list = [ + Path(token) for token in file_handle.read().split(";") if token != "" + ] + with open(source_file, "r") as file_handle: + source_list = [ Path(token) for token in file_handle.read().split(";") if token != "" ] + return (import_list, source_list) def execute( self, builder: Build, context: Path, args: Tuple[Dict[str, str], List[str]] @@ -116,17 +129,25 @@ def execute( # Read files and arguments locations = self.get_locations_file(builder) - inputs = self.get_fpp_inputs(builder, context) + imports, sources = self.get_fpp_inputs(builder, context) - if not inputs: - print("[WARNING] No FPP inputs found in this module.") + if not sources: + print("[WARNING] No FPP sources found in this module.") + + # Build the input argument list + input_args = [] + if self.imports_as_sources: + input_args.extend( + str(item) for item in itertools.chain([locations] + imports + sources) + ) + else: + input_args.extend(["-i", ",".join(map(str, imports))] if imports else []) + input_args.extend( + str(item) for item in itertools.chain([locations] + sources) + ) user_args = args[1] - app_args = ( - [self.utility] - + user_args - + [str(item) for item in itertools.chain([locations] + inputs)] - ) + app_args = [self.utility] + user_args + input_args if builder.cmake.verbose: print(f"[FPP] '{' '.join(app_args)}'") - subprocess.run(app_args, capture_output=False) + return subprocess.run(app_args, capture_output=False).returncode diff --git a/src/fprime/fpp/impl.py b/src/fprime/fpp/impl.py new file mode 100644 index 00000000..10e4717d --- /dev/null +++ b/src/fprime/fpp/impl.py @@ -0,0 +1,160 @@ +""" fprime.fpp.impl: Command line targets for `fprime-util impl` + +Processing and CLI entry points for `fprime-util impl` command line tool. + +@author thomas-bc +""" + + +import argparse +import os +import tempfile +from pathlib import Path + +from typing import TYPE_CHECKING, Callable, Dict, List, Tuple + +if TYPE_CHECKING: + from fprime.fbuild.builder import Build + +from fprime.fpp.common import FppUtility +from fprime.util.code_formatter import ClangFormatter + + +def fpp_generate_implementation( + build: "Build", + output_dir: Path, + context: Path, + apply_formatting: bool, + generate_ut: bool, + auto_test_helpers: bool = False, +) -> int: + """ + Generate implementation files from FPP templates. + + Args: + build: Build object + output_dir: The directory where the generated files will be written + context: The path to the F´ module to generate files for + apply_formatting: Whether to format the generated files using clang-format + ut: Generates UT files if set to True + """ + + prefixes = [ + build.get_settings("framework_path", ""), + *build.get_settings("library_locations", []), + build.get_settings("project_root", ""), + build.build_dir / "F-Prime", + build.build_dir, + ] + + # Holds the list of generated files to be passed to clang-format + gen_files = tempfile.NamedTemporaryFile(prefix="fprime-impl-") + + output_dir.mkdir(parents=True, exist_ok=True) + + # Run fpp-to-cpp --template + FppUtility("fpp-to-cpp", imports_as_sources=False).execute( + build, + context, + args=( + {}, + [ + "--template", + *(["--unit-test"] if generate_ut else []), + *(["--auto-test-helpers"] if auto_test_helpers else []), + "--names", + gen_files.name, + "--directory", + str(output_dir), + "--path-prefixes", + ",".join(map(str, prefixes)), + ], + ), + ) + + # Format files if clang-format is available + format_file = build.settings.get("framework_path", Path(".")) / ".clang-format" + if not format_file.is_file(): + print( + f"[INFO] .clang-format file not found at {format_file.resolve()}. Skipping formatting." + ) + return 0 + + clang_formatter = ClangFormatter("clang-format", format_file, {"backup": False}) + if apply_formatting and clang_formatter.is_supported(): + for line in gen_files.readlines(): + # FPP --names outputs a list of file names. output_dir is added to get relative path + filename = Path(line.decode("utf-8").strip()) + clang_formatter.stage_file(output_dir / filename) + clang_formatter.execute(None, None, ({}, [])) + + return 0 + + +def run_fpp_impl( + build: "Build", + parsed: argparse.Namespace, + _: Dict[str, str], + __: Dict[str, str], + ___: List[str], +): + """ + + Args: + build: build object + parsed: parsed input arguments + _: unused cmake_args + __: unused make_args + ___: unused pass-through arguments + """ + + return fpp_generate_implementation( + build, + Path(parsed.output_dir), + Path(parsed.path), + not parsed.no_format, + parsed.ut, + parsed.auto_test_helpers, + ) + + +def add_fpp_impl_parsers( + subparsers, common: argparse.ArgumentParser +) -> Tuple[Dict[str, Callable], Dict[str, argparse.ArgumentParser]]: + """Sets up the fprime-viz command line parsers + + Creates command line parsers for fprime-viz commands and associates these commands to processing functions for those fpp + commands. + + Args: + subparsers: subparsers to add to + common: common parser for all fprime-util commands + + Returns: + Tuple of dictionary mapping command name to processor, and command to parser + """ + impl_parser = subparsers.add_parser( + "impl", + help="Generate implementation templates", + parents=[common], + add_help=False, + ) + impl_parser.add_argument( + "--output-dir", + help="Directory to generate files in. Default: cwd", + required=False, + default=os.getcwd(), + ) + impl_parser.add_argument( + "--no-format", + action="store_true", + help="Disable formatting (using clang-format) of generated files", + required=False, + ) + impl_parser.add_argument( + "--auto-test-helpers", + action="store_true", + help="Enable automatic generation of test helper code", + required=False, + ) + return {"impl": run_fpp_impl}, {"impl": impl_parser} diff --git a/src/fprime/fpp/visualize.py b/src/fprime/fpp/visualize.py index fac2ff22..a1c092fc 100644 --- a/src/fprime/fpp/visualize.py +++ b/src/fprime/fpp/visualize.py @@ -149,7 +149,7 @@ def add_fpp_viz_parsers( """ viz_parser = subparsers.add_parser( "visualize", - help="Runs visualization pipeline", + help="Visualize FPP model in a web GUI", parents=[common], add_help=False, ) diff --git a/src/fprime/util/cli.py b/src/fprime/util/cli.py index 04a9d9d6..786acd71 100644 --- a/src/fprime/util/cli.py +++ b/src/fprime/util/cli.py @@ -19,6 +19,7 @@ from fprime.util.commands import run_code_format, run_hash_to_file, run_info, run_new from fprime.util.help_text import HelpText from fprime.fpp.visualize import add_fpp_viz_parsers +from fprime.fpp.impl import add_fpp_impl_parsers def utility_entry(args): @@ -309,12 +310,15 @@ def parse_args(args): ) fpp_runners, fpp_parsers = add_fpp_parsers(subparsers, common_parser) viz_runners, viz_parsers = add_fpp_viz_parsers(subparsers, common_parser) + impl_runners, impl_parsers = add_fpp_impl_parsers(subparsers, common_parser) parsers.update(fbuild_parsers) parsers.update(fpp_parsers) parsers.update(viz_parsers) + parsers.update(impl_parsers) runners.update(fbuild_runners) runners.update(fpp_runners) runners.update(viz_runners) + runners.update(impl_runners) runners.update(add_special_parsers(subparsers, common_parser, HelpText)) # Parse and prepare to run diff --git a/src/fprime/util/commands.py b/src/fprime/util/commands.py index 0245ee49..a026a6db 100644 --- a/src/fprime/util/commands.py +++ b/src/fprime/util/commands.py @@ -126,7 +126,7 @@ def run_new( ___: unused pass through arguments """ if parsed.new_component: - return new_component(build) + return new_component(build, parsed) if parsed.new_deployment: return new_deployment(build, parsed) if parsed.new_project: diff --git a/src/fprime/util/cookiecutter_wrapper.py b/src/fprime/util/cookiecutter_wrapper.py index 8c0991af..cc112085 100644 --- a/src/fprime/util/cookiecutter_wrapper.py +++ b/src/fprime/util/cookiecutter_wrapper.py @@ -13,65 +13,23 @@ from fprime.common.utils import confirm from fprime.fbuild.builder import Build from fprime.fbuild.cmake import CMakeExecutionException -from fprime.fbuild.target import Target -from fprime.util.code_formatter import ClangFormatter +from fprime.fpp.impl import fpp_generate_implementation -def run_impl(build: Build, source_path: Path): +def run_impl(build: Build, parsed_args, source_path: Path): """Run implementation of files in source_path""" - target = Target.get_target("impl", set()) - - hpp_files = glob.glob(f"{source_path}/*.hpp", recursive=False) - cpp_files = glob.glob(f"{source_path}/*.cpp", recursive=False) - cpp_files.sort(key=len) - - # Check destinations - if not hpp_files or not cpp_files: - print( - "[WARNING] Failed to find .cpp and .hpp destination files for implementation." - ) - return False - - common = [name for name in cpp_files if "Common" in name] + [] - hpp_dest = hpp_files[0] - cpp_dest = common[0] if common else cpp_files[0] - if not confirm("Generate implementation files?"): return False - print( - "Refreshing cache and generating implementation files (ignore 'Stop' CMake warning)..." - ) - with suppress_stdout(): - target.execute(build, source_path, ({}, [], {})) - - hpp_files_template = glob.glob(f"{source_path}/*.hpp-template", recursive=False) - cpp_files_template = glob.glob(f"{source_path}/*.cpp-template", recursive=False) - - if not hpp_files_template or not cpp_files_template: - print("[WARNING] Failed to find generated .cpp-template or .hpp-template files") - return False + print("Refreshing cache and generating implementation files...") - hpp_src = hpp_files_template[0] - cpp_src = cpp_files_template[0] - - # Move (and overwrite) files from *.(c|h)pp-template to *.(c|h)pp - shutil.move(hpp_src, hpp_dest) - shutil.move(cpp_src, cpp_dest) + with suppress_stdout(): + fpp_generate_implementation(build, source_path, source_path, True, False) - # Format files if clang-format is available - format_file = build.settings.get("framework_path", Path(".")) / ".clang-format" - if not format_file.is_file(): - print( - f"[WARNING] .clang-format file not found at {format_file.resolve()}. Skipping formatting." - ) - return True - clang_formatter = ClangFormatter("clang-format", format_file, {"backup": False}) - if clang_formatter.is_supported(): - clang_formatter.stage_file(Path(hpp_dest)) - clang_formatter.stage_file(Path(cpp_dest)) - clang_formatter.execute(None, None, ({}, [])) - else: - print("[WARNING] clang-format not found in PATH. Skipping formatting.") + # Path(source_path). + file_list = glob.glob(f"{source_path}/*.template.*pp", recursive=False) + for filename in file_list: + new_filename = filename.replace(".template", "") + os.rename(filename, new_filename) return True @@ -152,7 +110,7 @@ def find_nearest_cmake_file(component_dir: Path, cmake_root: Path, proj_root: Pa return None -def new_component(build: Build): +def new_component(build: Build, parsed_args: "argparse.Namespace"): """Uses cookiecutter for making new components""" try: proj_root = build.get_settings("project_root", None) @@ -201,7 +159,7 @@ def new_component(build: Build): ) return 0 # Attempt implementation - if not run_impl(build, final_component_dir): + if not run_impl(build, parsed_args, final_component_dir): print( f"[INFO] Did not generate implementations for {final_component_dir}. Please do so manually." ) diff --git a/src/fprime/util/help_text.py b/src/fprime/util/help_text.py index a9a41e1f..a3c1c315 100644 --- a/src/fprime/util/help_text.py +++ b/src/fprime/util/help_text.py @@ -108,7 +108,7 @@ development process. One of these commands should be the first argument to an invocation of {EXECUTABLE}. For more explanation on an individual command, please run '{EXECUTABLE} --help>'. """, - "build": f"""{EXECUTABLE} build ({VERSION}): Build fprime components, deployments, and unit tests + "build": f"""Build fprime components, deployments, and unit tests '{EXECUTABLE} build' builds in the selected directory ('-p/--path' or current working directory). When the selected directory contains a component, the component is built. When the current directory contains a deployment, the deployment @@ -131,16 +131,16 @@ {EXECUTABLE} build --ut """, - "impl": f"""{EXECUTABLE} impl ({VERSION}): Generate fprime implementation templates. + "impl": f"""Generate fprime implementation templates. '{EXECUTABLE} impl' generates the implementation templates for the specified directory ('-p/--path' or current working directory). Implementation generation is only available for component directories and should not be used on deployments. -Two files will be created .cpp-template and .hpp-template. These contain the fill-in base code for +Two files will be created .template.cpp and .template.hpp. These contain the fill-in base code for the component implementation as defined by the component's model. -When the '--ut' flag is specified the unit test implementation templates are created instead. The user should use the -Tester.cpp, Tester.hpp and TestMain.cpp files as their fill-in templates. Other files created can be safely removed as -they will be regenerated at build time. +When the '--ut' flag is specified the unit test implementation templates are created instead, under /test/ut. The +user should use the Tester.cpp, Tester.hpp and TestMain.cpp files as their fill-in templates. Other files created can be +safely removed as they will be regenerated at build time. Example: @@ -153,7 +153,7 @@ {EXECUTABLE} impl --ut """, - "check": f"""{EXECUTABLE} check ({VERSION}): Run fprime unit tests with optional leak checking and test coverage. + "check": f"""Run fprime unit tests with optional leak checking and test coverage. '{EXECUTABLE} check' handles the running of unit tests. It can be used on components to run the component's unit tests, deployments to run deployment unit tests, and with the '--all' flag to run all unit tests found in the build system. @@ -186,7 +186,7 @@ cd Ref/SignalGen {EXECUTABLE} check --coverage """, - "generate": f"""{EXECUTABLE} generate ({VERSION}): Generate build caches for the specified deployment + "generate": f"""Generate build caches for the specified deployment '{EXECUTABLE} generate' is used to setup a build cache to support other commands run by {EXECUTABLE}. Without additional arguments a build cache will be created for the project in the specified directory ('-p/--path', or current working @@ -236,7 +236,7 @@ {EXECUTABLE} generate --build-cache `pwd`/build-ref-with-baremetal -DFPRIME_USE_BAREMETAL_SCHEDULER=ON """, - "purge": f"""{EXECUTABLE} purge ({VERSION}): Removes build caches for specified project + "purge": f"""Remove build caches for specified project '{EXECUTABLE} purge' removes build caches for the specified project. It also removes the build_artifacts directory in that project as well. Caches are searched in pairs: normal build cache, paired unit testing build cache. The @@ -258,7 +258,7 @@ '{EXECUTABLE} info' will print information for both normal and unit testing builds when possible. If '--build-cache' is specified then only the information for that build cache will be printed. """, - "hash-to-file": f"""{EXECUTABLE} hash-to-file ({VERSION}): Convert FW_ASSERT file id hash to file path + "hash-to-file": f"""Convert FW_ASSERT file id hash to file path When a project compiles fprime with 'FW_ASSERT_LEVEL' set to 'FW_FILEID_ASSERT' a hash will be emitted in place of the file path in order to keep assert messages succinct. '{EXECUTABLE} hash-to-file ' will convert this hash value to @@ -276,7 +276,7 @@ cd Ref {EXECUTABLE} hash-to-file raspberrypi 0xABCD1234 """, - "new": f"""{EXECUTABLE} new ({VERSION}): -- Creates a new fprime object + "new": f"""Generate a new fprime object '{EXECUTABLE} new' runs a wizard to create new objects in fprime (component, deployment, project). @@ -294,7 +294,7 @@ include. At the end of the generation, the user can chose to automatically add the component to the build system and run the implementation generator. """, - "format": f"""{EXECUTABLE} format ({VERSION}): Formats C/C++ files using clang-format + "format": f"""Format C/C++ files using clang-format '{EXECUTABLE} format' uses 'clang-format' to format C/C++ files. It uses the style specified in the .clang-format file found at the root of the F' framework used by the project (i.e. the 'framework_path' specified in settings.ini).