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.
Files changed (38) hide show
  1. multi_lang_build/__init__.py +4 -4
  2. multi_lang_build/cli.py +3 -2
  3. multi_lang_build/compiler/__init__.py +1 -1
  4. multi_lang_build/compiler/go_compiler.py +388 -0
  5. multi_lang_build/compiler/go_support/__init__.py +19 -0
  6. multi_lang_build/compiler/go_support/binary.py +117 -0
  7. multi_lang_build/compiler/go_support/builder.py +228 -0
  8. multi_lang_build/compiler/go_support/cleaner.py +40 -0
  9. multi_lang_build/compiler/go_support/env.py +41 -0
  10. multi_lang_build/compiler/go_support/mirror.py +111 -0
  11. multi_lang_build/compiler/go_support/module.py +77 -0
  12. multi_lang_build/compiler/go_support/tester.py +199 -0
  13. multi_lang_build/compiler/pnpm.py +61 -209
  14. multi_lang_build/compiler/pnpm_support/__init__.py +6 -0
  15. multi_lang_build/compiler/pnpm_support/executor.py +148 -0
  16. multi_lang_build/compiler/pnpm_support/project.py +53 -0
  17. multi_lang_build/compiler/python.py +65 -222
  18. multi_lang_build/compiler/python_support/__init__.py +13 -0
  19. multi_lang_build/compiler/python_support/cleaner.py +56 -0
  20. multi_lang_build/compiler/python_support/cli.py +46 -0
  21. multi_lang_build/compiler/python_support/detector.py +64 -0
  22. multi_lang_build/compiler/python_support/installer.py +162 -0
  23. multi_lang_build/compiler/python_support/venv.py +63 -0
  24. multi_lang_build/ide_register.py +97 -0
  25. multi_lang_build/mirror/config.py +19 -0
  26. multi_lang_build/register/support/__init__.py +13 -0
  27. multi_lang_build/register/support/claude.py +94 -0
  28. multi_lang_build/register/support/codebuddy.py +100 -0
  29. multi_lang_build/register/support/opencode.py +62 -0
  30. multi_lang_build/register/support/trae.py +72 -0
  31. {multi_lang_build-0.2.8.dist-info → multi_lang_build-0.2.10.dist-info}/METADATA +1 -1
  32. multi_lang_build-0.2.10.dist-info/RECORD +40 -0
  33. multi_lang_build/compiler/go.py +0 -564
  34. multi_lang_build/register.py +0 -412
  35. multi_lang_build-0.2.8.dist-info/RECORD +0 -18
  36. {multi_lang_build-0.2.8.dist-info → multi_lang_build-0.2.10.dist-info}/WHEEL +0 -0
  37. {multi_lang_build-0.2.8.dist-info → multi_lang_build-0.2.10.dist-info}/entry_points.txt +0 -0
  38. {multi_lang_build-0.2.8.dist-info → multi_lang_build-0.2.10.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,77 @@
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
+ build_result = run_build(build_args, source_dir, output_dir, env_copy)
45
+
46
+ if build_result["success"]:
47
+ logger.info("✅ 构建成功")
48
+
49
+ return build_result
50
+
51
+ @staticmethod
52
+ def tidy(
53
+ go_executable: str,
54
+ source_dir: Path,
55
+ env: dict[str, str],
56
+ run_build,
57
+ stream_output: bool = True,
58
+ ) -> BuildResult:
59
+ """Run go mod tidy to clean up dependencies.
60
+
61
+ Args:
62
+ go_executable: Path to Go executable
63
+ source_dir: Source directory
64
+ env: Environment variables
65
+ run_build: Callback to execute build
66
+ stream_output: Whether to stream output
67
+
68
+ Returns:
69
+ BuildResult with success status
70
+ """
71
+ logger.info("📦 执行 go mod tidy...")
72
+ tidy_result = run_build([go_executable, "mod", "tidy"], source_dir, source_dir, env)
73
+
74
+ if tidy_result["success"]:
75
+ logger.info("✅ 依赖整理完成")
76
+
77
+ return tidy_result
@@ -0,0 +1,199 @@
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
+
53
+ if run_build:
54
+ result = run_build(test_args, source_dir, source_dir, env)
55
+ else:
56
+ result = GoTester._run_test(test_args, source_dir, env, stream_output)
57
+
58
+ if result["success"]:
59
+ logger.info("✅ 测试完成")
60
+
61
+ return result
62
+
63
+ @staticmethod
64
+ def run_with_coverage(
65
+ go_executable: str,
66
+ source_dir: Path,
67
+ output_file: Path,
68
+ env: dict[str, str],
69
+ stream_output: bool = True,
70
+ ) -> BuildResult:
71
+ """Run tests with coverage.
72
+
73
+ Args:
74
+ go_executable: Path to Go executable
75
+ source_dir: Source directory
76
+ output_file: Output file for coverage report
77
+ env: Environment variables
78
+ stream_output: Whether to stream output
79
+
80
+ Returns:
81
+ BuildResult with success status
82
+ """
83
+ test_args = [
84
+ go_executable, "test",
85
+ "-v",
86
+ "-coverprofile", str(output_file),
87
+ "./...",
88
+ ]
89
+
90
+ return GoTester._run_test(test_args, source_dir, env, stream_output)
91
+
92
+ @staticmethod
93
+ def _run_test(
94
+ command: list[str],
95
+ source_dir: Path,
96
+ environment: dict[str, str],
97
+ stream_output: bool,
98
+ ) -> BuildResult:
99
+ """Execute test command."""
100
+ start_time = time.perf_counter()
101
+
102
+ try:
103
+ if stream_output:
104
+ return GoTester._stream_test(
105
+ command, source_dir, environment, start_time
106
+ )
107
+ else:
108
+ return GoTester._capture_test(
109
+ command, source_dir, environment, start_time
110
+ )
111
+ except Exception as e:
112
+ duration = time.perf_counter() - start_time
113
+ return BuildResult(
114
+ success=False,
115
+ return_code=-2,
116
+ stdout="",
117
+ stderr=f"Test error: {str(e)}",
118
+ output_path=None,
119
+ duration_seconds=duration,
120
+ )
121
+
122
+ @staticmethod
123
+ def _stream_test(
124
+ command: list[str],
125
+ source_dir: Path,
126
+ environment: dict[str, str],
127
+ start_time: float,
128
+ ) -> BuildResult:
129
+ """Execute tests with streaming output."""
130
+ import os
131
+ import sys
132
+
133
+ stdout_buffer = []
134
+ stderr_buffer = []
135
+
136
+ process = subprocess.Popen(
137
+ command,
138
+ cwd=source_dir,
139
+ stdout=subprocess.PIPE,
140
+ stderr=subprocess.PIPE,
141
+ text=True,
142
+ env={**os.environ, **environment},
143
+ )
144
+
145
+ # Read stdout
146
+ if process.stdout:
147
+ for line in process.stdout:
148
+ line = line.rstrip('\n\r')
149
+ stdout_buffer.append(line)
150
+ print(line)
151
+ sys.stdout.flush()
152
+
153
+ # Read stderr
154
+ if process.stderr:
155
+ for line in process.stderr:
156
+ line = line.rstrip('\n\r')
157
+ stderr_buffer.append(line)
158
+ print(line, file=sys.stderr)
159
+ sys.stderr.flush()
160
+
161
+ return_code = process.wait()
162
+ duration = time.perf_counter() - start_time
163
+
164
+ return BuildResult(
165
+ success=return_code == 0,
166
+ return_code=return_code,
167
+ stdout='\n'.join(stdout_buffer),
168
+ stderr='\n'.join(stderr_buffer),
169
+ output_path=source_dir,
170
+ duration_seconds=duration,
171
+ )
172
+
173
+ @staticmethod
174
+ def _capture_test(
175
+ command: list[str],
176
+ source_dir: Path,
177
+ environment: dict[str, str],
178
+ start_time: float,
179
+ ) -> BuildResult:
180
+ """Execute tests with captured output."""
181
+ result = subprocess.run(
182
+ command,
183
+ cwd=source_dir,
184
+ capture_output=True,
185
+ text=True,
186
+ timeout=3600,
187
+ env={**os.environ, **environment},
188
+ )
189
+
190
+ duration = time.perf_counter() - start_time
191
+
192
+ return BuildResult(
193
+ success=result.returncode == 0,
194
+ return_code=result.returncode,
195
+ stdout=result.stdout,
196
+ stderr=result.stderr,
197
+ output_path=source_dir,
198
+ duration_seconds=duration,
199
+ )