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.
@@ -0,0 +1,539 @@
1
+ """Python compiler with mirror acceleration support."""
2
+
3
+ from pathlib import Path
4
+ from typing import Final
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+ import venv
9
+ import json
10
+ import time
11
+
12
+ from multi_lang_build.compiler.base import CompilerBase, BuildResult, CompilerInfo
13
+ from multi_lang_build.mirror.config import (
14
+ get_mirror_config,
15
+ apply_mirror_environment,
16
+ PYPI_MIRROR_TSINGHUA,
17
+ PYPI_MIRROR_ALIYUN,
18
+ PYPI_MIRROR_DOUBAN,
19
+ PYPI_MIRROR_HUAWEI,
20
+ DEFAULT_PIP_MIRROR,
21
+ )
22
+
23
+
24
+ class PythonCompiler(CompilerBase):
25
+ """Compiler for Python projects with pip and poetry support."""
26
+
27
+ NAME: Final[str] = "python"
28
+ DEFAULT_MIRROR: Final[str] = DEFAULT_PIP_MIRROR
29
+
30
+ def __init__(
31
+ self,
32
+ python_path: str | None = None,
33
+ mirror: str | None = None,
34
+ ) -> None:
35
+ """Initialize the Python compiler.
36
+
37
+ Args:
38
+ python_path: Optional path to python executable. If None, uses sys.executable.
39
+ mirror: Mirror configuration name (e.g., "pip", "pip_aliyun", "pip_douban", "pip_huawei").
40
+ Defaults to DEFAULT_PIP_MIRROR (Tsinghua).
41
+ """
42
+ self._python_path = python_path
43
+ self._mirror = mirror
44
+ self._version_cache: str | None = None
45
+
46
+ @property
47
+ def name(self) -> str:
48
+ """Get the compiler name."""
49
+ return self.NAME
50
+
51
+ @property
52
+ def version(self) -> str:
53
+ """Get the Python version."""
54
+ if self._version_cache:
55
+ return self._version_cache
56
+
57
+ python_executable = self._get_executable_path()
58
+
59
+ try:
60
+ result = subprocess.run(
61
+ [python_executable, "--version"],
62
+ capture_output=True,
63
+ text=True,
64
+ timeout=10,
65
+ )
66
+ output = result.stdout.strip() or result.stderr.strip()
67
+ self._version_cache = output.replace("Python ", "")
68
+ except Exception:
69
+ self._version_cache = sys.version.split()[0]
70
+
71
+ return self._version_cache
72
+
73
+ @property
74
+ def supported_mirrors(self) -> list[str]:
75
+ """Get list of supported mirror configurations."""
76
+ return ["pip", "pip_aliyun", "pip_douban", "pip_huawei"]
77
+
78
+ @property
79
+ def current_mirror(self) -> str:
80
+ """Get the current mirror configuration name."""
81
+ return self._mirror or "pip"
82
+
83
+ def get_info(self) -> CompilerInfo:
84
+ """Get compiler information."""
85
+ return {
86
+ "name": self.name,
87
+ "version": self.version,
88
+ "supported_mirrors": self.supported_mirrors,
89
+ "default_mirror": self.DEFAULT_MIRROR,
90
+ "executable": self._get_executable_path(),
91
+ }
92
+
93
+ def set_mirror(self, mirror: str) -> None:
94
+ """Set the mirror configuration.
95
+
96
+ Args:
97
+ mirror: Mirror configuration name (e.g., "pip", "pip_aliyun", "pip_douban", "pip_huawei")
98
+ """
99
+ self._mirror = mirror
100
+
101
+ def build(
102
+ self,
103
+ source_dir: Path,
104
+ output_dir: Path,
105
+ *,
106
+ environment: dict[str, str] | None = None,
107
+ mirror_enabled: bool = True,
108
+ extra_args: list[str] | None = None,
109
+ stream_output: bool = True,
110
+ ) -> BuildResult:
111
+ """Execute the Python build process.
112
+
113
+ Args:
114
+ source_dir: Source code directory
115
+ output_dir: Build output directory
116
+ environment: Additional environment variables
117
+ mirror_enabled: Whether to use PyPI mirror
118
+ extra_args: Additional arguments to pass to build command
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
+ python_executable = self._get_executable_path()
125
+
126
+ # Validate directories
127
+ source_dir = self._validate_directory(source_dir, create_if_not_exists=False)
128
+ output_dir = self._validate_directory(output_dir, create_if_not_exists=True)
129
+
130
+ # Prepare environment
131
+ env = environment.copy() if environment else {}
132
+
133
+ if mirror_enabled:
134
+ env = apply_mirror_environment("python", env)
135
+ env = apply_mirror_environment("pip", env)
136
+
137
+ # Determine build system
138
+ build_system = self._detect_build_system(source_dir)
139
+
140
+ if build_system == "poetry":
141
+ return self._build_with_poetry(
142
+ python_executable, source_dir, output_dir, env, extra_args, stream_output
143
+ )
144
+ elif build_system == "setuptools":
145
+ return self._build_with_setuptools(
146
+ python_executable, source_dir, output_dir, env, extra_args, stream_output
147
+ )
148
+ elif build_system == "pdm":
149
+ return self._build_with_pdm(
150
+ python_executable, source_dir, output_dir, env, extra_args, stream_output
151
+ )
152
+ else:
153
+ return BuildResult(
154
+ success=False,
155
+ return_code=-1,
156
+ stdout="",
157
+ stderr=f"No supported build system found in {source_dir}. "
158
+ "Expected pyproject.toml, setup.py, or setup.cfg",
159
+ output_path=None,
160
+ duration_seconds=0.0,
161
+ )
162
+
163
+ def _detect_build_system(self, source_dir: Path) -> str:
164
+ """Detect the build system used by the project.
165
+
166
+ Args:
167
+ source_dir: Source directory to check
168
+
169
+ Returns:
170
+ Build system name: "poetry", "setuptools", "pdm", or "none"
171
+ """
172
+ pyproject = source_dir / "pyproject.toml"
173
+ setup_py = source_dir / "setup.py"
174
+ setup_cfg = source_dir / "setup.cfg"
175
+ pdm_pyproject = source_dir / "pyproject.toml"
176
+
177
+ # Check for poetry
178
+ if pyproject.exists():
179
+ try:
180
+ content = pyproject.read_text()
181
+ if "[tool.poetry]" in content:
182
+ return "poetry"
183
+ if "[tool.pdm]" in content:
184
+ return "pdm"
185
+ except Exception:
186
+ pass
187
+
188
+ # Check for setuptools
189
+ if setup_py.exists() or setup_cfg.exists():
190
+ return "setuptools"
191
+
192
+ # Check pdm
193
+ if pdm_pyproject.exists():
194
+ return "pdm"
195
+
196
+ return "none"
197
+
198
+ def _build_with_poetry(
199
+ self,
200
+ python_executable: str,
201
+ source_dir: Path,
202
+ output_dir: Path,
203
+ env: dict[str, str],
204
+ extra_args: list[str] | None = None,
205
+ stream_output: bool = True,
206
+ ) -> BuildResult:
207
+ """Build using Poetry."""
208
+ # Install dependencies
209
+ install_cmd = [python_executable, "-m", "poetry", "install"]
210
+
211
+ install_result = self._run_build(
212
+ install_cmd,
213
+ source_dir,
214
+ output_dir,
215
+ environment=env,
216
+ stream_output=stream_output,
217
+ )
218
+
219
+ if not install_result["success"]:
220
+ return install_result
221
+
222
+ # Build package
223
+ build_cmd = [python_executable, "-m", "poetry", "build"]
224
+
225
+ if extra_args:
226
+ build_cmd.extend(extra_args)
227
+
228
+ return self._run_build(
229
+ build_cmd,
230
+ source_dir,
231
+ output_dir,
232
+ environment=env,
233
+ stream_output=stream_output,
234
+ )
235
+
236
+ def _build_with_setuptools(
237
+ self,
238
+ python_executable: str,
239
+ source_dir: Path,
240
+ output_dir: Path,
241
+ env: dict[str, str],
242
+ extra_args: list[str] | None = None,
243
+ stream_output: bool = True,
244
+ ) -> BuildResult:
245
+ """Build using setuptools."""
246
+ # Install build dependencies first
247
+ if (source_dir / "pyproject.toml").exists():
248
+ # Use build module for modern setuptools
249
+ build_cmd = [python_executable, "-m", "build", "--outdir", str(output_dir)]
250
+ else:
251
+ # Legacy setup.py
252
+ build_cmd = [python_executable, "setup.py", "bdist_wheel", "--dist-dir", str(output_dir)]
253
+
254
+ if extra_args:
255
+ build_cmd.extend(extra_args)
256
+
257
+ return self._run_build(
258
+ build_cmd,
259
+ source_dir,
260
+ output_dir,
261
+ environment=env,
262
+ stream_output=stream_output,
263
+ )
264
+
265
+ def _build_with_pdm(
266
+ self,
267
+ python_executable: str,
268
+ source_dir: Path,
269
+ output_dir: Path,
270
+ env: dict[str, str],
271
+ extra_args: list[str] | None = None,
272
+ stream_output: bool = True,
273
+ ) -> BuildResult:
274
+ """Build using PDM."""
275
+ # Build with PDM
276
+ build_cmd = [python_executable, "-m", "pdm", "build"]
277
+
278
+ if extra_args:
279
+ build_cmd.extend(extra_args)
280
+
281
+ return self._run_build(
282
+ build_cmd,
283
+ source_dir,
284
+ output_dir,
285
+ environment=env,
286
+ stream_output=stream_output,
287
+ )
288
+
289
+ def install_dependencies(
290
+ self,
291
+ source_dir: Path,
292
+ *,
293
+ environment: dict[str, str] | None = None,
294
+ mirror_enabled: bool = True,
295
+ dev: bool = False,
296
+ poetry: bool = False,
297
+ ) -> BuildResult:
298
+ """Install Python dependencies.
299
+
300
+ Args:
301
+ source_dir: Source code directory
302
+ environment: Additional environment variables
303
+ mirror_enabled: Whether to use PyPI mirror
304
+ dev: Include development dependencies
305
+ poetry: Force using poetry
306
+
307
+ Returns:
308
+ BuildResult containing success status and output information.
309
+ """
310
+ python_executable = self._get_executable_path()
311
+
312
+ source_dir = self._validate_directory(source_dir, create_if_not_exists=False)
313
+
314
+ env = environment.copy() if environment else {}
315
+
316
+ if mirror_enabled:
317
+ env = apply_mirror_environment("pip", env)
318
+
319
+ # Check for poetry first
320
+ if poetry or (source_dir / "pyproject.toml").exists() and "poetry" in (source_dir / "pyproject.toml").read_text():
321
+ cmd = [python_executable, "-m", "poetry", "install"]
322
+ if dev:
323
+ cmd.append("--with=dev")
324
+ return self._run_build(cmd, source_dir, source_dir, environment=env)
325
+
326
+ # Use pip
327
+ cmd = [python_executable, "-m", "pip", "install", "-r", str(source_dir / "requirements.txt")]
328
+
329
+ if dev:
330
+ cmd.append("-r")
331
+ cmd.append(str(source_dir / "requirements-dev.txt"))
332
+
333
+ return self._run_build(cmd, source_dir, source_dir, environment=env)
334
+
335
+ def create_venv(
336
+ self,
337
+ directory: Path,
338
+ *,
339
+ python_path: str | None = None,
340
+ mirror_enabled: bool = True,
341
+ ) -> BuildResult:
342
+ """Create a virtual environment.
343
+
344
+ Args:
345
+ directory: Directory to create the venv in
346
+ python_path: Python interpreter to use
347
+ mirror_enabled: Whether to configure pip mirror
348
+
349
+ Returns:
350
+ BuildResult containing success status and output information.
351
+ """
352
+ env_builder = venv.EnvBuilder(with_pip=True, clear=True)
353
+
354
+ start_time = time.perf_counter() # noqa: F821
355
+
356
+ try:
357
+ env_builder.create(directory)
358
+
359
+ # Configure pip mirror if enabled
360
+ if mirror_enabled:
361
+ pip_config_dir = directory / "pip.conf"
362
+ pip_config_dir.write_text(
363
+ "[global]\n"
364
+ "index-url = https://pypi.tuna.tsinghua.edu.cn/simple\n"
365
+ "trusted-host = pypi.tuna.tsinghua.edu.cn\n"
366
+ )
367
+
368
+ duration = time.perf_counter() - start_time # noqa: F821
369
+
370
+ return BuildResult(
371
+ success=True,
372
+ return_code=0,
373
+ stdout=f"Virtual environment created at {directory}",
374
+ stderr="",
375
+ output_path=directory,
376
+ duration_seconds=duration,
377
+ )
378
+ except Exception as e:
379
+ duration = time.perf_counter() - start_time # noqa: F821
380
+ return BuildResult(
381
+ success=False,
382
+ return_code=-1,
383
+ stdout="",
384
+ stderr=f"Failed to create virtual environment: {str(e)}",
385
+ output_path=None,
386
+ duration_seconds=duration,
387
+ )
388
+
389
+ def run_script(
390
+ self,
391
+ source_dir: Path,
392
+ script_name: str,
393
+ *,
394
+ environment: dict[str, str] | None = None,
395
+ mirror_enabled: bool = True,
396
+ ) -> BuildResult:
397
+ """Run a Python script or module.
398
+
399
+ Args:
400
+ source_dir: Source code directory
401
+ script_name: Script or module name to run
402
+ environment: Additional environment variables
403
+ mirror_enabled: Whether to use PyPI mirror
404
+
405
+ Returns:
406
+ BuildResult containing success status and output information.
407
+ """
408
+ python_executable = self._get_executable_path()
409
+
410
+ source_dir = self._validate_directory(source_dir, create_if_not_exists=False)
411
+
412
+ env = environment.copy() if environment else {}
413
+
414
+ if mirror_enabled:
415
+ env = apply_mirror_environment("pip", env)
416
+
417
+ return self._run_build(
418
+ [python_executable, "-m", script_name],
419
+ source_dir,
420
+ source_dir,
421
+ environment=env,
422
+ )
423
+
424
+ def clean(self, directory: Path) -> bool:
425
+ """Clean Python build artifacts in the specified directory.
426
+
427
+ Args:
428
+ directory: Directory to clean
429
+
430
+ Returns:
431
+ True if successful, False otherwise.
432
+ """
433
+ import shutil
434
+
435
+ try:
436
+ directory = self._validate_directory(directory, create_if_not_exists=False)
437
+
438
+ # Remove __pycache__ directories
439
+ for pycache in directory.rglob("__pycache__"):
440
+ shutil.rmtree(pycache)
441
+
442
+ # Remove .pyc files
443
+ for pyc in directory.rglob("*.pyc"):
444
+ pyc.unlink()
445
+
446
+ # Remove .pytest_cache
447
+ pytest_cache = directory / ".pytest_cache"
448
+ if pytest_cache.exists():
449
+ shutil.rmtree(pytest_cache)
450
+
451
+ # Remove .tox
452
+ tox_dir = directory / ".tox"
453
+ if tox_dir.exists():
454
+ shutil.rmtree(tox_dir)
455
+
456
+ # Remove dist directory
457
+ dist_dir = directory / "dist"
458
+ if dist_dir.exists():
459
+ shutil.rmtree(dist_dir)
460
+
461
+ # Remove build directory
462
+ build_dir = directory / "build"
463
+ if build_dir.exists():
464
+ shutil.rmtree(build_dir)
465
+
466
+ # Remove *.egg-info directories
467
+ for egg_info in directory.rglob("*.egg-info"):
468
+ shutil.rmtree(egg_info)
469
+
470
+ return True
471
+
472
+ except Exception:
473
+ return False
474
+
475
+ def _get_executable_path(self) -> str:
476
+ """Get the python executable path."""
477
+ if self._python_path:
478
+ return self._python_path
479
+
480
+ python_path = shutil.which("python3") or shutil.which("python")
481
+ if python_path is None:
482
+ return sys.executable
483
+
484
+ return python_path
485
+
486
+ @staticmethod
487
+ def create(source_dir: Path, *, mirror_enabled: bool = True) -> "PythonCompiler":
488
+ """Factory method to create a PythonCompiler instance.
489
+
490
+ Args:
491
+ source_dir: Source directory for the project
492
+ mirror_enabled: Whether to enable mirror acceleration by default
493
+
494
+ Returns:
495
+ Configured PythonCompiler instance
496
+ """
497
+ compiler = PythonCompiler()
498
+ return compiler
499
+
500
+
501
+ def main() -> None:
502
+ """Python compiler CLI entry point."""
503
+ import argparse
504
+ import sys
505
+
506
+ parser = argparse.ArgumentParser(description="Python Build Compiler")
507
+ parser.add_argument("source_dir", type=Path, help="Source directory")
508
+ parser.add_argument("-o", "--output", type=Path, required=True, help="Output directory")
509
+ parser.add_argument("--mirror", action="store_true", default=True, help="Enable PyPI mirror")
510
+ parser.add_argument("--no-mirror", dest="mirror", action="store_false", help="Disable PyPI mirror")
511
+ parser.add_argument("--install", action="store_true", help="Install dependencies only")
512
+ parser.add_argument("--dev", action="store_true", help="Include dev dependencies")
513
+ parser.add_argument("--poetry", action="store_true", help="Force using poetry")
514
+ parser.add_argument("--create-venv", type=Path, help="Create virtual environment at path")
515
+
516
+ args = parser.parse_args()
517
+
518
+ compiler = PythonCompiler()
519
+
520
+ if args.create_venv:
521
+ result = compiler.create_venv(
522
+ args.create_venv,
523
+ mirror_enabled=args.mirror,
524
+ )
525
+ elif args.install:
526
+ result = compiler.install_dependencies(
527
+ args.source_dir,
528
+ mirror_enabled=args.mirror,
529
+ dev=args.dev,
530
+ poetry=args.poetry,
531
+ )
532
+ else:
533
+ result = compiler.build(
534
+ args.source_dir,
535
+ args.output,
536
+ mirror_enabled=args.mirror,
537
+ )
538
+
539
+ sys.exit(0 if result["success"] else 1)
@@ -0,0 +1,21 @@
1
+ """Mirror configuration module for domestic acceleration."""
2
+
3
+ from multi_lang_build.mirror.config import (
4
+ MirrorConfig,
5
+ MIRROR_CONFIGS,
6
+ get_mirror_config,
7
+ apply_mirror_environment,
8
+ reset_mirror_environment,
9
+ get_all_mirror_names,
10
+ validate_mirror_config,
11
+ )
12
+
13
+ __all__ = [
14
+ "MirrorConfig",
15
+ "MIRROR_CONFIGS",
16
+ "get_mirror_config",
17
+ "apply_mirror_environment",
18
+ "reset_mirror_environment",
19
+ "get_all_mirror_names",
20
+ "validate_mirror_config",
21
+ ]