12
12
13
13
import collections
14
14
import contextlib
15
+ import dataclasses
15
16
import functools
16
17
import importlib .machinery
17
18
import io
@@ -497,24 +498,27 @@ def _install_path(
497
498
498
499
wheel_file .write (origin , location )
499
500
501
+ def _wheel_write_metadata (self , whl : mesonpy ._wheelfile .WheelFile , editable : bool = False ) -> None :
502
+ # add metadata
503
+ whl .writestr (f'{ self .distinfo_dir } /METADATA' , self ._project .metadata (editable ))
504
+ whl .writestr (f'{ self .distinfo_dir } /WHEEL' , self .wheel )
505
+ if self .entrypoints_txt :
506
+ whl .writestr (f'{ self .distinfo_dir } /entry_points.txt' , self .entrypoints_txt )
507
+
508
+ # add license (see https://github.com/mesonbuild/meson-python/issues/88)
509
+ if self ._project .license_file :
510
+ whl .write (
511
+ self ._source_dir / self ._project .license_file ,
512
+ f'{ self .distinfo_dir } /{ os .path .basename (self ._project .license_file )} ' ,
513
+ )
514
+
500
515
def build (self , directory : Path ) -> pathlib .Path :
501
516
self ._project .build () # ensure project is built
502
517
503
518
wheel_file = pathlib .Path (directory , f'{ self .name } .whl' )
504
519
505
520
with mesonpy ._wheelfile .WheelFile (wheel_file , 'w' ) as whl :
506
- # add metadata
507
- whl .writestr (f'{ self .distinfo_dir } /METADATA' , self ._project .metadata )
508
- whl .writestr (f'{ self .distinfo_dir } /WHEEL' , self .wheel )
509
- if self .entrypoints_txt :
510
- whl .writestr (f'{ self .distinfo_dir } /entry_points.txt' , self .entrypoints_txt )
511
-
512
- # add license (see https://github.com/mesonbuild/meson-python/issues/88)
513
- if self ._project .license_file :
514
- whl .write (
515
- self ._source_dir / self ._project .license_file ,
516
- f'{ self .distinfo_dir } /{ os .path .basename (self ._project .license_file )} ' ,
517
- )
521
+ self ._wheel_write_metadata (whl )
518
522
519
523
print ('{light_blue}{bold}Copying files to wheel...{reset}' .format (** _STYLES ))
520
524
with mesonpy ._util .cli_counter (
@@ -542,7 +546,49 @@ def build(self, directory: Path) -> pathlib.Path:
542
546
return wheel_file
543
547
544
548
def build_editable (self , directory : Path ) -> pathlib .Path :
545
- self ._build ()
549
+ self ._project .build () # ensure project is built
550
+
551
+ wheel_file = pathlib .Path (directory , f'{ self .name } .whl' )
552
+
553
+ project_path = self ._source_dir / '.mesonpy-editable'
554
+ top_level_modules = self ._project .top_level_modules
555
+ rebuild_commands = [list (cmd ) for cmd in self ._project .build_commands (project_path )]
556
+
557
+ with mesonpy ._wheelfile .WheelFile (wheel_file , 'w' ) as whl :
558
+ self ._wheel_write_metadata (whl , editable = True )
559
+ whl .writestr (
560
+ f'{ self .distinfo_dir } /direct_url.json.py' ,
561
+ f'file://{ os .fspath (self ._project .source_dir )} ' .encode (),
562
+ )
563
+
564
+ # install hook module
565
+ hook_module_name = f'_mesonpy_hook_{ self .normalized_name .replace ("." , "_" )} '
566
+ whl .writestr (
567
+ f'{ hook_module_name } .py' ,
568
+ textwrap .dedent (f'''
569
+ import mesonpy._editable
570
+ mesonpy._editable.MesonpyFinder.install(
571
+ project_path={ project_path } ,
572
+ top_level_modules={ top_level_modules } ,
573
+ rebuild_commands={ rebuild_commands } ,
574
+ )
575
+ ''' ).strip ().encode (),
576
+ )
577
+ # install .pth file
578
+ whl .writestr (
579
+ f'_mesonpy_{ self .normalized_name } .pth' ,
580
+ f'import { hook_module_name } ' .encode (),
581
+ )
582
+
583
+ # install non-code schemes
584
+ for scheme in self ._SCHEME_MAP :
585
+ if scheme in ('purelib' , 'platlib' , 'mesonpy-libs' ):
586
+ continue
587
+ for destination , origin in self ._wheel_files [scheme ]:
588
+ destination = pathlib .Path (self .data_dir , scheme , destination )
589
+ whl .write (origin , destination .as_posix ())
590
+
591
+ return wheel_file
546
592
547
593
548
594
MesonArgsKeys = Literal ['dist' , 'setup' , 'compile' , 'install' ]
@@ -733,11 +779,29 @@ def _wheel_builder(self) -> _WheelBuilder:
733
779
self ._copy_files ,
734
780
)
735
781
782
+ @property
783
+ def source_dir (self ) -> pathlib .Path :
784
+ return self ._source_dir
785
+
786
+ @property
787
+ def working_dir (self ) -> pathlib .Path :
788
+ return self ._working_dir
789
+
790
+ @property
791
+ def build_dir (self ) -> pathlib .Path :
792
+ return self ._build_dir
793
+
794
+ def build_commands (self , install_dir : Optional [pathlib .Path ] = None ) -> Collection [Sequence [str ]]:
795
+ return (
796
+ ('meson' , 'compile' , * self ._meson_args ['compile' ],),
797
+ ('meson' , 'install' , '--destdir' , os .fspath (install_dir or self ._install_dir ), * self ._meson_args ['install' ],),
798
+ )
799
+
736
800
@functools .lru_cache (maxsize = None )
737
801
def build (self ) -> None :
738
802
"""Trigger the Meson build."""
739
- self . _meson ( 'compile' , * self ._meson_args [ 'compile' ],)
740
- self . _meson ( 'install' , '--destdir' , os . fspath ( self . _install_dir ), * self ._meson_args [ 'install' ], )
803
+ for cmd in self .build_commands ():
804
+ self ._meson ( * cmd [ 1 :] )
741
805
742
806
@classmethod
743
807
@contextlib .contextmanager
@@ -817,21 +881,46 @@ def version(self) -> str:
817
881
assert isinstance (version , str )
818
882
return version
819
883
820
- @cached_property
821
- def metadata (self ) -> bytes :
884
+ @property
885
+ def top_level_modules (self ) -> Collection [str ]:
886
+ return {
887
+ os .path .dirname (self ._copy_files [file ])
888
+ for files in self ._install_plan .values ()
889
+ for file , details in files .items ()
890
+ if details ['destination' ].startswith ('{purelib}' ) or details ['destination' ].startswith ('{platlib}' )
891
+ }
892
+
893
+ @functools .lru_cache (maxsize = 2 )
894
+ def metadata (self , editable : bool = False ) -> bytes :
822
895
"""Project metadata."""
823
896
# the rest of the keys are only available when using PEP 621 metadata
824
897
if not self .pep621 :
825
- return textwrap .dedent (f'''
898
+ data = textwrap .dedent (f'''
826
899
Metadata-Version: 2.1
827
900
Name: { self .name }
828
901
Version: { self .version }
829
- ''' ).strip ().encode ()
902
+ ''' ).strip ()
903
+ if editable :
904
+ data += f'\n Requires-Dist: mesonpy=={ __version__ } '
905
+ return data .encode ()
906
+
830
907
# re-import pyproject_metadata to raise ModuleNotFoundError if it is really missing
831
908
import pyproject_metadata # noqa: F401, F811
832
909
assert self ._metadata
910
+
911
+ metadata = dataclasses .replace (self ._metadata )
912
+ # add extra dependency required for the editable hook
913
+ if editable :
914
+ from packaging .requirements import Requirement
915
+ metadata .dependencies .append (Requirement (f'mesonpy=={ __version__ } ' ))
916
+
917
+ core_metadata = metadata .as_rfc822 ()
918
+
833
919
# use self.version as the version may be dynamic -- fetched from Meson
834
- core_metadata = self ._metadata .as_rfc822 ()
920
+ #
921
+ # we need to overwrite this field in the RFC822 field as
922
+ # pyproject_metadata removes 'version' from the dynamic fields when
923
+ # giving it a value via the dataclass
835
924
core_metadata .headers ['Version' ] = [self .version ]
836
925
return bytes (core_metadata )
837
926
@@ -912,8 +1001,9 @@ def sdist(self, directory: Path) -> pathlib.Path:
912
1001
pkginfo_info = tarfile .TarInfo (f'{ dist_name } /PKG-INFO' )
913
1002
if mtime :
914
1003
pkginfo_info .mtime = mtime
915
- pkginfo_info .size = len (self .metadata )
916
- tar .addfile (pkginfo_info , fileobj = io .BytesIO (self .metadata ))
1004
+ metadata = self .metadata ()
1005
+ pkginfo_info .size = len (metadata )
1006
+ tar .addfile (pkginfo_info , fileobj = io .BytesIO (metadata ))
917
1007
918
1008
return sdist
919
1009
@@ -925,6 +1015,9 @@ def wheel(self, directory: Path) -> pathlib.Path: # noqa: F811
925
1015
shutil .move (os .fspath (wheel ), final_wheel )
926
1016
return final_wheel
927
1017
1018
+ def editable (self , directory : Path ) -> pathlib .Path :
1019
+ return self ._wheel_builder .build_editable (directory )
1020
+
928
1021
929
1022
@contextlib .contextmanager
930
1023
def _project (config_settings : Optional [Dict [Any , Any ]]) -> Iterator [Project ]:
@@ -1061,3 +1154,21 @@ def build_wheel(
1061
1154
out = pathlib .Path (wheel_directory )
1062
1155
with _project (config_settings ) as project :
1063
1156
return project .wheel (out ).name
1157
+
1158
+
1159
+ def build_editable (
1160
+ wheel_directory : str ,
1161
+ config_settings : Optional [Dict [Any , Any ]] = None ,
1162
+ metadata_directory : Optional [str ] = None ,
1163
+ ) -> str :
1164
+ _setup_cli ()
1165
+
1166
+ out = pathlib .Path (wheel_directory )
1167
+ with _project (config_settings ) as project :
1168
+ return project .editable (out ).name
1169
+
1170
+
1171
+ def get_requires_for_build_editable (
1172
+ config_settings : Optional [Dict [str , str ]] = None ,
1173
+ ) -> List [str ]:
1174
+ return get_requires_for_build_wheel ()
0 commit comments