bootstrap.py 13.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
###########################################################################
# This file is part of LImA, a Library for Image Acquisition
#
#  Copyright (C) : 2009-2017
#  European Synchrotron Radiation Facility
#  BP 220, Grenoble 38043
#  FRANCE
# 
#  Contact: lima@esrf.fr
# 
#  This is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 3 of the License, or
#  (at your option) any later version.
# 
#  This software is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
# 
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, see <http://www.gnu.org/licenses/>.
############################################################################
import sys, os
25
import platform, multiprocessing
26 27
from subprocess import Popen, PIPE
import contextlib
28 29 30
import argparse

prog_description = 'Lima build and install tool'
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
prog_instructions = '''
Description:
    This script will build and eventually install Lima project.

    The build and install process default configuration is determined by
    the scripts/config.txt file, which is created the very first time from
    the scripts/config.txt_default template. The file contains the list of
    variables that are passed to CMake. By default, only Lima-related
    variables are included, but any other can be added. If you plan to
    execute the process several times with the same parameters, you can
    edit this configuration file and fix your options there. By default
    the simulator camera plugin is compiled. You can change that by
    setting the corresponding "LIMACAMERA_SIMULATOR=0" option in
    config.txt.

    Running ./install.sh with no parameter will just build Lima with the
    options in config.txt. No installation will be performed. If at least
    one of --install-prefix or --install-python-prefix option is specified
    the --install=yes option is assumed (unless --install=no is explicitly
    specified)

    If not absolute paths, the --config-file option will be assumed to be
    relative to the source-prefix, and --build-prefix relative to the CWD.

Module/option description:
    It can be any camera name or saving format.
    Available saving formats: edf, cbf, tiff, lz4, gz, hdf5, fits.
    Other otions are:
     + python: Build Python wrapping.
     + pytango-server: install the PyTango server Python code
     + tests: build tests (in order to run them execute "ctest" in <build>)
     + config, sps-image, gldisplay: for the fun!

Examples:
    ./install.[bat | sh] --install=yes basler python cbf
        -> compile and install Lima with cameras simulator and basler with
           Python wrapping and cbf saving format.
        -> install directory for C library and Python library will be in
           default directory.

        This is equivalent to adding the following options in config.txt:
           + LIMACAMERA_BASLER=1
           + LIMA_ENABLE_CBF=1
           + LIMA_ENABLE_PYTHON=1

    ./install.[bat | sh] --install-prefix=${HOME} tests
        -> compile and install Lima with camera simulator, also compiling
           simulator tests.
        -> the install directory is set in the home directory (${HOME})

        This is equivalent to adding the following options in config.txt:
           + LIMA_ENABLE_TESTS=1
           + CMAKE_INSTALL_PREFIX=<path_to_home>

    ONLY ON LINUX:
    ./install.sh --git [options]
        -> clone and update (checkout) on every (sub)module in [options]
'''

90 91 92 93 94

OS_TYPE = platform.system()
if OS_TYPE not in ['Linux', 'Windows']:
	sys.exit('Platform not supported: ' + OS_TYPE)

95 96 97 98 99
def exec_cmd(cmd, exc_msg=''):
	print('Executing:' + cmd)
	sys.stdout.flush()
	ret = os.system(cmd)
	if ret != 0:
100
		raise Exception('%s [%s]' % (exc_msg, cmd))
101

102

103 104 105 106 107 108 109
@contextlib.contextmanager
def ch_dir(new_dir):
	cur_dir = os.getcwd()
	os.chdir(new_dir)
	yield
	os.chdir(cur_dir)

110

111
class Config:
112

113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
	bool_map = {'yes': True, 'no': False}

	@classmethod
	def get_bool_opt_default(klass, val):
		for o, v in klass.bool_map.items():
			if val == v:
				return '__%s__' % o
		raise ValueError('Invalid value: ' + val)

	# return (val, explicit), where explicit is True if val was
	# specified as argument, or False if val is the default option value
	@classmethod
	def get_bool_opt(klass, val):
		val = val.lower()
		for o, v in klass.bool_map.items():
			if val == o:
				return v, True
			if val == '__%s__' % o:
				return v, False
		raise ValueError('Invalid value: ' + val)
		
134
	def __init__(self, argv=None):
135
		self.cmd_opts = None
136 137 138 139 140 141 142 143
		self.config_opts = None
		self.cmake_opts = None
		self.git = None

		if argv is not None:
			self.decode_args(argv)

	def decode_args(self, argv):
144 145
		build_type = ('RelWithDebInfo' if OS_TYPE == 'Linux' 
			      else 'Release')
146
		cwd = os.getcwd()
147 148 149 150 151 152
		src = os.path.realpath(os.path.join(os.path.dirname(argv[0]), 
						    os.path.pardir))
		formatter = argparse.RawDescriptionHelpFormatter
		parser = argparse.ArgumentParser(formatter_class=formatter,
						 description=prog_description,
						 epilog=prog_instructions)
153 154 155 156
		parser.add_argument('--git', action='store_true',
				    help='init/update Git submodules')
		parser.add_argument('--find-root-path',
				    help='CMake find_package/library root path')
157
		parser.add_argument('--source-prefix', default=src,
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
				    help='path to the Lima sources')
		parser.add_argument('--config-file', 
				    default='scripts/config.txt',
				    help='file with configuration options')
		parser.add_argument('--build-prefix', default='build',
				    help='directory where binaries are built')
		parser.add_argument('--build-type', default=build_type,
				    help='CMake build target')
		parser.add_argument('--install', 
				    default=self.get_bool_opt_default(False),
				    help='perform installation [yes, no]')
		parser.add_argument('--install-prefix',
				    help='directory where Lima is installed')
		parser.add_argument('--install-python-prefix',
				    help='install directory for Python code')
		parser.add_argument('mod_opts', metavar='mod_opt', nargs='+',
				    help='module/option to process')
175
		self.cmd_opts = parser.parse_args(argv[1:])
176 177 178 179 180 181 182 183 184

		# do install if not explicitly specified and user
		# included install-[python-]prefix
		install, explicit = self.get_bool_opt(self.get('install'))
		install_prefix = (self.get('install-prefix') or 
				  self.get('install-python-prefix'))
		install = True if not explicit and install_prefix else install
		self.set_cmd('install', install)

185 186 187 188 189 190 191 192 193
		# if option paths are relative, make them absolute:
		# config-file is rel. to src, build-prefix is rel. to cwd
		src = self.get('source-prefix')
		rel_opt_map = [(src, ['config-file']), (cwd, ['build-prefix'])]
		for base, opt_list in rel_opt_map:
			for opt in opt_list:
				p = self.get(opt)
				if p and not os.path.isabs(p):
					self.set_cmd(opt, os.path.join(base, p))
194

195
	def set_cmd(self, x, v):
196
		setattr(self.cmd_opts, self.to_underscore(x), v)
197

198 199
	def get(self, x):
		return getattr(self.cmd_opts, self.to_underscore(x))
200 201

	def get_git_options(self):
202
		return self.get('mod-opts')
203

204 205 206 207
	def get_cmd_options(self):
		opts = dict([(self.from_underscore(k), v)
			     for k, v in self.cmd_opts._get_kwargs()])
		for arg in opts.pop('mod-opts'):
208 209 210 211 212 213
			for oprefix, sdir in [("limacamera", "camera"), 
					      ("lima-enable", "third-party")]:
				sdir += '/'
				if arg.startswith(sdir):
					arg = oprefix + '-' + arg[len(sdir):]
			opts[arg] = True
214 215 216 217 218 219 220 221 222 223 224 225
		return opts

	def read_config(self):
		config_file = self.get('config-file')
		self.config_opts = []
		with open(config_file) as f:
			for line in f:
				line = line.strip()
				if not line or line.startswith('#'):
					continue
				opt, val = line.split('=')
				opt = self.from_underscore(opt).lower()
226
				val = int(val) if val.isdigit() else val
227 228
				self.config_opts.append((opt, val))

229
	def get_config_options(self):
230 231
		if self.config_opts is None:
			self.read_config()
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
		return self.config_opts

	def is_install_required(self):
		cmd_opts = self.get_cmd_options()
		install_prefix = cmd_opts.get('install-prefix', '')
		return cmd_opts.get('install', install_prefix != '')

	@staticmethod
	def to_underscore(x):
		return x.replace('-', '_')

	@staticmethod
	def from_underscore(x):
		return x.replace('_', '-')

class CMakeOptions:

	cmd_2_cmake_map = [
		('build-type', 'cmake-build-type'),
		('install-prefix', 'cmake-install-prefix'),
		('install-python-prefix', 'python-site-packages-dir'),
		('find-root-path', 'cmake-find-root-path')
	]

	def __init__(self, cfg):
		self.cfg = cfg


	# return options in config file activated (=1) if passed as arguments,
	# and also those not specified as empty (=) or disabled (=0|no) in file
	def get_configure_options(self):
		cmd_opts = self.cfg.get_cmd_options()
		config_opts = self.cfg.get_config_options()
265 266

		def is_active(v):
267 268 269
			if type(val) in [bool, int]:
				return val
			return val and (val.lower() not in [str(0), 'no'])
270 271

		cmake_opts = []
272
		for opt, val in config_opts:
273 274 275
			for cmd_opt, cmd_val in cmd_opts.items():
				if cmd_opt == opt:
					val = cmd_val
276
					break
277 278 279 280 281
				# arg-passed option must match the end
				# of opt a nd must be preceeded by 
				# the '_' separator
				t = opt.split(cmd_opt)
				if ((len(t) == 2) and 
282
				    (t[0][-1] == '-') and not t[1]):
283
					val = cmd_val
284
					break
285 286 287 288
			if is_active(val):
				cmake_opts.append((opt, val))

		for cmd_key, cmake_key in self.cmd_2_cmake_map:
289
			val = self.cfg.get(cmd_key)
290 291 292 293
			if is_active(val) and cmake_key not in dict(cmake_opts):
				cmake_opts.append((cmake_key, val))

		if OS_TYPE == 'Linux':
294
			cmake_gen = 'Unix Makefiles'
295 296 297 298 299 300 301 302 303 304 305
		elif OS_TYPE == 'Windows':
			# for windows check compat between installed python 
			# and mandatory vc++ compiler
			# See, https://wiki.python.org/moin/WindowsCompilers
			if sys.version_info < (2, 6):
				sys.exit("Only python > 2.6 supported")
			elif sys.version_info <= (3, 2):
				win_compiler = "Visual Studio 9 2008"
			elif sys.version_info <= (3, 4):
				win_compiler = "Visual Studio 10 2010" 
			else:
306
				win_compiler = "Visual Studio 15 2017"
307 308 309 310 311 312
			# now check architecture
			if platform.architecture()[0] == '64bit':
				win_compiler += ' Win64' 

			print ('Found Python ', sys.version)
			print ('Used compiler: ', win_compiler)
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
			cmake_gen = win_compiler

		source_prefix = self.cfg.get('source-prefix')
		opts = [source_prefix, '-G"%s"' % cmake_gen]
		opts += map(self.cmd_option, cmake_opts)
		return self.get_cmd_line_from_options(opts)

	def get_build_options(self):
		opts = ['--build .']
		if OS_TYPE == 'Linux':
			nb_jobs = multiprocessing.cpu_count() + 1
			opts += ['--', '-j %d' % nb_jobs]
		if OS_TYPE == 'Windows':
			opts += ['--config %s' %  self.cfg.get('build-type')]
		return self.get_cmd_line_from_options(opts)
328

329
	def get_install_options(self):
330 331 332
		opts = ['--build .', '--target install']
		if OS_TYPE == 'Windows':
			opts += ['--config %s' %  self.cfg.get('build-type')]
333
		return self.get_cmd_line_from_options(opts)
334 335

	@staticmethod
336 337 338 339 340
	def get_cmd_line_from_options(opts):
		return ' '.join(['cmake'] + opts)

	@staticmethod
	def cmd_option(opt_val):
341 342
		o, v = opt_val
		def quoted(x):
343 344 345 346
			if type(x) is bool:
				x = int(x)
			if type(x) is not str:
				x = str(x)
347 348
			return (('"%s"' % x) 
				if ' ' in x and not x.startswith('"') else x)
349
		return '-D%s=%s' % (Config.to_underscore(o).upper(), quoted(v))
350 351 352 353 354 355 356 357 358 359 360 361


class GitHelper:

	not_submodules = (
		'git', 'python', 'tests', 'test', 'cbf', 'lz4', 'fits', 'gz', 
		'tiff', 'hdf5'
	)

	submodule_map = {
		'espia': 'camera/common/espia',
		'pytango-server': 'applications/tango/python',
362
		'sps-image': 'Sps'
363 364 365
	}

	basic_submods = (
366
		'Processlib',
367 368
	)

369 370 371
	def __init__(self, cfg):
		self.cfg = cfg
		self.opts = self.cfg.get_git_options()
372 373 374 375

	def check_submodules(self, submodules=None):
		if submodules is None:
			submodules = self.opts
376 377 378 379
		submodules = list(submodules)
		for submod in self.basic_submods:
			if submod not in submodules:
				submodules.append(submod)
380

381
		root = self.cfg.get('source-prefix')
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
		with ch_dir(root):
			submod_list = []
			for submod in submodules:
				if submod in self.not_submodules:
					continue
				if submod in self.submodule_map:
					submod = self.submodule_map[submod]
				for sdir in ['third-party', 'camera']:
					s = os.path.join(sdir, submod)
					if os.path.isdir(s):
						submod = s
						break
				if os.path.isdir(submod):
					submod_list.append(submod)

397
			for submod in submod_list:
398
				self.update_submodule(submod)
399

400 401 402 403 404 405 406 407 408 409 410 411 412 413 414
	def update_submodule(self, submod):
		try:
			action = 'init ' + submod
			exec_cmd('git submodule ' + action)
			action = 'update ' + submod
			exec_cmd('git submodule ' + action)
			with ch_dir(submod):
				exec_cmd('git submodule init')
				cmd = ['git', 'submodule']
				p = Popen(cmd, stdout=PIPE)
				for l in p.stdout.readlines():
					tok = l.strip().split()
					self.update_submodule(tok[1])
		except Exception as e:
			sys.exit('Problem with submodule %s: %s' % (submod, e))
415

416 417
def build_install_lima(cfg):
	build_prefix = cfg.get('build-prefix')
418 419 420 421
	if not os.path.exists(build_prefix):
		os.mkdir(build_prefix)
	os.chdir(build_prefix)

422
	cmake_opts = CMakeOptions(cfg)
423 424 425 426 427 428 429 430 431 432 433 434 435 436
	cmake_cmd = cmake_opts.get_configure_options()
	exec_cmd(cmake_cmd, ('Something is wrong in CMake environment. ' +
			     'Make sure your configuration is good.'))

	cmake_cmd = cmake_opts.get_build_options()
	exec_cmd(cmake_cmd, ('CMake could not build Lima. ' + 
			     'Pleae contact lima@esrf.fr for help.'))

	if not cfg.is_install_required():
		return

	cmake_cmd = cmake_opts.get_install_options()
	exec_cmd(cmake_cmd, ('CMake could not install libraries. ' + 
			     'Make sure you have necessary rights.'))
437

438

439
def main():
440
	cfg = Config(sys.argv)
441

442
	# No git option under windows for obvious reasons.
443
	if OS_TYPE == 'Linux' and cfg.get('git'):
444
		git = GitHelper(cfg)
445 446
		git.check_submodules()

447 448 449 450 451
	try:
		build_install_lima(cfg)
	except Exception as e:
		sys.exit('Problem building/installing Lima: %s' % e)

452

453 454 455

if __name__ == '__main__':
	main()
456