shipit-cli 0.6.1__py3-none-any.whl → 0.7.1__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,9 @@ 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
39
+ from dotenv import dotenv_values
35
40
 
36
41
 
37
42
  console = Console()
@@ -49,10 +54,18 @@ class Mount:
49
54
  serve_path: Path
50
55
 
51
56
 
57
+ @dataclass
58
+ class Volume:
59
+ name: str
60
+ serve_path: Path
61
+
62
+
52
63
  @dataclass
53
64
  class Service:
54
65
  name: str
55
- provider: Literal["postgres", "mysql", "redis"] # Right now we only support postgres and mysql
66
+ provider: Literal[
67
+ "postgres", "mysql", "redis"
68
+ ] # Right now we only support postgres and mysql
56
69
 
57
70
 
58
71
  @dataclass
@@ -63,10 +76,10 @@ class Serve:
63
76
  deps: List["Package"]
64
77
  commands: Dict[str, str]
65
78
  cwd: Optional[str] = None
66
- assets: Optional[Dict[str, str]] = None
67
79
  prepare: Optional[List["PrepareStep"]] = None
68
80
  workers: Optional[List[str]] = None
69
81
  mounts: Optional[List[Mount]] = None
82
+ volumes: Optional[List[Volume]] = None
70
83
  env: Optional[Dict[str, str]] = None
71
84
  services: Optional[List[Service]] = None
72
85
 
@@ -98,6 +111,11 @@ class CopyStep:
98
111
  source: str
99
112
  target: str
100
113
  ignore: Optional[List[str]] = None
114
+ # We can copy from the app source or from the shipit assets folder
115
+ base: Literal["source", "assets"] = "source"
116
+
117
+ def is_download(self) -> bool:
118
+ return self.source.startswith("http://") or self.source.startswith("https://")
101
119
 
102
120
 
103
121
  @dataclass
@@ -147,7 +165,6 @@ class Builder(Protocol):
147
165
  def build(
148
166
  self, env: Dict[str, str], mounts: List[Mount], steps: List[Step]
149
167
  ) -> None: ...
150
- def build_assets(self, assets: Dict[str, str]) -> None: ...
151
168
  def build_prepare(self, serve: Serve) -> None: ...
152
169
  def build_serve(self, serve: Serve) -> None: ...
153
170
  def finalize_build(self, serve: Serve) -> None: ...
@@ -157,8 +174,6 @@ class Builder(Protocol):
157
174
  def run_command(
158
175
  self, command: str, extra_args: Optional[List[str]] | None = None
159
176
  ) -> Any: ...
160
- def serve_mount(self, name: str) -> str: ...
161
- def get_asset(self, name: str) -> str: ...
162
177
  def get_build_mount_path(self, name: str) -> Path: ...
163
178
  def get_serve_mount_path(self, name: str) -> Path: ...
164
179
 
@@ -258,13 +273,23 @@ class DockerBuilder:
258
273
 
259
274
  def run_command(self, command: str, extra_args: Optional[List[str]] = None) -> Any:
260
275
  image_name = self.docker_name_path.read_text()
261
- return sh.Command(
262
- "docker"
263
- )(
276
+ docker_args: List[str] = [
264
277
  "run",
265
278
  "-p",
266
279
  "80:80",
267
280
  "--rm",
281
+ ]
282
+ # Attach volumes if present
283
+ # if serve.volumes:
284
+ # for vol in serve.volumes:
285
+ # docker_args += [
286
+ # "--mount",
287
+ # f"type=volume,source={vol.name},target={str(vol.serve_path)}",
288
+ # ]
289
+ return sh.Command(
290
+ "docker"
291
+ )(
292
+ *docker_args,
268
293
  image_name,
269
294
  command,
270
295
  *(extra_args or []),
@@ -372,7 +397,28 @@ RUN curl https://mise.run | sh
372
397
  pre = ""
373
398
  self.docker_file_contents += f"RUN {pre}{step.command}\n"
374
399
  elif isinstance(step, CopyStep):
375
- self.docker_file_contents += f"COPY {step.source} {step.target}\n"
400
+ if step.is_download():
401
+ self.docker_file_contents += (
402
+ "ADD " + step.source + " " + step.target + "\n"
403
+ )
404
+ elif step.base == "assets":
405
+ # Detect if the asset exists and is a file
406
+ if (ASSETS_PATH / step.source).is_file():
407
+ # Read the file content and write it to the target file
408
+ content_base64 = base64.b64encode(
409
+ (ASSETS_PATH / step.source).read_bytes()
410
+ )
411
+ self.docker_file_contents += (
412
+ f"RUN echo '{content_base64}' | base64 -d > {step.target}\n"
413
+ )
414
+ elif (ASSETS_PATH / step.source).is_dir():
415
+ raise Exception(
416
+ f"Asset {step.source} is a directory, shipit doesn't currently support coppying assets directories inside Docker"
417
+ )
418
+ else:
419
+ raise Exception(f"Asset {step.source} does not exist")
420
+ else:
421
+ self.docker_file_contents += f"COPY {step.source} {step.target}\n"
376
422
  elif isinstance(step, EnvStep):
377
423
  env_vars = " ".join(
378
424
  [f"{key}={value}" for key, value in step.variables.items()]
@@ -397,9 +443,6 @@ FROM scratch
397
443
  Shipit
398
444
  """)
399
445
 
400
- def build_assets(self, assets: Dict[str, str]) -> None:
401
- raise NotImplementedError
402
-
403
446
  def get_path(self) -> Path:
404
447
  return Path("/")
405
448
 
@@ -409,11 +452,6 @@ Shipit
409
452
  def get_serve_path(self) -> Path:
410
453
  return self.get_path() / "serve"
411
454
 
412
- def get_assets_path(self) -> Path:
413
- path = self.get_path() / "assets"
414
- self.mkdir(path)
415
- return path
416
-
417
455
  def prepare(self, env: Dict[str, str], prepare: List[PrepareStep]) -> None:
418
456
  raise NotImplementedError
419
457
 
@@ -433,14 +471,6 @@ Shipit
433
471
  mode=0o755,
434
472
  )
435
473
 
436
- def serve_mount(self, name: str) -> str:
437
- path = self.mkdir(Path("serve") / "mounts" / name)
438
- return str(path.absolute())
439
-
440
- def get_asset(self, name: str) -> str:
441
- asset_path = ASSETS_PATH / name
442
- return asset_path.read_text()
443
-
444
474
  def run_serve_command(self, command: str) -> None:
445
475
  path = self.shipit_docker_path / "serve" / "bin" / command
446
476
  self.run_command(str(path))
@@ -518,18 +548,31 @@ class LocalBuilder:
518
548
  ignore_matches = step.ignore if step.ignore else []
519
549
  ignore_matches.append(".shipit")
520
550
  ignore_matches.append("Shipit")
521
- if (self.src_dir / step.source).is_dir():
522
- copytree(
523
- (self.src_dir / step.source),
524
- (build_path / step.target),
525
- dirs_exist_ok=True,
526
- ignore=ignore_patterns(*ignore_matches),
527
- )
551
+
552
+ if step.is_download():
553
+ download_file(step.source, (build_path / step.target))
528
554
  else:
529
- copy(
530
- (self.src_dir / step.source),
531
- (build_path / step.target),
532
- )
555
+ if step.base == "source":
556
+ base = self.src_dir
557
+ elif step.base == "assets":
558
+ base = ASSETS_PATH
559
+ else:
560
+ raise Exception(f"Unknown base: {step.base}")
561
+
562
+ if (base / step.source).is_dir():
563
+ copytree(
564
+ (base / step.source),
565
+ (build_path / step.target),
566
+ dirs_exist_ok=True,
567
+ ignore=ignore_patterns(*ignore_matches),
568
+ )
569
+ elif (base / step.source).is_file():
570
+ copy(
571
+ (base / step.source),
572
+ (build_path / step.target),
573
+ )
574
+ else:
575
+ raise Exception(f"Source {step.source} is not a file or directory")
533
576
  elif isinstance(step, EnvStep):
534
577
  print(f"Setting environment variables: {step}")
535
578
  env.update(step.variables)
@@ -591,17 +634,6 @@ class LocalBuilder:
591
634
  def get_serve_path(self) -> Path:
592
635
  return self.get_path() / "serve"
593
636
 
594
- def get_assets_path(self) -> Path:
595
- path = self.get_path() / "assets"
596
- self.mkdir(path)
597
- return path
598
-
599
- def build_assets(self, assets: Dict[str, str]) -> None:
600
- assets_path = self.get_assets_path()
601
- for asset in assets:
602
- asset_path = assets_path / asset
603
- self.create_file(asset_path, assets[asset])
604
-
605
637
  def build_prepare(self, serve: Serve) -> None:
606
638
  self.prepare_bash_script.parent.mkdir(parents=True, exist_ok=True)
607
639
  commands: List[str] = []
@@ -642,6 +674,7 @@ class LocalBuilder:
642
674
  )
643
675
 
644
676
  def build_serve(self, serve: Serve) -> None:
677
+ # Remember serve configuration for run-time
645
678
  console.print("\n[bold]Building serve[/bold]")
646
679
  serve_command_path = self.get_serve_path() / "bin"
647
680
  serve_command_path.mkdir(parents=True, exist_ok=False)
@@ -651,10 +684,12 @@ class LocalBuilder:
651
684
  for command in serve.commands:
652
685
  console.print(f"* {command}")
653
686
  command_path = serve_command_path / command
654
- content = f"#!/bin/bash\ncd {serve.cwd}\nPATH={path_text}:$PATH {serve.commands[command]}"
655
- command_path.write_text(
656
- content
657
- )
687
+ env_vars = ""
688
+ if serve.env:
689
+ env_vars = " ".join([f"{k}={v}" for k, v in serve.env.items()])
690
+
691
+ content = f"#!/bin/bash\ncd {serve.cwd}\nPATH={path_text}:$PATH {env_vars} {serve.commands[command]}"
692
+ command_path.write_text(content)
658
693
  manifest_panel = Panel(
659
694
  Syntax(
660
695
  content.strip(),
@@ -676,15 +711,6 @@ class LocalBuilder:
676
711
  command_path = base_path / command
677
712
  sh.Command(str(command_path))(_out=write_stdout, _err=write_stderr)
678
713
 
679
- def serve_mount(self, name: str) -> str:
680
- base_path = self.get_serve_path() / "mounts" / name
681
- base_path.mkdir(parents=True, exist_ok=True)
682
- return str(base_path.absolute())
683
-
684
- def get_asset(self, name: str) -> str:
685
- asset_path = ASSETS_PATH / name
686
- return asset_path.read_text()
687
-
688
714
 
689
715
  class WasmerBuilder:
690
716
  def get_build_mount_path(self, name: str) -> Path:
@@ -725,8 +751,8 @@ class WasmerBuilder:
725
751
  },
726
752
  "php": {
727
753
  "dependencies": {
728
- "latest": "php/php-32@=8.3.2104",
729
- "8.3": "php/php-32@=8.3.2104",
754
+ "latest": "php/php-32@=8.3.2102",
755
+ "8.3": "php/php-32@=8.3.2102",
730
756
  },
731
757
  "scripts": {"php"},
732
758
  "aliases": {},
@@ -782,9 +808,6 @@ class WasmerBuilder:
782
808
  ) -> None:
783
809
  return self.inner_builder.build(env, mounts, build)
784
810
 
785
- def build_assets(self, assets: Dict[str, str]) -> None:
786
- return self.inner_builder.build_assets(assets)
787
-
788
811
  def get_build_path(self) -> Path:
789
812
  return Path("/app")
790
813
 
@@ -855,10 +878,10 @@ class WasmerBuilder:
855
878
  )
856
879
 
857
880
  def build_serve(self, serve: Serve) -> None:
858
- from tomlkit import comment, document, nl, table, aot, string
881
+ from tomlkit import comment, document, nl, table, aot, string, array
859
882
 
860
883
  doc = document()
861
- doc.add(comment(f"File generated by Shipit {shipit_version}"))
884
+ doc.add(comment(f"Wasmer manifest generated with Shipit v{shipit_version}"))
862
885
  package = table()
863
886
  doc.add("package", package)
864
887
  package.add("entrypoint", "start")
@@ -908,9 +931,6 @@ class WasmerBuilder:
908
931
  fs = table()
909
932
  doc.add("fs", fs)
910
933
  inner = cast(Any, self.inner_builder)
911
- if serve.assets:
912
- fs.add("/assets", str((inner.get_path() / "assets").absolute()))
913
- # fs.add("/app", str(inner.get_build_path().absolute()))
914
934
  if serve.mounts:
915
935
  for mount in serve.mounts:
916
936
  fs.add(
@@ -934,14 +954,14 @@ class WasmerBuilder:
934
954
  wasi_args = table()
935
955
  if serve.cwd:
936
956
  wasi_args.add("cwd", serve.cwd)
937
- wasi_args.add("main-args", parts[1:])
957
+ wasi_args.add("main-args", array(parts[1:]).multiline(True))
938
958
  env = program_binary.get("env") or {}
939
959
  if serve.env:
940
960
  env.update(serve.env)
941
961
  if env:
962
+ arr = array([f"{k}={v}" for k, v in env.items()]).multiline(True)
942
963
  wasi_args.add(
943
- "env",
944
- [f"{k}={v}" for k, v in env.items()],
964
+ "env", arr
945
965
  )
946
966
  title = string("annotations.wasi", literal=False)
947
967
  command.add(title, wasi_args)
@@ -986,25 +1006,40 @@ class WasmerBuilder:
986
1006
  # has_postgres = any(service.provider == "postgres" for service in serve.services)
987
1007
  # has_redis = any(service.provider == "redis" for service in serve.services)
988
1008
  if has_mysql:
989
- capabilities["database"] = {
990
- "engine": "mysql"
991
- }
1009
+ capabilities["database"] = {"engine": "mysql"}
992
1010
  yaml_config["capabilities"] = capabilities
993
1011
 
1012
+ # Attach declared volumes to the app manifest (serve-time mounts)
1013
+ if serve.volumes:
1014
+ volumes_yaml = yaml_config.get("volumes", [])
1015
+ for vol in serve.volumes:
1016
+ volumes_yaml.append(
1017
+ {
1018
+ "name": vol.name,
1019
+ "mount": str(vol.serve_path),
1020
+ }
1021
+ )
1022
+ yaml_config["volumes"] = volumes_yaml
1023
+
1024
+ # If it has a php dependency, set the scaling mode to single_concurrency
1025
+ has_php = any(dep.name == "php" for dep in serve.deps)
1026
+ if has_php:
1027
+ scaling = yaml_config.get("scaling", {})
1028
+ scaling["mode"] = "single_concurrency"
1029
+ yaml_config["scaling"] = scaling
1030
+
994
1031
  if "after_deploy" in serve.commands:
995
1032
  jobs = yaml_config.get("jobs", [])
996
- jobs.append({
997
- "name": "after_deploy",
998
- "trigger": "post-deployment",
999
- "action": {
1000
- "execute": {
1001
- "command": "after_deploy"
1002
- }
1033
+ jobs.append(
1034
+ {
1035
+ "name": "after_deploy",
1036
+ "trigger": "post-deployment",
1037
+ "action": {"execute": {"command": "after_deploy"}},
1003
1038
  }
1004
- })
1039
+ )
1005
1040
  yaml_config["jobs"] = jobs
1006
1041
 
1007
- app_yaml = yaml.dump(yaml_config)
1042
+ app_yaml = yaml.dump(yaml_config,)
1008
1043
 
1009
1044
  console.print(f"\n[bold]Created app.yaml manifest ✅[/bold]")
1010
1045
  app_yaml_panel = Panel(
@@ -1043,12 +1078,6 @@ class WasmerBuilder:
1043
1078
  ],
1044
1079
  )
1045
1080
 
1046
- def serve_mount(self, name: str) -> str:
1047
- return self.inner_builder.serve_mount(name)
1048
-
1049
- def get_asset(self, name: str) -> str:
1050
- return self.inner_builder.get_asset(name)
1051
-
1052
1081
  def run_command(
1053
1082
  self, command: str, extra_args: Optional[List[str]] | None = None
1054
1083
  ) -> Any:
@@ -1101,6 +1130,7 @@ class Ctx:
1101
1130
  self.steps: List[Step] = []
1102
1131
  self.serves: Dict[str, Serve] = {}
1103
1132
  self.mounts: List[Mount] = []
1133
+ self.volumes: List[Volume] = []
1104
1134
  self.services: Dict[str, Service] = {}
1105
1135
 
1106
1136
  def add_package(self, package: Package) -> str:
@@ -1123,6 +1153,8 @@ class Ctx:
1123
1153
  return self.steps[int(index[len("ref:step:") :])]
1124
1154
  elif index.startswith("ref:mount:"):
1125
1155
  return self.mounts[int(index[len("ref:mount:") :])]
1156
+ elif index.startswith("ref:volume:"):
1157
+ return self.volumes[int(index[len("ref:volume:") :])]
1126
1158
  elif index.startswith("ref:service:"):
1127
1159
  return self.services[index[len("ref:service:") :]]
1128
1160
  else:
@@ -1148,14 +1180,13 @@ class Ctx:
1148
1180
  def getenv(self, name: str) -> Optional[str]:
1149
1181
  return self.builder.getenv(name)
1150
1182
 
1151
- def get_asset(self, name: str) -> Optional[str]:
1152
- return self.builder.get_asset(name)
1153
-
1154
1183
  def dep(self, name: str, version: Optional[str] = None) -> str:
1155
1184
  package = Package(name, version)
1156
1185
  return self.add_package(package)
1157
-
1158
- def service(self, name: str, provider: Literal["postgres", "mysql", "redis"]) -> str:
1186
+
1187
+ def service(
1188
+ self, name: str, provider: Literal["postgres", "mysql", "redis"]
1189
+ ) -> str:
1159
1190
  service = Service(name, provider)
1160
1191
  return self.add_service(service)
1161
1192
 
@@ -1167,10 +1198,10 @@ class Ctx:
1167
1198
  deps: List[str],
1168
1199
  commands: Dict[str, str],
1169
1200
  cwd: Optional[str] = None,
1170
- assets: Optional[Dict[str, str]] = None,
1171
1201
  prepare: Optional[List[str]] = None,
1172
1202
  workers: Optional[List[str]] = None,
1173
1203
  mounts: Optional[List[Mount]] = None,
1204
+ volumes: Optional[List[Volume]] = None,
1174
1205
  env: Optional[Dict[str, str]] = None,
1175
1206
  services: Optional[List[str]] = None,
1176
1207
  ) -> str:
@@ -1188,7 +1219,6 @@ class Ctx:
1188
1219
  provider=provider,
1189
1220
  build=build_refs,
1190
1221
  cwd=cwd,
1191
- assets=assets,
1192
1222
  deps=dep_refs,
1193
1223
  commands=commands,
1194
1224
  prepare=prepare_steps,
@@ -1196,6 +1226,9 @@ class Ctx:
1196
1226
  mounts=self.get_refs([mount["ref"] for mount in mounts])
1197
1227
  if mounts
1198
1228
  else None,
1229
+ volumes=self.get_refs([volume["ref"] for volume in volumes])
1230
+ if volumes
1231
+ else None,
1199
1232
  env=env,
1200
1233
  services=self.get_refs(services) if services else None,
1201
1234
  )
@@ -1219,14 +1252,15 @@ class Ctx:
1219
1252
  return self.add_step(step)
1220
1253
 
1221
1254
  def copy(
1222
- self, source: str, target: str, ignore: Optional[List[str]] = None
1255
+ self,
1256
+ source: str,
1257
+ target: str,
1258
+ ignore: Optional[List[str]] = None,
1259
+ base: Optional[Literal["source", "assets"]] = None,
1223
1260
  ) -> Optional[str]:
1224
- step = CopyStep(source, target, ignore)
1261
+ step = CopyStep(source, target, ignore, base or "source")
1225
1262
  return self.add_step(step)
1226
1263
 
1227
- def buildpath(self, name: str) -> str:
1228
- return str((self.builder.get_build_path() / name).absolute())
1229
-
1230
1264
  def env(self, **env_vars: str) -> Optional[str]:
1231
1265
  step = EnvStep(env_vars)
1232
1266
  return self.add_step(step)
@@ -1246,8 +1280,18 @@ class Ctx:
1246
1280
  "serve": str(serve_path.absolute()),
1247
1281
  }
1248
1282
 
1249
- def serve_mount(self, name: str) -> Optional[str]:
1250
- return self.builder.serve_mount(name)
1283
+ def add_volume(self, volume: Volume) -> Optional[str]:
1284
+ self.volumes.append(volume)
1285
+ return f"ref:volume:{len(self.volumes) - 1}"
1286
+
1287
+ def volume(self, name: str, serve: str) -> Optional[str]:
1288
+ volume = Volume(name=name, serve_path=Path(serve))
1289
+ ref = self.add_volume(volume)
1290
+ return {
1291
+ "ref": ref,
1292
+ "name": name,
1293
+ "serve": str(volume.serve_path),
1294
+ }
1251
1295
 
1252
1296
 
1253
1297
  def print_help() -> None:
@@ -1260,6 +1304,13 @@ def print_help() -> None:
1260
1304
  console.print(panel)
1261
1305
 
1262
1306
 
1307
+ def download_file(url: str, path: Path) -> None:
1308
+ response = requests.get(url)
1309
+ response.raise_for_status()
1310
+ path.parent.mkdir(parents=True, exist_ok=True)
1311
+ path.write_bytes(response.content)
1312
+
1313
+
1263
1314
  @app.command(name="auto")
1264
1315
  def auto(
1265
1316
  path: Path = typer.Argument(
@@ -1319,12 +1370,39 @@ def auto(
1319
1370
  None,
1320
1371
  help="Name of the Wasmer app.",
1321
1372
  ),
1373
+ use_procfile: bool = typer.Option(
1374
+ True,
1375
+ help="Use the Procfile to generate the default custom commands (install, build, start, after_deploy).",
1376
+ ),
1377
+ install_command: Optional[str] = typer.Option(
1378
+ None,
1379
+ help="The install command to use (overwrites the default)",
1380
+ ),
1381
+ build_command: Optional[str] = typer.Option(
1382
+ None,
1383
+ help="The build command to use (overwrites the default)",
1384
+ ),
1385
+ start_command: Optional[str] = typer.Option(
1386
+ None,
1387
+ help="The start command to use (overwrites the default)",
1388
+ ),
1389
+ env_name: Optional[str] = typer.Option(
1390
+ None,
1391
+ help="The environment to use (defaults to `.env`, it will use .env.<env_name> if provided)",
1392
+ ),
1322
1393
  ):
1323
1394
  if not path.exists():
1324
1395
  raise Exception(f"The path {path} does not exist")
1325
1396
 
1326
1397
  if not (path / "Shipit").exists() or regenerate or regenerate_path is not None:
1327
- generate(path, out=regenerate_path)
1398
+ generate(
1399
+ path,
1400
+ out=regenerate_path,
1401
+ use_procfile=use_procfile,
1402
+ install_command=install_command,
1403
+ build_command=build_command,
1404
+ start_command=start_command,
1405
+ )
1328
1406
 
1329
1407
  build(
1330
1408
  path,
@@ -1335,6 +1413,7 @@ def auto(
1335
1413
  wasmer_token=wasmer_token,
1336
1414
  wasmer_bin=wasmer_bin,
1337
1415
  skip_prepare=skip_prepare,
1416
+ env_name=env_name,
1338
1417
  )
1339
1418
  if start or wasmer_deploy:
1340
1419
  serve(
@@ -1364,13 +1443,53 @@ def generate(
1364
1443
  None,
1365
1444
  help="Output path (defaults to the Shipit file in the provided path).",
1366
1445
  ),
1446
+ use_procfile: bool = typer.Option(
1447
+ True,
1448
+ help="Use the Procfile to generate the default custom commands (install, build, start, after_deploy).",
1449
+ ),
1450
+ install_command: Optional[str] = typer.Option(
1451
+ None,
1452
+ help="The install command to use (overwrites the default)",
1453
+ ),
1454
+ build_command: Optional[str] = typer.Option(
1455
+ None,
1456
+ help="The build command to use (overwrites the default)",
1457
+ ),
1458
+ start_command: Optional[str] = typer.Option(
1459
+ None,
1460
+ help="The start command to use (overwrites the default)",
1461
+ ),
1367
1462
  ):
1368
1463
  if not path.exists():
1369
1464
  raise Exception(f"The path {path} does not exist")
1370
1465
 
1371
1466
  if out is None:
1372
1467
  out = path / "Shipit"
1373
- content = generate_shipit(path)
1468
+ custom_commands = CustomCommands()
1469
+ # if (path / "Dockerfile").exists():
1470
+ # # We get the start command from the Dockerfile
1471
+ # with open(path / "Dockerfile", "r") as f:
1472
+ # cmd = None
1473
+ # for line in f:
1474
+ # if line.startswith("CMD "):
1475
+ # cmd = line[4:].strip()
1476
+ # cmd = json.loads(cmd)
1477
+ # # We get the last command
1478
+ # if cmd:
1479
+ # if isinstance(cmd, list):
1480
+ # cmd = " ".join(cmd)
1481
+ # custom_commands.start = cmd
1482
+ if use_procfile:
1483
+ if (path / "Procfile").exists():
1484
+ procfile = Procfile.loads((path / "Procfile").read_text())
1485
+ custom_commands.start = procfile.get_start_command()
1486
+ if start_command:
1487
+ custom_commands.start = start_command
1488
+ if install_command:
1489
+ custom_commands.install = install_command
1490
+ if build_command:
1491
+ custom_commands.build = build_command
1492
+ content = generate_shipit(path, custom_commands)
1374
1493
  out.write_text(content)
1375
1494
  console.print(f"[bold]Generated Shipit[/bold] at {out.absolute()}")
1376
1495
 
@@ -1499,6 +1618,10 @@ def build(
1499
1618
  None,
1500
1619
  help="Use a specific Docker client (such as depot, podman, etc.)",
1501
1620
  ),
1621
+ env_name: Optional[str] = typer.Option(
1622
+ None,
1623
+ help="The environment to use (defaults to `.env`, it will use .env.<env_name> if provided)",
1624
+ ),
1502
1625
  ) -> None:
1503
1626
  if not path.exists():
1504
1627
  raise Exception(f"The path {path} does not exist")
@@ -1529,15 +1652,12 @@ def build(
1529
1652
  mod.add_callable("serve", ctx.serve)
1530
1653
  mod.add_callable("run", ctx.run)
1531
1654
  mod.add_callable("mount", ctx.mount)
1655
+ mod.add_callable("volume", ctx.volume)
1532
1656
  mod.add_callable("workdir", ctx.workdir)
1533
1657
  mod.add_callable("copy", ctx.copy)
1534
1658
  mod.add_callable("path", ctx.path)
1535
- mod.add_callable("buildpath", ctx.buildpath)
1536
- mod.add_callable("get_asset", ctx.get_asset)
1537
1659
  mod.add_callable("env", ctx.env)
1538
1660
  mod.add_callable("use", ctx.use)
1539
- # REMOVE ME
1540
- mod.add_callable("serve_mount", ctx.serve_mount)
1541
1661
 
1542
1662
  dialect = sl.Dialect.extended()
1543
1663
  dialect.enable_f_strings = True
@@ -1545,9 +1665,7 @@ def build(
1545
1665
  ast = sl.parse("shipit", source, dialect=dialect)
1546
1666
 
1547
1667
  sl.eval(mod, ast, glb)
1548
- # assert len(ctx.builds) == 1, "Only one build is allowed for now"
1549
1668
  assert len(ctx.serves) <= 1, "Only one serve is allowed for now"
1550
- # build = ctx.builds[0]
1551
1669
  env = {
1552
1670
  "PATH": "",
1553
1671
  "COLORTERM": os.environ.get("COLORTERM", ""),
@@ -1556,13 +1674,19 @@ def build(
1556
1674
  "CLICOLOR": os.environ.get("CLICOLOR", "0"),
1557
1675
  }
1558
1676
  serve = next(iter(ctx.serves.values()))
1677
+ serve.env = serve.env or {}
1678
+ if (path / ".env").exists():
1679
+ env_vars = dotenv_values(path / ".env")
1680
+ serve.env.update(env_vars)
1681
+
1682
+ if (path / f".env.{env_name}").exists():
1683
+ env_vars = dotenv_values(path / f".env.{env_name}")
1684
+ serve.env.update(env_vars)
1559
1685
 
1560
1686
  # Build and serve
1561
1687
  builder.build(env, serve.mounts, serve.build)
1562
1688
  if serve.prepare:
1563
1689
  builder.build_prepare(serve)
1564
- if serve.assets:
1565
- builder.build_assets(serve.assets)
1566
1690
  builder.build_serve(serve)
1567
1691
  builder.finalize_build(serve)
1568
1692
  if serve.prepare and not skip_prepare:
@@ -1580,8 +1704,11 @@ def main() -> None:
1580
1704
  app()
1581
1705
  except Exception as e:
1582
1706
  console.print(f"[bold red]{type(e).__name__}[/bold red]: {e}")
1583
- # raise e
1707
+ raise e
1584
1708
 
1585
1709
 
1586
1710
  if __name__ == "__main__":
1587
1711
  main()
1712
+
1713
+ def flatten(xss):
1714
+ return [x for xs in xss for x in xs]