package_manager.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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. # TODO: FBP-1254
  76. # def calc_prepare_deps_inouts(self, store_path: str, has_deps: bool) -> (list[str], list[str]):
  77. def calc_prepare_deps_inouts(self, store_path, has_deps):
  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. if has_deps:
  87. for dep_path in self.get_local_peers_from_package_json():
  88. ins.append(b_rooted(build_ws_config_path(dep_path)))
  89. ins.append(b_rooted(build_pre_lockfile_path(dep_path)))
  90. for pkg in self.extract_packages_meta_from_lockfiles([build_lockfile_path(self.sources_path)]):
  91. ins.append(b_rooted(self._contrib_tarball_path(pkg)))
  92. outs.append(b_rooted(self._tarballs_store_path(pkg, store_path)))
  93. return ins, outs
  94. # TODO: FBP-1254
  95. # def calc_node_modules_inouts(self, local_cli=False) -> (list[str], list[str]):
  96. def calc_node_modules_inouts(self, local_cli=False):
  97. """
  98. Returns input and output paths for command that creates `node_modules` bundle.
  99. It relies on .PEERDIRSELF=TS_PREPARE_DEPS
  100. Inputs:
  101. - source package.json
  102. - merged lockfiles and workspace configs of TS_PREPARE_DEPS
  103. Outputs:
  104. - created node_modules bundle
  105. """
  106. ins = [s_rooted(build_pj_path(self.module_path))]
  107. outs = []
  108. pj = self.load_package_json_from_dir(self.sources_path)
  109. if pj.has_dependencies():
  110. ins.append(b_rooted(build_pre_lockfile_path(self.module_path)))
  111. ins.append(b_rooted(build_ws_config_path(self.module_path)))
  112. if not local_cli:
  113. outs.append(b_rooted(build_nm_bundle_path(self.module_path)))
  114. for dep_path in self.get_local_peers_from_package_json():
  115. ins.append(b_rooted(build_pj_path(dep_path)))
  116. return ins, outs
  117. def extract_packages_meta_from_lockfiles(self, lf_paths):
  118. """
  119. :type lf_paths: iterable of BaseLockfile
  120. :rtype: iterable of LockfilePackageMeta
  121. """
  122. tarballs = set()
  123. errors = []
  124. for lf_path in lf_paths:
  125. try:
  126. for pkg in self.load_lockfile(lf_path).get_packages_meta():
  127. if pkg.tarball_path not in tarballs:
  128. tarballs.add(pkg.tarball_path)
  129. yield pkg
  130. except Exception as e:
  131. errors.append("{}: {}".format(lf_path, e))
  132. if errors:
  133. raise PackageManagerError("Unable to process some lockfiles:\n{}".format("\n".join(errors)))
  134. def _prepare_workspace(self):
  135. lf = self.load_lockfile(build_pre_lockfile_path(self.build_path))
  136. lf.update_tarball_resolutions(lambda p: "file:" + os.path.join(self.build_root, p.tarball_url))
  137. lf.write(build_lockfile_path(self.build_path))
  138. return PnpmWorkspace.load(build_ws_config_path(self.build_path))
  139. def build_workspace(self, tarballs_store):
  140. """
  141. :rtype: PnpmWorkspace
  142. """
  143. pj = self._build_package_json()
  144. ws = PnpmWorkspace(build_ws_config_path(self.build_path))
  145. ws.set_from_package_json(pj)
  146. dep_paths = ws.get_paths(ignore_self=True)
  147. self._build_merged_workspace_config(ws, dep_paths)
  148. self._build_merged_pre_lockfile(tarballs_store, dep_paths)
  149. return ws
  150. def _build_package_json(self):
  151. """
  152. :rtype: PackageJson
  153. """
  154. pj = self.load_package_json_from_dir(self.sources_path)
  155. if not os.path.exists(self.build_path):
  156. os.makedirs(self.build_path, exist_ok=True)
  157. pj.path = build_pj_path(self.build_path)
  158. pj.write()
  159. return pj
  160. def _build_merged_pre_lockfile(self, tarballs_store, dep_paths):
  161. """
  162. :type dep_paths: list of str
  163. :rtype: PnpmLockfile
  164. """
  165. lf = self.load_lockfile_from_dir(self.sources_path)
  166. # Change to the output path for correct path calcs on merging.
  167. lf.path = build_pre_lockfile_path(self.build_path)
  168. lf.update_tarball_resolutions(lambda p: self._tarballs_store_path(p, tarballs_store))
  169. for dep_path in dep_paths:
  170. pre_lf_path = build_pre_lockfile_path(dep_path)
  171. if os.path.isfile(pre_lf_path):
  172. lf.merge(self.load_lockfile(pre_lf_path))
  173. lf.write()
  174. def _build_merged_workspace_config(self, ws, dep_paths):
  175. """
  176. NOTE: This method mutates `ws`.
  177. :type ws: PnpmWorkspaceConfig
  178. :type dep_paths: list of str
  179. """
  180. for dep_path in dep_paths:
  181. ws_config_path = build_ws_config_path(dep_path)
  182. if os.path.isfile(ws_config_path):
  183. ws.merge(PnpmWorkspace.load(ws_config_path))
  184. ws.write()
  185. def _run_apply_addons_if_need(self, yatool_prebuilder_path, virtual_store_dir):
  186. if not yatool_prebuilder_path:
  187. return
  188. self._exec_command(
  189. [
  190. "apply-addons",
  191. "--virtual-store",
  192. virtual_store_dir,
  193. ],
  194. include_defaults=False,
  195. script_path=os.path.join(yatool_prebuilder_path, "build", "bin", "prebuilder.js"),
  196. )
  197. def _replace_internal_lockfile_with_original(self, virtual_store_dir):
  198. original_lf_path = build_lockfile_path(self.sources_path)
  199. vs_lf_path = os.path.join(virtual_store_dir, "lock.yaml")
  200. shutil.copyfile(original_lf_path, vs_lf_path)
  201. def _copy_pnpm_patches(self):
  202. pj = self.load_package_json_from_dir(self.sources_path)
  203. patchedDependencies: dict[str, str] = pj.data.get("pnpm", {}).get("patchedDependencies", {})
  204. for p in patchedDependencies.values():
  205. patch_source_path = os.path.join(self.sources_path, p)
  206. patch_build_path = os.path.join(self.build_path, p)
  207. os.makedirs(os.path.dirname(patch_build_path), exist_ok=True)
  208. shutil.copyfile(patch_source_path, patch_build_path)
  209. def _get_default_options(self):
  210. return super(PnpmPackageManager, self)._get_default_options() + [
  211. "--stream",
  212. "--reporter",
  213. "append-only",
  214. "--no-color",
  215. ]
  216. def _get_debug_log_path(self):
  217. return self._nm_path(".pnpm-debug.log")