shipit-cli 0.13.4__py3-none-any.whl → 0.15.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.
- shipit/builders/__init__.py +9 -0
- shipit/builders/base.py +14 -0
- shipit/builders/docker.py +250 -0
- shipit/builders/local.py +161 -0
- shipit/cli.py +291 -1323
- shipit/generator.py +56 -36
- shipit/procfile.py +4 -72
- shipit/providers/base.py +50 -11
- shipit/providers/hugo.py +64 -14
- shipit/providers/jekyll.py +123 -0
- shipit/providers/laravel.py +40 -31
- shipit/providers/mkdocs.py +34 -19
- shipit/providers/node_static.py +219 -136
- shipit/providers/php.py +42 -38
- shipit/providers/python.py +284 -228
- shipit/providers/registry.py +2 -2
- shipit/providers/staticfile.py +45 -26
- shipit/providers/wordpress.py +26 -27
- shipit/runners/__init__.py +9 -0
- shipit/runners/base.py +17 -0
- shipit/runners/local.py +105 -0
- shipit/runners/wasmer.py +470 -0
- shipit/shipit_types.py +103 -0
- shipit/ui.py +14 -0
- shipit/utils.py +10 -0
- shipit/version.py +2 -2
- {shipit_cli-0.13.4.dist-info → shipit_cli-0.15.0.dist-info}/METADATA +6 -3
- shipit_cli-0.15.0.dist-info/RECORD +34 -0
- {shipit_cli-0.13.4.dist-info → shipit_cli-0.15.0.dist-info}/WHEEL +1 -1
- shipit_cli-0.13.4.dist-info/RECORD +0 -22
- {shipit_cli-0.13.4.dist-info → shipit_cli-0.15.0.dist-info}/entry_points.txt +0 -0
shipit/builders/base.py
ADDED
|
@@ -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
|
shipit/builders/local.py
ADDED
|
@@ -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
|