shipit-cli 0.14.0__tar.gz → 0.15.1__tar.gz

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 (44) hide show
  1. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/PKG-INFO +6 -3
  2. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/README.md +1 -1
  3. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/pyproject.toml +5 -2
  4. shipit_cli-0.15.1/src/shipit/builders/__init__.py +9 -0
  5. shipit_cli-0.15.1/src/shipit/builders/base.py +14 -0
  6. shipit_cli-0.15.1/src/shipit/builders/docker.py +250 -0
  7. shipit_cli-0.15.1/src/shipit/builders/local.py +161 -0
  8. shipit_cli-0.15.1/src/shipit/cli.py +1059 -0
  9. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/src/shipit/generator.py +54 -36
  10. shipit_cli-0.15.1/src/shipit/procfile.py +38 -0
  11. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/src/shipit/providers/base.py +49 -10
  12. shipit_cli-0.15.1/src/shipit/providers/hugo.py +108 -0
  13. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/src/shipit/providers/jekyll.py +48 -21
  14. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/src/shipit/providers/laravel.py +38 -29
  15. shipit_cli-0.15.1/src/shipit/providers/mkdocs.py +89 -0
  16. shipit_cli-0.15.1/src/shipit/providers/node_static.py +422 -0
  17. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/src/shipit/providers/php.py +41 -37
  18. shipit_cli-0.15.1/src/shipit/providers/python.py +573 -0
  19. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/src/shipit/providers/registry.py +0 -2
  20. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/src/shipit/providers/staticfile.py +44 -25
  21. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/src/shipit/providers/wordpress.py +25 -26
  22. shipit_cli-0.15.1/src/shipit/runners/__init__.py +9 -0
  23. shipit_cli-0.15.1/src/shipit/runners/base.py +17 -0
  24. shipit_cli-0.15.1/src/shipit/runners/local.py +105 -0
  25. shipit_cli-0.15.1/src/shipit/runners/wasmer.py +470 -0
  26. shipit_cli-0.15.1/src/shipit/shipit_types.py +103 -0
  27. shipit_cli-0.15.1/src/shipit/ui.py +14 -0
  28. shipit_cli-0.15.1/src/shipit/utils.py +10 -0
  29. shipit_cli-0.15.1/src/shipit/version.py +5 -0
  30. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/tests/test_e2e.py +16 -8
  31. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/tests/test_generate_shipit_examples.py +1 -3
  32. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/tests/test_version.py +3 -4
  33. shipit_cli-0.14.0/src/shipit/cli.py +0 -2066
  34. shipit_cli-0.14.0/src/shipit/procfile.py +0 -106
  35. shipit_cli-0.14.0/src/shipit/providers/hugo.py +0 -58
  36. shipit_cli-0.14.0/src/shipit/providers/mkdocs.py +0 -74
  37. shipit_cli-0.14.0/src/shipit/providers/node_static.py +0 -356
  38. shipit_cli-0.14.0/src/shipit/providers/python.py +0 -517
  39. shipit_cli-0.14.0/src/shipit/version.py +0 -5
  40. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/.gitignore +0 -0
  41. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/src/shipit/__init__.py +0 -0
  42. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/src/shipit/assets/php/php.ini +0 -0
  43. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/src/shipit/assets/wordpress/install.sh +0 -0
  44. {shipit_cli-0.14.0 → shipit_cli-0.15.1}/src/shipit/assets/wordpress/wp-config.php +0 -0
@@ -1,20 +1,23 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shipit-cli
3
- Version: 0.14.0
3
+ Version: 0.15.1
4
4
  Summary: Shipit CLI is the best way to build, serve and deploy your projects anywhere.
5
5
  Project-URL: homepage, https://wasmer.io
6
6
  Project-URL: repository, https://github.com/wasmerio/shipit
7
7
  Project-URL: Changelog, https://github.com/wasmerio/shipit/changelog
8
8
  Requires-Python: >=3.10
9
9
  Requires-Dist: dotenv>=0.9.9
10
+ Requires-Dist: pydantic-settings>=2.12.0
11
+ Requires-Dist: pydantic>=2.12.4
10
12
  Requires-Dist: pyyaml>=6.0.2
11
13
  Requires-Dist: requests>=2.32.5
12
14
  Requires-Dist: rich>=14.1.0
13
15
  Requires-Dist: semantic-version>=2.10.0
14
16
  Requires-Dist: sh>=2.2.2
15
- Requires-Dist: starlark-pyo3>=2025.1
17
+ Requires-Dist: toml>=0.10.2
16
18
  Requires-Dist: tomlkit>=0.13.3
17
19
  Requires-Dist: typer>=0.16.1
20
+ Requires-Dist: xingque>=0.2.1
18
21
  Description-Content-Type: text/markdown
19
22
 
20
23
  # Shipit
@@ -71,7 +74,7 @@ with `--use-provider`.
71
74
  uvx shipit-cli plan --out plan.json
72
75
  ```
73
76
 
74
- Evaluate the project and emit metadata, derived commands, and required
77
+ Evaluate the project and emit config, derived commands, and required
75
78
  services without building. Helpful for CI checks or debugging configuration.
76
79
 
77
80
  ### `build`
@@ -52,7 +52,7 @@ with `--use-provider`.
52
52
  uvx shipit-cli plan --out plan.json
53
53
  ```
54
54
 
55
- Evaluate the project and emit metadata, derived commands, and required
55
+ Evaluate the project and emit config, derived commands, and required
56
56
  services without building. Helpful for CI checks or debugging configuration.
57
57
 
58
58
  ### `build`
@@ -1,19 +1,22 @@
1
1
  [project]
2
2
  name = "shipit-cli"
3
- version = "0.14.0"
3
+ version = "0.15.1"
4
4
  description = "Shipit CLI is the best way to build, serve and deploy your projects anywhere."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  dependencies = [
8
8
  "dotenv>=0.9.9",
9
+ "pydantic>=2.12.4",
10
+ "pydantic-settings>=2.12.0",
9
11
  "pyyaml>=6.0.2",
10
12
  "requests>=2.32.5",
11
13
  "rich>=14.1.0",
12
14
  "semantic-version>=2.10.0",
13
15
  "sh>=2.2.2",
14
- "starlark-pyo3>=2025.1",
16
+ "xingque>=0.2.1",
15
17
  "tomlkit>=0.13.3",
16
18
  "typer>=0.16.1",
19
+ "toml>=0.10.2",
17
20
  ]
18
21
 
19
22
  [project.urls]
@@ -0,0 +1,9 @@
1
+ from .base import BuildBackend
2
+ from .docker import DockerBuildBackend
3
+ from .local import LocalBuildBackend
4
+
5
+ __all__ = [
6
+ "BuildBackend",
7
+ "DockerBuildBackend",
8
+ "LocalBuildBackend",
9
+ ]
@@ -0,0 +1,14 @@
1
+ from pathlib import Path
2
+ from typing import Dict, List, Optional, Protocol, TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from shipit.cli import Mount, Step
6
+
7
+
8
+ class BuildBackend(Protocol):
9
+ def build(
10
+ self, name: str, env: Dict[str, str], mounts: List["Mount"], steps: List["Step"]
11
+ ) -> None: ...
12
+ def get_build_mount_path(self, name: str) -> Path: ...
13
+ def get_artifact_mount_path(self, name: str) -> Path: ...
14
+ def get_runtime_path(self) -> Optional[str]: ...
@@ -0,0 +1,250 @@
1
+ import base64
2
+ import json
3
+ import os
4
+ import shutil
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional
7
+
8
+ import sh # type: ignore[import-untyped]
9
+ from rich import box
10
+ from rich.panel import Panel
11
+ from rich.rule import Rule
12
+ from rich.syntax import Syntax
13
+
14
+ from shipit.builders.base import BuildBackend
15
+ from shipit.shipit_types import (
16
+ CopyStep,
17
+ EnvStep,
18
+ Mount,
19
+ Package,
20
+ PathStep,
21
+ RunStep,
22
+ Step,
23
+ UseStep,
24
+ WorkdirStep,
25
+ )
26
+ from shipit.ui import console, write_stderr, write_stdout
27
+
28
+
29
+ class DockerBuildBackend:
30
+ mise_mapper = {
31
+ "php": {
32
+ "source": "ubi:adwinying/php",
33
+ },
34
+ "composer": {
35
+ "source": "ubi:composer/composer",
36
+ "postinstall": """composer_dir=$(mise where ubi:composer/composer); ln -s "$composer_dir/composer.phar" /usr/local/bin/composer""",
37
+ },
38
+ }
39
+
40
+ def __init__(
41
+ self, src_dir: Path, assets_path: Path, docker_client: Optional[str] = None
42
+ ) -> None:
43
+ self.src_dir = src_dir
44
+ self.assets_path = assets_path
45
+ self.docker_path = self.src_dir / ".shipit" / "docker"
46
+ self.docker_path.mkdir(parents=True, exist_ok=True)
47
+ self.docker_out_path = self.docker_path / "out"
48
+ self.docker_file_path = self.docker_path / "Dockerfile"
49
+ self.docker_name_path = self.docker_path / "name"
50
+ self.docker_ignore_path = self.docker_path / "Dockerfile.dockerignore"
51
+ self.shipit_docker_path = Path("/shipit")
52
+ self.docker_client = docker_client or "docker"
53
+ self.env = {
54
+ "HOME": "/root",
55
+ }
56
+ self.runtime_path: Optional[str] = None
57
+
58
+ def get_mount_path(self, name: str) -> Path:
59
+ if name == "app":
60
+ return Path("app")
61
+ else:
62
+ return Path("opt") / name
63
+
64
+ def get_build_mount_path(self, name: str) -> Path:
65
+ return Path("/") / self.get_mount_path(name)
66
+
67
+ def get_artifact_mount_path(self, name: str) -> Path:
68
+ return self.docker_out_path / self.get_mount_path(name)
69
+
70
+ @property
71
+ def is_depot(self) -> bool:
72
+ return self.docker_client == "depot"
73
+
74
+ def build_dockerfile(self, image_name: str, contents: str) -> None:
75
+ self.docker_file_path.write_text(contents)
76
+ self.docker_name_path.write_text(image_name)
77
+ self.print_dockerfile(contents)
78
+ extra_args: List[str] = []
79
+ sh.Command(self.docker_client)(
80
+ "build",
81
+ "-f",
82
+ (self.docker_path / "Dockerfile").absolute(),
83
+ "-t",
84
+ image_name,
85
+ "--platform",
86
+ "linux/amd64",
87
+ "--output",
88
+ self.docker_out_path.absolute(),
89
+ ".",
90
+ *extra_args,
91
+ _cwd=self.src_dir.absolute(),
92
+ _env=os.environ,
93
+ _out=write_stdout,
94
+ _err=write_stderr,
95
+ )
96
+
97
+ def print_dockerfile(self, contents: str) -> None:
98
+ manifest_panel = Panel(
99
+ Syntax(
100
+ contents,
101
+ "dockerfile",
102
+ theme="monokai",
103
+ background_color="default",
104
+ line_numbers=True,
105
+ ),
106
+ box=box.SQUARE,
107
+ border_style="bright_black",
108
+ expand=False,
109
+ )
110
+ console.print(manifest_panel, markup=False, highlight=True)
111
+
112
+ def build(
113
+ self, name: str, env: Dict[str, str], mounts: List[Mount], steps: List[Step]
114
+ ) -> None:
115
+ docker_file_contents = ""
116
+
117
+ base_path = self.docker_path
118
+ shutil.rmtree(base_path, ignore_errors=True)
119
+ base_path.mkdir(parents=True, exist_ok=True)
120
+
121
+ docker_file_contents = "# syntax=docker/dockerfile:1.7-labs\n"
122
+ docker_file_contents += "FROM debian:trixie-slim AS build\n"
123
+ docker_file_contents += """
124
+ RUN apt-get update \\
125
+ && apt-get -y --no-install-recommends install \\
126
+ build-essential gcc make autoconf libtool bison \\
127
+ dpkg-dev pkg-config re2c locate \\
128
+ libmariadb-dev libmariadb-dev-compat libpq-dev \\
129
+ libvips-dev default-libmysqlclient-dev libmagickwand-dev \\
130
+ libicu-dev libxml2-dev libxslt-dev libyaml-dev \\
131
+ sudo curl ca-certificates \\
132
+ && rm -rf /var/lib/apt/lists/*
133
+
134
+ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
135
+ ENV MISE_DATA_DIR="/mise"
136
+ ENV MISE_CONFIG_DIR="/mise"
137
+ ENV MISE_CACHE_DIR="/mise/cache"
138
+ ENV MISE_INSTALL_PATH="/usr/local/bin/mise"
139
+ ENV PATH="/mise/shims:$PATH"
140
+
141
+ RUN curl https://mise.run | sh
142
+ """
143
+ for mount in mounts:
144
+ docker_file_contents += f"RUN mkdir -p {mount.build_path.absolute()}\n"
145
+
146
+ for step in steps:
147
+ if isinstance(step, WorkdirStep):
148
+ docker_file_contents += f"WORKDIR {step.path.absolute()}\n"
149
+ elif isinstance(step, RunStep):
150
+ if step.inputs:
151
+ pre = "\\\n " + "".join(
152
+ [
153
+ f"--mount=type=bind,source={input},target={input} \\\n "
154
+ for input in step.inputs
155
+ ]
156
+ )
157
+ else:
158
+ pre = ""
159
+ docker_file_contents += f"RUN {pre}{step.command}\n"
160
+ elif isinstance(step, CopyStep):
161
+ if step.is_download():
162
+ docker_file_contents += (
163
+ "ADD " + step.source + " " + step.target + "\n"
164
+ )
165
+ elif step.base == "assets":
166
+ asset_path = self.assets_path / step.source
167
+ if asset_path.is_file():
168
+ content_base64 = base64.b64encode(
169
+ asset_path.read_bytes()
170
+ ).decode("utf-8")
171
+ docker_file_contents += (
172
+ f"RUN echo '{content_base64}' | base64 -d > {step.target}\n"
173
+ )
174
+ elif asset_path.is_dir():
175
+ raise Exception(
176
+ f"Asset {step.source} is a directory, shipit doesn't currently support coppying assets directories inside Docker"
177
+ )
178
+ else:
179
+ raise Exception(f"Asset {step.source} does not exist")
180
+ else:
181
+ if step.ignore:
182
+ exclude = (
183
+ " \\\n"
184
+ + " \\\n".join(
185
+ [f" --exclude={ignore}" for ignore in step.ignore]
186
+ )
187
+ + " \\\n "
188
+ )
189
+ else:
190
+ exclude = ""
191
+ docker_file_contents += (
192
+ f"COPY{exclude} {step.source} {step.target}\n"
193
+ )
194
+ elif isinstance(step, EnvStep):
195
+ env_vars = " ".join(
196
+ [f"{key}={value}" for key, value in step.variables.items()]
197
+ )
198
+ docker_file_contents += f"ENV {env_vars}\n"
199
+ env.update(step.variables)
200
+ elif isinstance(step, PathStep):
201
+ docker_file_contents += f"ENV PATH={step.path}:$PATH\n"
202
+ env["PATH"] = f"{step.path}{os.pathsep}{env.get('PATH', '')}"
203
+ elif isinstance(step, UseStep):
204
+ for dependency in step.dependencies:
205
+ if dependency.name == "pie":
206
+ docker_file_contents += "RUN apt-get update && apt-get -y --no-install-recommends install gcc make autoconf libtool bison re2c pkg-config libpq-dev\n"
207
+ docker_file_contents += "RUN curl -L --output /usr/bin/pie https://github.com/php/pie/releases/download/1.2.0/pie.phar && chmod +x /usr/bin/pie\n"
208
+ return
209
+ elif dependency.name == "static-web-server":
210
+ if dependency.version:
211
+ docker_file_contents += (
212
+ f"ENV SWS_INSTALL_VERSION={dependency.version}\n"
213
+ )
214
+ docker_file_contents += "RUN curl --proto '=https' --tlsv1.2 -sSfL https://get.static-web-server.net | sh\n"
215
+ return
216
+
217
+ mapped_dependency = self.mise_mapper.get(dependency.name, {})
218
+ package_name = mapped_dependency.get("source", dependency.name)
219
+ if dependency.version:
220
+ docker_file_contents += f"RUN mise use --global {package_name}@{dependency.version}\n"
221
+ else:
222
+ docker_file_contents += (
223
+ f"RUN mise use --global {package_name}\n"
224
+ )
225
+ if mapped_dependency.get("postinstall"):
226
+ docker_file_contents += (
227
+ f"RUN {mapped_dependency.get('postinstall')}\n"
228
+ )
229
+
230
+ docker_file_contents += """
231
+ FROM scratch
232
+ """
233
+ for mount in mounts:
234
+ docker_file_contents += (
235
+ f"COPY --from=build {mount.build_path} {mount.build_path}\n"
236
+ )
237
+
238
+ self.runtime_path = env.get("PATH")
239
+
240
+ self.docker_ignore_path.write_text("""
241
+ .shipit
242
+ Shipit
243
+ """)
244
+ console.print(f"\n[bold]Building Docker file[/bold]")
245
+ self.build_dockerfile(name, docker_file_contents)
246
+ console.print(Rule(characters="-", style="bright_black"))
247
+ console.print(f"[bold]Build complete ✅[/bold]")
248
+
249
+ def get_runtime_path(self) -> Optional[str]:
250
+ return self.runtime_path
@@ -0,0 +1,161 @@
1
+ import os
2
+ import shlex
3
+ import shutil
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional
6
+
7
+ import sh # type: ignore[import-untyped]
8
+ from rich.rule import Rule
9
+ from rich.syntax import Syntax
10
+
11
+ from shipit.builders.base import BuildBackend
12
+ from shipit.shipit_types import (
13
+ CopyStep,
14
+ EnvStep,
15
+ Mount,
16
+ PathStep,
17
+ RunStep,
18
+ Step,
19
+ UseStep,
20
+ WorkdirStep,
21
+ )
22
+ from shipit.ui import console, write_stderr, write_stdout
23
+ from shipit.utils import download_file
24
+
25
+
26
+ class LocalBuildBackend:
27
+ def __init__(self, src_dir: Path, assets_path: Path) -> None:
28
+ self.src_dir = src_dir
29
+ self.assets_path = assets_path
30
+ self.local_path = self.src_dir / ".shipit" / "local"
31
+ self.build_path = self.local_path / "build"
32
+ self.workdir = self.build_path
33
+ self.runtime_path: Optional[str] = None
34
+
35
+ def get_mount_path(self, name: str) -> Path:
36
+ if name == "app":
37
+ return self.build_path / "app"
38
+ else:
39
+ return self.build_path / "opt" / name
40
+
41
+ def get_build_mount_path(self, name: str) -> Path:
42
+ return self.get_mount_path(name)
43
+
44
+ def get_artifact_mount_path(self, name: str) -> Path:
45
+ return self.get_mount_path(name)
46
+
47
+ def execute_step(self, step: Step, env: Dict[str, str]) -> None:
48
+ build_path = self.workdir
49
+ if isinstance(step, UseStep):
50
+ console.print(
51
+ f"[bold]Using dependencies:[/bold] {', '.join([str(dep) for dep in step.dependencies])}"
52
+ )
53
+ elif isinstance(step, WorkdirStep):
54
+ console.print(f"[bold]Working in {step.path}[/bold]")
55
+ self.workdir = step.path
56
+ step.path.mkdir(parents=True, exist_ok=True)
57
+ elif isinstance(step, RunStep):
58
+ extra = ""
59
+ if step.inputs:
60
+ for input in step.inputs:
61
+ console.print(f"Copying {input} to {build_path / input}")
62
+ shutil.copy((self.src_dir / input), (build_path / input))
63
+ all_inputs = ", ".join(step.inputs)
64
+ extra = f" [bright_black]# using {all_inputs}[/bright_black]"
65
+ console.print(
66
+ f"[bright_black]$[/bright_black] [bold]{step.command}[/bold]{extra}"
67
+ )
68
+ command_line = step.command
69
+ parts = shlex.split(command_line)
70
+ program = parts[0]
71
+ extended_paths = [
72
+ str(build_path / path) for path in env["PATH"].split(os.pathsep)
73
+ ]
74
+ extended_paths.append(os.environ["PATH"])
75
+ PATH = os.pathsep.join(extended_paths)
76
+ exe = shutil.which(program, path=PATH)
77
+ if not exe:
78
+ raise Exception(f"Program is not installed: {program}")
79
+ cmd = sh.Command("bash")
80
+ cmd(
81
+ "-c",
82
+ command_line,
83
+ _env={**env, "PATH": PATH},
84
+ _cwd=build_path,
85
+ _out=write_stdout,
86
+ _err=write_stderr,
87
+ )
88
+ elif isinstance(step, CopyStep):
89
+ ignore_extra = ""
90
+ if step.ignore:
91
+ ignore_extra = (
92
+ f" [bright_black]# ignoring {', '.join(step.ignore)}[/bright_black]"
93
+ )
94
+ ignore_matches = list(step.ignore or [])
95
+ ignore_matches.append(".shipit")
96
+ ignore_matches.append("Shipit")
97
+
98
+ if step.is_download():
99
+ console.print(
100
+ f"[bold]Download from {step.source} to {step.target}[/bold]"
101
+ )
102
+ download_file(step.source, (build_path / step.target))
103
+ else:
104
+ if step.base == "source":
105
+ base = self.src_dir
106
+ elif step.base == "assets":
107
+ base = self.assets_path
108
+ else:
109
+ raise Exception(f"Unknown base: {step.base}")
110
+
111
+ console.print(
112
+ f"[bold]Copy to {step.target} from {step.source}[/bold]{ignore_extra}"
113
+ )
114
+
115
+ source = base / step.source
116
+ target = build_path / step.target
117
+ if source.is_dir():
118
+ shutil.copytree(
119
+ source,
120
+ target,
121
+ dirs_exist_ok=True,
122
+ ignore=shutil.ignore_patterns(*ignore_matches),
123
+ )
124
+ elif source.is_file():
125
+ shutil.copy(source, target)
126
+ else:
127
+ raise Exception(f"Source {step.source} is not a file or directory")
128
+ elif isinstance(step, EnvStep):
129
+ console.print(f"Setting environment variables: {step}")
130
+ env.update(step.variables)
131
+ elif isinstance(step, PathStep):
132
+ console.print(f"[bold]Add {step.path}[/bold] to PATH")
133
+ fullpath = step.path
134
+ env["PATH"] = f"{fullpath}{os.pathsep}{env['PATH']}"
135
+ else:
136
+ raise Exception(f"Unknown step type: {type(step)}")
137
+
138
+ def build(
139
+ self, name: str, env: Dict[str, str], mounts: List[Mount], steps: List[Step]
140
+ ) -> None:
141
+ console.print(f"\n[bold]Building... 🚀[/bold]")
142
+ base_path = self.local_path
143
+ shutil.rmtree(base_path, ignore_errors=True)
144
+ base_path.mkdir(parents=True, exist_ok=True)
145
+ self.build_path.mkdir(exist_ok=True)
146
+ for mount in mounts:
147
+ mount.build_path.mkdir(parents=True, exist_ok=True)
148
+ for step in steps:
149
+ console.print(Rule(characters="-", style="bright_black"))
150
+ self.execute_step(step, env)
151
+
152
+ if "PATH" in env:
153
+ path = base_path / ".path"
154
+ path.write_text(env["PATH"])
155
+ self.runtime_path = env.get("PATH")
156
+
157
+ console.print(Rule(characters="-", style="bright_black"))
158
+ console.print(f"[bold]Build complete ✅[/bold]")
159
+
160
+ def get_runtime_path(self) -> Optional[str]:
161
+ return self.runtime_path