# Released under the MIT License. See LICENSE for details. # """Procedurally regenerates our code Makefile. This Makefiles builds our generated code such as encrypted python strings, node types, etc). """ from __future__ import annotations import os import subprocess from pathlib import Path from typing import TYPE_CHECKING from dataclasses import dataclass from efro.error import CleanError from efro.terminal import Clr from efrotools import getconfig if TYPE_CHECKING: from typing import Optional, Set, List, Dict, Any, Tuple # These paths need to be relative to the dir we're writing the Makefile to. TOOLS_DIR = '../../tools' ROOT_DIR = '../..' OUT_DIR_CPP = '../ballistica/generated' @dataclass class Target: """A target to be added to the Makefile.""" src: List[str] dst: str cmd: str mkdir: bool = False def emit(self) -> str: """Gen a Makefile target.""" out: str = self.dst.replace(' ', '\\ ') out += ' : ' + ' '.join(s for s in self.src) + ( ('\n\t@mkdir -p "' + os.path.dirname(self.dst) + '"') if self.mkdir else '') + '\n\t@' + self.cmd + '\n' return out def _emit_group_build_lines(targets: List[Target], basename: str) -> List[str]: """Gen a group build target.""" del basename # Unused. out: List[str] = [] if not targets: return out all_dsts = set() for target in targets: all_dsts.add(target.dst) out.append('sources: \\\n ' + ' \\\n '.join( dst.replace(' ', '\\ ') for dst in sorted(all_dsts)) + '\n') return out def _emit_group_efrocache_lines(targets: List[Target], basename: str) -> List[str]: """Gen a group clean target.""" out: List[str] = [] if not targets: return out all_dsts = set() for target in targets: # We may need to make pipeline adjustments if/when we get filenames # with spaces in them. if ' ' in target.dst: raise CleanError('FIXME: need to account for spaces in filename' f' "{target.dst}".') all_dsts.add(target.dst) out.append('efrocache-list:\n\t@echo ' + ' \\\n '.join('"' + dst + '"' for dst in sorted(all_dsts)) + '\n') out.append(f'efrocache-build: resources-{basename}\n') return out def _add_python_embedded_targets(targets: List[Target]) -> None: pkg = 'bameta' for fname in os.listdir(f'src/meta/{pkg}/python_embedded'): if (not fname.endswith('.py') or fname == '__init__.py' or 'flycheck' in fname): continue name = os.path.splitext(fname)[0] src = [ f'{pkg}/python_embedded/{name}.py', os.path.join(TOOLS_DIR, 'batools', 'meta.py') ] dst = os.path.join(OUT_DIR_CPP, 'python_embedded', f'{name}.inc') if name == 'binding': targets.append( Target(src=src, dst=dst, cmd='$(PCOMMAND) gen_binding_code $< $@')) else: targets.append( Target( src=src, dst=dst, cmd=f'$(PCOMMAND) gen_flat_data_code $< $@ {name}_code')) def _add_python_embedded_targets_internal(targets: List[Target]) -> None: pkg = 'bametainternal' for fname in os.listdir(f'src/meta/{pkg}/python_embedded'): if (not fname.endswith('.py') or fname == '__init__.py' or 'flycheck' in fname): continue name = os.path.splitext(fname)[0] src = [ f'{pkg}/python_embedded/{name}.py', os.path.join(TOOLS_DIR, 'batoolsinternal', 'meta.py') ] dst = os.path.join(OUT_DIR_CPP, 'python_embedded', f'{name}.inc') targets.append( Target(src=src, dst=dst, cmd='$(PCOMMAND) gen_encrypted_python_code $< $@')) def _empty_line_if(condition: bool) -> List[str]: return [''] if condition else [] def update(projroot: str, check: bool) -> None: """Update the project meta Makefile.""" # pylint: disable=too-many-locals # Operate out of root dist dir for consistency. os.chdir(projroot) public = getconfig(Path('.'))['public'] assert isinstance(public, bool) fname = 'src/meta/Makefile' with open(fname) as infile: original = infile.read() lines = original.splitlines() auto_start_public = lines.index('#__AUTOGENERATED_PUBLIC_BEGIN__') auto_end_public = lines.index('#__AUTOGENERATED_PUBLIC_END__') auto_start_private = lines.index('#__AUTOGENERATED_PRIVATE_BEGIN__') auto_end_private = lines.index('#__AUTOGENERATED_PRIVATE_END__') # Public targets (full sources available in public) targets: List[Target] = [] basename = 'public' _add_python_embedded_targets(targets) our_lines_public = (_empty_line_if(bool(targets)) + _emit_group_build_lines(targets, basename) + [t.emit() for t in targets]) # Only rewrite the private section in the private repo; otherwise # keep the existing one intact. if public: our_lines_private = lines[auto_start_private + 1:auto_end_private] else: # Private targets (available in public through efrocache) targets = [] basename = 'private' our_lines_private_1 = ( _empty_line_if(bool(targets)) + _emit_group_build_lines(targets, basename) + ['#__EFROCACHE_TARGET__\n' + t.emit() for t in targets] + _emit_group_efrocache_lines(targets, basename)) # NOTE: currently not efrocaching anything here; we'll need to # add this makefile to the efrocache update if we ever do. assert not targets # Private-internal targets (not available at all in public) targets = [] basename = 'private-internal' _add_python_embedded_targets_internal(targets) our_lines_private_2 = (['#__PUBSYNC_STRIP_BEGIN__'] + _empty_line_if(bool(targets)) + _emit_group_build_lines(targets, basename) + [t.emit() for t in targets] + ['#__PUBSYNC_STRIP_END__']) our_lines_private = our_lines_private_1 + our_lines_private_2 filtered = (lines[:auto_start_public + 1] + our_lines_public + lines[auto_end_public:auto_start_private + 1] + our_lines_private + lines[auto_end_private:]) out = '\n'.join(filtered) + '\n' if out == original: print(f'{fname} is up to date.') else: if check: raise CleanError(f"ERROR: file is out of date: '{fname}'.") print(f'{Clr.SBLU}Updating {fname} (and cleaning existing output).' f'{Clr.RST}') with open(fname, 'w') as outfile: outfile.write(out) # Also clean existing meta output every time the Makefile changes; # this should minimize the chance of orphan outputs hanging around # causing trouble. subprocess.run(['make', 'clean'], cwd='src/meta', check=True)