wscript 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. #! /usr/bin/env python
  2. # encoding: utf-8
  3. # Thomas Nagy, 2005-2018
  4. """
  5. to make a custom waf file use the option --tools
  6. To add a tool that does not exist in the folder compat15, pass an absolute path:
  7. ./waf-light --tools=compat15,/comp/waf/aba.py --prelude=$'\tfrom waflib.extras import aba\n\taba.foo()'
  8. """
  9. VERSION="2.1.4"
  10. APPNAME='waf'
  11. REVISION=''
  12. top = '.'
  13. out = 'build'
  14. zip_types = ['bz2', 'gz', 'xz']
  15. PRELUDE = ''
  16. import os, sys, re, io, optparse, tokenize
  17. from hashlib import md5
  18. from waflib import Errors, Utils, Options, Logs, Scripting
  19. from waflib import Configure
  20. Configure.autoconfig = 1
  21. def sub_file(fname, lst):
  22. with open(fname, 'r') as f:
  23. txt = f.read()
  24. for (key, val) in lst:
  25. re_pat = re.compile(key, re.M)
  26. txt = re_pat.sub(val, txt)
  27. with open(fname, 'w') as f:
  28. f.write(txt)
  29. def to_bytes(x):
  30. if sys.hexversion>0x300000f:
  31. return x.encode()
  32. return x
  33. Logs.warn('------> Executing code from the top-level wscript <-----')
  34. def init(ctx):
  35. if Options.options.setver: # maintainer only (ita)
  36. ver = Options.options.setver
  37. hexver = Utils.num2ver(ver)
  38. hexver = '0x%x'%hexver
  39. sub_file('wscript', (('^VERSION=(.*)', 'VERSION="%s"' % ver), ))
  40. sub_file('waf-light', (('^VERSION=(.*)', 'VERSION="%s"' % ver), ))
  41. pats = []
  42. pats.append(('^WAFVERSION=(.*)', 'WAFVERSION="%s"' % ver))
  43. pats.append(('^HEXVERSION(.*)', 'HEXVERSION=%s' % hexver))
  44. try:
  45. rev = ctx.cmd_and_log("git rev-parse HEAD").strip()
  46. except Errors.WafError:
  47. rev = ''
  48. else:
  49. pats.append(('^WAFREVISION(.*)', 'WAFREVISION="%s"' % rev))
  50. sub_file('waflib/Context.py', pats)
  51. sys.exit(0)
  52. def check(ctx):
  53. Logs.warn('Nothing to do')
  54. # this function is called before any other for parsing the command-line
  55. def options(opt):
  56. # generate waf
  57. opt.add_option('--make-waf', action='store_true', default=True,
  58. help='creates the waf script', dest='waf')
  59. opt.add_option('--interpreter', action='store', default=None,
  60. help='specify the #! line on top of the waf file', dest='interpreter')
  61. opt.add_option('--sign', action='store_true', default=False, help='make a signed file', dest='signed')
  62. default_zip = 'bz2'
  63. if os.name == 'java':
  64. default_zip = 'gz'
  65. opt.add_option('--zip-type', action='store', default=default_zip,
  66. help='specify the zip type [Allowed values: %s]' % ' '.join(zip_types), dest='zip')
  67. opt.add_option('--make-batch', action='store_true', default=False,
  68. help='creates a convenience waf.bat file (done automatically on win32 systems)',
  69. dest='make_batch')
  70. opt.add_option('--yes', action='store_true', default=False,
  71. help=optparse.SUPPRESS_HELP,
  72. dest='yes')
  73. # those ones are not too interesting
  74. opt.add_option('--set-version', default='',
  75. help='sets the version number for waf releases (for the maintainer)', dest='setver')
  76. opt.add_option('--set-name', default='waf', help=optparse.SUPPRESS_HELP, dest='wafname')
  77. opt.add_option('--strip', action='store_true', default=True,
  78. help='shrinks waf (strip docstrings, saves 33kb)',
  79. dest='strip_comments')
  80. opt.add_option('--nostrip', action='store_false', help='no shrinking',
  81. dest='strip_comments')
  82. opt.add_option('--tools', action='store', help='Comma-separated 3rd party tools to add, eg: "compat,ocaml" [Default: "compat15"]',
  83. dest='add3rdparty', default='compat15')
  84. opt.add_option('--coretools', action='store', help='Comma-separated core tools to add, eg: "vala,tex" [Default: all of them]',
  85. dest='coretools', default='default')
  86. opt.add_option('--prelude', action='store', help='Code to execute before calling waf', dest='prelude', default=PRELUDE)
  87. opt.add_option('--namesfrom', action='store', help='Obtain the file names from a model archive', dest='namesfrom', default=None)
  88. opt.load('python')
  89. def process_tokens(tokens):
  90. accu = []
  91. prev = tokenize.NEWLINE
  92. indent = 0
  93. line_buf = []
  94. for (type, token, start, end, line) in tokens:
  95. token = token.replace('\r\n', '\n')
  96. if type == tokenize.NEWLINE:
  97. if line_buf:
  98. accu.append(indent * '\t')
  99. ln = "".join(line_buf)
  100. if ln == 'if __name__=="__main__":': break
  101. #ln = ln.replace('\n', '')
  102. accu.append(ln)
  103. accu.append('\n')
  104. line_buf = []
  105. prev = tokenize.NEWLINE
  106. elif type == tokenize.INDENT:
  107. indent += 1
  108. elif type == tokenize.DEDENT:
  109. indent -= 1
  110. elif type == tokenize.NAME:
  111. if prev == tokenize.NAME or prev == tokenize.NUMBER: line_buf.append(' ')
  112. line_buf.append(token)
  113. elif type == tokenize.NUMBER:
  114. if prev == tokenize.NAME or prev == tokenize.NUMBER: line_buf.append(' ')
  115. line_buf.append(token)
  116. elif type == tokenize.STRING:
  117. if not line_buf and token.startswith('"'): pass
  118. else: line_buf.append(token)
  119. elif type == tokenize.COMMENT:
  120. pass
  121. elif type == tokenize.OP:
  122. line_buf.append(token)
  123. else:
  124. if token != "\n": line_buf.append(token)
  125. if token != '\n':
  126. prev = type
  127. body = ''.join(accu)
  128. return body
  129. deco_re = re.compile('(def|class)\\s+(\\w+)\\(.*')
  130. def process_decorators(body):
  131. lst = body.splitlines()
  132. accu = []
  133. all_deco = []
  134. buf = [] # put the decorator lines
  135. for line in lst:
  136. if line.startswith('@'):
  137. buf.append(line[1:])
  138. elif buf:
  139. name = deco_re.sub('\\2', line)
  140. if not name:
  141. raise IOError("decorator not followed by a function!" + line)
  142. for x in buf:
  143. all_deco.append('%s(%s)' % (x, name))
  144. accu.append(line)
  145. buf = []
  146. else:
  147. accu.append(line)
  148. return '\n'.join(accu+all_deco)
  149. def sfilter(path):
  150. if path.endswith('.py') :
  151. if Options.options.strip_comments:
  152. if sys.version_info[0] >= 3:
  153. with open(path, 'rb') as f:
  154. tk = tokenize.tokenize(f.readline)
  155. next(tk) # the first one is always tokenize.ENCODING for Python 3, ignore it
  156. cnt = process_tokens(tk)
  157. else:
  158. with open(path, 'r') as f:
  159. cnt = process_tokens(tokenize.generate_tokens(f.readline))
  160. else:
  161. with open(path, 'r') as f:
  162. cnt = f.read()
  163. # WARNING: since python >= 2.5 is required, decorators are not processed anymore
  164. # uncomment the following to enable decorator replacement:
  165. #cnt = process_decorators(cnt)
  166. #if cnt.find('set(') > -1:
  167. # cnt = 'import sys\nif sys.hexversion < 0x020400f0: from sets import Set as set\n' + cnt
  168. cnt = '#! /usr/bin/env python\n# encoding: utf-8\n# WARNING! Do not edit! https://waf.io/book/index.html#_obtaining_the_waf_file\n\n' + cnt
  169. else:
  170. with open(path, 'r') as f:
  171. cnt = f.read()
  172. if sys.hexversion > 0x030000f0:
  173. return (io.BytesIO(cnt.encode('utf-8')), len(cnt.encode('utf-8')), cnt)
  174. return (io.BytesIO(cnt), len(cnt), cnt)
  175. def create_waf(self, *k, **kw):
  176. mw = 'tmp-waf-'+VERSION
  177. print('-> preparing %r' % mw)
  178. import tarfile, zipfile
  179. zipType = Options.options.zip.strip().lower()
  180. if zipType not in zip_types:
  181. zipType = zip_types[0]
  182. directory_files = {}
  183. files = []
  184. add3rdparty = []
  185. for x in Options.options.add3rdparty.split(','):
  186. if os.path.isdir(x):
  187. # Create mapping from files absolute path to path in module
  188. # directory (for module mylib):
  189. #
  190. # {"/home/path/mylib/__init__.py": "mylib/__init__.py",
  191. # "/home/path/mylib/lib.py": "mylib/lib.py",
  192. # "/home/path/mylib/sub/sub.py": "mylib/sub/lib.py"
  193. # }
  194. #
  195. x_dir = self.generator.bld.root.find_dir(
  196. os.path.abspath(os.path.expanduser(x)))
  197. file_list = x_dir.ant_glob('**/*.py')
  198. for f in file_list:
  199. file_from = f.abspath()
  200. file_to = os.path.join(x_dir.name, f.path_from(x_dir))
  201. # If this is executed on Windows, then file_to will contain
  202. # '\' path separators. These should be changed to '/', otherwise
  203. # the added tools will not be accessible on Unix systems.
  204. directory_files[file_from] = file_to.replace('\\', '/')
  205. files.append(file_from)
  206. elif os.path.isabs(x):
  207. files.append(x)
  208. else:
  209. add3rdparty.append(x + '.py')
  210. coretools = []
  211. for x in Options.options.coretools.split(','):
  212. coretools.append(x + '.py')
  213. up_node = self.generator.bld.path
  214. for node in up_node.find_dir('waflib').ant_glob(incl=['*.py', 'Tools/*.py', 'extras/*.py']):
  215. relpath = node.path_from(up_node)
  216. if node.name == '__init__.py':
  217. files.append(relpath)
  218. continue
  219. if node.parent.name == 'Tools' and Options.options.coretools != 'default':
  220. if node.name not in coretools:
  221. continue
  222. if node.parent.name == 'extras':
  223. if node.name not in add3rdparty:
  224. continue
  225. files.append(relpath)
  226. if Options.options.namesfrom:
  227. with tarfile.open(Options.options.namesfrom) as tar:
  228. oldfiles = files
  229. files = [x.name for x in tar.getmembers()]
  230. if set(files) ^ set(oldfiles):
  231. Logs.warn('The archive model has differences:')
  232. Logs.warn('- Added %r', list(set(files) - set(oldfiles)))
  233. Logs.warn('- Removed %r', list(set(oldfiles) - set(files)))
  234. #open a file as tar.[extension] for writing
  235. tar = tarfile.open('%s.tar.%s' % (mw, zipType), "w:%s" % zipType)
  236. z = zipfile.ZipFile("zip/waflib.zip", "w", compression=zipfile.ZIP_DEFLATED)
  237. for x in files:
  238. try:
  239. tarinfo = tar.gettarinfo(x, x)
  240. except NotImplementedError:
  241. # jython 2.7.0 workaround
  242. tarinfo = tarfile.TarInfo(x)
  243. tarinfo.uid = tarinfo.gid = 0
  244. tarinfo.uname = tarinfo.gname = 'root'
  245. if os.environ.get('SOURCE_DATE_EPOCH'):
  246. tarinfo.mtime = int(os.environ.get('SOURCE_DATE_EPOCH'))
  247. (code, size, cnt) = sfilter(x)
  248. tarinfo.size = size
  249. if x in directory_files:
  250. tarinfo.name = 'waflib/extras/' + directory_files[x]
  251. elif os.path.isabs(x):
  252. tarinfo.name = 'waflib/extras/' + os.path.split(x)[1]
  253. print(' adding %s as %s' % (x, tarinfo.name))
  254. def dest(x):
  255. if x in directory_files:
  256. return os.path.join('waflib', 'extras', directory_files[x])
  257. elif os.path.isabs(x):
  258. return os.path.join('waflib', 'extras', os.path.basename(x))
  259. else:
  260. return os.path.normpath(os.path.relpath(x, "."))
  261. z.write(x, dest(x))
  262. tar.addfile(tarinfo, code)
  263. tar.close()
  264. z.close()
  265. with open('waf-light', 'r') as f:
  266. code1 = f.read()
  267. # tune the application name if necessary
  268. if Options.options.wafname != 'waf':
  269. Options.options.prelude = '\tfrom waflib import Context\n\tContext.WAFNAME=%r\n' % Options.options.wafname + Options.options.prelude
  270. # now store the revision unique number in waf
  271. code1 = code1.replace("if sys.hexversion<0x206000f:\n\traise ImportError('Python >= 2.6 is required to create the waf file')\n", '')
  272. code1 = code1.replace('\t#import waflib.extras.compat15#PRELUDE', Options.options.prelude)
  273. # when possible, set the git revision in the waf file
  274. bld = self.generator.bld
  275. try:
  276. rev = bld.cmd_and_log('git rev-parse HEAD', quiet=0).strip()
  277. except Errors.WafError:
  278. rev = ''
  279. else:
  280. reg = re.compile('^GIT(.*)', re.M)
  281. code1 = reg.sub('GIT="%s"' % rev, code1)
  282. # if the waf file is installed somewhere... but do not do that
  283. prefix = ''
  284. reg = re.compile('^INSTALL=(.*)', re.M)
  285. code1 = reg.sub(r'INSTALL=%r' % prefix, code1)
  286. #change the tarfile extension in the waf script
  287. reg = re.compile('bz2', re.M)
  288. code1 = reg.sub(zipType, code1)
  289. if zipType == 'gz':
  290. code1 = code1.replace('bunzip2', 'gzip -d')
  291. elif zipType == 'xz':
  292. code1 = code1.replace('bunzip2', 'xz -d')
  293. with open('%s.tar.%s' % (mw, zipType), 'rb') as f:
  294. cnt = f.read()
  295. # the REVISION value is the md5 sum of the compressed data (facilitate audits)
  296. m = md5()
  297. m.update(cnt)
  298. REVISION = m.hexdigest()
  299. reg = re.compile('^REVISION=(.*)', re.M)
  300. code1 = reg.sub(r'REVISION="%s"' % REVISION, code1)
  301. def find_unused(kd, ch):
  302. for i in range(35, 125):
  303. for j in range(35, 125):
  304. if i==j: continue
  305. if i == 39 or j == 39: continue
  306. if i == 92 or j == 92: continue
  307. s = chr(i) + chr(j)
  308. if -1 == kd.find(s.encode()):
  309. return (kd.replace(ch.encode(), s.encode()), s)
  310. raise ValueError('Could not find a proper encoding')
  311. # The reverse order prevents collisions
  312. (cnt, C3) = find_unused(cnt, '\x00')
  313. (cnt, C2) = find_unused(cnt, '\r')
  314. (cnt, C1) = find_unused(cnt, '\n')
  315. ccc = code1.replace("C1='x'", "C1='%s'" % C1).replace("C2='x'", "C2='%s'" % C2).replace("C3='x'", "C3='%s'" % C3)
  316. if getattr(Options.options, 'interpreter', None):
  317. ccc = ccc.replace('#!/usr/bin/env python', Options.options.interpreter)
  318. with open('waf', 'wb') as f:
  319. f.write(ccc.encode())
  320. f.write(to_bytes('#==>\n#'))
  321. f.write(cnt)
  322. f.write(to_bytes('\n#<==\n'))
  323. if Options.options.signed:
  324. f.flush()
  325. try:
  326. os.remove('waf.asc')
  327. except OSError:
  328. pass
  329. ret = Utils.subprocess.Popen('gpg -bass waf', shell=True).wait()
  330. if ret:
  331. raise ValueError('Could not sign the waf file!')
  332. sig = Utils.readf('waf.asc')
  333. sig = sig.replace('\r', '').replace('\n', '\\n')
  334. f.write(to_bytes('#'))
  335. f.write(to_bytes(sig))
  336. f.write(to_bytes('\n'))
  337. os.remove('waf.asc')
  338. if sys.platform == 'win32' or Options.options.make_batch:
  339. with open('waf.bat', 'w') as f:
  340. f.write('@setlocal\n@set PYEXE=python\n@where %PYEXE% 1>NUL 2>NUL\n@if %ERRORLEVEL% neq 0 set PYEXE=py\n@%PYEXE% -x "%~dp0waf" %*\n@exit /b %ERRORLEVEL%\n')
  341. if sys.platform != 'win32':
  342. os.chmod('waf', Utils.O755)
  343. os.remove('%s.tar.%s' % (mw, zipType))
  344. def configure(conf):
  345. conf.load('python')
  346. def build(bld):
  347. waf = bld.path.make_node('waf') # do not use a build directory for this file
  348. bld(name='create_waf', rule=create_waf, target=waf, always=True, color='PINK')
  349. class Dist(Scripting.Dist):
  350. def get_excl(self):
  351. return super(self.__class__, self).get_excl() + ' **/waflib.zip'