Skip to content
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
3 changes: 1 addition & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Validates and formats `package.xml` files to enforce consistency and ROS 2 schem
## 🛠️ Usage Example

```bash
package-xml-validator [-h] [--check-only] [--file FILE] [--verbose] [--check-with-xmllint] [--skip-rosdep-key-validation] [--compare-with-cmake] [src ...]
package-xml-validator [-h] [--check-only] [--file FILE] [--verbose] [--skip-rosdep-key-validation] [--compare-with-cmake] [src ...]

Validate and format ROS2 package.xml files.

Expand All @@ -64,7 +64,6 @@ options:
--check-only Only check for errors without correcting.
--file FILE Path to a single XML file to process. If provided, 'src' arguments are ignored.
--verbose Enable verbose output.
--check-with-xmllint Recheck XML schema using xmllint.
--skip-rosdep-key-validation Check if rosdeps are valid.
--compare-with-cmake Check if all CMake dependencies are in package.xml.
--auto-fill-missing-deps Automatically fill missing dependencies in package.xml.
Expand Down
7 changes: 2 additions & 5 deletions package_xml_validation/helpers/find_launch_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,8 @@

def scan_file(path, found: set[str], verbose: bool = False):
"""Apply every regex to the file and add matches to `found`."""
try:
with open(path, encoding="utf-8") as f:
text = f.read()
except (UnicodeDecodeError, OSError):
return
with open(path, encoding="utf-8") as f:
text = f.read()

for i, rx in enumerate(COMPILED):
for m in rx.finditer(text):
Expand Down
14 changes: 14 additions & 0 deletions package_xml_validation/helpers/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,20 @@ def get_pkgs_in_wrs(path: Path) -> list[str]:
return sorted(pkgs)


def find_package_xml_files(paths) -> list[str]:
files = []
for p in paths:
p = Path(p)
if p.is_file() and p.name == "package.xml":
files.append(p)
elif p.is_file() and p.name == "CMakeLists.txt":
files.append(p.parent / "package.xml")
elif p.is_dir():
for xml in p.rglob("package.xml"):
files.append(xml)
return sorted({str(x) for x in files})


def main() -> None:
ap = argparse.ArgumentParser(
description=(
Expand Down
63 changes: 6 additions & 57 deletions package_xml_validation/package_xml_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
from .helpers.pkg_xml_formatter import PackageXmlFormatter
from .helpers.cmake_parsers import read_deps_from_cmake_file
from .helpers.find_launch_dependencies import scan_files
from .helpers.workspace import find_package_xml_files
except ImportError:
from helpers.logger import get_logger
from helpers.rosdep_validator import RosdepValidator
from helpers.pkg_xml_formatter import PackageXmlFormatter
from helpers.cmake_parsers import read_deps_from_cmake_file
from helpers.find_launch_dependencies import scan_files
import subprocess
from helpers.workspace import find_package_xml_files
import re


Expand All @@ -29,7 +30,6 @@ class PackageXmlValidator:
def __init__(
self,
check_only=False,
check_with_xmllint=False,
check_rosdeps=True,
compare_with_cmake=False,
auto_fill_missing_deps=False,
Expand All @@ -38,7 +38,6 @@ def __init__(
):
self.verbose = verbose
self.check_only = check_only
self.check_with_xmllint = check_with_xmllint
self.check_rosdeps = check_rosdeps
self.compare_with_cmake = compare_with_cmake
if self.compare_with_cmake and not self.check_rosdeps:
Expand All @@ -52,7 +51,7 @@ def __init__(
self.rosdep_validator = RosdepValidator(pkg_path=path)
self.formatter = PackageXmlFormatter(
check_only=check_only,
check_with_xmllint=check_with_xmllint,
check_with_xmllint=False,
verbose=verbose,
)
self.encountered_unresolvable_error = False
Expand All @@ -62,29 +61,9 @@ def __init__(
self.check_count = 1
if self.check_rosdeps:
self.num_checks += 1
if self.check_with_xmllint:
self.num_checks += 1
if self.compare_with_cmake:
self.num_checks += 1

def find_package_xml_files(self, paths):
"""Locate all package.xml files within the provided paths."""
package_xml_files = []
for path in paths:
if os.path.isfile(path) and os.path.basename(path) == "package.xml":
package_xml_files.append(path)
elif os.path.isfile(path) and os.path.basename(path) == "CMakeLists.txt":
package_xml_files.append(
os.path.join(os.path.dirname(path), "package.xml")
)
elif os.path.isdir(path):
for root, _, files in os.walk(path):
if "package.xml" in files:
package_xml_files.append(os.path.join(root, "package.xml"))
# Filter out duplicates
package_xml_files = list(set(package_xml_files))
return package_xml_files

def get_package_type(self, xml_file: str) -> tuple[PackageType, bool]:
"""Determine the package type based on the presence of CMakeLists.txt or setup.py.
Returns <PackageType, bool> where bool indicates if the package is a message package."""
Expand Down Expand Up @@ -136,7 +115,7 @@ def check_for_cmake(
pkg_name = os.path.basename(os.path.dirname(xml_file))
valid_xml = True
build_deps_cmake, test_deps_cmake = read_deps_from_cmake_file(cmake_file)
# make sure that all cmake dependencies are in the package.xml if they can be resolved
# ---------------------------- BUILD DEPENDENCIES ----------------------------
unresolvable = self.rosdep_validator.check_rosdeps_and_local_pkgs(
build_deps_cmake
)
Expand All @@ -161,7 +140,7 @@ def check_for_cmake(
unresolvable = self.rosdep_validator.check_rosdeps_and_local_pkgs(
test_deps_cmake
)

# ---------------------------- TEST DEPENDENCIES ----------------------------
missing_deps = [
dep
for dep in test_deps_cmake
Expand All @@ -181,24 +160,6 @@ def check_for_cmake(
self.formatter.add_dependencies(root, missing_deps, "test_depend")
return valid_xml

def validate_xml_with_xmllint(self, xml_file):
"""Validate XML file against the ROS package_format3.xsd schema using xmllint."""
schema_url = "http://download.ros.org/schema/package_format3.xsd"
try:
result = subprocess.run(
["xmllint", "--noout", "--schema", schema_url, xml_file],
capture_output=True,
text=True,
)
if result.returncode != 0:
self.logger.error(f"XML validation error in {xml_file}:")
self.logger.error(result.stderr)
return False
return True
except Exception as e:
self.logger.error(f"Error running xmllint on {xml_file}: {e}")
return False

def validate_launch_dependencies(
self,
root,
Expand Down Expand Up @@ -546,12 +507,6 @@ def check_and_format_files(self, package_xml_files):
"Check ROS dependencies", self.check_for_rosdeps, rosdeps, xml_file
)
self.encountered_unresolvable_error |= not valid
# Check with xmllint if enabled
if self.check_with_xmllint:
valid = self.perform_check(
"Check with xmllint", self.validate_xml_with_xmllint, xml_file
)
self.encountered_unresolvable_error |= not valid
# Check for CMake dependencies if enabled
if self.compare_with_cmake:
build_deps = self.formatter.retrieve_build_dependencies(root)
Expand Down Expand Up @@ -597,7 +552,7 @@ def check_and_format_files(self, package_xml_files):
return True

def check_and_format(self, src):
package_xml_files = self.find_package_xml_files(src)
package_xml_files = find_package_xml_files(src)
if not package_xml_files:
self.logger.info("No package.xml files found in the provided paths.")
return
Expand All @@ -623,11 +578,6 @@ def main():

parser.add_argument("--verbose", action="store_true", help="Enable verbose output.")

parser.add_argument(
"--check-with-xmllint",
action="store_true",
help="Recheck XML schema using xmllint.",
)
parser.add_argument(
"--skip-rosdep-key-validation",
action="store_true",
Expand Down Expand Up @@ -667,7 +617,6 @@ def main():
formatter = PackageXmlValidator(
check_only=args.check_only,
verbose=args.verbose,
check_with_xmllint=args.check_with_xmllint,
check_rosdeps=not args.skip_rosdep_key_validation,
compare_with_cmake=args.compare_with_cmake,
auto_fill_missing_deps=args.auto_fill_missing_deps,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
cmake_minimum_required(VERSION 3.8)
project(hector_gamepad_manager)

if (NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
endif()

if (CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()

find_package(ament_cmake REQUIRED)
set(DEPENDENCIES
pluginlib
rclcpp
sensor_msgs
yaml-cpp
hector_gamepad_plugin_interface
)

foreach(Dependency IN LISTS DEPENDENCIES)
find_package(${Dependency} REQUIRED)
endforeach()

set(HEADERS
include/hector_gamepad_manager/hector_gamepad_manager.hpp
)

set(SOURCES
src/hector_gamepad_manager.cpp
)

add_library(${PROJECT_NAME} SHARED ${SOURCES} ${HEADERS})
target_include_directories(${PROJECT_NAME} PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_link_libraries(${PROJECT_NAME} PUBLIC yaml-cpp)
ament_target_dependencies(${PROJECT_NAME} PUBLIC ${DEPENDENCIES})

add_executable(hector_gamepad_manager_node src/hector_gamepad_manager_node.cpp)
target_link_libraries(hector_gamepad_manager_node PUBLIC ${PROJECT_NAME})

install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME}-targets LIBRARY DESTINATION lib)
install(DIRECTORY include/ DESTINATION include)
install(TARGETS hector_gamepad_manager_node DESTINATION lib/${PROJECT_NAME})
install(DIRECTORY launch config DESTINATION share/${PROJECT_NAME})


if(BUILD_TESTING)
find_package(ament_cmake_gtest REQUIRED)
find_package(ros_testing REQUIRED)
ament_add_gtest_executable(${PROJECT_NAME}_test
test/test_hector_gamepad_manager.cpp
)
target_link_libraries(${PROJECT_NAME}_test ${PROJECT_NAME})
ament_target_dependencies(${PROJECT_NAME}_test ${THIS_PACKAGE_INCLUDE_DEPENDS})
add_ros_test(test/test_hector_gamepad_manager.launch.py ARGS "test_binary_dir:=${CMAKE_CURRENT_BINARY_DIR}")
endif ()


ament_export_targets(${PROJECT_NAME}-targets)
ament_export_include_directories(include)
ament_export_dependencies(${DEPENDENCIES})
ament_package()
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>hector_gamepad_manager</name>
<version>0.0.0</version>
<description>Package for managing gamepad inputs</description>
<maintainer email="simon.giegerich@stud.tu-darmstadt.de">Simon Giegerich</maintainer>
<license>TODO: License declaration</license>

<buildtool_depend>ament_cmake</buildtool_depend>

<depend>hector_gamepad_plugin_interface</depend>
<depend>pluginlib</depend>
<depend>rclcpp</depend>
<depend>sensor_msgs</depend>
<depend>yaml-cpp</depend>

<exec_depend>hector_gamepad_manager_plugins</exec_depend>

<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>

<export>
<build_type>ament_cmake</build_type>
</export>
</package>
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,5 @@
<test_depend>ament_lint_common</test_depend>
<test_depend>ros_testing</test_depend>

<export>
</export>
<export></export>
</package>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0"?>
<?xml version='1.0' encoding='UTF-8'?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>python_pkg</name>
Expand All @@ -12,7 +12,6 @@
<depend>rclpy</depend>
<depend>std_msgs</depend>

<export>
</export>
<export></export>

</package>
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<depend>std_msgs</depend>
<depend>std_srvs</depend>
<depend>transmission_interface</depend>
<depend>yaml-cpp</depend>
<depend>yaml_cpp_vendor</depend>

<exec_depend>controller_manager</exec_depend>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<depend>std_msgs</depend>
<depend>std_srvs</depend>
<depend>transmission_interface</depend>
<depend>yaml-cpp</depend>
<depend>yaml_cpp_vendor</depend>

<test_depend>ament_lint_auto</test_depend>
Expand Down
18 changes: 17 additions & 1 deletion tests/test_find_launch_dependencies.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import os
import unittest

from package_xml_validation.helpers.find_launch_dependencies import scan_file
from package_xml_validation.helpers.find_launch_dependencies import (
scan_file,
scan_files,
)


class TestFindLaunchDependencies(unittest.TestCase):
Expand Down Expand Up @@ -50,6 +53,19 @@ def test_scan_each_file(self):
),
)

def test_scan_each_file_given_file(self):
"""When given a file path, or an not existing directory scan_files should return an empty list"""
found = scan_files("non_existing_file.launch.py")
self.assertEqual(len(found), 0)
existing_file = os.path.join(
os.path.dirname(__file__),
"examples",
"launch_examples",
"python_example.launch.py",
)
found = scan_files(existing_file)
self.assertEqual(len(found), 0)


if __name__ == "__main__":
unittest.main()
Loading
Loading