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
@@ -74,6 +74,93 @@ def __init__(self, name: str, environment: Environment, kwargs: T.Dict[str, T.An
7474 self .compile_args = self .get_config_value (['--cflags' ], 'compile_args' )
7575
7676
77+ class PythonBuildConfig :
78+ """PEP 739 build-details.json config file."""
79+
80+ """Schema version currently implemented."""
81+ IMPLEMENTED_VERSION : T .Final [str ] = '1.0'
82+ """Path keys — may be relative, need to be expanded."""
83+ _PATH_KEYS = (
84+ 'base_interpreter' ,
85+ 'libpython.dynamic' ,
86+ 'libpython.dynamic_stableabi' ,
87+ 'libpython.static' ,
88+ 'c_api.headers' ,
89+ 'c_api.pkgconfig_path' ,
90+ )
91+
92+ def __init__ (self , path : str ) -> None :
93+ self ._path = Path (path )
94+
95+ try :
96+ self ._data = json .loads (self ._path .read_text (encoding = 'utf8' ))
97+ except OSError as e :
98+ raise DependencyException (f'Failed to read python.build_config: { e } ' ) from e
99+
100+ self ._validate_data ()
101+ self ._expand_paths ()
102+
103+ def __getitem__ (self , key : str ) -> T .Any :
104+ value = self ._data
105+ for part in key .split ('.' ):
106+ value = value [part ]
107+ return value
108+
109+ def __contains__ (self , key : str ) -> bool :
110+ try :
111+ self [key ]
112+ except KeyError :
113+ return False
114+ else :
115+ return True
116+
117+ def get (self , key : str , default : T .Any = None ) -> T .Any :
118+ try :
119+ self [key ]
120+ except KeyError :
121+ return default
122+
123+ def _validate_data (self ) -> None :
124+ schema_version = self ._data ['schema_version' ]
125+ if mesonlib .version_compare (schema_version , '< 1.0' ):
126+ raise DependencyException (f'Invalid schema_version in python.build_config: { schema_version } ' )
127+ if mesonlib .version_compare (schema_version , '>= 2.0' ):
128+ raise DependencyException (
129+ f'Unsupported schema_version { schema_version !r} in python.build_config, '
130+ f'but we only implement support for { self .IMPLEMENTED_VERSION !r} '
131+ )
132+ # Schema version that we currently understand
133+ if mesonlib .version_compare (schema_version , f'> { self .IMPLEMENTED_VERSION } ' ):
134+ mlog .log (
135+ f'python.build_config has schema_version { schema_version !r} , '
136+ f'but we only implement suport for { self .IMPLEMENTED_VERSION !r} , '
137+ 'new functionality might be missing'
138+ )
139+
140+ def _expand_paths (self ) -> None :
141+ """Expand relative path (they're relative to base_prefix)."""
142+ for key in self ._PATH_KEYS :
143+ if key not in self :
144+ continue
145+ parent , _ , child = key .rpartition ('.' )
146+ container = self [parent ] if parent else self ._data
147+ path = Path (container [child ])
148+ if not path .is_absolute ():
149+ container [child ] = os .fspath (self .base_prefix / path )
150+
151+ @property
152+ def config_path (self ) -> Path :
153+ return self ._path
154+
155+ @mesonlib .lazy_property
156+ def base_prefix (self ) -> Path :
157+ path = Path (self ._data ['base_prefix' ])
158+ if path .is_absolute ():
159+ return path
160+ # Non-absolute paths are relative to the build config directory
161+ return self .config_path .parent / path
162+
163+
77164class BasicPythonExternalProgram (ExternalProgram ):
78165 def __init__ (self , name : str , command : T .Optional [T .List [str ]] = None ,
79166 ext_prog : T .Optional [ExternalProgram ] = None ,
@@ -87,14 +174,7 @@ def __init__(self, name: str, command: T.Optional[T.List[str]] = None,
87174 self .cached_version = None
88175 self .version_arg = '--version'
89176
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
177+ self .build_config = PythonBuildConfig (build_config_path ) if build_config_path else None
98178
99179 # We want strong key values, so we always populate this with bogus data.
100180 # Otherwise to make the type checkers happy we'd have to do .get() for
0 commit comments