-
Notifications
You must be signed in to change notification settings - Fork 335
/
resolved_context.py
1747 lines (1439 loc) · 65.1 KB
/
resolved_context.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from __future__ import print_function
from rez import __version__, module_root_path
from rez.package_repository import package_repository_manager
from rez.solver import SolverCallbackReturn
from rez.resolver import Resolver, ResolverStatus
from rez.system import system
from rez.config import config
from rez.util import shlex_join, dedup
from rez.utils.sourcecode import SourceCodeError
from rez.utils.colorize import critical, heading, local, implicit, Printer
from rez.utils.formatting import columnise, PackageRequest, ENV_VAR_REGEX
from rez.utils.data_utils import deep_del
from rez.utils.filesystem import TempDirs
from rez.utils.memcached import pool_memcached_connections
from rez.backport.shutilwhich import which
from rez.rex import RexExecutor, Python, OutputStyle
from rez.rex_bindings import VersionBinding, VariantBinding, \
VariantsBinding, RequirementsBinding
from rez import package_order
from rez.packages_ import get_variant, iter_packages
from rez.package_filter import PackageFilterList
from rez.shells import create_shell
from rez.exceptions import ResolvedContextError, PackageCommandError, RezError
from rez.utils.graph_utils import write_dot, write_compacted, read_graph_from_string
from rez.vendor.version.version import VersionRange
from rez.vendor.enum import Enum
from rez.vendor import yaml
from rez.utils import json
from rez.utils.yaml import dump_yaml
from tempfile import mkdtemp
from functools import wraps
import getpass
import socket
import threading
import traceback
import inspect
import time
import sys
import os
import os.path
class RezToolsVisibility(Enum):
"""Determines if/how rez cli tools are added back to PATH within a
resolved environment."""
never = 0 # Don't expose rez in resolved env
append = 1 # Append to PATH in resolved env
prepend = 2 # Prepend to PATH in resolved env
class SuiteVisibility(Enum):
"""Defines what suites on $PATH stay visible when a new rez environment is
resolved."""
never = 0 # Don't attempt to keep any suites visible in a new env
always = 1 # Keep suites visible in any new env
parent = 2 # Keep only the parent suite of a tool visible
parent_priority = 3 # Keep all suites visible and the parent takes precedence
class PatchLock(Enum):
""" Enum to represent the 'lock type' used when patching context objects.
"""
no_lock = ("No locking", -1)
lock_2 = ("Minor version updates only (X.*)", 1)
lock_3 = ("Patch version updates only (X.X.*)", 2)
lock_4 = ("Build version updates only (X.X.X.*)", 3)
lock = ("Exact version", -1)
__order__ = "no_lock,lock_2,lock_3,lock_4,lock"
def __init__(self, description, rank):
self.description = description
self.rank = rank
def get_lock_request(name, version, patch_lock, weak=True):
"""Given a package and patch lock, return the equivalent request.
For example, for object 'foo-1.2.1' and lock type 'lock_3', the equivalent
request is '~foo-1.2'. This restricts updates to foo to patch-or-lower
version changes only.
For objects not versioned down to a given lock level, the closest possible
lock is applied. So 'lock_3' applied to 'foo-1' would give '~foo-1'.
Args:
name (str): Package name.
version (Version): Package version.
patch_lock (PatchLock): Lock type to apply.
Returns:
`PackageRequest` object, or None if there is no equivalent request.
"""
ch = '~' if weak else ''
if patch_lock == PatchLock.lock:
s = "%s%s==%s" % (ch, name, str(version))
return PackageRequest(s)
elif (patch_lock == PatchLock.no_lock) or (not version):
return None
version_ = version.trim(patch_lock.rank)
s = "%s%s-%s" % (ch, name, str(version_))
return PackageRequest(s)
class ResolvedContext(object):
"""A class that resolves, stores and spawns Rez environments.
The main Rez entry point for creating, saving, loading and executing
resolved environments. A ResolvedContext object can be saved to file and
loaded at a later date, and it can reconstruct the equivalent environment
at that time. It can spawn interactive and non-interactive shells, in any
supported shell plugin type, such as bash and tcsh. It can also run a
command within a configured python namespace, without spawning a child
shell.
"""
serialize_version = (4, 3)
tmpdir_manager = TempDirs(config.context_tmpdir, prefix="rez_context_")
context_tracking_payload = None
context_tracking_lock = threading.Lock()
class Callback(object):
def __init__(self, max_fails, time_limit, callback, buf=None):
self.max_fails = max_fails
self.time_limit = time_limit
self.callback = callback
self.start_time = time.time()
self.buf = buf or sys.stdout
def __call__(self, state):
if self.max_fails != -1 and state.num_fails >= self.max_fails:
reason = ("fail limit reached: aborted after %d failures"
% state.num_fails)
return SolverCallbackReturn.fail, reason
if self.time_limit != -1:
secs = time.time() - self.start_time
if secs > self.time_limit:
return SolverCallbackReturn.abort, "time limit exceeded"
if self.callback:
return self.callback(state)
return SolverCallbackReturn.keep_going, ''
def __init__(self, package_requests, verbosity=0, timestamp=None,
building=False, caching=None, package_paths=None,
package_filter=None, package_orderers=None, max_fails=-1,
add_implicit_packages=True, time_limit=-1, callback=None,
package_load_callback=None, buf=None, suppress_passive=False,
print_stats=False):
"""Perform a package resolve, and store the result.
Args:
package_requests: List of strings or PackageRequest objects
representing the request.
verbosity: Verbosity level. One of [0,1,2].
timestamp: Ignore packages released after this epoch time. Packages
released at exactly this time will not be ignored.
building: True if we're resolving for a build.
caching: If True, cache(s) may be used to speed the resolve. If
False, caches will not be used. If None, config.resolve_caching
is used.
package_paths: List of paths to search for pkgs, defaults to
config.packages_path.
package_filter (`PackageFilterBase`): Filter used to exclude certain
packages. Defaults to settings from config.package_filter. Use
`package_filter.no_filter` to remove all filtering.
package_orderers (list of `PackageOrder`): Custom package ordering.
add_implicit_packages: If True, the implicit package list defined
by config.implicit_packages is appended to the request.
max_fails (int): Abort the resolve if the number of failed steps is
greater or equal to this number. If -1, does not abort.
time_limit (int): Abort the resolve if it takes longer than this
many seconds. If -1, there is no time limit.
callback: See `Solver`.
package_load_callback: If not None, this callable will be called
prior to each package being loaded. It is passed a single
`Package` object.
buf (file-like object): Where to print verbose output to, defaults
to stdout.
suppress_passive (bool): If True, don't print debugging info that
has had no effect on the solve. This argument only has an
effect if `verbosity` > 2.
print_stats (bool): If true, print advanced solver stats at the end.
"""
self.load_path = None
# resolving settings
self.requested_timestamp = timestamp
self.timestamp = self.requested_timestamp or int(time.time())
self.building = building
self.implicit_packages = []
self.caching = config.resolve_caching if caching is None else caching
self.verbosity = verbosity
self._package_requests = []
for req in package_requests:
if isinstance(req, basestring):
req = PackageRequest(req)
self._package_requests.append(req)
if add_implicit_packages:
self.implicit_packages = [PackageRequest(x)
for x in config.implicit_packages]
self.package_paths = (config.packages_path if package_paths is None
else package_paths)
self.package_paths = list(dedup(self.package_paths))
self.package_filter = (PackageFilterList.singleton if package_filter is None
else package_filter)
self.package_orderers = package_orderers
# patch settings
self.default_patch_lock = PatchLock.no_lock
self.patch_locks = {}
# info about env the resolve occurred in
self.rez_version = __version__
self.rez_path = module_root_path
self.user = getpass.getuser()
self.host = system.hostname
self.platform = system.platform
self.arch = system.arch
self.os = system.os
self.created = int(time.time())
# resolve results
self.status_ = ResolverStatus.pending
self._resolved_packages = None
self.failure_description = None
self.graph_string = None
self.graph_ = None
self.from_cache = None
# stats
self.solve_time = 0.0 # total solve time, inclusive of load time
self.load_time = 0.0 # total time loading packages (disk or memcache)
self.num_loaded_packages = 0 # num packages loaded (disk or memcache)
# the pre-resolve bindings. We store these because @late package.py
# functions need them, and we cache them to avoid cost
self.pre_resolve_bindings = None
# suite information
self.parent_suite_path = None
self.suite_context_name = None
# perform the solve
callback_ = self.Callback(buf=buf,
max_fails=max_fails,
time_limit=time_limit,
callback=callback)
def _package_load_callback(package):
if package_load_callback:
package_load_callback(package)
self.num_loaded_packages += 1
request = self.requested_packages(include_implicit=True)
resolver = Resolver(context=self,
package_requests=request,
package_paths=self.package_paths,
package_filter=self.package_filter,
package_orderers=self.package_orderers,
timestamp=self.requested_timestamp,
building=self.building,
caching=self.caching,
callback=callback_,
package_load_callback=_package_load_callback,
verbosity=verbosity,
buf=buf,
suppress_passive=suppress_passive,
print_stats=print_stats)
resolver.solve()
# convert the results
self.status_ = resolver.status
self.solve_time = resolver.solve_time
self.load_time = resolver.load_time
self.failure_description = resolver.failure_description
self.graph_ = resolver.graph
self.from_cache = resolver.from_cache
if self.status_ == ResolverStatus.solved:
self._resolved_packages = []
for variant in resolver.resolved_packages:
variant.set_context(self)
self._resolved_packages.append(variant)
# track context usage
if config.context_tracking_host:
data = self.to_dict(fields=config.context_tracking_context_fields)
self._track_context(data, action="created")
def __str__(self):
request = self.requested_packages(include_implicit=True)
req_str = " ".join(str(x) for x in request)
if self.status == ResolverStatus.solved:
res_str = " ".join(x.qualified_name for x in self._resolved_packages)
return "%s(%s ==> %s)" % (self.status.name, req_str, res_str)
else:
return "%s:%s(%s)" % (self.__class__.__name__,
self.status.name, req_str)
@property
def success(self):
"""True if the context has been solved, False otherwise."""
return (self.status_ == ResolverStatus.solved)
@property
def status(self):
"""Return the current status of the context.
Returns:
ResolverStatus.
"""
return self.status_
def requested_packages(self, include_implicit=False):
"""Get packages in the request.
Args:
include_implicit (bool): If True, implicit packages are appended
to the result.
Returns:
List of `PackageRequest` objects.
"""
if include_implicit:
return self._package_requests + self.implicit_packages
else:
return self._package_requests
@property
def resolved_packages(self):
"""Get packages in the resolve.
Returns:
List of `Variant` objects, or None if the resolve failed.
"""
return self._resolved_packages
def set_load_path(self, path):
"""Set the path that this context was reportedly loaded from.
You may want to use this method in cases where a context is saved to
disk, but you need to associate this new path with the context while it
is still in use.
"""
self.load_path = path
def __eq__(self, other):
"""Equality test.
Two contexts are considered equal if they have a equivalent request,
and an equivalent resolve. Other details, such as timestamp, are not
considered.
"""
return (isinstance(other, ResolvedContext)
and other.requested_packages(True) == self.requested_packages(True)
and other.resolved_packages == self.resolved_packages)
def __hash__(self):
list_ = []
req = self.requested_packages(True)
list_.append(tuple(req))
res = self.resolved_packages
if res is None:
list_.append(None)
else:
list_.append(tuple(res))
value = tuple(list_)
return hash(value)
@property
def has_graph(self):
"""Return True if the resolve has a graph."""
return bool((self.graph_ is not None) or self.graph_string)
def get_resolved_package(self, name):
"""Returns a `Variant` object or None if the package is not in the
resolve.
"""
pkgs = [x for x in self._resolved_packages if x.name == name]
return pkgs[0] if pkgs else None
def copy(self):
"""Returns a shallow copy of the context."""
import copy
return copy.copy(self)
# TODO: deprecate in favor of patch() method
def get_patched_request(self, package_requests=None,
package_subtractions=None, strict=False, rank=0):
"""Get a 'patched' request.
A patched request is a copy of this context's request, but with some
changes applied. This can then be used to create a new, 'patched'
context.
New package requests override original requests based on the type -
normal, conflict or weak. So 'foo-2' overrides 'foo-1', '!foo-2'
overrides '!foo-1' and '~foo-2' overrides '~foo-1', but a request such
as '!foo-2' would not replace 'foo-1' - it would be added instead.
Note that requests in `package_requests` can have the form '^foo'. This
is another way of supplying package subtractions.
Any new requests that don't override original requests are appended,
in the order that they appear in `package_requests`.
Args:
package_requests (list of str or list of `PackageRequest`):
Overriding requests.
package_subtractions (list of str): Any original request with a
package name in this list is removed, before the new requests
are added.
strict (bool): If True, the current context's resolve is used as the
original request list, rather than the request.
rank (int): If > 1, package versions can only increase in this rank
and further - for example, rank=3 means that only version patch
numbers are allowed to increase, major and minor versions will
not change. This is only applied to packages that have not been
explicitly overridden in `package_requests`. If rank <= 1, or
`strict` is True, rank is ignored.
Returns:
List of `PackageRequest` objects that can be used to construct a
new `ResolvedContext` object.
"""
# assemble source request
if strict:
request = []
for variant in self.resolved_packages:
req = PackageRequest(variant.qualified_package_name)
request.append(req)
else:
request = self.requested_packages()[:]
# convert '^foo'-style requests to subtractions
if package_requests:
package_subtractions = package_subtractions or []
indexes = []
for i, req in enumerate(package_requests):
name = str(req)
if name.startswith('^'):
package_subtractions.append(name[1:])
indexes.append(i)
for i in reversed(indexes):
del package_requests[i]
# apply subtractions
if package_subtractions:
request = [x for x in request if x.name not in package_subtractions]
# apply overrides
if package_requests:
request_dict = dict((x.name, (i, x)) for i, x in enumerate(request))
request_ = []
for req in package_requests:
if isinstance(req, basestring):
req = PackageRequest(req)
if req.name in request_dict:
i, req_ = request_dict[req.name]
if (req_ is not None) and (req_.conflict == req.conflict) \
and (req_.weak == req.weak):
request[i] = req
del request_dict[req.name]
else:
request_.append(req)
else:
request_.append(req)
request += request_
# add rank limiters
if not strict and rank > 1:
overrides = set(x.name for x in package_requests if not x.conflict)
rank_limiters = []
for variant in self.resolved_packages:
if variant.name not in overrides:
if len(variant.version) >= rank:
version = variant.version.trim(rank - 1)
version = version.next()
req = "~%s<%s" % (variant.name, str(version))
rank_limiters.append(req)
request += rank_limiters
return request
def graph(self, as_dot=False):
"""Get the resolve graph.
Args:
as_dot: If True, get the graph as a dot-language string. Otherwise,
a pygraph.digraph object is returned.
Returns:
A string or `pygraph.digraph` object, or None if there is no graph
associated with the resolve.
"""
if not self.has_graph:
return None
if not as_dot:
if self.graph_ is None:
# reads either dot format or our compact format
self.graph_ = read_graph_from_string(self.graph_string)
return self.graph_
if self.graph_string:
if self.graph_string.startswith('{'): # compact format
self.graph_ = read_graph_from_string(self.graph_string)
else:
# already in dot format. Note that this will only happen in
# old rez contexts where the graph is not stored in the newer
# compact format.
return self.graph_string
return write_dot(self.graph_)
def save(self, path):
"""Save the resolved context to file."""
with open(path, 'w') as f:
self.write_to_buffer(f)
def write_to_buffer(self, buf):
"""Save the context to a buffer."""
doc = self.to_dict()
if config.rxt_as_yaml:
content = dump_yaml(doc)
else:
content = json.dumps(doc, indent=4, separators=(",", ": "))
buf.write(content)
@classmethod
def get_current(cls):
"""Get the context for the current env, if there is one.
Returns:
`ResolvedContext`: Current context, or None if not in a resolved env.
"""
filepath = os.getenv("REZ_RXT_FILE")
if not filepath or not os.path.exists(filepath):
return None
return cls.load(filepath)
@classmethod
def load(cls, path):
"""Load a resolved context from file."""
with open(path) as f:
context = cls.read_from_buffer(f, path)
context.set_load_path(path)
return context
@classmethod
def read_from_buffer(cls, buf, identifier_str=None):
"""Load the context from a buffer."""
try:
return cls._read_from_buffer(buf, identifier_str)
except Exception as e:
cls._load_error(e, identifier_str)
def get_resolve_diff(self, other):
"""Get the difference between the resolve in this context and another.
The difference is described from the point of view of the current context
- a newer package means that the package in `other` is newer than the
package in `self`.
Diffs can only be compared if their package search paths match, an error
is raised otherwise.
The diff is expressed in packages, not variants - the specific variant
of a package is ignored.
Returns:
A dict containing:
- 'newer_packages': A dict containing items:
- package name (str);
- List of `Package` objects. These are the packages up to and
including the newer package in `self`, in ascending order.
- 'older_packages': A dict containing:
- package name (str);
- List of `Package` objects. These are the packages down to and
including the older package in `self`, in descending order.
- 'added_packages': Set of `Package` objects present in `self` but
not in `other`;
- 'removed_packages': Set of `Package` objects present in `other`,
but not in `self`.
If any item ('added_packages' etc) is empty, it is not added to the
resulting dict. Thus, an empty dict is returned if there is no
difference between contexts.
"""
if self.package_paths != other.package_paths:
from difflib import ndiff
diff = ndiff(self.package_paths, other.package_paths)
raise ResolvedContextError("Cannot diff resolves, package search "
"paths differ:\n%s" % '\n'.join(diff))
d = {}
self_pkgs_ = set(x.parent for x in self._resolved_packages)
other_pkgs_ = set(x.parent for x in other._resolved_packages)
self_pkgs = self_pkgs_ - other_pkgs_
other_pkgs = other_pkgs_ - self_pkgs_
if not (self_pkgs or other_pkgs):
return d
self_fams = dict((x.name, x) for x in self_pkgs)
other_fams = dict((x.name, x) for x in other_pkgs)
newer_packages = {}
older_packages = {}
added_packages = set()
removed_packages = set()
for pkg in self_pkgs:
if pkg.name not in other_fams:
removed_packages.add(pkg)
else:
other_pkg = other_fams[pkg.name]
if other_pkg.version > pkg.version:
r = VersionRange.as_span(lower_version=pkg.version,
upper_version=other_pkg.version)
it = iter_packages(pkg.name, range_=r)
pkgs = sorted(it, key=lambda x: x.version)
newer_packages[pkg.name] = pkgs
elif other_pkg.version < pkg.version:
r = VersionRange.as_span(lower_version=other_pkg.version,
upper_version=pkg.version)
it = iter_packages(pkg.name, range_=r)
pkgs = sorted(it, key=lambda x: x.version, reverse=True)
older_packages[pkg.name] = pkgs
for pkg in other_pkgs:
if pkg.name not in self_fams:
added_packages.add(pkg)
if newer_packages:
d["newer_packages"] = newer_packages
if older_packages:
d["older_packages"] = older_packages
if added_packages:
d["added_packages"] = added_packages
if removed_packages:
d["removed_packages"] = removed_packages
return d
@pool_memcached_connections
def print_info(self, buf=sys.stdout, verbosity=0, source_order=False,
show_resolved_uris=False):
"""Prints a message summarising the contents of the resolved context.
Args:
buf (file-like object): Where to print this info to.
verbosity (bool): Verbose mode.
source_order (bool): If True, print resolved packages in the order
they are sourced, rather than alphabetical order.
show_resolved_uris (bool): By default, resolved packages have their
'root' property listed, or their 'uri' if 'root' is None. Use
this option to list 'uri' regardless.
"""
_pr = Printer(buf)
def _rt(t):
if verbosity:
s = time.strftime("%a %b %d %H:%M:%S %Z %Y", time.localtime(t))
return s + " (%d)" % int(t)
else:
return time.strftime("%a %b %d %H:%M:%S %Y", time.localtime(t))
if self.status_ in (ResolverStatus.failed, ResolverStatus.aborted):
_pr("The context failed to resolve:\n%s"
% self.failure_description, critical)
return
t_str = _rt(self.created)
_pr("resolved by %s@%s, on %s, using Rez v%s"
% (self.user, self.host, t_str, self.rez_version))
if self.requested_timestamp:
t_str = _rt(self.requested_timestamp)
_pr("packages released after %s were ignored" % t_str)
_pr()
if verbosity:
_pr("search paths:", heading)
rows = []
colors = []
for path in self.package_paths:
if package_repository_manager.are_same(path, config.local_packages_path):
label = "(local)"
col = local
else:
label = ""
col = None
rows.append((path, label))
colors.append(col)
for col, line in zip(colors, columnise(rows)):
_pr(line, col)
_pr()
if self.package_filter:
data = self.package_filter.to_pod()
txt = dump_yaml(data)
_pr("package filters:", heading)
_pr(txt)
_pr()
_pr("requested packages:", heading)
rows = []
colors = []
for request in self._package_requests:
rows.append((str(request), ""))
colors.append(None)
for request in self.implicit_packages:
rows.append((str(request), "(implicit)"))
colors.append(implicit)
for col, line in zip(colors, columnise(rows)):
_pr(line, col)
_pr()
_pr("resolved packages:", heading)
rows = []
colors = []
resolved_packages = self.resolved_packages or []
if not source_order:
resolved_packages = sorted(resolved_packages, key=lambda x: x.name)
for pkg in resolved_packages:
t = []
col = None
location = None
# print root/uri
if show_resolved_uris or not pkg.root:
location = pkg.uri
else:
location = pkg.root
if not os.path.exists(pkg.root):
t.append('NOT FOUND')
col = critical
if pkg.is_local:
t.append('local')
col = local
t = '(%s)' % ', '.join(t) if t else ''
rows.append((pkg.qualified_package_name, location, t))
colors.append(col)
for col, line in zip(colors, columnise(rows)):
_pr(line, col)
if verbosity:
_pr()
actual_solve_time = self.solve_time - self.load_time
_pr("resolve details:", heading)
_pr("load time: %.02f secs" % self.load_time)
_pr("solve time: %.02f secs" % actual_solve_time)
_pr("packages queried: %d" % self.num_loaded_packages)
_pr("from cache: %s" % self.from_cache)
if self.load_path:
_pr("rxt file: %s" % self.load_path)
if verbosity >= 2:
_pr()
_pr("tools:", heading)
self.print_tools(buf=buf)
def print_tools(self, buf=sys.stdout):
data = self.get_tools()
if not data:
return
_pr = Printer(buf)
conflicts = set(self.get_conflicting_tools().keys())
rows = [["TOOL", "PACKAGE", ""],
["----", "-------", ""]]
colors = [None, None]
for _, (variant, tools) in sorted(data.items()):
pkg_str = variant.qualified_package_name
for tool in sorted(tools):
col = None
row = [tool, pkg_str, ""]
if tool in conflicts:
col = critical
row[-1] = "(in conflict)"
rows.append(row)
colors.append(col)
for col, line in zip(colors, columnise(rows)):
_pr(line, col)
def print_resolve_diff(self, other, heading=None):
"""Print the difference between the resolve of two contexts.
Args:
other (`ResolvedContext`): Context to compare to.
heading: One of:
- None: Do not display a heading;
- True: Display the filename of each context as a heading, if
both contexts have a filepath;
- 2-tuple: Use the given two strings as headings - the first is
the heading for `self`, the second for `other`.
"""
d = self.get_resolve_diff(other)
if not d:
return
rows = []
if heading is True and self.load_path and other.load_path:
a = os.path.basename(self.load_path)
b = os.path.basename(other.load_path)
heading = (a, b)
if isinstance(heading, tuple):
rows.append(list(heading) + [""])
rows.append(('-' * len(heading[0]), '-' * len(heading[1]), ""))
newer_packages = d.get("newer_packages", {})
older_packages = d.get("older_packages", {})
added_packages = d.get("added_packages", set())
removed_packages = d.get("removed_packages", set())
if newer_packages:
for name, pkgs in newer_packages.iteritems():
this_pkg = pkgs[0]
other_pkg = pkgs[-1]
diff_str = "(+%d versions)" % (len(pkgs) - 1)
rows.append((this_pkg.qualified_name,
other_pkg.qualified_name,
diff_str))
if older_packages:
for name, pkgs in older_packages.iteritems():
this_pkg = pkgs[0]
other_pkg = pkgs[-1]
diff_str = "(-%d versions)" % (len(pkgs) - 1)
rows.append((this_pkg.qualified_name,
other_pkg.qualified_name,
diff_str))
if added_packages:
for pkg in sorted(added_packages, key=lambda x: x.name):
rows.append(("-", pkg.qualified_name, ""))
if removed_packages:
for pkg in sorted(removed_packages, key=lambda x: x.name):
rows.append((pkg.qualified_name, "-", ""))
print('\n'.join(columnise(rows)))
def _on_success(fn):
@wraps(fn)
def _check(self, *nargs, **kwargs):
if self.status_ == ResolverStatus.solved:
return fn(self, *nargs, **kwargs)
else:
raise ResolvedContextError(
"Cannot perform operation in a failed context")
return _check
@_on_success
def get_dependency_graph(self):
"""Generate the dependency graph.
The dependency graph is a simpler subset of the resolve graph. It
contains package name nodes connected directly to their dependencies.
Weak references and conflict requests are not included in the graph.
The dependency graph does not show conflicts.
Returns:
`pygraph.digraph` object.
"""
from rez.vendor.pygraph.classes.digraph import digraph
nodes = {}
edges = set()
for variant in self._resolved_packages:
nodes[variant.name] = variant.qualified_package_name
for request in variant.get_requires():
if not request.conflict:
edges.add((variant.name, request.name))
g = digraph()
node_color = "#AAFFAA"
node_fontsize = 10
attrs = [("fontsize", node_fontsize),
("fillcolor", node_color),
("style", "filled")]
for name, qname in nodes.iteritems():
g.add_node(name, attrs=attrs + [("label", qname)])
for edge in edges:
g.add_edge(edge)
return g
@_on_success
def validate(self):
"""Validate the context."""
try:
for pkg in self.resolved_packages:
pkg.validate_data()
except RezError as e:
raise ResolvedContextError("%s: %s" % (e.__class__.__name__, str(e)))
@_on_success
def get_environ(self, parent_environ=None):
"""Get the environ dict resulting from interpreting this context.
@param parent_environ Environment to interpret the context within,
defaults to os.environ if None.
@returns The environment dict generated by this context, when
interpreted in a python rex interpreter.
"""
interp = Python(target_environ={}, passive=True)
executor = self._create_executor(interp, parent_environ)
self._execute(executor)
return executor.get_output()
@_on_success
def get_key(self, key, request_only=False):
"""Get a data key value for each resolved package.
Args:
key (str): String key of property, eg 'tools'.
request_only (bool): If True, only return the key from resolved
packages that were also present in the request.
Returns:
Dict of {pkg-name: (variant, value)}.
"""
values = {}
requested_names = [x.name for x in self._package_requests
if not x.conflict]
for pkg in self.resolved_packages:
if (not request_only) or (pkg.name in requested_names):
value = getattr(pkg, key)
if value is not None:
values[pkg.name] = (pkg, value)
return values
@_on_success
def get_tools(self, request_only=False):
"""Returns the commandline tools available in the context.
Args:
request_only: If True, only return the tools from resolved packages
that were also present in the request.
Returns:
Dict of {pkg-name: (variant, [tools])}.
"""
return self.get_key("tools", request_only=request_only)
@_on_success
def get_tool_variants(self, tool_name):
"""Get the variant(s) that provide the named tool.
If there are more than one variants, the tool is in conflict, and Rez
does not know which variant's tool is actually exposed.
Args:
tool_name(str): Name of the tool to search for.
Returns:
Set of `Variant` objects. If no variant provides the tool, an
empty set is returned.
"""
variants = set()
tools_dict = self.get_tools(request_only=False)
for variant, tools in tools_dict.itervalues():
if tool_name in tools:
variants.add(variant)
return variants
@_on_success
def get_conflicting_tools(self, request_only=False):
"""Returns tools of the same name provided by more than one package.
Args:
request_only: If True, only return the key from resolved packages