Source code for batools.resourcesmakefile

# Released under the MIT License. See LICENSE for details.
#
"""Generate our resources Makefile.

(builds things like icons, banners, images, etc.)
"""

from __future__ import annotations

import os
from pathlib import Path
from dataclasses import dataclass

from efro.error import CleanError


# These paths need to be relative to the dir we're writing the Makefile to.
ROOT_DIR = '$(PROJ_DIR)'
TOOLS_DIR = '$(TOOLS_DIR)'
BUILD_DIR = '$(BUILD_DIR)'
RESIZE_CMD = os.path.join(TOOLS_DIR, 'pcommand resize_image')


[docs] @dataclass class Target: """A target to be added to the Makefile.""" src: list[str] dst: str cmd: str mkdir: bool = False
[docs] 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
[docs] class ResourcesMakefileGenerator: """Does the thing.""" def __init__( self, projroot: str, existing_data: str, projname: str, ) -> None: from efrotools.project import getprojectconfig self.public = getprojectconfig(Path(projroot))['public'] assert isinstance(self.public, bool) self.existing_data = existing_data self.projroot = projroot self.targets: list[Target] = [] # Regular and lowercase project name. self.nameu = projname self.namel = projname.lower()
[docs] def run(self) -> str: """Does the thing.""" # fname = 'src/resources/Makefile' # with open(fname, encoding='utf-8') as infile: # original = infile.read() original = self.existing_data 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) basename = 'public' our_lines_public = ( _empty_line_if(bool(self.targets)) + self._emit_group_build_lines(basename) + self._emit_group_clean_lines(basename) + [t.emit() for t in self.targets] ) # Only rewrite the private section in the private repo; otherwise # keep the existing one intact. if self.public: our_lines_private = lines[auto_start_private + 1 : auto_end_private] else: # Private targets (available in public through efrocache) self.targets = [] basename = 'private' self._add_windows_icon(generic=True, oculus=False, inputs=False) our_lines_private_1 = ( _empty_line_if(bool(self.targets)) + self._emit_group_build_lines(basename) + self._emit_group_clean_lines(basename) + ['# __EFROCACHE_TARGET__\n' + t.emit() for t in self.targets] + self._emit_group_efrocache_lines() ) # Private-internal targets (not available at all in public) self.targets = [] basename = 'private-internal' self._add_windows_icon(generic=False, oculus=True, inputs=True) self._add_ios_app_icon() self._add_macos_app_icon() self._add_android_app_icon() self._add_android_app_icon_new() self._add_android_cardboard_app_icon() self._add_android_cardboard_app_icon_new() self._add_android_tv_banner() self._add_apple_tv_top_shelf() self._add_apple_tv_3d_icon() self._add_apple_tv_store_icon() self._add_google_vr_icon() self._add_macos_cursor() our_lines_private_2 = ( ['# __PUBSYNC_STRIP_BEGIN__'] + _empty_line_if(bool(self.targets)) + self._emit_group_build_lines(basename) + self._emit_group_clean_lines(basename) + [t.emit() for t in self.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' return out
def _emit_group_build_lines(self, basename: str) -> list[str]: """Gen a group build target.""" del basename # Unused. out: list[str] = [] if not self.targets: return out all_dsts = set() for target in self.targets: all_dsts.add(target.dst) out.append( "# Add this section's targets to the overall resources target.\n" 'resources: \\\n ' + ' \\\n '.join( dst.replace(' ', '\\ ') for dst in sorted(all_dsts) ) + '\n' ) return out def _emit_group_clean_lines(self, basename: str) -> list[str]: """Gen a group clean target.""" out: list[str] = [] if not self.targets: return out all_dsts = set() for target in self.targets: all_dsts.add(target.dst) out.append( f'clean-{basename}:\n\trm -f ' + ' \\\n '.join('"' + dst + '"' for dst in sorted(all_dsts)) + '\n' ) out.append( '# Include this section in an overall clean.\n' f'clean: clean-{basename}\n' ) return out def _emit_group_efrocache_lines(self) -> list[str]: """Gen a group clean target.""" out: list[str] = [] if not self.targets: return out all_dsts = set() for target in self.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('efrocache-build: resources\n') return out def _add_windows_icon( self, generic: bool, oculus: bool, inputs: bool ) -> None: sizes = [256, 128, 96, 64, 48, 32, 16] all_icons = [] for size in sizes: dst_base = BUILD_DIR src = os.path.join('icon', 'icon_clipped.png') dst = os.path.join(dst_base, 'win_icon_' + str(size) + '_tmp.png') cmd = ' '.join( [ RESIZE_CMD, str(size), str(size), '"' + src + '"', '"' + dst + '"', ] ) all_icons.append(dst) if inputs: self.targets.append( Target(src=[src], dst=dst, cmd=cmd, mkdir=True) ) # Assemble all the bits we just made into .ico files. for path, enable in [ ( f'{ROOT_DIR}/{self.namel}-windows/Generic/{self.nameu}.ico', generic, ), ( f'{ROOT_DIR}/{self.namel}-windows/Oculus/{self.nameu}.ico', oculus, ), ]: cmd = ( 'convert ' + ''.join([' "' + f + '"' for f in all_icons]) + ' "' + path + '"' ) if enable: self.targets.append(Target(src=all_icons, dst=path, cmd=cmd)) def _add_ios_app_icon(self) -> None: sizes = [ # (20, 2), # (20, 3), # (29, 2), # (29, 3), # (40, 2), # (40, 3), # (60, 2), # (60, 3), # (20, 1), # (29, 1), # (40, 1), # (76, 1), # (76, 2), # (83.5, 2), (1024, 1), ] for size in sizes: res = int(size[0] * size[1]) src = os.path.join('icon', 'icon_flat.png') dst = os.path.join( ROOT_DIR, f'{self.namel}-xcode', f'{self.nameu} Shared', 'Assets.xcassets', 'AppIcon iOS.appiconset', 'icon_' + str(size[0]) + 'x' + str(size[1]) + '.png', ) cmd = ' '.join( [ RESIZE_CMD, str(res), str(res), '"' + src + '"', '"' + dst + '"', ] ) self.targets.append(Target(src=[src], dst=dst, cmd=cmd)) def _add_macos_app_icon(self) -> None: sizes = [ (16, 1), (16, 2), (32, 1), (32, 2), (128, 1), (128, 2), (256, 1), (256, 2), (512, 1), (512, 2), ] for size in sizes: res = int(size[0] * size[1]) # The largest size gets used by the Mac App Store, and lots # of games seem to fill their entire icon canvas instead of # sticking with the big-sur icon size, so ours looks kinda # small next to those if we don't do the same. Strangely, # iOS apps in the Mac App Store also show up large like that # (as of Nov 2023). So we use a separate as-big-as-possible # icon for our largest size only. The downside of this is # our icon changes in appearance if someone cranks the # finder view options icon size slider all the way up, but # who actually does that? srcname = ( 'icon_clipped_mac_app_store.png' if size[0] == 512 else 'icon_clipped_mac.png' ) src = os.path.join('icon', srcname) dst = os.path.join( ROOT_DIR, f'{self.namel}-xcode', f'{self.nameu} Shared', 'Assets.xcassets', 'AppIcon macOS.appiconset', 'icon_' + str(size[0]) + 'x' + str(size[1]) + '.png', ) cmd = ' '.join( [ RESIZE_CMD, str(res), str(res), '"' + src + '"', '"' + dst + '"', ] ) self.targets.append(Target(src=[src], dst=dst, cmd=cmd)) def _add_android_app_icon( self, src_name: str = 'icon_clipped.png', variant_name: str = 'main', ) -> None: sizes = [ ('mdpi', 48), ('hdpi', 72), ('xhdpi', 96), ('xxhdpi', 144), ('xxxhdpi', 192), ] for size in sizes: res = size[1] src = os.path.join('icon', src_name) dst = os.path.join( ROOT_DIR, f'{self.namel}-android', f'{self.nameu}', 'src', variant_name, 'res', 'mipmap-' + size[0], 'ic_launcher.png', ) cmd = ' '.join( [ RESIZE_CMD, str(res), str(res), '"' + src + '"', '"' + dst + '"', ] ) self.targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True)) def _add_android_app_icon_new( self, src_fg_name: str = 'icon_android_layered_fg.png', src_bg_name: str = 'icon_android_layered_bg.png', variant_name: str = 'main', ) -> None: sizes = [ ('mdpi', 108), ('hdpi', 162), ('xhdpi', 216), ('xxhdpi', 324), ('xxxhdpi', 432), ] for size in sizes: res = size[1] # Foreground component. src = os.path.join('icon', src_fg_name) dst = os.path.join( ROOT_DIR, f'{self.namel}-android', f'{self.nameu}', 'src', variant_name, 'res', 'mipmap-' + size[0], 'ic_launcher_foreground.png', ) cmd = ' '.join( [ RESIZE_CMD, str(res), str(res), '"' + src + '"', '"' + dst + '"', ] ) self.targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True)) # Background component. src = os.path.join('icon', src_bg_name) dst = os.path.join( ROOT_DIR, f'{self.namel}-android', f'{self.nameu}', 'src', variant_name, 'res', 'mipmap-' + size[0], 'ic_launcher_background.png', ) cmd = ' '.join( [ RESIZE_CMD, str(res), str(res), '"' + src + '"', '"' + dst + '"', ] ) self.targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True)) def _add_android_cardboard_app_icon(self) -> None: self._add_android_app_icon( src_name='icon_clipped_vr.png', variant_name='cardboard', ) def _add_android_cardboard_app_icon_new(self) -> None: self._add_android_app_icon_new( src_fg_name='icon_android_layered_fg_vr.png', variant_name='cardboard', ) def _add_android_tv_banner(self) -> None: res = (320, 180) src = os.path.join('banner', 'banner_16x9.png') dst = os.path.join( ROOT_DIR, f'{self.namel}-android', f'{self.nameu}', 'src', 'main', 'res', 'drawable-xhdpi', 'banner.png', ) cmd = ' '.join( [ RESIZE_CMD, str(res[0]), str(res[1]), '"' + src + '"', '"' + dst + '"', ] ) self.targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True)) def _add_apple_tv_top_shelf(self) -> None: instances = [ ('24x9', '', '', 1920, 720), ('29x9', ' Wide', '_wide', 2320, 720), ] for instance in instances: for scale in [1, 2]: res = (instance[3] * scale, instance[4] * scale) src = os.path.join('banner', 'banner_' + instance[0] + '.png') dst = os.path.join( ROOT_DIR, f'{self.namel}-xcode', f'{self.nameu} Shared', 'Assets.xcassets', 'tvOS App Icon & Top Shelf Image.brandassets', 'Top Shelf Image' + instance[1] + '.imageset', 'shelf' + instance[2] + '_' + str(scale) + 'x.png', ) cmd = ' '.join( [ RESIZE_CMD, str(res[0]), str(res[1]), '"' + src + '"', '"' + dst + '"', ] ) self.targets.append(Target(src=[src], dst=dst, cmd=cmd)) def _add_apple_tv_3d_icon(self) -> None: res = (400, 240) for layer in ['Layer1', 'Layer2', 'Layer3', 'Layer4', 'Layer5']: for scale in [1, 2]: src = os.path.join('icon_appletv', layer.lower() + '.png') dst = os.path.join( ROOT_DIR, f'{self.namel}-xcode', f'{self.nameu} Shared', 'Assets.xcassets', 'tvOS App Icon & Top Shelf Image.brandassets', 'App Icon.imagestack', layer + '.imagestacklayer', 'Content.imageset', layer.lower() + '_' + str(scale) + 'x.png', ) cmd = ' '.join( [ RESIZE_CMD, str(res[0] * scale), str(res[1] * scale), '"' + src + '"', '"' + dst + '"', ] ) self.targets.append(Target(src=[src], dst=dst, cmd=cmd)) def _add_apple_tv_store_icon(self) -> None: res = (1280, 768) for layer in ['Layer1', 'Layer2', 'Layer3', 'Layer4', 'Layer5']: for scale in [1]: src = os.path.join('icon_appletv', layer.lower() + '.png') dst = os.path.join( ROOT_DIR, f'{self.namel}-xcode', f'{self.nameu} Shared', 'Assets.xcassets', 'tvOS App Icon & Top Shelf Image.brandassets', 'App Icon - App Store.imagestack', layer + '.imagestacklayer', 'Content.imageset', layer.lower() + '_' + str(scale) + 'x.png', ) cmd = ' '.join( [ RESIZE_CMD, str(res[0] * scale), str(res[1] * scale), '"' + src + '"', '"' + dst + '"', ] ) self.targets.append(Target(src=[src], dst=dst, cmd=cmd)) def _add_google_vr_icon(self) -> None: res = (512, 512) for layer in ['vr_icon_background', 'vr_icon']: src = os.path.join('icon_googlevr', layer + '.png') dst = os.path.join( ROOT_DIR, f'{self.namel}-android', f'{self.nameu}', 'src', 'cardboard', 'res', 'drawable-nodpi', layer + '.png', ) cmd = ' '.join( [ RESIZE_CMD, str(res[0]), str(res[1]), '"' + src + '"', '"' + dst + '"', ] ) self.targets.append(Target(src=[src], dst=dst, cmd=cmd, mkdir=True)) def _add_macos_cursor(self) -> None: sizes = [ (64, 1), (64, 2), ] for size in sizes: res = int(size[0] * size[1]) src = os.path.join('cursor.png') dst = os.path.join( ROOT_DIR, f'{self.namel}-xcode', f'{self.nameu} Shared', 'Assets.xcassets', 'Cursor macOS.imageset', 'cursor_' + str(size[0]) + 'x' + str(size[1]) + '.png', ) cmd = ' '.join( [ RESIZE_CMD, str(res), str(res), '"' + src + '"', '"' + dst + '"', ] ) self.targets.append(Target(src=[src], dst=dst, cmd=cmd))
def _empty_line_if(condition: bool) -> list[str]: return [''] if condition else []