Skip to content

Commit f83f8b7

Browse files
committed
fix: added code to install missing runtimes
1 parent 1a759f0 commit f83f8b7

2 files changed

Lines changed: 98 additions & 23 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "default",
3-
"version": "1.15.0",
3+
"version": "1.15.1-beta.2",
44
"description": "Default plugin for Codify - provides 50+ declarative resources for managing development tools and system configuration across macOS and Linux",
55
"main": "dist/index.js",
66
"scripts": {

src/resources/ios/ios-simulator/ios-simulator.ts

Lines changed: 97 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,20 @@ const schema = z.object({
3232
'Automatically accept the Xcode license agreement if it has not been accepted yet. ' +
3333
'Runs `sudo xcodebuild -license accept`. Defaults to true.'
3434
),
35+
downloadRuntimes: z
36+
.boolean()
37+
.optional()
38+
.describe(
39+
'Automatically download missing simulator runtimes via `xcodebuild -downloadPlatform`. ' +
40+
'Defaults to true. Set to false if you manage runtimes manually through Xcode.'
41+
),
42+
destroyRuntimes: z
43+
.boolean()
44+
.optional()
45+
.describe(
46+
'Delete simulator runtimes that are no longer used by any simulator when this resource is destroyed. ' +
47+
'Defaults to false. Enable with caution — runtimes are several GB and take time to re-download.'
48+
),
3549
});
3650

3751
export type IosSimulatorConfig = z.infer<typeof schema>;
@@ -133,6 +147,8 @@ export class IosSimulatorResource extends Resource<IosSimulatorConfig> {
133147
canModify: true,
134148
},
135149
acceptLicense: { type: 'boolean', setting: true, default: true },
150+
downloadRuntimes: { type: 'boolean', setting: true, default: true },
151+
destroyRuntimes: { type: 'boolean', setting: true, default: false },
136152
},
137153
};
138154
}
@@ -160,17 +176,32 @@ export class IosSimulatorResource extends Resource<IosSimulatorConfig> {
160176
await this.acceptLicenseIfNeeded();
161177
}
162178
await this.assertSimctlAvailable();
163-
await this.assertRuntimesAvailable(plan.desiredConfig.simulators ?? []);
179+
const simulators = plan.desiredConfig.simulators ?? [];
180+
if (plan.desiredConfig.downloadRuntimes !== false) {
181+
await this.downloadMissingRuntimes(simulators);
182+
} else {
183+
await this.assertRuntimesAvailable(simulators);
184+
}
164185
const $ = getPty();
165-
for (const sim of plan.desiredConfig.simulators ?? []) {
166-
await $.spawn(
186+
for (const sim of simulators) {
187+
const { status, data } = await $.spawnSafe(
167188
`xcrun simctl create "${sim.name}" "${sim.deviceType}" "${sim.runtime}"`,
168189
{ interactive: true },
169190
);
191+
if (status !== SpawnStatus.SUCCESS) {
192+
if (data.includes('Invalid runtime')) {
193+
throw new Error(
194+
`Runtime "${sim.runtime}" is not installed or not available.\n` +
195+
'Download it in Xcode → Settings → Platforms, or via:\n' +
196+
` xcodebuild -downloadPlatform ${runtimeToXcodebuildPlatform(sim.runtime)}`,
197+
);
198+
}
199+
throw new Error(`Failed to create simulator "${sim.name}": ${data}`);
200+
}
170201
}
171202
}
172203

173-
async modify(pc: ParameterChange<IosSimulatorConfig>, _plan: ModifyPlan<IosSimulatorConfig>): Promise<void> {
204+
async modify(pc: ParameterChange<IosSimulatorConfig>, plan: ModifyPlan<IosSimulatorConfig>): Promise<void> {
174205
if (pc.name !== 'simulators') return;
175206

176207
const $ = getPty();
@@ -194,6 +225,11 @@ export class IosSimulatorResource extends Resource<IosSimulatorConfig> {
194225
}
195226

196227
const toAdd = desired.filter((d) => !previous.some((p) => p.name === d.name));
228+
if (toAdd.length > 0 && plan.desiredConfig.downloadRuntimes !== false) {
229+
await this.downloadMissingRuntimes(toAdd);
230+
} else if (toAdd.length > 0) {
231+
await this.assertRuntimesAvailable(toAdd);
232+
}
197233
for (const sim of toAdd) {
198234
await $.spawn(
199235
`xcrun simctl create "${sim.name}" "${sim.deviceType}" "${sim.runtime}"`,
@@ -207,7 +243,10 @@ export class IosSimulatorResource extends Resource<IosSimulatorConfig> {
207243
const allDevices = await this.listAllDevices();
208244
if (!allDevices) return;
209245

210-
for (const sim of plan.currentConfig.simulators ?? []) {
246+
const simulatorsToDestroy = plan.currentConfig.simulators ?? [];
247+
const runtimesInUse = new Set(simulatorsToDestroy.map((s) => s.runtime));
248+
249+
for (const sim of simulatorsToDestroy) {
211250
for (const devices of Object.values(allDevices)) {
212251
const match = devices.find((d) => d.name === sim.name);
213252
if (match) {
@@ -216,6 +255,10 @@ export class IosSimulatorResource extends Resource<IosSimulatorConfig> {
216255
}
217256
}
218257
}
258+
259+
if (plan.currentConfig.destroyRuntimes) {
260+
await this.deleteOrphanedRuntimes(runtimesInUse);
261+
}
219262
}
220263

221264
private async assertSimctlAvailable(): Promise<void> {
@@ -230,35 +273,67 @@ export class IosSimulatorResource extends Resource<IosSimulatorConfig> {
230273
}
231274
}
232275

233-
private async assertRuntimesAvailable(simulators: SimulatorDeclaration[]): Promise<void> {
276+
private async deleteOrphanedRuntimes(candidateRuntimes: Set<string>): Promise<void> {
277+
if (candidateRuntimes.size === 0) return;
278+
279+
const allDevices = await this.listAllDevices();
280+
const stillInUse = new Set<string>();
281+
if (allDevices) {
282+
for (const [runtimeId, devices] of Object.entries(allDevices)) {
283+
if (devices.length > 0) stillInUse.add(runtimeId);
284+
}
285+
}
286+
287+
const $ = getPty();
288+
for (const runtimeId of candidateRuntimes) {
289+
if (!stillInUse.has(runtimeId)) {
290+
await $.spawnSafe(`xcrun simctl runtime delete "${runtimeId}"`);
291+
}
292+
}
293+
}
294+
295+
private async getMissingRuntimes(simulators: SimulatorDeclaration[]): Promise<string[]> {
234296
const $ = getPty();
235297
const { status, data } = await $.spawnSafe('xcrun simctl list runtimes --json');
236-
if (status !== SpawnStatus.SUCCESS) return; // can't verify, let simctl fail with its own message
298+
if (status !== SpawnStatus.SUCCESS) return [];
237299

238-
let availableRuntimes: Set<string>;
300+
let allRuntimes: SimctlRuntime[];
239301
try {
240302
const parsed: SimctlRuntimesOutput = JSON.parse(data);
241-
availableRuntimes = new Set(
242-
parsed.runtimes.filter((r) => r.isAvailable).map((r) => r.identifier),
243-
);
303+
allRuntimes = parsed.runtimes;
244304
} catch {
245-
return;
305+
return [];
246306
}
247307

248-
const missing = [...new Set(simulators.map((s) => s.runtime))].filter(
249-
(r) => !availableRuntimes.has(r),
250-
);
308+
const availableIds = new Set(allRuntimes.filter((r) => r.isAvailable).map((r) => r.identifier));
309+
const requiredRuntimes = [...new Set(simulators.map((s) => s.runtime))];
310+
return requiredRuntimes.filter((r) => !availableIds.has(r));
311+
}
251312

252-
if (missing.length > 0) {
253-
throw new Error(
254-
`The following simulator runtime${missing.length > 1 ? 's are' : ' is'} not installed:\n` +
255-
missing.map((r) => ` ${r}`).join('\n') + '\n' +
256-
'Download runtimes in Xcode → Settings → Platforms, or via:\n' +
257-
missing.map((r) => ` xcodebuild -downloadPlatform ${runtimeToXcodebuildPlatform(r)}`).join('\n'),
258-
);
313+
private async downloadMissingRuntimes(simulators: SimulatorDeclaration[]): Promise<void> {
314+
const missing = await this.getMissingRuntimes(simulators);
315+
if (missing.length === 0) return;
316+
317+
const $ = getPty();
318+
const platforms = [...new Set(missing.map(runtimeToXcodebuildPlatform))];
319+
for (const platform of platforms) {
320+
await $.spawn(`xcodebuild -downloadPlatform ${platform}`, { stdin: true });
259321
}
260322
}
261323

324+
private async assertRuntimesAvailable(simulators: SimulatorDeclaration[]): Promise<void> {
325+
const missing = await this.getMissingRuntimes(simulators);
326+
if (missing.length === 0) return;
327+
328+
const lines: string[] = [
329+
`The following simulator runtime${missing.length > 1 ? 's are' : ' is'} not installed or not available:`,
330+
...missing.map((r) => ` ${r}`),
331+
'Download runtimes in Xcode → Settings → Platforms, or via:',
332+
...missing.map((r) => ` xcodebuild -downloadPlatform ${runtimeToXcodebuildPlatform(r)}`),
333+
];
334+
throw new Error(lines.join('\n'));
335+
}
336+
262337
private async acceptLicenseIfNeeded(): Promise<void> {
263338
const $ = getPty();
264339
const { status } = await $.spawnSafe('xcodebuild -license status');

0 commit comments

Comments
 (0)