Do not unnecessarily constrain values in verifyPropType()
[hiphop-php.git] / third-party / watchman / src / build / fbcode_builder / CMake / FBPythonBinary.cmake
blob99c33fb8c95373a2e5f583d73fdb7ee1d8214a75
1 # Copyright (c) Facebook, Inc. and its affiliates.
3 include(FBCMakeParseArgs)
6 # This file contains helper functions for building self-executing Python
7 # binaries.
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
16 # available.)
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
23 # library paths.
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
29 # are used.
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
34   if (WIN32)
35     # On Windows we need both the Intepreter as well as the Development
36     # libraries.
37     find_package(Python3 COMPONENTS Interpreter Development QUIET)
38   else()
39     find_package(Python3 COMPONENTS Interpreter QUIET)
40   endif()
41   if(Python3_Interpreter_FOUND)
42     message(STATUS "Found Python 3: ${Python3_EXECUTABLE}")
43   else()
44     # Try with the FindPythonInterp.cmake module available in older CMake
45     # versions.  Check to see if the caller has already searched for this
46     # themselves first.
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.
54     endif()
55     if(PYTHONINTERP_FOUND)
56       if("${PYTHON_VERSION_MAJOR}" GREATER_EQUAL 3)
57         set(Python3_EXECUTABLE "${PYTHON_EXECUTABLE}")
58         add_custom_target(Python3::Interpreter)
59       else()
60         string(
61           CONCAT FBPY_FIND_PYTHON_ERR
62           "found Python ${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}, "
63           "but need Python 3"
64         )
65       endif()
66     endif()
67   endif()
68 endif()
70 # Find our helper program.
71 # We typically install this in the same directory as this .cmake file.
72 find_program(
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")
77 set(
78   FB_PY_TEST_DISCOVER_SCRIPT
79   "${CMAKE_CURRENT_LIST_DIR}/FBPythonTestAddTests.cmake"
81 set(
82   FB_PY_WIN_MAIN_C
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}
88 set(
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
101 # manifest.
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)
109   fb_cmake_parse_args(
110     ARG "" "${one_value_args}" "${multi_value_args}" "${ARGN}"
111   )
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(
116     "${TARGET}.main_lib"
117     BASE_DIR "${ARG_BASE_DIR}"
118     NAMESPACE "${ARG_NAMESPACE}"
119     SOURCES ${ARG_SOURCES}
120     DEPENDS ${ARG_DEPENDS}
121   )
123   set(
124     manifest_files
125     "$<TARGET_PROPERTY:${TARGET}.main_lib.py_lib,INTERFACE_INCLUDE_DIRECTORIES>"
126   )
127   set(
128     source_files
129     "$<TARGET_PROPERTY:${TARGET}.main_lib.py_lib,INTERFACE_SOURCES>"
130   )
132   # The command to build the executable archive.
133   #
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+.
137   if (POLICY CMP0067)
138     set(extra_cmd_params COMMAND_EXPAND_LISTS)
139     set(make_py_args "${manifest_files}")
140   else()
141     set(extra_cmd_params)
142     set(make_py_args --manifest-separator "::" "$<JOIN:${manifest_files},::>")
143   endif()
145   set(output_file "${TARGET}${CMAKE_EXECUTABLE_SUFFIX}")
146   if(WIN32)
147     set(zipapp_output "${TARGET}.py_zipapp")
148   else()
149     set(zipapp_output "${output_file}")
150   endif()
151   set(zipapp_output_file "${zipapp_output}")
153   set(is_dir_output FALSE)
154   if(DEFINED ARG_TYPE)
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")
161       list(APPEND
162         extra_cmd_params
163         COMMAND "${CMAKE_COMMAND}" -E remove_directory "${zipapp_output}"
164       )
165     endif()
166   endif()
168   if(DEFINED ARG_MAIN_MODULE)
169     list(APPEND make_py_args "--main" "${ARG_MAIN_MODULE}")
170   endif()
172   add_custom_command(
173     OUTPUT "${zipapp_output_file}"
174     ${extra_cmd_params}
175     COMMAND
176       "${Python3_EXECUTABLE}" "${FB_MAKE_PYTHON_ARCHIVE}"
177       -o "${zipapp_output}"
178       ${make_py_args}
179     DEPENDS
180       ${source_files}
181       "${TARGET}.main_lib.py_sources_built"
182       "${FB_MAKE_PYTHON_ARCHIVE}"
183   )
185   if(WIN32)
186     if(is_dir_output)
187       # TODO: generate a main executable that will invoke Python3
188       # with the correct main module inside the output directory
189     else()
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(
196         "${TARGET}.winmain"
197         PUBLIC ${Python3_LIBRARY_DIRS}
198       )
199       add_custom_command(
200         OUTPUT "${output_file}"
201         DEPENDS "${TARGET}.winmain" "${zipapp_output_file}"
202         COMMAND
203           "cmd.exe" "/c" "copy" "/b"
204           "${TARGET}.winmain${CMAKE_EXECUTABLE_SUFFIX}+${zipapp_output}"
205           "${output_file}"
206       )
207     endif()
208   endif()
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}")
219 endfunction()
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)
236   set(
237     one_value_args
238     WORKING_DIRECTORY BASE_DIR NAMESPACE TEST_LIST DISCOVERY_TIMEOUT
239   )
240   fb_cmake_parse_args(
241     ARG "" "${one_value_args}" "${multi_value_args}" "${ARGN}"
242   )
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}")
249   endif()
250   if(NOT ARG_TEST_LIST)
251     set(ARG_TEST_LIST "${TARGET}_TESTS")
252   endif()
253   if(NOT ARG_DISCOVERY_TIMEOUT)
254     set(ARG_DISCOVERY_TIMEOUT 5)
255   endif()
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.
260   #
261   # This is written into a __test_modules__.py module that the test runner
262   # will look at.
263   set(
264     test_modules_path
265     "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_test_modules.py"
266   )
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}/")
271   endif()
272   set(test_modules)
273   foreach(src_path IN LISTS ARG_SOURCES)
274     fb_py_compute_dest_path(
275       abs_source dest_path
276       "${src_path}" "${namespace_dir}" "${ARG_BASE_DIR}"
277     )
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")
282   endforeach()
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(
290     "${TARGET}"
291     NAMESPACE "${ARG_NAMESPACE}"
292     BASE_DIR "${ARG_BASE_DIR}"
293     SOURCES ${ARG_SOURCES}
294     DEPENDS ${ARG_DEPENDS}
295   )
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")
302   add_custom_command(
303     TARGET "${TARGET}.GEN_PY_EXE" POST_BUILD
304     BYPRODUCTS "${ctest_tests_file}"
305     COMMAND
306       "${CMAKE_COMMAND}"
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}"
317     VERBATIM
318   )
320   file(
321     WRITE "${ctest_include_file}"
322     "if(EXISTS \"${ctest_tests_file}\")\n"
323     "  include(\"${ctest_tests_file}\")\n"
324     "else()\n"
325     "  add_test(\"${TARGET}_NOT_BUILT\" \"${TARGET}_NOT_BUILT\")\n"
326     "endif()\n"
327   )
328   set_property(
329     DIRECTORY APPEND PROPERTY TEST_INCLUDE_FILES
330     "${ctest_include_file}"
331   )
332 endfunction()
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.)
344 # Parameters:
345 # - BASE_DIR <dir>:
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)
399   fb_cmake_parse_args(
400     ARG "" "${one_value_args}" "${multi_value_args}" "${ARGN}"
401   )
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}/")
407   endif()
409   if(NOT DEFINED ARG_INSTALL_DIR)
410     set(install_dir "${FBPY_LIB_INSTALL_DIR}/")
411   elseif("${ARG_INSTALL_DIR}" STREQUAL "")
412     set(install_dir "")
413   else()
414     set(install_dir "${ARG_INSTALL_DIR}/")
415   endif()
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
423   # files.
425   # Define a library target to help pass around information about the library,
426   # and propagate dependency information.
427   #
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.
437   #
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")
446   set(abs_sources)
447   foreach(src_path IN LISTS ARG_SOURCES)
448     fb_py_compute_dest_path(
449       abs_source dest_path
450       "${src_path}" "${namespace_dir}" "${ARG_BASE_DIR}"
451     )
452     list(APPEND abs_sources "${abs_source}")
453     target_sources(
454       "${LIB_NAME}.py_lib" INTERFACE
455       "$<BUILD_INTERFACE:${abs_source}>"
456       "$<INSTALL_INTERFACE:${install_dir}${LIB_NAME}/${dest_path}>"
457     )
458     file(
459       APPEND "${tmp_manifest}"
460       "${abs_source} :: ${dest_path}\n"
461     )
462   endforeach()
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>"
469   )
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")
495   endforeach()
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.
504   #
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")
512   add_custom_command(
513     OUTPUT
514       "${build_install_dir}/${LIB_NAME}.manifest"
515     COMMAND "${CMAKE_COMMAND}" -E remove_directory "${build_install_dir}"
516     COMMAND
517       "${Python3_EXECUTABLE}" "${FB_MAKE_PYTHON_ARCHIVE}" --type lib-install
518       --install-dir "${LIB_NAME}"
519       -o "${build_install_dir}/${LIB_NAME}" "${manifest_path}"
520     DEPENDS
521       "${abs_sources}"
522       "${manifest_path}"
523       "${FB_MAKE_PYTHON_ARCHIVE}"
524   )
525   add_custom_target(
526     "${LIB_NAME}.py_lib_install"
527     DEPENDS "${build_install_dir}/${LIB_NAME}.manifest"
528   )
530   # Set some properties to pass through the install paths to
531   # install_fb_python_library()
532   #
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}")
540   endif()
541   string(REGEX REPLACE "/$" "" abs_install_dir "${abs_install_dir}")
542   set_target_properties(
543     "${LIB_NAME}.py_lib_install"
544     PROPERTIES
545     INSTALL_DIR "${abs_install_dir}"
546     BUILD_INSTALL_DIR "${build_install_dir}"
547   )
548 endfunction()
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)
560   fb_cmake_parse_args(
561     ARG "" "${one_value_args}" "${multi_value_args}" "${ARGN}"
562   )
564   if(NOT DEFINED ARG_DESTINATION)
565     set(ARG_DESTINATION bin)
566   endif()
568   install(
569     PROGRAMS "$<TARGET_PROPERTY:${TARGET}.GEN_PY_EXE,EXECUTABLE>"
570     DESTINATION "${ARG_DESTINATION}"
571   )
572 endfunction()
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}")
592   endif()
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.
597   #
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)
602   add_dependencies(
603     "${LIB_NAME}.py_lib_install_all" "${LIB_NAME}.py_lib_install"
604   )
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")
609   get_target_property(
610     build_install_dir "${LIB_NAME}.py_lib_install" "BUILD_INSTALL_DIR"
611   )
612   install(
613     DIRECTORY "${build_install_dir}/${LIB_NAME}"
614     DESTINATION "${dest_dir}"
615   )
616   install(
617     FILES "${build_install_dir}/${LIB_NAME}.manifest"
618     DESTINATION "${dest_dir}"
619   )
620 endfunction()
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}")
628     file(
629       RELATIVE_PATH "${NAMESPACE_VAR}"
630       "${CMAKE_SOURCE_DIR}"
631       "${CMAKE_CURRENT_SOURCE_DIR}"
632     )
633   endif()
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}")
638   else()
639     # If the base directory was specified, always convert it to an
640     # absolute path.
641     get_filename_component("${BASE_DIR_VAR}" "${${BASE_DIR_VAR}}" ABSOLUTE)
642   endif()
643 endmacro()
645 function(fb_py_check_available)
646   # Make sure that Python 3 and our make_fbpy_archive.py helper script are
647   # available.
648   if(NOT Python3_EXECUTABLE)
649     if(FBPY_FIND_PYTHON_ERR)
650       message(FATAL_ERROR "Unable to find Python 3: ${FBPY_FIND_PYTHON_ERR}")
651     else()
652       message(FATAL_ERROR "Unable to find Python 3")
653     endif()
654   endif()
656   if (NOT FB_MAKE_PYTHON_ARCHIVE)
657     message(
658       FATAL_ERROR "unable to find make_fbpy_archive.py helper program (it "
659       "should be located in the same directory as FBPythonBinary.cmake)"
660     )
661   endif()
662 endfunction()
664 function(
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`
677     # filename.
678     list(GET src_path_list 1 dest_path)
679   else()
680     unset(dest_path)
681   endif()
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 "^../")
687       message(
688         FATAL_ERROR "${LIB_NAME}: source file \"${abs_source}\" is not inside "
689         "the base directory ${ARG_BASE_DIR}"
690       )
691     endif()
692     set(dest_path "${namespace_dir}${rel_src}")
693   endif()
695   set("${src_path_output}" "${abs_source}" PARENT_SCOPE)
696   set("${dest_path_output}" "${dest_path}" PARENT_SCOPE)
697 endfunction()