Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reimplement impl with fpp-to-cpp #171

Merged
merged 17 commits into from
Oct 26, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ cmake-build-*
build-artifacts/
build-fprime-*
*-template
*.template.cpp
*.template.hpp

# Misc
/venv/
Expand Down
18 changes: 0 additions & 18 deletions src/fprime/fbuild/target_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/fprime/fpp/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}, {
Expand Down
59 changes: 40 additions & 19 deletions src/fprime/fpp/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?"
)


Expand All @@ -33,17 +33,22 @@ class FppUtility(ExecutableAction):
in a similar order across utilities. This action executes these utilities via a subprocess using the command line
format:

<utility> <user supplied args> <locations file> <fpp inputs>
<utility> <user supplied args> [<fpp imports>] <locations file> <fpp inputs>

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:
name: name of the fpp utility to run
"""
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"""
Expand All @@ -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
Expand All @@ -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]]
Expand Down Expand Up @@ -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
160 changes: 160 additions & 0 deletions src/fprime/fpp/impl.py
Original file line number Diff line number Diff line change
@@ -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}
2 changes: 1 addition & 1 deletion src/fprime/fpp/visualize.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
4 changes: 4 additions & 0 deletions src/fprime/util/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -303,12 +304,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
Expand Down
2 changes: 1 addition & 1 deletion src/fprime/util/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading
Loading