33
44from  __future__ import  annotations 
55
6- import  functools , json , os , textwrap 
6+ import  functools , json , os , sys ,  textwrap 
77from  pathlib  import  Path 
88import  typing  as  T 
99
@@ -45,6 +45,11 @@ class PythonIntrospectionDict(TypedDict):
4545    _Base  =  object 
4646
4747
48+ if  sys .version_info  >=  (3 , 8 ):
49+     from  functools  import  cached_property 
50+ else :
51+     cached_property  =  property 
52+ 
4853class  Pybind11ConfigToolDependency (ConfigToolDependency ):
4954
5055    tools  =  ['pybind11-config' ]
@@ -74,6 +79,93 @@ def __init__(self, name: str, environment: Environment, kwargs: T.Dict[str, T.An
7479        self .compile_args  =  self .get_config_value (['--cflags' ], 'compile_args' )
7580
7681
82+ class  PythonBuildConfig :
83+     """PEP 739 build-details.json config file.""" 
84+ 
85+     """Schema version currently implemented.""" 
86+     IMPLEMENTED_VERSION : T .Final [str ] =  '1.0' 
87+     """Path keys — may be relative, need to be expanded.""" 
88+     _PATH_KEYS  =  (
89+         'base_interpreter' ,
90+         'libpython.dynamic' ,
91+         'libpython.dynamic_stableabi' ,
92+         'libpython.static' ,
93+         'c_api.headers' ,
94+         'c_api.pkgconfig_path' ,
95+     )
96+ 
97+     def  __init__ (self , path : str ) ->  None :
98+         self ._path  =  Path (path )
99+ 
100+         try :
101+             self ._data  =  json .loads (self ._path .read_text (encoding = 'utf8' ))
102+         except  OSError  as  e :
103+             raise  DependencyException (f'Failed to read python.build_config: { e }  ' ) from  e 
104+ 
105+         self ._validate_data ()
106+         self ._expand_paths ()
107+ 
108+     def  __getitem__ (self , key : str ) ->  T .Any :
109+         value  =  self ._data 
110+         for  part  in  key .split ('.' ):
111+             value  =  value [part ]
112+         return  value 
113+ 
114+     def  __contains__ (self , key : str ) ->  bool :
115+         try :
116+             self [key ]
117+         except  KeyError :
118+             return  False 
119+         else :
120+             return  True 
121+ 
122+     def  get (self , key : str , default : T .Any  =  None ) ->  T .Any :
123+         try :
124+             self [key ]
125+         except  KeyError :
126+             return  default 
127+ 
128+     def  _validate_data (self ) ->  None :
129+         schema_version  =  self ._data ['schema_version' ]
130+         if  mesonlib .version_compare (schema_version , '< 1.0' ):
131+             raise  DependencyException (f'Invalid schema_version in python.build_config: { schema_version }  ' )
132+         if  mesonlib .version_compare (schema_version , '>= 2.0' ):
133+             raise  DependencyException (
134+                 f'Unsupported schema_version { schema_version !r}   in python.build_config, ' 
135+                 f'but we only implement suport for { self .IMPLEMENTED_VERSION !r}  ' 
136+             )
137+         # Schema version that we currently understand 
138+         if  mesonlib .version_compare (schema_version , f'> { self .IMPLEMENTED_VERSION }  ' ):
139+             mlog .log (
140+                 f'python.build_config has schema_version { schema_version !r}  , ' 
141+                 f'but we only implement suport for { self .IMPLEMENTED_VERSION !r}  , ' 
142+                 'new functionality might be missing' 
143+             )
144+ 
145+     def  _expand_paths (self ) ->  None :
146+         """Expand relative path (they're relative to base_prefix).""" 
147+         for  key  in  self ._PATH_KEYS :
148+             if  key  not  in   self :
149+                 continue 
150+             parent , _ , child  =  key .rpartition ('.' )
151+             container  =  self [parent ] if  parent  else  self ._data 
152+             path  =  Path (container [child ])
153+             if  not  path .is_absolute ():
154+                 container [child ] =  os .fspath (self .base_prefix  /  path )
155+ 
156+     @property  
157+     def  config_path (self ) ->  Path :
158+         return  self ._path 
159+ 
160+     @cached_property  
161+     def  base_prefix (self ) ->  Path :
162+         path  =  Path (self ._data ['base_prefix' ])
163+         if  path .is_absolute ():
164+             return  path 
165+         # Non-absolute paths are relative to the build config directory 
166+         return  self .config_path .parent  /  path 
167+ 
168+ 
77169class  BasicPythonExternalProgram (ExternalProgram ):
78170    def  __init__ (self , name : str , command : T .Optional [T .List [str ]] =  None ,
79171                 ext_prog : T .Optional [ExternalProgram ] =  None ,
@@ -87,14 +179,7 @@ def __init__(self, name: str, command: T.Optional[T.List[str]] = None,
87179            self .cached_version  =  None 
88180            self .version_arg  =  '--version' 
89181
90-         self .build_config  =  None 
91- 
92-         if  build_config_path :
93-             try :
94-                 with  open (build_config_path , encoding = 'utf8' ) as  f :
95-                     self .build_config  =  json .load (f )
96-             except  OSError  as  e :
97-                 raise  DependencyException (f'Failed to read python.build_config: { e }  ' ) from  e 
182+         self .build_config  =  PythonBuildConfig (build_config_path ) if  build_config_path  else  None 
98183
99184        # We want strong key values, so we always populate this with bogus data. 
100185        # Otherwise to make the type checkers happy we'd have to do .get() for 
0 commit comments