OXYGENHUB
◆ main openmsx / build / probe.py
12.5 KiB · Python · last changed chore(): initial commit 1 month ago source blame history raw
1
# Replacement for autoconf.
2
# Performs some test compiles, to check for headers and functions.
3
# It does not execute anything it builds, making it friendly for cross compiles.
4
5
from compilers import CompileCommand, LinkCommand
6
from components import iterComponents, requiredLibrariesFor
7
from configurations import getConfiguration
8
from executils import captureStdout, shjoin
9
from itertools import chain
10
from libraries import librariesByName
11
from makeutils import extractMakeVariables, parseBool
12
from outpututils import rewriteIfChanged
13
from packages import getPackage
14
from systemfuncs import systemFunctions
15
from systemfuncs2code import iterSystemFuncsHeader
16
17
from io import open
18
from os import environ, makedirs, remove
19
from os.path import isdir, isfile, pathsep
20
from shlex import split as shsplit
21
import sys
22
23
def resolve(log, expr):
24
	if expr is None:
25
		return ''
26
	# TODO: Since for example "sdl-config" is used in more than one
27
	#       CFLAGS definition, it will be executed multiple times.
28
	try:
29
		return normalizeWhitespace(evaluateBackticks(log, expr))
30
	except OSError:
31
		# Executing a lib-config script is expected to fail if the
32
		# script is not installed.
33
		# TODO: Report this explicitly in the probe results table.
34
		return ''
35
36
def writeFile(path, lines):
37
	with open(path, 'w', encoding='utf-8') as out:
38
		for line in lines:
39
			print(line, file=out)
40
41
def tryCompile(log, compileCommand, sourcePath, lines):
42
	'''Write the program defined by "lines" to a text file specified
43
	by "path" and try to compile it.
44
	Returns True iff compilation succeeded.
45
	'''
46
	assert sourcePath.endswith('.cc')
47
	objectPath = sourcePath[ : -3] + '.o'
48
	writeFile(sourcePath, lines)
49
	try:
50
		return compileCommand.compile(log, sourcePath, objectPath)
51
	finally:
52
		remove(sourcePath)
53
		if isfile(objectPath):
54
			remove(objectPath)
55
56
def checkCompiler(log, compileCommand, outDir):
57
	'''Checks whether compiler can compile anything at all.
58
	Returns True iff the compiler works.
59
	'''
60
	def hello():
61
		# The most famous program.
62
		yield '#include <iostream>'
63
		yield 'int main(int argc, char** argv) {'
64
		yield '  std::cout << "Hello World!" << std::endl;'
65
		yield '  return 0;'
66
		yield '}'
67
	return tryCompile(log, compileCommand, outDir + '/hello.cc', hello())
68
69
def checkFunc(log, compileCommand, outDir, checkName, funcName, headers):
70
	'''Checks whether the given function is declared by the given headers.
71
	Returns True iff the function is declared.
72
	'''
73
	def takeFuncAddr():
74
		# Try to include the necessary headers and get the function address.
75
		for header in headers:
76
			yield '#include %s' % header
77
		yield 'void (*f)() = reinterpret_cast<void (*)()>(%s);' % funcName
78
	return tryCompile(
79
		log, compileCommand, outDir + '/' + checkName + '.cc', takeFuncAddr()
80
		)
81
82
def evaluateBackticks(log, expression):
83
	parts = []
84
	index = 0
85
	while True:
86
		start = expression.find('`', index)
87
		if start == -1:
88
			parts.append(expression[index : ])
89
			break
90
		end = expression.find('`', start + 1)
91
		if end == -1:
92
			raise ValueError('Unmatched backtick: %s' % expression)
93
		parts.append(expression[index : start])
94
		command = expression[start + 1 : end].strip()
95
		result = captureStdout(log, command)
96
		if result is None:
97
			raise OSError('Backtick evaluation failed; see log')
98
		parts.append(result)
99
		index = end + 1
100
	return ''.join(parts)
101
102
def normalizeWhitespace(expression):
103
	return shjoin(shsplit(expression))
104
105
class TargetSystem(object):
106
107
	def __init__(
108
		self, log, logPath, compileCommandStr, outDir, platform, distroRoot,
109
		configuration
110
		):
111
		'''Create empty log and result files.
112
		'''
113
		self.log = log
114
		self.logPath = logPath
115
		self.compileCommandStr = compileCommandStr
116
		self.outDir = outDir
117
		self.platform = platform
118
		self.distroRoot = distroRoot
119
		self.configuration = configuration
120
		self.outMakePath = outDir + '/probed_defs.mk'
121
		self.outHeaderPath = outDir + '/systemfuncs.hh'
122
		self.outVars = {}
123
		self.functionResults = {}
124
		self.typeTraitsResult = None
125
		self.libraries = sorted(requiredLibrariesFor(
126
			configuration.iterDesiredComponents()
127
			))
128
129
	def checkAll(self):
130
		'''Run all probes.
131
		'''
132
		self.hello()
133
		for func in systemFunctions:
134
			self.checkFunc(func)
135
		for library in self.libraries:
136
			self.checkLibrary(library)
137
138
	def writeAll(self):
139
		def iterVars():
140
			yield '# Automatically generated by build system.'
141
			yield '# Non-empty value means found, empty means not found.'
142
			for library in self.libraries:
143
				for name in (
144
					'HAVE_%s_H' % library,
145
					'HAVE_%s_LIB' % library,
146
					'%s_CFLAGS' % library,
147
					'%s_LDFLAGS' % library,
148
					):
149
					yield '%s:=%s' % (name, self.outVars[name])
150
		rewriteIfChanged(self.outMakePath, iterVars())
151
152
		rewriteIfChanged(
153
			self.outHeaderPath,
154
			iterSystemFuncsHeader(self.functionResults),
155
			)
156
157
	def printResults(self):
158
		for line in iterProbeResults(
159
			self.outVars, self.configuration, self.logPath
160
			):
161
			print(line)
162
163
	def everything(self):
164
		self.checkAll()
165
		self.writeAll()
166
		self.printResults()
167
168
	def hello(self):
169
		'''Check compiler with the most famous program.
170
		'''
171
		compileCommand = CompileCommand.fromLine(self.compileCommandStr, '')
172
		ok = checkCompiler(self.log, compileCommand, self.outDir)
173
		print('Compiler %s: %s' % (
174
			'works' if ok else 'broken',
175
			compileCommand
176
			), file=self.log)
177
		self.outVars['COMPILER'] = str(ok).lower()
178
179
	def checkFunc(self, func):
180
		'''Probe for function.
181
		'''
182
		compileCommand = CompileCommand.fromLine(self.compileCommandStr, '')
183
		ok = checkFunc(
184
			self.log, compileCommand, self.outDir,
185
			func.name, func.getFunctionName(), func.iterHeaders(self.platform)
186
			)
187
		print('%s function: %s' % (
188
			'Found' if ok else 'Missing',
189
			func.getFunctionName()
190
			), file=self.log)
191
		self.functionResults[func.getMakeName()] = ok
192
193
	def checkLibrary(self, makeName):
194
		library = librariesByName[makeName]
195
		cflags = resolve(
196
			self.log,
197
			library.getCompileFlags(
198
				self.platform, self.configuration.linkStatic(), self.distroRoot
199
				)
200
			)
201
		ldflags = resolve(
202
			self.log,
203
			library.getLinkFlags(
204
				self.platform, self.configuration.linkStatic(), self.distroRoot
205
				)
206
			)
207
		self.outVars['%s_CFLAGS' % makeName] = cflags
208
		self.outVars['%s_LDFLAGS' % makeName] = ldflags
209
210
		sourcePath = self.outDir + '/' + makeName + '.cc'
211
		objectPath = self.outDir + '/' + makeName + '.o'
212
		binaryPath = self.outDir + '/' + makeName + '.bin'
213
		if self.platform == 'android':
214
			binaryPath = self.outDir + '/' + makeName + '.so'
215
			ldflags += ' -shared -Wl,--no-undefined'
216
217
		compileCommand = CompileCommand.fromLine(self.compileCommandStr, cflags)
218
		linkCommand = LinkCommand.fromLine(self.compileCommandStr, ldflags)
219
220
		funcName = library.function
221
		headers = library.getHeaders(self.platform)
222
		def takeFuncAddr():
223
			# Try to include the necessary headers and get the function address.
224
			for header in headers:
225
				yield '#include %s' % header
226
			yield 'void (*f)() = reinterpret_cast<void (*)()>(%s);' % funcName
227
			yield 'int main(int argc, char** argv) {'
228
			yield '  return 0;'
229
			yield '}'
230
		writeFile(sourcePath, takeFuncAddr())
231
		try:
232
			compileOK = compileCommand.compile(self.log, sourcePath, objectPath)
233
			print('%s: %s header' % (
234
				makeName,
235
				'Found' if compileOK else 'Missing'
236
				), file=self.log)
237
			if compileOK:
238
				linkOK = linkCommand.link(self.log, [ objectPath ], binaryPath)
239
				print('%s: %s lib' % (
240
					makeName,
241
					'Found' if linkOK else 'Missing'
242
					), file=self.log)
243
			else:
244
				linkOK = False
245
				print(
246
					'%s: Cannot test linking because compile failed' % makeName,
247
					file=self.log
248
					)
249
		finally:
250
			remove(sourcePath)
251
			if isfile(objectPath):
252
				remove(objectPath)
253
			if isfile(binaryPath):
254
				remove(binaryPath)
255
256
		self.outVars['HAVE_%s_H' % makeName] = 'true' if compileOK else ''
257
		self.outVars['HAVE_%s_LIB' % makeName] = 'true' if linkOK else ''
258
		if linkOK:
259
			versionGet = library.getVersion(
260
				self.platform, self.configuration.linkStatic(), self.distroRoot
261
				)
262
			if callable(versionGet):
263
				version = versionGet(compileCommand, self.log)
264
			else:
265
				version = resolve(self.log, versionGet)
266
			if version is None:
267
				version = 'error'
268
			self.outVars['VERSION_%s' % makeName] = version
269
270
def iterProbeResults(probeVars, configuration, logPath):
271
	'''Present probe results, so user can decide whether to start the build,
272
	or to change system configuration and rerun "configure".
273
	'''
274
	desiredComponents = set(configuration.iterDesiredComponents())
275
	requiredComponents = set(configuration.iterRequiredComponents())
276
	buildableComponents = set(configuration.iterBuildableComponents(probeVars))
277
	packages = sorted(
278
		(	getPackage(makeName)
279
			for makeName in requiredLibrariesFor(desiredComponents)
280
			),
281
		key = lambda package: package.niceName.lower()
282
		)
283
	customVars = extractMakeVariables('build/custom.mk')
284
285
	yield ''
286
	if not parseBool(probeVars['COMPILER']):
287
		yield 'No working C++ compiler was found.'
288
		yield "Please install a C++ compiler, such as GCC's g++."
289
		yield 'If you have a C++ compiler installed and openMSX did not ' \
290
			'detect it, please set the environment variable CXX to the name ' \
291
			'of your C++ compiler.'
292
		yield 'After you have corrected the situation, rerun "configure".'
293
		yield ''
294
	else:
295
		# Compute how wide the first column should be.
296
		def iterNiceNames():
297
			for package in packages:
298
				yield package.niceName
299
			for component in iterComponents():
300
				yield component.niceName
301
		maxLen = max(len(niceName) for niceName in iterNiceNames())
302
		formatStr = '  %-' + str(maxLen + 3) + 's %s'
303
304
		yield 'Found libraries:'
305
		for package in packages:
306
			makeName = package.getMakeName()
307
			if probeVars['HAVE_%s_LIB' % makeName]:
308
				found = 'version %s' % probeVars['VERSION_%s' % makeName]
309
			elif probeVars['HAVE_%s_H' % makeName]:
310
				# Dependency resolution of a typical distro will not allow
311
				# this situation. Most likely we got the link flags wrong.
312
				found = 'headers found, link test failed'
313
			else:
314
				found = 'no'
315
			yield formatStr % (package.niceName + ':', found)
316
		yield ''
317
318
		yield 'Components overview:'
319
		for component in iterComponents():
320
			if component in desiredComponents:
321
				status = 'yes' if component in buildableComponents else 'no'
322
			else:
323
				status = 'disabled'
324
			yield formatStr % (component.niceName + ':', status)
325
		yield ''
326
327
		yield 'Customisable options:'
328
		yield formatStr % ('Install to', customVars['INSTALL_BASE'])
329
		yield '  (you can edit these in build/custom.mk)'
330
		yield ''
331
332
		if buildableComponents == desiredComponents:
333
			yield 'All required and optional components can be built.'
334
		elif requiredComponents.issubset(buildableComponents):
335
			yield 'If you are satisfied with the probe results, ' \
336
				'run "make" to start the build.'
337
			yield 'Otherwise, install some libraries and headers ' \
338
				'and rerun "configure".'
339
		else:
340
			yield 'Please install missing libraries and headers ' \
341
				'and rerun "configure".'
342
		yield ''
343
		yield 'If the detected libraries differ from what you think ' \
344
			'is installed on this system, please check the log file: %s' \
345
			% logPath
346
		yield ''
347
348
def main(compileCommandStr, outDir, platform, linkMode, thirdPartyInstall):
349
	if not isdir(outDir):
350
		makedirs(outDir)
351
	logPath = outDir + '/probe.log'
352
	with open(logPath, 'w', encoding='utf-8') as log:
353
		print('Probing target system...')
354
		print('Probing system:', file=log)
355
		distroRoot = thirdPartyInstall or None
356
		if distroRoot is None:
357
			if platform == 'darwin':
358
				for searchPath in environ.get('PATH', '').split(pathsep):
359
					if searchPath == '/opt/local/bin':
360
						print('Using libraries from MacPorts.')
361
						distroRoot = '/opt/local'
362
						break
363
					elif searchPath == '/opt/homebrew/bin':
364
						print('Using libraries from Homebrew.')
365
						distroRoot = '/opt/homebrew'
366
						break
367
					elif searchPath == '/sw/bin':
368
						print('Using libraries from Fink.')
369
						distroRoot = '/sw'
370
						break
371
				else:
372
					distroRoot = '/usr/local'
373
			elif platform.endswith('bsd') or platform == 'dragonfly':
374
				distroRoot = environ.get('LOCALBASE', '/usr/local')
375
				print('Using libraries from ports directory %s.' % distroRoot)
376
			elif platform == 'pandora':
377
				distroRoot = environ.get('LIBTOOL_SYSROOT_PATH')
378
				if distroRoot is not None:
379
					distroRoot += '/usr'
380
					print(
381
						'Using libraries from sysroot directory %s.'
382
						% distroRoot
383
						)
384
385
		configuration = getConfiguration(linkMode)
386
387
		TargetSystem(
388
			log, logPath, compileCommandStr, outDir, platform, distroRoot,
389
			configuration
390
			).everything()
391
392
if __name__ == '__main__':
393
	if len(sys.argv) == 6:
394
		try:
395
			main(*sys.argv[1 : ])
396
		except ValueError as ve:
397
			print(ve, file=sys.stderr)
398
			sys.exit(2)
399
	else:
400
		print(
401
			'Usage: python3 probe.py '
402
			'COMPILE OUTDIR OPENMSX_TARGET_OS LINK_MODE 3RDPARTY_INSTALL_DIR',
403
			file=sys.stderr
404
			)
405
		sys.exit(2)