Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions news/setup.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
**Added:**

* Added additional runtime linker flags in `CustomBuildExt.run` to embed the `RPATH` flags for the built extensions.
* Support for retrieving GSL configuration from `CONDA_PREFIX`/ `GSL_PATH` on all platforms.

**Changed:**

* Merged the GSL configuration logic in `setup.py`.

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
137 changes: 91 additions & 46 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@


def get_compiler_type():
"""Find compiler used for building extensions."""
"""Return the compiler type used during the build."""
cc_arg = [a for a in sys.argv if a.startswith("--compiler=")]
if cc_arg:
return cc_arg[-1].split("=", 1)[1]
Expand All @@ -38,48 +38,97 @@ def get_compiler_type():


def get_gsl_config():
"""Return dictionary with paths to GSL library."""
gslcfgpaths = [Path(p) / "gsl-config" for p in ([MYDIR] + os.environ["PATH"].split(os.pathsep))]
gslcfgpaths = [p for p in gslcfgpaths if p.is_file()]
"""
Determine the GSL include and library directories by trying in order:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is much much clearer, thanks for doing this @Tieqiong

1) CONDA_PREFIX,
2) GSL_PATH,
3) gsl-config (for Unix-like systems).
Raises EnvironmentError if none are found.
"""
rv = {"include_dirs": [], "library_dirs": []}
if not gslcfgpaths:
warnings.warn(f"Cannot find gsl-config in {MYDIR} nor in system PATH.")
return rv
gslcfg = gslcfgpaths[0]
txt = gslcfg.read_text()
mprefix = re.search(r"(?m)^prefix=(.+)", txt)
minclude = re.search(r"(?m)^[^#]*\s-I(\S+)", txt)
mlibpath = re.search(r"(?m)^[^#]*\s-L(\S+)", txt)
if not mprefix:
raise RuntimeError(f"Cannot find 'prefix=' line in {gslcfg}.")
p = Path(mprefix.group(1))
rv["include_dirs"].append(str(minclude.group(1) if minclude else p / "include"))
rv["library_dirs"].append(str(mlibpath.group(1) if mlibpath else p / "lib"))
return rv


def get_gsl_config_win():
"""Return dictionary with paths to GSL library on Windows."""

# 1. Check using CONDA_PREFIX.
conda_prefix = os.environ.get("CONDA_PREFIX", "")
if conda_prefix:
if os.name == "nt":
inc = Path(conda_prefix) / "Library" / "include"
lib = Path(conda_prefix) / "Library" / "lib"
else:
inc = Path(conda_prefix) / "include"
lib = Path(conda_prefix) / "lib"
if inc.is_dir() and lib.is_dir():
rv["include_dirs"].append(str(inc))
rv["library_dirs"].append(str(lib))
return rv
else:
warnings.warn(
f"CONDA_PREFIX is set to {conda_prefix}, " "but GSL not found at those paths. Proceeding..."
)

# 2. Check using GSL_PATH.
gsl_path = os.environ.get("GSL_PATH", "")
if gsl_path:
inc = Path(gsl_path) / "include"
lib = Path(gsl_path) / "lib"
else:
conda_prefix = os.environ.get("CONDA_PREFIX")
if conda_prefix:
inc = Path(conda_prefix) / "Library" / "include"
lib = Path(conda_prefix) / "Library" / "lib"
if inc.is_dir() and lib.is_dir():
rv["include_dirs"].append(str(inc))
rv["library_dirs"].append(str(lib))
return rv
else:
raise EnvironmentError(
"Neither GSL_PATH nor CONDA_PREFIX environment variables are set. "
"Please ensure GSL is installed and GSL_PATH is correctly set."
f"GSL_PATH={gsl_path} is set, but {inc} or {lib} not found. " "Please verify your GSL_PATH."
)
return {"include_dirs": [str(inc)], "library_dirs": [str(lib)]}

# 3. Try using the gsl-config executable (only on Unix-like systems).
if os.name != "nt":
path_dirs = os.environ.get("PATH", "").split(os.pathsep)
gslcfg_paths = [Path(p) / "gsl-config" for p in path_dirs if p]
gslcfg_paths = [p for p in gslcfg_paths if p.is_file()]
if gslcfg_paths:
gslcfg = gslcfg_paths[0]
txt = gslcfg.read_text()
prefix_match = re.search(r"(?m)^prefix=(.+)", txt)
include_match = re.search(r"(?m)^[^#]*\s-I(\S+)", txt)
lib_match = re.search(r"(?m)^[^#]*\s-L(\S+)", txt)
if prefix_match:
prefix_path = Path(prefix_match.group(1))
inc_dir = include_match.group(1) if include_match else (prefix_path / "include")
lib_dir = lib_match.group(1) if lib_match else (prefix_path / "lib")
rv["include_dirs"].append(str(inc_dir))
rv["library_dirs"].append(str(lib_dir))
return rv
else:
raise RuntimeError(f"Cannot parse 'prefix=' from {gslcfg}.")
else:
warnings.warn(
"No gsl-config found in PATH. GSL may not be installed or not in PATH. "
"Proceeding without GSL configuration."
)

# 4. Nothing found: raise error.
raise EnvironmentError(
"Unable to locate GSL:\n"
"1) CONDA_PREFIX not set or no GSL there\n"
"2) GSL_PATH not set or invalid\n"
"3) gsl-config not available\n"
"Please set GSL_PATH or use a conda environment with GSL."
)


class CustomBuildExt(build_ext):
def run(self):
# Retrieve the GSL library directories and append them to each extension.
gsl_cfg = get_gsl_config()
lib_dirs = gsl_cfg.get("library_dirs", [])
for ext in self.extensions:
# Add gsl lib for linking.
ext.library_dirs.extend(lib_dirs)
# Embed RPATH flags, runtime linking without LD_LIBRARY_PATH.
ext.extra_link_args = ext.extra_link_args or []
for lib in lib_dirs:
ext.extra_link_args.append(f"-Wl,-rpath,{lib}")
super().run()
# Avoid dll error
gsl_path = (
Path(os.environ.get("GSL_PATH"))
if os.environ.get("GSL_PATH")
Expand All @@ -88,46 +137,44 @@ def run(self):
bin_path = gsl_path / "bin"
dest_path = Path(self.build_lib) / "diffpy" / "pdffit2"
dest_path.mkdir(parents=True, exist_ok=True)

for dll_file in bin_path.glob("gsl*.dll"):
shutil.copy(str(dll_file), str(dest_path))


# ----------------------------------------------------------------------------
# Compile and link options----------------------------------------------------

# Compile and link options
os_name = os.name
if os_name == "nt":
gcfg = get_gsl_config_win()
else:
gcfg = get_gsl_config()
gcfg = get_gsl_config()

# On macOS, dynamic linking may not be needed
if sys.platform == "darwin":
libraries = []
else:
libraries = ["gsl"]

include_dirs = [MYDIR] + gcfg["include_dirs"]
library_dirs = []
library_dirs = gcfg["library_dirs"]
define_macros = []
extra_objects = []
extra_compile_args = []
extra_link_args = []


compiler_type = get_compiler_type()
if compiler_type in ("unix", "cygwin", "mingw32"):
extra_compile_args = ["-std=c++11", "-Wall", "-Wno-write-strings", "-O3", "-funroll-loops", "-ffast-math"]
extra_objects += [
# Check for static GSL libraries and add them if found.
static_libs = [
os.path.join(p, "libgsl.a") for p in gcfg["library_dirs"] if os.path.isfile(os.path.join(p, "libgsl.a"))
]
if static_libs:
extra_objects += static_libs
# Use static linking: remove "-lgsl" to avoid dynamic linking conflicts.
libraries = []
elif compiler_type == "msvc":
define_macros += [("_USE_MATH_DEFINES", None)]
extra_compile_args = ["/EHs"]
library_dirs += gcfg["library_dirs"]
# add optimization flags for other compilers if needed

# Define extension arguments
# Extension keyword arguments.
ext_kws = {
"include_dirs": include_dirs,
"libraries": libraries,
Expand All @@ -139,8 +186,8 @@ def run(self):
}


# Define extensions
def create_extensions():
"""Create the list of Extension objects for the build."""
ext = Extension("diffpy.pdffit2.pdffit2", glob.glob("src/extensions/**/*.cc"), **ext_kws)
return [ext]

Expand All @@ -153,5 +200,3 @@ def create_extensions():
if __name__ == "__main__":
setup_args["ext_modules"] = create_extensions()
setup(**setup_args)

# End of file
Loading