shipit-cli 0.6.1__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 []),
@@ -372,7 +396,28 @@ RUN curl https://mise.run | sh
372
396
  pre = ""
373
397
  self.docker_file_contents += f"RUN {pre}{step.command}\n"
374
398
  elif isinstance(step, CopyStep):
375
- 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"
376
421
  elif isinstance(step, EnvStep):
377
422
  env_vars = " ".join(
378
423
  [f"{key}={value}" for key, value in step.variables.items()]
@@ -397,9 +442,6 @@ FROM scratch
397
442
  Shipit
398
443
  """)
399
444
 
400
- def build_assets(self, assets: Dict[str, str]) -> None:
401
- raise NotImplementedError
402
-
403
445
  def get_path(self) -> Path:
404
446
  return Path("/")
405
447
 
@@ -409,11 +451,6 @@ Shipit
409
451
  def get_serve_path(self) -> Path:
410
452
  return self.get_path() / "serve"
411
453
 
412
- def get_assets_path(self) -> Path:
413
- path = self.get_path() / "assets"
414
- self.mkdir(path)
415
- return path
416
-
417
454
  def prepare(self, env: Dict[str, str], prepare: List[PrepareStep]) -> None:
418
455
  raise NotImplementedError
419
456
 
@@ -433,14 +470,6 @@ Shipit
433
470
  mode=0o755,
434
471
  )
435
472
 
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
473
  def run_serve_command(self, command: str) -> None:
445
474
  path = self.shipit_docker_path / "serve" / "bin" / command
446
475
  self.run_command(str(path))
@@ -518,18 +547,31 @@ class LocalBuilder:
518
547
  ignore_matches = step.ignore if step.ignore else []
519
548
  ignore_matches.append(".shipit")
520
549
  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
- )
550
+
551
+ if step.is_download():
552
+ download_file(step.source, (build_path / step.target))
528
553
  else:
529
- copy(
530
- (self.src_dir / step.source),
531
- (build_path / step.target),
532
- )
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")
533
575
  elif isinstance(step, EnvStep):
534
576
  print(f"Setting environment variables: {step}")
535
577
  env.update(step.variables)
@@ -591,17 +633,6 @@ class LocalBuilder:
591
633
  def get_serve_path(self) -> Path:
592
634
  return self.get_path() / "serve"
593
635
 
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
636
  def build_prepare(self, serve: Serve) -> None:
606
637
  self.prepare_bash_script.parent.mkdir(parents=True, exist_ok=True)
607
638
  commands: List[str] = []
@@ -642,6 +673,7 @@ class LocalBuilder:
642
673
  )
643
674
 
644
675
  def build_serve(self, serve: Serve) -> None:
676
+ # Remember serve configuration for run-time
645
677
  console.print("\n[bold]Building serve[/bold]")
646
678
  serve_command_path = self.get_serve_path() / "bin"
647
679
  serve_command_path.mkdir(parents=True, exist_ok=False)
@@ -651,10 +683,12 @@ class LocalBuilder:
651
683
  for command in serve.commands:
652
684
  console.print(f"* {command}")
653
685
  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
- )
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)
658
692
  manifest_panel = Panel(
659
693
  Syntax(
660
694
  content.strip(),
@@ -676,15 +710,6 @@ class LocalBuilder:
676
710
  command_path = base_path / command
677
711
  sh.Command(str(command_path))(_out=write_stdout, _err=write_stderr)
678
712
 
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
713
 
689
714
  class WasmerBuilder:
690
715
  def get_build_mount_path(self, name: str) -> Path:
@@ -725,8 +750,8 @@ class WasmerBuilder:
725
750
  },
726
751
  "php": {
727
752
  "dependencies": {
728
- "latest": "php/php-32@=8.3.2104",
729
- "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",
730
755
  },
731
756
  "scripts": {"php"},
732
757
  "aliases": {},
@@ -782,9 +807,6 @@ class WasmerBuilder:
782
807
  ) -> None:
783
808
  return self.inner_builder.build(env, mounts, build)
784
809
 
785
- def build_assets(self, assets: Dict[str, str]) -> None:
786
- return self.inner_builder.build_assets(assets)
787
-
788
810
  def get_build_path(self) -> Path:
789
811
  return Path("/app")
790
812
 
@@ -908,9 +930,6 @@ class WasmerBuilder:
908
930
  fs = table()
909
931
  doc.add("fs", fs)
910
932
  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
933
  if serve.mounts:
915
934
  for mount in serve.mounts:
916
935
  fs.add(
@@ -986,22 +1005,37 @@ class WasmerBuilder:
986
1005
  # has_postgres = any(service.provider == "postgres" for service in serve.services)
987
1006
  # has_redis = any(service.provider == "redis" for service in serve.services)
988
1007
  if has_mysql:
989
- capabilities["database"] = {
990
- "engine": "mysql"
991
- }
1008
+ capabilities["database"] = {"engine": "mysql"}
992
1009
  yaml_config["capabilities"] = capabilities
993
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
+
994
1030
  if "after_deploy" in serve.commands:
995
1031
  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
- }
1032
+ jobs.append(
1033
+ {
1034
+ "name": "after_deploy",
1035
+ "trigger": "post-deployment",
1036
+ "action": {"execute": {"command": "after_deploy"}},
1003
1037
  }
1004
- })
1038
+ )
1005
1039
  yaml_config["jobs"] = jobs
1006
1040
 
1007
1041
  app_yaml = yaml.dump(yaml_config)
@@ -1043,12 +1077,6 @@ class WasmerBuilder:
1043
1077
  ],
1044
1078
  )
1045
1079
 
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
1080
  def run_command(
1053
1081
  self, command: str, extra_args: Optional[List[str]] | None = None
1054
1082
  ) -> Any:
@@ -1101,6 +1129,7 @@ class Ctx:
1101
1129
  self.steps: List[Step] = []
1102
1130
  self.serves: Dict[str, Serve] = {}
1103
1131
  self.mounts: List[Mount] = []
1132
+ self.volumes: List[Volume] = []
1104
1133
  self.services: Dict[str, Service] = {}
1105
1134
 
1106
1135
  def add_package(self, package: Package) -> str:
@@ -1123,6 +1152,8 @@ class Ctx:
1123
1152
  return self.steps[int(index[len("ref:step:") :])]
1124
1153
  elif index.startswith("ref:mount:"):
1125
1154
  return self.mounts[int(index[len("ref:mount:") :])]
1155
+ elif index.startswith("ref:volume:"):
1156
+ return self.volumes[int(index[len("ref:volume:") :])]
1126
1157
  elif index.startswith("ref:service:"):
1127
1158
  return self.services[index[len("ref:service:") :]]
1128
1159
  else:
@@ -1148,14 +1179,13 @@ class Ctx:
1148
1179
  def getenv(self, name: str) -> Optional[str]:
1149
1180
  return self.builder.getenv(name)
1150
1181
 
1151
- def get_asset(self, name: str) -> Optional[str]:
1152
- return self.builder.get_asset(name)
1153
-
1154
1182
  def dep(self, name: str, version: Optional[str] = None) -> str:
1155
1183
  package = Package(name, version)
1156
1184
  return self.add_package(package)
1157
-
1158
- 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:
1159
1189
  service = Service(name, provider)
1160
1190
  return self.add_service(service)
1161
1191
 
@@ -1167,10 +1197,10 @@ class Ctx:
1167
1197
  deps: List[str],
1168
1198
  commands: Dict[str, str],
1169
1199
  cwd: Optional[str] = None,
1170
- assets: Optional[Dict[str, str]] = None,
1171
1200
  prepare: Optional[List[str]] = None,
1172
1201
  workers: Optional[List[str]] = None,
1173
1202
  mounts: Optional[List[Mount]] = None,
1203
+ volumes: Optional[List[Volume]] = None,
1174
1204
  env: Optional[Dict[str, str]] = None,
1175
1205
  services: Optional[List[str]] = None,
1176
1206
  ) -> str:
@@ -1188,7 +1218,6 @@ class Ctx:
1188
1218
  provider=provider,
1189
1219
  build=build_refs,
1190
1220
  cwd=cwd,
1191
- assets=assets,
1192
1221
  deps=dep_refs,
1193
1222
  commands=commands,
1194
1223
  prepare=prepare_steps,
@@ -1196,6 +1225,9 @@ class Ctx:
1196
1225
  mounts=self.get_refs([mount["ref"] for mount in mounts])
1197
1226
  if mounts
1198
1227
  else None,
1228
+ volumes=self.get_refs([volume["ref"] for volume in volumes])
1229
+ if volumes
1230
+ else None,
1199
1231
  env=env,
1200
1232
  services=self.get_refs(services) if services else None,
1201
1233
  )
@@ -1219,14 +1251,15 @@ class Ctx:
1219
1251
  return self.add_step(step)
1220
1252
 
1221
1253
  def copy(
1222
- 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,
1223
1259
  ) -> Optional[str]:
1224
- step = CopyStep(source, target, ignore)
1260
+ step = CopyStep(source, target, ignore, base or "source")
1225
1261
  return self.add_step(step)
1226
1262
 
1227
- def buildpath(self, name: str) -> str:
1228
- return str((self.builder.get_build_path() / name).absolute())
1229
-
1230
1263
  def env(self, **env_vars: str) -> Optional[str]:
1231
1264
  step = EnvStep(env_vars)
1232
1265
  return self.add_step(step)
@@ -1246,8 +1279,18 @@ class Ctx:
1246
1279
  "serve": str(serve_path.absolute()),
1247
1280
  }
1248
1281
 
1249
- def serve_mount(self, name: str) -> Optional[str]:
1250
- 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
+ }
1251
1294
 
1252
1295
 
1253
1296
  def print_help() -> None:
@@ -1260,6 +1303,13 @@ def print_help() -> None:
1260
1303
  console.print(panel)
1261
1304
 
1262
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
+
1263
1313
  @app.command(name="auto")
1264
1314
  def auto(
1265
1315
  path: Path = typer.Argument(
@@ -1319,12 +1369,35 @@ def auto(
1319
1369
  None,
1320
1370
  help="Name of the Wasmer app.",
1321
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
+ ),
1322
1388
  ):
1323
1389
  if not path.exists():
1324
1390
  raise Exception(f"The path {path} does not exist")
1325
1391
 
1326
1392
  if not (path / "Shipit").exists() or regenerate or regenerate_path is not None:
1327
- 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
+ )
1328
1401
 
1329
1402
  build(
1330
1403
  path,
@@ -1364,13 +1437,53 @@ def generate(
1364
1437
  None,
1365
1438
  help="Output path (defaults to the Shipit file in the provided path).",
1366
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
+ ),
1367
1456
  ):
1368
1457
  if not path.exists():
1369
1458
  raise Exception(f"The path {path} does not exist")
1370
1459
 
1371
1460
  if out is None:
1372
1461
  out = path / "Shipit"
1373
- 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)
1374
1487
  out.write_text(content)
1375
1488
  console.print(f"[bold]Generated Shipit[/bold] at {out.absolute()}")
1376
1489
 
@@ -1529,15 +1642,12 @@ def build(
1529
1642
  mod.add_callable("serve", ctx.serve)
1530
1643
  mod.add_callable("run", ctx.run)
1531
1644
  mod.add_callable("mount", ctx.mount)
1645
+ mod.add_callable("volume", ctx.volume)
1532
1646
  mod.add_callable("workdir", ctx.workdir)
1533
1647
  mod.add_callable("copy", ctx.copy)
1534
1648
  mod.add_callable("path", ctx.path)
1535
- mod.add_callable("buildpath", ctx.buildpath)
1536
- mod.add_callable("get_asset", ctx.get_asset)
1537
1649
  mod.add_callable("env", ctx.env)
1538
1650
  mod.add_callable("use", ctx.use)
1539
- # REMOVE ME
1540
- mod.add_callable("serve_mount", ctx.serve_mount)
1541
1651
 
1542
1652
  dialect = sl.Dialect.extended()
1543
1653
  dialect.enable_f_strings = True
@@ -1545,9 +1655,7 @@ def build(
1545
1655
  ast = sl.parse("shipit", source, dialect=dialect)
1546
1656
 
1547
1657
  sl.eval(mod, ast, glb)
1548
- # assert len(ctx.builds) == 1, "Only one build is allowed for now"
1549
1658
  assert len(ctx.serves) <= 1, "Only one serve is allowed for now"
1550
- # build = ctx.builds[0]
1551
1659
  env = {
1552
1660
  "PATH": "",
1553
1661
  "COLORTERM": os.environ.get("COLORTERM", ""),
@@ -1561,8 +1669,6 @@ def build(
1561
1669
  builder.build(env, serve.mounts, serve.build)
1562
1670
  if serve.prepare:
1563
1671
  builder.build_prepare(serve)
1564
- if serve.assets:
1565
- builder.build_assets(serve.assets)
1566
1672
  builder.build_serve(serve)
1567
1673
  builder.finalize_build(serve)
1568
1674
  if serve.prepare and not skip_prepare:
@@ -1580,7 +1686,7 @@ def main() -> None:
1580
1686
  app()
1581
1687
  except Exception as e:
1582
1688
  console.print(f"[bold red]{type(e).__name__}[/bold red]: {e}")
1583
- # raise e
1689
+ raise e
1584
1690
 
1585
1691
 
1586
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