mirror of
https://github.com/google/pebble.git
synced 2025-05-28 05:53:12 +00:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
14
sdk/tools/__init__.py
Normal file
14
sdk/tools/__init__.py
Normal 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.
|
||||
|
309
sdk/tools/inject_metadata.py
Executable file
309
sdk/tools/inject_metadata.py
Executable file
|
@ -0,0 +1,309 @@
|
|||
#!/usr/bin/env python
|
||||
# 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 __future__ import with_statement
|
||||
from struct import pack, unpack
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import time
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from shutil import copy2
|
||||
from binascii import crc32
|
||||
from struct import pack
|
||||
from pbpack import ResourcePack
|
||||
|
||||
|
||||
import stm32_crc
|
||||
|
||||
# Pebble App Metadata Struct
|
||||
# These are offsets of the PebbleProcessInfo struct in src/fw/app_management/pebble_process_info.h
|
||||
HEADER_ADDR = 0x0 # 8 bytes
|
||||
STRUCT_VERSION_ADDR = 0x8 # 2 bytes
|
||||
SDK_VERSION_ADDR = 0xa # 2 bytes
|
||||
APP_VERSION_ADDR = 0xc # 2 bytes
|
||||
LOAD_SIZE_ADDR = 0xe # 2 bytes
|
||||
OFFSET_ADDR = 0x10 # 4 bytes
|
||||
CRC_ADDR = 0x14 # 4 bytes
|
||||
NAME_ADDR = 0x18 # 32 bytes
|
||||
COMPANY_ADDR = 0x38 # 32 bytes
|
||||
ICON_RES_ID_ADDR = 0x58 # 4 bytes
|
||||
JUMP_TABLE_ADDR = 0x5c # 4 bytes
|
||||
FLAGS_ADDR = 0x60 # 4 bytes
|
||||
NUM_RELOC_ENTRIES_ADDR = 0x64 # 4 bytes
|
||||
UUID_ADDR = 0x68 # 16 bytes
|
||||
RESOURCE_CRC_ADDR = 0x78 # 4 bytes
|
||||
RESOURCE_TIMESTAMP_ADDR = 0x7c # 4 bytes
|
||||
VIRTUAL_SIZE_ADDR = 0x80 # 2 bytes
|
||||
STRUCT_SIZE_BYTES = 0x82
|
||||
|
||||
# Pebble App Flags
|
||||
# These are PebbleAppFlags from src/fw/app_management/pebble_process_info.h
|
||||
PROCESS_INFO_STANDARD_APP = (0)
|
||||
PROCESS_INFO_WATCH_FACE = (1 << 0)
|
||||
PROCESS_INFO_VISIBILITY_HIDDEN = (1 << 1)
|
||||
PROCESS_INFO_VISIBILITY_SHOWN_ON_COMMUNICATION = (1 << 2)
|
||||
PROCESS_INFO_ALLOW_JS = (1 << 3)
|
||||
PROCESS_INFO_HAS_WORKER = (1 << 4)
|
||||
|
||||
# Max app size, including the struct and reloc table
|
||||
# Note that even if the app is smaller than this, it still may be too big, as it needs to share this
|
||||
# space with applib/ which changes in size from release to release.
|
||||
MAX_APP_BINARY_SIZE = 0x10000
|
||||
|
||||
# This number is a rough estimate, but should not be less than the available space.
|
||||
# Currently, app_state uses up a small part of the app space.
|
||||
# See also APP_RAM in stm32f2xx_flash_fw.ld and APP in pebble_app.ld.
|
||||
MAX_APP_MEMORY_SIZE = 24 * 1024
|
||||
|
||||
# This number is a rough estimate, but should not be less than the available space.
|
||||
# Currently, worker_state uses up a small part of the worker space.
|
||||
# See also WORKER_RAM in stm32f2xx_flash_fw.ld
|
||||
MAX_WORKER_MEMORY_SIZE = 10 * 1024
|
||||
|
||||
ENTRY_PT_SYMBOL = 'main'
|
||||
JUMP_TABLE_ADDR_SYMBOL = 'pbl_table_addr'
|
||||
DEBUG = False
|
||||
|
||||
|
||||
class InvalidBinaryError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def inject_metadata(target_binary, target_elf, resources_file, timestamp, allow_js=False,
|
||||
has_worker=False):
|
||||
|
||||
if target_binary[-4:] != '.bin':
|
||||
raise Exception("Invalid filename <%s>! The filename should end in .bin" % target_binary)
|
||||
|
||||
def get_nm_output(elf_file):
|
||||
nm_process = Popen(['arm-none-eabi-nm', elf_file], stdout=PIPE)
|
||||
# Popen.communicate returns a tuple of (stdout, stderr)
|
||||
nm_output = nm_process.communicate()[0]
|
||||
|
||||
if not nm_output:
|
||||
raise InvalidBinaryError()
|
||||
|
||||
nm_output = [ line.split() for line in nm_output.splitlines() ]
|
||||
return nm_output
|
||||
|
||||
def get_symbol_addr(nm_output, symbol):
|
||||
# nm output looks like the following...
|
||||
#
|
||||
# U _ITM_registerTMCloneTable
|
||||
# 00000084 t jump_to_pbl_function
|
||||
# U _Jv_RegisterClasses
|
||||
# 0000009c T main
|
||||
# 00000130 T memset
|
||||
#
|
||||
# We don't care about the lines that only have two columns, they're not functions.
|
||||
|
||||
for sym in nm_output:
|
||||
if symbol == sym[-1] and len(sym) == 3:
|
||||
return int(sym[0], 16)
|
||||
|
||||
raise Exception("Could not locate symbol <%s> in binary! Failed to inject app metadata" %
|
||||
(symbol))
|
||||
|
||||
def get_virtual_size(elf_file):
|
||||
""" returns the virtual size (static memory usage, .text + .data + .bss) in bytes """
|
||||
|
||||
readelf_bss_process = Popen("arm-none-eabi-readelf -S '%s'" % elf_file,
|
||||
shell=True, stdout=PIPE)
|
||||
readelf_bss_output = readelf_bss_process.communicate()[0]
|
||||
|
||||
# readelf -S output looks like the following...
|
||||
#
|
||||
# [Nr] Name Type Addr Off Size ES Flg Lk Inf Al
|
||||
# [ 0] NULL 00000000 000000 000000 00 0 0 0
|
||||
# [ 1] .header PROGBITS 00000000 008000 000082 00 A 0 0 1
|
||||
# [ 2] .text PROGBITS 00000084 008084 0006be 00 AX 0 0 4
|
||||
# [ 3] .rel.text REL 00000000 00b66c 0004d0 08 23 2 4
|
||||
# [ 4] .data PROGBITS 00000744 008744 000004 00 WA 0 0 4
|
||||
# [ 5] .bss NOBITS 00000748 008748 000054 00 WA 0 0 4
|
||||
|
||||
last_section_end_addr = 0
|
||||
|
||||
# Find the .bss section and calculate the size based on the end of the .bss section
|
||||
for line in readelf_bss_output.splitlines():
|
||||
if len(line) < 10:
|
||||
continue
|
||||
|
||||
# Carve off the first column, since it sometimes has a space in it which screws up the
|
||||
# split. Two leading spaces, a square bracket, 2 digits (with space padding),
|
||||
# a second square brack is 6
|
||||
line = line[6:]
|
||||
|
||||
columns = line.split()
|
||||
if len(columns) < 6:
|
||||
continue
|
||||
|
||||
if columns[0] == '.bss':
|
||||
addr = int(columns[2], 16)
|
||||
size = int(columns[4], 16)
|
||||
last_section_end_addr = addr + size
|
||||
elif columns[0] == '.data' and last_section_end_addr == 0:
|
||||
addr = int(columns[2], 16)
|
||||
size = int(columns[4], 16)
|
||||
last_section_end_addr = addr + size
|
||||
|
||||
if last_section_end_addr != 0:
|
||||
return last_section_end_addr
|
||||
|
||||
sys.stderr.writeline("Failed to parse ELF sections while calculating the virtual size\n")
|
||||
sys.stderr.write(readelf_bss_output)
|
||||
raise Exception("Failed to parse ELF sections while calculating the virtual size")
|
||||
|
||||
def get_relocate_entries(elf_file):
|
||||
""" returns a list of all the locations requiring an offset"""
|
||||
# TODO: insert link to the wiki page I'm about to write about PIC and relocatable values
|
||||
entries = []
|
||||
|
||||
# get the .data locations
|
||||
readelf_relocs_process = Popen(['arm-none-eabi-readelf', '-r', elf_file], stdout=PIPE)
|
||||
readelf_relocs_output = readelf_relocs_process.communicate()[0]
|
||||
lines = readelf_relocs_output.splitlines()
|
||||
|
||||
i = 0
|
||||
reading_section = False
|
||||
while i < len(lines):
|
||||
if not reading_section:
|
||||
# look for the next section
|
||||
if lines[i].startswith("Relocation section '.rel.data"):
|
||||
reading_section = True
|
||||
i += 1 # skip the column title section
|
||||
else:
|
||||
if len(lines[i]) == 0:
|
||||
# end of the section
|
||||
reading_section = False
|
||||
else:
|
||||
entries.append(int(lines[i].split(' ')[0], 16))
|
||||
i += 1
|
||||
|
||||
# get any Global Offset Table (.got) entries
|
||||
readelf_relocs_process = Popen(['arm-none-eabi-readelf', '--sections', elf_file],
|
||||
stdout=PIPE)
|
||||
readelf_relocs_output = readelf_relocs_process.communicate()[0]
|
||||
lines = readelf_relocs_output.splitlines()
|
||||
for line in lines:
|
||||
# We shouldn't need to do anything with the Procedure Linkage Table since we don't
|
||||
# actually export functions
|
||||
if '.got' in line and '.got.plt' not in line:
|
||||
words = line.split(' ')
|
||||
while '' in words:
|
||||
words.remove('')
|
||||
section_label_idx = words.index('.got')
|
||||
addr = int(words[section_label_idx + 2], 16)
|
||||
length = int(words[section_label_idx + 4], 16)
|
||||
for i in range(addr, addr + length, 4):
|
||||
entries.append(i)
|
||||
break
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
nm_output = get_nm_output(target_elf)
|
||||
|
||||
try:
|
||||
app_entry_address = get_symbol_addr(nm_output, ENTRY_PT_SYMBOL)
|
||||
except:
|
||||
raise Exception("Missing app entry point! Must be `int main(void) { ... }` ")
|
||||
jump_table_address = get_symbol_addr(nm_output, JUMP_TABLE_ADDR_SYMBOL)
|
||||
|
||||
reloc_entries = get_relocate_entries(target_elf)
|
||||
|
||||
statinfo = os.stat(target_binary)
|
||||
app_load_size = statinfo.st_size
|
||||
|
||||
if resources_file is not None:
|
||||
with open(resources_file, 'rb') as f:
|
||||
pbpack = ResourcePack.deserialize(f, is_system=False)
|
||||
resource_crc = pbpack.get_content_crc()
|
||||
else:
|
||||
resource_crc = 0
|
||||
|
||||
if DEBUG:
|
||||
copy2(target_binary, target_binary + ".orig")
|
||||
|
||||
with open(target_binary, 'r+b') as f:
|
||||
total_app_image_size = app_load_size + (len(reloc_entries) * 4)
|
||||
if total_app_image_size > MAX_APP_BINARY_SIZE:
|
||||
raise Exception("App image size is %u (app %u relocation table %u). Must be smaller "
|
||||
"than %u bytes" % (total_app_image_size,
|
||||
app_load_size,
|
||||
len(reloc_entries) * 4,
|
||||
MAX_APP_BINARY_SIZE))
|
||||
|
||||
def read_value_at_offset(offset, format_str, size):
|
||||
f.seek(offset)
|
||||
return unpack(format_str, f.read(size))
|
||||
|
||||
app_bin = f.read()
|
||||
app_crc = stm32_crc.crc32(app_bin[STRUCT_SIZE_BYTES:])
|
||||
|
||||
[app_flags] = read_value_at_offset(FLAGS_ADDR, '<L', 4)
|
||||
|
||||
if allow_js:
|
||||
app_flags = app_flags | PROCESS_INFO_ALLOW_JS
|
||||
|
||||
if has_worker:
|
||||
app_flags = app_flags | PROCESS_INFO_HAS_WORKER
|
||||
|
||||
app_virtual_size = get_virtual_size(target_elf)
|
||||
|
||||
struct_changes = {
|
||||
'load_size' : app_load_size,
|
||||
'entry_point' : "0x%08x" % app_entry_address,
|
||||
'symbol_table' : "0x%08x" % jump_table_address,
|
||||
'flags' : app_flags,
|
||||
'crc' : "0x%08x" % app_crc,
|
||||
'num_reloc_entries': "0x%08x" % len(reloc_entries),
|
||||
'resource_crc' : "0x%08x" % resource_crc,
|
||||
'timestamp' : timestamp,
|
||||
'virtual_size': app_virtual_size
|
||||
}
|
||||
|
||||
def write_value_at_offset(offset, format_str, value):
|
||||
f.seek(offset)
|
||||
f.write(pack(format_str, value))
|
||||
|
||||
write_value_at_offset(LOAD_SIZE_ADDR, '<H', app_load_size)
|
||||
write_value_at_offset(OFFSET_ADDR, '<L', app_entry_address)
|
||||
write_value_at_offset(CRC_ADDR, '<L', app_crc)
|
||||
|
||||
write_value_at_offset(RESOURCE_CRC_ADDR, '<L', resource_crc)
|
||||
write_value_at_offset(RESOURCE_TIMESTAMP_ADDR, '<L', timestamp)
|
||||
|
||||
write_value_at_offset(JUMP_TABLE_ADDR, '<L', jump_table_address)
|
||||
|
||||
write_value_at_offset(FLAGS_ADDR, '<L', app_flags)
|
||||
|
||||
write_value_at_offset(NUM_RELOC_ENTRIES_ADDR, '<L', len(reloc_entries))
|
||||
|
||||
write_value_at_offset(VIRTUAL_SIZE_ADDR, "<H", app_virtual_size)
|
||||
|
||||
# Write the reloc_entries past the end of the binary. This expands the size of the binary,
|
||||
# but this new stuff won't actually be loaded into ram.
|
||||
f.seek(app_load_size)
|
||||
for entry in reloc_entries:
|
||||
f.write(pack('<L', entry))
|
||||
|
||||
f.flush()
|
||||
|
||||
return struct_changes
|
||||
|
138
sdk/tools/memory_reports.py
Normal file
138
sdk/tools/memory_reports.py
Normal file
|
@ -0,0 +1,138 @@
|
|||
# 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.
|
||||
|
||||
def _convert_bytes_to_kilobytes(number_bytes):
|
||||
"""
|
||||
Convert the input from bytes into kilobytes
|
||||
:param number_bytes: the number of bytes to convert
|
||||
:return: the input value converted to kilobytes
|
||||
"""
|
||||
NUMBER_BYTES_IN_KBYTE = 1024
|
||||
return int(number_bytes) / NUMBER_BYTES_IN_KBYTE
|
||||
|
||||
|
||||
def app_memory_report(platform_name, bin_type, app_size, max_ram, free_ram, resource_size=None,
|
||||
max_resource_size=None):
|
||||
"""
|
||||
This method provides a formatted string for printing the memory usage of this binary to the
|
||||
console.
|
||||
:param platform_name: the name of the current HW platform being targeted
|
||||
:param bin_type: the type of binary being built (app, lib, worker)
|
||||
:param app_size: the size of the binary
|
||||
:param max_ram: the maximum allowed size of the binary
|
||||
:param free_ram: the amount of remaining memory
|
||||
:param resource_size: the size of the resource pack
|
||||
:param max_resource_size: the maximum allowed size of the resource pack
|
||||
:return: a tuple containing the color for the string print, and the string to print
|
||||
"""
|
||||
LABEL = "-------------------------------------------------------\n{} {} MEMORY USAGE\n"
|
||||
RESOURCE_SIZE = "Total size of resources: {} bytes / {}KB\n"
|
||||
MEMORY_USAGE = ("Total footprint in RAM: {} bytes / {}KB\n"
|
||||
"Free RAM available (heap): {} bytes\n"
|
||||
"-------------------------------------------------------")
|
||||
|
||||
if resource_size and max_resource_size:
|
||||
report = (LABEL.format(platform_name.upper(), bin_type.upper()) +
|
||||
RESOURCE_SIZE.format(resource_size,
|
||||
_convert_bytes_to_kilobytes(max_resource_size)) +
|
||||
MEMORY_USAGE.format(app_size, _convert_bytes_to_kilobytes(max_ram), free_ram))
|
||||
else:
|
||||
report = (LABEL.format(platform_name.upper(), bin_type.upper()) +
|
||||
MEMORY_USAGE.format(app_size, _convert_bytes_to_kilobytes(max_ram), free_ram))
|
||||
|
||||
return 'YELLOW', report
|
||||
|
||||
|
||||
def app_resource_memory_error(platform_name, resource_size, max_resource_size):
|
||||
"""
|
||||
This method provides a formatted error message for printing to the console when the resource
|
||||
size exceeds the maximum resource size supported by the Pebble firmware.
|
||||
:param platform_name: the name of the current HW platform being targeted
|
||||
:param resource_size: the size of the resource pack
|
||||
:param max_resource_size: the maximum allowed size of the resource pack
|
||||
:return: a tuple containing the color for the string print, and the string to print
|
||||
"""
|
||||
report = ("======================================================\n"
|
||||
"Build failed: {}\n"
|
||||
"Error: Resource pack is too large ({}KB / {}KB)\n"
|
||||
"======================================================\n".
|
||||
format(platform_name,
|
||||
_convert_bytes_to_kilobytes(resource_size),
|
||||
_convert_bytes_to_kilobytes(max_resource_size)))
|
||||
|
||||
return 'RED', report
|
||||
|
||||
|
||||
def app_appstore_resource_memory_error(platform_name, resource_size, max_appstore_resource_size):
|
||||
"""
|
||||
This method provides a formatted warning message for printing to the console when the resource
|
||||
pack size exceeds the maximum allowed resource size for the appstore.
|
||||
:param platform_name: the name of the current HW platform being targeted
|
||||
:param resource_size: the size of the resource pack
|
||||
:param max_appstore_resource_size: the maximum appstore-allowed size of the resource pack
|
||||
:return: a tuple containing the color for the string print, and the string to print
|
||||
"""
|
||||
report = ("WARNING: Your {} app resources are too large ({}KB / {}KB). You will not be "
|
||||
"able "
|
||||
"to publish your app.\n".
|
||||
format(platform_name,
|
||||
_convert_bytes_to_kilobytes(resource_size),
|
||||
_convert_bytes_to_kilobytes(max_appstore_resource_size)))
|
||||
|
||||
return 'RED', report
|
||||
|
||||
|
||||
def bytecode_memory_report(platform_name, bytecode_size, bytecode_max):
|
||||
"""
|
||||
This method provides a formatted string for printing the memory usage for this Rocky bytecode
|
||||
file to the console.
|
||||
:param platform_name: the name of the current HW platform being targeted
|
||||
:param bytecode_size: the size of the bytecode file, in bytes
|
||||
:param bytecode_max: the max allowed size of the bytecode file, in bytes
|
||||
:return: a tuple containing the color for the string print, and the string to print
|
||||
"""
|
||||
LABEL = "-------------------------------------------------------\n{} MEMORY USAGE\n"
|
||||
BYTECODE_USAGE = ("Total size of snapshot: {}KB / {}KB\n"
|
||||
"-------------------------------------------------------")
|
||||
|
||||
report = (LABEL.format(platform_name.upper()) +
|
||||
BYTECODE_USAGE.format(_convert_bytes_to_kilobytes(bytecode_size),
|
||||
_convert_bytes_to_kilobytes(bytecode_max)))
|
||||
|
||||
return 'YELLOW', report
|
||||
|
||||
|
||||
def simple_memory_report(platform_name, bin_size, resource_size=None):
|
||||
"""
|
||||
This method provides a formatted string for printing the memory usage for this binary to the
|
||||
console.
|
||||
:param platform_name: the name of the current HW platform being targeted
|
||||
:param bin_size: the size of the binary
|
||||
:param resource_size: the size of the resource pack
|
||||
:return: a tuple containing the color for the string print, and the string to print
|
||||
"""
|
||||
LABEL = "-------------------------------------------------------\n{} MEMORY USAGE\n"
|
||||
RESOURCE_SIZE = "Total size of resources: {} bytes\n"
|
||||
MEMORY_USAGE = ("Total footprint in RAM: {} bytes\n"
|
||||
"-------------------------------------------------------")
|
||||
|
||||
if resource_size:
|
||||
report = (LABEL.format(platform_name.upper()) +
|
||||
RESOURCE_SIZE.format(resource_size) +
|
||||
MEMORY_USAGE.format(bin_size))
|
||||
else:
|
||||
report = (LABEL.format(platform_name.upper()) +
|
||||
MEMORY_USAGE.format(bin_size))
|
||||
|
||||
return 'YELLOW', report
|
112
sdk/tools/pebble_package.py
Normal file
112
sdk/tools/pebble_package.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
# 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 __future__ import print_function
|
||||
import argparse
|
||||
import errno
|
||||
import os
|
||||
from shutil import rmtree
|
||||
import zipfile
|
||||
|
||||
|
||||
class MissingFileException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DuplicatePackageFileException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _calculate_file_size(path):
|
||||
return os.stat(path).st_size
|
||||
|
||||
|
||||
def _calculate_crc(path):
|
||||
pass
|
||||
|
||||
|
||||
class PebblePackage(object):
|
||||
def __init__(self, package_filename):
|
||||
self.package_filename = package_filename
|
||||
self.package_files = {}
|
||||
|
||||
def add_file(self, name, file_path):
|
||||
if not os.path.exists(file_path):
|
||||
raise MissingFileException("The file '{}' does not exist".format(file_path))
|
||||
if name in self.package_files and self.package_files.get(name) != file_path:
|
||||
raise DuplicatePackageFileException("The file '{}' cannot be added to the package "
|
||||
"because `{}` has already been assigned to `{}`".
|
||||
format(file_path,
|
||||
self.package_files.get(name),
|
||||
name))
|
||||
else:
|
||||
self.package_files[name] = file_path
|
||||
|
||||
def pack(self, package_path=None):
|
||||
with zipfile.ZipFile(os.path.join(package_path, self.package_filename), 'w') as zip_file:
|
||||
for filename, file_path in self.package_files.iteritems():
|
||||
zip_file.write(file_path, filename)
|
||||
zip_file.comment = type(self).__name__
|
||||
|
||||
def unpack(self, package_path=''):
|
||||
try:
|
||||
rmtree(package_path)
|
||||
except OSError as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
raise e
|
||||
with zipfile.ZipFile(self.package_filename, 'r') as zip_file:
|
||||
zip_file.extractall(package_path)
|
||||
|
||||
|
||||
class RockyPackage(PebblePackage):
|
||||
def __init__(self, package_filename):
|
||||
super(RockyPackage, self).__init__(package_filename)
|
||||
|
||||
def add_files(self, rockyjs, binaries, resources, pkjs, platforms):
|
||||
for platform in platforms:
|
||||
self.add_file(os.path.join(platform, rockyjs[platform]), rockyjs[platform])
|
||||
self.add_file(os.path.join(platform, binaries[platform]), binaries[platform])
|
||||
self.add_file(os.path.join(platform, resources[platform]), resources[platform])
|
||||
self.add_file(pkjs, pkjs)
|
||||
|
||||
def write_manifest(self):
|
||||
pass
|
||||
|
||||
class LibraryPackage(PebblePackage):
|
||||
def __init__(self, package_filename="dist.zip"):
|
||||
super(LibraryPackage, self).__init__(package_filename)
|
||||
|
||||
def add_files(self, includes, binaries, resources, js):
|
||||
for include, include_path in includes.iteritems():
|
||||
self.add_file(os.path.join('include', include), include_path)
|
||||
for binary, binary_path in binaries.iteritems():
|
||||
self.add_file(os.path.join('binaries', binary), binary_path)
|
||||
for resource, resource_path in resources.iteritems():
|
||||
self.add_file(os.path.join('resources', resource), resource_path)
|
||||
for js_file, js_file_path in js.iteritems():
|
||||
self.add_file(os.path.join('js', js_file), js_file_path)
|
||||
|
||||
def unpack(self, package_path='dist'):
|
||||
super(LibraryPackage, self).unpack(package_path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description="Manage Pebble packages")
|
||||
parser.add_argument('command', type=str, help="Command to use")
|
||||
parser.add_argument('filename', type=str, help="Path to your Pebble package")
|
||||
args = parser.parse_args()
|
||||
|
||||
with zipfile.ZipFile(args.filename, 'r') as package:
|
||||
cls = globals()[package.comment](args.filename)
|
||||
getattr(cls, args.command)()
|
212
sdk/tools/rocky-lint/rocky.d.ts
vendored
Normal file
212
sdk/tools/rocky-lint/rocky.d.ts
vendored
Normal file
|
@ -0,0 +1,212 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare namespace rocky {
|
||||
// helper type to indicate that a commonly expected feature is planned but not implement, yet
|
||||
interface IsNotImplementedInRockyYet {
|
||||
_doesNotWork: any
|
||||
}
|
||||
|
||||
interface Event {
|
||||
type: string
|
||||
}
|
||||
|
||||
interface DrawEvent extends Event {
|
||||
context: CanvasRenderingContext2D
|
||||
}
|
||||
|
||||
interface TickEvent extends Event {
|
||||
date: Date
|
||||
}
|
||||
|
||||
interface MemoryPressureEvent extends Event {
|
||||
level: 'high';
|
||||
}
|
||||
|
||||
interface MessageEvent extends Event {
|
||||
data: any;
|
||||
}
|
||||
|
||||
interface PostMessageConnectionEvent extends Event {
|
||||
}
|
||||
|
||||
interface AnyEvent extends Event, DrawEvent, TickEvent, MemoryPressureEvent, MessageEvent, PostMessageConnectionEvent { }
|
||||
|
||||
interface CanvasRenderingContext2D {
|
||||
canvas: CanvasElement
|
||||
fillStyle: string
|
||||
font: string // TODO list actually supported fonts
|
||||
lineWidth: number
|
||||
strokeStyle: string
|
||||
textAlign: string // TODO list actually supported values
|
||||
textBaseline: IsNotImplementedInRockyYet
|
||||
arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, anticlockwise?: boolean): void
|
||||
arcTo(IsNotImplementedInRockyYet : number, y1: number, x2: number, y2: number, radius: number): void
|
||||
beginPath(): void
|
||||
bezierCurveTo(cp1x: IsNotImplementedInRockyYet , cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void
|
||||
clearRect(x: number, y: number, w: number, h: number): void
|
||||
closePath(): void
|
||||
drawImage(image: IsNotImplementedInRockyYet, offsetX: number, offsetY: number, width?: number, height?: number, canvasOffsetX?: number, canvasOffsetY?: number, canvasImageWidth?: number, canvasImageHeight?: number): void
|
||||
fill(fillRule?: string): void
|
||||
fillRect(x: number, y: number, w: number, h: number): void
|
||||
fillText(text: string, x: number, y: number, maxWidth?: number): void
|
||||
lineTo(x: number, y: number): void
|
||||
measureText(text: string): TextMetrics
|
||||
moveTo(x: number, y: number): void
|
||||
quadraticCurveTo(cpx: IsNotImplementedInRockyYet, cpy: number, x: number, y: number): void
|
||||
rect(x: number, y: number, w: number, h: number): void
|
||||
restore(): void
|
||||
rotate(angle: IsNotImplementedInRockyYet): void
|
||||
save(): void
|
||||
scale(x: IsNotImplementedInRockyYet , y: number): void
|
||||
setTransform(m11: IsNotImplementedInRockyYet, m12: number, m21: number, m22: number, dx: number, dy: number): void
|
||||
stroke(): void
|
||||
strokeRect(x: number, y: number, w: number, h: number): void
|
||||
transform(m11: IsNotImplementedInRockyYet, m12: number, m21: number, m22: number, dx: number, dy: number): void
|
||||
translate(x: IsNotImplementedInRockyYet , y: number): void
|
||||
|
||||
rockyFillRadial(x: number, y: number, innerRadius: number, outerRadius: number, startAngle: number, endAngle: number): void
|
||||
}
|
||||
|
||||
interface TextMetrics {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
interface CanvasElement {
|
||||
clientWidth: number
|
||||
clientHeight: number
|
||||
unobstructedWidth: number
|
||||
unobstructedHeight: number
|
||||
unobstructedTop: number
|
||||
unobstructedLeft: number
|
||||
}
|
||||
|
||||
interface WatchInfo {
|
||||
platform: string
|
||||
model: string
|
||||
language: string
|
||||
firmware: { major: number, minor: number, patch: number, suffix: string }
|
||||
}
|
||||
|
||||
interface UserPreferences {
|
||||
contentSize: "small" | "medium" | "large" | "x-large"
|
||||
}
|
||||
|
||||
interface Rocky {
|
||||
on(eventName: "draw", eventListener: (event: DrawEvent) => void): void
|
||||
on(eventName: "memorypressure", eventListener: (event: MemoryPressureEvent) => void): void
|
||||
on(eventName: "message", eventListener: (event: MessageEvent) => void): void
|
||||
on(eventName: "postmessageconnected", eventListener: (event: PostMessageConnectionEvent) => void): void
|
||||
on(eventName: "postmessagedisconnected", eventListener: (event: PostMessageConnectionEvent) => void): void
|
||||
on(eventName: "postmessageerror", eventListener: (event: MessageEvent) => void): void
|
||||
on(eventName: "hourchange", eventListener: (event: TickEvent) => void): void
|
||||
on(eventName: "minutechange", eventListener: (event: TickEvent) => void): void
|
||||
on(eventName: "secondchange", eventListener: (event: TickEvent) => void): void
|
||||
on(eventName: "daychange", eventListener: (event: TickEvent) => void): void
|
||||
on(eventName: string, eventListener: (event: AnyEvent) => void): void
|
||||
addEventListener(eventName: "draw", eventListener: (event: DrawEvent) => void): void
|
||||
addEventListener(eventName: "memorypressure", eventListener: (event: MemoryPressureEvent) => void): void
|
||||
addEventListener(eventName: "message", eventListener: (event: MessageEvent) => void): void
|
||||
addEventListener(eventName: "postmessageconnected", eventListener: (event: PostMessageConnectionEvent) => void): void
|
||||
addEventListener(eventName: "postmessagedisconnected", eventListener: (event: PostMessageConnectionEvent) => void): void
|
||||
addEventListener(eventName: "postmessageerror", eventListener: (event: MessageEvent) => void): void
|
||||
addEventListener(eventName: "hourchange", eventListener: (event: TickEvent) => void): void
|
||||
addEventListener(eventName: "minutechange", eventListener: (event: TickEvent) => void): void
|
||||
addEventListener(eventName: "secondchange", eventListener: (event: TickEvent) => void): void
|
||||
addEventListener(eventName: "daychange", eventListener: (event: TickEvent) => void): void
|
||||
addEventListener(eventName: string, eventListener: (event: AnyEvent) => void): void
|
||||
off(eventName: "draw", eventListener: (event: DrawEvent) => void): void
|
||||
off(eventName: "memorypressure", eventListener: (event: MemoryPressureEvent) => void): void
|
||||
off(eventName: "message", eventListener: (event: MessageEvent) => void): void
|
||||
off(eventName: "postmessageconnected", eventListener: (event: PostMessageConnectionEvent) => void): void
|
||||
off(eventName: "postmessagedisconnected", eventListener: (event: PostMessageConnectionEvent) => void): void
|
||||
off(eventName: "postmessageerror", eventListener: (event: MessageEvent) => void): void
|
||||
off(eventName: "hourchange", eventListener: (event: TickEvent) => void): void
|
||||
off(eventName: "minutechange", eventListener: (event: TickEvent) => void): void
|
||||
off(eventName: "secondchange", eventListener: (event: TickEvent) => void): void
|
||||
off(eventName: "daychange", eventListener: (event: TickEvent) => void): void
|
||||
off(eventName: string, eventListener: (event: AnyEvent) => void): void
|
||||
removeEventListener(eventName: "draw", eventListener: (event: DrawEvent) => void): void
|
||||
removeEventListener(eventName: "memorypressure", eventListener: (event: MemoryPressureEvent) => void): void
|
||||
removeEventListener(eventName: "message", eventListener: (event: MessageEvent) => void): void
|
||||
removeEventListener(eventName: "postmessageconnected", eventListener: (event: PostMessageConnectionEvent) => void): void
|
||||
removeEventListener(eventName: "postmessagedisconnected", eventListener: (event: PostMessageConnectionEvent) => void): void
|
||||
removeEventListener(eventName: "postmessageerror", eventListener: (event: MessageEvent) => void): void
|
||||
removeEventListener(eventName: "hourchange", eventListener: (event: TickEvent) => void): void
|
||||
removeEventListener(eventName: "minutechange", eventListener: (event: TickEvent) => void): void
|
||||
removeEventListener(eventName: "secondchange", eventListener: (event: TickEvent) => void): void
|
||||
removeEventListener(eventName: "daychange", eventListener: (event: TickEvent) => void): void
|
||||
removeEventListener(eventName: string, eventListener: (event: AnyEvent) => void): void
|
||||
|
||||
postMessage(message: any): void
|
||||
requestDraw(): void
|
||||
watchInfo: WatchInfo
|
||||
userPreferences: UserPreferences
|
||||
Event: Event
|
||||
CanvasRenderingContext2D: CanvasRenderingContext2D
|
||||
CanvasElement: CanvasElement
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'rocky' {
|
||||
var rocky: rocky.Rocky;
|
||||
export = rocky
|
||||
}
|
||||
|
||||
interface Console {
|
||||
error(message?: string, ...optionalParams: any[]): void
|
||||
log(message?: string, ...optionalParams: any[]): void
|
||||
warn(message?: string, ...optionalParams: any[]): void
|
||||
}
|
||||
|
||||
declare var console: Console;
|
||||
|
||||
interface clearInterval {
|
||||
(handle: number): void
|
||||
}
|
||||
declare var clearInterval: clearInterval;
|
||||
|
||||
interface clearTimeout {
|
||||
(handle: number): void
|
||||
}
|
||||
declare var clearTimeout: clearTimeout;
|
||||
|
||||
interface setInterval {
|
||||
(handler: (...args: any[]) => void, timeout: number): number
|
||||
}
|
||||
declare var setInterval: setInterval;
|
||||
|
||||
interface setTimeout {
|
||||
(handler: (...args: any[]) => void, timeout: number): number
|
||||
}
|
||||
declare var setTimeout: setTimeout;
|
||||
|
||||
interface Require {
|
||||
(id: string): any
|
||||
}
|
||||
|
||||
interface RockyRequire extends Require {
|
||||
(id: 'rocky'): rocky.Rocky
|
||||
}
|
||||
|
||||
declare var require: RockyRequire;
|
||||
|
||||
interface Module {
|
||||
exports: any
|
||||
}
|
||||
|
||||
declare var module: Module;
|
7
sdk/tools/schemas/appinfo.json
Normal file
7
sdk/tools/schemas/appinfo.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Pebble JSON Schema",
|
||||
"description": "A project containing a Pebble application",
|
||||
"type": "object",
|
||||
"$ref": "file_types.json#/appinfo-json"
|
||||
}
|
76
sdk/tools/schemas/attributes.json
Normal file
76
sdk/tools/schemas/attributes.json
Normal file
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Pebble JSON Schema for Attributes",
|
||||
"description": "Schema for each type of valid attribute in Pebble projects",
|
||||
"appKeys": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^\\w*$": { "$ref": "data_types.json#/UInt32" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"capabilities": {
|
||||
"type": "array",
|
||||
"items": { "enum": ["location", "configurable", "health"] },
|
||||
"uniqueItems": true
|
||||
},
|
||||
"messageKeys": {
|
||||
"oneOf": [
|
||||
{ "$ref": "attributes.json#/appKeys" },
|
||||
{ "$ref": "data_types.json#/identifierArray" }
|
||||
]
|
||||
},
|
||||
"resources": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"media": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{ "$ref": "resource_types.json#/bitmap" },
|
||||
{ "$ref": "resource_types.json#/deprecatedImageFormat" },
|
||||
{ "$ref": "resource_types.json#/font" },
|
||||
{ "$ref": "resource_types.json#/raw" }
|
||||
]
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"publishedMedia": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{ "$ref": "resource_types.json#/publishedMediaAlias" },
|
||||
{ "$ref": "resource_types.json#/publishedMediaGlance" },
|
||||
{ "$ref": "resource_types.json#/publishedMediaTimeline" }
|
||||
]
|
||||
},
|
||||
"uniqueItems": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"dependencies": {
|
||||
"publishedMedia": [ "media" ]
|
||||
}
|
||||
},
|
||||
"sdkVersion": { "enum": [ "2", "3" ] },
|
||||
"targetPlatforms": {
|
||||
"type": "array",
|
||||
"items": { "enum": [ "aplite", "basalt", "chalk", "diorite" ] },
|
||||
"uniqueItems": true
|
||||
},
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
|
||||
},
|
||||
"watchapp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"watchface": { "type": "boolean" },
|
||||
"hiddenApp": { "type": "boolean" },
|
||||
"onlyShownOnCommunication": { "type": "boolean" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
27
sdk/tools/schemas/data_types.json
Normal file
27
sdk/tools/schemas/data_types.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Pebble JSON Schema for Types",
|
||||
"description": "Schema for complex data types in Pebble projects",
|
||||
"UInt8": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 255
|
||||
},
|
||||
"UInt32": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295
|
||||
},
|
||||
"identifier": {
|
||||
"type": "string",
|
||||
"pattern": "^\\w*$"
|
||||
},
|
||||
"identifierArray": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/identifier" }
|
||||
},
|
||||
"stringArray": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
67
sdk/tools/schemas/file_types.json
Normal file
67
sdk/tools/schemas/file_types.json
Normal file
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Pebble JSON Schema for Project JSON Files",
|
||||
"description": "Schema for supported JSON Pebble project files",
|
||||
"package-json": {
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"author": {
|
||||
"description": "https://docs.npmjs.com/files/package.json#people-fields-author-contributors",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^([^<(]+?)?[ \\\\t]*(?:<([^>(]+?)>)?[ \\\\t]*(?:\\\\(([^)]+?)\\\\)|$)"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"email": { "type": "string" },
|
||||
"url": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": { "type": "string" },
|
||||
"keywords": { "$ref": "data_types.json#/stringArray" },
|
||||
"private": { "type": "boolean" },
|
||||
"dependencies": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"files": { "$ref": "data_types.json#/stringArray" },
|
||||
"pebble": {
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{ "$ref": "project_types.json#/native-app" },
|
||||
{ "$ref": "project_types.json#/rocky-app" },
|
||||
{ "$ref": "project_types.json#/package" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [ "name", "author", "version", "pebble" ]
|
||||
},
|
||||
"appinfo-json": {
|
||||
"properties": {
|
||||
"uuid": { "$ref": "attributes.json#/uuid" },
|
||||
"shortName": { "type": "string" },
|
||||
"longName": { "type": "string" },
|
||||
"companyName": { "type": "string" },
|
||||
"versionCode": { "$ref": "data_types.json#/UInt8" },
|
||||
"versionLabel": { "type": "string" },
|
||||
"sdkVersion": { "$ref": "attributes.json#/sdkVersion" },
|
||||
"targetPlatforms": { "$ref": "attributes.json#/targetPlatforms" },
|
||||
"watchapp": { "$ref": "attributes.json#/watchapp" },
|
||||
"appKeys": { "$ref": "attributes.json#/appKeys" },
|
||||
"resources": { "$ref": "attributes.json#/resources" },
|
||||
"capabilities": { "$ref": "attributes.json#/capabilities" },
|
||||
"enableMultiJS": { "type": "boolean" },
|
||||
"projectType": { "enum": [ "native", "pebblejs" ] }
|
||||
},
|
||||
"required": ["uuid", "longName", "companyName", "versionLabel"]
|
||||
}
|
||||
}
|
7
sdk/tools/schemas/package.json
Normal file
7
sdk/tools/schemas/package.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Pebble JSON Schema",
|
||||
"description": "A project containing a Pebble application",
|
||||
"type": "object",
|
||||
"$ref": "file_types.json#/package-json"
|
||||
}
|
43
sdk/tools/schemas/project_types.json
Normal file
43
sdk/tools/schemas/project_types.json
Normal file
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Pebble JSON Schema for Project Types",
|
||||
"description": "Schema for each type of valid Pebble project",
|
||||
"native-app": {
|
||||
"properties": {
|
||||
"displayName": { "type": "string" },
|
||||
"uuid": { "$ref": "attributes.json#/uuid" },
|
||||
"sdkVersion": { "$ref": "attributes.json#/sdkVersion" },
|
||||
"projectType": { "enum": [ "native" ] },
|
||||
"enableMultiJS": { "type": "boolean" },
|
||||
"targetPlatforms": { "$ref": "attributes.json#/targetPlatforms" },
|
||||
"watchapp": { "$ref": "attributes.json#/watchapp" },
|
||||
"capabilities": { "$ref": "attributes.json#/capabilities" },
|
||||
"appKeys": { "$ref": "attributes.json#/appKeys" },
|
||||
"messageKeys": { "$ref": "attributes.json#/messageKeys" }
|
||||
},
|
||||
"required": [ "displayName", "uuid", "sdkVersion" ]
|
||||
},
|
||||
"rocky-app": {
|
||||
"properties": {
|
||||
"displayName": { "type": "string" },
|
||||
"uuid": { "$ref": "attributes.json#/uuid" },
|
||||
"sdkVersion": { "$ref": "attributes.json#/sdkVersion" },
|
||||
"projectType": { "enum": [ "rocky" ] },
|
||||
"enableMultiJS": { "type": "boolean" },
|
||||
"targetPlatforms": { "$ref": "attributes.json#/targetPlatforms" },
|
||||
"watchapp": { "$ref": "attributes.json#/watchapp" },
|
||||
"capabilities": { "$ref": "attributes.json#/capabilities" }
|
||||
},
|
||||
"required": [ "displayName", "uuid", "sdkVersion", "projectType" ]
|
||||
},
|
||||
"package": {
|
||||
"properties": {
|
||||
"sdkVersion": { "$ref": "attributes.json#/sdkVersion" },
|
||||
"projectType": { "enum": [ "package" ] },
|
||||
"targetPlatforms": { "$ref": "attributes.json#/targetPlatforms" },
|
||||
"capabilities": { "$ref": "attributes.json#/capabilities" },
|
||||
"messageKeys": { "$ref": "attributes.json#/messageKeys" }
|
||||
},
|
||||
"required": [ "sdkVersion", "projectType" ]
|
||||
}
|
||||
}
|
84
sdk/tools/schemas/resource_types.json
Normal file
84
sdk/tools/schemas/resource_types.json
Normal file
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Pebble JSON Schema for Resource Types",
|
||||
"description": "Schema for each type of valid resource in Pebble projects",
|
||||
"bitmap": {
|
||||
"properties": {
|
||||
"name": { "$ref": "data_types.json#/identifier" },
|
||||
"type": { "enum": ["bitmap"] },
|
||||
"file": { "type": "string" },
|
||||
"menuIcon": { "type": "boolean" },
|
||||
"targetPlatforms": { "$ref": "attributes.json#/targetPlatforms" },
|
||||
"storageFormat": { "enum": [ "pbi", "png" ] },
|
||||
"memoryFormat": {
|
||||
"enum": [
|
||||
"Smallest",
|
||||
"SmallestPalette",
|
||||
"1Bit",
|
||||
"8Bit",
|
||||
"1BitPalette",
|
||||
"2BitPalette",
|
||||
"4BitPalette"
|
||||
]
|
||||
},
|
||||
"spaceOptimization": { "enum": [ "storage", "memory" ] }
|
||||
}
|
||||
},
|
||||
"deprecatedImageFormat": {
|
||||
"properties": {
|
||||
"name": { "$ref": "data_types.json#/identifier" },
|
||||
"type": { "enum": ["png", "pbi", "pbi8", "png-trans"] },
|
||||
"file": { "type": "string" },
|
||||
"menuIcon": { "type": "boolean" },
|
||||
"targetPlatforms": { "$ref": "attributes.json#/targetPlatforms" }
|
||||
},
|
||||
"required": ["name", "type", "file"]
|
||||
},
|
||||
"font": {
|
||||
"properties": {
|
||||
"name": { "$ref": "data_types.json#/identifier" },
|
||||
"type": { "enum": ["font"] },
|
||||
"file": { "type": "string" },
|
||||
"targetPlatforms": { "$ref": "attributes.json#/targetPlatforms" },
|
||||
"characterRegex": { "type": "string" }
|
||||
},
|
||||
"required": ["name", "type", "file"]
|
||||
},
|
||||
"raw": {
|
||||
"properties": {
|
||||
"name": { "$ref": "data_types.json#/identifier" },
|
||||
"type": { "enum": ["raw"] },
|
||||
"file": { "type": "string" },
|
||||
"targetPlatforms": { "$ref": "attributes.json#/targetPlatforms" }
|
||||
},
|
||||
"required": ["name", "type", "file"]
|
||||
},
|
||||
|
||||
"publishedMediaItem": {
|
||||
"name": { "$ref": "data_types.json#/identifier" },
|
||||
"id": { "$ref": "data_types.json#/UInt32" },
|
||||
"alias": { "$ref": "data_types.json#/identifier" },
|
||||
"glance": { "$ref": "data_types.json#/identifier" },
|
||||
"timeline": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tiny": { "$ref": "data_types.json#/identifier" },
|
||||
"small": { "$ref": "data_types.json#/identifier" },
|
||||
"large": { "$ref": "data_types.json#/identifier" }
|
||||
},
|
||||
"required": [ "tiny" ]
|
||||
}
|
||||
},
|
||||
"publishedMediaAlias": {
|
||||
"properties": { "$ref": "#/publishedMediaItem" },
|
||||
"required": ["name", "id", "alias"]
|
||||
},
|
||||
"publishedMediaGlance": {
|
||||
"properties": { "$ref": "#/publishedMediaItem" },
|
||||
"required": ["name", "id", "glance"]
|
||||
},
|
||||
"publishedMediaTimeline": {
|
||||
"properties": { "$ref": "#/publishedMediaItem" },
|
||||
"required": ["name", "id", "timeline"]
|
||||
}
|
||||
}
|
40
sdk/tools/webpack/restricted-resource-loader.js
Normal file
40
sdk/tools/webpack/restricted-resource-loader.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
var fs = require('fs');
|
||||
|
||||
module.exports = function(source) {
|
||||
// Set this loader to cacheable
|
||||
this.cacheable();
|
||||
|
||||
// Whitelist files in the current project
|
||||
var whitelisted_folders = [this.options.context];
|
||||
|
||||
// Whitelist files from the SDK-appended search paths
|
||||
whitelisted_folders = whitelisted_folders.concat(this.options.resolve.root);
|
||||
|
||||
// Iterate over whitelisted file paths
|
||||
for (var i=0; i<whitelisted_folders.length; i++) {
|
||||
// If resource file is from a whitelisted path, return source
|
||||
if (~this.resourcePath.indexOf(fs.realpathSync(whitelisted_folders[i]))) {
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
// If the resource file is not from a whitelisted path, emit an error and fail the build
|
||||
this.emitError("Requiring a file outside of the current project folder is not permitted.");
|
||||
return "";
|
||||
};
|
96
sdk/tools/webpack/webpack-config.js.pytemplate
Normal file
96
sdk/tools/webpack/webpack-config.js.pytemplate
Normal file
|
@ -0,0 +1,96 @@
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Template vars injected by projess_js.py:
|
||||
|
||||
// boolean
|
||||
const isSandbox = ${IS_SANDBOX};
|
||||
|
||||
// Array with absolute file path strings
|
||||
const entryFilenames = ${ENTRY_FILENAMES};
|
||||
|
||||
// folder path string
|
||||
const outputPath = ${OUTPUT_PATH};
|
||||
|
||||
// file name string
|
||||
const outputFilename = ${OUTPUT_FILENAME};
|
||||
|
||||
// Array with absolute folder path strings
|
||||
const resolveRoots = ${RESOLVE_ROOTS};
|
||||
|
||||
// Object, { alias1: 'path1', ... }
|
||||
const resolveAliases = ${RESOLVE_ALIASES};
|
||||
|
||||
// null or Object with key 'sourceMapFilename'
|
||||
const sourceMapConfig = ${SOURCE_MAP_CONFIG};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// NOTE: Must escape dollar-signs, because this is a Python template!
|
||||
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = (() => {
|
||||
// The basic config:
|
||||
const config = {
|
||||
entry: entryFilenames,
|
||||
output: {
|
||||
path: outputPath,
|
||||
filename: outputFilename
|
||||
},
|
||||
target: 'node',
|
||||
resolve: {
|
||||
root: resolveRoots,
|
||||
extensions: ['', '.js', '.json'],
|
||||
alias: resolveAliases
|
||||
},
|
||||
resolveLoader: {
|
||||
root: resolveRoots
|
||||
}
|
||||
};
|
||||
|
||||
if (sourceMapConfig) {
|
||||
// Enable webpack's source map output:
|
||||
config.devtool = 'source-map';
|
||||
config.output.sourceMapFilename = sourceMapConfig.sourceMapFilename;
|
||||
config.output.devtoolModuleFilenameTemplate = '[resource-path]';
|
||||
config.output.devtoolFallbackModuleFilenameTemplate = '[resourcePath]?[hash]';
|
||||
}
|
||||
|
||||
return config;
|
||||
})();
|
||||
|
||||
module.exports.plugins = (() => {
|
||||
const plugins = [
|
||||
// Returns a non-zero exit code when webpack reports an error:
|
||||
require('webpack-fail-plugin'),
|
||||
|
||||
// Includes _message_keys_wrapper in every build to mimic old loader.js:
|
||||
new webpack.ProvidePlugin({ require: '_message_key_wrapper' })
|
||||
];
|
||||
|
||||
if (isSandbox) {
|
||||
// Prevents using `require('evil_loader!mymodule')` to execute custom
|
||||
// loader code during the webpack build.
|
||||
const RestrictResourcePlugin = require('restrict-resource-webpack-plugin');
|
||||
const plugin = new RestrictResourcePlugin(/!+/,
|
||||
'Custom inline loaders are not permitted.');
|
||||
plugins.push(plugin);
|
||||
}
|
||||
|
||||
return plugins;
|
||||
})();
|
||||
|
||||
module.exports.module = {
|
||||
loaders: (() => {
|
||||
const loaders = [{'test': /\.json$$/, 'loader': 'json-loader'}];
|
||||
|
||||
if (isSandbox) {
|
||||
// See restricted-resource-loader.js, prevents loading files outside
|
||||
// of the project folder, i.e. `require(../../not_your_business)`:
|
||||
const restrictLoader = {
|
||||
'test': /^.*/, 'loader': 'restricted-resource-loader'
|
||||
};
|
||||
loaders.push(restrictLoader);
|
||||
}
|
||||
|
||||
return loaders;
|
||||
})()
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue