1 # Copyright (c) Facebook, Inc. and its affiliates.
3 include(FBCMakeParseArgs)
6 # This file contains helper functions for building self-executing Python
9 # This is somewhat different than typical python installation with
10 # distutils/pip/virtualenv/etc. We primarily want to build a standalone
11 # executable, isolated from other Python packages on the system. We don't want
12 # to install files into the standard library python paths. This is more
13 # similar to PEX (https://github.com/pantsbuild/pex) and XAR
14 # (https://github.com/facebookincubator/xar). (In the future it would be nice
15 # to update this code to also support directly generating XAR files if XAR is
18 # We also want to be able to easily define "libraries" of python files that can
19 # be shared and re-used between these standalone python executables, and can be
20 # shared across projects in different repositories. This means that we do need
21 # a way to "install" libraries so that they are visible to CMake builds in
22 # other repositories, without actually installing them in the standard python
26 # If the caller has not already found Python, do so now.
27 # If we fail to find python now we won't fail immediately, but
28 # add_fb_python_executable() or add_fb_python_library() will fatal out if they
30 if(NOT TARGET Python3::Interpreter)
31 # CMake 3.12+ ships with a FindPython3.cmake module. Try using it first.
32 # We find with QUIET here, since otherwise this generates some noisy warnings
33 # on versions of CMake before 3.12
35 # On Windows we need both the Intepreter as well as the Development
37 find_package(Python3 COMPONENTS Interpreter Development QUIET)
39 find_package(Python3 COMPONENTS Interpreter QUIET)
41 if(Python3_Interpreter_FOUND)
42 message(STATUS "Found Python 3: ${Python3_EXECUTABLE}")
44 # Try with the FindPythonInterp.cmake module available in older CMake
45 # versions. Check to see if the caller has already searched for this
47 if(NOT PYTHONINTERP_FOUND)
48 set(Python_ADDITIONAL_VERSIONS 3 3.6 3.5 3.4 3.3 3.2 3.1)
49 find_package(PythonInterp)
50 # TODO: On Windows we require the Python libraries as well.
51 # We currently do not search for them on this code path.
52 # For now we require building with CMake 3.12+ on Windows, so that the
53 # FindPython3 code path above is available.
55 if(PYTHONINTERP_FOUND)
56 if("${PYTHON_VERSION_MAJOR}" GREATER_EQUAL 3)
57 set(Python3_EXECUTABLE "${PYTHON_EXECUTABLE}")
58 add_custom_target(Python3::Interpreter)
61 CONCAT FBPY_FIND_PYTHON_ERR
62 "found Python ${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}, "
70 # Find our helper program.
71 # We typically install this in the same directory as this .cmake file.
73 FB_MAKE_PYTHON_ARCHIVE "make_fbpy_archive.py"
74 PATHS ${CMAKE_MODULE_PATH}
76 set(FB_PY_TEST_MAIN "${CMAKE_CURRENT_LIST_DIR}/fb_py_test_main.py")
78 FB_PY_TEST_DISCOVER_SCRIPT
79 "${CMAKE_CURRENT_LIST_DIR}/FBPythonTestAddTests.cmake"
83 "${CMAKE_CURRENT_LIST_DIR}/fb_py_win_main.c"
86 # An option to control the default installation location for
87 # install_fb_python_library(). This is relative to ${CMAKE_INSTALL_PREFIX}
89 FBPY_LIB_INSTALL_DIR "lib/fb-py-libs" CACHE STRING
90 "The subdirectory where FB python libraries should be installed"
94 # Build a self-executing python binary.
96 # This accepts the same arguments as add_fb_python_library().
98 # In addition, a MAIN_MODULE argument is accepted. This argument specifies
99 # which module should be started as the __main__ module when the executable is
100 # run. If left unspecified, a __main__.py script must be present in the
103 function(add_fb_python_executable TARGET)
104 fb_py_check_available()
106 # Parse the arguments
107 set(one_value_args BASE_DIR NAMESPACE MAIN_MODULE TYPE)
108 set(multi_value_args SOURCES DEPENDS)
110 ARG "" "${one_value_args}" "${multi_value_args}" "${ARGN}"
112 fb_py_process_default_args(ARG_NAMESPACE ARG_BASE_DIR)
114 # Use add_fb_python_library() to perform most of our source handling
115 add_fb_python_library(
117 BASE_DIR "${ARG_BASE_DIR}"
118 NAMESPACE "${ARG_NAMESPACE}"
119 SOURCES ${ARG_SOURCES}
120 DEPENDS ${ARG_DEPENDS}
125 "$<TARGET_PROPERTY:${TARGET}.main_lib.py_lib,INTERFACE_INCLUDE_DIRECTORIES>"
129 "$<TARGET_PROPERTY:${TARGET}.main_lib.py_lib,INTERFACE_SOURCES>"
132 # The command to build the executable archive.
134 # If we are using CMake 3.8+ we can use COMMAND_EXPAND_LISTS.
135 # CMP0067 isn't really the policy we care about, but seems like the best way
136 # to check if we are running 3.8+.
138 set(extra_cmd_params COMMAND_EXPAND_LISTS)
139 set(make_py_args "${manifest_files}")
141 set(extra_cmd_params)
142 set(make_py_args --manifest-separator "::" "$<JOIN:${manifest_files},::>")
145 set(output_file "${TARGET}${CMAKE_EXECUTABLE_SUFFIX}")
147 set(zipapp_output "${TARGET}.py_zipapp")
149 set(zipapp_output "${output_file}")
151 set(zipapp_output_file "${zipapp_output}")
153 set(is_dir_output FALSE)
155 list(APPEND make_py_args "--type" "${ARG_TYPE}")
156 if ("${ARG_TYPE}" STREQUAL "dir")
157 set(is_dir_output TRUE)
158 # CMake doesn't really seem to like having a directory specified as an
159 # output; specify the __main__.py file as the output instead.
160 set(zipapp_output_file "${zipapp_output}/__main__.py")
163 COMMAND "${CMAKE_COMMAND}" -E remove_directory "${zipapp_output}"
168 if(DEFINED ARG_MAIN_MODULE)
169 list(APPEND make_py_args "--main" "${ARG_MAIN_MODULE}")
173 OUTPUT "${zipapp_output_file}"
176 "${Python3_EXECUTABLE}" "${FB_MAKE_PYTHON_ARCHIVE}"
177 -o "${zipapp_output}"
181 "${TARGET}.main_lib.py_sources_built"
182 "${FB_MAKE_PYTHON_ARCHIVE}"
187 # TODO: generate a main executable that will invoke Python3
188 # with the correct main module inside the output directory
190 add_executable("${TARGET}.winmain" "${FB_PY_WIN_MAIN_C}")
191 target_link_libraries("${TARGET}.winmain" Python3::Python)
192 # The Python3::Python target doesn't seem to be set up completely
193 # correctly on Windows for some reason, and we have to explicitly add
194 # ${Python3_LIBRARY_DIRS} to the target link directories.
195 target_link_directories(
197 PUBLIC ${Python3_LIBRARY_DIRS}
200 OUTPUT "${output_file}"
201 DEPENDS "${TARGET}.winmain" "${zipapp_output_file}"
203 "cmd.exe" "/c" "copy" "/b"
204 "${TARGET}.winmain${CMAKE_EXECUTABLE_SUFFIX}+${zipapp_output}"
210 # Add an "ALL" target that depends on force ${TARGET},
211 # so that ${TARGET} will be included in the default list of build targets.
212 add_custom_target("${TARGET}.GEN_PY_EXE" ALL DEPENDS "${output_file}")
214 # Allow resolving the executable path for the target that we generate
215 # via a generator expression like:
216 # "WATCHMAN_WAIT_PATH=$<TARGET_PROPERTY:watchman-wait.GEN_PY_EXE,EXECUTABLE>"
217 set_property(TARGET "${TARGET}.GEN_PY_EXE"
218 PROPERTY EXECUTABLE "${CMAKE_CURRENT_BINARY_DIR}/${output_file}")
221 # Define a python unittest executable.
222 # The executable is built using add_fb_python_executable and has the
223 # following differences:
225 # Each of the source files specified in SOURCES will be imported
226 # and have unittest discovery performed upon them.
227 # Those sources will be imported in the top level namespace.
229 # The ENV argument allows specifying a list of "KEY=VALUE"
230 # pairs that will be used by the test runner to set up the environment
231 # in the child process prior to running the test. This is useful for
232 # passing additional configuration to the test.
233 function(add_fb_python_unittest TARGET)
234 # Parse the arguments
235 set(multi_value_args SOURCES DEPENDS ENV PROPERTIES)
238 WORKING_DIRECTORY BASE_DIR NAMESPACE TEST_LIST DISCOVERY_TIMEOUT
241 ARG "" "${one_value_args}" "${multi_value_args}" "${ARGN}"
243 fb_py_process_default_args(ARG_NAMESPACE ARG_BASE_DIR)
244 if(NOT ARG_WORKING_DIRECTORY)
245 # Default the working directory to the current binary directory.
246 # This matches the default behavior of add_test() and other standard
247 # test functions like gtest_discover_tests()
248 set(ARG_WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")
250 if(NOT ARG_TEST_LIST)
251 set(ARG_TEST_LIST "${TARGET}_TESTS")
253 if(NOT ARG_DISCOVERY_TIMEOUT)
254 set(ARG_DISCOVERY_TIMEOUT 5)
257 # Tell our test program the list of modules to scan for tests.
258 # We scan all modules directly listed in our SOURCES argument, and skip
259 # modules that came from dependencies in the DEPENDS list.
261 # This is written into a __test_modules__.py module that the test runner
265 "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_test_modules.py"
267 file(WRITE "${test_modules_path}" "TEST_MODULES = [\n")
268 string(REPLACE "." "/" namespace_dir "${ARG_NAMESPACE}")
269 if (NOT "${namespace_dir}" STREQUAL "")
270 set(namespace_dir "${namespace_dir}/")
273 foreach(src_path IN LISTS ARG_SOURCES)
274 fb_py_compute_dest_path(
276 "${src_path}" "${namespace_dir}" "${ARG_BASE_DIR}"
278 string(REPLACE "/" "." module_name "${dest_path}")
279 string(REGEX REPLACE "\\.py$" "" module_name "${module_name}")
280 list(APPEND test_modules "${module_name}")
281 file(APPEND "${test_modules_path}" " '${module_name}',\n")
283 file(APPEND "${test_modules_path}" "]\n")
285 # The __main__ is provided by our runner wrapper/bootstrap
286 list(APPEND ARG_SOURCES "${FB_PY_TEST_MAIN}=__main__.py")
287 list(APPEND ARG_SOURCES "${test_modules_path}=__test_modules__.py")
289 add_fb_python_executable(
291 NAMESPACE "${ARG_NAMESPACE}"
292 BASE_DIR "${ARG_BASE_DIR}"
293 SOURCES ${ARG_SOURCES}
294 DEPENDS ${ARG_DEPENDS}
297 # Run test discovery after the test executable is built.
298 # This logic is based on the code for gtest_discover_tests()
299 set(ctest_file_base "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}")
300 set(ctest_include_file "${ctest_file_base}_include.cmake")
301 set(ctest_tests_file "${ctest_file_base}_tests.cmake")
303 TARGET "${TARGET}.GEN_PY_EXE" POST_BUILD
304 BYPRODUCTS "${ctest_tests_file}"
307 -D "TEST_TARGET=${TARGET}"
308 -D "TEST_INTERPRETER=${Python3_EXECUTABLE}"
309 -D "TEST_ENV=${ARG_ENV}"
310 -D "TEST_EXECUTABLE=$<TARGET_PROPERTY:${TARGET}.GEN_PY_EXE,EXECUTABLE>"
311 -D "TEST_WORKING_DIR=${ARG_WORKING_DIRECTORY}"
312 -D "TEST_LIST=${ARG_TEST_LIST}"
313 -D "TEST_PREFIX=${TARGET}::"
314 -D "TEST_PROPERTIES=${ARG_PROPERTIES}"
315 -D "CTEST_FILE=${ctest_tests_file}"
316 -P "${FB_PY_TEST_DISCOVER_SCRIPT}"
321 WRITE "${ctest_include_file}"
322 "if(EXISTS \"${ctest_tests_file}\")\n"
323 " include(\"${ctest_tests_file}\")\n"
325 " add_test(\"${TARGET}_NOT_BUILT\" \"${TARGET}_NOT_BUILT\")\n"
329 DIRECTORY APPEND PROPERTY TEST_INCLUDE_FILES
330 "${ctest_include_file}"
335 # Define a python library.
337 # If you want to install a python library generated from this rule note that
338 # you need to use install_fb_python_library() rather than CMake's built-in
339 # install() function. This will make it available for other downstream
340 # projects to use in their add_fb_python_executable() and
341 # add_fb_python_library() calls. (You do still need to use `install(EXPORT)`
342 # later to install the CMake exports.)
346 # The base directory path to strip off from each source path. All source
347 # files must be inside this directory. If not specified it defaults to
348 # ${CMAKE_CURRENT_SOURCE_DIR}.
349 # - NAMESPACE <namespace>:
350 # The destination namespace where these files should be installed in python
351 # binaries. If not specified, this defaults to the current relative path of
352 # ${CMAKE_CURRENT_SOURCE_DIR} inside ${CMAKE_SOURCE_DIR}. e.g., a python
353 # library defined in the directory repo_root/foo/bar will use a default
354 # namespace of "foo.bar"
355 # - SOURCES <src1> <...>:
356 # The python source files.
357 # You may optionally specify as source using the form: PATH=ALIAS where
358 # PATH is a relative path in the source tree and ALIAS is the relative
359 # path into which PATH should be rewritten. This is useful for mapping
360 # an executable script to the main module in a python executable.
361 # e.g.: `python/bin/watchman-wait=__main__.py`
362 # - DEPENDS <target1> <...>:
363 # Other python libraries that this one depends on.
364 # - INSTALL_DIR <dir>:
365 # The directory where this library should be installed.
366 # install_fb_python_library() must still be called later to perform the
367 # installation. If a relative path is given it will be treated relative to
368 # ${CMAKE_INSTALL_PREFIX}
370 # CMake is unfortunately pretty crappy at being able to define custom build
371 # rules & behaviors. It doesn't support transitive property propagation
372 # between custom targets; only the built-in add_executable() and add_library()
373 # targets support transitive properties.
375 # We hack around this janky CMake behavior by (ab)using interface libraries to
376 # propagate some of the data we want between targets, without actually
377 # generating a C library.
379 # add_fb_python_library(SOMELIB) generates the following things:
380 # - An INTERFACE library rule named SOMELIB.py_lib which tracks some
381 # information about transitive dependencies:
382 # - the transitive set of source files in the INTERFACE_SOURCES property
383 # - the transitive set of manifest files that this library depends on in
384 # the INTERFACE_INCLUDE_DIRECTORIES property.
385 # - A custom command that generates a SOMELIB.manifest file.
386 # This file contains the mapping of source files to desired destination
387 # locations in executables that depend on this library. This manifest file
388 # will then be read at build-time in order to build executables.
390 function(add_fb_python_library LIB_NAME)
391 fb_py_check_available()
393 # Parse the arguments
394 # We use fb_cmake_parse_args() rather than cmake_parse_arguments() since
395 # cmake_parse_arguments() does not handle empty arguments, and it is common
396 # for callers to want to specify an empty NAMESPACE parameter.
397 set(one_value_args BASE_DIR NAMESPACE INSTALL_DIR)
398 set(multi_value_args SOURCES DEPENDS)
400 ARG "" "${one_value_args}" "${multi_value_args}" "${ARGN}"
402 fb_py_process_default_args(ARG_NAMESPACE ARG_BASE_DIR)
404 string(REPLACE "." "/" namespace_dir "${ARG_NAMESPACE}")
405 if (NOT "${namespace_dir}" STREQUAL "")
406 set(namespace_dir "${namespace_dir}/")
409 if(NOT DEFINED ARG_INSTALL_DIR)
410 set(install_dir "${FBPY_LIB_INSTALL_DIR}/")
411 elseif("${ARG_INSTALL_DIR}" STREQUAL "")
414 set(install_dir "${ARG_INSTALL_DIR}/")
417 # message(STATUS "fb py library ${LIB_NAME}: "
418 # "NS=${namespace_dir} BASE=${ARG_BASE_DIR}")
420 # TODO: In the future it would be nice to support pre-compiling the source
421 # files. We could emit a rule to compile each source file and emit a
422 # .pyc/.pyo file here, and then have the manifest reference the pyc/pyo
425 # Define a library target to help pass around information about the library,
426 # and propagate dependency information.
428 # CMake make a lot of assumptions that libraries are C++ libraries. To help
429 # avoid confusion we name our target "${LIB_NAME}.py_lib" rather than just
430 # "${LIB_NAME}". This helps avoid confusion if callers try to use
431 # "${LIB_NAME}" on their own as a target name. (e.g., attempting to install
432 # it directly with install(TARGETS) won't work. Callers must use
433 # install_fb_python_library() instead.)
434 add_library("${LIB_NAME}.py_lib" INTERFACE)
436 # Emit the manifest file.
438 # We write the manifest file to a temporary path first, then copy it with
439 # configure_file(COPYONLY). This is necessary to get CMake to understand
440 # that "${manifest_path}" is generated by the CMake configure phase,
441 # and allow using it as a dependency for add_custom_command().
442 # (https://gitlab.kitware.com/cmake/cmake/issues/16367)
443 set(manifest_path "${CMAKE_CURRENT_BINARY_DIR}/${LIB_NAME}.manifest")
444 set(tmp_manifest "${manifest_path}.tmp")
445 file(WRITE "${tmp_manifest}" "FBPY_MANIFEST 1\n")
447 foreach(src_path IN LISTS ARG_SOURCES)
448 fb_py_compute_dest_path(
450 "${src_path}" "${namespace_dir}" "${ARG_BASE_DIR}"
452 list(APPEND abs_sources "${abs_source}")
454 "${LIB_NAME}.py_lib" INTERFACE
455 "$<BUILD_INTERFACE:${abs_source}>"
456 "$<INSTALL_INTERFACE:${install_dir}${LIB_NAME}/${dest_path}>"
459 APPEND "${tmp_manifest}"
460 "${abs_source} :: ${dest_path}\n"
463 configure_file("${tmp_manifest}" "${manifest_path}" COPYONLY)
465 target_include_directories(
466 "${LIB_NAME}.py_lib" INTERFACE
467 "$<BUILD_INTERFACE:${manifest_path}>"
468 "$<INSTALL_INTERFACE:${install_dir}${LIB_NAME}.manifest>"
471 # Add a target that depends on all of the source files.
472 # This is needed in case some of the source files are generated. This will
473 # ensure that these source files are brought up-to-date before we build
474 # any python binaries that depend on this library.
475 add_custom_target("${LIB_NAME}.py_sources_built" DEPENDS ${abs_sources})
476 add_dependencies("${LIB_NAME}.py_lib" "${LIB_NAME}.py_sources_built")
478 # Hook up library dependencies, and also make the *.py_sources_built target
479 # depend on the sources for all of our dependencies also being up-to-date.
480 foreach(dep IN LISTS ARG_DEPENDS)
481 target_link_libraries("${LIB_NAME}.py_lib" INTERFACE "${dep}.py_lib")
483 # Mark that our .py_sources_built target depends on each our our dependent
484 # libraries. This serves two functions:
485 # - This causes CMake to generate an error message if one of the
486 # dependencies is never defined. The target_link_libraries() call above
487 # won't complain if one of the dependencies doesn't exist (since it is
488 # intended to allow passing in file names for plain library files rather
489 # than just targets).
490 # - It ensures that sources for our depencencies are built before any
491 # executable that depends on us. Note that we depend on "${dep}.py_lib"
492 # rather than "${dep}.py_sources_built" for this purpose because the
493 # ".py_sources_built" target won't be available for imported targets.
494 add_dependencies("${LIB_NAME}.py_sources_built" "${dep}.py_lib")
497 # Add a custom command to help with library installation, in case
498 # install_fb_python_library() is called later for this library.
499 # add_custom_command() only works with file dependencies defined in the same
500 # CMakeLists.txt file, so we want to make sure this is defined here, rather
501 # then where install_fb_python_library() is called.
502 # This command won't be run by default, but will only be run if it is needed
503 # by a subsequent install_fb_python_library() call.
505 # This command copies the library contents into the build directory.
506 # It would be nicer if we could skip this intermediate copy, and just run
507 # make_fbpy_archive.py at install time to copy them directly to the desired
508 # installation directory. Unfortunately this is difficult to do, and seems
509 # to interfere with some of the CMake code that wants to generate a manifest
510 # of installed files.
511 set(build_install_dir "${CMAKE_CURRENT_BINARY_DIR}/${LIB_NAME}.lib_install")
514 "${build_install_dir}/${LIB_NAME}.manifest"
515 COMMAND "${CMAKE_COMMAND}" -E remove_directory "${build_install_dir}"
517 "${Python3_EXECUTABLE}" "${FB_MAKE_PYTHON_ARCHIVE}" --type lib-install
518 --install-dir "${LIB_NAME}"
519 -o "${build_install_dir}/${LIB_NAME}" "${manifest_path}"
523 "${FB_MAKE_PYTHON_ARCHIVE}"
526 "${LIB_NAME}.py_lib_install"
527 DEPENDS "${build_install_dir}/${LIB_NAME}.manifest"
530 # Set some properties to pass through the install paths to
531 # install_fb_python_library()
533 # Passing through ${build_install_dir} allows install_fb_python_library()
534 # to work even if used from a different CMakeLists.txt file than where
535 # add_fb_python_library() was called (i.e. such that
536 # ${CMAKE_CURRENT_BINARY_DIR} is different between the two calls).
537 set(abs_install_dir "${install_dir}")
538 if(NOT IS_ABSOLUTE "${abs_install_dir}")
539 set(abs_install_dir "${CMAKE_INSTALL_PREFIX}/${abs_install_dir}")
541 string(REGEX REPLACE "/$" "" abs_install_dir "${abs_install_dir}")
542 set_target_properties(
543 "${LIB_NAME}.py_lib_install"
545 INSTALL_DIR "${abs_install_dir}"
546 BUILD_INSTALL_DIR "${build_install_dir}"
551 # Install an FB-style packaged python binary.
553 # - DESTINATION <export-name>:
554 # Associate the installed target files with the given export-name.
556 function(install_fb_python_executable TARGET)
557 # Parse the arguments
558 set(one_value_args DESTINATION)
559 set(multi_value_args)
561 ARG "" "${one_value_args}" "${multi_value_args}" "${ARGN}"
564 if(NOT DEFINED ARG_DESTINATION)
565 set(ARG_DESTINATION bin)
569 PROGRAMS "$<TARGET_PROPERTY:${TARGET}.GEN_PY_EXE,EXECUTABLE>"
570 DESTINATION "${ARG_DESTINATION}"
575 # Install a python library.
577 # - EXPORT <export-name>:
578 # Associate the installed target files with the given export-name.
580 # Note that unlike the built-in CMake install() function we do not accept a
581 # DESTINATION parameter. Instead, use the INSTALL_DIR parameter to
582 # add_fb_python_library() to set the installation location.
584 function(install_fb_python_library LIB_NAME)
585 set(one_value_args EXPORT)
586 fb_cmake_parse_args(ARG "" "${one_value_args}" "" "${ARGN}")
588 # Export our "${LIB_NAME}.py_lib" target so that it will be available to
589 # downstream projects in our installed CMake config files.
590 if(DEFINED ARG_EXPORT)
591 install(TARGETS "${LIB_NAME}.py_lib" EXPORT "${ARG_EXPORT}")
594 # add_fb_python_library() emits a .py_lib_install target that will prepare
595 # the installation directory. However, it isn't part of the "ALL" target and
596 # therefore isn't built by default.
598 # Make sure the ALL target depends on it now. We have to do this by
599 # introducing yet another custom target.
600 # Add it as a dependency to the ALL target now.
601 add_custom_target("${LIB_NAME}.py_lib_install_all" ALL)
603 "${LIB_NAME}.py_lib_install_all" "${LIB_NAME}.py_lib_install"
606 # Copy the intermediate install directory generated at build time into
607 # the desired install location.
608 get_target_property(dest_dir "${LIB_NAME}.py_lib_install" "INSTALL_DIR")
610 build_install_dir "${LIB_NAME}.py_lib_install" "BUILD_INSTALL_DIR"
613 DIRECTORY "${build_install_dir}/${LIB_NAME}"
614 DESTINATION "${dest_dir}"
617 FILES "${build_install_dir}/${LIB_NAME}.manifest"
618 DESTINATION "${dest_dir}"
622 # Helper macro to process the BASE_DIR and NAMESPACE arguments for
623 # add_fb_python_executable() and add_fb_python_executable()
624 macro(fb_py_process_default_args NAMESPACE_VAR BASE_DIR_VAR)
625 # If the namespace was not specified, default to the relative path to the
626 # current directory (starting from the repository root).
627 if(NOT DEFINED "${NAMESPACE_VAR}")
629 RELATIVE_PATH "${NAMESPACE_VAR}"
630 "${CMAKE_SOURCE_DIR}"
631 "${CMAKE_CURRENT_SOURCE_DIR}"
635 if(NOT DEFINED "${BASE_DIR_VAR}")
636 # If the base directory was not specified, default to the current directory
637 set("${BASE_DIR_VAR}" "${CMAKE_CURRENT_SOURCE_DIR}")
639 # If the base directory was specified, always convert it to an
641 get_filename_component("${BASE_DIR_VAR}" "${${BASE_DIR_VAR}}" ABSOLUTE)
645 function(fb_py_check_available)
646 # Make sure that Python 3 and our make_fbpy_archive.py helper script are
648 if(NOT Python3_EXECUTABLE)
649 if(FBPY_FIND_PYTHON_ERR)
650 message(FATAL_ERROR "Unable to find Python 3: ${FBPY_FIND_PYTHON_ERR}")
652 message(FATAL_ERROR "Unable to find Python 3")
656 if (NOT FB_MAKE_PYTHON_ARCHIVE)
658 FATAL_ERROR "unable to find make_fbpy_archive.py helper program (it "
659 "should be located in the same directory as FBPythonBinary.cmake)"
665 fb_py_compute_dest_path
666 src_path_output dest_path_output src_path namespace_dir base_dir
668 if("${src_path}" MATCHES "=")
669 # We want to split the string on the `=` sign, but cmake doesn't
670 # provide much in the way of helpers for this, so we rewrite the
671 # `=` sign to `;` so that we can treat it as a cmake list and
672 # then index into the components
673 string(REPLACE "=" ";" src_path_list "${src_path}")
674 list(GET src_path_list 0 src_path)
675 # Note that we ignore the `namespace_dir` in the alias case
676 # in order to allow aliasing a source to the top level `__main__.py`
678 list(GET src_path_list 1 dest_path)
683 get_filename_component(abs_source "${src_path}" ABSOLUTE)
684 if(NOT DEFINED dest_path)
685 file(RELATIVE_PATH rel_src "${ARG_BASE_DIR}" "${abs_source}")
686 if("${rel_src}" MATCHES "^../")
688 FATAL_ERROR "${LIB_NAME}: source file \"${abs_source}\" is not inside "
689 "the base directory ${ARG_BASE_DIR}"
692 set(dest_path "${namespace_dir}${rel_src}")
695 set("${src_path_output}" "${abs_source}" PARENT_SCOPE)
696 set("${dest_path_output}" "${dest_path}" PARENT_SCOPE)