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

lestarch: switching version checking to use requirements.txt #85

Merged
merged 2 commits into from
Jun 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ Utils
valgrind
vals
venv
versioning
viewcode
whitelist
workdir
Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Build Package
on:
release:
types: [published]
jobs:
Build-PyPI-Package:
runs-on: ubuntu-latest
steps:
- name: Test PyPI
uses: fprime-community/publish-pypi@main
env:
TWINE_PASSWORD: ${{ secrets.TESTPYPI_CREDENTIAL }}
with:
package: "fprime-tools"
steps: "sdist"
- name: PyPI
uses: fprime-community/publish-pypi@main
env:
TWINE_PASSWORD: ${{ secrets.PYPI_CREDENTIAL }}
with:
repo: "pypi"
package: "fprime-tools"
steps: "sdist"
5 changes: 4 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@
tests_require=["pytest"],
# Create a set of executable entry-points for running directly from the package
entry_points={
"console_scripts": ["fprime-util = fprime.util.__main__:main"],
"console_scripts": [
"fprime-util = fprime.util.__main__:main",
"fprime-version-check = fprime.util.versioning:main",
],
"gui_scripts": [],
},
)
1 change: 0 additions & 1 deletion src/fprime/fbuild/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ def run_fbuild_cli(
make_args: arguments to the make system
"""
if parsed.command == "generate":
build.invent(parsed.platform, build_dir=parsed.build_cache)
toolchain = build.find_toolchain()
print(f"[INFO] Generating build directory at: {build.build_dir}")
print(f"[INFO] Using toolchain file {toolchain} for platform {parsed.platform}")
Expand Down
84 changes: 62 additions & 22 deletions src/fprime/util/build_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import re
import sys
from pathlib import Path
from typing import Union

from fprime.fbuild.types import InvalidBuildCacheException
from fprime.fbuild.target import Target, NoSuchTargetException
Expand All @@ -28,6 +29,7 @@
UnableToDetectDeploymentException,
)
from .help_text import HelpText
from .versioning import get_version, VersionException
from fprime.fbuild.cli import add_fbuild_parsers, get_target
from fprime.fpp.cli import add_fpp_parsers
from fprime.util.cli import add_special_parsers
Expand All @@ -39,25 +41,60 @@ class ArgValidationException(Exception):
"""An exception used for argument validation"""


def package_version_check():
def package_version_check(package: str, requirement_path: Path):
"""Checks the version of the packages installed match the expected packages of the fprime aggregate package"""
import pkg_resources

expected_version = get_version(package, requirement_path).lstrip(
"v"
) # Python version
try:
import pkg_resources
version = pkg_resources.get_distribution(package).version
if version != expected_version:
print(
f"[WARNING] {package} has unexpected version. Expected: {expected_version} found {version}"
)
except pkg_resources.DistributionNotFound:
print(f"[WARNING] {package} is not installed")

fprime_distribution = pkg_resources.get_distribution("fprime")
tools_requirements = fprime_distribution.requires()

for requirement in tools_requirements:
def validate_tools_from_requirements(build: Build):
"""Uses build settings to find requirements file and validate the correct versions installed"""
# Find prioritized order of requirements.txt, ensuring that each member exists
possibilities = [
build.settings.get("project_root", None),
build.settings.get("framework_path", None),
]
possibilities = [
Path(possible) / "requirements.txt"
for possible in possibilities
if possible is not None
]
possibilities = [possible for possible in possibilities if possible.exists()]
# Skip tools check, as not requirements.txt found
if not possibilities:
print(
f"[WARNING] Could not find 'requirements.txt' in: {possibilities}. Will not check tool versions."
)
return
# Pre-roll import errors
try:
import pkg_resources
except ImportError:
print("[WARNING] Cannot import 'pkg_resources'. Will not check tool versions.")
return

# Now check each required tool for fprime
tools = ["fprime-fpp", "fprime-tools", "fprime-gds"]
for tool in tools:
for possible in possibilities:
try:
result = pkg_resources.working_set.find(requirement)
if not result:
print(f"[WARNING] Expected package {requirement} not found")
except pkg_resources.VersionConflict as version_exception:
print(
f"[WARNING] Expected package version {version_exception.req} but found {version_exception.dist}"
)
except (ImportError, pkg_resources.DistributionNotFound):
print("[WARNING] 'fprime' package not installed, skipping tools version check")
package_version_check(tool, possible)
break
except (OSError, VersionException) as exc:
message = f"[WARNING] {exc}"
else:
print(message)


def validate(parsed, unknown):
Expand Down Expand Up @@ -178,7 +215,6 @@ def parse_args(args):

def utility_entry(args):
"""Main interface to F prime utility"""
package_version_check()
parsed, cmake_args, make_args, parser, runners = parse_args(args)

try:
Expand All @@ -198,17 +234,21 @@ def utility_entry(args):
build = Build(build_type, deployment, verbose=parsed.verbose)

# All commands need to load the build cache to setup the basic information for the build with the exception of
# generate, which is run before the creation of the build cache.
# generate, which is run before the creation of the build cache and thus must invent the cache instead. This
# call will ensure the build is in a ready state before attempting to check tool versions and run the command.
#
# Some commands, like purge and info, run on sets of directories an will attempt to load the sets later.
# Some commands, like purge and info, run on sets of directories and will attempt to load those sets later.
# However, the base directory must be setup here. Errors in this load are ignored to allow the command to find
# build caches related to that set.
if parsed.command not in ["generate"]:
try:
try:
if parsed.command == "generate":
build.invent(parsed.platform, build_dir=parsed.build_cache)
else:
build.load(parsed.platform, parsed.build_cache)
except InvalidBuildCacheException:
if parsed.command not in ["purge", "info"]:
raise
except InvalidBuildCacheException:
if parsed.command not in ["purge", "info"]:
raise
validate_tools_from_requirements(build)
runners[parsed.command](
build, parsed, cmake_args, make_args, getattr(parsed, "pass_through", [])
)
Expand Down
71 changes: 71 additions & 0 deletions src/fprime/util/versioning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
""" FPP tools to requirements file version check """
import argparse
import sys
from pathlib import Path


class VersionException(Exception):
pass


def get_version(package: str, requirements: Path):
"""Get the version as specified in the requirements file

This will read all requirements from the requirements file and attempt to print the version of the package that is
listed within. This can handle multiple styles of requirements. Firm requirements designated by a "==" and developer
requirements designated with an "@".

Args:
package: name of package to look for
requirements: path to requirements file to parse
"""
with open(requirements, "r") as file_handle:
matching_lines = [
line.strip() for line in file_handle.readlines() if package in line
]
if not matching_lines:
raise VersionException(f"Could not find {package} in requirements file")
valid_lines = [line for line in matching_lines if "==" in line or "@" in line]
if not valid_lines:
raise VersionException(
f"{package} has inexact version, use '==' or '@' format. Found: {matching_lines}"
)

# Collapse versions that match
versions = list(set(line.split("==")[-1].split("@")[-1] for line in valid_lines))
if len(versions) != 1:
raise VersionException(
f"Conflicting versions specified for {package}: {versions}"
)
return versions[0]


def main():
"""Parses arguments and acts as entry point for the tool"""
parser = argparse.ArgumentParser(
description="Detects the required package using a requirements file"
)
parser.add_argument("package", help="Package to check for version")
parser.add_argument(
"requirements", help="Python formatted requirements file to parse"
)

try:
args_ns = parser.parse_args()
python_version = get_version(args_ns.package, Path(args_ns.requirements))

# Add expected v at the front, if missing
if "." in python_version and not python_version.lower().startswith("v"):
python_version = f"v{python_version}"
# Add in a g as a version-control tag for hash versions:
elif "." not in python_version:
python_version = f"g{python_version}"
print(python_version)
except (IOError, VersionException) as exc:
print(f"[ERROR] Could not detect expected version: {exc}", file=sys.stderr)
sys.exit(-1)
sys.exit(0)


if __name__ == "__main__":
main()