summaryrefslogtreecommitdiff
path: root/tools/gen_products.py
blob: 7504287881fcaadf07773948aee7f7d39b4d394c (plain) (blame)
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())