cmake_minimum_required(VERSION 3.20)

project(rclpy)

# Default to C++17
if(NOT CMAKE_CXX_STANDARD)
  set(CMAKE_CXX_STANDARD 17)
  set(CMAKE_CXX_STANDARD_REQUIRED ON)
endif()
# Default to C11
if(NOT CMAKE_C_STANDARD)
  set(CMAKE_C_STANDARD 11)
endif()
if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_C_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra)
endif()

find_package(ament_cmake REQUIRED)
find_package(ament_cmake_python REQUIRED)
find_package(lifecycle_msgs REQUIRED)
find_package(rcl REQUIRED)
find_package(rcl_action REQUIRED)
find_package(rcl_interfaces REQUIRED)
find_package(rcl_lifecycle REQUIRED)
find_package(rcl_logging_interface REQUIRED)
find_package(rcl_yaml_param_parser REQUIRED)
find_package(rcpputils REQUIRED)
find_package(rcutils REQUIRED)
find_package(rmw REQUIRED)
find_package(rmw_implementation_cmake REQUIRED)
find_package(rosidl_runtime_c REQUIRED)

# By default, without the settings below, find_package(Python3) will attempt
# to find the newest python version it can, and additionally will find the
# most specific version.  For instance, on a system that has
# /usr/bin/python3.10, /usr/bin/python3.11, and /usr/bin/python3, it will find
# /usr/bin/python3.11, even if /usr/bin/python3 points to /usr/bin/python3.10.
# The behavior we want is to prefer the "system" installed version unless the
# user specifically tells us otherwise through the Python3_EXECUTABLE hint.
# Setting CMP0094 to NEW means that the search will stop after the first
# python version is found.  Setting Python3_FIND_UNVERSIONED_NAMES means that
# the search will prefer /usr/bin/python3 over /usr/bin/python3.11.  And that
# latter functionality is only available in CMake 3.20 or later, so we need
# at least that version.
cmake_policy(SET CMP0094 NEW)
set(Python3_FIND_UNVERSIONED_NAMES FIRST)

# Find python before pybind11
find_package(Python3 REQUIRED COMPONENTS Interpreter Development)

find_package(pybind11 REQUIRED)

# enables using the Python extensions from the build space for testing
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/test_rclpy/__init__.py" "")

ament_python_install_package(${PROJECT_NAME})

# Only build the library if a C typesupport exists
get_rmw_typesupport(typesupport_impls "rmw_implementation" LANGUAGE "c")
if(typesupport_impls STREQUAL "")
  message(STATUS "Skipping rclpy because no C typesupport library was found.")
  return()
endif()

# Set the build location and install location for a CPython extension
function(configure_build_install_location _library_name)
  # Install into test_rclpy folder in build space for unit tests to import
  set_target_properties(${_library_name} PROPERTIES
    # Use generator expression to avoid prepending a build type specific directory on Windows
    LIBRARY_OUTPUT_DIRECTORY $<1:${CMAKE_CURRENT_BINARY_DIR}/test_rclpy>
    RUNTIME_OUTPUT_DIRECTORY $<1:${CMAKE_CURRENT_BINARY_DIR}/test_rclpy>)

  # Install library for actual use
  install(TARGETS ${_library_name}
    DESTINATION "${PYTHON_INSTALL_DIR}/${PROJECT_NAME}"
  )
endfunction()

# Split from main extension and converted to pybind11
pybind11_add_module(_rclpy_pybind11
  src/rclpy/_rclpy_logging.cpp
  src/rclpy/_rclpy_pybind11.cpp
  src/rclpy/action_client.cpp
  src/rclpy/action_goal_handle.cpp
  src/rclpy/action_server.cpp
  src/rclpy/client.cpp
  src/rclpy/clock.cpp
  src/rclpy/context.cpp
  src/rclpy/destroyable.cpp
  src/rclpy/duration.cpp
  src/rclpy/clock_event.cpp
  src/rclpy/events_executor/delayed_event_thread.cpp
  src/rclpy/events_executor/events_executor.cpp
  src/rclpy/events_executor/events_queue.cpp
  src/rclpy/events_executor/rcl_support.cpp
  src/rclpy/events_executor/timers_manager.cpp
  src/rclpy/exceptions.cpp
  src/rclpy/graph.cpp
  src/rclpy/guard_condition.cpp
  src/rclpy/lifecycle.cpp
  src/rclpy/logging.cpp
  src/rclpy/names.cpp
  src/rclpy/node.cpp
  src/rclpy/publisher.cpp
  src/rclpy/qos.cpp
  src/rclpy/event_handle.cpp
  src/rclpy/serialization.cpp
  src/rclpy/service.cpp
  src/rclpy/service_info.cpp
  src/rclpy/service_introspection.cpp
  src/rclpy/signal_handler.cpp
  src/rclpy/subscription.cpp
  src/rclpy/time_point.cpp
  src/rclpy/timer.cpp
  src/rclpy/type_description_service.cpp
  src/rclpy/utils.cpp
  src/rclpy/wait_set.cpp
)

if(CMAKE_C_COMPILER_ID MATCHES "Clang" AND NOT APPLE)
  target_link_libraries(_rclpy_pybind11 PRIVATE atomic)
endif()

target_include_directories(_rclpy_pybind11 PRIVATE
  src/rclpy/
)
target_link_libraries(_rclpy_pybind11 PRIVATE
  lifecycle_msgs::lifecycle_msgs__rosidl_generator_c
  lifecycle_msgs::lifecycle_msgs__rosidl_typesupport_c
  rcl::rcl
  rcl_action::rcl_action
  rcl_interfaces::rcl_interfaces
  rcl_lifecycle::rcl_lifecycle
  rcl_logging_interface::rcl_logging_interface
  rcpputils::rcpputils
  rcutils::rcutils
  rosidl_runtime_c::rosidl_runtime_c
)
configure_build_install_location(_rclpy_pybind11)

if(NOT WIN32)
  ament_environment_hooks(
    "${ament_cmake_package_templates_ENVIRONMENT_HOOK_LIBRARY_PATH}"
  )
endif()

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # Give cppcheck hints about macro definitions coming from outside this package
  get_target_property(ament_cmake_cppcheck_ADDITIONAL_INCLUDE_DIRS rcutils::rcutils INTERFACE_INCLUDE_DIRECTORIES)
  list(APPEND AMENT_LINT_AUTO_EXCLUDE "ament_cmake_cppcheck")

  if(Python3_VERSION VERSION_LESS 3.12)
    file(GLOB AMENT_LINT_AUTO_FILE_EXCLUDE
      "${CMAKE_CURRENT_SOURCE_DIR}/rclpy/experimental/async_*.py"
      "${CMAKE_CURRENT_SOURCE_DIR}/test/test_async_*.py"
    )
  endif()

  ament_lint_auto_find_test_dependencies()

  find_package(ament_cmake_cppcheck REQUIRED)
  ament_cppcheck()
  set_tests_properties(cppcheck PROPERTIES TIMEOUT 420)

  find_package(ament_cmake_pytest REQUIRED)
  find_package(rosidl_generator_py REQUIRED)
  find_package(ament_cmake_gtest REQUIRED)

  rosidl_generator_py_get_typesupports(_typesupport_impls)
  ament_index_get_prefix_path(ament_index_build_path SKIP_AMENT_PREFIX_PATH)
  # Get the first item (it will be the build space version of the build path).
  list(GET ament_index_build_path 0 ament_index_build_path)
  if(WIN32)
    # On Windows prevent CMake errors and prevent it being evaluated as a list.
    string(REPLACE "\\" "/" ament_index_build_path "${ament_index_build_path}")
  endif()

  ament_add_gtest(test_python_allocator
    test/test_python_allocator.cpp)
  target_include_directories(test_python_allocator PRIVATE src/rclpy)
  target_link_libraries(test_python_allocator pybind11::embed)

  if(NOT _typesupport_impls STREQUAL "")
    # Run each test in its own pytest invocation to isolate any global state in rclpy
    set(_rclpy_pytest_tests
      test/test_action_client.py
      test/test_action_graph.py
      test/test_action_server.py
      test/test_callback_group.py
      test/test_client.py
      test/test_clock.py
      test/test_context.py
      test/test_create_node.py
      test/test_create_while_spinning.py
      test/test_destruction.py
      test/test_events_executor.py
      test/test_executor.py
      test/test_expand_topic_name.py
      test/test_guard_condition.py
      test/test_init_shutdown.py
      test/test_lifecycle.py
      test/test_logging.py
      test/test_logging_rosout.py
      test/test_logging_service.py
      test/test_messages.py
      test/test_node.py
      test/test_parameter.py
      test/test_parameter_client.py
      test/test_parameter_service.py
      test/test_parameter_event_handler.py
      test/test_publisher.py
      test/test_qos.py
      test/test_qos_event.py
      test/test_qos_overriding_options.py
      test/test_rate.py
      test/test_rosout_subscription.py
      test/test_serialization.py
      test/test_service.py
      test/test_service_introspection.py
      test/test_subscription.py
      test/test_task.py
      test/test_time_source.py
      test/test_time.py
      test/test_timer.py
      test/test_topic_or_service_is_hidden.py
      test/test_topic_endpoint_info.py
      test/test_type_support.py
      test/test_type_hash.py
      test/test_utilities.py
      test/test_validate_full_topic_name.py
      test/test_validate_namespace.py
      test/test_validate_node_name.py
      test/test_validate_topic_name.py
      test/test_waitable.py
      test/test_wait_for_message.py
    )

    if(Python3_VERSION VERSION_GREATER_EQUAL 3.12)
      list(APPEND _rclpy_pytest_tests
        test/test_async_node.py
        test/test_async_publisher.py
        test/test_async_subscription.py
        test/test_async_service.py
        test/test_async_client.py
        test/test_async_timer.py
        test/test_async_clock.py
      )
    endif()

    foreach(_test_path ${_rclpy_pytest_tests})
      get_filename_component(_test_name ${_test_path} NAME_WE)
      ament_add_pytest_test(${_test_name} ${_test_path}
        APPEND_ENV AMENT_PREFIX_PATH=${ament_index_build_path}
          PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR}
        TIMEOUT 120
        WERROR ON
      )
    endforeach()
  endif()
endif()

ament_package()
