11#! /usr/bin/env python3
22
33import os
4+ import shlex
45import subprocess
56import sys
67
78from pybind11 .setup_helpers import Pybind11Extension , build_ext
89from setuptools import find_packages , setup
910
1011
11- def check_output ( args ) :
12- output = subprocess . check_output ( args ). decode ()
13- return output . rstrip ( " \n " )
12+ class MapnikBuildConfig :
13+ """
14+ Centralizes build configuration for the Mapnik pybind11 extension.
1415
16+ Keep this file import-safe for setuptools/pip: do all external discovery
17+ (pkg-config) inside this class, not at module import time.
18+ """
1519
16- linkflags = []
17- lib_path = os .path .join (
18- check_output (["pkg-config" , "--variable=prefix" , "libmapnik" ]), "lib"
19- )
20- linkflags .extend (check_output (["pkg-config" , "--libs" , "libmapnik" ]).split (" " ))
21-
22- # Dynamically make the mapnik/paths.py file
23- f_paths = open ("packaging/mapnik/paths.py" , "w" )
24- f_paths .write ("import os\n " )
25- f_paths .write ("\n " )
26-
27- input_plugin_path = check_output (["pkg-config" , "--variable=plugins_dir" , "libmapnik" ])
28- font_path = check_output (["pkg-config" , "--variable=fonts_dir" , "libmapnik" ])
29-
30- if os .environ .get ("LIB_DIR_NAME" ):
31- mapnik_lib_path = lib_path + os .environ .get ("LIB_DIR_NAME" )
32- else :
33- mapnik_lib_path = lib_path + "/mapnik"
34- f_paths .write ("mapniklibpath = '{path}'\n " .format (path = mapnik_lib_path ))
35- f_paths .write ("inputpluginspath = '{path}'\n " .format (path = input_plugin_path ))
36- f_paths .write ("fontscollectionpath = '{path}'\n " .format (path = font_path ))
37- f_paths .write ("__all__ = [mapniklibpath,inputpluginspath,fontscollectionpath]\n " )
38- f_paths .close ()
39-
40- extra_comp_args = check_output (["pkg-config" , "--cflags" , "libmapnik" ]).split (" " )
41- extra_comp_args = list (
42- filter (lambda arg : arg != "-fvisibility=hidden" , extra_comp_args )
43- )
20+ def __init__ (self , pkg_name : str = "libmapnik" ):
21+ self .pkg_name = pkg_name
22+ self .linkflags : list [str ] = []
23+ self .extra_comp_args : list [str ] = []
24+ self .mapnik_lib_path : str = ""
25+ self .input_plugin_path : str = ""
26+ self .font_path : str = ""
27+
28+ @staticmethod
29+ def _check_output (args : list [str ]) -> str :
30+ output = subprocess .check_output (args ).decode ()
31+ return output .rstrip ("\n " )
32+
33+ def _pkg_config_var (self , var_name : str ) -> str :
34+ return self ._check_output (["pkg-config" , "--variable=" + var_name , self .pkg_name ])
35+
36+ @staticmethod
37+ def _split_flags (s : str ) -> list [str ]:
38+ # pkg-config/mapnik-config return shell-like strings; shlex handles quoted paths safely.
39+ return [arg for arg in shlex .split (s ) if arg ]
40+
41+ def _ensure_cpp_std (self ) -> None :
42+ """
43+ Some environments don't inject a C++ standard in pkg-config/mapnik-config flags.
44+ This project uses C++17 features (e.g., `template <auto Key>`), so ensure C++17.
45+ """
46+ # Always force C++17 as the last -std flag so it wins over older standards.
47+ # (GCC/Clang take the last -std=... argument.)
48+ if not any (arg .startswith ("/std:" ) for arg in self .extra_comp_args ):
49+ self .extra_comp_args .append ("-std=c++17" )
50+
51+ def _discover_with_pkg_config (self ) -> None :
52+ # Linker flags / library location.
53+ prefix = self ._pkg_config_var ("prefix" )
54+ lib_path = os .path .join (prefix , "lib" )
55+ self .linkflags .extend (self ._split_flags (self ._check_output (["pkg-config" , "--libs" , self .pkg_name ])))
56+
57+ # Runtime data locations.
58+ self .input_plugin_path = self ._pkg_config_var ("plugins_dir" )
59+ self .font_path = self ._pkg_config_var ("fonts_dir" )
60+
61+ lib_dir_name = os .environ .get ("LIB_DIR_NAME" )
62+ if lib_dir_name :
63+ self .mapnik_lib_path = lib_path + lib_dir_name
64+ else :
65+ self .mapnik_lib_path = lib_path + "/mapnik"
66+
67+ # Compiler flags.
68+ extra_comp_args = self ._split_flags (self ._check_output (["pkg-config" , "--cflags" , self .pkg_name ]))
69+ self .extra_comp_args = [arg for arg in extra_comp_args if arg != "-fvisibility=hidden" ]
70+
71+ def _mapnik_config (self ) -> str :
72+ # Allow pinning a specific mapnik-config (useful in CI / non-standard prefixes).
73+ return os .environ .get ("MAPNIK_CONFIG" , "mapnik-config" )
74+
75+ def _mapnik_config_try_flag (self , flag_candidates : list [str ]) -> str :
76+ """
77+ Try mapnik-config with one of the provided flags and return the first
78+ successful (non-empty) output. Returns "" if none work.
79+ """
80+ cmd = self ._mapnik_config ()
81+ for flag in flag_candidates :
82+ try :
83+ out = self ._check_output ([cmd , flag ]).strip ()
84+ except (FileNotFoundError , subprocess .CalledProcessError ):
85+ continue
86+ if out :
87+ return out
88+ return ""
89+
90+ def _discover_with_mapnik_config (self ) -> None :
91+ cmd = self ._mapnik_config ()
92+ prefix = self ._check_output ([cmd , "--prefix" ])
93+ lib_path = os .path .join (prefix , "lib" )
94+
95+ # flags
96+ self .linkflags .extend (self ._split_flags (self ._check_output ([cmd , "--libs" ])))
97+ extra_comp_args = self ._split_flags (self ._check_output ([cmd , "--cflags" ]))
98+ self .extra_comp_args = [arg for arg in extra_comp_args if arg != "-fvisibility=hidden" ]
99+
100+ # runtime locations (best-effort: flags vary slightly across mapnik versions/distros)
101+ self .input_plugin_path = self ._mapnik_config_try_flag (
102+ ["--input-plugins" , "--input-plugins-dir" , "--input-plugins-path" ]
103+ )
104+ self .font_path = self ._mapnik_config_try_flag (["--fonts" , "--fonts-dir" , "--fonts-path" ])
105+
106+ lib_dir_name = os .environ .get ("LIB_DIR_NAME" )
107+ if lib_dir_name :
108+ self .mapnik_lib_path = lib_path + lib_dir_name
109+ else :
110+ self .mapnik_lib_path = lib_path + "/mapnik"
111+
112+ def discover (self ) -> None :
113+ # Prefer pkg-config, but fall back to mapnik-config (common on some distros/builds).
114+ try :
115+ self ._discover_with_pkg_config ()
116+ except (FileNotFoundError , subprocess .CalledProcessError ):
117+ self ._discover_with_mapnik_config ()
118+
119+ if not self .input_plugin_path or not self .font_path :
120+ raise RuntimeError (
121+ "Failed to discover Mapnik runtime paths. "
122+ "Tried pkg-config variables (plugins_dir/fonts_dir) and mapnik-config flags "
123+ "(--input-plugins/--fonts). You can set MAPNIK_CONFIG to point to mapnik-config."
124+ )
125+
126+ self ._ensure_cpp_std ()
127+
128+ # Platform-specific linker flags.
129+ if sys .platform != "darwin" :
130+ self .linkflags .append ("-lrt" )
131+ self .linkflags .append ("-Wl,-z,origin" )
132+ self .linkflags .append ("-Wl,-rpath=$ORIGIN/lib" )
133+
134+ self .linkflags = [arg for arg in self .linkflags if arg ]
135+
136+ def write_paths_py (self , target_file : str = "packaging/mapnik/paths.py" ) -> None :
137+ os .makedirs (os .path .dirname (target_file ), exist_ok = True )
138+ with open (target_file , "w" , encoding = "utf-8" ) as f_paths :
139+ f_paths .write ("import os\n \n " )
140+ f_paths .write (f"mapniklibpath = { self .mapnik_lib_path !r} \n " )
141+ f_paths .write (f"inputpluginspath = { self .input_plugin_path !r} \n " )
142+ f_paths .write (f"fontscollectionpath = { self .font_path !r} \n " )
143+ # __all__ should be a list of names (strings), not the values.
144+ f_paths .write ('__all__ = ["mapniklibpath", "inputpluginspath", "fontscollectionpath"]\n ' )
44145
45- if sys .platform == "darwin" :
46- pass
47- else :
48- linkflags .append ("-lrt" )
49- linkflags .append ("-Wl,-z,origin" )
50- linkflags .append ("-Wl,-rpath=$ORIGIN/lib" )
51146
52- extra_comp_args = list (filter (lambda arg : arg != "" , extra_comp_args ))
53- linkflags = list (filter (lambda arg : arg != "" , linkflags ))
147+ cfg = MapnikBuildConfig ("libmapnik" )
148+ cfg .discover ()
149+ cfg .write_paths_py ()
54150
55151ext_modules = [
56152 Pybind11Extension (
@@ -104,8 +200,9 @@ def check_output(args):
104200 "src/mapnik_shield_symbolizer.cpp" ,
105201 "src/mapnik_group_symbolizer.cpp" ,
106202 ],
107- extra_compile_args = extra_comp_args ,
108- extra_link_args = linkflags ,
203+ cxx_std = 17 ,
204+ extra_compile_args = cfg .extra_comp_args ,
205+ extra_link_args = cfg .linkflags ,
109206 )
110207]
111208
@@ -116,7 +213,7 @@ def check_output(args):
116213
117214setup (
118215 name = "mapnik" ,
119- version = "4.0 .0.dev" ,
216+ version = "4.2 .0.dev" ,
120217 packages = find_packages (where = "packaging" ),
121218 package_dir = {"" : "packaging" },
122219 package_data = {
0 commit comments