1313# See the License for the specific language governing permissions and
1414# limitations under the License.
1515
16+ from __future__ import annotations
17+
1618import os
1719import platform
1820import shutil
1921import subprocess # nosec
2022import sys
23+ from dataclasses import dataclass , field
2124from sysconfig import get_config_var
25+ from typing import (Dict ,
26+ List ,
27+ Optional )
28+
29+ from setuptools import Command , Extension
2230
23- from setuptools import Extension
31+ # need at least setuptools v62.3.0
2432from setuptools .command .build import build
2533from setuptools .command .build_ext import build_ext
34+ from setuptools .errors import OptionError , SetupError
2635
2736CMAKE_EXE = os .environ .get ('CMAKE_EXE' , shutil .which ('cmake' ))
2837PYCBC_ROOT = os .path .dirname (__file__ )
@@ -98,13 +107,146 @@ def process_build_env_vars(): # noqa: C901
98107 os .environ ['CMAKE_COMMON_VARIABLES' ] = ' ' .join (cmake_extra_args )
99108
100109
110+ @dataclass
111+ class CMakeConfig :
112+ build_type : str
113+ num_threads : int
114+ set_cpm_cache : bool
115+ env : Dict [str , str ] = field (default_factory = dict )
116+ config_args : List [str ] = field (default_factory = list )
117+
118+ @classmethod
119+ def create_cmake_config (cls , # noqa: C901
120+ output_dir : str ,
121+ source_dir : str ,
122+ set_cpm_cache : Optional [bool ] = None
123+ ) -> CMakeConfig :
124+ env = os .environ .copy ()
125+ num_threads = env .pop ('PYCBC_CMAKE_PARALLEL_THREADS' , '4' )
126+ build_type = env .pop ('PYCBC_BUILD_TYPE' )
127+ cmake_generator = env .pop ('PYCBC_CMAKE_SET_GENERATOR' , None )
128+ cmake_arch = env .pop ('PYCBC_CMAKE_SET_ARCH' , None )
129+ cmake_config_args = [CMAKE_EXE ,
130+ source_dir ,
131+ f'-DCMAKE_BUILD_TYPE={ build_type } ' ,
132+ f'-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={ output_dir } ' ,
133+ f'-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{ build_type .upper ()} ={ output_dir } ' ]
134+
135+ cmake_config_args .extend (
136+ [x for x in
137+ os .environ .get ('CMAKE_COMMON_VARIABLES' , '' ).split (' ' )
138+ if x ])
139+
140+ python3_executable = env .pop ('PYCBC_PYTHON3_EXECUTABLE' , None )
141+ if python3_executable :
142+ cmake_config_args += [f'-DPython3_EXECUTABLE={ python3_executable } ' ]
143+
144+ python3_include = env .pop ('PYCBC_PYTHON3_INCLUDE_DIR' , None )
145+ if python3_include :
146+ cmake_config_args += [f'-DPython3_INCLUDE_DIR={ python3_include } ' ]
147+
148+ if set_cpm_cache is None :
149+ set_cpm_cache = env .pop ('PYCBC_SET_CPM_CACHE' , 'false' ).lower () in ENV_TRUE
150+ use_cpm_cache = env .pop ('PYCBC_USE_CPM_CACHE' , 'true' ).lower () in ENV_TRUE
151+
152+ if set_cpm_cache is True :
153+ # if we are setting the cache, we don't want to attempt a build (it will fail).
154+ use_cpm_cache = False
155+ if os .path .exists (CXXCBC_CACHE_DIR ):
156+ shutil .rmtree (CXXCBC_CACHE_DIR )
157+ cmake_config_args += [f'-DCOUCHBASE_CXX_CPM_CACHE_DIR={ CXXCBC_CACHE_DIR } ' ,
158+ '-DCPM_DOWNLOAD_ALL=ON' ,
159+ '-DCPM_USE_NAMED_CACHE_DIRECTORIES=ON' ,
160+ '-DCPM_USE_LOCAL_PACKAGES=OFF' ]
161+
162+ if use_cpm_cache is True :
163+ if not os .path .exists (CXXCBC_CACHE_DIR ):
164+ raise OptionError (f'Cannot use cached dependencies, path={ CXXCBC_CACHE_DIR } does not exist.' )
165+ cmake_config_args += ['-DCPM_DOWNLOAD_ALL=OFF' ,
166+ '-DCPM_USE_NAMED_CACHE_DIRECTORIES=ON' ,
167+ '-DCPM_USE_LOCAL_PACKAGES=OFF' ,
168+ f'-DCPM_SOURCE_CACHE={ CXXCBC_CACHE_DIR } ' ,
169+ f'-DCOUCHBASE_CXX_CLIENT_EMBED_MOZILLA_CA_BUNDLE_ROOT={ CXXCBC_CACHE_DIR } "' ]
170+
171+ if platform .system () == "Windows" :
172+ cmake_config_args += [f'-DCMAKE_RUNTIME_OUTPUT_DIRECTORY_{ build_type .upper ()} ={ output_dir } ' ]
173+
174+ if cmake_generator :
175+ if cmake_generator .upper () == 'TRUE' :
176+ cmake_config_args += ['-G' , 'Visual Studio 16 2019' ]
177+ else :
178+ cmake_config_args += ['-G' , f'{ cmake_generator } ' ]
179+
180+ if cmake_arch :
181+ if cmake_arch .upper () == 'TRUE' :
182+ if sys .maxsize > 2 ** 32 :
183+ cmake_config_args += ['-A' , 'x64' ]
184+ else :
185+ cmake_config_args += ['-A' , f'{ cmake_arch } ' ]
186+ # maybe??
187+ # '-DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=TRUE',
188+
189+ return CMakeConfig (build_type ,
190+ num_threads ,
191+ set_cpm_cache ,
192+ env ,
193+ cmake_config_args )
194+
195+
101196class CMakeExtension (Extension ):
102197 def __init__ (self , name , sourcedir = '' ):
103198 check_for_cmake ()
104199 Extension .__init__ (self , name , sources = [])
105200 self .sourcedir = os .path .abspath (sourcedir )
106201
107202
203+ class CMakeConfigureExt (Command ):
204+ description = 'Configure Python Operational SDK C Extension'
205+ user_options = []
206+
207+ def initialize_options (self ) -> None :
208+ return
209+
210+ def finalize_options (self ) -> None :
211+ return
212+
213+ def run (self ) -> None :
214+ check_for_cmake ()
215+ process_build_env_vars ()
216+ build_ext = self .get_finalized_command ('build_ext' )
217+ if len (self .distribution .ext_modules ) != 1 :
218+ raise SetupError ('Should have only the Python SDK extension module.' )
219+ ext = self .distribution .ext_modules [0 ]
220+ output_dir = os .path .abspath (os .path .dirname (build_ext .get_ext_fullpath (ext .name )))
221+ set_cpm_cache = os .environ .get ('PYCBC_SET_CPM_CACHE' , 'true' ).lower () in ENV_TRUE
222+ cmake_config = CMakeConfig .create_cmake_config (output_dir , ext .sourcedir , set_cpm_cache = set_cpm_cache )
223+ if not os .path .exists (build_ext .build_temp ):
224+ os .makedirs (build_ext .build_temp )
225+ print (f'cmake config args: { cmake_config .config_args } ' )
226+ # configure (i.e. cmake ..)
227+ subprocess .check_call (cmake_config .config_args , # nosec
228+ cwd = build_ext .build_temp ,
229+ env = cmake_config .env )
230+
231+ self ._clean_cache_cpm_dependencies ()
232+
233+ def _clean_cache_cpm_dependencies (self ):
234+ import re
235+ from fileinput import FileInput
236+ from pathlib import Path
237+
238+ cxx_cache_path = Path (CXXCBC_CACHE_DIR )
239+ cmake_cpm = next ((p for p in cxx_cache_path .glob ('cpm/*' ) if f'{ p } ' .endswith ('.cmake' )), None )
240+ if cmake_cpm is not None :
241+ with FileInput (files = [cmake_cpm ], inplace = True ) as cpm_cmake :
242+ for line in cpm_cmake :
243+ # used so that we don't have a dependency on git w/in environment
244+ if 'find_package(Git REQUIRED)' in line :
245+ line = re .sub (r'Git REQUIRED' , 'Git' , line )
246+ # remove ending whitespace to avoid double spaced output
247+ print (line .rstrip ())
248+
249+
108250class CMakeBuildExt (build_ext ):
109251
110252 def get_ext_filename (self , ext_name ):
@@ -117,100 +259,29 @@ def build_extension(self, ext): # noqa: C901
117259 check_for_cmake ()
118260 process_build_env_vars ()
119261 if isinstance (ext , CMakeExtension ):
120- env = os .environ .copy ()
121- output_dir = os .path .abspath (
122- os .path .dirname (self .get_ext_fullpath (ext .name )))
123-
124- num_threads = env .pop ('PYCBC_CMAKE_PARALLEL_THREADS' , '4' )
125- build_type = env .pop ('PYCBC_BUILD_TYPE' )
126- cmake_generator = env .pop ('PYCBC_CMAKE_SET_GENERATOR' , None )
127- cmake_arch = env .pop ('PYCBC_CMAKE_SET_ARCH' , None )
128-
129- cmake_config_args = [CMAKE_EXE ,
130- ext .sourcedir ,
131- f'-DCMAKE_BUILD_TYPE={ build_type } ' ,
132- f'-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={ output_dir } ' ,
133- f'-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{ build_type .upper ()} ={ output_dir } ' ]
134-
135- cmake_config_args .extend (
136- [x for x in
137- os .environ .get ('CMAKE_COMMON_VARIABLES' , '' ).split (' ' )
138- if x ])
139-
140- python3_executable = env .pop ('PYCBC_PYTHON3_EXECUTABLE' , None )
141- if python3_executable :
142- cmake_config_args += [f'-DPython3_EXECUTABLE={ python3_executable } ' ]
143-
144- python3_include = env .pop ('PYCBC_PYTHON3_INCLUDE_DIR' , None )
145- if python3_include :
146- cmake_config_args += [f'-DPython3_INCLUDE_DIR={ python3_include } ' ]
147-
148- set_cpm_cache = env .pop ('PYCBC_SET_CPM_CACHE' , 'false' ).lower () in ENV_TRUE
149- use_cpm_cache = env .pop ('PYCBC_USE_CPM_CACHE' , 'true' ).lower () in ENV_TRUE
150-
151- if set_cpm_cache is True :
152- # if we are setting the cache, we don't want to attempt a build (it will fail).
153- use_cpm_cache = False
154- if os .path .exists (CXXCBC_CACHE_DIR ):
155- shutil .rmtree (CXXCBC_CACHE_DIR )
156- cmake_config_args += [f'-DCOUCHBASE_CXX_CPM_CACHE_DIR={ CXXCBC_CACHE_DIR } ' ,
157- '-DCPM_DOWNLOAD_ALL=ON' ,
158- '-DCPM_USE_NAMED_CACHE_DIRECTORIES=ON' ,
159- '-DCPM_USE_LOCAL_PACKAGES=OFF' ]
160-
161- if use_cpm_cache is True :
162- if not os .path .exists (CXXCBC_CACHE_DIR ):
163- raise Exception (f'Cannot use cached dependencies, path={ CXXCBC_CACHE_DIR } does not exist.' )
164- cmake_config_args += ['-DCPM_DOWNLOAD_ALL=OFF' ,
165- '-DCPM_USE_NAMED_CACHE_DIRECTORIES=ON' ,
166- '-DCPM_USE_LOCAL_PACKAGES=OFF' ,
167- f'-DCPM_SOURCE_CACHE={ CXXCBC_CACHE_DIR } ' ,
168- f'-DCOUCHBASE_CXX_CLIENT_EMBED_MOZILLA_CA_BUNDLE_ROOT={ CXXCBC_CACHE_DIR } "' ]
169-
170- if platform .system () == "Windows" :
171- cmake_config_args += [f'-DCMAKE_RUNTIME_OUTPUT_DIRECTORY_{ build_type .upper ()} ={ output_dir } ' ]
172-
173- if cmake_generator :
174- if cmake_generator .upper () == 'TRUE' :
175- cmake_config_args += ['-G' , 'Visual Studio 16 2019' ]
176- else :
177- cmake_config_args += ['-G' , f'{ cmake_generator } ' ]
178-
179- if cmake_arch :
180- if cmake_arch .upper () == 'TRUE' :
181- if sys .maxsize > 2 ** 32 :
182- cmake_config_args += ['-A' , 'x64' ]
183- else :
184- cmake_config_args += ['-A' , f'{ cmake_arch } ' ]
185- # maybe??
186- # '-DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=TRUE',
262+ output_dir = os .path .abspath (os .path .dirname (self .get_ext_fullpath (ext .name )))
263+ cmake_config = CMakeConfig .create_cmake_config (output_dir , ext .sourcedir )
187264
188265 cmake_build_args = [CMAKE_EXE ,
189266 '--build' ,
190267 '.' ,
191268 '--config' ,
192- f'{ build_type } ' ,
269+ f'{ cmake_config . build_type } ' ,
193270 '--parallel' ,
194- f'{ num_threads } ' ]
271+ f'{ cmake_config . num_threads } ' ]
195272
196273 if not os .path .exists (self .build_temp ):
197274 os .makedirs (self .build_temp )
198- print (f'cmake config args: { cmake_config_args } ' )
275+ print (f'cmake config args: { cmake_config . config_args } ' )
199276 # configure (i.e. cmake ..)
200- subprocess .check_call (cmake_config_args , # nosec
277+ subprocess .check_call (cmake_config .config_args , # nosec
278+ cwd = self .build_temp ,
279+ env = cmake_config .env )
280+ print (f'cmake build args: { cmake_build_args } ' )
281+ # build (i.e. cmake --build .)
282+ subprocess .check_call (cmake_build_args , # nosec
201283 cwd = self .build_temp ,
202- env = env )
203-
204- if set_cpm_cache is True :
205- self ._clean_cache_cpm_dependencies ()
206- # NOTE: since we are not building, this will create an error. Okay, as attempting the
207- # build will fail anyway and we really just want to update the CXX cache.
208- else :
209- print (f'cmake build args: { cmake_build_args } ' )
210- # build (i.e. cmake --build .)
211- subprocess .check_call (cmake_build_args , # nosec
212- cwd = self .build_temp ,
213- env = env )
284+ env = cmake_config .env )
214285
215286 else :
216287 super ().build_extension (ext )
0 commit comments