about summary refs log tree commit diff stats
path: root/setup.py
diff options
context:
space:
mode:
authorDuncan Ogilvie <mr.exodia.tpodt@gmail.com>2022-10-18 13:45:30 +0200
committerDuncan Ogilvie <mr.exodia.tpodt@gmail.com>2022-10-18 14:42:30 +0200
commitc75d0e0b1c066032d34375dec4c010d42a18381f (patch)
tree8aa085b290f21787b358050b634db9f329a897b0 /setup.py
parent458b98b28d059c81638d7642ebbb7f476a781432 (diff)
downloadmiasm-c75d0e0b1c066032d34375dec4c010d42a18381f.tar.gz
miasm-c75d0e0b1c066032d34375dec4c010d42a18381f.zip
Further improve the user experience when building on Windows
Diffstat (limited to 'setup.py')
-rw-r--r--setup.py145
1 files changed, 104 insertions, 41 deletions
diff --git a/setup.py b/setup.py
index 22855492..1a7cdee4 100644
--- a/setup.py
+++ b/setup.py
@@ -1,20 +1,23 @@
 #! /usr/bin/env python2
 
 from __future__ import print_function
-from distutils.core import setup, Extension
+# Reference: https://stackoverflow.com/a/13468644/1806760
+from setuptools import setup, Extension
 from distutils.util import get_platform
 from distutils.sysconfig import get_python_lib, get_config_vars
 from distutils.dist import DistributionMetadata
 from distutils.command.install_data import install_data
+import subprocess
 from tempfile import TemporaryFile
 import fnmatch
 import io
 import os
 import platform
-from shutil import copy2, copyfile, rmtree
+from shutil import copy2, copyfile, rmtree, which
 import sys
 import tempfile
 import atexit
+import re
 
 is_win = platform.system() == "Windows"
 is_mac = platform.system() == "Darwin"
@@ -23,10 +26,10 @@ if is_win:
     import winreg
 
 def set_extension_compile_args(extension):
-    rel_lib_path = extension.name.replace('.', '/')
+    rel_lib_path = extension.name.replace(".", "/")
     abs_lib_path = os.path.join(get_python_lib(), rel_lib_path)
-    lib_name = abs_lib_path + '.so'
-    extension.extra_link_args = [ '-Wl,-install_name,' + lib_name]
+    lib_name = abs_lib_path + ".so"
+    extension.extra_link_args = [ "-Wl,-install_name," + lib_name]
 
 class smart_install_data(install_data):
     """Replacement for distutils.command.install_data to handle
@@ -53,38 +56,77 @@ def win_find_clang_path():
         with win_get_llvm_reg() as rkey:
             return winreg.QueryValueEx(rkey, None)[0]
     except FileNotFoundError:
+        # Visual Studio ships with an optional Clang distribution, try to detect it
+        clang_cl = which("clang-cl")
+        if clang_cl is None:
+            return None
+        return os.path.abspath(os.path.join(os.path.dirname(clang_cl), "..", ".."))
+
+def win_get_clang_version(clang_path):
+    try:
+        clang_cl = os.path.join(clang_path, "bin", "clang.exe")
+        stdout = subprocess.check_output(f"\"{clang_cl}\" --version")
+        version = stdout.splitlines(False)[0].decode()
+        match = re.search(r"version (\d+\.\d+\.\d+)", version)
+        if match is None:
+            return None
+        version = list(map(lambda s: int(s), match.group(1).split(".")))
+        return version
+    except FileNotFoundError:
         return None
 
 def win_use_clang():
-    # Recent (>= 8 ?) LLVM versions does not ship anymore a cl.exe binary in
-    # the msbuild-bin directory. Thus, we need to
-    # * copy-paste bin/clang-cl.exe into a temporary directory
-    # * rename it to cl.exe
-    # * copy-paste bin/lld-link.exe into a temporary directory
-    # * rename it to link.exe
-    # * add that path first in %Path%
-    # * clean this mess on exit
-    # We could use the build directory created by distutils for this, but it
-    # seems non trivial to gather
+    # To force python to use clang we copy the binaries in a temporary directory that"s added to the PATH.
+    # We could use the build directory created by distutils for this, but it seems non-trivial to gather
     # (https://stackoverflow.com/questions/12896367/reliable-way-to-get-the-build-directory-from-within-setup-py).
+
     clang_path = win_find_clang_path()
     if clang_path is None:
         return False
+    clang_version = win_get_clang_version(clang_path)
+    if clang_version is None:
+        return False
     tmpdir = tempfile.mkdtemp(prefix="llvm")
+
     copyfile(os.path.join(clang_path, "bin", "clang-cl.exe"), os.path.join(tmpdir, "cl.exe"))
-    copyfile(os.path.join(clang_path, "bin", "lld-link.exe"), os.path.join(tmpdir, "link.exe"))
-    os.environ['Path'] = "%s;%s" % (tmpdir, os.environ["Path"])
-    atexit.register(lambda dir_: rmtree(dir_), tmpdir)
 
+    # If you run the installation from a Visual Studio command prompt link.exe will already exist
+    # Fall back to LLVM"s lld-link.exe which is compatible with link"s command line
+    if which("link") is None:
+        # LLVM >= 14.0.0 started supporting the /LTCG flag
+        # Earlier versions will error during the linking phase so bail out now
+        if clang_version[0] < 14:
+            return False
+        copyfile(os.path.join(clang_path, "bin", "lld-link.exe"), os.path.join(tmpdir, "link.exe"))
+
+    # Add the temporary directory at the front of the PATH and clean up on exit
+    os.environ["PATH"] = "%s;%s" % (tmpdir, os.environ["PATH"])
+    atexit.register(lambda dir_: rmtree(dir_), tmpdir)
+    print(f"Found Clang {clang_version[0]}.{clang_version[1]}.{clang_version[2]}: {clang_path}")
     return True
 
+build_extensions = True
+build_warnings = []
 win_force_clang = False
-if is_win and is_64bit:
-    # We do not change to clang if under 32 bits, because even with Clang we
-    # don't have uint128_t with the 32 bits ABI.
-    win_force_clang = win_use_clang()
-    if not win_force_clang:
-        print("Warning: couldn't find a Clang/LLVM installation. Some runtime functions needed by the jitter won't be compiled.")
+if is_win:
+    if is_64bit or which("cl") is None:
+        # We do not change to clang if under 32 bits, because even with Clang we
+        # do not use uint128_t with the 32 bits ABI. Regardless we can try to
+        # find it when building in 32-bit mode if cl.exe was not found in the PATH.
+        win_force_clang = win_use_clang()
+        if is_64bit and not win_force_clang:
+            build_warnings.append("Could not find a suitable Clang/LLVM installation. You can download LLVM from https://releases.llvm.org")
+            build_warnings.append("Alternatively you can select the 'C++ Clang-cl build tools' in the Visual Studio Installer")
+            build_extensions = False
+    cl = which("cl")
+    link = which("link")
+    if cl is None or link is None:
+        build_warnings.append("Could not find cl.exe and/or link.exe in the PATH, try building miasm from a Visual Studio command prompt")
+        build_warnings.append("More information at: https://wiki.python.org/moin/WindowsCompilers")
+        build_extensions = False
+    else:
+        print(f"Found cl.exe: {cl}")
+        print(f"Found link.exe: {link}")
 
 def build_all():
     packages=[
@@ -226,29 +268,41 @@ def build_all():
 
     if is_win:
         # Force setuptools to use whatever msvc version installed
-        os.environ['MSSdk'] = '1'
-        os.environ['DISTUTILS_USE_SDK'] = '1'
+        # https://docs.python.org/3/distutils/apiref.html#module-distutils.msvccompiler
+        os.environ["MSSdk"] = "1"
+        os.environ["DISTUTILS_USE_SDK"] = "1"
+        extra_compile_args = ["-D_CRT_SECURE_NO_WARNINGS"]
         if win_force_clang:
             march = "-m64" if is_64bit else "-m32"
-            for extension in ext_modules_all:
-                extension.extra_compile_args = [march]
+            extra_compile_args += [
+                march,
+                "-Wno-unused-command-line-argument",
+                "-Wno-visibility",
+                "-Wno-dll-attribute-on-redeclaration",
+                "-Wno-tautological-compare",
+            ]
+        for extension in ext_modules_all:
+            extension.extra_compile_args = extra_compile_args
     elif is_mac:
         for extension in ext_modules_all:
             set_extension_compile_args(extension)
         cfg_vars = get_config_vars()
-        cfg_vars['LDSHARED'] = cfg_vars['LDSHARED'].replace('-bundle', '-dynamiclib')
+        cfg_vars["LDSHARED"] = cfg_vars["LDSHARED"].replace("-bundle", "-dynamiclib")
+
+    # Do not attempt to build the extensions when disabled
+    if not build_extensions:
+        ext_modules_all = []
 
     print("building")
     build_ok = False
-    for name, ext_modules in [("all", ext_modules_all),
-    ]:
+    for name, ext_modules in [("all", ext_modules_all)]:
         print("build with", repr(name))
         try:
             s = setup(
                 name = "miasm",
                 version = __import__("miasm").VERSION,
                 packages = packages,
-                data_files=[('', ["README.md"])],
+                data_files=[("", ["README.md"])],
                 package_data = {
                     "miasm": [
                         "jitter/*.h",
@@ -256,7 +310,7 @@ def build_all():
                         "VERSION"
                     ]
                 },
-                install_requires=['future', 'pyparsing~=2.0'],
+                install_requires=["future", "pyparsing~=2.0"],
                 cmdclass={"install_data": smart_install_data},
                 ext_modules = ext_modules,
                 # Metadata
@@ -288,6 +342,10 @@ def build_all():
         build_ok = True
         break
     if not build_ok:
+        if len(build_warnings) > 0:
+            print(f"ERROR: There was an issue setting up the build environment:")
+            for warning in build_warnings:
+                print("  " + warning)
         raise ValueError("Unable to build Miasm!")
     print("build", name)
     # we copy libraries from build dir to current miasm directory
@@ -297,7 +355,7 @@ def build_all():
             build_base = s.command_options["build"]["build_base"]
 
     print(build_base)
-    if is_win:
+    if is_win and build_extensions:
         libs = []
         for root, _, files in os.walk(build_base):
             for filename in files:
@@ -325,11 +383,16 @@ def build_all():
                 print("Copying", lib, "to", dst)
                 copy2(lib, dst)
 
+    # Inform the user about the skipped build
+    if not build_extensions:
+        print(f"WARNING: miasm jit extensions were not compiled, details:")
+        for warning in build_warnings:
+            print("  " + warning)
 
-with io.open(os.path.join(os.path.abspath(os.path.dirname('__file__')),
-                       'README.md'), encoding='utf-8') as fdesc:
+with io.open(os.path.join(os.path.abspath(os.path.dirname("__file__")),
+                       "README.md"), encoding="utf-8") as fdesc:
     long_description = fdesc.read()
-long_description_content_type = 'text/markdown'
+long_description_content_type = "text/markdown"
 
 
 # Monkey patching (distutils does not handle Description-Content-Type
@@ -342,10 +405,10 @@ def _write_pkg_file(self, file):
         _write_pkg_file_orig(self, tmpfd)
         tmpfd.seek(0)
         for line in tmpfd:
-            if line.startswith('Metadata-Version: '):
-                file.write('Metadata-Version: 2.1\n')
-            elif line.startswith('Description: '):
-                file.write('Description-Content-Type: %s; charset=UTF-8\n' %
+            if line.startswith("Metadata-Version: "):
+                file.write("Metadata-Version: 2.1\n")
+            elif line.startswith("Description: "):
+                file.write("Description-Content-Type: %s; charset=UTF-8\n" %
                            long_description_content_type)
                 file.write(line)
             else: