528 lines
18 KiB
Python
Executable File
528 lines
18 KiB
Python
Executable File
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# Build script for the Bos Wars engine.
|
|
#
|
|
# (c) Copyright 2010-2023 by Francois Beerten.
|
|
#
|
|
# This program 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 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program 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, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
# This script is compatible with both Python 2 and Python 3.
|
|
|
|
import os
|
|
import glob
|
|
import sys
|
|
import shutil
|
|
try:
|
|
import Queue as queue
|
|
except ImportError:
|
|
import queue
|
|
import threading
|
|
from warnings import warn
|
|
from fabricate import *
|
|
|
|
|
|
target = 'boswars'
|
|
gccflags = '-Wall -fsigned-char -D_GNU_SOURCE=1 -D_REENTRANT'.split()
|
|
incpaths = 'engine/include engine/guichan/include'.split()
|
|
|
|
def find(startdir, pattern):
|
|
import fnmatch
|
|
results = []
|
|
for dirpath,dirnames,files in os.walk(startdir):
|
|
dirnames.sort()
|
|
files.sort()
|
|
for f in files:
|
|
# Ignore dot files. GNU Emacs especially creates lock files
|
|
# with names like ".#script.cpp". Don't try to compile those.
|
|
if fnmatch.fnmatch(f, pattern) and not f.startswith("."):
|
|
results.append(dirpath+'/'+f)
|
|
return results
|
|
|
|
sources = find('engine', '*.cpp')
|
|
|
|
def tolist(x):
|
|
if isinstance(x, str):
|
|
return x.split(' ')
|
|
else:
|
|
return list(x)
|
|
|
|
class Gcc(object):
|
|
def __init__(self, cflags=[], ldflags=[], cc='g++', builddir='fbuild',
|
|
usepkgconfig=True):
|
|
self.cflags = gccflags + tolist(cflags)
|
|
self.ldflags = tolist(ldflags)
|
|
self.cc = cc
|
|
self.builddir = builddir
|
|
self.usepkgconfig = usepkgconfig
|
|
def copy(self):
|
|
g = Gcc(self.cflags, self.ldflags, self.cc, self.builddir, self.usepkgconfig)
|
|
return g
|
|
def oname(self, source):
|
|
base = os.path.splitext(source)[0]
|
|
return '%s.o' % (base)
|
|
def lib(self, *names):
|
|
self.ldflags += ['-l'+ i for i in names]
|
|
def incpath(self, *names):
|
|
self.cflags += ['-I'+i for i in names]
|
|
def libpath(self, *names):
|
|
self.ldflags += ['-L'+i for i in names]
|
|
def define(self,name, value=''):
|
|
if value:
|
|
name += '='+value
|
|
self.cflags += ['-D' + name]
|
|
def debug(self):
|
|
self.cflags += ['-g', '-Werror']
|
|
def optimize(self, level=2):
|
|
self.cflags += ['-O%d'%level]
|
|
def profile(self):
|
|
self.cflags += ['-pg']
|
|
self.ldflags += ['-pg']
|
|
def cxx(self, target, source, flags=[]):
|
|
cflags = flags + self.cflags
|
|
target = self.oname(target)
|
|
return (self.cc, '-c', source, '-o', target, cflags)
|
|
def ld(self, target, sources, flags=[]):
|
|
ldflags = flags + self.ldflags
|
|
objects = [self.oname(s) for s in sources]
|
|
return (self.cc, objects, '-o', target, ldflags)
|
|
def build(self, target, sources, flags=[]):
|
|
flags = [] + flags + self.ldflags + self.cflags
|
|
return (self.cc, sources, '-o', target, flags)
|
|
|
|
def compiler(**kwargs):
|
|
return Gcc(**kwargs)
|
|
|
|
|
|
def inBuildDir(source, builddir):
|
|
d = builddir+'/'+os.path.basename(source)
|
|
return d
|
|
|
|
def mkdir(dir):
|
|
if not os.access(dir, os.F_OK):
|
|
os.makedirs(dir)
|
|
|
|
def Check(b, lib=None, header='', function='', name=''):
|
|
t = b.copy()
|
|
name = name or lib or function or header.replace('/','_')
|
|
testdir = b.builddir + '/conftests/'
|
|
testname = testdir+'test'+name
|
|
s = testname+'.c'
|
|
mkdir(testdir)
|
|
f = open(s, 'wt')
|
|
if header:
|
|
f.write('#include "%s"\n' % header)
|
|
if function:
|
|
f.write('#ifdef __cplusplus\nextern "C"\n#endif\nchar %s();\n'%function)
|
|
f.write('int main()\n{\n%s();\nreturn 0;\n}\n\n'%function)
|
|
else:
|
|
f.write('int main()\n{\nreturn 0;\n}\n\n')
|
|
f.close()
|
|
if lib:
|
|
t.lib(lib)
|
|
try:
|
|
run(*t.build(testname, [s]))
|
|
except ExecutionError as e:
|
|
return False
|
|
return True
|
|
|
|
def cmdline2list(s):
|
|
r"""Split a string into words and remove quotation marks and backslashes,
|
|
as a Bourne shell would.
|
|
|
|
If e.g. pkg-config --cflags lua5.1 outputs:
|
|
-DDEB_HOST_MULTIARCH=\"x86_64-linux-gnu\" -I/usr/include/lua5.1
|
|
this function can convert it to a list of words:
|
|
('-DDEB_HOST_MULTIARCH="x86_64-linux-gnu"', '-I/usr/include/lua5.1')
|
|
that you can then pass to subprocess.list2cmdline and get the
|
|
backslashes back.
|
|
|
|
This function neither parses nor executes any shell expansions. For
|
|
example, it does not support the ${variable} or `command` syntaxes.
|
|
If it sees any of those, it warns and passes the metacharacters
|
|
through unchanged."""
|
|
words = ()
|
|
pos = 0
|
|
# Read words from the string until we get to the end.
|
|
while pos < len(s):
|
|
if s[pos].isspace():
|
|
# Spaces delimit words. Discard them.
|
|
pos += 1
|
|
else:
|
|
# Any non-space character begins a word.
|
|
word = ""
|
|
if pos < len(s) and s[pos] == '~':
|
|
# An unquoted tilde at the beginning of a word should
|
|
# refer to a home directory or to a directory stack.
|
|
# We support neither.
|
|
warn("tilde expansion not supported")
|
|
while pos < len(s) and not s[pos].isspace():
|
|
if s[pos] == '\\':
|
|
# An unquoted backslash escapes the following character.
|
|
pos += 1
|
|
if pos >= len(s):
|
|
warn("trailing backslash")
|
|
else:
|
|
word += s[pos]
|
|
pos += 1
|
|
continue
|
|
if s[pos] == '"':
|
|
# A double-quoted string. Read characters until we get
|
|
# to the closing double-quotation mark.
|
|
pos += 1
|
|
while True:
|
|
if pos >= len(s):
|
|
warn("unterminated double-quoted string")
|
|
break
|
|
elif s[pos] == '"':
|
|
# This closes the string.
|
|
pos += 1
|
|
break
|
|
elif s[pos] == '\\':
|
|
# Within a double-quoted string, a
|
|
# backslash followed by another backslash,
|
|
# double-quotation mark, dollar sign, or
|
|
# backtick escapes that character.
|
|
# Otherwise, it's just part of the string.
|
|
if pos + 1 >= len(s):
|
|
warn("trailing backslash in double-quoted string")
|
|
elif s[pos + 1] in '\\"$`':
|
|
word += s[pos + 1]
|
|
pos += 2
|
|
continue
|
|
elif s[pos] in "$`":
|
|
# Within a double-quoted string, a dollar
|
|
# sign or a backtick would begin some
|
|
# shell-expansion syntax that this
|
|
# function does not support.
|
|
warn("unsupported character %s in double-quoted string" % s[pos])
|
|
# Otherwise, the character is part of the string.
|
|
word += s[pos]
|
|
pos += 1
|
|
continue
|
|
elif s[pos] == "'":
|
|
# A single-quoted string. Backslashes have
|
|
# no special meaning here. Just search for
|
|
# the closing single-quotation mark.
|
|
pos += 1
|
|
while True:
|
|
if pos >= len(s):
|
|
warn("unterminated single-quoted string")
|
|
break
|
|
elif s[pos] == "'":
|
|
# This closes the string.
|
|
pos += 1
|
|
break
|
|
# Otherwise, the character is part of the string.
|
|
word += s[pos]
|
|
pos += 1
|
|
continue
|
|
elif s[pos] in "#$&()*;<>?[]`|":
|
|
# When not part of a {double,single}-quoted
|
|
# string, these characters invoke various shell
|
|
# features that this function does not support.
|
|
warn("unsupported character %s" % s[pos])
|
|
word += s[pos]
|
|
pos += 1
|
|
# Finished reading the word; reached a space or the end of
|
|
# the input string.
|
|
words += (word,)
|
|
return words
|
|
|
|
def pkgconfig(b, package):
|
|
try:
|
|
b.cflags += cmdline2list(shell('pkg-config', '--cflags', package).decode())
|
|
b.ldflags += cmdline2list(shell('pkg-config', '--libs', package).decode())
|
|
except ExecutionError as e:
|
|
return False
|
|
except OSError as e:
|
|
return False
|
|
return True
|
|
|
|
def CheckLib(b, lib, header='', function=''):
|
|
if Check(b, lib, header, function=function):
|
|
b.lib(lib)
|
|
return True
|
|
return False
|
|
|
|
def RequireLib(b, lib, header=''):
|
|
r = CheckLib(b, lib, header)
|
|
if not r:
|
|
print(
|
|
'Did not find the required %s lib or headers, exiting!' % lib)
|
|
sys.exit(1)
|
|
|
|
def CheckLibAlternatives(b, libs, header=''):
|
|
for i in libs:
|
|
if CheckLib(b, i, header):
|
|
return True
|
|
|
|
def detectLua(b):
|
|
# lua5.1-c++ is what Debian calls a Lua library built to use C++
|
|
# exceptions rather than longjmp. http://bugs.debian.org/560139
|
|
# Prefer that because it will call our C++ destructors on unwind.
|
|
# This avoids memory leaks that would be very cumbersome to fix in
|
|
# any other way.
|
|
libs = 'lua5.1-c++ lua5.1 lua51 lua-5.1 lua'.split()
|
|
if b.usepkgconfig:
|
|
for i in libs:
|
|
if pkgconfig(b, i):
|
|
return
|
|
if CheckLibAlternatives(b, libs, header='lua.h'):
|
|
return
|
|
print('Did not find the Lua library, exiting !')
|
|
sys.exit(1)
|
|
|
|
def detectSdl(b):
|
|
if b.usepkgconfig and pkgconfig(b, 'sdl2'):
|
|
return
|
|
b.incpath('/usr/include/SDL2')
|
|
header = 'SDL.h'
|
|
if '-DUSE_WIN32' in b.cflags:
|
|
header = ''
|
|
if CheckLib(b, 'SDL2', header=header):
|
|
return
|
|
print('Did not find the SDL library, exiting !')
|
|
sys.exit(1)
|
|
|
|
def detectAlwaysDynamic(b):
|
|
RequireLib(b, 'z', 'zlib.h')
|
|
detectSdl(b)
|
|
if Check(b, function='strcasestr'):
|
|
b.define('HAVE_STRCASESTR')
|
|
if Check(b, function='strnlen'):
|
|
b.define('HAVE_STRNLEN')
|
|
for i in incpaths:
|
|
b.incpath(i)
|
|
|
|
def detectEmbedable(b):
|
|
detectLua(b)
|
|
RequireLib(b, 'png', 'png.h')
|
|
if CheckLib(b, 'vorbis'):
|
|
b.define('USE_VORBIS')
|
|
if CheckLib(b, 'theora', function='theora_decode_packetin'):
|
|
b.define('USE_THEORA')
|
|
if CheckLib(b, 'ogg'):
|
|
b.define('USE_OGG')
|
|
|
|
def detect(b):
|
|
detectAlwaysDynamic(b)
|
|
detectEmbedable(b)
|
|
|
|
def parallel_run(commands, jobs=2, builder=default_builder):
|
|
""" The different commands must be independent. """
|
|
requests, results = queue.Queue(), queue.Queue()
|
|
for i in commands:
|
|
c = builder.prepare(i)
|
|
if builder.should_run(*c):
|
|
requests.put(c)
|
|
totalrequests = requests.qsize()
|
|
requests.active = True
|
|
|
|
def runQueuedCommands(requests, results):
|
|
while requests.active:
|
|
c = requests.get()
|
|
if c is None:
|
|
return
|
|
arglist, command = c
|
|
builder.echo_command(command)
|
|
try:
|
|
deps, outputs = builder.runner(*arglist)
|
|
results.put((command, deps, outputs))
|
|
except ExecutionError as e:
|
|
results.put(e)
|
|
requests.active = True # abort future requests
|
|
|
|
for i in range(jobs):
|
|
t = threading.Thread(target=runQueuedCommands, args=(requests, results))
|
|
t.daemon = True
|
|
requests.put(None)
|
|
t.start()
|
|
for _ in range(totalrequests):
|
|
r = results.get()
|
|
if isinstance(r, ExecutionError): raise r
|
|
command, deps, outputs = r
|
|
builder.store_deps(command, deps, outputs)
|
|
|
|
def runall(commands, jobs=None):
|
|
jobs= jobs or int(main.options.jobs)
|
|
if jobs > 1:
|
|
parallel_run(commands)
|
|
else:
|
|
for i in commands:
|
|
run(*i)
|
|
|
|
def compile(b):
|
|
commands = [b.cxx(inBuildDir(s, b.builddir), s) for s in sources]
|
|
runall(commands)
|
|
|
|
def link(b):
|
|
objects = [b.oname(inBuildDir(s, b.builddir)) for s in sources]
|
|
apptarget = b.builddir + '/' + target
|
|
run(*b.ld(apptarget, objects))
|
|
|
|
def make(b):
|
|
compile(b)
|
|
link(b)
|
|
|
|
def release(builddir='fbuild/release',**kwargs):
|
|
b = compiler(builddir=builddir, **kwargs)
|
|
mkdir(builddir)
|
|
detect(b)
|
|
b.optimize()
|
|
make(b)
|
|
|
|
def debug(builddir='fbuild/debug',**kwargs):
|
|
b = compiler(builddir=builddir, **kwargs)
|
|
mkdir(builddir)
|
|
detect(b)
|
|
b.debug()
|
|
b.define('DEBUG')
|
|
make(b)
|
|
|
|
def profile(builddir='fbuild/profile',**kwargs):
|
|
b = compiler(builddir=builddir, **kwargs)
|
|
mkdir(builddir)
|
|
detect(b)
|
|
b.profile()
|
|
make(b)
|
|
|
|
class StaticGcc(Gcc):
|
|
def __init__(self,*args,**kwargs):
|
|
super(StaticGcc,self).__init__(*args,**kwargs)
|
|
def lib(self, *names):
|
|
self.ldflags += ['-Wl,-Bstatic']
|
|
super(StaticGcc, self).lib(*names)
|
|
self.ldflags += ['-Wl,-Bdynamic']
|
|
def copy(self):
|
|
return StaticGcc(self.cflags, self.ldflags, self.cc, self.builddir, self.usepkgconfig)
|
|
|
|
def static(builddir='fbuild/static', **kwargs):
|
|
b = compiler(builddir=builddir, usepkgconfig=False, **kwargs)
|
|
b.incpath('engine/apbuild')
|
|
b.incpath('deps/incs')
|
|
b.libpath('deps/libs')
|
|
b.cflags += [
|
|
'-fno-stack-protector', # disable stack protection to avoid dependency on
|
|
#__stack_chk_fail@@GLIBC_2.4
|
|
'-U_FORTIFY_SOURCE', # requires glibc 2.3.4 or higher
|
|
'-include', 'engine/apbuild/apsymbols.h',
|
|
]
|
|
b.ldflags += [
|
|
'-Wl,--as-needed', # must be set after all object files or the binary breaks
|
|
'-Wl,--hash-style=both', # By default FC6 only generates a .gnu.hash section
|
|
# Do all main distros support .gnu.hash now ?
|
|
'-static-libgcc',
|
|
'-Wl,-Bstatic', '-lstdc++', '-Wl,-Bdynamic',
|
|
'-Wl,-s'
|
|
]
|
|
p = os.popen(b.cc + ' -print-file-name=libstdc++.a')
|
|
stdcxx = p.read().strip()
|
|
run('ln', '-sf', stdcxx)
|
|
|
|
mkdir(builddir)
|
|
detectAlwaysDynamic(b)
|
|
b = StaticGcc(b.cflags,b.ldflags, b.cc,b.builddir, usepkgconfig=False)
|
|
detectEmbedable(b)
|
|
b.optimize()
|
|
b.libpath('.')
|
|
make(b)
|
|
|
|
def mingw(builddir='fbuild/mingw', cc='i486-mingw32-g++', **kwargs):
|
|
b = compiler(builddir=builddir, cc=cc, usepkgconfig=False, **kwargs)
|
|
b.define('USE_WIN32')
|
|
b.incpath('mingwdeps/include')
|
|
b.libpath('mingwdeps/lib')
|
|
b.lib('mingw32', 'SDLmain', 'wsock32', 'ws2_32')
|
|
b.ldflags += ['-mwindows']
|
|
mkdir(builddir)
|
|
detect(b)
|
|
b.cflags += ['-UHAVE_STRCASESTR','-UHAVE_STRNLEN']
|
|
make(b)
|
|
|
|
def clean():
|
|
autoclean()
|
|
|
|
# Keep this equivalent to languages/genpot.sh.
|
|
def pot():
|
|
luas = find('.', '*.lua')
|
|
luas.sort()
|
|
run('xgettext','-d','bos','--from-code', 'utf-8', '-k_','-o','languages/bos.pot', luas)
|
|
s = sorted(sources)
|
|
run('xgettext','-d','engine','-C','-k_','--add-comments=TRANSLATORS','-o','languages/engine.pot', s)
|
|
|
|
def all(**kwargs):
|
|
release(**kwargs)
|
|
debug(**kwargs)
|
|
|
|
def default(**kwargs):
|
|
release(**kwargs)
|
|
|
|
|
|
def copyfiles(files, destdir):
|
|
for f in files:
|
|
dirpath = destdir + '/' + os.path.dirname(f)
|
|
os.makedirs(dirpath, exist_ok=True)
|
|
shutil.copy2(f, dirpath)
|
|
|
|
def install_data(datadir='./dist'):
|
|
copyfiles(glob.glob('*.txt'), datadir)
|
|
copyfiles(glob.glob('CHANGELOG*'), datadir)
|
|
copyfiles(glob.glob('campaigns/*/*.*'), datadir)
|
|
copyfiles(glob.glob('campaigns/*/*/*.*'), datadir)
|
|
copyfiles(glob.glob('doc/*.txt'), datadir)
|
|
copyfiles(glob.glob('doc/*.html'), datadir)
|
|
copyfiles(glob.glob('graphics/*/*.png'), datadir)
|
|
copyfiles(glob.glob('graphics/*/*/*.png'), datadir)
|
|
copyfiles(glob.glob('intro/*.*'), datadir)
|
|
copyfiles(glob.glob('languages/*.po'), datadir)
|
|
copyfiles(glob.glob('maps/*.map/*.*'), datadir)
|
|
copyfiles(glob.glob('maps/campaigns/*/*.map/*.*'), datadir)
|
|
copyfiles(glob.glob('patches/*.lua'), datadir)
|
|
copyfiles(glob.glob('patches/*.png'), datadir)
|
|
copyfiles(glob.glob('patches/*/*.png'), datadir)
|
|
copyfiles(glob.glob('scripts/*.lua'), datadir)
|
|
copyfiles(glob.glob('scripts/*/*.lua'), datadir)
|
|
copyfiles(glob.glob('scripts/*/*/*.lua'), datadir)
|
|
copyfiles(glob.glob('intro/*.*'), datadir)
|
|
copyfiles(glob.glob('sounds/*.*'), datadir)
|
|
copyfiles(glob.glob('sounds/ui/*.*'), datadir)
|
|
copyfiles(glob.glob('units/*/*.*'), datadir)
|
|
|
|
|
|
def install(installdir='./dist', datadir=None, bindir=None, builddir='fbuild/release/', **kwargs):
|
|
datadir = datadir or installdir
|
|
bindir = bindir or installdir
|
|
release(builddir=builddir, **kwargs)
|
|
install_data(installdatadir)
|
|
shutil.copy2(builddir+'/'+ 'boswars', installdir)
|
|
|
|
|
|
def manpage(builddir='fbuild/release/', **kwargs):
|
|
release(builddir=builddir, **kwargs)
|
|
run('help2man', '--help-option=-h', '--section=6', '-N',
|
|
'--version-option=-v',
|
|
builddir + '/boswars', '-o', 'boswars.6')
|
|
|
|
|
|
setup(default='default')
|
|
if __name__ == '__main__':
|
|
import optparse
|
|
j = optparse.Option('-j', '--jobs', action='store',
|
|
help='the number of jobs to run simultaneously',
|
|
default=1)
|
|
main(extra_options=[j])
|
|
|