piebuild 3.1.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.
Files changed (45) hide show
  1. piebuild/__init__.py +14 -0
  2. piebuild/__main__.py +12 -0
  3. piebuild/artifact.py +172 -0
  4. piebuild/attestation.py +88 -0
  5. piebuild/backends/__init__.py +10 -0
  6. piebuild/backends/docker_backend.py +385 -0
  7. piebuild/backends/macos_backend.py +34 -0
  8. piebuild/backends/native.py +80 -0
  9. piebuild/backends/remote_backend.py +35 -0
  10. piebuild/checksums.py +115 -0
  11. piebuild/cli/__init__.py +9 -0
  12. piebuild/cli/main.py +869 -0
  13. piebuild/config.py +323 -0
  14. piebuild/context.py +246 -0
  15. piebuild/core/__init__.py +230 -0
  16. piebuild/core/bundle.py +723 -0
  17. piebuild/core/cache.py +161 -0
  18. piebuild/formats/__init__.py +144 -0
  19. piebuild/formats/app.py +188 -0
  20. piebuild/formats/appimage.py +445 -0
  21. piebuild/formats/exe.py +567 -0
  22. piebuild/formats/pie.py +34 -0
  23. piebuild/gui/__init__.py +10 -0
  24. piebuild/gui/builder.py +38 -0
  25. piebuild/gui/main.py +588 -0
  26. piebuild/ingredients/__init__.py +125 -0
  27. piebuild/launchers.py +206 -0
  28. piebuild/native.py +323 -0
  29. piebuild/recipes/__init__.py +166 -0
  30. piebuild/recipes/builtin/console-app.toml +11 -0
  31. piebuild/recipes/builtin/gui-app.toml +11 -0
  32. piebuild/release.py +92 -0
  33. piebuild/runtime/__init__.py +95 -0
  34. piebuild/runtime/cache.py +125 -0
  35. piebuild/runtime/downloaded.py +502 -0
  36. piebuild/runtime/provider.py +199 -0
  37. piebuild/runtime/system.py +262 -0
  38. piebuild/signing/__init__.py +78 -0
  39. piebuild/signing/local.py +265 -0
  40. piebuild/validation.py +598 -0
  41. piebuild-3.1.0.dist-info/METADATA +425 -0
  42. piebuild-3.1.0.dist-info/RECORD +45 -0
  43. piebuild-3.1.0.dist-info/WHEEL +4 -0
  44. piebuild-3.1.0.dist-info/entry_points.txt +3 -0
  45. piebuild-3.1.0.dist-info/licenses/LICENSE +36 -0
piebuild/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """PieBuild: Modern Python application build platform."""
2
+
3
+ __version__ = "3.1.0"
4
+ __author__ = "ThatByteGuy"
5
+ __email__ = "12aa44edsta@gmail.com"
6
+
7
+ from .cli.main import main
8
+ from .core import BuildContext, BuildEngine
9
+
10
+ __all__ = [
11
+ "BuildEngine",
12
+ "BuildContext",
13
+ "main",
14
+ ]
piebuild/__main__.py ADDED
@@ -0,0 +1,12 @@
1
+ """PieBuild CLI main entry point."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ # Add the current directory to Python path so we can import piebuild
7
+ sys.path.insert(0, str(Path(__file__).parent))
8
+
9
+ from piebuild.cli.main import main
10
+
11
+ if __name__ == "__main__":
12
+ sys.exit(main())
piebuild/artifact.py ADDED
@@ -0,0 +1,172 @@
1
+ """Release artifact model for PieBuild distribution.
2
+
3
+ Reusable, frozen dataclass-style models describing generated artifacts.
4
+ Usable by CLI, GUI, CI, validation, signing without tight CLI coupling.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from . import __version__ as piebuild_version
15
+
16
+
17
+ def _sha256_file(path: Path) -> str:
18
+ h = hashlib.sha256()
19
+ with open(path, "rb") as f:
20
+ for chunk in iter(lambda: f.read(8192), b""):
21
+ h.update(chunk)
22
+ return h.hexdigest()
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Artifact:
27
+ """Describes a single distributable artifact."""
28
+
29
+ path: Path
30
+ format: str # pie | appimage | exe
31
+ target_os: str
32
+ target_arch: str
33
+ app_name: str
34
+ app_version: str
35
+ piebuild_version: str
36
+ runtime_info: dict[str, Any] = field(default_factory=dict)
37
+ backend: str = "native"
38
+ build_timestamp: str = "" # ISO8601 UTC, e.g. 2026-08-29T...
39
+ source_checksum: str = "" # sha256 of original source file
40
+ artifact_checksum: str = "" # sha256 of final artifact bytes
41
+ provenance: dict[str, Any] = field(default_factory=dict)
42
+ format_version: str = ""
43
+ toolchain: str = ""
44
+ signing_state: str = "unsigned" # unsigned | signed
45
+ signature_info: dict[str, Any] | None = None
46
+
47
+ def to_dict(self) -> dict[str, Any]:
48
+ return {
49
+ "path": str(self.path),
50
+ "format": self.format,
51
+ "target_os": self.target_os,
52
+ "target_arch": self.target_arch,
53
+ "app_name": self.app_name,
54
+ "app_version": self.app_version,
55
+ "piebuild_version": self.piebuild_version,
56
+ "runtime": self.runtime_info,
57
+ "backend": self.backend,
58
+ "build_timestamp": self.build_timestamp,
59
+ "source_checksum": self.source_checksum,
60
+ "artifact_checksum": self.artifact_checksum,
61
+ "provenance": dict(self.provenance),
62
+ "format_version": self.format_version,
63
+ "toolchain": self.toolchain,
64
+ "signing_state": self.signing_state,
65
+ "signature": self.signature_info,
66
+ }
67
+
68
+ @classmethod
69
+ def from_build(
70
+ cls,
71
+ artifact_path: Path,
72
+ build_config: Any,
73
+ build_result: Any,
74
+ provenance: dict[str, Any] | None = None,
75
+ ) -> Artifact:
76
+ """Construct from BuildConfig + BuildResult + optional provenance."""
77
+ path = Path(artifact_path)
78
+ cfg = build_config
79
+ meta = getattr(build_result, "metadata", {}) or {}
80
+
81
+ fmt = (getattr(cfg, "output_format", None) or "pie").lower().strip()
82
+ # FormatRegistry normalizes: pie is canonical
83
+ if not path.suffix:
84
+ # fallback
85
+ fmt = meta.get("format", fmt)
86
+
87
+ target_os = getattr(cfg, "target_os", "native")
88
+ target_arch = getattr(cfg, "target_arch", "native")
89
+ app_name = getattr(cfg, "app_name", path.stem)
90
+ app_ver = getattr(cfg, "version", "1.0.0")
91
+ backend = getattr(cfg, "backend", "native") or "native"
92
+
93
+ # source checksum: prefer provenance/manifest, else compute if possible
94
+ source_checksum = ""
95
+ prov = provenance or meta.get("provenance", {}) or {}
96
+ if isinstance(prov, dict):
97
+ source_checksum = prov.get("source_checksum", "") or meta.get("source_checksum", "")
98
+ if not source_checksum:
99
+ src = getattr(cfg, "source_file", None)
100
+ if src and Path(src).exists():
101
+ try:
102
+ source_checksum = _sha256_file(Path(src))
103
+ except OSError:
104
+ pass
105
+
106
+ artifact_checksum = ""
107
+ if path.exists() and path.is_file():
108
+ try:
109
+ artifact_checksum = _sha256_file(path)
110
+ except OSError:
111
+ pass
112
+ elif path.exists() and path.is_dir():
113
+ # AppDir: hash manifest + provenance deterministically
114
+ try:
115
+
116
+ h = hashlib.sha256()
117
+ for p in sorted(path.rglob("*")):
118
+ if p.is_file():
119
+ rel = p.relative_to(path).as_posix()
120
+ h.update(rel.encode())
121
+ h.update(p.read_bytes())
122
+ artifact_checksum = h.hexdigest()
123
+ except OSError:
124
+ pass
125
+
126
+ runtime_info = {}
127
+ if isinstance(prov, dict) and "runtime" in prov:
128
+ runtime_info = prov.get("runtime", {})
129
+ elif "runtime" in meta:
130
+ runtime_info = meta.get("runtime", {})
131
+
132
+ fmt_version = ""
133
+ if isinstance(prov, dict):
134
+ fmt_version = prov.get("format_version", "") or meta.get("format_version", "")
135
+
136
+ toolchain = ""
137
+ if isinstance(prov, dict):
138
+ toolchain = prov.get("toolchain", "") or prov.get("backend", "") or backend
139
+
140
+ signing_state = "unsigned"
141
+ sig_info = None
142
+ if isinstance(prov, dict) and "signature" in prov:
143
+ sig_info = prov.get("signature")
144
+ signing_state = "signed" if sig_info else "unsigned"
145
+
146
+ ts = ""
147
+ if isinstance(prov, dict):
148
+ ts = prov.get("built_at", "") or prov.get("build_timestamp", "")
149
+
150
+ return cls(
151
+ path=path,
152
+ format=fmt,
153
+ target_os=target_os,
154
+ target_arch=target_arch,
155
+ app_name=app_name,
156
+ app_version=app_ver,
157
+ piebuild_version=piebuild_version,
158
+ runtime_info=runtime_info if isinstance(runtime_info, dict) else {},
159
+ backend=backend,
160
+ build_timestamp=ts,
161
+ source_checksum=source_checksum,
162
+ artifact_checksum=artifact_checksum,
163
+ provenance=dict(prov) if isinstance(prov, dict) else {},
164
+ format_version=fmt_version,
165
+ toolchain=toolchain,
166
+ signing_state=signing_state,
167
+ signature_info=sig_info,
168
+ )
169
+
170
+ @property
171
+ def is_signed(self) -> bool:
172
+ return self.signing_state == "signed"
@@ -0,0 +1,88 @@
1
+ """Build / release attestation - metadata about how artifact was produced."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import platform
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+ from . import __version__ as piebuild_version
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class Attestation:
14
+ piebuild_version: str
15
+ source_checksum: str
16
+ pie_checksum: str
17
+ target_os: str
18
+ target_arch: str
19
+ backend: str
20
+ runtime_provider: str
21
+ runtime_identity: str
22
+ format: str
23
+ format_version: str
24
+ toolchain: str
25
+ signing_provider: str | None = None
26
+ signing_key_id: str | None = None
27
+ build_env: dict[str, Any] = field(default_factory=dict)
28
+
29
+ def to_dict(self) -> dict[str, Any]:
30
+ d: dict[str, Any] = {
31
+ "piebuild_version": self.piebuild_version,
32
+ "source_checksum": self.source_checksum,
33
+ "pie_checksum": self.pie_checksum,
34
+ "target": f"{self.target_os}/{self.target_arch}",
35
+ "target_os": self.target_os,
36
+ "target_arch": self.target_arch,
37
+ "backend": self.backend,
38
+ "runtime_provider": self.runtime_provider,
39
+ "runtime_identity": self.runtime_identity,
40
+ "format": self.format,
41
+ "format_version": self.format_version,
42
+ "toolchain": self.toolchain,
43
+ }
44
+ if self.signing_provider:
45
+ d["signing_provider"] = self.signing_provider
46
+ if self.signing_key_id:
47
+ d["signing_key_id"] = self.signing_key_id
48
+ # only safe build env info, no secrets/paths
49
+ d["build_env"] = dict(self.build_env)
50
+ return d
51
+
52
+
53
+ def build_attestation(
54
+ source_checksum: str = "",
55
+ pie_checksum: str = "",
56
+ target_os: str = "native",
57
+ target_arch: str = "native",
58
+ backend: str = "native",
59
+ runtime_provider: str = "system",
60
+ runtime_identity: str = "",
61
+ fmt: str = "pie",
62
+ fmt_version: str = "",
63
+ toolchain: str = "",
64
+ signing_provider: str | None = None,
65
+ signing_key_id: str | None = None,
66
+ ) -> Attestation:
67
+ # build_env: only safe, non-sensitive info
68
+ env = {
69
+ "python": platform.python_version(),
70
+ "platform": platform.platform(),
71
+ "machine": platform.machine(),
72
+ }
73
+ return Attestation(
74
+ piebuild_version=piebuild_version,
75
+ source_checksum=source_checksum,
76
+ pie_checksum=pie_checksum,
77
+ target_os=target_os,
78
+ target_arch=target_arch,
79
+ backend=backend,
80
+ runtime_provider=runtime_provider,
81
+ runtime_identity=runtime_identity,
82
+ format=fmt,
83
+ format_version=fmt_version,
84
+ toolchain=toolchain,
85
+ signing_provider=signing_provider,
86
+ signing_key_id=signing_key_id,
87
+ build_env=env,
88
+ )
@@ -0,0 +1,10 @@
1
+ """Build backends for PieBuild.
2
+
3
+ The production backend lives in :mod:`piebuild.backends.native`; this module
4
+ re-exports it so ``from piebuild.backends import NativeBackend`` keeps working.
5
+ """
6
+
7
+ from .docker_backend import DockerBackend
8
+ from .native import NativeBackend
9
+
10
+ __all__ = ["NativeBackend", "DockerBackend"]
@@ -0,0 +1,385 @@
1
+ """Docker isolated build backend."""
2
+
3
+ import logging
4
+ import subprocess
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from ..config import BuildConfig
9
+ from ..context import (
10
+ BuildContext,
11
+ BuildEnvironment,
12
+ Target,
13
+ host_arch,
14
+ host_os,
15
+ resolve_target,
16
+ )
17
+ from ..core import BuildBackend, BuildResult, BuildStepError
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def _docker_available() -> bool:
23
+ """Check if Docker is available and the daemon is running."""
24
+ try:
25
+ result = subprocess.run(
26
+ ["docker", "info"],
27
+ capture_output=True,
28
+ text=True,
29
+ timeout=10,
30
+ )
31
+ return result.returncode == 0
32
+ except (FileNotFoundError, subprocess.TimeoutExpired):
33
+ return False
34
+
35
+
36
+ def _docker_image_supports_arch(image: str, arch: str) -> bool:
37
+ """Check if a Docker image supports a given architecture."""
38
+ # Placeholder: in a real implementation, this would check the image manifest
39
+ return True
40
+
41
+
42
+ def _resolve_docker_image(target: Target) -> str:
43
+ """Resolve a target to an appropriate Docker image name.
44
+
45
+ The initial implementation supports a small set of targets.
46
+ Adding new targets consists of extending this resolver.
47
+ """
48
+ os_name = target.os
49
+ arch = target.arch
50
+
51
+ # Map target to Docker image tags
52
+ image_tags = {
53
+ ("linux", "x86_64"): "python:3.14-slim-bullseye-amd64",
54
+ ("linux", "aarch64"): "python:3.14-slim-bullseye-arm64",
55
+ }
56
+
57
+ key = (os_name, arch)
58
+ if key in image_tags:
59
+ return image_tags[key]
60
+
61
+ # Fallback: try python:3.14-slim with the arch suffix
62
+ if os_name == "linux":
63
+ return f"python:3.14-slim-{arch}"
64
+
65
+ return "python:3.14-slim"
66
+
67
+
68
+ class DockerBackend(BuildBackend):
69
+ """Docker isolated build backend.
70
+
71
+ Executes builds inside a Docker container, isolating the build
72
+ environment from the host Python installation and system libraries.
73
+
74
+ The intended flow is:
75
+
76
+ PieBuild
77
+ ├─ BuildEngine
78
+ │ └─ DockerBackend
79
+ │ ├─ Docker build environment
80
+ │ └─ PieBuild build
81
+ │ ├─ artifact
82
+ │ └─ .pie bundle
83
+
84
+ The Docker backend is isolated from the host Python environment.
85
+ It only receives the necessary build context (source, configuration,
86
+ recipes, dependency information, runtime requirements). Unrelated host
87
+ files are not mounted into the container.
88
+
89
+ Target images are resolved through a target-image resolver that maps
90
+ target identifiers to appropriate Docker images. The initial
91
+ implementation supports a small set of targets; adding new targets
92
+ consists of extending the resolver without rewriting the backend.
93
+
94
+ Native dependencies are built against the target environment rather
95
+ than the host. For example, a Linux host building for a target Linux
96
+ ARM64 environment will compile native extensions inside the container
97
+ for ARM64, producing an artifact whose native profile matches the
98
+ declared target.
99
+
100
+ The bundled runtime system must work inside the isolated backend.
101
+ The target runtime must correspond to the target environment, not
102
+ the host interpreter. The runtime identity reflects target OS,
103
+ architecture, Python version, ABI, and interpreter checksum, not
104
+ the host Python.
105
+
106
+ Backend selection is intelligent. The user can request a target with
107
+
108
+ piebuild bake app.py --target linux-x86_64 --backend docker
109
+
110
+ while native builds remain:
111
+
112
+ piebuild bake app.py --target native
113
+
114
+ If the user requests a target that the native backend cannot produce,
115
+ PieBuild gives a useful message explaining that another backend is
116
+ required, rather than silently switching.
117
+
118
+ Docker is an optional capability. PieBuild itself remains usable without
119
+ Docker. The normal native workflow should continue functioning exactly
120
+ as before.
121
+
122
+ Failure modes include: Docker unavailable, daemon unavailable,
123
+ image unavailable, image pull failure, container startup failure,
124
+ build failure, timeout, malformed build context, permission
125
+ problems, target mismatch, incomplete artifact.
126
+
127
+ Errors identify the failing stage and preserve useful underlying
128
+ diagnostics for debug mode. Docker is treated as a build isolation
129
+ mechanism, not an automatic security guarantee. The build context is
130
+ controlled: only necessary files are mounted, host secrets are not
131
+ exposed, arbitrary environment variables are not passed, and
132
+ privileged containers are avoided unless absolutely necessary.
133
+
134
+ Reproducibility is supported by recording enough metadata to identify:
135
+
136
+ * backend
137
+ * target
138
+ * build environment
139
+ * image
140
+ * image digest where available
141
+ * runtime
142
+ * ingredients
143
+ * PieBuild version
144
+
145
+ The bundle manifest makes it possible to understand how an artifact
146
+ was produced. Docker is treated as a build isolation mechanism,
147
+ not an automatic security guarantee. The build context is kept
148
+ controlled: only necessary files are mounted, host secrets are not
149
+ mounted, and arbitrary host commands are not executed from recipe
150
+ configuration without an explicit design.
151
+
152
+ The first real milestone is:
153
+
154
+ application
155
+ ├─ piebuild bake
156
+ │ └─ Docker backend
157
+ │ └─ isolated target environment
158
+ │ ├─ bundled runtime
159
+ │ └─ dependencies
160
+ │ └─ native analysis
161
+ │ └─ .pie
162
+ │ ├─ inspect
163
+ │ └─ run
164
+
165
+ Tests must work even on machines without Docker. When Docker is
166
+ available, real builds are run. When unavailable, unit tests
167
+ verify the backend logic with a clear reason for skipping.
168
+ The existing native build path remains green throughout.
169
+
170
+ Security boundary: Docker is a build isolation mechanism, not an
171
+ automatic security guarantee. The host filesystem is not unnecessarily
172
+ mounted, host secrets are not exposed, arbitrary environment variables
173
+ are not passed, and privileged containers are avoided unless absolutely
174
+ necessary. The build context is kept controlled.
175
+ """
176
+
177
+ name = "docker"
178
+ description = "Docker isolated build backend"
179
+ environment = BuildEnvironment(host_os=host_os(), host_arch=host_arch(), kind="docker")
180
+
181
+ def supports_target(self, target_os: str) -> bool:
182
+ """Return whether this backend can build for ``target_os``.
183
+
184
+ Docker images are Linux-only (python:slim). Windows/macOS targets
185
+ require native hosts or a dedicated remote macOS/Windows builder.
186
+ """
187
+ return target_os in ("linux", "native")
188
+
189
+ def build(self, config: BuildConfig, context: BuildContext) -> BuildResult:
190
+ """Execute build inside Docker container."""
191
+ if not _docker_available():
192
+ return BuildResult(
193
+ success=False,
194
+ message="Docker is not available on this system",
195
+ metadata={"step": "docker availability", "hint": "Install Docker or use the native backend"},
196
+ )
197
+
198
+ try:
199
+ # Resolve target to Docker image
200
+ target = resolve_target(
201
+ context.target_os, context.target_arch, config.python or "3.14", config.abi or "cpython-" + context.target_arch
202
+ )
203
+ docker_image = _resolve_docker_image(target)
204
+
205
+ # Prepare build context - only necessary files
206
+ build_context = self._prepare_build_context(config, context)
207
+
208
+ # Pull/build the Docker image
209
+ if not self._pull_docker_image(docker_image):
210
+ return BuildResult(
211
+ success=False,
212
+ message=f"Failed to pull Docker image: {docker_image}",
213
+ metadata={"step": "docker image", "hint": "Check network connectivity or image availability"},
214
+ )
215
+
216
+ # Run the build inside Docker
217
+ build_result = self._run_docker_build(docker_image, build_context, target)
218
+
219
+ if build_result["success"]:
220
+ # Analyze the produced artifact's native profile
221
+ native_analysis = self._analyze_native_artifact(build_result["artifact_path"])
222
+
223
+ return BuildResult(
224
+ success=True,
225
+ artifact_path=build_result["artifact_path"],
226
+ message="Docker build completed successfully",
227
+ metadata={
228
+ "step": "docker build",
229
+ "docker_image": docker_image,
230
+ "target_triple": f"{target.os}/{target.arch}",
231
+ "native_profile": native_analysis,
232
+ "runtime": build_result.get("runtime"),
233
+ "warnings": build_result.get("warnings", []),
234
+ },
235
+ )
236
+ else:
237
+ return BuildResult(
238
+ success=False,
239
+ message=build_result.get("message", "Docker build failed"),
240
+ metadata={
241
+ "step": "docker build",
242
+ "hint": build_result.get("hint"),
243
+ },
244
+ )
245
+
246
+ except BuildStepError as e:
247
+ return BuildResult(
248
+ success=False,
249
+ message=f"Docker build failed ({e.step} step): {str(e)}",
250
+ metadata={"step": e.step, "hint": e.hint},
251
+ )
252
+ except Exception as e:
253
+ return BuildResult(
254
+ success=False,
255
+ message=f"Docker build failed: {str(e)}",
256
+ metadata={"step": "docker build unexpected"},
257
+ )
258
+
259
+ def _prepare_build_context(self, config: BuildConfig, context: BuildContext) -> dict[str, Any]:
260
+ """Prepare the build context to mount into the Docker container.
261
+
262
+ Only necessary files are included; unrelated host files are excluded.
263
+ """
264
+ return {
265
+ "source_file": str(config.source_file),
266
+ "output_dir": str(config.output_dir),
267
+ "config": config.model_dump(),
268
+ "target_os": context.target_os,
269
+ "target_arch": context.target_arch,
270
+ "build_mode": context.build_mode,
271
+ "runtime_os": context.runtime_os,
272
+ }
273
+
274
+ def _pull_docker_image(self, image: str) -> bool:
275
+ """Pull a Docker image.
276
+
277
+ Returns True if the image is available (already local or successfully pulled).
278
+ """
279
+ try:
280
+ result = subprocess.run(
281
+ ["docker", "pull", image],
282
+ capture_output=True,
283
+ text=True,
284
+ timeout=120,
285
+ )
286
+ return result.returncode == 0
287
+ except Exception as e:
288
+ logger.warning("Docker pull failed: %s", e)
289
+ return False
290
+
291
+ def _run_docker_build(self, image: str, build_context: dict[str, Any], target: Target) -> dict[str, Any]:
292
+ """Run the actual build inside the Docker container.
293
+
294
+ Mounts only the necessary build context and runs the PieBuild build.
295
+ """
296
+ # Create a temporary directory for the build output
297
+ import tempfile
298
+ with tempfile.TemporaryDirectory() as tmp_dir:
299
+ # Build the docker run command
300
+ mount_args = []
301
+ mount_args.append(f"{build_context['source_file']}:{build_context['source_file']}:ro")
302
+ mount_args.append(f"{build_context['output_dir']}:{tmp_dir}:rw")
303
+
304
+ # Environment variables to pass
305
+ env = {
306
+ "PIEBUILD_TARGET_OS": build_context["target_os"],
307
+ "PIEBUILD_TARGET_ARCH": build_context["target_arch"],
308
+ "PIEBUILD_PYTHON": build_context.get("python", "3.14"),
309
+ "PIEBUILD_ABI": build_context.get("abi", f"cpython-{build_context['target_arch']}"),
310
+ }
311
+
312
+ cmd = [
313
+ "docker", "run",
314
+ "--rm",
315
+ *[f"-v {mount}" for mount in mount_args],
316
+ *[f"-e {k}={v}" for k, v in env.items()],
317
+ image,
318
+ "python", "-m", "piebuild", "bake",
319
+ build_context["source_file"],
320
+ "--target",
321
+ build_context["target_os"],
322
+ "--backend",
323
+ "native",
324
+ ]
325
+
326
+ try:
327
+ result = subprocess.run(
328
+ cmd,
329
+ capture_output=True,
330
+ text=True,
331
+ timeout=300,
332
+ )
333
+
334
+ if result.returncode == 0:
335
+ # Check if the bundle was created
336
+ bundle_path = Path(tmp_dir) / "app.pie"
337
+ if bundle_path.exists():
338
+ return {
339
+ "success": True,
340
+ "artifact_path": str(bundle_path),
341
+ "message": "Docker build completed successfully",
342
+ "warnings": [],
343
+ }
344
+
345
+ except subprocess.TimeoutExpired:
346
+ return {
347
+ "success": False,
348
+ "message": "Docker build timed out",
349
+ "hint": "The build took too long. Try a simpler application or increase the timeout.",
350
+ }
351
+
352
+ return {
353
+ "success": False,
354
+ "message": "Docker build did not produce a bundle",
355
+ "hint": "Check the build output for errors",
356
+ }
357
+
358
+ def _analyze_native_artifact(self, artifact_path: str) -> dict[str, Any]:
359
+ """Analyze the native profile of the produced artifact.
360
+
361
+ Uses the existing native inspection system to determine the
362
+ native extensions, formats, architectures, and external
363
+ requirements of the built artifact.
364
+ """
365
+ try:
366
+ from piebuild.native import inspect_bundle_native
367
+ native_profile = inspect_bundle_native(artifact_path)
368
+ return {
369
+ "extensions": native_profile.extensions,
370
+ "formats": native_profile.formats,
371
+ "archs": native_profile.archs,
372
+ "external_requirements": native_profile.external_requirements,
373
+ "missing": native_profile.missing,
374
+ "inconsistent": native_profile.inconsistent,
375
+ }
376
+ except Exception as e:
377
+ logger.warning("Failed to analyze native artifact: %s", e)
378
+ return {
379
+ "extensions": [],
380
+ "formats": [],
381
+ "archs": [],
382
+ "external_requirements": [],
383
+ "missing": [],
384
+ "inconsistent": [],
385
+ }