Skip to content

Latest commit

Β 

History

110 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

ROS 2 Package XML Validator & Formatter

CI Lint codecov

Automate package.xml consistency in your ROS 2 projects.

This tool checks your package manifests for required tags, schema-defined ordering, REP-149 invariants, and missing dependencies discovered in CMakeLists.txt and launch files, then automatically formats the XML to standard conventions. Designed primarily as a pre-commit hook.

Requires Python β‰₯ 3.9 and an initialized rosdep install (any ROS 2 distro). Tested on Ubuntu 22.04 / 24.04.


πŸš€ Quick Start: Pre-commit Hook

The recommended way to use this tool is to integrate it into your pre-commit workflow. This ensures that every commit is automatically validated and formatted without manual intervention.

1. Add to .pre-commit-config.yaml

repos:
  - repo: https://github.com/Joschi3/package_xml_validation.git
    rev: v1.4.2  # Use the latest tag
    hooks:
      - id: format-package-xml
        name: Format package.xml
      # Optional. See "Launch Tree Integrity" below.
      - id: check-launch-tree
        name: Check launch tree references

2. Install the Hook

If you haven't already installed pre-commit hooks in your repository:

pip install pre-commit
pre-commit install

Now, package.xml files will be checked and formatted automatically on every git commit.


πŸ” Visual Example

This tool enforces the standard ROS 2 element order: name β†’ version β†’ description β†’ maintainer β†’ license β†’ dependencies β†’ export.

Before (Disorganized & Missing Build Type): Contains valid tags, but the order is random, grouping is missing, and the export tag is absent.

<package format="3">
  <name>my_package</name>
  <description>My cool package</description>
  <version>0.0.0</version>
  <license>Apache-2.0</license>
  <maintainer email="me@example.com">Me</maintainer>
  <test_depend>ament_lint_auto</test_depend>
  <buildtool_depend>ament_cmake</buildtool_depend>
  <depend>std_msgs</depend>
  <depend>rclcpp</depend>
</package>

After (Standardized, Sorted & Fixed): Elements are reordered to match the schema, dependencies are grouped alphabetically, and missing dependencies detected in CMakeLists.txt or launch files are automatically added.

<package format="3">
  <name>my_package</name>
  <version>0.0.0</version>
  <description>My cool package</description>
  <maintainer email="me@example.com">Me</maintainer>
  <license>Apache-2.0</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <depend>example_from_cmake</depend> <!-- Automatically added missing dep from the CMakeLists.txt -->
  <depend>rclcpp</depend>
  <depend>std_msgs</depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>test_launch_example</test_depend> <!-- Automatically added missing dep from a test launch file-->

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

πŸ–₯️ Sample Output

Running the validator on the Before manifest above with --auto-fill-missing-deps produces:

Processing my_package...
	Element order in my_package/package.xml is incorrect.
	Misplaced elements: version, maintainer, buildtool_depend
	Corrected dependency order in my_package/package.xml.
	Check element order corrected in my_package/package.xml.
	Check dependency order corrected in my_package/package.xml.
	Auto-filling <export><build_type>ament_cmake</build_type></export> in my_package/package.xml.
βœ… Corrected `package.xml` files successfully. πŸŽ‰

The on-disk file now matches the After snippet above. The exit code is non-zero because the file was modified β€” pre-commit treats that as "please re-stage and re-commit."


✨ Features

XML Formatting & Standards

  • Required Tags: Enforces the presence of required tags (name, version, description, maintainer, license) and rejects unknown top-level child tags. Accepts every REP-149 dependency tag including the anti-dependency <conflict> and <replace> tags.
  • Strict Ordering: Reorders elements to match the official ROS 2 standard (package_format3.xsd).
  • Intelligent Sorting: Groups dependencies (e.g., build_depend, exec_depend) and sorts them alphabetically.
  • Non-Destructive: Preserves your existing comments and indentation.

REP-149 Conformance

  • Manifest Invariants: Validates that the root element is <package>, that <package format="3"> is present, <name> syntax matches ^[a-z][a-z0-9_]*$, <version> syntax matches MAJOR.MINOR.PATCH, and every <maintainer> has a non-empty email="…" attribute. These can't be safely auto-filled, so they're report-only.
  • Conditional Dependencies: Honours REP-149 condition="…" attributes on dependency tags. Entries whose condition evaluates to False against os.environ are skipped during rosdep checks, CMake comparison, launch-file scanning, build-type matching, and <member_of_group> checks β€” matching what colcon does at build time. Disable with --ignore-conditions if your validation environment differs from build time.
  • <depend> Exclusivity: REP-149 forbids declaring the same key in both <depend> and any of <build_depend>/<build_export_depend>/<exec_depend>. The validator reports overlaps; with --auto-fill-missing-deps it collapses redundant granular tags into the canonical <depend> form.
  • Multiple <build_type>: When more than one <build_type> is active after condition evaluation, the validator picks the last one (REP-149 last-wins rule) and warns β€” multiple actives almost always indicate a config mistake.
  • Interface Packages: Message/service/action packages are required to declare <exec_depend>rosidl_default_runtime</exec_depend> (or the unified <depend> equivalent), per the rolling "Custom interfaces" tutorial. Auto-filled when --auto-fill-missing-deps is on.

Dependency Integrity

  • Launch File Scanning: Scans .py, .yaml, and .xml launch files. If a package is used in a launch file but missing from package.xml, it adds it as an <exec_depend> or <test_depend>. Can be disabled with --skip-launch-dep-check when launch scanning produces false positives or is not desired for a given package.
  • CMake Synchronization: Compares package.xml against CMakeLists.txt to ensure build dependencies match, adding missing entries as <depend> or <test_depend>. Calls of the form find_package(<pkg> QUIET) are treated as optional and skipped. all other forms such as find_package(<pkg>), find_package(<pkg> REQUIRED), and find_package(<pkg> REQUIRED QUIET) are enforced in package.xml.
  • Rosdep Validation: Verifies that your dependency names exist as valid keys in the rosdep database.

Launch-Manager Install Sets

Some workspaces keep every launch file in one package and declare the dependencies in sibling install sets, one per machine β€” the shape you end up with when a robot's computers are not all big enough to install everything. Neither of the checks above can make sense of that on its own: the package naming a dependency is not the package declaring it.

A package says which machine it is the install set for in its own <export> section:

<export>
  <build_type>ament_cmake</build_type>
  <launch_manager_host>athena-gripper</launch_manager_host>
</export>

With that, the validator reads launch_manager_configs/*.yaml to see which components run on that host, follows each component into its launch tree, and checks the install set against everything those trees name. --auto-fill-missing-deps adds what is missing. The package holding the launch files stops reporting those references as missing, since an install set declares them.

The whole feature is opt-in: with no launch_manager_host export anywhere, nothing changes.

Three properties worth knowing:

  • Additive only. The derivation is a lower bound β€” a controller plugin named by class in a parameter file, or a package handed to a generic component as an argument, is named nowhere a launch-file scan can reach. A declared dependency that was not derived is left alone and not reported; removing it would uninstall something the robot needs.
  • Nothing is filled from an incomplete derivation. If a component's definition cannot be found β€” commonly because the shared component library is not installed β€” the run says so and fills nothing, rather than writing a manifest that looks complete.
  • A host with no install set is reported. If a configuration launches on a machine no package claims, nothing declares what that machine runs, and no per-package check could notice.

Launch Tree Integrity

The separate check-launch-tree hook follows every include out of a package's launch files, across package boundaries, and grades what it finds by whether anyone can act on it:

Finding Meaning Effect
not declared a launch file names a package its own package.xml does not depend on fails the run
dead include the package is installed, the launch file it names is not fails the run
not installed nothing in this workspace provides the package warning
could not be followed a name built from $(eval …), or an argument with no value warning

The two fatal findings only fail the run when the package that owns the offending launch file is one you can fix. Three things count:

  1. it is in the repository being scanned;
  2. its source is elsewhere in the same workspace's src,
  3. it is below a --fatal-under prefix, which is how CI marks an install space the pipeline populates itself.

Crossing package boundaries resolves through AMENT_PREFIX_PATH. Without a sourced workspace the walk stops at the checkout's own launch files and says so; the manifest check still runs.

Option
--fatal-under PATH also treat packages below PATH as yours to fix. Repeatable. In CI, --fatal-under /opt/<distro> covers packages the pipeline installs itself. The repository and the surrounding workspace already count without it
--warn-only report everything, always exit zero β€” for introducing the hook to a repository that already has findings
--ignore NAME leave a package out of every report. Repeatable
--arg NAME=VALUE follow the tree a particular set of launch arguments produces, rather than the one the declared defaults describe. Repeatable

Build Configuration

  • Export Validation: Ensures the correct <build_type> (e.g., ament_cmake) is exported.
  • Test Dependencies: Parses test/ folders to ensure testing libraries are declared as <test_depend>.

πŸ› οΈ Manual Usage (CLI)

If you need to run the validator manually or in a CI environment without pre-commit, you can install it via pip.

Installation

pip install package-xml-validator
# OR install from source
pip install .

Usage Examples

Check only (Don't modify files)

package-xml-validator . --check-only

Auto-fill missing dependencies from CMake

package-xml-validator . --compare-with-cmake --auto-fill-missing-deps

CLI Options

Option Description
--check-only Report errors/formatting issues without modifying files (Exit code 1 on failure).
--compare-with-cmake Check if dependencies used in CMakeLists.txt are declared in package.xml.
--auto-fill-missing-deps Automatically add dependencies found in CMake/Launch files to package.xml.
--strict-cmake-checking Treat unresolved CMake dependencies as errors instead of warnings.
--skip-rosdep-key-validation Skip verifying if dependency names exist in the rosdep database.
--missing-deps-only Skips formatting checks; only looks for missing dependencies.
--ignore-cmake-key KEY Treat find_package(KEY ...) in CMakeLists.txt as not requiring a package.xml <depend> entry. Repeatable. Merged with the built-in defaults (Threads, OpenMP, ament_cmake).
--ignore-deps dep1,dep2 Comma-separated list of dependency names to globally ignore in validation.
--skip-launch-dep-check Skip checking for missing dependencies in launch and test files.
--exclude-package NAME Leave a package alone entirely β€” neither checked nor rewritten, and it cannot fail the run. Matched against the <name> tag, not the directory. Repeatable.
--ignore-conditions Disable evaluation of REP-149 condition="…" attributes; every entry is then evaluated regardless of its condition.

Ignoring Dependencies

Some dependencies detected in CMakeLists.txt or launch files should not be declared in package.xml β€” typically when a package pulls in heavy transitive dependencies (e.g. rviz2 and its GUI stack) that should not be installed on every target machine.

Prefer splitting the package first. For example, rather than a single robot_description package containing both URDF/xacro files and an RViz visualization launch file, split it into robot_description (model files) and robot_description_visualization (RViz launch files). Each package then declares only what it truly needs, and deploying robot_description to a robot won't drag in rviz2.

If splitting is not practical, the tool recognizes a validator:ignore directive inside XML comments:

<package format="3">
  <name>robot_description</name>
  <!-- ... -->

  <!-- validator:ignore rviz2 joint_state_publisher_gui -->

  <buildtool_depend>ament_cmake</buildtool_depend>
  <!-- ... -->
</package>
  • Names are space-separated after validator:ignore.
  • The directive may appear anywhere inside the <package> element; multiple directives in the same file are merged.
  • Listed dependencies are neither flagged as missing nor added by --auto-fill-missing-deps.
  • Scope is per-file β€” each package.xml manages its own ignore list.

For a global override β€” typically in CI β€” use the --ignore-deps CLI argument:

package-xml-validator . --compare-with-cmake --ignore-deps rviz2,joint_state_publisher_gui

πŸ§ͺ CI Integration

The simplest CI integration is to run your existing pre-commit config in GitHub Actions:

# .github/workflows/lint.yml
on: [push, pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - uses: pre-commit/action@v3.0.1

If rosdep is not initialized in your CI image, add --skip-rosdep-key-validation to the hook's args in .pre-commit-config.yaml.


🧭 Architecture

The validator is structured as a small pipeline. For each package.xml, PackageXmlValidator parses the file once, runs a list of validation steps against the in-memory tree, and writes back only if a step actually mutated.

Module Responsibility
package_xml_validator.py CLI entry point and per-file orchestration (parse β†’ run steps β†’ optionally write).
check_launch_tree.py Second entry point: follows the launch tree across packages, and grades each finding by whether the package that owns the offending file is one the committer can fix.
helpers/validation_steps/ One *Step class per validation rule. Each docstring states the rule, inputs, and when (if ever) it mutates the tree.
helpers/formatter/ Pure structural checks (structural_checks.py), tree mutators (mutations.py), indentation/pretty-print helpers, and shared schema constants. PackageXmlFormatter is a thin facade.
helpers/cmake_parsers.py Lightweight regex-based CMake parser used by CMakeComparisonStep.
helpers/find_launch_dependencies.py Extracts package names referenced from launch files for LaunchDependencyStep.
helpers/rosdep_validator.py, rosdep_wrapper.py Resolve rosdep keys + workspace packages; the wrapper is the single boundary against the untyped rosdep2.
helpers/workspace.py ROS workspace layout discovery (locate the <ws>/src for a given path).

To add a new validation rule, create a new file under helpers/validation_steps/ exporting a subclass of ValidationStep, then register it in PackageXmlValidator._build_steps.


⌨️ Autocompletion

To enable tab autocompletion for CLI arguments:

  1. Install: pip install .
  2. Enable (Temporary): eval "$(register-python-argcomplete package-xml-validator)"
  3. Enable (Permanent): echo 'eval "$(register-python-argcomplete package-xml-validator)"' >> ~/.bashrc

About

Validate and auto-format ROS 2 package.xml files with schema checks, non-destructive fixes, and optional CMake/launch dependency sync.

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages