401 lines
12 KiB
Python
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)
|