diff --git a/ament_cmake_python/ament_cmake_python-extras.cmake b/ament_cmake_python/ament_cmake_python-extras.cmake
index d8825818..bd239c4a 100644
--- a/ament_cmake_python/ament_cmake_python-extras.cmake
+++ b/ament_cmake_python/ament_cmake_python-extras.cmake
@@ -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")
diff --git a/ament_cmake_python/cmake/ament_python_install_package.cmake b/ament_cmake_python/cmake/ament_python_install_package.cmake
index 25a08092..26c4e87d 100644
--- a/ament_cmake_python/cmake/ament_python_install_package.cmake
+++ b/ament_cmake_python/cmake/ament_python_install_package.cmake
@@ -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
@@ -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}")
@@ -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()
diff --git a/ament_cmake_python/cmake/ament_python_install_registered_packages.cmake b/ament_cmake_python/cmake/ament_python_install_registered_packages.cmake
new file mode 100644
index 00000000..305d92f9
--- /dev/null
+++ b/ament_cmake_python/cmake/ament_python_install_registered_packages.cmake
@@ -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()
diff --git a/ament_cmake_python/package.xml b/ament_cmake_python/package.xml
index 00b98ffc..f59c8381 100644
--- a/ament_cmake_python/package.xml
+++ b/ament_cmake_python/package.xml
@@ -13,7 +13,6 @@
Michel Hidalgo
ament_cmake_core
-
ament_cmake_core
diff --git a/ament_cmake_python_test/CMakeLists.txt b/ament_cmake_python_test/CMakeLists.txt
new file mode 100644
index 00000000..671fe398
--- /dev/null
+++ b/ament_cmake_python_test/CMakeLists.txt
@@ -0,0 +1,54 @@
+cmake_minimum_required(VERSION 3.12)
+
+project(ament_cmake_python_test)
+
+find_package(ament_cmake_core REQUIRED)
+
+if(BUILD_TESTING)
+ find_package(ament_cmake_pytest REQUIRED)
+ find_package(ament_cmake_python REQUIRED)
+
+ ament_python_install_package(
+ ament_python_test_package
+ PACKAGE_DIR test/ament_python_test_package
+ )
+ ament_python_install_package(
+ ament_python_test_package_overlay
+ PACKAGE_DIR test/ament_python_test_package
+ )
+ ament_python_install_package(
+ ament_python_test_package_overlay
+ PACKAGE_DIR test/ament_python_test_package_overlay
+ )
+
+ ament_python_install_package(
+ ament_python_test_package_no_compile
+ PACKAGE_DIR test/ament_python_test_package
+ SKIP_COMPILE
+ )
+
+ ament_python_install_package(
+ ament_python_test_package_versioned
+ PACKAGE_DIR test/ament_python_test_package
+ VERSION 1.2.3
+ )
+
+ set(_generated_dir "${CMAKE_CURRENT_BINARY_DIR}/generated_pkg")
+ file(WRITE "${_generated_dir}/__init__.py" "generated = True\n")
+ add_custom_target(generate_test_files
+ COMMAND ${CMAKE_COMMAND} -E touch "${_generated_dir}/_generated_marker.py"
+ )
+ ament_python_install_package(
+ ament_python_test_package_generated
+ PACKAGE_DIR "${_generated_dir}"
+ DEPENDS generate_test_files
+ )
+
+ ament_add_pytest_test(
+ test_ament_python_install_package
+ test/test_ament_python_install_package.py
+ ENV TEST_PACKAGE_INSTALL_DIR=${CMAKE_INSTALL_PREFIX}/${PYTHON_INSTALL_DIR}
+ )
+endif()
+
+ament_package()
diff --git a/ament_cmake_python_test/package.xml b/ament_cmake_python_test/package.xml
new file mode 100644
index 00000000..676473ce
--- /dev/null
+++ b/ament_cmake_python_test/package.xml
@@ -0,0 +1,21 @@
+
+
+
+ ament_cmake_python_test
+ 0.0.0
+ Test package for ament_cmake_python.
+
+ Nadav Elkabets
+
+ Apache License 2.0
+
+ ament_cmake_core
+ ament_cmake_pytest
+ ament_cmake_python
+
+ ament_cmake_core
+
+
+ ament_cmake
+
+
diff --git a/ament_cmake_python_test/test/ament_python_test_package/__init__.py b/ament_cmake_python_test/test/ament_python_test_package/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ament_cmake_python_test/test/ament_python_test_package/main.py b/ament_cmake_python_test/test/ament_python_test_package/main.py
new file mode 100644
index 00000000..5e3944c4
--- /dev/null
+++ b/ament_cmake_python_test/test/ament_python_test_package/main.py
@@ -0,0 +1,3 @@
+class Bar:
+ def __init__(self) -> None:
+ pass
diff --git a/ament_cmake_python_test/test/ament_python_test_package_overlay/__init__.py b/ament_cmake_python_test/test/ament_python_test_package_overlay/__init__.py
new file mode 100644
index 00000000..8fa4805b
--- /dev/null
+++ b/ament_cmake_python_test/test/ament_python_test_package_overlay/__init__.py
@@ -0,0 +1 @@
+from .subdir.utils import foo
diff --git a/ament_cmake_python_test/test/ament_python_test_package_overlay/main.py b/ament_cmake_python_test/test/ament_python_test_package_overlay/main.py
new file mode 100644
index 00000000..1589c95c
--- /dev/null
+++ b/ament_cmake_python_test/test/ament_python_test_package_overlay/main.py
@@ -0,0 +1,5 @@
+class Bar:
+ """Overlay version."""
+
+ def __init__(self) -> None:
+ pass
diff --git a/ament_cmake_python_test/test/ament_python_test_package_overlay/subdir/__init__.py b/ament_cmake_python_test/test/ament_python_test_package_overlay/subdir/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ament_cmake_python_test/test/ament_python_test_package_overlay/subdir/utils.py b/ament_cmake_python_test/test/ament_python_test_package_overlay/subdir/utils.py
new file mode 100644
index 00000000..256ce6d1
--- /dev/null
+++ b/ament_cmake_python_test/test/ament_python_test_package_overlay/subdir/utils.py
@@ -0,0 +1,2 @@
+def foo() -> None:
+ pass
diff --git a/ament_cmake_python_test/test/test_ament_python_install_package.py b/ament_cmake_python_test/test/test_ament_python_install_package.py
new file mode 100644
index 00000000..a69b3617
--- /dev/null
+++ b/ament_cmake_python_test/test/test_ament_python_install_package.py
@@ -0,0 +1,69 @@
+from pathlib import Path
+import filecmp
+import os
+import shutil
+
+INSTALL_DIR = Path(os.environ["TEST_PACKAGE_INSTALL_DIR"])
+TEST_DIR = Path(__file__).parent
+PKG = "ament_python_test_package"
+PKG_OVERLAY = PKG + "_overlay"
+PKG_NO_COMPILE = PKG + "_no_compile"
+PKG_VERSIONED = PKG + "_versioned"
+PKG_GENERATED = PKG + "_generated"
+
+
+def _assert_dirs_match(expected, actual):
+ """Recursively assert two directories have identical contents."""
+ cmp = filecmp.dircmp(expected, actual)
+ assert not cmp.diff_files
+ assert not cmp.left_only
+ assert not cmp.right_only
+ for subdir in cmp.common_dirs:
+ if subdir == "__pycache__":
+ continue
+ _assert_dirs_match(Path(expected) / subdir, Path(actual) / subdir)
+
+
+def test_single_package():
+ """Single package dir installs all files correctly."""
+ _assert_dirs_match(TEST_DIR / PKG, INSTALL_DIR / PKG)
+
+
+def test_overlay_merges_files(tmp_path):
+ """Two package dirs merge correctly, last dir wins on conflicts."""
+ merged = tmp_path / "merged"
+ shutil.copytree(TEST_DIR / PKG, merged)
+ shutil.copytree(TEST_DIR / PKG_OVERLAY, merged, dirs_exist_ok=True)
+ _assert_dirs_match(merged, INSTALL_DIR / PKG_OVERLAY)
+
+
+def test_skip_compile():
+ """SKIP_COMPILE prevents .pyc generation."""
+ pkg_dir = INSTALL_DIR / PKG_NO_COMPILE
+ assert pkg_dir.exists()
+ assert not list(pkg_dir.rglob("*.pyc"))
+
+
+def test_default_compiles():
+ """Default behavior produces .pyc files."""
+ pkg_dir = INSTALL_DIR / PKG
+ assert list(pkg_dir.rglob("*.pyc"))
+
+
+def test_egg_info():
+ """Egg-info is generated."""
+ egg_dirs = list(INSTALL_DIR.glob("*.egg-info"))
+ assert egg_dirs
+
+
+def test_explicit_version():
+ """Explicit VERSION propagates to egg-info."""
+ egg_dirs = list(INSTALL_DIR.glob("*versioned*1.2.3*.egg-info"))
+ assert len(egg_dirs) == 1
+
+
+def test_depends_generates_files():
+ """DEPENDS ensures build-time generated files are copied by sync."""
+ pkg_dir = INSTALL_DIR / PKG_GENERATED
+ assert pkg_dir.exists()
+ assert (pkg_dir / "_generated_marker.py").exists()