waf_xattr.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. #! /usr/bin/env python
  2. # encoding: utf-8
  3. """
  4. Use extended attributes instead of database files
  5. 1. Input files will be made writable
  6. 2. This is only for systems providing extended filesystem attributes
  7. 3. By default, hashes are calculated only if timestamp/size change (HASH_CACHE below)
  8. 4. The module enables "deep_inputs" on all tasks by propagating task signatures
  9. 5. This module also skips task signature comparisons for task code changes due to point 4.
  10. 6. This module is for Python3/Linux only, but it could be extended to Python2/other systems
  11. using the xattr library
  12. 7. For projects in which tasks always declare output files, it should be possible to
  13. store the rest of build context attributes on output files (imp_sigs, raw_deps and node_deps)
  14. but this is not done here
  15. On a simple C++ project benchmark, the variations before and after adding waf_xattr.py were observed:
  16. total build time: 20s -> 22s
  17. no-op build time: 2.4s -> 1.8s
  18. pickle file size: 2.9MB -> 2.6MB
  19. """
  20. import os
  21. from waflib import Logs, Node, Task, Utils, Errors
  22. from waflib.Task import SKIP_ME, RUN_ME, CANCEL_ME, ASK_LATER, SKIPPED, MISSING
  23. HASH_CACHE = True
  24. SIG_VAR = 'user.waf.sig'
  25. SEP = ','.encode()
  26. TEMPLATE = '%b%d,%d'.encode()
  27. try:
  28. PermissionError
  29. except NameError:
  30. PermissionError = IOError
  31. def getxattr(self):
  32. return os.getxattr(self.abspath(), SIG_VAR)
  33. def setxattr(self, val):
  34. os.setxattr(self.abspath(), SIG_VAR, val)
  35. def h_file(self):
  36. try:
  37. ret = getxattr(self)
  38. except OSError:
  39. if HASH_CACHE:
  40. st = os.stat(self.abspath())
  41. mtime = st.st_mtime
  42. size = st.st_size
  43. else:
  44. if len(ret) == 16:
  45. # for build directory files
  46. return ret
  47. if HASH_CACHE:
  48. # check if timestamp and mtime match to avoid re-hashing
  49. st = os.stat(self.abspath())
  50. mtime, size = ret[16:].split(SEP)
  51. if int(1000 * st.st_mtime) == int(mtime) and st.st_size == int(size):
  52. return ret[:16]
  53. ret = Utils.h_file(self.abspath())
  54. if HASH_CACHE:
  55. val = TEMPLATE % (ret, int(1000 * st.st_mtime), int(st.st_size))
  56. try:
  57. setxattr(self, val)
  58. except PermissionError:
  59. os.chmod(self.abspath(), st.st_mode | 128)
  60. setxattr(self, val)
  61. return ret
  62. def runnable_status(self):
  63. bld = self.generator.bld
  64. if bld.is_install < 0:
  65. return SKIP_ME
  66. for t in self.run_after:
  67. if not t.hasrun:
  68. return ASK_LATER
  69. elif t.hasrun < SKIPPED:
  70. # a dependency has an error
  71. return CANCEL_ME
  72. # first compute the signature
  73. try:
  74. new_sig = self.signature()
  75. except Errors.TaskNotReady:
  76. return ASK_LATER
  77. if not self.outputs:
  78. # compare the signature to a signature computed previously
  79. # this part is only for tasks with no output files
  80. key = self.uid()
  81. try:
  82. prev_sig = bld.task_sigs[key]
  83. except KeyError:
  84. Logs.debug('task: task %r must run: it was never run before or the task code changed', self)
  85. return RUN_ME
  86. if new_sig != prev_sig:
  87. Logs.debug('task: task %r must run: the task signature changed', self)
  88. return RUN_ME
  89. # compare the signatures of the outputs to make a decision
  90. for node in self.outputs:
  91. try:
  92. sig = node.h_file()
  93. except EnvironmentError:
  94. Logs.debug('task: task %r must run: an output node does not exist', self)
  95. return RUN_ME
  96. if sig != new_sig:
  97. Logs.debug('task: task %r must run: an output node is stale', self)
  98. return RUN_ME
  99. return (self.always_run and RUN_ME) or SKIP_ME
  100. def post_run(self):
  101. bld = self.generator.bld
  102. sig = self.signature()
  103. for node in self.outputs:
  104. if not node.exists():
  105. self.hasrun = MISSING
  106. self.err_msg = '-> missing file: %r' % node.abspath()
  107. raise Errors.WafError(self.err_msg)
  108. os.setxattr(node.abspath(), 'user.waf.sig', sig)
  109. if not self.outputs:
  110. # only for task with no outputs
  111. bld.task_sigs[self.uid()] = sig
  112. if not self.keep_last_cmd:
  113. try:
  114. del self.last_cmd
  115. except AttributeError:
  116. pass
  117. try:
  118. os.getxattr
  119. except AttributeError:
  120. pass
  121. else:
  122. h_file.__doc__ = Node.Node.h_file.__doc__
  123. # keep file hashes as file attributes
  124. Node.Node.h_file = h_file
  125. # enable "deep_inputs" on all tasks
  126. Task.Task.runnable_status = runnable_status
  127. Task.Task.post_run = post_run
  128. Task.Task.sig_deep_inputs = Utils.nada