shipit-cli 0.20.1__tar.gz → 0.21.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.
Files changed (55) hide show
  1. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/PKG-INFO +1 -1
  2. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/pyproject.toml +1 -1
  3. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/assets/wordpress/install.sh +4 -1
  4. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/builders/docker.py +9 -0
  5. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/builders/local.py +25 -5
  6. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/cli.py +292 -48
  7. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/generator.py +15 -7
  8. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/providers/base.py +23 -0
  9. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/providers/go.py +47 -14
  10. shipit_cli-0.21.0/src/shipit/providers/install_context.py +635 -0
  11. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/providers/node.py +131 -19
  12. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/providers/node_static.py +5 -1
  13. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/providers/python.py +72 -35
  14. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/providers/staticfile.py +7 -3
  15. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/runners/base.py +1 -0
  16. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/runners/local.py +7 -1
  17. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/runners/wasmer.py +21 -19
  18. shipit_cli-0.21.0/src/shipit/version.py +5 -0
  19. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/tests/test_cli_after_deploy.py +70 -0
  20. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/tests/test_generate_shipit_examples.py +21 -5
  21. shipit_cli-0.21.0/tests/test_install_context.py +233 -0
  22. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/tests/test_node_provider.py +22 -0
  23. shipit_cli-0.21.0/tests/test_subdir.py +555 -0
  24. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/tests/test_wasmer_annotations.py +27 -5
  25. shipit_cli-0.20.1/src/shipit/version.py +0 -5
  26. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/.gitignore +0 -0
  27. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/README.md +0 -0
  28. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/__init__.py +0 -0
  29. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/assets/node/optimize-node-modules.sh +0 -0
  30. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/assets/php/php.ini +0 -0
  31. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/assets/wordpress/.htaccess +0 -0
  32. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/assets/wordpress/start.php +0 -0
  33. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/assets/wordpress/wp-config.php +0 -0
  34. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/builders/__init__.py +0 -0
  35. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/builders/base.py +0 -0
  36. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/procfile.py +0 -0
  37. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/providers/hugo.py +0 -0
  38. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/providers/jekyll.py +0 -0
  39. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/providers/laravel.py +0 -0
  40. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/providers/mkdocs.py +0 -0
  41. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/providers/php.py +0 -0
  42. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/providers/registry.py +0 -0
  43. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/providers/wordpress.py +0 -0
  44. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/runners/__init__.py +0 -0
  45. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/shipit_types.py +0 -0
  46. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/ui.py +0 -0
  47. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/utils.py +0 -0
  48. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/src/shipit/volumes.py +0 -0
  49. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/tests/test_e2e.py +0 -0
  50. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/tests/test_node_static_provider.py +0 -0
  51. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/tests/test_php_provider.py +0 -0
  52. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/tests/test_staticfile_provider.py +0 -0
  53. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/tests/test_version.py +0 -0
  54. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/tests/test_volumes.py +0 -0
  55. {shipit_cli-0.20.1 → shipit_cli-0.21.0}/tests/test_wordpress_phpix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shipit-cli
3
- Version: 0.20.1
3
+ Version: 0.21.0
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.20.1"
3
+ version = "0.21.0"
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"
@@ -19,7 +19,10 @@ mkdir -p wp-content/upgrade
19
19
 
20
20
  if [ -n "${WPCONTENT_BASE_PATH:-}" ] && [ -d "${WPCONTENT_BASE_PATH}" ]; then
21
21
  shopt -s dotglob nullglob
22
- cp -R "${WPCONTENT_BASE_PATH}"/* /app/wp-content || true
22
+ # Note: change this back to copy all, once using the WP Zip files.
23
+ # cp -R "${WPCONTENT_BASE_PATH}"/* /app/wp-content || true
24
+ cp -R "${WPCONTENT_BASE_PATH}"/plugins/* /app/wp-content/plugins || true
25
+ cp -R "${WPCONTENT_BASE_PATH}"/themes/twentytwenty* /app/wp-content/themes || true
23
26
  shopt -u dotglob nullglob
24
27
  fi
25
28
 
@@ -162,6 +162,15 @@ RUN curl https://mise.run | sh
162
162
  docker_file_contents += f"WORKDIR {step.path.absolute()}\n"
163
163
  elif isinstance(step, RunStep):
164
164
  if step.inputs:
165
+ parents = sorted(
166
+ {
167
+ Path(input).parent.as_posix()
168
+ for input in step.inputs
169
+ if Path(input).parent != Path(".")
170
+ }
171
+ )
172
+ for parent in parents:
173
+ docker_file_contents += f"RUN mkdir -p {parent}\n"
165
174
  pre = "\\\n " + "".join(
166
175
  [
167
176
  f"--mount=type=bind,source={input},target={input} \\\n "
@@ -62,8 +62,14 @@ class LocalBuildBackend:
62
62
  extra = ""
63
63
  if step.inputs:
64
64
  for input in step.inputs:
65
- console.print(f"Copying {input} to {build_path / input}")
66
- shutil.copy((self.src_dir / input), (build_path / input))
65
+ source = self.src_dir / input
66
+ target = build_path / input
67
+ console.print(f"Copying {input} to {target}")
68
+ target.parent.mkdir(parents=True, exist_ok=True)
69
+ if source.is_dir():
70
+ shutil.copytree(source, target, dirs_exist_ok=True)
71
+ else:
72
+ shutil.copy(source, target)
67
73
  all_inputs = ", ".join(step.inputs)
68
74
  extra = f" [bright_black]# using {all_inputs}[/bright_black]"
69
75
  console.print(
@@ -113,11 +119,25 @@ class LocalBuildBackend:
113
119
  raise Exception(f"Unknown base: {step.base}")
114
120
 
115
121
  console.print(
116
- f"[bold]Copy to {step.target} from {step.source}[/bold]{ignore_extra}"
122
+ "[bold]Copy to "
123
+ f"{step.target} from {step.source}[/bold]{ignore_extra}"
117
124
  )
118
125
 
119
- source = base / step.source
120
- target = build_path / step.target
126
+ source_path = Path(step.source)
127
+ target_path = Path(step.target)
128
+ source = (
129
+ source_path
130
+ if source_path.is_absolute()
131
+ else base / source_path
132
+ )
133
+ target = (
134
+ target_path
135
+ if target_path.is_absolute()
136
+ else build_path / target_path
137
+ )
138
+ if source.resolve(strict=False) == target.resolve(strict=False):
139
+ return
140
+ target.parent.mkdir(parents=True, exist_ok=True)
121
141
  if source.is_dir():
122
142
  shutil.copytree(
123
143
  source,
@@ -2,6 +2,7 @@ import tempfile
2
2
  import os
3
3
  import sys
4
4
  import json
5
+ import re
5
6
  from pathlib import Path
6
7
  from dataclasses import dataclass
7
8
  from typing import Any, Dict, List, Optional, Tuple, cast, Literal
@@ -57,6 +58,148 @@ class CtxMount:
57
58
  serve_path: str
58
59
 
59
60
 
61
+ @dataclass(frozen=True)
62
+ class ProjectPaths:
63
+ workspace_root: Path
64
+ app_path: Path
65
+ subdir: Optional[str] = None
66
+
67
+
68
+ def shipit_subdir_slug(subdir: str) -> str:
69
+ slug = re.sub(r"[^A-Za-z0-9._-]+", "-", subdir.replace("/", "-"))
70
+ return slug.strip("-") or "app"
71
+
72
+
73
+ def default_shipit_path(project_paths: ProjectPaths) -> Path:
74
+ if project_paths.subdir is None:
75
+ return project_paths.workspace_root / "Shipit"
76
+ return (
77
+ project_paths.workspace_root
78
+ / f"Shipit.{shipit_subdir_slug(project_paths.subdir)}"
79
+ )
80
+
81
+
82
+ def resolve_project_paths(path: Path, subdir: Optional[Path] = None) -> ProjectPaths:
83
+ workspace_root = path.resolve()
84
+ if subdir is None:
85
+ return ProjectPaths(workspace_root, workspace_root)
86
+
87
+ if subdir.is_absolute():
88
+ raise ValueError("--subdir must be relative to the project path")
89
+
90
+ subdir_text = subdir.as_posix().strip("/")
91
+ if not subdir_text or subdir_text == ".":
92
+ return ProjectPaths(workspace_root, workspace_root)
93
+
94
+ app_path = (workspace_root / subdir_text).resolve()
95
+ try:
96
+ app_path.relative_to(workspace_root)
97
+ except ValueError:
98
+ raise ValueError("--subdir must stay inside the project path") from None
99
+
100
+ if not app_path.exists():
101
+ raise ValueError(f"--subdir does not exist: {subdir_text}")
102
+ if not app_path.is_dir():
103
+ raise ValueError(f"--subdir is not a directory: {subdir_text}")
104
+
105
+ normalized_subdir = app_path.relative_to(workspace_root).as_posix()
106
+ return ProjectPaths(workspace_root, app_path, normalized_subdir)
107
+
108
+
109
+ def read_shipit_subdir(shipit_file: Path) -> Optional[Path]:
110
+ if not shipit_file.exists():
111
+ return None
112
+ match = re.search(
113
+ r"^app_subdir\s*=\s*(\"(?:\\.|[^\"])*\")\s*$",
114
+ shipit_file.read_text(),
115
+ re.MULTILINE,
116
+ )
117
+ if not match:
118
+ return None
119
+ value = json.loads(match.group(1))
120
+ return Path(value) if value else None
121
+
122
+
123
+ def load_env_files(
124
+ project_paths: ProjectPaths,
125
+ env_name: Optional[str],
126
+ target: Dict[str, str],
127
+ ) -> None:
128
+ env_dirs = [project_paths.workspace_root]
129
+ if project_paths.subdir:
130
+ env_dirs.append(project_paths.app_path)
131
+
132
+ env_names = [".env"]
133
+ if env_name:
134
+ env_names.append(f".env.{env_name}")
135
+
136
+ for env_dir in env_dirs:
137
+ for env_file_name in env_names:
138
+ env_file = env_dir / env_file_name
139
+ if env_file.exists():
140
+ target.update(dotenv_values(env_file))
141
+
142
+
143
+ def _rewrite_package_manager_command(
144
+ command: Optional[str],
145
+ old_manager: Any,
146
+ new_manager: Any,
147
+ ) -> Optional[str]:
148
+ if not command:
149
+ return command
150
+ old_run = f"{old_manager.value} run "
151
+ if command.startswith(old_run):
152
+ return f"{new_manager.value} run {command[len(old_run):]}"
153
+ return command
154
+
155
+
156
+ def apply_subdir_workspace_config(
157
+ project_paths: ProjectPaths,
158
+ provider_config: Any,
159
+ ) -> None:
160
+ if not project_paths.subdir or not hasattr(provider_config, "package_manager"):
161
+ return
162
+ if not (project_paths.app_path / "package.json").exists():
163
+ return
164
+
165
+ from shipit.providers.node import NodeProvider, PackageManager
166
+
167
+ app_has_lockfile = any(
168
+ (project_paths.app_path / manager.lockfile()).exists()
169
+ for manager in PackageManager
170
+ )
171
+ if app_has_lockfile:
172
+ return
173
+
174
+ workspace_manager = NodeProvider.detect_package_manager(
175
+ project_paths.workspace_root
176
+ )
177
+ current_manager = provider_config.package_manager
178
+ if current_manager == workspace_manager:
179
+ return
180
+
181
+ provider_config.package_manager = workspace_manager
182
+ provider_config.build_command = _rewrite_package_manager_command(
183
+ getattr(provider_config, "build_command", None),
184
+ current_manager,
185
+ workspace_manager,
186
+ )
187
+ if provider_config.commands:
188
+ provider_config.commands.build = _rewrite_package_manager_command(
189
+ provider_config.commands.build,
190
+ current_manager,
191
+ workspace_manager,
192
+ )
193
+
194
+
195
+ def apply_subdir_provider_config(
196
+ project_paths: ProjectPaths,
197
+ provider_config: Any,
198
+ ) -> None:
199
+ if hasattr(provider_config, "app_subdir"):
200
+ provider_config.app_subdir = project_paths.subdir
201
+
202
+
60
203
  class Ctx:
61
204
  def __init__(self, build_backend: BuildBackend, runner: Runner) -> None:
62
205
  self.build_backend = build_backend
@@ -350,6 +493,11 @@ def auto(
350
493
  help="Project path (defaults to current directory).",
351
494
  show_default=False,
352
495
  ),
496
+ subdir: Optional[Path] = typer.Option(
497
+ None,
498
+ "--subdir",
499
+ help="App subdirectory relative to the project path.",
500
+ ),
353
501
  wasmer: bool = typer.Option(
354
502
  False,
355
503
  help="Use Wasmer to build and run the project.",
@@ -405,7 +553,7 @@ def auto(
405
553
  ),
406
554
  shipit_path: Optional[Path] = typer.Option(
407
555
  None,
408
- help="The path to the Shipit file (defaults to Shipit in the provided path).",
556
+ help="The path to the Shipit file (defaults to Shipit or Shipit.<subdir>).",
409
557
  ),
410
558
  temp_shipit: bool = typer.Option(
411
559
  False,
@@ -469,6 +617,7 @@ def auto(
469
617
 
470
618
  if not path.exists():
471
619
  raise Exception(f"The path {path} does not exist")
620
+ project_paths = resolve_project_paths(path, subdir)
472
621
 
473
622
  if temp_shipit:
474
623
  if shipit_path:
@@ -481,12 +630,13 @@ def auto(
481
630
  if not regenerate:
482
631
  if shipit_path and not shipit_path.exists():
483
632
  regenerate = True
484
- elif not (path / "Shipit").exists():
633
+ elif not shipit_path and not default_shipit_path(project_paths).exists():
485
634
  regenerate = True
486
635
 
487
636
  if regenerate:
488
637
  generate(
489
- path,
638
+ project_paths.workspace_root,
639
+ subdir=Path(project_paths.subdir) if project_paths.subdir else None,
490
640
  out=shipit_path,
491
641
  install_command=install_command,
492
642
  build_command=build_command,
@@ -496,7 +646,8 @@ def auto(
496
646
  )
497
647
 
498
648
  build(
499
- path,
649
+ project_paths.workspace_root,
650
+ subdir=Path(project_paths.subdir) if project_paths.subdir else None,
500
651
  shipit_path=shipit_path,
501
652
  install_command=install_command,
502
653
  build_command=build_command,
@@ -521,7 +672,7 @@ def auto(
521
672
  or after_deploy
522
673
  ):
523
674
  run(
524
- path,
675
+ project_paths.workspace_root,
525
676
  wasmer=wasmer,
526
677
  wasmer_bin=wasmer_bin,
527
678
  docker=docker,
@@ -532,11 +683,12 @@ def auto(
532
683
  start=start,
533
684
  after_deploy=after_deploy,
534
685
  wasmer_registry=wasmer_registry,
686
+ serve_port=serve_port,
535
687
  )
536
688
 
537
689
  if wasmer_deploy or wasmer_deploy_config:
538
690
  deploy(
539
- path,
691
+ project_paths.workspace_root,
540
692
  wasmer_deploy=wasmer_deploy,
541
693
  wasmer_deploy_config=wasmer_deploy_config,
542
694
  wasmer_bin=wasmer_bin,
@@ -554,13 +706,18 @@ def generate(
554
706
  help="Project path (defaults to current directory).",
555
707
  show_default=False,
556
708
  ),
709
+ subdir: Optional[Path] = typer.Option(
710
+ None,
711
+ "--subdir",
712
+ help="App subdirectory relative to the project path.",
713
+ ),
557
714
  out: Optional[Path] = typer.Option(
558
715
  None,
559
716
  "-o",
560
717
  "--out",
561
718
  "--output",
562
719
  "--shipit-path",
563
- help="Output path (defaults to the Shipit file in the provided path).",
720
+ help="Output path (defaults to Shipit or Shipit.<subdir>).",
564
721
  ),
565
722
  install_command: Optional[str] = typer.Option(
566
723
  None,
@@ -585,24 +742,38 @@ def generate(
585
742
  ):
586
743
  if not path.exists():
587
744
  raise Exception(f"The path {path} does not exist")
745
+ project_paths = resolve_project_paths(path, subdir)
588
746
 
589
747
  if out is None:
590
- out = path / "Shipit"
748
+ out = default_shipit_path(project_paths)
591
749
 
592
750
  base_config = Config()
593
- base_config.commands.enrich_from_path(path)
751
+ base_config.commands.enrich_from_path(project_paths.app_path)
594
752
  if start_command:
595
753
  base_config.commands.start = start_command
596
754
  if install_command:
597
755
  base_config.commands.install = install_command
598
756
  if build_command:
599
757
  base_config.commands.build = build_command
600
- provider_cls = load_provider(path, base_config, use_provider=provider)
758
+ provider_cls = load_provider(
759
+ project_paths.app_path,
760
+ base_config,
761
+ use_provider=provider,
762
+ )
601
763
  provider_config = load_provider_config(
602
- provider_cls, path, base_config, config=config
764
+ provider_cls,
765
+ project_paths.app_path,
766
+ base_config,
767
+ config=config,
768
+ )
769
+ apply_subdir_provider_config(project_paths, provider_config)
770
+ apply_subdir_workspace_config(project_paths, provider_config)
771
+ provider = provider_cls(project_paths.app_path, provider_config)
772
+ content = generate_shipit(
773
+ project_paths.app_path,
774
+ provider,
775
+ subdir=project_paths.subdir,
603
776
  )
604
- provider = provider_cls(path, provider_config)
605
- content = generate_shipit(path, provider)
606
777
  config_json = provider_config.model_dump_json(indent=2, exclude_defaults=True)
607
778
  if config_json and config_json != "{}":
608
779
  manifest_panel = Panel(
@@ -737,6 +908,10 @@ def run(
737
908
  None,
738
909
  help="Wasmer registry.",
739
910
  ),
911
+ serve_port: Optional[int] = typer.Option(
912
+ None,
913
+ help="The port to use (defaults to 8080).",
914
+ ),
740
915
  ) -> None:
741
916
  if not path.exists():
742
917
  raise Exception(f"The path {path} does not exist")
@@ -765,7 +940,13 @@ def run(
765
940
  )
766
941
 
767
942
  if commands_to_run:
768
- run_serve_commands(path, runner, commands_to_run, volume_specs=volume_specs)
943
+ run_serve_commands(
944
+ path,
945
+ runner,
946
+ commands_to_run,
947
+ volume_specs=volume_specs,
948
+ env=runtime_serve_env(serve_port),
949
+ )
769
950
  else:
770
951
  console.print("[bold]No commands specified. Use `--command` to run a command.[/bold]")
771
952
 
@@ -776,6 +957,11 @@ def plan(
776
957
  help="Project path (defaults to current directory).",
777
958
  show_default=False,
778
959
  ),
960
+ subdir: Optional[Path] = typer.Option(
961
+ None,
962
+ "--subdir",
963
+ help="App subdirectory relative to the project path.",
964
+ ),
779
965
  out: Optional[Path] = typer.Option(
780
966
  None,
781
967
  "-o",
@@ -793,7 +979,7 @@ def plan(
793
979
  ),
794
980
  shipit_path: Optional[Path] = typer.Option(
795
981
  None,
796
- help="The path to the Shipit file (defaults to Shipit in the provided path).",
982
+ help="The path to the Shipit file (defaults to Shipit or Shipit.<subdir>).",
797
983
  ),
798
984
  wasmer: bool = typer.Option(
799
985
  False,
@@ -846,6 +1032,7 @@ def plan(
846
1032
  ) -> None:
847
1033
  if not path.exists():
848
1034
  raise Exception(f"The path {path} does not exist")
1035
+ project_paths = resolve_project_paths(path, subdir)
849
1036
 
850
1037
  if temp_shipit:
851
1038
  if shipit_path:
@@ -858,12 +1045,13 @@ def plan(
858
1045
  if not regenerate:
859
1046
  if shipit_path and not shipit_path.exists():
860
1047
  regenerate = True
861
- elif not (path / "Shipit").exists():
1048
+ elif not shipit_path and not default_shipit_path(project_paths).exists():
862
1049
  regenerate = True
863
1050
 
864
1051
  if regenerate:
865
1052
  generate(
866
- path,
1053
+ project_paths.workspace_root,
1054
+ subdir=Path(project_paths.subdir) if project_paths.subdir else None,
867
1055
  out=shipit_path,
868
1056
  install_command=install_command,
869
1057
  build_command=build_command,
@@ -872,27 +1060,32 @@ def plan(
872
1060
  config=config,
873
1061
  )
874
1062
 
875
- shipit_file = get_shipit_path(path, shipit_path)
1063
+ shipit_file = get_shipit_path(project_paths, shipit_path)
1064
+ if project_paths.subdir is None:
1065
+ project_paths = resolve_project_paths(
1066
+ project_paths.workspace_root,
1067
+ read_shipit_subdir(shipit_file),
1068
+ )
876
1069
 
877
1070
  if docker or docker_client:
878
1071
  build_backend: BuildBackend = DockerBuildBackend(
879
- path, ASSETS_PATH, docker_client
1072
+ project_paths.workspace_root, ASSETS_PATH, docker_client
880
1073
  )
881
1074
  else:
882
- build_backend = LocalBuildBackend(path, ASSETS_PATH)
1075
+ build_backend = LocalBuildBackend(project_paths.workspace_root, ASSETS_PATH)
883
1076
  if wasmer:
884
1077
  runner: Runner = WasmerRunner(
885
1078
  build_backend,
886
- path,
1079
+ project_paths.workspace_root,
887
1080
  registry=wasmer_registry,
888
1081
  token=wasmer_token,
889
1082
  bin=wasmer_bin,
890
1083
  )
891
1084
  else:
892
- runner = LocalRunner(build_backend, path)
1085
+ runner = LocalRunner(build_backend, project_paths.workspace_root)
893
1086
 
894
1087
  base_config = Config()
895
- base_config.commands.enrich_from_path(path)
1088
+ base_config.commands.enrich_from_path(project_paths.app_path)
896
1089
  if install_command:
897
1090
  base_config.commands.install = install_command
898
1091
  if build_command:
@@ -901,10 +1094,19 @@ def plan(
901
1094
  base_config.commands.start = start_command
902
1095
  if serve_port:
903
1096
  base_config.port = serve_port
904
- provider_cls = load_provider(path, base_config, use_provider=provider)
1097
+ provider_cls = load_provider(
1098
+ project_paths.app_path,
1099
+ base_config,
1100
+ use_provider=provider,
1101
+ )
905
1102
  provider_config = load_provider_config(
906
- provider_cls, path, base_config, config=config
1103
+ provider_cls,
1104
+ project_paths.app_path,
1105
+ base_config,
1106
+ config=config,
907
1107
  )
1108
+ apply_subdir_provider_config(project_paths, provider_config)
1109
+ apply_subdir_workspace_config(project_paths, provider_config)
908
1110
  # provider_config = runner.prepare_config(provider_config)
909
1111
  ctx, serve = evaluate_shipit(shipit_file, build_backend, runner, provider_config)
910
1112
 
@@ -955,9 +1157,14 @@ def build(
955
1157
  help="Project path (defaults to current directory).",
956
1158
  show_default=False,
957
1159
  ),
1160
+ subdir: Optional[Path] = typer.Option(
1161
+ None,
1162
+ "--subdir",
1163
+ help="App subdirectory relative to the project path.",
1164
+ ),
958
1165
  shipit_path: Optional[Path] = typer.Option(
959
1166
  None,
960
- help="The path to the Shipit file (defaults to Shipit in the provided path).",
1167
+ help="The path to the Shipit file (defaults to Shipit or Shipit.<subdir>).",
961
1168
  ),
962
1169
  start_command: Optional[str] = typer.Option(
963
1170
  None,
@@ -1027,27 +1234,39 @@ def build(
1027
1234
  if not path.exists():
1028
1235
  raise Exception(f"The path {path} does not exist")
1029
1236
 
1030
- shipit_file = get_shipit_path(path, shipit_path)
1237
+ project_paths = resolve_project_paths(
1238
+ path,
1239
+ subdir,
1240
+ )
1241
+ shipit_file = get_shipit_path(project_paths, shipit_path)
1242
+ if project_paths.subdir is None:
1243
+ project_paths = resolve_project_paths(
1244
+ path,
1245
+ read_shipit_subdir(shipit_file),
1246
+ )
1031
1247
 
1032
1248
  if docker or docker_client:
1033
1249
  build_backend: BuildBackend = DockerBuildBackend(
1034
- path, ASSETS_PATH, docker_client, docker_opts=docker_opts
1250
+ project_paths.workspace_root,
1251
+ ASSETS_PATH,
1252
+ docker_client,
1253
+ docker_opts=docker_opts,
1035
1254
  )
1036
1255
  else:
1037
- build_backend = LocalBuildBackend(path, ASSETS_PATH)
1256
+ build_backend = LocalBuildBackend(project_paths.workspace_root, ASSETS_PATH)
1038
1257
  if wasmer:
1039
1258
  runner: Runner = WasmerRunner(
1040
1259
  build_backend,
1041
- path,
1260
+ project_paths.workspace_root,
1042
1261
  registry=wasmer_registry,
1043
1262
  token=wasmer_token,
1044
1263
  bin=wasmer_bin,
1045
1264
  )
1046
1265
  else:
1047
- runner = LocalRunner(build_backend, path)
1266
+ runner = LocalRunner(build_backend, project_paths.workspace_root)
1048
1267
 
1049
1268
  base_config = Config()
1050
- base_config.commands.enrich_from_path(path)
1269
+ base_config.commands.enrich_from_path(project_paths.app_path)
1051
1270
  if start_command:
1052
1271
  base_config.commands.start = start_command
1053
1272
  if install_command:
@@ -1058,10 +1277,19 @@ def build(
1058
1277
  if serve_port:
1059
1278
  base_config.port = serve_port
1060
1279
 
1061
- provider_cls = load_provider(path, base_config, use_provider=provider)
1280
+ provider_cls = load_provider(
1281
+ project_paths.app_path,
1282
+ base_config,
1283
+ use_provider=provider,
1284
+ )
1062
1285
  provider_config = load_provider_config(
1063
- provider_cls, path, base_config, config=config
1286
+ provider_cls,
1287
+ project_paths.app_path,
1288
+ base_config,
1289
+ config=config,
1064
1290
  )
1291
+ apply_subdir_provider_config(project_paths, provider_config)
1292
+ apply_subdir_workspace_config(project_paths, provider_config)
1065
1293
  provider_config = runner.prepare_config(provider_config)
1066
1294
  ctx, serve = evaluate_shipit(shipit_file, build_backend, runner, provider_config)
1067
1295
  env = {
@@ -1081,7 +1309,10 @@ def build(
1081
1309
  f"[bold]ℹ️ Building locally instead of Docker to speed up the build, as all commands are safe to run locally[/bold]"
1082
1310
  )
1083
1311
  return build(
1084
- path,
1312
+ project_paths.workspace_root,
1313
+ subdir=Path(project_paths.subdir)
1314
+ if project_paths.subdir
1315
+ else None,
1085
1316
  shipit_path=shipit_path,
1086
1317
  install_command=install_command,
1087
1318
  build_command=build_command,
@@ -1102,13 +1333,7 @@ def build(
1102
1333
  )
1103
1334
 
1104
1335
  serve.env = serve.env or {}
1105
- if (path / ".env").exists():
1106
- env_vars = dotenv_values(path / ".env")
1107
- serve.env.update(env_vars)
1108
-
1109
- if (path / f".env.{env_name}").exists():
1110
- env_vars = dotenv_values(path / f".env.{env_name}")
1111
- serve.env.update(env_vars)
1336
+ load_env_files(project_paths, env_name, serve.env)
1112
1337
 
1113
1338
  assert serve.commands.get("start"), (
1114
1339
  "No start command could be found, please provide a start command"
@@ -1119,23 +1344,29 @@ def build(
1119
1344
 
1120
1345
  # Build and serve
1121
1346
  build_backend.build(serve.name, env, serve.mounts or [], build_steps)
1122
- build_volumes(path, serve)
1347
+ build_volumes(project_paths.workspace_root, serve)
1123
1348
  runner.build(serve)
1124
1349
  if serve.prepare and not skip_prepare:
1125
1350
  console.print("\n[bold]Running prepare step[/bold]")
1126
1351
  runner.prepare(env, serve.prepare)
1127
1352
 
1128
1353
 
1129
- def get_shipit_path(path: Path, shipit_path: Optional[Path] = None) -> Path:
1354
+ def get_shipit_path(
1355
+ project_paths: ProjectPaths,
1356
+ shipit_path: Optional[Path] = None,
1357
+ ) -> Path:
1130
1358
  if shipit_path is None:
1131
- shipit_path = path / "Shipit"
1359
+ shipit_path = default_shipit_path(project_paths)
1132
1360
  if not shipit_path.exists():
1361
+ command = f"shipit generate {project_paths.workspace_root}"
1362
+ if project_paths.subdir:
1363
+ command = f"{command} --subdir={project_paths.subdir}"
1133
1364
  raise Exception(
1134
- f"Shipit file not found at {shipit_path}. Run `shipit generate {path}` to create it."
1365
+ f"Shipit file not found at {shipit_path}. Run `{command}` to create it."
1135
1366
  )
1136
1367
  elif not shipit_path.exists():
1137
1368
  raise Exception(
1138
- f"Shipit file not found at {shipit_path}. Run `shipit generate {path} -o {shipit_path}` to create it."
1369
+ f"Shipit file not found at {shipit_path}. Run `shipit generate {project_paths.workspace_root} -o {shipit_path}` to create it."
1139
1370
  )
1140
1371
  return shipit_path
1141
1372
 
@@ -1158,6 +1389,7 @@ def run_serve_commands(
1158
1389
  runner: Runner,
1159
1390
  commands: List[str],
1160
1391
  volume_specs: Optional[List[str]] = None,
1392
+ env: Optional[Dict[str, str]] = None,
1161
1393
  ) -> None:
1162
1394
  volume_mappings = merge_volume_mappings(
1163
1395
  load_volume_mappings(path),
@@ -1167,7 +1399,19 @@ def run_serve_commands(
1167
1399
  if command in OPTIONAL_RUN_COMMANDS and not runner.has_serve_command(command):
1168
1400
  continue
1169
1401
  console.print(f"\nRunning command [bold]{command}[/bold]")
1170
- runner.run_serve_command(command, volume_mappings=volume_mappings)
1402
+ runner.run_serve_command(
1403
+ command,
1404
+ volume_mappings=volume_mappings,
1405
+ env=env,
1406
+ )
1407
+
1408
+
1409
+ def runtime_serve_env(serve_port: Optional[int]) -> Dict[str, str]:
1410
+ if serve_port is not None:
1411
+ port = str(serve_port)
1412
+ else:
1413
+ port = os.environ.get("PORT", "8080")
1414
+ return {"PORT": port}
1171
1415
 
1172
1416
  def main() -> None:
1173
1417
  args = sys.argv[1:]