springdocker 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- springdocker/__init__.py +9 -0
- springdocker/analyze.py +284 -0
- springdocker/benchmarks/__init__.py +2 -0
- springdocker/benchmarks/generate.py +104 -0
- springdocker/benchmarks/runner.py +343 -0
- springdocker/cli.py +289 -0
- springdocker/commands.py +388 -0
- springdocker/compare.py +138 -0
- springdocker/config.py +365 -0
- springdocker/dockerfile.py +332 -0
- springdocker/errors.py +16 -0
- springdocker/plugins.py +75 -0
- springdocker/project_detect.py +256 -0
- springdocker/regression.py +151 -0
- springdocker/services/__init__.py +2 -0
- springdocker/services/benchmark_service.py +124 -0
- springdocker/services/dockerfile_service.py +76 -0
- springdocker/services/project_service.py +37 -0
- springdocker-1.0.1.dist-info/METADATA +189 -0
- springdocker-1.0.1.dist-info/RECORD +23 -0
- springdocker-1.0.1.dist-info/WHEEL +5 -0
- springdocker-1.0.1.dist-info/entry_points.txt +2 -0
- springdocker-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .analyze import VariantSummary
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class RegressionViolation:
|
|
12
|
+
scenario: str
|
|
13
|
+
variant: str
|
|
14
|
+
metric: str
|
|
15
|
+
baseline: float | None
|
|
16
|
+
current: float | None
|
|
17
|
+
delta_pct: float | None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
METRICS = (
|
|
21
|
+
("startup_avg_ms", "startup_avg_ms"),
|
|
22
|
+
("startup_p95_ms", "startup_p95_ms"),
|
|
23
|
+
("image_mb_avg", "image_mb_avg"),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _metric_value(summary: VariantSummary, field: str) -> float | None:
|
|
28
|
+
return getattr(summary, field)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _pct_change(current: float, baseline: float) -> float:
|
|
32
|
+
return ((current - baseline) / baseline) * 100.0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_summaries(path: Path) -> list[VariantSummary]:
|
|
36
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
37
|
+
if not isinstance(payload, list):
|
|
38
|
+
raise ValueError("baseline report must be a JSON list")
|
|
39
|
+
|
|
40
|
+
summaries: list[VariantSummary] = []
|
|
41
|
+
for item in payload:
|
|
42
|
+
if not isinstance(item, dict):
|
|
43
|
+
raise ValueError("baseline report must contain JSON objects")
|
|
44
|
+
summaries.append(
|
|
45
|
+
VariantSummary(
|
|
46
|
+
scenario=str(item.get("scenario", "")),
|
|
47
|
+
variant=str(item.get("variant", "")),
|
|
48
|
+
runs=int(item.get("runs", 0)),
|
|
49
|
+
build_avg_ms=item.get("build_avg_ms"),
|
|
50
|
+
build_stddev_ms=item.get("build_stddev_ms"),
|
|
51
|
+
build_ci95_low_ms=item.get("build_ci95_low_ms"),
|
|
52
|
+
build_ci95_high_ms=item.get("build_ci95_high_ms"),
|
|
53
|
+
startup_avg_ms=item.get("startup_avg_ms"),
|
|
54
|
+
startup_p95_ms=item.get("startup_p95_ms"),
|
|
55
|
+
startup_p99_ms=item.get("startup_p99_ms"),
|
|
56
|
+
startup_stddev_ms=item.get("startup_stddev_ms"),
|
|
57
|
+
startup_ci95_low_ms=item.get("startup_ci95_low_ms"),
|
|
58
|
+
startup_ci95_high_ms=item.get("startup_ci95_high_ms"),
|
|
59
|
+
gc_pause_ms_avg=item.get("gc_pause_ms_avg"),
|
|
60
|
+
alloc_mb_avg=item.get("alloc_mb_avg"),
|
|
61
|
+
startup_phase_boot_ms_avg=item.get("startup_phase_boot_ms_avg"),
|
|
62
|
+
startup_phase_context_ms_avg=item.get("startup_phase_context_ms_avg"),
|
|
63
|
+
startup_phase_web_server_ms_avg=item.get("startup_phase_web_server_ms_avg"),
|
|
64
|
+
startup_phase_total_ms_avg=item.get("startup_phase_total_ms_avg"),
|
|
65
|
+
image_mb_avg=item.get("image_mb_avg"),
|
|
66
|
+
success_rate_pct=float(item.get("success_rate_pct", 0.0)),
|
|
67
|
+
rss_mb_avg=item.get("rss_mb_avg"),
|
|
68
|
+
cpu_pct_avg=item.get("cpu_pct_avg"),
|
|
69
|
+
host=item.get("host"),
|
|
70
|
+
docker_version=item.get("docker_version"),
|
|
71
|
+
run_profile=item.get("run_profile"),
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
return summaries
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def detect_regressions(
|
|
78
|
+
baseline: list[VariantSummary], current: list[VariantSummary], threshold_pct: float
|
|
79
|
+
) -> list[RegressionViolation]:
|
|
80
|
+
baseline_map = {(summary.scenario, summary.variant): summary for summary in baseline}
|
|
81
|
+
current_map = {(summary.scenario, summary.variant): summary for summary in current}
|
|
82
|
+
violations: list[RegressionViolation] = []
|
|
83
|
+
|
|
84
|
+
for key, baseline_summary in sorted(baseline_map.items()):
|
|
85
|
+
current_summary = current_map.get(key)
|
|
86
|
+
if current_summary is None:
|
|
87
|
+
continue
|
|
88
|
+
for metric_name, field in METRICS:
|
|
89
|
+
baseline_value = _metric_value(baseline_summary, field)
|
|
90
|
+
current_value = _metric_value(current_summary, field)
|
|
91
|
+
if baseline_value is None and current_value is None:
|
|
92
|
+
continue
|
|
93
|
+
if baseline_value is None and current_value is not None:
|
|
94
|
+
continue
|
|
95
|
+
if baseline_value is not None and current_value is None:
|
|
96
|
+
violations.append(
|
|
97
|
+
RegressionViolation(
|
|
98
|
+
scenario=baseline_summary.scenario,
|
|
99
|
+
variant=baseline_summary.variant,
|
|
100
|
+
metric=metric_name,
|
|
101
|
+
baseline=baseline_value,
|
|
102
|
+
current=None,
|
|
103
|
+
delta_pct=None,
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
continue
|
|
107
|
+
assert baseline_value is not None
|
|
108
|
+
assert current_value is not None
|
|
109
|
+
delta_pct = _pct_change(current_value, baseline_value)
|
|
110
|
+
if delta_pct > threshold_pct:
|
|
111
|
+
violations.append(
|
|
112
|
+
RegressionViolation(
|
|
113
|
+
scenario=baseline_summary.scenario,
|
|
114
|
+
variant=baseline_summary.variant,
|
|
115
|
+
metric=metric_name,
|
|
116
|
+
baseline=baseline_value,
|
|
117
|
+
current=current_value,
|
|
118
|
+
delta_pct=delta_pct,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
return violations
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def format_regression_table(violations: list[RegressionViolation]) -> str:
|
|
125
|
+
lines = [
|
|
126
|
+
"| Scenario | Variant | Metric | Baseline | Current | Δ% |",
|
|
127
|
+
"|---|---|---|---:|---:|---:|",
|
|
128
|
+
]
|
|
129
|
+
for violation in violations:
|
|
130
|
+
baseline = "-" if violation.baseline is None else f"{violation.baseline:.2f}"
|
|
131
|
+
current = "-" if violation.current is None else f"{violation.current:.2f}"
|
|
132
|
+
delta = "-" if violation.delta_pct is None else f"{violation.delta_pct:+.1f}%"
|
|
133
|
+
lines.append(
|
|
134
|
+
f"| {violation.scenario} | {violation.variant} | {violation.metric} | {baseline} | {current} | {delta} |"
|
|
135
|
+
)
|
|
136
|
+
return "\n".join(lines)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def format_regression_json(violations: list[RegressionViolation]) -> str:
|
|
140
|
+
payload = [
|
|
141
|
+
{
|
|
142
|
+
"scenario": violation.scenario,
|
|
143
|
+
"variant": violation.variant,
|
|
144
|
+
"metric": violation.metric,
|
|
145
|
+
"baseline": violation.baseline,
|
|
146
|
+
"current": violation.current,
|
|
147
|
+
"delta_pct": violation.delta_pct,
|
|
148
|
+
}
|
|
149
|
+
for violation in violations
|
|
150
|
+
]
|
|
151
|
+
return json.dumps(payload, indent=2, sort_keys=True)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..analyze import format_json, format_table, summarize_csv
|
|
7
|
+
from ..compare import compare_summaries, format_delta_json, format_delta_table
|
|
8
|
+
from ..regression import RegressionViolation, detect_regressions, load_summaries
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def resolve_path(project_root: Path, raw_path: str) -> Path:
|
|
12
|
+
path = Path(raw_path)
|
|
13
|
+
if not path.is_absolute():
|
|
14
|
+
path = project_root / path
|
|
15
|
+
return path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def validate_reproducibility_with_legacy(
|
|
19
|
+
use_legacy_scripts: bool,
|
|
20
|
+
cpuset_cpus: str | None,
|
|
21
|
+
memory_limit: str | None,
|
|
22
|
+
warmup_runs: int,
|
|
23
|
+
max_workers: int,
|
|
24
|
+
normalized_runtime: bool,
|
|
25
|
+
) -> None:
|
|
26
|
+
if use_legacy_scripts and any([cpuset_cpus, memory_limit, warmup_runs > 0, max_workers > 1, normalized_runtime]):
|
|
27
|
+
raise ValueError("benchmark reproducibility/concurrency controls require the internal benchmark runner")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def render_comparison(
|
|
31
|
+
project_root: Path,
|
|
32
|
+
raw_csv: str,
|
|
33
|
+
baseline_variant: str,
|
|
34
|
+
output_format: str,
|
|
35
|
+
scenario: str | None,
|
|
36
|
+
) -> str:
|
|
37
|
+
csv_path = resolve_path(project_root, raw_csv)
|
|
38
|
+
if not csv_path.exists():
|
|
39
|
+
raise ValueError(f"missing CSV file: {csv_path}")
|
|
40
|
+
summaries = summarize_csv(csv_path, scenario=scenario)
|
|
41
|
+
deltas = compare_summaries(baseline_variant, summaries)
|
|
42
|
+
return format_delta_json(deltas) if output_format == "json" else format_delta_table(deltas)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class AnalyzeOutcome:
|
|
47
|
+
rendered: str
|
|
48
|
+
output_destination: Path | None
|
|
49
|
+
success_rate_violations: tuple[str, ...]
|
|
50
|
+
regression_violations: tuple[RegressionViolation, ...]
|
|
51
|
+
baseline_missing: Path | None
|
|
52
|
+
baseline_path_used: Path | None
|
|
53
|
+
regression_threshold_pct: float
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def analyze_csv(
|
|
57
|
+
project_root: Path,
|
|
58
|
+
raw_csv: str,
|
|
59
|
+
output_format: str,
|
|
60
|
+
scenario: str | None,
|
|
61
|
+
variant: str | None,
|
|
62
|
+
output_path: str | None,
|
|
63
|
+
fail_on_success_rate_below: float | None,
|
|
64
|
+
baseline_path: str | None,
|
|
65
|
+
fail_on_regression_above: float | None,
|
|
66
|
+
) -> AnalyzeOutcome:
|
|
67
|
+
csv_path = resolve_path(project_root, raw_csv)
|
|
68
|
+
if not csv_path.exists():
|
|
69
|
+
raise ValueError(f"missing CSV file: {csv_path}")
|
|
70
|
+
if fail_on_success_rate_below is not None and not 0.0 <= fail_on_success_rate_below <= 100.0:
|
|
71
|
+
raise ValueError("--fail-on-success-rate-below must be between 0 and 100")
|
|
72
|
+
|
|
73
|
+
summaries = summarize_csv(csv_path, scenario=scenario, variant=variant)
|
|
74
|
+
if not summaries:
|
|
75
|
+
return AnalyzeOutcome(
|
|
76
|
+
rendered="No rows matched the provided filters.",
|
|
77
|
+
output_destination=None,
|
|
78
|
+
success_rate_violations=(),
|
|
79
|
+
regression_violations=(),
|
|
80
|
+
baseline_missing=None,
|
|
81
|
+
baseline_path_used=None,
|
|
82
|
+
regression_threshold_pct=fail_on_regression_above or 20.0,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
rendered = format_json(summaries) if output_format == "json" else format_table(summaries)
|
|
86
|
+
destination: Path | None = None
|
|
87
|
+
if output_path:
|
|
88
|
+
destination = resolve_path(project_root, output_path)
|
|
89
|
+
|
|
90
|
+
violations: list[str] = []
|
|
91
|
+
if fail_on_success_rate_below is not None:
|
|
92
|
+
violations = [
|
|
93
|
+
f"success_rate below threshold for {summary.scenario}/{summary.variant}: "
|
|
94
|
+
f"{summary.success_rate_pct:.1f}% < {fail_on_success_rate_below:.1f}%"
|
|
95
|
+
for summary in summaries
|
|
96
|
+
if summary.success_rate_pct < fail_on_success_rate_below
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
baseline_missing: Path | None = None
|
|
100
|
+
baseline_path_used: Path | None = None
|
|
101
|
+
regression_violations: list[RegressionViolation] = []
|
|
102
|
+
threshold = fail_on_regression_above or 20.0
|
|
103
|
+
if baseline_path is not None:
|
|
104
|
+
baseline_file = resolve_path(project_root, baseline_path)
|
|
105
|
+
baseline_path_used = baseline_file
|
|
106
|
+
if not baseline_file.exists():
|
|
107
|
+
baseline_missing = baseline_file
|
|
108
|
+
else:
|
|
109
|
+
baseline_summaries = load_summaries(baseline_file)
|
|
110
|
+
regression_violations = detect_regressions(
|
|
111
|
+
baseline=baseline_summaries,
|
|
112
|
+
current=summaries,
|
|
113
|
+
threshold_pct=threshold,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return AnalyzeOutcome(
|
|
117
|
+
rendered=rendered,
|
|
118
|
+
output_destination=destination,
|
|
119
|
+
success_rate_violations=tuple(violations),
|
|
120
|
+
regression_violations=tuple(regression_violations),
|
|
121
|
+
baseline_missing=baseline_missing,
|
|
122
|
+
baseline_path_used=baseline_path_used,
|
|
123
|
+
regression_threshold_pct=threshold,
|
|
124
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..dockerfile import DockerfileOptions, build_dockerfile, explain_dockerfile_text
|
|
8
|
+
from ..plugins import apply_dockerfile_mutators
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def resolve_path(project_root: Path, raw_path: str) -> Path:
|
|
12
|
+
path = Path(raw_path)
|
|
13
|
+
if not path.is_absolute():
|
|
14
|
+
path = project_root / path
|
|
15
|
+
return path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_must_have_modules(project_root: Path, must_have_modules_file: str | None) -> tuple[str, ...]:
|
|
19
|
+
if not must_have_modules_file:
|
|
20
|
+
return ()
|
|
21
|
+
modules_path = resolve_path(project_root, must_have_modules_file)
|
|
22
|
+
if not modules_path.exists():
|
|
23
|
+
raise ValueError(f"missing must-have modules file: {modules_path}")
|
|
24
|
+
parsed: list[str] = []
|
|
25
|
+
seen: set[str] = set()
|
|
26
|
+
for line in modules_path.read_text(encoding="utf-8").splitlines():
|
|
27
|
+
entry = line.split("#", 1)[0].strip()
|
|
28
|
+
if not entry:
|
|
29
|
+
continue
|
|
30
|
+
for token in [part.strip() for part in entry.split(",")]:
|
|
31
|
+
if not token:
|
|
32
|
+
continue
|
|
33
|
+
if not re.fullmatch(r"[A-Za-z0-9._-]+", token):
|
|
34
|
+
raise ValueError(f"invalid module name in {modules_path}: {token}")
|
|
35
|
+
if token not in seen:
|
|
36
|
+
parsed.append(token)
|
|
37
|
+
seen.add(token)
|
|
38
|
+
return tuple(parsed)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def generate_dockerfile(
|
|
42
|
+
project_root: Path,
|
|
43
|
+
output_path: str,
|
|
44
|
+
build_tool: str,
|
|
45
|
+
java_version: int,
|
|
46
|
+
must_have_modules_file: str | None,
|
|
47
|
+
) -> GeneratedDockerfile:
|
|
48
|
+
must_have_modules = parse_must_have_modules(project_root, must_have_modules_file)
|
|
49
|
+
options = DockerfileOptions(
|
|
50
|
+
build_tool=build_tool,
|
|
51
|
+
java_version=java_version,
|
|
52
|
+
must_have_modules=must_have_modules,
|
|
53
|
+
)
|
|
54
|
+
generated = apply_dockerfile_mutators(
|
|
55
|
+
dockerfile_text=build_dockerfile(options),
|
|
56
|
+
options=options,
|
|
57
|
+
)
|
|
58
|
+
destination = resolve_path(project_root, output_path)
|
|
59
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
destination.write_text(generated.dockerfile_text, encoding="utf-8")
|
|
61
|
+
return GeneratedDockerfile(path=destination, plugin_warnings=generated.warnings)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class GeneratedDockerfile:
|
|
66
|
+
path: Path
|
|
67
|
+
plugin_warnings: tuple[str, ...]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def explain_dockerfile(project_root: Path, dockerfile_path: str) -> dict[str, object]:
|
|
71
|
+
path = resolve_path(project_root, dockerfile_path)
|
|
72
|
+
if not path.exists():
|
|
73
|
+
raise ValueError(f"missing Dockerfile: {path}")
|
|
74
|
+
payload = dict(explain_dockerfile_text(path.read_text(encoding="utf-8")))
|
|
75
|
+
payload["path"] = str(path)
|
|
76
|
+
return payload
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..config import render_default_config, write_default_config
|
|
7
|
+
from ..project_detect import inspect_project, inspect_project_details
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_project_info(project_root: Path, build_tool: str | None):
|
|
11
|
+
return inspect_project(project_root, build_tool)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_project_details(project_root: Path, build_tool: str | None):
|
|
15
|
+
return inspect_project_details(project_root, build_tool)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class InitConfigResult:
|
|
20
|
+
rendered: str | None
|
|
21
|
+
written_path: Path | None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def prepare_default_config(
|
|
25
|
+
project_root: Path,
|
|
26
|
+
build_tool: str | None,
|
|
27
|
+
config_path: Path,
|
|
28
|
+
profile: str,
|
|
29
|
+
force: bool,
|
|
30
|
+
print_only: bool,
|
|
31
|
+
) -> InitConfigResult:
|
|
32
|
+
info = inspect_project(project_root, build_tool)
|
|
33
|
+
if print_only:
|
|
34
|
+
return InitConfigResult(rendered=render_default_config(build_tool=info.build_tool, profile=profile), written_path=None)
|
|
35
|
+
write_default_config(path=config_path, build_tool=info.build_tool, profile=profile, force=force)
|
|
36
|
+
return InitConfigResult(rendered=None, written_path=config_path)
|
|
37
|
+
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: springdocker
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: CLI for Spring Boot Dockerfile and benchmark workflows (Maven/Gradle).
|
|
5
|
+
Author: springdocker contributors
|
|
6
|
+
License: UNLICENSED
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
13
|
+
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
14
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# springdocker CLI
|
|
17
|
+
|
|
18
|
+
CLI for Spring Boot Dockerfile and benchmark workflows across Maven and Gradle projects.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
### Local editable
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python3 -m pip install -e .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### pipx
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pipx install springdocker
|
|
32
|
+
springdocker --help
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Upgrade:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pipx upgrade springdocker
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### uv
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv tool install springdocker
|
|
45
|
+
uv tool upgrade springdocker
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick usage
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
springdocker init --project-root samples/java-spring-docker --build-tool maven --profile quick
|
|
52
|
+
springdocker doctor --project-root samples/java-spring-docker
|
|
53
|
+
springdocker inspect --project-root samples/java-spring-docker --format json
|
|
54
|
+
springdocker explain --project-root samples/java-spring-docker Dockerfile.generated --format json
|
|
55
|
+
springdocker benchmark compare --project-root samples/java-spring-docker benchmarks/03-custom-jre-jlink/results/raw.csv --baseline-variant with-jlink-runtime --format json
|
|
56
|
+
springdocker dockerfile generate --project-root samples/java-spring-docker --output Dockerfile.generated
|
|
57
|
+
springdocker benchmark generate --project-root samples/java-spring-docker --java-version 25
|
|
58
|
+
springdocker benchmark run --project-root samples/java-spring-docker --profile quick --runner-arg --skip-native
|
|
59
|
+
springdocker benchmark analyze --project-root samples/java-spring-docker benchmarks/04-jep483-aot-cache/results/raw.csv --format table
|
|
60
|
+
springdocker benchmark analyze --project-root samples/java-spring-docker benchmarks/04-jep483-aot-cache/results/raw.csv --format json --output benchmarks/04-jep483-aot-cache/results/summary.json
|
|
61
|
+
springdocker benchmark analyze --project-root samples/java-spring-docker benchmarks/04-jep483-aot-cache/results/raw.csv --fail-on-success-rate-below 95
|
|
62
|
+
springdocker benchmark analyze --project-root samples/java-spring-docker benchmarks/04-jep483-aot-cache/results/raw.csv --baseline benchmarks/04-jep483-aot-cache/results/baseline.json --fail-on-regression-above 20
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Config file (`.springdocker.toml`)
|
|
66
|
+
|
|
67
|
+
All command resolvers use precedence:
|
|
68
|
+
|
|
69
|
+
1. CLI flags
|
|
70
|
+
2. `.springdocker.toml`
|
|
71
|
+
3. defaults
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
|
|
75
|
+
```toml
|
|
76
|
+
[project]
|
|
77
|
+
build_tool = "maven"
|
|
78
|
+
|
|
79
|
+
[doctor]
|
|
80
|
+
build_tool = "maven"
|
|
81
|
+
|
|
82
|
+
[dockerfile]
|
|
83
|
+
output = "Dockerfile.generated"
|
|
84
|
+
java_version = 25
|
|
85
|
+
must_have_modules_file = "must-have.txt"
|
|
86
|
+
legacy_scripts = false
|
|
87
|
+
wizard_args = []
|
|
88
|
+
|
|
89
|
+
[benchmark.generate]
|
|
90
|
+
java_version = 25
|
|
91
|
+
legacy_scripts = false
|
|
92
|
+
|
|
93
|
+
[benchmark.run]
|
|
94
|
+
profile = "quick"
|
|
95
|
+
runner_args = ["--skip-native"]
|
|
96
|
+
cpuset_cpus = "0-1"
|
|
97
|
+
memory_limit = "2g"
|
|
98
|
+
warmup_runs = 1
|
|
99
|
+
max_workers = 1
|
|
100
|
+
normalized_runtime = true
|
|
101
|
+
legacy_scripts = false
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
When `dockerfile.must_have_modules_file` is set, springdocker reads modules from that file
|
|
105
|
+
(`must-have.txt` style, one module per line, `#` comments allowed) and injects them into
|
|
106
|
+
the jlink module list for reflection/dynamic-loading edge cases.
|
|
107
|
+
|
|
108
|
+
Create template config:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
springdocker init --project-root samples/java-spring-docker --build-tool gradle
|
|
112
|
+
springdocker init --project-root samples/java-spring-docker --build-tool gradle --profile full --print
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Legacy compatibility mode
|
|
116
|
+
|
|
117
|
+
Main command paths are internal and do not require project script files.
|
|
118
|
+
|
|
119
|
+
To force script wrappers for compatibility:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
springdocker dockerfile generate --use-legacy-scripts ...
|
|
123
|
+
springdocker benchmark generate --use-legacy-scripts ...
|
|
124
|
+
springdocker benchmark run --use-legacy-scripts ...
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
or set:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
export SPRINGDOCKER_LEGACY_SCRIPTS=1
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Inspect command
|
|
134
|
+
|
|
135
|
+
`springdocker inspect` prints static metadata about the target project:
|
|
136
|
+
|
|
137
|
+
- detected build tool
|
|
138
|
+
- Spring Boot version when present
|
|
139
|
+
- Java version when present
|
|
140
|
+
- direct dependency coordinates
|
|
141
|
+
- generated Dockerfile artifacts in the project root
|
|
142
|
+
- basic runtime compatibility guidance
|
|
143
|
+
|
|
144
|
+
Use `--format json` for machine-readable output.
|
|
145
|
+
|
|
146
|
+
## Explain command
|
|
147
|
+
|
|
148
|
+
`springdocker explain` reads a springdocker-generated Dockerfile and describes the optimizations it contains:
|
|
149
|
+
|
|
150
|
+
- multi-stage layout
|
|
151
|
+
- BuildKit cache usage
|
|
152
|
+
- jlink runtime stage
|
|
153
|
+
- non-root runtime
|
|
154
|
+
- tuned JVM flags
|
|
155
|
+
- curated must-have modules
|
|
156
|
+
|
|
157
|
+
Use `--format json` when you want stable structured output.
|
|
158
|
+
|
|
159
|
+
## Security hardening
|
|
160
|
+
|
|
161
|
+
See `docs/security-hardening.md` for the runtime hardening defaults and recommended `docker run` flags.
|
|
162
|
+
|
|
163
|
+
## Binary distribution
|
|
164
|
+
|
|
165
|
+
See `docs/distribution.md` for packaging notes and sample Homebrew, Scoop, standalone binary, and Docker runtime artifacts.
|
|
166
|
+
|
|
167
|
+
## Multi-architecture builds
|
|
168
|
+
|
|
169
|
+
See `docs/multiarch.md` for the Buildx-friendly Dockerfile output and example multi-arch build command.
|
|
170
|
+
|
|
171
|
+
## Compare command
|
|
172
|
+
|
|
173
|
+
`springdocker benchmark compare` compares each variant against a required baseline variant and reports deltas.
|
|
174
|
+
|
|
175
|
+
- `--baseline-variant` selects the variant to compare against.
|
|
176
|
+
- `--scenario` narrows the CSV to one scenario.
|
|
177
|
+
- `--format json` produces machine-readable deltas.
|
|
178
|
+
|
|
179
|
+
## Benchmark run reproducibility
|
|
180
|
+
|
|
181
|
+
`springdocker benchmark run` supports deterministic benchmark controls for local or CI runs:
|
|
182
|
+
|
|
183
|
+
- `--cpuset-cpus` pins benchmark containers to specific CPUs.
|
|
184
|
+
- `--memory` caps container memory.
|
|
185
|
+
- `--warmup-runs` executes discarded warmup probes before recording results.
|
|
186
|
+
- `--max-workers` runs standard scenarios concurrently with controlled worker count.
|
|
187
|
+
- `--normalized-runtime` applies read-only, no-new-privileges, and tmpfs isolation.
|
|
188
|
+
|
|
189
|
+
These settings can also come from `[benchmark.run]` in `.springdocker.toml`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
springdocker/__init__.py,sha256=33B3aRpZXrsoZ7GLyStF7MzVgIHt47MlR_EhxEjQZ9E,272
|
|
2
|
+
springdocker/analyze.py,sha256=z_j29739dZF_R4T7jwakTWgOMfuelQEwkrkVC0jDDQw,11992
|
|
3
|
+
springdocker/cli.py,sha256=3uVF0NByriJLPwvVVrxD-e9XsZmOYT0sa7EhFzD3RcE,12315
|
|
4
|
+
springdocker/commands.py,sha256=QBvyjPAt7y7iAVA226dBNxgJNZh9-WWvRPMkZ4JDcPA,12397
|
|
5
|
+
springdocker/compare.py,sha256=dK-h7hKEJoI8GlKmd8AZAV5--5SlZ7_C_kW0ighXooY,4931
|
|
6
|
+
springdocker/config.py,sha256=t9n5a8FUMETHO0r_udeYHUvNODAlxu1xXJN4l6I34cA,14660
|
|
7
|
+
springdocker/dockerfile.py,sha256=9Ov5vigs31AFe8LZOT_eyXRw1JSIVt6mwR2CDoqS49M,12504
|
|
8
|
+
springdocker/errors.py,sha256=nwBquBupwIB9VTWeyrtDlKXVXqOmvpDe6-oEUsFPfo8,275
|
|
9
|
+
springdocker/plugins.py,sha256=7uGnMRl4Dah_isi8SyLfELlUmDh4HWfNG6J5ky77mFg,2857
|
|
10
|
+
springdocker/project_detect.py,sha256=cih0zh9ZVZgtzfcXNeJZ7EwA7GDSkCZbI7EqSI1nr-8,9189
|
|
11
|
+
springdocker/regression.py,sha256=gOaPs0Fabs_-RGlp7xECBpXYL-jGNoSLKLCftK2t-Oc,6003
|
|
12
|
+
springdocker/benchmarks/__init__.py,sha256=WiQrmreDaPt7F3PhIkZQKaeYhegrolZEaL589su99sI,51
|
|
13
|
+
springdocker/benchmarks/generate.py,sha256=pHw5QrusKBqncgSdU1mCIoiaWS5KpiXKqdkZVW6oQmA,4069
|
|
14
|
+
springdocker/benchmarks/runner.py,sha256=AjEErI6ysN3aDXAEfM7wEXdP8gPp-CupVvCq_FboxTU,12210
|
|
15
|
+
springdocker/services/__init__.py,sha256=PHh4jqD7i0aL8W8hXThczKcT4xsNZiXanBxOH5IOW5I,36
|
|
16
|
+
springdocker/services/benchmark_service.py,sha256=Skj9x5w6Ix6rGVuTJWmeWPhx7qtvzI3SdYW5h2ElMsI,4480
|
|
17
|
+
springdocker/services/dockerfile_service.py,sha256=S1z1EkKePTqA96zaTRfAb7z59Z0BkrQzHah3MASPPzU,2630
|
|
18
|
+
springdocker/services/project_service.py,sha256=A0iUVyNvcHRDdq2khSvLk_MU7CSHuGdn0PRfiFKFUFA,1147
|
|
19
|
+
springdocker-1.0.1.dist-info/METADATA,sha256=EK3D1tOBmHU5eQijJRqa8cO8Az1WWM-EwN12YWhMUNI,5818
|
|
20
|
+
springdocker-1.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
21
|
+
springdocker-1.0.1.dist-info/entry_points.txt,sha256=KthNZ57ZMW6pFk6BMXuKpJYp6lOjHAebaDv5nSkvIOU,55
|
|
22
|
+
springdocker-1.0.1.dist-info/top_level.txt,sha256=7HhuHYUI1oB5IcfzRfIoX4D6jld22B-NWtvDPcOnneU,13
|
|
23
|
+
springdocker-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
springdocker
|