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