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 b2.util.utility import *
13from b2.build import feature
14from b2.util import sequence, qualify_jam_action
15import b2.util.set
16from b2.manager import get_manager
17
18__re_two_ampersands = re.compile ('&&')
19__re_comma = re.compile (',')
20__re_split_condition = re.compile ('(.*):(<.*)')
21__re_split_conditional = re.compile (r'(.+):<(.+)')
22__re_colon = re.compile (':')
23__re_has_condition = re.compile (r':<')
24__re_separate_condition_and_property = re.compile (r'(.*):(<.*)')
25
26__not_applicable_feature='not-applicable-in-this-context'
27feature.feature(__not_applicable_feature, [], ['free'])
28
29__abbreviated_paths = False
30
31class Property(object):
32
33    __slots__ = ('_feature', '_value', '_condition')
34
35    def __init__(self, f, value, condition = []):
36        if type(f) == type(""):
37            f = feature.get(f)
38        # At present, single property has a single value.
39        assert type(value) != type([])
40        assert(f.free() or value.find(':') == -1)
41        self._feature = f
42        self._value = value
43        self._condition = condition
44
45    def feature(self):
46        return self._feature
47
48    def value(self):
49        return self._value
50
51    def condition(self):
52        return self._condition
53
54    def to_raw(self):
55        result = "<" + self._feature.name() + ">" + str(self._value)
56        if self._condition:
57            result = ",".join(str(p) for p in self._condition) + ':' + result
58        return result
59
60    def __str__(self):
61        return self.to_raw()
62
63    def __hash__(self):
64        # FIXME: consider if this class should be value-is-identity one
65        return hash((self._feature, self._value, tuple(self._condition)))
66
67    def __cmp__(self, other):
68        return cmp((self._feature.name(), self._value, self._condition),
69                   (other._feature.name(), other._value, other._condition))
70
71
72def create_from_string(s, allow_condition=False,allow_missing_value=False):
73
74    condition = []
75    import types
76    if not isinstance(s, types.StringType):
77        print type(s)
78    if __re_has_condition.search(s):
79
80        if not allow_condition:
81            raise BaseException("Conditional property is not allowed in this context")
82
83        m = __re_separate_condition_and_property.match(s)
84        condition = m.group(1)
85        s = m.group(2)
86
87    # FIXME: break dependency cycle
88    from b2.manager import get_manager
89
90    feature_name = get_grist(s)
91    if not feature_name:
92        if feature.is_implicit_value(s):
93            f = feature.implied_feature(s)
94            value = s
95        else:
96            raise get_manager().errors()("Invalid property '%s' -- unknown feature" % s)
97    else:
98        if feature.valid(feature_name):
99            f = feature.get(feature_name)
100            value = get_value(s)
101        else:
102            # In case feature name is not known, it is wrong to do a hard error.
103            # Feature sets change depending on the toolset. So e.g.
104            # <toolset-X:version> is an unknown feature when using toolset Y.
105            #
106            # Ideally we would like to ignore this value, but most of
107            # Boost.Build code expects that we return a valid Property. For this
108            # reason we use a sentinel <not-applicable-in-this-context> feature.
109            #
110            # The underlying cause for this problem is that python port Property
111            # is more strict than its Jam counterpart and must always reference
112            # a valid feature.
113            f = feature.get(__not_applicable_feature)
114            value = s
115
116        if not value and not allow_missing_value:
117            get_manager().errors()("Invalid property '%s' -- no value specified" % s)
118
119
120    if condition:
121        condition = [create_from_string(x) for x in condition.split(',')]
122
123    return Property(f, value, condition)
124
125def create_from_strings(string_list, allow_condition=False):
126
127    return [create_from_string(s, allow_condition) for s in string_list]
128
129def reset ():
130    """ Clear the module state. This is mainly for testing purposes.
131    """
132    global __results
133
134    # A cache of results from as_path
135    __results = {}
136
137reset ()
138
139
140def set_abbreviated_paths(on=True):
141    global __abbreviated_paths
142    __abbreviated_paths = on
143
144
145def get_abbreviated_paths():
146    return __abbreviated_paths or '--abbreviated-paths' in sys.argv
147
148
149def path_order (x, y):
150    """ Helper for as_path, below. Orders properties with the implicit ones
151        first, and within the two sections in alphabetical order of feature
152        name.
153    """
154    if x == y:
155        return 0
156
157    xg = get_grist (x)
158    yg = get_grist (y)
159
160    if yg and not xg:
161        return -1
162
163    elif xg and not yg:
164        return 1
165
166    else:
167        if not xg:
168            x = feature.expand_subfeatures([x])
169            y = feature.expand_subfeatures([y])
170
171        if x < y:
172            return -1
173        elif x > y:
174            return 1
175        else:
176            return 0
177
178def identify(string):
179    return string
180
181# Uses Property
182def refine (properties, requirements):
183    """ Refines 'properties' by overriding any non-free properties
184        for which a different value is specified in 'requirements'.
185        Conditional requirements are just added without modification.
186        Returns the resulting list of properties.
187    """
188    # The result has no duplicates, so we store it in a set
189    result = set()
190
191    # Records all requirements.
192    required = {}
193
194    # All the elements of requirements should be present in the result
195    # Record them so that we can handle 'properties'.
196    for r in requirements:
197        # Don't consider conditional requirements.
198        if not r.condition():
199            required[r.feature()] = r
200
201    for p in properties:
202        # Skip conditional properties
203        if p.condition():
204            result.add(p)
205        # No processing for free properties
206        elif p.feature().free():
207            result.add(p)
208        else:
209            if required.has_key(p.feature()):
210                result.add(required[p.feature()])
211            else:
212                result.add(p)
213
214    return sequence.unique(list(result) + requirements)
215
216def translate_paths (properties, path):
217    """ Interpret all path properties in 'properties' as relative to 'path'
218        The property values are assumed to be in system-specific form, and
219        will be translated into normalized form.
220        """
221    result = []
222
223    for p in properties:
224
225        if p.feature().path():
226            values = __re_two_ampersands.split(p.value())
227
228            new_value = "&&".join(os.path.join(path, v) for v in values)
229
230            if new_value != p.value():
231                result.append(Property(p.feature(), new_value, p.condition()))
232            else:
233                result.append(p)
234
235        else:
236            result.append (p)
237
238    return result
239
240def translate_indirect(properties, context_module):
241    """Assumes that all feature values that start with '@' are
242    names of rules, used in 'context-module'. Such rules can be
243    either local to the module or global. Qualified local rules
244    with the name of the module."""
245    result = []
246    for p in properties:
247        if p.value()[0] == '@':
248            q = qualify_jam_action(p.value()[1:], context_module)
249            get_manager().engine().register_bjam_action(q)
250            result.append(Property(p.feature(), '@' + q, p.condition()))
251        else:
252            result.append(p)
253
254    return result
255
256def validate (properties):
257    """ Exit with error if any of the properties is not valid.
258        properties may be a single property or a sequence of properties.
259    """
260
261    if isinstance (properties, str):
262        __validate1 (properties)
263    else:
264        for p in properties:
265            __validate1 (p)
266
267def expand_subfeatures_in_conditions (properties):
268
269    result = []
270    for p in properties:
271
272        if not p.condition():
273            result.append(p)
274        else:
275            expanded = []
276            for c in p.condition():
277
278                if c.feature().name().startswith("toolset") or c.feature().name() == "os":
279                    # It common that condition includes a toolset which
280                    # was never defined, or mentiones subfeatures which
281                    # were never defined. In that case, validation will
282                    # only produce an spirious error, so don't validate.
283                    expanded.extend(feature.expand_subfeatures ([c], True))
284                else:
285                    expanded.extend(feature.expand_subfeatures([c]))
286
287            result.append(Property(p.feature(), p.value(), expanded))
288
289    return result
290
291# FIXME: this should go
292def split_conditional (property):
293    """ If 'property' is conditional property, returns
294        condition and the property, e.g
295        <variant>debug,<toolset>gcc:<inlining>full will become
296        <variant>debug,<toolset>gcc <inlining>full.
297        Otherwise, returns empty string.
298    """
299    m = __re_split_conditional.match (property)
300
301    if m:
302        return (m.group (1), '<' + m.group (2))
303
304    return None
305
306
307def select (features, properties):
308    """ Selects properties which correspond to any of the given features.
309    """
310    result = []
311
312    # add any missing angle brackets
313    features = add_grist (features)
314
315    return [p for p in properties if get_grist(p) in features]
316
317def validate_property_sets (sets):
318    for s in sets:
319        validate(s.all())
320
321def evaluate_conditionals_in_context (properties, context):
322    """ Removes all conditional properties which conditions are not met
323        For those with met conditions, removes the condition. Properies
324        in conditions are looked up in 'context'
325    """
326    base = []
327    conditional = []
328
329    for p in properties:
330        if p.condition():
331            conditional.append (p)
332        else:
333            base.append (p)
334
335    result = base[:]
336    for p in conditional:
337
338        # Evaluate condition
339        # FIXME: probably inefficient
340        if all(x in context for x in p.condition()):
341            result.append(Property(p.feature(), p.value()))
342
343    return result
344
345
346def change (properties, feature, value = None):
347    """ Returns a modified version of properties with all values of the
348        given feature replaced by the given value.
349        If 'value' is None the feature will be removed.
350    """
351    result = []
352
353    feature = add_grist (feature)
354
355    for p in properties:
356        if get_grist (p) == feature:
357            if value:
358                result.append (replace_grist (value, feature))
359
360        else:
361            result.append (p)
362
363    return result
364
365
366################################################################
367# Private functions
368
369def __validate1 (property):
370    """ Exit with error if property is not valid.
371    """
372    msg = None
373
374    if not property.feature().free():
375        feature.validate_value_string (property.feature(), property.value())
376
377
378###################################################################
379# Still to port.
380# Original lines are prefixed with "#   "
381#
382#
383#   import utility : ungrist ;
384#   import sequence : unique ;
385#   import errors : error ;
386#   import feature ;
387#   import regex ;
388#   import sequence ;
389#   import set ;
390#   import path ;
391#   import assert ;
392#
393#
394
395
396#   rule validate-property-sets ( property-sets * )
397#   {
398#       for local s in $(property-sets)
399#       {
400#           validate [ feature.split $(s) ] ;
401#       }
402#   }
403#
404
405def remove(attributes, properties):
406    """Returns a property sets which include all the elements
407    in 'properties' that do not have attributes listed in 'attributes'."""
408
409    result = []
410    for e in properties:
411        attributes_new = feature.attributes(get_grist(e))
412        has_common_features = 0
413        for a in attributes_new:
414            if a in attributes:
415                has_common_features = 1
416                break
417
418        if not has_common_features:
419            result += e
420
421    return result
422
423
424def take(attributes, properties):
425    """Returns a property set which include all
426    properties in 'properties' that have any of 'attributes'."""
427    result = []
428    for e in properties:
429        if b2.util.set.intersection(attributes, feature.attributes(get_grist(e))):
430            result.append(e)
431    return result
432
433def translate_dependencies(properties, project_id, location):
434
435    result = []
436    for p in properties:
437
438        if not p.feature().dependency():
439            result.append(p)
440        else:
441            v = p.value()
442            m = re.match("(.*)//(.*)", v)
443            if m:
444                rooted = m.group(1)
445                if rooted[0] == '/':
446                    # Either project id or absolute Linux path, do nothing.
447                    pass
448                else:
449                    rooted = os.path.join(os.getcwd(), location, rooted)
450
451                result.append(Property(p.feature(), rooted + "//" + m.group(2), p.condition()))
452
453            elif os.path.isabs(v):
454                result.append(p)
455            else:
456                result.append(Property(p.feature(), project_id + "//" + v, p.condition()))
457
458    return result
459
460
461class PropertyMap:
462    """ Class which maintains a property set -> string mapping.
463    """
464    def __init__ (self):
465        self.__properties = []
466        self.__values = []
467
468    def insert (self, properties, value):
469        """ Associate value with properties.
470        """
471        self.__properties.append(properties)
472        self.__values.append(value)
473
474    def find (self, properties):
475        """ Return the value associated with properties
476        or any subset of it. If more than one
477        subset has value assigned to it, return the
478        value for the longest subset, if it's unique.
479        """
480        return self.find_replace (properties)
481
482    def find_replace(self, properties, value=None):
483        matches = []
484        match_ranks = []
485
486        for i in range(0, len(self.__properties)):
487            p = self.__properties[i]
488
489            if b2.util.set.contains (p, properties):
490                matches.append (i)
491                match_ranks.append(len(p))
492
493        best = sequence.select_highest_ranked (matches, match_ranks)
494
495        if not best:
496            return None
497
498        if len (best) > 1:
499            raise NoBestMatchingAlternative ()
500
501        best = best [0]
502
503        original = self.__values[best]
504
505        if value:
506            self.__values[best] = value
507
508        return original
509
510#   local rule __test__ ( )
511#   {
512#       import errors : try catch ;
513#       import feature ;
514#       import feature : feature subfeature compose ;
515#
516#       # local rules must be explicitly re-imported
517#       import property : path-order ;
518#
519#       feature.prepare-test property-test-temp ;
520#
521#       feature toolset : gcc : implicit symmetric ;
522#       subfeature toolset gcc : version : 2.95.2 2.95.3 2.95.4
523#         3.0 3.0.1 3.0.2 : optional ;
524#       feature define : : free ;
525#       feature runtime-link : dynamic static : symmetric link-incompatible ;
526#       feature optimization : on off ;
527#       feature variant : debug release : implicit composite symmetric ;
528#       feature rtti : on off : link-incompatible ;
529#
530#       compose <variant>debug : <define>_DEBUG <optimization>off ;
531#       compose <variant>release : <define>NDEBUG <optimization>on ;
532#
533#       import assert ;
534#       import "class" : new ;
535#
536#       validate <toolset>gcc  <toolset>gcc-3.0.1 : $(test-space) ;
537#
538#       assert.result <toolset>gcc <rtti>off <define>FOO
539#           : refine <toolset>gcc <rtti>off
540#           : <define>FOO
541#           : $(test-space)
542#           ;
543#
544#       assert.result <toolset>gcc <optimization>on
545#           : refine <toolset>gcc <optimization>off
546#           : <optimization>on
547#           : $(test-space)
548#           ;
549#
550#       assert.result <toolset>gcc <rtti>off
551#           : refine <toolset>gcc : <rtti>off : $(test-space)
552#           ;
553#
554#       assert.result <toolset>gcc <rtti>off <rtti>off:<define>FOO
555#           : refine <toolset>gcc : <rtti>off <rtti>off:<define>FOO
556#           : $(test-space)
557#           ;
558#
559#       assert.result <toolset>gcc:<define>foo <toolset>gcc:<define>bar
560#           : refine <toolset>gcc:<define>foo : <toolset>gcc:<define>bar
561#           : $(test-space)
562#           ;
563#
564#       assert.result <define>MY_RELEASE
565#           : evaluate-conditionals-in-context
566#             <variant>release,<rtti>off:<define>MY_RELEASE
567#             : <toolset>gcc <variant>release <rtti>off
568#
569#           ;
570#
571#       try ;
572#           validate <feature>value : $(test-space) ;
573#       catch "Invalid property '<feature>value': unknown feature 'feature'." ;
574#
575#       try ;
576#           validate <rtti>default : $(test-space) ;
577#       catch \"default\" is not a known value of feature <rtti> ;
578#
579#       validate <define>WHATEVER : $(test-space) ;
580#
581#       try ;
582#           validate <rtti> : $(test-space) ;
583#       catch "Invalid property '<rtti>': No value specified for feature 'rtti'." ;
584#
585#       try ;
586#           validate value : $(test-space) ;
587#       catch "value" is not a value of an implicit feature ;
588#
589#
590#       assert.result <rtti>on
591#           : remove free implicit : <toolset>gcc <define>foo <rtti>on : $(test-space) ;
592#
593#       assert.result <include>a
594#           : select include : <include>a <toolset>gcc ;
595#
596#       assert.result <include>a
597#           : select include bar : <include>a <toolset>gcc ;
598#
599#       assert.result <include>a <toolset>gcc
600#           : select include <bar> <toolset> : <include>a <toolset>gcc ;
601#
602#       assert.result <toolset>kylix <include>a
603#           : change <toolset>gcc <include>a : <toolset> kylix ;
604#
605#       # Test ordinary properties
606#       assert.result
607#         : split-conditional <toolset>gcc
608#         ;
609#
610#       # Test properties with ":"
611#       assert.result
612#         : split-conditional <define>FOO=A::B
613#         ;
614#
615#       # Test conditional feature
616#       assert.result <toolset>gcc,<toolset-gcc:version>3.0 <define>FOO
617#         : split-conditional <toolset>gcc,<toolset-gcc:version>3.0:<define>FOO
618#         ;
619#
620#       feature.finish-test property-test-temp ;
621#   }
622#
623
624