utils.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. # Copyright (C) 2022 The Qt Company Ltd.
  2. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
  3. from __future__ import annotations
  4. import subprocess
  5. import sys
  6. import xml.etree.ElementTree as ET
  7. from pathlib import Path
  8. from . import (QTPATHS_CMD, PYPROJECT_JSON_PATTERN, PYPROJECT_TOML_PATTERN, PYPROJECT_FILE_PATTERNS,
  9. ClOptions)
  10. from .pyproject_toml import parse_pyproject_toml
  11. from .pyproject_json import parse_pyproject_json
  12. def run_command(command: list[str], cwd: str = None, ignore_fail: bool = False) -> int:
  13. """
  14. Run a command using a subprocess.
  15. If dry run is enabled, the command will be printed to stdout instead of being executed.
  16. :param command: The command to run including the arguments
  17. :param cwd: The working directory to run the command in
  18. :param ignore_fail: If True, the current process will not exit if the command fails
  19. :return: The exit code of the command
  20. """
  21. cloptions = ClOptions()
  22. if not cloptions.quiet or cloptions.dry_run:
  23. print(" ".join(command))
  24. if cloptions.dry_run:
  25. return 0
  26. ex = subprocess.call(command, cwd=cwd)
  27. if ex != 0 and not ignore_fail:
  28. sys.exit(ex)
  29. return ex
  30. def qrc_file_requires_rebuild(resources_file_path: Path, compiled_resources_path: Path) -> bool:
  31. """Returns whether a compiled qrc file needs to be rebuilt based on the files that references"""
  32. root_element = ET.parse(resources_file_path).getroot()
  33. project_root = resources_file_path.parent
  34. files = [project_root / file.text for file in root_element.findall(".//file")]
  35. compiled_resources_time = compiled_resources_path.stat().st_mtime
  36. # If any of the resource files has been modified after the compiled qrc file, the compiled qrc
  37. # file needs to be rebuilt
  38. if any(file.is_file() and file.stat().st_mtime > compiled_resources_time for file in files):
  39. return True
  40. return False
  41. def requires_rebuild(sources: list[Path], artifact: Path) -> bool:
  42. """Returns whether artifact needs to be rebuilt depending on sources"""
  43. if not artifact.is_file():
  44. return True
  45. artifact_mod_time = artifact.stat().st_mtime
  46. for source in sources:
  47. if source.stat().st_mtime > artifact_mod_time:
  48. return True
  49. # The .qrc file references other files that might have changed
  50. if source.suffix == ".qrc" and qrc_file_requires_rebuild(source, artifact):
  51. return True
  52. return False
  53. def _remove_path_recursion(path: Path):
  54. """Recursion to remove a file or directory."""
  55. if path.is_file():
  56. path.unlink()
  57. elif path.is_dir():
  58. for item in path.iterdir():
  59. _remove_path_recursion(item)
  60. path.rmdir()
  61. def remove_path(path: Path):
  62. """Remove path (file or directory) observing opt_dry_run."""
  63. cloptions = ClOptions()
  64. if not path.exists():
  65. return
  66. if not cloptions.quiet:
  67. print(f"Removing {path.name}...")
  68. if cloptions.dry_run:
  69. return
  70. _remove_path_recursion(path)
  71. def package_dir() -> Path:
  72. """Return the PySide6 root."""
  73. return Path(__file__).resolve().parents[2]
  74. _qtpaths_info: dict[str, str] = {}
  75. def qtpaths() -> dict[str, str]:
  76. """Run qtpaths and return a dict of values."""
  77. global _qtpaths_info
  78. if not _qtpaths_info:
  79. output = subprocess.check_output([QTPATHS_CMD, "--query"])
  80. for line in output.decode("utf-8").split("\n"):
  81. tokens = line.strip().split(":", maxsplit=1) # "Path=C:\..."
  82. if len(tokens) == 2:
  83. _qtpaths_info[tokens[0]] = tokens[1]
  84. return _qtpaths_info
  85. _qt_metatype_json_dir: Path | None = None
  86. def qt_metatype_json_dir() -> Path:
  87. """Return the location of the Qt QML metatype files."""
  88. global _qt_metatype_json_dir
  89. if not _qt_metatype_json_dir:
  90. qt_dir = package_dir()
  91. if sys.platform != "win32":
  92. qt_dir /= "Qt"
  93. metatypes_dir = qt_dir / "metatypes"
  94. if metatypes_dir.is_dir(): # Fully installed case
  95. _qt_metatype_json_dir = metatypes_dir
  96. else:
  97. # Fallback for distro builds/development.
  98. print(
  99. f"Falling back to {QTPATHS_CMD} to determine metatypes directory.", file=sys.stderr
  100. )
  101. _qt_metatype_json_dir = Path(qtpaths()["QT_INSTALL_ARCHDATA"]) / "metatypes"
  102. return _qt_metatype_json_dir
  103. def resolve_valid_project_file(
  104. project_path_input: str = None, project_file_patterns: list[str] = PYPROJECT_FILE_PATTERNS
  105. ) -> Path:
  106. """
  107. Find a valid project file given a preferred project file name and a list of project file name
  108. patterns for a fallback search.
  109. If the provided file name is a valid project file, return it. Otherwise, search for a known
  110. project file in the current working directory with the given patterns.
  111. Raises a ValueError if no project file is found, multiple project files are found in the same
  112. directory or the provided path is not a valid project file or folder.
  113. :param project_path_input: The command-line argument specifying a project file or folder path.
  114. :param project_file_patterns: The list of project file patterns to search for.
  115. :return: The resolved project file path
  116. """
  117. if project_path_input and (project_file := Path(project_path_input).resolve()).is_file():
  118. if project_file.match(PYPROJECT_TOML_PATTERN):
  119. if bool(parse_pyproject_toml(project_file).errors):
  120. raise ValueError(f"Invalid project file: {project_file}")
  121. elif project_file.match(PYPROJECT_JSON_PATTERN):
  122. pyproject_json_result = parse_pyproject_json(project_file)
  123. if errors := '\n'.join(str(e) for e in pyproject_json_result.errors):
  124. raise ValueError(f"Invalid project file: {project_file}\n{errors}")
  125. else:
  126. raise ValueError(f"Unknown project file: {project_file}")
  127. return project_file
  128. project_folder = Path.cwd()
  129. if project_path_input:
  130. if not Path(project_path_input).resolve().is_dir():
  131. raise ValueError(f"Invalid project path: {project_path_input}")
  132. project_folder = Path(project_path_input).resolve()
  133. # Search a project file in the project folder using the provided patterns
  134. for pattern in project_file_patterns:
  135. if not (matches := list(project_folder.glob(pattern))):
  136. # No project files found with the specified pattern
  137. continue
  138. if len(matches) > 1:
  139. matched_files = '\n'.join(str(f) for f in matches)
  140. raise ValueError(f"Multiple project files found:\n{matched_files}")
  141. project_file = matches[0]
  142. if pattern == PYPROJECT_TOML_PATTERN:
  143. if parse_pyproject_toml(project_file).errors:
  144. # Invalid file, but a .pyproject file may exist
  145. # We can not raise an error due to ensuring backward compatibility
  146. continue
  147. elif pattern == PYPROJECT_JSON_PATTERN:
  148. pyproject_json_result = parse_pyproject_json(project_file)
  149. if errors := '\n'.join(str(e) for e in pyproject_json_result.errors):
  150. raise ValueError(f"Invalid project file: {project_file}\n{errors}")
  151. # Found a valid project file
  152. return project_file
  153. raise ValueError("No project file found in the current directory")