From d2fbd3ce8d962e4a46351df0f34f14de71871e00 Mon Sep 17 00:00:00 2001
From: Sefa Eyeoglu <contact@scrumplex.net>
Date: Mon, 21 Oct 2024 16:25:11 +0200
Subject: [PATCH] a/bindings: improve reproducibility of bindings generation

Signed-off-by: Sefa Eyeoglu <contact@scrumplex.net>
Part-of: <https://gitlab.freedesktop.org/monado/monado/-/merge_requests/2338>
---
 src/xrt/auxiliary/bindings/bindings.py        | 37 ++++++++-----------
 .../auxiliary/bindings/steamvr_profiles.py    |  2 +-
 2 files changed, 17 insertions(+), 22 deletions(-)

diff --git a/src/xrt/auxiliary/bindings/bindings.py b/src/xrt/auxiliary/bindings/bindings.py
index d10255962..56cbc43e6 100755
--- a/src/xrt/auxiliary/bindings/bindings.py
+++ b/src/xrt/auxiliary/bindings/bindings.py
@@ -7,8 +7,7 @@ bindings."""
 import argparse
 import json
 import copy
-import itertools
-
+from operator import attrgetter
 
 def find_component_in_list_by_name(name, component_list, subaction_path=None, identifier_json_path=None):
     """Find a component with the given name in a list of components."""
@@ -124,7 +123,6 @@ class Component:
         Turn a Identifier's component paths into a list of Component objects.
         """
 
-        monado_bindings = json_subpath["monado_bindings"]
         component_list = []
         for component_name in json_subpath["components"]:  # click, touch, ...
             matched_dpad_emulation = None
@@ -132,9 +130,7 @@ class Component:
                     json_subpath["dpad_emulation"]["position"] == component_name):
                 matched_dpad_emulation = json_subpath["dpad_emulation"]
 
-            monado_binding = None
-            if component_name in monado_bindings:
-                monado_binding = monado_bindings[component_name]
+            monado_binding = json_subpath["monado_bindings"].get(component_name, None)
 
             steamvr_path = steamvr_subpath_name(identifier_json_path, json_subpath["type"])
             if "steamvr_path" in json_subpath:
@@ -224,10 +220,9 @@ class Identifier:
 
         identifier_list = []
         for subaction_path in json_subaction_paths:  # /user/hand/*
-            for json_sub_path_itm in json_subpaths.items():  # /input/*, /output/*
-                json_path = json_sub_path_itm[0]  # /input/trackpad
+            for json_path in sorted(json_subpaths.keys()):  # /input/*, /output/*
                 # json object associated with a subpath (type, localized_name, ...)
-                json_subpath = json_sub_path_itm[1]
+                json_subpath = json_subpaths[json_path]
 
                 # Oculus Touch a,b/x,y components only exist on one controller
                 if "side" in json_subpath and "/user/hand/" + json_subpath["side"] != subaction_path:
@@ -370,8 +365,8 @@ class Profile:
     def __update_component_list(self):
         self.components = []
         for identifier in self.identifiers:
-            for component in identifier.components:
-                self.components.append(component)
+            self.components += identifier.components
+        self.components = sorted(self.components, key=attrgetter("steamvr_path"))
 
 
 oxr_verify_extension_status_struct_name = "oxr_verify_extension_status"
@@ -395,8 +390,8 @@ class Bindings:
 
     def __init__(self, json_root):
         """Construct a bindings from a dictionary of profiles."""
-        self.profiles = [Profile(profile_name, json_profile) for
-                         profile_name, json_profile in json_root["profiles"].items()]
+        self.profiles = [Profile(profile_name, json_root["profiles"][profile_name]) for
+                         profile_name in sorted(json_root["profiles"].keys())]
         self.__set_parent_profile_refs()
         self.__mine_for_diamond_errors()
 
@@ -422,7 +417,7 @@ class Bindings:
         if profile.name in parent_path_set:
             return True
         parent_path_set.append(profile.name)
-        for parent in profile.parent_profiles:
+        for parent in sorted(profile.parent_profiles, key=attrgetter("name")):
             if self.__has_diamonds(parent, parent_path_set):
                 return True
         return False
@@ -468,9 +463,9 @@ def write_verify_switch_body(f, dict_of_lists, profile, profile_name, ext_name,
     Input is a file to write the code into, a dict where keys are length and
     the values are lists of strings of that length. And a suffix if any."""
     f.write(f"{tab_char}\tswitch (length) {{\n")
-    for length in dict_of_lists:
+    for length in sorted(dict_of_lists.keys()):
         f.write(f"{tab_char}\tcase {str(length)}:\n\t\t")
-        for path in dict_of_lists[length]:
+        for path in sorted(dict_of_lists[length]):
             f.write(if_strcmp.format(exttab=tab_char, check=path))
         f.write(f"{tab_char}{{\n{tab_char}\t\t\tbreak;\n{tab_char}\t\t}}\n")
     f.write(f"{tab_char}\tdefault: break;\n{tab_char}\t}}\n")
@@ -514,7 +509,7 @@ def write_verify_func_body(f, profile, dict_name):
         profile, dict_name), profile, profile.name, profile.extension_name)
     if profile.parent_profiles is None:
         return
-    for pp in profile.parent_profiles:
+    for pp in sorted(profile.parent_profiles, key=attrgetter("name")):
         write_verify_func_body(f, pp, dict_name)
 
 
@@ -722,7 +717,7 @@ def generate_bindings_c(file, b):
     f.write('{\n')
     f.write('\tswitch(input)\n')
     f.write('\t{\n')
-    for input in inputs:
+    for input in sorted(inputs):
         f.write(f'\tcase {input}: return "{input}";\n')
     f.write(f'\tdefault: return "UNKNOWN";\n')
     f.write('\t}\n')
@@ -731,7 +726,7 @@ def generate_bindings_c(file, b):
     f.write('enum xrt_input_name\n')
     f.write('xrt_input_name_enum(const char *input)\n')
     f.write('{\n')
-    for input in inputs:
+    for input in sorted(inputs):
         f.write(f'\tif(strcmp("{input}", input) == 0) return {input};\n')
     f.write(f'\treturn XRT_INPUT_GENERIC_TRACKER_POSE;\n')
     f.write('}\n')
@@ -741,7 +736,7 @@ def generate_bindings_c(file, b):
     f.write('{\n')
     f.write('\tswitch(output)\n')
     f.write('\t{\n')
-    for output in outputs:
+    for output in sorted(outputs):
         f.write(f'\tcase {output}: return "{output}";\n')
     f.write(f'\tdefault: return "UNKNOWN";\n')
     f.write('\t}\n')
@@ -750,7 +745,7 @@ def generate_bindings_c(file, b):
     f.write('enum xrt_output_name\n')
     f.write('xrt_output_name_enum(const char *output)\n')
     f.write('{\n')
-    for output in outputs:
+    for output in sorted(outputs):
         f.write(f'\tif(strcmp("{output}", output) == 0) return {output};\n')
     f.write(f'\treturn XRT_OUTPUT_NAME_SIMPLE_VIBRATION;\n')
     f.write('}\n')
diff --git a/src/xrt/auxiliary/bindings/steamvr_profiles.py b/src/xrt/auxiliary/bindings/steamvr_profiles.py
index abcbfabe6..f54a303f9 100644
--- a/src/xrt/auxiliary/bindings/steamvr_profiles.py
+++ b/src/xrt/auxiliary/bindings/steamvr_profiles.py
@@ -114,7 +114,7 @@ def main():
 
         # print("Creating SteamVR input profile", f.name)
 
-        json.dump(j, f, indent=2)
+        json.dump(j, f, indent=2, sort_keys=True)
 
         f.close()