From 833efc0eadbf389abf28be8d488a14f935f61fe0 Mon Sep 17 00:00:00 2001 From: Adam Simpkins Date: Mon, 18 Nov 2019 18:38:50 -0800 Subject: [PATCH] getdeps: update FBPythonBinary.cmake to generate executable files on Windows Summary: On Windows, compile a small C executable to prepend to the zip file, to allow the resulting executable files to be run directly on Windows, without needing to explicitly invoke the Python interpreter to run the output file. Reviewed By: wez Differential Revision: D17733616 fbshipit-source-id: 989a93851412d0bbe1e7857aa9111db082f67a4c --- .../fbcode_builder/CMake/FBPythonBinary.cmake | 81 +++++++++++++++---- build/fbcode_builder/CMake/fb_py_win_main.c | 27 +++++++ 2 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 build/fbcode_builder/CMake/fb_py_win_main.c diff --git a/build/fbcode_builder/CMake/FBPythonBinary.cmake b/build/fbcode_builder/CMake/FBPythonBinary.cmake index cf098ffb6..9900b37e5 100644 --- a/build/fbcode_builder/CMake/FBPythonBinary.cmake +++ b/build/fbcode_builder/CMake/FBPythonBinary.cmake @@ -27,11 +27,17 @@ include(FBCMakeParseArgs) # If we fail to find python now we won't fail immediately, but # add_fb_python_executable() or add_fb_python_library() will fatal out if they # are used. -if(NOT Python3_EXECUTABLE) +if(NOT TARGET Python3::Interpreter) # CMake 3.12+ ships with a FindPython3.cmake module. Try using it first. # We find with QUIET here, since otherwise this generates some noisy warnings # on versions of CMake before 3.12 - find_package(Python3 COMPONENTS Interpreter QUIET) + if (WIN32) + # On Windows we need both the Intepreter as well as the Development + # libraries. + find_package(Python3 COMPONENTS Interpreter Development QUIET) + else() + find_package(Python3 COMPONENTS Interpreter QUIET) + endif() if(Python3_Interpreter_FOUND) message(STATUS "Found Python 3: ${Python3_EXECUTABLE}") else() @@ -41,10 +47,15 @@ if(NOT Python3_EXECUTABLE) if(NOT PYTHONINTERP_FOUND) set(Python_ADDITIONAL_VERSIONS 3 3.6 3.5 3.4 3.3 3.2 3.1) find_package(PythonInterp) + # TODO: On Windows we require the Python libraries as well. + # We currently do not search for them on this code path. + # For now we require building with CMake 3.12+ on Windows, so that the + # FindPython3 code path above is available. endif() if(PYTHONINTERP_FOUND) if("${PYTHON_VERSION_MAJOR}" GREATER_EQUAL 3) set(Python3_EXECUTABLE "${PYTHON_EXECUTABLE}") + add_custom_target(Python3::Interpreter) else() string( CONCAT FBPY_FIND_PYTHON_ERR @@ -67,6 +78,10 @@ set( FB_PY_TEST_DISCOVER_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/FBPythonTestAddTests.cmake" ) +set( + FB_PY_WIN_MAIN_C + "${CMAKE_CURRENT_LIST_DIR}/fb_py_win_main.c" +) # An option to control the default installation location for # install_fb_python_library(). This is relative to ${CMAKE_INSTALL_PREFIX} @@ -85,7 +100,7 @@ set( # run. If left unspecified, a __main__.py script must be present in the # manifest. # -function(add_fb_python_executable EXE_NAME) +function(add_fb_python_executable TARGET) fb_py_check_available() # Parse the arguments @@ -98,7 +113,7 @@ function(add_fb_python_executable EXE_NAME) # Use add_fb_python_library() to perform most of our source handling add_fb_python_library( - "${EXE_NAME}.main_lib" + "${TARGET}.main_lib" BASE_DIR "${ARG_BASE_DIR}" NAMESPACE "${ARG_NAMESPACE}" SOURCES ${ARG_SOURCES} @@ -107,11 +122,11 @@ function(add_fb_python_executable EXE_NAME) set( manifest_files - "$" + "$" ) set( source_files - "$" + "$" ) # The command to build the executable archive. @@ -127,16 +142,25 @@ function(add_fb_python_executable EXE_NAME) set(make_py_args --manifest-separator "::" "$") endif() - set(output_file "${EXE_NAME}") + set(output_file "${TARGET}${CMAKE_EXECUTABLE_SUFFIX}") + if(WIN32) + set(zipapp_output "${TARGET}.py_zipapp") + else() + set(zipapp_output "${output_file}") + endif() + set(zipapp_output_file "${zipapp_output}") + + set(is_dir_output FALSE) if(DEFINED ARG_TYPE) list(APPEND make_py_args "--type" "${ARG_TYPE}") if ("${ARG_TYPE}" STREQUAL "dir") + set(is_dir_output TRUE) # CMake doesn't really seem to like having a directory specified as an # output; specify the __main__.py file as the output instead. - set(output_file "${EXE_NAME}/__main__.py") + set(zipapp_output_file "${zipapp_output}/__main__.py") list(APPEND extra_cmd_params - COMMAND "${CMAKE_COMMAND}" -E remove_directory "${EXE_NAME}" + COMMAND "${CMAKE_COMMAND}" -E remove_directory "${zipapp_output}" ) endif() endif() @@ -146,26 +170,51 @@ function(add_fb_python_executable EXE_NAME) endif() add_custom_command( - OUTPUT "${output_file}" + OUTPUT "${zipapp_output_file}" ${extra_cmd_params} COMMAND "${Python3_EXECUTABLE}" "${FB_MAKE_PYTHON_ARCHIVE}" - -o "${EXE_NAME}" + -o "${zipapp_output}" ${make_py_args} DEPENDS ${source_files} - "${EXE_NAME}.main_lib.py_sources_built" + "${TARGET}.main_lib.py_sources_built" "${FB_MAKE_PYTHON_ARCHIVE}" ) - # Add an "ALL" target that depends on force ${EXE_NAME}, - # so that ${EXE_NAME} will be included in the default list of build targets. - add_custom_target("${EXE_NAME}.GEN_PY_EXE" ALL DEPENDS "${output_file}") + if(WIN32) + if(is_dir_output) + # TODO: generate a main executable that will invoke Python3 + # with the correct main module inside the output directory + else() + add_executable("${TARGET}.winmain" "${FB_PY_WIN_MAIN_C}") + target_link_libraries("${TARGET}.winmain" Python3::Python) + # The Python3::Python target doesn't seem to be set up completely + # correctly on Windows for some reason, and we have to explicitly add + # ${Python3_LIBRARY_DIRS} to the target link directories. + target_link_directories( + "${TARGET}.winmain" + PUBLIC ${Python3_LIBRARY_DIRS} + ) + add_custom_command( + OUTPUT "${output_file}" + DEPENDS "${TARGET}.winmain" "${zipapp_output_file}" + COMMAND + "cmd.exe" "/c" "copy" "/b" + "${TARGET}.winmain${CMAKE_EXECUTABLE_SUFFIX}+${zipapp_output}" + "${output_file}" + ) + endif() + endif() + + # Add an "ALL" target that depends on force ${TARGET}, + # so that ${TARGET} will be included in the default list of build targets. + add_custom_target("${TARGET}.GEN_PY_EXE" ALL DEPENDS "${output_file}") # Allow resolving the executable path for the target that we generate # via a generator expression like: # "WATCHMAN_WAIT_PATH=$" - set_property(TARGET "${EXE_NAME}.GEN_PY_EXE" + set_property(TARGET "${TARGET}.GEN_PY_EXE" PROPERTY EXECUTABLE "${CMAKE_CURRENT_BINARY_DIR}/${output_file}") endfunction() diff --git a/build/fbcode_builder/CMake/fb_py_win_main.c b/build/fbcode_builder/CMake/fb_py_win_main.c new file mode 100644 index 000000000..a5aee3b4a --- /dev/null +++ b/build/fbcode_builder/CMake/fb_py_win_main.c @@ -0,0 +1,27 @@ +// Copyright (c) Facebook, Inc. and its affiliates. + +#define Py_LIMITED_API 1 +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include + +int wmain() { + /* + * This executable will be prepended to the start of a Python ZIP archive. + * Python will be able to directly execute the ZIP archive, so we simply + * need to tell Py_Main() to run our own file. Duplicate the argument list + * and add our file name to the beginning to tell Python what file to invoke. + */ + wchar_t** pyargv = malloc(sizeof(wchar_t*) * (__argc + 1)); + if (!pyargv) { + fprintf(stderr, "error: failed to allocate argument vector\n"); + return 1; + } + pyargv[0] = __wargv[0]; + for (int n = 0; n < __argc; ++n) { + pyargv[n + 1] = __wargv[n]; + } + return Py_Main(__argc + 1, pyargv); +}