1#!/usr/bin/env python 2# encoding: utf-8 3# Thomas Nagy, 2005-2018 (ita) 4 5"Module called for configuring, compiling and installing targets" 6 7from __future__ import with_statement 8 9import os, shlex, shutil, traceback, errno, sys, stat 10from waflib import Utils, Configure, Logs, Options, ConfigSet, Context, Errors, Build, Node 11 12build_dir_override = None 13 14no_climb_commands = ['configure'] 15 16default_cmd = "build" 17 18def waf_entry_point(current_directory, version, wafdir): 19 """ 20 This is the main entry point, all Waf execution starts here. 21 22 :param current_directory: absolute path representing the current directory 23 :type current_directory: string 24 :param version: version number 25 :type version: string 26 :param wafdir: absolute path representing the directory of the waf library 27 :type wafdir: string 28 """ 29 Logs.init_log() 30 31 if Context.WAFVERSION != version: 32 Logs.error('Waf script %r and library %r do not match (directory %r)', version, Context.WAFVERSION, wafdir) 33 sys.exit(1) 34 35 # Store current directory before any chdir 36 Context.waf_dir = wafdir 37 Context.run_dir = Context.launch_dir = current_directory 38 start_dir = current_directory 39 no_climb = os.environ.get('NOCLIMB') 40 41 if len(sys.argv) > 1: 42 # os.path.join handles absolute paths 43 # if sys.argv[1] is not an absolute path, then it is relative to the current working directory 44 potential_wscript = os.path.join(current_directory, sys.argv[1]) 45 if os.path.basename(potential_wscript) == Context.WSCRIPT_FILE and os.path.isfile(potential_wscript): 46 # need to explicitly normalize the path, as it may contain extra '/.' 47 path = os.path.normpath(os.path.dirname(potential_wscript)) 48 start_dir = os.path.abspath(path) 49 no_climb = True 50 sys.argv.pop(1) 51 52 ctx = Context.create_context('options') 53 (options, commands, env) = ctx.parse_cmd_args(allow_unknown=True) 54 if options.top: 55 start_dir = Context.run_dir = Context.top_dir = options.top 56 no_climb = True 57 if options.out: 58 Context.out_dir = options.out 59 60 # if 'configure' is in the commands, do not search any further 61 if not no_climb: 62 for k in no_climb_commands: 63 for y in commands: 64 if y.startswith(k): 65 no_climb = True 66 break 67 68 # try to find a lock file (if the project was configured) 69 # at the same time, store the first wscript file seen 70 cur = start_dir 71 while cur: 72 try: 73 lst = os.listdir(cur) 74 except OSError: 75 lst = [] 76 Logs.error('Directory %r is unreadable!', cur) 77 if Options.lockfile in lst: 78 env = ConfigSet.ConfigSet() 79 try: 80 env.load(os.path.join(cur, Options.lockfile)) 81 ino = os.stat(cur)[stat.ST_INO] 82 except EnvironmentError: 83 pass 84 else: 85 # check if the folder was not moved 86 for x in (env.run_dir, env.top_dir, env.out_dir): 87 if not x: 88 continue 89 if Utils.is_win32: 90 if cur == x: 91 load = True 92 break 93 else: 94 # if the filesystem features symlinks, compare the inode numbers 95 try: 96 ino2 = os.stat(x)[stat.ST_INO] 97 except OSError: 98 pass 99 else: 100 if ino == ino2: 101 load = True 102 break 103 else: 104 Logs.warn('invalid lock file in %s', cur) 105 load = False 106 107 if load: 108 Context.run_dir = env.run_dir 109 Context.top_dir = env.top_dir 110 Context.out_dir = env.out_dir 111 break 112 113 if not Context.run_dir: 114 if Context.WSCRIPT_FILE in lst: 115 Context.run_dir = cur 116 117 next = os.path.dirname(cur) 118 if next == cur: 119 break 120 cur = next 121 122 if no_climb: 123 break 124 125 wscript = os.path.normpath(os.path.join(Context.run_dir, Context.WSCRIPT_FILE)) 126 if not os.path.exists(wscript): 127 if options.whelp: 128 Logs.warn('These are the generic options (no wscript/project found)') 129 ctx.parser.print_help() 130 sys.exit(0) 131 Logs.error('Waf: Run from a folder containing a %r file (or try -h for the generic options)', Context.WSCRIPT_FILE) 132 sys.exit(1) 133 134 try: 135 os.chdir(Context.run_dir) 136 except OSError: 137 Logs.error('Waf: The folder %r is unreadable', Context.run_dir) 138 sys.exit(1) 139 140 try: 141 set_main_module(wscript) 142 except Errors.WafError as e: 143 Logs.pprint('RED', e.verbose_msg) 144 Logs.error(str(e)) 145 sys.exit(1) 146 except Exception as e: 147 Logs.error('Waf: The wscript in %r is unreadable', Context.run_dir) 148 traceback.print_exc(file=sys.stdout) 149 sys.exit(2) 150 151 if options.profile: 152 import cProfile, pstats 153 cProfile.runctx('from waflib import Scripting; Scripting.run_commands()', {}, {}, 'profi.txt') 154 p = pstats.Stats('profi.txt') 155 p.sort_stats('time').print_stats(75) # or 'cumulative' 156 else: 157 try: 158 try: 159 run_commands() 160 except: 161 if options.pdb: 162 import pdb 163 type, value, tb = sys.exc_info() 164 traceback.print_exc() 165 pdb.post_mortem(tb) 166 else: 167 raise 168 except Errors.WafError as e: 169 if Logs.verbose > 1: 170 Logs.pprint('RED', e.verbose_msg) 171 Logs.error(e.msg) 172 sys.exit(1) 173 except SystemExit: 174 raise 175 except Exception as e: 176 traceback.print_exc(file=sys.stdout) 177 sys.exit(2) 178 except KeyboardInterrupt: 179 Logs.pprint('RED', 'Interrupted') 180 sys.exit(68) 181 182def set_main_module(file_path): 183 """ 184 Read the main wscript file into :py:const:`waflib.Context.Context.g_module` and 185 bind default functions such as ``init``, ``dist``, ``distclean`` if not defined. 186 Called by :py:func:`waflib.Scripting.waf_entry_point` during the initialization. 187 188 :param file_path: absolute path representing the top-level wscript file 189 :type file_path: string 190 """ 191 Context.g_module = Context.load_module(file_path) 192 Context.g_module.root_path = file_path 193 194 # note: to register the module globally, use the following: 195 # sys.modules['wscript_main'] = g_module 196 197 def set_def(obj): 198 name = obj.__name__ 199 if not name in Context.g_module.__dict__: 200 setattr(Context.g_module, name, obj) 201 for k in (dist, distclean, distcheck): 202 set_def(k) 203 # add dummy init and shutdown functions if they're not defined 204 if not 'init' in Context.g_module.__dict__: 205 Context.g_module.init = Utils.nada 206 if not 'shutdown' in Context.g_module.__dict__: 207 Context.g_module.shutdown = Utils.nada 208 if not 'options' in Context.g_module.__dict__: 209 Context.g_module.options = Utils.nada 210 211def parse_options(): 212 """ 213 Parses the command-line options and initialize the logging system. 214 Called by :py:func:`waflib.Scripting.waf_entry_point` during the initialization. 215 """ 216 ctx = Context.create_context('options') 217 ctx.execute() 218 if not Options.commands: 219 if isinstance(default_cmd, list): 220 Options.commands.extend(default_cmd) 221 else: 222 Options.commands.append(default_cmd) 223 if Options.options.whelp: 224 ctx.parser.print_help() 225 sys.exit(0) 226 227def run_command(cmd_name): 228 """ 229 Executes a single Waf command. Called by :py:func:`waflib.Scripting.run_commands`. 230 231 :param cmd_name: command to execute, like ``build`` 232 :type cmd_name: string 233 """ 234 ctx = Context.create_context(cmd_name) 235 ctx.log_timer = Utils.Timer() 236 ctx.options = Options.options # provided for convenience 237 ctx.cmd = cmd_name 238 try: 239 ctx.execute() 240 finally: 241 # Issue 1374 242 ctx.finalize() 243 return ctx 244 245def run_commands(): 246 """ 247 Execute the Waf commands that were given on the command-line, and the other options 248 Called by :py:func:`waflib.Scripting.waf_entry_point` during the initialization, and executed 249 after :py:func:`waflib.Scripting.parse_options`. 250 """ 251 parse_options() 252 run_command('init') 253 while Options.commands: 254 cmd_name = Options.commands.pop(0) 255 ctx = run_command(cmd_name) 256 Logs.info('%r finished successfully (%s)', cmd_name, ctx.log_timer) 257 run_command('shutdown') 258 259########################################################################################### 260 261def distclean_dir(dirname): 262 """ 263 Distclean function called in the particular case when:: 264 265 top == out 266 267 :param dirname: absolute path of the folder to clean 268 :type dirname: string 269 """ 270 for (root, dirs, files) in os.walk(dirname): 271 for f in files: 272 if f.endswith(('.o', '.moc', '.exe')): 273 fname = os.path.join(root, f) 274 try: 275 os.remove(fname) 276 except OSError: 277 Logs.warn('Could not remove %r', fname) 278 279 for x in (Context.DBFILE, 'config.log'): 280 try: 281 os.remove(x) 282 except OSError: 283 pass 284 285 try: 286 shutil.rmtree(Build.CACHE_DIR) 287 except OSError: 288 pass 289 290def distclean(ctx): 291 '''removes build folders and data''' 292 293 def remove_and_log(k, fun): 294 try: 295 fun(k) 296 except EnvironmentError as e: 297 if e.errno != errno.ENOENT: 298 Logs.warn('Could not remove %r', k) 299 300 # remove waf cache folders on the top-level 301 if not Options.commands: 302 for k in os.listdir('.'): 303 for x in '.waf-2 waf-2 .waf3-2 waf3-2'.split(): 304 if k.startswith(x): 305 remove_and_log(k, shutil.rmtree) 306 307 # remove a build folder, if any 308 cur = '.' 309 if os.environ.get('NO_LOCK_IN_TOP') or ctx.options.no_lock_in_top: 310 cur = ctx.options.out 311 312 try: 313 lst = os.listdir(cur) 314 except OSError: 315 Logs.warn('Could not read %r', cur) 316 return 317 318 if Options.lockfile in lst: 319 f = os.path.join(cur, Options.lockfile) 320 try: 321 env = ConfigSet.ConfigSet(f) 322 except EnvironmentError: 323 Logs.warn('Could not read %r', f) 324 return 325 326 if not env.out_dir or not env.top_dir: 327 Logs.warn('Invalid lock file %r', f) 328 return 329 330 if env.out_dir == env.top_dir: 331 distclean_dir(env.out_dir) 332 else: 333 remove_and_log(env.out_dir, shutil.rmtree) 334 335 env_dirs = [env.out_dir] 336 if not (os.environ.get('NO_LOCK_IN_TOP') or ctx.options.no_lock_in_top): 337 env_dirs.append(env.top_dir) 338 if not (os.environ.get('NO_LOCK_IN_RUN') or ctx.options.no_lock_in_run): 339 env_dirs.append(env.run_dir) 340 for k in env_dirs: 341 p = os.path.join(k, Options.lockfile) 342 remove_and_log(p, os.remove) 343 344class Dist(Context.Context): 345 '''creates an archive containing the project source code''' 346 cmd = 'dist' 347 fun = 'dist' 348 algo = 'tar.bz2' 349 ext_algo = {} 350 351 def execute(self): 352 """ 353 See :py:func:`waflib.Context.Context.execute` 354 """ 355 self.recurse([os.path.dirname(Context.g_module.root_path)]) 356 self.archive() 357 358 def archive(self): 359 """ 360 Creates the source archive. 361 """ 362 import tarfile 363 364 arch_name = self.get_arch_name() 365 366 try: 367 self.base_path 368 except AttributeError: 369 self.base_path = self.path 370 371 node = self.base_path.make_node(arch_name) 372 try: 373 node.delete() 374 except OSError: 375 pass 376 377 files = self.get_files() 378 379 if self.algo.startswith('tar.'): 380 tar = tarfile.open(node.abspath(), 'w:' + self.algo.replace('tar.', '')) 381 382 for x in files: 383 self.add_tar_file(x, tar) 384 tar.close() 385 elif self.algo == 'zip': 386 import zipfile 387 zip = zipfile.ZipFile(node.abspath(), 'w', compression=zipfile.ZIP_DEFLATED) 388 389 for x in files: 390 archive_name = self.get_base_name() + '/' + x.path_from(self.base_path) 391 zip.write(x.abspath(), archive_name, zipfile.ZIP_DEFLATED) 392 zip.close() 393 else: 394 self.fatal('Valid algo types are tar.bz2, tar.gz, tar.xz or zip') 395 396 try: 397 from hashlib import sha256 398 except ImportError: 399 digest = '' 400 else: 401 digest = ' (sha256=%r)' % sha256(node.read(flags='rb')).hexdigest() 402 403 Logs.info('New archive created: %s%s', self.arch_name, digest) 404 405 def get_tar_path(self, node): 406 """ 407 Return the path to use for a node in the tar archive, the purpose of this 408 is to let subclases resolve symbolic links or to change file names 409 410 :return: absolute path 411 :rtype: string 412 """ 413 return node.abspath() 414 415 def add_tar_file(self, x, tar): 416 """ 417 Adds a file to the tar archive. Symlinks are not verified. 418 419 :param x: file path 420 :param tar: tar file object 421 """ 422 p = self.get_tar_path(x) 423 tinfo = tar.gettarinfo(name=p, arcname=self.get_tar_prefix() + '/' + x.path_from(self.base_path)) 424 tinfo.uid = 0 425 tinfo.gid = 0 426 tinfo.uname = 'root' 427 tinfo.gname = 'root' 428 429 if os.path.isfile(p): 430 with open(p, 'rb') as f: 431 tar.addfile(tinfo, fileobj=f) 432 else: 433 tar.addfile(tinfo) 434 435 def get_tar_prefix(self): 436 """ 437 Returns the base path for files added into the archive tar file 438 439 :rtype: string 440 """ 441 try: 442 return self.tar_prefix 443 except AttributeError: 444 return self.get_base_name() 445 446 def get_arch_name(self): 447 """ 448 Returns the archive file name. 449 Set the attribute *arch_name* to change the default value:: 450 451 def dist(ctx): 452 ctx.arch_name = 'ctx.tar.bz2' 453 454 :rtype: string 455 """ 456 try: 457 self.arch_name 458 except AttributeError: 459 self.arch_name = self.get_base_name() + '.' + self.ext_algo.get(self.algo, self.algo) 460 return self.arch_name 461 462 def get_base_name(self): 463 """ 464 Returns the default name of the main directory in the archive, which is set to *appname-version*. 465 Set the attribute *base_name* to change the default value:: 466 467 def dist(ctx): 468 ctx.base_name = 'files' 469 470 :rtype: string 471 """ 472 try: 473 self.base_name 474 except AttributeError: 475 appname = getattr(Context.g_module, Context.APPNAME, 'noname') 476 version = getattr(Context.g_module, Context.VERSION, '1.0') 477 self.base_name = appname + '-' + version 478 return self.base_name 479 480 def get_excl(self): 481 """ 482 Returns the patterns to exclude for finding the files in the top-level directory. 483 Set the attribute *excl* to change the default value:: 484 485 def dist(ctx): 486 ctx.excl = 'build **/*.o **/*.class' 487 488 :rtype: string 489 """ 490 try: 491 return self.excl 492 except AttributeError: 493 self.excl = Node.exclude_regs + ' **/waf-2.* **/.waf-2.* **/waf3-2.* **/.waf3-2.* **/*~ **/*.rej **/*.orig **/*.pyc **/*.pyo **/*.bak **/*.swp **/.lock-w*' 494 if Context.out_dir: 495 nd = self.root.find_node(Context.out_dir) 496 if nd: 497 self.excl += ' ' + nd.path_from(self.base_path) 498 return self.excl 499 500 def get_files(self): 501 """ 502 Files to package are searched automatically by :py:func:`waflib.Node.Node.ant_glob`. 503 Set *files* to prevent this behaviour:: 504 505 def dist(ctx): 506 ctx.files = ctx.path.find_node('wscript') 507 508 Files are also searched from the directory 'base_path', to change it, set:: 509 510 def dist(ctx): 511 ctx.base_path = path 512 513 :rtype: list of :py:class:`waflib.Node.Node` 514 """ 515 try: 516 files = self.files 517 except AttributeError: 518 files = self.base_path.ant_glob('**/*', excl=self.get_excl()) 519 return files 520 521def dist(ctx): 522 '''makes a tarball for redistributing the sources''' 523 pass 524 525class DistCheck(Dist): 526 """creates an archive with dist, then tries to build it""" 527 fun = 'distcheck' 528 cmd = 'distcheck' 529 530 def execute(self): 531 """ 532 See :py:func:`waflib.Context.Context.execute` 533 """ 534 self.recurse([os.path.dirname(Context.g_module.root_path)]) 535 self.archive() 536 self.check() 537 538 def make_distcheck_cmd(self, tmpdir): 539 cfg = [] 540 if Options.options.distcheck_args: 541 cfg = shlex.split(Options.options.distcheck_args) 542 else: 543 cfg = [x for x in sys.argv if x.startswith('-')] 544 cmd = [sys.executable, sys.argv[0], 'configure', 'build', 'install', 'uninstall', '--destdir=' + tmpdir] + cfg 545 return cmd 546 547 def check(self): 548 """ 549 Creates the archive, uncompresses it and tries to build the project 550 """ 551 import tempfile, tarfile 552 553 with tarfile.open(self.get_arch_name()) as t: 554 for x in t: 555 t.extract(x) 556 557 instdir = tempfile.mkdtemp('.inst', self.get_base_name()) 558 cmd = self.make_distcheck_cmd(instdir) 559 ret = Utils.subprocess.Popen(cmd, cwd=self.get_base_name()).wait() 560 if ret: 561 raise Errors.WafError('distcheck failed with code %r' % ret) 562 563 if os.path.exists(instdir): 564 raise Errors.WafError('distcheck succeeded, but files were left in %s' % instdir) 565 566 shutil.rmtree(self.get_base_name()) 567 568 569def distcheck(ctx): 570 '''checks if the project compiles (tarball from 'dist')''' 571 pass 572 573def autoconfigure(execute_method): 574 """ 575 Decorator that enables context commands to run *configure* as needed. 576 """ 577 def execute(self): 578 """ 579 Wraps :py:func:`waflib.Context.Context.execute` on the context class 580 """ 581 if not Configure.autoconfig: 582 return execute_method(self) 583 584 env = ConfigSet.ConfigSet() 585 do_config = False 586 try: 587 env.load(os.path.join(Context.top_dir, Options.lockfile)) 588 except EnvironmentError: 589 Logs.warn('Configuring the project') 590 do_config = True 591 else: 592 if env.run_dir != Context.run_dir: 593 do_config = True 594 else: 595 h = 0 596 for f in env.files: 597 try: 598 h = Utils.h_list((h, Utils.readf(f, 'rb'))) 599 except EnvironmentError: 600 do_config = True 601 break 602 else: 603 do_config = h != env.hash 604 605 if do_config: 606 cmd = env.config_cmd or 'configure' 607 if Configure.autoconfig == 'clobber': 608 tmp = Options.options.__dict__ 609 launch_dir_tmp = Context.launch_dir 610 if env.options: 611 Options.options.__dict__ = env.options 612 Context.launch_dir = env.launch_dir 613 try: 614 run_command(cmd) 615 finally: 616 Options.options.__dict__ = tmp 617 Context.launch_dir = launch_dir_tmp 618 else: 619 run_command(cmd) 620 run_command(self.cmd) 621 else: 622 return execute_method(self) 623 return execute 624Build.BuildContext.execute = autoconfigure(Build.BuildContext.execute) 625 626