multi-lang-build 0.2.0__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,530 @@
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
+ ) -> BuildResult:
110
+ """Execute the Python build process.
111
+
112
+ Args:
113
+ source_dir: Source code directory
114
+ output_dir: Build output directory
115
+ environment: Additional environment variables
116
+ mirror_enabled: Whether to use PyPI mirror
117
+ extra_args: Additional arguments to pass to build command
118
+
119
+ Returns:
120
+ BuildResult containing success status and output information.
121
+ """
122
+ python_executable = self._get_executable_path()
123
+
124
+ # Validate directories
125
+ source_dir = self._validate_directory(source_dir, create_if_not_exists=False)
126
+ output_dir = self._validate_directory(output_dir, create_if_not_exists=True)
127
+
128
+ # Prepare environment
129
+ env = environment.copy() if environment else {}
130
+
131
+ if mirror_enabled:
132
+ env = apply_mirror_environment("python", env)
133
+ env = apply_mirror_environment("pip", env)
134
+
135
+ # Determine build system
136
+ build_system = self._detect_build_system(source_dir)
137
+
138
+ if build_system == "poetry":
139
+ return self._build_with_poetry(
140
+ python_executable, source_dir, output_dir, env, extra_args
141
+ )
142
+ elif build_system == "setuptools":
143
+ return self._build_with_setuptools(
144
+ python_executable, source_dir, output_dir, env, extra_args
145
+ )
146
+ elif build_system == "pdm":
147
+ return self._build_with_pdm(
148
+ python_executable, source_dir, output_dir, env, extra_args
149
+ )
150
+ else:
151
+ return BuildResult(
152
+ success=False,
153
+ return_code=-1,
154
+ stdout="",
155
+ stderr=f"No supported build system found in {source_dir}. "
156
+ "Expected pyproject.toml, setup.py, or setup.cfg",
157
+ output_path=None,
158
+ duration_seconds=0.0,
159
+ )
160
+
161
+ def _detect_build_system(self, source_dir: Path) -> str:
162
+ """Detect the build system used by the project.
163
+
164
+ Args:
165
+ source_dir: Source directory to check
166
+
167
+ Returns:
168
+ Build system name: "poetry", "setuptools", "pdm", or "none"
169
+ """
170
+ pyproject = source_dir / "pyproject.toml"
171
+ setup_py = source_dir / "setup.py"
172
+ setup_cfg = source_dir / "setup.cfg"
173
+ pdm_pyproject = source_dir / "pyproject.toml"
174
+
175
+ # Check for poetry
176
+ if pyproject.exists():
177
+ try:
178
+ content = pyproject.read_text()
179
+ if "[tool.poetry]" in content:
180
+ return "poetry"
181
+ if "[tool.pdm]" in content:
182
+ return "pdm"
183
+ except Exception:
184
+ pass
185
+
186
+ # Check for setuptools
187
+ if setup_py.exists() or setup_cfg.exists():
188
+ return "setuptools"
189
+
190
+ # Check pdm
191
+ if pdm_pyproject.exists():
192
+ return "pdm"
193
+
194
+ return "none"
195
+
196
+ def _build_with_poetry(
197
+ self,
198
+ python_executable: str,
199
+ source_dir: Path,
200
+ output_dir: Path,
201
+ env: dict[str, str],
202
+ extra_args: list[str] | None = None,
203
+ ) -> BuildResult:
204
+ """Build using Poetry."""
205
+ # Install dependencies
206
+ install_cmd = [python_executable, "-m", "poetry", "install"]
207
+
208
+ install_result = self._run_build(
209
+ install_cmd,
210
+ source_dir,
211
+ output_dir,
212
+ environment=env,
213
+ )
214
+
215
+ if not install_result["success"]:
216
+ return install_result
217
+
218
+ # Build package
219
+ build_cmd = [python_executable, "-m", "poetry", "build"]
220
+
221
+ if extra_args:
222
+ build_cmd.extend(extra_args)
223
+
224
+ return self._run_build(
225
+ build_cmd,
226
+ source_dir,
227
+ output_dir,
228
+ environment=env,
229
+ )
230
+
231
+ def _build_with_setuptools(
232
+ self,
233
+ python_executable: str,
234
+ source_dir: Path,
235
+ output_dir: Path,
236
+ env: dict[str, str],
237
+ extra_args: list[str] | None = None,
238
+ ) -> BuildResult:
239
+ """Build using setuptools."""
240
+ # Install build dependencies first
241
+ if (source_dir / "pyproject.toml").exists():
242
+ # Use build module for modern setuptools
243
+ build_cmd = [python_executable, "-m", "build", "--outdir", str(output_dir)]
244
+ else:
245
+ # Legacy setup.py
246
+ build_cmd = [python_executable, "setup.py", "bdist_wheel", "--dist-dir", str(output_dir)]
247
+
248
+ if extra_args:
249
+ build_cmd.extend(extra_args)
250
+
251
+ return self._run_build(
252
+ build_cmd,
253
+ source_dir,
254
+ output_dir,
255
+ environment=env,
256
+ )
257
+
258
+ def _build_with_pdm(
259
+ self,
260
+ python_executable: str,
261
+ source_dir: Path,
262
+ output_dir: Path,
263
+ env: dict[str, str],
264
+ extra_args: list[str] | None = None,
265
+ ) -> BuildResult:
266
+ """Build using PDM."""
267
+ # Build with PDM
268
+ build_cmd = [python_executable, "-m", "pdm", "build"]
269
+
270
+ if extra_args:
271
+ build_cmd.extend(extra_args)
272
+
273
+ return self._run_build(
274
+ build_cmd,
275
+ source_dir,
276
+ output_dir,
277
+ environment=env,
278
+ )
279
+
280
+ def install_dependencies(
281
+ self,
282
+ source_dir: Path,
283
+ *,
284
+ environment: dict[str, str] | None = None,
285
+ mirror_enabled: bool = True,
286
+ dev: bool = False,
287
+ poetry: bool = False,
288
+ ) -> BuildResult:
289
+ """Install Python dependencies.
290
+
291
+ Args:
292
+ source_dir: Source code directory
293
+ environment: Additional environment variables
294
+ mirror_enabled: Whether to use PyPI mirror
295
+ dev: Include development dependencies
296
+ poetry: Force using poetry
297
+
298
+ Returns:
299
+ BuildResult containing success status and output information.
300
+ """
301
+ python_executable = self._get_executable_path()
302
+
303
+ source_dir = self._validate_directory(source_dir, create_if_not_exists=False)
304
+
305
+ env = environment.copy() if environment else {}
306
+
307
+ if mirror_enabled:
308
+ env = apply_mirror_environment("pip", env)
309
+
310
+ # Check for poetry first
311
+ if poetry or (source_dir / "pyproject.toml").exists() and "poetry" in (source_dir / "pyproject.toml").read_text():
312
+ cmd = [python_executable, "-m", "poetry", "install"]
313
+ if dev:
314
+ cmd.append("--with=dev")
315
+ return self._run_build(cmd, source_dir, source_dir, environment=env)
316
+
317
+ # Use pip
318
+ cmd = [python_executable, "-m", "pip", "install", "-r", str(source_dir / "requirements.txt")]
319
+
320
+ if dev:
321
+ cmd.append("-r")
322
+ cmd.append(str(source_dir / "requirements-dev.txt"))
323
+
324
+ return self._run_build(cmd, source_dir, source_dir, environment=env)
325
+
326
+ def create_venv(
327
+ self,
328
+ directory: Path,
329
+ *,
330
+ python_path: str | None = None,
331
+ mirror_enabled: bool = True,
332
+ ) -> BuildResult:
333
+ """Create a virtual environment.
334
+
335
+ Args:
336
+ directory: Directory to create the venv in
337
+ python_path: Python interpreter to use
338
+ mirror_enabled: Whether to configure pip mirror
339
+
340
+ Returns:
341
+ BuildResult containing success status and output information.
342
+ """
343
+ env_builder = venv.EnvBuilder(with_pip=True, clear=True)
344
+
345
+ start_time = time.perf_counter() # noqa: F821
346
+
347
+ try:
348
+ env_builder.create(directory)
349
+
350
+ # Configure pip mirror if enabled
351
+ if mirror_enabled:
352
+ pip_config_dir = directory / "pip.conf"
353
+ pip_config_dir.write_text(
354
+ "[global]\n"
355
+ "index-url = https://pypi.tuna.tsinghua.edu.cn/simple\n"
356
+ "trusted-host = pypi.tuna.tsinghua.edu.cn\n"
357
+ )
358
+
359
+ duration = time.perf_counter() - start_time # noqa: F821
360
+
361
+ return BuildResult(
362
+ success=True,
363
+ return_code=0,
364
+ stdout=f"Virtual environment created at {directory}",
365
+ stderr="",
366
+ output_path=directory,
367
+ duration_seconds=duration,
368
+ )
369
+ except Exception as e:
370
+ duration = time.perf_counter() - start_time # noqa: F821
371
+ return BuildResult(
372
+ success=False,
373
+ return_code=-1,
374
+ stdout="",
375
+ stderr=f"Failed to create virtual environment: {str(e)}",
376
+ output_path=None,
377
+ duration_seconds=duration,
378
+ )
379
+
380
+ def run_script(
381
+ self,
382
+ source_dir: Path,
383
+ script_name: str,
384
+ *,
385
+ environment: dict[str, str] | None = None,
386
+ mirror_enabled: bool = True,
387
+ ) -> BuildResult:
388
+ """Run a Python script or module.
389
+
390
+ Args:
391
+ source_dir: Source code directory
392
+ script_name: Script or module name to run
393
+ environment: Additional environment variables
394
+ mirror_enabled: Whether to use PyPI mirror
395
+
396
+ Returns:
397
+ BuildResult containing success status and output information.
398
+ """
399
+ python_executable = self._get_executable_path()
400
+
401
+ source_dir = self._validate_directory(source_dir, create_if_not_exists=False)
402
+
403
+ env = environment.copy() if environment else {}
404
+
405
+ if mirror_enabled:
406
+ env = apply_mirror_environment("pip", env)
407
+
408
+ return self._run_build(
409
+ [python_executable, "-m", script_name],
410
+ source_dir,
411
+ source_dir,
412
+ environment=env,
413
+ )
414
+
415
+ def clean(self, directory: Path) -> bool:
416
+ """Clean Python build artifacts in the specified directory.
417
+
418
+ Args:
419
+ directory: Directory to clean
420
+
421
+ Returns:
422
+ True if successful, False otherwise.
423
+ """
424
+ import shutil
425
+
426
+ try:
427
+ directory = self._validate_directory(directory, create_if_not_exists=False)
428
+
429
+ # Remove __pycache__ directories
430
+ for pycache in directory.rglob("__pycache__"):
431
+ shutil.rmtree(pycache)
432
+
433
+ # Remove .pyc files
434
+ for pyc in directory.rglob("*.pyc"):
435
+ pyc.unlink()
436
+
437
+ # Remove .pytest_cache
438
+ pytest_cache = directory / ".pytest_cache"
439
+ if pytest_cache.exists():
440
+ shutil.rmtree(pytest_cache)
441
+
442
+ # Remove .tox
443
+ tox_dir = directory / ".tox"
444
+ if tox_dir.exists():
445
+ shutil.rmtree(tox_dir)
446
+
447
+ # Remove dist directory
448
+ dist_dir = directory / "dist"
449
+ if dist_dir.exists():
450
+ shutil.rmtree(dist_dir)
451
+
452
+ # Remove build directory
453
+ build_dir = directory / "build"
454
+ if build_dir.exists():
455
+ shutil.rmtree(build_dir)
456
+
457
+ # Remove *.egg-info directories
458
+ for egg_info in directory.rglob("*.egg-info"):
459
+ shutil.rmtree(egg_info)
460
+
461
+ return True
462
+
463
+ except Exception:
464
+ return False
465
+
466
+ def _get_executable_path(self) -> str:
467
+ """Get the python executable path."""
468
+ if self._python_path:
469
+ return self._python_path
470
+
471
+ python_path = shutil.which("python3") or shutil.which("python")
472
+ if python_path is None:
473
+ return sys.executable
474
+
475
+ return python_path
476
+
477
+ @staticmethod
478
+ def create(source_dir: Path, *, mirror_enabled: bool = True) -> "PythonCompiler":
479
+ """Factory method to create a PythonCompiler instance.
480
+
481
+ Args:
482
+ source_dir: Source directory for the project
483
+ mirror_enabled: Whether to enable mirror acceleration by default
484
+
485
+ Returns:
486
+ Configured PythonCompiler instance
487
+ """
488
+ compiler = PythonCompiler()
489
+ return compiler
490
+
491
+
492
+ def main() -> None:
493
+ """Python compiler CLI entry point."""
494
+ import argparse
495
+ import sys
496
+
497
+ parser = argparse.ArgumentParser(description="Python Build Compiler")
498
+ parser.add_argument("source_dir", type=Path, help="Source directory")
499
+ parser.add_argument("-o", "--output", type=Path, required=True, help="Output directory")
500
+ parser.add_argument("--mirror", action="store_true", default=True, help="Enable PyPI mirror")
501
+ parser.add_argument("--no-mirror", dest="mirror", action="store_false", help="Disable PyPI mirror")
502
+ parser.add_argument("--install", action="store_true", help="Install dependencies only")
503
+ parser.add_argument("--dev", action="store_true", help="Include dev dependencies")
504
+ parser.add_argument("--poetry", action="store_true", help="Force using poetry")
505
+ parser.add_argument("--create-venv", type=Path, help="Create virtual environment at path")
506
+
507
+ args = parser.parse_args()
508
+
509
+ compiler = PythonCompiler()
510
+
511
+ if args.create_venv:
512
+ result = compiler.create_venv(
513
+ args.create_venv,
514
+ mirror_enabled=args.mirror,
515
+ )
516
+ elif args.install:
517
+ result = compiler.install_dependencies(
518
+ args.source_dir,
519
+ mirror_enabled=args.mirror,
520
+ dev=args.dev,
521
+ poetry=args.poetry,
522
+ )
523
+ else:
524
+ result = compiler.build(
525
+ args.source_dir,
526
+ args.output,
527
+ mirror_enabled=args.mirror,
528
+ )
529
+
530
+ 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
+ ]