Skip to content
Closed
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
147 changes: 133 additions & 14 deletions ament_nodl/cmake/ament_nodl_register_node.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@
#
# The source file is also installed under ``share/<package>/nodl/`` for direct filesystem access.
#
# If the document uses relative ``include`` references, what is installed is a rewritten copy in
# which each relative reference has become ``nodl://<package>/<name>``.
# A relative reference is an intra-package convenience that means nothing to a consumer reading the
# document back out of the index, so it is resolved to a name that does.
# The referenced document must already be registered, by an earlier call to this macro, which is
# what gives it the name to rewrite to.
#
# Example::
#
# ament_nodl_register_node(my_node
# FILE nodl/my_node.nodl.yaml
# )
# ament_nodl_register_node(telemetry FILE nodl/common/telemetry.nodl.yaml)
# ament_nodl_register_node(my_node FILE nodl/my_node.nodl.yaml) # may include common/telemetry.nodl.yaml
#
# :param executable_name: name of the executable this NoDL document describes.
# Combined with PACKAGE to form the resource key.
Expand Down Expand Up @@ -49,31 +55,144 @@ function(ament_nodl_register_node executable_name)
"ament_nodl_register_node: file not found at configure time: ${_abs_file}")
endif()

# Validate the file at build time so authoring errors surface when registering, not downstream when consuming.
# This only runs when ${_abs_file} changes.
set(_stamp_dir "${CMAKE_CURRENT_BINARY_DIR}/ament_nodl/nodl_nodes")
set(_stamp "${_stamp_dir}/${_ARGS_PACKAGE}__${executable_name}.valid.stamp")
file(MAKE_DIRECTORY "${_stamp_dir}")
_ament_nodl_record_registration("${_abs_file}" "${_ARGS_PACKAGE}" "${executable_name}")
_ament_nodl_local_references("${_abs_file}" _local_refs)
_ament_nodl_reference_map(_map_args)

# Every relative reference must already be registered, so the rewrite below cannot fail on a
# name that does not exist. Checking here rather than at build time puts the error where the
# fix is: the ordering of calls in this CMakeLists.
get_property(_registered GLOBAL PROPERTY _AMENT_NODL_REGISTERED_PATHS)
foreach(_ref IN LISTS _local_refs)
if(NOT "${_ref}" IN_LIST _registered)
message(FATAL_ERROR
"ament_nodl_register_node(${executable_name}): ${_abs_file} references ${_ref}, "
"which is not registered.\n"
"Add an ament_nodl_register_node() call for it above this one.")
endif()
endforeach()

# Rewriting reads only ${_abs_file}, but validation follows relative references into the source
# tree, so the stamp has to depend on everything reachable through them.
get_filename_component(_basename "${_abs_file}" NAME)
set(_gen_dir "${CMAKE_CURRENT_BINARY_DIR}/ament_nodl/${_ARGS_PACKAGE}__${executable_name}")
set(_gen_file "${_gen_dir}/${_basename}")
add_custom_command(
OUTPUT "${_stamp}"
DEPENDS "${_abs_file}"
OUTPUT "${_gen_file}"
DEPENDS "${_abs_file}" ${_local_refs}
COMMAND "${Python3_EXECUTABLE}" -m nodl_schema "${_abs_file}"
COMMAND "${Python3_EXECUTABLE}" -m nodl_schema "${_abs_file}"
COMMAND "${CMAKE_COMMAND}" -E touch "${_stamp}"
--rewrite-to "${_gen_file}" ${_map_args}
COMMENT "Validating NoDL node ${_ARGS_PACKAGE}/${executable_name}"
VERBATIM
)
add_custom_target(_ament_nodl_validate_node_${_ARGS_PACKAGE}__${executable_name} ALL
DEPENDS "${_stamp}"
DEPENDS "${_gen_file}"
)

# Install to ament index
install(
FILES "${_abs_file}"
FILES "${_gen_file}"
DESTINATION "share/ament_index/resource_index/nodl_nodes"
RENAME "${_ARGS_PACKAGE}__${executable_name}")

# Install to package's share directory
install(
FILES "${_abs_file}"
FILES "${_gen_file}"
DESTINATION "share/${_ARGS_PACKAGE}/nodl")
endfunction()

#
# Record a registration so later calls can rewrite relative references to it.
#
# Two parallel global lists hold the record: an absolute path and the ``<package>/<name>`` it was
# registered as. A name may map to exactly one file, because a second registration would otherwise
# quietly take over a name that an already-rewritten reference points at.
#
# :param abs_file: absolute path of the document being registered.
# :param package: package the resource key uses.
# :param name: executable name the resource key uses.
#
function(_ament_nodl_record_registration abs_file package name)
get_property(_paths GLOBAL PROPERTY _AMENT_NODL_REGISTERED_PATHS)
get_property(_targets GLOBAL PROPERTY _AMENT_NODL_REGISTERED_TARGETS)
set(_target "${package}/${name}")

list(FIND _targets "${_target}" _index)
if(NOT _index EQUAL -1)
list(GET _paths ${_index} _existing)
if(NOT "${_existing}" STREQUAL "${abs_file}")
message(FATAL_ERROR
"ament_nodl_register_node: ${_target} is already registered from a different file.\n"
" first: ${_existing}\n"
" second: ${abs_file}\n"
"A name may refer to only one document, since references are rewritten to it by name.")
endif()
message(FATAL_ERROR
"ament_nodl_register_node: ${_target} is already registered from ${abs_file}.\n"
"Remove the duplicate call.")
endif()

set_property(GLOBAL APPEND PROPERTY _AMENT_NODL_REGISTERED_PATHS "${abs_file}")
set_property(GLOBAL APPEND PROPERTY _AMENT_NODL_REGISTERED_TARGETS "${_target}")
endfunction()

#
# Return the documents reachable from a file through relative references, transitively.
#
# Also marks them as configure dependencies, so adding or removing a reference in any of them
# reruns configure and refreshes the dependency list computed here.
#
# :param abs_file: absolute path of the document to walk.
# :param out_var: name of the variable to set in the caller's scope.
#
function(_ament_nodl_local_references abs_file out_var)
# A document generated by another custom command does not exist yet, and registering one is
# supported (the missing-file case is a warning above, not an error). There is nothing to read,
# so it contributes no references and no dependencies.
if(NOT EXISTS "${abs_file}")
set(${out_var} "" PARENT_SCOPE)
return()
endif()

execute_process(
COMMAND "${Python3_EXECUTABLE}" -m nodl_schema "${abs_file}" --list-references
OUTPUT_VARIABLE _output
ERROR_VARIABLE _error
RESULT_VARIABLE _result
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(NOT _result EQUAL 0)
message(FATAL_ERROR
"ament_nodl_register_node: could not read includes of ${abs_file}:\n${_error}")
endif()

string(REPLACE "\n" ";" _refs "${_output}")
list(REMOVE_ITEM _refs "")
if(_refs)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${_refs})
endif()
set(${out_var} "${_refs}" PARENT_SCOPE)
endfunction()

#
# Build the ``--map <path>=<package>/<name>`` arguments describing every registration so far.
#
# :param out_var: name of the variable to set in the caller's scope.
#
function(_ament_nodl_reference_map out_var)
get_property(_paths GLOBAL PROPERTY _AMENT_NODL_REGISTERED_PATHS)
get_property(_targets GLOBAL PROPERTY _AMENT_NODL_REGISTERED_TARGETS)

set(_args "")
list(LENGTH _paths _count)
if(_count GREATER 0)
math(EXPR _last "${_count} - 1")
foreach(_i RANGE ${_last})
list(GET _paths ${_i} _path)
list(GET _targets ${_i} _target)
list(APPEND _args "--map" "${_path}=${_target}")
endforeach()
endif()
set(${out_var} "${_args}" PARENT_SCOPE)
endfunction()
41 changes: 37 additions & 4 deletions ament_nodl/doc/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ For what a NoDL document declares, see {external+nodl:doc}`concepts`.

Register a NoDL document for an executable. This does three things:

1. Validates the file at build time (via `python -m nodl_schema`), so authoring errors surface when registering
rather than downstream when a consumer reads the spec.
2. Installs the file into the ament index under the `nodl_nodes` resource type, keyed `<package>__<executable>`.
3. Installs the file under `share/<package>/nodl/` for direct filesystem access.
1. Validates the file at build time, so authoring errors surface when registering rather than downstream when a consumer reads it.
If the document uses `include`, this step also checks that references resolve correctly.
Because validation runs before install, an included `nodl://<package>/<name>` must belong to a package that is already built and a dependency of this one.
2. Rewrites relative `include` references to `nodl://<package>/<name>`, if the document has any.
A document without them is installed exactly as authored.
3. Installs the result into the ament index under the `nodl_nodes` resource type, keyed `<package>__<executable>`, and under `share/<package>/nodl/` for direct filesystem access.

```cmake
find_package(ament_nodl REQUIRED)
Expand All @@ -22,6 +24,37 @@ ament_nodl_register_node(my_node
)
```

### Relative includes

A document may reference another document in the same package by relative path, which is the only form that can reach a
document in the package being built. Such a reference means nothing to a consumer reading the document back out of the
index, which gets text and no path to resolve against, so registration rewrites it to the name the referenced document
was registered under.

That name has to exist, so **the referenced document must be registered first**:

```cmake
# telemetry.nodl.yaml first: composed_node references it by path, and the rewrite needs its name.
ament_nodl_register_node(telemetry FILE nodl/common/telemetry.nodl.yaml)
ament_nodl_register_node(composed_node FILE nodl/composed_node.nodl.yaml)
```

With `composed_node.nodl.yaml` authored as:

```yaml
nodl_version: 2
include:
- ref: common/telemetry.nodl.yaml
```

what installs is that same document with the reference reading `nodl://<package>/telemetry`.

Two mistakes are caught when CMake configures rather than later:

- Referencing a document that no earlier `ament_nodl_register_node` call registered. There is no name to rewrite to.
- Registering one name from two different files. The second would take over a name that an already-rewritten reference
points at.

### Arguments

:`executable_name`: Name of the executable the document describes. Combined with `PACKAGE` to form the resource key.
Expand Down
Loading
Loading