Files
oboe/SConstruct
Nat Quayle Nelson 945c9d3105 check for zlib consistently with other lib checks
For platforms where zlib isn't installed by default, this change
ensures that zlib gets bundled in the output
2024-09-10 22:18:23 -04:00

535 lines
18 KiB
Python

import os.path as path
import os
import subprocess
import atexit
# Build options
opts = Variables(None, ARGUMENTS)
opts.Add(EnumVariable('OS', "Target platform", str(Platform()), ('darwin', 'win32', 'posix')))
opts.Add('toolset', "Toolset to pass to the SCons builder", 'default')
opts.Add(BoolVariable('debug', "Build with debug symbols and no optimization", False))
opts.Add(EnumVariable('bits', "Build for 32-bit or 64-bit architectures", '64', ('32', '64')))
# Partial build flags -- by default, all targets will be built,
# but if at least one is specified, ONLY the specified targets will be built
partial_options = ('true', 'false', 'default')
opts.Add(EnumVariable('game', 'Build the game', 'default', partial_options))
opts.Add(EnumVariable('pcedit', 'Build the character editor', 'default', partial_options))
opts.Add(EnumVariable('scenedit', 'Build the scenario editor', 'default', partial_options))
opts.Add(EnumVariable('test', 'Build the tests', 'default', partial_options))
# Package build flag -- when explicitly specified, Mac and Windows builds will also
# try to build an installer.
opts.Add(BoolVariable('package', "Build an installer", False))
# Compiler configuration
opts.Add("CXX", "C++ compiler")
opts.Add("CC", "C compiler")
opts.Add("LINK", "Linker")
opts.Add("CCFLAGS", "Custom flags for both the C and C++ compilers")
opts.Add("CXXFLAGS", "Custom flags for the C++ compiler")
opts.Add("CFLAGS", "Custom flags for the C compiler")
opts.Add("LINKFLAGS", "Custom flags for the linker")
# Initialize environment with options and full user environment
env = Environment(variables=opts, ENV=os.environ)
Help(opts.GenerateHelpText(env))
platform = env['OS']
toolset = env['toolset']
arch = 'x86_64' if (env['bits'] == '64') else 'x86'
arch_short = '64' if (env['bits'] == '64') else '86'
# Some kinda gnarly logic required to figure out which targets to build
possible_targets = ['game', 'pcedit', 'scenedit', 'test']
# First, eliminate any which are specified NOT to build
targets = [target for target in possible_targets if env[target] != 'false']
# Then, we will assume the remaining targets should all build by default, UNLESS one
# or more targets are specified TO build.
any_specified_targets=False
for target in targets:
if env[target] == 'true':
any_specified_targets = True
if any_specified_targets:
targets = [target for target in possible_targets if env[target] != 'default']
# Update env based on options
env.Replace(TARGET_ARCH=arch)
env.Replace(tools=[toolset])
# Check for platform support
if platform not in ("darwin", "win32", "posix"):
print("Sorry, your platform is not supported.")
print("Platform is:", platform)
print("Specify OS=<your-platform> if you believe this is incorrect.")
print("(Supported platforms are: darwin, win32, posix)")
Exit(1)
print('Building for:', platform)
print('Using toolchain:', toolset)
print('C++ compiler:', env['CXX'])
env.VariantDir('#build/obj', 'src')
env.VariantDir('#build/obj/test', 'test')
env.VariantDir('#build/obj/test/deps', 'deps')
if env['debug']:
if platform in ['posix', 'darwin']:
env.Append(CCFLAGS=['-g','-O0'])
elif platform == 'win32':
env.Append(CCFLAGS=['/Zi', '/Od'])
# This command generates the header with git revision information
def gen_gitrev(env, target, source):
revid = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True);
fulltag = subprocess.check_output(["git", "tag", "--sort=v:refname"], text=True).split('\n')[-1]
tagrev = subprocess.check_output(["git", "rev-parse", fulltag], text=True) if fulltag else ""
with open(target[0].path, 'w') as gitrev_hpp:
print(file=gitrev_hpp)
print('#define GIT_REVISION "' + revid[0:7] + '"', file=gitrev_hpp)
print('#define GIT_TAG "' + fulltag + '"', file=gitrev_hpp)
print('#define GIT_TAG_REVISION "' + tagrev[0:7] + '"', file=gitrev_hpp)
print(file=gitrev_hpp)
if path.exists(".git"):
git_refs = ['.git/HEAD']
with open('.git/HEAD') as git_head:
git_head = git_head.read().split()
if git_head[0] == 'ref:':
git_refs.append(path.join('.git', git_head[1]))
env.Command('src/tools/gitrev.hpp', git_refs, gen_gitrev)
else:
# Zipped source downloads from github do not include the repo (probably a good thing)
# TODO: This does not work on Windows
env.Command('src/tools/gitrev.hpp', '', r"""
echo -e "\n#define GIT_REVISION \"\"\n#define GIT_TAG \"\"\n#define GIT_TAG_REVISION \"\"\n" > #TARGET
""")
if platform == "darwin":
env.Append(CXXFLAGS=["-std=c++11","-stdlib=libc++","-include","global.hpp"], RPATH='../Frameworks')
env["CC"] = 'clang'
env["CXX"] = 'clang++'
env.Append(BUILDERS={
'Nib': Builder(
action = "ibtool --compile $TARGET $SOURCE",
suffix = ".nib",
src_suffix = ".xib"
)
}, LIBPATH=Split("""
/usr/lib
/usr/local/lib
"""), CPPPATH=Split("""
/usr/include
/usr/local/include
"""), FRAMEWORKPATH=Split("""
{SDKROOT}/System/Library/Frameworks
{SDKROOT}/Library/Frameworks
{HOME}/Library/Frameworks
""".format(HOME = os.environ['HOME'], SDKROOT = os.environ['SDKROOT'])))
def build_app_package(env, source, build_dir, info):
source_name = source[0].name
pkg_path = path.join(build_dir, "%s.app/Contents/" % source_name)
nib = env.Nib(info['nib'].replace('rsrc', 'build/obj'), info['nib'])
icons = [path.join("#rsrc/icons/mac/", x + ".icns") for x in Split(info['icons'])]
env.Install(path.join(pkg_path, "MacOS"), source)
env.Install(path.join(pkg_path, "Resources"), icons + nib)
env.InstallAs(path.join(pkg_path, "Info.plist"), info['plist'])
env.Command(path.join(pkg_path, 'PkgInfo'), '', action="echo 'APPL%s' > $TARGET" % info['creator'])
def change_install_name(lib, source, target):
subprocess.call(["install_name_tool", "-change", source, target, lib])
system_prefixes = ('/System', '/usr/lib')
def is_user_lib(lib):
return all(not lib.startswith(x) for x in system_prefixes)
def get_deps_for(source):
deps = subprocess.check_output(['otool', '-L', source]).splitlines()[1:]
deps = map(lambda s: s.decode('utf-8'), deps)
deps = list(map(str.strip, deps))
deps = list(filter(is_user_lib, deps))
deps = [x.split()[0] for x in deps]
return deps
def check_deps(source):
direct_deps = get_deps_for(source)
deps = set()
for i in range(len(direct_deps)):
dep = direct_deps[i]
if dep.startswith('@rpath/'):
direct_deps[i] = dep = dep.split('/', 1)[1]
if dep.startswith('../Frameworks/'):
direct_deps[i] = dep = dep.split('/', 2)[2]
else:
pass#change_install_name(dep, path.join('@rpath', dep), source)
if dep == source:
continue
deps.add(dep)
return deps
def split_path(lib):
while '.' not in path.basename(lib):
lib = path.dirname(lib)
return path.dirname(lib), path.basename(lib)
def bundle_libraries_for(target, source, env):
if not path.exists(target[0].path):
Execute(Mkdir(target))
deps = check_deps(source[0].path)
for dep in deps:
if dep.endswith('.framework'):
paths = Split(env["FRAMEWORKPATH"]) + env["LIBPATH"]
else:
paths = env["LIBPATH"]
for search_path in paths:
check_path = path.join(search_path, dep)
if path.exists(check_path):
check_path = path.realpath(check_path)
src_dir, basefile = split_path(check_path)
src_path = path.join(src_dir, basefile)
dest_path = path.join(target[0].path, basefile)
if path.exists(dest_path):
break
# Copying .frameworks needs to preserve symlinks by using cp -a.
# SCons provides Copy(symlinks=True) but that doesn't seem to work.
try:
print(subprocess.check_output(['cp', '-av', src_path, dest_path], text=True))
except:
print('cp -av failed')
Exit(1)
# Recursively bundle the dependencies of each dependency:
bundle_libraries_for(target, [File(check_path)], env)
break
elif platform == "win32":
if 'msvc' in env['TOOLS']:
vcpkg_prefix = path.join((os.environ['HOME'] if 'HOME' in os.environ else 'C:\\'), 'vcpkg')
vcpkg_installed = path.join(vcpkg_prefix, 'installed', f'x{arch_short}-windows')
vcpkg_other_packages = Glob(path.join(vcpkg_prefix, 'packages', f'**x{arch_short}-windows'))
vcpkg_other_includes = [path.join(d.get_abspath(), 'include') for d in vcpkg_other_packages]
vcpkg_other_includes = list(filter(path.exists, vcpkg_other_includes))
vcpkg_other_libs = [path.join(d.get_abspath(), 'lib') for d in vcpkg_other_packages]
vcpkg_other_libs = list(filter(path.exists, vcpkg_other_libs))
vcpkg_other_bins = [path.join(d.get_abspath(), 'bin') for d in vcpkg_other_packages]
vcpkg_other_bins = list(filter(path.exists, vcpkg_other_bins))
include_paths=[path.join(vcpkg_installed, 'include')] + vcpkg_other_includes
env.Append(
LINKFLAGS=['/SUBSYSTEM:WINDOWS','/ENTRY:mainCRTStartup',f'/MACHINE:X{arch_short}'],
CXXFLAGS=['/EHsc','/MD','/FIglobal.hpp'],
CPPPATH=include_paths,
LIBPATH=[path.join(vcpkg_installed, 'lib')] + vcpkg_other_libs + vcpkg_other_bins,
LIBS=Split("""
kernel32
user32
gdi32
winspool
comdlg32
advapi32
shell32
ole32
oleaut32
uuid
odbc32
odbccp32
""")
)
else:
env.Append(CXXFLAGS=["-include","global.hpp"])
def build_app_package(env, source, build_dir, info):
env.Install(build_dir, source)
elif platform == "posix":
env.Append(CXXFLAGS=["-std=c++14","-include","global.hpp"])
def build_app_package(env, source, build_dir, info):
env.Install(build_dir, source)
env.AddMethod(build_app_package, "Package")
# Allow user to specify additional library/include paths
env.Append(
LIBPATH=ARGUMENTS.get('LIBPATH', '').split(path.pathsep),
CPPPATH=ARGUMENTS.get('INCLUDEPATH', '').split(path.pathsep)
)
if platform == 'darwin':
env.Append(FRAMEWORKPATH=ARGUMENTS.get('FRAMEWORKPATH', '').split(path.pathsep))
# If any package managers are installed, add their dirs too.
if subprocess.call(['which', '-s', 'port']) == 0: # MacPorts
env.Append(
LIBPATH=['/opt/local/lib'],
CPPPATH=['/opt/local/include'],
FRAMEWORKPATH=['/opt/local/Library/Frameworks']
)
if subprocess.call(['which', '-s', 'fink']) == 0: # Fink
env.Append(
LIBPATH=['/sw/lib'],
CPPPATH=['/sw/include']
)
# pretty sketchy, but should point to your brew installation directories
if subprocess.call(['which', '-s', 'brew']) == 0: # HomeBrew
possible_brew_dirs = ['/usr/local', '/opt/homebrew']
brew_lib_versions = {
'boost': '1.85.0',
'sfml': '2.6.1'
}
for dir in possible_brew_dirs:
if path.exists(f'{dir}/Cellar'):
for lib, version in brew_lib_versions.items():
env.Append(
LIBPATH=[f'{dir}/Cellar/{lib}/{version}/lib'],
CPPPATH=[f'{dir}/Cellar/{lib}/{version}/include'])
# Sometimes it's easier just to copy the dependencies into the repo dir
# We try to auto-detect this.
if path.exists('deps/lib'):
env.Append(LIBPATH=[path.join(os.getcwd(), 'deps/lib')])
if platform == 'darwin':
env.Append(FRAMEWORKPATH=[path.join(os.getcwd(), 'deps/lib')])
if path.exists('deps/lib64'):
env.Append(LIBPATH=[path.join(os.getcwd(), 'deps/lib64')])
if path.exists('deps/include'):
env.Append(CPPPATH=[path.join(os.getcwd(), 'deps/include')])
# Include directories
env.Append(CPPPATH=Split("""
#src/
#src/fileio/gzstream/
#src/fileio/xml-parser/
"""))
env['CONFIGUREDIR'] = '#build/conf'
env['CONFIGURELOG'] = '#build/conf/config.log'
bundled_libs = []
if not env.GetOption('clean'):
conf = Configure(env)
if not conf.CheckCC() or not conf.CheckCXX():
print("There's a problem with your compiler!")
Exit(1)
def check_lib(lib, disp, suffixes=[], versions=[], msvc_versions=[]):
if "mingw" in env["TOOLS"] and lib.startswith("sfml"):
lib = "lib" + lib
possible_names = [lib]
if platform == "win32":
if 'msvc' in env['TOOLS']:
msvc_version = env['MSVC_VERSION'].replace('.','')
vc_suffix = '-vc' + msvc_version
possible_names.append(lib + vc_suffix)
n = len(possible_names)
for i in range(n):
for suff in suffixes:
possible_names.append(possible_names[i] + suff)
for test in possible_names:
if conf.CheckLib(test, language='C++'):
bundled_libs.append(test)
return # Success!
for ver in versions:
if conf.CheckLib(test + ver, language='C++'):
bundled_libs.append(test + ver)
return # Success!
print(disp, 'must be installed!')
print(" If you're sure it's installed, try passing LIBPATH=...")
Exit(1)
def check_header(header, disp):
if not conf.CheckCXXHeader(header, '<>'):
print(disp, 'must be installed!')
print(" If you're sure it's installed, try passing INCLUDEPATH=...")
Exit(1)
boost_versions = ['-1_84'] # This is a bit of a hack. :(
suffixes = ['-mt', f'-mt-x{env["bits"]}']
zlib = 'zlib' if (platform == "win32" and 'mingw' not in env["TOOLS"]) else 'z'
check_lib(zlib, 'zlib', [], [])
check_header('boost/lexical_cast.hpp', 'Boost.LexicalCast')
check_header('boost/optional.hpp', 'Boost.Optional')
check_header('boost/ptr_container/ptr_container.hpp', 'Boost.PointerContainer')
check_header('boost/any.hpp', 'Boost.Any')
check_header('boost/math_fwd.hpp', 'Boost.Math')
check_header('boost/spirit/include/classic.hpp', 'Boost.Spirit.Classic')
check_lib('boost_system', 'Boost.System', suffixes, boost_versions)
check_lib('boost_filesystem', 'Boost.Filesystem', suffixes, boost_versions)
check_lib('sfml-system', 'SFML-system')
check_lib('sfml-window', 'SFML-window')
check_lib('sfml-audio', 'SFML-audio')
check_lib('sfml-graphics', 'SFML-graphics')
# Make sure Catch2 is cloned
if not path.exists('deps/Catch2/README.md'):
subprocess.call(["git", "submodule", "update", "--init", "deps/Catch2"])
# We also use the Clara bundled with Catch in the main program
env.Append(CPPPATH=[path.join(os.getcwd(), 'deps/Catch2/include/external')])
# Make sure cppcodec is cloned
if not path.exists('deps/cppcodec/README.md'):
subprocess.call(["git", "submodule", "update", "--init", "deps/cppcodec"])
env.Append(CPPPATH=[path.join(os.getcwd(), 'deps/cppcodec')])
# On Linux, build TGUI from the subtree if necessary
if platform == 'posix':
def check_tgui(conf, second_attempt=False):
if conf.CheckLib('libtgui', language='C++'):
return conf
else:
if second_attempt:
print('TGUI is missing, even after trying to build it!')
Exit(1)
else:
subprocess.call(["git", "submodule", "update", "--init", "deps/TGUI"])
subprocess.call(["cmake", "-D", "TGUI_CXX_STANDARD=14", "-D", "CMAKE_INSTALL_PREFIX=../", "."], cwd="deps/TGUI")
subprocess.call(["make"], cwd="deps/TGUI")
subprocess.call(["make", "install"], cwd="deps/TGUI")
env = conf.Finish()
env.Append(CPPPATH=[path.join(os.getcwd(), 'deps/include')], LIBPATH=[path.join(os.getcwd(), 'deps/lib'), path.join(os.getcwd(), 'deps/lib64')])
conf = Configure(env)
return check_tgui(conf, True)
conf = check_tgui(conf)
env = conf.Finish()
env.Append(CPPDEFINES=["TIXML_USE_TICPP"])
project_includes = []
for (root, dirs, files) in os.walk('src'):
project_includes.append(path.join(os.getcwd(), root))
env.Append(CPPPATH=project_includes)
if platform == "win32":
# For the *resource.h headers
env.Append(CPPPATH=["#rsrc/menus"])
# Icons
env.Append(CPPPATH=["#rsrc/icons/win"])
if platform == "darwin":
env.Append(LIBS=Split("""
objc
c++
"""))
env.Append(FRAMEWORKS=Split("""
OpenGL
Cocoa
"""))
elif platform == "win32":
env.Append(LIBS=Split("""
opengl32
"""))
elif platform == "posix":
env.Append(LIBS=Split("""
GL
X11
tgui
"""))
Export("env platform")
# The VariantDir directives near the top mean that the SConscript files are
# copied from src/ and test/ into build/obj/ and build/obj/test respectively.
# Thus, any edits to them should be made to the originals in src/ or test/.
# However, when referencing them we have to reference the copies.
# Gather common sources
tools = SConscript("build/obj/tools/SConscript")
dlog_util = SConscript("build/obj/dialogxml/SConscript")
common_sources = dlog_util + tools
install_dir = "#build/Blades of Exile"
party_classes = Glob("#src/universe/*.cpp")
Export("install_dir party_classes common_sources")
# Programs
SConscript([f"build/obj/{target}/SConscript" for target in targets])
# Data files
data_dir = path.join(install_dir, "data")
Export("data_dir")
SConscript(["rsrc/SConscript", "doc/SConscript"])
# Bundle required frameworks and libraries
if platform == "darwin":
app_targets = []
if 'game' in targets:
app_targets.append("Blades of Exile")
if 'pcedit' in targets:
app_targets.append("BoE Character Editor")
if 'scenedit' in targets:
app_targets.append("BoE Scenario Editor")
for targ in app_targets:
target_dir = path.join(install_dir, targ + '.app', 'Contents/Frameworks')
binary = path.join(install_dir, targ + '.app', 'Contents/MacOS', targ)
env.Command(Dir(target_dir), binary, [Delete(target_dir), bundle_libraries_for])
elif platform == "win32":
bundled_libs += Split("""
libsndfile-1
openal32
sfml-audio-2
sfml-graphics-2
sfml-system-2
sfml-window-2
zlib1
freetype
vorbis
vorbisfile
vorbisenc
ogg
FLAC
bz2
brotlidec
brotlicommon
""")
target_dirs = ["#build/Blades of Exile", "#build/test"]
for lib in bundled_libs:
for lpath in env['LIBPATH']:
src_file = path.join(lpath, lib + ".dll")
if path.exists(src_file):
for targ in target_dirs:
env.Install(targ, src_file)
break
elif 'lib' in lpath:
src_file = path.join(lpath.replace('lib', 'bin'), lib + ".dll")
if path.exists(src_file):
for targ in target_dirs:
env.Install(targ, src_file)
break
# Extra: Microsoft redistributable libraries installer
if 'msvc' in env["TOOLS"]:
if path.exists("deps/VCRedistInstall.exe"):
env.Install("build/Blades of Exile/", "dep/VCRedistInstall.exe")
else:
print("WARNING: Cannot find installer for the MSVC redistributable libraries for your version of Visual Studio.")
print("Please download it from Microsoft's website and place it at:")
print(" deps/VCRedistInstall.exe")
# Create it so its lack doesn't cause makensis to break
# (Because the installer is an optional component.)
os.makedirs("build/Blades of Exile", exist_ok=True)
open("build/Blades of Exile/VCRedistInstall.exe", 'w').close()
if env["package"]:
if platform == "darwin":
env.VariantDir("#build/pkg", "pkg/mac")
SConscript("build/pkg/SConscript")
elif platform == "win32":
if subprocess.call(['where', '/Q', 'makensis']) == 0:
env.VariantDir("#build/pkg", "pkg/win")
SConscript("build/pkg/SConscript")
else:
print('NSIS must be installed to generate an installer for Windows.')
Exit(1)
# Cleanup
env.Clean('.', 'build')
env.Clean('.', Glob('.sconsign.*'))
if env.GetOption('clean'):
atexit.register(lambda: print('If the build fails immediately after cleaning, delete .sconsign.dblite manually and try again.'))