~starkingdoms/starkingdoms

85f89d3a734b494aa211f8a5bcf4cc2430cd4889 — c0repwn3r 2 years ago e259a67
[wip] spacetime build beginnings
4 files changed, 441 insertions(+), 1 deletions(-)

M .gitignore
A spacetime/ninja_syntax.py
A spacetime/spacetime.py
A st
M .gitignore => .gitignore +5 -1
@@ 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

A spacetime/ninja_syntax.py => spacetime/ninja_syntax.py +199 -0
@@ 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)

A spacetime/spacetime.py => spacetime/spacetime.py +123 -0
@@ 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()

A st => st +114 -0
@@ 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