1# Status: ported. 2# Base revision: 64488 3 4# Copyright 2002, 2003 Dave Abrahams 5# Copyright 2002, 2005, 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 copy at 9# http://www.boost.org/LICENSE_1_0.txt) 10 11# Implements project representation and loading. Each project is represented 12# by: 13# - a module where all the Jamfile content live. 14# - an instance of 'project-attributes' class. 15# (given a module name, can be obtained using the 'attributes' rule) 16# - an instance of 'project-target' class (from targets.jam) 17# (given a module name, can be obtained using the 'target' rule) 18# 19# Typically, projects are created as result of loading a Jamfile, which is done 20# by rules 'load' and 'initialize', below. First, module for Jamfile is loaded 21# and new project-attributes instance is created. Some rules necessary for 22# project are added to the module (see 'project-rules' module) at the bottom of 23# this file. Default project attributes are set (inheriting attributes of 24# parent project, if it exists). After that the Jamfile is read. It can declare 25# its own attributes using the 'project' rule which will be combined with any 26# already set attributes. 27# 28# The 'project' rule can also declare a project id which will be associated 29# with the project module. 30# 31# There can also be 'standalone' projects. They are created by calling 32# 'initialize' on an arbitrary module and not specifying their location. After 33# the call, the module can call the 'project' rule, declare main targets and 34# behave as a regular project except that, since it is not associated with any 35# location, it should only declare prebuilt targets. 36# 37# The list of all loaded Jamfiles is stored in the .project-locations variable. 38# It is possible to obtain a module name for a location using the 'module-name' 39# rule. Standalone projects are not recorded and can only be references using 40# their project id. 41 42import b2.util.path 43import b2.build.targets 44from b2.build import property_set, property 45from b2.build.errors import ExceptionWithUserContext 46from b2.manager import get_manager 47 48import bjam 49import b2 50 51import re 52import sys 53import pkgutil 54import os 55import string 56import imp 57import traceback 58import b2.util.option as option 59 60from b2.util import ( 61 record_jam_to_value_mapping, qualify_jam_action, is_iterable_typed, bjam_signature, 62 is_iterable) 63 64 65class ProjectRegistry: 66 67 def __init__(self, manager, global_build_dir): 68 self.manager = manager 69 self.global_build_dir = global_build_dir 70 self.project_rules_ = ProjectRules(self) 71 72 # The target corresponding to the project being loaded now 73 self.current_project = None 74 75 # The set of names of loaded project modules 76 self.jamfile_modules = {} 77 78 # Mapping from location to module name 79 self.location2module = {} 80 81 # Mapping from project id to project module 82 self.id2module = {} 83 84 # Map from Jamfile directory to parent Jamfile/Jamroot 85 # location. 86 self.dir2parent_jamfile = {} 87 88 # Map from directory to the name of Jamfile in 89 # that directory (or None). 90 self.dir2jamfile = {} 91 92 # Map from project module to attributes object. 93 self.module2attributes = {} 94 95 # Map from project module to target for the project 96 self.module2target = {} 97 98 # Map from names to Python modules, for modules loaded 99 # via 'using' and 'import' rules in Jamfiles. 100 self.loaded_tool_modules_ = {} 101 102 self.loaded_tool_module_path_ = {} 103 104 # Map from project target to the list of 105 # (id,location) pairs corresponding to all 'use-project' 106 # invocations. 107 # TODO: should not have a global map, keep this 108 # in ProjectTarget. 109 self.used_projects = {} 110 111 self.saved_current_project = [] 112 113 self.JAMROOT = self.manager.getenv("JAMROOT"); 114 115 # Note the use of character groups, as opposed to listing 116 # 'Jamroot' and 'jamroot'. With the latter, we'd get duplicate 117 # matches on windows and would have to eliminate duplicates. 118 if not self.JAMROOT: 119 self.JAMROOT = ["project-root.jam", "[Jj]amroot", "[Jj]amroot.jam"] 120 121 # Default patterns to search for the Jamfiles to use for build 122 # declarations. 123 self.JAMFILE = self.manager.getenv("JAMFILE") 124 125 if not self.JAMFILE: 126 self.JAMFILE = ["[Bb]uild.jam", "[Jj]amfile.v2", "[Jj]amfile", 127 "[Jj]amfile.jam"] 128 129 self.__python_module_cache = {} 130 131 132 def load (self, jamfile_location): 133 """Loads jamfile at the given location. After loading, project global 134 file and jamfile needed by the loaded one will be loaded recursively. 135 If the jamfile at that location is loaded already, does nothing. 136 Returns the project module for the Jamfile.""" 137 assert isinstance(jamfile_location, basestring) 138 139 absolute = os.path.join(os.getcwd(), jamfile_location) 140 absolute = os.path.normpath(absolute) 141 jamfile_location = b2.util.path.relpath(os.getcwd(), absolute) 142 143 mname = self.module_name(jamfile_location) 144 # If Jamfile is already loaded, do not try again. 145 if not mname in self.jamfile_modules: 146 147 if "--debug-loading" in self.manager.argv(): 148 print "Loading Jamfile at '%s'" % jamfile_location 149 150 self.load_jamfile(jamfile_location, mname) 151 152 # We want to make sure that child project are loaded only 153 # after parent projects. In particular, because parent projects 154 # define attributes which are inherited by children, and we do not 155 # want children to be loaded before parents has defined everything. 156 # 157 # While "build-project" and "use-project" can potentially refer 158 # to child projects from parent projects, we do not immediately 159 # load child projects when seeing those attributes. Instead, 160 # we record the minimal information that will be used only later. 161 162 self.load_used_projects(mname) 163 164 return mname 165 166 def load_used_projects(self, module_name): 167 assert isinstance(module_name, basestring) 168 # local used = [ modules.peek $(module-name) : .used-projects ] ; 169 used = self.used_projects[module_name] 170 171 location = self.attribute(module_name, "location") 172 for u in used: 173 id = u[0] 174 where = u[1] 175 176 self.use(id, os.path.join(location, where)) 177 178 def load_parent(self, location): 179 """Loads parent of Jamfile at 'location'. 180 Issues an error if nothing is found.""" 181 assert isinstance(location, basestring) 182 found = b2.util.path.glob_in_parents( 183 location, self.JAMROOT + self.JAMFILE) 184 185 if not found: 186 print "error: Could not find parent for project at '%s'" % location 187 print "error: Did not find Jamfile.jam or Jamroot.jam in any parent directory." 188 sys.exit(1) 189 190 return self.load(os.path.dirname(found[0])) 191 192 def find(self, name, current_location): 193 """Given 'name' which can be project-id or plain directory name, 194 return project module corresponding to that id or directory. 195 Returns nothing of project is not found.""" 196 assert isinstance(name, basestring) 197 assert isinstance(current_location, basestring) 198 199 project_module = None 200 201 # Try interpreting name as project id. 202 if name[0] == '/': 203 project_module = self.id2module.get(name) 204 205 if not project_module: 206 location = os.path.join(current_location, name) 207 # If no project is registered for the given location, try to 208 # load it. First see if we have Jamfile. If not we might have project 209 # root, willing to act as Jamfile. In that case, project-root 210 # must be placed in the directory referred by id. 211 212 project_module = self.module_name(location) 213 if not project_module in self.jamfile_modules: 214 if b2.util.path.glob([location], self.JAMROOT + self.JAMFILE): 215 project_module = self.load(location) 216 else: 217 project_module = None 218 219 return project_module 220 221 def module_name(self, jamfile_location): 222 """Returns the name of module corresponding to 'jamfile-location'. 223 If no module corresponds to location yet, associates default 224 module name with that location.""" 225 assert isinstance(jamfile_location, basestring) 226 module = self.location2module.get(jamfile_location) 227 if not module: 228 # Root the path, so that locations are always umbiguious. 229 # Without this, we can't decide if '../../exe/program1' and '.' 230 # are the same paths, or not. 231 jamfile_location = os.path.realpath( 232 os.path.join(os.getcwd(), jamfile_location)) 233 module = "Jamfile<%s>" % jamfile_location 234 self.location2module[jamfile_location] = module 235 return module 236 237 def find_jamfile (self, dir, parent_root=0, no_errors=0): 238 """Find the Jamfile at the given location. This returns the 239 exact names of all the Jamfiles in the given directory. The optional 240 parent-root argument causes this to search not the given directory 241 but the ones above it up to the directory given in it.""" 242 assert isinstance(dir, basestring) 243 assert isinstance(parent_root, (int, bool)) 244 assert isinstance(no_errors, (int, bool)) 245 246 # Glob for all the possible Jamfiles according to the match pattern. 247 # 248 jamfile_glob = None 249 if parent_root: 250 parent = self.dir2parent_jamfile.get(dir) 251 if not parent: 252 parent = b2.util.path.glob_in_parents(dir, 253 self.JAMFILE) 254 self.dir2parent_jamfile[dir] = parent 255 jamfile_glob = parent 256 else: 257 jamfile = self.dir2jamfile.get(dir) 258 if not jamfile: 259 jamfile = b2.util.path.glob([dir], self.JAMFILE) 260 self.dir2jamfile[dir] = jamfile 261 jamfile_glob = jamfile 262 263 if len(jamfile_glob) > 1: 264 # Multiple Jamfiles found in the same place. Warn about this. 265 # And ensure we use only one of them. 266 # As a temporary convenience measure, if there's Jamfile.v2 amount 267 # found files, suppress the warning and use it. 268 # 269 pattern = "(.*[Jj]amfile\\.v2)|(.*[Bb]uild\\.jam)" 270 v2_jamfiles = [x for x in jamfile_glob if re.match(pattern, x)] 271 if len(v2_jamfiles) == 1: 272 jamfile_glob = v2_jamfiles 273 else: 274 print """warning: Found multiple Jamfiles at '%s'!""" % (dir) 275 for j in jamfile_glob: 276 print " -", j 277 print "Loading the first one" 278 279 # Could not find it, error. 280 if not no_errors and not jamfile_glob: 281 self.manager.errors()( 282 """Unable to load Jamfile. 283Could not find a Jamfile in directory '%s' 284Attempted to find it with pattern '%s'. 285Please consult the documentation at 'http://boost.org/boost-build2'.""" 286 % (dir, string.join(self.JAMFILE))) 287 288 if jamfile_glob: 289 return jamfile_glob[0] 290 291 def load_jamfile(self, dir, jamfile_module): 292 """Load a Jamfile at the given directory. Returns nothing. 293 Will attempt to load the file as indicated by the JAMFILE patterns. 294 Effect of calling this rule twice with the same 'dir' is underfined.""" 295 assert isinstance(dir, basestring) 296 assert isinstance(jamfile_module, basestring) 297 298 # See if the Jamfile is where it should be. 299 is_jamroot = False 300 jamfile_to_load = b2.util.path.glob([dir], self.JAMROOT) 301 if jamfile_to_load: 302 if len(jamfile_to_load) > 1: 303 get_manager().errors()( 304 "Multiple Jamfiles found at '{}'\n" 305 "Filenames are: {}" 306 .format(dir, ' '.join(os.path.basename(j) for j in jamfile_to_load)) 307 ) 308 is_jamroot = True 309 jamfile_to_load = jamfile_to_load[0] 310 else: 311 jamfile_to_load = self.find_jamfile(dir) 312 313 dir = os.path.dirname(jamfile_to_load) 314 if not dir: 315 dir = "." 316 317 self.used_projects[jamfile_module] = [] 318 319 # Now load the Jamfile in it's own context. 320 # The call to 'initialize' may load parent Jamfile, which might have 321 # 'use-project' statement that causes a second attempt to load the 322 # same project we're loading now. Checking inside .jamfile-modules 323 # prevents that second attempt from messing up. 324 if not jamfile_module in self.jamfile_modules: 325 previous_project = self.current_project 326 # Initialize the jamfile module before loading. 327 self.initialize(jamfile_module, dir, os.path.basename(jamfile_to_load)) 328 329 if not jamfile_module in self.jamfile_modules: 330 saved_project = self.current_project 331 self.jamfile_modules[jamfile_module] = True 332 333 bjam.call("load", jamfile_module, jamfile_to_load) 334 335 if is_jamroot: 336 jamfile = self.find_jamfile(dir, no_errors=True) 337 if jamfile: 338 bjam.call("load", jamfile_module, jamfile) 339 340 # Now do some checks 341 if self.current_project != saved_project: 342 from textwrap import dedent 343 self.manager.errors()(dedent( 344 """ 345 The value of the .current-project variable has magically changed 346 after loading a Jamfile. This means some of the targets might be 347 defined a the wrong project. 348 after loading %s 349 expected value %s 350 actual value %s 351 """ 352 % (jamfile_module, saved_project, self.current_project) 353 )) 354 355 self.end_load(previous_project) 356 357 if self.global_build_dir: 358 id = self.attributeDefault(jamfile_module, "id", None) 359 project_root = self.attribute(jamfile_module, "project-root") 360 location = self.attribute(jamfile_module, "location") 361 362 if location and project_root == dir: 363 # This is Jamroot 364 if not id: 365 # FIXME: go via errors module, so that contexts are 366 # shown? 367 print "warning: the --build-dir option was specified" 368 print "warning: but Jamroot at '%s'" % dir 369 print "warning: specified no project id" 370 print "warning: the --build-dir option will be ignored" 371 372 def end_load(self, previous_project=None): 373 if not self.current_project: 374 self.manager.errors()( 375 'Ending project loading requested when there was no project currently ' 376 'being loaded.' 377 ) 378 379 if not previous_project and self.saved_current_project: 380 self.manager.errors()( 381 'Ending project loading requested with no "previous project" when there ' 382 'other projects still being loaded recursively.' 383 ) 384 385 self.current_project = previous_project 386 387 def load_standalone(self, jamfile_module, file): 388 """Loads 'file' as standalone project that has no location 389 associated with it. This is mostly useful for user-config.jam, 390 which should be able to define targets, but although it has 391 some location in filesystem, we do not want any build to 392 happen in user's HOME, for example. 393 394 The caller is required to never call this method twice on 395 the same file. 396 """ 397 assert isinstance(jamfile_module, basestring) 398 assert isinstance(file, basestring) 399 400 self.used_projects[jamfile_module] = [] 401 bjam.call("load", jamfile_module, file) 402 self.load_used_projects(jamfile_module) 403 404 def is_jamroot(self, basename): 405 assert isinstance(basename, basestring) 406 match = [ pat for pat in self.JAMROOT if re.match(pat, basename)] 407 if match: 408 return 1 409 else: 410 return 0 411 412 def initialize(self, module_name, location=None, basename=None, standalone_path=''): 413 """Initialize the module for a project. 414 415 module-name is the name of the project module. 416 location is the location (directory) of the project to initialize. 417 If not specified, standalone project will be initialized 418 standalone_path is the path to the source-location. 419 this should only be called from the python side. 420 """ 421 assert isinstance(module_name, basestring) 422 assert isinstance(location, basestring) or location is None 423 assert isinstance(basename, basestring) or basename is None 424 jamroot = False 425 parent_module = None 426 if module_name == "test-config": 427 # No parent 428 pass 429 elif module_name == "site-config": 430 parent_module = "test-config" 431 elif module_name == "user-config": 432 parent_module = "site-config" 433 elif module_name == "project-config": 434 parent_module = "user-config" 435 elif location and not self.is_jamroot(basename): 436 # We search for parent/project-root only if jamfile was specified 437 # --- i.e 438 # if the project is not standalone. 439 parent_module = self.load_parent(location) 440 elif location: 441 # It's either jamroot, or standalone project. 442 # If it's jamroot, inherit from user-config. 443 # If project-config module exist, inherit from it. 444 parent_module = 'user-config' 445 if 'project-config' in self.module2attributes: 446 parent_module = 'project-config' 447 jamroot = True 448 449 # TODO: need to consider if standalone projects can do anything but defining 450 # prebuilt targets. If so, we need to give more sensible "location", so that 451 # source paths are correct. 452 if not location: 453 location = "" 454 455 # the call to load_parent() above can end up loading this module again 456 # make sure we don't reinitialize the module's attributes 457 if module_name not in self.module2attributes: 458 if "--debug-loading" in self.manager.argv(): 459 print "Initializing project '%s'" % module_name 460 attributes = ProjectAttributes(self.manager, location, module_name) 461 self.module2attributes[module_name] = attributes 462 463 python_standalone = False 464 if location: 465 attributes.set("source-location", [location], exact=1) 466 elif not module_name in ["test-config", "site-config", "user-config", "project-config"]: 467 # This is a standalone project with known location. Set source location 468 # so that it can declare targets. This is intended so that you can put 469 # a .jam file in your sources and use it via 'using'. Standard modules 470 # (in 'tools' subdir) may not assume source dir is set. 471 source_location = standalone_path 472 if not source_location: 473 source_location = self.loaded_tool_module_path_.get(module_name) 474 if not source_location: 475 self.manager.errors()('Standalone module path not found for "{}"' 476 .format(module_name)) 477 attributes.set("source-location", [source_location], exact=1) 478 python_standalone = True 479 480 attributes.set("requirements", property_set.empty(), exact=True) 481 attributes.set("usage-requirements", property_set.empty(), exact=True) 482 attributes.set("default-build", property_set.empty(), exact=True) 483 attributes.set("projects-to-build", [], exact=True) 484 attributes.set("project-root", None, exact=True) 485 attributes.set("build-dir", None, exact=True) 486 487 self.project_rules_.init_project(module_name, python_standalone) 488 489 if parent_module: 490 self.inherit_attributes(module_name, parent_module) 491 attributes.set("parent-module", parent_module, exact=1) 492 493 if jamroot: 494 attributes.set("project-root", location, exact=1) 495 496 parent = None 497 if parent_module: 498 parent = self.target(parent_module) 499 500 if module_name not in self.module2target: 501 target = b2.build.targets.ProjectTarget(self.manager, 502 module_name, module_name, parent, 503 self.attribute(module_name, "requirements"), 504 # FIXME: why we need to pass this? It's not 505 # passed in jam code. 506 self.attribute(module_name, "default-build")) 507 self.module2target[module_name] = target 508 509 self.current_project = self.target(module_name) 510 511 def inherit_attributes(self, project_module, parent_module): 512 """Make 'project-module' inherit attributes of project 513 root and parent module.""" 514 assert isinstance(project_module, basestring) 515 assert isinstance(parent_module, basestring) 516 517 attributes = self.module2attributes[project_module] 518 pattributes = self.module2attributes[parent_module] 519 520 # Parent module might be locationless user-config. 521 # FIXME: 522 #if [ modules.binding $(parent-module) ] 523 #{ 524 # $(attributes).set parent : [ path.parent 525 # [ path.make [ modules.binding $(parent-module) ] ] ] ; 526 # } 527 528 attributes.set("project-root", pattributes.get("project-root"), exact=True) 529 attributes.set("default-build", pattributes.get("default-build"), exact=True) 530 attributes.set("requirements", pattributes.get("requirements"), exact=True) 531 attributes.set("usage-requirements", 532 pattributes.get("usage-requirements"), exact=1) 533 534 parent_build_dir = pattributes.get("build-dir") 535 536 if parent_build_dir: 537 # Have to compute relative path from parent dir to our dir 538 # Convert both paths to absolute, since we cannot 539 # find relative path from ".." to "." 540 541 location = attributes.get("location") 542 parent_location = pattributes.get("location") 543 544 our_dir = os.path.join(os.getcwd(), location) 545 parent_dir = os.path.join(os.getcwd(), parent_location) 546 547 build_dir = os.path.join(parent_build_dir, 548 os.path.relpath(our_dir, parent_dir)) 549 attributes.set("build-dir", build_dir, exact=True) 550 551 def register_id(self, id, module): 552 """Associate the given id with the given project module.""" 553 assert isinstance(id, basestring) 554 assert isinstance(module, basestring) 555 self.id2module[id] = module 556 557 def current(self): 558 """Returns the project which is currently being loaded.""" 559 if not self.current_project: 560 get_manager().errors()( 561 'Reference to the project currently being loaded requested ' 562 'when there was no project module being loaded.' 563 ) 564 return self.current_project 565 566 def set_current(self, c): 567 if __debug__: 568 from .targets import ProjectTarget 569 assert isinstance(c, ProjectTarget) 570 self.current_project = c 571 572 def push_current(self, project): 573 """Temporary changes the current project to 'project'. Should 574 be followed by 'pop-current'.""" 575 if __debug__: 576 from .targets import ProjectTarget 577 assert isinstance(project, ProjectTarget) 578 self.saved_current_project.append(self.current_project) 579 self.current_project = project 580 581 def pop_current(self): 582 if self.saved_current_project: 583 self.current_project = self.saved_current_project.pop() 584 else: 585 self.current_project = None 586 587 def attributes(self, project): 588 """Returns the project-attribute instance for the 589 specified jamfile module.""" 590 assert isinstance(project, basestring) 591 return self.module2attributes[project] 592 593 def attribute(self, project, attribute): 594 """Returns the value of the specified attribute in the 595 specified jamfile module.""" 596 assert isinstance(project, basestring) 597 assert isinstance(attribute, basestring) 598 try: 599 return self.module2attributes[project].get(attribute) 600 except: 601 raise BaseException("No attribute '%s' for project %s" % (attribute, project)) 602 603 def attributeDefault(self, project, attribute, default): 604 """Returns the value of the specified attribute in the 605 specified jamfile module.""" 606 assert isinstance(project, basestring) 607 assert isinstance(attribute, basestring) 608 assert isinstance(default, basestring) or default is None 609 return self.module2attributes[project].getDefault(attribute, default) 610 611 def target(self, project_module): 612 """Returns the project target corresponding to the 'project-module'.""" 613 assert isinstance(project_module, basestring) 614 if project_module not in self.module2target: 615 self.module2target[project_module] = \ 616 b2.build.targets.ProjectTarget(project_module, project_module, 617 self.attribute(project_module, "requirements")) 618 619 return self.module2target[project_module] 620 621 def use(self, id, location): 622 # Use/load a project. 623 assert isinstance(id, basestring) 624 assert isinstance(location, basestring) 625 saved_project = self.current_project 626 project_module = self.load(location) 627 declared_id = self.attributeDefault(project_module, "id", "") 628 629 if not declared_id or declared_id != id: 630 # The project at 'location' either have no id or 631 # that id is not equal to the 'id' parameter. 632 if id in self.id2module and self.id2module[id] != project_module: 633 self.manager.errors()( 634"""Attempt to redeclare already existing project id '%s' at location '%s'""" % (id, location)) 635 self.id2module[id] = project_module 636 637 self.current_project = saved_project 638 639 def add_rule(self, name, callable_): 640 """Makes rule 'name' available to all subsequently loaded Jamfiles. 641 642 Calling that rule will relay to 'callable'.""" 643 assert isinstance(name, basestring) 644 assert callable(callable_) 645 self.project_rules_.add_rule(name, callable_) 646 647 def project_rules(self): 648 return self.project_rules_ 649 650 def glob_internal(self, project, wildcards, excludes, rule_name): 651 if __debug__: 652 from .targets import ProjectTarget 653 assert isinstance(project, ProjectTarget) 654 assert is_iterable_typed(wildcards, basestring) 655 assert is_iterable_typed(excludes, basestring) or excludes is None 656 assert isinstance(rule_name, basestring) 657 location = project.get("source-location")[0] 658 659 result = [] 660 callable = b2.util.path.__dict__[rule_name] 661 662 paths = callable([location], wildcards, excludes) 663 has_dir = 0 664 for w in wildcards: 665 if os.path.dirname(w): 666 has_dir = 1 667 break 668 669 if has_dir or rule_name != "glob": 670 result = [] 671 # The paths we've found are relative to current directory, 672 # but the names specified in sources list are assumed to 673 # be relative to source directory of the corresponding 674 # prject. Either translate them or make absolute. 675 676 for p in paths: 677 rel = os.path.relpath(p, location) 678 # If the path is below source location, use relative path. 679 if not ".." in rel: 680 result.append(rel) 681 else: 682 # Otherwise, use full path just to avoid any ambiguities. 683 result.append(os.path.abspath(p)) 684 685 else: 686 # There were not directory in wildcard, so the files are all 687 # in the source directory of the project. Just drop the 688 # directory, instead of making paths absolute. 689 result = [os.path.basename(p) for p in paths] 690 691 return result 692 693 def __build_python_module_cache(self): 694 """Recursively walks through the b2/src subdirectories and 695 creates an index of base module name to package name. The 696 index is stored within self.__python_module_cache and allows 697 for an O(1) module lookup. 698 699 For example, given the base module name `toolset`, 700 self.__python_module_cache['toolset'] will return 701 'b2.build.toolset' 702 703 pkgutil.walk_packages() will find any python package 704 provided a directory contains an __init__.py. This has the 705 added benefit of allowing libraries to be installed and 706 automatically available within the contrib directory. 707 708 *Note*: pkgutil.walk_packages() will import any subpackage 709 in order to access its __path__variable. Meaning: 710 any initialization code will be run if the package hasn't 711 already been imported. 712 """ 713 cache = {} 714 for importer, mname, ispkg in pkgutil.walk_packages(b2.__path__, prefix='b2.'): 715 basename = mname.split('.')[-1] 716 # since the jam code is only going to have "import toolset ;" 717 # it doesn't matter if there are separately named "b2.build.toolset" and 718 # "b2.contrib.toolset" as it is impossible to know which the user is 719 # referring to. 720 if basename in cache: 721 self.manager.errors()('duplicate module name "{0}" ' 722 'found in boost-build path'.format(basename)) 723 cache[basename] = mname 724 self.__python_module_cache = cache 725 726 def load_module(self, name, extra_path=None): 727 """Load a Python module that should be usable from Jamfiles. 728 729 There are generally two types of modules Jamfiles might want to 730 use: 731 - Core Boost.Build. Those are imported using plain names, e.g. 732 'toolset', so this function checks if we have module named 733 b2.package.module already. 734 - Python modules in the same directory as Jamfile. We don't 735 want to even temporary add Jamfile's directory to sys.path, 736 since then we might get naming conflicts between standard 737 Python modules and those. 738 """ 739 assert isinstance(name, basestring) 740 assert is_iterable_typed(extra_path, basestring) or extra_path is None 741 # See if we loaded module of this name already 742 existing = self.loaded_tool_modules_.get(name) 743 if existing: 744 return existing 745 746 # check the extra path as well as any paths outside 747 # of the b2 package and import the module if it exists 748 b2_path = os.path.normpath(b2.__path__[0]) 749 # normalize the pathing in the BOOST_BUILD_PATH. 750 # this allows for using startswith() to determine 751 # if a path is a subdirectory of the b2 root_path 752 paths = [os.path.normpath(p) for p in self.manager.boost_build_path()] 753 # remove all paths that start with b2's root_path 754 paths = [p for p in paths if not p.startswith(b2_path)] 755 # add any extra paths 756 paths.extend(extra_path) 757 758 try: 759 # find_module is used so that the pyc's can be used. 760 # an ImportError is raised if not found 761 f, location, description = imp.find_module(name, paths) 762 except ImportError: 763 # if the module is not found in the b2 package, 764 # this error will be handled later 765 pass 766 else: 767 # we've found the module, now let's try loading it. 768 # it's possible that the module itself contains an ImportError 769 # which is why we're loading it in this else clause so that the 770 # proper error message is shown to the end user. 771 # TODO: does this module name really need to be mangled like this? 772 mname = name + "__for_jamfile" 773 self.loaded_tool_module_path_[mname] = location 774 module = imp.load_module(mname, f, location, description) 775 self.loaded_tool_modules_[name] = module 776 return module 777 778 # the cache is created here due to possibly importing packages 779 # that end up calling get_manager() which might fail 780 if not self.__python_module_cache: 781 self.__build_python_module_cache() 782 783 underscore_name = name.replace('-', '_') 784 # check to see if the module is within the b2 package 785 # and already loaded 786 mname = self.__python_module_cache.get(underscore_name) 787 if mname in sys.modules: 788 return sys.modules[mname] 789 # otherwise, if the module name is within the cache, 790 # the module exists within the BOOST_BUILD_PATH, 791 # load it. 792 elif mname: 793 # in some cases, self.loaded_tool_module_path_ needs to 794 # have the path to the file during the import 795 # (project.initialize() for example), 796 # so the path needs to be set *before* importing the module. 797 path = os.path.join(b2.__path__[0], *mname.split('.')[1:]) 798 self.loaded_tool_module_path_[mname] = path 799 # mname is guaranteed to be importable since it was 800 # found within the cache 801 __import__(mname) 802 module = sys.modules[mname] 803 self.loaded_tool_modules_[name] = module 804 return module 805 806 self.manager.errors()("Cannot find module '%s'" % name) 807 808 809 810# FIXME: 811# Defines a Boost.Build extension project. Such extensions usually 812# contain library targets and features that can be used by many people. 813# Even though extensions are really projects, they can be initialize as 814# a module would be with the "using" (project.project-rules.using) 815# mechanism. 816#rule extension ( id : options * : * ) 817#{ 818# # The caller is a standalone module for the extension. 819# local mod = [ CALLER_MODULE ] ; 820# 821# # We need to do the rest within the extension module. 822# module $(mod) 823# { 824# import path ; 825# 826# # Find the root project. 827# local root-project = [ project.current ] ; 828# root-project = [ $(root-project).project-module ] ; 829# while 830# [ project.attribute $(root-project) parent-module ] && 831# [ project.attribute $(root-project) parent-module ] != user-config 832# { 833# root-project = [ project.attribute $(root-project) parent-module ] ; 834# } 835# 836# # Create the project data, and bring in the project rules 837# # into the module. 838# project.initialize $(__name__) : 839# [ path.join [ project.attribute $(root-project) location ] ext $(1:L) ] ; 840# 841# # Create the project itself, i.e. the attributes. 842# # All extensions are created in the "/ext" project space. 843# project /ext/$(1) : $(2) : $(3) : $(4) : $(5) : $(6) : $(7) : $(8) : $(9) ; 844# local attributes = [ project.attributes $(__name__) ] ; 845# 846# # Inherit from the root project of whomever is defining us. 847# project.inherit-attributes $(__name__) : $(root-project) ; 848# $(attributes).set parent-module : $(root-project) : exact ; 849# } 850#} 851 852 853class ProjectAttributes: 854 """Class keeping all the attributes of a project. 855 856 The standard attributes are 'id', "location", "project-root", "parent" 857 "requirements", "default-build", "source-location" and "projects-to-build". 858 """ 859 860 def __init__(self, manager, location, project_module): 861 self.manager = manager 862 self.location = location 863 self.project_module = project_module 864 self.attributes = {} 865 self.usage_requirements = None 866 867 def set(self, attribute, specification, exact=False): 868 """Set the named attribute from the specification given by the user. 869 The value actually set may be different.""" 870 assert isinstance(attribute, basestring) 871 assert isinstance(exact, (int, bool)) 872 if __debug__ and not exact: 873 if attribute == 'requirements': 874 assert (isinstance(specification, property_set.PropertySet) 875 or all(isinstance(s, basestring) for s in specification)) 876 elif attribute in ( 877 'usage-requirements', 'default-build', 'source-location', 'build-dir', 'id'): 878 assert is_iterable_typed(specification, basestring) 879 elif __debug__: 880 assert ( 881 isinstance(specification, (property_set.PropertySet, type(None), basestring)) 882 or all(isinstance(s, basestring) for s in specification) 883 ) 884 if exact: 885 self.__dict__[attribute] = specification 886 887 elif attribute == "requirements": 888 self.requirements = property_set.refine_from_user_input( 889 self.requirements, specification, 890 self.project_module, self.location) 891 892 elif attribute == "usage-requirements": 893 unconditional = [] 894 for p in specification: 895 split = property.split_conditional(p) 896 if split: 897 unconditional.append(split[1]) 898 else: 899 unconditional.append(p) 900 901 non_free = property.remove("free", unconditional) 902 if non_free: 903 get_manager().errors()("usage-requirements %s have non-free properties %s" \ 904 % (specification, non_free)) 905 906 t = property.translate_paths( 907 property.create_from_strings(specification, allow_condition=True), 908 self.location) 909 910 existing = self.__dict__.get("usage-requirements") 911 if existing: 912 new = property_set.create(existing.all() + t) 913 else: 914 new = property_set.create(t) 915 self.__dict__["usage-requirements"] = new 916 917 918 elif attribute == "default-build": 919 self.__dict__["default-build"] = property_set.create(specification) 920 921 elif attribute == "source-location": 922 source_location = [] 923 for path in specification: 924 source_location.append(os.path.join(self.location, path)) 925 self.__dict__["source-location"] = source_location 926 927 elif attribute == "build-dir": 928 self.__dict__["build-dir"] = os.path.join(self.location, specification[0]) 929 930 elif attribute == "id": 931 id = specification[0] 932 if id[0] != '/': 933 id = "/" + id 934 self.manager.projects().register_id(id, self.project_module) 935 self.__dict__["id"] = id 936 937 elif not attribute in ["default-build", "location", 938 "source-location", "parent", 939 "projects-to-build", "project-root"]: 940 self.manager.errors()( 941"""Invalid project attribute '%s' specified 942for project at '%s'""" % (attribute, self.location)) 943 else: 944 self.__dict__[attribute] = specification 945 946 def get(self, attribute): 947 assert isinstance(attribute, basestring) 948 return self.__dict__[attribute] 949 950 def getDefault(self, attribute, default): 951 assert isinstance(attribute, basestring) 952 return self.__dict__.get(attribute, default) 953 954 def dump(self): 955 """Prints the project attributes.""" 956 id = self.get("id") 957 if not id: 958 id = "(none)" 959 else: 960 id = id[0] 961 962 parent = self.get("parent") 963 if not parent: 964 parent = "(none)" 965 else: 966 parent = parent[0] 967 968 print "'%s'" % id 969 print "Parent project:%s", parent 970 print "Requirements:%s", self.get("requirements") 971 print "Default build:%s", string.join(self.get("debuild-build")) 972 print "Source location:%s", string.join(self.get("source-location")) 973 print "Projects to build:%s", string.join(self.get("projects-to-build").sort()); 974 975class ProjectRules: 976 """Class keeping all rules that are made available to Jamfile.""" 977 978 def __init__(self, registry): 979 self.registry = registry 980 self.manager_ = registry.manager 981 self.rules = {} 982 self.local_names = [x for x in self.__class__.__dict__ 983 if x not in ["__init__", "init_project", "add_rule", 984 "error_reporting_wrapper", "add_rule_for_type", "reverse"]] 985 self.all_names_ = [x for x in self.local_names] 986 987 def _import_rule(self, bjam_module, name, callable_): 988 assert isinstance(bjam_module, basestring) 989 assert isinstance(name, basestring) 990 assert callable(callable_) 991 if hasattr(callable_, "bjam_signature"): 992 bjam.import_rule(bjam_module, name, self.make_wrapper(callable_), callable_.bjam_signature) 993 else: 994 bjam.import_rule(bjam_module, name, self.make_wrapper(callable_)) 995 996 997 def add_rule_for_type(self, type): 998 assert isinstance(type, basestring) 999 rule_name = type.lower().replace("_", "-") 1000 1001 @bjam_signature([['name'], ['sources', '*'], ['requirements', '*'], 1002 ['default_build', '*'], ['usage_requirements', '*']]) 1003 def xpto (name, sources=[], requirements=[], default_build=[], usage_requirements=[]): 1004 1005 return self.manager_.targets().create_typed_target( 1006 type, self.registry.current(), name, sources, 1007 requirements, default_build, usage_requirements) 1008 1009 self.add_rule(rule_name, xpto) 1010 1011 def add_rule(self, name, callable_): 1012 assert isinstance(name, basestring) 1013 assert callable(callable_) 1014 self.rules[name] = callable_ 1015 self.all_names_.append(name) 1016 1017 # Add new rule at global bjam scope. This might not be ideal, 1018 # added because if a jamroot does 'import foo' where foo calls 1019 # add_rule, we need to import new rule to jamroot scope, and 1020 # I'm lazy to do this now. 1021 self._import_rule("", name, callable_) 1022 1023 def all_names(self): 1024 return self.all_names_ 1025 1026 def call_and_report_errors(self, callable_, *args, **kw): 1027 assert callable(callable_) 1028 result = None 1029 try: 1030 self.manager_.errors().push_jamfile_context() 1031 result = callable_(*args, **kw) 1032 except ExceptionWithUserContext, e: 1033 e.report() 1034 except Exception, e: 1035 try: 1036 self.manager_.errors().handle_stray_exception (e) 1037 except ExceptionWithUserContext, e: 1038 e.report() 1039 finally: 1040 self.manager_.errors().pop_jamfile_context() 1041 1042 return result 1043 1044 def make_wrapper(self, callable_): 1045 """Given a free-standing function 'callable', return a new 1046 callable that will call 'callable' and report all exceptins, 1047 using 'call_and_report_errors'.""" 1048 assert callable(callable_) 1049 def wrapper(*args, **kw): 1050 return self.call_and_report_errors(callable_, *args, **kw) 1051 return wrapper 1052 1053 def init_project(self, project_module, python_standalone=False): 1054 assert isinstance(project_module, basestring) 1055 assert isinstance(python_standalone, bool) 1056 if python_standalone: 1057 m = sys.modules[project_module] 1058 1059 for n in self.local_names: 1060 if n != "import_": 1061 setattr(m, n, getattr(self, n)) 1062 1063 for n in self.rules: 1064 setattr(m, n, self.rules[n]) 1065 1066 return 1067 1068 for n in self.local_names: 1069 # Using 'getattr' here gives us a bound method, 1070 # while using self.__dict__[r] would give unbound one. 1071 v = getattr(self, n) 1072 if callable(v): 1073 if n == "import_": 1074 n = "import" 1075 else: 1076 n = string.replace(n, "_", "-") 1077 1078 self._import_rule(project_module, n, v) 1079 1080 for n in self.rules: 1081 self._import_rule(project_module, n, self.rules[n]) 1082 1083 def project(self, *args): 1084 assert is_iterable(args) and all(is_iterable(arg) for arg in args) 1085 jamfile_module = self.registry.current().project_module() 1086 attributes = self.registry.attributes(jamfile_module) 1087 1088 id = None 1089 if args and args[0]: 1090 id = args[0][0] 1091 args = args[1:] 1092 1093 if id: 1094 attributes.set('id', [id]) 1095 1096 explicit_build_dir = None 1097 for a in args: 1098 if a: 1099 attributes.set(a[0], a[1:], exact=0) 1100 if a[0] == "build-dir": 1101 explicit_build_dir = a[1] 1102 1103 # If '--build-dir' is specified, change the build dir for the project. 1104 if self.registry.global_build_dir: 1105 1106 location = attributes.get("location") 1107 # Project with empty location is 'standalone' project, like 1108 # user-config, or qt. It has no build dir. 1109 # If we try to set build dir for user-config, we'll then 1110 # try to inherit it, with either weird, or wrong consequences. 1111 if location and location == attributes.get("project-root"): 1112 # Re-read the project id, since it might have been changed in 1113 # the project's attributes. 1114 id = attributes.get('id') 1115 1116 # This is Jamroot. 1117 if id: 1118 if explicit_build_dir and os.path.isabs(explicit_build_dir): 1119 self.registry.manager.errors()( 1120"""Absolute directory specified via 'build-dir' project attribute 1121Don't know how to combine that with the --build-dir option.""") 1122 1123 rid = id 1124 if rid[0] == '/': 1125 rid = rid[1:] 1126 1127 p = os.path.join(self.registry.global_build_dir, rid) 1128 if explicit_build_dir: 1129 p = os.path.join(p, explicit_build_dir) 1130 attributes.set("build-dir", p, exact=1) 1131 elif explicit_build_dir: 1132 self.registry.manager.errors()( 1133"""When --build-dir is specified, the 'build-dir' 1134attribute is allowed only for top-level 'project' invocations""") 1135 1136 def constant(self, name, value): 1137 """Declare and set a project global constant. 1138 Project global constants are normal variables but should 1139 not be changed. They are applied to every child Jamfile.""" 1140 assert is_iterable_typed(name, basestring) 1141 assert is_iterable_typed(value, basestring) 1142 self.registry.current().add_constant(name[0], value) 1143 1144 def path_constant(self, name, value): 1145 """Declare and set a project global constant, whose value is a path. The 1146 path is adjusted to be relative to the invocation directory. The given 1147 value path is taken to be either absolute, or relative to this project 1148 root.""" 1149 assert is_iterable_typed(name, basestring) 1150 assert is_iterable_typed(value, basestring) 1151 if len(value) > 1: 1152 self.registry.manager.errors()("path constant should have one element") 1153 self.registry.current().add_constant(name[0], value, path=1) 1154 1155 def use_project(self, id, where): 1156 # See comment in 'load' for explanation why we record the 1157 # parameters as opposed to loading the project now. 1158 assert is_iterable_typed(id, basestring) 1159 assert is_iterable_typed(where, basestring) 1160 m = self.registry.current().project_module() 1161 self.registry.used_projects[m].append((id[0], where[0])) 1162 1163 def build_project(self, dir): 1164 assert is_iterable_typed(dir, basestring) 1165 jamfile_module = self.registry.current().project_module() 1166 attributes = self.registry.attributes(jamfile_module) 1167 now = attributes.get("projects-to-build") 1168 attributes.set("projects-to-build", now + dir, exact=True) 1169 1170 def explicit(self, target_names): 1171 assert is_iterable_typed(target_names, basestring) 1172 self.registry.current().mark_targets_as_explicit(target_names) 1173 1174 def always(self, target_names): 1175 assert is_iterable_typed(target_names, basestring) 1176 self.registry.current().mark_targets_as_always(target_names) 1177 1178 def glob(self, wildcards, excludes=None): 1179 assert is_iterable_typed(wildcards, basestring) 1180 assert is_iterable_typed(excludes, basestring)or excludes is None 1181 return self.registry.glob_internal(self.registry.current(), 1182 wildcards, excludes, "glob") 1183 1184 def glob_tree(self, wildcards, excludes=None): 1185 assert is_iterable_typed(wildcards, basestring) 1186 assert is_iterable_typed(excludes, basestring) or excludes is None 1187 bad = 0 1188 for p in wildcards: 1189 if os.path.dirname(p): 1190 bad = 1 1191 1192 if excludes: 1193 for p in excludes: 1194 if os.path.dirname(p): 1195 bad = 1 1196 1197 if bad: 1198 self.registry.manager.errors()( 1199"The patterns to 'glob-tree' may not include directory") 1200 return self.registry.glob_internal(self.registry.current(), 1201 wildcards, excludes, "glob_tree") 1202 1203 1204 def using(self, toolset, *args): 1205 # The module referred by 'using' can be placed in 1206 # the same directory as Jamfile, and the user 1207 # will expect the module to be found even though 1208 # the directory is not in BOOST_BUILD_PATH. 1209 # So temporary change the search path. 1210 assert is_iterable_typed(toolset, basestring) 1211 current = self.registry.current() 1212 location = current.get('location') 1213 1214 m = self.registry.load_module(toolset[0], [location]) 1215 if "init" not in m.__dict__: 1216 self.registry.manager.errors()( 1217 "Tool module '%s' does not define the 'init' method" % toolset[0]) 1218 m.init(*args) 1219 1220 # The above might have clobbered .current-project. Restore the correct 1221 # value. 1222 self.registry.set_current(current) 1223 1224 def import_(self, name, names_to_import=None, local_names=None): 1225 assert is_iterable_typed(name, basestring) 1226 assert is_iterable_typed(names_to_import, basestring) or names_to_import is None 1227 assert is_iterable_typed(local_names, basestring)or local_names is None 1228 name = name[0] 1229 py_name = name 1230 if py_name == "os": 1231 py_name = "os_j" 1232 jamfile_module = self.registry.current().project_module() 1233 attributes = self.registry.attributes(jamfile_module) 1234 location = attributes.get("location") 1235 1236 saved = self.registry.current() 1237 1238 m = self.registry.load_module(py_name, [location]) 1239 1240 for f in m.__dict__: 1241 v = m.__dict__[f] 1242 f = f.replace("_", "-") 1243 if callable(v): 1244 qn = name + "." + f 1245 self._import_rule(jamfile_module, qn, v) 1246 record_jam_to_value_mapping(qualify_jam_action(qn, jamfile_module), v) 1247 1248 1249 if names_to_import: 1250 if not local_names: 1251 local_names = names_to_import 1252 1253 if len(names_to_import) != len(local_names): 1254 self.registry.manager.errors()( 1255"""The number of names to import and local names do not match.""") 1256 1257 for n, l in zip(names_to_import, local_names): 1258 self._import_rule(jamfile_module, l, m.__dict__[n]) 1259 1260 self.registry.set_current(saved) 1261 1262 def conditional(self, condition, requirements): 1263 """Calculates conditional requirements for multiple requirements 1264 at once. This is a shorthand to be reduce duplication and to 1265 keep an inline declarative syntax. For example: 1266 1267 lib x : x.cpp : [ conditional <toolset>gcc <variant>debug : 1268 <define>DEBUG_EXCEPTION <define>DEBUG_TRACE ] ; 1269 """ 1270 assert is_iterable_typed(condition, basestring) 1271 assert is_iterable_typed(requirements, basestring) 1272 c = string.join(condition, ",") 1273 if c.find(":") != -1: 1274 return [c + r for r in requirements] 1275 else: 1276 return [c + ":" + r for r in requirements] 1277 1278 def option(self, name, value): 1279 assert is_iterable(name) and isinstance(name[0], basestring) 1280 assert is_iterable(value) and isinstance(value[0], basestring) 1281 name = name[0] 1282 if not name in ["site-config", "user-config", "project-config"]: 1283 get_manager().errors()("The 'option' rule may be used only in site-config or user-config") 1284 1285 option.set(name, value[0]) 1286