logo

qmk_firmware

custom branch of QMK firmware git clone https://anongit.hacktivis.me/git/qmk_firmware.git

userspace.py (8170B)


  1. # Copyright 2023-2024 Nick Brassel (@tzarc)
  2. # SPDX-License-Identifier: GPL-2.0-or-later
  3. from os import environ
  4. from pathlib import Path
  5. import json
  6. import jsonschema
  7. from milc import cli
  8. from qmk.json_schema import validate, json_load
  9. from qmk.json_encoders import UserspaceJSONEncoder
  10. def qmk_userspace_paths():
  11. test_dirs = []
  12. # If we're already in a directory with a qmk.json and a keyboards or layouts directory, interpret it as userspace
  13. if environ.get('ORIG_CWD') is not None:
  14. current_dir = Path(environ['ORIG_CWD'])
  15. while len(current_dir.parts) > 1:
  16. if (current_dir / 'qmk.json').is_file():
  17. test_dirs.append(current_dir)
  18. current_dir = current_dir.parent
  19. # If we have a QMK_USERSPACE environment variable, use that
  20. if environ.get('QMK_USERSPACE') is not None:
  21. current_dir = Path(environ['QMK_USERSPACE']).expanduser()
  22. if current_dir.is_dir():
  23. test_dirs.append(current_dir)
  24. # If someone has configured a directory, use that
  25. if cli.config.user.overlay_dir is not None:
  26. current_dir = Path(cli.config.user.overlay_dir).expanduser().resolve()
  27. if current_dir.is_dir():
  28. test_dirs.append(current_dir)
  29. # remove duplicates while maintaining the current order
  30. return list(dict.fromkeys(test_dirs))
  31. def qmk_userspace_validate(path):
  32. # Construct a UserspaceDefs object to ensure it validates correctly
  33. if (path / 'qmk.json').is_file():
  34. UserspaceDefs(path / 'qmk.json')
  35. return
  36. # No qmk.json file found
  37. raise FileNotFoundError('No qmk.json file found.')
  38. def detect_qmk_userspace():
  39. # Iterate through all the detected userspace paths and return the first one that validates correctly
  40. test_dirs = qmk_userspace_paths()
  41. for test_dir in test_dirs:
  42. try:
  43. qmk_userspace_validate(test_dir)
  44. return test_dir
  45. except FileNotFoundError:
  46. continue
  47. except UserspaceValidationError:
  48. continue
  49. return None
  50. class UserspaceDefs:
  51. def __init__(self, userspace_json: Path):
  52. self.path = userspace_json
  53. self.build_targets = []
  54. json = json_load(userspace_json)
  55. exception = UserspaceValidationError()
  56. success = False
  57. try:
  58. validate(json, 'qmk.user_repo.v0') # `qmk.json` must have a userspace_version at minimum
  59. except jsonschema.ValidationError as err:
  60. exception.add('qmk.user_repo.v0', err)
  61. raise exception
  62. # Iterate through each version of the schema, starting with the latest and decreasing to v1
  63. schema_versions = [
  64. ('qmk.user_repo.v1_1', self.__load_v1_1), #
  65. ('qmk.user_repo.v1', self.__load_v1) #
  66. ]
  67. for v in schema_versions:
  68. schema = v[0]
  69. loader = v[1]
  70. try:
  71. validate(json, schema)
  72. loader(json)
  73. success = True
  74. break
  75. except jsonschema.ValidationError as err:
  76. exception.add(schema, err)
  77. if not success:
  78. raise exception
  79. def save(self):
  80. target_json = {
  81. "userspace_version": "1.1", # Needs to match latest version
  82. "build_targets": []
  83. }
  84. for e in self.build_targets:
  85. if isinstance(e, dict):
  86. entry = [e['keyboard'], e['keymap']]
  87. if 'env' in e:
  88. entry.append(e['env'])
  89. target_json['build_targets'].append(entry)
  90. elif isinstance(e, Path):
  91. target_json['build_targets'].append(str(e.relative_to(self.path.parent)))
  92. try:
  93. # Ensure what we're writing validates against the latest version of the schema
  94. validate(target_json, 'qmk.user_repo.v1_1')
  95. except jsonschema.ValidationError as err:
  96. cli.log.error(f'Could not save userspace file: {err}')
  97. return False
  98. # Only actually write out data if it changed
  99. old_data = json.dumps(json.loads(self.path.read_text()), cls=UserspaceJSONEncoder, sort_keys=True)
  100. new_data = json.dumps(target_json, cls=UserspaceJSONEncoder, sort_keys=True)
  101. if old_data != new_data:
  102. self.path.write_text(new_data)
  103. cli.log.info(f'Saved userspace file to {self.path}.')
  104. return True
  105. def add_target(self, keyboard=None, keymap=None, build_env=None, json_path=None, do_print=True):
  106. if json_path is not None:
  107. # Assume we're adding a json filename/path
  108. json_path = Path(json_path)
  109. if json_path not in self.build_targets:
  110. self.build_targets.append(json_path)
  111. if do_print:
  112. cli.log.info(f'Added {json_path} to userspace build targets.')
  113. else:
  114. cli.log.info(f'{json_path} is already a userspace build target.')
  115. elif keyboard is not None and keymap is not None:
  116. # Both keyboard/keymap specified
  117. e = {"keyboard": keyboard, "keymap": keymap}
  118. if build_env is not None:
  119. e['env'] = build_env
  120. if e not in self.build_targets:
  121. self.build_targets.append(e)
  122. if do_print:
  123. cli.log.info(f'Added {keyboard}:{keymap} to userspace build targets.')
  124. else:
  125. if do_print:
  126. cli.log.info(f'{keyboard}:{keymap} is already a userspace build target.')
  127. def remove_target(self, keyboard=None, keymap=None, build_env=None, json_path=None, do_print=True):
  128. if json_path is not None:
  129. # Assume we're removing a json filename/path
  130. json_path = Path(json_path)
  131. if json_path in self.build_targets:
  132. self.build_targets.remove(json_path)
  133. if do_print:
  134. cli.log.info(f'Removed {json_path} from userspace build targets.')
  135. else:
  136. cli.log.info(f'{json_path} is not a userspace build target.')
  137. elif keyboard is not None and keymap is not None:
  138. # Both keyboard/keymap specified
  139. e = {"keyboard": keyboard, "keymap": keymap}
  140. if build_env is not None:
  141. e['env'] = build_env
  142. if e in self.build_targets:
  143. self.build_targets.remove(e)
  144. if do_print:
  145. cli.log.info(f'Removed {keyboard}:{keymap} from userspace build targets.')
  146. else:
  147. if do_print:
  148. cli.log.info(f'{keyboard}:{keymap} is not a userspace build target.')
  149. def __load_v1(self, json):
  150. for e in json['build_targets']:
  151. self.__load_v1_target(e)
  152. def __load_v1_1(self, json):
  153. for e in json['build_targets']:
  154. self.__load_v1_1_target(e)
  155. def __load_v1_target(self, e):
  156. if isinstance(e, list) and len(e) == 2:
  157. self.add_target(keyboard=e[0], keymap=e[1], do_print=False)
  158. if isinstance(e, str):
  159. p = self.path.parent / e
  160. if p.exists() and p.suffix == '.json':
  161. self.add_target(json_path=p, do_print=False)
  162. def __load_v1_1_target(self, e):
  163. # v1.1 adds support for a third item in the build target tuple; kvp's for environment
  164. if isinstance(e, list) and len(e) == 3:
  165. self.add_target(keyboard=e[0], keymap=e[1], build_env=e[2], do_print=False)
  166. else:
  167. self.__load_v1_target(e)
  168. class UserspaceValidationError(Exception):
  169. def __init__(self, *args, **kwargs):
  170. super().__init__(*args, **kwargs)
  171. self.__exceptions = []
  172. def __str__(self):
  173. return self.message
  174. @property
  175. def exceptions(self):
  176. return self.__exceptions
  177. def add(self, schema, exception):
  178. self.__exceptions.append((schema, exception))
  179. errorlist = "\n\n".join([f"{schema}: {exception}" for schema, exception in self.__exceptions])
  180. self.message = f'Could not validate against any version of the userspace schema. Errors:\n\n{errorlist}'