subconfigure.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. # This Source Code Form is subject to the terms of the Mozilla Public
  2. # License, v. 2.0. If a copy of the MPL was not distributed with this
  3. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
  4. # This script is used to capture the content of config.status-generated
  5. # files and subsequently restore their timestamp if they haven't changed.
  6. import argparse
  7. import errno
  8. import itertools
  9. import os
  10. import re
  11. import subprocess
  12. import sys
  13. import pickle
  14. import mozpack.path as mozpath
  15. class Pool(object):
  16. def __new__(cls, size):
  17. try:
  18. import multiprocessing
  19. size = min(size, multiprocessing.cpu_count())
  20. return multiprocessing.Pool(size)
  21. except:
  22. return super(Pool, cls).__new__(cls)
  23. def imap_unordered(self, fn, iterable):
  24. return itertools.imap(fn, iterable)
  25. def close(self):
  26. pass
  27. def join(self):
  28. pass
  29. class File(object):
  30. def __init__(self, path):
  31. self._path = path
  32. self._content = open(path, 'rb').read()
  33. stat = os.stat(path)
  34. self._times = (stat.st_atime, stat.st_mtime)
  35. @property
  36. def path(self):
  37. return self._path
  38. @property
  39. def mtime(self):
  40. return self._times[1]
  41. @property
  42. def modified(self):
  43. '''Returns whether the file was modified since the instance was
  44. created. Result is memoized.'''
  45. if hasattr(self, '_modified'):
  46. return self._modified
  47. modified = True
  48. if os.path.exists(self._path):
  49. if open(self._path, 'rb').read() == self._content:
  50. modified = False
  51. self._modified = modified
  52. return modified
  53. def update_time(self):
  54. '''If the file hasn't changed since the instance was created,
  55. restore its old modification time.'''
  56. if not self.modified:
  57. os.utime(self._path, self._times)
  58. # As defined in the various sub-configures in the tree
  59. PRECIOUS_VARS = set([
  60. 'build_alias',
  61. 'host_alias',
  62. 'target_alias',
  63. 'CC',
  64. 'CFLAGS',
  65. 'LDFLAGS',
  66. 'LIBS',
  67. 'CPPFLAGS',
  68. 'CPP',
  69. 'CCC',
  70. 'CXXFLAGS',
  71. 'CXX',
  72. 'CCASFLAGS',
  73. 'CCAS',
  74. ])
  75. CONFIGURE_DATA = 'configure.pkl'
  76. # Autoconf, in some of the sub-configures used in the tree, likes to error
  77. # out when "precious" variables change in value. The solution it gives to
  78. # straighten things is to either run make distclean or remove config.cache.
  79. # There's no reason not to do the latter automatically instead of failing,
  80. # doing the cleanup (which, on buildbots means a full clobber), and
  81. # restarting from scratch.
  82. def maybe_clear_cache(data):
  83. env = dict(data['env'])
  84. for kind in ('target', 'host', 'build'):
  85. arg = data[kind]
  86. if arg is not None:
  87. env['%s_alias' % kind] = arg
  88. # configure can take variables assignments in its arguments, and that
  89. # overrides whatever is in the environment.
  90. for arg in data['args']:
  91. if arg[:1] != '-' and '=' in arg:
  92. key, value = arg.split('=', 1)
  93. env[key] = value
  94. comment = re.compile(r'^\s+#')
  95. cache = {}
  96. with open(data['cache-file']) as f:
  97. for line in f:
  98. if not comment.match(line) and '=' in line:
  99. key, value = line.rstrip(os.linesep).split('=', 1)
  100. # If the value is quoted, unquote it
  101. if value[:1] == "'":
  102. value = value[1:-1].replace("'\\''", "'")
  103. cache[key] = value
  104. for precious in PRECIOUS_VARS:
  105. # If there is no entry at all for that precious variable, then
  106. # its value is not precious for that particular configure.
  107. if 'ac_cv_env_%s_set' % precious not in cache:
  108. continue
  109. is_set = cache.get('ac_cv_env_%s_set' % precious) == 'set'
  110. value = cache.get('ac_cv_env_%s_value' % precious) if is_set else None
  111. if value != env.get(precious):
  112. print 'Removing %s because of %s value change from:' \
  113. % (data['cache-file'], precious)
  114. print ' %s' % (value if value is not None else 'undefined')
  115. print 'to:'
  116. print ' %s' % env.get(precious, 'undefined')
  117. os.remove(data['cache-file'])
  118. return True
  119. return False
  120. def split_template(s):
  121. """Given a "file:template" string, returns "file", "template". If the string
  122. is of the form "file" (without a template), returns "file", "file.in"."""
  123. if ':' in s:
  124. return s.split(':', 1)
  125. return s, '%s.in' % s
  126. def get_config_files(data):
  127. config_status = mozpath.join(data['objdir'], 'config.status')
  128. if not os.path.exists(config_status):
  129. return [], []
  130. configure = mozpath.join(data['srcdir'], 'configure')
  131. config_files = []
  132. command_files = []
  133. # Scan the config.status output for information about configuration files
  134. # it generates.
  135. config_status_output = subprocess.check_output(
  136. [data['shell'], '-c', '%s --help' % config_status],
  137. stderr=subprocess.STDOUT).splitlines()
  138. state = None
  139. for line in config_status_output:
  140. if line.startswith('Configuration') and line.endswith(':'):
  141. if line.endswith('commands:'):
  142. state = 'commands'
  143. else:
  144. state = 'config'
  145. elif not line.strip():
  146. state = None
  147. elif state:
  148. for f, t in (split_template(couple) for couple in line.split()):
  149. f = mozpath.join(data['objdir'], f)
  150. t = mozpath.join(data['srcdir'], t)
  151. if state == 'commands':
  152. command_files.append(f)
  153. else:
  154. config_files.append((f, t))
  155. return config_files, command_files
  156. def prepare(srcdir, objdir, shell, args):
  157. parser = argparse.ArgumentParser()
  158. parser.add_argument('--target', type=str)
  159. parser.add_argument('--host', type=str)
  160. parser.add_argument('--build', type=str)
  161. parser.add_argument('--cache-file', type=str)
  162. # The --srcdir argument is simply ignored. It's a useless autoconf feature
  163. # that we don't support well anyways. This makes it stripped from `others`
  164. # and allows to skip setting it when calling the subconfigure (configure
  165. # will take it from the configure path anyways).
  166. parser.add_argument('--srcdir', type=str)
  167. data_file = os.path.join(objdir, CONFIGURE_DATA)
  168. previous_args = None
  169. if os.path.exists(data_file):
  170. with open(data_file, 'rb') as f:
  171. data = pickle.load(f)
  172. previous_args = data['args']
  173. # Msys likes to break environment variables and command line arguments,
  174. # so read those from stdin, as they are passed from the configure script
  175. # when necessary (on windows).
  176. input = sys.stdin.read()
  177. if input:
  178. data = {a: b for [a, b] in eval(input)}
  179. environ = {a: b for a, b in data['env']}
  180. # These environment variables as passed from old-configure may contain
  181. # posix-style paths, which will not be meaningful to the js
  182. # subconfigure, which runs as a native python process, so use their
  183. # values from the environment. In the case of autoconf implemented
  184. # subconfigures, Msys will re-convert them properly.
  185. for var in ('HOME', 'TERM', 'PATH', 'TMPDIR', 'TMP',
  186. 'TEMP', 'INCLUDE'):
  187. if var in environ and var in os.environ:
  188. environ[var] = os.environ[var]
  189. args = data['args']
  190. else:
  191. environ = os.environ
  192. args, others = parser.parse_known_args(args)
  193. data = {
  194. 'target': args.target,
  195. 'host': args.host,
  196. 'build': args.build,
  197. 'args': others,
  198. 'shell': shell,
  199. 'srcdir': srcdir,
  200. 'env': environ,
  201. }
  202. if args.cache_file:
  203. data['cache-file'] = mozpath.normpath(mozpath.join(os.getcwd(),
  204. args.cache_file))
  205. else:
  206. data['cache-file'] = mozpath.join(objdir, 'config.cache')
  207. if previous_args is not None:
  208. data['previous-args'] = previous_args
  209. try:
  210. os.makedirs(objdir)
  211. except OSError as e:
  212. if e.errno != errno.EEXIST:
  213. raise
  214. with open(data_file, 'wb') as f:
  215. pickle.dump(data, f)
  216. def prefix_lines(text, prefix):
  217. return ''.join('%s> %s' % (prefix, line) for line in text.splitlines(True))
  218. def run(objdir):
  219. ret = 0
  220. output = ''
  221. with open(os.path.join(objdir, CONFIGURE_DATA), 'rb') as f:
  222. data = pickle.load(f)
  223. data['objdir'] = objdir
  224. cache_file = data['cache-file']
  225. cleared_cache = True
  226. if os.path.exists(cache_file):
  227. cleared_cache = maybe_clear_cache(data)
  228. config_files, command_files = get_config_files(data)
  229. contents = []
  230. for f, t in config_files:
  231. contents.append(File(f))
  232. # AC_CONFIG_COMMANDS actually only registers tags, not file names
  233. # but most commands are tagged with the file name they create.
  234. # However, a few don't, or are tagged with a directory name (and their
  235. # command is just to create that directory)
  236. for f in command_files:
  237. if os.path.isfile(f):
  238. contents.append(File(f))
  239. # Only run configure if one of the following is true:
  240. # - config.status doesn't exist
  241. # - config.status is older than configure
  242. # - the configure arguments changed
  243. # - the environment changed in a way that requires a cache clear.
  244. configure = mozpath.join(data['srcdir'], 'configure')
  245. config_status_path = mozpath.join(objdir, 'config.status')
  246. skip_configure = True
  247. if not os.path.exists(config_status_path):
  248. skip_configure = False
  249. config_status = None
  250. else:
  251. config_status = File(config_status_path)
  252. if config_status.mtime < os.path.getmtime(configure) or \
  253. data.get('previous-args', data['args']) != data['args'] or \
  254. cleared_cache:
  255. skip_configure = False
  256. relobjdir = os.path.relpath(objdir, os.getcwd())
  257. if not skip_configure:
  258. if mozpath.normsep(relobjdir) == 'js/src':
  259. # Because configure is a shell script calling a python script
  260. # calling a shell script, on Windows, with msys screwing the
  261. # environment, we lose the benefits from our own efforts in this
  262. # script to get past the msys problems. So manually call the python
  263. # script instead, so that we don't do a native->msys transition
  264. # here. Then the python configure will still have the right
  265. # environment when calling the shell configure.
  266. command = [
  267. sys.executable,
  268. os.path.join(os.path.dirname(__file__), '..', 'configure.py'),
  269. '--enable-project=js',
  270. ]
  271. data['env']['OLD_CONFIGURE'] = os.path.join(
  272. os.path.dirname(configure), 'old-configure')
  273. else:
  274. command = [data['shell'], configure]
  275. for kind in ('target', 'build', 'host'):
  276. if data.get(kind) is not None:
  277. command += ['--%s=%s' % (kind, data[kind])]
  278. command += data['args']
  279. command += ['--cache-file=%s' % cache_file]
  280. # Pass --no-create to configure so that it doesn't run config.status.
  281. # We're going to run it ourselves.
  282. command += ['--no-create']
  283. print prefix_lines('configuring', relobjdir)
  284. print prefix_lines('running %s' % ' '.join(command[:-1]), relobjdir)
  285. sys.stdout.flush()
  286. try:
  287. output += subprocess.check_output(command,
  288. stderr=subprocess.STDOUT, cwd=objdir, env=data['env'])
  289. except subprocess.CalledProcessError as e:
  290. return relobjdir, e.returncode, e.output
  291. # Leave config.status with a new timestamp if configure is newer than
  292. # its original mtime.
  293. if config_status and os.path.getmtime(configure) <= config_status.mtime:
  294. config_status.update_time()
  295. # Only run config.status if one of the following is true:
  296. # - config.status changed or did not exist
  297. # - one of the templates for config files is newer than the corresponding
  298. # config file.
  299. skip_config_status = True
  300. if not config_status or config_status.modified:
  301. # If config.status doesn't exist after configure (because it's not
  302. # an autoconf configure), skip it.
  303. if os.path.exists(config_status_path):
  304. skip_config_status = False
  305. else:
  306. # config.status changed or was created, so we need to update the
  307. # list of config and command files.
  308. config_files, command_files = get_config_files(data)
  309. for f, t in config_files:
  310. if not os.path.exists(t) or \
  311. os.path.getmtime(f) < os.path.getmtime(t):
  312. skip_config_status = False
  313. if not skip_config_status:
  314. if skip_configure:
  315. print prefix_lines('running config.status', relobjdir)
  316. sys.stdout.flush()
  317. try:
  318. output += subprocess.check_output([data['shell'], '-c',
  319. './config.status'], stderr=subprocess.STDOUT, cwd=objdir,
  320. env=data['env'])
  321. except subprocess.CalledProcessError as e:
  322. ret = e.returncode
  323. output += e.output
  324. for f in contents:
  325. f.update_time()
  326. return relobjdir, ret, output
  327. def subconfigure(args):
  328. parser = argparse.ArgumentParser()
  329. parser.add_argument('--list', type=str,
  330. help='File containing a list of subconfigures to run')
  331. parser.add_argument('--skip', type=str,
  332. help='File containing a list of Subconfigures to skip')
  333. parser.add_argument('subconfigures', type=str, nargs='*',
  334. help='Subconfigures to run if no list file is given')
  335. args, others = parser.parse_known_args(args)
  336. subconfigures = args.subconfigures
  337. if args.list:
  338. subconfigures.extend(open(args.list, 'rb').read().splitlines())
  339. if args.skip:
  340. skips = set(open(args.skip, 'rb').read().splitlines())
  341. subconfigures = [s for s in subconfigures if s not in skips]
  342. if not subconfigures:
  343. return 0
  344. ret = 0
  345. # One would think using a ThreadPool would be faster, considering
  346. # everything happens in subprocesses anyways, but no, it's actually
  347. # slower on Windows. (20s difference overall!)
  348. pool = Pool(len(subconfigures))
  349. for relobjdir, returncode, output in \
  350. pool.imap_unordered(run, subconfigures):
  351. print prefix_lines(output, relobjdir)
  352. sys.stdout.flush()
  353. ret = max(returncode, ret)
  354. if ret:
  355. break
  356. pool.close()
  357. pool.join()
  358. return ret
  359. def main(args):
  360. if args[0] != '--prepare':
  361. return subconfigure(args)
  362. topsrcdir = os.path.abspath(args[1])
  363. subdir = args[2]
  364. # subdir can be of the form srcdir:objdir
  365. if ':' in subdir:
  366. srcdir, subdir = subdir.split(':', 1)
  367. else:
  368. srcdir = subdir
  369. srcdir = os.path.join(topsrcdir, srcdir)
  370. objdir = os.path.abspath(subdir)
  371. return prepare(srcdir, objdir, args[3], args[4:])
  372. if __name__ == '__main__':
  373. sys.exit(main(sys.argv[1:]))