18
18
import inspect
19
19
import pathlib
20
20
import re
21
+ import shutil
21
22
from copy import deepcopy
22
23
from semantic_version import Version , SimpleSpec
23
24
from types import MappingProxyType
24
25
from . import invocationjob , paths , logging
25
26
from .config import get_config
26
27
from .toml_coders import TomlFlatConfigEncoder
27
- from .process_utils import oh_no_its_windows
28
+ from .process_utils import create_process , oh_no_its_windows
29
+ from .exceptions import ProcessInitializationError
28
30
29
- from typing import Dict , Mapping , Optional , Type , Iterable
31
+ from typing import Dict , Iterable , List , Mapping , Optional , Type
30
32
31
33
32
34
_resolvers : Dict [str , Type ["BaseEnvironmentResolver" ]] = {} # this should be loaded from plugins
33
35
34
36
35
37
def _populate_resolvers ():
38
+ # TODO: This horrible logic needs refactoring. All plugins must be loaded with pluginloader
39
+ # everything is tightly coupled here, interdependent... no planing
36
40
for k , v in dict (globals ()).items ():
37
41
if not inspect .isclass (v ) \
38
42
or not issubclass (v , BaseEnvironmentResolver ) \
@@ -53,7 +57,7 @@ class ResolutionImpossibleError(RuntimeError):
53
57
54
58
class EnvironmentResolverArguments :
55
59
"""
56
- this class objects specity requirements a task/invocation have for int 's worker environment wrapper.
60
+ this class objects specify requirements a task/invocation have for it 's worker environment wrapper.
57
61
"""
58
62
def __init__ (self , resolver_name = None , arguments : Optional [Mapping ] = None ):
59
63
"""
@@ -86,19 +90,24 @@ def remove_argument(self, name: str):
86
90
def get_resolver (self ):
87
91
return get_resolver (self .__resolver_name )
88
92
89
- def get_environment (self ) -> "invocationjob.Environment" :
90
- return get_resolver (self .name ()).get_environment (self .arguments ())
93
+ async def get_environment (self ) -> "invocationjob.Environment" :
94
+ return await get_resolver (self .name ()).get_environment (self .arguments ())
91
95
92
96
def serialize (self ) -> bytes :
93
- return json .dumps (self .__dict__ ).encode ('utf-8' )
97
+ return json .dumps ({
98
+ '_EnvironmentResolverArguments__resolver_name' : self .__resolver_name ,
99
+ '_EnvironmentResolverArguments__args' : self .__args ,
100
+ }).encode ('utf-8' )
94
101
95
102
async def serialize_async (self ):
96
103
return await asyncio .get_running_loop ().run_in_executor (None , self .serialize )
97
104
98
105
@classmethod
99
106
def deserialize (cls , data : bytes ):
100
107
wrp = EnvironmentResolverArguments (None )
101
- wrp .__dict__ .update (json .loads (data .decode ('utf-8' )))
108
+ data_dict = json .loads (data .decode ('utf-8' ))
109
+ wrp .__resolver_name = data_dict ['_EnvironmentResolverArguments__resolver_name' ]
110
+ wrp .__args = data_dict ['_EnvironmentResolverArguments__args' ]
102
111
return wrp
103
112
104
113
@classmethod
@@ -107,7 +116,7 @@ async def deserialize_async(cls, data: bytes):
107
116
108
117
109
118
class BaseEnvironmentResolver :
110
- def get_environment (self , arguments : Mapping ) -> "invocationjob.Environment" :
119
+ async def get_environment (self , arguments : Mapping ) -> "invocationjob.Environment" :
111
120
"""
112
121
this is the main reason for environment wrapper's existance.
113
122
give it your specific arguments
@@ -118,17 +127,65 @@ def get_environment(self, arguments: Mapping) -> "invocationjob.Environment":
118
127
"""
119
128
raise NotImplementedError ()
120
129
130
+ async def create_process (self , arguments : Mapping , call_args : List [str ], * , env : Optional [invocationjob .Environment ] = None , cwd : Optional [str ] = None ) -> asyncio .subprocess .Process :
131
+ """
132
+ this should create process, maybe in a special way
133
+
134
+ :param arguments: EnvironmentResolverArguments for the resolver
135
+ :param call_args: what to call: process and arguments
136
+ :param env: optional environment to launch process in. If None - get_environment should be called
137
+ :param cwd: current working directory for the process
138
+ """
139
+ raise NotImplementedError ()
140
+
141
+
142
+ class BaseSimpleProcessSpawnEnvironmentResolver (BaseEnvironmentResolver ):
143
+ async def create_process (self , arguments : Mapping , call_args : List [str ], * , env : Optional [invocationjob .Environment ] = None , cwd : Optional [str ] = None ) -> asyncio .subprocess .Process :
144
+ if env is None :
145
+ env = await self .get_environment (arguments )
146
+
147
+ if os .path .isabs (call_args [0 ]):
148
+ bin_path = call_args [0 ]
149
+ else :
150
+ bin_path = shutil .which (call_args [0 ], path = env .get ('PATH' , '' ))
151
+ if bin_path is None :
152
+ raise ProcessInitializationError (f'"{ call_args [0 ]} " was not found. Check environment resolver arguments and system setup' )
153
+
154
+ if cwd is None :
155
+ cwd = os .path .dirname (bin_path )
121
156
122
- class TrivialEnvironmentResolver (BaseEnvironmentResolver ):
157
+ return await create_process (call_args , env , cwd )
158
+
159
+
160
+ class BaseSimpleProcessSpawnEnvironmentResolverWithPythonCheat (BaseSimpleProcessSpawnEnvironmentResolver ):
161
+ # even though lifeblood worker runs with python interpreter - it does NOT expect
162
+ # python to be available in run environment. User might want to have multiple versions of packaged python.
163
+ # However, simple one user artist might not care, and would just want python code to work without any packages setup.
164
+ # So to simplify the life of smaller setup users, this hack was introduced.
165
+ async def create_process (self , arguments : Mapping , call_args : List [str ], * , env : Optional [invocationjob .Environment ] = None , cwd : Optional [str ] = None ) -> asyncio .subprocess .Process :
166
+ """
167
+ This introduces path to sys.executable if no python is found in PATH, yet `python` is first arg in call_args
168
+ """
169
+ if env is None :
170
+ env = await self .get_environment (arguments )
171
+ if call_args [0 ] in ('python' , 'python.exe' ) and shutil .which (call_args [0 ], path = env .get ('PATH' , '' )) is None :
172
+ env .append ('PATH' , os .path .dirname (sys .executable ))
173
+
174
+ return await super ().create_process (arguments , call_args , env = env , cwd = cwd )
175
+
176
+
177
+ class TrivialEnvironmentResolver (BaseSimpleProcessSpawnEnvironmentResolverWithPythonCheat ):
123
178
"""
124
179
trivial environment wrapper does nothing
125
180
"""
126
- def get_environment (self , arguments : dict ) -> "invocationjob.Environment" :
181
+ async def get_environment (self , arguments : Mapping ) -> "invocationjob.Environment" :
127
182
env = invocationjob .Environment (os .environ )
183
+ for key , value in arguments .items ():
184
+ env [key ] = value
128
185
return env
129
186
130
187
131
- class StandardEnvironmentResolver (BaseEnvironmentResolver ):
188
+ class StandardEnvironmentResolver (BaseSimpleProcessSpawnEnvironmentResolverWithPythonCheat ):
132
189
"""
133
190
will initialize environment based on requested software versions and it's own config
134
191
will raise ResolutionImpossibleError if he doesn't know how to resolve given configuration
@@ -162,7 +219,7 @@ def __init__(self):
162
219
self .logger .error ('environment resolver configs found, but all have errors! Aborting!' )
163
220
raise RuntimeError ('all resolver configs are broken' )
164
221
165
- def get_environment (self , arguments : Mapping ) -> "invocationjob.Environment" :
222
+ async def get_environment (self , arguments : Mapping ) -> "invocationjob.Environment" :
166
223
"""
167
224
168
225
:param arguments: are expected to be in format of package_name: version_specification
@@ -377,21 +434,21 @@ def main(args):
377
434
378
435
opts = parser .parse_args (args )
379
436
437
+ logger = logging .get_logger ('environment resolver' )
438
+ logger .info ('auto detecting packages...' )
380
439
packages = StandardEnvironmentResolver .autodetect_software ()
381
440
if opts .basepath :
382
441
for basepath in opts .basepath .split (',' ):
383
442
packages .update (StandardEnvironmentResolver .autodetect_software (basepath ))
443
+ for pkgname , v in packages .items ():
444
+ for verstr in v .keys ():
445
+ logger .info (f'found { pkgname } : { verstr } ' )
384
446
385
447
if opts .command == 'generate' :
386
448
config = get_config ('standard_environment_resolver' )
387
449
if opts .output :
388
450
config .override_config_save_location (opts .output )
389
451
config .set_toml_encoder_generator (TomlFlatConfigEncoder )
390
- logger = logging .get_logger ('environment resolver' )
391
- logger .info ('standard environment resolver is used, but no configuration found. auto generating configuration...' )
392
- for pkgname , v in packages .items ():
393
- for verstr in v .keys ():
394
- logger .info (f'found { pkgname } : { verstr } ' )
395
452
396
453
if opts .override : # do full config override
397
454
config .set_option_noasync ('packages' , packages )
@@ -413,6 +470,7 @@ def main(args):
413
470
config .set_option_noasync ('packages' , conf_packages )
414
471
415
472
elif opts .command == 'scan' :
473
+ print ('\n ' )
416
474
for pkgname , stuff in packages .items ():
417
475
print (f'{ pkgname } :' )
418
476
for ver , meta in stuff .items ():
0 commit comments