multi-lang-build 0.2.8__py3-none-any.whl → 0.2.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- multi_lang_build/__init__.py +4 -4
- multi_lang_build/cli.py +3 -2
- multi_lang_build/compiler/__init__.py +1 -1
- multi_lang_build/compiler/go_compiler.py +388 -0
- multi_lang_build/compiler/go_support/__init__.py +19 -0
- multi_lang_build/compiler/go_support/binary.py +117 -0
- multi_lang_build/compiler/go_support/builder.py +228 -0
- multi_lang_build/compiler/go_support/cleaner.py +40 -0
- multi_lang_build/compiler/go_support/env.py +41 -0
- multi_lang_build/compiler/go_support/mirror.py +111 -0
- multi_lang_build/compiler/go_support/module.py +77 -0
- multi_lang_build/compiler/go_support/tester.py +199 -0
- multi_lang_build/compiler/pnpm.py +61 -209
- multi_lang_build/compiler/pnpm_support/__init__.py +6 -0
- multi_lang_build/compiler/pnpm_support/executor.py +148 -0
- multi_lang_build/compiler/pnpm_support/project.py +53 -0
- multi_lang_build/compiler/python.py +65 -222
- multi_lang_build/compiler/python_support/__init__.py +13 -0
- multi_lang_build/compiler/python_support/cleaner.py +56 -0
- multi_lang_build/compiler/python_support/cli.py +46 -0
- multi_lang_build/compiler/python_support/detector.py +64 -0
- multi_lang_build/compiler/python_support/installer.py +162 -0
- multi_lang_build/compiler/python_support/venv.py +63 -0
- multi_lang_build/ide_register.py +97 -0
- multi_lang_build/mirror/config.py +19 -0
- multi_lang_build/register/support/__init__.py +13 -0
- multi_lang_build/register/support/claude.py +94 -0
- multi_lang_build/register/support/codebuddy.py +100 -0
- multi_lang_build/register/support/opencode.py +62 -0
- multi_lang_build/register/support/trae.py +72 -0
- {multi_lang_build-0.2.8.dist-info → multi_lang_build-0.2.10.dist-info}/METADATA +1 -1
- multi_lang_build-0.2.10.dist-info/RECORD +40 -0
- multi_lang_build/compiler/go.py +0 -564
- multi_lang_build/register.py +0 -412
- multi_lang_build-0.2.8.dist-info/RECORD +0 -18
- {multi_lang_build-0.2.8.dist-info → multi_lang_build-0.2.10.dist-info}/WHEEL +0 -0
- {multi_lang_build-0.2.8.dist-info → multi_lang_build-0.2.10.dist-info}/entry_points.txt +0 -0
- {multi_lang_build-0.2.8.dist-info → multi_lang_build-0.2.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,43 +3,42 @@
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Final
|
|
5
5
|
import shutil
|
|
6
|
-
import json
|
|
7
6
|
import os
|
|
8
|
-
import time
|
|
9
7
|
|
|
10
8
|
from multi_lang_build.compiler.base import CompilerBase, BuildResult, CompilerInfo
|
|
11
|
-
from multi_lang_build.mirror.config import
|
|
9
|
+
from multi_lang_build.mirror.config import apply_mirror_environment
|
|
10
|
+
from multi_lang_build.compiler.pnpm_support import PnpmProject, PnpmExecutor
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
class PnpmCompiler(CompilerBase):
|
|
15
14
|
"""Compiler for pnpm-based frontend projects."""
|
|
16
|
-
|
|
15
|
+
|
|
17
16
|
NAME: Final[str] = "pnpm"
|
|
18
17
|
DEFAULT_MIRROR: Final[str] = "https://registry.npmmirror.com"
|
|
19
|
-
|
|
18
|
+
|
|
20
19
|
def __init__(self, pnpm_path: str | None = None) -> None:
|
|
21
20
|
"""Initialize the PNPM compiler.
|
|
22
|
-
|
|
21
|
+
|
|
23
22
|
Args:
|
|
24
23
|
pnpm_path: Optional path to pnpm executable. If None, uses system PATH.
|
|
25
24
|
"""
|
|
26
25
|
self._pnpm_path = pnpm_path
|
|
27
26
|
self._version_cache: str | None = None
|
|
28
|
-
|
|
27
|
+
|
|
29
28
|
@property
|
|
30
29
|
def name(self) -> str:
|
|
31
30
|
"""Get the compiler name."""
|
|
32
31
|
return self.NAME
|
|
33
|
-
|
|
32
|
+
|
|
34
33
|
@property
|
|
35
34
|
def version(self) -> str:
|
|
36
35
|
"""Get the pnpm version."""
|
|
37
36
|
if self._version_cache:
|
|
38
37
|
return self._version_cache
|
|
39
|
-
|
|
38
|
+
|
|
40
39
|
pnpm_executable = self._get_executable_path()
|
|
41
40
|
import subprocess
|
|
42
|
-
|
|
41
|
+
|
|
43
42
|
try:
|
|
44
43
|
result = subprocess.run(
|
|
45
44
|
[pnpm_executable, "--version"],
|
|
@@ -50,14 +49,14 @@ class PnpmCompiler(CompilerBase):
|
|
|
50
49
|
self._version_cache = result.stdout.strip()
|
|
51
50
|
except Exception:
|
|
52
51
|
self._version_cache = "unknown"
|
|
53
|
-
|
|
52
|
+
|
|
54
53
|
return self._version_cache
|
|
55
|
-
|
|
54
|
+
|
|
56
55
|
@property
|
|
57
56
|
def supported_mirrors(self) -> list[str]:
|
|
58
57
|
"""Get list of supported mirror configurations."""
|
|
59
58
|
return ["npm", "pnpm", "yarn"]
|
|
60
|
-
|
|
59
|
+
|
|
61
60
|
def get_info(self) -> CompilerInfo:
|
|
62
61
|
"""Get compiler information."""
|
|
63
62
|
return {
|
|
@@ -67,158 +66,11 @@ class PnpmCompiler(CompilerBase):
|
|
|
67
66
|
"default_mirror": self.DEFAULT_MIRROR,
|
|
68
67
|
"executable": self._get_executable_path(),
|
|
69
68
|
}
|
|
70
|
-
|
|
71
|
-
def find_project_root(self, start_path: Path) -> Path | None:
|
|
72
|
-
"""Find the frontend project root directory containing package.json.
|
|
73
|
-
|
|
74
|
-
Searches from start_path up to its ancestors and also checks subdirectories.
|
|
75
|
-
Returns the first directory containing package.json, or None if not found.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
start_path: Starting path to search from
|
|
79
|
-
|
|
80
|
-
Returns:
|
|
81
|
-
Path to project root or None if not found
|
|
82
|
-
"""
|
|
83
|
-
start_path = start_path.resolve()
|
|
84
|
-
|
|
85
|
-
# Check if start_path itself is the root
|
|
86
|
-
if (start_path / "package.json").exists():
|
|
87
|
-
return start_path
|
|
88
|
-
|
|
89
|
-
# Search upwards in the directory tree
|
|
90
|
-
current = start_path.parent
|
|
91
|
-
original_cwd = start_path
|
|
92
|
-
while current != current.parent:
|
|
93
|
-
if (current / "package.json").exists():
|
|
94
|
-
return current
|
|
95
|
-
current = current.parent
|
|
96
|
-
|
|
97
|
-
# Search subdirectories
|
|
98
|
-
for subdir in start_path.iterdir():
|
|
99
|
-
if subdir.is_dir() and not subdir.name.startswith("."):
|
|
100
|
-
subdir_root = self.find_project_root(subdir)
|
|
101
|
-
if subdir_root:
|
|
102
|
-
return subdir_root
|
|
103
|
-
|
|
104
|
-
return None
|
|
105
|
-
|
|
106
|
-
def _execute_in_directory(
|
|
107
|
-
self,
|
|
108
|
-
command: list[str],
|
|
109
|
-
working_dir: Path,
|
|
110
|
-
environment: dict[str, str],
|
|
111
|
-
stream_output: bool = True,
|
|
112
|
-
) -> BuildResult:
|
|
113
|
-
"""Execute a command in the specified directory, returning to original cwd afterwards.
|
|
114
|
-
|
|
115
|
-
Args:
|
|
116
|
-
command: Command to execute
|
|
117
|
-
working_dir: Working directory for the command
|
|
118
|
-
environment: Environment variables
|
|
119
|
-
stream_output: Whether to stream output in real-time (default: True)
|
|
120
69
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
import subprocess
|
|
125
|
-
import sys
|
|
126
|
-
|
|
127
|
-
original_cwd = Path.cwd()
|
|
128
|
-
full_command = command.copy()
|
|
129
|
-
|
|
130
|
-
start_time: float = 0.0
|
|
70
|
+
def find_project_root(self, start_path: Path) -> Path | None:
|
|
71
|
+
"""Find the frontend project root directory containing package.json."""
|
|
72
|
+
return PnpmProject.find_root(start_path)
|
|
131
73
|
|
|
132
|
-
try:
|
|
133
|
-
os.chdir(working_dir)
|
|
134
|
-
|
|
135
|
-
start_time = time.perf_counter()
|
|
136
|
-
|
|
137
|
-
if stream_output:
|
|
138
|
-
stdout_buffer = []
|
|
139
|
-
stderr_buffer = []
|
|
140
|
-
|
|
141
|
-
process = subprocess.Popen(
|
|
142
|
-
full_command,
|
|
143
|
-
stdout=subprocess.PIPE,
|
|
144
|
-
stderr=subprocess.PIPE,
|
|
145
|
-
text=True,
|
|
146
|
-
env={**os.environ, **environment},
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
stdout_buffer = []
|
|
150
|
-
stderr_buffer = []
|
|
151
|
-
|
|
152
|
-
# Read stdout in real-time
|
|
153
|
-
if process.stdout:
|
|
154
|
-
for line in process.stdout:
|
|
155
|
-
line = line.rstrip('\n\r')
|
|
156
|
-
stdout_buffer.append(line)
|
|
157
|
-
print(line)
|
|
158
|
-
sys.stdout.flush()
|
|
159
|
-
|
|
160
|
-
# Read stderr in real-time
|
|
161
|
-
if process.stderr:
|
|
162
|
-
for line in process.stderr:
|
|
163
|
-
line = line.rstrip('\n\r')
|
|
164
|
-
stderr_buffer.append(line)
|
|
165
|
-
print(line, file=sys.stderr)
|
|
166
|
-
sys.stderr.flush()
|
|
167
|
-
|
|
168
|
-
return_code = process.wait()
|
|
169
|
-
duration = time.perf_counter() - start_time
|
|
170
|
-
|
|
171
|
-
return BuildResult(
|
|
172
|
-
success=return_code == 0,
|
|
173
|
-
return_code=return_code,
|
|
174
|
-
stdout='\n'.join(stdout_buffer),
|
|
175
|
-
stderr='\n'.join(stderr_buffer),
|
|
176
|
-
output_path=working_dir,
|
|
177
|
-
duration_seconds=duration,
|
|
178
|
-
)
|
|
179
|
-
else:
|
|
180
|
-
result = subprocess.run(
|
|
181
|
-
full_command,
|
|
182
|
-
capture_output=True,
|
|
183
|
-
text=True,
|
|
184
|
-
timeout=3600,
|
|
185
|
-
env={**os.environ, **environment},
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
duration = time.perf_counter() - start_time
|
|
189
|
-
|
|
190
|
-
return BuildResult(
|
|
191
|
-
success=result.returncode == 0,
|
|
192
|
-
return_code=result.returncode,
|
|
193
|
-
stdout=result.stdout,
|
|
194
|
-
stderr=result.stderr,
|
|
195
|
-
output_path=working_dir,
|
|
196
|
-
duration_seconds=duration,
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
except subprocess.TimeoutExpired:
|
|
200
|
-
duration = time.perf_counter() - start_time
|
|
201
|
-
return BuildResult(
|
|
202
|
-
success=False,
|
|
203
|
-
return_code=-1,
|
|
204
|
-
stdout="",
|
|
205
|
-
stderr="Build timed out after 1 hour",
|
|
206
|
-
output_path=None,
|
|
207
|
-
duration_seconds=duration,
|
|
208
|
-
)
|
|
209
|
-
except Exception as e:
|
|
210
|
-
duration = time.perf_counter() - start_time
|
|
211
|
-
return BuildResult(
|
|
212
|
-
success=False,
|
|
213
|
-
return_code=-2,
|
|
214
|
-
stdout="",
|
|
215
|
-
stderr=f"Build error: {str(e)}",
|
|
216
|
-
output_path=None,
|
|
217
|
-
duration_seconds=duration,
|
|
218
|
-
)
|
|
219
|
-
finally:
|
|
220
|
-
os.chdir(original_cwd)
|
|
221
|
-
|
|
222
74
|
def build(
|
|
223
75
|
self,
|
|
224
76
|
source_dir: Path,
|
|
@@ -243,10 +95,10 @@ class PnpmCompiler(CompilerBase):
|
|
|
243
95
|
BuildResult containing success status and output information.
|
|
244
96
|
"""
|
|
245
97
|
pnpm_executable = self._get_executable_path()
|
|
246
|
-
|
|
98
|
+
|
|
247
99
|
# Auto-detect project root
|
|
248
100
|
project_root = self.find_project_root(source_dir)
|
|
249
|
-
|
|
101
|
+
|
|
250
102
|
if project_root is None:
|
|
251
103
|
return BuildResult(
|
|
252
104
|
success=False,
|
|
@@ -256,19 +108,19 @@ class PnpmCompiler(CompilerBase):
|
|
|
256
108
|
output_path=None,
|
|
257
109
|
duration_seconds=0.0,
|
|
258
110
|
)
|
|
259
|
-
|
|
111
|
+
|
|
260
112
|
# Validate directories
|
|
261
113
|
output_dir = self._validate_directory(output_dir, create_if_not_exists=True)
|
|
262
|
-
|
|
114
|
+
|
|
263
115
|
# Prepare environment
|
|
264
116
|
env = environment.copy() if environment else {}
|
|
265
|
-
|
|
117
|
+
|
|
266
118
|
if mirror_enabled:
|
|
267
119
|
env = apply_mirror_environment("pnpm", env)
|
|
268
120
|
env = apply_mirror_environment("npm", env)
|
|
269
|
-
|
|
121
|
+
|
|
270
122
|
# Install dependencies in project root
|
|
271
|
-
install_result =
|
|
123
|
+
install_result = PnpmExecutor.execute(
|
|
272
124
|
[pnpm_executable, "install"],
|
|
273
125
|
project_root,
|
|
274
126
|
env,
|
|
@@ -283,13 +135,13 @@ class PnpmCompiler(CompilerBase):
|
|
|
283
135
|
if extra_args:
|
|
284
136
|
build_args.extend(extra_args)
|
|
285
137
|
|
|
286
|
-
return
|
|
138
|
+
return PnpmExecutor.execute(
|
|
287
139
|
build_args,
|
|
288
140
|
project_root,
|
|
289
141
|
env,
|
|
290
142
|
stream_output=stream_output,
|
|
291
143
|
)
|
|
292
|
-
|
|
144
|
+
|
|
293
145
|
def install_dependencies(
|
|
294
146
|
self,
|
|
295
147
|
source_dir: Path,
|
|
@@ -299,20 +151,20 @@ class PnpmCompiler(CompilerBase):
|
|
|
299
151
|
production: bool = False,
|
|
300
152
|
) -> BuildResult:
|
|
301
153
|
"""Install dependencies using pnpm with auto-detection of project root.
|
|
302
|
-
|
|
154
|
+
|
|
303
155
|
Args:
|
|
304
156
|
source_dir: Source code directory or subdirectory
|
|
305
157
|
environment: Additional environment variables
|
|
306
158
|
mirror_enabled: Whether to use mirror acceleration
|
|
307
159
|
production: Install only production dependencies
|
|
308
|
-
|
|
160
|
+
|
|
309
161
|
Returns:
|
|
310
162
|
BuildResult containing success status and output information.
|
|
311
163
|
"""
|
|
312
164
|
pnpm_executable = self._get_executable_path()
|
|
313
|
-
|
|
165
|
+
|
|
314
166
|
project_root = self.find_project_root(source_dir)
|
|
315
|
-
|
|
167
|
+
|
|
316
168
|
if project_root is None:
|
|
317
169
|
return BuildResult(
|
|
318
170
|
success=False,
|
|
@@ -322,23 +174,23 @@ class PnpmCompiler(CompilerBase):
|
|
|
322
174
|
output_path=None,
|
|
323
175
|
duration_seconds=0.0,
|
|
324
176
|
)
|
|
325
|
-
|
|
177
|
+
|
|
326
178
|
env = environment.copy() if environment else {}
|
|
327
|
-
|
|
179
|
+
|
|
328
180
|
if mirror_enabled:
|
|
329
181
|
env = apply_mirror_environment("pnpm", env)
|
|
330
|
-
|
|
182
|
+
|
|
331
183
|
command = [pnpm_executable, "install"]
|
|
332
184
|
if production:
|
|
333
185
|
command.append("--prod")
|
|
334
186
|
|
|
335
|
-
return
|
|
187
|
+
return PnpmExecutor.execute(
|
|
336
188
|
command,
|
|
337
189
|
project_root,
|
|
338
190
|
env,
|
|
339
191
|
stream_output=True,
|
|
340
192
|
)
|
|
341
|
-
|
|
193
|
+
|
|
342
194
|
def run_script(
|
|
343
195
|
self,
|
|
344
196
|
source_dir: Path,
|
|
@@ -348,20 +200,20 @@ class PnpmCompiler(CompilerBase):
|
|
|
348
200
|
mirror_enabled: bool = True,
|
|
349
201
|
) -> BuildResult:
|
|
350
202
|
"""Run a specific npm script with auto-detection of project root.
|
|
351
|
-
|
|
203
|
+
|
|
352
204
|
Args:
|
|
353
205
|
source_dir: Source code directory or subdirectory
|
|
354
206
|
script_name: Name of the script to run
|
|
355
207
|
environment: Additional environment variables
|
|
356
208
|
mirror_enabled: Whether to use mirror acceleration
|
|
357
|
-
|
|
209
|
+
|
|
358
210
|
Returns:
|
|
359
211
|
BuildResult containing success status and output information.
|
|
360
212
|
"""
|
|
361
213
|
pnpm_executable = self._get_executable_path()
|
|
362
|
-
|
|
214
|
+
|
|
363
215
|
project_root = self.find_project_root(source_dir)
|
|
364
|
-
|
|
216
|
+
|
|
365
217
|
if project_root is None:
|
|
366
218
|
return BuildResult(
|
|
367
219
|
success=False,
|
|
@@ -371,71 +223,71 @@ class PnpmCompiler(CompilerBase):
|
|
|
371
223
|
output_path=None,
|
|
372
224
|
duration_seconds=0.0,
|
|
373
225
|
)
|
|
374
|
-
|
|
226
|
+
|
|
375
227
|
env = environment.copy() if environment else {}
|
|
376
|
-
|
|
228
|
+
|
|
377
229
|
if mirror_enabled:
|
|
378
230
|
env = apply_mirror_environment("pnpm", env)
|
|
379
|
-
|
|
380
|
-
return
|
|
231
|
+
|
|
232
|
+
return PnpmExecutor.execute(
|
|
381
233
|
[pnpm_executable, "run", script_name],
|
|
382
234
|
project_root,
|
|
383
235
|
env,
|
|
384
236
|
)
|
|
385
|
-
|
|
237
|
+
|
|
386
238
|
def clean(self, directory: Path) -> bool:
|
|
387
239
|
"""Clean pnpm artifacts in the specified directory.
|
|
388
|
-
|
|
240
|
+
|
|
389
241
|
Args:
|
|
390
242
|
directory: Directory to clean
|
|
391
|
-
|
|
243
|
+
|
|
392
244
|
Returns:
|
|
393
245
|
True if successful, False otherwise.
|
|
394
246
|
"""
|
|
395
247
|
import shutil
|
|
396
|
-
|
|
248
|
+
|
|
397
249
|
try:
|
|
398
250
|
directory = self._validate_directory(directory, create_if_not_exists=False)
|
|
399
|
-
|
|
251
|
+
|
|
400
252
|
# Remove node_modules
|
|
401
253
|
node_modules = directory / "node_modules"
|
|
402
254
|
if node_modules.exists():
|
|
403
255
|
shutil.rmtree(node_modules)
|
|
404
|
-
|
|
256
|
+
|
|
405
257
|
# Remove pnpm-lock.yaml
|
|
406
258
|
lock_file = directory / "pnpm-lock.yaml"
|
|
407
259
|
if lock_file.exists():
|
|
408
260
|
lock_file.unlink()
|
|
409
|
-
|
|
261
|
+
|
|
410
262
|
# Remove .pnpm-store if exists
|
|
411
263
|
pnpm_store = directory / ".pnpm-store"
|
|
412
264
|
if pnpm_store.exists():
|
|
413
265
|
shutil.rmtree(pnpm_store)
|
|
414
|
-
|
|
266
|
+
|
|
415
267
|
return True
|
|
416
|
-
|
|
268
|
+
|
|
417
269
|
except Exception:
|
|
418
270
|
return False
|
|
419
|
-
|
|
271
|
+
|
|
420
272
|
def _get_executable_path(self) -> str:
|
|
421
273
|
"""Get the pnpm executable path."""
|
|
422
274
|
if self._pnpm_path:
|
|
423
275
|
return self._pnpm_path
|
|
424
|
-
|
|
276
|
+
|
|
425
277
|
pnpm_path = shutil.which("pnpm")
|
|
426
278
|
if pnpm_path is None:
|
|
427
279
|
raise RuntimeError("pnpm not found in PATH. Please install pnpm or provide pnpm_path.")
|
|
428
|
-
|
|
280
|
+
|
|
429
281
|
return pnpm_path
|
|
430
|
-
|
|
282
|
+
|
|
431
283
|
@staticmethod
|
|
432
284
|
def create(source_dir: Path, *, mirror_enabled: bool = True) -> "PnpmCompiler":
|
|
433
285
|
"""Factory method to create a PnpmCompiler instance.
|
|
434
|
-
|
|
286
|
+
|
|
435
287
|
Args:
|
|
436
288
|
source_dir: Source directory for the project
|
|
437
289
|
mirror_enabled: Whether to enable mirror acceleration by default
|
|
438
|
-
|
|
290
|
+
|
|
439
291
|
Returns:
|
|
440
292
|
Configured PnpmCompiler instance
|
|
441
293
|
"""
|
|
@@ -447,7 +299,7 @@ def main() -> None:
|
|
|
447
299
|
"""PNPM compiler CLI entry point."""
|
|
448
300
|
import argparse
|
|
449
301
|
import sys
|
|
450
|
-
|
|
302
|
+
|
|
451
303
|
parser = argparse.ArgumentParser(description="PNPM Build Compiler")
|
|
452
304
|
parser.add_argument("source_dir", type=Path, help="Source directory")
|
|
453
305
|
parser.add_argument("-o", "--output", type=Path, required=True, help="Output directory")
|
|
@@ -455,11 +307,11 @@ def main() -> None:
|
|
|
455
307
|
parser.add_argument("--no-mirror", dest="mirror", action="store_false", help="Disable mirror acceleration")
|
|
456
308
|
parser.add_argument("--script", type=str, help="Run specific npm script instead of build")
|
|
457
309
|
parser.add_argument("--install", action="store_true", help="Install dependencies only")
|
|
458
|
-
|
|
310
|
+
|
|
459
311
|
args = parser.parse_args()
|
|
460
|
-
|
|
312
|
+
|
|
461
313
|
compiler = PnpmCompiler()
|
|
462
|
-
|
|
314
|
+
|
|
463
315
|
if args.script:
|
|
464
316
|
result = compiler.run_script(
|
|
465
317
|
args.source_dir,
|
|
@@ -477,5 +329,5 @@ def main() -> None:
|
|
|
477
329
|
args.output,
|
|
478
330
|
mirror_enabled=args.mirror,
|
|
479
331
|
)
|
|
480
|
-
|
|
332
|
+
|
|
481
333
|
sys.exit(0 if result["success"] else 1)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""PNPM command execution."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Final
|
|
9
|
+
|
|
10
|
+
from multi_lang_build.compiler.base import BuildResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PnpmExecutor:
|
|
14
|
+
"""Execute pnpm commands with streaming support."""
|
|
15
|
+
|
|
16
|
+
DEFAULT_TIMEOUT: Final[int] = 3600 # 1 hour
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def execute(
|
|
20
|
+
command: list[str],
|
|
21
|
+
working_dir: Path,
|
|
22
|
+
environment: dict[str, str],
|
|
23
|
+
stream_output: bool = True,
|
|
24
|
+
) -> BuildResult:
|
|
25
|
+
"""Execute a command in the specified directory.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
command: Command to execute
|
|
29
|
+
working_dir: Working directory for the command
|
|
30
|
+
environment: Environment variables
|
|
31
|
+
stream_output: Whether to stream output in real-time
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
BuildResult containing success status and output information
|
|
35
|
+
"""
|
|
36
|
+
original_cwd = Path.cwd()
|
|
37
|
+
full_command = command.copy()
|
|
38
|
+
start_time: float = 0.0
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
os.chdir(working_dir)
|
|
42
|
+
start_time = time.perf_counter()
|
|
43
|
+
|
|
44
|
+
if stream_output:
|
|
45
|
+
return PnpmExecutor._execute_stream(
|
|
46
|
+
full_command, working_dir, environment, start_time
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
return PnpmExecutor._execute_capture(
|
|
50
|
+
full_command, working_dir, environment, start_time
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
except subprocess.TimeoutExpired:
|
|
54
|
+
duration = time.perf_counter() - start_time
|
|
55
|
+
return BuildResult(
|
|
56
|
+
success=False,
|
|
57
|
+
return_code=-1,
|
|
58
|
+
stdout="",
|
|
59
|
+
stderr="Build timed out after 1 hour",
|
|
60
|
+
output_path=None,
|
|
61
|
+
duration_seconds=duration,
|
|
62
|
+
)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
duration = time.perf_counter() - start_time
|
|
65
|
+
return BuildResult(
|
|
66
|
+
success=False,
|
|
67
|
+
return_code=-2,
|
|
68
|
+
stdout="",
|
|
69
|
+
stderr=f"Build error: {str(e)}",
|
|
70
|
+
output_path=None,
|
|
71
|
+
duration_seconds=duration,
|
|
72
|
+
)
|
|
73
|
+
finally:
|
|
74
|
+
os.chdir(original_cwd)
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def _execute_stream(
|
|
78
|
+
command: list[str],
|
|
79
|
+
working_dir: Path,
|
|
80
|
+
environment: dict[str, str],
|
|
81
|
+
start_time: float,
|
|
82
|
+
) -> BuildResult:
|
|
83
|
+
"""Execute with real-time output streaming."""
|
|
84
|
+
stdout_buffer = []
|
|
85
|
+
stderr_buffer = []
|
|
86
|
+
|
|
87
|
+
process = subprocess.Popen(
|
|
88
|
+
command,
|
|
89
|
+
stdout=subprocess.PIPE,
|
|
90
|
+
stderr=subprocess.PIPE,
|
|
91
|
+
text=True,
|
|
92
|
+
env={**os.environ, **environment},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Read stdout in real-time
|
|
96
|
+
if process.stdout:
|
|
97
|
+
for line in process.stdout:
|
|
98
|
+
line = line.rstrip('\n\r')
|
|
99
|
+
stdout_buffer.append(line)
|
|
100
|
+
print(line)
|
|
101
|
+
sys.stdout.flush()
|
|
102
|
+
|
|
103
|
+
# Read stderr in real-time
|
|
104
|
+
if process.stderr:
|
|
105
|
+
for line in process.stderr:
|
|
106
|
+
line = line.rstrip('\n\r')
|
|
107
|
+
stderr_buffer.append(line)
|
|
108
|
+
print(line, file=sys.stderr)
|
|
109
|
+
sys.stderr.flush()
|
|
110
|
+
|
|
111
|
+
return_code = process.wait()
|
|
112
|
+
duration = time.perf_counter() - start_time
|
|
113
|
+
|
|
114
|
+
return BuildResult(
|
|
115
|
+
success=return_code == 0,
|
|
116
|
+
return_code=return_code,
|
|
117
|
+
stdout='\n'.join(stdout_buffer),
|
|
118
|
+
stderr='\n'.join(stderr_buffer),
|
|
119
|
+
output_path=working_dir,
|
|
120
|
+
duration_seconds=duration,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _execute_capture(
|
|
125
|
+
command: list[str],
|
|
126
|
+
working_dir: Path,
|
|
127
|
+
environment: dict[str, str],
|
|
128
|
+
start_time: float,
|
|
129
|
+
) -> BuildResult:
|
|
130
|
+
"""Execute with captured output."""
|
|
131
|
+
result = subprocess.run(
|
|
132
|
+
command,
|
|
133
|
+
capture_output=True,
|
|
134
|
+
text=True,
|
|
135
|
+
timeout=PnpmExecutor.DEFAULT_TIMEOUT,
|
|
136
|
+
env={**os.environ, **environment},
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
duration = time.perf_counter() - start_time
|
|
140
|
+
|
|
141
|
+
return BuildResult(
|
|
142
|
+
success=result.returncode == 0,
|
|
143
|
+
return_code=result.returncode,
|
|
144
|
+
stdout=result.stdout,
|
|
145
|
+
stderr=result.stderr,
|
|
146
|
+
output_path=working_dir,
|
|
147
|
+
duration_seconds=duration,
|
|
148
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""PNPM project detection and utilities."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PnpmProject:
|
|
7
|
+
"""PNPM project detection and root finding."""
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def find_root(start_path: Path) -> Path | None:
|
|
11
|
+
"""Find the frontend project root directory containing package.json.
|
|
12
|
+
|
|
13
|
+
Searches from start_path up to its ancestors and also checks subdirectories.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
start_path: Starting path to search from
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Path to project root or None if not found
|
|
20
|
+
"""
|
|
21
|
+
start_path = start_path.resolve()
|
|
22
|
+
|
|
23
|
+
# Check if start_path itself is the root
|
|
24
|
+
if (start_path / "package.json").exists():
|
|
25
|
+
return start_path
|
|
26
|
+
|
|
27
|
+
# Search upwards in the directory tree
|
|
28
|
+
current = start_path.parent
|
|
29
|
+
while current != current.parent:
|
|
30
|
+
if (current / "package.json").exists():
|
|
31
|
+
return current
|
|
32
|
+
current = current.parent
|
|
33
|
+
|
|
34
|
+
# Search subdirectories
|
|
35
|
+
for subdir in start_path.iterdir():
|
|
36
|
+
if subdir.is_dir() and not subdir.name.startswith("."):
|
|
37
|
+
subdir_root = PnpmProject.find_root(subdir)
|
|
38
|
+
if subdir_root:
|
|
39
|
+
return subdir_root
|
|
40
|
+
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def validate(root: Path) -> bool:
|
|
45
|
+
"""Validate that the path is a valid PNPM project.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
root: Project root path
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if valid, False otherwise
|
|
52
|
+
"""
|
|
53
|
+
return (root / "package.json").exists()
|