OXYGENHUB
◆ main openmsx / build / libraries.py
16.2 KiB · Python · last changed chore(): initial commit 1 month ago source blame history raw
1
# Some notes about static linking:
2
# There are two ways of linking to static library: using the -l command line
3
# option or specifying the full path to the library file as one of the inputs.
4
# When using the -l option, the library search paths will be searched for a
5
# dynamic version of the library, if that is not found, the search paths will
6
# be searched for a static version of the library. This means we cannot force
7
# static linking of a library this way. It is possible to force static linking
8
# of all libraries, but we want to control it per library.
9
# Conclusion: We have to specify the full path to each library that should be
10
#             linked statically.
11
12
from executils import captureStdout, shjoin
13
from msysutils import msysActive, msysPathToNative
14
15
from io import open
16
from os import listdir
17
from os.path import isdir, isfile
18
from os import environ
19
20
def _get_pkg_config(distroRoot):
21
	if distroRoot is None:
22
		return 'pkg-config'
23
	elif distroRoot.startswith('derived/'):
24
		toolsDir = '%s/../tools/bin' % distroRoot
25
		for name in listdir(toolsDir):
26
			if name.endswith('-pkg-config'):
27
				return toolsDir + '/' + name
28
		raise RuntimeError('No cross-pkg-config found in 3rdparty build')
29
	else:
30
		return '%s/bin/pkg-config' % distroRoot
31
32
class Library(object):
33
	libName = None
34
	makeName = None
35
	header = None
36
	configScriptName = None
37
	dynamicLibsOption = '--libs'
38
	staticLibsOption = None
39
	function = None
40
	# TODO: A library can give an application compile time and run time
41
	#       dependencies on other libraries. For example SDL_ttf depends on
42
	#       FreeType only at run time, but depends on SDL both compile time
43
	#       and run time, since SDL is part of its interface and FreeType is
44
	#       only used by the implementation. As a result, it is possible to
45
	#       compile against SDL_ttf without having the FreeType headers
46
	#       installed. But our getCompileFlags() does not support this.
47
	#       In pkg-config these are called private dependencies.
48
	dependsOn = ()
49
50
	@classmethod
51
	def isSystemLibrary(cls, platform): # pylint: disable-msg=W0613
52
		'''Returns True iff this library is a system library on the given
53
		platform.
54
		A system library is a library that is available systemwide in the
55
		minimal installation of the OS.
56
		The default implementation returns False.
57
		'''
58
		return False
59
60
	@classmethod
61
	def getConfigScript( # pylint: disable-msg=W0613
62
		cls, platform, linkStatic, distroRoot
63
		):
64
		scriptName = cls.configScriptName
65
		if scriptName is None:
66
			return None
67
		elif distroRoot is None:
68
			return scriptName
69
		else:
70
			return '%s/bin/%s' % (distroRoot, scriptName)
71
72
	@classmethod
73
	def getHeaders(cls, platform): # pylint: disable-msg=W0613
74
		header = cls.header
75
		return (header, ) if isinstance(header, str) else header
76
77
	@classmethod
78
	def getLibName(cls, platform): # pylint: disable-msg=W0613
79
		return cls.libName
80
81
	@classmethod
82
	def getCompileFlags(cls, platform, linkStatic, distroRoot):
83
		configScript = cls.getConfigScript(platform, linkStatic, distroRoot)
84
		if configScript is not None:
85
			flags = [ '`%s --cflags`' % configScript ]
86
		elif distroRoot is None or cls.isSystemLibrary(platform):
87
			flags = []
88
		else:
89
			flags = [ '-isystem %s/include' % distroRoot ]
90
		dependentFlags = [
91
			librariesByName[name].getCompileFlags(
92
				platform, linkStatic, distroRoot
93
				)
94
			for name in cls.dependsOn
95
			]
96
		return ' '.join(flags + dependentFlags)
97
98
	@classmethod
99
	def getLinkFlags(cls, platform, linkStatic, distroRoot):
100
		configScript = cls.getConfigScript(platform, linkStatic, distroRoot)
101
		if configScript is not None:
102
			libsOption = (
103
				cls.dynamicLibsOption
104
				if not linkStatic or cls.isSystemLibrary(platform)
105
				else cls.staticLibsOption
106
				)
107
			if libsOption is not None:
108
				return '`%s %s`' % (configScript, libsOption)
109
		if distroRoot is None or cls.isSystemLibrary(platform):
110
			return '-l%s' % cls.getLibName(platform)
111
		else:
112
			flags = [
113
				'%s/lib/lib%s.a' % (distroRoot, cls.getLibName(platform))
114
				] if linkStatic else [
115
				'-L%s/lib -l%s' % (distroRoot, cls.getLibName(platform))
116
				]
117
			dependentFlags = [
118
				librariesByName[name].getLinkFlags(
119
					platform, linkStatic, distroRoot
120
					)
121
				for name in cls.dependsOn
122
				]
123
			return ' '.join(flags + dependentFlags)
124
125
	@classmethod
126
	def getVersion(cls, platform, linkStatic, distroRoot):
127
		'''Returns the version of this library, "unknown" if there is no
128
		mechanism to retrieve the version, None if there is a mechanism
129
		to retrieve the version but it failed, or a callable that takes a
130
		CompileCommand and a log stream as its arguments and returns the
131
		version or None if retrieval failed.
132
		'''
133
		configScript = cls.getConfigScript(platform, linkStatic, distroRoot)
134
		if configScript is None:
135
			return 'unknown'
136
		elif 'pkg-config' in configScript:
137
			return '`%s --modversion`' % configScript
138
		else:
139
			return '`%s --version`' % configScript
140
141
class ALSA(Library):
142
	libName = 'asound'
143
	makeName = 'ALSA'
144
	header = '<alsa/asoundlib.h>'
145
	function = 'snd_seq_open'
146
147
	@classmethod
148
	def isSystemLibrary(cls, platform):
149
		return True
150
151
	@classmethod
152
	def getLinkFlags(cls, platform, linkStatic, distroRoot):
153
		flags = super().getLinkFlags(platform, linkStatic, distroRoot)
154
		if linkStatic:
155
			flags += ' -lpthread'
156
		return flags
157
158
	@classmethod
159
	def getVersion(cls, platform, linkStatic, distroRoot):
160
		def execute(cmd, log):
161
			version = cmd.expand(log, cls.getHeaders(platform),
162
				'SND_LIB_VERSION_STR'
163
				)
164
			return None if version is None else version.strip('"')
165
		return execute
166
167
class FreeType(Library):
168
	libName = 'freetype'
169
	makeName = 'FREETYPE'
170
	header = ('<ft2build.h>', 'FT_FREETYPE_H')
171
	configScriptName = 'freetype-config'
172
	function = 'FT_Open_Face'
173
174
	@classmethod
175
	def getConfigScript(cls, platform, linkStatic, distroRoot):
176
		if platform in ('netbsd', 'openbsd'):
177
			if distroRoot == '/usr/local':
178
				# FreeType is located in the X11 tree, not the ports tree.
179
				distroRoot = '/usr/X11R6'
180
		script = super().getConfigScript(
181
			platform, linkStatic, distroRoot
182
			)
183
		if isfile(script):
184
			return script
185
		# FreeType 2.9.1 no longer installs the freetype-config script
186
		# by default and expects pkg-config to be used instead.
187
		return '%s freetype2' % _get_pkg_config(distroRoot)
188
189
	@classmethod
190
	def getVersion(cls, platform, linkStatic, distroRoot):
191
		configScript = cls.getConfigScript(platform, linkStatic, distroRoot)
192
		return '`%s --version`' % configScript
193
194
class GL(Library):
195
	libName = 'GL'
196
	makeName = 'GL'
197
	function = 'glGenTextures'
198
199
	@classmethod
200
	def isSystemLibrary(cls, platform):
201
		# On *BSD, OpenGL is in ports, not in the base system.
202
		return not platform.endswith('bsd')
203
204
	@classmethod
205
	def getHeaders(cls, platform):
206
		if platform == 'darwin':
207
			return ('<OpenGL/gl.h>', )
208
		else:
209
			return ('<GL/gl.h>', )
210
211
	@classmethod
212
	def getCompileFlags(cls, platform, linkStatic, distroRoot):
213
		if platform in ('netbsd', 'openbsd'):
214
			return '-isystem /usr/X11R6/include -isystem /usr/X11R7/include'
215
		else:
216
			return super().getCompileFlags(
217
				platform, linkStatic, distroRoot
218
				)
219
220
	@classmethod
221
	def getLinkFlags(cls, platform, linkStatic, distroRoot):
222
		if platform == 'darwin':
223
			return '-framework OpenGL'
224
		elif platform.startswith('mingw'):
225
			return '-lopengl32'
226
		elif platform in ('netbsd', 'openbsd'):
227
			return '-L/usr/X11R6/lib -L/usr/X11R7/lib -lGL'
228
		else:
229
			return super().getLinkFlags(platform, linkStatic, distroRoot)
230
231
	@classmethod
232
	def getVersion(cls, platform, linkStatic, distroRoot):
233
		def execute(cmd, log):
234
			versionPairs = tuple(
235
				( major, minor )
236
				for major in range(1, 10)
237
				for minor in range(0, 10)
238
				)
239
			version = cmd.expand(log, cls.getHeaders(platform), *(
240
				'GL_VERSION_%d_%d' % pair for pair in versionPairs
241
				))
242
			try:
243
				return '%d.%d' % max(
244
					ver
245
					for ver, exp in zip(versionPairs, version)
246
					if exp is not None
247
					)
248
			except ValueError:
249
				return None
250
		return execute
251
252
class GLEW(Library):
253
	makeName = 'GLEW'
254
	header = '<GL/glew.h>'
255
	function = 'glewInit'
256
	dependsOn = ('GL', )
257
258
	@classmethod
259
	def getLibName(cls, platform):
260
		if platform.startswith('mingw'):
261
			return 'glew32'
262
		else:
263
			return 'GLEW'
264
265
	@classmethod
266
	def getCompileFlags(cls, platform, linkStatic, distroRoot):
267
		flags = super().getCompileFlags(
268
			platform, linkStatic, distroRoot
269
			)
270
		if platform.startswith('mingw') and linkStatic:
271
			return '%s -DGLEW_STATIC' % flags
272
		else:
273
			return flags
274
275
class LibPNG(Library):
276
	libName = 'png16'
277
	makeName = 'PNG'
278
	header = '<png.h>'
279
	configScriptName = 'libpng-config'
280
	dynamicLibsOption = '--ldflags'
281
	function = 'png_write_image'
282
	dependsOn = ('ZLIB', )
283
284
class OGG(Library):
285
	libName = 'ogg'
286
	makeName = 'OGG'
287
	header = '<ogg/ogg.h>'
288
	function = 'ogg_stream_init'
289
290
class SDL2(Library):
291
	libName = 'SDL2'
292
	makeName = 'SDL2'
293
	header = '<SDL.h>'
294
	configScriptName = 'sdl2-config'
295
	staticLibsOption = '--static-libs'
296
	function = 'SDL_Init'
297
298
	@classmethod
299
	def getLinkFlags(cls, platform, linkStatic, distroRoot):
300
		flags = super().getLinkFlags(platform, linkStatic, distroRoot)
301
		if platform in ('android',):
302
			flags += ' -lOpenSLES'
303
		return flags
304
305
class SDL2_ttf(Library):
306
	libName = 'SDL2_ttf'
307
	makeName = 'SDL2_TTF'
308
	header = '<SDL_ttf.h>'
309
	function = 'TTF_OpenFont'
310
	dependsOn = ('SDL2', 'FREETYPE')
311
312
	@classmethod
313
	def getLinkFlags(cls, platform, linkStatic, distroRoot):
314
		flags = super().getLinkFlags(
315
			platform, linkStatic, distroRoot
316
			)
317
		if not linkStatic:
318
			# Because of the SDLmain trickery, we need SDL's link flags too
319
			# on some platforms even though we're linking dynamically.
320
			flags += ' ' + SDL2.getLinkFlags(platform, linkStatic, distroRoot)
321
		return flags
322
323
	@classmethod
324
	def getVersion(cls, platform, linkStatic, distroRoot):
325
		def execute(cmd, log):
326
			version = cmd.expand(log, cls.getHeaders(platform),
327
				'SDL_TTF_MAJOR_VERSION',
328
				'SDL_TTF_MINOR_VERSION',
329
				'SDL_TTF_PATCHLEVEL',
330
				)
331
			return None if None in version else '%s.%s.%s' % version
332
		return execute
333
334
class TCL(Library):
335
	libName = 'tcl'
336
	makeName = 'TCL'
337
	header = '<tcl.h>'
338
	function = 'Tcl_CreateInterp'
339
340
	@classmethod
341
	def getTclConfig(cls, platform, distroRoot):
342
		'''Tcl has a config script that is unlike the typical lib-config script.
343
		Information is gathered by sourcing the config script, instead of
344
		executing it and capturing the queried value from stdout. This script
345
		is located in a library directory, not in a directory in the PATH.
346
		Also, it does not have the executable bit set.
347
		This method returns the location of the Tcl config script, or None if
348
		it could not be found.
349
		'''
350
		if hasattr(cls, 'tclConfig'):
351
			# Return cached value.
352
			return cls.tclConfig
353
354
		def iterLocations():
355
			# Allow the user to specify the location of the config script,
356
			# by setting an environment variable.
357
			tclpath = environ.get('TCL_CONFIG')
358
			if tclpath is not None:
359
				yield tclpath
360
				return
361
362
			if distroRoot is None or cls.isSystemLibrary(platform):
363
				if msysActive():
364
					roots = (msysPathToNative('/mingw32'), )
365
				else:
366
					roots = ('/usr/local', '/usr')
367
			else:
368
				roots = (distroRoot, )
369
			for root in roots:
370
				if isdir(root):
371
					for libdir in ('lib', 'lib64', 'lib/tcl'):
372
						libpath = root + '/' + libdir
373
						if isdir(libpath):
374
							yield libpath
375
							for entry in listdir(libpath):
376
								if entry.startswith('tcl8.'):
377
									tclpath = libpath + '/' + entry
378
									if isdir(tclpath):
379
										yield tclpath
380
381
		tclConfigs = {}
382
		with open('derived/tcl-search.log', 'w', encoding='utf-8') as log:
383
			print('Looking for Tcl...', file=log)
384
			for location in iterLocations():
385
				path = location + '/tclConfig.sh'
386
				if isfile(path):
387
					print('Config script: %s' % path, file=log)
388
					text = captureStdout(
389
						log,
390
						"sh -c '. %s && echo %s'" % (
391
							path, '$TCL_MAJOR_VERSION $TCL_MINOR_VERSION'
392
							)
393
						)
394
					if text is not None:
395
						try:
396
							# pylint: disable-msg=E1103
397
							major, minor = text.split()
398
							version = int(major), int(minor)
399
						except ValueError:
400
							pass
401
						else:
402
							print('Found: version %d.%d' % version, file=log)
403
							tclConfigs[path] = version
404
			try:
405
				# Minimum required version is 8.5.
406
				# Pick the oldest possible version to minimize the risk of
407
				# running into incompatible changes.
408
				tclConfig = min(
409
					( version, path )
410
					for path, version in tclConfigs.items()
411
					if version >= (8, 5)
412
					)[1]
413
			except ValueError:
414
				tclConfig = None
415
				print('No suitable versions found.', file=log)
416
			else:
417
				print('Selected: %s' % tclConfig, file=log)
418
419
		cls.tclConfig = tclConfig
420
		return tclConfig
421
422
	@classmethod
423
	def evalTclConfigExpr(cls, platform, distroRoot, expr, description):
424
		tclConfig = cls.getTclConfig(platform, distroRoot)
425
		if tclConfig is None:
426
			return None
427
		with open('derived/tcl-search.log', 'a', encoding='utf-8') as log:
428
			print('Getting Tcl %s...' % description, file=log)
429
			text = captureStdout(
430
				log,
431
				shjoin([
432
					'sh', '-c',
433
						'. %s && eval "echo \\"%s\\""' % (tclConfig, expr)
434
					])
435
				)
436
			if text is not None:
437
				print('Result: %s' % text.strip(), file=log)
438
		return None if text is None else text.strip()
439
440
	@classmethod
441
	def getCompileFlags(cls, platform, linkStatic, distroRoot):
442
		wantShared = not linkStatic or cls.isSystemLibrary(platform)
443
		# The -DSTATIC_BUILD is a hack to avoid including the complete
444
		# TCL_DEFS (see 9f1dbddda2) but still being able to link on
445
		# MinGW (tcl.h depends on this being defined properly).
446
		return cls.evalTclConfigExpr(
447
			platform,
448
			distroRoot,
449
			'${TCL_INCLUDE_SPEC}' + ('' if wantShared else ' -DSTATIC_BUILD'),
450
			'compile flags'
451
			)
452
453
	@classmethod
454
	def getLinkFlags(cls, platform, linkStatic, distroRoot):
455
		# Tcl can be built as a shared or as a static library, but not both.
456
		# Check whether the library type of Tcl matches the one we want.
457
		wantShared = not linkStatic or cls.isSystemLibrary(platform)
458
		tclShared = cls.evalTclConfigExpr(
459
			platform,
460
			distroRoot,
461
			'${TCL_SHARED_BUILD}',
462
			'library type (shared/static)'
463
			)
464
		with open('derived/tcl-search.log', 'a', encoding='utf-8') as log:
465
			if tclShared == '0':
466
				if wantShared:
467
					print(
468
						'Dynamic linking requested, but Tcl installation has '
469
						'static library.', file=log
470
						)
471
					return None
472
			elif tclShared == '1':
473
				if not wantShared:
474
					print(
475
						'Static linking requested, but Tcl installation has '
476
						'dynamic library.', file=log
477
						)
478
					return None
479
			else:
480
				print(
481
					'Unable to determine whether Tcl installation has '
482
					'shared or static library.', file=log
483
					)
484
				return None
485
486
		# Now get the link flags.
487
		if wantShared:
488
			return cls.evalTclConfigExpr(
489
				platform,
490
				distroRoot,
491
				'${TCL_LIB_SPEC}',
492
				'dynamic link flags'
493
				)
494
		else:
495
			return cls.evalTclConfigExpr(
496
				platform,
497
				distroRoot,
498
				'${TCL_EXEC_PREFIX}/lib/${TCL_LIB_FILE} ${TCL_LIBS}',
499
				'static link flags'
500
				)
501
502
	@classmethod
503
	def getVersion(cls, platform, linkStatic, distroRoot):
504
		return cls.evalTclConfigExpr(
505
			platform,
506
			distroRoot,
507
			'${TCL_MAJOR_VERSION}.${TCL_MINOR_VERSION}${TCL_PATCH_LEVEL}',
508
			'version'
509
			)
510
511
class Theora(Library):
512
	libName = 'theoradec'
513
	makeName = 'THEORA'
514
	header = '<theora/theoradec.h>'
515
	function = 'th_decode_ycbcr_out'
516
	dependsOn = ('OGG', )
517
518
class Vorbis(Library):
519
	libName = 'vorbis'
520
	makeName = 'VORBIS'
521
	header = '<vorbis/codec.h>'
522
	function = 'vorbis_synthesis_pcmout'
523
	dependsOn = ('OGG', )
524
525
class ZLib(Library):
526
	libName = 'z'
527
	makeName = 'ZLIB'
528
	header = '<zlib.h>'
529
	function = 'inflate'
530
531
	@classmethod
532
	def getVersion(cls, platform, linkStatic, distroRoot):
533
		def execute(cmd, log):
534
			version = cmd.expand(log, cls.getHeaders(platform), 'ZLIB_VERSION')
535
			return None if version is None else version.strip('"')
536
		return execute
537
538
# Build a dictionary of libraries using introspection.
539
librariesByName = {
540
	obj.makeName: obj
541
	for obj in locals().values()
542
	if isinstance(obj, type)
543
		and issubclass(obj, Library)
544
		and obj is not Library
545
	}
546
547
def allDependencies(makeNames):
548
	'''Compute the set of all directly and indirectly required libraries to
549
	build and use the given set of libraries.
550
	Returns the make names of the required libraries.
551
	'''
552
	# Compute the reflexive-transitive closure.
553
	transLibs = set()
554
	newLibs = set(makeNames)
555
	while newLibs:
556
		transLibs.update(newLibs)
557
		newLibs = set(
558
			depMakeName
559
			for makeName in newLibs
560
			for depMakeName in librariesByName[makeName].dependsOn
561
			if depMakeName not in transLibs
562
			)
563
	return transLibs