Skip to content

Commit 58f5aeb

Browse files
author
vturov
committed
Повторить локальные оптимизации для тестов (jest, eslint, typecheck)
- Выставляем флаг локального запуска при работе через nots - При запуске install с указанием virtual-store директории исключаем гонку и хешируем по pre-lock файлу commit_hash:fcc3afdd0d702a9846ee627d6e709f3d3cf97482
1 parent 23ea28c commit 58f5aeb

File tree

2 files changed

+188
-22
lines changed

2 files changed

+188
-22
lines changed

build/plugins/lib/nots/package_manager/pnpm/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,8 @@
55
# This file has a structure same to pnpm-lock.yaml, but all tarballs
66
# a set relative to the build root.
77
PNPM_PRE_LOCKFILE_FILENAME = "pre.pnpm-lock.yaml"
8+
9+
# File is to store the last install status hash to avoid installing the same thing
10+
LOCAL_PNPM_INSTALL_HASH_FILENAME = ".__install_hash__"
11+
# File is to syncronize processes using the local nm_store for the project simultaneously
12+
LOCAL_PNPM_INSTALL_MUTEX_FILENAME = ".__install_mutex__"

build/plugins/lib/nots/package_manager/pnpm/package_manager.py

Lines changed: 183 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22
import json
33
import os
44
import shutil
5+
import sys
56

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+
)
712
from .lockfile import PnpmLockfile
813
from .utils import build_lockfile_path, build_pre_lockfile_path, build_ws_config_path
914
from .workspace import PnpmWorkspace
@@ -27,6 +32,113 @@
2732
)
2833

2934

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+
30142
class PnpmPackageManager(BasePackageManager):
31143
_STORE_NM_PATH = os.path.join(".pnpm", "store")
32144
_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
101213
os.makedirs(os.path.dirname(dst), exist_ok=True)
102214
shutil.copy(src, dst)
103215

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)
105217

106218
# Write node_modules.json to prevent extra `pnpm install` running 1
107219
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
132244

133245
self._create_local_node_modules(nm_store_path, store_dir, virtual_store_dir)
134246

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)
136248

137249
self._run_apply_addons_if_need(yatool_prebuilder_path, virtual_store_dir)
138250
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
145257
bundle_path=os.path.join(self.build_path, NODE_MODULES_WORKSPACE_BUNDLE_FILENAME),
146258
)
147259

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+
148277
@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+
]
167299

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+
"""
169330

170331
@timeit
171332
def calc_prepare_deps_inouts_and_resources(

0 commit comments

Comments
 (0)