@@ -69,6 +69,9 @@ class Package:
6969    by legacy distutils/setuptools and described in: 
7070    https://pip.pypa.io/en/stable/reference/build-system/setup-py/ 
7171
72+     The file pyproject.toml must exist; this is checked if/when fn_build() is 
73+     called. 
74+ 
7275    Here is a `doctest` example of using pipcl to create a SWIG extension 
7376    module. Requires `swig`. 
7477
@@ -335,63 +338,86 @@ def __init__(self,
335338            wheel_compresslevel  =  None ,
336339            ):
337340        ''' 
338-         The initial args before `root ` define the package  
339-         metadata and closely follow the definitions in: 
341+         The initial args before `entry_points ` define the 
342+         package  metadata and closely follow the definitions in: 
340343        https://packaging.python.org/specifications/core-metadata/ 
341344
342345        Args: 
343346
344347            name: 
348+                 Used for metadata `Name`. 
345349                A string, the name of the Python package. 
346350            version: 
351+                 Used for metadata `Version`. 
347352                A string, the version of the Python package. Also see PEP-440 
348353                `Version Identification and Dependency Specification`. 
349354            platform: 
355+                 Used for metadata `Platform`. 
350356                A string or list of strings. 
351357            supported_platform: 
358+                 Used for metadata `Supported-Platform`. 
352359                A string or list of strings. 
353360            summary: 
361+                 Used for metadata `Summary`. 
354362                A string, short description of the package. 
355363            description: 
364+                 Used for metadata `Description`. 
356365                A string. If contains newlines, a detailed description of the 
357366                package. Otherwise the path of a file containing the detailed 
358367                description of the package. 
359368            description_content_type: 
369+                 Used for metadata `Description-Content-Type`. 
360370                A string describing markup of `description` arg. For example 
361371                `text/markdown; variant=GFM`. 
362372            keywords: 
373+                 Used for metadata `Keywords`. 
363374                A string containing comma-separated keywords. 
364375            home_page: 
376+                 Used for metadata `Home-page`. 
365377                URL of home page. 
366378            download_url: 
379+                 Used for metadata `Download-URL`. 
367380                Where this version can be downloaded from. 
368381            author: 
382+                 Used for metadata `Author`. 
369383                Author. 
370384            author_email: 
385+                 Used for metadata `Author-email`. 
371386                Author email. 
372387            maintainer: 
388+                 Used for metadata `Maintainer`. 
373389                Maintainer. 
374390            maintainer_email: 
391+                 Used for metadata `Maintainer-email`. 
375392                Maintainer email. 
376393            license: 
394+                 Used for metadata `License`. 
377395                A string containing the license text. Written into metadata 
378396                file `COPYING`. Is also written into metadata itself if not 
379397                multi-line. 
380398            classifier: 
399+                 Used for metadata `Classifier`. 
381400                A string or list of strings. Also see: 
382401
383402                * https://pypi.org/pypi?%3Aaction=list_classifiers 
384403                * https://pypi.org/classifiers/ 
385404
386405            requires_dist: 
387-                 A string or list of strings. None items are ignored. Also see PEP-508. 
406+                 Used for metadata `Requires-Dist`. 
407+                 A string or list of strings, Python packages required 
408+                 at runtime. None items are ignored. 
388409            requires_python: 
410+                 Used for metadata `Requires-Python`. 
389411                A string or list of strings. 
390412            requires_external: 
413+                 Used for metadata `Requires-External`. 
391414                A string or list of strings. 
392415            project_url: 
393-                 A string or list of strings, each of the form: `{name}, {url}`. 
416+                 Used for metadata `Project-URL`. 
417+                 A string or list of strings, each of the form: `{name}, 
418+                 {url}`. 
394419            provides_extra: 
420+                 Used for metadata `Provides-Extra`. 
395421                A string or list of strings. 
396422
397423            entry_points: 
@@ -456,6 +482,11 @@ def __init__(self,
456482                default being `sysconfig.get_path('platlib')` e.g. 
457483                `myvenv/lib/python3.9/site-packages/`. 
458484
485+                 When calling this function, we assert that the file 
486+                 pyproject.toml exists in the current directory. (We do this 
487+                 here rather than in pipcl.Package's constructor, as otherwise 
488+                 importing setup.py from non-package-related code could fail.) 
489+ 
459490            fn_clean: 
460491                A function taking a single arg `all_` that cleans generated 
461492                files. `all_` is true iff `--all` is in argv. 
@@ -474,8 +505,7 @@ def __init__(self,
474505                It can be convenient to use `pipcl.git_items()`. 
475506
476507                The specification for sdists requires that the list contains 
477-                 `pyproject.toml`; we enforce this with a diagnostic rather than 
478-                 raising an exception, to allow legacy command-line usage. 
508+                 `pyproject.toml`; we enforce this with a Python assert. 
479509
480510            tag_python: 
481511                First element of wheel tag defined in PEP-425. If None we use 
@@ -822,12 +852,11 @@ def add_string(text, name):
822852                        assert  0 , f'Path is inside sdist_directory={ sdist_directory }  : { from_ !r}  ' 
823853                    assert  os .path .exists (from_ ), f'Path does not exist: { from_ !r}  ' 
824854                    assert  os .path .isfile (from_ ), f'Path is not a file: { from_ !r}  ' 
825-                     if  to_rel  ==  'pyproject.toml' :
826-                         found_pyproject_toml  =  True 
827855                    add (from_ , to_rel )
856+                 if  to_rel  ==  'pyproject.toml' :
857+                     found_pyproject_toml  =  True 
828858
829-             if  not  found_pyproject_toml :
830-                 log0 (f'Warning: no pyproject.toml specified.' )
859+             assert  found_pyproject_toml , f'Cannot create sdist because file not specified: pyproject.toml' 
831860
832861            # Always add a PKG-INFO file. 
833862            add_string (self ._metainfo (), 'PKG-INFO' )
@@ -978,13 +1007,38 @@ def _entry_points_text(self):
9781007
9791008    def  _call_fn_build ( self , config_settings = None ):
9801009        assert  self .fn_build 
1010+         assert  os .path .isfile ('pyproject.toml' ), (
1011+                 'Cannot create package because file does not exist: pyproject.toml' 
1012+                 )
9811013        log2 (f'calling self.fn_build={ self .fn_build }  ' )
9821014        if  inspect .signature (self .fn_build ).parameters :
9831015            ret  =  self .fn_build (config_settings )
9841016        else :
9851017            ret  =  self .fn_build ()
9861018        assert  isinstance ( ret , (list , tuple )), \
9871019                f'Expected list/tuple from { self .fn_build }   but got: { ret !r}  ' 
1020+ 
1021+         # Check that any extensions that we have built, have same 
1022+         # py_limited_api value. If package is marked with py_limited_api=True 
1023+         # then non-py_limited_api extensions seem to fail at runtime on 
1024+         # Windows. 
1025+         # 
1026+         # (We could possibly allow package py_limited_api=False and extensions 
1027+         # py_limited_api=True, but haven't tested this, and it seems simpler to 
1028+         # be strict.) 
1029+         for  item  in  ret :
1030+             from_ , (to_abs , to_rel ) =  self ._fromto (item )
1031+             from_abs  =  os .path .abspath (from_ )
1032+             is_py_limited_api  =  _extensions_to_py_limited_api .get (from_abs )
1033+             if  is_py_limited_api  is  not   None :
1034+                 assert  bool (self .py_limited_api ) ==  bool (is_py_limited_api ), (
1035+                         f'Extension was built with' 
1036+                         f' py_limited_api={ is_py_limited_api }   but pipcl.Package' 
1037+                         f' name={ self .name !r}   has' 
1038+                         f' py_limited_api={ self .py_limited_api }  :' 
1039+                         f' { from_abs !r}  ' 
1040+                         )
1041+         
9881042        return  ret 
9891043
9901044
@@ -1519,6 +1573,7 @@ def _fromto(self, p):
15191573        log2 (f'returning { from_ = }   { to_ = }  ' )
15201574        return  from_ , to_ 
15211575
1576+ _extensions_to_py_limited_api  =  dict ()
15221577
15231578def  build_extension (
15241579        name ,
@@ -1629,6 +1684,11 @@ def build_extension(
16291684        py_limited_api: 
16301685            If true we build for current Python's limited API / stable ABI. 
16311686
1687+             Note that we will assert false if this extension is added to a 
1688+             pipcl.Package that has a different <py_limited_api>, because 
1689+             on Windows importing a non-py_limited_api extension inside a 
1690+             py_limited=True package fails. 
1691+ 
16321692    Returns the leafname of the generated library file within `outdir`, e.g. 
16331693    `_{name}.so` on Unix or `_{name}.cp311-win_amd64.pyd` on Windows. 
16341694    ''' 
@@ -1886,6 +1946,8 @@ def build_extension(
18861946        #run(f'ls -l {path_so}', check=0) 
18871947        #run(f'file {path_so}', check=0) 
18881948
1949+     _extensions_to_py_limited_api [os .path .abspath (path_so )] =  py_limited_api 
1950+     
18891951    return  path_so_leaf 
18901952
18911953
@@ -2864,6 +2926,9 @@ def log_line_numbers(yes):
28642926    global  g_log_line_numbers 
28652927    g_log_line_numbers  =  bool (yes )
28662928
2929+ def  log (text = '' , caller = 1 ):
2930+     _log (text , 0 , caller + 1 )
2931+ 
28672932def  log0 (text = '' , caller = 1 ):
28682933    _log (text , 0 , caller + 1 )
28692934
@@ -3146,11 +3211,8 @@ def swig_get(swig, quick, swig_local='pipcl-swig-git'):
31463211                # > If you need to have bison first in your PATH, run: 
31473212                # >   echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc 
31483213                # 
3149-                 run (f'brew install bison' )
3150-                 PATH  =  os .environ ['PATH' ]
3151-                 prefix_bison  =  run ('brew --prefix bison' , capture = 1 ).strip ()
3152-                 PATH  =  f'{ prefix_bison }  /bin:{ PATH }  ' 
3153-                 swig_env_extra  =  dict (PATH = PATH )
3214+                 swig_env_extra  =  dict ()
3215+                 macos_add_brew_path ('bison' , swig_env_extra )
31543216                run (f'which bison' )
31553217                run (f'which bison' , env_extra = swig_env_extra )
31563218            # Build swig. 
@@ -3164,6 +3226,38 @@ def swig_get(swig, quick, swig_local='pipcl-swig-git'):
31643226        return  swig 
31653227
31663228
3229+ def  macos_add_brew_path (package , env = None , gnubin = True ):
3230+     ''' 
3231+     Adds path(s) for Brew <package>'s binaries to env['PATH']. 
3232+      
3233+     Args: 
3234+         package: 
3235+             Name of package. We get <package_root> of installed package by 
3236+             running `brew --prefix <package>`. 
3237+         env: 
3238+             The environment dict to modify. If None we use os.environ. If PATH 
3239+             is not in <env>, we first copy os.environ['PATH'] into <env>. 
3240+         gnubin: 
3241+             If true, we also add path to gnu binaries if it exists, 
3242+             <package_root>/libexe/gnubin. 
3243+     ''' 
3244+     if  not  darwin ():
3245+         return 
3246+     if  env  is  None :
3247+         env  =  os .environ 
3248+     if  'PATH'  not  in   env :
3249+         env ['PATH' ] =  os .environ ['PATH' ]
3250+     package_root  =  run (f'brew --prefix { package }  ' , capture = 1 ).strip ()
3251+     def  add (path ):
3252+         if  os .path .isdir (path ):
3253+             log1 (f'Adding to $PATH: { path }  ' )
3254+             PATH  =  env ['PATH' ]
3255+             env ['PATH' ] =  f'{ path }  :{ PATH }  ' 
3256+     add (f'{ package_root }  /bin' )
3257+     if  gnubin :
3258+         add (f'{ package_root }  /libexec/gnubin' )
3259+ 
3260+ 
31673261def  _show_dict (d ):
31683262    ret  =  '' 
31693263    for  n  in  sorted (d .keys ()):
0 commit comments