mirror of
https://github.com/ZDoom/zdoom-macos-deps.git
synced 2025-02-16 16:41:42 +00:00
put all code into new package, build script will use it as a black box split build state from builder class, and pass state to target methods place base, main, dependency, special targets into separate files, and put them in own package This implements #19
301 lines
11 KiB
Python
301 lines
11 KiB
Python
#
|
|
# 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
|
|
state.build_path = arguments.build_path
|
|
state.output_path = arguments.output_path
|
|
state.verbose = arguments.verbose
|
|
|
|
self._platforms = []
|
|
self._populate_platforms(arguments)
|
|
|
|
state.platform = self._platforms[0]
|
|
|
|
if arguments.target:
|
|
self.target = self._targets[arguments.target]
|
|
state.source_path = state.root_source_path + self.target.name + os.sep
|
|
state.external_source = False
|
|
else:
|
|
assert arguments.source_path
|
|
state.source_path = arguments.source_path + os.sep
|
|
state.external_source = True
|
|
self._detect_target()
|
|
|
|
patch_path = f'{state.root_path}patch/{self.target.name}.patch'
|
|
if os.path.exists(patch_path):
|
|
state.patch_path = patch_path
|
|
|
|
if not state.build_path:
|
|
state.build_path = state.root_path + 'build' + os.sep + self.target.name + \
|
|
os.sep + (state.xcode and 'xcode' or 'make')
|
|
|
|
if not state.output_path:
|
|
state.output_path = state.root_path + 'output'
|
|
|
|
state.build_path += os.sep
|
|
state.output_path += os.sep
|
|
state.native_build_path = state.build_path
|
|
|
|
state.jobs = arguments.jobs and arguments.jobs or \
|
|
subprocess.check_output(['sysctl', '-n', 'hw.ncpu']).decode('ascii').strip()
|
|
|
|
self.target.initialize(state)
|
|
|
|
def _populate_platforms(self, arguments):
|
|
state = self._state
|
|
|
|
def adjust_sdk_path(path: str) -> str:
|
|
if path:
|
|
return path
|
|
|
|
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
|
|
target = self.target
|
|
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)
|
|
target.build(state)
|
|
target.post_build(state)
|
|
|
|
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):
|
|
self.target = self._targets[name]
|
|
break
|
|
|
|
assert self.target
|
|
|
|
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')
|
|
group.add_argument('--source-path', metavar='path', help='path to target\'s source code')
|
|
|
|
group = parser.add_argument_group()
|
|
group.add_argument('--xcode', action='store_true', help='generate Xcode project instead of build')
|
|
group.add_argument('--checkout-commit', metavar='commit',
|
|
help='target\'s source code commit or tag to checkout')
|
|
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)
|