shipit-cli 0.10.1__tar.gz → 0.11.1__tar.gz

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.
Files changed (27) hide show
  1. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/PKG-INFO +1 -1
  2. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/pyproject.toml +1 -1
  3. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/src/shipit/cli.py +192 -42
  4. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/src/shipit/generator.py +27 -11
  5. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/src/shipit/providers/base.py +3 -13
  6. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/src/shipit/providers/hugo.py +3 -0
  7. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/src/shipit/providers/laravel.py +3 -0
  8. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/src/shipit/providers/mkdocs.py +3 -0
  9. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/src/shipit/providers/node_static.py +4 -0
  10. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/src/shipit/providers/php.py +41 -17
  11. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/src/shipit/providers/python.py +12 -5
  12. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/src/shipit/providers/registry.py +0 -2
  13. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/src/shipit/providers/staticfile.py +3 -0
  14. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/src/shipit/providers/wordpress.py +7 -5
  15. shipit_cli-0.11.1/src/shipit/version.py +5 -0
  16. shipit_cli-0.10.1/src/shipit/providers/gatsby.py +0 -87
  17. shipit_cli-0.10.1/src/shipit/version.py +0 -5
  18. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/.gitignore +0 -0
  19. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/README.md +0 -0
  20. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/src/shipit/__init__.py +0 -0
  21. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/src/shipit/assets/php/php.ini +0 -0
  22. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/src/shipit/assets/wordpress/install.sh +0 -0
  23. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/src/shipit/assets/wordpress/wp-config.php +0 -0
  24. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/src/shipit/procfile.py +0 -0
  25. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/tests/test_e2e.py +0 -0
  26. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/tests/test_generate_shipit_examples.py +0 -0
  27. {shipit_cli-0.10.1 → shipit_cli-0.11.1}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shipit-cli
3
- Version: 0.10.1
3
+ Version: 0.11.1
4
4
  Summary: Shipit CLI is the best way to build, serve and deploy your projects anywhere.
5
5
  Project-URL: homepage, https://wasmer.io
6
6
  Project-URL: repository, https://github.com/wasmerio/shipit
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shipit-cli"
3
- version = "0.10.1"
3
+ version = "0.11.1"
4
4
  description = "Shipit CLI is the best way to build, serve and deploy your projects anywhere."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -16,6 +16,7 @@ from typing import (
16
16
  Optional,
17
17
  Protocol,
18
18
  Set,
19
+ Tuple,
19
20
  TypedDict,
20
21
  Union,
21
22
  Literal,
@@ -33,7 +34,7 @@ from rich.rule import Rule
33
34
  from rich.syntax import Syntax
34
35
 
35
36
  from shipit.version import version as shipit_version
36
- from shipit.generator import generate_shipit
37
+ from shipit.generator import generate_shipit, detect_provider
37
38
  from shipit.providers.base import CustomCommands
38
39
  from shipit.procfile import Procfile
39
40
  from dotenv import dotenv_values
@@ -88,11 +89,13 @@ class Serve:
88
89
  class Package:
89
90
  name: str
90
91
  version: Optional[str] = None
92
+ architecture: Optional[Literal["64-bit", "32-bit"]] = None
91
93
 
92
94
  def __str__(self) -> str: # pragma: no cover - simple representation
95
+ name = f"{self.name}({self.architecture})" if self.architecture else self.name
93
96
  if self.version is None:
94
- return self.name
95
- return f"{self.name}@{self.version}"
97
+ return name
98
+ return f"{name}@{self.version}"
96
99
 
97
100
 
98
101
  @dataclass
@@ -780,6 +783,25 @@ class WasmerBuilder:
780
783
  "dependencies": {
781
784
  "latest": "php/php-32@=8.3.2102",
782
785
  "8.3": "php/php-32@=8.3.2102",
786
+ "8.2": "php/php-32@=8.2.2801",
787
+ "8.1": "php/php-32@=8.1.3201",
788
+ "7.4": "php/php-32@=7.4.3301",
789
+ },
790
+ "architecture_dependencies": {
791
+ "64-bit": {
792
+ "latest": "php/php-64@=8.3.2102",
793
+ "8.3": "php/php-64@=8.3.2102",
794
+ "8.2": "php/php-64@=8.2.2801",
795
+ "8.1": "php/php-64@=8.1.3201",
796
+ "7.4": "php/php-64@=7.4.3301",
797
+ },
798
+ "32-bit": {
799
+ "latest": "php/php-32@=8.3.2102",
800
+ "8.3": "php/php-32@=8.3.2102",
801
+ "8.2": "php/php-32@=8.2.2801",
802
+ "8.1": "php/php-32@=8.1.3201",
803
+ "7.4": "php/php-32@=7.4.3301",
804
+ },
783
805
  },
784
806
  "scripts": {"php"},
785
807
  "aliases": {},
@@ -925,13 +947,20 @@ class WasmerBuilder:
925
947
  for dep in deps:
926
948
  if dep.name in self.mapper:
927
949
  version = dep.version or "latest"
928
- if version in self.mapper[dep.name]["dependencies"]:
950
+ mapped_dependencies = self.mapper[dep.name]["dependencies"]
951
+ if dep.architecture:
952
+ architecture_dependencies = (
953
+ self.mapper[dep.name]
954
+ .get("architecture_dependencies", {})
955
+ .get(dep.architecture, {})
956
+ )
957
+ if architecture_dependencies:
958
+ mapped_dependencies = architecture_dependencies
959
+ if version in mapped_dependencies:
929
960
  console.print(
930
961
  f"* {dep.name}@{version} mapped to {self.mapper[dep.name]['dependencies'][version]}"
931
962
  )
932
- package_name, version = self.mapper[dep.name]["dependencies"][
933
- version
934
- ].split("@")
963
+ package_name, version = mapped_dependencies[version].split("@")
935
964
  dependencies.add(package_name, version)
936
965
  scripts = self.mapper[dep.name].get("scripts") or []
937
966
  for script in scripts:
@@ -1156,6 +1185,7 @@ class Ctx:
1156
1185
  self.mounts: List[Mount] = []
1157
1186
  self.volumes: List[Volume] = []
1158
1187
  self.services: Dict[str, Service] = {}
1188
+ self.getenv_variables: Set[str] = set()
1159
1189
 
1160
1190
  def add_package(self, package: Package) -> str:
1161
1191
  index = f"{package.name}@{package.version}" if package.version else package.name
@@ -1202,10 +1232,16 @@ class Ctx:
1202
1232
  return f"ref:step:{len(self.steps) - 1}"
1203
1233
 
1204
1234
  def getenv(self, name: str) -> Optional[str]:
1235
+ self.getenv_variables.add(name)
1205
1236
  return self.builder.getenv(name)
1206
1237
 
1207
- def dep(self, name: str, version: Optional[str] = None) -> str:
1208
- package = Package(name, version)
1238
+ def dep(
1239
+ self,
1240
+ name: str,
1241
+ version: Optional[str] = None,
1242
+ architecture: Optional[Literal["64-bit", "32-bit"]] = None,
1243
+ ) -> str:
1244
+ package = Package(name, version, architecture)
1209
1245
  return self.add_package(package)
1210
1246
 
1211
1247
  def service(
@@ -1318,6 +1354,43 @@ class Ctx:
1318
1354
  }
1319
1355
 
1320
1356
 
1357
+ def evaluate_shipit(path: Path, builder: Builder) -> Tuple[Ctx, Serve]:
1358
+ shipit_file = path / "Shipit"
1359
+ if not shipit_file.exists():
1360
+ raise FileNotFoundError(
1361
+ f"Shipit file not found at {shipit_file}. Run `shipit generate {path}` to create it."
1362
+ )
1363
+ source = shipit_file.read_text()
1364
+ ctx = Ctx(builder)
1365
+ glb = sl.Globals.standard()
1366
+ mod = sl.Module()
1367
+
1368
+ mod.add_callable("service", ctx.service)
1369
+ mod.add_callable("getenv", ctx.getenv)
1370
+ mod.add_callable("dep", ctx.dep)
1371
+ mod.add_callable("serve", ctx.serve)
1372
+ mod.add_callable("run", ctx.run)
1373
+ mod.add_callable("mount", ctx.mount)
1374
+ mod.add_callable("volume", ctx.volume)
1375
+ mod.add_callable("workdir", ctx.workdir)
1376
+ mod.add_callable("copy", ctx.copy)
1377
+ mod.add_callable("path", ctx.path)
1378
+ mod.add_callable("env", ctx.env)
1379
+ mod.add_callable("use", ctx.use)
1380
+
1381
+ dialect = sl.Dialect.extended()
1382
+ dialect.enable_f_strings = True
1383
+
1384
+ ast = sl.parse("shipit", source, dialect=dialect)
1385
+
1386
+ sl.eval(mod, ast, glb)
1387
+ if not ctx.serves:
1388
+ raise ValueError(f"No serve definition found in {shipit_file}")
1389
+ assert len(ctx.serves) <= 1, "Only one serve is allowed for now"
1390
+ serve = next(iter(ctx.serves.values()))
1391
+ return ctx, serve
1392
+
1393
+
1321
1394
  def print_help() -> None:
1322
1395
  panel = Panel(
1323
1396
  f"Shipit {shipit_version}",
@@ -1418,6 +1491,10 @@ def auto(
1418
1491
  None,
1419
1492
  help="The environment to use (defaults to `.env`, it will use .env.<env_name> if provided)",
1420
1493
  ),
1494
+ use_provider: Optional[str] = typer.Option(
1495
+ None,
1496
+ help="Use a specific provider to build the project.",
1497
+ ),
1421
1498
  ):
1422
1499
  if not path.exists():
1423
1500
  raise Exception(f"The path {path} does not exist")
@@ -1430,6 +1507,7 @@ def auto(
1430
1507
  install_command=install_command,
1431
1508
  build_command=build_command,
1432
1509
  start_command=start_command,
1510
+ use_provider=use_provider,
1433
1511
  )
1434
1512
 
1435
1513
  build(
@@ -1488,6 +1566,10 @@ def generate(
1488
1566
  None,
1489
1567
  help="The start command to use (overwrites the default)",
1490
1568
  ),
1569
+ use_provider: Optional[str] = typer.Option(
1570
+ None,
1571
+ help="Use a specific provider to build the project.",
1572
+ ),
1491
1573
  ):
1492
1574
  if not path.exists():
1493
1575
  raise Exception(f"The path {path} does not exist")
@@ -1518,7 +1600,7 @@ def generate(
1518
1600
  custom_commands.install = install_command
1519
1601
  if build_command:
1520
1602
  custom_commands.build = build_command
1521
- content = generate_shipit(path, custom_commands)
1603
+ content = generate_shipit(path, custom_commands, use_provider=use_provider)
1522
1604
  out.write_text(content)
1523
1605
  console.print(f"[bold]Generated Shipit[/bold] at {out.absolute()}")
1524
1606
 
@@ -1528,7 +1610,8 @@ def generate(
1528
1610
  context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
1529
1611
  )
1530
1612
  def _default(ctx: typer.Context) -> None:
1531
- print_help()
1613
+ if ctx.invoked_subcommand is None:
1614
+ print_help()
1532
1615
 
1533
1616
 
1534
1617
  @app.command(name="deploy")
@@ -1612,6 +1695,103 @@ def serve(
1612
1695
  raise Exception("Wasmer deploy is only supported for Wasmer builders")
1613
1696
 
1614
1697
 
1698
+ @app.command(name="plan")
1699
+ def plan(
1700
+ path: Path = typer.Argument(
1701
+ Path("."),
1702
+ help="Project path (defaults to current directory).",
1703
+ show_default=False,
1704
+ ),
1705
+ wasmer: bool = typer.Option(
1706
+ False,
1707
+ help="Use Wasmer to evaluate the project.",
1708
+ ),
1709
+ wasmer_bin: Optional[Path] = typer.Option(
1710
+ None,
1711
+ help="The path to the Wasmer binary.",
1712
+ ),
1713
+ wasmer_registry: Optional[str] = typer.Option(
1714
+ None,
1715
+ help="Wasmer registry.",
1716
+ ),
1717
+ wasmer_token: Optional[str] = typer.Option(
1718
+ None,
1719
+ help="Wasmer token.",
1720
+ ),
1721
+ docker: bool = typer.Option(
1722
+ False,
1723
+ help="Use Docker to evaluate the project.",
1724
+ ),
1725
+ docker_client: Optional[str] = typer.Option(
1726
+ None,
1727
+ help="Use a specific Docker client (such as depot, podman, etc.)",
1728
+ ),
1729
+ ) -> None:
1730
+ if not path.exists():
1731
+ raise Exception(f"The path {path} does not exist")
1732
+
1733
+ custom_commands = CustomCommands()
1734
+ procfile_path = path / "Procfile"
1735
+ if procfile_path.exists():
1736
+ try:
1737
+ procfile = Procfile.loads(procfile_path.read_text())
1738
+ custom_commands.start = procfile.get_start_command()
1739
+ except Exception:
1740
+ pass
1741
+
1742
+ builder: Builder
1743
+ if docker or docker_client:
1744
+ builder = DockerBuilder(path, docker_client)
1745
+ else:
1746
+ builder = LocalBuilder(path)
1747
+ if wasmer:
1748
+ builder = WasmerBuilder(
1749
+ builder, path, registry=wasmer_registry, token=wasmer_token, bin=wasmer_bin
1750
+ )
1751
+
1752
+ ctx, serve = evaluate_shipit(path, builder)
1753
+ metadata_commands: Dict[str, Optional[str]] = {
1754
+ "start": serve.commands.get("start"),
1755
+ "after_deploy": serve.commands.get("after_deploy"),
1756
+ }
1757
+
1758
+ def _collect_group_commands(group: str) -> Optional[str]:
1759
+ commands = [
1760
+ step.command
1761
+ for step in serve.build
1762
+ if isinstance(step, RunStep) and step.group == group
1763
+ ]
1764
+ if not commands:
1765
+ return None
1766
+ return " && ".join(commands)
1767
+
1768
+ metadata_install = _collect_group_commands("install")
1769
+ metadata_build = _collect_group_commands("build")
1770
+ metadata_commands["install"] = metadata_install
1771
+ metadata_commands["build"] = metadata_build
1772
+ platform: Optional[str]
1773
+ try:
1774
+ provider_cls = detect_provider(path, custom_commands)
1775
+ provider_instance = provider_cls(path, custom_commands)
1776
+ provider_instance.initialize()
1777
+ platform = provider_instance.platform()
1778
+ except Exception:
1779
+ platform = None
1780
+ plan_output = {
1781
+ "provider": serve.provider,
1782
+ "metadata": {
1783
+ "platform": platform,
1784
+ "commands": metadata_commands,
1785
+ },
1786
+ "config": sorted(ctx.getenv_variables),
1787
+ "services": [
1788
+ {"name": svc.name, "provider": svc.provider}
1789
+ for svc in (serve.services or [])
1790
+ ],
1791
+ }
1792
+ print(json.dumps(plan_output, indent=4))
1793
+
1794
+
1615
1795
  @app.command(name="build")
1616
1796
  def build(
1617
1797
  path: Path = typer.Argument(
@@ -1659,12 +1839,6 @@ def build(
1659
1839
  if not path.exists():
1660
1840
  raise Exception(f"The path {path} does not exist")
1661
1841
 
1662
- ab_file = path / "Shipit"
1663
- if not ab_file.exists():
1664
- raise FileNotFoundError(
1665
- f"Shipit file not found at {ab_file}. Run `shipit generate {path}` to create it."
1666
- )
1667
- source = open(ab_file).read()
1668
1842
  builder: Builder
1669
1843
  if docker or docker_client:
1670
1844
  builder = DockerBuilder(path, docker_client)
@@ -1675,30 +1849,7 @@ def build(
1675
1849
  builder, path, registry=wasmer_registry, token=wasmer_token, bin=wasmer_bin
1676
1850
  )
1677
1851
 
1678
- ctx = Ctx(builder)
1679
- glb = sl.Globals.standard()
1680
- mod = sl.Module()
1681
-
1682
- mod.add_callable("service", ctx.service)
1683
- mod.add_callable("getenv", ctx.getenv)
1684
- mod.add_callable("dep", ctx.dep)
1685
- mod.add_callable("serve", ctx.serve)
1686
- mod.add_callable("run", ctx.run)
1687
- mod.add_callable("mount", ctx.mount)
1688
- mod.add_callable("volume", ctx.volume)
1689
- mod.add_callable("workdir", ctx.workdir)
1690
- mod.add_callable("copy", ctx.copy)
1691
- mod.add_callable("path", ctx.path)
1692
- mod.add_callable("env", ctx.env)
1693
- mod.add_callable("use", ctx.use)
1694
-
1695
- dialect = sl.Dialect.extended()
1696
- dialect.enable_f_strings = True
1697
-
1698
- ast = sl.parse("shipit", source, dialect=dialect)
1699
-
1700
- sl.eval(mod, ast, glb)
1701
- assert len(ctx.serves) <= 1, "Only one serve is allowed for now"
1852
+ ctx, serve = evaluate_shipit(path, builder)
1702
1853
  env = {
1703
1854
  "PATH": "",
1704
1855
  "COLORTERM": os.environ.get("COLORTERM", ""),
@@ -1706,7 +1857,6 @@ def build(
1706
1857
  "LS_COLORS": os.environ.get("LS_COLORS", "0"),
1707
1858
  "CLICOLOR": os.environ.get("CLICOLOR", "0"),
1708
1859
  }
1709
- serve = next(iter(ctx.serves.values()))
1710
1860
 
1711
1861
  if skip_docker_if_safe_build and serve.build and len(serve.build) > 0:
1712
1862
  # If it doesn't have a run step, then it's safe to skip Docker and run all the
@@ -64,27 +64,39 @@ def _emit_dependencies_declarations(
64
64
  declared.add(alias)
65
65
 
66
66
  version_var = None
67
+ architecture_var = None
67
68
  if dep.env_var:
68
69
  default = f' or "{dep.default_version}"' if dep.default_version else ""
69
70
  version_key = alias + "_version"
70
71
  lines.append(f'{version_key} = getenv("{dep.env_var}"){default}')
71
72
  version_var = version_key
73
+ if dep.architecture_var:
74
+ architecture_key = alias + "_architecture"
75
+ lines.append(f'{architecture_key} = getenv("{dep.architecture_var}")')
76
+ architecture_var = architecture_key
77
+ vars = [f'"{dep.name}"']
72
78
  if version_var:
73
- lines.append(f'{alias} = dep("{dep.name}", {version_var})')
74
- else:
75
- lines.append(f'{alias} = dep("{dep.name}")')
79
+ vars.append(version_var)
80
+ if architecture_var:
81
+ vars.append(f"architecture={architecture_var}")
82
+ lines.append(f"{alias} = dep({', '.join(vars)})")
76
83
 
77
84
  return "\n".join(lines), serve_vars, build_vars
78
85
 
79
86
 
80
- def generate_shipit(path: Path, custom_commands: CustomCommands) -> str:
81
- provider_cls = detect_provider(path, custom_commands)
87
+ def generate_shipit(path: Path, custom_commands: CustomCommands, use_provider: Optional[str] = None) -> str:
88
+ provider_cls = None
89
+ if use_provider:
90
+ provider_cls = next((p for p in _providers() if p.name().lower() == use_provider.lower()), None)
91
+ if not provider_cls:
92
+ provider_cls = detect_provider(path, custom_commands)
82
93
  provider = provider_cls(path, custom_commands)
83
94
 
84
95
  # Collect parts
85
96
  plan = ProviderPlan(
86
97
  serve_name=provider.serve_name(),
87
98
  provider=provider.provider_kind(),
99
+ platform=provider.platform(),
88
100
  mounts=provider.mounts(),
89
101
  volumes=provider.volumes(),
90
102
  declarations=provider.declarations(),
@@ -109,7 +121,9 @@ def generate_shipit(path: Path, custom_commands: CustomCommands) -> str:
109
121
 
110
122
  build_steps_block = ",\n".join([f" {s}" for s in build_steps])
111
123
  deps_array = ", ".join(serve_dep_vars)
112
- commands_lines = ",\n".join([f' "{k}": {v}.replace("$PORT", PORT)' for k, v in plan.commands.items()])
124
+ commands_lines = ",\n".join(
125
+ [f' "{k}": {v}.replace("$PORT", PORT)' for k, v in plan.commands.items()]
126
+ )
113
127
  env_lines = None
114
128
  if plan.env is not None:
115
129
  if len(plan.env) == 0:
@@ -137,24 +151,26 @@ def generate_shipit(path: Path, custom_commands: CustomCommands) -> str:
137
151
  out.append("")
138
152
 
139
153
  for m in plan.mounts:
140
- out.append(f"{m.name} = mount(\"{m.name}\")")
154
+ out.append(f'{m.name} = mount("{m.name}")')
141
155
  out.append("")
142
156
 
143
157
  if plan.volumes:
144
158
  for v in plan.volumes:
145
- out.append(f"{v.var_name or v.name} = volume(\"{v.name}\", {v.serve_path})")
159
+ out.append(f'{v.var_name or v.name} = volume("{v.name}", {v.serve_path})')
146
160
  out.append("")
147
161
 
148
162
  if plan.services:
149
163
  for s in plan.services:
150
- out.append(f"{s.name} = service(\n name=\"{s.name}\",\n provider=\"{s.provider}\"\n)")
164
+ out.append(
165
+ f'{s.name} = service(\n name="{s.name}",\n provider="{s.provider}"\n)'
166
+ )
151
167
  out.append("")
152
168
 
153
- out.append("PORT = getenv(\"PORT\") or\"8080\"")
169
+ out.append('PORT = getenv("PORT") or "8080"')
154
170
 
155
171
  if plan.declarations:
156
172
  out.append(plan.declarations)
157
-
173
+
158
174
  out.append("")
159
175
  out.append("serve(")
160
176
  out.append(f' name="{plan.serve_name}",')
@@ -29,6 +29,7 @@ class Provider(Protocol):
29
29
  # Structured plan steps (no path args; use self.path)
30
30
  def serve_name(self) -> str: ...
31
31
  def provider_kind(self) -> str: ...
32
+ def platform(self) -> Optional[str]: ...
32
33
  def dependencies(self) -> list["DependencySpec"]: ...
33
34
  def declarations(self) -> Optional[str]: ...
34
35
  def build_steps(self) -> list[str]: ...
@@ -46,6 +47,7 @@ class DependencySpec:
46
47
  name: str
47
48
  env_var: Optional[str] = None
48
49
  default_version: Optional[str] = None
50
+ architecture_var: Optional[str] = None
49
51
  alias: Optional[str] = None # Variable name in Shipit plan
50
52
  use_in_build: bool = False
51
53
  use_in_serve: bool = False
@@ -77,6 +79,7 @@ class ProviderPlan:
77
79
  serve_name: str
78
80
  provider: str
79
81
  mounts: List[MountSpec]
82
+ platform: Optional[str] = None
80
83
  volumes: List[VolumeSpec] = field(default_factory=list)
81
84
  declarations: Optional[str] = None
82
85
  dependencies: List[DependencySpec] = field(default_factory=list)
@@ -89,16 +92,3 @@ class ProviderPlan:
89
92
 
90
93
  def _exists(path: Path, *candidates: str) -> bool:
91
94
  return any((path / c).exists() for c in candidates)
92
-
93
-
94
- def _has_dependency(pkg_json: Path, dep: str) -> bool:
95
- try:
96
- import json
97
-
98
- data = json.loads(pkg_json.read_text())
99
- for section in ("dependencies", "devDependencies", "peerDependencies"):
100
- if dep in data.get(section, {}):
101
- return True
102
- except Exception:
103
- return False
104
- return False
@@ -30,6 +30,9 @@ class HugoProvider(StaticFileProvider):
30
30
  def provider_kind(self) -> str:
31
31
  return "staticsite"
32
32
 
33
+ def platform(self) -> Optional[str]:
34
+ return "hugo"
35
+
33
36
  def dependencies(self) -> list[DependencySpec]:
34
37
  return [
35
38
  DependencySpec(
@@ -39,6 +39,9 @@ class LaravelProvider:
39
39
  def provider_kind(self) -> str:
40
40
  return "php"
41
41
 
42
+ def platform(self) -> Optional[str]:
43
+ return "laravel"
44
+
42
45
  def dependencies(self) -> list[DependencySpec]:
43
46
  return [
44
47
  DependencySpec(
@@ -43,6 +43,9 @@ class MkdocsProvider(StaticFileProvider):
43
43
  def provider_kind(self) -> str:
44
44
  return "mkdocs-site"
45
45
 
46
+ def platform(self) -> Optional[str]:
47
+ return "mkdocs"
48
+
46
49
  def dependencies(self) -> list[DependencySpec]:
47
50
  return [
48
51
  *self.python_provider.dependencies(),
@@ -122,6 +122,7 @@ class NodeStaticProvider(StaticFileProvider):
122
122
 
123
123
  def __init__(self, path: Path, custom_commands: CustomCommands):
124
124
  super().__init__(path, custom_commands)
125
+ self.static_generator: Optional[StaticGenerator] = None
125
126
  if (path / "package-lock.json").exists():
126
127
  self.package_manager = PackageManager.NPM
127
128
  elif (path / "pnpm-lock.yaml").exists():
@@ -233,6 +234,9 @@ class NodeStaticProvider(StaticFileProvider):
233
234
  def provider_kind(self) -> str:
234
235
  return "staticsite"
235
236
 
237
+ def platform(self) -> Optional[str]:
238
+ return self.static_generator.value if self.static_generator else None
239
+
236
240
  def dependencies(self) -> list[DependencySpec]:
237
241
  package_manager_dep = self.package_manager.as_dependency(self.path)
238
242
  package_manager_dep.use_in_build = True
@@ -20,19 +20,30 @@ class PhpProvider:
20
20
  def __init__(self, path: Path, custom_commands: CustomCommands):
21
21
  self.path = path
22
22
  self.custom_commands = custom_commands
23
-
23
+ self.has_composer = _exists(self.path, "composer.json", "composer.lock") or (
24
+ custom_commands.install and custom_commands.install.startswith("composer ")
25
+ )
26
+
24
27
  @classmethod
25
28
  def name(cls) -> str:
26
29
  return "php"
27
30
 
28
31
  @classmethod
29
- def detect(cls, path: Path, custom_commands: CustomCommands) -> Optional[DetectResult]:
32
+ def detect(
33
+ cls, path: Path, custom_commands: CustomCommands
34
+ ) -> Optional[DetectResult]:
30
35
  if _exists(path, "composer.json") and _exists(path, "public/index.php"):
31
36
  return DetectResult(cls.name(), 60)
32
- if _exists(path, "index.php") or _exists(path, "public/index.php") or _exists(path, "app/index.php"):
37
+ if (
38
+ _exists(path, "index.php")
39
+ or _exists(path, "public/index.php")
40
+ or _exists(path, "app/index.php")
41
+ ):
33
42
  return DetectResult(cls.name(), 10)
34
43
  if custom_commands.start and custom_commands.start.startswith("php "):
35
44
  return DetectResult(cls.name(), 70)
45
+ if custom_commands.install and custom_commands.install.startswith("composer "):
46
+ return DetectResult(cls.name(), 30)
36
47
  return None
37
48
 
38
49
  def initialize(self) -> None:
@@ -44,8 +55,8 @@ class PhpProvider:
44
55
  def provider_kind(self) -> str:
45
56
  return "php"
46
57
 
47
- def has_composer(self) -> bool:
48
- return _exists(self.path, "composer.json", "composer.lock")
58
+ def platform(self) -> Optional[str]:
59
+ return None
49
60
 
50
61
  def dependencies(self) -> list[DependencySpec]:
51
62
  deps = [
@@ -53,32 +64,39 @@ class PhpProvider:
53
64
  "php",
54
65
  env_var="SHIPIT_PHP_VERSION",
55
66
  default_version="8.3",
67
+ architecture_var="SHIPIT_PHP_ARCHITECTURE",
56
68
  use_in_build=True,
57
69
  use_in_serve=True,
58
70
  ),
59
71
  ]
60
- if self.has_composer():
72
+ if self.has_composer:
61
73
  deps.append(DependencySpec("composer", use_in_build=True))
62
74
  deps.append(DependencySpec("bash", use_in_serve=True))
63
75
  return deps
64
76
 
65
77
  def declarations(self) -> Optional[str]:
66
- return "HOME = getenv(\"HOME\")\n"
78
+ if self.has_composer:
79
+ return 'HOME = getenv("HOME")\n'
80
+ return None
67
81
 
68
82
  def build_steps(self) -> list[str]:
69
83
  steps = [
70
- "workdir(app[\"build\"])",
84
+ 'workdir(app["build"])',
71
85
  ]
72
86
  if _exists(self.path, "php.ini"):
73
87
  steps.append('copy("php.ini", "{}/php.ini".format(assets["build"]))')
74
88
  else:
75
- steps.append('copy("php/php.ini", "{}/php.ini".format(assets["build"]), base="assets")')
89
+ steps.append(
90
+ 'copy("php/php.ini", "{}/php.ini".format(assets["build"]), base="assets")'
91
+ )
76
92
 
77
- if self.has_composer():
78
- steps.append("env(HOME=HOME, COMPOSER_FUND=\"0\")")
79
- steps.append("run(\"composer install --optimize-autoloader --no-scripts --no-interaction\", inputs=[\"composer.json\", \"composer.lock\"], outputs=[\".\"], group=\"install\")")
93
+ if self.has_composer:
94
+ steps.append('env(HOME=HOME, COMPOSER_FUND="0")')
95
+ steps.append(
96
+ 'run("composer install --optimize-autoloader --no-scripts --no-interaction", inputs=["composer.json", "composer.lock"], outputs=["."], group="install")'
97
+ )
80
98
 
81
- steps.append("copy(\".\", \".\", ignore=[\".git\"])")
99
+ steps.append('copy(".", ".", ignore=[".git"])')
82
100
  return steps
83
101
 
84
102
  def prepare_steps(self) -> Optional[list[str]]:
@@ -92,12 +110,18 @@ class PhpProvider:
92
110
 
93
111
  def base_commands(self) -> Dict[str, str]:
94
112
  if _exists(self.path, "public/index.php"):
95
- return {"start": '"php -S localhost:{} -t {}/public".format(PORT, app["serve"])'}
113
+ return {
114
+ "start": '"php -S localhost:{} -t {}/public".format(PORT, app["serve"])'
115
+ }
96
116
  elif _exists(self.path, "app/index.php"):
97
- return {"start": '"php -S localhost:{} -t {}/app".format(PORT, app["serve"])'}
117
+ return {
118
+ "start": '"php -S localhost:{} -t {}/app".format(PORT, app["serve"])'
119
+ }
98
120
  elif _exists(self.path, "index.php"):
99
121
  return {"start": '"php -S localhost:{} -t {}".format(PORT, app["serve"])'}
100
- return {}
122
+ return {
123
+ "start": '"php -S localhost:{} -t {}".format(PORT, app["serve"])',
124
+ }
101
125
 
102
126
  def mounts(self) -> list[MountSpec]:
103
127
  return [
@@ -112,6 +136,6 @@ class PhpProvider:
112
136
  return {
113
137
  "PHP_INI_SCAN_DIR": '"{}".format(assets["serve"])',
114
138
  }
115
-
139
+
116
140
  def services(self) -> list[ServiceSpec]:
117
141
  return []
@@ -216,7 +216,7 @@ class PythonProvider:
216
216
  return DetectResult(cls.name(), 70)
217
217
  return DetectResult(cls.name(), 50)
218
218
  if custom_commands.start:
219
- if custom_commands.start.startswith("python ") or custom_commands.start.startswith("uv ") or custom_commands.start.startswith("uvicorn "):
219
+ if custom_commands.start.startswith("python ") or custom_commands.start.startswith("uv ") or custom_commands.start.startswith("uvicorn ") or custom_commands.start.startswith("gunicorn "):
220
220
  return DetectResult(cls.name(), 80)
221
221
  return None
222
222
 
@@ -229,6 +229,9 @@ class PythonProvider:
229
229
  def provider_kind(self) -> str:
230
230
  return "python"
231
231
 
232
+ def platform(self) -> Optional[str]:
233
+ return self.framework.value if self.framework else None
234
+
232
235
  def dependencies(self) -> list[DependencySpec]:
233
236
  deps = [
234
237
  DependencySpec(
@@ -307,9 +310,9 @@ class PythonProvider:
307
310
  # Join inputs
308
311
  inputs = ", ".join([f'"{input}"' for input in input_files])
309
312
  steps += [
310
- 'env(UV_PROJECT_ENVIRONMENT=local_venv["build"] if cross_platform else venv["build"])',
313
+ 'env(UV_PROJECT_ENVIRONMENT=local_venv["build"] if cross_platform else venv["build"], UV_PYTHON_PREFERENCE="only-system", UV_PYTHON=f"python{python_version}")',
311
314
  'copy(".", ".")' if self.install_requires_all_files else None,
312
- f'run(f"uv sync --compile --python python{{python_version}} --no-managed-python{extra_args}", inputs=[{inputs}], group="install")',
315
+ f'run(f"uv sync{extra_args}", inputs=[{inputs}], group="install")',
313
316
  'copy("pyproject.toml", "pyproject.toml")'
314
317
  if not self.install_requires_all_files
315
318
  else None,
@@ -317,14 +320,14 @@ class PythonProvider:
317
320
  ]
318
321
  if not self.only_build:
319
322
  steps += [
320
- 'run(f"uv pip compile pyproject.toml --python-version={python_version} --universal --extra-index-url {python_extra_index_url} --index-url=https://pypi.org/simple --emit-index-url --no-deps -o cross-requirements.txt", outputs=["cross-requirements.txt"]) if cross_platform else None',
323
+ 'run(f"uv pip compile pyproject.toml --universal --extra-index-url {python_extra_index_url} --index-url=https://pypi.org/simple --emit-index-url --no-deps -o cross-requirements.txt", outputs=["cross-requirements.txt"]) if cross_platform else None',
321
324
  f'run(f"uvx pip install -r cross-requirements.txt {extra_deps} --target {{python_cross_packages_path}} --platform {{cross_platform}} --only-binary=:all: --python-version={{python_version}} --compile") if cross_platform else None',
322
325
  'run("rm cross-requirements.txt") if cross_platform else None',
323
326
  ]
324
327
  elif has_requirements or extra_deps:
325
328
  steps += [
326
329
  'env(UV_PROJECT_ENVIRONMENT=local_venv["build"] if cross_platform else venv["build"])',
327
- 'run(f"uv init --no-workspace --no-managed-python --python python{python_version}", inputs=[], outputs=["uv.lock"], group="install")',
330
+ 'run(f"uv init --no-workspace", inputs=[], outputs=["uv.lock"], group="install")',
328
331
  'copy(".", ".")' if self.install_requires_all_files else None,
329
332
  ]
330
333
  if has_requirements:
@@ -351,6 +354,10 @@ class PythonProvider:
351
354
  'run("mkdir -p {}/bin".format(venv["build"])) if cross_platform else None',
352
355
  'run("cp {}/bin/mcp {}/bin/mcp".format(local_venv["build"], venv["build"])) if cross_platform else None',
353
356
  ]
357
+ if self.framework == PythonFramework.Django:
358
+ steps += [
359
+ 'run("python manage.py collectstatic --noinput", group="build")',
360
+ ]
354
361
  return list(filter(None, steps))
355
362
 
356
363
  def prepare_steps(self) -> Optional[list[str]]:
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from .base import Provider
4
- from .gatsby import GatsbyProvider
5
4
  from .hugo import HugoProvider
6
5
  from .laravel import LaravelProvider
7
6
  from .mkdocs import MkdocsProvider
@@ -16,7 +15,6 @@ def providers() -> list[type[Provider]]:
16
15
  # Order matters: more specific providers first
17
16
  return [
18
17
  LaravelProvider,
19
- # GatsbyProvider,
20
18
  HugoProvider,
21
19
  MkdocsProvider,
22
20
  PythonProvider,
@@ -61,6 +61,9 @@ class StaticFileProvider:
61
61
  def provider_kind(self) -> str:
62
62
  return "staticfile"
63
63
 
64
+ def platform(self) -> Optional[str]:
65
+ return None
66
+
64
67
  def dependencies(self) -> list[DependencySpec]:
65
68
  return [
66
69
  DependencySpec(
@@ -19,8 +19,7 @@ from .php import PhpProvider
19
19
 
20
20
  class WordPressProvider(PhpProvider):
21
21
  def __init__(self, path: Path, custom_commands: CustomCommands):
22
- self.path = path
23
- self.custom_commands = custom_commands
22
+ super().__init__(path, custom_commands)
24
23
 
25
24
  @classmethod
26
25
  def name(cls) -> str:
@@ -47,6 +46,9 @@ class WordPressProvider(PhpProvider):
47
46
  def provider_kind(self) -> str:
48
47
  return "php"
49
48
 
49
+ def platform(self) -> Optional[str]:
50
+ return "wordpress"
51
+
50
52
  def dependencies(self) -> list[DependencySpec]:
51
53
  return [
52
54
  *super().dependencies(),
@@ -54,7 +56,7 @@ class WordPressProvider(PhpProvider):
54
56
  ]
55
57
 
56
58
  def declarations(self) -> Optional[str]:
57
- return super().declarations() + (
59
+ return (super().declarations() or "") + (
58
60
  'wp_cli_version = getenv("SHIPIT_WPCLI_VERSION")\n'
59
61
  "if wp_cli_version:\n"
60
62
  ' wp_cli_download_url = f"https://github.com/wp-cli/wp-cli/releases/download/v{wp_cli_version}/wp-cli-{wp_cli_version}.phar"\n'
@@ -65,7 +67,7 @@ class WordPressProvider(PhpProvider):
65
67
  def build_steps(self) -> list[str]:
66
68
  steps = [
67
69
  'copy(wp_cli_download_url, "{}/wp-cli.phar".format(assets["build"]))',
68
- 'copy("wordpress/install.sh", "{}/wordpress-install.sh".format(assets["build"]), base="assets")',
70
+ 'copy("wordpress/install.sh", "{}/setup-wp.sh".format(assets["build"]), base="assets")',
69
71
  ]
70
72
  if not _exists(self.path, "wp-config.php"):
71
73
  steps.append(
@@ -80,7 +82,7 @@ class WordPressProvider(PhpProvider):
80
82
  commands = super().commands()
81
83
  return {
82
84
  "wp": '"php {}/wp-cli.phar --allow-root --path={}".format(assets["serve"], app["serve"])',
83
- "after_deploy": '"bash {}/wordpress-install.sh".format(assets["serve"])',
85
+ "after_deploy": '"bash {}/setup-wp.sh".format(assets["serve"])',
84
86
  **commands,
85
87
  }
86
88
 
@@ -0,0 +1,5 @@
1
+ __all__ = ["version", "version_info"]
2
+
3
+
4
+ version = "0.11.1"
5
+ version_info = (0, 11, 1, "final", 0)
@@ -1,87 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
- from typing import Dict, Optional
5
-
6
- from .base import (
7
- DetectResult,
8
- DependencySpec,
9
- Provider,
10
- _exists,
11
- _has_dependency,
12
- MountSpec,
13
- ServiceSpec,
14
- VolumeSpec,
15
- CustomCommands,
16
- )
17
-
18
-
19
- class GatsbyProvider:
20
- def __init__(self, path: Path, custom_commands: CustomCommands):
21
- self.path = path
22
- self.custom_commands = custom_commands
23
-
24
- @classmethod
25
- def name(cls) -> str:
26
- return "gatsby"
27
-
28
- @classmethod
29
- def detect(cls, path: Path, custom_commands: CustomCommands) -> Optional[DetectResult]:
30
- pkg = path / "package.json"
31
- if not pkg.exists():
32
- return None
33
- if _exists(path, "gatsby-config.js", "gatsby-config.ts") or _has_dependency(
34
- pkg, "gatsby"
35
- ):
36
- return DetectResult(cls.name(), 90)
37
- return None
38
-
39
- def initialize(self) -> None:
40
- pass
41
-
42
- def serve_name(self) -> str:
43
- return self.path.name
44
-
45
- def provider_kind(self) -> str:
46
- return "staticsite"
47
-
48
- def declarations(self) -> Optional[str]:
49
- return None
50
-
51
- def dependencies(self) -> list[DependencySpec]:
52
- return [
53
- DependencySpec(
54
- "node",
55
- env_var="SHIPIT_NODE_VERSION",
56
- default_version="22",
57
- use_in_build=True,
58
- ),
59
- DependencySpec("npm", use_in_build=True),
60
- DependencySpec("static-web-server", env_var="SHIPIT_SWS_VERSION", use_in_serve=True),
61
- ]
62
-
63
- def build_steps(self) -> list[str]:
64
- return [
65
- "run(\"npm install\", inputs=[\"package.json\", \"package-lock.json\"], group=\"install\")",
66
- "copy(\".\", \".\", ignore=[\"node_modules\", \".git\"])",
67
- "run(\"npm run build\", outputs=[\"public\"], group=\"build\")",
68
- "run(\"cp -R public/* {}/\".format(app[\"build\"]))",
69
- ]
70
-
71
- def prepare_steps(self) -> Optional[list[str]]:
72
- return None
73
-
74
- def commands(self) -> Dict[str, str]:
75
- return {"start": '"static-web-server --root /app"'}
76
-
77
- def mounts(self) -> list[MountSpec]:
78
- return [MountSpec("app")]
79
-
80
- def volumes(self) -> list[VolumeSpec]:
81
- return []
82
-
83
- def env(self) -> Optional[Dict[str, str]]:
84
- return None
85
-
86
- def services(self) -> list[ServiceSpec]:
87
- return []
@@ -1,5 +0,0 @@
1
- __all__ = ["version", "version_info"]
2
-
3
-
4
- version = "0.10.1"
5
- version_info = (0, 10, 1, "final", 0)
File without changes
File without changes