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