@leejungkiin/awkit 1.4.0 → 1.4.3

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 (119) hide show
  1. package/bin/awk.js +458 -7
  2. package/bin/claude-generators.js +122 -0
  3. package/core/AGENTS.md +16 -0
  4. package/core/CLAUDE.md +155 -0
  5. package/core/GEMINI.md +44 -9
  6. package/package.json +1 -1
  7. package/skills/ai-sprite-maker/SKILL.md +81 -0
  8. package/skills/ai-sprite-maker/scripts/animate_sprite.py +102 -0
  9. package/skills/ai-sprite-maker/scripts/process_sprites.py +140 -0
  10. package/skills/code-review/SKILL.md +21 -33
  11. package/skills/lucylab-tts/SKILL.md +64 -0
  12. package/skills/lucylab-tts/resources/voices_library.json +908 -0
  13. package/skills/lucylab-tts/scripts/.env +1 -0
  14. package/skills/lucylab-tts/scripts/lucylab_tts.py +506 -0
  15. package/skills/orchestrator/SKILL.md +5 -0
  16. package/skills/short-maker/SKILL.md +150 -0
  17. package/skills/short-maker/_backup/storyboard.html +106 -0
  18. package/skills/short-maker/_backup/video_mixer.py +296 -0
  19. package/skills/short-maker/outputs/fitbite-promo/background.jpg +0 -0
  20. package/skills/short-maker/outputs/fitbite-promo/final/promo-final.mp4 +0 -0
  21. package/skills/short-maker/outputs/fitbite-promo/script.md +19 -0
  22. package/skills/short-maker/outputs/fitbite-promo/segments/scene-01.mp4 +0 -0
  23. package/skills/short-maker/outputs/fitbite-promo/segments/scene-02.mp4 +0 -0
  24. package/skills/short-maker/outputs/fitbite-promo/segments/scene-03.mp4 +0 -0
  25. package/skills/short-maker/outputs/fitbite-promo/segments/scene-04.mp4 +0 -0
  26. package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-01.png +0 -0
  27. package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-02.png +0 -0
  28. package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-03.png +0 -0
  29. package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-04.png +0 -0
  30. package/skills/short-maker/outputs/fitbite-promo/storyboard.html +133 -0
  31. package/skills/short-maker/outputs/fitbite-promo/storyboard.json +38 -0
  32. package/skills/short-maker/outputs/fitbite-promo/temp/merged_chroma.mp4 +0 -0
  33. package/skills/short-maker/outputs/fitbite-promo/temp/merged_crossfaded.mp4 +0 -0
  34. package/skills/short-maker/outputs/fitbite-promo/temp/ready_00.mp4 +0 -0
  35. package/skills/short-maker/outputs/fitbite-promo/temp/ready_01.mp4 +0 -0
  36. package/skills/short-maker/outputs/fitbite-promo/temp/ready_02.mp4 +0 -0
  37. package/skills/short-maker/outputs/fitbite-promo/temp/ready_03.mp4 +0 -0
  38. package/skills/short-maker/outputs/fitbite-promo/tts/manifest.json +31 -0
  39. package/skills/short-maker/outputs/fitbite-promo/tts/scene-01.wav +0 -0
  40. package/skills/short-maker/outputs/fitbite-promo/tts/scene-02.wav +0 -0
  41. package/skills/short-maker/outputs/fitbite-promo/tts/scene-03.wav +0 -0
  42. package/skills/short-maker/outputs/fitbite-promo/tts/scene-04.wav +0 -0
  43. package/skills/short-maker/outputs/fitbite-promo/tts_script.txt +11 -0
  44. package/skills/short-maker/scripts/google-flow-cli/.project-identity +41 -0
  45. package/skills/short-maker/scripts/google-flow-cli/.trae/rules/project_rules.md +52 -0
  46. package/skills/short-maker/scripts/google-flow-cli/CODEBASE.md +67 -0
  47. package/skills/short-maker/scripts/google-flow-cli/GoogleFlowCli.code-workspace +29 -0
  48. package/skills/short-maker/scripts/google-flow-cli/README.md +168 -0
  49. package/skills/short-maker/scripts/google-flow-cli/docs/specs/PROJECT.md +12 -0
  50. package/skills/short-maker/scripts/google-flow-cli/docs/specs/REQUIREMENTS.md +22 -0
  51. package/skills/short-maker/scripts/google-flow-cli/docs/specs/ROADMAP.md +16 -0
  52. package/skills/short-maker/scripts/google-flow-cli/docs/specs/TECH-SPEC.md +13 -0
  53. package/skills/short-maker/scripts/google-flow-cli/gflow/__init__.py +3 -0
  54. package/skills/short-maker/scripts/google-flow-cli/gflow/api/__init__.py +19 -0
  55. package/skills/short-maker/scripts/google-flow-cli/gflow/api/client.py +1921 -0
  56. package/skills/short-maker/scripts/google-flow-cli/gflow/api/models.py +64 -0
  57. package/skills/short-maker/scripts/google-flow-cli/gflow/api/rpc_ids.py +98 -0
  58. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/__init__.py +15 -0
  59. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/browser_auth.py +692 -0
  60. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/humanizer.py +417 -0
  61. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/proxy_ext.py +120 -0
  62. package/skills/short-maker/scripts/google-flow-cli/gflow/auth/recaptcha.py +482 -0
  63. package/skills/short-maker/scripts/google-flow-cli/gflow/batchexecute/__init__.py +5 -0
  64. package/skills/short-maker/scripts/google-flow-cli/gflow/batchexecute/client.py +414 -0
  65. package/skills/short-maker/scripts/google-flow-cli/gflow/cli/__init__.py +1 -0
  66. package/skills/short-maker/scripts/google-flow-cli/gflow/cli/main.py +1075 -0
  67. package/skills/short-maker/scripts/google-flow-cli/pyproject.toml +36 -0
  68. package/skills/short-maker/scripts/google-flow-cli/script.txt +22 -0
  69. package/skills/short-maker/scripts/google-flow-cli/tests/__init__.py +0 -0
  70. package/skills/short-maker/scripts/google-flow-cli/tests/test_batchexecute.py +113 -0
  71. package/skills/short-maker/scripts/google-flow-cli/tests/test_client.py +190 -0
  72. package/skills/short-maker/templates/aida_script.md +40 -0
  73. package/skills/short-maker/templates/mimic_analyzer.md +29 -0
  74. package/skills/single-flow-task-execution/SKILL.md +9 -6
  75. package/skills/skill-creator/SKILL.md +44 -0
  76. package/skills/spm-build-analysis/SKILL.md +92 -0
  77. package/skills/spm-build-analysis/references/build-optimization-sources.md +155 -0
  78. package/skills/spm-build-analysis/references/recommendation-format.md +85 -0
  79. package/skills/spm-build-analysis/references/spm-analysis-checks.md +105 -0
  80. package/skills/spm-build-analysis/scripts/check_spm_pins.py +118 -0
  81. package/skills/symphony-enforcer/SKILL.md +51 -83
  82. package/skills/symphony-orchestrator/SKILL.md +1 -1
  83. package/skills/trello-sync/SKILL.md +27 -28
  84. package/skills/verification-gate/SKILL.md +13 -2
  85. package/skills/xcode-build-benchmark/SKILL.md +88 -0
  86. package/skills/xcode-build-benchmark/references/benchmark-artifacts.md +94 -0
  87. package/skills/xcode-build-benchmark/references/benchmarking-workflow.md +67 -0
  88. package/skills/xcode-build-benchmark/schemas/build-benchmark.schema.json +230 -0
  89. package/skills/xcode-build-benchmark/scripts/benchmark_builds.py +308 -0
  90. package/skills/xcode-build-fixer/SKILL.md +218 -0
  91. package/skills/xcode-build-fixer/references/build-settings-best-practices.md +216 -0
  92. package/skills/xcode-build-fixer/references/fix-patterns.md +290 -0
  93. package/skills/xcode-build-fixer/references/recommendation-format.md +85 -0
  94. package/skills/xcode-build-fixer/scripts/benchmark_builds.py +308 -0
  95. package/skills/xcode-build-orchestrator/SKILL.md +156 -0
  96. package/skills/xcode-build-orchestrator/references/benchmark-artifacts.md +94 -0
  97. package/skills/xcode-build-orchestrator/references/build-settings-best-practices.md +216 -0
  98. package/skills/xcode-build-orchestrator/references/orchestration-report-template.md +143 -0
  99. package/skills/xcode-build-orchestrator/references/recommendation-format.md +85 -0
  100. package/skills/xcode-build-orchestrator/scripts/benchmark_builds.py +308 -0
  101. package/skills/xcode-build-orchestrator/scripts/diagnose_compilation.py +273 -0
  102. package/skills/xcode-build-orchestrator/scripts/generate_optimization_report.py +533 -0
  103. package/skills/xcode-compilation-analyzer/SKILL.md +89 -0
  104. package/skills/xcode-compilation-analyzer/references/build-optimization-sources.md +155 -0
  105. package/skills/xcode-compilation-analyzer/references/code-compilation-checks.md +106 -0
  106. package/skills/xcode-compilation-analyzer/references/recommendation-format.md +85 -0
  107. package/skills/xcode-compilation-analyzer/scripts/diagnose_compilation.py +273 -0
  108. package/skills/xcode-project-analyzer/SKILL.md +76 -0
  109. package/skills/xcode-project-analyzer/references/build-optimization-sources.md +155 -0
  110. package/skills/xcode-project-analyzer/references/build-settings-best-practices.md +216 -0
  111. package/skills/xcode-project-analyzer/references/project-audit-checks.md +101 -0
  112. package/skills/xcode-project-analyzer/references/recommendation-format.md +85 -0
  113. package/templates/project-identity/android.json +0 -10
  114. package/templates/project-identity/backend-nestjs.json +0 -10
  115. package/templates/project-identity/expo.json +0 -10
  116. package/templates/project-identity/ios.json +0 -10
  117. package/templates/project-identity/web-nextjs.json +0 -10
  118. package/workflows/_uncategorized/ship-to-code.md +85 -0
  119. package/workflows/context/codebase-sync.md +10 -87
@@ -0,0 +1,230 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Xcode Build Benchmark Artifact",
4
+ "type": "object",
5
+ "required": [
6
+ "schema_version",
7
+ "created_at",
8
+ "build",
9
+ "runs",
10
+ "summary"
11
+ ],
12
+ "properties": {
13
+ "schema_version": {
14
+ "type": "string",
15
+ "enum": ["1.0.0", "1.1.0", "1.2.0"]
16
+ },
17
+ "created_at": {
18
+ "type": "string",
19
+ "format": "date-time"
20
+ },
21
+ "build": {
22
+ "type": "object",
23
+ "required": [
24
+ "entrypoint",
25
+ "scheme",
26
+ "configuration",
27
+ "destination",
28
+ "command"
29
+ ],
30
+ "properties": {
31
+ "entrypoint": {
32
+ "type": "string",
33
+ "enum": [
34
+ "project",
35
+ "workspace"
36
+ ]
37
+ },
38
+ "path": {
39
+ "type": "string"
40
+ },
41
+ "scheme": {
42
+ "type": "string"
43
+ },
44
+ "configuration": {
45
+ "type": "string"
46
+ },
47
+ "destination": {
48
+ "type": "string"
49
+ },
50
+ "derived_data_path": {
51
+ "type": "string"
52
+ },
53
+ "command": {
54
+ "type": "string"
55
+ }
56
+ },
57
+ "additionalProperties": true
58
+ },
59
+ "environment": {
60
+ "type": "object",
61
+ "properties": {
62
+ "host": {
63
+ "type": "string"
64
+ },
65
+ "xcode_version": {
66
+ "type": "string"
67
+ },
68
+ "macos_version": {
69
+ "type": "string"
70
+ }
71
+ },
72
+ "additionalProperties": true
73
+ },
74
+ "runs": {
75
+ "type": "object",
76
+ "required": [
77
+ "clean",
78
+ "incremental"
79
+ ],
80
+ "properties": {
81
+ "clean": {
82
+ "type": "array",
83
+ "items": {
84
+ "$ref": "#/definitions/run"
85
+ }
86
+ },
87
+ "cached_clean": {
88
+ "type": "array",
89
+ "items": {
90
+ "$ref": "#/definitions/run"
91
+ }
92
+ },
93
+ "incremental": {
94
+ "type": "array",
95
+ "items": {
96
+ "$ref": "#/definitions/run"
97
+ }
98
+ }
99
+ },
100
+ "additionalProperties": false
101
+ },
102
+ "summary": {
103
+ "type": "object",
104
+ "required": [
105
+ "clean",
106
+ "incremental"
107
+ ],
108
+ "properties": {
109
+ "clean": {
110
+ "$ref": "#/definitions/stats"
111
+ },
112
+ "cached_clean": {
113
+ "$ref": "#/definitions/stats"
114
+ },
115
+ "incremental": {
116
+ "$ref": "#/definitions/stats"
117
+ }
118
+ },
119
+ "additionalProperties": false
120
+ },
121
+ "notes": {
122
+ "type": "array",
123
+ "items": {
124
+ "type": "string"
125
+ }
126
+ }
127
+ },
128
+ "definitions": {
129
+ "run": {
130
+ "type": "object",
131
+ "required": [
132
+ "id",
133
+ "build_type",
134
+ "duration_seconds",
135
+ "success",
136
+ "command"
137
+ ],
138
+ "properties": {
139
+ "id": {
140
+ "type": "string"
141
+ },
142
+ "build_type": {
143
+ "type": "string",
144
+ "enum": [
145
+ "clean",
146
+ "cached-clean",
147
+ "incremental"
148
+ ]
149
+ },
150
+ "duration_seconds": {
151
+ "type": "number",
152
+ "minimum": 0
153
+ },
154
+ "success": {
155
+ "type": "boolean"
156
+ },
157
+ "command": {
158
+ "type": "string"
159
+ },
160
+ "exit_code": {
161
+ "type": "integer"
162
+ },
163
+ "raw_log_path": {
164
+ "type": "string"
165
+ },
166
+ "timing_summary_categories": {
167
+ "type": "array",
168
+ "items": {
169
+ "$ref": "#/definitions/category"
170
+ }
171
+ }
172
+ },
173
+ "additionalProperties": true
174
+ },
175
+ "category": {
176
+ "type": "object",
177
+ "required": [
178
+ "name",
179
+ "seconds"
180
+ ],
181
+ "properties": {
182
+ "name": {
183
+ "type": "string"
184
+ },
185
+ "seconds": {
186
+ "type": "number",
187
+ "minimum": 0
188
+ },
189
+ "task_count": {
190
+ "type": "integer",
191
+ "minimum": 0
192
+ }
193
+ },
194
+ "additionalProperties": true
195
+ },
196
+ "stats": {
197
+ "type": "object",
198
+ "required": [
199
+ "count",
200
+ "min_seconds",
201
+ "max_seconds",
202
+ "median_seconds",
203
+ "average_seconds"
204
+ ],
205
+ "properties": {
206
+ "count": {
207
+ "type": "integer",
208
+ "minimum": 0
209
+ },
210
+ "min_seconds": {
211
+ "type": "number",
212
+ "minimum": 0
213
+ },
214
+ "max_seconds": {
215
+ "type": "number",
216
+ "minimum": 0
217
+ },
218
+ "median_seconds": {
219
+ "type": "number",
220
+ "minimum": 0
221
+ },
222
+ "average_seconds": {
223
+ "type": "number",
224
+ "minimum": 0
225
+ }
226
+ },
227
+ "additionalProperties": true
228
+ }
229
+ }
230
+ }
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import platform
7
+ import re
8
+ import shutil
9
+ import statistics
10
+ import subprocess
11
+ import sys
12
+ import tempfile
13
+ import time
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+ from typing import Dict, List, Optional
17
+
18
+
19
+ def parse_args() -> argparse.Namespace:
20
+ parser = argparse.ArgumentParser(description="Benchmark Xcode clean and incremental builds.")
21
+ group = parser.add_mutually_exclusive_group(required=True)
22
+ group.add_argument("--workspace", help="Path to the .xcworkspace file")
23
+ group.add_argument("--project", help="Path to the .xcodeproj file")
24
+ parser.add_argument("--scheme", required=True, help="Scheme to build")
25
+ parser.add_argument("--configuration", default="Debug", help="Build configuration")
26
+ parser.add_argument("--destination", help="xcodebuild destination string")
27
+ parser.add_argument("--derived-data-path", help="DerivedData path override")
28
+ parser.add_argument("--output-dir", default=".build-benchmark", help="Output directory for artifacts")
29
+ parser.add_argument("--repeats", type=int, default=3, help="Measured runs per build type")
30
+ parser.add_argument("--skip-warmup", action="store_true", help="Skip the validation build")
31
+ parser.add_argument(
32
+ "--touch-file",
33
+ help="Path to a source file to touch before each incremental build. "
34
+ "When provided, measures a real edit-rebuild loop instead of a zero-change build.",
35
+ )
36
+ parser.add_argument(
37
+ "--no-cached-clean",
38
+ action="store_true",
39
+ help="Skip cached clean builds even when COMPILATION_CACHING is detected.",
40
+ )
41
+ parser.add_argument(
42
+ "--extra-arg",
43
+ action="append",
44
+ default=[],
45
+ help="Additional xcodebuild argument to append. Can be passed multiple times.",
46
+ )
47
+ return parser.parse_args()
48
+
49
+
50
+ def command_base(args: argparse.Namespace) -> List[str]:
51
+ command = ["xcodebuild"]
52
+ if args.workspace:
53
+ command.extend(["-workspace", args.workspace])
54
+ if args.project:
55
+ command.extend(["-project", args.project])
56
+ command.extend(["-scheme", args.scheme, "-configuration", args.configuration])
57
+ if args.destination:
58
+ command.extend(["-destination", args.destination])
59
+ if args.derived_data_path:
60
+ command.extend(["-derivedDataPath", args.derived_data_path])
61
+ command.extend(args.extra_arg)
62
+ return command
63
+
64
+
65
+ def shell_join(parts: List[str]) -> str:
66
+ return " ".join(subprocess.list2cmdline([part]) for part in parts)
67
+
68
+
69
+ _TASK_COUNT_RE = re.compile(r"^(.+?)\s*\((\d+)\s+tasks?\)$")
70
+
71
+
72
+ def _extract_task_count(name: str) -> tuple[str, Optional[int]]:
73
+ """Split 'Category (N tasks)' into ('Category', N)."""
74
+ match = _TASK_COUNT_RE.match(name)
75
+ if match:
76
+ return match.group(1).strip(), int(match.group(2))
77
+ return name, None
78
+
79
+
80
+ def parse_timing_summary(output: str) -> List[Dict]:
81
+ categories: Dict[str, float] = {}
82
+ task_counts: Dict[str, Optional[int]] = {}
83
+ for raw_line in output.splitlines():
84
+ line = raw_line.strip()
85
+ if not line:
86
+ continue
87
+ for suffix in (" seconds", " second", " sec"):
88
+ if not line.endswith(suffix):
89
+ continue
90
+ trimmed = line[: -len(suffix)]
91
+ if "|" in trimmed:
92
+ name_part, _, seconds_text = trimmed.rpartition("|")
93
+ else:
94
+ name_part, _, seconds_text = trimmed.rpartition(" ")
95
+ try:
96
+ seconds = float(seconds_text.strip())
97
+ except ValueError:
98
+ continue
99
+ cleaned_name = name_part.replace(" ", " ").strip(" -:")
100
+ if len(cleaned_name) < 3:
101
+ continue
102
+ base_name, count = _extract_task_count(cleaned_name)
103
+ categories[base_name] = categories.get(base_name, 0.0) + seconds
104
+ if count is not None:
105
+ task_counts[base_name] = (task_counts.get(base_name) or 0) + count
106
+ break
107
+ result: List[Dict] = []
108
+ for name, seconds in sorted(categories.items(), key=lambda item: item[1], reverse=True):
109
+ entry: Dict = {"name": name, "seconds": round(seconds, 3)}
110
+ if name in task_counts:
111
+ entry["task_count"] = task_counts[name]
112
+ result.append(entry)
113
+ return result
114
+
115
+
116
+ def run_command(command: List[str]) -> subprocess.CompletedProcess:
117
+ return subprocess.run(command, capture_output=True, text=True)
118
+
119
+
120
+ def stats_for(runs: List[Dict[str, object]]) -> Dict[str, float]:
121
+ durations = [run["duration_seconds"] for run in runs if run.get("success")]
122
+ if not durations:
123
+ return {
124
+ "count": 0,
125
+ "min_seconds": 0.0,
126
+ "max_seconds": 0.0,
127
+ "median_seconds": 0.0,
128
+ "average_seconds": 0.0,
129
+ }
130
+ return {
131
+ "count": len(durations),
132
+ "min_seconds": round(min(durations), 3),
133
+ "max_seconds": round(max(durations), 3),
134
+ "median_seconds": round(statistics.median(durations), 3),
135
+ "average_seconds": round(statistics.fmean(durations), 3),
136
+ }
137
+
138
+
139
+ def xcode_version() -> str:
140
+ result = run_command(["xcodebuild", "-version"])
141
+ return result.stdout.strip() if result.returncode == 0 else "unknown"
142
+
143
+
144
+ def detect_compilation_caching(base_command: List[str]) -> bool:
145
+ """Check whether COMPILATION_CACHING is enabled in the resolved build settings."""
146
+ result = run_command([*base_command, "-showBuildSettings"])
147
+ if result.returncode != 0:
148
+ return False
149
+ for line in result.stdout.splitlines():
150
+ stripped = line.strip()
151
+ if stripped.startswith("COMPILATION_CACHING") and "=" in stripped:
152
+ value = stripped.split("=", 1)[1].strip()
153
+ return value == "YES"
154
+ return False
155
+
156
+
157
+ def measure_build(
158
+ base_command: List[str],
159
+ artifact_stem: str,
160
+ output_dir: Path,
161
+ build_type: str,
162
+ run_index: int,
163
+ ) -> Dict[str, object]:
164
+ build_command = [*base_command, "build", "-showBuildTimingSummary"]
165
+ started = time.perf_counter()
166
+ result = run_command(build_command)
167
+ elapsed = round(time.perf_counter() - started, 3)
168
+ log_path = output_dir / f"{artifact_stem}-{build_type}-{run_index}.log"
169
+ log_path.write_text(result.stdout + result.stderr)
170
+ return {
171
+ "id": f"{build_type}-{run_index}",
172
+ "build_type": build_type,
173
+ "duration_seconds": elapsed,
174
+ "success": result.returncode == 0,
175
+ "exit_code": result.returncode,
176
+ "command": shell_join(build_command),
177
+ "raw_log_path": str(log_path),
178
+ "timing_summary_categories": parse_timing_summary(result.stdout + result.stderr),
179
+ }
180
+
181
+
182
+ def main() -> int:
183
+ args = parse_args()
184
+ output_dir = Path(args.output_dir)
185
+ output_dir.mkdir(parents=True, exist_ok=True)
186
+
187
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
188
+ artifact_stem = f"{timestamp}-{args.scheme.replace(' ', '-').lower()}"
189
+ base_command = command_base(args)
190
+
191
+ if not args.skip_warmup:
192
+ warmup = run_command([*base_command, "build"])
193
+ if warmup.returncode != 0:
194
+ sys.stderr.write(warmup.stdout + warmup.stderr)
195
+ return warmup.returncode
196
+ warmup_clean = run_command([*base_command, "clean"])
197
+ if warmup_clean.returncode != 0:
198
+ sys.stderr.write(warmup_clean.stdout + warmup_clean.stderr)
199
+ return warmup_clean.returncode
200
+ warmup_rebuild = run_command([*base_command, "build"])
201
+ if warmup_rebuild.returncode != 0:
202
+ sys.stderr.write(warmup_rebuild.stdout + warmup_rebuild.stderr)
203
+ return warmup_rebuild.returncode
204
+
205
+ runs: Dict[str, list] = {"clean": [], "incremental": []}
206
+
207
+ for index in range(1, args.repeats + 1):
208
+ clean_result = run_command([*base_command, "clean"])
209
+ clean_log_path = output_dir / f"{artifact_stem}-clean-prep-{index}.log"
210
+ clean_log_path.write_text(clean_result.stdout + clean_result.stderr)
211
+ if clean_result.returncode != 0:
212
+ sys.stderr.write(clean_result.stdout + clean_result.stderr)
213
+ return clean_result.returncode
214
+ runs["clean"].append(measure_build(base_command, artifact_stem, output_dir, "clean", index))
215
+
216
+ # --- Cached clean builds ---------------------------------------------------
217
+ # When COMPILATION_CACHING is enabled, the compilation cache lives outside
218
+ # DerivedData and survives product deletion. We measure "cached clean"
219
+ # builds by pointing DerivedData at a temp directory, warming the cache with
220
+ # one build, then deleting the DerivedData directory (but not the cache)
221
+ # before each measured rebuild. This captures the realistic scenario:
222
+ # branch switching, pulling changes, or Clean Build Folder.
223
+ should_cached_clean = not args.no_cached_clean and detect_compilation_caching(base_command)
224
+ if should_cached_clean:
225
+ dd_path = Path(args.derived_data_path) if args.derived_data_path else Path(
226
+ tempfile.mkdtemp(prefix="xcode-bench-dd-")
227
+ )
228
+ cached_cmd = list(base_command)
229
+ if not args.derived_data_path:
230
+ cached_cmd.extend(["-derivedDataPath", str(dd_path)])
231
+
232
+ cache_warmup = run_command([*cached_cmd, "build"])
233
+ if cache_warmup.returncode != 0:
234
+ sys.stderr.write("Warning: cached clean warmup build failed, skipping cached clean benchmarks.\n")
235
+ sys.stderr.write(cache_warmup.stdout + cache_warmup.stderr)
236
+ should_cached_clean = False
237
+
238
+ if should_cached_clean:
239
+ runs["cached_clean"] = []
240
+ for index in range(1, args.repeats + 1):
241
+ shutil.rmtree(dd_path, ignore_errors=True)
242
+ runs["cached_clean"].append(
243
+ measure_build(cached_cmd, artifact_stem, output_dir, "cached-clean", index)
244
+ )
245
+ shutil.rmtree(dd_path, ignore_errors=True)
246
+
247
+ # --- Incremental / zero-change builds --------------------------------------
248
+ incremental_label = "incremental"
249
+ if args.touch_file:
250
+ touch_path = Path(args.touch_file)
251
+ if not touch_path.exists():
252
+ sys.stderr.write(f"--touch-file path does not exist: {touch_path}\n")
253
+ return 1
254
+ incremental_label = "incremental"
255
+ else:
256
+ incremental_label = "zero-change"
257
+
258
+ for index in range(1, args.repeats + 1):
259
+ if args.touch_file:
260
+ touch_path.touch()
261
+ runs["incremental"].append(
262
+ measure_build(base_command, artifact_stem, output_dir, incremental_label, index)
263
+ )
264
+
265
+ summary: Dict[str, object] = {
266
+ "clean": stats_for(runs["clean"]),
267
+ "incremental": stats_for(runs["incremental"]),
268
+ }
269
+ if "cached_clean" in runs:
270
+ summary["cached_clean"] = stats_for(runs["cached_clean"])
271
+
272
+ artifact = {
273
+ "schema_version": "1.2.0" if "cached_clean" in runs else "1.1.0",
274
+ "created_at": datetime.now(timezone.utc).isoformat(),
275
+ "build": {
276
+ "entrypoint": "workspace" if args.workspace else "project",
277
+ "path": args.workspace or args.project,
278
+ "scheme": args.scheme,
279
+ "configuration": args.configuration,
280
+ "destination": args.destination or "",
281
+ "derived_data_path": args.derived_data_path or "",
282
+ "command": shell_join(base_command),
283
+ },
284
+ "environment": {
285
+ "host": platform.node(),
286
+ "macos_version": platform.platform(),
287
+ "xcode_version": xcode_version(),
288
+ "cwd": os.getcwd(),
289
+ },
290
+ "runs": runs,
291
+ "summary": summary,
292
+ "notes": [f"touch-file: {args.touch_file}"] if args.touch_file else [],
293
+ }
294
+
295
+ artifact_path = output_dir / f"{artifact_stem}.json"
296
+ artifact_path.write_text(json.dumps(artifact, indent=2) + "\n")
297
+
298
+ print(f"Saved benchmark artifact: {artifact_path}")
299
+ print(f"Clean median: {artifact['summary']['clean']['median_seconds']}s")
300
+ if "cached_clean" in artifact["summary"]:
301
+ print(f"Cached clean median: {artifact['summary']['cached_clean']['median_seconds']}s")
302
+ inc_label = "Incremental" if args.touch_file else "Zero-change"
303
+ print(f"{inc_label} median: {artifact['summary']['incremental']['median_seconds']}s")
304
+ return 0
305
+
306
+
307
+ if __name__ == "__main__":
308
+ raise SystemExit(main())