123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import multiprocessing
  2. import shutil
  3. import os
  4. from pathlib import Path
  5. import copy
  6. from typing import Optional, TypeVar, Iterable
  7. from . import paths, proc, toolchain as tc_mod
  8. from dds_ci.util import Pathish
  9. T = TypeVar('T')
  10. class DDSWrapper:
  11. """
  12. Wraps a 'dds' executable with some convenience APIs that invoke various
  13. 'dds' subcommands.
  14. """
  15. def __init__(self,
  16. path: Path,
  17. *,
  18. repo_dir: Optional[Pathish] = None,
  19. pkg_db_path: Optional[Pathish] = None,
  20. default_cwd: Optional[Pathish] = None) -> None:
  21. self.path = path
  22. self.repo_dir = Path(repo_dir or (paths.PREBUILT_DIR / 'ci-repo'))
  23. self.pkg_db_path = Path(pkg_db_path or (self.repo_dir.parent / 'ci-catalog.db'))
  24. self.default_cwd = default_cwd or Path.cwd()
  25. def clone(self: T) -> T:
  26. return copy.deepcopy(self)
  27. @property
  28. def pkg_db_path_arg(self) -> str:
  29. """The arguments for --catalog"""
  30. return f'--catalog={self.pkg_db_path}'
  31. @property
  32. def cache_dir_arg(self) -> str:
  33. """The arguments for --repo-dir"""
  34. return f'--repo-dir={self.repo_dir}'
  35. @property
  36. def project_dir_flag(self) -> str:
  37. return '--project-dir'
  38. def set_repo_scratch(self, path: Pathish) -> None:
  39. self.repo_dir = Path(path) / 'data'
  40. self.pkg_db_path = Path(path) / 'pkgs.db'
  41. def clean(self, *, build_dir: Optional[Path] = None, repo: bool = True, pkg_db: bool = True) -> None:
  42. """
  43. Clean out prior executable output, including repos, pkg_db, and
  44. the build results at 'build_dir', if given.
  45. """
  46. if build_dir and build_dir.exists():
  47. shutil.rmtree(build_dir)
  48. if repo and self.repo_dir.exists():
  49. shutil.rmtree(self.repo_dir)
  50. if pkg_db and self.pkg_db_path.exists():
  51. self.pkg_db_path.unlink()
  52. def run(self, args: proc.CommandLine, *, cwd: Optional[Pathish] = None, timeout: Optional[int] = None) -> None:
  53. """Execute the 'dds' executable with the given arguments"""
  54. env = os.environ.copy()
  55. env['DDS_NO_ADD_INITIAL_REPO'] = '1'
  56. proc.check_run([self.path, args], cwd=cwd or self.default_cwd, env=env, timeout=timeout)
  57. def catalog_json_import(self, path: Path) -> None:
  58. """Run 'catalog import' to import the given JSON. Only applicable to older 'dds'"""
  59. self.run(['catalog', 'import', self.pkg_db_path_arg, f'--json={path}'])
  60. def catalog_get(self, what: str) -> None:
  61. self.run(['catalog', 'get', self.pkg_db_path_arg, what])
  62. def pkg_get(self, what: str) -> None:
  63. self.run(['pkg', 'get', self.pkg_db_path_arg, what])
  64. def repo_add(self, url: str) -> None:
  65. self.run(['pkg', 'repo', 'add', self.pkg_db_path_arg, url])
  66. def repo_remove(self, name: str) -> None:
  67. self.run(['pkg', 'repo', 'remove', self.pkg_db_path_arg, name])
  68. def repo_import(self, sdist: Path) -> None:
  69. self.run(['repo', self.cache_dir_arg, 'import', sdist])
  70. def pkg_import(self, filepath: Pathish) -> None:
  71. self.run(['pkg', 'import', filepath, self.cache_dir_arg])
  72. def build(self,
  73. *,
  74. root: Path,
  75. toolchain: Optional[Path] = None,
  76. build_root: Optional[Path] = None,
  77. jobs: Optional[int] = None,
  78. tweaks_dir: Optional[Path] = None,
  79. more_args: Optional[proc.CommandLine] = None,
  80. timeout: Optional[int] = None) -> None:
  81. """
  82. Run 'dds build' with the given arguments.
  83. :param toolchain: The toolchain to use for the build.
  84. :param root: The root project directory.
  85. :param build_root: The root directory where the output will be written.
  86. :param jobs: The number of jobs to use. Default is CPU-count + 2
  87. """
  88. toolchain = toolchain or tc_mod.get_default_audit_toolchain()
  89. jobs = jobs or multiprocessing.cpu_count() + 2
  90. self.run(
  91. [
  92. 'build',
  93. f'--toolchain={toolchain}',
  94. self.cache_dir_arg,
  95. self.pkg_db_path_arg,
  96. f'--jobs={jobs}',
  97. f'{self.project_dir_flag}={root}',
  98. f'--out={build_root}',
  99. f'--tweaks-dir={tweaks_dir}' if tweaks_dir else (),
  100. more_args or (),
  101. ],
  102. timeout=timeout,
  103. )
  104. def compile_file(self,
  105. paths: Iterable[Pathish],
  106. *,
  107. toolchain: Optional[Pathish] = None,
  108. project_dir: Pathish,
  109. out: Optional[Pathish] = None) -> None:
  110. """
  111. Run 'dds compile-file' for the given paths.
  112. """
  113. toolchain = toolchain or tc_mod.get_default_audit_toolchain()
  114. self.run([
  115. 'compile-file',
  116. self.pkg_db_path_arg,
  117. self.cache_dir_arg,
  118. paths,
  119. f'--toolchain={toolchain}',
  120. f'{self.project_dir_flag}={project_dir}',
  121. f'--out={out}',
  122. ])
  123. def build_deps(self, args: proc.CommandLine, *, toolchain: Optional[Path] = None) -> None:
  124. toolchain = toolchain or tc_mod.get_default_audit_toolchain()
  125. self.run([
  126. 'build-deps',
  127. f'--toolchain={toolchain}',
  128. self.pkg_db_path_arg,
  129. self.cache_dir_arg,
  130. args,
  131. ])
  132. class NewDDSWrapper(DDSWrapper):
  133. """
  134. Wraps the new 'dds' executable with some convenience APIs
  135. """
  136. @property
  137. def cache_dir_arg(self) -> str:
  138. return f'--pkg-cache-dir={self.repo_dir}'
  139. @property
  140. def pkg_db_path_arg(self) -> str:
  141. return f'--pkg-db-path={self.pkg_db_path}'
  142. @property
  143. def project_dir_flag(self) -> str:
  144. return '--project'