Вот задача:

Кеша и CPython

Легенда

Попугай Кеша каким-то образом попал на стажировку в IT-команду "Союзмультфильма". Не теряя ни минуты, он заявил, что когда в последний раз отдыхал на Таити, выучил C++ и Python. Однако попугай не учел, что ему могут выдать задание.

Руководитель команды Вовка давно хотел совместить Python и C++, используя CPython API. На всякий случай, напомним пернатому другу, что каноническая реализация Python - это CPython. Как понятно из названия, Python написан на C.

В команде используют систему сборки Bazel. Она очень требовательна к объявлению зависимостей. Все файлы должны быть предоставлены заранее, равно как и остальные параметры и настройки. И все бы ничего, но команда Вовки так и не определилась с версией Python, которую она будет использовать. Если учитывать минорные версии, то между 3.8.0 и 3.12.4 вышло 69 версий! Никто не будет писать конфигурации под 69 возможных случаев. Это мы еще не учли поиск самих файлов! Тем не менее, компилировать проект как-то надо.

Кеша, как обычно, свою задачу повесил на Вас! Внимательно прочитайте техническое задание и помогите Кеше дожить до первой зарплаты.

Техническое задание

Краткое описание

Вам необходимо реализовать программу, которая выполнит следующую последовательность действий:

Проверит текущую систему в соответствии с ограничениями, описанными ниже;
Удалит устаревшие файлы;
Найдет файлы Python, которые необходимы для сборки проекта, по алгоритму ниже;
Скопирует найденные файлы в директорию, выделенную для сборки проекта;
Запишет конфигурационный файл для Bazel, опираясь на скопированные файлы.
Ограничения

Вовка сразу напомнил забывчивому попугаю ограничения, предъявляемые командой:

Никакая реализация, кроме CPython, нас не интересует. Действительно, как можно пользоваться CPython API, если нет самого CPython!

Хотим видеть версию Python не младше 3.8.0, так как именно с этой версии появляется нужная Вовке фича.

Команда гарантирует стабильность своих приложений только на Linux, Windows и macOS, поэтому программу разрешено запускать только на них. Никакие JavaOS предусматривать не нужно.

Если какое-либо из условий нарушено, то Кеша должен вызвать исключение AssertionError с понятным для пользователя сообщением.

Система сборки

Для сборки на Bazel будет использовано правило cc_import. Его применяют для импортирования заранее скомпилированных библиотек для C или C++. Давайте покажем пример, чтобы было более понятно, что от Вас потребуется дальше.

cc_import(
    ...,
    hdrs = [
        "dir/bar.hpp",
        "dir/baz.hpp",
    ],
    includes = ["dir"],
    interface_library = "mylib.lib",
    shared_library = "mylib.dll",
    ...,
)
Мы объявили цель со следующими атрибутами:

hdrs - заголовочные файлы, доступные для использования в директиве #include;
includes - список директорий, которые добавляются в поисковый путь компилятора;
interface_library - библиотека-интерфейс, используемая при линковке разделяемой библиотеки;
shared_library - предварительно скомпилированная разделяемая библиотека.
Поиск файлов

Корень CPython

Корнем CPython считайте директорию для данных. Изучите документацию к sysconfig, чтобы узнать, как получить путь до этой директории.

Заголовочные файлы (hdrs)

К заголовочным файлам отнесем все файлы с расширением .h, которые находятся в директории для неплатформозависимых заголовочных файлов. Изучите документацию к sysconfig, чтобы узнать, как получить путь до этой директории.

Включаемые директории (includes)

Необходимо собрать список всех директорий, содержащих заголовочные файлы из пункта выше. Кроме того, эти директории должны быть префиксом для каждого файла. Ниже приведем пример.

Если вы нашли следующие заголовочные файлы:

one/two/file.hpp;
one/two/three/four/file.hpp;
one/two/three/five/file.hpp;
one/two/six/file.hpp;
То в качестве includes Вы запишете:

one/;
one/two/.
Потому что они являются префиксом для каждого файла из пункта выше.

Разделяемая библиотека (shared_library)

Сначала определим, где будем искать:

На Windows используем корень CPython;
На macOS и Linux используем конфигурационную переменную LIBDIR.
Далее, предполагая, что версия Python - это {major}.{minor}.{patch}, необходимо найти файл, имя которого удовлетворяет платформозависимому паттерну:

На Windows ищем python{major}{minor}.dll;
На macOS ищем libpython{major}.{minor}.dylib;
На Linux ищем libpython{major}{minor}.so.
Гарантируется, что файл ровно один и он существует. Файл может быть вложен в какие-то директории относительно директории поиска.

Библиотека-интерфейс (interface_library)

Библиотека-интерфейс нужна только для Windows. Для остальных платформ атрибут interface_library не требуется. В директории libs/ относительно корня CPython требуется найти файл, имя которого удовлетворяет паттерну python{major}{minor}.lib.

Гарантируется, что файл ровно один и он существует. Файл может быть вложен в какие-то директории относительно директории поиска.

Копирование

Допустим, что мы нашли некоторый нужный файл в некоторой директории поиска. Его будет необходимо скопировать в директорию third_party/cpython/internal/, сохранив путь относительно директории поиска. Удалять оригинальные файлы после копирования не нужно.

Например, мы искали заголовочные файлы (hdrs) в директории /home/dev/ и нашли файл, который имеет путь /home/dev/abc/example.hpp. После копирования относительно текущей директории он будет перенес как third_party/cpython/internal/abc/example.hpp.

Очистка мусора

Нужно предусмотреть, что в third_party/cpython/ может быть мусор от предыдущих запусков скрипта. Вам необходимо предусмотреть удаление third_party/cpython/ вместе с ее содержимым при повторном запуске программы.

Конфигурационный файл

После того, как Вы нашли все пути и скопировали файлы в директорию сборки, Вам останется только создать конфигурационный файл для Bazel.

В файл third_party/cpython/BUILD.bazel Вы должны записать следующее содержимое:

cc_import(
    name = "cpython",
    hdrs = [
        "...",
        "...",
        "...",
    ],
    includes = [
        "...",
        "...",
        "...",
    ],
    interface_library = "...",
    shared_library = "...",
    visibility = ["//visibility:public"],
)
Вместо многоточий будут записаны пути до файлов относительно third_party/cpython/. Например, Вы скопировали некоторый заголовочный файл, и он находится в third_party/cpython/internal/file.hpp. Тогда он будет записан в hdrs как internal/file.hpp.

Вот мое решение:

from pathlib import Path
import platform
import sys
import sysconfig
import glob
from typing import Union
import shutil
import os

def is_cpython() -> bool:
    """Проверить, что используется `CPython`."""
    return platform.python_implementation() == "CPython"


def is_linux() -> bool:
    """Проверить, что используется `Linux`."""
    return sys.platform == "linux"

def is_windows() -> bool:
    """Проверить, что используется `Windows`."""
    return sys.platform == "win32"


def is_macos() -> bool:
    """Проверить, что используется `is_macOS`."""
    return sys.platform == "darwin"


def is_supported_platform() -> bool:
    """Проверить, что платформа поддерживается."""
    return is_macos() or is_linux() or is_windows()



def is_supported_python_version() -> bool:
    """Проверить, что версия `Python` поддерживается."""
    return sys.version_info >= (3, 8)


def get_cpython_root() -> Path:
    """Получить путь до корня CPython."""
    return Path(sysconfig.get_path('include')).parent


def get_interface_library() -> Union[Path, None]:
    """Получить путь до библиотеки-интерфейса, если она существует."""
    if is_windows():
        major = sys.version_info.major
        minor = sys.version_info.major
        pattern = f"python{major}{minor}.lib"
        root = get_cpython_root() / "libs"
        for lib_path in glob.glob(str(root/ '**' / pattern), recursive = True):
            return Path(lib_path)
        return None


def get_shared_library() -> Path:
    """Получить путь до разделяемой библиотеки Python."""
    major = sys.version_info.major
    minor = sys.version_info.minor

    if is_windows():
        pattern = f"python{major}{minor}.dll"
        root = get_cpython_root()
    elif is_linux():
        pattern = f"libpython{major}{minor}.so"
        root = Path(sysconfig.get_config_var('LIBDIR'))
    elif is_macos():
        pattern = f"libpython{major}.{minor}.dylib"
        root = Path(sysconfig.get_config_var('LIBDIR'))
    else:
        raise AssertionError("Unsupported platform.")
    print(root)
    lib_paths = list(glob.glob(str(root / '**' / pattern), recursive=True))
    if lib_paths:
        for lib_path in lib_paths:
            print(f"Found shared library: {lib_path}")
            return Path(lib_path)
    else:
        print(
            f"No shared library found matching pattern: {pattern} in {root}")
        raise FileNotFoundError(f"Shared library not found with pattern {pattern} in {root}")

    raise FileNotFoundError(f"Shared library not found {pattern} in {root}")


def get_header_files() -> list[Path]:
    """Получить список загловочных файлов."""
    include_dir = sysconfig.get_path('include')
    return list(Path(include_dir).rglob('*.h'))


def get_include_directories() -> list[Path]:
    """Получить список директорий-префиксов."""
    header_files = get_header_files()
    include_dirs = {file.parent for file in header_files}
    return list(include_dirs)

def clean_third_party_dir() -> None:
    """Удалить папку `third_party/cpython/` вместе с ее содержимым."""
    targer_dir = Path("third_party/cpython")
    if targer_dir.exists():
        shutil.rmtree(targer_dir)


def copy_to_third_party(src: Path, dst_root: Path) -> Path:
    """Скопировать файл в директорию сборки."""
    try:
        relative_path = src.relative_to(get_cpython_root())
    except ValueError:
        relative_path = src.relative_to(sysconfig.get_path('include'))
    destination = dst_root / relative_path
    destination.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(src, destination)
    return destination


def write_bazel_config(hdrs: list[Path], includes:list[Path],shared_library: Path, interface_library: Union[Path, None]) -> None:
    """Создать файл конфигурации BUILD.bazel."""
    target_dir = Path("third_party/cpython")
    build_file = target_dir / "BUILD.bazel"
    with build_file.open("w") as f:
        f.write('cc_import(\n')
        f.write('    name = "cpython",\n')

        f.write('    hdrs = [\n')
        for hdr in hdrs:
            f.write(f'        "{hdr.relative_to(target_dir)}",\n')
        f.write('    ],\n')

        f.write('    includes = [\n')
        for include in includes:
            f.write(f'        "{include.relative_to(target_dir)}",\n')
        f.write('    ],\n')

        f.write(f'    shared_library = "{shared_library.relative_to(target_dir)}",\n')

        if interface_library:
            f.write(f'    interface_library = "{interface_library.relative_to(target_dir)}",\n')

        f.write('    visibility = ["//visibility:public"],\n')
        f.write(')\n')



def main() -> None:
    """Запустить скрипт."""
    if not is_supported_platform():
        raise AssertionError("Unsupported platform.")

    if not is_cpython():
        raise AssertionError("Only CPython is supported.")

    if not is_supported_python_version():
        raise AssertionError("Wrong python version.")

    print(get_cpython_root())

    clean_third_party_dir()

    build_dir = Path("third_party/cpython/internal")
    build_dir.mkdir(parents=True, exist_ok=True)

    headers = get_header_files()
    copied_headers = [copy_to_third_party(header, build_dir) for header in headers]

    include_dirs = get_include_directories()

    shared_lib = copy_to_third_party(get_shared_library(), build_dir)

    interface_lib = get_interface_library()
    if interface_lib:
        interface_lib = copy_to_third_party(interface_lib, build_dir)

    write_bazel_config(copied_headers, include_dirs, shared_lib, interface_lib)



if __name__ == "__main__":
    main()

Вот ошибки:


test_public.py:30 (test__build_file)
src = PosixPath('/Library/Frameworks/Python.framework/Versions/3.12/lib/libpython3.12.dylib')
dst_root = PosixPath('third_party/cpython/internal')

    def copy_to_third_party(src: Path, dst_root: Path) -> Path:
        """Скопировать файл в директорию сборки."""
        try:
>           relative_path = src.relative_to(get_cpython_root())

/Users/aleksandrvoytsev/PycharmProjects/hw-1-deep-python-Kyronison/cpython/cpython.py:109: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = PosixPath('/Library/Frameworks/Python.framework/Versions/3.12/lib/libpython3.12.dylib')
other = PosixPath('/Library/Frameworks/Python.framework/Versions/3.12/include')
walk_up = False, _deprecated = (), step = 0

    def relative_to(self, other, /, *_deprecated, walk_up=False):
        """Return the relative path to another path identified by the passed
        arguments.  If the operation is not possible (because this is not
        related to the other path), raise ValueError.
    
        The *walk_up* parameter controls whether `..` may be used to resolve
        the path.
        """
        if _deprecated:
            msg = ("support for supplying more than one positional argument "
                   "to pathlib.PurePath.relative_to() is deprecated and "
                   "scheduled for removal in Python {remove}")
            warnings._deprecated("pathlib.PurePath.relative_to(*args)", msg,
                                 remove=(3, 14))
        other = self.with_segments(other, *_deprecated)
        for step, path in enumerate([other] + list(other.parents)):
            if self.is_relative_to(path):
                break
            elif not walk_up:
>               raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}")
E               ValueError: '/Library/Frameworks/Python.framework/Versions/3.12/lib/libpython3.12.dylib' is not in the subpath of '/Library/Frameworks/Python.framework/Versions/3.12/include'

/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/pathlib.py:682: ValueError

During handling of the above exception, another exception occurred:

sandbox = PosixPath('/var/folders/v1/dz46c90j3sd9yk1509w8z1vr0000gn/T/87bc7219-a4a5-4d28-af84-65c0c7cf802b')

    def test__build_file(sandbox: Path) -> None:
        """Кейс: проверяем валидность файла сборки."""
>       main()

/Users/aleksandrvoytsev/PycharmProjects/hw-1-deep-python-Kyronison/cpython/test_public.py:33: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/Users/aleksandrvoytsev/PycharmProjects/hw-1-deep-python-Kyronison/cpython/cpython.py:169: in main
    shared_lib = copy_to_third_party(get_shared_library(), build_dir)
/Users/aleksandrvoytsev/PycharmProjects/hw-1-deep-python-Kyronison/cpython/cpython.py:111: in copy_to_third_party
    relative_path = src.relative_to(sysconfig.get_path('include'))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = PosixPath('/Library/Frameworks/Python.framework/Versions/3.12/lib/libpython3.12.dylib')
other = PosixPath('/Library/Frameworks/Python.framework/Versions/3.12/include/python3.12')
walk_up = False, _deprecated = (), step = 0

    def relative_to(self, other, /, *_deprecated, walk_up=False):
        """Return the relative path to another path identified by the passed
        arguments.  If the operation is not possible (because this is not
        related to the other path), raise ValueError.
    
        The *walk_up* parameter controls whether `..` may be used to resolve
        the path.
        """
        if _deprecated:
            msg = ("support for supplying more than one positional argument "
                   "to pathlib.PurePath.relative_to() is deprecated and "
                   "scheduled for removal in Python {remove}")
            warnings._deprecated("pathlib.PurePath.relative_to(*args)", msg,
                                 remove=(3, 14))
        other = self.with_segments(other, *_deprecated)
        for step, path in enumerate([other] + list(other.parents)):
            if self.is_relative_to(path):
                break
            elif not walk_up:
>               raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}")
E               ValueError: '/Library/Frameworks/Python.framework/Versions/3.12/lib/libpython3.12.dylib' is not in the subpath of '/Library/Frameworks/Python.framework/Versions/3.12/include/python3.12'

/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/pathlib.py:682: ValueError

Объясни, в чем ошибка и исправь код, чтобы их не было