path.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. """Functions that help us work with files and folders.
  2. """
  3. import logging
  4. import os
  5. import argparse
  6. from pathlib import Path, PureWindowsPath, PurePosixPath
  7. from qmk.constants import MAX_KEYBOARD_SUBFOLDERS, QMK_FIRMWARE, QMK_USERSPACE, HAS_QMK_USERSPACE
  8. from qmk.errors import NoSuchKeyboardError
  9. def is_keyboard(keyboard_name):
  10. """Returns True if `keyboard_name` is a keyboard we can compile.
  11. """
  12. if not keyboard_name:
  13. return False
  14. # keyboard_name values of 'c:/something' or '/something' trigger append issues
  15. # due to "If the argument is an absolute path, the previous path is ignored"
  16. # however it should always be a folder located under qmk_firmware/keyboards
  17. if Path(keyboard_name).is_absolute():
  18. return False
  19. keyboard_json = QMK_FIRMWARE / 'keyboards' / keyboard_name / 'keyboard.json'
  20. return keyboard_json.exists()
  21. def under_qmk_firmware(path=Path(os.environ['ORIG_CWD'])):
  22. """Returns a Path object representing the relative path under qmk_firmware, or None.
  23. """
  24. try:
  25. return path.relative_to(QMK_FIRMWARE)
  26. except ValueError:
  27. return None
  28. def under_qmk_userspace(path=Path(os.environ['ORIG_CWD'])):
  29. """Returns a Path object representing the relative path under $QMK_USERSPACE, or None.
  30. """
  31. try:
  32. if HAS_QMK_USERSPACE:
  33. return path.relative_to(QMK_USERSPACE)
  34. except ValueError:
  35. pass
  36. return None
  37. def is_under_qmk_firmware(path=Path(os.environ['ORIG_CWD'])):
  38. """Returns a boolean if the input path is a child under qmk_firmware.
  39. """
  40. if path is None:
  41. return False
  42. try:
  43. return Path(os.path.commonpath([Path(path), QMK_FIRMWARE])) == QMK_FIRMWARE
  44. except ValueError:
  45. return False
  46. def is_under_qmk_userspace(path=Path(os.environ['ORIG_CWD'])):
  47. """Returns a boolean if the input path is a child under $QMK_USERSPACE.
  48. """
  49. if path is None:
  50. return False
  51. try:
  52. if HAS_QMK_USERSPACE:
  53. return Path(os.path.commonpath([Path(path), QMK_USERSPACE])) == QMK_USERSPACE
  54. except ValueError:
  55. return False
  56. def keyboard(keyboard_name):
  57. """Returns the path to a keyboard's directory relative to the qmk root.
  58. """
  59. return Path('keyboards') / keyboard_name
  60. def keymaps(keyboard_name):
  61. """Returns all of the `keymaps/` directories for a given keyboard.
  62. Args:
  63. keyboard_name
  64. The name of the keyboard. Example: clueboard/66/rev3
  65. """
  66. keyboard_folder = keyboard(keyboard_name)
  67. found_dirs = []
  68. if HAS_QMK_USERSPACE:
  69. this_keyboard_folder = Path(QMK_USERSPACE) / keyboard_folder
  70. for _ in range(MAX_KEYBOARD_SUBFOLDERS):
  71. if (this_keyboard_folder / 'keymaps').exists():
  72. found_dirs.append((this_keyboard_folder / 'keymaps').resolve())
  73. this_keyboard_folder = this_keyboard_folder.parent
  74. if this_keyboard_folder.resolve() == QMK_USERSPACE.resolve():
  75. break
  76. # We don't have any relevant keymap directories in userspace, so we'll use the fully-qualified path instead.
  77. if len(found_dirs) == 0:
  78. found_dirs.append((QMK_USERSPACE / keyboard_folder / 'keymaps').resolve())
  79. this_keyboard_folder = QMK_FIRMWARE / keyboard_folder
  80. for _ in range(MAX_KEYBOARD_SUBFOLDERS):
  81. if (this_keyboard_folder / 'keymaps').exists():
  82. found_dirs.append((this_keyboard_folder / 'keymaps').resolve())
  83. this_keyboard_folder = this_keyboard_folder.parent
  84. if this_keyboard_folder.resolve() == QMK_FIRMWARE.resolve():
  85. break
  86. if len(found_dirs) > 0:
  87. return found_dirs
  88. logging.error('Could not find the keymaps directory!')
  89. raise NoSuchKeyboardError('Could not find keymaps directory for: %s' % keyboard_name)
  90. def keymap(keyboard_name, keymap_name):
  91. """Locate the directory of a given keymap.
  92. Args:
  93. keyboard_name
  94. The name of the keyboard. Example: clueboard/66/rev3
  95. keymap_name
  96. The name of the keymap. Example: default
  97. """
  98. for keymap_dir in keymaps(keyboard_name):
  99. if (keymap_dir / keymap_name).exists():
  100. return (keymap_dir / keymap_name).resolve()
  101. def normpath(path):
  102. """Returns a `pathlib.Path()` object for a given path.
  103. This will use the path to a file as seen from the directory the script was called from. You should use this to normalize filenames supplied from the command line.
  104. """
  105. path = Path(path)
  106. if path.is_absolute():
  107. return path
  108. return Path(os.environ['ORIG_CWD']) / path
  109. def is_relative_to(file, other):
  110. """Provide normpath behavior to Path.is_relative_to
  111. """
  112. return normpath(file).is_relative_to(normpath(other))
  113. def unix_style_path(path):
  114. """Converts a Windows-style path with drive letter to a Unix path.
  115. Path().as_posix() normally returns the path with drive letter and forward slashes, so is inappropriate for `Makefile` paths.
  116. Passes through unadulterated if the path is not a Windows-style path.
  117. Args:
  118. path
  119. The path to convert.
  120. Returns:
  121. The input path converted to Unix format.
  122. """
  123. if isinstance(path, PureWindowsPath):
  124. p = list(path.parts)
  125. p[0] = f'/{p[0][0].lower()}' # convert from `X:/` to `/x`
  126. path = PurePosixPath(*p)
  127. return path
  128. class FileType(argparse.FileType):
  129. def __init__(self, *args, **kwargs):
  130. # Use UTF8 by default for stdin
  131. if 'encoding' not in kwargs:
  132. kwargs['encoding'] = 'UTF-8'
  133. return super().__init__(*args, **kwargs)
  134. def __call__(self, string):
  135. """normalize and check exists
  136. otherwise magic strings like '-' for stdin resolve to bad paths
  137. """
  138. # TODO: This should not return both Path and TextIOWrapper as consumers
  139. # assume that they can call Path.as_posix without checking type
  140. # Handle absolute paths and relative paths to CWD
  141. norm = normpath(string)
  142. if norm.exists():
  143. return norm
  144. # Handle relative paths to QMK_HOME
  145. relative = Path(string)
  146. if relative.exists():
  147. return relative
  148. return super().__call__(string)