package_manager.py 10 KB

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