Files
spark/tools/py/build_admin_web_app.py
T

199 lines
7.0 KiB
Python

from shutil import copytree
import glob
from typing import Iterable
import os
from pathlib import Path
from core.file_utils import CWD, SparkPaths, as_absolute_paths, link_dir, rmdir_or_unlink
from core.build_step import BuildStep
from core.logging import exec_and_log_error
from core.table_formatter import fmt_table
import subprocess
import argparse
class BuildArgs:
def __init__(self, args):
self.protos = args.protos
self.mode = args.mode
self.tsconfig = args.tsconfig
self.root_path = SparkPaths.root()
self.src_path = self.root_path.joinpath('src', 'Spark.Admin.WebApp')
self.out_path = Path(args.output_dir).absolute()
self.pbufs_abs = as_absolute_paths(
self.protos, with_respect_to=self.src_path) or []
def __repr__(self):
return fmt_table(self.__dict__)
def _wrapped_run(*args, require_success=True, **kwargs):
# by default, print header in green and flush buffer immediately to avoid
# non-linear build output
if 'color' not in kwargs:
kwargs['color'] = 'green'
if 'flush' not in kwargs:
kwargs['flush'] = True
cmd = ' '.join(args)
print(f'BEGIN {cmd}', flush=True)
res = subprocess.run(args, shell=True)
if require_success and res.returncode != 0:
raise ValueError(
f'Command \'{cmd}\' exited with code {res.returncode}')
print(f'END {cmd}', flush=True)
def _get_arg_parser():
parser = argparse.ArgumentParser(description='Build spark web server')
parser.add_argument(
'--mode',
metavar='development|production',
type=str,
nargs='?',
help='The build configuration',
default='development')
parser.add_argument(
'--tsconfig',
metavar='path',
type=str,
help='The tsconfig file to use',
required=True)
parser.add_argument(
'-o',
'--output-dir',
metavar='path',
type=str,
nargs='?',
help='The directory in which to build')
parser.add_argument(
'--protos',
type=str,
nargs='+',
help='The protocol buffers to transpile')
parser.set_defaults(mode='development')
return parser
def _gen_fs_pbufs(
root_path: Path,
src_path: Path,
out_path: Path,
protos: Iterable[str],
):
with CWD(src_path):
protos_rel = [os.path.relpath(p, src_path) for p in protos]
protoc_path = root_path.joinpath('externals', 'linux', 'bin', 'protoc')
plugin_script_path = root_path.joinpath(
'src', 'lib', 'api', 'service_compiler', 'plugin.sh')
cmd_parts = [
f'{protoc_path}',
f'--plugin=protoc-gen-service={plugin_script_path}',
f'--service_out=lang=fs,strip_out_path=1:{out_path.joinpath("fable")}',
f'-I {os.path.relpath(root_path.joinpath("src"), os.curdir)}',
f'-I {os.path.relpath(str(root_path.joinpath("externals", "linux", "include")), os.curdir)}',
' '.join(protos_rel)
]
cmd = ' '.join(cmd_parts)
with BuildStep('generate fs protos', cmd):
return subprocess.run(cmd, shell=True)
def _gen_js_pbufs(src_path: Path, out_path: Path, protos: Iterable):
with CWD(src_path):
protos_rel = [os.path.relpath(p, src_path) for p in protos]
cmd_parts = [
f'npx --no-install pbjs', f'-t json-module', f'-w commonjs',
f'-o {out_path.joinpath("proto.js")}', ' '.join(protos_rel)
]
cmd = ' '.join(cmd_parts)
with BuildStep('generate js protocol buffers', cmd):
return subprocess.run(cmd, shell=True)
def _gen_fsts_stubs(api_source_path: Path, api_output_path: Path):
def gen_fs(ts):
# The triple 'with_suffix' chain isn't a typo. It's a hack to remove
# (up to) 2 suffixes to account for the compound ext '.d.ts'.
fs_out = Path(ts).with_suffix('').with_suffix('').with_suffix('.fs')
subprocess.run(f'npx --no-install ts2fable {ts} {fs_out}', shell=True)
# Generate fs stubs from typescript that was just built
typescripts = glob.glob(f'{api_output_path.joinpath("*.d.ts")}')
with BuildStep('generate fs -> ts stubs',
'\n'.join(typescripts)), CWD(api_source_path):
return [gen_fs(ts) for ts in typescripts]
def _tsc(src_path, tscfg_path):
with BuildStep('tsc', str(tscfg_path)), CWD(src_path):
return subprocess.run(
f'npx --no-install tsc -p {tscfg_path}', shell=True)
def _fable_webpack(src_path, out_path, mode):
with BuildStep('fable webpack'):
copytree(str(src_path.joinpath('public')), out_path.joinpath('public'))
os.chdir(src_path.joinpath('src'))
_wrapped_run('dotnet restore')
_wrapped_run('dotnet tool restore')
# Note: We used to convert fs -> js via webpack, using the node pkgs
# fable-compiler & fable-loader. However, as of 02/24/2023, they are
# incompatible w/ net7.0. Fortunately the dotnet tool 'fable' IS
# compatible w/ net7.0.
_wrapped_run(f'dotnet fable -o {out_path}')
# Run webpack via 'npm run build'
# (the build cmd is defined in package.json)
_wrapped_run(f'npm run build -- --mode {mode}')
def _main():
parser = _get_arg_parser()
args = BuildArgs(parser.parse_args())
# needed bc 'Syntax Error: f-string expression part cannot include a backslash'
newline = '\n'
print(
f'protos: {(newline.join([str(p) for p in args.protos]))}\n\n',
flush=True)
# setup dirs/env vars
rmdir_or_unlink(args.out_path.joinpath('public'))
rmdir_or_unlink(args.out_path.joinpath('fable'))
os.makedirs(args.out_path.joinpath('fable'))
os.putenv('SPARK_ADMIN_WEB_APP_OUTPUT_DIR', str(args.out_path))
# This build is part of the larger CMake 'sparkd' build, which means the
# output dir depends on the CMake 'configure' step. Dotnet isn't privy to
# CMake configuration, so the output dir is passed to this script as a
# command line argument (per a custom target defined CMakeLists). Since the
# output dir isn't defined in a dotnet proj file, intellisense won't know
# where to look for generated source files. As a workaround, link the
# actual output dir to a path defined in the fsproj file.
out_proxy = SparkPaths.root().joinpath('build', 'Spark.Admin.WebApp')
if args.out_path != out_proxy:
rmdir_or_unlink(out_proxy)
link_dir(args.out_path, out_proxy)
os.chdir(args.src_path)
# 1) Build js protobufs
_gen_js_pbufs(args.src_path, args.out_path, args.pbufs_abs)
# 2) Build fs protobufs
_gen_fs_pbufs(args.root_path, args.src_path, args.out_path, args.pbufs_abs)
# 3) Build typescript
_tsc(args.src_path, args.tsconfig)
# 4) Generate fs stubs from typescript that was just built
_gen_fsts_stubs(args.src_path, args.out_path)
# 5) Build fs fable (webpack to js)
_fable_webpack(args.src_path, args.out_path, args.mode)
if __name__ == '__main__':
exec_and_log_error(_main)