Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson 2024-12-12 16:43:03 -08:00 committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions

16
tools/resources/README.md Normal file
View file

@ -0,0 +1,16 @@
Resource Generation
===================
The old resource code was crazy and slow. Let's redesign everything!
Design Goals
------------
1. Decouple processing different types of resources from each other into their own files
2. Be completely SDK vs Firmware independent. Any differences in behaviour between the two resource
generation variants should be captured in parameters as opposed to explicitly checking which
one we are.
3. No more shelling out
4. Capture as much intermediate state in the filesystem itself as possible as opposed to
generating large data structures that need to be done on each build.
5. Remove the need to put dynamically generated resource content like the bluetooth patch and
stored apps into our static resource definition json files for more modularity.

View file

@ -0,0 +1,14 @@
# Copyright 2024 Google LLC
#
# 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.

View file

@ -0,0 +1,69 @@
# Copyright 2024 Google LLC
#
# 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.
from pebble_sdk_platform import pebble_platforms, maybe_import_internal
import os
import os.path
import sys
from glob import glob
__author__ = 'katharine'
def find_most_specific_filename(bld, env, root_node, general_filename):
maybe_import_internal(env)
if '~' in general_filename:
bld.fatal("Generic resource filenames cannot contain a tilde (~).")
basename, extension = os.path.splitext(general_filename)
# The filenames we get will have extra bits of folder at the start, so trim those.
root_len = len(root_node.relpath()) + 1
if root_node.relpath() == '.':
root_len = 2
glob_result = glob("{}*{}".format(os.path.join(root_node.relpath(), basename), extension))
options = [x[root_len:] for x in glob_result if os.path.isfile(x)]
specificities = {}
try:
valid_tags = set(pebble_platforms[env.PLATFORM_NAME]['TAGS'])
except KeyError:
bld.fatal("Unrecognized platform %s. Did you mean to configure with --internal_sdk_build?" %
env.PLATFORM_NAME)
for option in options:
# We can get names that don't match if the name we have is a prefix of other files. Drop those.
if option.split('~', 1)[0] != basename:
continue
tags = set(os.path.splitext(option)[0].split('~')[1:])
# If there exist tags that aren't valid, we skip this.
if len(tags - valid_tags) > 0:
continue
# If it's valid, the specificity is the number of tags that exist in both sets.
specificities[option] = len(valid_tags & tags)
if len(specificities) == 0:
return general_filename
top_score = max(specificities.itervalues())
top_candidates = [k for k, v in specificities.iteritems() if v == top_score]
if len(top_candidates) > 1:
bld.fatal("The correct file for {general} on {platform} is ambiguous: {count} files have "
"specificity {score}:\n\t{files}".format(general=general_filename,
count=len(top_candidates), score=top_score, platform=env.PLATFORM_NAME,
files="\n\t".join(top_candidates)))
return top_candidates[0]

View file

@ -0,0 +1,14 @@
# Copyright 2024 Google LLC
#
# 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.

View file

@ -0,0 +1,91 @@
# Copyright 2024 Google LLC
#
# 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.
import os
from resources.find_resource_filename import find_most_specific_filename
from resources.types.resource_definition import ResourceDefinition, StorageType
# This is all the classes that have been registered by inheriting from the ResourceGenerator
# metaclass with a 'type' attribute set.
_ResourceGenerators = {}
class ResourceGeneratorMetaclass(type):
type = None
def __init__(cls, name, bases, dict):
super(ResourceGeneratorMetaclass, cls).__init__(name, bases, dict)
if cls.type:
_ResourceGenerators[cls.type] = cls
# Instatiate the metaclass into a baseclass we can use elsewhere.
ResourceGeneratorBase = ResourceGeneratorMetaclass('ResourceGenerator', (object,), {})
class ResourceGenerator(ResourceGeneratorBase):
@staticmethod
def definitions_from_dict(bld, definition_dict, resource_source_path):
"""
Default implementation of definitions_from_dict. Subclasses of ResourceGenerator can
override this implementation if they'd like to customize this. Returns a list of definitions.
"""
resource = {'name': definition_dict['name'],
'filename': str(definition_dict['file'] if 'file' in definition_dict else None)}
resources = [resource]
# Now generate ResourceDefintion objects for each resource
target_platforms = definition_dict.get('targetPlatforms', None)
aliases = definition_dict.get('aliases', [])
builtin = False if bld.variant == 'applib' else definition_dict.get('builtin', False)
definitions = []
for r in resources:
if resource['filename'] is not None:
filename_path = os.path.join(resource_source_path, r['filename'])
filename_path = find_most_specific_filename(bld, bld.env, bld.path, filename_path)
else:
filename_path = ''
storage = StorageType.builtin if builtin else StorageType.pbpack
d = ResourceDefinition(definition_dict['type'], r['name'],
filename_path, storage=storage,
target_platforms=target_platforms,
aliases=aliases)
if 'size' in r:
d.size = r['size']
definitions.append(d)
return definitions
@classmethod
def generate_object(cls, task, definition):
"""
Stub implementation of generate_object. Subclasses must override this method.
"""
raise NotImplemented('%r missing a generate_object implementation' % cls)
def definitions_from_dict(bld, definition_dict, resource_source_path):
cls = _ResourceGenerators[definition_dict['type']]
return cls.definitions_from_dict(bld, definition_dict, resource_source_path)
def generate_object(task, definition):
cls = _ResourceGenerators[definition.type]
return cls.generate_object(task, definition)

View file

@ -0,0 +1,151 @@
# Copyright 2024 Google LLC
#
# 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.
from resources.types.resource_object import ResourceObject
from resources.resource_map.resource_generator import ResourceGenerator
from pebble_sdk_platform import pebble_platforms
import bitmapgen
import png2pblpng
import re
PNG_MIN_APP_MEMORY = 0x8000 # 32k, fairly arbitrarily
class BitmapResourceGenerator(ResourceGenerator):
type = 'bitmap'
@staticmethod
def definitions_from_dict(bld, definition_dict, resource_source_path):
definitions = ResourceGenerator.definitions_from_dict(bld, definition_dict,
resource_source_path)
for d in definitions:
d.memory_format = definition_dict.get('memoryFormat', 'smallest')
d.space_optimization = definition_dict.get('spaceOptimization', None)
d.storage_format = definition_dict.get('storageFormat', None)
return definitions
@classmethod
def generate_object(cls, task, definition):
env = task.generator.env
memory_format = definition.memory_format.lower()
# Some options are mutually exclusive.
if definition.space_optimization == 'memory' and definition.storage_format == 'png':
task.generator.bld.fatal("{}: spaceOptimization: memory and storageFormat: "
"png are mutually exclusive.".format(definition.name))
if definition.space_optimization == 'storage' and definition.storage_format == 'pbi':
task.generator.bld.fatal("{}: spaceOptimization: storage and storageFormat: "
"pbi are mutually exclusive.".format(definition.name))
if definition.storage_format == 'png' and memory_format == '1bit':
task.generator.bld.fatal("{}: PNG storage does not support non-palettised 1-bit images."
.format(definition.name))
# If storage_format is not specified, it is completely determined by space_optimization.
if definition.storage_format is None and definition.space_optimization is not None:
format_mapping = {
'storage': 'png',
'memory': 'pbi',
}
try:
definition.storage_format = format_mapping[definition.space_optimization]
except KeyError:
task.generator.bld.fatal("{}: Invalid spaceOptimization '{}'. Use one of {}."
.format(definition.name, definition.space_optimization,
', '.join(format_mapping.keys())))
if definition.storage_format is None:
if pebble_platforms[env.PLATFORM_NAME]['MAX_APP_MEMORY_SIZE'] < PNG_MIN_APP_MEMORY:
definition.storage_format = 'pbi'
else:
definition.storage_format = 'png'
# At this point, what we want to do should be completely determined (though, depending on
# image content, not necessarily actually possible); begin figuring out what to actually do.
is_color = 'color' in pebble_platforms[env.PLATFORM_NAME]['TAGS']
palette_name = png2pblpng.get_ideal_palette(is_color)
_, _, bitdepth, _ = png2pblpng.get_palette_for_png(task.inputs[0].abspath(), palette_name,
png2pblpng.DEFAULT_COLOR_REDUCTION)
formats = {
'1bit',
'8bit',
'smallest',
'smallestpalette',
'1bitpalette',
'2bitpalette',
'4bitpalette',
}
if memory_format not in formats:
task.generator.bld.fatal("{}: Invalid memoryFormat {} (pick one of {})."
.format(definition.name, definition.memory_format,
', '.join(formats)))
# "smallest" is always palettised, unless the image has too many colours, in which case it
# cannot be.
if memory_format == 'smallest':
if bitdepth <= 4:
memory_format = 'smallestpalette'
else:
memory_format = '8bit'
if 'palette' in memory_format:
if memory_format == 'smallestpalette':
# If they asked for "smallestpalette", replace that with its actual value.
if bitdepth > 4:
task.generator.bld.fatal("{} has too many colours for a palettised image"
"(max 16), but 'SmallestPalette' specified."
.format(definition.name))
else:
memory_format = '{}bitpalette'.format(bitdepth)
# Pull out however many bits we're supposed to use (which is exact, not a min or max)
bits = int(re.match(r'^(\d+)bitpalette$', memory_format).group(1))
if bits < bitdepth:
task.generator.bld.fatal("{}: requires at least {} bits.".format(definition.name,
bitdepth))
if bits > 2 and not is_color:
task.generator.bld.fatal("{}: can't use more than two bits on a black-and-white"
"platform." .format(definition.name))
if definition.storage_format == 'pbi':
pb = bitmapgen.PebbleBitmap(task.inputs[0].abspath(), bitmap_format='color',
bitdepth=bits, crop=False, palette_name=palette_name)
return ResourceObject(definition, pb.convert_to_pbi())
else:
image_bytes = png2pblpng.convert_png_to_pebble_png_bytes(task.inputs[0].abspath(),
palette_name,
bitdepth=bits)
return ResourceObject(definition, image_bytes)
else:
if memory_format == '1bit':
pb = bitmapgen.PebbleBitmap(task.inputs[0].abspath(), bitmap_format='bw',
crop=False)
return ResourceObject(definition, pb.convert_to_pbi())
elif memory_format == '8bit':
if not is_color:
task.generator.bld.fatal("{}: can't use more than two bits on a black-and-white"
"platform.".format(definition.name))
# generate an 8-bit pbi or png, as appropriate.
if definition.storage_format == 'pbi':
pb = bitmapgen.PebbleBitmap(task.inputs[0].abspath(), bitmap_format='color_raw',
crop=False, palette_name=palette_name)
return ResourceObject(definition, pb.convert_to_pbi())
else:
image_bytes = png2pblpng.convert_png_to_pebble_png_bytes(
task.inputs[0].abspath(), palette_name, bitdepth=8)
return ResourceObject(definition, image_bytes)
raise Exception("Got to the end without doing anything?")

View file

@ -0,0 +1,101 @@
# Copyright 2024 Google LLC
#
# 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.
from resources.types.resource_object import ResourceObject
from resources.resource_map.resource_generator import ResourceGenerator
from font.fontgen import Font, MAX_GLYPHS_EXTENDED, MAX_GLYPHS
from pebble_sdk_platform import pebble_platforms, maybe_import_internal
from threading import Lock
import re
class FontResourceGenerator(ResourceGenerator):
"""
ResourceGenerator for the 'font' type
"""
type = 'font'
lock = Lock()
@staticmethod
def definitions_from_dict(bld, definition_dict, resource_source_path):
maybe_import_internal(bld.env)
definitions = ResourceGenerator.definitions_from_dict(bld, definition_dict,
resource_source_path)
# Parse additional font specific fields
for d in definitions:
d.max_glyph_size = pebble_platforms[bld.env.PLATFORM_NAME]['MAX_FONT_GLYPH_SIZE']
d.character_list = definition_dict.get('characterList')
d.character_regex = definition_dict.get('characterRegex')
d.compatibility = definition_dict.get('compatibility')
d.compress = definition_dict.get('compress')
d.extended = bool(definition_dict.get('extended'))
d.tracking_adjust = definition_dict.get('trackingAdjust')
return definitions
@classmethod
def generate_object(cls, task, definition):
font_data = cls.build_font_data(task.inputs[0].abspath(), definition)
return ResourceObject(definition, font_data)
@classmethod
def build_font_data(cls, ttf_path, definition):
# PBL-23964: it turns out that font generation is not thread-safe with freetype
# 2.4 (and possibly later versions). To avoid running into this, we use a lock.
with cls.lock:
height = FontResourceGenerator._get_font_height_from_name(definition.name)
is_legacy = definition.compatibility == "2.7"
max_glyphs = MAX_GLYPHS_EXTENDED if definition.extended else MAX_GLYPHS
font = Font(ttf_path, height, max_glyphs, definition.max_glyph_size, is_legacy)
if definition.character_regex is not None:
font.set_regex_filter(definition.character_regex.encode('utf8'))
if definition.character_list is not None:
font.set_codepoint_list(definition.character_list)
if definition.compress:
font.set_compression(definition.compress)
if definition.tracking_adjust is not None:
font.set_tracking_adjust(definition.tracking_adjust)
font.build_tables()
return font.bitstring()
@staticmethod
def _get_font_height_from_name(name):
"""
Search the name of the font for an integer which will be used as the
pixel height of the generated font
"""
match = re.search('([0-9]+)', name)
if match is None:
if name != 'FONT_FALLBACK' and name != 'FONT_FALLBACK_INTERNAL':
raise ValueError('Font {0}: no height found in name\n'.format(name))
return 14
return int(match.group(0))

View file

@ -0,0 +1,70 @@
# Copyright 2024 Google LLC
#
# 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.
import json
from subprocess import Popen, PIPE
from resources.types.resource_object import ResourceObject
from resources.resource_map.resource_generator import ResourceGenerator
class JsResourceGenerator(ResourceGenerator):
type = 'js'
@staticmethod
def generate_object(task, definition):
node_command = task.generator.env.NODE
# FIXME:
# We use this file from both our firmware build (waf version 1.8.x) and from our sdk build
# (waf version 1.7.x) and the functionality of Configure::find_program has changed between
# these two release. On waf 1.7.x, it returns a list, where in waf 1.8.x it gives us a
# string. Flatten it into a string regardless of version. To fix this we should just
# update our SDK waf as it's redistributed alongside this file.
if len(node_command) == 1:
node_command = node_command[0]
script_node = task.generator.env.JS_TOOLING_SCRIPT
bytecode = task.outputs[0].change_ext('.bytecode')
bytecode.parent.mkdir()
memory_usage_output = (
task.generator.bld.path.get_bld().make_node("{}_snapshot_size.json".
format(task.generator.env.PLATFORM_NAME)))
cmd = [node_command,
script_node.abspath(),
task.inputs[0].abspath(),
bytecode.abspath(),
memory_usage_output.abspath()]
proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
out, err = proc.communicate()
if proc.returncode != 0:
task.generator.bld.fatal("JS compilation failed.\n"
"STDOUT: {}\n"
"STDERR: {}".format(out, err))
# Save bytecode computed size and max size for SDK memory report
if task.generator.env.PLATFORM_NAME in task.generator.bld.all_envs:
env = task.generator.bld.all_envs[task.generator.env.PLATFORM_NAME]
with open(memory_usage_output.abspath(), 'r') as f:
content = json.load(f)
env.SNAPSHOT_SIZE = content['size']
env.SNAPSHOT_MAX = content['max']
with open(bytecode.abspath(), 'rb') as f:
data = f.read()
return ResourceObject(definition, data)

View file

@ -0,0 +1,62 @@
# Copyright 2024 Google LLC
#
# 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.
from resources.types.resource_object import ResourceObject
from resources.resource_map.resource_generator import ResourceGenerator
from pebble_sdk_platform import pebble_platforms
import bitmapgen
def _pbi_generator(task, definition, format_str):
pb = bitmapgen.PebbleBitmap(task.inputs[0].abspath(), bitmap_format=format_str)
return ResourceObject(definition, pb.convert_to_pbi())
class PbiResourceGenerator(ResourceGenerator):
type = 'pbi'
@staticmethod
def generate_object(task, definition):
return _pbi_generator(task, definition, 'bw')
class Pbi8ResourceGenerator(ResourceGenerator):
type = 'pbi8'
@staticmethod
def generate_object(task, definition):
env = task.generator.env
format = 'color' if 'color' in pebble_platforms[env.PLATFORM_NAME]['TAGS'] else 'bw'
return _pbi_generator(task, definition, format)
# This implementation is in the "pbi" file because it's implemented using pbis, even though it's
# named with "png" prefix and we have a "png" file.
class PngTransResourceGenerator(ResourceGenerator):
type = 'png-trans'
@staticmethod
def generate_object(task, definition):
if 'WHITE' in definition.name:
color_map = bitmapgen.WHITE_COLOR_MAP
elif 'BLACK' in definition.name:
color_map = bitmapgen.BLACK_COLOR_MAP
else:
task.generator.bld.fatal('png-trans with neither white nor black in the name: ' +
resource_definition.name)
pb = bitmapgen.PebbleBitmap(task.inputs[0].abspath(), color_map=color_map)
return ResourceObject(definition, pb.convert_to_pbi())

View file

@ -0,0 +1,65 @@
# Copyright 2024 Google LLC
#
# 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.
from resources.types.resource_object import ResourceObject
from resources.resource_map.resource_generator import ResourceGenerator
from tools.generate_pdcs import pdc_gen
import os
class ResourceGeneratorPdc(ResourceGenerator):
type = 'pdc'
@staticmethod
def definitions_from_dict(bld, definition_dict, resource_source_path):
definitions = ResourceGenerator.definitions_from_dict(bld, definition_dict,
resource_source_path)
source_root = bld.path.make_node('.')
for d in definitions:
node = bld.path.find_node(d.file)
# PDCS (animated vectors) are described as a folder path. Adjust the sources so if any
# frame in the animation changes we regenerate the pdcs
if os.path.isdir(node.abspath()):
source_nodes = bld.path.find_node(d.file).ant_glob("*.svg")
d.sources = [n.path_from(source_root) for n in source_nodes]
return definitions
@staticmethod
def generate_object(task, definition):
node = task.generator.path.make_node(definition.file)
if os.path.isdir(node.abspath()):
output, errors = pdc_gen.create_pdc_data_from_path(
node.abspath(),
viewbox_size=(0, 0),
verbose=False,
duration=33,
play_count=1,
precise=False)
else:
output, errors = pdc_gen.create_pdc_data_from_path(
task.inputs[0].abspath(),
viewbox_size=(0, 0),
verbose=False,
duration=0,
play_count=0,
precise=True)
return ResourceObject(definition, output)

View file

@ -0,0 +1,33 @@
# Copyright 2024 Google LLC
#
# 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.
from resources.types.resource_object import ResourceObject
from resources.resource_map.resource_generator import ResourceGenerator
from pebble_sdk_platform import pebble_platforms
import png2pblpng
class PngResourceGenerator(ResourceGenerator):
type = 'png'
@staticmethod
def generate_object(task, definition):
env = task.generator.env
is_color = 'color' in pebble_platforms[env.PLATFORM_NAME]['TAGS']
palette_name = png2pblpng.get_ideal_palette(is_color=is_color)
image_bytes = png2pblpng.convert_png_to_pebble_png_bytes(task.inputs[0].abspath(),
palette_name)
return ResourceObject(definition, image_bytes)

View file

@ -0,0 +1,24 @@
# Copyright 2024 Google LLC
#
# 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.
from resources.types.resource_object import ResourceObject
from resources.resource_map.resource_generator import ResourceGenerator
class ResourceGeneratorRaw(ResourceGenerator):
type = 'raw'
@staticmethod
def generate_object(task, definition):
with open(task.inputs[0].abspath(), 'rb') as f:
return ResourceObject(definition, f.read())

View file

@ -0,0 +1,33 @@
# Copyright 2024 Google LLC
#
# 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.
from resources.types.resource_object import ResourceObject
from resources.resource_map.resource_generator import ResourceGenerator
from pebble_sdk_platform import pebble_platforms
import json2vibe
import StringIO
class VibeResourceGenerator(ResourceGenerator):
type = 'vibe'
@staticmethod
def generate_object(task, definition):
out = StringIO.StringIO()
json2vibe.convert_to_file(task.inputs[0].abspath(), out)
return ResourceObject(definition, out.getvalue())

View file

@ -0,0 +1,14 @@
# Copyright 2024 Google LLC
#
# 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.

View file

@ -0,0 +1,55 @@
# Copyright 2024 Google LLC
#
# 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.
import pickle
class ResourceBall(object):
""" A object meant to be serialized to the filesystem that represents the complete set of
resources for a firmware variant. Resources that are distributed with the firmware are present
as ResourceObject instances where resources that are not (such as language packs) are present
as ResourceDeclaration instances. This data structure is ordered, with the resource_objects
conceptually coming first followed by the resource_declarations.
"""
def __init__(self, resource_objects, resource_declarations):
self.resource_objects = resource_objects
self.resource_declarations = resource_declarations
def get_all_declarations(self):
return [o.definition for o in self.resource_objects] + self.resource_declarations
def dump(self, output_node):
output_node.parent.mkdir()
with open(output_node.abspath(), 'wb') as f:
pickle.dump(self, f, pickle.HIGHEST_PROTOCOL)
@classmethod
def load(cls, path):
with open(path, 'rb') as f:
return pickle.load(f)
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('resball')
args = parser.parse_args()
rb = ResourceBall.load(args.resball)
for i, o in enumerate(rb.resource_objects, start=1):
print "%4u: %-50s %-10s %6u" % (i, o.definition.name, o.definition.type, len(o.data))

View file

@ -0,0 +1,23 @@
# Copyright 2024 Google LLC
#
# 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.
class ResourceDeclaration(object):
""" A resource that has a name and nothing else.
Used for resources that are stored on the filesystem. We don't know anything about these
resources other than that they exist at firmware compile time.
"""
def __init__(self, name):
self.name = name

View file

@ -0,0 +1,52 @@
# Copyright 2024 Google LLC
#
# 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.
import os
from resources.types.resource_declaration import ResourceDeclaration
class StorageType(object):
pbpack = 1
builtin = 2
pfs = 3
class ResourceDefinition(ResourceDeclaration):
def __init__(self, type, name, file, storage=StorageType.pbpack,
target_platforms=None, aliases=()):
self.type = type
self.name = name
self.file = file
self.storage = storage
# A list of platforms this resource is valid for. None means all platforms. [] means none.
self.target_platforms = target_platforms
self.aliases = list(aliases)
self.sources = [self.file]
def is_in_target_platform(self, bld):
if self.target_platforms is None:
return True
return bld.env.PLATFORM_NAME in self.target_platforms
def find_specific_filename(self, task_gen):
return find_most_specific_filename(task_gen.bld, task_gen.env, task_gen.bld.path, self.file)
def __repr__(self):
return '<ResourceDefinition %r>' % self.__dict__

View file

@ -0,0 +1,38 @@
# Copyright 2024 Google LLC
#
# 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.
import pickle
class ResourceObject(object):
"""
Defines a single resource object in a namespace. Must be serializable.
"""
def __init__(self, definition, data):
self.definition = definition
if isinstance(data, list):
self.data = b"".join(data)
else:
self.data = data
def dump(self, output_node):
output_node.parent.mkdir()
with open(output_node.abspath(), 'wb') as f:
pickle.dump(self, f, pickle.HIGHEST_PROTOCOL)
@classmethod
def load(cls, path):
with open(path, 'rb') as f:
return pickle.load(f)

View file

@ -0,0 +1,14 @@
# Copyright 2024 Google LLC
#
# 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.

View file

@ -0,0 +1,64 @@
# Copyright 2024 Google LLC
#
# 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.
from waflib import Node, Task, TaskGen
from resources.types.resource_ball import ResourceBall
from resources.types.resource_definition import StorageType
import generate_c_byte_array
class generate_builtin(Task.Task):
def run(self):
resource_ball = ResourceBall.load(self.inputs[0].abspath())
resource_objects = [reso for reso in resource_ball.resource_objects
if reso.definition.storage == StorageType.builtin]
with open(self.outputs[0].abspath(), 'w') as f:
fw_bld_node = self.generator.bld.bldnode.find_node('src/fw')
f.write('#include "{}"\n'.format(self.resource_id_header.path_from(fw_bld_node)))
f.write('#include "resource/resource_storage.h"\n')
f.write('#include "resource/resource_storage_builtin.h"\n\n')
def var_name(reso):
return "{}_builtin_bytes".format(reso.definition.name)
# Write the blobs of data:
for reso in resource_objects:
# some resources require 8-byte aligned addresses
# to simplify the handling we align all resources
f.write('__attribute__ ((aligned (8)))\n')
generate_c_byte_array.write(f, reso.data, var_name(reso))
f.write("\n")
f.write("const uint32_t g_num_builtin_resources = {};\n".format(len(resource_objects)))
f.write("const BuiltInResourceData g_builtin_resources[] = {\n")
for reso in resource_objects:
f.write(' {{ RESOURCE_ID_{resource_id}, {var_name}, sizeof({var_name}) }},\n'
.format(resource_id=reso.definition.name,
var_name=var_name(reso)))
f.write("};\n")
@TaskGen.feature('generate_builtin')
@TaskGen.before_method('process_source', 'process_rule')
def process_generate_builtin(self):
task = self.create_task('generate_builtin',
self.resource_ball,
self.builtin_target)
task.resource_id_header = self.resource_id_header

View file

@ -0,0 +1,81 @@
# Copyright 2024 Google LLC
#
# 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.
from waflib import Task, TaskGen
from resources.types.resource_ball import ResourceBall
def get_font_keys_from_resource_ball(resource_ball_node):
resource_ball = ResourceBall.load(resource_ball_node.abspath())
definitions = (o.definition for o in resource_ball.resource_objects)
font_keys = []
for d in definitions:
if d.type == 'font':
font_keys.append(d.name)
font_keys.extend(d.aliases)
return font_keys
@Task.update_outputs
class generate_font_header(Task.Task):
def run(self):
font_keys = get_font_keys_from_resource_ball(self.inputs[0])
with open(self.outputs[0].abspath(), 'w') as output_file:
output_file.write("#pragma once\n\n")
for key in font_keys:
output_file.write("#define FONT_KEY_{key} "
"\"RESOURCE_ID_{key}\"\n".format(key=key))
# See PBL-9335. We removed this define as it's no longer a complete font. It looked the
# same as Gothic 14, so going forward use that visual lookalike instead.
output_file.write('#define FONT_KEY_FONT_FALLBACK "RESOURCE_ID_GOTHIC_14"\n')
class generate_font_table(Task.Task):
def run(self):
font_keys = get_font_keys_from_resource_ball(self.inputs[0])
with open(self.outputs[0].abspath(), 'w') as output_file:
output_file.write("""
static const struct {
const char *key_name;
ResourceId resource_id;
ResourceId extension_id;
} s_font_resource_keys[] = {
""")
for key in font_keys:
output_file.write(" {{ FONT_KEY_{key}, "
"RESOURCE_ID_{key}, "
"RESOURCE_ID_{key}_EXTENDED }},\n".format(key=key))
output_file.write("};\n")
@TaskGen.feature('generate_fonts')
@TaskGen.before_method('process_source', 'process_rule')
def process_generate_fonts(self):
task = self.create_task('generate_font_header',
self.resource_ball,
self.font_key_header)
if self.font_key_table is not None:
task = self.create_task('generate_font_table',
self.resource_ball,
self.font_key_table)

View file

@ -0,0 +1,44 @@
# Copyright 2024 Google LLC
#
# 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.
from waflib import Node, Task, TaskGen
from resources.types.resource_ball import ResourceBall
from resources.types.resource_definition import StorageType
from pbpack import ResourcePack
class generate_pbpack(Task.Task):
def run(self):
resource_ball = ResourceBall.load(self.inputs[0].abspath())
resource_objects = [reso for reso in resource_ball.resource_objects
if reso.definition.storage == StorageType.pbpack]
pack = ResourcePack(self.is_system)
for r in resource_objects:
pack.add_resource(r.data)
with open(self.outputs[0].abspath(), 'wb') as f:
pack.serialize(f)
@TaskGen.feature('generate_pbpack')
@TaskGen.before_method('process_source', 'process_rule')
def process_generate_pbpack(self):
task = self.create_task('generate_pbpack',
self.resource_ball,
self.pbpack_target)
task.is_system = getattr(self, 'is_system', False)

View file

@ -0,0 +1,64 @@
# Copyright 2024 Google LLC
#
# 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.
from waflib import Task, TaskGen
class pfs_resources_table(Task.Task):
def run(self):
with open(self.outputs[0].abspath(), 'w') as f:
f.write("""
//
// AUTOGENERATED
// DO NOT MODIFY
//
#include "resource/resource_storage_file.h"
""")
fw_bld_node = self.generator.bld.bldnode.find_node('src/fw')
f.write('#include "{}"\n'.format(self.resource_id_header.path_from(fw_bld_node)))
f.write('\n')
f.write('const uint32_t g_num_file_resource_stores = {};\n'
.format(len(self.file_definitions)))
f.write('\n')
f.write('const FileResourceData g_file_resource_stores[] = {\n')
for d in self.file_definitions:
first_resource_id = 'RESOURCE_ID_{}'.format(d['resources'][0])
last_resource_id = 'RESOURCE_ID_{}'.format(d['resources'][-1])
# FIXME: We should just get rid of this concept since it's trivially calculated at
# compile time
id_offset_expr = '({} - 1)'.format(first_resource_id)
filename = d['name']
f.write(' {{ {first}, {last}, {id_offset}, "{filename}" }},\n'
.format(first=first_resource_id, last=last_resource_id,
id_offset=id_offset_expr, filename=filename))
f.write("};\n")
@TaskGen.feature('generate_pfs_resources')
@TaskGen.before_method('process_source', 'process_rule')
def generate_pfs_resources(self):
task = self.create_task('pfs_resources_table',
self.resource_definition_files + [self.resource_id_header],
self.pfs_table_node)
task.file_definitions = self.file_definitions
task.resource_id_header = self.resource_id_header

View file

@ -0,0 +1,117 @@
# Copyright 2024 Google LLC
#
# 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.
from waflib import Node, Task, TaskGen
from resources.resource_map import resource_generator
from resources.resource_map.resource_generator_js import JsResourceGenerator
from resources.types.resource_definition import StorageType
from resources.types.resource_object import ResourceObject
from resources.types.resource_ball import ResourceBall
class reso(Task.Task):
def run(self):
reso = resource_generator.generate_object(self, self.definition)
reso.dump(self.outputs[0])
@Task.update_outputs
class resource_ball(Task.Task):
def run(self):
resos = [ResourceObject.load(r.abspath()) for r in self.inputs]
resource_id_mapping = getattr(self.env, 'RESOURCE_ID_MAPPING', {})
ordered_resos = []
# Sort the resources into an ordered list by storage. Don't just use sorted
# because we want to preserve the ordering within the storage type. If a
# resource id mapping exists, use that to sort the resources instead.
if not resource_id_mapping:
for s in [StorageType.pbpack, StorageType.builtin, StorageType.pfs]:
ordered_resos.extend((o for o in resos if o.definition.storage == s))
else:
resos_dict = {resource_id_mapping[reso.definition.name]: reso for reso in resos}
ordered_resos = [resos_dict[x] for x in range(1, len(resos) + 1)]
res_ball = ResourceBall(ordered_resos, getattr(self, 'resource_declarations', []))
res_ball.dump(self.outputs[0])
def process_resource_definition(task_gen, resource_definition):
"""
Create a task that generates a .reso and returns the node pointing to
the output
"""
sources = []
for s in resource_definition.sources:
source_node = task_gen.path.make_node(s)
if source_node is None:
task_gen.bld.fatal("Could not find resource at %s" %
task_gen.bld.path.find_node(s).abspath())
sources.append(source_node)
output_name = '%s.%s.%s' % (sources[0].relpath(), str(resource_definition.name), 'reso')
# Build our outputs in a directory relative to where our final pbpack is going to go
output = task_gen.resource_ball.parent.make_node(output_name)
task = task_gen.create_task('reso', sources, output)
task.definition = resource_definition
task.dep_nodes = getattr(task_gen, 'resource_dependencies', [])
# Save JS bytecode filename for dependency calculation for SDK memory report
if resource_definition.type == 'js' and 'PEBBLE_SDK_ROOT' in task_gen.env:
task_gen.bld.all_envs[task_gen.env.PLATFORM_NAME].JS_RESO = output
return output
@TaskGen.feature('generate_resource_ball')
@TaskGen.before_method('process_source', 'process_rule')
def process_resource_ball(task_gen):
"""
resources: a list of ResourceDefinitions objects and nodes pointing to
.reso files
resource_dependencies: node list that all our generated resources depend on
resource_ball: a node to where the ball should be generated
vars: a list of environment variables that generated resources should depend on as a source
"""
resource_objects = []
bundled_resos = []
for r in task_gen.resources:
if isinstance(r, Node.Node):
# It's already a node, presumably pointing to a .reso file
resource_objects.append(r)
else:
# It's a resource definition, we need to process it into a .reso
# file. Note this is where the task that does the data conversion
# gets created.
processed_resource = process_resource_definition(task_gen, r)
resource_objects.append(processed_resource)
bundled_resos.append(processed_resource)
if getattr(task_gen, 'project_resource_ball', None):
prb_task = task_gen.create_task('resource_ball',
bundled_resos,
task_gen.project_resource_ball)
prb_task.dep_node = getattr(task_gen, 'resource_dependencies', [])
prb_task.dep_vars = getattr(task_gen, 'vars', [])
task = task_gen.create_task('resource_ball', resource_objects, task_gen.resource_ball)
task.resource_declarations = getattr(task_gen, 'resource_declarations', [])
task.dep_nodes = getattr(task_gen, 'resource_dependencies', [])
task.dep_vars = getattr(task_gen, 'vars', [])

View file

@ -0,0 +1,177 @@
# Copyright 2024 Google LLC
#
# 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.
from waflib import Task, TaskGen
from resources.types.resource_ball import ResourceBall
from resources.types.resource_definition import ResourceDefinition
enum_header = (
"""#pragma once
//
// AUTOGENERATED BY BUILD
// DO NOT MODIFY - CHANGES WILL BE OVERWRITTEN
//
typedef enum {
INVALID_RESOURCE = 0,
RESOURCE_ID_INVALID = 0,
DEFAULT_MENU_ICON = 0,
""")
define_header = (
"""#pragma once
//
// AUTOGENERATED BY BUILD
// DO NOT MODIFY - CHANGES WILL BE OVERWRITTEN
//
#define DEFAULT_MENU_ICON 0
"""
)
extern_header = (
"""#pragma once
#include <stdint.h>
//
// AUTOGENERATED BY BUILD
// DO NOT MODIFY - CHANGES WILL BE OVERWRITTEN
//
""")
definitions_file = (
"""#include <stdint.h>
//
// AUTOGENERATED BY BUILD
// DO NOT MODIFY - CHANGES WILL BE OVERWRITTEN
//
""")
@Task.update_outputs
class generate_resource_id_header(Task.Task):
def run(self):
use_extern = getattr(self, 'use_extern', False)
use_define = getattr(self, 'use_define', False)
published_media = getattr(self, 'published_media', [])
if use_extern:
RESOURCE_ID_DECLARATION = "extern uint32_t RESOURCE_ID_{};"
PUBLISHED_ID_DECLARATION = "extern uint32_t PUBLISHED_ID_{};"
elif use_define:
RESOURCE_ID_DECLARATION = "#define RESOURCE_ID_{} {}"
PUBLISHED_ID_DECLARATION = "#define PUBLISHED_ID_{} {}"
else:
RESOURCE_ID_DECLARATION = " RESOURCE_ID_{} = {},"
INVALID_RESOURCE_ID_DECLARATION = " RESOURCE_ID_{} = INVALID_RESOURCE,"
if published_media:
self.generator.bld.fatal("publishedMedia is only supported for resource headers "
"using externs and defines. Check your "
"generate_resource_id_header arguments.")
resource_ball = ResourceBall.load(self.inputs[0].abspath())
declarations_dict = {d.name: d for d in resource_ball.get_all_declarations()}
self.outputs[0].parent.mkdir()
with open(self.outputs[0].abspath(), 'w') as output_file:
if use_extern:
output_file.write(extern_header)
elif use_define:
output_file.write(define_header)
else:
output_file.write(enum_header)
# Write out all the fonts and their aliases
for i, declaration in enumerate(resource_ball.get_all_declarations(), start=1):
output_file.write(RESOURCE_ID_DECLARATION.format(declaration.name, i) + "\n")
if isinstance(declaration, ResourceDefinition):
for alias in declaration.aliases:
output_file.write(RESOURCE_ID_DECLARATION.format(alias, i) + " // alias\n")
for item in published_media:
output_file.write(
PUBLISHED_ID_DECLARATION.format(item['name'], item['id'] or 0) + "\n")
# Handle defining extended font ids for extended fonts that don't actually exist.
# Every font should have a matching ID defined, but if the resource itself doesn't
# exist we generate a fake ID for them.
if not use_extern and not use_define:
def write_invalid_id_if_needed(name):
extended_name = name + '_EXTENDED'
if extended_name not in declarations_dict:
output_file.write(INVALID_RESOURCE_ID_DECLARATION.format(extended_name) +
"\n")
for o in resource_ball.resource_objects:
if o.definition.type == 'font':
write_invalid_id_if_needed(o.definition.name)
for alias in o.definition.aliases:
write_invalid_id_if_needed(alias)
output_file.write('} ResourceId;')
@Task.update_outputs
class generate_resource_id_definitions(Task.Task):
def run(self):
RESOURCE_ID_DEFINITION = "uint32_t RESOURCE_ID_{} = {};"
PUBLISHED_ID_DEFINITION = "uint32_t PUBLISHED_ID_{} = {};"
resource_ball = ResourceBall.load(self.inputs[0].abspath())
published_media = getattr(self, 'published_media', [])
self.outputs[0].parent.mkdir()
with open(self.outputs[0].abspath(), 'w') as output_file:
output_file.write(definitions_file)
for i, declaration in enumerate(resource_ball.get_all_declarations(), start=1):
output_file.write(RESOURCE_ID_DEFINITION.format(declaration.name, i) + "\n")
if isinstance(declaration, ResourceDefinition):
for alias in declaration.aliases:
output_file.write(RESOURCE_ID_DEFINITION.format(alias, i) + " // alias\n")
for item in published_media:
output_file.write(PUBLISHED_ID_DEFINITION.format(item['name'], item['id']))
@TaskGen.feature('generate_resource_id_header')
@TaskGen.before_method('process_source', 'process_rule')
def process_generate_resource_id_header(self):
task = self.create_task('generate_resource_id_header',
self.resource_ball,
self.resource_id_header_target)
task.use_extern = getattr(self, 'use_extern', False)
task.use_define = getattr(self, 'use_define', False)
task.published_media = getattr(self, 'published_media', [])
@TaskGen.feature('generate_resource_id_definitions')
@TaskGen.before_method('process_source', 'process_rule')
def generate_resource_id_definitions(self):
task = self.create_task('generate_resource_id_definitions',
self.resource_ball,
self.resource_id_definitions_target)
task.published_media = getattr(self, 'published_media', [])

View file

@ -0,0 +1,95 @@
# Copyright 2024 Google LLC
#
# 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.
from waflib import Task, TaskGen
import json
class generate_timeline_table(Task.Task):
def run(self):
with open(self.outputs[0].abspath(), 'w') as f:
f.write("""
//
// AUTOGENERATED
// DO NOT MODIFY
//
#include "resource/resource_ids.auto.h"
#include "services/normal/timeline/timeline_resources.h"
#include <stdint.h>
const uint16_t g_timeline_resources[][TimelineResourceSizeCount] = {
""")
for res in self.timeline_dict:
f.write(" [{}] = {{ {}, {}, {} }},\n"
.format(res["id"],
res["sizes"].get('tiny', 'RESOURCE_ID_INVALID'),
res["sizes"].get('small', 'RESOURCE_ID_INVALID'),
res["sizes"].get('large', 'RESOURCE_ID_INVALID')))
f.write("};\n")
class generate_timeline_ids(Task.Task):
def run(self):
with open(self.outputs[0].abspath(), 'w') as f:
f.write("""
#pragma once
//
// AUTOGENERATED
// DO NOT MODIFY
//
typedef enum {
TIMELINE_RESOURCE_INVALID = 0,
""")
SYSTEM_RESOURCE_FLAG = 0x80000000
max_timeline_id = 0
for res in self.timeline_dict:
max_timeline_id = max(max_timeline_id, res["id"])
id = res["id"] | SYSTEM_RESOURCE_FLAG
f.write(" TIMELINE_RESOURCE_{} = {:#x},\n".format(res["name"], id))
f.write("} TimelineResourceId;\n\n")
f.write("#define NUM_TIMELINE_RESOURCES {}\n".format(max_timeline_id + 1))
@TaskGen.feature('generate_timeline')
@TaskGen.before_method('process_source', 'process_rule')
def process_generate_timeline(self):
# map the resources into URIs and invert the key/val
timeline_uris = {"system://images/" + r["name"]: r["id"] for r in self.timeline_dict
if not r.get("internal", False)}
self.RESOURCE_URIS = json.dumps(timeline_uris, indent=4)
# substitute it into the json template
layouts_in = self.bld.path.find_node('resources/normal/base/layouts/layouts.json.in')
task = self.create_task('subst',
[layouts_in] + self.resource_definition_files,
self.layouts_node)
task = self.create_task('generate_timeline_table',
self.resource_definition_files,
self.timeline_table_node)
task.timeline_dict = self.timeline_dict
task = self.create_task('generate_timeline_ids',
self.resource_definition_files,
self.timeline_ids_node)
task.timeline_dict = self.timeline_dict

View file

@ -0,0 +1,54 @@
# Copyright 2024 Google LLC
#
# 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.
from waflib import Node, Task, TaskGen
from resources.types.resource_ball import ResourceBall
from pbpack import ResourcePack
class generate_version_header(Task.Task):
def run(self):
if len(self.inputs):
# is_system=True because only firmwares use version headers
with open(self.inputs[0].abspath(), 'rb') as f:
pbpack = ResourcePack.deserialize(f, is_system=True)
resource_crc = pbpack.get_content_crc()
else:
resource_crc = 0
self.outputs[0].parent.mkdir() # Make sure the output directory exists
with open(self.outputs[0].abspath(), 'w') as output_file:
output_file.write("""
#pragma once
//
// AUTOGENERATED
// DO NOT MODIFY
//
static const ResourceVersion SYSTEM_RESOURCE_VERSION = {{
.crc = {},
.timestamp = 0
}};
""".format(resource_crc))
@TaskGen.feature('generate_version_header')
@TaskGen.before_method('process_source', 'process_rule')
def process_generate_version_header(self):
task = self.create_task('generate_version_header',
self.pbpack,
self.version_header_target)