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