From 85f89d3a734b494aa211f8a5bcf4cc2430cd4889 Mon Sep 17 00:00:00 2001 From: c0repwn3r Date: Wed, 12 Apr 2023 10:43:02 -0400 Subject: [PATCH] [wip] spacetime build beginnings --- .gitignore | 6 +- spacetime/ninja_syntax.py | 199 ++++++++++++++++++++++++++++++++++++++ spacetime/spacetime.py | 123 +++++++++++++++++++++++ st | 114 ++++++++++++++++++++++ 4 files changed, 441 insertions(+), 1 deletion(-) create mode 100644 spacetime/ninja_syntax.py create mode 100644 spacetime/spacetime.py create mode 100755 st diff --git a/.gitignore b/.gitignore index 3eae17230e3a1977c19926ecae18020796c4810e..d27744329876733e75dfa94f956de67eae054e08 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,8 @@ target client/pkg web/dist .idea -assets/svg/*.png \ No newline at end of file +assets/svg/*.png +build.ninja +.ninja_log +assets/final +assets/dist \ No newline at end of file diff --git a/spacetime/ninja_syntax.py b/spacetime/ninja_syntax.py new file mode 100644 index 0000000000000000000000000000000000000000..ca73b5ba9955456dfb3500bda6b8620d2db22e3a --- /dev/null +++ b/spacetime/ninja_syntax.py @@ -0,0 +1,199 @@ +#!/usr/bin/python + +# Copyright 2011 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Python module for generating .ninja files. + +Note that this is emphatically not a required piece of Ninja; it's +just a helpful utility for build-file-generation systems that already +use Python. +""" + +import re +import textwrap + +def escape_path(word): + return word.replace('$ ', '$$ ').replace(' ', '$ ').replace(':', '$:') + +class Writer(object): + def __init__(self, output, width=78): + self.output = output + self.width = width + + def newline(self): + self.output.write('\n') + + def comment(self, text): + for line in textwrap.wrap(text, self.width - 2, break_long_words=False, + break_on_hyphens=False): + self.output.write('# ' + line + '\n') + + def variable(self, key, value, indent=0): + if value is None: + return + if isinstance(value, list): + value = ' '.join(filter(None, value)) # Filter out empty strings. + self._line('%s = %s' % (key, value), indent) + + def pool(self, name, depth): + self._line('pool %s' % name) + self.variable('depth', depth, indent=1) + + def rule(self, name, command, description=None, depfile=None, + generator=False, pool=None, restat=False, rspfile=None, + rspfile_content=None, deps=None): + self._line('rule %s' % name) + self.variable('command', command, indent=1) + if description: + self.variable('description', description, indent=1) + if depfile: + self.variable('depfile', depfile, indent=1) + if generator: + self.variable('generator', '1', indent=1) + if pool: + self.variable('pool', pool, indent=1) + if restat: + self.variable('restat', '1', indent=1) + if rspfile: + self.variable('rspfile', rspfile, indent=1) + if rspfile_content: + self.variable('rspfile_content', rspfile_content, indent=1) + if deps: + self.variable('deps', deps, indent=1) + + def build(self, outputs, rule, inputs=None, implicit=None, order_only=None, + variables=None, implicit_outputs=None, pool=None, dyndep=None): + outputs = as_list(outputs) + out_outputs = [escape_path(x) for x in outputs] + all_inputs = [escape_path(x) for x in as_list(inputs)] + + if implicit: + implicit = [escape_path(x) for x in as_list(implicit)] + all_inputs.append('|') + all_inputs.extend(implicit) + if order_only: + order_only = [escape_path(x) for x in as_list(order_only)] + all_inputs.append('||') + all_inputs.extend(order_only) + if implicit_outputs: + implicit_outputs = [escape_path(x) + for x in as_list(implicit_outputs)] + out_outputs.append('|') + out_outputs.extend(implicit_outputs) + + self._line('build %s: %s' % (' '.join(out_outputs), + ' '.join([rule] + all_inputs))) + if pool is not None: + self._line(' pool = %s' % pool) + if dyndep is not None: + self._line(' dyndep = %s' % dyndep) + + if variables: + if isinstance(variables, dict): + iterator = iter(variables.items()) + else: + iterator = iter(variables) + + for key, val in iterator: + self.variable(key, val, indent=1) + + return outputs + + def include(self, path): + self._line('include %s' % path) + + def subninja(self, path): + self._line('subninja %s' % path) + + def default(self, paths): + self._line('default %s' % ' '.join(as_list(paths))) + + def _count_dollars_before_index(self, s, i): + """Returns the number of '$' characters right in front of s[i].""" + dollar_count = 0 + dollar_index = i - 1 + while dollar_index > 0 and s[dollar_index] == '$': + dollar_count += 1 + dollar_index -= 1 + return dollar_count + + def _line(self, text, indent=0): + """Write 'text' word-wrapped at self.width characters.""" + leading_space = ' ' * indent + while len(leading_space) + len(text) > self.width: + # The text is too wide; wrap if possible. + + # Find the rightmost space that would obey our width constraint and + # that's not an escaped space. + available_space = self.width - len(leading_space) - len(' $') + space = available_space + while True: + space = text.rfind(' ', 0, space) + if (space < 0 or + self._count_dollars_before_index(text, space) % 2 == 0): + break + + if space < 0: + # No such space; just use the first unescaped space we can find. + space = available_space - 1 + while True: + space = text.find(' ', space + 1) + if (space < 0 or + self._count_dollars_before_index(text, space) % 2 == 0): + break + if space < 0: + # Give up on breaking. + break + + self.output.write(leading_space + text[0:space] + ' $\n') + text = text[space+1:] + + # Subsequent lines are continuations, so indent them. + leading_space = ' ' * (indent+2) + + self.output.write(leading_space + text + '\n') + + def close(self): + self.output.close() + + +def as_list(input): + if input is None: + return [] + if isinstance(input, list): + return input + return [input] + + +def escape(string): + """Escape a string such that it can be embedded into a Ninja file without + further interpretation.""" + assert '\n' not in string, 'Ninja syntax does not allow newlines' + # We only have one special metacharacter: '$'. + return string.replace('$', '$$') + + +def expand(string, vars, local_vars={}): + """Expand a string containing $vars as Ninja would. + + Note: doesn't handle the full Ninja variable syntax, but it's enough + to make configure.py's use of it work. + """ + def exp(m): + var = m.group(1) + if var == '$': + return '$' + return local_vars.get(var, vars.get(var, '')) + return re.sub(r'\$(\$|\w*)', exp, string) diff --git a/spacetime/spacetime.py b/spacetime/spacetime.py new file mode 100644 index 0000000000000000000000000000000000000000..da923aa062fef41223a339928a3fce36041e75de --- /dev/null +++ b/spacetime/spacetime.py @@ -0,0 +1,123 @@ +import sys +from ninja_syntax import Writer +import os + + +def scan_assets(build_root): + print(f'[spacetime] Scanning {build_root}/assets/src for assets') + assets = [] + for entry in os.scandir(f'{build_root}/assets/src'): + if entry.is_file() and entry.name.endswith('.ink.svg'): + assets.append(f'{build_root}/assets/src/{entry.name}') + print(f'[spacetime] Found {len(assets)} assets') + return assets + + +default_asset_size = 512 +asset_override = { + 'earth.ink.svg': 2048 +} + + +def gen_inkscape_rules_for_asset_size(size, writer): + writer.rule(f'inkscape_{size}px_full', f'inkscape -w {size * 1} -h {size * 1} $in -o $out') + writer.rule(f'inkscape_{size}px_375', f'inkscape -w {int(size * 0.375)} -h {int(size * 0.375)} $in -o $out') + writer.rule(f'inkscape_{size}px_125', f'inkscape -w {int(size * 0.125)} -h {int(size * 0.125)} $in -o $out') + + +def gen_inkscape_rules_for_asset_sizes(writer): + gen_inkscape_rules_for_asset_size(default_asset_size, writer) + for override in asset_override: + gen_inkscape_rules_for_asset_size(asset_override[override], writer) + + +def asset_size(asset): + if asset.split('/')[-1] in asset_override: + return asset_override[asset.split('/')[-1]] + else: + return default_asset_size + + +def gen_inkscape_rules_for_asset(root, asset, writer, files_375, files_full, files_125): + in_file = asset + out_file_name = asset.split('.')[0].split('/')[-1] + + out_full = f'{root}/assets/final/full/{out_file_name}.png' + files_full.append(out_full) + rule_full = f'inkscape_{asset_size(asset)}px_full' + + out_375 = f'{root}/assets/final/375/{out_file_name}.png' + files_375.append(out_375) + rule_375 = f'inkscape_{asset_size(asset)}px_375' + + out_125 = f'{root}/assets/final/125/{out_file_name}.png' + files_125.append(out_125) + rule_125 = f'inkscape_{asset_size(asset)}px_125' + + writer.build([out_full], rule_full, [in_file]) + writer.build([out_375], rule_375, [in_file]) + writer.build([out_125], rule_125, [in_file]) + + +def gen_inkscape(root, assets, writer, files_375, files_full, files_125): + gen_inkscape_rules_for_asset_sizes(writer) + + for asset in assets: + gen_inkscape_rules_for_asset(root, asset, writer, files_375, files_full, files_125) + + +def gen_packers(root, writer, files_375, files_full, files_125): + # sheep pack assets/final/full/*.png -f amethyst_named -o assets/dist/spritesheet-full + writer.rule(f'pack', 'sheep pack -f amethyst_named -o $out $in && touch $out') + + writer.build(f'{root}/assets/dist/spritesheet-full', 'pack', inputs=files_full, + implicit_outputs=[f'{root}/assets/dist/spritesheet-full.png', + f'{root}/assets/dist/spritesheet-full.ron']) + writer.build(f'asset-full', 'phony', inputs=[f'{root}/assets/dist/spritesheet-full']) + + writer.build(f'{root}/assets/dist/spritesheet-375', 'pack', inputs=files_375, + implicit_outputs=[f'{root}/assets/dist/spritesheet-375.png', + f'{root}/assets/dist/spritesheet-375.ron']) + writer.build(f'asset-375', 'phony', inputs=[f'{root}/assets/dist/spritesheet-375']) + + writer.build(f'{root}/assets/dist/spritesheet-125', 'pack', inputs=files_125, + implicit_outputs=[f'{root}/assets/dist/spritesheet-125.png', + f'{root}/assets/dist/spritesheet-125.ron']) + writer.build(f'asset-125', 'phony', inputs=[f'{root}/assets/dist/spritesheet-125']) + + writer.build(f'asset', 'phony', + inputs=[f'{root}/assets/dist/spritesheet-full', f'{root}/assets/dist/spritesheet-375', + f'{root}/assets/dist/spritesheet-125']) + + +def generate_assets_build_command(root, assets, writer): + files_full = [] + files_375 = [] + files_125 = [] + gen_inkscape(root, assets, writer, files_375, files_full, files_125) + gen_packers(root, writer, files_375, files_full, files_125) + + +def main(): + target = sys.argv[1] + env = sys.argv[2] + root = sys.argv[3] + print(f'[spacetime] Configuring target {target} with ENV={env}, buildroot={root}') + + with open(f'{root}/build.ninja', 'w') as f: + writer = Writer(f) + + writer.comment('Generated by spacetime.py') + writer.comment('Do not manually edit this file') + + if env == 'prod' or target == 'asset': + assets = scan_assets(root) + generate_assets_build_command(root, assets, writer) + + + + print(f'[spacetime] Configured build') + + +if __name__ == "__main__": + main() diff --git a/st b/st new file mode 100755 index 0000000000000000000000000000000000000000..94d37460bc8744d87f2352cf0f69ffbe9a9ec8c6 --- /dev/null +++ b/st @@ -0,0 +1,114 @@ +#!/bin/bash + +set -e + +SCRIPT_PATH=$(readlink -f "${BASH_SOURCE:-$0}") +SCRIPT_DIR=$(dirname "$SCRIPT_PATH") + +exec_spacetime() { + # args: target, environment, build root + echo "[*] Running configure command: 'python3 $SCRIPT_DIR/spacetime/spacetime.py $1 $2 $SCRIPT_DIR'" + python3 "$SCRIPT_DIR/spacetime/spacetime.py" "$1" "$2" "$SCRIPT_DIR" +} + +exec_ninja() { + # args: target + echo "[*] Running build command: 'ninja -C $SCRIPT_DIR $1'" + ninja -C "$SCRIPT_DIR" "$1" +} + +sub_help() { + echo "Spacetime - StarKingdoms build utility" + echo "Spacetime is a small utility program to generate Ninja build manifests for compiling StarKingdoms." + echo "Available targets:" + echo " help - Show this help screen" # done + echo " run_http - Compile the client and run a development http server for testing it" + echo " run_http_prod - Compile the client in production mode and run a development http server for testing it" + echo " run_server (default) - Compile and run the game server" + echo " build_client_bundle - Compile an optimized WebAssembly client bundle" + echo " build_client_bundle_prod - Compile an optimized WebAssembly client bundle using textures-fast" + echo " install_tooling - Install the compilation utilities required for compiling StarKingdoms" # done + echo " build_assets - Compile spritesheets in all three texture sizes for textures-fast" # done + echo " build_assets_full - Compile spritesheets in full size for textures-fast" # done + echo " build_assets_375 - Commpile 37.5% spritesheets for textures-fast" # done + echo " build_assets_125 - Compile 12.5% spritesheets for textures-fast" # done + echo " clean - Remove all generated files" +} + +check_install_cargo() { + echo "[*] Checking for $1" + if ! command -v "$1" &> /dev/null + then + echo "[+] $1 was not found, installing via Cargo..." + cargo install "$2" $3 + fi +} + +check() { + echo "[*] Checking for $1" + if ! command -v "$1" &> /dev/null + then + echo "[x] $1 was not found but is required for the build process to continue. Install it with your system package manager, or, if supported, 'st install_tooling'" + exit 1 + fi +} + +check_all() { + check wasm-pack + check sheep + check inkscape +} + +sub_install_tooling() { + check_install_cargo wasm-pack wasm-pack --no-default-features + check_install_cargo sheep sheep_cli + check inkscape + echo "[*] All required tools are installed" +} + +sub_build_client_bundle() { + check_all + exec_spacetime client dev "$SCRIPT_DIR" + exec_ninja client +} + +sub_build_assets() { + check_all + exec_spacetime asset dev "$SCRIPT_DIR" + exec_ninja asset +} + +sub_build_assets_full() { + check_all + exec_spacetime asset dev "$SCRIPT_DIR" + exec_ninja asset-full +} + +sub_build_assets_375() { + check_all + exec_spacetime asset dev "$SCRIPT_DIR" + exec_ninja asset-375 +} + +sub_build_assets_125() { + check_all + exec_spacetime asset dev "$SCRIPT_DIR" + exec_ninja asset-125 +} + +subcommand=$1 +case $subcommand in + "" | "-h" | "--help" | "help") + sub_help + ;; + *) + echo "[*] Running build command $subcommand" + shift + sub_${subcommand} $@ + if [ $? = 127 ]; then + echo "Error: '$subcommand' is not a known subcommand." >&2 + echo " Run 'st --help' for a list of known subcommands." >&2 + exit 1 + fi + ;; +esac \ No newline at end of file