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
11 changes: 11 additions & 0 deletions ament_cmake_python/ament_cmake_python-extras.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ print(os.path.relpath(sysconfig.get_path('purelib', **kwargs), start='${CMAKE_IN
endif()
endmacro()

macro(_ament_cmake_python_register_extension_hook)
if(NOT DEFINED AMENT_CMAKE_PYTHON_EXTENSION_REGISTERED)
set(AMENT_CMAKE_PYTHON_EXTENSION_REGISTERED TRUE)

ament_register_extension(
"ament_package"
"ament_cmake_python"
"ament_python_install_registered_packages.cmake")
endif()
endmacro()

include("${ament_cmake_python_DIR}/ament_python_install_module.cmake")
include("${ament_cmake_python_DIR}/ament_python_install_package.cmake")
include("${ament_cmake_python_DIR}/ament_get_python_install_dir.cmake")
149 changes: 25 additions & 124 deletions ament_cmake_python/cmake/ament_python_install_package.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@
# limitations under the License.

#
# Install a Python package (and its recursive subpackages)
# Install a Python package (and its recursive subpackages).
#
# Can be called multiple times with the same package name to merge
# multiple source directories into a single installed package.
# When called more than once, source directories are merged at build time
# and the last call's parameters take precedence (last registered
# directory wins on file conflicts). Callers providing build-time-generated
# files should pass DEPENDS to ensure generation completes before the merge.
#
# :param package_name: the Python package name
# :type package_name: string
Expand All @@ -33,15 +40,19 @@
# :type SCRIPTS_DESTINATION: string
# :param SKIP_COMPILE: if set do not byte-compile the installed package
# :type SKIP_COMPILE: option
# :param DEPENDS: build targets that must complete before
# the package files are synced to the build directory
# :type DEPENDS: list of strings
#
macro(ament_python_install_package)
_ament_cmake_python_register_extension_hook()
_ament_cmake_python_register_environment_hook()
_ament_cmake_python_install_package(${ARGN})
endmacro()

function(_ament_cmake_python_install_package package_name)
cmake_parse_arguments(
ARG "SKIP_COMPILE" "PACKAGE_DIR;VERSION;SETUP_CFG;DESTINATION;SCRIPTS_DESTINATION" "" ${ARGN})
ARG "SKIP_COMPILE" "PACKAGE_DIR;VERSION;SETUP_CFG;DESTINATION;SCRIPTS_DESTINATION" "DEPENDS" ${ARGN})
if(ARG_UNPARSED_ARGUMENTS)
message(FATAL_ERROR "ament_python_install_package() called with unused "
"arguments: ${ARG_UNPARSED_ARGUMENTS}")
Expand Down Expand Up @@ -83,129 +94,19 @@ function(_ament_cmake_python_install_package package_name)
set(ARG_DESTINATION ${PYTHON_INSTALL_DIR})
endif()

set(build_dir "${CMAKE_CURRENT_BINARY_DIR}/ament_cmake_python/${package_name}")

string(CONFIGURE "\
from setuptools import find_packages
from setuptools import setup

setup(
name='${package_name}',
version='${ARG_VERSION}',
packages=find_packages(
include=('${package_name}', '${package_name}.*')),
)
" setup_py_content)

file(GENERATE
OUTPUT "${build_dir}/setup.py"
CONTENT "${setup_py_content}"
)

if(AMENT_CMAKE_SYMLINK_INSTALL)
add_custom_target(
ament_cmake_python_symlink_${package_name}
COMMAND ${CMAKE_COMMAND} -E create_symlink
"${ARG_PACKAGE_DIR}" "${build_dir}/${package_name}"
)
set(egg_dependencies ament_cmake_python_symlink_${package_name})

if(ARG_SETUP_CFG)
add_custom_target(
ament_cmake_python_symlink_${package_name}_setup
COMMAND ${CMAKE_COMMAND} -E create_symlink
"${ARG_SETUP_CFG}" "${build_dir}/setup.cfg"
)
list(APPEND egg_dependencies ament_cmake_python_symlink_${package_name}_setup)
endif()
get_property(_pkgs GLOBAL PROPERTY AMENT_CMAKE_PYTHON_PKGS)
list(FIND _pkgs "${package_name}" _idx)
if(_idx EQUAL -1)
set_property(GLOBAL APPEND PROPERTY AMENT_CMAKE_PYTHON_PKGS "${package_name}")
else()
add_custom_target(
ament_cmake_python_copy_${package_name}
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${ARG_PACKAGE_DIR}" "${build_dir}/${package_name}"
)
set(egg_dependencies ament_cmake_python_copy_${package_name})

if(ARG_SETUP_CFG)
add_custom_target(
ament_cmake_python_copy_${package_name}_setup
COMMAND ${CMAKE_COMMAND} -E copy
"${ARG_SETUP_CFG}" "${build_dir}/setup.cfg"
)
list(APPEND egg_dependencies ament_cmake_python_copy_${package_name}_setup)
endif()
endif()

# Technically, we should call find_package(Python3) first to ensure that Python3::Interpreter
# is available. But we skip this here because this macro requires ament_cmake, and ament_cmake
# calls find_package(Python3) for us.
get_executable_path(python_interpreter Python3::Interpreter BUILD)

add_custom_target(
ament_cmake_python_build_${package_name}_egg ALL
COMMAND ${python_interpreter} setup.py egg_info
WORKING_DIRECTORY "${build_dir}"
DEPENDS ${egg_dependencies}
)

set(python_version "py${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}")

set(egg_name "${package_name}")
set(egg_install_name "${egg_name}-${ARG_VERSION}")
set(egg_install_name "${egg_install_name}-${python_version}")

install(
DIRECTORY "${build_dir}/${egg_name}.egg-info/"
DESTINATION "${ARG_DESTINATION}/${egg_install_name}.egg-info"
)

if(ARG_SCRIPTS_DESTINATION)
file(MAKE_DIRECTORY "${build_dir}/scripts") # setup.py may or may not create it

add_custom_target(
ament_cmake_python_build_${package_name}_scripts ALL
COMMAND ${python_interpreter} setup.py install_scripts -d scripts
WORKING_DIRECTORY "${build_dir}"
DEPENDS ${egg_dependencies}
)

if(NOT AMENT_CMAKE_SYMLINK_INSTALL)
# Not needed for nor supported by symlink installs
set(_extra_install_args USE_SOURCE_PERMISSIONS)
endif()

install(
DIRECTORY "${build_dir}/scripts/"
DESTINATION "${ARG_SCRIPTS_DESTINATION}/"
${_extra_install_args}
)
endif()

install(
DIRECTORY "${ARG_PACKAGE_DIR}/"
DESTINATION "${ARG_DESTINATION}/${package_name}"
PATTERN "*.pyc" EXCLUDE
PATTERN "__pycache__" EXCLUDE
)

if(NOT ARG_SKIP_COMPILE)
get_executable_path(python_interpreter_config Python3::Interpreter CONFIGURE)
# compile Python files
install(CODE
"execute_process(
COMMAND
\"${python_interpreter_config}\" \"-m\" \"compileall\"
\"${CMAKE_INSTALL_PREFIX}/${ARG_DESTINATION}/${package_name}\"
)"
)
message(STATUS "ament_python_install_package: extending '${package_name}'")
endif()

if(package_name IN_LIST AMENT_CMAKE_PYTHON_INSTALL_INSTALLED_NAMES)
message(FATAL_ERROR
"ament_python_install_package() a Python module file or package with "
"the same name '${package_name}' has been installed before")
endif()
list(APPEND AMENT_CMAKE_PYTHON_INSTALL_INSTALLED_NAMES "${package_name}")
set(AMENT_CMAKE_PYTHON_INSTALL_INSTALLED_NAMES
"${AMENT_CMAKE_PYTHON_INSTALL_INSTALLED_NAMES}" PARENT_SCOPE)
set_property(GLOBAL PROPERTY AMENT_CMAKE_PYTHON_${package_name}_SKIP_COMPILE "${ARG_SKIP_COMPILE}")
set_property(GLOBAL PROPERTY AMENT_CMAKE_PYTHON_${package_name}_VERSION "${ARG_VERSION}")
set_property(GLOBAL PROPERTY AMENT_CMAKE_PYTHON_${package_name}_SETUP_CFG "${ARG_SETUP_CFG}")
set_property(GLOBAL PROPERTY AMENT_CMAKE_PYTHON_${package_name}_DESTINATION "${ARG_DESTINATION}")
set_property(GLOBAL PROPERTY AMENT_CMAKE_PYTHON_${package_name}_SCRIPTS_DESTINATION "${ARG_SCRIPTS_DESTINATION}")
set_property(GLOBAL APPEND PROPERTY AMENT_CMAKE_PYTHON_${package_name}_DEPENDS ${ARG_DEPENDS})
set_property(GLOBAL APPEND PROPERTY AMENT_CMAKE_PYTHON_${package_name}_PACKAGE_DIRS "${ARG_PACKAGE_DIR}")
endfunction()
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Copyright 2025 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

function(ament_cmake_python_install_registered_packages)
get_property(_pkgs GLOBAL PROPERTY AMENT_CMAKE_PYTHON_PKGS)
foreach(pkg IN LISTS _pkgs)
_ament_cmake_python_install_package_impl(${pkg})
endforeach()
endfunction()

function(_ament_cmake_python_install_package_impl package_name)
foreach(_prop IN ITEMS SKIP_COMPILE VERSION SETUP_CFG DESTINATION SCRIPTS_DESTINATION PACKAGE_DIRS DEPENDS)
get_property(_${_prop} GLOBAL PROPERTY AMENT_CMAKE_PYTHON_${package_name}_${_prop})
endforeach()

_ament_cmake_python_prepare_build(${package_name})
_ament_cmake_python_copy_build_files(${package_name})

# Technically, we should call find_package(Python3) first to ensure that Python3::Interpreter
# is available. But we skip this here because this macro requires ament_cmake, and ament_cmake
# calls find_package(Python3) for us.
get_executable_path(python_interpreter Python3::Interpreter BUILD)

_ament_cmake_python_generate_egg(${package_name})

if(_SCRIPTS_DESTINATION)
_ament_cmake_python_install_scripts(${package_name})
endif()

_ament_cmake_python_install_sources(${package_name})

if(NOT _SKIP_COMPILE)
_ament_cmake_python_byte_compile(${package_name})
endif()

endfunction()

macro(_ament_cmake_python_prepare_build package_name)
set(_build_dir "${CMAKE_CURRENT_BINARY_DIR}/ament_cmake_python/${package_name}")

string(CONFIGURE "\
from setuptools import find_packages
from setuptools import setup

setup(
name='${package_name}',
version='${_VERSION}',
packages=find_packages(
include=('${package_name}', '${package_name}.*')),
)
" setup_py_content)

file(GENERATE
OUTPUT "${_build_dir}/setup.py"
CONTENT "${setup_py_content}"
)

endmacro()

macro(_ament_cmake_python_copy_build_files package_name)
set(_sync_target "ament_cmake_python_sync_${package_name}")

set(_sync_commands)
foreach(_dir IN LISTS _PACKAGE_DIRS)
list(APPEND _sync_commands
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${_dir}" "${_build_dir}/${package_name}")
endforeach()

if(_SETUP_CFG)
list(APPEND _sync_commands
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${_SETUP_CFG}" "${_build_dir}/setup.cfg")
endif()

add_custom_target(${_sync_target} ${_sync_commands})

if(_DEPENDS)
add_dependencies(${_sync_target} ${_DEPENDS})
endif()

endmacro()

macro(_ament_cmake_python_generate_egg package_name)
add_custom_target(
ament_cmake_python_build_${package_name}_egg ALL
COMMAND ${python_interpreter} setup.py egg_info
WORKING_DIRECTORY "${_build_dir}"
DEPENDS ${_sync_target}
)

set(python_version "py${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}")

set(egg_name "${package_name}")
set(egg_install_name "${egg_name}-${_VERSION}")
set(egg_install_name "${egg_install_name}-${python_version}")

install(
DIRECTORY "${_build_dir}/${egg_name}.egg-info/"
DESTINATION "${_DESTINATION}/${egg_install_name}.egg-info"
)
endmacro()

macro(_ament_cmake_python_install_scripts package_name)
file(MAKE_DIRECTORY "${_build_dir}/scripts") # setup.py may or may not create it

add_custom_target(
ament_cmake_python_build_${package_name}_scripts ALL
COMMAND ${python_interpreter} setup.py install_scripts -d scripts
WORKING_DIRECTORY "${_build_dir}"
DEPENDS ${_sync_target}
)

if(NOT AMENT_CMAKE_SYMLINK_INSTALL)
# Not needed for nor supported by symlink installs
set(_extra_install_args USE_SOURCE_PERMISSIONS)
endif()

install(
DIRECTORY "${_build_dir}/scripts/"
DESTINATION "${_SCRIPTS_DESTINATION}/"
${_extra_install_args}
)
endmacro()

macro(_ament_cmake_python_install_sources package_name)
list(LENGTH _PACKAGE_DIRS _num_dirs)
if(_num_dirs EQUAL 1)
# For single dir we install from source to maintain the original behavior
install(
DIRECTORY "${_PACKAGE_DIRS}/"
DESTINATION "${_DESTINATION}/${package_name}"
PATTERN "*.pyc" EXCLUDE
PATTERN "__pycache__" EXCLUDE
)
elseif(AMENT_CMAKE_SYMLINK_INSTALL)
set(_DIRS_TO_INSTALL "${_PACKAGE_DIRS}")
list(TRANSFORM _DIRS_TO_INSTALL APPEND "/")
foreach(_dir IN LISTS _DIRS_TO_INSTALL)
install(
DIRECTORY "${_dir}"
DESTINATION "${_DESTINATION}/${package_name}"
PATTERN "*.pyc" EXCLUDE
PATTERN "__pycache__" EXCLUDE
)
endforeach()
else()
install(
DIRECTORY "${_build_dir}/${package_name}/"
DESTINATION "${_DESTINATION}/${package_name}"
PATTERN "*.pyc" EXCLUDE
PATTERN "__pycache__" EXCLUDE
)
endif()
endmacro()

macro(_ament_cmake_python_byte_compile package_name)
get_executable_path(python_interpreter_config Python3::Interpreter CONFIGURE)
# compile Python files
install(CODE
"execute_process(
COMMAND
\"${python_interpreter_config}\" \"-m\" \"compileall\"
\"${CMAKE_INSTALL_PREFIX}/${_DESTINATION}/${package_name}\"
)"
)
endmacro()

ament_cmake_python_install_registered_packages()
1 change: 0 additions & 1 deletion ament_cmake_python/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
<author email="michel@ekumenlabs.com">Michel Hidalgo</author>

<buildtool_depend>ament_cmake_core</buildtool_depend>

<buildtool_export_depend>ament_cmake_core</buildtool_export_depend>

<export>
Expand Down
Loading