Skip to content

Instantly share code, notes, and snippets.

@toryano0820
Last active November 7, 2020 04:10
Show Gist options
  • Save toryano0820/b597756672288e57ab7d35ea4e985694 to your computer and use it in GitHub Desktop.
Save toryano0820/b597756672288e57ab7d35ea4e985694 to your computer and use it in GitHub Desktop.
Create `.so` package from Python3 package or module.
'''
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