shipit-cli 0.6.0__py3-none-any.whl → 0.7.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/cli.py CHANGED
@@ -1,10 +1,12 @@
1
1
  import logging
2
+ import requests
2
3
  import os
3
4
  import shlex
4
5
  import shutil
5
6
  import sys
6
7
  import json
7
8
  import yaml
9
+ import base64
8
10
  from dataclasses import dataclass
9
11
  from pathlib import Path
10
12
  from typing import (
@@ -32,6 +34,8 @@ from rich.syntax import Syntax
32
34
 
33
35
  from shipit.version import version as shipit_version
34
36
  from shipit.generator import generate_shipit
37
+ from shipit.providers.base import CustomCommands
38
+ from shipit.procfile import Procfile
35
39
 
36
40
 
37
41
  console = Console()
@@ -49,10 +53,18 @@ class Mount:
49
53
  serve_path: Path
50
54
 
51
55
 
56
+ @dataclass
57
+ class Volume:
58
+ name: str
59
+ serve_path: Path
60
+
61
+
52
62
  @dataclass
53
63
  class Service:
54
64
  name: str
55
- provider: Literal["postgres", "mysql", "redis"] # Right now we only support postgres and mysql
65
+ provider: Literal[
66
+ "postgres", "mysql", "redis"
67
+ ] # Right now we only support postgres and mysql
56
68
 
57
69
 
58
70
  @dataclass
@@ -63,10 +75,10 @@ class Serve:
63
75
  deps: List["Package"]
64
76
  commands: Dict[str, str]
65
77
  cwd: Optional[str] = None
66
- assets: Optional[Dict[str, str]] = None
67
78
  prepare: Optional[List["PrepareStep"]] = None
68
79
  workers: Optional[List[str]] = None
69
80
  mounts: Optional[List[Mount]] = None
81
+ volumes: Optional[List[Volume]] = None
70
82
  env: Optional[Dict[str, str]] = None
71
83
  services: Optional[List[Service]] = None
72
84
 
@@ -98,6 +110,11 @@ class CopyStep:
98
110
  source: str
99
111
  target: str
100
112
  ignore: Optional[List[str]] = None
113
+ # We can copy from the app source or from the shipit assets folder
114
+ base: Literal["source", "assets"] = "source"
115
+
116
+ def is_download(self) -> bool:
117
+ return self.source.startswith("http://") or self.source.startswith("https://")
101
118
 
102
119
 
103
120
  @dataclass
@@ -147,7 +164,6 @@ class Builder(Protocol):
147
164
  def build(
148
165
  self, env: Dict[str, str], mounts: List[Mount], steps: List[Step]
149
166
  ) -> None: ...
150
- def build_assets(self, assets: Dict[str, str]) -> None: ...
151
167
  def build_prepare(self, serve: Serve) -> None: ...
152
168
  def build_serve(self, serve: Serve) -> None: ...
153
169
  def finalize_build(self, serve: Serve) -> None: ...
@@ -157,8 +173,6 @@ class Builder(Protocol):
157
173
  def run_command(
158
174
  self, command: str, extra_args: Optional[List[str]] | None = None
159
175
  ) -> Any: ...
160
- def serve_mount(self, name: str) -> str: ...
161
- def get_asset(self, name: str) -> str: ...
162
176
  def get_build_mount_path(self, name: str) -> Path: ...
163
177
  def get_serve_mount_path(self, name: str) -> Path: ...
164
178
 
@@ -258,13 +272,23 @@ class DockerBuilder:
258
272
 
259
273
  def run_command(self, command: str, extra_args: Optional[List[str]] = None) -> Any:
260
274
  image_name = self.docker_name_path.read_text()
261
- return sh.Command(
262
- "docker"
263
- )(
275
+ docker_args: List[str] = [
264
276
  "run",
265
277
  "-p",
266
278
  "80:80",
267
279
  "--rm",
280
+ ]
281
+ # Attach volumes if present
282
+ # if serve.volumes:
283
+ # for vol in serve.volumes:
284
+ # docker_args += [
285
+ # "--mount",
286
+ # f"type=volume,source={vol.name},target={str(vol.serve_path)}",
287
+ # ]
288
+ return sh.Command(
289
+ "docker"
290
+ )(
291
+ *docker_args,
268
292
  image_name,
269
293
  command,
270
294
  *(extra_args or []),
@@ -317,10 +341,10 @@ RUN chmod {oct(mode)[2:]} {path.absolute()}
317
341
  return
318
342
  if dependency.version:
319
343
  self.docker_file_contents += (
320
- f"RUN pkgm install {dependency.name}@{dependency.version}\n"
344
+ f"RUN mise use --global {dependency.name}@{dependency.version}\n"
321
345
  )
322
346
  else:
323
- self.docker_file_contents += f"RUN pkgm install {dependency.name}\n"
347
+ self.docker_file_contents += f"RUN mise use --global {dependency.name}\n"
324
348
 
325
349
  def build(
326
350
  self, env: Dict[str, str], mounts: List[Mount], steps: List[Step]
@@ -328,15 +352,26 @@ RUN chmod {oct(mode)[2:]} {path.absolute()}
328
352
  base_path = self.docker_path
329
353
  shutil.rmtree(base_path, ignore_errors=True)
330
354
  base_path.mkdir(parents=True, exist_ok=True)
331
- self.docker_file_contents = "FROM debian:bookworm-slim AS build\n"
355
+ self.docker_file_contents = "FROM debian:trixie-slim AS build\n"
356
+
332
357
  self.docker_file_contents += """
333
358
  RUN apt-get update \\
334
- && apt-get -y --no-install-recommends install sudo curl ca-certificates locate git zip unzip \\
359
+ && apt-get -y --no-install-recommends install \\
360
+ build-essential gcc make \\
361
+ dpkg-dev pkg-config \\
362
+ libmariadb-dev libmariadb-dev-compat libpq-dev \\
363
+ sudo curl ca-certificates \\
335
364
  && rm -rf /var/lib/apt/lists/*
336
365
 
337
366
  SHELL ["/bin/bash", "-o", "pipefail", "-c"]
338
-
339
- RUN curl https://pkgx.sh | sh
367
+ ENV MISE_DATA_DIR="/mise"
368
+ ENV MISE_CONFIG_DIR="/mise"
369
+ ENV MISE_CACHE_DIR="/mise/cache"
370
+ ENV MISE_INSTALL_PATH="/usr/local/bin/mise"
371
+ ENV PATH="/mise/shims:$PATH"
372
+ # ENV MISE_VERSION="..."
373
+
374
+ RUN curl https://mise.run | sh
340
375
  """
341
376
  # docker_file_contents += "RUN curl https://mise.run | sh\n"
342
377
  # self.docker_file_contents += """
@@ -361,7 +396,28 @@ RUN curl https://pkgx.sh | sh
361
396
  pre = ""
362
397
  self.docker_file_contents += f"RUN {pre}{step.command}\n"
363
398
  elif isinstance(step, CopyStep):
364
- self.docker_file_contents += f"COPY {step.source} {step.target}\n"
399
+ if step.is_download():
400
+ self.docker_file_contents += (
401
+ "ADD " + step.source + " " + step.target + "\n"
402
+ )
403
+ elif step.base == "assets":
404
+ # Detect if the asset exists and is a file
405
+ if (ASSETS_PATH / step.source).is_file():
406
+ # Read the file content and write it to the target file
407
+ content_base64 = base64.b64encode(
408
+ (ASSETS_PATH / step.source).read_bytes()
409
+ )
410
+ self.docker_file_contents += (
411
+ f"RUN echo '{content_base64}' | base64 -d > {step.target}\n"
412
+ )
413
+ elif (ASSETS_PATH / step.source).is_dir():
414
+ raise Exception(
415
+ f"Asset {step.source} is a directory, shipit doesn't currently support coppying assets directories inside Docker"
416
+ )
417
+ else:
418
+ raise Exception(f"Asset {step.source} does not exist")
419
+ else:
420
+ self.docker_file_contents += f"COPY {step.source} {step.target}\n"
365
421
  elif isinstance(step, EnvStep):
366
422
  env_vars = " ".join(
367
423
  [f"{key}={value}" for key, value in step.variables.items()]
@@ -386,9 +442,6 @@ FROM scratch
386
442
  Shipit
387
443
  """)
388
444
 
389
- def build_assets(self, assets: Dict[str, str]) -> None:
390
- raise NotImplementedError
391
-
392
445
  def get_path(self) -> Path:
393
446
  return Path("/")
394
447
 
@@ -398,11 +451,6 @@ Shipit
398
451
  def get_serve_path(self) -> Path:
399
452
  return self.get_path() / "serve"
400
453
 
401
- def get_assets_path(self) -> Path:
402
- path = self.get_path() / "assets"
403
- self.mkdir(path)
404
- return path
405
-
406
454
  def prepare(self, env: Dict[str, str], prepare: List[PrepareStep]) -> None:
407
455
  raise NotImplementedError
408
456
 
@@ -422,14 +470,6 @@ Shipit
422
470
  mode=0o755,
423
471
  )
424
472
 
425
- def serve_mount(self, name: str) -> str:
426
- path = self.mkdir(Path("serve") / "mounts" / name)
427
- return str(path.absolute())
428
-
429
- def get_asset(self, name: str) -> str:
430
- asset_path = ASSETS_PATH / name
431
- return asset_path.read_text()
432
-
433
473
  def run_serve_command(self, command: str) -> None:
434
474
  path = self.shipit_docker_path / "serve" / "bin" / command
435
475
  self.run_command(str(path))
@@ -507,18 +547,31 @@ class LocalBuilder:
507
547
  ignore_matches = step.ignore if step.ignore else []
508
548
  ignore_matches.append(".shipit")
509
549
  ignore_matches.append("Shipit")
510
- if (self.src_dir / step.source).is_dir():
511
- copytree(
512
- (self.src_dir / step.source),
513
- (build_path / step.target),
514
- dirs_exist_ok=True,
515
- ignore=ignore_patterns(*ignore_matches),
516
- )
550
+
551
+ if step.is_download():
552
+ download_file(step.source, (build_path / step.target))
517
553
  else:
518
- copy(
519
- (self.src_dir / step.source),
520
- (build_path / step.target),
521
- )
554
+ if step.base == "source":
555
+ base = self.src_dir
556
+ elif step.base == "assets":
557
+ base = ASSETS_PATH
558
+ else:
559
+ raise Exception(f"Unknown base: {step.base}")
560
+
561
+ if (base / step.source).is_dir():
562
+ copytree(
563
+ (base / step.source),
564
+ (build_path / step.target),
565
+ dirs_exist_ok=True,
566
+ ignore=ignore_patterns(*ignore_matches),
567
+ )
568
+ elif (base / step.source).is_file():
569
+ copy(
570
+ (base / step.source),
571
+ (build_path / step.target),
572
+ )
573
+ else:
574
+ raise Exception(f"Source {step.source} is not a file or directory")
522
575
  elif isinstance(step, EnvStep):
523
576
  print(f"Setting environment variables: {step}")
524
577
  env.update(step.variables)
@@ -580,17 +633,6 @@ class LocalBuilder:
580
633
  def get_serve_path(self) -> Path:
581
634
  return self.get_path() / "serve"
582
635
 
583
- def get_assets_path(self) -> Path:
584
- path = self.get_path() / "assets"
585
- self.mkdir(path)
586
- return path
587
-
588
- def build_assets(self, assets: Dict[str, str]) -> None:
589
- assets_path = self.get_assets_path()
590
- for asset in assets:
591
- asset_path = assets_path / asset
592
- self.create_file(asset_path, assets[asset])
593
-
594
636
  def build_prepare(self, serve: Serve) -> None:
595
637
  self.prepare_bash_script.parent.mkdir(parents=True, exist_ok=True)
596
638
  commands: List[str] = []
@@ -631,6 +673,7 @@ class LocalBuilder:
631
673
  )
632
674
 
633
675
  def build_serve(self, serve: Serve) -> None:
676
+ # Remember serve configuration for run-time
634
677
  console.print("\n[bold]Building serve[/bold]")
635
678
  serve_command_path = self.get_serve_path() / "bin"
636
679
  serve_command_path.mkdir(parents=True, exist_ok=False)
@@ -640,10 +683,12 @@ class LocalBuilder:
640
683
  for command in serve.commands:
641
684
  console.print(f"* {command}")
642
685
  command_path = serve_command_path / command
643
- content = f"#!/bin/bash\ncd {serve.cwd}\nPATH={path_text}:$PATH {serve.commands[command]}"
644
- command_path.write_text(
645
- content
646
- )
686
+ env_vars = ""
687
+ if serve.env:
688
+ env_vars = " ".join([f"{k}={v}" for k, v in serve.env.items()])
689
+
690
+ content = f"#!/bin/bash\ncd {serve.cwd}\nPATH={path_text}:$PATH {env_vars} {serve.commands[command]}"
691
+ command_path.write_text(content)
647
692
  manifest_panel = Panel(
648
693
  Syntax(
649
694
  content.strip(),
@@ -665,15 +710,6 @@ class LocalBuilder:
665
710
  command_path = base_path / command
666
711
  sh.Command(str(command_path))(_out=write_stdout, _err=write_stderr)
667
712
 
668
- def serve_mount(self, name: str) -> str:
669
- base_path = self.get_serve_path() / "mounts" / name
670
- base_path.mkdir(parents=True, exist_ok=True)
671
- return str(base_path.absolute())
672
-
673
- def get_asset(self, name: str) -> str:
674
- asset_path = ASSETS_PATH / name
675
- return asset_path.read_text()
676
-
677
713
 
678
714
  class WasmerBuilder:
679
715
  def get_build_mount_path(self, name: str) -> Path:
@@ -714,8 +750,8 @@ class WasmerBuilder:
714
750
  },
715
751
  "php": {
716
752
  "dependencies": {
717
- "latest": "php/php-32@=8.3.2104",
718
- "8.3": "php/php-32@=8.3.2104",
753
+ "latest": "php/php-32@=8.3.2102",
754
+ "8.3": "php/php-32@=8.3.2102",
719
755
  },
720
756
  "scripts": {"php"},
721
757
  "aliases": {},
@@ -771,9 +807,6 @@ class WasmerBuilder:
771
807
  ) -> None:
772
808
  return self.inner_builder.build(env, mounts, build)
773
809
 
774
- def build_assets(self, assets: Dict[str, str]) -> None:
775
- return self.inner_builder.build_assets(assets)
776
-
777
810
  def get_build_path(self) -> Path:
778
811
  return Path("/app")
779
812
 
@@ -897,9 +930,6 @@ class WasmerBuilder:
897
930
  fs = table()
898
931
  doc.add("fs", fs)
899
932
  inner = cast(Any, self.inner_builder)
900
- if serve.assets:
901
- fs.add("/assets", str((inner.get_path() / "assets").absolute()))
902
- # fs.add("/app", str(inner.get_build_path().absolute()))
903
933
  if serve.mounts:
904
934
  for mount in serve.mounts:
905
935
  fs.add(
@@ -975,22 +1005,37 @@ class WasmerBuilder:
975
1005
  # has_postgres = any(service.provider == "postgres" for service in serve.services)
976
1006
  # has_redis = any(service.provider == "redis" for service in serve.services)
977
1007
  if has_mysql:
978
- capabilities["database"] = {
979
- "engine": "mysql"
980
- }
1008
+ capabilities["database"] = {"engine": "mysql"}
981
1009
  yaml_config["capabilities"] = capabilities
982
1010
 
1011
+ # Attach declared volumes to the app manifest (serve-time mounts)
1012
+ if serve.volumes:
1013
+ volumes_yaml = yaml_config.get("volumes", [])
1014
+ for vol in serve.volumes:
1015
+ volumes_yaml.append(
1016
+ {
1017
+ "name": vol.name,
1018
+ "mount": str(vol.serve_path),
1019
+ }
1020
+ )
1021
+ yaml_config["volumes"] = volumes_yaml
1022
+
1023
+ # If it has a php dependency, set the scaling mode to single_concurrency
1024
+ has_php = any(dep.name == "php" for dep in serve.deps)
1025
+ if has_php:
1026
+ scaling = yaml_config.get("scaling", {})
1027
+ scaling["mode"] = "single_concurrency"
1028
+ yaml_config["scaling"] = scaling
1029
+
983
1030
  if "after_deploy" in serve.commands:
984
1031
  jobs = yaml_config.get("jobs", [])
985
- jobs.append({
986
- "name": "after_deploy",
987
- "trigger": "post-deployment",
988
- "action": {
989
- "execute": {
990
- "command": "after_deploy"
991
- }
1032
+ jobs.append(
1033
+ {
1034
+ "name": "after_deploy",
1035
+ "trigger": "post-deployment",
1036
+ "action": {"execute": {"command": "after_deploy"}},
992
1037
  }
993
- })
1038
+ )
994
1039
  yaml_config["jobs"] = jobs
995
1040
 
996
1041
  app_yaml = yaml.dump(yaml_config)
@@ -1032,12 +1077,6 @@ class WasmerBuilder:
1032
1077
  ],
1033
1078
  )
1034
1079
 
1035
- def serve_mount(self, name: str) -> str:
1036
- return self.inner_builder.serve_mount(name)
1037
-
1038
- def get_asset(self, name: str) -> str:
1039
- return self.inner_builder.get_asset(name)
1040
-
1041
1080
  def run_command(
1042
1081
  self, command: str, extra_args: Optional[List[str]] | None = None
1043
1082
  ) -> Any:
@@ -1090,6 +1129,7 @@ class Ctx:
1090
1129
  self.steps: List[Step] = []
1091
1130
  self.serves: Dict[str, Serve] = {}
1092
1131
  self.mounts: List[Mount] = []
1132
+ self.volumes: List[Volume] = []
1093
1133
  self.services: Dict[str, Service] = {}
1094
1134
 
1095
1135
  def add_package(self, package: Package) -> str:
@@ -1112,6 +1152,8 @@ class Ctx:
1112
1152
  return self.steps[int(index[len("ref:step:") :])]
1113
1153
  elif index.startswith("ref:mount:"):
1114
1154
  return self.mounts[int(index[len("ref:mount:") :])]
1155
+ elif index.startswith("ref:volume:"):
1156
+ return self.volumes[int(index[len("ref:volume:") :])]
1115
1157
  elif index.startswith("ref:service:"):
1116
1158
  return self.services[index[len("ref:service:") :]]
1117
1159
  else:
@@ -1137,14 +1179,13 @@ class Ctx:
1137
1179
  def getenv(self, name: str) -> Optional[str]:
1138
1180
  return self.builder.getenv(name)
1139
1181
 
1140
- def get_asset(self, name: str) -> Optional[str]:
1141
- return self.builder.get_asset(name)
1142
-
1143
1182
  def dep(self, name: str, version: Optional[str] = None) -> str:
1144
1183
  package = Package(name, version)
1145
1184
  return self.add_package(package)
1146
-
1147
- def service(self, name: str, provider: Literal["postgres", "mysql", "redis"]) -> str:
1185
+
1186
+ def service(
1187
+ self, name: str, provider: Literal["postgres", "mysql", "redis"]
1188
+ ) -> str:
1148
1189
  service = Service(name, provider)
1149
1190
  return self.add_service(service)
1150
1191
 
@@ -1156,10 +1197,10 @@ class Ctx:
1156
1197
  deps: List[str],
1157
1198
  commands: Dict[str, str],
1158
1199
  cwd: Optional[str] = None,
1159
- assets: Optional[Dict[str, str]] = None,
1160
1200
  prepare: Optional[List[str]] = None,
1161
1201
  workers: Optional[List[str]] = None,
1162
1202
  mounts: Optional[List[Mount]] = None,
1203
+ volumes: Optional[List[Volume]] = None,
1163
1204
  env: Optional[Dict[str, str]] = None,
1164
1205
  services: Optional[List[str]] = None,
1165
1206
  ) -> str:
@@ -1177,7 +1218,6 @@ class Ctx:
1177
1218
  provider=provider,
1178
1219
  build=build_refs,
1179
1220
  cwd=cwd,
1180
- assets=assets,
1181
1221
  deps=dep_refs,
1182
1222
  commands=commands,
1183
1223
  prepare=prepare_steps,
@@ -1185,6 +1225,9 @@ class Ctx:
1185
1225
  mounts=self.get_refs([mount["ref"] for mount in mounts])
1186
1226
  if mounts
1187
1227
  else None,
1228
+ volumes=self.get_refs([volume["ref"] for volume in volumes])
1229
+ if volumes
1230
+ else None,
1188
1231
  env=env,
1189
1232
  services=self.get_refs(services) if services else None,
1190
1233
  )
@@ -1208,14 +1251,15 @@ class Ctx:
1208
1251
  return self.add_step(step)
1209
1252
 
1210
1253
  def copy(
1211
- self, source: str, target: str, ignore: Optional[List[str]] = None
1254
+ self,
1255
+ source: str,
1256
+ target: str,
1257
+ ignore: Optional[List[str]] = None,
1258
+ base: Optional[Literal["source", "assets"]] = None,
1212
1259
  ) -> Optional[str]:
1213
- step = CopyStep(source, target, ignore)
1260
+ step = CopyStep(source, target, ignore, base or "source")
1214
1261
  return self.add_step(step)
1215
1262
 
1216
- def buildpath(self, name: str) -> str:
1217
- return str((self.builder.get_build_path() / name).absolute())
1218
-
1219
1263
  def env(self, **env_vars: str) -> Optional[str]:
1220
1264
  step = EnvStep(env_vars)
1221
1265
  return self.add_step(step)
@@ -1235,8 +1279,18 @@ class Ctx:
1235
1279
  "serve": str(serve_path.absolute()),
1236
1280
  }
1237
1281
 
1238
- def serve_mount(self, name: str) -> Optional[str]:
1239
- return self.builder.serve_mount(name)
1282
+ def add_volume(self, volume: Volume) -> Optional[str]:
1283
+ self.volumes.append(volume)
1284
+ return f"ref:volume:{len(self.volumes) - 1}"
1285
+
1286
+ def volume(self, name: str, serve: str) -> Optional[str]:
1287
+ volume = Volume(name=name, serve_path=Path(serve))
1288
+ ref = self.add_volume(volume)
1289
+ return {
1290
+ "ref": ref,
1291
+ "name": name,
1292
+ "serve": str(volume.serve_path),
1293
+ }
1240
1294
 
1241
1295
 
1242
1296
  def print_help() -> None:
@@ -1249,6 +1303,13 @@ def print_help() -> None:
1249
1303
  console.print(panel)
1250
1304
 
1251
1305
 
1306
+ def download_file(url: str, path: Path) -> None:
1307
+ response = requests.get(url)
1308
+ response.raise_for_status()
1309
+ path.parent.mkdir(parents=True, exist_ok=True)
1310
+ path.write_bytes(response.content)
1311
+
1312
+
1252
1313
  @app.command(name="auto")
1253
1314
  def auto(
1254
1315
  path: Path = typer.Argument(
@@ -1308,12 +1369,35 @@ def auto(
1308
1369
  None,
1309
1370
  help="Name of the Wasmer app.",
1310
1371
  ),
1372
+ use_procfile: bool = typer.Option(
1373
+ True,
1374
+ help="Use the Procfile to generate the default custom commands (install, build, start, after_deploy).",
1375
+ ),
1376
+ install_command: Optional[str] = typer.Option(
1377
+ None,
1378
+ help="The install command to use (overwrites the default)",
1379
+ ),
1380
+ build_command: Optional[str] = typer.Option(
1381
+ None,
1382
+ help="The build command to use (overwrites the default)",
1383
+ ),
1384
+ start_command: Optional[str] = typer.Option(
1385
+ None,
1386
+ help="The start command to use (overwrites the default)",
1387
+ ),
1311
1388
  ):
1312
1389
  if not path.exists():
1313
1390
  raise Exception(f"The path {path} does not exist")
1314
1391
 
1315
1392
  if not (path / "Shipit").exists() or regenerate or regenerate_path is not None:
1316
- generate(path, out=regenerate_path)
1393
+ generate(
1394
+ path,
1395
+ out=regenerate_path,
1396
+ use_procfile=use_procfile,
1397
+ install_command=install_command,
1398
+ build_command=build_command,
1399
+ start_command=start_command,
1400
+ )
1317
1401
 
1318
1402
  build(
1319
1403
  path,
@@ -1353,13 +1437,53 @@ def generate(
1353
1437
  None,
1354
1438
  help="Output path (defaults to the Shipit file in the provided path).",
1355
1439
  ),
1440
+ use_procfile: bool = typer.Option(
1441
+ True,
1442
+ help="Use the Procfile to generate the default custom commands (install, build, start, after_deploy).",
1443
+ ),
1444
+ install_command: Optional[str] = typer.Option(
1445
+ None,
1446
+ help="The install command to use (overwrites the default)",
1447
+ ),
1448
+ build_command: Optional[str] = typer.Option(
1449
+ None,
1450
+ help="The build command to use (overwrites the default)",
1451
+ ),
1452
+ start_command: Optional[str] = typer.Option(
1453
+ None,
1454
+ help="The start command to use (overwrites the default)",
1455
+ ),
1356
1456
  ):
1357
1457
  if not path.exists():
1358
1458
  raise Exception(f"The path {path} does not exist")
1359
1459
 
1360
1460
  if out is None:
1361
1461
  out = path / "Shipit"
1362
- content = generate_shipit(path)
1462
+ custom_commands = CustomCommands()
1463
+ # if (path / "Dockerfile").exists():
1464
+ # # We get the start command from the Dockerfile
1465
+ # with open(path / "Dockerfile", "r") as f:
1466
+ # cmd = None
1467
+ # for line in f:
1468
+ # if line.startswith("CMD "):
1469
+ # cmd = line[4:].strip()
1470
+ # cmd = json.loads(cmd)
1471
+ # # We get the last command
1472
+ # if cmd:
1473
+ # if isinstance(cmd, list):
1474
+ # cmd = " ".join(cmd)
1475
+ # custom_commands.start = cmd
1476
+ if use_procfile:
1477
+ if (path / "Procfile").exists():
1478
+ procfile = Procfile.loads((path / "Procfile").read_text())
1479
+ custom_commands.start = procfile.get_start_command()
1480
+ if start_command:
1481
+ custom_commands.start = start_command
1482
+ if install_command:
1483
+ custom_commands.install = install_command
1484
+ if build_command:
1485
+ custom_commands.build = build_command
1486
+ content = generate_shipit(path, custom_commands)
1363
1487
  out.write_text(content)
1364
1488
  console.print(f"[bold]Generated Shipit[/bold] at {out.absolute()}")
1365
1489
 
@@ -1518,15 +1642,12 @@ def build(
1518
1642
  mod.add_callable("serve", ctx.serve)
1519
1643
  mod.add_callable("run", ctx.run)
1520
1644
  mod.add_callable("mount", ctx.mount)
1645
+ mod.add_callable("volume", ctx.volume)
1521
1646
  mod.add_callable("workdir", ctx.workdir)
1522
1647
  mod.add_callable("copy", ctx.copy)
1523
1648
  mod.add_callable("path", ctx.path)
1524
- mod.add_callable("buildpath", ctx.buildpath)
1525
- mod.add_callable("get_asset", ctx.get_asset)
1526
1649
  mod.add_callable("env", ctx.env)
1527
1650
  mod.add_callable("use", ctx.use)
1528
- # REMOVE ME
1529
- mod.add_callable("serve_mount", ctx.serve_mount)
1530
1651
 
1531
1652
  dialect = sl.Dialect.extended()
1532
1653
  dialect.enable_f_strings = True
@@ -1534,9 +1655,7 @@ def build(
1534
1655
  ast = sl.parse("shipit", source, dialect=dialect)
1535
1656
 
1536
1657
  sl.eval(mod, ast, glb)
1537
- # assert len(ctx.builds) == 1, "Only one build is allowed for now"
1538
1658
  assert len(ctx.serves) <= 1, "Only one serve is allowed for now"
1539
- # build = ctx.builds[0]
1540
1659
  env = {
1541
1660
  "PATH": "",
1542
1661
  "COLORTERM": os.environ.get("COLORTERM", ""),
@@ -1550,8 +1669,6 @@ def build(
1550
1669
  builder.build(env, serve.mounts, serve.build)
1551
1670
  if serve.prepare:
1552
1671
  builder.build_prepare(serve)
1553
- if serve.assets:
1554
- builder.build_assets(serve.assets)
1555
1672
  builder.build_serve(serve)
1556
1673
  builder.finalize_build(serve)
1557
1674
  if serve.prepare and not skip_prepare:
@@ -1569,7 +1686,7 @@ def main() -> None:
1569
1686
  app()
1570
1687
  except Exception as e:
1571
1688
  console.print(f"[bold red]{type(e).__name__}[/bold red]: {e}")
1572
- # raise e
1689
+ raise e
1573
1690
 
1574
1691
 
1575
1692
  if __name__ == "__main__":
shipit/env.py ADDED
@@ -0,0 +1,30 @@
1
+ import shlex
2
+ import re
3
+
4
+ def parse(content):
5
+ """
6
+ Parse the content of a .env file (a line-delimited KEY=value format) into a
7
+ dictionary mapping keys to values.
8
+ """
9
+ values = {}
10
+ for line in content.splitlines():
11
+ lexer = shlex.shlex(line, posix=True)
12
+ tokens = list(lexer)
13
+
14
+ # parses the assignment statement
15
+ if len(tokens) < 3:
16
+ continue
17
+
18
+ name, op = tokens[:2]
19
+ value = ''.join(tokens[2:])
20
+
21
+ if op != '=':
22
+ continue
23
+ if not re.match(r'[A-Za-z_][A-Za-z_0-9]*', name):
24
+ continue
25
+
26
+ value = value.replace(r'\n', '\n')
27
+ value = value.replace(r'\t', '\t')
28
+ values[name] = value
29
+
30
+ return values