1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
|
from setuptools import setup, find_packages
from setuptools.extension import Extension
from os import path, environ
import sys
import sysconfig
from pathlib import Path
# needed for isolated environment
sys.path.insert(0, str(Path(__file__).parent.resolve()))
from scalene.scalene_config import scalene_version
sys.path.pop(0)
if sys.platform == "darwin":
import sysconfig
mdt = "MACOSX_DEPLOYMENT_TARGET"
target = environ[mdt] if mdt in environ else sysconfig.get_config_var(mdt)
# target >= 10.9 is required for gcc/clang to find libstdc++ headers
if [int(n) for n in target.split(".")] < [10, 9]:
from os import execve
newenv = environ.copy()
newenv[mdt] = "10.9"
execve(sys.executable, [sys.executable] + sys.argv, newenv)
def compiler_archs(compiler: str):
"""Discovers what platforms the given compiler supports; intended for MacOS use"""
import tempfile
import subprocess
print(f"Compiler: {compiler}")
arch_flags = []
# see also the architectures tested for in .github/workflows/build-and-upload.yml
for arch in ["x86_64", "arm64", "arm64e"]:
with tempfile.TemporaryDirectory() as tmpdir:
cpp = Path(tmpdir) / "test.cxx"
cpp.write_text("int main() {return 0;}\n")
out = Path(tmpdir) / "a.out"
p = subprocess.run(
[compiler, "-arch", arch, str(cpp), "-o", str(out)], capture_output=True
)
if p.returncode == 0:
arch_flags += ["-arch", arch]
print(f"Discovered {compiler} arch flags: {arch_flags}")
return arch_flags
def extra_compile_args():
"""Returns extra compiler args for platform."""
if sys.platform == "win32":
return ["/std:c++14"] # for Visual Studio C++
return ["-std=c++14"]
def make_command():
# return 'nmake' if sys.platform == 'win32' else 'make' # 'nmake' isn't found on github actions' VM
return "make"
def dll_suffix():
"""Returns the file suffix ("extension") of a DLL"""
if sys.platform == "win32":
return ".dll"
if sys.platform == "darwin":
return ".dylib"
return ".so"
def read_file(name):
"""Returns a file's contents"""
with open(path.join(path.dirname(__file__), name), encoding="utf-8") as f:
return f.read()
import setuptools.command.egg_info
class EggInfoCommand(setuptools.command.egg_info.egg_info):
"""Custom command to download vendor libs before creating the egg_info."""
def run(self):
if sys.platform != "win32":
# self.spawn([make_command(), "vendor-deps"])
pass
super().run()
# Force building platform-specific wheel to avoid the Windows wheel
# (which doesn't include libscalene, and thus would be considered "pure")
# being used for other platforms.
try:
from wheel.bdist_wheel import bdist_wheel
class BdistWheelCommand(bdist_wheel):
def finalize_options(self):
super().finalize_options()
self.root_is_pure = False
except (ImportError, ModuleNotFoundError):
# Disable wheel if `wheel` not installed.
print(
"If this installation does not work, run `pip install setuptools wheel` and try again."
)
BdistWheelCommand = None
import setuptools.command.build_ext
class BuildExtCommand(setuptools.command.build_ext.build_ext):
"""Custom command that runs 'make' to generate libscalene, and also does MacOS
supported --arch flag discovery."""
def build_extensions(self):
arch_flags = []
if sys.platform == "darwin":
# The only sure way to tell which compiler build_ext is going to use
# seems to be to customize a build_ext and look at its internal flags :(
# Also, note that self.plat_name here isn't "...-universal2" even if that
# is what we're building; that's only in bdist_wheel.plat_name.
arch_flags += compiler_archs(self.compiler.compiler_cxx[0])
for ext in self.extensions:
# While the flags _could_ be different between the programs used for
# C and C++ compilation and linking, we have no way to adapt them here,
# so it seems best to just use them and let it error out if not recognized.
ext.extra_compile_args += arch_flags
ext.extra_link_args += arch_flags
super().build_extensions()
# No build of DLL for Windows currently.
if sys.platform != "win32":
self.build_libscalene(arch_flags) # XXX should we pass compiler_cxx here?
def build_libscalene(self, arch_flags):
scalene_temp = path.join(self.build_temp, "scalene")
scalene_lib = path.join(self.build_lib, "scalene")
libscalene = "libscalene" + dll_suffix()
self.mkpath(scalene_temp)
self.mkpath(scalene_lib)
self.spawn(
[make_command(), "OUTDIR=" + scalene_temp, "ARCH=" + " ".join(arch_flags)]
)
self.copy_file(
path.join(scalene_temp, libscalene), path.join(scalene_lib, libscalene)
)
def copy_extensions_to_source(self):
# self.inplace is temporarily overridden while running build_extensions,
# so inplace copying (for pip install -e, setup.py develop) must be done here.
super().copy_extensions_to_source()
if sys.platform != "win32":
scalene_lib = path.join(self.build_lib, "scalene")
inplace_dir = self.get_finalized_command("build_py").get_package_dir(
"scalene"
)
libscalene = "libscalene" + dll_suffix()
self.copy_file(
path.join(scalene_lib, libscalene), path.join(inplace_dir, libscalene)
)
get_line_atomic = Extension(
"scalene.get_line_atomic",
include_dirs=[".", "vendor/Heap-Layers", "vendor/Heap-Layers/utility"],
sources=["src/source/get_line_atomic.cpp"],
extra_compile_args=extra_compile_args(),
py_limited_api=True, # for binary compatibility
language="c++",
)
pywhere = Extension(
"scalene.pywhere",
include_dirs=[".", "src", "src/include"],
depends=["src/include/traceconfig.hpp"],
sources=["src/source/pywhere.cpp", "src/source/traceconfig.cpp"],
extra_compile_args=extra_compile_args(),
py_limited_api=False,
language="c++",
)
# If we're testing packaging, build using a ".devN" suffix in the version number,
# so that we can upload new files (as testpypi/pypi don't allow re-uploading files with
# the same name as previously uploaded).
# Numbering scheme: https://www.python.org/dev/peps/pep-0440
dev_build = (".dev" + environ["DEV_BUILD"]) if "DEV_BUILD" in environ else ""
def bdist_wheel_options():
if sys.platform == "darwin":
# Build universal wheels on MacOS.
# ---
# On MacOS >= 11, all builds are compatible within a major MacOS version, so Python "floors"
# all minor versions to 0, leading to tags like like "macosx_11_0_universal2". If you use
# the actual (non-0) minor name in the build platform, it isn't recognized.
# ---
# It would be nice to check whether we're actually building multi-architecture,
# but that depends on the platforms supported by the compiler build_ext wants to use,
# which is hard to obtain (see BuildExtCommand above).
import platform
v = platform.mac_ver()[0]
major = int(v.split(".")[0])
if major >= 11:
v = f"{major}.0"
return {"plat_name": f"macosx-{v}-universal2"}
return {}
setup(
version=scalene_version + dev_build,
packages=find_packages(),
cmdclass={
"bdist_wheel": BdistWheelCommand,
"egg_info": EggInfoCommand,
"build_ext": BuildExtCommand,
},
ext_modules=([get_line_atomic, pywhere] if sys.platform != "win32" else []),
include_package_data=True,
options={"bdist_wheel": bdist_wheel_options()},
)
|