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
9 changes: 9 additions & 0 deletions src/fprime/fbuild/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,15 @@ def is_project_root(self, context: Path) -> bool:
except CMakeException:
return False

def is_submodule_build_structure(self) -> bool:
"""Returns whether the F´ source code is a parent of the build.

Returns:
bool: True if the F´ code is a externally linked directory (e.g. a submodule)
False if the build is self-contained within the F´ code (e.g. the Ref app)
"""
return self.get_settings("framework_path", "") not in self.cmake_root.parents

def find_toolchain(self):
"""Locates a toolchain file in know locations

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
58 changes: 39 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,24 @@ 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 = [
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(source) for source in sources])
input_args.append(str(locations))
thomas-bc marked this conversation as resolved.
Show resolved Hide resolved

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
161 changes: 161 additions & 0 deletions src/fprime/fpp/impl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
""" 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.build_dir / "F-Prime",
build.build_dir,
]
if build.is_submodule_build_structure():
Copy link
Collaborator

@LeStarch LeStarch Oct 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? Also, this should not use "cmake root", but build.get_settings("project_path") should it not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes absolutely, my mistake. By using project_root, we don't need to check whether the build structure is old or new, as project_root and framework_path will simply be the same. Note, both of these values are guaranteed to exist as per the following:
https://github.com/fprime-community/fprime-tools/blob/4fc88dc0a20fcadd5b09bb5a188bc41373237d5d/src/fprime/fbuild/settings.py#L52-L53

since find_fprime will error out if it can't find framework_location

prefixes.append(build.cmake_root)

# 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",
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