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

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())