diff --git a/setup.py b/setup.py index 507dd32d0..5a47457f7 100644 --- a/setup.py +++ b/setup.py @@ -8,15 +8,15 @@ import argparse import glob -import io as cStringIO import os -import pathlib import re import shutil import sys import sysconfig import time from collections import defaultdict +from itertools import chain +from pathlib import Path from subprocess import PIPE, Popen import numpy @@ -26,7 +26,7 @@ from setuptools.command.install import install # non-empty DEBUG variable turns off optimization and adds -g flag -DEBUG = bool(os.getenv("DEBUG", "")) +DEBUG = os.getenv("DEBUG", False) WIN = sys.platform.startswith("win") MAC = sys.platform.startswith("darwin") @@ -34,89 +34,55 @@ # Have to copy from "create_shadertext.py" script due to the use of pyproject.toml # Full explanation: # https://github.com/pypa/setuptools/issues/3939 -def create_all(generated_dir, pymoldir="."): +def create_all(generated_dir: str, pymol_dir: str = "."): """ Generate various stuff """ - create_shadertext( - os.path.join(pymoldir, "data", "shaders"), - generated_dir, - os.path.join(generated_dir, "ShaderText.h"), - os.path.join(generated_dir, "ShaderText.cpp"), - ) - create_buildinfo(generated_dir, pymoldir) - - -class openw(object): - """ - File-like object for writing files. File is actually only - written if the content changed. - """ - - def __init__(self, filename): - if os.path.exists(filename): - self.out = cStringIO.StringIO() - self.filename = filename - else: - os.makedirs(os.path.dirname(filename), exist_ok=True) - self.out = open(filename, "w") - self.filename = None + generated_dir_path = Path(generated_dir) + pymol_dir_path = Path(pymol_dir) - def close(self): - if self.out.closed: - return - if self.filename: - with open(self.filename) as handle: - oldcontents = handle.read() - newcontents = self.out.getvalue() - if oldcontents != newcontents: - self.out = open(self.filename, "w") - self.out.write(newcontents) - self.out.close() - - def __getattr__(self, name): - return getattr(self.out, name) - - def __enter__(self): - return self - - def __exit__(self, *a, **k): - self.close() - - def __del__(self): - self.close() + generated_dir_path.mkdir(parents=True, exist_ok=True) + pymol_dir_path.mkdir(parents=True, exist_ok=True) + create_shadertext( + shader_dir=pymol_dir_path / "data" / "shaders", + shader_dir2=pymol_dir_path, + output_header_file=pymol_dir_path / "ShaderText.h", + output_source_file=generated_dir_path / "ShaderText.cpp", + ) + create_buildinfo(generated_dir, pymol_dir) -def create_shadertext(shaderdir, shaderdir2, outputheader, outputfile): - outputheader = openw(outputheader) - outputfile = openw(outputfile) +def create_shadertext( + shader_dir: Path, + shader_dir2: Path, + output_header_file: Path, + output_source_file: Path, +): + varname = "_shader_cache_raw" include_deps = defaultdict(set) ifdef_deps = defaultdict(set) + extension_regexp = "*.[gs][vs][fs][shared][tsc][tse]" # get all *.gs *.vs *.fs *.shared from the two input directories - shaderfiles = set() - for sdir in [shaderdir, shaderdir2]: - for ext in ["gs", "vs", "fs", "shared", "tsc", "tse"]: - shaderfiles.update( - map(os.path.basename, sorted(glob.glob(os.path.join(sdir, "*." + ext)))) - ) - - varname = "_shader_cache_raw" - outputheader.write("extern const char * %s[];\n" % varname) - outputfile.write("const char * %s[] = {\n" % varname) - - for filename in sorted(shaderfiles): - shaderfile = os.path.join(shaderdir, filename) - if not os.path.exists(shaderfile): - shaderfile = os.path.join(shaderdir2, filename) + shaderfiles = set( + chain( + shader_dir.glob(extension_regexp), + shader_dir2.glob(extension_regexp), + ) + ) - with open(shaderfile, "r") as handle: - contents = handle.read() + with ( + open(output_header_file, "w") as output_header_descriptor, + open(output_source_file, "w") as output_source_descriptor, + ): + output_header_descriptor.write(f"extern const char * {varname}[];\n") + output_source_descriptor.write(f"const char * {varname}[] = {{\n") - if True: - outputfile.write('"%s", ""\n' % (filename)) + for filename in shaderfiles: + output_source_descriptor.write(f'"{filename.name}", ""\n') + contents = filename.read_text() for line in contents.splitlines(): line = line.strip() @@ -125,9 +91,8 @@ def create_shadertext(shaderdir, shaderdir2, outputheader, outputfile): continue # write line, quoted, escaped and with a line feed - outputfile.write( - '"%s\\n"\n' % line.replace("\\", "\\\\").replace('"', r"\"") - ) + escaped_line = line.replace("\\", "\\\\").replace('"', r"\"") + output_source_descriptor.write(f'"{escaped_line}\\n"\n') # include and ifdef dependencies if line.startswith("#include"): @@ -135,42 +100,34 @@ def create_shadertext(shaderdir, shaderdir2, outputheader, outputfile): elif line.startswith("#ifdef") or line.startswith("#ifndef"): ifdef_deps[line.split()[1]].add(filename) - outputfile.write(",\n") - - outputfile.write("0};\n") - - # include and ifdef dependencies - for varname, deps in [("_include_deps", include_deps), ("_ifdef_deps", ifdef_deps)]: - outputheader.write("extern const char * %s[];\n" % varname) - outputfile.write("const char * %s[] = {\n" % varname) - for name, itemdeps in deps.items(): - outputfile.write('"%s", "%s", 0,\n' % (name, '", "'.join(sorted(itemdeps)))) - outputfile.write("0};\n") + output_source_descriptor.write(",\n") + output_source_descriptor.write("0};\n") - outputheader.close() - outputfile.close() - - -def create_buildinfo(outputdir, pymoldir="."): - try: - sha = ( - Popen(["git", "rev-parse", "HEAD"], cwd=pymoldir, stdout=PIPE) - .stdout.read() - .strip() - .decode() - ) - except OSError: - sha = "" - - with openw(os.path.join(outputdir, "PyMOLBuildInfo.h")) as out: - print( - """ -#define _PyMOL_BUILD_DATE %d -#define _PYMOL_BUILD_GIT_SHA "%s" - """ - % (time.time(), sha), - file=out, - ) + # include and ifdef dependencies + for varname, deps in [ + ("_include_deps", include_deps), + ("_ifdef_deps", ifdef_deps), + ]: + output_header_descriptor.write(f"extern const char * {varname}[];\n") + output_source_descriptor.write(f"const char * {varname}[] = {{\n") + for name, item_deps in deps.items(): + item_deps = '", "'.join(sorted(item_deps)) + output_source_descriptor.write(f'"{name}", "{item_deps}", 0,\n') + output_source_descriptor.write("0};\n") + + +def create_buildinfo(output_dir_path: str, pymoldir: str = "."): + output_dir = Path(output_dir_path) + sha_raw = Popen(["git", "rev-parse", "HEAD"], cwd=pymoldir, stdout=PIPE).stdout + sha = sha_raw.read().strip().decode() if sha_raw is not None else "" + + info_file = output_dir / "PyMOLBuildInfo.h" + info_file.write_text( + f""" + #define _PyMOL_BUILD_DATE {time.time()} + #define _PYMOL_BUILD_GIT_SHA "{sha}" + """ + ) # handle extra arguments @@ -302,7 +259,6 @@ def guess_msgpackc(): class CMakeExtension(Extension): - def __init__( self, name, @@ -336,15 +292,15 @@ def run(self): self.build_cmake(ext) def build_cmake(self, ext): - cwd = pathlib.Path().absolute() + cwd = Path().absolute() # these dirs will be created in build_py, so if you don't have # any python sources to bundle, the dirs will be missing name_split = ext.name.split(".") target_name = name_split[-1] - build_temp = pathlib.Path(self.build_temp) / target_name + build_temp = Path(self.build_temp) / target_name build_temp.mkdir(parents=True, exist_ok=True) - extdir = pathlib.Path(self.get_ext_fullpath(ext.name)) + extdir = Path(self.get_ext_fullpath(ext.name)) extdirabs = extdir.absolute() extdir.parent.mkdir(parents=True, exist_ok=True) @@ -394,7 +350,7 @@ def concat_paths(paths): if WIN: # Move up from VS release folder - cmake_lib_loc = pathlib.Path( + cmake_lib_loc = Path( lib_output_dir, "Release", f"{target_name}{shared_suffix}" ) if cmake_lib_loc.exists(): @@ -487,7 +443,7 @@ def make_launch_script(self): launch_script = os.path.join(self.install_scripts, launch_script) python_exe = os.path.abspath(sys.executable) - site_packages_dir = sysconfig.get_path('purelib') + site_packages_dir = sysconfig.get_path("purelib") pymol_file = self.unchroot( os.path.join(site_packages_dir, "pymol", "__init__.py") ) @@ -701,8 +657,6 @@ def make_launch_script(self): libs += [ "opengl32", ] - # TODO: Remove when we move to setup-CMake - ext_comp_args += ["/std:c++17"] if not (MAC or WIN): libs += [ @@ -816,7 +770,7 @@ def get_packages(base, parent="", r=None): if WIN: # pyconfig.py forces linking against pythonXY.lib on MSVC - py_lib = pathlib.Path(sysconfig.get_paths()["stdlib"]).parent / "libs" + py_lib = Path(sysconfig.get_paths()["stdlib"]).parent / "libs" lib_dirs.append(str(py_lib)) ext_modules += [