2
2
import json
3
3
import os
4
4
import shutil
5
+ import sys
5
6
6
- from .constants import PNPM_PRE_LOCKFILE_FILENAME
7
+ from .constants import (
8
+ PNPM_PRE_LOCKFILE_FILENAME ,
9
+ LOCAL_PNPM_INSTALL_HASH_FILENAME ,
10
+ LOCAL_PNPM_INSTALL_MUTEX_FILENAME ,
11
+ )
7
12
from .lockfile import PnpmLockfile
8
13
from .utils import build_lockfile_path , build_pre_lockfile_path , build_ws_config_path
9
14
from .workspace import PnpmWorkspace
27
32
)
28
33
29
34
35
+ """
36
+ Creates a decorator that synchronizes access to a function using a mutex file.
37
+
38
+ The decorator uses file locking (fcntl.LOCK_EX) to ensure only one process can execute the decorated function at a time.
39
+ The lock is released (fcntl.LOCK_UN) when the function completes.
40
+
41
+ Args:
42
+ mutex_filename (str): Path to the file used as a mutex lock.
43
+
44
+ Returns:
45
+ function: A decorator function that applies the synchronization logic.
46
+ """
47
+
48
+
49
+ def sync_mutex_file (mutex_filename ):
50
+ def decorator (function ):
51
+ def wrapper (* args , ** kwargs ):
52
+ import fcntl
53
+
54
+ with open (mutex_filename , "w+" ) as mutex :
55
+ fcntl .lockf (mutex , fcntl .LOCK_EX )
56
+ result = function (* args , ** kwargs )
57
+ fcntl .lockf (mutex , fcntl .LOCK_UN )
58
+
59
+ return result
60
+
61
+ return wrapper
62
+
63
+ return decorator
64
+
65
+
66
+ """
67
+ Calculates the MD5 hash of multiple files.
68
+
69
+ Reads files in chunks of 64KB and updates the MD5 hash incrementally. Files are processed in sorted order to ensure consistent results.
70
+
71
+ Args:
72
+ files (list): List of file paths to be hashed.
73
+
74
+ Returns:
75
+ str: Hexadecimal MD5 hash digest of the concatenated file contents.
76
+ """
77
+
78
+
79
+ def hash_files (files ):
80
+ BUF_SIZE = 65536 # read in 64kb chunks
81
+ md5 = hashlib .md5 ()
82
+ for filename in sorted (files ):
83
+ with open (filename , 'rb' ) as f :
84
+ while True :
85
+ data = f .read (BUF_SIZE )
86
+ if not data :
87
+ break
88
+ md5 .update (data )
89
+
90
+ return md5 .hexdigest ()
91
+
92
+
93
+ """
94
+ Creates a decorator that runs the decorated function only if specified files have changed.
95
+
96
+ The decorator checks the hash of provided files against a saved hash from previous runs.
97
+ If hashes differ (files changed) or no saved hash exists, runs the decorated function
98
+ and updates the saved hash. If hashes are the same, skips the function execution.
99
+
100
+ Args:
101
+ files_to_hash: List of files to track for changes.
102
+ hash_storage_filename: Path to file where hash state is stored.
103
+
104
+ Returns:
105
+ A decorator function that implements the described behavior.
106
+ """
107
+
108
+
109
+ def hashed_by_files (files_to_hash , paths_to_exist , hash_storage_filename ):
110
+ def decorator (function ):
111
+ def wrapper (* args , ** kwargs ):
112
+ all_paths_exist = True
113
+ for p in paths_to_exist :
114
+ if not os .path .exists (p ):
115
+ sys .stderr .write (f"Path { p } does not exist\n " )
116
+ all_paths_exist = False
117
+ break
118
+
119
+ current_state_hash = hash_files (files_to_hash )
120
+ saved_hash = None
121
+ if all_paths_exist and os .path .exists (hash_storage_filename ):
122
+ with open (hash_storage_filename , "r" ) as f :
123
+ saved_hash = f .read ()
124
+
125
+ if saved_hash == current_state_hash :
126
+ return None
127
+ else :
128
+ sys .stderr .write (
129
+ f"Saved hash { saved_hash } != current hash { current_state_hash } for { hash_storage_filename } \n "
130
+ )
131
+ result = function (* args , ** kwargs )
132
+ with open (hash_storage_filename , "w+" ) as f :
133
+ f .write (current_state_hash )
134
+
135
+ return result
136
+
137
+ return wrapper
138
+
139
+ return decorator
140
+
141
+
30
142
class PnpmPackageManager (BasePackageManager ):
31
143
_STORE_NM_PATH = os .path .join (".pnpm" , "store" )
32
144
_VSTORE_NM_PATH = os .path .join (".pnpm" , "virtual-store" )
@@ -101,7 +213,7 @@ def _create_local_node_modules(self, nm_store_path: str, store_dir: str, virtual
101
213
os .makedirs (os .path .dirname (dst ), exist_ok = True )
102
214
shutil .copy (src , dst )
103
215
104
- self ._run_pnpm_install (store_dir , virtual_store_dir , nm_store_path )
216
+ self ._run_pnpm_install (store_dir , virtual_store_dir , nm_store_path , True )
105
217
106
218
# Write node_modules.json to prevent extra `pnpm install` running 1
107
219
with open (os .path .join (nm_store_path , "node_modules.json" ), "w" ) as f :
@@ -132,7 +244,7 @@ def create_node_modules(self, yatool_prebuilder_path=None, local_cli=False, nm_b
132
244
133
245
self ._create_local_node_modules (nm_store_path , store_dir , virtual_store_dir )
134
246
135
- self ._run_pnpm_install (store_dir , virtual_store_dir , self .build_path )
247
+ self ._run_pnpm_install (store_dir , virtual_store_dir , self .build_path , local_cli )
136
248
137
249
self ._run_apply_addons_if_need (yatool_prebuilder_path , virtual_store_dir )
138
250
self ._replace_internal_lockfile_with_original (virtual_store_dir )
@@ -145,27 +257,76 @@ def create_node_modules(self, yatool_prebuilder_path=None, local_cli=False, nm_b
145
257
bundle_path = os .path .join (self .build_path , NODE_MODULES_WORKSPACE_BUNDLE_FILENAME ),
146
258
)
147
259
260
+ """
261
+ Runs pnpm install command with specified parameters in an exclusive and hashed manner.
262
+
263
+ This method executes the pnpm install command with various flags and options, ensuring it's run exclusively
264
+ using a mutex file and only if the specified files have changed (using a hash check). The command is executed
265
+ in the given working directory (cwd) with the provided store and virtual store directories.
266
+
267
+ Args:
268
+ store_dir (str): Path to the store directory where packages will be stored.
269
+ virtual_store_dir (str): Path to the virtual store directory.
270
+ cwd (str): Working directory where the command will be executed.
271
+
272
+ Note:
273
+ Uses file locking via fcntl to ensure exclusive execution.
274
+ The command execution is hashed based on the pnpm-lock.yaml file.
275
+ """
276
+
148
277
@timeit
149
- def _run_pnpm_install (self , store_dir : str , virtual_store_dir : str , cwd : str ):
150
- install_cmd = [
151
- "install" ,
152
- "--frozen-lockfile" ,
153
- "--ignore-pnpmfile" ,
154
- "--ignore-scripts" ,
155
- "--no-verify-store-integrity" ,
156
- "--offline" ,
157
- "--config.confirmModulesPurge=false" , # hack for https://st.yandex-team.ru/FBP-1295
158
- "--package-import-method" ,
159
- "hardlink" ,
160
- # "--registry" will be set later inside self._exec_command()
161
- "--store-dir" ,
162
- store_dir ,
163
- "--strict-peer-dependencies" ,
164
- "--virtual-store-dir" ,
165
- virtual_store_dir ,
166
- ]
278
+ def _run_pnpm_install (self , store_dir : str , virtual_store_dir : str , cwd : str , local_cli : bool ):
279
+ # Use fcntl to lock a temp file
280
+
281
+ def execute_install_cmd ():
282
+ install_cmd = [
283
+ "install" ,
284
+ "--frozen-lockfile" ,
285
+ "--ignore-pnpmfile" ,
286
+ "--ignore-scripts" ,
287
+ "--no-verify-store-integrity" ,
288
+ "--offline" ,
289
+ "--config.confirmModulesPurge=false" , # hack for https://st.yandex-team.ru/FBP-1295
290
+ "--package-import-method" ,
291
+ "hardlink" ,
292
+ # "--registry" will be set later inside self._exec_command()
293
+ "--store-dir" ,
294
+ store_dir ,
295
+ "--strict-peer-dependencies" ,
296
+ "--virtual-store-dir" ,
297
+ virtual_store_dir ,
298
+ ]
167
299
168
- self ._exec_command (install_cmd , cwd = cwd )
300
+ self ._exec_command (install_cmd , cwd = cwd )
301
+
302
+ if local_cli :
303
+ files_to_hash = [build_pre_lockfile_path (self .build_path )]
304
+ paths_to_exist = [build_nm_path (cwd )]
305
+ hash_file = os .path .join (build_nm_store_path (self .module_path ), LOCAL_PNPM_INSTALL_HASH_FILENAME )
306
+ mutex_file = os .path .join (build_nm_store_path (self .module_path ), LOCAL_PNPM_INSTALL_MUTEX_FILENAME )
307
+ execute_cmd_hashed = hashed_by_files (files_to_hash , paths_to_exist , hash_file )(execute_install_cmd )
308
+ execute_hashed_cmd_exclusively = sync_mutex_file (mutex_file )(execute_cmd_hashed )
309
+ execute_hashed_cmd_exclusively ()
310
+
311
+ else :
312
+ execute_install_cmd ()
313
+
314
+ """
315
+ Calculate inputs, outputs and resources for dependency preparation phase.
316
+
317
+ Args:
318
+ store_path: Path to the store where tarballs will be stored.
319
+ has_deps: Boolean flag indicating whether the module has dependencies.
320
+
321
+ Returns:
322
+ tuple[list[str], list[str], list[str]]: A tuple containing three lists:
323
+ - ins: List of input file paths
324
+ - outs: List of output file paths
325
+ - resources: List of package URIs (when has_deps is True)
326
+
327
+ Note:
328
+ Uses @timeit decorator to measure execution time of this method.
329
+ """
169
330
170
331
@timeit
171
332
def calc_prepare_deps_inouts_and_resources (
0 commit comments