2021-01-14 08:34:20 +00:00
|
|
|
#
|
|
|
|
# Helper module to build macOS version of various source ports
|
|
|
|
# Copyright (C) 2020-2021 Alexey Lysiuk
|
|
|
|
#
|
|
|
|
# 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 3 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, see <http://www.gnu.org/licenses/>.
|
|
|
|
#
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import copy
|
|
|
|
import os
|
|
|
|
from platform import machine
|
|
|
|
import shutil
|
|
|
|
import subprocess
|
|
|
|
import typing
|
|
|
|
|
|
|
|
from .state import BuildState
|
|
|
|
from .target import Target, targets
|
|
|
|
from .utility import CaseInsensitiveDict, TargetPlatform, symlink_directory
|
|
|
|
|
|
|
|
|
|
|
|
class Builder(object):
|
|
|
|
def __init__(self, args: list):
|
|
|
|
self._targets = CaseInsensitiveDict({target.name: target for target in targets()})
|
|
|
|
|
|
|
|
arguments = self._parse_arguments(args)
|
|
|
|
|
|
|
|
state = self._state = BuildState()
|
|
|
|
state.xcode = arguments.xcode
|
|
|
|
state.checkout_commit = arguments.checkout_commit
|
2021-03-13 14:53:14 +00:00
|
|
|
state.skip_checkout = arguments.skip_checkout
|
2021-01-14 08:34:20 +00:00
|
|
|
state.verbose = arguments.verbose
|
|
|
|
|
|
|
|
self._platforms = []
|
|
|
|
self._populate_platforms(arguments)
|
|
|
|
|
|
|
|
state.platform = self._platforms[0]
|
|
|
|
|
2021-03-02 16:06:20 +00:00
|
|
|
if arguments.source_path:
|
|
|
|
state.source_path = os.path.abspath(arguments.source_path)
|
|
|
|
|
2021-01-14 08:34:20 +00:00
|
|
|
if arguments.target:
|
2021-01-16 09:54:50 +00:00
|
|
|
self._target = self._targets[arguments.target]
|
2021-01-17 10:37:35 +00:00
|
|
|
state.source = state.source_path + self._target.name + os.sep
|
2021-01-14 08:34:20 +00:00
|
|
|
state.external_source = False
|
|
|
|
else:
|
2021-01-17 10:37:35 +00:00
|
|
|
assert arguments.source
|
2021-02-25 14:00:46 +00:00
|
|
|
state.source = os.path.abspath(arguments.source) + os.sep
|
2021-01-14 08:34:20 +00:00
|
|
|
state.external_source = True
|
|
|
|
self._detect_target()
|
|
|
|
|
2021-01-16 09:54:50 +00:00
|
|
|
del self._targets
|
|
|
|
|
|
|
|
patch_path = f'{state.root_path}patch/{self._target.name}.patch'
|
2021-01-14 08:34:20 +00:00
|
|
|
if os.path.exists(patch_path):
|
|
|
|
state.patch_path = patch_path
|
|
|
|
|
2021-03-02 16:06:27 +00:00
|
|
|
if arguments.build_path:
|
|
|
|
state.build_path = os.path.abspath(arguments.build_path)
|
|
|
|
else:
|
2021-01-16 09:54:50 +00:00
|
|
|
state.build_path = state.root_path + 'build' + os.sep + self._target.name + \
|
2021-03-02 16:06:27 +00:00
|
|
|
os.sep + ('xcode' if state.xcode else 'make')
|
2021-01-14 08:34:20 +00:00
|
|
|
|
2021-03-02 16:06:27 +00:00
|
|
|
if arguments.output_path:
|
|
|
|
state.output_path = os.path.abspath(arguments.output_path)
|
|
|
|
else:
|
2021-01-14 08:34:20 +00:00
|
|
|
state.output_path = state.root_path + 'output'
|
|
|
|
|
|
|
|
state.build_path += os.sep
|
|
|
|
state.output_path += os.sep
|
|
|
|
|
|
|
|
state.jobs = arguments.jobs and arguments.jobs or \
|
|
|
|
subprocess.check_output(['sysctl', '-n', 'hw.ncpu']).decode('ascii').strip()
|
|
|
|
|
2021-01-16 09:54:50 +00:00
|
|
|
self._target.initialize(state)
|
2021-01-14 08:34:20 +00:00
|
|
|
|
|
|
|
def _populate_platforms(self, arguments):
|
|
|
|
state = self._state
|
|
|
|
|
|
|
|
def adjust_sdk_path(path: str) -> str:
|
|
|
|
if path:
|
2021-03-02 16:06:27 +00:00
|
|
|
return os.path.abspath(path)
|
2021-01-14 08:34:20 +00:00
|
|
|
|
|
|
|
sdk_probe_path = f'{state.root_path}sdk{os.sep}MacOSX{os_version}.sdk'
|
|
|
|
return sdk_probe_path if os.path.exists(sdk_probe_path) else None
|
|
|
|
|
|
|
|
if not arguments.disable_x64:
|
|
|
|
os_version = arguments.os_version_x64 if arguments.os_version_x64 else '10.9'
|
|
|
|
sdk_path = adjust_sdk_path(arguments.sdk_path_x64)
|
|
|
|
platform = TargetPlatform('x86_64', 'x86_64-apple-darwin', os_version, sdk_path, state.prefix_path)
|
|
|
|
self._platforms.append(platform)
|
|
|
|
|
|
|
|
if not arguments.disable_arm:
|
|
|
|
os_version = arguments.os_version_arm if arguments.os_version_arm else '11.0'
|
|
|
|
sdk_path = adjust_sdk_path(arguments.sdk_path_arm)
|
|
|
|
platform = TargetPlatform('arm64', 'aarch64-apple-darwin', os_version, sdk_path, state.prefix_path)
|
|
|
|
self._platforms.append(platform)
|
|
|
|
|
|
|
|
assert len(self._platforms) > 0
|
|
|
|
|
|
|
|
# Put native platform first in the list of platforms
|
|
|
|
if self._platforms[0].architecture == machine():
|
|
|
|
return
|
|
|
|
|
|
|
|
for platform in self._platforms:
|
|
|
|
if platform.architecture == machine():
|
|
|
|
native_platform = platform
|
|
|
|
self._platforms.remove(platform)
|
|
|
|
self._platforms.insert(0, native_platform)
|
|
|
|
break
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
self._create_prefix_directory()
|
|
|
|
|
|
|
|
state = self._state
|
2021-01-16 09:54:50 +00:00
|
|
|
target = self._target
|
2021-01-14 08:34:20 +00:00
|
|
|
target.prepare_source(state)
|
|
|
|
|
|
|
|
if target.destination == Target.DESTINATION_DEPS:
|
|
|
|
state.install_path = state.deps_path + target.name + os.sep
|
|
|
|
elif target.destination == Target.DESTINATION_OUTPUT:
|
|
|
|
state.install_path = state.output_path + target.name + os.sep
|
|
|
|
|
|
|
|
assert state.install_path
|
|
|
|
|
|
|
|
if target.multi_platform and not state.xcode:
|
|
|
|
self._build_multiple_platforms(target)
|
|
|
|
else:
|
|
|
|
self._build(target)
|
|
|
|
|
|
|
|
def _build(self, target: Target):
|
|
|
|
state = self._state
|
|
|
|
|
|
|
|
target.configure(state)
|
2021-01-16 09:55:41 +00:00
|
|
|
target.build(state)
|
|
|
|
target.post_build(state)
|
2021-01-14 08:34:20 +00:00
|
|
|
|
|
|
|
def _build_multiple_platforms(self, base_target: Target):
|
|
|
|
assert base_target.multi_platform
|
|
|
|
|
|
|
|
state = self._state
|
|
|
|
base_build_path = state.build_path
|
|
|
|
base_install_path = state.install_path
|
|
|
|
install_paths = []
|
|
|
|
|
|
|
|
for platform in self._platforms:
|
|
|
|
if platform.architecture in base_target.unsupported_architectures:
|
|
|
|
continue
|
|
|
|
|
|
|
|
state.platform = platform
|
|
|
|
state.build_path = base_build_path + 'build_' + platform.architecture + os.sep
|
|
|
|
|
|
|
|
if platform.architecture == machine():
|
|
|
|
state.native_build_path = state.build_path
|
|
|
|
|
|
|
|
target = copy.deepcopy(base_target)
|
|
|
|
state.install_path = base_build_path + 'install_' + platform.architecture + os.sep
|
|
|
|
|
|
|
|
self._build(target)
|
|
|
|
|
|
|
|
install_paths.append(state.install_path)
|
|
|
|
|
|
|
|
Builder._merge_install_paths(install_paths, base_install_path)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _compare_files(paths: typing.Sequence[str]) -> bool:
|
|
|
|
content = None
|
|
|
|
|
|
|
|
for path in paths:
|
|
|
|
if not os.path.exists(path):
|
|
|
|
return False
|
|
|
|
|
|
|
|
with open(path, 'rb') as f:
|
|
|
|
if content:
|
|
|
|
if content != f.read():
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
content = f.read()
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _merge_file(src: os.DirEntry, src_sub_paths: typing.Sequence[str], dst_path: str):
|
|
|
|
with open(src.path, 'rb') as f:
|
|
|
|
header = f.read(8)
|
|
|
|
|
|
|
|
is_executable = header[:4] == b'\xcf\xfa\xed\xfe'
|
|
|
|
is_library = header == b'!<arch>\n'
|
|
|
|
|
|
|
|
if is_executable or is_library:
|
|
|
|
# Merge executable and library files
|
|
|
|
dst_file = dst_path + os.sep + src.name
|
|
|
|
|
|
|
|
args = ['lipo']
|
|
|
|
args += src_sub_paths
|
|
|
|
args += ['-create', '-output', dst_file]
|
|
|
|
subprocess.check_call(args)
|
|
|
|
|
|
|
|
# Apply ad-hoc code signing on executable files outside of application bundles
|
|
|
|
if is_executable and '.app/Contents/' not in src.path:
|
|
|
|
args = ('codesign', '--sign', '-', dst_file)
|
|
|
|
subprocess.check_call(args)
|
|
|
|
else:
|
|
|
|
if not Builder._compare_files(src_sub_paths):
|
|
|
|
print(f'WARNING: Source files for {dst_path + os.sep + src.name} don\'t match')
|
|
|
|
shutil.copy(src_sub_paths[0], dst_path)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _merge_missing_files(src_paths: typing.Sequence[str], dst_path: str):
|
|
|
|
shifted_src_paths = [path for path in src_paths]
|
|
|
|
last_path_index = len(src_paths) - 1
|
|
|
|
|
|
|
|
for _ in range(last_path_index):
|
|
|
|
shifted_src_paths.append(shifted_src_paths[0])
|
|
|
|
del shifted_src_paths[0]
|
|
|
|
|
|
|
|
if not os.path.exists(shifted_src_paths[0]):
|
|
|
|
continue
|
|
|
|
|
|
|
|
Builder._merge_install_paths(shifted_src_paths, dst_path, missing_files_only=True)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _merge_install_paths(src_paths: typing.Sequence[str], dst_path: str, missing_files_only=False):
|
|
|
|
if len(src_paths) == 0:
|
|
|
|
return
|
|
|
|
|
|
|
|
if not missing_files_only:
|
|
|
|
if os.path.exists(dst_path):
|
|
|
|
shutil.rmtree(dst_path)
|
|
|
|
|
|
|
|
os.makedirs(dst_path, exist_ok=True)
|
|
|
|
|
|
|
|
for src in os.scandir(src_paths[0]):
|
|
|
|
src_sub_paths = [path + os.sep + src.name for path in src_paths]
|
|
|
|
|
|
|
|
if src.is_dir():
|
|
|
|
Builder._merge_install_paths(src_sub_paths, dst_path + os.sep + src.name, missing_files_only)
|
|
|
|
elif src.name.endswith('.la'):
|
|
|
|
# Skip libtool files
|
|
|
|
continue
|
|
|
|
elif missing_files_only:
|
|
|
|
for src_sub_path in src_sub_paths[1:]:
|
|
|
|
if not os.path.exists(src_sub_path):
|
|
|
|
shutil.copy(src_sub_paths[0], dst_path)
|
|
|
|
else:
|
|
|
|
Builder._merge_file(src, src_sub_paths, dst_path)
|
|
|
|
|
|
|
|
if not missing_files_only:
|
|
|
|
Builder._merge_missing_files(src_paths, dst_path)
|
|
|
|
|
|
|
|
def _create_prefix_directory(self):
|
|
|
|
state = self._state
|
|
|
|
os.makedirs(state.prefix_path, exist_ok=True)
|
|
|
|
|
|
|
|
cleanup = True
|
|
|
|
|
|
|
|
for dep in os.scandir(state.deps_path):
|
|
|
|
if dep.is_dir():
|
|
|
|
symlink_directory(dep.path, state.prefix_path, cleanup)
|
|
|
|
|
|
|
|
# Do symlink cleanup only once
|
|
|
|
cleanup = False
|
|
|
|
|
|
|
|
def _detect_target(self):
|
|
|
|
for name, target in self._targets.items():
|
|
|
|
if target.detect(self._state):
|
2021-01-16 09:54:50 +00:00
|
|
|
self._target = self._targets[name]
|
2021-01-14 08:34:20 +00:00
|
|
|
break
|
|
|
|
|
2021-01-16 09:54:50 +00:00
|
|
|
assert self._target
|
2021-01-14 08:34:20 +00:00
|
|
|
|
|
|
|
def _parse_arguments(self, args: list):
|
|
|
|
assert self._targets
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(description='*ZDoom binary dependencies for macOS')
|
|
|
|
|
|
|
|
group = parser.add_mutually_exclusive_group(required=True)
|
|
|
|
group.add_argument('--target', choices=self._targets.keys(), help='target to build')
|
2021-01-17 10:37:35 +00:00
|
|
|
group.add_argument('--source', metavar='path', help='path to target\'s source code')
|
2021-01-14 08:34:20 +00:00
|
|
|
|
2021-03-13 14:53:14 +00:00
|
|
|
group = parser.add_mutually_exclusive_group()
|
|
|
|
group.add_argument('--checkout-commit', metavar='commit',
|
|
|
|
help='source code commit or tag to checkout')
|
|
|
|
group.add_argument('--skip-checkout', action='store_true',
|
|
|
|
help='avoid any operations with source code')
|
|
|
|
|
2021-01-14 08:34:20 +00:00
|
|
|
group = parser.add_argument_group()
|
|
|
|
group.add_argument('--xcode', action='store_true', help='generate Xcode project instead of build')
|
2021-01-17 10:37:35 +00:00
|
|
|
group.add_argument('--source-path', metavar='path',
|
|
|
|
help='path to store downloaded and checked out source code')
|
2021-01-14 08:34:20 +00:00
|
|
|
group.add_argument('--build-path', metavar='path', help='target build path')
|
|
|
|
group.add_argument('--output-path', metavar='path', help='output path for main targets')
|
|
|
|
group.add_argument('--sdk-path-x64', metavar='path', help='path to macOS SDK for x86_64')
|
|
|
|
group.add_argument('--sdk-path-arm', metavar='path', help='path to macOS SDK for ARM64')
|
|
|
|
group.add_argument('--os-version-x64', metavar='version', help='macOS deployment version for x86_64')
|
|
|
|
group.add_argument('--os-version-arm', metavar='version', help='macOS deployment version for ARM64')
|
|
|
|
group.add_argument('--verbose', action='store_true', help='enable verbose build output')
|
|
|
|
group.add_argument('--jobs', help='number of parallel compilation jobs')
|
|
|
|
|
|
|
|
group = parser.add_mutually_exclusive_group()
|
|
|
|
group.add_argument('--disable-x64', action='store_true', help='disable x86_64 support')
|
|
|
|
group.add_argument('--disable-arm', action='store_true', help='disable ARM64 support')
|
|
|
|
|
|
|
|
return parser.parse_args(args)
|