mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-19 21:37:57 +08:00
619 lines
20 KiB
Python
Executable File
619 lines
20 KiB
Python
Executable File
# 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')
|
|
|
|
|
|
@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
|
|
|
|
|
|
class ResourcesMakefileGenerator:
|
|
"""Does the thing."""
|
|
|
|
def __init__(
|
|
self,
|
|
projroot: str,
|
|
existing_data: str,
|
|
projname: str,
|
|
) -> None:
|
|
from efrotools 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()
|
|
|
|
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 []
|