@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.
- package/bin/awk.js +458 -7
- package/bin/claude-generators.js +122 -0
- package/core/AGENTS.md +16 -0
- package/core/CLAUDE.md +155 -0
- package/core/GEMINI.md +44 -9
- package/package.json +1 -1
- package/skills/ai-sprite-maker/SKILL.md +81 -0
- package/skills/ai-sprite-maker/scripts/animate_sprite.py +102 -0
- package/skills/ai-sprite-maker/scripts/process_sprites.py +140 -0
- package/skills/code-review/SKILL.md +21 -33
- package/skills/lucylab-tts/SKILL.md +64 -0
- package/skills/lucylab-tts/resources/voices_library.json +908 -0
- package/skills/lucylab-tts/scripts/.env +1 -0
- package/skills/lucylab-tts/scripts/lucylab_tts.py +506 -0
- package/skills/orchestrator/SKILL.md +5 -0
- package/skills/short-maker/SKILL.md +150 -0
- package/skills/short-maker/_backup/storyboard.html +106 -0
- package/skills/short-maker/_backup/video_mixer.py +296 -0
- package/skills/short-maker/outputs/fitbite-promo/background.jpg +0 -0
- package/skills/short-maker/outputs/fitbite-promo/final/promo-final.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/script.md +19 -0
- package/skills/short-maker/outputs/fitbite-promo/segments/scene-01.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/segments/scene-02.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/segments/scene-03.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/segments/scene-04.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-01.png +0 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-02.png +0 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-03.png +0 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard/scene-04.png +0 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard.html +133 -0
- package/skills/short-maker/outputs/fitbite-promo/storyboard.json +38 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/merged_chroma.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/merged_crossfaded.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/ready_00.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/ready_01.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/ready_02.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/temp/ready_03.mp4 +0 -0
- package/skills/short-maker/outputs/fitbite-promo/tts/manifest.json +31 -0
- package/skills/short-maker/outputs/fitbite-promo/tts/scene-01.wav +0 -0
- package/skills/short-maker/outputs/fitbite-promo/tts/scene-02.wav +0 -0
- package/skills/short-maker/outputs/fitbite-promo/tts/scene-03.wav +0 -0
- package/skills/short-maker/outputs/fitbite-promo/tts/scene-04.wav +0 -0
- package/skills/short-maker/outputs/fitbite-promo/tts_script.txt +11 -0
- package/skills/short-maker/scripts/google-flow-cli/.project-identity +41 -0
- package/skills/short-maker/scripts/google-flow-cli/.trae/rules/project_rules.md +52 -0
- package/skills/short-maker/scripts/google-flow-cli/CODEBASE.md +67 -0
- package/skills/short-maker/scripts/google-flow-cli/GoogleFlowCli.code-workspace +29 -0
- package/skills/short-maker/scripts/google-flow-cli/README.md +168 -0
- package/skills/short-maker/scripts/google-flow-cli/docs/specs/PROJECT.md +12 -0
- package/skills/short-maker/scripts/google-flow-cli/docs/specs/REQUIREMENTS.md +22 -0
- package/skills/short-maker/scripts/google-flow-cli/docs/specs/ROADMAP.md +16 -0
- package/skills/short-maker/scripts/google-flow-cli/docs/specs/TECH-SPEC.md +13 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/__init__.py +3 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/api/__init__.py +19 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/api/client.py +1921 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/api/models.py +64 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/api/rpc_ids.py +98 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/auth/__init__.py +15 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/auth/browser_auth.py +692 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/auth/humanizer.py +417 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/auth/proxy_ext.py +120 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/auth/recaptcha.py +482 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/batchexecute/__init__.py +5 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/batchexecute/client.py +414 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/cli/__init__.py +1 -0
- package/skills/short-maker/scripts/google-flow-cli/gflow/cli/main.py +1075 -0
- package/skills/short-maker/scripts/google-flow-cli/pyproject.toml +36 -0
- package/skills/short-maker/scripts/google-flow-cli/script.txt +22 -0
- package/skills/short-maker/scripts/google-flow-cli/tests/__init__.py +0 -0
- package/skills/short-maker/scripts/google-flow-cli/tests/test_batchexecute.py +113 -0
- package/skills/short-maker/scripts/google-flow-cli/tests/test_client.py +190 -0
- package/skills/short-maker/templates/aida_script.md +40 -0
- package/skills/short-maker/templates/mimic_analyzer.md +29 -0
- package/skills/single-flow-task-execution/SKILL.md +9 -6
- package/skills/skill-creator/SKILL.md +44 -0
- package/skills/spm-build-analysis/SKILL.md +92 -0
- package/skills/spm-build-analysis/references/build-optimization-sources.md +155 -0
- package/skills/spm-build-analysis/references/recommendation-format.md +85 -0
- package/skills/spm-build-analysis/references/spm-analysis-checks.md +105 -0
- package/skills/spm-build-analysis/scripts/check_spm_pins.py +118 -0
- package/skills/symphony-enforcer/SKILL.md +51 -83
- package/skills/symphony-orchestrator/SKILL.md +1 -1
- package/skills/trello-sync/SKILL.md +27 -28
- package/skills/verification-gate/SKILL.md +13 -2
- package/skills/xcode-build-benchmark/SKILL.md +88 -0
- package/skills/xcode-build-benchmark/references/benchmark-artifacts.md +94 -0
- package/skills/xcode-build-benchmark/references/benchmarking-workflow.md +67 -0
- package/skills/xcode-build-benchmark/schemas/build-benchmark.schema.json +230 -0
- package/skills/xcode-build-benchmark/scripts/benchmark_builds.py +308 -0
- package/skills/xcode-build-fixer/SKILL.md +218 -0
- package/skills/xcode-build-fixer/references/build-settings-best-practices.md +216 -0
- package/skills/xcode-build-fixer/references/fix-patterns.md +290 -0
- package/skills/xcode-build-fixer/references/recommendation-format.md +85 -0
- package/skills/xcode-build-fixer/scripts/benchmark_builds.py +308 -0
- package/skills/xcode-build-orchestrator/SKILL.md +156 -0
- package/skills/xcode-build-orchestrator/references/benchmark-artifacts.md +94 -0
- package/skills/xcode-build-orchestrator/references/build-settings-best-practices.md +216 -0
- package/skills/xcode-build-orchestrator/references/orchestration-report-template.md +143 -0
- package/skills/xcode-build-orchestrator/references/recommendation-format.md +85 -0
- package/skills/xcode-build-orchestrator/scripts/benchmark_builds.py +308 -0
- package/skills/xcode-build-orchestrator/scripts/diagnose_compilation.py +273 -0
- package/skills/xcode-build-orchestrator/scripts/generate_optimization_report.py +533 -0
- package/skills/xcode-compilation-analyzer/SKILL.md +89 -0
- package/skills/xcode-compilation-analyzer/references/build-optimization-sources.md +155 -0
- package/skills/xcode-compilation-analyzer/references/code-compilation-checks.md +106 -0
- package/skills/xcode-compilation-analyzer/references/recommendation-format.md +85 -0
- package/skills/xcode-compilation-analyzer/scripts/diagnose_compilation.py +273 -0
- package/skills/xcode-project-analyzer/SKILL.md +76 -0
- package/skills/xcode-project-analyzer/references/build-optimization-sources.md +155 -0
- package/skills/xcode-project-analyzer/references/build-settings-best-practices.md +216 -0
- package/skills/xcode-project-analyzer/references/project-audit-checks.md +101 -0
- package/skills/xcode-project-analyzer/references/recommendation-format.md +85 -0
- package/templates/project-identity/android.json +0 -10
- package/templates/project-identity/backend-nestjs.json +0 -10
- package/templates/project-identity/expo.json +0 -10
- package/templates/project-identity/ios.json +0 -10
- package/templates/project-identity/web-nextjs.json +0 -10
- package/workflows/_uncategorized/ship-to-code.md +85 -0
- 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())
|