import argparse # Разбираем путь выходной библиотеки и целевую архитектуру настольной сборки. import hashlib # Проверяем закреплённый архив Eigen перед использованием заголовков. import os # Находим стандартный каталог установщика Visual Studio в Windows. from pathlib import Path # Разрешаем пути независимо от текущего рабочего каталога сборки. import platform # Выбираем расширение библиотеки и инструментарий целевой ОС. import shutil # Находим доступный системный компилятор для Unix-подобных ОС. import subprocess # Запускаем компиляторы с проверкой кода завершения. import tarfile # Читаем заголовки и лицензии из проверенного архива без сетевого доступа. ROOT = Path(__file__).resolve().parent # Все исходники и закреплённая зависимость расположены рядом со скриптом. CORE = ROOT.parents[1] / "c" / "set-protocol" # Подключаем исходный C99-алгоритм из того же репозитория, не создавая его копию. SHA256 = "8586084f71f9bde545ee7fa6d00288b264a2b7ac3607b974e54d13e7162c1c72" # Контрольная сумма официального архива Eigen 3.4.0 из GitLab. def headers(directory): # Подготавливаем только необходимые заголовки и лицензии в каталоге конкретной сборки. archive = ROOT / "vendor" / "eigen-3.4.0.tar.gz" # Закреплённый архив позволяет собирать EXE без скачивания зависимостей. if hashlib.sha256(archive.read_bytes()).hexdigest() != SHA256: # Повреждённая или подменённая зависимость не должна попадать в бинарный модуль. raise RuntimeError("Неверная SHA-256 архива Eigen 3.4.0") # Останавливаем сборку с явным указанием нарушенной проверки. destination = directory / "eigen-3.4.0" # Раздельный каталог исключает пересечение разных целевых сборок. with tarfile.open(archive, "r:gz") as source: # Читаем gzip-архив штатной библиотекой Python, совместимой с Python 3.8. for member in source.getmembers(): # Рассматриваем каждый элемент, но не используем небезопасную полную распаковку архива. parts = Path(member.name).parts[1:] # Удаляем единственный верхний каталог официального дистрибутива. if not member.isfile() or not parts or any(part in ("..", "/", "\\") for part in parts): # Разрешаем только обычные файлы с безопасным относительным путём. continue # Пропускаем ссылки, каталоги и элементы, способные выйти за корень распаковки. if parts[0] != "Eigen" and not parts[0].startswith("COPYING"): # Для QR достаточно основного дерева Eigen и его лицензионных файлов. continue # Примеры, тесты и неподдерживаемые модули не нужны при сборке приложения. target = destination.joinpath(*parts) # Сохраняем исходное расположение внутренних include относительно корня Eigen. target.resolve().relative_to(destination.resolve()) # Дополнительно проверяем итоговый абсолютный путь перед записью. target.parent.mkdir(parents=True, exist_ok=True) # Создаём только каталог текущего безопасного файла. with source.extractfile(member) as content: # Открываем содержимое обычного файла внутри проверенного архива. target.write_bytes(content.read()) # Сохраняем оригинальные заголовки без изменения стороннего кода. return destination # Возвращаем корень, который компилятор должен добавить в include-пути. def build(output, arch): # Собираем отдельный модуль, сохраняя основное C99-ядро без зависимости от C++. output = output.resolve() # Абсолютный путь надёжен при смене рабочего каталога компилятора. directory = output.parent / (".signal-eigen-" + arch) # Объектные файлы x86 и x64 не смешиваются. directory.mkdir(parents=True, exist_ok=True) # Создаём каталог выходной библиотеки и промежуточных файлов. include = headers(directory) # Проверяем архив и получаем локальные заголовки Eigen до запуска компилятора. if platform.system() == "Windows": # На Windows используем тот же установленный MSVC, что основная DLL. vswhere = Path(os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")) / "Microsoft Visual Studio/Installer/vswhere.exe" # Стандартный путь установщика соответствует основному сборщику SETGUI. installation = subprocess.check_output([str(vswhere), "-latest", "-products", "*", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "-property", "installationPath"], text=True).strip() # Находим установку с компиляторами C/C++ для обеих архитектур. if not installation: # Пустой ответ означает отсутствие подходящего инструментария. raise RuntimeError("Не найдены Visual Studio C++ Build Tools") # Сообщаем причину вместо создания приложения без выбранного варианта Eigen. vcvars = Path(installation) / "VC/Auxiliary/Build" / ("vcvars32.bat" if arch == "x86" else "vcvars64.bat") # Настраиваем среду именно под разрядность Python и EXE. common = ["cl", "/nologo", "/O2", "/MT", "/DPCAN_ABI_BUILD_DLL", "/I" + str(CORE / "include")] # Оба варианта C и Eigen используют оптимизацию и статический CRT без новой зависимости от установленного runtime. commands = [common + ["/std:c11", "/c", str(CORE / "src/set_signal.c"), "/Fo" + str(directory / "signal.obj")], common + ["/std:c++14", "/EHsc", "/I" + str(include), "/c", str(ROOT / "signal_eigen.cpp"), "/Fo" + str(directory / "eigen.obj")], ["link", "/nologo", "/DLL", "/MACHINE:" + arch.upper(), "/SUBSYSTEM:WINDOWS,6.01", "/OUT:" + str(output), str(directory / "signal.obj"), str(directory / "eigen.obj")]] # Отдельно компилируем C99 и C++, а PE-подсистему закрепляем на Windows 7 (6.01). for command in commands: # Последовательные этапы используют только готовые объектные файлы предыдущих команд. subprocess.run('call "%s" && %s' % (vcvars, subprocess.list2cmdline(command)), cwd=directory, shell=True, check=True) # Batch-настройка MSVC и компилятор выполняются в одном процессе cmd. else: # Настольные Linux/macOS используют системные C/C++-компиляторы и тот же исходный алгоритм. cc, cxx = shutil.which("cc"), shutil.which("c++") # C-исходник компилируем как C99, а Eigen исключительно как C++14. if not cc or not cxx: # Обе стадии необходимы для сохранения языковой границы общего ядра. raise RuntimeError("Нужны компиляторы C и C++") # Отсутствие инструментария не приводит к скрытому отключению Eigen. subprocess.run([cc, "-O2", "-std=c99", "-fPIC", "-I" + str(CORE / "include"), "-c", str(CORE / "src/set_signal.c"), "-o", str(directory / "signal.o")], check=True) # Получаем позиционно-независимый объект оригинального C99-ядра. subprocess.run([cxx, "-O2", "-std=c++14", "-fPIC", "-shared", "-I" + str(CORE / "include"), "-I" + str(include), str(ROOT / "signal_eigen.cpp"), str(directory / "signal.o"), "-o", str(output)], check=True) # Линкер C++ добавляет свою стандартную библиотеку к общему настольному модулю. for name in ("COPYING.MPL2",): # Распространяем основной текст лицензии вместе с бинарным компонентом. shutil.copyfile(include / name, output.parent / ("Eigen-" + name)) # Имя явно указывает, к какой встроенной библиотеке относится лицензия. print(output) # Показываем точный путь успешно собранного компонента для журналов сборки. if __name__ == "__main__": # CLI используется как вручную, так и сборщиком EXE. parser = argparse.ArgumentParser(description="Сборка альтернативного МНК Eigen QR") # Объясняем назначение отдельного настольного модуля. parser.add_argument("--output", type=Path, required=True) # Вызывающий определяет выходной путь и расширение DLL/SO. parser.add_argument("--arch", choices=("x86", "x64"), default="x64") # Windows 7 x86 явно выбирает 32-битный PE вместо значения по умолчанию. arguments = parser.parse_args() # Проверяем обязательные параметры до работы с файловой системой. build(arguments.output, arguments.arch) # Полностью выполняем сборку либо завершаем процесс исключением и ненулевым кодом.