multi-lang-build 0.2.5__py3-none-any.whl → 0.3.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.
- multi_lang_build/__init__.py +4 -4
- multi_lang_build/cli.py +5 -2
- multi_lang_build/compiler/__init__.py +1 -1
- multi_lang_build/compiler/go_compiler.py +403 -0
- multi_lang_build/compiler/go_support/__init__.py +19 -0
- multi_lang_build/compiler/go_support/binary.py +119 -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 +80 -0
- multi_lang_build/compiler/go_support/tester.py +200 -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.5.dist-info → multi_lang_build-0.3.0.dist-info}/METADATA +41 -5
- multi_lang_build-0.3.0.dist-info/RECORD +40 -0
- multi_lang_build/compiler/go.py +0 -468
- multi_lang_build/register.py +0 -412
- multi_lang_build-0.2.5.dist-info/RECORD +0 -18
- {multi_lang_build-0.2.5.dist-info → multi_lang_build-0.3.0.dist-info}/WHEEL +0 -0
- {multi_lang_build-0.2.5.dist-info → multi_lang_build-0.3.0.dist-info}/entry_points.txt +0 -0
- {multi_lang_build-0.2.5.dist-info → multi_lang_build-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Go build utilities."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Final
|
|
7
|
+
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
from multi_lang_build.compiler.base import BuildResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GoBuilder:
|
|
14
|
+
"""Build Go binaries with various options."""
|
|
15
|
+
|
|
16
|
+
DEFAULT_TARGETS: Final[list[str]] = ["linux/amd64", "linux/arm64", "darwin/amd64", "darwin/arm64"]
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def build(
|
|
20
|
+
go_executable: str,
|
|
21
|
+
source_dir: Path,
|
|
22
|
+
output_path: Path,
|
|
23
|
+
*,
|
|
24
|
+
ldflags: str | None = None,
|
|
25
|
+
tags: list[str] | None = None,
|
|
26
|
+
environment: dict[str, str],
|
|
27
|
+
stream_output: bool = True,
|
|
28
|
+
) -> BuildResult:
|
|
29
|
+
"""Build a Go binary.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
go_executable: Path to Go executable
|
|
33
|
+
source_dir: Source directory
|
|
34
|
+
output_path: Output path for binary
|
|
35
|
+
ldflags: Linker flags
|
|
36
|
+
tags: Build tags
|
|
37
|
+
environment: Environment variables
|
|
38
|
+
stream_output: Whether to stream output
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
BuildResult with success status
|
|
42
|
+
"""
|
|
43
|
+
build_args = [go_executable, "build", "-o", str(output_path)]
|
|
44
|
+
|
|
45
|
+
if ldflags:
|
|
46
|
+
build_args.extend(["-ldflags", ldflags])
|
|
47
|
+
|
|
48
|
+
if tags:
|
|
49
|
+
build_args.extend(["-tags", ",".join(tags)])
|
|
50
|
+
|
|
51
|
+
return GoBuilder._run_build(
|
|
52
|
+
build_args, source_dir, environment, stream_output
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def build_all(
|
|
57
|
+
go_executable: str,
|
|
58
|
+
source_dir: Path,
|
|
59
|
+
output_dir: Path,
|
|
60
|
+
*,
|
|
61
|
+
environment: dict[str, str],
|
|
62
|
+
stream_output: bool = True,
|
|
63
|
+
) -> BuildResult:
|
|
64
|
+
"""Build all packages in the module.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
go_executable: Path to Go executable
|
|
68
|
+
source_dir: Source directory
|
|
69
|
+
output_dir: Output directory for binaries
|
|
70
|
+
environment: Environment variables
|
|
71
|
+
stream_output: Whether to stream output
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
BuildResult with success status
|
|
75
|
+
"""
|
|
76
|
+
build_args = [go_executable, "build", "-o", str(output_dir), "./..."]
|
|
77
|
+
|
|
78
|
+
return GoBuilder._run_build(
|
|
79
|
+
build_args, source_dir, environment, stream_output
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def cross_compile(
|
|
84
|
+
go_executable: str,
|
|
85
|
+
source_dir: Path,
|
|
86
|
+
output_path: Path,
|
|
87
|
+
target: str,
|
|
88
|
+
*,
|
|
89
|
+
ldflags: str | None = None,
|
|
90
|
+
environment: dict[str, str],
|
|
91
|
+
stream_output: bool = True,
|
|
92
|
+
) -> BuildResult:
|
|
93
|
+
"""Cross-compile for a specific platform.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
go_executable: Path to Go executable
|
|
97
|
+
source_dir: Source directory
|
|
98
|
+
output_path: Output path for binary
|
|
99
|
+
target: Target platform (e.g., "linux/amd64")
|
|
100
|
+
ldflags: Linker flags
|
|
101
|
+
environment: Environment variables
|
|
102
|
+
stream_output: Whether to stream output
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
BuildResult with success status
|
|
106
|
+
"""
|
|
107
|
+
build_args = [
|
|
108
|
+
go_executable, "build",
|
|
109
|
+
"-o", str(output_path),
|
|
110
|
+
"-ldflags", f"{ldflags or ''} -s -w",
|
|
111
|
+
"-GOOS", target.split("/")[0],
|
|
112
|
+
"-GOARCH", target.split("/")[1],
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
return GoBuilder._run_build(
|
|
116
|
+
build_args, source_dir, environment, stream_output
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _run_build(
|
|
121
|
+
command: list[str],
|
|
122
|
+
source_dir: Path,
|
|
123
|
+
environment: dict[str, str],
|
|
124
|
+
stream_output: bool,
|
|
125
|
+
) -> BuildResult:
|
|
126
|
+
"""Execute build command."""
|
|
127
|
+
import time
|
|
128
|
+
|
|
129
|
+
start_time = time.perf_counter()
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
if stream_output:
|
|
133
|
+
return GoBuilder._stream_build(
|
|
134
|
+
command, source_dir, environment, start_time
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
return GoBuilder._capture_build(
|
|
138
|
+
command, source_dir, environment, start_time
|
|
139
|
+
)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
duration = time.perf_counter() - start_time
|
|
142
|
+
return BuildResult(
|
|
143
|
+
success=False,
|
|
144
|
+
return_code=-2,
|
|
145
|
+
stdout="",
|
|
146
|
+
stderr=f"Build error: {str(e)}",
|
|
147
|
+
output_path=None,
|
|
148
|
+
duration_seconds=duration,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _stream_build(
|
|
153
|
+
command: list[str],
|
|
154
|
+
source_dir: Path,
|
|
155
|
+
environment: dict[str, str],
|
|
156
|
+
start_time: float,
|
|
157
|
+
) -> BuildResult:
|
|
158
|
+
"""Execute build with streaming output."""
|
|
159
|
+
import os
|
|
160
|
+
import sys
|
|
161
|
+
|
|
162
|
+
stdout_buffer = []
|
|
163
|
+
stderr_buffer = []
|
|
164
|
+
|
|
165
|
+
process = subprocess.Popen(
|
|
166
|
+
command,
|
|
167
|
+
cwd=source_dir,
|
|
168
|
+
stdout=subprocess.PIPE,
|
|
169
|
+
stderr=subprocess.PIPE,
|
|
170
|
+
text=True,
|
|
171
|
+
env={**os.environ, **environment},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Read stdout
|
|
175
|
+
if process.stdout:
|
|
176
|
+
for line in process.stdout:
|
|
177
|
+
line = line.rstrip('\n\r')
|
|
178
|
+
stdout_buffer.append(line)
|
|
179
|
+
print(line)
|
|
180
|
+
sys.stdout.flush()
|
|
181
|
+
|
|
182
|
+
# Read stderr
|
|
183
|
+
if process.stderr:
|
|
184
|
+
for line in process.stderr:
|
|
185
|
+
line = line.rstrip('\n\r')
|
|
186
|
+
stderr_buffer.append(line)
|
|
187
|
+
print(line, file=sys.stderr)
|
|
188
|
+
sys.stderr.flush()
|
|
189
|
+
|
|
190
|
+
return_code = process.wait()
|
|
191
|
+
duration = time.perf_counter() - start_time
|
|
192
|
+
|
|
193
|
+
return BuildResult(
|
|
194
|
+
success=return_code == 0,
|
|
195
|
+
return_code=return_code,
|
|
196
|
+
stdout='\n'.join(stdout_buffer),
|
|
197
|
+
stderr='\n'.join(stderr_buffer),
|
|
198
|
+
output_path=source_dir if return_code == 0 else None,
|
|
199
|
+
duration_seconds=duration,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def _capture_build(
|
|
204
|
+
command: list[str],
|
|
205
|
+
source_dir: Path,
|
|
206
|
+
environment: dict[str, str],
|
|
207
|
+
start_time: float,
|
|
208
|
+
) -> BuildResult:
|
|
209
|
+
"""Execute build with captured output."""
|
|
210
|
+
result = subprocess.run(
|
|
211
|
+
command,
|
|
212
|
+
cwd=source_dir,
|
|
213
|
+
capture_output=True,
|
|
214
|
+
text=True,
|
|
215
|
+
timeout=3600,
|
|
216
|
+
env={**os.environ, **environment},
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
duration = time.perf_counter() - start_time
|
|
220
|
+
|
|
221
|
+
return BuildResult(
|
|
222
|
+
success=result.returncode == 0,
|
|
223
|
+
return_code=result.returncode,
|
|
224
|
+
stdout=result.stdout,
|
|
225
|
+
stderr=result.stderr,
|
|
226
|
+
output_path=source_dir if result.returncode == 0 else None,
|
|
227
|
+
duration_seconds=duration,
|
|
228
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Go build artifact cleaning utilities."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GoCleaner:
|
|
8
|
+
"""Clean Go build artifacts."""
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def clean(directory: Path) -> bool:
|
|
12
|
+
"""Clean Go build artifacts in the specified directory.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
directory: Directory to clean
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
True if successful, False otherwise.
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
# Remove go.sum
|
|
22
|
+
go_sum = directory / "go.sum"
|
|
23
|
+
if go_sum.exists():
|
|
24
|
+
go_sum.unlink()
|
|
25
|
+
|
|
26
|
+
# Remove vendor directory
|
|
27
|
+
vendor_dir = directory / "vendor"
|
|
28
|
+
if vendor_dir.exists() and vendor_dir.is_dir():
|
|
29
|
+
shutil.rmtree(vendor_dir)
|
|
30
|
+
|
|
31
|
+
# Remove build artifacts
|
|
32
|
+
for pattern in ["*.exe", "*.exe~", "*.test", "*.test~"]:
|
|
33
|
+
for file in directory.glob(pattern):
|
|
34
|
+
if file.is_file():
|
|
35
|
+
file.unlink()
|
|
36
|
+
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
except Exception:
|
|
40
|
+
return False
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Go environment preparation utilities."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GoEnvironment:
|
|
10
|
+
"""Prepare Go build environment with cache paths."""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def prepare(environment: dict[str, str] | None = None) -> dict[str, str]:
|
|
14
|
+
"""Prepare environment with GOMODCACHE and GOCACHE set.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
environment: Base environment variables
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Environment with cache paths set
|
|
21
|
+
"""
|
|
22
|
+
env = environment.copy() if environment else {}
|
|
23
|
+
|
|
24
|
+
# Set HOME if not present (needed for Path.home() and GOCACHE)
|
|
25
|
+
if "HOME" not in env:
|
|
26
|
+
home = os.environ.get("HOME") or "/root"
|
|
27
|
+
env["HOME"] = home
|
|
28
|
+
|
|
29
|
+
# Set GOMODCACHE if not present
|
|
30
|
+
if "GOMODCACHE" not in env and "GOPATH" not in env:
|
|
31
|
+
default_cache = Path(env["HOME"]) / "go" / "pkg" / "mod"
|
|
32
|
+
env["GOMODCACHE"] = str(default_cache)
|
|
33
|
+
logger.info(f"Go模块缓存: {default_cache}")
|
|
34
|
+
|
|
35
|
+
# Set GOCACHE if not present
|
|
36
|
+
if "GOCACHE" not in env:
|
|
37
|
+
cache_dir = Path(env["HOME"]) / ".cache" / "go-build"
|
|
38
|
+
env["GOCACHE"] = str(cache_dir)
|
|
39
|
+
logger.info(f"Go构建缓存: {cache_dir}")
|
|
40
|
+
|
|
41
|
+
return env
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Go mirror failover support."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from loguru import logger
|
|
5
|
+
|
|
6
|
+
from multi_lang_build.compiler.base import BuildResult
|
|
7
|
+
from multi_lang_build.mirror.config import (
|
|
8
|
+
MIRROR_CONFIGS,
|
|
9
|
+
get_go_mirror_with_fallback,
|
|
10
|
+
apply_mirror_environment,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GoMirrorFailover:
|
|
15
|
+
"""Handle Go mirror failover logic."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, configured_mirror: str | None = None):
|
|
18
|
+
"""Initialize mirror failover handler.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
configured_mirror: User-configured mirror key (if any).
|
|
22
|
+
If None, will use failover mode.
|
|
23
|
+
"""
|
|
24
|
+
self.configured_mirror = configured_mirror
|
|
25
|
+
|
|
26
|
+
def get_mirror_info(self, mirror_key: str) -> tuple[str, str]:
|
|
27
|
+
"""Get mirror name and URL.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
mirror_key: Mirror configuration key
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Tuple of (name, url)
|
|
34
|
+
"""
|
|
35
|
+
config = MIRROR_CONFIGS.get(mirror_key, {})
|
|
36
|
+
return config.get("name", mirror_key), config.get("url", "")
|
|
37
|
+
|
|
38
|
+
def apply_mirror(self, mirror_key: str, environment: dict[str, str]) -> dict[str, str]:
|
|
39
|
+
"""Apply mirror environment variables.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
mirror_key: Mirror configuration key
|
|
43
|
+
environment: Base environment variables
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Updated environment with mirror variables
|
|
47
|
+
"""
|
|
48
|
+
config_key = self.configured_mirror or mirror_key
|
|
49
|
+
return apply_mirror_environment(config_key, environment)
|
|
50
|
+
|
|
51
|
+
def try_build_with_fallback(
|
|
52
|
+
self,
|
|
53
|
+
command: list[str],
|
|
54
|
+
source_dir: Path,
|
|
55
|
+
output_dir: Path,
|
|
56
|
+
environment: dict[str, str],
|
|
57
|
+
stream_output: bool,
|
|
58
|
+
run_build_func,
|
|
59
|
+
) -> BuildResult:
|
|
60
|
+
"""Try build command with mirror failover.
|
|
61
|
+
|
|
62
|
+
Tries mirrors in order: goproxy.io → goproxy.cn → goproxy.vip.cn
|
|
63
|
+
Only switches to next mirror on failure.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
command: Build command to execute
|
|
67
|
+
source_dir: Source directory for the build
|
|
68
|
+
output_dir: Output directory for build artifacts
|
|
69
|
+
environment: Environment variables
|
|
70
|
+
stream_output: Whether to stream output in real-time
|
|
71
|
+
run_build_func: Function to execute the build
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
BuildResult with success status and output information.
|
|
75
|
+
"""
|
|
76
|
+
# If user configured a specific mirror, use it without failover
|
|
77
|
+
if self.configured_mirror:
|
|
78
|
+
mirror_name, _ = self.get_mirror_info(self.configured_mirror)
|
|
79
|
+
logger.info(f"🔗 使用代理: {mirror_name}")
|
|
80
|
+
env = self.apply_mirror(self.configured_mirror, environment)
|
|
81
|
+
return run_build_func(command, source_dir, output_dir, env, stream_output)
|
|
82
|
+
|
|
83
|
+
# Use failover mode
|
|
84
|
+
mirror_keys = get_go_mirror_with_fallback(failover=True)
|
|
85
|
+
last_result: BuildResult | None = None
|
|
86
|
+
|
|
87
|
+
for i, mirror_key in enumerate(mirror_keys):
|
|
88
|
+
env = environment.copy()
|
|
89
|
+
mirror_name, mirror_url = self.get_mirror_info(mirror_key)
|
|
90
|
+
logger.info(f"🔗 使用代理: {mirror_name} ({mirror_url})")
|
|
91
|
+
|
|
92
|
+
env = self.apply_mirror(mirror_key, env)
|
|
93
|
+
|
|
94
|
+
result = run_build_func(command, source_dir, output_dir, env, stream_output)
|
|
95
|
+
last_result = result
|
|
96
|
+
|
|
97
|
+
if result["success"]:
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
# Failed, try next mirror
|
|
101
|
+
if i < len(mirror_keys) - 1:
|
|
102
|
+
logger.warning(f"❌ {mirror_name} 失败,切换到备用镜像...")
|
|
103
|
+
else:
|
|
104
|
+
logger.error(f"❌ 所有镜像均失败")
|
|
105
|
+
|
|
106
|
+
assert last_result is not None
|
|
107
|
+
return last_result
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Also export fallback order constant for direct access
|
|
111
|
+
GO_MIRROR_FALLBACK_ORDER = ["go_qiniu", "go", "go_vip"]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Go module building utilities."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from multi_lang_build.compiler.base import BuildResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GoModuleBuilder:
|
|
11
|
+
"""Build all packages in a Go module."""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def build_all(
|
|
15
|
+
go_executable: str,
|
|
16
|
+
source_dir: Path,
|
|
17
|
+
output_dir: Path,
|
|
18
|
+
env: dict[str, str],
|
|
19
|
+
platform: str | None,
|
|
20
|
+
run_build,
|
|
21
|
+
stream_output: bool = True,
|
|
22
|
+
) -> BuildResult:
|
|
23
|
+
"""Build all packages in the module.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
go_executable: Path to Go executable
|
|
27
|
+
source_dir: Source directory
|
|
28
|
+
output_dir: Output directory for binaries
|
|
29
|
+
env: Environment variables
|
|
30
|
+
platform: Target platform (e.g., "linux/amd64")
|
|
31
|
+
run_build: Callback to execute build
|
|
32
|
+
stream_output: Whether to stream output
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
BuildResult with success status
|
|
36
|
+
"""
|
|
37
|
+
build_args = [go_executable, "build", "-o", str(output_dir), "./..."]
|
|
38
|
+
|
|
39
|
+
env_copy = env.copy()
|
|
40
|
+
if platform:
|
|
41
|
+
env_copy["GOOS"], env_copy["GOARCH"] = platform.split("/")
|
|
42
|
+
|
|
43
|
+
logger.info("🔨 开始构建...")
|
|
44
|
+
logger.info(f"构建命令: {' '.join(build_args)}")
|
|
45
|
+
build_result = run_build(build_args, source_dir, output_dir, env_copy)
|
|
46
|
+
|
|
47
|
+
if build_result["success"]:
|
|
48
|
+
logger.info("✅ 构建成功")
|
|
49
|
+
|
|
50
|
+
return build_result
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def tidy(
|
|
54
|
+
go_executable: str,
|
|
55
|
+
source_dir: Path,
|
|
56
|
+
env: dict[str, str],
|
|
57
|
+
run_build,
|
|
58
|
+
stream_output: bool = True,
|
|
59
|
+
) -> BuildResult:
|
|
60
|
+
"""Run go mod tidy to clean up dependencies.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
go_executable: Path to Go executable
|
|
64
|
+
source_dir: Source directory
|
|
65
|
+
env: Environment variables
|
|
66
|
+
run_build: Callback to execute build
|
|
67
|
+
stream_output: Whether to stream output
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
BuildResult with success status
|
|
71
|
+
"""
|
|
72
|
+
tidy_cmd = [go_executable, "mod", "tidy"]
|
|
73
|
+
logger.info("📦 执行 go mod tidy...")
|
|
74
|
+
logger.info(f"构建命令: {' '.join(tidy_cmd)}")
|
|
75
|
+
tidy_result = run_build(tidy_cmd, source_dir, source_dir, env)
|
|
76
|
+
|
|
77
|
+
if tidy_result["success"]:
|
|
78
|
+
logger.info("✅ 依赖整理完成")
|
|
79
|
+
|
|
80
|
+
return tidy_result
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Go test utilities."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
from multi_lang_build.compiler.base import BuildResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GoTester:
|
|
16
|
+
"""Run Go tests."""
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def run(
|
|
20
|
+
go_executable: str,
|
|
21
|
+
source_dir: Path,
|
|
22
|
+
env: dict[str, str],
|
|
23
|
+
verbose: bool = True,
|
|
24
|
+
race: bool = False,
|
|
25
|
+
stream_output: bool = True,
|
|
26
|
+
run_build: Callable | None = None,
|
|
27
|
+
) -> BuildResult:
|
|
28
|
+
"""Run Go tests.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
go_executable: Path to Go executable
|
|
32
|
+
source_dir: Source directory
|
|
33
|
+
env: Environment variables
|
|
34
|
+
verbose: Enable verbose test output
|
|
35
|
+
race: Enable race detector
|
|
36
|
+
stream_output: Whether to stream output
|
|
37
|
+
run_build: Optional custom build runner
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
BuildResult with success status
|
|
41
|
+
"""
|
|
42
|
+
test_args = [go_executable, "test"]
|
|
43
|
+
|
|
44
|
+
if verbose:
|
|
45
|
+
test_args.append("-v")
|
|
46
|
+
if race:
|
|
47
|
+
test_args.append("-race")
|
|
48
|
+
|
|
49
|
+
test_args.append("./...")
|
|
50
|
+
|
|
51
|
+
logger.info("🧪 开始运行测试...")
|
|
52
|
+
logger.info(f"构建命令: {' '.join(test_args)}")
|
|
53
|
+
|
|
54
|
+
if run_build:
|
|
55
|
+
result = run_build(test_args, source_dir, source_dir, env)
|
|
56
|
+
else:
|
|
57
|
+
result = GoTester._run_test(test_args, source_dir, env, stream_output)
|
|
58
|
+
|
|
59
|
+
if result["success"]:
|
|
60
|
+
logger.info("✅ 测试完成")
|
|
61
|
+
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def run_with_coverage(
|
|
66
|
+
go_executable: str,
|
|
67
|
+
source_dir: Path,
|
|
68
|
+
output_file: Path,
|
|
69
|
+
env: dict[str, str],
|
|
70
|
+
stream_output: bool = True,
|
|
71
|
+
) -> BuildResult:
|
|
72
|
+
"""Run tests with coverage.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
go_executable: Path to Go executable
|
|
76
|
+
source_dir: Source directory
|
|
77
|
+
output_file: Output file for coverage report
|
|
78
|
+
env: Environment variables
|
|
79
|
+
stream_output: Whether to stream output
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
BuildResult with success status
|
|
83
|
+
"""
|
|
84
|
+
test_args = [
|
|
85
|
+
go_executable, "test",
|
|
86
|
+
"-v",
|
|
87
|
+
"-coverprofile", str(output_file),
|
|
88
|
+
"./...",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
return GoTester._run_test(test_args, source_dir, env, stream_output)
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _run_test(
|
|
95
|
+
command: list[str],
|
|
96
|
+
source_dir: Path,
|
|
97
|
+
environment: dict[str, str],
|
|
98
|
+
stream_output: bool,
|
|
99
|
+
) -> BuildResult:
|
|
100
|
+
"""Execute test command."""
|
|
101
|
+
start_time = time.perf_counter()
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
if stream_output:
|
|
105
|
+
return GoTester._stream_test(
|
|
106
|
+
command, source_dir, environment, start_time
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
return GoTester._capture_test(
|
|
110
|
+
command, source_dir, environment, start_time
|
|
111
|
+
)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
duration = time.perf_counter() - start_time
|
|
114
|
+
return BuildResult(
|
|
115
|
+
success=False,
|
|
116
|
+
return_code=-2,
|
|
117
|
+
stdout="",
|
|
118
|
+
stderr=f"Test error: {str(e)}",
|
|
119
|
+
output_path=None,
|
|
120
|
+
duration_seconds=duration,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _stream_test(
|
|
125
|
+
command: list[str],
|
|
126
|
+
source_dir: Path,
|
|
127
|
+
environment: dict[str, str],
|
|
128
|
+
start_time: float,
|
|
129
|
+
) -> BuildResult:
|
|
130
|
+
"""Execute tests with streaming output."""
|
|
131
|
+
import os
|
|
132
|
+
import sys
|
|
133
|
+
|
|
134
|
+
stdout_buffer = []
|
|
135
|
+
stderr_buffer = []
|
|
136
|
+
|
|
137
|
+
process = subprocess.Popen(
|
|
138
|
+
command,
|
|
139
|
+
cwd=source_dir,
|
|
140
|
+
stdout=subprocess.PIPE,
|
|
141
|
+
stderr=subprocess.PIPE,
|
|
142
|
+
text=True,
|
|
143
|
+
env={**os.environ, **environment},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Read stdout
|
|
147
|
+
if process.stdout:
|
|
148
|
+
for line in process.stdout:
|
|
149
|
+
line = line.rstrip('\n\r')
|
|
150
|
+
stdout_buffer.append(line)
|
|
151
|
+
print(line)
|
|
152
|
+
sys.stdout.flush()
|
|
153
|
+
|
|
154
|
+
# Read stderr
|
|
155
|
+
if process.stderr:
|
|
156
|
+
for line in process.stderr:
|
|
157
|
+
line = line.rstrip('\n\r')
|
|
158
|
+
stderr_buffer.append(line)
|
|
159
|
+
print(line, file=sys.stderr)
|
|
160
|
+
sys.stderr.flush()
|
|
161
|
+
|
|
162
|
+
return_code = process.wait()
|
|
163
|
+
duration = time.perf_counter() - start_time
|
|
164
|
+
|
|
165
|
+
return BuildResult(
|
|
166
|
+
success=return_code == 0,
|
|
167
|
+
return_code=return_code,
|
|
168
|
+
stdout='\n'.join(stdout_buffer),
|
|
169
|
+
stderr='\n'.join(stderr_buffer),
|
|
170
|
+
output_path=source_dir,
|
|
171
|
+
duration_seconds=duration,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def _capture_test(
|
|
176
|
+
command: list[str],
|
|
177
|
+
source_dir: Path,
|
|
178
|
+
environment: dict[str, str],
|
|
179
|
+
start_time: float,
|
|
180
|
+
) -> BuildResult:
|
|
181
|
+
"""Execute tests with captured output."""
|
|
182
|
+
result = subprocess.run(
|
|
183
|
+
command,
|
|
184
|
+
cwd=source_dir,
|
|
185
|
+
capture_output=True,
|
|
186
|
+
text=True,
|
|
187
|
+
timeout=3600,
|
|
188
|
+
env={**os.environ, **environment},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
duration = time.perf_counter() - start_time
|
|
192
|
+
|
|
193
|
+
return BuildResult(
|
|
194
|
+
success=result.returncode == 0,
|
|
195
|
+
return_code=result.returncode,
|
|
196
|
+
stdout=result.stdout,
|
|
197
|
+
stderr=result.stderr,
|
|
198
|
+
output_path=source_dir,
|
|
199
|
+
duration_seconds=duration,
|
|
200
|
+
)
|