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.
- piebuild/__init__.py +14 -0
- piebuild/__main__.py +12 -0
- piebuild/artifact.py +172 -0
- piebuild/attestation.py +88 -0
- piebuild/backends/__init__.py +10 -0
- piebuild/backends/docker_backend.py +385 -0
- piebuild/backends/macos_backend.py +34 -0
- piebuild/backends/native.py +80 -0
- piebuild/backends/remote_backend.py +35 -0
- piebuild/checksums.py +115 -0
- piebuild/cli/__init__.py +9 -0
- piebuild/cli/main.py +869 -0
- piebuild/config.py +323 -0
- piebuild/context.py +246 -0
- piebuild/core/__init__.py +230 -0
- piebuild/core/bundle.py +723 -0
- piebuild/core/cache.py +161 -0
- piebuild/formats/__init__.py +144 -0
- piebuild/formats/app.py +188 -0
- piebuild/formats/appimage.py +445 -0
- piebuild/formats/exe.py +567 -0
- piebuild/formats/pie.py +34 -0
- piebuild/gui/__init__.py +10 -0
- piebuild/gui/builder.py +38 -0
- piebuild/gui/main.py +588 -0
- piebuild/ingredients/__init__.py +125 -0
- piebuild/launchers.py +206 -0
- piebuild/native.py +323 -0
- piebuild/recipes/__init__.py +166 -0
- piebuild/recipes/builtin/console-app.toml +11 -0
- piebuild/recipes/builtin/gui-app.toml +11 -0
- piebuild/release.py +92 -0
- piebuild/runtime/__init__.py +95 -0
- piebuild/runtime/cache.py +125 -0
- piebuild/runtime/downloaded.py +502 -0
- piebuild/runtime/provider.py +199 -0
- piebuild/runtime/system.py +262 -0
- piebuild/signing/__init__.py +78 -0
- piebuild/signing/local.py +265 -0
- piebuild/validation.py +598 -0
- piebuild-3.1.0.dist-info/METADATA +425 -0
- piebuild-3.1.0.dist-info/RECORD +45 -0
- piebuild-3.1.0.dist-info/WHEEL +4 -0
- piebuild-3.1.0.dist-info/entry_points.txt +3 -0
- 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"
|
piebuild/attestation.py
ADDED
|
@@ -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
|
+
}
|