meson: untested macOS bundle generation, bunch of other crap (WIP)

This commit is contained in:
Andrei Alexeyev 2017-11-27 15:27:32 +02:00 committed by Martin Herkt
parent 29acd5f58a
commit 0dcbc6bcf4
11 changed files with 323 additions and 168 deletions

3
.gitignore vendored
View file

@ -3,6 +3,5 @@ winbuild/
osxbuild/
.ctagsdb
.*DS_Store
# autogenerated
__pycache__
*.out

View file

@ -13,9 +13,18 @@ project('taisei', 'c',
]
)
ver_fb = get_option('version_fallback').strip()
version_fallback = ver_fb != '' ? ver_fb : meson.project_version()
version_script = find_program('scripts/version.py')
version_command = [version_script, '--rootdir', meson.source_root(), '--fallback', version_fallback]
taisei_version_string = run_command(version_command, '{string}').stdout().strip()
config = configuration_data()
cc = meson.get_compiler('c')
python3 = import('python3').find_python()
taisei_c_warnargs = []
taisei_c_args = []
@ -86,20 +95,44 @@ if not (have_vla and have_complex)
error('Your C implementation needs to support complex numbers and variable-length arrays.')
endif
macos_app_bundle = get_option('macos_bundle') and host_machine.system() == 'darwin'
if get_option('install_relative') == 'auto'
config.set('TAISEI_BUILDCONF_RELATIVE_DATA_PATH', host_machine.system() == 'windows' or host_machine.system() == 'darwin')
if macos_app_bundle
bundle_dir = 'Taisei.app'
datadir = join_paths(bundle_dir, 'Contents', 'Resources')
bindir = join_paths(bundle_dir, 'Contents', 'MacOS')
config.set('TAISEI_BUILDCONF_RELATIVE_DATA_PATH', true)
# arguments must be strings...
meson.add_install_script(
python3.path(),
join_paths(meson.source_root(), 'scripts', 'macos-install-dylibs.py'),
get_option('macos_lib_path'),
get_option('macos_tool_path'),
get_option('macos_tool_prefix'),
)
else
config.set('TAISEI_BUILDCONF_RELATIVE_DATA_PATH', get_option('install_relative') == 'true')
datadir = get_option('datadir')
bindir = get_option('bindir')
if get_option('install_relative') == 'auto'
config.set('TAISEI_BUILDCONF_RELATIVE_DATA_PATH', host_machine.system() == 'windows')
else
config.set('TAISEI_BUILDCONF_RELATIVE_DATA_PATH', get_option('install_relative') == 'true')
endif
endif
if config.get('TAISEI_BUILDCONF_RELATIVE_DATA_PATH')
if host_machine.system() == 'darwin'
data_path = join_paths(get_option('datadir'), 'data')
config.set('TAISEI_BUILDCONF_DATA_PATH', '"@0@"'.format(join_paths('../..', data_path)))
else
data_path = 'data'
data_path = 'data'
doc_path = '.'
if macos_app_bundle
# This is relative to SDL_GetBasePath(), which will return '/path/to/Taisei.app/Contents/Resources' for a macOS bundle.
config.set_quoted('TAISEI_BUILDCONF_DATA_PATH', data_path)
# Actual installation path
data_path = join_paths(datadir, data_path)
else
# HACK
rel_dpath = []
foreach p : get_option('bindir').split('/')
@ -109,18 +142,14 @@ if config.get('TAISEI_BUILDCONF_RELATIVE_DATA_PATH')
endforeach
rel_dpath += data_path
config.set('TAISEI_BUILDCONF_DATA_PATH', '"@0@"'.format(join_paths(rel_dpath)))
config.set_quoted('TAISEI_BUILDCONF_DATA_PATH', format(join_paths(rel_dpath)))
endif
doc_path = ''
else
data_path = join_paths(get_option('datadir'), 'taisei')
config.set('TAISEI_BUILDCONF_DATA_PATH', '"@0@"'.format(join_paths(get_option('prefix'), data_path)))
doc_path = join_paths(get_option('datadir'), 'doc', 'taisei')
data_path = join_paths(datadir, 'taisei')
config.set_quoted('TAISEI_BUILDCONF_DATA_PATH', join_paths(get_option('prefix'), data_path))
doc_path = join_paths(datadir, 'doc', 'taisei')
endif
systype = (have_posix ? 'POSIX (@0@)' : '@0@').format(host_machine.system())
if get_option('buildtype').startswith('debug')
config.set('TAISEI_BUILDCONF_DEBUG', true)
if have_backtrace
@ -129,7 +158,7 @@ if get_option('buildtype').startswith('debug')
endif
if host_machine.system() == 'windows' or host_machine.system() == 'darwin'
config.set('TAISEI_BUILDCONF_LOG_FATAL_MSGBOX')
config.set('TAISEI_BUILDCONF_LOG_FATAL_MSGBOX', true)
endif
if host_machine.system() == 'windows'
@ -141,9 +170,14 @@ else
install_data('COPYING', install_dir : doc_path)
endif
systype = (have_posix ? 'POSIX (@0@)' : '@0@').format(host_machine.system())
systype = '@0@, @1@, @2@'.format(systype, host_machine.cpu_family(), host_machine.cpu())
summary = '''
Summary:
Taisei version: @9@
System type: @0@
Audio enabled: @1@
Package data: @2@
@ -161,10 +195,11 @@ Summary:
taisei_deps.contains(dep_zip),
config.get('TAISEI_BUILDCONF_RELATIVE_DATA_PATH'),
get_option('prefix'),
get_option('bindir'),
bindir,
data_path,
doc_path,
get_option('buildtype')
get_option('buildtype'),
taisei_version_string
)
subdir('src')
@ -174,3 +209,11 @@ subdir('doc')
subdir('xdg')
message(summary)
if macos_app_bundle
dmg_command = find_program('scripts/macos-gen-dmg.sh')
dmg_filename = 'Taisei-@0@-macOS.dmg'.format(taisei_version_string)
dmg_target = run_target('dmg',
command: [dmg_command, get_option('prefix'), join_paths(meson.build_root(), dmg_filename)],
)
endif

View file

@ -1,7 +1,12 @@
option('version_fallback', type : 'string', description : 'Overrides the version string when not building in a git repository')
option('enable_audio', type : 'combo', choices : ['auto', 'true', 'false'], description : 'Enable audio support (needs SDL2_mixer)')
option('package_data', type : 'combo', choices : ['auto', 'true', 'false'], description : 'Package the games assets into a compressed archive instead of bundling plain files (needs libzip)')
option('install_relative', type : 'combo', choices : ['auto', 'true', 'false'], description : 'Use only relative paths to the executable and install everything in the same directory')
option('install_relative', type : 'combo', choices : ['auto', 'true', 'false'], description : 'Use only relative paths to the executable and install everything in the same directory. Always enabled for macOS bundles')
option('win_console', type : 'boolean', value : false, description : 'Use the console subsystem on Windows')
option('static', type : 'boolean', value : false, description : 'Build statically linked executable')
option('intel_intrin', type : 'boolean', value : true, description : 'Use some x86-specific intrinsics for optimizations where appropriate (if possible). Note that this is not equivalent to e.g. supplying -march in CFLAGS')
option('macos_bundle', type : 'boolean', value : true, description : 'Make a macOS application bundle on install (ignored on other platforms)')
option('macos_lib_path', type : 'string', description : 'List of paths (separated like the PATH environment variable) from where required runtime libraries will be copied into the bundle (useful for cross-compiling)')
option('macos_tool_path', type : 'string', description : 'List of paths (separated like the PATH environment variable) from where macOS-specific utilities (such as otool and install_name_tool) can be found. This is prepended to PATH (useful for cross-compiling)')
option('macos_tool_prefix', type : 'string', description : 'Names of macOS-specific tools are prefixed with this string (useful for cross-compiling)')

28
misc/Info.plist.in Normal file
View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>Taisei</string>
<key>CFBundleIconFile</key>
<string>Taisei</string>
<key>CFBundleIdentifier</key>
<string>org.taisei-project.taisei</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Taisei</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${TAISEI_VERSION}</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>${TAISEI_VERSION_MAJOR}.${TAISEI_VERSION_MINOR}.${TAISEI_VERSION_PATCH}.${TAISEI_VERSION_TWEAK}</string>
<key>NSHumanReadableCopyright</key>
<string>© 2011-2017, Taisei Project</string>
</dict>
</plist>

View file

@ -1,5 +1,13 @@
install_data('gamecontrollerdb/gamecontrollerdb.txt', install_dir : data_path)
if host_machine.system() == 'darwin'
install_data('icons/Taisei.icns', install_dir : get_option('datadir'))
if macos_app_bundle
install_data('icons/Taisei.icns', install_dir : datadir)
version_deps += custom_target('macOS property list',
command : version_template_command,
build_always : true,
input : 'Info.plist.in',
output : 'Info.plist',
install : true,
install_dir : join_paths(bundle_dir, 'Contents'))
endif

View file

@ -8,6 +8,6 @@ if [[ -z "$IN" ]] || [[ -z "$OUT" ]] || [[ ! -d "$IN" ]]; then
exit 1
fi
ln -svf /Applications "$IN/Applications" || exit 1
ln -svf /Applications "$IN/Applications" || exit $?
genisoimage -V Taisei -D -R -apple -no-pad -o "$OUT" "$IN" || exit $?
rm -v "$IN/Applications"

84
scripts/macos-install-dylibs.py Executable file
View file

@ -0,0 +1,84 @@
#!/usr/bin/env python3
#
# Install dylib dependencies, strip debugging symbols, and deal with the shitfucked digital abortion that is the macOS dynamic linker.
#
import re
import sys
import subprocess
import itertools
import shutil
from os import environ, pathsep
from pathlib import Path
build_root = Path(environ['MESON_BUILD_ROOT'])
source_root = Path(environ['MESON_SOURCE_ROOT'])
install_prefix = Path(environ['MESON_INSTALL_DESTDIR_PREFIX'])
args = sys.argv[1:]
macos_lib_paths = filter(None, (Path(x) for x in args.pop(0).split(pathsep)))
macos_tool_paths = filter(None, (Path(x) for x in args.pop(0).split(pathsep)))
macos_tool_prefix = args.pop(0)
if macos_tool_paths:
environ['PATH'] = pathsep.join(itertools.chain((str(p) for p in macos_tool_paths), [environ['PATH']]))
exe_path = install_prefix / 'Taisei.app' / 'Contents' / 'MacOS' / 'Taisei'
dylib_dir_path = exe_path.parent / 'dylibs'
dylib_dir_path.mkdir(mode=0o755, parents=True, exist_ok=True)
def tool(name):
def func(*args):
cmd = [macos_tool_prefix + name] + list(args)
print(' '.join(('{0!r}' if ' ' in str(s) else '{0}').format(str(s)) for s in cmd))
return subprocess.check_output(cmd, universal_newlines=True)
return func
otool = tool('otool')
install_name_tool = tool('install_name_tool')
strip = tool('strip')
def fix_libs(opath):
handled = set()
regex = re.compile(r'\s*(.*?\.dylib) \(')
def install(src, dst):
src = str(src)
dst = str(dst)
print('Installing {0} as {1}'.format(src, dst))
shutil.copy(str(src), str(dst))
def fix(path):
for lib in regex.findall(otool('-L', path)):
if lib.startswith('/usr/lib') or lib.startswith('@'):
continue
src_lib_path = Path(lib)
dst_lib_path = dylib_dir_path / src_lib_path.name
install_name_tool(path, '-change', lib, '@executable_path/dylibs/{0}'.format(dst_lib_path.name))
if dst_lib_path in handled:
continue
install(src_lib_path, dst_lib_path)
handled.add(dst_lib_path)
fix(dst_lib_path)
install_name_tool(path, '-id', path.name)
strip('-S', path)
fix(opath)
return handled
new_files = fix_libs(exe_path)
if new_files:
with (build_root / 'meson-logs' / 'install-log.txt').open('a') as f:
f.write('# ...except when it does :^)\n')
for i in new_files:
f.write('{0}\n'.format(str(i)))

View file

@ -1,77 +0,0 @@
#!/usr/bin/env bash
if [[ "$1" != "--iknowwhatimdoing" ]]; then
>&2 echo "This script should not be ran directly."
exit 1
fi
if [[ -z "$BASH_VERSINFO" ]] || [[ "$BASH_VERSINFO" -lt 4 ]]; then
>&2 echo "Your Bash version is too old. 4.0+ is required."
exit 1
fi
shift
EXE_PATH="$1"; shift
DYLIB_PATH="$1"; shift
DYLIB_INTERMEDIATE_PATH="$1"; shift
DYLIB_REL_PATH="$1"; shift
OSX_LIB_PATH="$1"; shift
OSX_TOOL_PATH="$1"; shift
OSX_TOOL_PREFIX="$1"; shift
GENERATED_SCRIPT_PATH="$1"; shift
mkdir -p "$DYLIB_PATH" || exit 2
mkdir -p "$DYLIB_INTERMEDIATE_PATH" || exit 3
[[ -f "$EXE_PATH" ]] || exit 4
if [[ -n "$OSX_TOOL_PATH" ]]; then
export PATH="$OSX_TOOL_PATH:$PATH"
fi
otool="${OSX_TOOL_PREFIX}otool"
install_name_tool="${OSX_TOOL_PREFIX}install_name_tool"
$otool --version
rm -vf "$DYLIB_INTERMEDIATE_PATH"/*.dylib "$DYLIB_PATH"/*.dylib
declare -A handled_libs
function find_dylib {
for p in $(echo "$OSX_LIB_PATH" | tr ':' '\n'); do
if [[ -f "$p/$1" ]]; then
echo "$p/$1"
return 0
fi
done
return 1
}
function handle_dylibs {
$otool -L "$1" || exit 7
for lib in $($otool -L "$1" | sed -e '/:$/d' -e 's/[[:blank:]]*//' -e 's/[[:blank:]].*//' -e '/libSystem/d' -e '/.*\.dylib/!d' -e '/^\/usr\//d' | sort | uniq); do
libname="${lib##*/}"
libpath="$(find_dylib "$libname")"
if [[ -z "$libpath" ]]; then
>&2 "Library $libname not found in OSX_LIB_PATH (required by $1)"
fi
$install_name_tool "$1" -change "$lib" "@executable_path/$DYLIB_REL_PATH/$libname" || exit 5
if [[ -n "${handled_libs[$libname]}" ]]; then
continue
fi
cp -v "$libpath" "$DYLIB_INTERMEDIATE_PATH/$libname" || exit 6
handled_libs[$libname]=1
handle_dylibs "$DYLIB_INTERMEDIATE_PATH/$libname"
done
}
handle_dylibs "$EXE_PATH"

50
scripts/version-template.py Executable file
View file

@ -0,0 +1,50 @@
#!/usr/bin/env python3
import os
import sys
import version
assert(len(sys.argv) == 6)
root = sys.argv[1]
inpath = sys.argv[2]
outpath = sys.argv[3]
buildtype = sys.argv[4].strip()
v_fallback = sys.argv[5].strip()
version = version.get(fallback=v_fallback, rootdir=root)
if buildtype.lower().startswith("debug"):
buildtype_def = "#define DEBUG_BUILD"
else:
buildtype_def = "#define RELEASE_BUILD"
version = [("${TAISEI_VERSION_MAJOR}", version.major),
("${TAISEI_VERSION_MINOR}", version.minor),
("${TAISEI_VERSION_PATCH}", version.patch),
("${TAISEI_VERSION_TWEAK}", version.tweak),
("${TAISEI_VERSION}", version.string),
("${TAISEI_VERSION_FULL_STR}", version.full_string),
("${MESON_BUILD_TYPE}", buildtype),
("${ICONS_DIR}", os.path.join(root, "misc", "icons")),
("${BUILDTYPE_DEFINE}", buildtype_def)]
with open(inpath, "r") as infile:
template = infile.read()
for k, v in version:
template = template.replace(k, str(v))
try:
with open(outpath, "r+t") as outfile:
contents = outfile.read()
if contents == template:
exit(0)
outfile.seek(0)
outfile.write(template)
outfile.truncate()
except FileNotFoundError:
with open(outpath, "w") as outfile:
outfile.write(template)

View file

@ -1,64 +1,71 @@
#!/usr/bin/env python3
import os
import sys
from subprocess import check_output
from collections import OrderedDict
assert(len(sys.argv) == 6)
class VerionFormatError(RuntimeError):
pass
root = sys.argv[1]
inpath = sys.argv[2]
outpath = sys.argv[3]
buildtype = sys.argv[4].strip()
v_fallback = sys.argv[5].strip()
try:
git = check_output(['git', 'describe', '--tags', '--match', 'v[0-9]*[!asz]'], cwd=root, universal_newlines=True)
version_str = git.strip()
except:
print("Warning: git not found or not a git repository; using fallback version {0}".format(v_fallback), file=sys.stderr)
version_str = v_fallback
class Version(object):
def __init__(self, version_str):
version_str = version_str.lstrip("v")
vparts = version_str.split("-")[0].split(".")
vextra = version_str.replace("+", "-").split("-")
version_str = version_str.lstrip("v")
vparts = version_str.split("-")[0].split(".")
vextra = version_str.replace("+", "-").split("-")
if len(vextra) > 1:
vextra = vextra[1]
else:
vextra = "0"
if len(vextra) > 1:
vextra = vextra[1]
else:
vextra = "0"
if len(vparts) > 3:
print("Error: Too many dot-separated elements in version string '{0}'. Please use the following format: [v]major[.minor[.patch]][-tweak[-extrainfo]]".format(version_str), file=sys.stderr)
sys.exit(1)
elif len(vparts[0]) == 0 or not vextra.isnumeric():
print("Error: Invalid version string '{0}'. Please use the following format: [v]major[.minor[.patch]][-tweak[-extrainfo]]".format(version_str), file=sys.stderr)
sys.exit(1)
if len(vparts) > 3:
raise VerionFormatError("Error: Too many dot-separated elements in version string '{0}'. Please use the following format: [v]major[.minor[.patch]][-tweak[-extrainfo]]".format(version_str))
sys.exit(1)
elif len(vparts[0]) == 0 or not vextra.isnumeric():
raise VerionFormatError("Error: Invalid version string '{0}'. Please use the following format: [v]major[.minor[.patch]][-tweak[-extrainfo]]".format(version_str))
if buildtype.lower().startswith("debug"):
buildtype_def = "#define DEBUG_BUILD"
else:
buildtype_def = "#define RELEASE_BUILD"
while len(vparts) < 3:
vparts.append(0)
version = [("${TAISEI_VERSION_MAJOR}", 0),
("${TAISEI_VERSION_MINOR}", 0),
("${TAISEI_VERSION_PATCH}", 0),
("${TAISEI_VERSION_TWEAK}", vextra),
("${TAISEI_VERSION}", version_str),
("${TAISEI_VERSION_FULL_STR}", "Taisei v" + version_str),
("${MESON_BUILD_TYPE}", buildtype),
("${ICONS_DIR}", os.path.join(root, "misc", "icons")),
("${BUILDTYPE_DEFINE}", buildtype_def)]
self.major = int(vparts[0])
self.minor = int(vparts[1])
self.patch = int(vparts[2])
self.tweak = int(vextra)
self.string = version_str
self.full_string = "Taisei v{0}".format(version_str)
for p in vparts:
c = version.pop(0)
version.append((c[0], int(p)))
def format(self, template='{string}'):
return template.format(**self.__dict__)
with open(inpath, "r") as infile:
template = infile.read()
for k, v in version:
template = template.replace(k, str(v))
def get(fallback=None, rootdir=None):
if rootdir is None:
import pathlib
rootdir = pathlib.Path(__file__).parent
with open(outpath, "w") as outfile:
print(template, file=outfile)
try:
from subprocess import check_output
git = check_output(['git', 'describe', '--tags', '--match', 'v[0-9]*[!asz]'], cwd=rootdir, universal_newlines=True)
version_str = git.strip()
except:
if not fallback:
raise
print("Warning: git not found or not a git repository; using fallback version {0}".format(fallback), file=sys.stderr)
version_str = fallback
return Version(version_str)
if __name__ == '__main__':
import sys
import argparse
parser = argparse.ArgumentParser(description='Print the Taisei version.')
parser.add_argument('format', type=str, nargs='?', default='{string}',
help='format string; variables: major, minor, patch, tweak, string, full_string')
parser.add_argument('--fallback', type=str, default=None,
help='fallback source string, used if git lookup fails')
parser.add_argument('--rootdir', type=str, default=None,
help='CWD for the git command')
args = parser.parse_args()
print(get(fallback=args.fallback, rootdir=args.rootdir).format(template=args.format))

View file

@ -1,10 +1,8 @@
ver_fb = get_option('version_fallback').strip()
version_fallback = ver_fb != '' ? ver_fb : meson.project_version()
version_script = find_program('../scripts/version.py')
version_command = [version_script, meson.source_root(), '@INPUT@', '@OUTPUT@', get_option('buildtype'), version_fallback]
version_template_script = find_program('../scripts/version-template.py')
version_template_command = [version_template_script, meson.source_root(), '@INPUT@', '@OUTPUT@', get_option('buildtype'), version_fallback]
version_deps = [custom_target('version file',
command : version_command,
command : version_template_command,
build_always : true,
input : 'version_auto.c.in',
output : 'version_auto.c')]
@ -14,12 +12,12 @@ if host_machine.system() == 'windows'
# HACK: compile_resources only accepts strings, not generated dependencies.
# We still want to generate this every time though.
rcpath = join_paths(meson.current_build_dir(), 'taisei.rc')
c = run_command(version_script, meson.source_root(), 'taisei.rc.in', rcpath, get_option('buildtype'), version_fallback)
c = run_command(version_template_script, meson.source_root(), 'taisei.rc.in', rcpath, get_option('buildtype'), version_fallback)
if c.returncode() != 0
error('Failed to generate Windows resource file:\n' + c.stderr())
endif
version_deps += custom_target('Windows resource file',
command : version_command,
command : version_template_command,
build_always : true,
input : 'taisei.rc.in',
output : 'taisei.rc')
@ -32,7 +30,7 @@ __attribute__((target("sse4.2")))
int main(int argc, char **argv) {
return _mm_crc32_u8(argc, 42);
}
''')
''', name : 'SSE 4.2 intrinsics test')
if use_intel_intrin
# All that just to append that -msse4.2 argument for this one file.
@ -41,7 +39,7 @@ if use_intel_intrin
taisei_deps += sse42_dep
message('SEE4.2 intrinsics will be used')
elif get_option('intel_intrin')
warning('SSE4.2 intrinsics can not be used')
message('SSE4.2 intrinsics can not be used')
endif
configure_file(configuration : config, output : 'build_config.h')
@ -164,7 +162,9 @@ if taisei_deps.contains(dep_zip)
'vfs/zippath.c',
]
else
src_base += ['vfs/zipfile_null.c']
src_base += [
'vfs/zipfile_null.c',
]
endif
if have_posix
@ -185,8 +185,16 @@ else
]
endif
taisei_exe = executable('taisei', src_base, version_deps,
dependencies : taisei_deps,
c_args : taisei_c_args,
gui_app : not get_option('win_console'),
install : true)
if macos_app_bundle
taisei_exe_name = 'Taisei'
else
taisei_exe_name = 'taisei'
endif
taisei_exe = executable(taisei_exe_name, src_base, version_deps,
dependencies : taisei_deps,
c_args : taisei_c_args,
gui_app : not get_option('win_console'),
install : true,
install_dir : bindir,
)