1# Status: ported.
2# Base revision: 40480
3
4#  Copyright (C) Vladimir Prus 2002. Permission to copy, use, modify, sell and
5#  distribute this software is granted provided this copyright notice appears in
6#  all copies. This software is provided "as is" without express or implied
7#  warranty, and with no claim as to its suitability for any purpose.
8
9import hashlib
10
11from b2.util.utility import *
12import property, feature
13import b2.build.feature
14from b2.exceptions import *
15from b2.build.property import get_abbreviated_paths
16from b2.util.sequence import unique
17from b2.util.set import difference
18from b2.util import cached, abbreviate_dashed
19
20from b2.manager import get_manager
21
22
23def reset ():
24    """ Clear the module state. This is mainly for testing purposes.
25    """
26    global __cache
27
28    # A cache of property sets
29    # TODO: use a map of weak refs?
30    __cache = {}
31
32reset ()
33
34
35def create (raw_properties = []):
36    """ Creates a new 'PropertySet' instance for the given raw properties,
37        or returns an already existing one.
38    """
39    # FIXME: propagate to callers.
40    if len(raw_properties) > 0 and isinstance(raw_properties[0], property.Property):
41        x = raw_properties
42    else:
43        x = [property.create_from_string(ps) for ps in raw_properties]
44    x.sort()
45    x = unique(x, stable=True)
46
47    # FIXME: can we do better, e.g. by directly computing
48    # hash value of the list?
49    key = tuple(x)
50
51    if not __cache.has_key (key):
52        __cache [key] = PropertySet(x)
53
54    return __cache [key]
55
56def create_with_validation (raw_properties):
57    """ Creates new 'PropertySet' instances after checking
58        that all properties are valid and converting implicit
59        properties into gristed form.
60    """
61    properties = [property.create_from_string(s) for s in raw_properties]
62    property.validate(properties)
63
64    return create(properties)
65
66def empty ():
67    """ Returns PropertySet with empty set of properties.
68    """
69    return create ()
70
71def create_from_user_input(raw_properties, jamfile_module, location):
72    """Creates a property-set from the input given by the user, in the
73    context of 'jamfile-module' at 'location'"""
74
75    properties = property.create_from_strings(raw_properties, True)
76    properties = property.translate_paths(properties, location)
77    properties = property.translate_indirect(properties, jamfile_module)
78
79    project_id = get_manager().projects().attributeDefault(jamfile_module, 'id', None)
80    if not project_id:
81        project_id = os.path.abspath(location)
82    properties = property.translate_dependencies(properties, project_id, location)
83    properties = property.expand_subfeatures_in_conditions(properties)
84    return create(properties)
85
86
87def refine_from_user_input(parent_requirements, specification, jamfile_module,
88                           location):
89    """Refines requirements with requirements provided by the user.
90    Specially handles "-<property>value" syntax in specification
91     to remove given requirements.
92     - parent-requirements -- property-set object with requirements
93       to refine
94     - specification -- string list of requirements provided by the use
95     - project-module -- the module to which context indirect features
96       will be bound.
97     - location -- the path to which path features are relative."""
98
99
100    if not specification:
101        return parent_requirements
102
103
104    add_requirements = []
105    remove_requirements = []
106
107    for r in specification:
108        if r[0] == '-':
109            remove_requirements.append(r[1:])
110        else:
111            add_requirements.append(r)
112
113    if remove_requirements:
114        # Need to create property set, so that path features
115        # and indirect features are translated just like they
116        # are in project requirements.
117        ps = create_from_user_input(remove_requirements,
118                                    jamfile_module, location)
119
120        parent_requirements = create(difference(parent_requirements.all(),
121                                                ps.all()))
122        specification = add_requirements
123
124    requirements = create_from_user_input(specification,
125                                          jamfile_module, location)
126
127    return parent_requirements.refine(requirements)
128
129class PropertySet:
130    """ Class for storing a set of properties.
131        - there's 1<->1 correspondence between identity and value. No
132          two instances of the class are equal. To maintain this property,
133          the 'PropertySet.create' rule should be used to create new instances.
134          Instances are immutable.
135
136        - each property is classified with regard to it's effect on build
137          results. Incidental properties have no effect on build results, from
138          Boost.Build point of view. Others are either free, or non-free, which we
139          call 'base'. Each property belong to exactly one of those categories and
140          it's possible to get list of properties in each category.
141
142          In addition, it's possible to get list of properties with specific
143          attribute.
144
145        - several operations, like and refine and as_path are provided. They all use
146          caching whenever possible.
147    """
148    def __init__ (self, properties = []):
149
150
151        raw_properties = []
152        for p in properties:
153            raw_properties.append(p.to_raw())
154
155        self.all_ = properties
156        self.all_raw_ = raw_properties
157        self.all_set_ = set(properties)
158
159        self.incidental_ = []
160        self.free_ = []
161        self.base_ = []
162        self.dependency_ = []
163        self.non_dependency_ = []
164        self.conditional_ = []
165        self.non_conditional_ = []
166        self.propagated_ = []
167        self.link_incompatible = []
168
169        # A cache of refined properties.
170        self.refined_ = {}
171
172        # A cache of property sets created by adding properties to this one.
173        self.added_ = {}
174
175        # Cache for the default properties.
176        self.defaults_ = None
177
178        # Cache for the expanded properties.
179        self.expanded_ = None
180
181        # Cache for the expanded composite properties
182        self.composites_ = None
183
184        # Cache for property set with expanded subfeatures
185        self.subfeatures_ = None
186
187        # Cache for the property set containing propagated properties.
188        self.propagated_ps_ = None
189
190        # A map of features to its values.
191        self.feature_map_ = None
192
193        # A tuple (target path, is relative to build directory)
194        self.target_path_ = None
195
196        self.as_path_ = None
197
198        # A cache for already evaluated sets.
199        self.evaluated_ = {}
200
201        for p in raw_properties:
202            if not get_grist (p):
203                raise BaseException ("Invalid property: '%s'" % p)
204
205            att = feature.attributes (get_grist (p))
206
207            if 'propagated' in att:
208                self.propagated_.append (p)
209
210            if 'link_incompatible' in att:
211                self.link_incompatible.append (p)
212
213        for p in properties:
214
215            # A feature can be both incidental and free,
216            # in which case we add it to incidental.
217            if p.feature().incidental():
218                self.incidental_.append(p)
219            elif p.feature().free():
220                self.free_.append(p)
221            else:
222                self.base_.append(p)
223
224            if p.condition():
225                self.conditional_.append(p)
226            else:
227                self.non_conditional_.append(p)
228
229            if p.feature().dependency():
230                self.dependency_.append (p)
231            else:
232                self.non_dependency_.append (p)
233
234
235    def all(self):
236        return self.all_
237
238    def raw (self):
239        """ Returns the list of stored properties.
240        """
241        return self.all_raw_
242
243    def __str__(self):
244        return ' '.join(str(p) for p in self.all_)
245
246    def base (self):
247        """ Returns properties that are neither incidental nor free.
248        """
249        return self.base_
250
251    def free (self):
252        """ Returns free properties which are not dependency properties.
253        """
254        return self.free_
255
256    def non_free(self):
257        return self.base_ + self.incidental_
258
259    def dependency (self):
260        """ Returns dependency properties.
261        """
262        return self.dependency_
263
264    def non_dependency (self):
265        """ Returns properties that are not dependencies.
266        """
267        return self.non_dependency_
268
269    def conditional (self):
270        """ Returns conditional properties.
271        """
272        return self.conditional_
273
274    def non_conditional (self):
275        """ Returns properties that are not conditional.
276        """
277        return self.non_conditional_
278
279    def incidental (self):
280        """ Returns incidental properties.
281        """
282        return self.incidental_
283
284    def refine (self, requirements):
285        """ Refines this set's properties using the requirements passed as an argument.
286        """
287        assert isinstance(requirements, PropertySet)
288        if not self.refined_.has_key (requirements):
289            r = property.refine(self.all_, requirements.all_)
290
291            self.refined_[requirements] = create(r)
292
293        return self.refined_[requirements]
294
295    def expand (self):
296        if not self.expanded_:
297            expanded = feature.expand(self.all_)
298            self.expanded_ = create(expanded)
299        return self.expanded_
300
301    def expand_subfeatures(self):
302        if not self.subfeatures_:
303            self.subfeatures_ = create(feature.expand_subfeatures(self.all_))
304        return self.subfeatures_
305
306    def evaluate_conditionals(self, context=None):
307        if not context:
308            context = self
309
310        if not self.evaluated_.has_key(context):
311            # FIXME: figure why the call messes up first parameter
312            self.evaluated_[context] = create(
313                property.evaluate_conditionals_in_context(self.all(), context))
314
315        return self.evaluated_[context]
316
317    def propagated (self):
318        if not self.propagated_ps_:
319            self.propagated_ps_ = create (self.propagated_)
320        return self.propagated_ps_
321
322    def add_defaults (self):
323        # FIXME: this caching is invalidated when new features
324        # are declare inside non-root Jamfiles.
325        if not self.defaults_:
326            expanded = feature.add_defaults(self.all_)
327            self.defaults_ = create(expanded)
328        return self.defaults_
329
330    def as_path (self):
331        if not self.as_path_:
332
333            def path_order (p1, p2):
334
335                i1 = p1.feature().implicit()
336                i2 = p2.feature().implicit()
337
338                if i1 != i2:
339                    return i2 - i1
340                else:
341                    return cmp(p1.feature().name(), p2.feature().name())
342
343            # trim redundancy
344            properties = feature.minimize(self.base_)
345
346            # sort according to path_order
347            properties.sort (path_order)
348
349            components = []
350            for p in properties:
351                if p.feature().implicit():
352                    components.append(p.value())
353                else:
354                    value = p.feature().name() + "-" + p.value()
355                    if property.get_abbreviated_paths():
356                        value = abbreviate_dashed(value)
357                    components.append(value)
358
359            self.as_path_ = '/'.join (components)
360
361        return self.as_path_
362
363    def target_path (self):
364        """ Computes the target path that should be used for
365            target with these properties.
366            Returns a tuple of
367              - the computed path
368              - if the path is relative to build directory, a value of
369                'true'.
370        """
371        if not self.target_path_:
372            # The <location> feature can be used to explicitly
373            # change the location of generated targets
374            l = self.get ('<location>')
375            if l:
376                computed = l[0]
377                is_relative = False
378
379            else:
380                p = self.as_path()
381                if hash_maybe:
382                    p = hash_maybe(p)
383
384                # Really, an ugly hack. Boost regression test system requires
385                # specific target paths, and it seems that changing it to handle
386                # other directory layout is really hard. For that reason,
387                # we teach V2 to do the things regression system requires.
388                # The value o '<location-prefix>' is predended to the path.
389                prefix = self.get ('<location-prefix>')
390
391                if prefix:
392                    if len (prefix) > 1:
393                        raise AlreadyDefined ("Two <location-prefix> properties specified: '%s'" % prefix)
394
395                    computed = os.path.join(prefix[0], p)
396
397                else:
398                    computed = p
399
400                if not computed:
401                    computed = "."
402
403                is_relative = True
404
405            self.target_path_ = (computed, is_relative)
406
407        return self.target_path_
408
409    def add (self, ps):
410        """ Creates a new property set containing the properties in this one,
411            plus the ones of the property set passed as argument.
412        """
413        if not self.added_.has_key(ps):
414            self.added_[ps] = create(self.all_ + ps.all())
415        return self.added_[ps]
416
417    def add_raw (self, properties):
418        """ Creates a new property set containing the properties in this one,
419            plus the ones passed as argument.
420        """
421        return self.add (create (properties))
422
423
424    def get (self, feature):
425        """ Returns all values of 'feature'.
426        """
427        if type(feature) == type([]):
428            feature = feature[0]
429        if not isinstance(feature, b2.build.feature.Feature):
430            feature = b2.build.feature.get(feature)
431
432        if not self.feature_map_:
433            self.feature_map_ = {}
434
435            for v in self.all_:
436                if not self.feature_map_.has_key(v.feature()):
437                    self.feature_map_[v.feature()] = []
438                self.feature_map_[v.feature()].append(v.value())
439
440        return self.feature_map_.get(feature, [])
441
442    @cached
443    def get_properties(self, feature):
444        """Returns all contained properties associated with 'feature'"""
445
446        if not isinstance(feature, b2.build.feature.Feature):
447            feature = b2.build.feature.get(feature)
448
449        result = []
450        for p in self.all_:
451            if p.feature() == feature:
452                result.append(p)
453        return result
454
455    def __contains__(self, item):
456        return item in self.all_set_
457
458def hash(p):
459    m = hashlib.md5()
460    m.update(p)
461    return m.hexdigest()
462
463hash_maybe = hash if "--hash" in bjam.variable("ARGV") else None
464
465