1# Status: ported, except for tests.
2# Base revision: 64070
3#
4# Copyright 2001, 2002, 2003 Dave Abrahams
5# Copyright 2006 Rene Rivera
6# Copyright 2002, 2003, 2004, 2005, 2006 Vladimir Prus
7# Distributed under the Boost Software License, Version 1.0.
8# (See accompanying file LICENSE_1_0.txt or http://www.boost.org/LICENSE_1_0.txt)
9
10import re
11import sys
12from functools import total_ordering
13
14from b2.util.utility import *
15from b2.build import feature
16from b2.util import sequence, qualify_jam_action, is_iterable_typed
17import b2.util.set
18from b2.manager import get_manager
19
20
21__re_two_ampersands = re.compile ('&&')
22__re_comma = re.compile (',')
23__re_split_condition = re.compile ('(.*):(<.*)')
24__re_split_conditional = re.compile (r'(.+):<(.+)')
25__re_colon = re.compile (':')
26__re_has_condition = re.compile (r':<')
27__re_separate_condition_and_property = re.compile (r'(.*):(<.*)')
28
29_not_applicable_feature='not-applicable-in-this-context'
30feature.feature(_not_applicable_feature, [], ['free'])
31
32__abbreviated_paths = False
33
34
35class PropertyMeta(type):
36    """
37    This class exists to implement the isinstance() and issubclass()
38    hooks for the Property class. Since we've introduce the concept of
39    a LazyProperty, isinstance(p, Property) will fail when p is a LazyProperty.
40    Implementing both __instancecheck__ and __subclasscheck__ will allow
41    LazyProperty instances to pass the isinstance() and issubclass check for
42    the Property class.
43
44    Additionally, the __call__ method intercepts the call to the Property
45    constructor to ensure that calling Property with the same arguments
46    will always return the same Property instance.
47    """
48    _registry = {}
49    current_id = 1
50
51    def __call__(mcs, f, value, condition=None):
52        """
53        This intercepts the call to the Property() constructor.
54
55        This exists so that the same arguments will always return the same Property
56        instance. This allows us to give each instance a unique ID.
57        """
58        from b2.build.feature import Feature
59        if not isinstance(f, Feature):
60            f = feature.get(f)
61        if condition is None:
62            condition = []
63        key = (f, value) + tuple(sorted(condition))
64        if key not in mcs._registry:
65            instance = super(PropertyMeta, mcs).__call__(f, value, condition)
66            mcs._registry[key] = instance
67        return mcs._registry[key]
68
69    @staticmethod
70    def check(obj):
71        return (hasattr(obj, 'feature') and
72                hasattr(obj, 'value') and
73                hasattr(obj, 'condition'))
74
75    def __instancecheck__(self, instance):
76        return self.check(instance)
77
78    def __subclasscheck__(self, subclass):
79        return self.check(subclass)
80
81
82@total_ordering
83class Property(object):
84
85    __slots__ = ('feature', 'value', 'condition', '_to_raw', '_hash', 'id')
86    __metaclass__ = PropertyMeta
87
88    def __init__(self, f, value, condition=None):
89        assert(f.free or ':' not in value)
90        if condition is None:
91            condition = []
92
93        self.feature = f
94        self.value = value
95        self.condition = condition
96        self._hash = hash((self.feature, self.value) + tuple(sorted(self.condition)))
97        self.id = PropertyMeta.current_id
98        # increment the id counter.
99        # this allows us to take a list of Property
100        # instances and use their unique integer ID
101        # to create a key for PropertySet caching. This is
102        # much faster than string comparison.
103        PropertyMeta.current_id += 1
104
105        condition_str = ''
106        if condition:
107            condition_str = ",".join(str(p) for p in self.condition) + ':'
108
109        self._to_raw = '{}<{}>{}'.format(condition_str, f.name, value)
110
111    def to_raw(self):
112        return self._to_raw
113
114    def __str__(self):
115
116        return self._to_raw
117
118    def __hash__(self):
119        return self._hash
120
121    def __eq__(self, other):
122        return self._hash == other._hash
123
124    def __lt__(self, other):
125        return (self.feature.name, self.value) < (other.feature.name, other.value)
126
127
128@total_ordering
129class LazyProperty(object):
130    def __init__(self, feature_name, value, condition=None):
131        if condition is None:
132            condition = []
133
134        self.__property = Property(
135            feature.get(_not_applicable_feature), feature_name + value, condition=condition)
136        self.__name = feature_name
137        self.__value = value
138        self.__condition = condition
139        self.__feature = None
140
141    def __getattr__(self, item):
142        if self.__feature is None:
143            try:
144                self.__feature = feature.get(self.__name)
145                self.__property = Property(self.__feature, self.__value, self.__condition)
146            except KeyError:
147                pass
148        return getattr(self.__property, item)
149
150    def __hash__(self):
151        return hash(self.__property)
152
153    def __str__(self):
154        return self.__property._to_raw
155
156    def __eq__(self, other):
157        return self.__property == other
158
159    def __lt__(self, other):
160        return (self.feature.name, self.value) < (other.feature.name, other.value)
161
162
163def create_from_string(s, allow_condition=False,allow_missing_value=False):
164    assert isinstance(s, basestring)
165    assert isinstance(allow_condition, bool)
166    assert isinstance(allow_missing_value, bool)
167    condition = []
168    import types
169    if not isinstance(s, types.StringType):
170        print type(s)
171    if __re_has_condition.search(s):
172
173        if not allow_condition:
174            raise BaseException("Conditional property is not allowed in this context")
175
176        m = __re_separate_condition_and_property.match(s)
177        condition = m.group(1)
178        s = m.group(2)
179
180    # FIXME: break dependency cycle
181    from b2.manager import get_manager
182
183    if condition:
184        condition = [create_from_string(x) for x in condition.split(',')]
185
186    feature_name = get_grist(s)
187    if not feature_name:
188        if feature.is_implicit_value(s):
189            f = feature.implied_feature(s)
190            value = s
191            p = Property(f, value, condition=condition)
192        else:
193            raise get_manager().errors()("Invalid property '%s' -- unknown feature" % s)
194    else:
195        value = get_value(s)
196        if not value and not allow_missing_value:
197            get_manager().errors()("Invalid property '%s' -- no value specified" % s)
198
199        if feature.valid(feature_name):
200            p = Property(feature.get(feature_name), value, condition=condition)
201        else:
202            # In case feature name is not known, it is wrong to do a hard error.
203            # Feature sets change depending on the toolset. So e.g.
204            # <toolset-X:version> is an unknown feature when using toolset Y.
205            #
206            # Ideally we would like to ignore this value, but most of
207            # Boost.Build code expects that we return a valid Property. For this
208            # reason we use a sentinel <not-applicable-in-this-context> feature.
209            #
210            # The underlying cause for this problem is that python port Property
211            # is more strict than its Jam counterpart and must always reference
212            # a valid feature.
213            p = LazyProperty(feature_name, value, condition=condition)
214
215    return p
216
217def create_from_strings(string_list, allow_condition=False):
218    assert is_iterable_typed(string_list, basestring)
219    return [create_from_string(s, allow_condition) for s in string_list]
220
221def reset ():
222    """ Clear the module state. This is mainly for testing purposes.
223    """
224    global __results
225
226    # A cache of results from as_path
227    __results = {}
228
229reset ()
230
231
232def set_abbreviated_paths(on=True):
233    global __abbreviated_paths
234    if on == 'off':
235        on = False
236    on = bool(on)
237    __abbreviated_paths = on
238
239
240def get_abbreviated_paths():
241    return __abbreviated_paths or '--abbreviated-paths' in sys.argv
242
243
244def path_order (x, y):
245    """ Helper for as_path, below. Orders properties with the implicit ones
246        first, and within the two sections in alphabetical order of feature
247        name.
248    """
249    if x == y:
250        return 0
251
252    xg = get_grist (x)
253    yg = get_grist (y)
254
255    if yg and not xg:
256        return -1
257
258    elif xg and not yg:
259        return 1
260
261    else:
262        if not xg:
263            x = feature.expand_subfeatures([x])
264            y = feature.expand_subfeatures([y])
265
266        if x < y:
267            return -1
268        elif x > y:
269            return 1
270        else:
271            return 0
272
273def identify(string):
274    return string
275
276# Uses Property
277def refine (properties, requirements):
278    """ Refines 'properties' by overriding any non-free properties
279        for which a different value is specified in 'requirements'.
280        Conditional requirements are just added without modification.
281        Returns the resulting list of properties.
282    """
283    assert is_iterable_typed(properties, Property)
284    assert is_iterable_typed(requirements, Property)
285    # The result has no duplicates, so we store it in a set
286    result = set()
287
288    # Records all requirements.
289    required = {}
290
291    # All the elements of requirements should be present in the result
292    # Record them so that we can handle 'properties'.
293    for r in requirements:
294        # Don't consider conditional requirements.
295        if not r.condition:
296            required[r.feature] = r
297
298    for p in properties:
299        # Skip conditional properties
300        if p.condition:
301            result.add(p)
302        # No processing for free properties
303        elif p.feature.free:
304            result.add(p)
305        else:
306            if p.feature in required:
307                result.add(required[p.feature])
308            else:
309                result.add(p)
310
311    return sequence.unique(list(result) + requirements)
312
313def translate_paths (properties, path):
314    """ Interpret all path properties in 'properties' as relative to 'path'
315        The property values are assumed to be in system-specific form, and
316        will be translated into normalized form.
317        """
318    assert is_iterable_typed(properties, Property)
319    result = []
320
321    for p in properties:
322
323        if p.feature.path:
324            values = __re_two_ampersands.split(p.value)
325
326            new_value = "&&".join(os.path.normpath(os.path.join(path, v)) for v in values)
327
328            if new_value != p.value:
329                result.append(Property(p.feature, new_value, p.condition))
330            else:
331                result.append(p)
332
333        else:
334            result.append (p)
335
336    return result
337
338def translate_indirect(properties, context_module):
339    """Assumes that all feature values that start with '@' are
340    names of rules, used in 'context-module'. Such rules can be
341    either local to the module or global. Qualified local rules
342    with the name of the module."""
343    assert is_iterable_typed(properties, Property)
344    assert isinstance(context_module, basestring)
345    result = []
346    for p in properties:
347        if p.value[0] == '@':
348            q = qualify_jam_action(p.value[1:], context_module)
349            get_manager().engine().register_bjam_action(q)
350            result.append(Property(p.feature, '@' + q, p.condition))
351        else:
352            result.append(p)
353
354    return result
355
356def validate (properties):
357    """ Exit with error if any of the properties is not valid.
358        properties may be a single property or a sequence of properties.
359    """
360    if isinstance(properties, Property):
361        properties = [properties]
362    assert is_iterable_typed(properties, Property)
363    for p in properties:
364        __validate1(p)
365
366def expand_subfeatures_in_conditions (properties):
367    assert is_iterable_typed(properties, Property)
368    result = []
369    for p in properties:
370
371        if not p.condition:
372            result.append(p)
373        else:
374            expanded = []
375            for c in p.condition:
376                # It common that condition includes a toolset which
377                # was never defined, or mentiones subfeatures which
378                # were never defined. In that case, validation will
379                # only produce an spirious error, so don't validate.
380                expanded.extend(feature.expand_subfeatures ([c], True))
381
382            # we need to keep LazyProperties lazy
383            if isinstance(p, LazyProperty):
384                value = p.value
385                feature_name = get_grist(value)
386                value = value.replace(feature_name, '')
387                result.append(LazyProperty(feature_name, value, condition=expanded))
388            else:
389                result.append(Property(p.feature, p.value, expanded))
390
391    return result
392
393# FIXME: this should go
394def split_conditional (property):
395    """ If 'property' is conditional property, returns
396        condition and the property, e.g
397        <variant>debug,<toolset>gcc:<inlining>full will become
398        <variant>debug,<toolset>gcc <inlining>full.
399        Otherwise, returns empty string.
400    """
401    assert isinstance(property, basestring)
402    m = __re_split_conditional.match (property)
403
404    if m:
405        return (m.group (1), '<' + m.group (2))
406
407    return None
408
409
410def select (features, properties):
411    """ Selects properties which correspond to any of the given features.
412    """
413    assert is_iterable_typed(properties, basestring)
414    result = []
415
416    # add any missing angle brackets
417    features = add_grist (features)
418
419    return [p for p in properties if get_grist(p) in features]
420
421def validate_property_sets (sets):
422    if __debug__:
423        from .property_set import PropertySet
424        assert is_iterable_typed(sets, PropertySet)
425    for s in sets:
426        validate(s.all())
427
428def evaluate_conditionals_in_context (properties, context):
429    """ Removes all conditional properties which conditions are not met
430        For those with met conditions, removes the condition. Properties
431        in conditions are looked up in 'context'
432    """
433    if __debug__:
434        from .property_set import PropertySet
435        assert is_iterable_typed(properties, Property)
436        assert isinstance(context, PropertySet)
437    base = []
438    conditional = []
439
440    for p in properties:
441        if p.condition:
442            conditional.append (p)
443        else:
444            base.append (p)
445
446    result = base[:]
447    for p in conditional:
448
449        # Evaluate condition
450        # FIXME: probably inefficient
451        if all(x in context for x in p.condition):
452            result.append(Property(p.feature, p.value))
453
454    return result
455
456
457def change (properties, feature, value = None):
458    """ Returns a modified version of properties with all values of the
459        given feature replaced by the given value.
460        If 'value' is None the feature will be removed.
461    """
462    assert is_iterable_typed(properties, basestring)
463    assert isinstance(feature, basestring)
464    assert isinstance(value, (basestring, type(None)))
465    result = []
466
467    feature = add_grist (feature)
468
469    for p in properties:
470        if get_grist (p) == feature:
471            if value:
472                result.append (replace_grist (value, feature))
473
474        else:
475            result.append (p)
476
477    return result
478
479
480################################################################
481# Private functions
482
483def __validate1 (property):
484    """ Exit with error if property is not valid.
485    """
486    assert isinstance(property, Property)
487    msg = None
488
489    if not property.feature.free:
490        feature.validate_value_string (property.feature, property.value)
491
492
493###################################################################
494# Still to port.
495# Original lines are prefixed with "#   "
496#
497#
498#   import utility : ungrist ;
499#   import sequence : unique ;
500#   import errors : error ;
501#   import feature ;
502#   import regex ;
503#   import sequence ;
504#   import set ;
505#   import path ;
506#   import assert ;
507#
508#
509
510
511#   rule validate-property-sets ( property-sets * )
512#   {
513#       for local s in $(property-sets)
514#       {
515#           validate [ feature.split $(s) ] ;
516#       }
517#   }
518#
519
520def remove(attributes, properties):
521    """Returns a property sets which include all the elements
522    in 'properties' that do not have attributes listed in 'attributes'."""
523    if isinstance(attributes, basestring):
524        attributes = [attributes]
525    assert is_iterable_typed(attributes, basestring)
526    assert is_iterable_typed(properties, basestring)
527    result = []
528    for e in properties:
529        attributes_new = feature.attributes(get_grist(e))
530        has_common_features = 0
531        for a in attributes_new:
532            if a in attributes:
533                has_common_features = 1
534                break
535
536        if not has_common_features:
537            result += e
538
539    return result
540
541
542def take(attributes, properties):
543    """Returns a property set which include all
544    properties in 'properties' that have any of 'attributes'."""
545    assert is_iterable_typed(attributes, basestring)
546    assert is_iterable_typed(properties, basestring)
547    result = []
548    for e in properties:
549        if b2.util.set.intersection(attributes, feature.attributes(get_grist(e))):
550            result.append(e)
551    return result
552
553def translate_dependencies(properties, project_id, location):
554    assert is_iterable_typed(properties, Property)
555    assert isinstance(project_id, basestring)
556    assert isinstance(location, basestring)
557    result = []
558    for p in properties:
559
560        if not p.feature.dependency:
561            result.append(p)
562        else:
563            v = p.value
564            m = re.match("(.*)//(.*)", v)
565            if m:
566                rooted = m.group(1)
567                if rooted[0] == '/':
568                    # Either project id or absolute Linux path, do nothing.
569                    pass
570                else:
571                    rooted = os.path.join(os.getcwd(), location, rooted)
572
573                result.append(Property(p.feature, rooted + "//" + m.group(2), p.condition))
574
575            elif os.path.isabs(v):
576                result.append(p)
577            else:
578                result.append(Property(p.feature, project_id + "//" + v, p.condition))
579
580    return result
581
582
583class PropertyMap:
584    """ Class which maintains a property set -> string mapping.
585    """
586    def __init__ (self):
587        self.__properties = []
588        self.__values = []
589
590    def insert (self, properties, value):
591        """ Associate value with properties.
592        """
593        assert is_iterable_typed(properties, basestring)
594        assert isinstance(value, basestring)
595        self.__properties.append(properties)
596        self.__values.append(value)
597
598    def find (self, properties):
599        """ Return the value associated with properties
600        or any subset of it. If more than one
601        subset has value assigned to it, return the
602        value for the longest subset, if it's unique.
603        """
604        assert is_iterable_typed(properties, basestring)
605        return self.find_replace (properties)
606
607    def find_replace(self, properties, value=None):
608        assert is_iterable_typed(properties, basestring)
609        assert isinstance(value, (basestring, type(None)))
610        matches = []
611        match_ranks = []
612
613        for i in range(0, len(self.__properties)):
614            p = self.__properties[i]
615
616            if b2.util.set.contains (p, properties):
617                matches.append (i)
618                match_ranks.append(len(p))
619
620        best = sequence.select_highest_ranked (matches, match_ranks)
621
622        if not best:
623            return None
624
625        if len (best) > 1:
626            raise NoBestMatchingAlternative ()
627
628        best = best [0]
629
630        original = self.__values[best]
631
632        if value:
633            self.__values[best] = value
634
635        return original
636
637#   local rule __test__ ( )
638#   {
639#       import errors : try catch ;
640#       import feature ;
641#       import feature : feature subfeature compose ;
642#
643#       # local rules must be explicitly re-imported
644#       import property : path-order ;
645#
646#       feature.prepare-test property-test-temp ;
647#
648#       feature toolset : gcc : implicit symmetric ;
649#       subfeature toolset gcc : version : 2.95.2 2.95.3 2.95.4
650#         3.0 3.0.1 3.0.2 : optional ;
651#       feature define : : free ;
652#       feature runtime-link : dynamic static : symmetric link-incompatible ;
653#       feature optimization : on off ;
654#       feature variant : debug release : implicit composite symmetric ;
655#       feature rtti : on off : link-incompatible ;
656#
657#       compose <variant>debug : <define>_DEBUG <optimization>off ;
658#       compose <variant>release : <define>NDEBUG <optimization>on ;
659#
660#       import assert ;
661#       import "class" : new ;
662#
663#       validate <toolset>gcc  <toolset>gcc-3.0.1 : $(test-space) ;
664#
665#       assert.result <toolset>gcc <rtti>off <define>FOO
666#           : refine <toolset>gcc <rtti>off
667#           : <define>FOO
668#           : $(test-space)
669#           ;
670#
671#       assert.result <toolset>gcc <optimization>on
672#           : refine <toolset>gcc <optimization>off
673#           : <optimization>on
674#           : $(test-space)
675#           ;
676#
677#       assert.result <toolset>gcc <rtti>off
678#           : refine <toolset>gcc : <rtti>off : $(test-space)
679#           ;
680#
681#       assert.result <toolset>gcc <rtti>off <rtti>off:<define>FOO
682#           : refine <toolset>gcc : <rtti>off <rtti>off:<define>FOO
683#           : $(test-space)
684#           ;
685#
686#       assert.result <toolset>gcc:<define>foo <toolset>gcc:<define>bar
687#           : refine <toolset>gcc:<define>foo : <toolset>gcc:<define>bar
688#           : $(test-space)
689#           ;
690#
691#       assert.result <define>MY_RELEASE
692#           : evaluate-conditionals-in-context
693#             <variant>release,<rtti>off:<define>MY_RELEASE
694#             : <toolset>gcc <variant>release <rtti>off
695#
696#           ;
697#
698#       try ;
699#           validate <feature>value : $(test-space) ;
700#       catch "Invalid property '<feature>value': unknown feature 'feature'." ;
701#
702#       try ;
703#           validate <rtti>default : $(test-space) ;
704#       catch \"default\" is not a known value of feature <rtti> ;
705#
706#       validate <define>WHATEVER : $(test-space) ;
707#
708#       try ;
709#           validate <rtti> : $(test-space) ;
710#       catch "Invalid property '<rtti>': No value specified for feature 'rtti'." ;
711#
712#       try ;
713#           validate value : $(test-space) ;
714#       catch "value" is not a value of an implicit feature ;
715#
716#
717#       assert.result <rtti>on
718#           : remove free implicit : <toolset>gcc <define>foo <rtti>on : $(test-space) ;
719#
720#       assert.result <include>a
721#           : select include : <include>a <toolset>gcc ;
722#
723#       assert.result <include>a
724#           : select include bar : <include>a <toolset>gcc ;
725#
726#       assert.result <include>a <toolset>gcc
727#           : select include <bar> <toolset> : <include>a <toolset>gcc ;
728#
729#       assert.result <toolset>kylix <include>a
730#           : change <toolset>gcc <include>a : <toolset> kylix ;
731#
732#       # Test ordinary properties
733#       assert.result
734#         : split-conditional <toolset>gcc
735#         ;
736#
737#       # Test properties with ":"
738#       assert.result
739#         : split-conditional <define>FOO=A::B
740#         ;
741#
742#       # Test conditional feature
743#       assert.result <toolset>gcc,<toolset-gcc:version>3.0 <define>FOO
744#         : split-conditional <toolset>gcc,<toolset-gcc:version>3.0:<define>FOO
745#         ;
746#
747#       feature.finish-test property-test-temp ;
748#   }
749#
750
751