diff --git a/CMakeLists.txt b/CMakeLists.txt index dabe7f266..730e0fa21 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -386,6 +386,14 @@ python_extract_version_info( message(STATUS "PY_VERSION : ${PY_VERSION}") message(STATUS "PY_VERSION_LONG: ${PY_VERSION_LONG}") +# Extract "Field3" value and set variable PY_FIELD3_VALUE +python_compute_release_field3_value( + VERSION_PATCH "${PY_VERSION_PATCH}" + RELEASE_LEVEL "${PY_RELEASE_LEVEL}" + RELEASE_SERIAL "${PY_RELEASE_SERIAL}" +) +message(STATUS "PY_FIELD3_VALUE: ${PY_FIELD3_VALUE}") + # Check version if(NOT DEFINED _download_${PY_VERSION_LONG}_md5) message(WARNING "warning: selected python version '${PY_VERSION_LONG}' is not tested.") @@ -634,6 +642,9 @@ add_subdirectory(cmake/tools CMakeBuild/tools) if(BUILD_WININST) add_subdirectory(cmake/PC/bdist_wininst CMakeBuild/bdist_wininst) endif() +if(WIN32) + add_subdirectory(cmake/PC/launcher CMakeBuild/launcher) +endif() # Ensure the "_testcapi" extension introduced in Python 3.12 can find # find "Python3.lib" as it is specified in "PC/pyconfig." using @@ -861,6 +872,10 @@ if(BUILD_TESTING) python_extract_version_info cmake/PythonExtractVersionInfo.cmake ) + add_cmakescript_test( + python_compute_release_field3_value + cmake/PythonExtractVersionInfo.cmake + ) endif() include(CMakePackageConfigHelpers) diff --git a/cmake/PC/launcher/CMakeLists.txt b/cmake/PC/launcher/CMakeLists.txt new file mode 100644 index 000000000..7ed270e91 --- /dev/null +++ b/cmake/PC/launcher/CMakeLists.txt @@ -0,0 +1,139 @@ + +function(_add_executable_without_windows target_name) + # Work around the lack of a "target_remove_definitions()" CMake command by + # explicitly undefining _WINDOWS. The following alternatives are either ineffective + # or not applicable: + # + # (1) Modifying and restoring CMAKE_C_FLAGS does not work reliably, as CMake appears + # to apply cached values during the generation step after the function executes. + # + # (2) Using get_target_property/set_property to manipulate COMPILE_DEFINITIONS + # is ineffective, since /D_WINDOWS is introduced via CMAKE_C_FLAGS and not + # associated with the target's properties. + # + # See: https://gitlab.kitware.com/cmake/cmake/-/issues/19796 + + add_executable(${target_name} ${ARGN}) + + # Note: /U_WINDOWS overrides the implicit /D_WINDOWS flag, resulting in MSVC warning D9025. + # This warning is harmless and cannot be suppressed directly. + target_compile_options(${target_name} PRIVATE /U_WINDOWS) +endfunction() + +# Install tree directory +set(LAUNCHER_INSTALL_DIR ${BIN_BUILD_DIR}) + +# Build tree directory +set(LAUNCHER_BUILD_DIR ${PROJECT_BINARY_DIR}/${LAUNCHER_INSTALL_DIR}) + +set(launcher_targets) + +set(build_venvlauncher 0) +# While support for building "venvlauncher" was introduced in Python 3.3, +# we require at least Python 3.9 to avoid the need to generate "pythonnt_rc.h", +# which was removed in python/cpython@4efc3360c9a ("bpo-41054: Simplify resource compilation on Windows (GH-21004)", 2020-06-24). +if(PY_VERSION VERSION_GREATER_EQUAL "3.9") + set(build_venvlauncher 1) +endif() + +if(build_venvlauncher) + set(target_sources + ${SRC_DIR}/PC/launcher.c + ${SRC_DIR}/PC/pylauncher.rc + ) + set(target_include_dirs + ${SRC_DIR}/PC/ + ) + set(target_libraries + version + ) + + # venvlauncher + set(target_name "venvlauncher") + + _add_executable_without_windows(${target_name} ${target_sources}) + target_include_directories(${target_name} PRIVATE ${target_include_dirs}) + target_link_libraries(${target_name} PRIVATE ${target_libraries}) + target_compile_definitions(${target_name} + PRIVATE + _CONSOLE + _UNICODE + VENV_REDIRECT + PY_ICON # For "PC/pylauncher.rc" + FIELD3=${PY_FIELD3_VALUE} + ) + list(APPEND launcher_targets ${target_name}) + + # venvwlauncher + set(target_name "venvwlauncher") + + add_executable(${target_name} WIN32 ${target_sources}) + target_include_directories(${target_name} PRIVATE ${target_include_dirs}) + target_link_libraries(${target_name} PRIVATE ${target_libraries}) + target_compile_definitions(${target_name} + PRIVATE + _UNICODE + _WINDOWS + VENV_REDIRECT + PYW_ICON # For "PC/pylauncher.rc" + FIELD3=${PY_FIELD3_VALUE} + ) + list(APPEND launcher_targets ${target_name}) +endif() + +set(build_pylauncher 0) +if(PY_VERSION VERSION_GREATER_EQUAL "3.11") + set(build_pylauncher 1) +endif() + +if(build_pylauncher) + set(target_sources + ${SRC_DIR}/PC/launcher2.c + ${SRC_DIR}/PC/pylauncher.rc + ) + set(target_include_dirs + ${SRC_DIR}/PC/ + ) + set(target_libraries + pathcch + shell32 + ) + + # pylauncher + set(target_name "pylauncher") + + _add_executable_without_windows(${target_name} ${target_sources}) + target_include_directories(${target_name} PRIVATE ${target_include_dirs}) + target_link_libraries(${target_name} PRIVATE ${target_libraries}) + target_compile_definitions(${target_name} + PRIVATE + _CONSOLE + _UNICODE + FIELD3=${PY_FIELD3_VALUE} + ) + list(APPEND launcher_targets ${target_name}) + + # pywlauncher + set(target_name "pywlauncher") + + add_executable(${target_name} WIN32 ${target_sources}) + target_include_directories(${target_name} PRIVATE ${target_include_dirs}) + target_link_libraries(${target_name} PRIVATE ${target_libraries}) + target_compile_definitions(${target_name} + PRIVATE + _UNICODE + _WINDOWS + FIELD3=${PY_FIELD3_VALUE} + ) + + list(APPEND launcher_targets ${target_name}) +endif() + +if(launcher_targets) + set_target_properties(${launcher_targets} + PROPERTIES + LINK_FLAGS "/MANIFEST:NO" + RUNTIME_OUTPUT_DIRECTORY ${LAUNCHER_INSTALL_DIR} + ) + install(TARGETS ${launcher_targets} RUNTIME DESTINATION ${BIN_INSTALL_DIR} COMPONENT Runtime) +endif() diff --git a/cmake/PythonExtractVersionInfo.cmake b/cmake/PythonExtractVersionInfo.cmake index e20a4dcb3..85948d4e5 100644 --- a/cmake/PythonExtractVersionInfo.cmake +++ b/cmake/PythonExtractVersionInfo.cmake @@ -139,3 +139,131 @@ endfunction() if(TEST_python_extract_version_info) python_extract_version_info_test() endif() + +# Compute the "Field3" value from a Python version's patch, release level, and serial. +# +# The Field3 value is defined as: +# Field3 = patch * 1000 + release_level_number * 10 + release_serial +# +# Where release_level_number is: +# - 10 for alpha (a) +# - 11 for beta (b) +# - 12 for release candidate (rc) +# - 15 for final (no pre-release tag) +# +# Arguments: +# VERSION_PATCH - Patch version (Z in X.Y.Z) +# RELEASE_LEVEL - One of 'a', 'b', 'rc', or empty +# RELEASE_SERIAL - An integer (or empty for final) +# +# Output: +# Sets variable PY_FIELD3_VALUE in the caller's scope. +function(python_compute_release_field3_value) + set(options) + set(oneValueArgs + VERSION_PATCH + RELEASE_LEVEL + RELEASE_SERIAL + ) + set(multiValueArgs) + cmake_parse_arguments(MY + "${options}" + "${oneValueArgs}" + "${multiValueArgs}" + ${ARGN} + ) + + # Default ReleaseLevelNumber = 15 (final release) + set(_level_number 15) + + # Map release level string to numeric code + if(MY_RELEASE_LEVEL STREQUAL "a") + set(_level_number 10) + elseif(MY_RELEASE_LEVEL STREQUAL "b") + set(_level_number 11) + elseif(MY_RELEASE_LEVEL STREQUAL "rc") + set(_level_number 12) + endif() + + # Fallback for empty serial + if("${MY_RELEASE_SERIAL}" STREQUAL "") + set(MY_RELEASE_SERIAL 0) + endif() + + # Convert to integers + set(_patch "${MY_VERSION_PATCH}") + set(_serial "${MY_RELEASE_SERIAL}") + math(EXPR _field3 "${_patch} * 1000 + ${_level_number} * 10 + ${_serial}") + + # Return in the variable specified by caller + set(PY_FIELD3_VALUE "${_field3}" PARENT_SCOPE) +endfunction() + +# +# cmake -DTEST_python_compute_release_field3_value:BOOL=ON -P PythonExtractVersionInfo.cmake +# +function(python_compute_release_field3_value_test) + + function(display_field3_test_values) + message(" PATCH: ${patch}") + message(" LEVEL: ${level}") + message(" SERIAL: ${serial}") + message(" Expected: ${expected}") + message(" Computed: ${PY_FIELD3_VALUE}") + endfunction() + + set(id 1) + set(case${id}_patch 2) + set(case${id}_level "") # final release + set(case${id}_serial "") # default to 0 + set(case${id}_expected 2150) # 2*1000 + 15*10 + 0 + + set(id 2) + set(case${id}_patch 2) + set(case${id}_level a) + set(case${id}_serial 1) + set(case${id}_expected 2101) # 2*1000 + 10*10 + 1 + + set(id 3) + set(case${id}_patch 5) + set(case${id}_level b) + set(case${id}_serial 0) + set(case${id}_expected 5110) # 5*1000 + 11*10 + 0 + + set(id 4) + set(case${id}_patch 14) + set(case${id}_level rc) + set(case${id}_serial 3) + set(case${id}_expected 14123) # 14*1000 + 12*10 + 3 + + set(id 5) + set(case${id}_patch 0) + set(case${id}_level "") # final + set(case${id}_serial "") # default 0 + set(case${id}_expected 150) # 0 * 1000 + 15 * 10 + 0 + + foreach(caseid RANGE 1 ${id}) + set(patch "${case${caseid}_patch}") + set(level "${case${caseid}_level}") + set(serial "${case${caseid}_serial}") + set(expected "${case${caseid}_expected}") + + python_compute_release_field3_value( + VERSION_PATCH "${patch}" + RELEASE_LEVEL "${level}" + RELEASE_SERIAL "${serial}" + ) + + if(NOT "${PY_FIELD3_VALUE}" STREQUAL "${expected}") + message("FAILED: case ${caseid}") + display_field3_test_values() + message(FATAL_ERROR "Test failed at case ${caseid}") + endif() + endforeach() + + message("SUCCESS") +endfunction() + +if(TEST_python_compute_release_field3_value) + python_compute_release_field3_value_test() +endif()