Skip to content

Commit 889ee05

Browse files
authored
Merge pull request #121 from Tieqiong/setup
perf: improve gsl linking
2 parents fdc2e16 + 4c4d557 commit 889ee05

File tree

2 files changed

+115
-46
lines changed

2 files changed

+115
-46
lines changed

news/setup.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
**Added:**
2+
3+
* Added additional runtime linker flags in `CustomBuildExt.run` to embed the `RPATH` flags for the built extensions.
4+
* Support for retrieving GSL configuration from `CONDA_PREFIX`/ `GSL_PATH` on all platforms.
5+
6+
**Changed:**
7+
8+
* Merged the GSL configuration logic in `setup.py`.
9+
10+
**Deprecated:**
11+
12+
* <news item>
13+
14+
**Removed:**
15+
16+
* <news item>
17+
18+
**Fixed:**
19+
20+
* <news item>
21+
22+
**Security:**
23+
24+
* <news item>

setup.py

Lines changed: 91 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929

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

3939

4040
def get_gsl_config():
41-
"""Return dictionary with paths to GSL library."""
42-
gslcfgpaths = [Path(p) / "gsl-config" for p in ([MYDIR] + os.environ["PATH"].split(os.pathsep))]
43-
gslcfgpaths = [p for p in gslcfgpaths if p.is_file()]
41+
"""
42+
Determine the GSL include and library directories by trying in order:
43+
1) CONDA_PREFIX,
44+
2) GSL_PATH,
45+
3) gsl-config (for Unix-like systems).
46+
Raises EnvironmentError if none are found.
47+
"""
4448
rv = {"include_dirs": [], "library_dirs": []}
45-
if not gslcfgpaths:
46-
warnings.warn(f"Cannot find gsl-config in {MYDIR} nor in system PATH.")
47-
return rv
48-
gslcfg = gslcfgpaths[0]
49-
txt = gslcfg.read_text()
50-
mprefix = re.search(r"(?m)^prefix=(.+)", txt)
51-
minclude = re.search(r"(?m)^[^#]*\s-I(\S+)", txt)
52-
mlibpath = re.search(r"(?m)^[^#]*\s-L(\S+)", txt)
53-
if not mprefix:
54-
raise RuntimeError(f"Cannot find 'prefix=' line in {gslcfg}.")
55-
p = Path(mprefix.group(1))
56-
rv["include_dirs"].append(str(minclude.group(1) if minclude else p / "include"))
57-
rv["library_dirs"].append(str(mlibpath.group(1) if mlibpath else p / "lib"))
58-
return rv
59-
60-
61-
def get_gsl_config_win():
62-
"""Return dictionary with paths to GSL library on Windows."""
49+
50+
# 1. Check using CONDA_PREFIX.
51+
conda_prefix = os.environ.get("CONDA_PREFIX", "")
52+
if conda_prefix:
53+
if os.name == "nt":
54+
inc = Path(conda_prefix) / "Library" / "include"
55+
lib = Path(conda_prefix) / "Library" / "lib"
56+
else:
57+
inc = Path(conda_prefix) / "include"
58+
lib = Path(conda_prefix) / "lib"
59+
if inc.is_dir() and lib.is_dir():
60+
rv["include_dirs"].append(str(inc))
61+
rv["library_dirs"].append(str(lib))
62+
return rv
63+
else:
64+
warnings.warn(
65+
f"CONDA_PREFIX is set to {conda_prefix}, " "but GSL not found at those paths. Proceeding..."
66+
)
67+
68+
# 2. Check using GSL_PATH.
6369
gsl_path = os.environ.get("GSL_PATH", "")
6470
if gsl_path:
6571
inc = Path(gsl_path) / "include"
6672
lib = Path(gsl_path) / "lib"
67-
else:
68-
conda_prefix = os.environ.get("CONDA_PREFIX")
69-
if conda_prefix:
70-
inc = Path(conda_prefix) / "Library" / "include"
71-
lib = Path(conda_prefix) / "Library" / "lib"
73+
if inc.is_dir() and lib.is_dir():
74+
rv["include_dirs"].append(str(inc))
75+
rv["library_dirs"].append(str(lib))
76+
return rv
7277
else:
7378
raise EnvironmentError(
74-
"Neither GSL_PATH nor CONDA_PREFIX environment variables are set. "
75-
"Please ensure GSL is installed and GSL_PATH is correctly set."
79+
f"GSL_PATH={gsl_path} is set, but {inc} or {lib} not found. " "Please verify your GSL_PATH."
7680
)
77-
return {"include_dirs": [str(inc)], "library_dirs": [str(lib)]}
81+
82+
# 3. Try using the gsl-config executable (only on Unix-like systems).
83+
if os.name != "nt":
84+
path_dirs = os.environ.get("PATH", "").split(os.pathsep)
85+
gslcfg_paths = [Path(p) / "gsl-config" for p in path_dirs if p]
86+
gslcfg_paths = [p for p in gslcfg_paths if p.is_file()]
87+
if gslcfg_paths:
88+
gslcfg = gslcfg_paths[0]
89+
txt = gslcfg.read_text()
90+
prefix_match = re.search(r"(?m)^prefix=(.+)", txt)
91+
include_match = re.search(r"(?m)^[^#]*\s-I(\S+)", txt)
92+
lib_match = re.search(r"(?m)^[^#]*\s-L(\S+)", txt)
93+
if prefix_match:
94+
prefix_path = Path(prefix_match.group(1))
95+
inc_dir = include_match.group(1) if include_match else (prefix_path / "include")
96+
lib_dir = lib_match.group(1) if lib_match else (prefix_path / "lib")
97+
rv["include_dirs"].append(str(inc_dir))
98+
rv["library_dirs"].append(str(lib_dir))
99+
return rv
100+
else:
101+
raise RuntimeError(f"Cannot parse 'prefix=' from {gslcfg}.")
102+
else:
103+
warnings.warn(
104+
"No gsl-config found in PATH. GSL may not be installed or not in PATH. "
105+
"Proceeding without GSL configuration."
106+
)
107+
108+
# 4. Nothing found: raise error.
109+
raise EnvironmentError(
110+
"Unable to locate GSL:\n"
111+
"1) CONDA_PREFIX not set or no GSL there\n"
112+
"2) GSL_PATH not set or invalid\n"
113+
"3) gsl-config not available\n"
114+
"Please set GSL_PATH or use a conda environment with GSL."
115+
)
78116

79117

80118
class CustomBuildExt(build_ext):
81119
def run(self):
120+
# Retrieve the GSL library directories and append them to each extension.
121+
gsl_cfg = get_gsl_config()
122+
lib_dirs = gsl_cfg.get("library_dirs", [])
123+
for ext in self.extensions:
124+
# Add gsl lib for linking.
125+
ext.library_dirs.extend(lib_dirs)
126+
# Embed RPATH flags, runtime linking without LD_LIBRARY_PATH.
127+
ext.extra_link_args = ext.extra_link_args or []
128+
for lib in lib_dirs:
129+
ext.extra_link_args.append(f"-Wl,-rpath,{lib}")
82130
super().run()
131+
# Avoid dll error
83132
gsl_path = (
84133
Path(os.environ.get("GSL_PATH"))
85134
if os.environ.get("GSL_PATH")
@@ -88,46 +137,44 @@ def run(self):
88137
bin_path = gsl_path / "bin"
89138
dest_path = Path(self.build_lib) / "diffpy" / "pdffit2"
90139
dest_path.mkdir(parents=True, exist_ok=True)
91-
92140
for dll_file in bin_path.glob("gsl*.dll"):
93141
shutil.copy(str(dll_file), str(dest_path))
94142

95143

96-
# ----------------------------------------------------------------------------
144+
# Compile and link options----------------------------------------------------
97145

98-
# Compile and link options
99146
os_name = os.name
100-
if os_name == "nt":
101-
gcfg = get_gsl_config_win()
102-
else:
103-
gcfg = get_gsl_config()
147+
gcfg = get_gsl_config()
104148

149+
# On macOS, dynamic linking may not be needed
105150
if sys.platform == "darwin":
106151
libraries = []
107152
else:
108153
libraries = ["gsl"]
109154

110155
include_dirs = [MYDIR] + gcfg["include_dirs"]
111-
library_dirs = []
156+
library_dirs = gcfg["library_dirs"]
112157
define_macros = []
113158
extra_objects = []
114159
extra_compile_args = []
115160
extra_link_args = []
116161

117-
118162
compiler_type = get_compiler_type()
119163
if compiler_type in ("unix", "cygwin", "mingw32"):
120164
extra_compile_args = ["-std=c++11", "-Wall", "-Wno-write-strings", "-O3", "-funroll-loops", "-ffast-math"]
121-
extra_objects += [
165+
# Check for static GSL libraries and add them if found.
166+
static_libs = [
122167
os.path.join(p, "libgsl.a") for p in gcfg["library_dirs"] if os.path.isfile(os.path.join(p, "libgsl.a"))
123168
]
169+
if static_libs:
170+
extra_objects += static_libs
171+
# Use static linking: remove "-lgsl" to avoid dynamic linking conflicts.
172+
libraries = []
124173
elif compiler_type == "msvc":
125174
define_macros += [("_USE_MATH_DEFINES", None)]
126175
extra_compile_args = ["/EHs"]
127-
library_dirs += gcfg["library_dirs"]
128-
# add optimization flags for other compilers if needed
129176

130-
# Define extension arguments
177+
# Extension keyword arguments.
131178
ext_kws = {
132179
"include_dirs": include_dirs,
133180
"libraries": libraries,
@@ -139,8 +186,8 @@ def run(self):
139186
}
140187

141188

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

@@ -153,5 +200,3 @@ def create_extensions():
153200
if __name__ == "__main__":
154201
setup_args["ext_modules"] = create_extensions()
155202
setup(**setup_args)
156-
157-
# End of file

0 commit comments

Comments
 (0)