Files
spark/tools/buildspark.py
T

401 lines
12 KiB
Python

import argparse
import os
import subprocess
import shutil
from typing import Tuple
class VisualStudioContext:
def __init__(self, version, year, prompt_path, msbuild_path, msbuild_arg_prefix):
self.version = version
self.year = year
self.prompt_path = prompt_path
self.msbuild_path = msbuild_path
self._msbuild_arg_prefix = msbuild_arg_prefix
def prompt_cmd(self):
return f'{self.prompt_path} -arch=x64 -host_arch=x64'
def configure_libspark(self, build_dir):
with CWD(build_dir):
subprocess.call(
f'cmake.exe -G "Visual Studio {self.version} 2022" {SparkPaths.root()}'
)
shutil.copy(
f'{SparkPaths.tools()}\\cpp.build.props',
'Directory.Build.Props')
def msbuild_flag(self, flag):
return f'{self._msbuild_arg_prefix}{flag}'
def msbuild_clp(self, key, value = None):
rv = f'{self._msbuild_arg_prefix}clp:{key}'
if value is not None:
rv = f'{rv}={value}'
return rv
def msbuild_target(self, target):
return f'{self._msbuild_arg_prefix}t:{target}'
def msbuild_prop(self, key, value):
return f'{self._msbuild_arg_prefix}p:{key}={value}'
def _deduce_visual_studio_context():
# Wrap paths in quotes so spaces don't need to be escaped
# promp_path values are tuples with fields 'version, year, prompt_path', respectively
vs_contexts = [
VisualStudioContext(
17, 2022,
r'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat',
r'"C:\Program Files\Microsoft Visual Studio\2022\Community\Msbuild\Current\Bin\amd64\MSBuild.exe"',
'-'),
VisualStudioContext(
16, 2019,
r'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\VsDevCmd.bat',
r'"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe"',
'/')
]
for vs_context in vs_contexts:
if os.path.exists(f'{vs_context.prompt_path}'):
return vs_context
raise ValueError('Failed to deduce visual studio context')
def _workspace_root():
tools_dir = os.path.dirname(os.path.realpath(__file__))
return os.path.abspath(f'{tools_dir}\\{os.path.pardir}')
def _add_toggle_opt(parser, name, **kwargs):
parser.add_argument(f'--{name}', action=f'store_true', dest=name, **kwargs)
parser.add_argument(f'--no-{name}', action=f'store_false', dest=name)
return parser
class SparkPaths:
@staticmethod
def root():
return _workspace_root()
@staticmethod
def client_csproj(name):
return fr'{_workspace_root()}\src\client\{name}\{name}.csproj'
@staticmethod
def client_sln():
return fr'{_workspace_root()}\spark_client.sln'
@staticmethod
def tools():
return fr'{_workspace_root()}\tools'
@staticmethod
def default_build():
return fr'{_workspace_root()}\build'
@staticmethod
def externals():
return fr'{_workspace_root()}\externals'
class BuildTarget:
def __init__(self, name, path):
self.name = name
self.path = path
class BuildContext:
def __init__(self, workspace_root = None, build_dir = None):
workspace_root = workspace_root or _workspace_root()
build_dir = build_dir or f'{workspace_root}\\build'
self.workspace_root = workspace_root
self.build_dir = build_dir
def get_csharp_targets() -> dict:
targets = {}
def add_target(target: BuildTarget):
targets[target.name] = target
add_target(BuildTarget('spark_client', f'{workspace_root}\\spark_client.sln'))
def add_csproj_target(name: str):
add_target(BuildTarget(name, f'{workspace_root}\\src\\client\\{name}\\{name}.csproj'))
# TODO: rename 'Spark.csproj' -> 'spark.csproj' to be consistent
# with all other csproj filenames, which are snake_case
add_target(BuildTarget('spark', f'{workspace_root}\\src\\client\\spark\\Spark.csproj'))
add_csproj_target('spark_api')
add_csproj_target('spark_core')
add_csproj_target('spark_sdk')
add_csproj_target('spark_test')
add_csproj_target('sparkcmd')
return targets
self.csharp_targets = get_csharp_targets()
self.csharp_aliases = {
'all': ['spark_client'],
'api': ['spark_api'],
'core': ['spark_core'],
'cmd': ['sparkcmd'],
'test': ['test'],
'sdk': ['spark_sdk']
}
def get_cpp_targets() -> dict:
targets = {}
def add_target(target: BuildTarget):
targets[target.name] = target
add_target(BuildTarget('libspark', f'{build_dir}\\ALL_BUILD.vcxproj'))
return targets
self.cpp_targets = get_cpp_targets()
self.cpp_aliases = { }
def parse_targets(self, targets) -> Tuple[list, list]:
'''Parses target input (from cli) and returns a tuple whose items are a
list of csharp targets, and cpp targets, respectively'''
csharp_ts = []
cpp_ts = []
for t in targets:
# Always try to parse `t` as both cpp and csharp targets, since it
# could be an alias for a combination of csharp and cpp targets.
csharp_ts.extend(self._parse_target(t, self.csharp_targets, self.csharp_aliases))
cpp_ts.extend(self._parse_target(t, self.cpp_targets, self.cpp_aliases))
return (csharp_ts, cpp_ts)
def _parse_target(self, t, targets, aliases) -> list:
if t in targets:
return [targets[t]]
elif t in aliases:
alias_ts = aliases[t]
for a in alias_ts:
if a not in targets:
_exit(f"Failed to find target for '{t}' (alias '{a}'). known targets: {targets}")
return [targets[alias_t] for alias_t in aliases[t]]
return []
def _parse_targets(self, targets_in, targets, aliases):
rv = []
for t in targets_in:
res = self._parse_target(t, targets, aliases)
if res is not None:
rv[res.name] = res
return rv
class CWD:
def __init__(self, push_path, pop_path=os.getcwd()):
self.push_path = push_path
self.pop_path = pop_path
def __enter__(self):
if self.push_path is not None:
os.chdir(self.push_path)
def __exit__(self, exc_type, exc_val, exc_tb):
if self.pop_path is not None:
os.chdir(self.pop_path)
def _get_arg_parser():
parser = argparse.ArgumentParser(description='Build spark_client.sln')
parser.add_argument(
'--configuration',
metavar='debug|release',
type=str,
nargs='?',
help='The build configuration',
default='Debug')
parser.add_argument(
'-o',
'--output-dir',
metavar='path',
type=str,
nargs='?',
help='The directory in which to build')
parser.add_argument(
'-l',
'--link-output-dir',
metavar='link_path',
type=str,
nargs='?',
help='The symbolink link to --output-dir')
parser.add_argument(
'-t',
'--target',
metavar='targets',
type=str,
action='append',
nargs='+',
help='The target(s) to build')
_add_toggle_opt(parser, 'cpp', help='Whether or not to build cpp')
_add_toggle_opt(parser, 'csharp', help='Whether or not to build csharp')
_add_toggle_opt(
parser, 'restore', help='Whether or not to run msbuild /t:restore')
_add_toggle_opt(
parser,
'clean',
help=
'Whether or not to delete and overwrite an existing build directory')
parser.set_defaults(restore=True, cpp=True, csharp=True)
return parser
def _unlink(path):
if not os.path.exists(path):
return
if not os.path.islink(path):
raise ValueError(
'The specified path already exists and is not a symbolic link')
os.unlink(path)
def _rmdir_or_unlink(dir):
if not os.path.exists(dir):
return
if os.path.islink(dir):
os.unlink(dir)
else:
shutil.rmtree(dir)
def _link_dir(source_path, link_path):
if source_path is None:
raise TypeError('source_path is None')
if not os.path.isdir(source_path):
raise ValueError('source_path is not a directory')
if link_path is None:
raise TypeError('link_path is None')
if source_path == link_path:
return
if not os.path.exists(source_path):
os.makedirs(source_path)
if os.path.exists(link_path):
if os.path.islink(link_path):
if os.readlink(link_path) == source_path:
return
os.unlink(link_path)
elif os.path.isdir(link_path):
shutil.rmtree(link_path)
elif os.path.isfile(link_path):
os.remove(link_path)
else:
raise ValueError('link_path is an unexpected filetype')
os.symlink(source_path, link_path)
def _create_build_dir(build_dir, clean):
if build_dir is None:
raise TypeError('build_dir is None')
if not os.path.exists(build_dir):
os.makedirs(build_dir)
elif clean:
_rmdir_or_unlink(build_dir)
os.makedirs(build_dir)
def _exit(code=0, msg=None):
if msg is not None:
print(msg)
exit(code)
def _get_arg(args, name, value_or=None):
if name in args:
return args[name]
return value_or
if __name__ == "__main__":
parser = _get_arg_parser()
args = parser.parse_args()
args_dict = vars(args)
clean = args.clean
restore = args.restore
build_dir = _get_arg(args_dict, 'output_dir') or SparkPaths.default_build()
link_build_dir = _get_arg(args_dict, 'link_output_dir')
targets = _get_arg(args_dict, 'target') or []
targets = [t for grp in targets for t in grp]
if len(targets) == 0:
targets.append('all')
vs_context = _deduce_visual_studio_context()
try:
build_dir = os.path.realpath(build_dir)
if link_build_dir is not None:
_unlink(link_build_dir)
_create_build_dir(build_dir, clean)
vs_context.configure_libspark(build_dir)
except Exception as e:
_exit(1, e)
build_context = BuildContext(_workspace_root(), build_dir)
csharp_targets, cpp_targets = build_context.parse_targets(targets)
build_cmd_steps = [vs_context.prompt_cmd()]
base_output_path_arg = vs_context.msbuild_prop(
'BaseOutputPath', f'{build_dir}\\{args.configuration}')
# restore build (downloads nuget package deps)
if restore or clean:
restore_cmd_prefix = ' '.join([
f"{vs_context.msbuild_path}",
f"{vs_context.msbuild_flag('m')}",
f"{vs_context.msbuild_prop('debugtype', 'portable')}",
f"{vs_context.msbuild_prop('platform', 'x64')}",
f"{vs_context.msbuild_prop('configuration', args.configuration)}",
f"{vs_context.msbuild_target('restore')}",
f"{vs_context.msbuild_clp('verbosity', 'minimal')}",
f"{vs_context.msbuild_clp('summary')}"])
for t in cpp_targets:
build_cmd_steps.append(
f'{restore_cmd_prefix} {t.path}')
for t in csharp_targets:
build_cmd_steps.append(
f'{restore_cmd_prefix} {base_output_path_arg} {t.path}')
# main build
build_cmd_prefix = ' '.join([
f"{vs_context.msbuild_path}",
f"{vs_context.msbuild_flag('m')}",
f"{vs_context.msbuild_prop('debugtype', 'portable')}",
f"{vs_context.msbuild_prop('platform', 'x64')}",
f"{vs_context.msbuild_prop('GenerateFullPaths', 'true')}",
f"{vs_context.msbuild_prop('configuration', args.configuration)}",
f"{vs_context.msbuild_clp('verbosity', 'minimal')}",
f"{vs_context.msbuild_clp('summary')}"])
for t in cpp_targets:
build_cmd_steps.append(
f'{build_cmd_prefix} {t.path}')
for t in csharp_targets:
build_cmd_steps.append(
f'{build_cmd_prefix} {base_output_path_arg} {t.path}')
build_cmd = ' && '.join(build_cmd_steps)
subprocess.run(build_cmd)
if link_build_dir is not None:
_link_dir(build_dir, link_build_dir)