shipit-cli 0.3.3__tar.gz → 0.4.0__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.
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/PKG-INFO +1 -1
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/pyproject.toml +1 -1
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/cli.py +45 -6
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/generator.py +24 -22
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/base.py +17 -14
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/gatsby.py +19 -15
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/hugo.py +12 -10
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/laravel.py +19 -15
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/mkdocs.py +20 -16
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/node_static.py +21 -17
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/php.py +26 -22
- shipit_cli-0.4.0/src/shipit/providers/python.py +309 -0
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/registry.py +9 -10
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/providers/staticfile.py +21 -16
- shipit_cli-0.4.0/src/shipit/version.py +5 -0
- shipit_cli-0.3.3/src/shipit/providers/python.py +0 -135
- shipit_cli-0.3.3/src/shipit/version.py +0 -5
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/.gitignore +0 -0
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/README.md +0 -0
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/__init__.py +0 -0
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/src/shipit/assets/php/php.ini +0 -0
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/tests/test_examples_build.py +0 -0
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/tests/test_generate_shipit_examples.py +0 -0
- {shipit_cli-0.3.3 → shipit_cli-0.4.0}/tests/test_version.py +0 -0
|
@@ -55,6 +55,7 @@ class Serve:
|
|
|
55
55
|
build: List["Step"]
|
|
56
56
|
deps: List["Package"]
|
|
57
57
|
commands: Dict[str, str]
|
|
58
|
+
cwd: Optional[str] = None
|
|
58
59
|
assets: Optional[Dict[str, str]] = None
|
|
59
60
|
prepare: Optional[List["PrepareStep"]] = None
|
|
60
61
|
workers: Optional[List[str]] = None
|
|
@@ -577,18 +578,33 @@ class LocalBuilder:
|
|
|
577
578
|
self.create_file(asset_path, assets[asset])
|
|
578
579
|
|
|
579
580
|
def build_prepare(self, serve: Serve) -> None:
|
|
580
|
-
app_dir = self.get_build_path()
|
|
581
581
|
self.prepare_bash_script.parent.mkdir(parents=True, exist_ok=True)
|
|
582
582
|
commands: List[str] = []
|
|
583
|
+
if serve.cwd:
|
|
584
|
+
commands.append(f"cd {serve.cwd}")
|
|
583
585
|
if serve.prepare:
|
|
584
586
|
for step in serve.prepare:
|
|
585
587
|
if isinstance(step, RunStep):
|
|
586
588
|
commands.append(step.command)
|
|
587
589
|
elif isinstance(step, WorkdirStep):
|
|
588
590
|
commands.append(f"cd {step.path}")
|
|
589
|
-
content = "#!/bin/bash\
|
|
590
|
-
|
|
591
|
+
content = "#!/bin/bash\n{body}".format(
|
|
592
|
+
body="\n".join(commands)
|
|
591
593
|
)
|
|
594
|
+
console.print(f"\n[bold]Created prepare.sh script to run before packaging ✅[/bold]")
|
|
595
|
+
manifest_panel = Panel(
|
|
596
|
+
Syntax(
|
|
597
|
+
content,
|
|
598
|
+
"bash",
|
|
599
|
+
theme="monokai",
|
|
600
|
+
background_color="default",
|
|
601
|
+
line_numbers=True,
|
|
602
|
+
),
|
|
603
|
+
box=box.SQUARE,
|
|
604
|
+
border_style="bright_black",
|
|
605
|
+
expand=False,
|
|
606
|
+
)
|
|
607
|
+
console.print(manifest_panel, markup=False, highlight=True)
|
|
592
608
|
self.prepare_bash_script.write_text(content)
|
|
593
609
|
self.prepare_bash_script.chmod(0o755)
|
|
594
610
|
|
|
@@ -739,15 +755,35 @@ class WasmerBuilder:
|
|
|
739
755
|
env_lines = ""
|
|
740
756
|
|
|
741
757
|
commands: List[str] = []
|
|
758
|
+
if serve.cwd:
|
|
759
|
+
commands.append(f"cd {serve.cwd}")
|
|
760
|
+
|
|
742
761
|
if serve.prepare:
|
|
743
762
|
for step in serve.prepare:
|
|
744
763
|
if isinstance(step, RunStep):
|
|
745
764
|
commands.append(step.command)
|
|
746
765
|
elif isinstance(step, WorkdirStep):
|
|
747
766
|
commands.append(f"cd {step.path}")
|
|
767
|
+
|
|
748
768
|
body = "\n".join(filter(None, [env_lines, *commands]))
|
|
769
|
+
content = f"#!/bin/bash\n\n{body}"
|
|
770
|
+
console.print(f"\n[bold]Created prepare.sh script to run before packaging ✅[/bold]")
|
|
771
|
+
manifest_panel = Panel(
|
|
772
|
+
Syntax(
|
|
773
|
+
content,
|
|
774
|
+
"bash",
|
|
775
|
+
theme="monokai",
|
|
776
|
+
background_color="default",
|
|
777
|
+
line_numbers=True,
|
|
778
|
+
),
|
|
779
|
+
box=box.SQUARE,
|
|
780
|
+
border_style="bright_black",
|
|
781
|
+
expand=False,
|
|
782
|
+
)
|
|
783
|
+
console.print(manifest_panel, markup=False, highlight=True)
|
|
784
|
+
|
|
749
785
|
(prepare_dir / "prepare.sh").write_text(
|
|
750
|
-
|
|
786
|
+
content,
|
|
751
787
|
)
|
|
752
788
|
(prepare_dir / "prepare.sh").chmod(0o755)
|
|
753
789
|
|
|
@@ -842,7 +878,8 @@ class WasmerBuilder:
|
|
|
842
878
|
command.add("module", program_binary["script"])
|
|
843
879
|
command.add("runner", "wasi")
|
|
844
880
|
wasi_args = table()
|
|
845
|
-
|
|
881
|
+
if serve.cwd:
|
|
882
|
+
wasi_args.add("cwd", serve.cwd)
|
|
846
883
|
wasi_args.add("main-args", parts[1:])
|
|
847
884
|
env = program_binary.get("env") or {}
|
|
848
885
|
if serve.env:
|
|
@@ -1024,6 +1061,7 @@ class Ctx:
|
|
|
1024
1061
|
build: List[str],
|
|
1025
1062
|
deps: List[str],
|
|
1026
1063
|
commands: Dict[str, str],
|
|
1064
|
+
cwd: Optional[str] = None,
|
|
1027
1065
|
assets: Optional[Dict[str, str]] = None,
|
|
1028
1066
|
prepare: Optional[List[str]] = None,
|
|
1029
1067
|
workers: Optional[List[str]] = None,
|
|
@@ -1043,6 +1081,7 @@ class Ctx:
|
|
|
1043
1081
|
name=name,
|
|
1044
1082
|
provider=provider,
|
|
1045
1083
|
build=build_refs,
|
|
1084
|
+
cwd=cwd,
|
|
1046
1085
|
assets=assets,
|
|
1047
1086
|
deps=dep_refs,
|
|
1048
1087
|
commands=commands,
|
|
@@ -1433,7 +1472,7 @@ def main() -> None:
|
|
|
1433
1472
|
app()
|
|
1434
1473
|
except Exception as e:
|
|
1435
1474
|
console.print(f"[bold red]{type(e).__name__}[/bold red]: {e}")
|
|
1436
|
-
raise e
|
|
1475
|
+
# raise e
|
|
1437
1476
|
|
|
1438
1477
|
|
|
1439
1478
|
if __name__ == "__main__":
|
|
@@ -7,22 +7,19 @@ from shipit.providers.base import DependencySpec, Provider, ProviderPlan, Detect
|
|
|
7
7
|
from shipit.providers.registry import providers as registry_providers
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def _providers() -> list[Provider]:
|
|
11
|
-
# Load
|
|
10
|
+
def _providers() -> list[type[Provider]]:
|
|
11
|
+
# Load provider classes from modular registry
|
|
12
12
|
return registry_providers()
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def detect_provider(path: Path) -> Provider:
|
|
16
|
-
matches: list[tuple[Provider, DetectResult]] = []
|
|
17
|
-
for
|
|
18
|
-
res =
|
|
16
|
+
matches: list[tuple[type[Provider], DetectResult]] = []
|
|
17
|
+
for provider_cls in _providers():
|
|
18
|
+
res = provider_cls.detect(path)
|
|
19
19
|
if res:
|
|
20
|
-
matches.append((
|
|
20
|
+
matches.append((provider_cls, res))
|
|
21
21
|
if not matches:
|
|
22
|
-
|
|
23
|
-
from shipit.providers.staticfile import StaticFileProvider
|
|
24
|
-
|
|
25
|
-
return StaticFileProvider()
|
|
22
|
+
raise Exception("Shipit could not detect a provider for this project")
|
|
26
23
|
# Highest score wins; tie-breaker by order
|
|
27
24
|
matches.sort(key=lambda x: x[1].score, reverse=True)
|
|
28
25
|
return matches[0][0]
|
|
@@ -80,20 +77,20 @@ def _render_assets(assets: Optional[Dict[str, str]]) -> Optional[str]:
|
|
|
80
77
|
|
|
81
78
|
|
|
82
79
|
def generate_shipit(path: Path) -> str:
|
|
83
|
-
|
|
84
|
-
provider
|
|
80
|
+
provider_cls = detect_provider(path)
|
|
81
|
+
provider = provider_cls(path)
|
|
85
82
|
|
|
86
83
|
# Collect parts
|
|
87
84
|
plan = ProviderPlan(
|
|
88
|
-
serve_name=provider.serve_name(
|
|
89
|
-
provider=provider.provider_kind(
|
|
90
|
-
mounts=provider.mounts(
|
|
91
|
-
declarations=provider.declarations(
|
|
92
|
-
dependencies=provider.dependencies(
|
|
93
|
-
build_steps=provider.build_steps(
|
|
94
|
-
prepare=provider.prepare_steps(
|
|
95
|
-
commands=provider.commands(
|
|
96
|
-
env=provider.env(
|
|
85
|
+
serve_name=provider.serve_name(),
|
|
86
|
+
provider=provider.provider_kind(),
|
|
87
|
+
mounts=provider.mounts(),
|
|
88
|
+
declarations=provider.declarations(),
|
|
89
|
+
dependencies=provider.dependencies(),
|
|
90
|
+
build_steps=provider.build_steps(),
|
|
91
|
+
prepare=provider.prepare_steps(),
|
|
92
|
+
commands=provider.commands(),
|
|
93
|
+
env=provider.env(),
|
|
97
94
|
)
|
|
98
95
|
|
|
99
96
|
# Declare dependency variables (combined) and collect serve deps
|
|
@@ -118,8 +115,10 @@ def generate_shipit(path: Path) -> str:
|
|
|
118
115
|
env_lines = ",\n".join([f' "{k}": {v}' for k, v in plan.env.items()])
|
|
119
116
|
assets_block = _render_assets(plan.assets)
|
|
120
117
|
mounts_block = None
|
|
118
|
+
attach_serve_names: list[str] = []
|
|
121
119
|
if plan.mounts:
|
|
122
|
-
mounts = filter(lambda m: m.attach_to_serve, plan.mounts)
|
|
120
|
+
mounts = list(filter(lambda m: m.attach_to_serve, plan.mounts))
|
|
121
|
+
attach_serve_names = [m.name for m in mounts]
|
|
123
122
|
mounts_block = ",\n".join([f" {m.name}" for m in mounts])
|
|
124
123
|
|
|
125
124
|
out: List[str] = []
|
|
@@ -134,6 +133,9 @@ def generate_shipit(path: Path) -> str:
|
|
|
134
133
|
out.append("serve(")
|
|
135
134
|
out.append(f' name="{plan.serve_name}",')
|
|
136
135
|
out.append(f' provider="{plan.provider}",')
|
|
136
|
+
# If app is mounted for serve, set cwd to the app serve path
|
|
137
|
+
if "app" in attach_serve_names:
|
|
138
|
+
out.append(' cwd=app["serve"],')
|
|
137
139
|
out.append(" build=[")
|
|
138
140
|
out.append(build_steps_block)
|
|
139
141
|
out.append(" ],")
|
|
@@ -12,21 +12,24 @@ class DetectResult:
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Provider(Protocol):
|
|
15
|
-
def
|
|
16
|
-
|
|
17
|
-
def
|
|
18
|
-
|
|
19
|
-
def
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
def
|
|
23
|
-
def
|
|
15
|
+
def __init__(self, path: Path): ...
|
|
16
|
+
@classmethod
|
|
17
|
+
def name(cls) -> str: ...
|
|
18
|
+
@classmethod
|
|
19
|
+
def detect(cls, path: Path) -> Optional[DetectResult]: ...
|
|
20
|
+
def initialize(self) -> None: ...
|
|
21
|
+
# Structured plan steps (no path args; use self.path)
|
|
22
|
+
def serve_name(self) -> str: ...
|
|
23
|
+
def provider_kind(self) -> str: ...
|
|
24
|
+
def dependencies(self) -> list["DependencySpec"]: ...
|
|
25
|
+
def declarations(self) -> Optional[str]: ...
|
|
26
|
+
def build_steps(self) -> list[str]: ...
|
|
24
27
|
# Prepare: list of Starlark step calls (currently only run(...))
|
|
25
|
-
def prepare_steps(self
|
|
26
|
-
def commands(self
|
|
27
|
-
def assets(self
|
|
28
|
-
def mounts(self
|
|
29
|
-
def env(self
|
|
28
|
+
def prepare_steps(self) -> Optional[List[str]]: ...
|
|
29
|
+
def commands(self) -> Dict[str, str]: ...
|
|
30
|
+
def assets(self) -> Optional[Dict[str, str]]: ...
|
|
31
|
+
def mounts(self) -> List["MountSpec"]: ...
|
|
32
|
+
def env(self) -> Optional[Dict[str, str]]: ...
|
|
30
33
|
|
|
31
34
|
|
|
32
35
|
@dataclass
|
|
@@ -7,32 +7,36 @@ from .base import DetectResult, DependencySpec, Provider, _exists, _has_dependen
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class GatsbyProvider:
|
|
10
|
-
def
|
|
10
|
+
def __init__(self, path: Path):
|
|
11
|
+
self.path = path
|
|
12
|
+
@classmethod
|
|
13
|
+
def name(cls) -> str:
|
|
11
14
|
return "gatsby"
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
@classmethod
|
|
17
|
+
def detect(cls, path: Path) -> Optional[DetectResult]:
|
|
14
18
|
pkg = path / "package.json"
|
|
15
19
|
if not pkg.exists():
|
|
16
20
|
return None
|
|
17
21
|
if _exists(path, "gatsby-config.js", "gatsby-config.ts") or _has_dependency(
|
|
18
22
|
pkg, "gatsby"
|
|
19
23
|
):
|
|
20
|
-
return DetectResult(
|
|
24
|
+
return DetectResult(cls.name(), 90)
|
|
21
25
|
return None
|
|
22
26
|
|
|
23
|
-
def initialize(self
|
|
27
|
+
def initialize(self) -> None:
|
|
24
28
|
pass
|
|
25
29
|
|
|
26
|
-
def serve_name(self
|
|
27
|
-
return path.name
|
|
30
|
+
def serve_name(self) -> str:
|
|
31
|
+
return self.path.name
|
|
28
32
|
|
|
29
|
-
def provider_kind(self
|
|
33
|
+
def provider_kind(self) -> str:
|
|
30
34
|
return "staticsite"
|
|
31
35
|
|
|
32
|
-
def declarations(self
|
|
36
|
+
def declarations(self) -> Optional[str]:
|
|
33
37
|
return None
|
|
34
38
|
|
|
35
|
-
def dependencies(self
|
|
39
|
+
def dependencies(self) -> list[DependencySpec]:
|
|
36
40
|
return [
|
|
37
41
|
DependencySpec(
|
|
38
42
|
"node",
|
|
@@ -44,7 +48,7 @@ class GatsbyProvider:
|
|
|
44
48
|
DependencySpec("static-web-server", env_var="SHIPIT_SWS_VERSION", use_in_serve=True),
|
|
45
49
|
]
|
|
46
50
|
|
|
47
|
-
def build_steps(self
|
|
51
|
+
def build_steps(self) -> list[str]:
|
|
48
52
|
return [
|
|
49
53
|
"run(\"npm install\", inputs=[\"package.json\", \"package-lock.json\"], group=\"install\")",
|
|
50
54
|
"copy(\".\", \".\", ignore=[\"node_modules\", \".git\"])",
|
|
@@ -52,17 +56,17 @@ class GatsbyProvider:
|
|
|
52
56
|
"run(\"cp -R public/* {}/\".format(app[\"build\"]))",
|
|
53
57
|
]
|
|
54
58
|
|
|
55
|
-
def prepare_steps(self
|
|
59
|
+
def prepare_steps(self) -> Optional[list[str]]:
|
|
56
60
|
return None
|
|
57
61
|
|
|
58
|
-
def commands(self
|
|
62
|
+
def commands(self) -> Dict[str, str]:
|
|
59
63
|
return {"start": '"static-web-server --root /app"'}
|
|
60
64
|
|
|
61
|
-
def assets(self
|
|
65
|
+
def assets(self) -> Optional[Dict[str, str]]:
|
|
62
66
|
return None
|
|
63
67
|
|
|
64
|
-
def mounts(self
|
|
68
|
+
def mounts(self) -> list[MountSpec]:
|
|
65
69
|
return [MountSpec("app")]
|
|
66
70
|
|
|
67
|
-
def env(self
|
|
71
|
+
def env(self) -> Optional[Dict[str, str]]:
|
|
68
72
|
return None
|
|
@@ -7,27 +7,29 @@ from .base import DetectResult, DependencySpec, Provider, _exists
|
|
|
7
7
|
from .staticfile import StaticFileProvider
|
|
8
8
|
|
|
9
9
|
class HugoProvider(StaticFileProvider):
|
|
10
|
-
|
|
10
|
+
@classmethod
|
|
11
|
+
def name(cls) -> str:
|
|
11
12
|
return "hugo"
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
@classmethod
|
|
15
|
+
def detect(cls, path: Path) -> Optional[DetectResult]:
|
|
14
16
|
if _exists(path, "hugo.toml", "hugo.json", "hugo.yaml", "hugo.yml"):
|
|
15
|
-
return DetectResult(
|
|
17
|
+
return DetectResult(cls.name(), 80)
|
|
16
18
|
if (
|
|
17
19
|
_exists(path, "config.toml", "config.json", "config.yaml", "config.yml")
|
|
18
20
|
and _exists(path, "content")
|
|
19
21
|
and (_exists(path, "static") or _exists(path, "themes"))
|
|
20
22
|
):
|
|
21
|
-
return DetectResult(
|
|
23
|
+
return DetectResult(cls.name(), 40)
|
|
22
24
|
return None
|
|
23
25
|
|
|
24
|
-
def serve_name(self
|
|
25
|
-
return path.name
|
|
26
|
+
def serve_name(self) -> str:
|
|
27
|
+
return self.path.name
|
|
26
28
|
|
|
27
|
-
def provider_kind(self
|
|
29
|
+
def provider_kind(self) -> str:
|
|
28
30
|
return "staticsite"
|
|
29
31
|
|
|
30
|
-
def dependencies(self
|
|
32
|
+
def dependencies(self) -> list[DependencySpec]:
|
|
31
33
|
return [
|
|
32
34
|
DependencySpec(
|
|
33
35
|
"hugo",
|
|
@@ -35,10 +37,10 @@ class HugoProvider(StaticFileProvider):
|
|
|
35
37
|
default_version="0.149.0",
|
|
36
38
|
use_in_build=True,
|
|
37
39
|
),
|
|
38
|
-
*super().dependencies(
|
|
40
|
+
*super().dependencies(),
|
|
39
41
|
]
|
|
40
42
|
|
|
41
|
-
def build_steps(self
|
|
43
|
+
def build_steps(self) -> list[str]:
|
|
42
44
|
return [
|
|
43
45
|
'copy(".", ".", ignore=[".git"])',
|
|
44
46
|
'run("hugo build --destination={}".format(app["build"]), group="build")',
|
|
@@ -7,24 +7,28 @@ from .base import DetectResult, DependencySpec, Provider, _exists, MountSpec
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class LaravelProvider:
|
|
10
|
-
def
|
|
10
|
+
def __init__(self, path: Path):
|
|
11
|
+
self.path = path
|
|
12
|
+
@classmethod
|
|
13
|
+
def name(cls) -> str:
|
|
11
14
|
return "laravel"
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
@classmethod
|
|
17
|
+
def detect(cls, path: Path) -> Optional[DetectResult]:
|
|
14
18
|
if _exists(path, "artisan") and _exists(path, "composer.json"):
|
|
15
|
-
return DetectResult(
|
|
19
|
+
return DetectResult(cls.name(), 95)
|
|
16
20
|
return None
|
|
17
21
|
|
|
18
|
-
def initialize(self
|
|
22
|
+
def initialize(self) -> None:
|
|
19
23
|
pass
|
|
20
24
|
|
|
21
|
-
def serve_name(self
|
|
22
|
-
return path.name
|
|
25
|
+
def serve_name(self) -> str:
|
|
26
|
+
return self.path.name
|
|
23
27
|
|
|
24
|
-
def provider_kind(self
|
|
28
|
+
def provider_kind(self) -> str:
|
|
25
29
|
return "php"
|
|
26
30
|
|
|
27
|
-
def dependencies(self
|
|
31
|
+
def dependencies(self) -> list[DependencySpec]:
|
|
28
32
|
return [
|
|
29
33
|
DependencySpec(
|
|
30
34
|
"php",
|
|
@@ -39,10 +43,10 @@ class LaravelProvider:
|
|
|
39
43
|
DependencySpec("bash", use_in_serve=True),
|
|
40
44
|
]
|
|
41
45
|
|
|
42
|
-
def declarations(self
|
|
46
|
+
def declarations(self) -> Optional[str]:
|
|
43
47
|
return "HOME = getenv(\"HOME\")"
|
|
44
48
|
|
|
45
|
-
def build_steps(self
|
|
49
|
+
def build_steps(self) -> list[str]:
|
|
46
50
|
return [
|
|
47
51
|
"env(HOME=HOME, COMPOSER_FUND=\"0\")",
|
|
48
52
|
"workdir(app[\"build\"])",
|
|
@@ -53,7 +57,7 @@ class LaravelProvider:
|
|
|
53
57
|
"run(\"pnpm run build\", outputs=[\".\"], group=\"build\")",
|
|
54
58
|
]
|
|
55
59
|
|
|
56
|
-
def prepare_steps(self
|
|
60
|
+
def prepare_steps(self) -> Optional[list[str]]:
|
|
57
61
|
return [
|
|
58
62
|
'workdir(app["serve"])',
|
|
59
63
|
'run("mkdir -p storage/framework/{sessions,views,cache,testing} storage/logs bootstrap/cache")',
|
|
@@ -63,17 +67,17 @@ class LaravelProvider:
|
|
|
63
67
|
'run("php artisan view:cache")',
|
|
64
68
|
]
|
|
65
69
|
|
|
66
|
-
def commands(self
|
|
70
|
+
def commands(self) -> Dict[str, str]:
|
|
67
71
|
return {
|
|
68
72
|
"start": '"php -S localhost:8080 -t public"',
|
|
69
73
|
"after_deploy": '"php artisan migrate"',
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
def assets(self
|
|
76
|
+
def assets(self) -> Optional[Dict[str, str]]:
|
|
73
77
|
return None
|
|
74
78
|
|
|
75
|
-
def mounts(self
|
|
79
|
+
def mounts(self) -> list[MountSpec]:
|
|
76
80
|
return [MountSpec("app")]
|
|
77
81
|
|
|
78
|
-
def env(self
|
|
82
|
+
def env(self) -> Optional[Dict[str, str]]:
|
|
79
83
|
return None
|
|
@@ -7,24 +7,28 @@ from .base import DetectResult, DependencySpec, Provider, _exists, MountSpec
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class MkdocsProvider:
|
|
10
|
-
def
|
|
10
|
+
def __init__(self, path: Path):
|
|
11
|
+
self.path = path
|
|
12
|
+
@classmethod
|
|
13
|
+
def name(cls) -> str:
|
|
11
14
|
return "mkdocs"
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
@classmethod
|
|
17
|
+
def detect(cls, path: Path) -> Optional[DetectResult]:
|
|
14
18
|
if _exists(path, "mkdocs.yml", "mkdocs.yaml"):
|
|
15
|
-
return DetectResult(
|
|
19
|
+
return DetectResult(cls.name(), 85)
|
|
16
20
|
return None
|
|
17
21
|
|
|
18
|
-
def initialize(self
|
|
22
|
+
def initialize(self) -> None:
|
|
19
23
|
pass
|
|
20
24
|
|
|
21
|
-
def serve_name(self
|
|
22
|
-
return path.name
|
|
25
|
+
def serve_name(self) -> str:
|
|
26
|
+
return self.path.name
|
|
23
27
|
|
|
24
|
-
def provider_kind(self
|
|
28
|
+
def provider_kind(self) -> str:
|
|
25
29
|
return "mkdocs-site"
|
|
26
30
|
|
|
27
|
-
def dependencies(self
|
|
31
|
+
def dependencies(self) -> list[DependencySpec]:
|
|
28
32
|
return [
|
|
29
33
|
DependencySpec(
|
|
30
34
|
"python",
|
|
@@ -46,11 +50,11 @@ class MkdocsProvider:
|
|
|
46
50
|
),
|
|
47
51
|
]
|
|
48
52
|
|
|
49
|
-
def declarations(self
|
|
53
|
+
def declarations(self) -> Optional[str]:
|
|
50
54
|
return None
|
|
51
55
|
|
|
52
|
-
def build_steps(self
|
|
53
|
-
has_requirements = _exists(path, "requirements.txt")
|
|
56
|
+
def build_steps(self) -> list[str]:
|
|
57
|
+
has_requirements = _exists(self.path, "requirements.txt")
|
|
54
58
|
if has_requirements:
|
|
55
59
|
install_lines = [
|
|
56
60
|
"run(\"uv init --no-managed-python\", inputs=[], outputs=[\".\"], group=\"install\")",
|
|
@@ -68,17 +72,17 @@ class MkdocsProvider:
|
|
|
68
72
|
"run(\"uv run mkdocs build --site-dir={}\".format(app[\"build\"]), outputs=[\".\"], group=\"build\")",
|
|
69
73
|
]
|
|
70
74
|
|
|
71
|
-
def prepare_steps(self
|
|
75
|
+
def prepare_steps(self) -> Optional[list[str]]:
|
|
72
76
|
return None
|
|
73
77
|
|
|
74
|
-
def commands(self
|
|
78
|
+
def commands(self) -> Dict[str, str]:
|
|
75
79
|
return {"start": '"static-web-server --root /app"'}
|
|
76
80
|
|
|
77
|
-
def assets(self
|
|
81
|
+
def assets(self) -> Optional[Dict[str, str]]:
|
|
78
82
|
return None
|
|
79
83
|
|
|
80
|
-
def mounts(self
|
|
84
|
+
def mounts(self) -> list[MountSpec]:
|
|
81
85
|
return [MountSpec("app")]
|
|
82
86
|
|
|
83
|
-
def env(self
|
|
87
|
+
def env(self) -> Optional[Dict[str, str]]:
|
|
84
88
|
return None
|
|
@@ -7,28 +7,32 @@ from .base import DetectResult, DependencySpec, Provider, _exists, _has_dependen
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class NodeStaticProvider:
|
|
10
|
-
def
|
|
10
|
+
def __init__(self, path: Path):
|
|
11
|
+
self.path = path
|
|
12
|
+
@classmethod
|
|
13
|
+
def name(cls) -> str:
|
|
11
14
|
return "node-static"
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
@classmethod
|
|
17
|
+
def detect(cls, path: Path) -> Optional[DetectResult]:
|
|
14
18
|
pkg = path / "package.json"
|
|
15
19
|
if not pkg.exists():
|
|
16
20
|
return None
|
|
17
21
|
static_generators = ["astro", "vite", "next", "nuxt"]
|
|
18
22
|
if any(_has_dependency(pkg, dep) for dep in static_generators):
|
|
19
|
-
return DetectResult(
|
|
23
|
+
return DetectResult(cls.name(), 40)
|
|
20
24
|
return None
|
|
21
25
|
|
|
22
|
-
def initialize(self
|
|
26
|
+
def initialize(self) -> None:
|
|
23
27
|
pass
|
|
24
28
|
|
|
25
|
-
def serve_name(self
|
|
26
|
-
return path.name
|
|
29
|
+
def serve_name(self) -> str:
|
|
30
|
+
return self.path.name
|
|
27
31
|
|
|
28
|
-
def provider_kind(self
|
|
32
|
+
def provider_kind(self) -> str:
|
|
29
33
|
return "staticsite"
|
|
30
34
|
|
|
31
|
-
def dependencies(self
|
|
35
|
+
def dependencies(self) -> list[DependencySpec]:
|
|
32
36
|
return [
|
|
33
37
|
DependencySpec(
|
|
34
38
|
"node",
|
|
@@ -40,11 +44,11 @@ class NodeStaticProvider:
|
|
|
40
44
|
DependencySpec("static-web-server", use_in_serve=True),
|
|
41
45
|
]
|
|
42
46
|
|
|
43
|
-
def declarations(self
|
|
47
|
+
def declarations(self) -> Optional[str]:
|
|
44
48
|
return None
|
|
45
49
|
|
|
46
|
-
def build_steps(self
|
|
47
|
-
output_dir = "dist" if (path / "dist").exists() else "public"
|
|
50
|
+
def build_steps(self) -> list[str]:
|
|
51
|
+
output_dir = "dist" if (self.path / "dist").exists() else "public"
|
|
48
52
|
return [
|
|
49
53
|
"run(\"npm install\", inputs=[\"package.json\", \"package-lock.json\"], group=\"install\")",
|
|
50
54
|
"copy(\".\", \".\", ignore=[\"node_modules\", \".git\"])",
|
|
@@ -52,18 +56,18 @@ class NodeStaticProvider:
|
|
|
52
56
|
f"run(\"cp -R {output_dir}/* {{}}/\".format(app[\"build\"]))",
|
|
53
57
|
]
|
|
54
58
|
|
|
55
|
-
def prepare_steps(self
|
|
59
|
+
def prepare_steps(self) -> Optional[list[str]]:
|
|
56
60
|
return None
|
|
57
61
|
|
|
58
|
-
def commands(self
|
|
59
|
-
output_dir = "dist" if (path / "dist").exists() else "public"
|
|
62
|
+
def commands(self) -> Dict[str, str]:
|
|
63
|
+
output_dir = "dist" if (self.path / "dist").exists() else "public"
|
|
60
64
|
return {"start": f'"static-web-server --root /app/{output_dir}"'}
|
|
61
65
|
|
|
62
|
-
def assets(self
|
|
66
|
+
def assets(self) -> Optional[Dict[str, str]]:
|
|
63
67
|
return None
|
|
64
68
|
|
|
65
|
-
def mounts(self
|
|
69
|
+
def mounts(self) -> list[MountSpec]:
|
|
66
70
|
return [MountSpec("app")]
|
|
67
71
|
|
|
68
|
-
def env(self
|
|
72
|
+
def env(self) -> Optional[Dict[str, str]]:
|
|
69
73
|
return None
|
|
@@ -7,29 +7,33 @@ from .base import DetectResult, DependencySpec, Provider, _exists, MountSpec
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class PhpProvider:
|
|
10
|
-
def
|
|
10
|
+
def __init__(self, path: Path):
|
|
11
|
+
self.path = path
|
|
12
|
+
@classmethod
|
|
13
|
+
def name(cls) -> str:
|
|
11
14
|
return "php"
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
@classmethod
|
|
17
|
+
def detect(cls, path: Path) -> Optional[DetectResult]:
|
|
14
18
|
if _exists(path, "composer.json") and _exists(path, "public/index.php"):
|
|
15
|
-
return DetectResult(
|
|
19
|
+
return DetectResult(cls.name(), 60)
|
|
16
20
|
if _exists(path, "index.php") and not _exists(path, "composer.json"):
|
|
17
|
-
return DetectResult(
|
|
21
|
+
return DetectResult(cls.name(), 10)
|
|
18
22
|
return None
|
|
19
23
|
|
|
20
|
-
def initialize(self
|
|
24
|
+
def initialize(self) -> None:
|
|
21
25
|
pass
|
|
22
26
|
|
|
23
|
-
def serve_name(self
|
|
24
|
-
return path.name
|
|
27
|
+
def serve_name(self) -> str:
|
|
28
|
+
return self.path.name
|
|
25
29
|
|
|
26
|
-
def provider_kind(self
|
|
30
|
+
def provider_kind(self) -> str:
|
|
27
31
|
return "php"
|
|
28
32
|
|
|
29
|
-
def has_composer(self
|
|
30
|
-
return _exists(path, "composer.json", "composer.lock")
|
|
33
|
+
def has_composer(self) -> bool:
|
|
34
|
+
return _exists(self.path, "composer.json", "composer.lock")
|
|
31
35
|
|
|
32
|
-
def dependencies(self
|
|
36
|
+
def dependencies(self) -> list[DependencySpec]:
|
|
33
37
|
deps = [
|
|
34
38
|
DependencySpec(
|
|
35
39
|
"php",
|
|
@@ -39,40 +43,40 @@ class PhpProvider:
|
|
|
39
43
|
use_in_serve=True,
|
|
40
44
|
),
|
|
41
45
|
]
|
|
42
|
-
if self.has_composer(
|
|
46
|
+
if self.has_composer():
|
|
43
47
|
deps.append(DependencySpec("composer", use_in_build=True))
|
|
44
48
|
deps.append(DependencySpec("bash", use_in_serve=True))
|
|
45
49
|
return deps
|
|
46
50
|
|
|
47
|
-
def declarations(self
|
|
51
|
+
def declarations(self) -> Optional[str]:
|
|
48
52
|
return "HOME = getenv(\"HOME\")"
|
|
49
53
|
|
|
50
|
-
def build_steps(self
|
|
54
|
+
def build_steps(self) -> list[str]:
|
|
51
55
|
steps = [
|
|
52
56
|
"workdir(app[\"build\"])",
|
|
53
57
|
]
|
|
54
58
|
|
|
55
|
-
if self.has_composer(
|
|
59
|
+
if self.has_composer():
|
|
56
60
|
steps.append("env(HOME=HOME, COMPOSER_FUND=\"0\")")
|
|
57
61
|
steps.append("run(\"composer install --optimize-autoloader --no-scripts --no-interaction\", inputs=[\"composer.json\", \"composer.lock\"], outputs=[\".\"], group=\"install\")")
|
|
58
62
|
|
|
59
63
|
steps.append("copy(\".\", \".\", ignore=[\".git\"])")
|
|
60
64
|
return steps
|
|
61
65
|
|
|
62
|
-
def prepare_steps(self
|
|
66
|
+
def prepare_steps(self) -> Optional[list[str]]:
|
|
63
67
|
return None
|
|
64
68
|
|
|
65
|
-
def commands(self
|
|
66
|
-
if _exists(path, "public/index.php"):
|
|
69
|
+
def commands(self) -> Dict[str, str]:
|
|
70
|
+
if _exists(self.path, "public/index.php"):
|
|
67
71
|
return {"start": '"php -S localhost:8080 -t public"'}
|
|
68
|
-
elif _exists(path, "index.php"):
|
|
72
|
+
elif _exists(self.path, "index.php"):
|
|
69
73
|
return {"start": '"php -S localhost:8080" -t .'}
|
|
70
74
|
|
|
71
|
-
def assets(self
|
|
75
|
+
def assets(self) -> Optional[Dict[str, str]]:
|
|
72
76
|
return {"php.ini": "get_asset(\"php/php.ini\")"}
|
|
73
77
|
|
|
74
|
-
def mounts(self
|
|
78
|
+
def mounts(self) -> list[MountSpec]:
|
|
75
79
|
return [MountSpec("app")]
|
|
76
80
|
|
|
77
|
-
def env(self
|
|
81
|
+
def env(self) -> Optional[Dict[str, str]]:
|
|
78
82
|
return None
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, Optional, Set
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
from .base import (
|
|
9
|
+
DetectResult,
|
|
10
|
+
DependencySpec,
|
|
11
|
+
Provider,
|
|
12
|
+
_exists,
|
|
13
|
+
MountSpec,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PythonFramework(Enum):
|
|
18
|
+
Django = "django"
|
|
19
|
+
FastAPI = "fastapi"
|
|
20
|
+
Flask = "flask"
|
|
21
|
+
FastHTML = "python-fasthtml"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PythonServer(Enum):
|
|
25
|
+
Hypercorn = "hypercorn"
|
|
26
|
+
Uvicorn = "uvicorn"
|
|
27
|
+
# Gunicorn = "gunicorn"
|
|
28
|
+
Daphne = "daphne"
|
|
29
|
+
|
|
30
|
+
class DatabaseType(Enum):
|
|
31
|
+
MySQL = "mysql"
|
|
32
|
+
PostgreSQL = "postgresql"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PythonProvider:
|
|
36
|
+
framework: Optional[PythonFramework] = None
|
|
37
|
+
server: Optional[PythonServer] = None
|
|
38
|
+
database: Optional[DatabaseType] = None
|
|
39
|
+
extra_dependencies: Set[str]
|
|
40
|
+
asgi_application: Optional[str] = None
|
|
41
|
+
wsgi_application: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
def __init__(self, path: Path):
|
|
44
|
+
self.path = path
|
|
45
|
+
if _exists(self.path, ".python-version"):
|
|
46
|
+
python_version = (self.path / ".python-version").read_text().strip()
|
|
47
|
+
else:
|
|
48
|
+
python_version = "3.13"
|
|
49
|
+
self.default_python_version = python_version
|
|
50
|
+
self.extra_dependencies = set()
|
|
51
|
+
|
|
52
|
+
pg_deps = {
|
|
53
|
+
"asyncpg",
|
|
54
|
+
"aiopg",
|
|
55
|
+
"psycopg",
|
|
56
|
+
"psycopg2",
|
|
57
|
+
"psycopg-binary",
|
|
58
|
+
"psycopg2-binary"}
|
|
59
|
+
mysql_deps = {"mysqlclient", "pymysql", "mysql-connector-python", "aiomysql"}
|
|
60
|
+
found_deps = self.check_deps(
|
|
61
|
+
"django",
|
|
62
|
+
"fastapi",
|
|
63
|
+
"flask",
|
|
64
|
+
"python-fasthtml",
|
|
65
|
+
"daphne",
|
|
66
|
+
"hypercorn",
|
|
67
|
+
"uvicorn",
|
|
68
|
+
# "gunicorn",
|
|
69
|
+
*mysql_deps,
|
|
70
|
+
*pg_deps,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# ASGI/WSGI Server
|
|
74
|
+
if "uvicorn" in found_deps:
|
|
75
|
+
server = PythonServer.Uvicorn
|
|
76
|
+
elif "hypercorn" in found_deps:
|
|
77
|
+
server = PythonServer.Hypercorn
|
|
78
|
+
# elif "gunicorn" in found_deps:
|
|
79
|
+
# server = PythonServer.Gunicorn
|
|
80
|
+
elif "daphne" in found_deps:
|
|
81
|
+
server = PythonServer.Daphne
|
|
82
|
+
else:
|
|
83
|
+
server = None
|
|
84
|
+
self.server = server
|
|
85
|
+
|
|
86
|
+
# Set framework
|
|
87
|
+
if _exists(self.path, "manage.py") and ("django" in found_deps):
|
|
88
|
+
framework = PythonFramework.Django
|
|
89
|
+
# Find the settings.py file using glob
|
|
90
|
+
settings_file = next(self.path.glob( "**/settings.py"))
|
|
91
|
+
if settings_file:
|
|
92
|
+
asgi_match = re.search(r"ASGI_APPLICATION\s*=\s*['\"](.*)['\"]", settings_file.read_text())
|
|
93
|
+
if asgi_match:
|
|
94
|
+
self.asgi_application = asgi_match.group(1)
|
|
95
|
+
else:
|
|
96
|
+
wsgi_match = re.search(r"WSGI_APPLICATION\s*=\s*['\"](.*)['\"]", settings_file.read_text())
|
|
97
|
+
if wsgi_match:
|
|
98
|
+
self.wsgi_application = wsgi_match.group(1)
|
|
99
|
+
|
|
100
|
+
if not self.server:
|
|
101
|
+
if self.asgi_application:
|
|
102
|
+
self.extra_dependencies = {"uvicorn"}
|
|
103
|
+
self.server = PythonServer.Uvicorn
|
|
104
|
+
elif self.wsgi_application:
|
|
105
|
+
# gunicorn can't run with Wasmer atm
|
|
106
|
+
self.extra_dependencies = {"uvicorn"}
|
|
107
|
+
self.server = PythonServer.Uvicorn
|
|
108
|
+
elif "fastapi" in found_deps:
|
|
109
|
+
framework = PythonFramework.FastAPI
|
|
110
|
+
if not self.server:
|
|
111
|
+
self.extra_dependencies = {"uvicorn"}
|
|
112
|
+
self.server = PythonServer.Uvicorn
|
|
113
|
+
elif "flask" in found_deps:
|
|
114
|
+
framework = PythonFramework.Flask
|
|
115
|
+
elif "fastapi" in found_deps:
|
|
116
|
+
framework = PythonFramework.FastAPI
|
|
117
|
+
elif "flask" in found_deps:
|
|
118
|
+
framework = PythonFramework.Flask
|
|
119
|
+
elif "python-fasthtml" in found_deps:
|
|
120
|
+
framework = PythonFramework.FastHTML
|
|
121
|
+
else:
|
|
122
|
+
framework = None
|
|
123
|
+
self.framework = framework
|
|
124
|
+
|
|
125
|
+
# Database
|
|
126
|
+
if mysql_deps & found_deps:
|
|
127
|
+
database = DatabaseType.MySQL
|
|
128
|
+
elif pg_deps & found_deps:
|
|
129
|
+
database = DatabaseType.PostgreSQL
|
|
130
|
+
else:
|
|
131
|
+
database = None
|
|
132
|
+
self.database = database
|
|
133
|
+
|
|
134
|
+
def check_deps(self, *deps: str) -> Set[str]:
|
|
135
|
+
deps = set([dep.lower() for dep in deps])
|
|
136
|
+
initial_deps = set(deps)
|
|
137
|
+
for file in ["requirements.txt", "pyproject.toml"]:
|
|
138
|
+
if _exists(self.path, file):
|
|
139
|
+
for line in (self.path / file).read_text().splitlines():
|
|
140
|
+
for dep in set(deps):
|
|
141
|
+
if dep in line.lower():
|
|
142
|
+
deps.remove(dep)
|
|
143
|
+
if not deps:
|
|
144
|
+
break
|
|
145
|
+
if not deps:
|
|
146
|
+
break
|
|
147
|
+
if not deps:
|
|
148
|
+
break
|
|
149
|
+
return initial_deps-deps
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def name(cls) -> str:
|
|
153
|
+
return "python"
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def detect(cls, path: Path) -> Optional[DetectResult]:
|
|
157
|
+
if _exists(path, "pyproject.toml", "requirements.txt"):
|
|
158
|
+
if _exists(path, "manage.py"):
|
|
159
|
+
return DetectResult(cls.name(), 70)
|
|
160
|
+
return DetectResult(cls.name(), 50)
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
def initialize(self) -> None:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
def serve_name(self) -> str:
|
|
167
|
+
return self.path.name
|
|
168
|
+
|
|
169
|
+
def provider_kind(self) -> str:
|
|
170
|
+
return "python"
|
|
171
|
+
|
|
172
|
+
def dependencies(self) -> list[DependencySpec]:
|
|
173
|
+
return [
|
|
174
|
+
DependencySpec(
|
|
175
|
+
"python",
|
|
176
|
+
env_var="SHIPIT_PYTHON_VERSION",
|
|
177
|
+
default_version=self.default_python_version,
|
|
178
|
+
use_in_build=True,
|
|
179
|
+
use_in_serve=True,
|
|
180
|
+
),
|
|
181
|
+
DependencySpec(
|
|
182
|
+
"uv",
|
|
183
|
+
env_var="SHIPIT_UV_VERSION",
|
|
184
|
+
default_version="0.8.15",
|
|
185
|
+
use_in_build=True,
|
|
186
|
+
),
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
def declarations(self) -> Optional[str]:
|
|
190
|
+
return (
|
|
191
|
+
"cross_platform = getenv(\"SHIPIT_PYTHON_CROSS_PLATFORM\")\n"
|
|
192
|
+
"python_extra_index_url = getenv(\"SHIPIT_PYTHON_EXTRA_INDEX_URL\")\n"
|
|
193
|
+
"precompile_python = getenv(\"SHIPIT_PYTHON_PRECOMPILE\") in [\"true\", \"True\", \"TRUE\", \"1\", \"on\", \"yes\", \"y\", \"Y\", \"YES\", \"On\", \"ON\"]\n"
|
|
194
|
+
"python_cross_packages_path = venv[\"build\"] + f\"/lib/python{python_version}/site-packages\"\n"
|
|
195
|
+
"python_serve_path = \"{}/lib/python{}/site-packages\".format(venv[\"serve\"], python_version)\n"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def build_steps(self) -> list[str]:
|
|
199
|
+
steps = [
|
|
200
|
+
"workdir(app[\"build\"])"
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
if _exists(self.path, "pyproject.toml"):
|
|
204
|
+
input_files = ["pyproject.toml"]
|
|
205
|
+
extra_args = ""
|
|
206
|
+
if _exists(self.path, "uv.lock"):
|
|
207
|
+
input_files.append("uv.lock")
|
|
208
|
+
extra_args = " --locked"
|
|
209
|
+
inputs = ", ".join([f"\"{input}\"" for input in input_files])
|
|
210
|
+
extra_deps = ", ".join([f"{dep}" for dep in self.extra_dependencies])
|
|
211
|
+
steps += list(filter(None, [
|
|
212
|
+
"env(UV_PROJECT_ENVIRONMENT=local_venv[\"build\"] if cross_platform else venv[\"build\"])",
|
|
213
|
+
"run(f\"uv sync --compile --python python{python_version} --no-managed-python" + extra_args + "\", inputs=[" + inputs + "], group=\"install\")",
|
|
214
|
+
f"run(\"uv add {extra_deps}\", inputs=[\"pyproject.toml\"], group=\"install\")" if extra_deps else None,
|
|
215
|
+
"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 --only-binary :all: -o cross-requirements.txt\", inputs=[\"pyproject.toml\"], outputs=[\"cross-requirements.txt\"]) if cross_platform else None",
|
|
216
|
+
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",
|
|
217
|
+
"run(\"rm cross-requirements.txt\") if cross_platform else None",
|
|
218
|
+
]))
|
|
219
|
+
if _exists(self.path, "requirements.txt"):
|
|
220
|
+
steps += [
|
|
221
|
+
"env(UV_PROJECT_ENVIRONMENT=local_venv[\"build\"] if cross_platform else venv[\"build\"])",
|
|
222
|
+
"run(f\"uv init --no-managed-python --python python{python_version}\", inputs=[], outputs=[\"uv.lock\"], group=\"install\")",
|
|
223
|
+
"run(f\"uv add -r requirements.txt\", inputs=[\"requirements.txt\"], group=\"install\")",
|
|
224
|
+
"run(f\"uv pip compile requirements.txt --python-version={python_version} --universal --extra-index-url {python_extra_index_url} --index-url=https://pypi.org/simple --emit-index-url --only-binary :all: -o cross-requirements.txt\", inputs=[\"requirements.txt\"], outputs=[\"cross-requirements.txt\"]) if cross_platform else None",
|
|
225
|
+
"run(f\"uvx pip install -r cross-requirements.txt --target {python_cross_packages_path} --platform {cross_platform} --only-binary=:all: --python-version={python_version} --compile\") if cross_platform else None",
|
|
226
|
+
"run(\"rm cross-requirements.txt\") if cross_platform else None",
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
steps += [
|
|
230
|
+
"path((local_venv[\"build\"] if cross_platform else venv[\"build\"]) + \"/bin\")",
|
|
231
|
+
"copy(\".\", \".\", ignore=[\".venv\", \".git\", \"__pycache__\"])",
|
|
232
|
+
]
|
|
233
|
+
return steps
|
|
234
|
+
|
|
235
|
+
def prepare_steps(self) -> Optional[list[str]]:
|
|
236
|
+
return [
|
|
237
|
+
'run("echo \\\"Precompiling Python code...\\\"") if precompile_python else None',
|
|
238
|
+
'run(f"python -m compileall -o 2 {python_serve_path}") if precompile_python else None',
|
|
239
|
+
'run("echo \\\"Precompiling package code...\\\"") if precompile_python else None',
|
|
240
|
+
'run("python -m compileall -o 2 {}".format(app["serve"])) if precompile_python else None',
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
def commands(self) -> Dict[str, str]:
|
|
244
|
+
if self.framework == PythonFramework.Django:
|
|
245
|
+
start_cmd = None
|
|
246
|
+
if self.server == PythonServer.Daphne and self.asgi_application:
|
|
247
|
+
asgi_application = format_app_import(self.asgi_application)
|
|
248
|
+
start_cmd = f'"python -m daphne {asgi_application} --bind 0.0.0.0 --port 8000"'
|
|
249
|
+
elif self.server == PythonServer.Uvicorn:
|
|
250
|
+
if self.asgi_application:
|
|
251
|
+
asgi_application = format_app_import(self.asgi_application)
|
|
252
|
+
start_cmd = f'"python -m uvicorn {asgi_application} --host 0.0.0.0 --port 8000"'
|
|
253
|
+
elif self.wsgi_application:
|
|
254
|
+
wsgi_application = format_app_import(self.wsgi_application)
|
|
255
|
+
start_cmd = f'"python -m uvicorn {wsgi_application} --interface=wsgi --host 0.0.0.0 --port 8000"'
|
|
256
|
+
# elif self.server == PythonServer.Gunicorn:
|
|
257
|
+
# start_cmd = f'"python -m gunicorn {self.wsgi_application} --bind 0.0.0.0 --port 8000"'
|
|
258
|
+
if not start_cmd:
|
|
259
|
+
# We run the default runserver command if no server is specified
|
|
260
|
+
start_cmd = '"python manage.py runserver 0.0.0.0:8000"'
|
|
261
|
+
migrate_cmd = '"python manage.py migrate"'
|
|
262
|
+
return {"start": start_cmd, "after_deploy": migrate_cmd}
|
|
263
|
+
elif self.framework == PythonFramework.FastAPI:
|
|
264
|
+
if _exists(self.path, "main.py"):
|
|
265
|
+
path = "main:app"
|
|
266
|
+
elif _exists(self.path, "src/main.py"):
|
|
267
|
+
path = "src.main:app"
|
|
268
|
+
|
|
269
|
+
if self.server == PythonServer.Uvicorn:
|
|
270
|
+
start_cmd = f'"python -m uvicorn {path} --host 0.0.0.0 --port 8000"'
|
|
271
|
+
elif self.server == PythonServer.Hypercorn:
|
|
272
|
+
start_cmd = f'"python -m hypercorn {path} --bind 0.0.0.0:8000"'
|
|
273
|
+
else:
|
|
274
|
+
start_cmd = '"python -c \'print(\\\"No start command detected, please provide a start command manually\\\")\'"'
|
|
275
|
+
return {"start": start_cmd}
|
|
276
|
+
elif self.framework == PythonFramework.FastHTML:
|
|
277
|
+
if _exists(self.path, "main.py"):
|
|
278
|
+
path = "main:app"
|
|
279
|
+
elif _exists(self.path, "src/main.py"):
|
|
280
|
+
path = "src.main:app"
|
|
281
|
+
start_cmd = f'"python -m uvicorn {path} --host 0.0.0.0 --port 8000"'
|
|
282
|
+
elif _exists(self.path, "main.py"):
|
|
283
|
+
start_cmd = '"python main.py"'
|
|
284
|
+
elif _exists(self.path, "src/main.py"):
|
|
285
|
+
start_cmd = '"python src/main.py"'
|
|
286
|
+
else:
|
|
287
|
+
start_cmd = '"python -c \'print(\\\"No start command detected, please provide a start command manually\\\")\'"'
|
|
288
|
+
return {"start": start_cmd}
|
|
289
|
+
|
|
290
|
+
def assets(self) -> Optional[Dict[str, str]]:
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
def mounts(self) -> list[MountSpec]:
|
|
294
|
+
return [
|
|
295
|
+
MountSpec("app"),
|
|
296
|
+
MountSpec("venv"),
|
|
297
|
+
MountSpec("local_venv", attach_to_serve=False),
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
def env(self) -> Optional[Dict[str, str]]:
|
|
301
|
+
# For Django projects, generate an empty env dict to surface the field
|
|
302
|
+
# in the Shipit file. Other Python projects omit it by default.
|
|
303
|
+
return {
|
|
304
|
+
"PYTHONPATH": "python_serve_path"
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
def format_app_import(asgi_application: str) -> str:
|
|
308
|
+
# Transform "mysite.asgi.application" to "mysite.asgi:application" using regex
|
|
309
|
+
return re.sub(r"\.([^.]+)$", r":\1", asgi_application)
|
|
@@ -11,16 +11,15 @@ from .python import PythonProvider
|
|
|
11
11
|
from .staticfile import StaticFileProvider
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def providers() -> list[Provider]:
|
|
14
|
+
def providers() -> list[type[Provider]]:
|
|
15
15
|
# Order matters: more specific providers first
|
|
16
16
|
return [
|
|
17
|
-
LaravelProvider
|
|
18
|
-
GatsbyProvider
|
|
19
|
-
HugoProvider
|
|
20
|
-
MkdocsProvider
|
|
21
|
-
PythonProvider
|
|
22
|
-
PhpProvider
|
|
23
|
-
NodeStaticProvider
|
|
24
|
-
StaticFileProvider
|
|
17
|
+
LaravelProvider,
|
|
18
|
+
GatsbyProvider,
|
|
19
|
+
HugoProvider,
|
|
20
|
+
MkdocsProvider,
|
|
21
|
+
PythonProvider,
|
|
22
|
+
PhpProvider,
|
|
23
|
+
NodeStaticProvider,
|
|
24
|
+
StaticFileProvider,
|
|
25
25
|
]
|
|
26
|
-
|
|
@@ -7,28 +7,33 @@ from .base import DetectResult, DependencySpec, Provider, _exists, MountSpec
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class StaticFileProvider:
|
|
10
|
-
def
|
|
10
|
+
def __init__(self, path: Path):
|
|
11
|
+
self.path = path
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def name(cls) -> str:
|
|
11
15
|
return "staticfile"
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
@classmethod
|
|
18
|
+
def detect(cls, path: Path) -> Optional[DetectResult]:
|
|
14
19
|
if _exists(path, "Staticfile"):
|
|
15
|
-
return DetectResult(
|
|
20
|
+
return DetectResult(cls.name(), 50)
|
|
16
21
|
if _exists(path, "index.html") and not _exists(
|
|
17
22
|
path, "package.json", "pyproject.toml", "composer.json"
|
|
18
23
|
):
|
|
19
|
-
return DetectResult(
|
|
24
|
+
return DetectResult(cls.name(), 10)
|
|
20
25
|
return None
|
|
21
26
|
|
|
22
|
-
def initialize(self
|
|
27
|
+
def initialize(self) -> None:
|
|
23
28
|
pass
|
|
24
29
|
|
|
25
|
-
def serve_name(self
|
|
26
|
-
return path.name
|
|
30
|
+
def serve_name(self) -> str:
|
|
31
|
+
return self.path.name
|
|
27
32
|
|
|
28
|
-
def provider_kind(self
|
|
33
|
+
def provider_kind(self) -> str:
|
|
29
34
|
return "staticfile"
|
|
30
35
|
|
|
31
|
-
def dependencies(self
|
|
36
|
+
def dependencies(self) -> list[DependencySpec]:
|
|
32
37
|
return [
|
|
33
38
|
DependencySpec(
|
|
34
39
|
"static-web-server",
|
|
@@ -38,28 +43,28 @@ class StaticFileProvider:
|
|
|
38
43
|
)
|
|
39
44
|
]
|
|
40
45
|
|
|
41
|
-
def build_steps(self
|
|
46
|
+
def build_steps(self) -> list[str]:
|
|
42
47
|
return [
|
|
43
48
|
'workdir(app["build"])',
|
|
44
49
|
'copy(".", ".", ignore=[".git"])'
|
|
45
50
|
]
|
|
46
51
|
|
|
47
|
-
def prepare_steps(self
|
|
52
|
+
def prepare_steps(self) -> Optional[list[str]]:
|
|
48
53
|
return None
|
|
49
54
|
|
|
50
|
-
def declarations(self
|
|
55
|
+
def declarations(self) -> Optional[str]:
|
|
51
56
|
return None
|
|
52
57
|
|
|
53
|
-
def commands(self
|
|
58
|
+
def commands(self) -> Dict[str, str]:
|
|
54
59
|
return {
|
|
55
60
|
"start": '"static-web-server --root={} --log-level=info".format(app["serve"])'
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
def assets(self
|
|
63
|
+
def assets(self) -> Optional[Dict[str, str]]:
|
|
59
64
|
return None
|
|
60
65
|
|
|
61
|
-
def mounts(self
|
|
66
|
+
def mounts(self) -> list[MountSpec]:
|
|
62
67
|
return [MountSpec("app")]
|
|
63
68
|
|
|
64
|
-
def env(self
|
|
69
|
+
def env(self) -> Optional[Dict[str, str]]:
|
|
65
70
|
return None
|
|
@@ -1,135 +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
|
-
MountSpec,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class PythonProvider:
|
|
16
|
-
def name(self) -> str:
|
|
17
|
-
return "python"
|
|
18
|
-
|
|
19
|
-
def detect(self, path: Path) -> Optional[DetectResult]:
|
|
20
|
-
if _exists(path, "pyproject.toml", "requirements.txt"):
|
|
21
|
-
if _exists(path, "manage.py"):
|
|
22
|
-
return DetectResult(self.name(), 70)
|
|
23
|
-
return DetectResult(self.name(), 50)
|
|
24
|
-
return None
|
|
25
|
-
|
|
26
|
-
def initialize(self, path: Path) -> None:
|
|
27
|
-
pass
|
|
28
|
-
|
|
29
|
-
def serve_name(self, path: Path) -> str:
|
|
30
|
-
return path.name
|
|
31
|
-
|
|
32
|
-
def provider_kind(self, path: Path) -> str:
|
|
33
|
-
return "python"
|
|
34
|
-
|
|
35
|
-
def dependencies(self, path: Path) -> list[DependencySpec]:
|
|
36
|
-
if _exists(path, ".python-version"):
|
|
37
|
-
python_version = (path / ".python-version").read_text().strip()
|
|
38
|
-
else:
|
|
39
|
-
python_version = "3.13"
|
|
40
|
-
|
|
41
|
-
return [
|
|
42
|
-
DependencySpec(
|
|
43
|
-
"python",
|
|
44
|
-
env_var="SHIPIT_PYTHON_VERSION",
|
|
45
|
-
default_version=python_version,
|
|
46
|
-
use_in_build=True,
|
|
47
|
-
use_in_serve=True,
|
|
48
|
-
),
|
|
49
|
-
DependencySpec(
|
|
50
|
-
"uv",
|
|
51
|
-
env_var="SHIPIT_UV_VERSION",
|
|
52
|
-
default_version="0.8.15",
|
|
53
|
-
use_in_build=True,
|
|
54
|
-
),
|
|
55
|
-
]
|
|
56
|
-
|
|
57
|
-
def declarations(self, path: Path) -> Optional[str]:
|
|
58
|
-
return (
|
|
59
|
-
"cross_platform = getenv(\"SHIPIT_PYTHON_CROSS_PLATFORM\")\n"
|
|
60
|
-
"python_extra_index_url = getenv(\"SHIPIT_PYTHON_EXTRA_INDEX_URL\")\n"
|
|
61
|
-
"precompile_python = getenv(\"SHIPIT_PYTHON_PRECOMPILE\") in [\"true\", \"True\", \"TRUE\", \"1\", \"on\", \"yes\", \"y\", \"Y\", \"YES\", \"On\", \"ON\"]\n"
|
|
62
|
-
"python_cross_packages_path = venv[\"build\"] + f\"/lib/python{python_version}/site-packages\""
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
def build_steps(self, path: Path) -> list[str]:
|
|
66
|
-
steps = [
|
|
67
|
-
"workdir(app[\"build\"])"
|
|
68
|
-
]
|
|
69
|
-
|
|
70
|
-
if _exists(path, "pyproject.toml"):
|
|
71
|
-
input_files = ["pyproject.toml"]
|
|
72
|
-
if _exists(path, "uv.lock"):
|
|
73
|
-
input_files.append("uv.lock")
|
|
74
|
-
inputs = ", ".join([f"\"{input}\"" for input in input_files])
|
|
75
|
-
steps += [
|
|
76
|
-
"env(UV_PROJECT_ENVIRONMENT=local_venv[\"build\"] if cross_platform else venv[\"build\"])",
|
|
77
|
-
"run(f\"uv sync --compile --python python{python_version} --locked --no-managed-python\", inputs=[" + inputs + "], group=\"install\")",
|
|
78
|
-
"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 --only-binary :all: -o cross-requirements.txt\", inputs=[\"pyproject.toml\"], outputs=[\"cross-requirements.txt\"]) if cross_platform else None",
|
|
79
|
-
"run(f\"uvx pip install -r cross-requirements.txt --target {python_cross_packages_path} --platform {cross_platform} --only-binary=:all: --python-version={python_version} --compile\") if cross_platform else None",
|
|
80
|
-
"run(\"rm cross-requirements.txt\") if cross_platform else None",
|
|
81
|
-
]
|
|
82
|
-
if _exists(path, "requirements.txt"):
|
|
83
|
-
steps += [
|
|
84
|
-
"env(UV_PROJECT_ENVIRONMENT=local_venv[\"build\"] if cross_platform else venv[\"build\"])",
|
|
85
|
-
"run(f\"uv init --no-managed-python --python python{python_version}\", inputs=[], outputs=[\"uv.lock\"], group=\"install\")",
|
|
86
|
-
"run(f\"uv add -r requirements.txt\", inputs=[\"requirements.txt\"], group=\"install\")",
|
|
87
|
-
"run(f\"uv pip compile requirements.txt --python-version={python_version} --universal --extra-index-url {python_extra_index_url} --index-url=https://pypi.org/simple --emit-index-url --only-binary :all: -o cross-requirements.txt\", inputs=[\"requirements.txt\"], outputs=[\"cross-requirements.txt\"]) if cross_platform else None",
|
|
88
|
-
"run(f\"uvx pip install -r cross-requirements.txt --target {python_cross_packages_path} --platform {cross_platform} --only-binary=:all: --python-version={python_version} --compile\") if cross_platform else None",
|
|
89
|
-
"run(\"rm cross-requirements.txt\") if cross_platform else None",
|
|
90
|
-
]
|
|
91
|
-
|
|
92
|
-
steps += [
|
|
93
|
-
"path((local_venv[\"build\"] if cross_platform else venv[\"build\"]) + \"/bin\")",
|
|
94
|
-
"copy(\".\", \".\", ignore=[\".venv\", \".git\", \"__pycache__\"])",
|
|
95
|
-
]
|
|
96
|
-
return steps
|
|
97
|
-
|
|
98
|
-
def prepare_steps(self, path: Path) -> Optional[list[str]]:
|
|
99
|
-
return [
|
|
100
|
-
'workdir(app["serve"])',
|
|
101
|
-
'run("echo \\\"Precompiling Python code...\\\"") if precompile_python else None',
|
|
102
|
-
'run("python -m compileall -o 2 $PYTHONPATH") if precompile_python else None',
|
|
103
|
-
'run("echo \\\"Precompiling package code...\\\"") if precompile_python else None',
|
|
104
|
-
'run("python -m compileall -o 2 .") if precompile_python else None',
|
|
105
|
-
]
|
|
106
|
-
|
|
107
|
-
def commands(self, path: Path) -> Dict[str, str]:
|
|
108
|
-
if _exists(path, "manage.py"):
|
|
109
|
-
start_cmd = '"python manage.py runserver 0.0.0.0:8000"'
|
|
110
|
-
migrate_cmd = '"python manage.py migrate"'
|
|
111
|
-
return {"start": start_cmd, "after_deploy": migrate_cmd}
|
|
112
|
-
elif _exists(path, "main.py"):
|
|
113
|
-
start_cmd = '"python main.py"'
|
|
114
|
-
elif _exists(path, "src/main.py"):
|
|
115
|
-
start_cmd = '"python src/main.py"'
|
|
116
|
-
else:
|
|
117
|
-
start_cmd = '"python -c \'print(\\\"Hello, World!\\\")\'"'
|
|
118
|
-
return {"start": start_cmd}
|
|
119
|
-
|
|
120
|
-
def assets(self, path: Path) -> Optional[Dict[str, str]]:
|
|
121
|
-
return None
|
|
122
|
-
|
|
123
|
-
def mounts(self, path: Path) -> list[MountSpec]:
|
|
124
|
-
return [
|
|
125
|
-
MountSpec("app"),
|
|
126
|
-
MountSpec("venv"),
|
|
127
|
-
MountSpec("local_venv", attach_to_serve=False),
|
|
128
|
-
]
|
|
129
|
-
|
|
130
|
-
def env(self, path: Path) -> Optional[Dict[str, str]]:
|
|
131
|
-
# For Django projects, generate an empty env dict to surface the field
|
|
132
|
-
# in the Shipit file. Other Python projects omit it by default.
|
|
133
|
-
return {
|
|
134
|
-
"PYTHONPATH": "\"{}/lib/python{}/site-packages\".format(venv[\"serve\"], python_version)"
|
|
135
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|