Last active
November 7, 2020 04:10
-
-
Save toryano0820/b597756672288e57ab7d35ea4e985694 to your computer and use it in GitHub Desktop.
Create `.so` package from Python3 package or module.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
''' | |
Create `.so` package from Python3 package or module. | |
- Put this script outside working directory. | |
- Compile package: `cd /path/to/src/folder && python3 /path/to/py3compile.py` | |
- Compile module: `cd /path/to/src/folder && python3 /path/to/py3compile.py FILE1 [FILE2 ...]` | |
- Output: `/path/to/src/folder_so` | |
''' | |
from distutils.core import setup | |
from distutils import sysconfig | |
from Cython.Build import cythonize | |
from Cython.Distutils import build_ext | |
import os | |
import shutil | |
import time | |
from sys import argv | |
from tempfile import mkdtemp | |
import signal | |
import sys | |
import traceback | |
class NoSuffixBuilder(build_ext): | |
def get_ext_filename(self, ext_name): | |
filename = super().get_ext_filename(ext_name) | |
suffix = sysconfig.get_config_var('EXT_SUFFIX') | |
ext = os.path.splitext(filename)[1] | |
return filename.replace(suffix, "") + ext | |
def clean(): | |
try: | |
shutil.rmtree(os.path.dirname(tmpd)) | |
except: | |
pass | |
def interrupt_handler(signum, frame): | |
print("[INFO] Cancelled by user") | |
clean() | |
sys.exit(-2) | |
cwd = os.getcwd() | |
tmpd = mkdtemp() | |
out_dir = cwd + "_so" | |
py_modules = [] | |
resources = [] | |
exclude_dirs = [".git", "__pycache__"] | |
exclude_files = [".gitignore", ".dockerignore", "Dockerfile", "dockerfile", "docker-compose.yml", "requirements.txt"] | |
exclude_file_ends = [".whl", ".pyc", ".bak", ".BAK", ".orig", ".swp"] | |
signal.signal(signal.SIGINT, interrupt_handler) | |
shutil.rmtree(out_dir, ignore_errors=True) | |
def any_endswith(text_list, compare_list): | |
if type(text_list) is str: | |
text_list = [text_list] | |
if type(compare_list) is str: | |
compare_list = [compare_list] | |
for compare in compare_list: | |
for text in text_list: | |
if text.endswith(compare): | |
return True | |
return False | |
def file_size(fpath): | |
with open(fpath) as fobject: | |
fsize = len(fobject.read()) | |
return fsize | |
def tree_any_endswith(compare_list, root): | |
for _, _, files in os.walk(root): | |
if any_endswith(files, compare_list): | |
return True | |
return False | |
if any_endswith(argv[1:], [".py", ".pyx"]): | |
py_modules[:] = [f for f in argv[1:] if any_endswith(f, [".py", ".pyx"])] | |
for f in py_modules: | |
argv.remove(f) | |
dst = os.path.join(tmpd, os.path.dirname(f)) | |
os.makedirs(dst, exist_ok=True) | |
shutil.copy(f, dst) | |
else: | |
# REMOVE unnecessary __init__.py's | |
# for root, _, files in os.walk("./", topdown=True): | |
# if "__init__.py" in files: | |
# fpath = os.path.join(root, "__init__.py") | |
# if file_size(fpath) == 0: | |
# os.remove(fpath) | |
# Collect python modules and resources | |
for root, dirs, files in os.walk("./", topdown=True): | |
dirs[:] = [d for d in dirs if d not in exclude_dirs] | |
files[:] = [f for f in files if f not in exclude_files and not any_endswith(f, exclude_file_ends)] | |
for fname in files: | |
fpath = os.path.join(root, fname) | |
if any_endswith(fname, [".py", ".pyx"]): | |
# Don't compile empty __init__.py/__init__.pyx | |
if (fname in ["__init__.py", "__init__.pyx"]) and file_size(fpath) == 0: | |
continue | |
py_modules.append(fpath) | |
# Create temporary directory and copy src files | |
dst = os.path.join(tmpd, root) | |
os.makedirs(dst, exist_ok=True) | |
shutil.copy(fpath, dst) | |
else: | |
resources.append(fpath) | |
# Work in temporary directory | |
print(f"[INFO] Entering '{tmpd}'") | |
os.chdir(tmpd) | |
# Add needed __init__.py's in temporary directory | |
# Doesn't compile __init__.py, cython/distutils just want them there so folder structure don't mess up | |
for root, dirs, files in os.walk("./", topdown=True): | |
dirs[:] = [d for d in dirs if d not in exclude_dirs] | |
files[:] = [f for f in files if f not in exclude_files] | |
if "__init__.py" not in files and "__init__.pyx" not in files: | |
if any_endswith(files, ".py") or tree_any_endswith(".py", root): | |
with open(os.path.join(root, "__init__.py"), "w"): | |
pass | |
elif any_endswith(files, ".pyx") or tree_any_endswith(".pyx", root): | |
with open(os.path.join(root, "__init__.pyx"), "w"): | |
pass | |
# Build | |
argv.append("build_ext") | |
try: | |
setup( | |
ext_modules=cythonize( | |
py_modules, | |
language_level=3, | |
build_dir="./cython" | |
), | |
options={ | |
"build_ext": { | |
"build_lib": tmpd | |
} | |
}, | |
# cmdclass={ | |
# "build_ext": NoSuffixBuilder | |
# } | |
) | |
# Move compiled folder to defined output directory | |
shutil.move(os.path.join(tmpd, os.path.basename(tmpd)), out_dir) | |
# Go back to execution directory | |
os.chdir(cwd) | |
# Copy resources | |
for resource in resources: | |
dest_dir = os.path.join(out_dir, os.path.dirname(resource)) | |
os.makedirs(dest_dir, exist_ok=True) | |
shutil.copyfile(resource, os.path.join(out_dir, resource)) | |
except: | |
traceback.print_exc() | |
pass | |
# Clean | |
clean() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment