multi-lang-build 0.2.5__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 +64 -0
- multi_lang_build/cli.py +320 -0
- multi_lang_build/compiler/__init__.py +22 -0
- multi_lang_build/compiler/base.py +309 -0
- multi_lang_build/compiler/go.py +468 -0
- multi_lang_build/compiler/pnpm.py +481 -0
- multi_lang_build/compiler/python.py +539 -0
- multi_lang_build/mirror/__init__.py +21 -0
- multi_lang_build/mirror/cli.py +340 -0
- multi_lang_build/mirror/config.py +251 -0
- multi_lang_build/py.typed +2 -0
- multi_lang_build/register.py +412 -0
- multi_lang_build/types.py +43 -0
- multi_lang_build-0.2.5.dist-info/METADATA +402 -0
- multi_lang_build-0.2.5.dist-info/RECORD +18 -0
- multi_lang_build-0.2.5.dist-info/WHEEL +4 -0
- multi_lang_build-0.2.5.dist-info/entry_points.txt +7 -0
- multi_lang_build-0.2.5.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"""PNPM compiler with mirror acceleration support."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Final
|
|
5
|
+
import shutil
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from multi_lang_build.compiler.base import CompilerBase, BuildResult, CompilerInfo
|
|
11
|
+
from multi_lang_build.mirror.config import get_mirror_config, apply_mirror_environment
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PnpmCompiler(CompilerBase):
|
|
15
|
+
"""Compiler for pnpm-based frontend projects."""
|
|
16
|
+
|
|
17
|
+
NAME: Final[str] = "pnpm"
|
|
18
|
+
DEFAULT_MIRROR: Final[str] = "https://registry.npmmirror.com"
|
|
19
|
+
|
|
20
|
+
def __init__(self, pnpm_path: str | None = None) -> None:
|
|
21
|
+
"""Initialize the PNPM compiler.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
pnpm_path: Optional path to pnpm executable. If None, uses system PATH.
|
|
25
|
+
"""
|
|
26
|
+
self._pnpm_path = pnpm_path
|
|
27
|
+
self._version_cache: str | None = None
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def name(self) -> str:
|
|
31
|
+
"""Get the compiler name."""
|
|
32
|
+
return self.NAME
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def version(self) -> str:
|
|
36
|
+
"""Get the pnpm version."""
|
|
37
|
+
if self._version_cache:
|
|
38
|
+
return self._version_cache
|
|
39
|
+
|
|
40
|
+
pnpm_executable = self._get_executable_path()
|
|
41
|
+
import subprocess
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
result = subprocess.run(
|
|
45
|
+
[pnpm_executable, "--version"],
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
48
|
+
timeout=10,
|
|
49
|
+
)
|
|
50
|
+
self._version_cache = result.stdout.strip()
|
|
51
|
+
except Exception:
|
|
52
|
+
self._version_cache = "unknown"
|
|
53
|
+
|
|
54
|
+
return self._version_cache
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def supported_mirrors(self) -> list[str]:
|
|
58
|
+
"""Get list of supported mirror configurations."""
|
|
59
|
+
return ["npm", "pnpm", "yarn"]
|
|
60
|
+
|
|
61
|
+
def get_info(self) -> CompilerInfo:
|
|
62
|
+
"""Get compiler information."""
|
|
63
|
+
return {
|
|
64
|
+
"name": self.name,
|
|
65
|
+
"version": self.version,
|
|
66
|
+
"supported_mirrors": self.supported_mirrors,
|
|
67
|
+
"default_mirror": self.DEFAULT_MIRROR,
|
|
68
|
+
"executable": self._get_executable_path(),
|
|
69
|
+
}
|
|
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
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
BuildResult containing success status and output information
|
|
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
|
|
131
|
+
|
|
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
|
+
def build(
|
|
223
|
+
self,
|
|
224
|
+
source_dir: Path,
|
|
225
|
+
output_dir: Path,
|
|
226
|
+
*,
|
|
227
|
+
environment: dict[str, str] | None = None,
|
|
228
|
+
mirror_enabled: bool = True,
|
|
229
|
+
extra_args: list[str] | None = None,
|
|
230
|
+
stream_output: bool = True,
|
|
231
|
+
) -> BuildResult:
|
|
232
|
+
"""Execute the pnpm build process with auto-detection of project root.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
source_dir: Source code directory or subdirectory
|
|
236
|
+
output_dir: Build output directory
|
|
237
|
+
environment: Additional environment variables
|
|
238
|
+
mirror_enabled: Whether to use mirror acceleration
|
|
239
|
+
extra_args: Additional arguments to pass to pnpm build
|
|
240
|
+
stream_output: Whether to stream output in real-time (default: True)
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
BuildResult containing success status and output information.
|
|
244
|
+
"""
|
|
245
|
+
pnpm_executable = self._get_executable_path()
|
|
246
|
+
|
|
247
|
+
# Auto-detect project root
|
|
248
|
+
project_root = self.find_project_root(source_dir)
|
|
249
|
+
|
|
250
|
+
if project_root is None:
|
|
251
|
+
return BuildResult(
|
|
252
|
+
success=False,
|
|
253
|
+
return_code=-1,
|
|
254
|
+
stdout="",
|
|
255
|
+
stderr=f"package.json not found in {source_dir} or its parent/sub directories",
|
|
256
|
+
output_path=None,
|
|
257
|
+
duration_seconds=0.0,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Validate directories
|
|
261
|
+
output_dir = self._validate_directory(output_dir, create_if_not_exists=True)
|
|
262
|
+
|
|
263
|
+
# Prepare environment
|
|
264
|
+
env = environment.copy() if environment else {}
|
|
265
|
+
|
|
266
|
+
if mirror_enabled:
|
|
267
|
+
env = apply_mirror_environment("pnpm", env)
|
|
268
|
+
env = apply_mirror_environment("npm", env)
|
|
269
|
+
|
|
270
|
+
# Install dependencies in project root
|
|
271
|
+
install_result = self._execute_in_directory(
|
|
272
|
+
[pnpm_executable, "install"],
|
|
273
|
+
project_root,
|
|
274
|
+
env,
|
|
275
|
+
stream_output=stream_output,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if not install_result["success"]:
|
|
279
|
+
return install_result
|
|
280
|
+
|
|
281
|
+
# Run build
|
|
282
|
+
build_args = [pnpm_executable, "build"]
|
|
283
|
+
if extra_args:
|
|
284
|
+
build_args.extend(extra_args)
|
|
285
|
+
|
|
286
|
+
return self._execute_in_directory(
|
|
287
|
+
build_args,
|
|
288
|
+
project_root,
|
|
289
|
+
env,
|
|
290
|
+
stream_output=stream_output,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def install_dependencies(
|
|
294
|
+
self,
|
|
295
|
+
source_dir: Path,
|
|
296
|
+
*,
|
|
297
|
+
environment: dict[str, str] | None = None,
|
|
298
|
+
mirror_enabled: bool = True,
|
|
299
|
+
production: bool = False,
|
|
300
|
+
) -> BuildResult:
|
|
301
|
+
"""Install dependencies using pnpm with auto-detection of project root.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
source_dir: Source code directory or subdirectory
|
|
305
|
+
environment: Additional environment variables
|
|
306
|
+
mirror_enabled: Whether to use mirror acceleration
|
|
307
|
+
production: Install only production dependencies
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
BuildResult containing success status and output information.
|
|
311
|
+
"""
|
|
312
|
+
pnpm_executable = self._get_executable_path()
|
|
313
|
+
|
|
314
|
+
project_root = self.find_project_root(source_dir)
|
|
315
|
+
|
|
316
|
+
if project_root is None:
|
|
317
|
+
return BuildResult(
|
|
318
|
+
success=False,
|
|
319
|
+
return_code=-1,
|
|
320
|
+
stdout="",
|
|
321
|
+
stderr=f"package.json not found in {source_dir} or its parent/sub directories",
|
|
322
|
+
output_path=None,
|
|
323
|
+
duration_seconds=0.0,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
env = environment.copy() if environment else {}
|
|
327
|
+
|
|
328
|
+
if mirror_enabled:
|
|
329
|
+
env = apply_mirror_environment("pnpm", env)
|
|
330
|
+
|
|
331
|
+
command = [pnpm_executable, "install"]
|
|
332
|
+
if production:
|
|
333
|
+
command.append("--prod")
|
|
334
|
+
|
|
335
|
+
return self._execute_in_directory(
|
|
336
|
+
command,
|
|
337
|
+
project_root,
|
|
338
|
+
env,
|
|
339
|
+
stream_output=True,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def run_script(
|
|
343
|
+
self,
|
|
344
|
+
source_dir: Path,
|
|
345
|
+
script_name: str,
|
|
346
|
+
*,
|
|
347
|
+
environment: dict[str, str] | None = None,
|
|
348
|
+
mirror_enabled: bool = True,
|
|
349
|
+
) -> BuildResult:
|
|
350
|
+
"""Run a specific npm script with auto-detection of project root.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
source_dir: Source code directory or subdirectory
|
|
354
|
+
script_name: Name of the script to run
|
|
355
|
+
environment: Additional environment variables
|
|
356
|
+
mirror_enabled: Whether to use mirror acceleration
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
BuildResult containing success status and output information.
|
|
360
|
+
"""
|
|
361
|
+
pnpm_executable = self._get_executable_path()
|
|
362
|
+
|
|
363
|
+
project_root = self.find_project_root(source_dir)
|
|
364
|
+
|
|
365
|
+
if project_root is None:
|
|
366
|
+
return BuildResult(
|
|
367
|
+
success=False,
|
|
368
|
+
return_code=-1,
|
|
369
|
+
stdout="",
|
|
370
|
+
stderr=f"package.json not found in {source_dir} or its parent/sub directories",
|
|
371
|
+
output_path=None,
|
|
372
|
+
duration_seconds=0.0,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
env = environment.copy() if environment else {}
|
|
376
|
+
|
|
377
|
+
if mirror_enabled:
|
|
378
|
+
env = apply_mirror_environment("pnpm", env)
|
|
379
|
+
|
|
380
|
+
return self._execute_in_directory(
|
|
381
|
+
[pnpm_executable, "run", script_name],
|
|
382
|
+
project_root,
|
|
383
|
+
env,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
def clean(self, directory: Path) -> bool:
|
|
387
|
+
"""Clean pnpm artifacts in the specified directory.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
directory: Directory to clean
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
True if successful, False otherwise.
|
|
394
|
+
"""
|
|
395
|
+
import shutil
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
directory = self._validate_directory(directory, create_if_not_exists=False)
|
|
399
|
+
|
|
400
|
+
# Remove node_modules
|
|
401
|
+
node_modules = directory / "node_modules"
|
|
402
|
+
if node_modules.exists():
|
|
403
|
+
shutil.rmtree(node_modules)
|
|
404
|
+
|
|
405
|
+
# Remove pnpm-lock.yaml
|
|
406
|
+
lock_file = directory / "pnpm-lock.yaml"
|
|
407
|
+
if lock_file.exists():
|
|
408
|
+
lock_file.unlink()
|
|
409
|
+
|
|
410
|
+
# Remove .pnpm-store if exists
|
|
411
|
+
pnpm_store = directory / ".pnpm-store"
|
|
412
|
+
if pnpm_store.exists():
|
|
413
|
+
shutil.rmtree(pnpm_store)
|
|
414
|
+
|
|
415
|
+
return True
|
|
416
|
+
|
|
417
|
+
except Exception:
|
|
418
|
+
return False
|
|
419
|
+
|
|
420
|
+
def _get_executable_path(self) -> str:
|
|
421
|
+
"""Get the pnpm executable path."""
|
|
422
|
+
if self._pnpm_path:
|
|
423
|
+
return self._pnpm_path
|
|
424
|
+
|
|
425
|
+
pnpm_path = shutil.which("pnpm")
|
|
426
|
+
if pnpm_path is None:
|
|
427
|
+
raise RuntimeError("pnpm not found in PATH. Please install pnpm or provide pnpm_path.")
|
|
428
|
+
|
|
429
|
+
return pnpm_path
|
|
430
|
+
|
|
431
|
+
@staticmethod
|
|
432
|
+
def create(source_dir: Path, *, mirror_enabled: bool = True) -> "PnpmCompiler":
|
|
433
|
+
"""Factory method to create a PnpmCompiler instance.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
source_dir: Source directory for the project
|
|
437
|
+
mirror_enabled: Whether to enable mirror acceleration by default
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
Configured PnpmCompiler instance
|
|
441
|
+
"""
|
|
442
|
+
compiler = PnpmCompiler()
|
|
443
|
+
return compiler
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def main() -> None:
|
|
447
|
+
"""PNPM compiler CLI entry point."""
|
|
448
|
+
import argparse
|
|
449
|
+
import sys
|
|
450
|
+
|
|
451
|
+
parser = argparse.ArgumentParser(description="PNPM Build Compiler")
|
|
452
|
+
parser.add_argument("source_dir", type=Path, help="Source directory")
|
|
453
|
+
parser.add_argument("-o", "--output", type=Path, required=True, help="Output directory")
|
|
454
|
+
parser.add_argument("--mirror", action="store_true", default=True, help="Enable mirror acceleration")
|
|
455
|
+
parser.add_argument("--no-mirror", dest="mirror", action="store_false", help="Disable mirror acceleration")
|
|
456
|
+
parser.add_argument("--script", type=str, help="Run specific npm script instead of build")
|
|
457
|
+
parser.add_argument("--install", action="store_true", help="Install dependencies only")
|
|
458
|
+
|
|
459
|
+
args = parser.parse_args()
|
|
460
|
+
|
|
461
|
+
compiler = PnpmCompiler()
|
|
462
|
+
|
|
463
|
+
if args.script:
|
|
464
|
+
result = compiler.run_script(
|
|
465
|
+
args.source_dir,
|
|
466
|
+
args.script,
|
|
467
|
+
mirror_enabled=args.mirror,
|
|
468
|
+
)
|
|
469
|
+
elif args.install:
|
|
470
|
+
result = compiler.install_dependencies(
|
|
471
|
+
args.source_dir,
|
|
472
|
+
mirror_enabled=args.mirror,
|
|
473
|
+
)
|
|
474
|
+
else:
|
|
475
|
+
result = compiler.build(
|
|
476
|
+
args.source_dir,
|
|
477
|
+
args.output,
|
|
478
|
+
mirror_enabled=args.mirror,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
sys.exit(0 if result["success"] else 1)
|