SET(INTERPRETER_DIR "${DEPLOY_DIR}/interpreter" )
SET(INTERPRETER_DIR "${DEPLOY_DIR}/interpreter" PARENT_SCOPE)

SET(PYTORCH_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../../")

# Build cpython
SET(PYTHON_INSTALL_DIR "${INTERPRETER_DIR}/cpython")
SET(PYTHON_INC_DIR "${PYTHON_INSTALL_DIR}/include/python3.8")
SET(PYTHON_LIB "${PYTHON_INSTALL_DIR}/lib/libpython3.8.a")
SET(PYTHON_BIN "${PYTHON_INSTALL_DIR}/bin/python3")
ExternalProject_Add(
  cpython
  PREFIX cpython
  GIT_REPOSITORY https://github.com/python/cpython.git
  GIT_TAG v3.8.6
  UPDATE_COMMAND ""
  BUILD_IN_SOURCE True
  CONFIGURE_COMMAND CFLAGS=-fPIC CPPFLAGS=-fPIC <SOURCE_DIR>/configure --prefix ${PYTHON_INSTALL_DIR}
  BUILD_COMMAND CFLAGS=-fPIC CPPFLAGS=-fPIC make -j8
  INSTALL_COMMAND make install
  BYPRODUCTS ${PYTHON_MODULES} ${PYTHON_LIB} ${PYTHON_BIN}
  LOG_OUTPUT_ON_FAILURE True
)

# We find the built python modules, this is confusing because python build already outputs
# the modules in a strange nested path, and then that path is relative to the
# Cmake ExternalProject root in the cmake build dir.
ExternalProject_Get_property(cpython SOURCE_DIR)
SET(PYTHON_MODULE_DIR "${SOURCE_DIR}/build/temp.linux-x86_64-3.8/${SOURCE_DIR}/Modules")
SET(PYTHON_STDLIB_DIR "${SOURCE_DIR}/Lib")
SET(PYTHON_STDLIB "${PYTHON_INSTALL_DIR}/lib/libpython_stdlib3.8.a")
# Then we use a hardcoded list of expected module names and include them in our lib
include("CMakePythonModules.txt")
ExternalProject_Add_Step(
  cpython
  archive_stdlib
  DEPENDEES install
  BYPRODUCTS ${PYTHON_STDLIB}
  COMMAND ar -rc ${PYTHON_STDLIB} ${PYTHON_MODULES}
  VERBATIM
)
# Get python typing extension, needed by torch
SET(TYPING_PKG "${INTERPRETER_DIR}/third_party/typing_extensions.py")
ExternalProject_Add(
  typing
  PREFIX typing
  GIT_REPOSITORY https://github.com/python/typing.git
  GIT_TAG 3.7.4.3
  UPDATE_COMMAND ""
  CONFIGURE_COMMAND ""
  BUILD_COMMAND ""
  INSTALL_COMMAND cp ../typing/typing_extensions/src_py3/typing_extensions.py ${TYPING_PKG}
  BYPRODUCTS ${TYPING_PKG}
  LOG_OUTPUT_ON_FAILURE True
)

# Output files generated by freeze script, containing frozen bytecode
SET(FROZEN_DIR "${INTERPRETER_DIR}/frozen")
set(FROZEN_FILES
  ${FROZEN_DIR}/main.c
  ${FROZEN_DIR}/bytecode_0.c
  ${FROZEN_DIR}/bytecode_1.c
  ${FROZEN_DIR}/bytecode_2.c
  ${FROZEN_DIR}/bytecode_3.c
  ${FROZEN_DIR}/bytecode_4.c
)
# Packages to freeze: python stdlib, typing extension, and torch
add_custom_command(
   OUTPUT ${FROZEN_FILES}
   WORKING_DIRECTORY ${INTERPRETER_DIR}
   COMMAND mkdir -p ${FROZEN_DIR}
   COMMAND ${PYTHON_BIN} freeze.py ${PYTHON_STDLIB_DIR} ${TYPING_PKG} ${PYTORCH_ROOT}/torch --oss --install_dir ${FROZEN_DIR} --verbose
   DEPENDS cpython typing
   VERBATIM
)

# instantiate a library based on the objects that make up torch_python
# make sure system python isn't used here
target_include_directories(torch_python_obj BEFORE PRIVATE ${PYTHON_INC_DIR})
add_library(torch_python_static STATIC $<TARGET_OBJECTS:torch_python_obj>)
# Build the interpreter lib, designed to be standalone and dlopened
# We bake the python and torch_python binding objs into libinterpreter
set(LINKER_SCRIPT "${INTERPRETER_DIR}/hide_symbols.script")
set(INTERPRETER_LIB_SOURCES
  ${INTERPRETER_DIR}/interpreter.cpp
  ${FROZEN_FILES}
  ${LINKER_SCRIPT}
)
add_library(interpreter ${INTERPRETER_LIB_SOURCES} ${LINKER_SCRIPT})
set_property(TARGET interpreter APPEND_STRING PROPERTY
             LINK_FLAGS " -Wl,--version-script=${LINKER_SCRIPT}")
# need to ensure headers are present before any .cpp in interpreter are compiled,
# but cpp themselves don't clearly depend on cpython so there is a race otherwise
add_dependencies(interpreter cpython)
target_compile_options(
    interpreter PRIVATE
    -fvisibility=hidden
)
target_include_directories(interpreter PRIVATE ${INTERPRETER_DIR})
target_include_directories(interpreter PUBLIC ${PYTHON_INC_DIR})
target_link_libraries(interpreter PRIVATE ${PYTHON_LIB} ${PYTHON_STDLIB} torch_python_static)
target_link_libraries(interpreter PRIVATE crypt crypto ssl pthread dl util m z ffi lzma readline nsl ncursesw panelw) # for python builtins
target_link_libraries(interpreter PRIVATE fmt::fmt-header-only protobuf::libprotobuf-lite)

# handy to have a standalone app to verify linkage and usage of interpreter before embedding it in another lib
set(INTERPRETER_TEST_SOURCES
  ${INTERPRETER_DIR}/test_main.cpp
)
add_executable(interpreter_test ${INTERPRETER_TEST_SOURCES})
target_include_directories(interpreter_test PRIVATE ${PYTORCH_ROOT}/torch)
target_include_directories(interpreter_test PRIVATE ${PYTHON_INC_DIR})
target_link_libraries(interpreter_test PUBLIC gtest dl)
# no-as-needed to ensure shm and torch are included to satisfy runtime dlopen
# dependencies for libinterpreter, regardless of whether they are used in interpreter_test
target_link_libraries(interpreter_test PUBLIC "-Wl,--no-as-needed" shm torch protobuf::libprotobuf-lite)
