13
13
# See the License for the specific language governing permissions and
14
14
# limitations under the License.
15
15
16
+ from __future__ import annotations
17
+
16
18
import os
17
19
import platform
18
20
import shutil
19
21
import subprocess # nosec
20
22
import sys
23
+ from dataclasses import dataclass , field
21
24
from sysconfig import get_config_var
25
+ from typing import (Dict ,
26
+ List ,
27
+ Optional )
28
+
29
+ from setuptools import Command , Extension
22
30
23
- from setuptools import Extension
31
+ # need at least setuptools v62.3.0
24
32
from setuptools .command .build import build
25
33
from setuptools .command .build_ext import build_ext
34
+ from setuptools .errors import OptionError , SetupError
26
35
27
36
CMAKE_EXE = os .environ .get ('CMAKE_EXE' , shutil .which ('cmake' ))
28
37
PYCBC_ROOT = os .path .dirname (__file__ )
@@ -98,13 +107,146 @@ def process_build_env_vars(): # noqa: C901
98
107
os .environ ['CMAKE_COMMON_VARIABLES' ] = ' ' .join (cmake_extra_args )
99
108
100
109
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
+
101
196
class CMakeExtension (Extension ):
102
197
def __init__ (self , name , sourcedir = '' ):
103
198
check_for_cmake ()
104
199
Extension .__init__ (self , name , sources = [])
105
200
self .sourcedir = os .path .abspath (sourcedir )
106
201
107
202
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
+
108
250
class CMakeBuildExt (build_ext ):
109
251
110
252
def get_ext_filename (self , ext_name ):
@@ -117,100 +259,29 @@ def build_extension(self, ext): # noqa: C901
117
259
check_for_cmake ()
118
260
process_build_env_vars ()
119
261
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 )
187
264
188
265
cmake_build_args = [CMAKE_EXE ,
189
266
'--build' ,
190
267
'.' ,
191
268
'--config' ,
192
- f'{ build_type } ' ,
269
+ f'{ cmake_config . build_type } ' ,
193
270
'--parallel' ,
194
- f'{ num_threads } ' ]
271
+ f'{ cmake_config . num_threads } ' ]
195
272
196
273
if not os .path .exists (self .build_temp ):
197
274
os .makedirs (self .build_temp )
198
- print (f'cmake config args: { cmake_config_args } ' )
275
+ print (f'cmake config args: { cmake_config . config_args } ' )
199
276
# 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
201
283
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 )
214
285
215
286
else :
216
287
super ().build_extension (ext )
0 commit comments