Цель этого поста — создать setup.py
, который создаст исходный дистрибутив. Это означает, что после запуска
python setup.py sdist
полученный dist/grumbo-1.0.tar.gz
можно использовать для установки через
pip install grumbo-1.0.tar.gz
Мы начнем с версии setup.py
для Linux/MacOS, но затем настроим ее так, чтобы она работала и для Windows.
Первый шаг — добавить в дистрибутив дополнительные данные (включает/библиотека). Я не уверен, что действительно невозможно добавить данные для модуля, но setuptools
предлагает функциональность для добавления данных для пакетов, поэтому давайте создадим пакет из вашего модуля (что, вероятно, в любом случае является хорошей идеей).
Новая структура package grumbo
выглядит следующим образом:
src/
grumbo/
__init__.py # empty
grumbo.c
include/
plumbus.h
lib/
libplumbus.so
setup.py
и изменил setup.py
:
from setuptools import setup, Extension, find_packages
native_module = Extension(
name='grumbo.grumbo',
sources = ["src/grumbo/grumbo.c"],
)
kwargs = {
'name' : 'grumbo',
'version' : '1.0',
'ext_modules' : [native_module],
'packages':find_packages(where='src'),
'package_dir':{"": "src"},
}
setup(**kwargs)
Это пока мало что дает, но, по крайней мере, наш пакет может быть найден setuptools
. Сборка не удалась, потому что отсутствуют включения.
Теперь добавим нужные include из папки include
в раздачу через package-data
:
...
kwargs = {
...,
'package_data' : { 'grumbo': ['include/*.h']},
}
...
При этом наши include-файлы копируются в исходный дистрибутив. Однако, поскольку он будет построен где-то, о чем мы еще не знаем, добавление include_dirs = ['include']
к определению Extension
не поможет.
Должен быть лучший способ (и менее хрупкий) найти правильный путь включения, но это то, что я придумал:
...
import os
import sys
import sysconfig
def path_to_build_folder():
"""Returns the name of a distutils build directory"""
f = "{dirname}.{platform}-{version[0]}.{version[1]}"
dir_name = f.format(dirname='lib',
platform=sysconfig.get_platform(),
version=sys.version_info)
return os.path.join('build', dir_name, 'grumbo')
native_module = Extension(
...,
include_dirs = [os.path.join(path_to_build_folder(),'include')],
)
...
Теперь расширение построено, но еще не может быть загружено, поскольку оно не связано с общим объектом libplumbus.so
, и поэтому некоторые символы не разрешены.
Подобно заголовочным файлам, мы можем добавить нашу библиотеку в дистрибутив:
kwargs = {
...,
'package_data' : { 'grumbo': ['include/*.h', 'lib/*.so']},
}
...
и добавьте правильный путь к библиотеке для компоновщика:
...
native_module = Extension(
...
libraries = ['plumbus'],
library_dirs = [os.path.join(path_to_build_folder(), 'lib')],
)
...
Вот, мы почти у цели:
- расширение встроено в
site-packages/grumbo/
- расширение зависит от
libplumbus.so
как видно с помощью ldd
libplumbus.so
помещается в site-packages/grumbo/lib
Однако мы по-прежнему не можем импортировать расширение, так как import grumbo.grumbo
ведет к
ImportError: libplumbus.so: невозможно открыть общий объектный файл: нет такого файла или каталога
потому что загрузчик не может найти нужный общий объект, который находится в папке .\lib
относительно нашего расширения. Мы могли бы использовать rpath
, чтобы помочь загрузчику:
...
native_module = Extension(
...
extra_link_args = ["-Wl,-rpath=$ORIGIN/lib/."],
)
...
И вот мы закончили:
>>> import grumbo.grumbo
# works!
Также сборка и установка колеса должны работать:
python setup.py bdist_wheel
а потом:
pip install grumbo-1.0-xxxx.whl
Камень первой мили пройден. Теперь мы расширяем его, чтобы он работал и на других платформах.
Один и тот же исходный дистрибутив для Linux и Macos:
Чтобы иметь возможность установить один и тот же исходный дистрибутив в Linux и MacOS, должны присутствовать обе версии общей библиотеки (для Linux и MacOS). Можно добавить суффикс к именам общих объектов: например. имея libplumbus.linux.so
и libplumbis.macos.so
. Правильный общий объект можно выбрать в setup.py
в зависимости от платформы:
...
import platform
def pick_library():
my_system = platform.system()
if my_system == 'Linux':
return "plumbus.linux"
if my_system == 'Darwin':
return "plumbus.macos"
if my_system == 'Windows':
return "plumbus"
raise ValueError("Unknown platform: " + my_system)
native_module = Extension(
...
libraries = [pick_library()],
...
)
Настройка для Windows:
В Windows динамические библиотеки — это библиотеки DLL, а не общие объекты, поэтому необходимо учитывать некоторые различия:
- когда C-расширение построено, ему нужен
plumbus.lib
-файл, который нам нужно поместить в подпапку lib
.
- когда C-расширение загружается во время выполнения, ему нужен
plumbus.dll
-файл.
- В Windows нет понятия
rpath
, поэтому нам нужно поместить dll рядом с расширением, чтобы его можно было найти (см. Также это SO -пост для более подробной информации).
Это означает, что структура папок должна быть следующей:
src/
grumbo/
__init__.py
grumbo.c
plumbus.dll # needed for Windows
include/
plumbus.h
lib/
libplumbus.linux.so # needed on Linux
libplumbus.macos.so # needed on Macos
plumbus.lib # needed on Windows
setup.py
Есть также некоторые изменения в setup.py
. Во-первых, расширение package_data
, чтобы dll
и lib
были подняты:
...
kwargs = {
...
'package_data' : { 'grumbo': ['include/*.h', 'lib/*.so',
'lib/*.lib', '*.dll', # for windows
]},
}
...
Во-вторых, rpath
можно использовать только в Linux/MacOS, поэтому:
def get_extra_link_args():
if platform.system() == 'Windows':
return []
else:
return ["-Wl,-rpath=$ORIGIN/lib/."]
native_module = Extension(
...
extra_link_args = get_extra_link_args(),
)
Это!
Полный установочный файл (вы можете добавить определение макроса или что-то подобное, что я пропустил):
from setuptools import setup, Extension, find_packages
import os
import sys
import sysconfig
def path_to_build_folder():
"""Returns the name of a distutils build directory"""
f = "{dirname}.{platform}-{version[0]}.{version[1]}"
dir_name = f.format(dirname='lib',
platform=sysconfig.get_platform(),
version=sys.version_info)
return os.path.join('build', dir_name, 'grumbo')
import platform
def pick_library():
my_system = platform.system()
if my_system == 'Linux':
return "plumbus.linux"
if my_system == 'Darwin':
return "plumbus.macos"
if my_system == 'Windows':
return "plumbus"
raise ValueError("Unknown platform: " + my_system)
def get_extra_link_args():
if platform.system() == 'Windows':
return []
else:
return ["-Wl,-rpath=$ORIGIN/lib/."]
native_module = Extension(
name='grumbo.grumbo',
sources = ["src/grumbo/grumbo.c"],
include_dirs = [os.path.join(path_to_build_folder(),'include')],
libraries = [pick_library()],
library_dirs = [os.path.join(path_to_build_folder(), 'lib')],
extra_link_args = get_extra_link_args(),
)
kwargs = {
'name' : 'grumbo',
'version' : '1.0',
'ext_modules' : [native_module],
'packages':find_packages(where='src'),
'package_dir':{"": "src"},
'package_data' : { 'grumbo': ['include/*.h', 'lib/*.so',
'lib/*.lib', '*.dll', # for windows
]},
}
setup(**kwargs)
person
ead
schedule
10.09.2020