# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# 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.

# Python extension support for folly.
# This builds the folly Python bindings (iobuf, executor, etc.) for use by
# downstream projects like thrift-python.

# Create tree of symbolic links in structure required for successful
# compilation by Cython.
#   - must be in path named same as extension

set(_cybld "${CMAKE_CURRENT_BINARY_DIR}/cybld")

add_custom_target(create_binding_symlink ALL)
set(BindingFiles
  "${CMAKE_CURRENT_SOURCE_DIR}/executor.pyx"
  "${CMAKE_CURRENT_SOURCE_DIR}/iobuf.pyx"
  "${CMAKE_CURRENT_SOURCE_DIR}/iobuf_ext.cpp"
  "${CMAKE_CURRENT_SOURCE_DIR}/ProactorExecutor.cpp"
  "${CMAKE_CURRENT_SOURCE_DIR}/__init__.pxd"
  "${CMAKE_CURRENT_SOURCE_DIR}/async_generator.pxd"
  "${CMAKE_CURRENT_SOURCE_DIR}/cast.pxd"
  "${CMAKE_CURRENT_SOURCE_DIR}/coro.pxd"
  "${CMAKE_CURRENT_SOURCE_DIR}/executor.pxd"
  "${CMAKE_CURRENT_SOURCE_DIR}/executor_detail.pxd"
  "${CMAKE_CURRENT_SOURCE_DIR}/expected.pxd"
  "${CMAKE_CURRENT_SOURCE_DIR}/fbstring.pxd"
  "${CMAKE_CURRENT_SOURCE_DIR}/fiber_manager.pxd"
  "${CMAKE_CURRENT_SOURCE_DIR}/fibers.pxd"
  "${CMAKE_CURRENT_SOURCE_DIR}/function.pxd"
  "${CMAKE_CURRENT_SOURCE_DIR}/futures.pxd"
  "${CMAKE_CURRENT_SOURCE_DIR}/iobuf.pxd"
  "${CMAKE_CURRENT_SOURCE_DIR}/memory.pxd"
  "${CMAKE_CURRENT_SOURCE_DIR}/optional.pxd"
  "${CMAKE_CURRENT_SOURCE_DIR}/range.pxd"
  "${CMAKE_CURRENT_SOURCE_DIR}/request_context.pxd"
  "${CMAKE_CURRENT_SOURCE_DIR}/async_generator.h"
  "${CMAKE_CURRENT_SOURCE_DIR}/AsyncioExecutor.h"
  "${CMAKE_CURRENT_SOURCE_DIR}/coro.h"
  "${CMAKE_CURRENT_SOURCE_DIR}/error.h"
  "${CMAKE_CURRENT_SOURCE_DIR}/executor.h"
  "${CMAKE_CURRENT_SOURCE_DIR}/fibers.h"
  "${CMAKE_CURRENT_SOURCE_DIR}/futures.h"
  "${CMAKE_CURRENT_SOURCE_DIR}/GILAwareManualExecutor.h"
  "${CMAKE_CURRENT_SOURCE_DIR}/import.h"
  "${CMAKE_CURRENT_SOURCE_DIR}/iobuf.h"
  "${CMAKE_CURRENT_SOURCE_DIR}/iobuf_ext.h"
  "${CMAKE_CURRENT_SOURCE_DIR}/ProactorExecutor.h"
  "${CMAKE_CURRENT_SOURCE_DIR}/request_context.h"
  "${CMAKE_CURRENT_SOURCE_DIR}/Weak.h"
)
file(MAKE_DIRECTORY "${_cybld}/folly/")
file(MAKE_DIRECTORY "${_cybld}/folly/python/")
# Create __init__.py for folly package.
# Required for Python to recognize it as a package.
file(WRITE "${_cybld}/folly/__init__.py" "# Auto-generated by CMake\n")

foreach(_src ${BindingFiles})
  get_filename_component(_target_file "${_src}" NAME)

  message(
    STATUS
    "Linking ${_src} "
    "to ${_cybld}/folly/${_target_file}"
  )
  add_custom_command(TARGET create_binding_symlink PRE_BUILD
    COMMAND
      ${CMAKE_COMMAND} -E create_symlink
      "${_src}"
      "${_cybld}/folly/${_target_file}"
  )
endforeach()

# Tell setup.py where to find includes and libfolly.so
set(prop "$<TARGET_PROPERTY:folly,INCLUDE_DIRECTORIES>")
set(incs "$<$<BOOL:${prop}>:-I$<JOIN:${prop},:>>")
set(glog_lib "${GLOG_LIBRARY}")
cmake_path(REMOVE_FILENAME glog_lib)
set(libs "-L${CMAKE_CURRENT_BINARY_DIR}:${CMAKE_BINARY_DIR}:${glog_lib}")

# Generate API headers only (no linking)
add_custom_command(
  OUTPUT ${_cybld}/folly/iobuf_api.h
  COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/setup.py --api-only
  DEPENDS create_binding_symlink
  WORKING_DIRECTORY ${_cybld}
)

add_custom_target(folly_python_api_headers ALL
  DEPENDS ${_cybld}/folly/iobuf_api.h
)

add_custom_command(
  OUTPUT ${_cybld}/folly/python/iobuf_api.h
  COMMAND
    ${CMAKE_COMMAND} -E create_symlink
    "${_cybld}/folly/iobuf_api.h"
    "${_cybld}/folly/python/iobuf_api.h"
  DEPENDS ${_cybld}/folly/iobuf_api.h
)

add_custom_target(create_post_binding_symlink ALL
  DEPENDS ${_cybld}/folly/python/iobuf_api.h
)

add_library(
  folly_python_cpp
    AsyncioExecutor.cpp
    error.cpp
    executor.cpp
    iobuf.cpp
)

target_include_directories(folly_python_cpp PRIVATE ${Python3_INCLUDE_DIRS})
target_link_libraries(folly_python_cpp PRIVATE Python3::Python)
add_dependencies(folly_python_cpp folly create_post_binding_symlink)
set_property(TARGET folly_python_cpp PROPERTY VERSION ${PACKAGE_VERSION})
target_compile_definitions(folly_python_cpp PRIVATE BOOST_NO_AUTO_PTR)
target_include_directories(folly_python_cpp PRIVATE "${_cybld}")
target_link_libraries(folly_python_cpp PUBLIC folly)
apply_folly_compile_options_to_target(folly_python_cpp)

# Build Python extensions after C++ library is ready
add_custom_target(folly_python_bindings ALL
  DEPENDS folly_python_cpp folly_python_api_headers
  WORKING_DIRECTORY ${_cybld}
)

add_custom_command(TARGET folly_python_bindings POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E env
    "CFLAGS=${CMAKE_C_FLAGS}"
    "CXXFLAGS=${CMAKE_CXX_FLAGS}"
    "CC=${CMAKE_C_COMPILER}"
    "CXX=${CMAKE_CXX_COMPILER}"
    python3 ${CMAKE_CURRENT_SOURCE_DIR}/setup.py
    build_ext -f ${incs} ${libs}
  WORKING_DIRECTORY ${_cybld}
)

install(
  TARGETS folly_python_cpp
  EXPORT folly
  DESTINATION ${LIB_INSTALL_DIR}
)

# Install .pxd files for downstream Cython code that imports folly
install(
  DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/
  DESTINATION ${INCLUDE_INSTALL_DIR}/folly/
  FILES_MATCHING
    PATTERN "*.pxd"
)

add_custom_command(TARGET folly_python_bindings
  COMMAND ${CMAKE_COMMAND} -E env
    "CFLAGS=${CMAKE_C_FLAGS}"
    "CXXFLAGS=${CMAKE_CXX_FLAGS}"
    "CC=${CMAKE_C_COMPILER}"
    "CXX=${CMAKE_CXX_COMPILER}"
    python3 ${CMAKE_CURRENT_SOURCE_DIR}/setup.py
    bdist_wheel
  WORKING_DIRECTORY ${_cybld}
)

install(
  FILES
    ${_cybld}/folly/iobuf_api.h
  DESTINATION ${INCLUDE_INSTALL_DIR}/folly/python
  COMPONENT dev
)

# Install Folly Python bindings
set(INSTALL_DIR ${CMAKE_INSTALL_PREFIX})
if (NOT "${PYTHON_PACKAGE_INSTALL_DIR}" STREQUAL "")
  set(INSTALL_DIR ${PYTHON_PACKAGE_INSTALL_DIR})
endif()

install(CODE "
  file(GLOB _folly_wheel \"${_cybld}/dist/folly-*.whl\")
  if(_folly_wheel)
    execute_process(
      COMMAND python3 -m pip install \${_folly_wheel}
        --prefix ${INSTALL_DIR}
        --no-deps
        --force-reinstall
      COMMAND_ECHO STDOUT
    )
  else()
    message(FATAL_ERROR \"No folly wheel found in ${_cybld}/dist/\")
  endif()
")
