Scripting.py 16 KB

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