1#!/usr/bin/env python 2# 3# Script to build and install Python-bindings. 4# Version: 20191025 5 6from __future__ import print_function 7 8import copy 9import glob 10import gzip 11import platform 12import os 13import shlex 14import shutil 15import subprocess 16import sys 17import tarfile 18 19from distutils import sysconfig 20from distutils.ccompiler import new_compiler 21from distutils.command.bdist import bdist 22from setuptools import dist 23from setuptools import Extension 24from setuptools import setup 25from setuptools.command.build_ext import build_ext 26from setuptools.command.sdist import sdist 27 28try: 29 from distutils.command.bdist_msi import bdist_msi 30except ImportError: 31 bdist_msi = None 32 33 34if not bdist_msi: 35 custom_bdist_msi = None 36else: 37 class custom_bdist_msi(bdist_msi): 38 """Custom handler for the bdist_msi command.""" 39 40 def run(self): 41 """Builds an MSI.""" 42 # Make a deepcopy of distribution so the following version changes 43 # only apply to bdist_msi. 44 self.distribution = copy.deepcopy(self.distribution) 45 46 # bdist_msi does not support the library version so we add ".1" 47 # as a work around. 48 self.distribution.metadata.version = "{0:s}.1".format( 49 self.distribution.metadata.version) 50 51 bdist_msi.run(self) 52 53 54class custom_bdist_rpm(bdist): 55 """Custom handler for the bdist_rpm command.""" 56 57 def run(self): 58 """Builds a RPM.""" 59 print("'setup.py bdist_rpm' command not supported use 'rpmbuild' instead.") 60 sys.exit(1) 61 62 63class custom_build_ext(build_ext): 64 """Custom handler for the build_ext command.""" 65 66 def _RunCommand(self, command): 67 """Runs the command.""" 68 arguments = shlex.split(command) 69 process = subprocess.Popen( 70 arguments, stderr=subprocess.PIPE, stdout=subprocess.PIPE, 71 universal_newlines=True) 72 if not process: 73 raise RuntimeError("Running: {0:s} failed.".format(command)) 74 75 output, error = process.communicate() 76 if process.returncode != 0: 77 error = "\n".join(error.split("\n")[-5:]) 78 raise RuntimeError("Running: {0:s} failed with error:\n{1:s}.".format( 79 command, error)) 80 81 return output 82 83 def build_extensions(self): 84 """Set up the build extensions.""" 85 # TODO: move build customization here? 86 build_ext.build_extensions(self) 87 88 def run(self): 89 """Runs the build extension.""" 90 compiler = new_compiler(compiler=self.compiler) 91 if compiler.compiler_type == "msvc": 92 self.define = [ 93 ("UNICODE", ""), 94 ] 95 96 else: 97 command = "sh configure --disable-shared-libs" 98 output = self._RunCommand(command) 99 100 print_line = False 101 for line in output.split("\n"): 102 line = line.rstrip() 103 if line == "configure:": 104 print_line = True 105 106 if print_line: 107 print(line) 108 109 self.define = [ 110 ("HAVE_CONFIG_H", ""), 111 ("LOCALEDIR", "\"/usr/share/locale\""), 112 ] 113 114 build_ext.run(self) 115 116 117class custom_sdist(sdist): 118 """Custom handler for the sdist command.""" 119 120 def run(self): 121 """Builds a source distribution (sdist) package.""" 122 if self.formats != ["gztar"]: 123 print("'setup.py sdist' unsupported format.") 124 sys.exit(1) 125 126 if glob.glob("*.tar.gz"): 127 print("'setup.py sdist' remove existing *.tar.gz files from " 128 "source directory.") 129 sys.exit(1) 130 131 command = "make dist" 132 exit_code = subprocess.call(command, shell=True) 133 if exit_code != 0: 134 raise RuntimeError("Running: {0:s} failed.".format(command)) 135 136 if not os.path.exists("dist"): 137 os.mkdir("dist") 138 139 source_package_file = glob.glob("*.tar.gz")[0] 140 source_package_prefix, _, source_package_suffix = ( 141 source_package_file.partition("-")) 142 sdist_package_file = "{0:s}-python-{1:s}".format( 143 source_package_prefix, source_package_suffix) 144 sdist_package_file = os.path.join("dist", sdist_package_file) 145 os.rename(source_package_file, sdist_package_file) 146 147 # Create and add the PKG-INFO file to the source package. 148 with gzip.open(sdist_package_file, 'rb') as input_file: 149 with open(sdist_package_file[:-3], 'wb') as output_file: 150 shutil.copyfileobj(input_file, output_file) 151 os.remove(sdist_package_file) 152 153 self.distribution.metadata.write_pkg_info(".") 154 pkg_info_path = "{0:s}-{1:s}/PKG-INFO".format( 155 source_package_prefix, source_package_suffix[:-7]) 156 with tarfile.open(sdist_package_file[:-3], "a:") as tar_file: 157 tar_file.add("PKG-INFO", arcname=pkg_info_path) 158 os.remove("PKG-INFO") 159 160 with open(sdist_package_file[:-3], 'rb') as input_file: 161 with gzip.open(sdist_package_file, 'wb') as output_file: 162 shutil.copyfileobj(input_file, output_file) 163 os.remove(sdist_package_file[:-3]) 164 165 # Inform distutils what files were created. 166 dist_files = getattr(self.distribution, "dist_files", []) 167 dist_files.append(("sdist", "", sdist_package_file)) 168 169 170class ProjectInformation(object): 171 """Project information.""" 172 173 def __init__(self): 174 """Initializes project information.""" 175 super(ProjectInformation, self).__init__() 176 self.include_directories = [] 177 self.library_name = None 178 self.library_names = [] 179 self.library_version = None 180 181 self._ReadConfigureAc() 182 self._ReadMakefileAm() 183 184 @property 185 def module_name(self): 186 """The Python module name.""" 187 return "py{0:s}".format(self.library_name[3:]) 188 189 @property 190 def package_name(self): 191 """The package name.""" 192 return "{0:s}-python".format(self.library_name) 193 194 @property 195 def package_description(self): 196 """The package description.""" 197 return "Python bindings module for {0:s}".format(self.library_name) 198 199 @property 200 def project_url(self): 201 """The project URL.""" 202 return "https://github.com/libyal/{0:s}/".format(self.library_name) 203 204 def _ReadConfigureAc(self): 205 """Reads configure.ac to initialize the project information.""" 206 file_object = open("configure.ac", "rb") 207 if not file_object: 208 raise IOError("Unable to open: configure.ac") 209 210 found_ac_init = False 211 found_library_name = False 212 for line in file_object.readlines(): 213 line = line.strip() 214 if found_library_name: 215 library_version = line[1:-2] 216 if sys.version_info[0] >= 3: 217 library_version = library_version.decode("ascii") 218 self.library_version = library_version 219 break 220 221 elif found_ac_init: 222 library_name = line[1:-2] 223 if sys.version_info[0] >= 3: 224 library_name = library_name.decode("ascii") 225 self.library_name = library_name 226 found_library_name = True 227 228 elif line.startswith(b"AC_INIT"): 229 found_ac_init = True 230 231 file_object.close() 232 233 if not self.library_name or not self.library_version: 234 raise RuntimeError( 235 "Unable to find library name and version in: configure.ac") 236 237 def _ReadMakefileAm(self): 238 """Reads Makefile.am to initialize the project information.""" 239 if not self.library_name: 240 raise RuntimeError("Missing library name") 241 242 file_object = open("Makefile.am", "rb") 243 if not file_object: 244 raise IOError("Unable to open: Makefile.am") 245 246 found_subdirs = False 247 for line in file_object.readlines(): 248 line = line.strip() 249 if found_subdirs: 250 library_name, _, _ = line.partition(b" ") 251 if sys.version_info[0] >= 3: 252 library_name = library_name.decode("ascii") 253 254 self.include_directories.append(library_name) 255 256 if library_name.startswith("lib"): 257 self.library_names.append(library_name) 258 259 if library_name == self.library_name: 260 break 261 262 elif line.startswith(b"SUBDIRS"): 263 found_subdirs = True 264 265 file_object.close() 266 267 if not self.include_directories or not self.library_names: 268 raise RuntimeError( 269 "Unable to find include directories and library names in: " 270 "Makefile.am") 271 272 273project_information = ProjectInformation() 274 275SOURCES = [] 276 277# TODO: replace by detection of MSC 278DEFINE_MACROS = [] 279if platform.system() == "Windows": 280 DEFINE_MACROS.append(("WINVER", "0x0501")) 281 # TODO: determine how to handle third party DLLs. 282 for library_name in project_information.library_names: 283 if library_name != project_information.library_name: 284 definition = "HAVE_LOCAL_{0:s}".format(library_name.upper()) 285 286 DEFINE_MACROS.append((definition, "")) 287 288# Put everything inside the Python module to prevent issues with finding 289# shared libaries since pip does not integrate well with the system package 290# management. 291for library_name in project_information.library_names: 292 for source_file in glob.glob(os.path.join(library_name, "*.[ly]")): 293 generated_source_file = "{0:s}.c".format(source_file[:-2]) 294 if not os.path.exists(generated_source_file): 295 raise RuntimeError("Missing generated source file: {0:s}".format( 296 generated_source_file)) 297 298 source_files = glob.glob(os.path.join(library_name, "*.c")) 299 SOURCES.extend(source_files) 300 301source_files = glob.glob(os.path.join(project_information.module_name, "*.c")) 302SOURCES.extend(source_files) 303 304# TODO: find a way to detect missing python.h 305# e.g. on Ubuntu python-dev is not installed by python-pip 306 307# TODO: what about description and platform in egg file 308 309setup( 310 name=project_information.package_name, 311 url=project_information.project_url, 312 version=project_information.library_version, 313 description=project_information.package_description, 314 long_description=project_information.package_description, 315 author="Joachim Metz", 316 author_email="joachim.metz@gmail.com", 317 license="GNU Lesser General Public License v3 or later (LGPLv3+)", 318 cmdclass={ 319 "build_ext": custom_build_ext, 320 "bdist_msi": custom_bdist_msi, 321 "bdist_rpm": custom_bdist_rpm, 322 "sdist": custom_sdist, 323 }, 324 ext_modules=[ 325 Extension( 326 project_information.module_name, 327 define_macros=DEFINE_MACROS, 328 include_dirs=project_information.include_directories, 329 libraries=[], 330 library_dirs=[], 331 sources=SOURCES, 332 ), 333 ], 334) 335 336