1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
#!/usr/bin/env python3
# Author: cirakg & claude ai
# Summary: Check that every product in manifest.xml is a watch with API 5.0+ and at least 512 KB for watch apps.
"""Check the manifest's product list against the downloaded device files.
The target rule (README section 14, decision D4): a product belongs in
manifest.xml when it is in Garmin's "Watches/Wearables" group, every firmware
variant supports Connect IQ API 5.0.0 or newer, and watch apps get at least
512 KB of memory. The data comes from each device's compiler.json, which
`make sdk` downloads for exactly the products in the manifest.
Usage: gen_products.py --check [--manifest manifest.xml] [--devices DIR]
Exit: 0 every product qualifies, 1 some don't, 2 device files missing.
Finding devices Garmin releases later needs the device catalogue (a login);
re-run `make sdk` after adding a new product to the manifest.
"""
from __future__ import annotations
import argparse
import json
import sys
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from pathlib import Path
EXIT_OK, EXIT_UNQUALIFIED, EXIT_MISSING = 0, 1, 2
IQ_NS = "{http://www.garmin.com/xml/connectiq}"
MIN_API = (5, 0, 0)
MIN_WATCH_APP_MEMORY = 512 * 1024
WATCH_GROUP = "Watches/Wearables"
DEFAULT_DEVICES = Path.home() / ".Garmin" / "ConnectIQ" / "Devices"
@dataclass(frozen=True)
class Device:
"""What the target rule needs from one compiler.json."""
product: str
name: str
group: str
min_api: tuple[int, ...]
watch_app_memory: int
def problems(self) -> list[str]:
reasons = []
if self.group != WATCH_GROUP:
reasons.append(f"group is '{self.group}', not '{WATCH_GROUP}'")
if self.min_api < MIN_API:
reasons.append(f"API {'.'.join(map(str, self.min_api))} is below 5.0.0")
if self.watch_app_memory < MIN_WATCH_APP_MEMORY:
reasons.append(f"watch apps get {self.watch_app_memory // 1024} KB, below 512 KB")
return reasons
def parse_version(text: str) -> tuple[int, ...]:
return tuple(int(part) for part in text.split("."))
def load_device(devices: Path, product: str) -> Device:
compiler = json.loads((devices / product / "compiler.json").read_text(encoding="utf-8"))
memory = {app["type"]: int(app["memoryLimit"]) for app in compiler["appTypes"]}
return Device(
product=product,
name=str(compiler.get("displayName", product)),
group=str(compiler.get("webDocDeviceGroup", "")),
min_api=min(parse_version(p["connectIQVersion"]) for p in compiler["partNumbers"]),
watch_app_memory=memory.get("watchApp", 0),
)
def manifest_products(manifest: Path) -> list[str]:
root = ET.parse(manifest).getroot()
return [p.attrib["id"] for p in root.iter(f"{IQ_NS}product")]
def check(manifest: Path, devices: Path) -> int:
products = manifest_products(manifest)
worst = EXIT_OK
for product in products:
if not (devices / product / "compiler.json").exists():
print(f"{product}: no device files (run 'make sdk')")
worst = max(worst, EXIT_MISSING)
continue
device = load_device(devices, product)
for reason in device.problems():
print(f"{product} ({device.name}): {reason}")
worst = max(worst, EXIT_UNQUALIFIED)
print(f"gen_products: {len(products)} products checked")
return worst
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Check the manifest's products against the target rule.")
parser.add_argument("--check", action="store_true", required=True)
parser.add_argument("--manifest", type=Path, default=Path("manifest.xml"))
parser.add_argument("--devices", type=Path, default=DEFAULT_DEVICES)
args = parser.parse_args(argv)
return check(args.manifest, args.devices)
if __name__ == "__main__":
sys.exit(main())
|