shipit-cli 0.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.
- shipit/__init__.py +0 -0
- shipit/assets/php/php.ini +103 -0
- shipit/cli.py +1281 -0
- shipit/generator.py +148 -0
- shipit/providers/base.py +68 -0
- shipit/providers/gatsby.py +64 -0
- shipit/providers/hugo.py +47 -0
- shipit/providers/laravel.py +74 -0
- shipit/providers/mkdocs.py +81 -0
- shipit/providers/node_static.py +65 -0
- shipit/providers/php.py +73 -0
- shipit/providers/python.py +104 -0
- shipit/providers/registry.py +26 -0
- shipit/providers/staticfile.py +61 -0
- shipit/version.py +5 -0
- shipit_cli-0.1.0.dist-info/METADATA +13 -0
- shipit_cli-0.1.0.dist-info/RECORD +19 -0
- shipit_cli-0.1.0.dist-info/WHEEL +4 -0
- shipit_cli-0.1.0.dist-info/entry_points.txt +2 -0
shipit/cli.py
ADDED
|
@@ -0,0 +1,1281 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import shlex
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
import json
|
|
7
|
+
import yaml
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import (
|
|
11
|
+
Any,
|
|
12
|
+
Dict,
|
|
13
|
+
List,
|
|
14
|
+
Optional,
|
|
15
|
+
Protocol,
|
|
16
|
+
Set,
|
|
17
|
+
TypedDict,
|
|
18
|
+
Union,
|
|
19
|
+
cast,
|
|
20
|
+
)
|
|
21
|
+
from shutil import copy, copytree, ignore_patterns
|
|
22
|
+
|
|
23
|
+
import sh # type: ignore[import-untyped]
|
|
24
|
+
import starlark as sl
|
|
25
|
+
import typer
|
|
26
|
+
from rich import box
|
|
27
|
+
from rich.console import Console
|
|
28
|
+
from rich.panel import Panel
|
|
29
|
+
from rich.rule import Rule
|
|
30
|
+
from rich.syntax import Syntax
|
|
31
|
+
|
|
32
|
+
from shipit.version import version as shipit_version
|
|
33
|
+
from shipit.generator import generate_shipit
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
console = Console()
|
|
37
|
+
|
|
38
|
+
app = typer.Typer(invoke_without_command=True)
|
|
39
|
+
|
|
40
|
+
DIR_PATH = Path(__file__).resolve().parent
|
|
41
|
+
ASSETS_PATH = DIR_PATH / "assets"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Serve:
|
|
46
|
+
name: str
|
|
47
|
+
provider: str
|
|
48
|
+
build: List["Step"]
|
|
49
|
+
deps: List["Package"]
|
|
50
|
+
commands: Dict[str, str]
|
|
51
|
+
assets: Optional[Dict[str, str]] = None
|
|
52
|
+
prepare: Optional[List["PrepareStep"]] = None
|
|
53
|
+
workers: Optional[List[str]] = None
|
|
54
|
+
mounts: Optional[Dict[str, str]] = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class Package:
|
|
59
|
+
name: str
|
|
60
|
+
version: Optional[str] = None
|
|
61
|
+
|
|
62
|
+
def __str__(self) -> str: # pragma: no cover - simple representation
|
|
63
|
+
return f"{self.name}@{self.version}"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class RunStep:
|
|
68
|
+
command: str
|
|
69
|
+
inputs: Optional[List[str]] = None
|
|
70
|
+
outputs: Optional[List[str]] = None
|
|
71
|
+
group: Optional[str] = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class CopyStep:
|
|
76
|
+
source: str
|
|
77
|
+
target: str
|
|
78
|
+
ignore: Optional[List[str]] = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class EnvStep:
|
|
83
|
+
variables: Dict[str, str]
|
|
84
|
+
|
|
85
|
+
def __str__(self) -> str: # pragma: no cover - simple representation
|
|
86
|
+
return " ".join([f"{key}={value}" for key, value in self.variables.items()])
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class UseStep:
|
|
91
|
+
dependencies: List[Package]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class PathStep:
|
|
96
|
+
path: str
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
Step = Union[RunStep, CopyStep, EnvStep, PathStep, UseStep]
|
|
100
|
+
PrepareStep = Union[RunStep]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class Build:
|
|
105
|
+
deps: List[Package]
|
|
106
|
+
steps: List[Step]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def write_stdout(line: str) -> None:
|
|
110
|
+
sys.stdout.write(line) # print to console
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def write_stderr(line: str) -> None:
|
|
114
|
+
sys.stderr.write(line) # print to console
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class MapperItem(TypedDict):
|
|
118
|
+
dependencies: Dict[str, str]
|
|
119
|
+
scripts: Set[str]
|
|
120
|
+
env: Dict[str, str]
|
|
121
|
+
aliases: Dict[str, str]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class Builder(Protocol):
|
|
125
|
+
def build(self, env: Dict[str, str], steps: List[Step]) -> None: ...
|
|
126
|
+
def build_assets(self, assets: Dict[str, str]) -> None: ...
|
|
127
|
+
def build_prepare(self, serve: Serve) -> None: ...
|
|
128
|
+
def build_serve(self, serve: Serve) -> None: ...
|
|
129
|
+
def finalize_build(self, serve: Serve) -> None: ...
|
|
130
|
+
def prepare(self, env: Dict[str, str], prepare: List[PrepareStep]) -> None: ...
|
|
131
|
+
def getenv(self, name: str) -> Optional[str]: ...
|
|
132
|
+
def run_serve_command(self, command: str) -> None: ...
|
|
133
|
+
def run_command(
|
|
134
|
+
self, command: str, extra_args: Optional[List[str]] | None = None
|
|
135
|
+
) -> Any: ...
|
|
136
|
+
def serve_mount(self, name: str) -> str: ...
|
|
137
|
+
def get_asset(self, name: str) -> str: ...
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class DockerBuilder:
|
|
141
|
+
def __init__(self, src_dir: Path, docker_client: Optional[str] = None) -> None:
|
|
142
|
+
self.src_dir = src_dir
|
|
143
|
+
self.docker_file_contents = ""
|
|
144
|
+
self.docker_path = self.src_dir / ".shipit" / "docker"
|
|
145
|
+
self.depot_metadata = self.docker_path / "depot-build.json"
|
|
146
|
+
self.docker_file_path = self.docker_path / "Dockerfile"
|
|
147
|
+
self.docker_name_path = self.docker_path / "name"
|
|
148
|
+
self.docker_ignore_path = self.docker_path / "Dockerfile.dockerignore"
|
|
149
|
+
self.shipit_docker_path = Path("/shipit")
|
|
150
|
+
self.docker_client = docker_client or "docker"
|
|
151
|
+
self.env = {
|
|
152
|
+
"HOME": "/root",
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def is_depot(self) -> bool:
|
|
157
|
+
return self.docker_client == "depot"
|
|
158
|
+
|
|
159
|
+
def getenv(self, name: str) -> Optional[str]:
|
|
160
|
+
return self.env.get(name) or os.environ.get(name)
|
|
161
|
+
|
|
162
|
+
def mkdir(self, path: Path) -> Path:
|
|
163
|
+
path = self.shipit_docker_path / path
|
|
164
|
+
self.docker_file_contents += f"RUN mkdir -p {str(path.absolute())}\n"
|
|
165
|
+
return path.absolute()
|
|
166
|
+
|
|
167
|
+
def build_dockerfile(self, image_name: str) -> None:
|
|
168
|
+
self.docker_file_path.write_text(self.docker_file_contents)
|
|
169
|
+
self.docker_name_path.write_text(image_name)
|
|
170
|
+
self.print_dockerfile()
|
|
171
|
+
extra_args = []
|
|
172
|
+
if self.is_depot:
|
|
173
|
+
# We load the docker image back into the local docker daemon
|
|
174
|
+
# extra_args += ["--load"]
|
|
175
|
+
extra_args += ["--save", f"--metadata-file={self.depot_metadata.absolute()}"]
|
|
176
|
+
sh.Command(self.docker_client)(
|
|
177
|
+
"build",
|
|
178
|
+
"-f",
|
|
179
|
+
(self.docker_path / "Dockerfile").absolute(),
|
|
180
|
+
"-t",
|
|
181
|
+
image_name,
|
|
182
|
+
"--platform",
|
|
183
|
+
"linux/amd64",
|
|
184
|
+
".",
|
|
185
|
+
*extra_args,
|
|
186
|
+
_cwd=self.src_dir.absolute(),
|
|
187
|
+
_env=os.environ, # Pass the current environment variables to the Docker client
|
|
188
|
+
_out=write_stdout,
|
|
189
|
+
_err=write_stderr,
|
|
190
|
+
)
|
|
191
|
+
if self.is_depot:
|
|
192
|
+
json_text = self.depot_metadata.read_text()
|
|
193
|
+
json_data = json.loads(json_text)
|
|
194
|
+
build_data = json_data["depot.build"]
|
|
195
|
+
image_id = build_data["buildID"]
|
|
196
|
+
project = build_data["projectID"]
|
|
197
|
+
sh.Command("depot")(
|
|
198
|
+
"pull",
|
|
199
|
+
"--platform",
|
|
200
|
+
"linux/amd64",
|
|
201
|
+
"--project",
|
|
202
|
+
project,
|
|
203
|
+
image_id,
|
|
204
|
+
_cwd=self.src_dir.absolute(),
|
|
205
|
+
_env=os.environ, # Pass the current environment variables to the Docker client
|
|
206
|
+
_out=write_stdout,
|
|
207
|
+
_err=write_stderr,
|
|
208
|
+
)
|
|
209
|
+
# console.print(f"[bold]Image ID:[/bold] {image_id}")
|
|
210
|
+
|
|
211
|
+
def finalize_build(self, serve: Serve) -> None:
|
|
212
|
+
console.print(f"\n[bold]Building Docker file[/bold]")
|
|
213
|
+
self.build_dockerfile(serve.name)
|
|
214
|
+
console.print(Rule(characters="-", style="bright_black"))
|
|
215
|
+
console.print(f"[bold]Build complete ✅[/bold]")
|
|
216
|
+
|
|
217
|
+
def run_command(self, command: str, extra_args: Optional[List[str]] = None) -> Any:
|
|
218
|
+
image_name = self.docker_name_path.read_text()
|
|
219
|
+
return sh.Command(
|
|
220
|
+
"docker"
|
|
221
|
+
)(
|
|
222
|
+
"run",
|
|
223
|
+
"-p",
|
|
224
|
+
"80:80",
|
|
225
|
+
"--rm",
|
|
226
|
+
image_name,
|
|
227
|
+
command,
|
|
228
|
+
*(extra_args or []),
|
|
229
|
+
_env=os.environ, # Pass the current environment variables to the Docker client
|
|
230
|
+
_out=write_stdout,
|
|
231
|
+
_err=write_stderr,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def create_file(self, path: Path, content: str, mode: int = 0o755) -> Path:
|
|
235
|
+
# docker_files = self.docker_path / "files" / path.name
|
|
236
|
+
# docker_files.write_text(content)
|
|
237
|
+
# docker_files.chmod(mode)
|
|
238
|
+
self.docker_file_contents += f"""
|
|
239
|
+
RUN cat > {path.absolute()} <<'EOF'
|
|
240
|
+
{content}
|
|
241
|
+
EOF
|
|
242
|
+
|
|
243
|
+
RUN chmod {oct(mode)[2:]} {path.absolute()}
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
return path.absolute()
|
|
247
|
+
|
|
248
|
+
def print_dockerfile(self) -> None:
|
|
249
|
+
docker_file = self.docker_path / "Dockerfile"
|
|
250
|
+
manifest_panel = Panel(
|
|
251
|
+
Syntax(
|
|
252
|
+
docker_file.read_text(),
|
|
253
|
+
"dockerfile",
|
|
254
|
+
theme="monokai",
|
|
255
|
+
background_color="default",
|
|
256
|
+
line_numbers=True,
|
|
257
|
+
),
|
|
258
|
+
box=box.SQUARE,
|
|
259
|
+
border_style="bright_black",
|
|
260
|
+
expand=False,
|
|
261
|
+
)
|
|
262
|
+
console.print(manifest_panel, markup=False, highlight=True)
|
|
263
|
+
|
|
264
|
+
def add_dependency(self, dependency: Package):
|
|
265
|
+
if dependency.name == "pie":
|
|
266
|
+
self.docker_file_contents += f"RUN apt-get update && apt-get -y --no-install-recommends install gcc make autoconf libtool bison re2c pkg-config libpq-dev\n"
|
|
267
|
+
self.docker_file_contents += f"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"
|
|
268
|
+
return
|
|
269
|
+
elif dependency.name == "static-web-server":
|
|
270
|
+
if dependency.version:
|
|
271
|
+
self.docker_file_contents += (
|
|
272
|
+
f"ENV SWS_INSTALL_VERSION={dependency.version}\n"
|
|
273
|
+
)
|
|
274
|
+
self.docker_file_contents += f"RUN curl --proto '=https' --tlsv1.2 -sSfL https://get.static-web-server.net | sh\n"
|
|
275
|
+
return
|
|
276
|
+
if dependency.version:
|
|
277
|
+
self.docker_file_contents += (
|
|
278
|
+
f"RUN pkgm install {dependency.name}@{dependency.version}\n"
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
self.docker_file_contents += f"RUN pkgm install {dependency.name}\n"
|
|
282
|
+
|
|
283
|
+
def build(self, env: Dict[str, str], steps: List[Step]) -> None:
|
|
284
|
+
base_path = self.docker_path
|
|
285
|
+
shutil.rmtree(base_path, ignore_errors=True)
|
|
286
|
+
base_path.mkdir(parents=True, exist_ok=True)
|
|
287
|
+
self.docker_file_contents = "FROM debian:bookworm-slim\n"
|
|
288
|
+
self.docker_file_contents += """
|
|
289
|
+
RUN apt-get update \\
|
|
290
|
+
&& apt-get -y --no-install-recommends install sudo curl ca-certificates locate git zip unzip \\
|
|
291
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
292
|
+
|
|
293
|
+
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
294
|
+
|
|
295
|
+
RUN curl https://pkgx.sh | sh
|
|
296
|
+
"""
|
|
297
|
+
# docker_file_contents += "RUN curl https://mise.run | sh\n"
|
|
298
|
+
self.docker_file_contents += """
|
|
299
|
+
RUN curl https://get.wasmer.io -sSfL | sh -s "v6.1.0-rc.3"
|
|
300
|
+
ENV PATH="/root/.wasmer/bin:${PATH}"
|
|
301
|
+
"""
|
|
302
|
+
self.docker_file_contents += "WORKDIR /app\n"
|
|
303
|
+
for step in steps:
|
|
304
|
+
if isinstance(step, RunStep):
|
|
305
|
+
if step.inputs:
|
|
306
|
+
pre = "\\\n " + "".join(
|
|
307
|
+
[
|
|
308
|
+
f"--mount=type=bind,source={input},target={input} \\\n "
|
|
309
|
+
for input in step.inputs
|
|
310
|
+
]
|
|
311
|
+
)
|
|
312
|
+
else:
|
|
313
|
+
pre = ""
|
|
314
|
+
self.docker_file_contents += f"RUN {pre}{step.command}\n"
|
|
315
|
+
elif isinstance(step, CopyStep):
|
|
316
|
+
self.docker_file_contents += f"COPY {step.source} {step.target}\n"
|
|
317
|
+
elif isinstance(step, EnvStep):
|
|
318
|
+
env_vars = " ".join(
|
|
319
|
+
[f"{key}={value}" for key, value in step.variables.items()]
|
|
320
|
+
)
|
|
321
|
+
self.docker_file_contents += f"ENV {env_vars}\n"
|
|
322
|
+
elif isinstance(step, PathStep):
|
|
323
|
+
self.docker_file_contents += f"ENV PATH={step.path}:$PATH\n"
|
|
324
|
+
elif isinstance(step, UseStep):
|
|
325
|
+
for dependency in step.dependencies:
|
|
326
|
+
self.add_dependency(dependency)
|
|
327
|
+
|
|
328
|
+
self.docker_ignore_path.write_text("""
|
|
329
|
+
.shipit
|
|
330
|
+
Shipit
|
|
331
|
+
""")
|
|
332
|
+
|
|
333
|
+
def build_assets(self, assets: Dict[str, str]) -> None:
|
|
334
|
+
raise NotImplementedError
|
|
335
|
+
|
|
336
|
+
def get_path(self) -> Path:
|
|
337
|
+
return Path("/")
|
|
338
|
+
|
|
339
|
+
def get_build_path(self) -> Path:
|
|
340
|
+
return self.get_path() / "app"
|
|
341
|
+
|
|
342
|
+
def get_serve_path(self) -> Path:
|
|
343
|
+
return self.get_path() / "serve"
|
|
344
|
+
|
|
345
|
+
def get_assets_path(self) -> Path:
|
|
346
|
+
path = self.get_path() / "assets"
|
|
347
|
+
self.mkdir(path)
|
|
348
|
+
return path
|
|
349
|
+
|
|
350
|
+
def prepare(self, env: Dict[str, str], prepare: List[PrepareStep]) -> None:
|
|
351
|
+
raise NotImplementedError
|
|
352
|
+
|
|
353
|
+
def build_serve(self, serve: Serve) -> None:
|
|
354
|
+
serve_command_path = self.mkdir(Path("serve") / "bin")
|
|
355
|
+
console.print(f"[bold]Serve Commands:[/bold]")
|
|
356
|
+
for dep in serve.deps:
|
|
357
|
+
self.add_dependency(dep)
|
|
358
|
+
|
|
359
|
+
build_path = self.get_build_path()
|
|
360
|
+
for command in serve.commands:
|
|
361
|
+
console.print(f"* {command}")
|
|
362
|
+
command_path = serve_command_path / command
|
|
363
|
+
self.create_file(
|
|
364
|
+
command_path,
|
|
365
|
+
f"#!/bin/bash\ncd {build_path}\n{serve.commands[command]}",
|
|
366
|
+
mode=0o755,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def serve_mount(self, name: str) -> str:
|
|
370
|
+
path = self.mkdir(Path("serve") / "mounts" / name)
|
|
371
|
+
return str(path.absolute())
|
|
372
|
+
|
|
373
|
+
def get_asset(self, name: str) -> str:
|
|
374
|
+
asset_path = ASSETS_PATH / name
|
|
375
|
+
return asset_path.read_text()
|
|
376
|
+
|
|
377
|
+
def run_serve_command(self, command: str) -> None:
|
|
378
|
+
path = self.shipit_docker_path / "serve" / "bin" / command
|
|
379
|
+
self.run_command(str(path))
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class LocalBuilder:
|
|
383
|
+
def __init__(self, src_dir: Path) -> None:
|
|
384
|
+
self.src_dir = src_dir
|
|
385
|
+
self.local_path = self.src_dir / ".shipit" / "local"
|
|
386
|
+
self.prepare_bash_script = self.local_path / "prepare" / "prepare.sh"
|
|
387
|
+
|
|
388
|
+
def execute_step(self, step: Step, env: Dict[str, str], build_path: Path) -> None:
|
|
389
|
+
if isinstance(step, UseStep):
|
|
390
|
+
console.print(f"[bold]Using dependencies:[/bold] {step.dependencies}")
|
|
391
|
+
elif isinstance(step, RunStep):
|
|
392
|
+
extra = ""
|
|
393
|
+
if step.inputs:
|
|
394
|
+
for input in step.inputs:
|
|
395
|
+
copy((self.src_dir / input), (build_path / input))
|
|
396
|
+
all_inputs = ", ".join(step.inputs)
|
|
397
|
+
extra = f" [bright_black]# using {all_inputs}[/bright_black]"
|
|
398
|
+
console.print(
|
|
399
|
+
f"[bright_black]$[/bright_black] [bold]{step.command}[/bold]{extra}"
|
|
400
|
+
)
|
|
401
|
+
command_line = step.command
|
|
402
|
+
parts = shlex.split(command_line)
|
|
403
|
+
program = parts[0]
|
|
404
|
+
extended_paths = [
|
|
405
|
+
str(build_path / path) for path in env["PATH"].split(os.pathsep)
|
|
406
|
+
]
|
|
407
|
+
extended_paths.append(os.environ["PATH"])
|
|
408
|
+
PATH = os.pathsep.join(extended_paths) # type: ignore
|
|
409
|
+
exe = shutil.which(program, path=PATH)
|
|
410
|
+
if not exe:
|
|
411
|
+
raise Exception(f"Program is not installed: {program}")
|
|
412
|
+
cmd = sh.Command(exe) # "grep"
|
|
413
|
+
result = cmd(
|
|
414
|
+
*parts[1:],
|
|
415
|
+
_env={**env, "PATH": PATH},
|
|
416
|
+
_cwd=build_path,
|
|
417
|
+
_out=write_stdout,
|
|
418
|
+
_err=write_stderr,
|
|
419
|
+
)
|
|
420
|
+
elif isinstance(step, CopyStep):
|
|
421
|
+
ignore_extra = ""
|
|
422
|
+
if step.ignore:
|
|
423
|
+
ignore_extra = (
|
|
424
|
+
f" [bright_black]# ignoring {', '.join(step.ignore)}[/bright_black]"
|
|
425
|
+
)
|
|
426
|
+
if step.target == ".":
|
|
427
|
+
console.print(f"[bold]Copy from {step.source}[/bold]{ignore_extra}")
|
|
428
|
+
else:
|
|
429
|
+
console.print(
|
|
430
|
+
f"[bold]Copy to {step.target} from {step.source}[/bold]{ignore_extra}"
|
|
431
|
+
)
|
|
432
|
+
ignore_matches = step.ignore if step.ignore else []
|
|
433
|
+
ignore_matches.append(".shipit")
|
|
434
|
+
ignore_matches.append("Shipit")
|
|
435
|
+
copytree(
|
|
436
|
+
(self.src_dir / step.source),
|
|
437
|
+
(build_path / step.target),
|
|
438
|
+
dirs_exist_ok=True,
|
|
439
|
+
ignore=ignore_patterns(*ignore_matches),
|
|
440
|
+
)
|
|
441
|
+
elif isinstance(step, EnvStep):
|
|
442
|
+
print(f"Setting environment variables: {step}")
|
|
443
|
+
env.update(step.variables)
|
|
444
|
+
elif isinstance(step, PathStep):
|
|
445
|
+
console.print(f"[bold]Add {step.path}[/bold] to PATH")
|
|
446
|
+
fullpath = step.path
|
|
447
|
+
env["PATH"] = f"{fullpath}{os.pathsep}{env['PATH']}"
|
|
448
|
+
else:
|
|
449
|
+
raise Exception(f"Unknown step type: {type(step)}")
|
|
450
|
+
|
|
451
|
+
def build(self, env: Dict[str, str], steps: List[Step]) -> None:
|
|
452
|
+
console.print(f"\n[bold]Building package[/bold]")
|
|
453
|
+
base_path = self.local_path
|
|
454
|
+
shutil.rmtree(base_path, ignore_errors=True)
|
|
455
|
+
base_path.mkdir(parents=True, exist_ok=True)
|
|
456
|
+
temp_path = base_path / "build"
|
|
457
|
+
temp_path.mkdir(exist_ok=False)
|
|
458
|
+
logging.info(f"Initialized temporary build path: {temp_path}")
|
|
459
|
+
for step in steps:
|
|
460
|
+
console.print(Rule(characters="-", style="bright_black"))
|
|
461
|
+
self.execute_step(step, env, temp_path)
|
|
462
|
+
|
|
463
|
+
if "PATH" in env:
|
|
464
|
+
path = base_path / ".path"
|
|
465
|
+
path.write_text(env["PATH"]) # type: ignore
|
|
466
|
+
|
|
467
|
+
console.print(Rule(characters="-", style="bright_black"))
|
|
468
|
+
console.print(f"[bold]Build complete ✅[/bold]")
|
|
469
|
+
|
|
470
|
+
def mkdir(self, path: Path) -> Path:
|
|
471
|
+
path = self.get_path() / path
|
|
472
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
473
|
+
return path.absolute()
|
|
474
|
+
|
|
475
|
+
def create_file(self, path: Path, content: str, mode: int = 0o755) -> Path:
|
|
476
|
+
path.write_text(content)
|
|
477
|
+
path.chmod(mode)
|
|
478
|
+
return path.absolute()
|
|
479
|
+
|
|
480
|
+
def run_command(self, command: str, extra_args: Optional[List[str]] = None) -> Any:
|
|
481
|
+
return sh.Command(command)(
|
|
482
|
+
*(extra_args or []),
|
|
483
|
+
_out=write_stdout,
|
|
484
|
+
_err=write_stderr,
|
|
485
|
+
_env=os.environ,
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
def getenv(self, name: str) -> Optional[str]:
|
|
489
|
+
return os.environ.get(name)
|
|
490
|
+
|
|
491
|
+
def get_path(self) -> Path:
|
|
492
|
+
return self.local_path
|
|
493
|
+
|
|
494
|
+
def get_build_path(self) -> Path:
|
|
495
|
+
return self.get_path() / "build"
|
|
496
|
+
|
|
497
|
+
def get_serve_path(self) -> Path:
|
|
498
|
+
return self.get_path() / "serve"
|
|
499
|
+
|
|
500
|
+
def get_assets_path(self) -> Path:
|
|
501
|
+
path = self.get_path() / "assets"
|
|
502
|
+
self.mkdir(path)
|
|
503
|
+
return path
|
|
504
|
+
|
|
505
|
+
def build_assets(self, assets: Dict[str, str]) -> None:
|
|
506
|
+
assets_path = self.get_assets_path()
|
|
507
|
+
for asset in assets:
|
|
508
|
+
asset_path = assets_path / asset
|
|
509
|
+
self.create_file(asset_path, assets[asset])
|
|
510
|
+
|
|
511
|
+
def build_prepare(self, serve: Serve) -> None:
|
|
512
|
+
app_dir = self.get_build_path()
|
|
513
|
+
self.prepare_bash_script.parent.mkdir(parents=True, exist_ok=True)
|
|
514
|
+
commands: List[str] = []
|
|
515
|
+
if serve.prepare:
|
|
516
|
+
for step in serve.prepare:
|
|
517
|
+
if isinstance(step, RunStep):
|
|
518
|
+
commands.append(step.command)
|
|
519
|
+
content = "#!/bin/bash\ncd {app_dir}\n{body}".format(
|
|
520
|
+
app_dir=app_dir, body="\n".join(commands)
|
|
521
|
+
)
|
|
522
|
+
self.prepare_bash_script.write_text(content)
|
|
523
|
+
self.prepare_bash_script.chmod(0o755)
|
|
524
|
+
|
|
525
|
+
def finalize_build(self, serve: Serve) -> None:
|
|
526
|
+
pass
|
|
527
|
+
|
|
528
|
+
def prepare(self, env: Dict[str, str], prepare: List[PrepareStep]) -> None:
|
|
529
|
+
sh.Command(f"{self.prepare_bash_script.absolute()}")(
|
|
530
|
+
_out=write_stdout, _err=write_stderr
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
def build_serve(self, serve: Serve) -> None:
|
|
534
|
+
console.print("\n[bold]Building serve[/bold]")
|
|
535
|
+
build_path = self.get_build_path()
|
|
536
|
+
serve_command_path = self.get_serve_path() / "bin"
|
|
537
|
+
serve_command_path.mkdir(parents=True, exist_ok=False)
|
|
538
|
+
path = self.get_path() / ".path"
|
|
539
|
+
# path_resolved = [str((build_path/path).resolve()) for path in path.read_text().split(os.pathsep) if path]
|
|
540
|
+
# path_text = os.pathsep.join(path_resolved)
|
|
541
|
+
path_text = path.read_text()
|
|
542
|
+
console.print(f"[bold]Serve Commands:[/bold]")
|
|
543
|
+
for command in serve.commands:
|
|
544
|
+
console.print(f"* {command}")
|
|
545
|
+
command_path = serve_command_path / command
|
|
546
|
+
command_path.write_text(
|
|
547
|
+
f"#!/bin/bash\ncd {build_path}\nPATH={path_text}:$PATH {serve.commands[command]}"
|
|
548
|
+
)
|
|
549
|
+
command_path.chmod(0o755)
|
|
550
|
+
|
|
551
|
+
def run_serve_command(self, command: str) -> None:
|
|
552
|
+
console.print(f"\n[bold]Running {command} command[/bold]")
|
|
553
|
+
base_path = self.get_serve_path() / "bin"
|
|
554
|
+
command_path = base_path / command
|
|
555
|
+
sh.Command(str(command_path))(_out=write_stdout, _err=write_stderr)
|
|
556
|
+
|
|
557
|
+
def serve_mount(self, name: str) -> str:
|
|
558
|
+
base_path = self.get_serve_path() / "mounts" / name
|
|
559
|
+
base_path.mkdir(parents=True, exist_ok=True)
|
|
560
|
+
return str(base_path.absolute())
|
|
561
|
+
|
|
562
|
+
def get_asset(self, name: str) -> str:
|
|
563
|
+
asset_path = ASSETS_PATH / name
|
|
564
|
+
return asset_path.read_text()
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
class WasmerBuilder:
|
|
568
|
+
mapper: Dict[str, MapperItem] = {
|
|
569
|
+
"python": {
|
|
570
|
+
"dependencies": {
|
|
571
|
+
"latest": "wasmer/python-native@=0.1.11",
|
|
572
|
+
"3.13": "wasmer/python-native@=0.1.11",
|
|
573
|
+
},
|
|
574
|
+
"scripts": {"python"},
|
|
575
|
+
"aliases": {},
|
|
576
|
+
"env": {
|
|
577
|
+
"PYTHONEXECUTABLE": "/bin/python",
|
|
578
|
+
"PYTHONHOME": "/cpython",
|
|
579
|
+
"PYTHONPATH": "/.venv/lib/python3.13/site-packages",
|
|
580
|
+
"HOME": "/app",
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
"php": {
|
|
584
|
+
"dependencies": {
|
|
585
|
+
"latest": "php/php-32@=8.3.2104",
|
|
586
|
+
"8.3": "php/php-32@=8.3.2104",
|
|
587
|
+
},
|
|
588
|
+
"scripts": {"php"},
|
|
589
|
+
"aliases": {},
|
|
590
|
+
"env": {},
|
|
591
|
+
},
|
|
592
|
+
"bash": {
|
|
593
|
+
"dependencies": {
|
|
594
|
+
"latest": "wasmer/bash@=1.0.24",
|
|
595
|
+
"8.3": "wasmer/bash@=1.0.24",
|
|
596
|
+
},
|
|
597
|
+
"scripts": {"bash", "sh"},
|
|
598
|
+
"aliases": {},
|
|
599
|
+
"env": {},
|
|
600
|
+
},
|
|
601
|
+
"static-web-server": {
|
|
602
|
+
"dependencies": {
|
|
603
|
+
"latest": "wasmer/static-web-server@=1.1.0",
|
|
604
|
+
"2.38.0": "wasmer/static-web-server@=1.1.0",
|
|
605
|
+
"0.1": "wasmer/static-web-server@=1.1.0",
|
|
606
|
+
},
|
|
607
|
+
"scripts": {"webserver"},
|
|
608
|
+
"aliases": {"static-web-server": "webserver"},
|
|
609
|
+
"env": {},
|
|
610
|
+
},
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
def __init__(
|
|
614
|
+
self,
|
|
615
|
+
inner_builder: Builder,
|
|
616
|
+
src_dir: Path,
|
|
617
|
+
registry: Optional[str] = None,
|
|
618
|
+
token: Optional[str] = None,
|
|
619
|
+
) -> None:
|
|
620
|
+
self.src_dir = src_dir
|
|
621
|
+
self.inner_builder = inner_builder
|
|
622
|
+
# The path where we store the directory of the wasmer app in the inner builder
|
|
623
|
+
self.wasmer_dir_path = Path(self.src_dir / ".shipit" / "wasmer_dir")
|
|
624
|
+
self.wasmer_registry = registry
|
|
625
|
+
self.wasmer_token = token
|
|
626
|
+
self.default_env = {
|
|
627
|
+
"SHIPIT_PYTHON_EXTRA_INDEX_URL": "https://pythonindex.wasix.org/simple",
|
|
628
|
+
"SHIPIT_PYTHON_CROSS_PLATFORM": "wasix_wasm32",
|
|
629
|
+
# "SHIPIT_PYTHON_PRECOMPILE": "true",
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
def getenv(self, name: str) -> Optional[str]:
|
|
633
|
+
return self.inner_builder.getenv(name) or self.default_env.get(name)
|
|
634
|
+
|
|
635
|
+
def build(self, env: Dict[str, str], build: List[Step]) -> None:
|
|
636
|
+
return self.inner_builder.build(env, build)
|
|
637
|
+
|
|
638
|
+
def build_assets(self, assets: Dict[str, str]) -> None:
|
|
639
|
+
return self.inner_builder.build_assets(assets)
|
|
640
|
+
|
|
641
|
+
def get_build_path(self) -> Path:
|
|
642
|
+
return Path("/app")
|
|
643
|
+
|
|
644
|
+
def build_prepare(self, serve: Serve) -> None:
|
|
645
|
+
print("Building prepare")
|
|
646
|
+
inner = cast(Any, self.inner_builder)
|
|
647
|
+
prepare_dir = inner.mkdir(Path("wasmer") / "prepare")
|
|
648
|
+
env = {}
|
|
649
|
+
for dep in serve.deps:
|
|
650
|
+
if dep.name in self.mapper:
|
|
651
|
+
dep_env = self.mapper[dep.name].get("env")
|
|
652
|
+
if dep_env is not None:
|
|
653
|
+
env.update(dep_env)
|
|
654
|
+
if env:
|
|
655
|
+
env_lines = [f"export {k}={v}" for k, v in env.items()]
|
|
656
|
+
env_lines = "\n".join(env_lines)
|
|
657
|
+
else:
|
|
658
|
+
env_lines = ""
|
|
659
|
+
|
|
660
|
+
commands: List[str] = []
|
|
661
|
+
if serve.prepare:
|
|
662
|
+
for step in serve.prepare:
|
|
663
|
+
if isinstance(step, RunStep):
|
|
664
|
+
commands.append(step.command)
|
|
665
|
+
body = "\n".join(filter(None, [env_lines, *commands]))
|
|
666
|
+
inner.create_file(
|
|
667
|
+
Path(prepare_dir) / "prepare.sh",
|
|
668
|
+
f"#!/bin/bash\ncd /app\n{body}",
|
|
669
|
+
mode=0o755,
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
def finalize_build(self, serve: Serve) -> None:
|
|
673
|
+
inner = cast(Any, self.inner_builder)
|
|
674
|
+
inner.finalize_build(serve)
|
|
675
|
+
|
|
676
|
+
def prepare(self, env: Dict[str, str], prepare: List[PrepareStep]) -> None:
|
|
677
|
+
inner = cast(Any, self.inner_builder)
|
|
678
|
+
prepare_dir = inner.mkdir(Path("wasmer") / "prepare")
|
|
679
|
+
self.run_serve_command(
|
|
680
|
+
"bash",
|
|
681
|
+
extra_args=[
|
|
682
|
+
f"--mapdir=/prepare:{prepare_dir}",
|
|
683
|
+
"--",
|
|
684
|
+
"/prepare/prepare.sh",
|
|
685
|
+
],
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
def build_serve(self, serve: Serve) -> None:
|
|
689
|
+
from tomlkit import comment, document, nl, table, aot, string
|
|
690
|
+
|
|
691
|
+
doc = document()
|
|
692
|
+
doc.add(comment(f"File generated by Shipit {shipit_version}"))
|
|
693
|
+
package = table()
|
|
694
|
+
doc.add("package", package)
|
|
695
|
+
package.add("entrypoint", "start")
|
|
696
|
+
dependencies = table()
|
|
697
|
+
doc.add("dependencies", dependencies)
|
|
698
|
+
|
|
699
|
+
binaries = {}
|
|
700
|
+
|
|
701
|
+
deps = serve.deps or []
|
|
702
|
+
# We add bash if it's not present, as the prepare command is run in bash
|
|
703
|
+
if serve.prepare:
|
|
704
|
+
if not any(dep.name == "bash" for dep in deps):
|
|
705
|
+
deps.append(Package("bash"))
|
|
706
|
+
|
|
707
|
+
if deps:
|
|
708
|
+
console.print(f"[bold]Mapping dependencies:[/bold]")
|
|
709
|
+
for dep in deps:
|
|
710
|
+
if dep.name in self.mapper:
|
|
711
|
+
version = dep.version or "latest"
|
|
712
|
+
if version in self.mapper[dep.name]["dependencies"]:
|
|
713
|
+
console.print(
|
|
714
|
+
f"* {dep.name}@{version} mapped to {self.mapper[dep.name]['dependencies'][version]}"
|
|
715
|
+
)
|
|
716
|
+
package_name, version = self.mapper[dep.name]["dependencies"][
|
|
717
|
+
version
|
|
718
|
+
].split("@")
|
|
719
|
+
dependencies.add(package_name, version)
|
|
720
|
+
for script in self.mapper[dep.name]["scripts"]:
|
|
721
|
+
binaries[script] = {
|
|
722
|
+
"script": f"{package_name}:{script}",
|
|
723
|
+
"env": self.mapper[dep.name].get("env"),
|
|
724
|
+
}
|
|
725
|
+
for alias, script in self.mapper[dep.name]["aliases"].items():
|
|
726
|
+
binaries[alias] = {
|
|
727
|
+
"script": f"{package_name}:{script}",
|
|
728
|
+
"env": self.mapper[dep.name].get("env"),
|
|
729
|
+
}
|
|
730
|
+
else:
|
|
731
|
+
raise Exception(
|
|
732
|
+
f"Dependency {dep.name}@{version} not found in Wasmer"
|
|
733
|
+
)
|
|
734
|
+
else:
|
|
735
|
+
raise Exception(f"Dependency {dep.name} not found in Wasmer")
|
|
736
|
+
|
|
737
|
+
fs = table()
|
|
738
|
+
doc.add("fs", fs)
|
|
739
|
+
inner = cast(Any, self.inner_builder)
|
|
740
|
+
if serve.assets:
|
|
741
|
+
fs.add("/assets", str((inner.get_path() / "assets").absolute()))
|
|
742
|
+
fs.add("/app", str(inner.get_build_path().absolute()))
|
|
743
|
+
if serve.mounts:
|
|
744
|
+
for mount in serve.mounts:
|
|
745
|
+
fs.add(mount, serve.mounts[mount])
|
|
746
|
+
|
|
747
|
+
doc.add(nl())
|
|
748
|
+
if serve.commands:
|
|
749
|
+
commands = aot()
|
|
750
|
+
doc.add("command", commands)
|
|
751
|
+
for command_name, command_line in serve.commands.items():
|
|
752
|
+
command = table()
|
|
753
|
+
commands.append(command)
|
|
754
|
+
parts = shlex.split(command_line)
|
|
755
|
+
program = parts[0]
|
|
756
|
+
command.add("name", command_name)
|
|
757
|
+
program_binary = binaries[program]
|
|
758
|
+
command.add("module", program_binary["script"])
|
|
759
|
+
command.add("runner", "wasi")
|
|
760
|
+
wasi_args = table()
|
|
761
|
+
wasi_args.add("cwd", "/app")
|
|
762
|
+
wasi_args.add("main-args", parts[1:])
|
|
763
|
+
env = program_binary.get("env")
|
|
764
|
+
if env is not None:
|
|
765
|
+
wasi_args.add(
|
|
766
|
+
"env",
|
|
767
|
+
[f"{k}={v}" for k, v in env.items()],
|
|
768
|
+
)
|
|
769
|
+
title = string("annotations.wasi", literal=False)
|
|
770
|
+
command.add(title, wasi_args)
|
|
771
|
+
|
|
772
|
+
inner = cast(Any, self.inner_builder)
|
|
773
|
+
wasmer_dir = inner.mkdir(Path("wasmer"))
|
|
774
|
+
# Dump the wasmer_dir path to a file
|
|
775
|
+
self.wasmer_dir_path.write_text(str(wasmer_dir))
|
|
776
|
+
|
|
777
|
+
manifest = doc.as_string().replace(
|
|
778
|
+
'[command."annotations.wasi"]', "[command.annotations.wasi]"
|
|
779
|
+
)
|
|
780
|
+
console.print(f"\n[bold]Created wasmer.toml manifest ✅[/bold]")
|
|
781
|
+
manifest_panel = Panel(
|
|
782
|
+
Syntax(
|
|
783
|
+
manifest.strip(),
|
|
784
|
+
"toml",
|
|
785
|
+
theme="monokai",
|
|
786
|
+
background_color="default",
|
|
787
|
+
line_numbers=True,
|
|
788
|
+
),
|
|
789
|
+
box=box.SQUARE,
|
|
790
|
+
border_style="bright_black",
|
|
791
|
+
expand=False,
|
|
792
|
+
)
|
|
793
|
+
console.print(manifest_panel, markup=False, highlight=True)
|
|
794
|
+
inner.create_file(Path(wasmer_dir) / "wasmer.toml", manifest)
|
|
795
|
+
|
|
796
|
+
# Crete app.yaml
|
|
797
|
+
yaml_config = {
|
|
798
|
+
"kind": "wasmer.io/App.v0",
|
|
799
|
+
"package": ".",
|
|
800
|
+
# "capabilities": {
|
|
801
|
+
# "database": {
|
|
802
|
+
# "engine": "mysql"
|
|
803
|
+
# }
|
|
804
|
+
# },
|
|
805
|
+
# "volumes": [
|
|
806
|
+
# {
|
|
807
|
+
# "name": "wp-content",
|
|
808
|
+
# "mount": "/app/wp-content"
|
|
809
|
+
# }
|
|
810
|
+
# ]
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
app_yaml = yaml.dump(yaml_config)
|
|
814
|
+
inner.create_file(Path(wasmer_dir) / "app.yaml", app_yaml)
|
|
815
|
+
|
|
816
|
+
# self.inner_builder.build_serve(serve)
|
|
817
|
+
|
|
818
|
+
@property
|
|
819
|
+
def wasmer_path(self) -> Path:
|
|
820
|
+
return self.wasmer_dir_path.read_text()
|
|
821
|
+
|
|
822
|
+
def run_serve_command(
|
|
823
|
+
self, command: str, extra_args: Optional[List[str]] = None
|
|
824
|
+
) -> None:
|
|
825
|
+
console.print(f"\n[bold]Serving site[/bold]: running {command} command")
|
|
826
|
+
wasmer_path = self.wasmer_dir_path.read_text()
|
|
827
|
+
extra_args = extra_args or []
|
|
828
|
+
|
|
829
|
+
if self.wasmer_registry:
|
|
830
|
+
extra_args = [f"--registry={self.wasmer_registry}"] + extra_args
|
|
831
|
+
self.inner_builder.run_command(
|
|
832
|
+
"wasmer",
|
|
833
|
+
["run", str(wasmer_path), "--net", f"--command={command}", *extra_args],
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
def serve_mount(self, name: str) -> str:
|
|
837
|
+
return self.inner_builder.serve_mount(name)
|
|
838
|
+
|
|
839
|
+
def get_asset(self, name: str) -> str:
|
|
840
|
+
return self.inner_builder.get_asset(name)
|
|
841
|
+
|
|
842
|
+
def run_command(
|
|
843
|
+
self, command: str, extra_args: Optional[List[str]] | None = None
|
|
844
|
+
) -> Any:
|
|
845
|
+
return self.inner_builder.run_command(command, extra_args or [])
|
|
846
|
+
|
|
847
|
+
def deploy(
|
|
848
|
+
self, app_owner: Optional[str] = None, app_name: Optional[str] = None
|
|
849
|
+
) -> str:
|
|
850
|
+
if not app_owner or not app_name:
|
|
851
|
+
raise Exception("app_owner and app_name must be set")
|
|
852
|
+
extra_args = []
|
|
853
|
+
if self.wasmer_registry:
|
|
854
|
+
extra_args += ["--registry", self.wasmer_registry]
|
|
855
|
+
if self.wasmer_token:
|
|
856
|
+
extra_args += ["--token", self.wasmer_token]
|
|
857
|
+
self.inner_builder.run_command(
|
|
858
|
+
"wasmer",
|
|
859
|
+
[
|
|
860
|
+
"package",
|
|
861
|
+
"push",
|
|
862
|
+
self.wasmer_path,
|
|
863
|
+
"--namespace",
|
|
864
|
+
app_owner,
|
|
865
|
+
"--non-interactive",
|
|
866
|
+
*extra_args,
|
|
867
|
+
],
|
|
868
|
+
)
|
|
869
|
+
return self.inner_builder.run_command(
|
|
870
|
+
"wasmer",
|
|
871
|
+
[
|
|
872
|
+
"deploy",
|
|
873
|
+
"--publish-package",
|
|
874
|
+
"--dir",
|
|
875
|
+
self.wasmer_path,
|
|
876
|
+
"--app-name",
|
|
877
|
+
app_name,
|
|
878
|
+
"--owner",
|
|
879
|
+
app_owner,
|
|
880
|
+
"--non-interactive",
|
|
881
|
+
*extra_args,
|
|
882
|
+
],
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
class Ctx:
|
|
887
|
+
def __init__(self, builder: Builder) -> None:
|
|
888
|
+
self.builder = builder
|
|
889
|
+
self.packages: Dict[str, Package] = {}
|
|
890
|
+
self.builds: List[Build] = []
|
|
891
|
+
self.steps: List[Step] = []
|
|
892
|
+
self.serves: Dict[str, Serve] = {}
|
|
893
|
+
|
|
894
|
+
def add_package(self, package: Package) -> str:
|
|
895
|
+
index = f"{package.name}@{package.version}" if package.version else package.name
|
|
896
|
+
self.packages[index] = package
|
|
897
|
+
return f"ref:package:{index}"
|
|
898
|
+
|
|
899
|
+
def get_ref(self, index: str) -> Any:
|
|
900
|
+
if index.startswith("ref:package:"):
|
|
901
|
+
return self.packages[index[len("ref:package:") :]]
|
|
902
|
+
elif index.startswith("ref:build:"):
|
|
903
|
+
return self.builds[int(index[len("ref:build:") :])]
|
|
904
|
+
elif index.startswith("ref:serve:"):
|
|
905
|
+
return self.serves[index[len("ref:serve:") :]]
|
|
906
|
+
elif index.startswith("ref:step:"):
|
|
907
|
+
return self.steps[int(index[len("ref:step:") :])]
|
|
908
|
+
else:
|
|
909
|
+
raise Exception(f"Invalid reference: {index}")
|
|
910
|
+
|
|
911
|
+
def get_refs(self, indices: List[str]) -> List[Any]:
|
|
912
|
+
return [self.get_ref(index) for index in indices if index is not None]
|
|
913
|
+
|
|
914
|
+
def add_build(self, build: Build) -> str:
|
|
915
|
+
self.builds.append(build)
|
|
916
|
+
return f"ref:build:{len(self.builds) - 1}"
|
|
917
|
+
|
|
918
|
+
def add_serve(self, serve: Serve) -> str:
|
|
919
|
+
self.serves[serve.name] = serve
|
|
920
|
+
return f"ref:serve:{serve.name}"
|
|
921
|
+
|
|
922
|
+
def add_step(self, step: Step) -> Optional[str]:
|
|
923
|
+
if step is None:
|
|
924
|
+
return None
|
|
925
|
+
self.steps.append(step)
|
|
926
|
+
return f"ref:step:{len(self.steps) - 1}"
|
|
927
|
+
|
|
928
|
+
def getenv(self, name: str) -> Optional[str]:
|
|
929
|
+
return self.builder.getenv(name)
|
|
930
|
+
|
|
931
|
+
def get_asset(self, name: str) -> Optional[str]:
|
|
932
|
+
return self.builder.get_asset(name)
|
|
933
|
+
|
|
934
|
+
def dep(self, name: str, version: Optional[str] = None) -> str:
|
|
935
|
+
package = Package(name, version)
|
|
936
|
+
return self.add_package(package)
|
|
937
|
+
|
|
938
|
+
def serve(
|
|
939
|
+
self,
|
|
940
|
+
name: str,
|
|
941
|
+
provider: str,
|
|
942
|
+
build: List[str],
|
|
943
|
+
deps: List[str],
|
|
944
|
+
commands: Dict[str, str],
|
|
945
|
+
assets: Optional[Dict[str, str]] = None,
|
|
946
|
+
prepare: Optional[List[str]] = None,
|
|
947
|
+
workers: Optional[List[str]] = None,
|
|
948
|
+
mounts: Optional[Dict[str, str]] = None,
|
|
949
|
+
) -> str:
|
|
950
|
+
build_refs = [cast(Step, r) for r in self.get_refs(build)]
|
|
951
|
+
prepare_steps: Optional[List[PrepareStep]] = None
|
|
952
|
+
if prepare is not None:
|
|
953
|
+
# Resolve referenced steps and keep only RunStep for prepare
|
|
954
|
+
resolved = [cast(Step, r) for r in self.get_refs(prepare)]
|
|
955
|
+
prepare_steps = [
|
|
956
|
+
cast(RunStep, s) for s in resolved if isinstance(s, RunStep)
|
|
957
|
+
]
|
|
958
|
+
dep_refs = [cast(Package, r) for r in self.get_refs(deps)]
|
|
959
|
+
serve = Serve(
|
|
960
|
+
name=name,
|
|
961
|
+
provider=provider,
|
|
962
|
+
build=build_refs,
|
|
963
|
+
assets=assets,
|
|
964
|
+
deps=dep_refs,
|
|
965
|
+
commands=commands,
|
|
966
|
+
prepare=prepare_steps,
|
|
967
|
+
workers=workers,
|
|
968
|
+
mounts=mounts,
|
|
969
|
+
)
|
|
970
|
+
return self.add_serve(serve)
|
|
971
|
+
|
|
972
|
+
def path(self, path: str) -> Optional[str]:
|
|
973
|
+
step = PathStep(path)
|
|
974
|
+
return self.add_step(step)
|
|
975
|
+
|
|
976
|
+
def use(self, *dependencies: str) -> Optional[str]:
|
|
977
|
+
deps = [cast(Package, r) for r in self.get_refs(list(dependencies))]
|
|
978
|
+
step = UseStep(deps)
|
|
979
|
+
return self.add_step(step)
|
|
980
|
+
|
|
981
|
+
def run(self, *args: Any, **kwargs: Any) -> Optional[str]:
|
|
982
|
+
step = RunStep(*args, **kwargs)
|
|
983
|
+
return self.add_step(step)
|
|
984
|
+
|
|
985
|
+
def copy(
|
|
986
|
+
self, source: str, target: str, ignore: Optional[List[str]] = None
|
|
987
|
+
) -> Optional[str]:
|
|
988
|
+
step = CopyStep(source, target, ignore)
|
|
989
|
+
return self.add_step(step)
|
|
990
|
+
|
|
991
|
+
def buildpath(self, name: str) -> str:
|
|
992
|
+
return str((self.builder.get_build_path() / name).absolute())
|
|
993
|
+
|
|
994
|
+
def env(self, **env_vars: str) -> Optional[str]:
|
|
995
|
+
step = EnvStep(env_vars)
|
|
996
|
+
return self.add_step(step)
|
|
997
|
+
|
|
998
|
+
def serve_mount(self, name: str) -> Optional[str]:
|
|
999
|
+
return self.builder.serve_mount(name)
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def print_help() -> None:
|
|
1003
|
+
panel = Panel(
|
|
1004
|
+
f"Shipit {shipit_version}",
|
|
1005
|
+
box=box.ROUNDED,
|
|
1006
|
+
border_style="blue",
|
|
1007
|
+
expand=False,
|
|
1008
|
+
)
|
|
1009
|
+
console.print(panel)
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
@app.command(name="auto")
|
|
1013
|
+
def auto(
|
|
1014
|
+
path: Path = typer.Argument(
|
|
1015
|
+
Path("."),
|
|
1016
|
+
help="Project path (defaults to current directory).",
|
|
1017
|
+
show_default=False,
|
|
1018
|
+
),
|
|
1019
|
+
wasmer: bool = typer.Option(
|
|
1020
|
+
False,
|
|
1021
|
+
help="Use Wasmer to build and serve the project.",
|
|
1022
|
+
),
|
|
1023
|
+
docker: bool = typer.Option(
|
|
1024
|
+
False,
|
|
1025
|
+
help="Use Docker to build the project.",
|
|
1026
|
+
),
|
|
1027
|
+
docker_client: Optional[str] = typer.Option(
|
|
1028
|
+
None,
|
|
1029
|
+
help="Use a specific Docker client (such as depot, podman, etc.)",
|
|
1030
|
+
),
|
|
1031
|
+
start: bool = typer.Option(
|
|
1032
|
+
False,
|
|
1033
|
+
help="Run the start command after building.",
|
|
1034
|
+
),
|
|
1035
|
+
regenerate: bool = typer.Option(
|
|
1036
|
+
None,
|
|
1037
|
+
help="Regenerate the Shipit file.",
|
|
1038
|
+
),
|
|
1039
|
+
regenerate_path: Optional[Path] = typer.Option(
|
|
1040
|
+
None,
|
|
1041
|
+
help="Regenerate the Shipit file onto the provided path.",
|
|
1042
|
+
),
|
|
1043
|
+
wasmer_deploy: Optional[bool] = typer.Option(
|
|
1044
|
+
False,
|
|
1045
|
+
help="Deploy the project to Wasmer.",
|
|
1046
|
+
),
|
|
1047
|
+
wasmer_token: Optional[str] = typer.Option(
|
|
1048
|
+
None,
|
|
1049
|
+
help="Wasmer token.",
|
|
1050
|
+
),
|
|
1051
|
+
wasmer_registry: Optional[str] = typer.Option(
|
|
1052
|
+
None,
|
|
1053
|
+
help="Wasmer registry.",
|
|
1054
|
+
),
|
|
1055
|
+
wasmer_app_owner: Optional[str] = typer.Option(
|
|
1056
|
+
None,
|
|
1057
|
+
help="Owner of the Wasmer app.",
|
|
1058
|
+
),
|
|
1059
|
+
wasmer_app_name: Optional[str] = typer.Option(
|
|
1060
|
+
None,
|
|
1061
|
+
help="Name of the Wasmer app.",
|
|
1062
|
+
),
|
|
1063
|
+
):
|
|
1064
|
+
if not (path / "Shipit").exists() or regenerate or regenerate_path is not None:
|
|
1065
|
+
generate(path, out=regenerate_path)
|
|
1066
|
+
|
|
1067
|
+
build(path, wasmer=(wasmer or wasmer_deploy), docker=docker, docker_client=docker_client,)
|
|
1068
|
+
if start or wasmer_deploy:
|
|
1069
|
+
serve(
|
|
1070
|
+
path,
|
|
1071
|
+
wasmer=wasmer,
|
|
1072
|
+
docker=docker,
|
|
1073
|
+
docker_client=docker_client,
|
|
1074
|
+
start=start,
|
|
1075
|
+
wasmer_token=wasmer_token,
|
|
1076
|
+
wasmer_registry=wasmer_registry,
|
|
1077
|
+
wasmer_deploy=wasmer_deploy,
|
|
1078
|
+
wasmer_app_owner=wasmer_app_owner,
|
|
1079
|
+
wasmer_app_name=wasmer_app_name,
|
|
1080
|
+
)
|
|
1081
|
+
# deploy(path)
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
@app.command(name="generate")
|
|
1085
|
+
def generate(
|
|
1086
|
+
path: Path = typer.Argument(
|
|
1087
|
+
Path("."),
|
|
1088
|
+
help="Project path (defaults to current directory).",
|
|
1089
|
+
show_default=False,
|
|
1090
|
+
),
|
|
1091
|
+
out: Optional[Path] = typer.Option(
|
|
1092
|
+
None,
|
|
1093
|
+
help="Output path (defaults to the Shipit file in the provided path).",
|
|
1094
|
+
),
|
|
1095
|
+
):
|
|
1096
|
+
if out is None:
|
|
1097
|
+
out = path / "Shipit"
|
|
1098
|
+
content = generate_shipit(path)
|
|
1099
|
+
out.write_text(content)
|
|
1100
|
+
console.print(f"[bold]Generated Shipit[/bold] at {out.absolute()}")
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
@app.callback(
|
|
1104
|
+
invoke_without_command=True,
|
|
1105
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
1106
|
+
)
|
|
1107
|
+
def _default(ctx: typer.Context) -> None:
|
|
1108
|
+
print_help()
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
@app.command(name="deploy")
|
|
1112
|
+
def deploy(
|
|
1113
|
+
path: Path = typer.Argument(
|
|
1114
|
+
Path("."),
|
|
1115
|
+
help="Project path (defaults to current directory).",
|
|
1116
|
+
show_default=False,
|
|
1117
|
+
),
|
|
1118
|
+
) -> None:
|
|
1119
|
+
pass
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
@app.command(name="serve")
|
|
1123
|
+
def serve(
|
|
1124
|
+
path: Path = typer.Argument(
|
|
1125
|
+
Path("."),
|
|
1126
|
+
help="Project path (defaults to current directory).",
|
|
1127
|
+
show_default=False,
|
|
1128
|
+
),
|
|
1129
|
+
wasmer: bool = typer.Option(
|
|
1130
|
+
False,
|
|
1131
|
+
help="Use Wasmer to build and serve the project.",
|
|
1132
|
+
),
|
|
1133
|
+
docker: bool = typer.Option(
|
|
1134
|
+
False,
|
|
1135
|
+
help="Use Docker to build the project.",
|
|
1136
|
+
),
|
|
1137
|
+
docker_client: Optional[str] = typer.Option(
|
|
1138
|
+
None,
|
|
1139
|
+
help="Use a specific Docker client (such as depot, podman, etc.)",
|
|
1140
|
+
),
|
|
1141
|
+
start: Optional[bool] = typer.Option(
|
|
1142
|
+
True,
|
|
1143
|
+
help="Run the start command after building.",
|
|
1144
|
+
),
|
|
1145
|
+
wasmer_deploy: Optional[bool] = typer.Option(
|
|
1146
|
+
False,
|
|
1147
|
+
help="Deploy the project to Wasmer.",
|
|
1148
|
+
),
|
|
1149
|
+
wasmer_token: Optional[str] = typer.Option(
|
|
1150
|
+
None,
|
|
1151
|
+
help="Wasmer token.",
|
|
1152
|
+
),
|
|
1153
|
+
wasmer_registry: Optional[str] = typer.Option(
|
|
1154
|
+
None,
|
|
1155
|
+
help="Wasmer registry.",
|
|
1156
|
+
),
|
|
1157
|
+
wasmer_app_owner: Optional[str] = typer.Option(
|
|
1158
|
+
None,
|
|
1159
|
+
help="Owner of the Wasmer app.",
|
|
1160
|
+
),
|
|
1161
|
+
wasmer_app_name: Optional[str] = typer.Option(
|
|
1162
|
+
None,
|
|
1163
|
+
help="Name of the Wasmer app.",
|
|
1164
|
+
),
|
|
1165
|
+
) -> None:
|
|
1166
|
+
builder: Builder
|
|
1167
|
+
if docker or docker_client:
|
|
1168
|
+
builder = DockerBuilder(path, docker_client)
|
|
1169
|
+
else:
|
|
1170
|
+
builder = LocalBuilder(path)
|
|
1171
|
+
if wasmer or wasmer_deploy:
|
|
1172
|
+
builder = WasmerBuilder(
|
|
1173
|
+
builder, path, registry=wasmer_registry, token=wasmer_token
|
|
1174
|
+
)
|
|
1175
|
+
if start:
|
|
1176
|
+
builder.run_serve_command("start")
|
|
1177
|
+
|
|
1178
|
+
if wasmer_deploy:
|
|
1179
|
+
if isinstance(builder, WasmerBuilder):
|
|
1180
|
+
builder.deploy(app_owner=wasmer_app_owner, app_name=wasmer_app_name)
|
|
1181
|
+
else:
|
|
1182
|
+
raise Exception("Wasmer deploy is only supported for Wasmer builders")
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
@app.command(name="build")
|
|
1186
|
+
def build(
|
|
1187
|
+
path: Path = typer.Argument(
|
|
1188
|
+
Path("."),
|
|
1189
|
+
help="Project path (defaults to current directory).",
|
|
1190
|
+
show_default=False,
|
|
1191
|
+
),
|
|
1192
|
+
wasmer: bool = typer.Option(
|
|
1193
|
+
False,
|
|
1194
|
+
help="Use Wasmer to build and serve the project.",
|
|
1195
|
+
),
|
|
1196
|
+
docker: bool = typer.Option(
|
|
1197
|
+
False,
|
|
1198
|
+
help="Use Docker to build the project.",
|
|
1199
|
+
),
|
|
1200
|
+
docker_client: Optional[str] = typer.Option(
|
|
1201
|
+
None,
|
|
1202
|
+
help="Use a specific Docker client (such as depot, podman, etc.)",
|
|
1203
|
+
),
|
|
1204
|
+
) -> None:
|
|
1205
|
+
ab_file = path / "Shipit"
|
|
1206
|
+
if not ab_file.exists():
|
|
1207
|
+
raise FileNotFoundError(
|
|
1208
|
+
f"Shipit file not found at {ab_file}. Please run `shipit generate {path}` to create it."
|
|
1209
|
+
)
|
|
1210
|
+
source = open(ab_file).read()
|
|
1211
|
+
builder: Builder
|
|
1212
|
+
if docker or docker_client:
|
|
1213
|
+
builder = DockerBuilder(path, docker_client)
|
|
1214
|
+
else:
|
|
1215
|
+
builder = LocalBuilder(path)
|
|
1216
|
+
if wasmer:
|
|
1217
|
+
builder = WasmerBuilder(builder, path)
|
|
1218
|
+
|
|
1219
|
+
ctx = Ctx(builder)
|
|
1220
|
+
glb = sl.Globals.standard()
|
|
1221
|
+
mod = sl.Module()
|
|
1222
|
+
|
|
1223
|
+
mod.add_callable("getenv", ctx.getenv)
|
|
1224
|
+
mod.add_callable("dep", ctx.dep)
|
|
1225
|
+
mod.add_callable("serve", ctx.serve)
|
|
1226
|
+
mod.add_callable("run", ctx.run)
|
|
1227
|
+
mod.add_callable("use", ctx.run)
|
|
1228
|
+
mod.add_callable("copy", ctx.copy)
|
|
1229
|
+
mod.add_callable("path", ctx.path)
|
|
1230
|
+
mod.add_callable("buildpath", ctx.buildpath)
|
|
1231
|
+
mod.add_callable("get_asset", ctx.get_asset)
|
|
1232
|
+
mod.add_callable("env", ctx.env)
|
|
1233
|
+
mod.add_callable("use", ctx.use)
|
|
1234
|
+
mod.add_callable("serve_mount", ctx.serve_mount)
|
|
1235
|
+
|
|
1236
|
+
dialect = sl.Dialect.extended()
|
|
1237
|
+
dialect.enable_f_strings = True
|
|
1238
|
+
|
|
1239
|
+
ast = sl.parse("shipit", source, dialect=dialect)
|
|
1240
|
+
|
|
1241
|
+
sl.eval(mod, ast, glb)
|
|
1242
|
+
# assert len(ctx.builds) == 1, "Only one build is allowed for now"
|
|
1243
|
+
assert len(ctx.serves) <= 1, "Only one serve is allowed for now"
|
|
1244
|
+
# build = ctx.builds[0]
|
|
1245
|
+
env = {
|
|
1246
|
+
"PATH": "",
|
|
1247
|
+
"COLORTERM": os.environ.get("COLORTERM", ""),
|
|
1248
|
+
"LSCOLORS": os.environ.get("LSCOLORS", "0"),
|
|
1249
|
+
"LS_COLORS": os.environ.get("LS_COLORS", "0"),
|
|
1250
|
+
"CLICOLOR": os.environ.get("CLICOLOR", "0"),
|
|
1251
|
+
}
|
|
1252
|
+
serve = next(iter(ctx.serves.values()))
|
|
1253
|
+
|
|
1254
|
+
# Build and serve
|
|
1255
|
+
builder.build(env, serve.build)
|
|
1256
|
+
if serve.prepare:
|
|
1257
|
+
builder.build_prepare(serve)
|
|
1258
|
+
if serve.assets:
|
|
1259
|
+
builder.build_assets(serve.assets)
|
|
1260
|
+
builder.build_serve(serve)
|
|
1261
|
+
builder.finalize_build(serve)
|
|
1262
|
+
if serve.prepare:
|
|
1263
|
+
builder.prepare(env, serve.prepare)
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
def main() -> None:
|
|
1267
|
+
args = sys.argv[1:]
|
|
1268
|
+
# If no subcommand or first token looks like option/path → default to "build"
|
|
1269
|
+
available_commands = [cmd.name for cmd in app.registered_commands]
|
|
1270
|
+
if not args or args[0].startswith("-") or args[0] not in available_commands:
|
|
1271
|
+
sys.argv = [sys.argv[0], "auto", *args]
|
|
1272
|
+
|
|
1273
|
+
try:
|
|
1274
|
+
app()
|
|
1275
|
+
except Exception as e:
|
|
1276
|
+
console.print(f"[bold red]{type(e).__name__}[/bold red]: {e}")
|
|
1277
|
+
# raise e
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
if __name__ == "__main__":
|
|
1281
|
+
main()
|