#!/usr/bin/env python # # Script to build and install Python-bindings. # Version: 20230909 from __future__ import print_function import copy import datetime import glob import gzip import platform import os import shlex import shutil import subprocess import sys import tarfile import zipfile from distutils.ccompiler import new_compiler from setuptools import Extension from setuptools import setup from setuptools.command.build_ext import build_ext from setuptools.command.sdist import sdist if (sys.version_info[0], sys.version_info[1]) < (3, 7): print(("Unsupported Python version: {0:s}, version 3.7 or higher " "required.").format(sys.version)) sys.exit(1) class custom_build_ext(build_ext): """Custom handler for the build_ext command.""" def _RunCommand(self, command): """Runs the command.""" arguments = shlex.split(command) process = subprocess.Popen( arguments, stderr=subprocess.PIPE, stdout=subprocess.PIPE, universal_newlines=True) if not process: raise RuntimeError("Running: {0:s} failed.".format(command)) output, error = process.communicate() if process.returncode != 0: error = "\n".join(error.split("\n")[-5:]) raise RuntimeError("Running: {0:s} failed with error:\n{1:s}.".format( command, error)) return output def build_extensions(self): """Set up the build extensions.""" # TODO: move build customization here? build_ext.build_extensions(self) def run(self): """Runs the build extension.""" compiler = new_compiler(compiler=self.compiler) if compiler.compiler_type == "msvc": self.define = [ ("_CRT_SECURE_NO_WARNINGS", ""), ("UNICODE", ""), ] else: command = "sh configure --disable-nls --disable-shared-libs" output = self._RunCommand(command) print_line = False for line in output.split("\n"): line = line.rstrip() if line == "configure:": print_line = True if print_line: print(line) self.define = [ ("HAVE_CONFIG_H", ""), ] build_ext.run(self) class custom_sdist(sdist): """Custom handler for the sdist command.""" def run(self): """Builds a source distribution (sdist) package.""" if self.formats != ["gztar"] and self.formats != ["zip"]: print("'setup.py sdist' unsupported format.") sys.exit(1) if glob.glob("*.tar.gz"): print("'setup.py sdist' remove existing *.tar.gz files from " "source directory.") sys.exit(1) command = "make dist" exit_code = subprocess.call(command, shell=True) if exit_code != 0: raise RuntimeError("Running: {0:s} failed.".format(command)) if not os.path.exists(self.dist_dir): os.mkdir(self.dist_dir) source_package_file = glob.glob("*.tar.gz")[0] source_package_prefix, _, source_package_suffix = ( source_package_file.partition("-")) sdist_package_file = "{0:s}-python-{1:s}".format( source_package_prefix, source_package_suffix) sdist_package_file = os.path.join(self.dist_dir, sdist_package_file) os.rename(source_package_file, sdist_package_file) # Create and add the PKG-INFO file to the source package. with gzip.open(sdist_package_file, "rb") as input_file: with open(sdist_package_file[:-3], "wb") as output_file: shutil.copyfileobj(input_file, output_file) os.remove(sdist_package_file) self.distribution.metadata.write_pkg_info(".") pkg_info_path = "{0:s}-{1:s}/PKG-INFO".format( source_package_prefix, source_package_suffix[:-7]) with tarfile.open(sdist_package_file[:-3], "a:") as tar_file: tar_file.add("PKG-INFO", arcname=pkg_info_path) os.remove("PKG-INFO") with open(sdist_package_file[:-3], "rb") as input_file: with gzip.open(sdist_package_file, "wb") as output_file: shutil.copyfileobj(input_file, output_file) os.remove(sdist_package_file[:-3]) # Convert the .tar.gz into a .zip if self.formats == ["zip"]: zip_sdist_package_file = "{0:s}.zip".format(sdist_package_file[:-7]) with tarfile.open(sdist_package_file, "r|gz") as tar_file: with zipfile.ZipFile( zip_sdist_package_file, "w", zipfile.ZIP_DEFLATED) as zip_file: for tar_file_entry in tar_file: file_entry = tar_file.extractfile(tar_file_entry) if tar_file_entry.isfile(): modification_time = datetime.datetime.fromtimestamp( tar_file_entry.mtime) zip_modification_time = ( modification_time.year, modification_time.month, modification_time.day, modification_time.hour, modification_time.minute, modification_time.second) zip_info = zipfile.ZipInfo( date_time=zip_modification_time, filename=tar_file_entry.name) zip_info.external_attr = (tar_file_entry.mode & 0xff) << 16 file_data = file_entry.read() zip_file.writestr(zip_info, file_data) os.remove(sdist_package_file) sdist_package_file = zip_sdist_package_file # Inform distutils what files were created. dist_files = getattr(self.distribution, "dist_files", []) dist_files.append(("sdist", "", sdist_package_file)) class ProjectInformation(object): """Project information.""" def __init__(self): """Initializes project information.""" super(ProjectInformation, self).__init__() self.include_directories = [] self.library_name = None self.library_names = [] self.library_version = None self._ReadConfigureAc() self._ReadMakefileAm() @property def module_name(self): """The Python module name.""" return "py{0:s}".format(self.library_name[3:]) def _ReadConfigureAc(self): """Reads configure.ac to initialize the project information.""" with open("configure.ac", "r", encoding="utf-8") as file_object: found_ac_init = False found_library_name = False for line in file_object.readlines(): line = line.strip() if found_library_name: library_version = line[1:-2] self.library_version = library_version break elif found_ac_init: library_name = line[1:-2] self.library_name = library_name found_library_name = True elif line.startswith("AC_INIT"): found_ac_init = True if not self.library_name or not self.library_version: raise RuntimeError( "Unable to find library name and version in: configure.ac") def _ReadMakefileAm(self): """Reads Makefile.am to initialize the project information.""" if not self.library_name: raise RuntimeError("Missing library name") with open("Makefile.am", "r", encoding="utf-8") as file_object: found_subdirs = False for line in file_object.readlines(): line = line.strip() if found_subdirs: library_name, _, _ = line.partition(" ") self.include_directories.append(library_name) if library_name.startswith("lib"): self.library_names.append(library_name) if library_name == self.library_name: break elif line.startswith("SUBDIRS"): found_subdirs = True if not self.include_directories or not self.library_names: raise RuntimeError( "Unable to find include directories and library names in: " "Makefile.am") project_information = ProjectInformation() CMDCLASS = { "build_ext": custom_build_ext, "sdist": custom_sdist} SOURCES = [] # TODO: replace by detection of MSC DEFINE_MACROS = [] if platform.system() == "Windows": DEFINE_MACROS.append(("WINVER", "0x0501")) # TODO: determine how to handle third party DLLs. for library_name in project_information.library_names: if library_name != project_information.library_name: definition = "HAVE_LOCAL_{0:s}".format(library_name.upper()) DEFINE_MACROS.append((definition, "")) # Put everything inside the Python module to prevent issues with finding # shared libaries since pip does not integrate well with the system package # management. for library_name in project_information.library_names: for source_file in glob.glob(os.path.join(library_name, "*.[ly]")): generated_source_file = "{0:s}.c".format(source_file[:-2]) if not os.path.exists(generated_source_file): raise RuntimeError("Missing generated source file: {0:s}".format( generated_source_file)) source_files = glob.glob(os.path.join(library_name, "*.c")) SOURCES.extend(source_files) source_files = glob.glob(os.path.join(project_information.module_name, "*.c")) SOURCES.extend(source_files) # TODO: find a way to detect missing python.h # e.g. on Ubuntu python-dev is not installed by python-pip setup_args = dict( cmdclass=CMDCLASS, ext_modules=[ Extension( project_information.module_name, define_macros=DEFINE_MACROS, include_dirs=project_information.include_directories, libraries=[], library_dirs=[], sources=SOURCES ) ] ) setup(**setup_args)