package_manager.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import os
  2. import shutil
  3. from .lockfile import PnpmLockfile
  4. from .utils import build_lockfile_path, build_pre_lockfile_path, build_ws_config_path
  5. from .workspace import PnpmWorkspace
  6. from ..base import BasePackageManager, PackageManagerError
  7. from ..base.constants import NODE_MODULES_WORKSPACE_BUNDLE_FILENAME
  8. from ..base.node_modules_bundler import bundle_node_modules
  9. from ..base.timeit import timeit
  10. from ..base.utils import (
  11. b_rooted,
  12. build_nm_bundle_path,
  13. build_nm_path,
  14. build_nm_store_path,
  15. build_pj_path,
  16. home_dir,
  17. s_rooted,
  18. )
  19. class PnpmPackageManager(BasePackageManager):
  20. _STORE_NM_PATH = os.path.join(".pnpm", "store")
  21. _VSTORE_NM_PATH = os.path.join(".pnpm", "virtual-store")
  22. _STORE_VER = "v3"
  23. @classmethod
  24. def load_lockfile(cls, path):
  25. """
  26. :param path: path to lockfile
  27. :type path: str
  28. :rtype: PnpmLockfile
  29. """
  30. return PnpmLockfile.load(path)
  31. @classmethod
  32. def load_lockfile_from_dir(cls, dir_path):
  33. """
  34. :param dir_path: path to directory with lockfile
  35. :type dir_path: str
  36. :rtype: PnpmLockfile
  37. """
  38. return cls.load_lockfile(build_lockfile_path(dir_path))
  39. @staticmethod
  40. def get_local_pnpm_store():
  41. return os.path.join(home_dir(), ".cache", "pnpm-store")
  42. @timeit
  43. def create_node_modules(self, yatool_prebuilder_path=None, local_cli=False, bundle=True):
  44. """
  45. Creates node_modules directory according to the lockfile.
  46. """
  47. ws = self._prepare_workspace()
  48. self._copy_pnpm_patches()
  49. # Pure `tier 0` logic - isolated stores in the `build_root` (works in `distbuild` and `CI autocheck`)
  50. store_dir = self._nm_path(self._STORE_NM_PATH)
  51. virtual_store_dir = self._nm_path(self._VSTORE_NM_PATH)
  52. # Local mode optimizations (run from the `ya tool nots`)
  53. if local_cli:
  54. # Use single CAS for all the projects built locally
  55. store_dir = self.get_local_pnpm_store()
  56. nm_store_path = build_nm_store_path(self.module_path)
  57. # Use single virtual-store location in ~/.nots/nm_store/$MODDIR/node_modules/.pnpm/virtual-store
  58. virtual_store_dir = os.path.join(build_nm_path(nm_store_path), self._VSTORE_NM_PATH)
  59. self._run_pnpm_install(store_dir, virtual_store_dir)
  60. self._run_apply_addons_if_need(yatool_prebuilder_path, virtual_store_dir)
  61. self._replace_internal_lockfile_with_original(virtual_store_dir)
  62. if not local_cli and bundle:
  63. bundle_node_modules(
  64. build_root=self.build_root,
  65. node_modules_path=self._nm_path(),
  66. peers=ws.get_paths(base_path=self.module_path, ignore_self=True),
  67. bundle_path=os.path.join(self.build_path, NODE_MODULES_WORKSPACE_BUNDLE_FILENAME),
  68. )
  69. @timeit
  70. def _run_pnpm_install(self, store_dir: str, virtual_store_dir: str):
  71. install_cmd = [
  72. "install",
  73. "--frozen-lockfile",
  74. "--ignore-pnpmfile",
  75. "--ignore-scripts",
  76. "--no-verify-store-integrity",
  77. "--offline",
  78. "--config.confirmModulesPurge=false", # hack for https://st.yandex-team.ru/FBP-1295
  79. "--package-import-method",
  80. "hardlink",
  81. # "--registry" will be set later inside self._exec_command()
  82. "--store-dir",
  83. store_dir,
  84. "--strict-peer-dependencies",
  85. "--virtual-store-dir",
  86. virtual_store_dir,
  87. ]
  88. self._exec_command(install_cmd)
  89. def calc_prepare_deps_inouts_and_resources(
  90. self, store_path: str, has_deps: bool
  91. ) -> tuple[list[str], list[str], list[str]]:
  92. ins = [
  93. s_rooted(build_pj_path(self.module_path)),
  94. s_rooted(build_lockfile_path(self.module_path)),
  95. ]
  96. outs = [
  97. b_rooted(build_ws_config_path(self.module_path)),
  98. b_rooted(build_pre_lockfile_path(self.module_path)),
  99. ]
  100. resources = []
  101. if has_deps:
  102. for pkg in self.extract_packages_meta_from_lockfiles([build_lockfile_path(self.sources_path)]):
  103. resources.append(pkg.to_uri())
  104. outs.append(b_rooted(self._tarballs_store_path(pkg, store_path)))
  105. return ins, outs, resources
  106. def calc_node_modules_inouts(self, local_cli: bool, has_deps: bool) -> tuple[list[str], list[str]]:
  107. """
  108. Returns input and output paths for command that creates `node_modules` bundle.
  109. It relies on .PEERDIRSELF=TS_PREPARE_DEPS
  110. Inputs:
  111. - source package.json
  112. Outputs:
  113. - created node_modules bundle
  114. """
  115. ins = [s_rooted(build_pj_path(self.module_path))]
  116. outs = []
  117. if not local_cli and has_deps:
  118. outs.append(b_rooted(build_nm_bundle_path(self.module_path)))
  119. return ins, outs
  120. def extract_packages_meta_from_lockfiles(self, lf_paths):
  121. """
  122. :type lf_paths: iterable of BaseLockfile
  123. :rtype: iterable of LockfilePackageMeta
  124. """
  125. tarballs = set()
  126. errors = []
  127. for lf_path in lf_paths:
  128. try:
  129. for pkg in self.load_lockfile(lf_path).get_packages_meta():
  130. if pkg.tarball_path not in tarballs:
  131. tarballs.add(pkg.tarball_path)
  132. yield pkg
  133. except Exception as e:
  134. errors.append("{}: {}".format(lf_path, e))
  135. if errors:
  136. raise PackageManagerError("Unable to process some lockfiles:\n{}".format("\n".join(errors)))
  137. @timeit
  138. def _prepare_workspace(self):
  139. lf = self.load_lockfile(build_pre_lockfile_path(self.build_path))
  140. lf.update_tarball_resolutions(lambda p: "file:" + os.path.join(self.build_root, p.tarball_url))
  141. lf.write(build_lockfile_path(self.build_path))
  142. return PnpmWorkspace.load(build_ws_config_path(self.build_path))
  143. def build_workspace(self, tarballs_store: str):
  144. """
  145. :rtype: PnpmWorkspace
  146. """
  147. pj = self._build_package_json()
  148. ws = PnpmWorkspace(build_ws_config_path(self.build_path))
  149. ws.set_from_package_json(pj)
  150. dep_paths = ws.get_paths(ignore_self=True)
  151. self._build_merged_workspace_config(ws, dep_paths)
  152. self._build_merged_pre_lockfile(tarballs_store, dep_paths)
  153. return ws
  154. def _build_merged_pre_lockfile(self, tarballs_store, dep_paths):
  155. """
  156. :type dep_paths: list of str
  157. :rtype: PnpmLockfile
  158. """
  159. lf = self.load_lockfile_from_dir(self.sources_path)
  160. # Change to the output path for correct path calcs on merging.
  161. lf.path = build_pre_lockfile_path(self.build_path)
  162. lf.update_tarball_resolutions(lambda p: self._tarballs_store_path(p, tarballs_store))
  163. for dep_path in dep_paths:
  164. pre_lf_path = build_pre_lockfile_path(dep_path)
  165. if os.path.isfile(pre_lf_path):
  166. lf.merge(self.load_lockfile(pre_lf_path))
  167. lf.write()
  168. def _build_merged_workspace_config(self, ws, dep_paths):
  169. """
  170. NOTE: This method mutates `ws`.
  171. :type ws: PnpmWorkspaceConfig
  172. :type dep_paths: list of str
  173. """
  174. for dep_path in dep_paths:
  175. ws_config_path = build_ws_config_path(dep_path)
  176. if os.path.isfile(ws_config_path):
  177. ws.merge(PnpmWorkspace.load(ws_config_path))
  178. ws.write()
  179. @timeit
  180. def _run_apply_addons_if_need(self, yatool_prebuilder_path, virtual_store_dir):
  181. if not yatool_prebuilder_path:
  182. return
  183. self._exec_command(
  184. [
  185. "apply-addons",
  186. "--virtual-store",
  187. virtual_store_dir,
  188. ],
  189. include_defaults=False,
  190. script_path=os.path.join(yatool_prebuilder_path, "build", "bin", "prebuilder.js"),
  191. )
  192. def _replace_internal_lockfile_with_original(self, virtual_store_dir):
  193. original_lf_path = build_lockfile_path(self.sources_path)
  194. vs_lf_path = os.path.join(virtual_store_dir, "lock.yaml")
  195. shutil.copyfile(original_lf_path, vs_lf_path)
  196. @timeit
  197. def _copy_pnpm_patches(self):
  198. pj = self.load_package_json_from_dir(self.sources_path)
  199. patchedDependencies: dict[str, str] = pj.data.get("pnpm", {}).get("patchedDependencies", {})
  200. for p in patchedDependencies.values():
  201. patch_source_path = os.path.join(self.sources_path, p)
  202. patch_build_path = os.path.join(self.build_path, p)
  203. os.makedirs(os.path.dirname(patch_build_path), exist_ok=True)
  204. shutil.copyfile(patch_source_path, patch_build_path)
  205. def _get_default_options(self):
  206. return super(PnpmPackageManager, self)._get_default_options() + [
  207. "--stream",
  208. "--reporter",
  209. "append-only",
  210. "--no-color",
  211. ]
  212. def _get_debug_log_path(self):
  213. return self._nm_path(".pnpm-debug.log")