develop.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. #!/usr/bin/env python
  2. #
  3. # @file develop.py
  4. # @authors Bryan O'Sullivan, Mark Palange, Aaron Brashears
  5. # @brief Fire and forget script to appropriately configure cmake for SL.
  6. #
  7. # $LicenseInfo:firstyear=2007&license=viewergpl$
  8. #
  9. # Copyright (c) 2007-2009, Linden Research, Inc.
  10. #
  11. # Second Life Viewer Source Code
  12. # The source code in this file ("Source Code") is provided by Linden Lab
  13. # to you under the terms of the GNU General Public License, version 2.0
  14. # ("GPL"), unless you have obtained a separate licensing agreement
  15. # ("Other License"), formally executed by you and Linden Lab. Terms of
  16. # the GPL can be found in doc/GPL-license.txt in this distribution, or
  17. # online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
  18. #
  19. # There are special exceptions to the terms and conditions of the GPL as
  20. # it is applied to this Source Code. View the full text of the exception
  21. # in the file doc/FLOSS-exception.txt in this software distribution, or
  22. # online at
  23. # http://secondlifegrid.net/programs/open_source/licensing/flossexception
  24. #
  25. # By copying, modifying or distributing this software, you acknowledge
  26. # that you have read and understood your obligations described above,
  27. # and agree to abide by those obligations.
  28. #
  29. # ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
  30. # WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
  31. # COMPLETENESS OR PERFORMANCE.
  32. # $/LicenseInfo$
  33. from __future__ import print_function
  34. import errno
  35. import getopt
  36. import os
  37. import random
  38. import re
  39. import shutil
  40. import socket
  41. import sys
  42. if sys.version_info[0] == 3:
  43. import subprocess as cmds
  44. else:
  45. import commands as cmds
  46. # Force English on cmake and compiler messages (easier to search on the Web for
  47. # the error messages that could arise).
  48. os.environ['LANG'] = 'C'
  49. if os.path.isdir('indra'):
  50. os.chdir('indra')
  51. elif os.path.isdir(os.path.join('..', 'indra')):
  52. os.chdir(os.path.join('..', 'indra'))
  53. elif not os.path.isdir('newview'):
  54. print('Error: cannot find the "indra" sub-directory', file=sys.stderr)
  55. sys.exit(1)
  56. class CommandError(Exception):
  57. pass
  58. def mkdir(path):
  59. try:
  60. os.mkdir(path)
  61. return path
  62. except OSError as err:
  63. if err.errno != errno.EEXIST or not os.path.isdir(path):
  64. raise
  65. def getcwd():
  66. cwd = os.getcwd()
  67. if 'a' <= cwd[0] <= 'z' and cwd[1] == ':':
  68. # CMake wants DOS drive letters to be in uppercase. The above
  69. # condition never asserts on platforms whose full path names
  70. # always begin with a slash, so we do not need to test whether
  71. # we are running on Windows.
  72. cwd = cwd[0].upper() + cwd[1:]
  73. return cwd
  74. def quote(opts):
  75. return '"' + '" "'.join([ opt.replace('"', '') for opt in opts ]) + '"'
  76. class PlatformSetup(object):
  77. generator = None
  78. build_types = {}
  79. for t in ('Debug', 'Release', 'RelWithDebInfo'):
  80. build_types[t.lower()] = t
  81. build_type = build_types['release']
  82. systemlibs = 'OFF'
  83. project_name = 'CoolVLViewer'
  84. distcc = True
  85. cmake_opts = []
  86. def __init__(self):
  87. self.script_dir = os.path.realpath(
  88. os.path.dirname(__import__(__name__).__file__))
  89. def os(self):
  90. '''Return the name of the OS.'''
  91. raise NotImplemented('os')
  92. def arch(self):
  93. '''Return the CPU architecture.'''
  94. return None
  95. def platform(self):
  96. '''Return a stringified two-tuple of the OS name and CPU
  97. architecture.'''
  98. ret = self.os()
  99. if self.arch():
  100. ret += '-' + self.arch()
  101. return ret
  102. def build_dirs(self):
  103. '''Return the top-level directories in which builds occur.'''
  104. return [os.path.join('..', 'build-' + self.platform())]
  105. def cmake_commandline(self, src_dir, build_dir, opts):
  106. '''Return the command line to run cmake with.'''
  107. args = dict(
  108. dir=src_dir,
  109. generator=self.generator,
  110. opts=quote(opts) if opts else '',
  111. systemlibs=self.systemlibs,
  112. type=self.build_type.upper(),
  113. )
  114. return ('cmake -DCMAKE_BUILD_TYPE:STRING=%(type)s '
  115. '-DUSESYSTEMLIBS:BOOL=%(systemlibs)s '
  116. '-G %(generator)r %(opts)s %(dir)r' % args)
  117. def run_cmake(self, args=[]):
  118. '''Run cmake.'''
  119. # do a sanity check to make sure we have a generator
  120. if not hasattr(self, 'generator'):
  121. raise "No generator available for '%s'" % (self.__name__,)
  122. cwd = getcwd()
  123. created = []
  124. try:
  125. for d in self.build_dirs():
  126. if mkdir(d):
  127. created.append(d)
  128. try:
  129. os.chdir(d)
  130. cmd = self.cmake_commandline(cwd, d, args)
  131. print('Running %r in %r' % (cmd, d))
  132. self.run(cmd, 'cmake')
  133. finally:
  134. os.chdir(cwd)
  135. except:
  136. # If we created a directory in which to run cmake and
  137. # something went wrong, the directory probably just
  138. # contains garbage, so delete it.
  139. os.chdir(cwd)
  140. for d in created:
  141. print('Cleaning %r' % d)
  142. shutil.rmtree(d)
  143. raise
  144. def parse_build_opts(self, arguments):
  145. opts, targets = getopt.getopt(arguments, 'o:', ['option='])
  146. build_opts = []
  147. for o, a in opts:
  148. if o in ('-o', '--option'):
  149. build_opts.append(a)
  150. return build_opts, targets
  151. def run_build(self, opts, targets):
  152. '''Build the default targets for this platform.'''
  153. raise NotImplemented('run_build')
  154. def cleanup(self):
  155. '''Delete all build directories.'''
  156. cleaned = 0
  157. for d in self.build_dirs():
  158. if os.path.isdir(d):
  159. print('Cleaning %r' % d)
  160. shutil.rmtree(d)
  161. cleaned += 1
  162. if not cleaned:
  163. print('Nothing to clean up!')
  164. def find_in_path(self, name, defval=None, basename=False):
  165. for ext in self.exe_suffixes:
  166. name_ext = name + ext
  167. if os.sep in name_ext:
  168. path = os.path.abspath(name_ext)
  169. if os.access(path, os.X_OK):
  170. return [basename and os.path.basename(path) or path]
  171. for p in os.getenv('PATH', self.search_path).split(os.pathsep):
  172. path = os.path.join(p, name_ext)
  173. if os.access(path, os.X_OK):
  174. return [basename and os.path.basename(path) or path]
  175. if defval:
  176. return [defval]
  177. return []
  178. class UnixSetup(PlatformSetup):
  179. '''Generic Unixy build instructions.'''
  180. search_path = '/usr/bin:/usr/local/bin'
  181. exe_suffixes = ('',)
  182. def __init__(self):
  183. super(UnixSetup, self).__init__()
  184. self.generator = 'Unix Makefiles'
  185. def os(self):
  186. return 'unix'
  187. def arch(self):
  188. cpu = os.uname()[-1]
  189. return cpu
  190. def run(self, command, name=None):
  191. '''Run a program. If the program fails, raise an exception.'''
  192. ret = os.system(command)
  193. if ret:
  194. if name is None:
  195. name = command.split(None, 1)[0]
  196. if os.WIFEXITED(ret):
  197. st = os.WEXITSTATUS(ret)
  198. if st == 127:
  199. event = 'was not found'
  200. else:
  201. event = 'exited with status %d' % st
  202. elif os.WIFSIGNALED(ret):
  203. event = 'was killed by signal %d' % os.WTERMSIG(ret)
  204. else:
  205. event = 'died unexpectedly (!?) with 16-bit status %d' % ret
  206. raise CommandError('the command %r %s' %
  207. (name, event))
  208. class LinuxSetup(UnixSetup):
  209. def __init__(self):
  210. super(LinuxSetup, self).__init__()
  211. def os(self):
  212. return 'linux'
  213. def build_dirs(self):
  214. platform_build = '%s-%s' % (self.platform(), self.build_type.lower())
  215. return [os.path.join('..', 'viewer-' + platform_build)]
  216. def cmake_commandline(self, src_dir, build_dir, opts):
  217. args = dict(
  218. dir=src_dir,
  219. generator=self.generator,
  220. opts=quote(opts) if opts else '',
  221. systemlibs=self.systemlibs,
  222. type=self.build_type.upper(),
  223. project_name=self.project_name,
  224. cxx="g++"
  225. )
  226. cmd = (('cmake -DCMAKE_BUILD_TYPE:STRING=%(type)s '
  227. '-G %(generator)r '
  228. '-DUSESYSTEMLIBS:BOOL=%(systemlibs)s '
  229. '-DROOT_PROJECT_NAME:STRING=%(project_name)s '
  230. '%(opts)s %(dir)r')
  231. % args)
  232. if 'CXX' not in os.environ:
  233. args.update({'cmd':cmd})
  234. cmd = ('CXX=%(cxx)r %(cmd)s' % args)
  235. return cmd
  236. def run_build(self, opts, targets):
  237. job_count = None
  238. for i in range(len(opts)):
  239. if opts[i].startswith('-j'):
  240. try:
  241. job_count = int(opts[i][2:])
  242. except ValueError:
  243. try:
  244. job_count = int(opts[i+1])
  245. except ValueError:
  246. job_count = True
  247. def get_cpu_count():
  248. count = 0
  249. for line in open('/proc/cpuinfo'):
  250. if re.match(r'processor\s*:', line):
  251. count += 1
  252. return count
  253. def localhost():
  254. count = get_cpu_count()
  255. return 'localhost/' + str(count), count
  256. def get_distcc_hosts():
  257. try:
  258. hosts = []
  259. name = os.getenv('DISTCC_DIR', '/etc/distcc') + '/hosts'
  260. for l in open(name):
  261. l = l[l.find('#')+1:].strip()
  262. if l: hosts.append(l)
  263. return hosts
  264. except IOError:
  265. return (os.getenv('DISTCC_HOSTS', '').split() or
  266. [localhost()[0]])
  267. def count_distcc_hosts():
  268. cpus = 0
  269. hosts = 0
  270. for host in get_distcc_hosts():
  271. m = re.match(r'.*/(\d+)', host)
  272. hosts += 1
  273. cpus += m and int(m.group(1)) or 1
  274. return hosts, cpus
  275. if job_count is None:
  276. hosts, job_count = count_distcc_hosts()
  277. '''Saturate the cores (including during I/O mutex waits by'''
  278. ''' increasing the jobs count at 50% above the cores count.'''
  279. job_count = int((job_count * 3) / 2)
  280. opts.extend(['-j', str(job_count)])
  281. if targets:
  282. targets = ' '.join(targets)
  283. else:
  284. targets = 'all'
  285. for d in self.build_dirs():
  286. if self.generator == 'Ninja':
  287. cmd = 'ninja -C %r %s %s' % (d, ' '.join(opts), targets)
  288. else:
  289. cmd = 'make -C %r %s %s' % (d, ' '.join(opts), targets)
  290. print('Running %r' % cmd)
  291. self.run(cmd)
  292. class DarwinSetup(UnixSetup):
  293. def __init__(self):
  294. super(DarwinSetup, self).__init__()
  295. self.generator = 'Xcode'
  296. def os(self):
  297. return 'darwin'
  298. def arch(self):
  299. return UnixSetup.arch(self)
  300. def cmake_commandline(self, src_dir, build_dir, opts):
  301. args = dict(
  302. dir=src_dir,
  303. generator=self.generator,
  304. opts=quote(opts) if opts else '',
  305. systemlibs=self.systemlibs,
  306. project_name=self.project_name,
  307. type=self.build_type.upper(),
  308. )
  309. return ('cmake -G %(generator)r '
  310. '-DCMAKE_BUILD_TYPE:STRING=%(type)s '
  311. '-DUSESYSTEMLIBS:BOOL=%(systemlibs)s '
  312. '-DROOT_PROJECT_NAME:STRING=%(project_name)s '
  313. '%(opts)s %(dir)r' % args)
  314. class WindowsSetup(PlatformSetup):
  315. gens = {
  316. 'vs2022' : {
  317. 'gen' : r'Visual Studio 17 2022',
  318. 'toolset' : r'v143'
  319. },
  320. 'vs2022-clang' : {
  321. 'gen' : r'Visual Studio 17 2022',
  322. 'toolset' : r'ClangCL'
  323. }
  324. }
  325. search_path = r'C:\windows'
  326. exe_suffixes = ('.exe', '.bat', '.com')
  327. def __init__(self):
  328. super(WindowsSetup, self).__init__()
  329. self._generator = None
  330. def _get_generator(self):
  331. return self._generator
  332. def _set_generator(self, gen):
  333. self._generator = gen
  334. generator = property(_get_generator, _set_generator)
  335. def os(self):
  336. osname = 'win64'
  337. return osname
  338. def arch(self):
  339. cpu ='x86_64'
  340. return cpu
  341. def build_dirs(self):
  342. return [os.path.join('..', 'build-' + self.generator)]
  343. def cmake_commandline(self, src_dir, build_dir, opts):
  344. genstring = self.gens[self.generator.lower()]['gen']
  345. toolsetstr = self.gens[self.generator.lower()]['toolset']
  346. args = dict(
  347. dir=src_dir,
  348. generator=genstring,
  349. toolset=toolsetstr,
  350. opts=quote(opts) if opts else '',
  351. systemlibs=self.systemlibs,
  352. project_name=self.project_name,
  353. )
  354. return ('cmake -G "%(generator)s" -T %(toolset)s '
  355. '-DUSESYSTEMLIBS:BOOL=%(systemlibs)s '
  356. '-DROOT_PROJECT_NAME:STRING=%(project_name)s '
  357. '%(opts)s "%(dir)s"' % args)
  358. def get_HKLM_registry_value(self, key_str, value_str):
  359. if sys.version_info[0] == 3:
  360. import winreg as wreg
  361. else:
  362. import _winreg as wreg
  363. reg = wreg.ConnectRegistry(None, wreg.HKEY_LOCAL_MACHINE)
  364. key = wreg.OpenKey(reg, key_str)
  365. value = wreg.QueryValueEx(key, value_str)[0]
  366. print('Found: %s' % value)
  367. return value
  368. def run(self, command, name=None):
  369. '''Run a program. If the program fails, raise an exception.'''
  370. ret = os.system(command)
  371. if ret:
  372. if name is None:
  373. name = command.split(None, 1)[0]
  374. path = self.find_in_path(name)
  375. if not path:
  376. ret = 'was not found'
  377. else:
  378. ret = 'exited with status %d' % ret
  379. raise CommandError('the command %r %s' %
  380. (name, ret))
  381. def run_cmake(self, args=[]):
  382. PlatformSetup.run_cmake(self, args)
  383. class CygwinSetup(WindowsSetup):
  384. def __init__(self):
  385. super(CygwinSetup, self).__init__()
  386. self.generator = 'vc143'
  387. def cmake_commandline(self, src_dir, build_dir, opts):
  388. dos_dir = cmds.getoutput("cygpath -w %s" % src_dir)
  389. args = dict(
  390. dir=dos_dir,
  391. generator=self.gens[self.generator.lower()]['gen'],
  392. opts=quote(opts) if opts else '',
  393. systemlibs=self.systemlibs,
  394. project_name=self.project_name,
  395. )
  396. return ('cmake -G "%(generator)s" '
  397. '-DUSESYSTEMLIBS:BOOL=%(systemlibs)s '
  398. '-DROOT_PROJECT_NAME:STRING=%(project_name)s '
  399. '%(opts)s "%(dir)s"' % args)
  400. setup_platform = {
  401. 'darwin' : DarwinSetup,
  402. 'linux' : LinuxSetup,
  403. 'linux2' : LinuxSetup,
  404. 'win32' : WindowsSetup,
  405. 'cygwin' : CygwinSetup
  406. }
  407. usage_msg = '''
  408. Usage: develop.py [options] [command [command-options]]
  409. Options:
  410. -h | --help print this help message
  411. --systemlibs build against available system libs instead of prebuilt libs
  412. -t | --type=NAME build type ("Release", "RelWithDebInfo" or "Debug")
  413. -N | --no-distcc disable use of distcc
  414. -G | --generator=NAME generator name
  415. Windows: "vs2022" or "vs2022-clang"
  416. Mac OS X: "Xcode" (default) or "Unix Makefiles"
  417. Linux: "Unix Makefiles" (default) or "Ninja"
  418. -p | --project=NAME overrides the root project name (does not affect makefiles)
  419. Commands:
  420. configure configure project by running cmake (default if none given)
  421. build configure and build default target (for Linux only !)
  422. clean delete all build directories, does not affect sources
  423. printbuilddirs print the build directory that will be used
  424. Command-options for "configure":
  425. We use cmake variables to change the build configuration. E.g., to set up
  426. the project for the "Release" target and ignore fatal warnings:
  427. develop.py -t Release configure -DNO_FATAL_WARNINGS:BOOL=TRUE
  428. '''
  429. def main(arguments):
  430. setup = setup_platform[sys.platform]()
  431. try:
  432. opts, args = getopt.getopt(
  433. arguments,
  434. '?hNt:p:G:',
  435. ['help', 'systemlibs', 'no-distcc', 'type=', 'generator=', 'project='])
  436. except getopt.GetoptError as err:
  437. print('Error:', err, file=sys.stderr)
  438. print("""
  439. Note: You must pass -D options to cmake after the "configure" command
  440. For example: develop.py configure -DCMAKE_BUILD_TYPE=Release""", file=sys.stderr)
  441. print(usage_msg.strip(), file=sys.stderr)
  442. sys.exit(1)
  443. for o, a in opts:
  444. if o in ('-?', '-h', '--help'):
  445. print(usage_msg.strip())
  446. sys.exit(0)
  447. elif o in ('--systemlibs',):
  448. setup.systemlibs = 'ON'
  449. elif o in ('-t', '--type'):
  450. try:
  451. setup.build_type = setup.build_types[a.lower()]
  452. except KeyError:
  453. print('Error: unknown build type', repr(a), file=sys.stderr)
  454. print('Supported build types:', file=sys.stderr)
  455. types = list(setup.build_types.values())
  456. types.sort()
  457. for t in types:
  458. print(' ', t)
  459. sys.exit(1)
  460. elif o in ('-G', '--generator'):
  461. setup.generator = a
  462. elif o in ('-N', '--no-distcc'):
  463. setup.distcc = False
  464. elif o in ('-p', '--project'):
  465. setup.project_name = a
  466. else:
  467. print('INTERNAL ERROR: unhandled option', repr(o), file=sys.stderr)
  468. sys.exit(1)
  469. if not args:
  470. setup.run_cmake()
  471. return
  472. try:
  473. cmd = args.pop(0)
  474. if cmd in ('cmake', 'configure'):
  475. setup.run_cmake(args)
  476. elif cmd == 'build':
  477. if os.getenv('DISTCC_DIR') is None:
  478. distcc_dir = os.path.join(getcwd(), '.distcc')
  479. if not os.path.exists(distcc_dir):
  480. os.mkdir(distcc_dir)
  481. print("setting DISTCC_DIR to %s" % distcc_dir)
  482. os.environ['DISTCC_DIR'] = distcc_dir
  483. else:
  484. print("DISTCC_DIR is set to %s" % os.getenv('DISTCC_DIR'))
  485. for d in setup.build_dirs():
  486. if not os.path.exists(d):
  487. raise CommandError('run "develop.py cmake" first')
  488. setup.run_cmake()
  489. opts, targets = setup.parse_build_opts(args)
  490. setup.run_build(opts, targets)
  491. elif cmd == 'clean':
  492. if args:
  493. raise CommandError('clean takes no arguments')
  494. setup.cleanup()
  495. elif cmd == 'printbuilddirs':
  496. for d in setup.build_dirs():
  497. print(d, file=sys.stdout)
  498. else:
  499. print('Error: unknown subcommand', repr(cmd), file=sys.stderr)
  500. print("(run 'develop.py --help' for help)", file=sys.stderr)
  501. sys.exit(1)
  502. except getopt.GetoptError as err:
  503. print('Error with %r subcommand: %s' % (cmd, err), file=sys.stderr)
  504. sys.exit(1)
  505. if __name__ == '__main__':
  506. try:
  507. main(sys.argv[1:])
  508. except CommandError as err:
  509. print('Error:', err, file=sys.stderr)
  510. sys.exit(1)