@invarn/cibuild 2.7.9 → 2.8.0

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.
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Which built `.app` belongs to the scheme that was built.
3
+ *
4
+ * A simulator build leaves every application bundle its graph produced under
5
+ * `DerivedData/Build/Products/<configuration>-<platform>/`, and a scheme that
6
+ * builds a watchOS or tvOS companion alongside the app produces two. Taking
7
+ * whichever one a directory scan reaches first is a coin toss, and it lands the
8
+ * wrong way often enough to matter: a project whose scheme builds both an iOS
9
+ * app and a watch companion exported the 588 KB watch bundle as the app under
10
+ * test. The build was green, the artifact manifest was non-empty, and the
11
+ * thing a user downloaded could not be installed on the device they built for.
12
+ * That is worse than delivering nothing, because nothing is visibly wrong and
13
+ * a plausible wrong answer is not.
14
+ *
15
+ * The scheme already knows. A shared `.xcscheme` lists its build entries with a
16
+ * `BuildableName` whose extension IS the product type, and `buildForRunning`
17
+ * separates what the scheme builds from what it merely references — a scheme
18
+ * may list several `.app` entries it does not build, so "mentions an app" is
19
+ * not the question and a check that asked it would pick just as badly.
20
+ *
21
+ * Where the scheme gives no answer this falls back rather than failing. A
22
+ * project with no shared scheme at all is ordinary — plenty of them never
23
+ * commit one — and those builds work. Absence of evidence is not evidence, so
24
+ * the destination's own platform directory decides, and failing to resolve an
25
+ * app is left to the caller's existing guard.
26
+ *
27
+ * The whole resolver runs on the machine that did the build, against the
28
+ * products that build actually produced, so it needs nothing from the step
29
+ * generator but the scheme name and the destination.
30
+ */
31
+ /**
32
+ * The SDK suffix the Products directory carries for a given destination.
33
+ *
34
+ * `-destination 'generic/platform=iOS Simulator'` builds into
35
+ * `Debug-iphonesimulator`; the watchOS and tvOS spellings are the ones that
36
+ * matter here, because a companion target is exactly what produces the second
37
+ * bundle this resolver exists to skip past.
38
+ */
39
+ export function platformSuffixForDestination(destination) {
40
+ const d = String(destination ?? '').toLowerCase();
41
+ if (d.includes('watchos'))
42
+ return '-watchsimulator';
43
+ if (d.includes('tvos'))
44
+ return '-appletvsimulator';
45
+ if (d.includes('visionos') || d.includes('xros'))
46
+ return '-xrsimulator';
47
+ return '-iphonesimulator';
48
+ }
49
+ /**
50
+ * The application bundles a scheme's build action actually builds.
51
+ *
52
+ * `buildForRunning="NO"` entries are listed and not built. A scheme can carry
53
+ * three `.app` entries that are API-compatibility testers in other containers
54
+ * beside the one framework it really produces, so the attribute is the whole
55
+ * filter rather than a refinement of one.
56
+ *
57
+ * Returns an empty list for a file that cannot be read or carries no build
58
+ * action — no evidence, which the caller must not read as "builds no app".
59
+ */
60
+ export function runnableAppProducts(schemeXml) {
61
+ if (!schemeXml)
62
+ return [];
63
+ const build = /<BuildAction\b[\s\S]*?<\/BuildAction>/.exec(schemeXml);
64
+ if (!build)
65
+ return [];
66
+ const products = [];
67
+ for (const entry of build[0].matchAll(/<BuildActionEntry\b([\s\S]*?)<\/BuildActionEntry>/g)) {
68
+ const attrs = entry[1];
69
+ if (!/buildForRunning\s*=\s*"YES"/.test(attrs))
70
+ continue;
71
+ for (const ref of attrs.matchAll(/BuildableName\s*=\s*"([^"]*)"/g)) {
72
+ if (ref[1].endsWith('.app'))
73
+ products.push(ref[1]);
74
+ }
75
+ }
76
+ return products;
77
+ }
78
+ /**
79
+ * The bundle to export, given everything that was built and what the scheme
80
+ * says it builds.
81
+ *
82
+ * In order: the scheme's own product on the destination's platform, the
83
+ * scheme's own product anywhere, anything on the destination's platform,
84
+ * anything at all. The last two are the no-shared-scheme path, and they are
85
+ * what keeps a project that never committed a scheme building exactly as it
86
+ * did before.
87
+ */
88
+ export function chooseAppProduct(apps, schemeProducts, destination) {
89
+ if (apps.length === 0)
90
+ return null;
91
+ const suffix = platformSuffixForDestination(destination);
92
+ const onPlatform = apps.filter((a) => a.platform.endsWith(suffix));
93
+ const named = (list) => list.find((a) => schemeProducts.includes(a.name)) ?? null;
94
+ return named(onPlatform) ?? named(apps) ?? onPlatform[0] ?? apps[0] ?? null;
95
+ }
96
+ /**
97
+ * The resolver as it runs on the build machine: dependency-free CommonJS,
98
+ * written to a temp file and invoked with
99
+ * `node <file> <productsDir> <projectPath> <scheme> <destination>`.
100
+ *
101
+ * It prints the chosen bundle's path and nothing else, and exits non-zero with
102
+ * no output when it cannot choose — which is the signal for the caller's
103
+ * existing scan to take over, so a machine without a usable node is no worse
104
+ * off than before this existed.
105
+ *
106
+ * Kept as source text rather than bundled because the step ships a shell
107
+ * script, not a module graph. The three functions above are the same rules in
108
+ * testable form, and `xcode-app-product.test.ts` holds them to each other.
109
+ *
110
+ * Must contain no backtick and no dollar-brace: it is emitted into a bash
111
+ * heredoc, and it is a TypeScript template literal on the way there.
112
+ */
113
+ export const APP_PRODUCT_RESOLVER_SOURCE = String.raw `
114
+ var fs = require('fs');
115
+ var path = require('path');
116
+
117
+ var productsDir = process.argv[2];
118
+ var projectPath = process.argv[3] || '';
119
+ var scheme = process.argv[4] || '';
120
+ var destination = process.argv[5] || '';
121
+
122
+ // Every .app under Products, both directly and one platform directory down,
123
+ // which is where -derivedDataPath leaves them.
124
+ function builtApps(dir) {
125
+ var out = [];
126
+ var entries = readDir(dir);
127
+ for (var i = 0; i < entries.length; i++) {
128
+ var entry = entries[i];
129
+ if (!entry.isDirectory()) continue;
130
+ var full = path.join(dir, entry.name);
131
+ if (/\.app$/.test(entry.name)) {
132
+ out.push({ platform: '', name: entry.name, path: full });
133
+ continue;
134
+ }
135
+ var inner = readDir(full);
136
+ for (var j = 0; j < inner.length; j++) {
137
+ if (inner[j].isDirectory() && /\.app$/.test(inner[j].name)) {
138
+ out.push({
139
+ platform: entry.name,
140
+ name: inner[j].name,
141
+ path: path.join(full, inner[j].name)
142
+ });
143
+ }
144
+ }
145
+ }
146
+ return out;
147
+ }
148
+
149
+ function readDir(dir) {
150
+ try {
151
+ return fs.readdirSync(dir, { withFileTypes: true });
152
+ } catch (err) {
153
+ return [];
154
+ }
155
+ }
156
+
157
+ // Shared schemes only. A scheme under xcuserdata belongs to one developer's
158
+ // checkout and is not in the repository, which is also what xcodebuild -list
159
+ // would see here.
160
+ function sharedSchemeFiles(dir, depth, found) {
161
+ var entries = readDir(dir);
162
+ for (var i = 0; i < entries.length; i++) {
163
+ var entry = entries[i];
164
+ var full = path.join(dir, entry.name);
165
+ if (!entry.isDirectory()) {
166
+ if (/\.xcscheme$/.test(entry.name)) found.push(full);
167
+ continue;
168
+ }
169
+ if (entry.name === 'xcuserdata' || entry.name === 'node_modules') continue;
170
+ if (entry.name.charAt(0) === '.') continue;
171
+ var container = /\.(xcodeproj|xcworkspace)$/.test(entry.name);
172
+ if (depth > 0 || container || entry.name === 'xcshareddata' || entry.name === 'xcschemes') {
173
+ sharedSchemeFiles(full, depth - 1, found);
174
+ }
175
+ }
176
+ return found;
177
+ }
178
+
179
+ // The products a scheme's build action RUNS. buildForRunning="NO" entries are
180
+ // listed and not built, so a scheme naming three .app targets it merely
181
+ // references must not be read as building any of them.
182
+ function runnableAppProducts(schemeFile) {
183
+ var xml;
184
+ try {
185
+ xml = fs.readFileSync(schemeFile, 'utf-8');
186
+ } catch (err) {
187
+ return [];
188
+ }
189
+ var build = /<BuildAction\b[\s\S]*?<\/BuildAction>/.exec(xml);
190
+ if (!build) return [];
191
+ var products = [];
192
+ var entryRe = /<BuildActionEntry\b([\s\S]*?)<\/BuildActionEntry>/g;
193
+ var entry;
194
+ while ((entry = entryRe.exec(build[0])) !== null) {
195
+ if (!/buildForRunning\s*=\s*"YES"/.test(entry[1])) continue;
196
+ var nameRe = /BuildableName\s*=\s*"([^"]*)"/g;
197
+ var ref;
198
+ while ((ref = nameRe.exec(entry[1])) !== null) {
199
+ if (/\.app$/.test(ref[1])) products.push(ref[1]);
200
+ }
201
+ }
202
+ return products;
203
+ }
204
+
205
+ function platformSuffix(dest) {
206
+ var d = String(dest).toLowerCase();
207
+ if (d.indexOf('watchos') !== -1) return '-watchsimulator';
208
+ if (d.indexOf('tvos') !== -1) return '-appletvsimulator';
209
+ if (d.indexOf('visionos') !== -1 || d.indexOf('xros') !== -1) return '-xrsimulator';
210
+ return '-iphonesimulator';
211
+ }
212
+
213
+ function endsWith(text, suffix) {
214
+ return suffix.length <= text.length && text.slice(text.length - suffix.length) === suffix;
215
+ }
216
+
217
+ var apps = builtApps(productsDir);
218
+ if (apps.length === 0) process.exit(1);
219
+
220
+ var schemeProducts = [];
221
+ if (scheme) {
222
+ var searchRoot = path.dirname(path.resolve(projectPath || '.'));
223
+ var files = sharedSchemeFiles(searchRoot, 3, []);
224
+ for (var k = 0; k < files.length; k++) {
225
+ if (path.basename(files[k]) !== scheme + '.xcscheme') continue;
226
+ var named = runnableAppProducts(files[k]);
227
+ for (var n = 0; n < named.length; n++) {
228
+ if (schemeProducts.indexOf(named[n]) === -1) schemeProducts.push(named[n]);
229
+ }
230
+ }
231
+ }
232
+
233
+ var suffix = platformSuffix(destination);
234
+ var onPlatform = apps.filter(function (a) { return endsWith(a.platform, suffix); });
235
+
236
+ function firstNamed(list) {
237
+ for (var i = 0; i < list.length; i++) {
238
+ if (schemeProducts.indexOf(list[i].name) !== -1) return list[i];
239
+ }
240
+ return null;
241
+ }
242
+
243
+ var pick = firstNamed(onPlatform) || firstNamed(apps) || onPlatform[0] || apps[0];
244
+ if (!pick) process.exit(1);
245
+ process.stdout.write(pick.path);
246
+ `;
247
+ //# sourceMappingURL=xcode-app-product.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=xcode-app-product.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xcode-app-product.test.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/xcode-app-product.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * The simulator build must export the app the scheme builds.
3
+ *
4
+ * A project whose scheme builds an iOS app and a watchOS companion produces two
5
+ * bundles under Products, and the step took whichever a directory scan reached
6
+ * first. It reached the watch companion: a green build whose artifact manifest
7
+ * held a 588 KB watch app and not the application the scheme is named after.
8
+ * An empty manifest is visibly wrong; a plausible wrong answer is not, and a
9
+ * watch companion is common enough that this would not have stayed rare.
10
+ *
11
+ * Two halves are tested here — the rules, directly, and the resolver script
12
+ * that runs on the build machine, by running it against a real directory tree.
13
+ * The second is what the step actually ships, so the first alone would be a
14
+ * test of a description.
15
+ */
16
+ import { execFileSync } from 'node:child_process';
17
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
18
+ import { tmpdir } from 'node:os';
19
+ import { join } from 'node:path';
20
+ import { APP_PRODUCT_RESOLVER_SOURCE, chooseAppProduct, platformSuffixForDestination, runnableAppProducts, } from './xcode-app-product.js';
21
+ const app = (platform, name) => ({
22
+ platform,
23
+ name,
24
+ path: `/tmp/Products/${platform}/${name}`,
25
+ });
26
+ /** A scheme whose build action runs `products`, with `listed` merely referenced. */
27
+ const schemeXml = (products, listed = []) => {
28
+ const entry = (name, runs) => `
29
+ <BuildActionEntry buildForRunning = "${runs ? 'YES' : 'NO'}" buildForArchiving = "${runs ? 'YES' : 'NO'}">
30
+ <BuildableReference BuildableName = "${name}"></BuildableReference>
31
+ </BuildActionEntry>`;
32
+ return `<?xml version="1.0" encoding="UTF-8"?>
33
+ <Scheme version = "1.7">
34
+ <BuildAction parallelizeBuildables = "YES">
35
+ <BuildActionEntries>${products.map((p) => entry(p, true)).join('')}${listed
36
+ .map((p) => entry(p, false))
37
+ .join('')}
38
+ </BuildActionEntries>
39
+ </BuildAction>
40
+ </Scheme>
41
+ `;
42
+ };
43
+ describe('platformSuffixForDestination', () => {
44
+ test('an iOS simulator destination names the iphonesimulator products', () => {
45
+ expect(platformSuffixForDestination('generic/platform=iOS Simulator')).toBe('-iphonesimulator');
46
+ });
47
+ test.each([
48
+ ['generic/platform=watchOS Simulator', '-watchsimulator'],
49
+ ['generic/platform=tvOS Simulator', '-appletvsimulator'],
50
+ ['generic/platform=visionOS Simulator', '-xrsimulator'],
51
+ ])('%s builds into %s', (destination, suffix) => {
52
+ expect(platformSuffixForDestination(destination)).toBe(suffix);
53
+ });
54
+ // The default matters: an unrecognised or empty destination must not send the
55
+ // resolver looking in a directory nothing built into.
56
+ test('an unreadable destination falls back to iOS rather than to nothing', () => {
57
+ expect(platformSuffixForDestination('')).toBe('-iphonesimulator');
58
+ });
59
+ });
60
+ describe('runnableAppProducts', () => {
61
+ test('reads the application bundles a scheme builds', () => {
62
+ expect(runnableAppProducts(schemeXml(['MyApp.app']))).toEqual(['MyApp.app']);
63
+ });
64
+ // A scheme can carry several .app entries that are compatibility testers in
65
+ // other containers, beside the one product it really builds. "Mentions an
66
+ // app" would pick just as badly as a directory scan does.
67
+ test('ignores an entry the scheme lists but does not build', () => {
68
+ const xml = schemeXml(['MyApp.app'], ['APITester.app', 'ObjCAPITester.app']);
69
+ expect(runnableAppProducts(xml)).toEqual(['MyApp.app']);
70
+ });
71
+ test('ignores a product that is not an application bundle', () => {
72
+ expect(runnableAppProducts(schemeXml(['MyLib.framework', 'MyApp.app']))).toEqual(['MyApp.app']);
73
+ });
74
+ // No evidence, which the caller must not read as "builds no app".
75
+ test.each([
76
+ ['a file that could not be read', null],
77
+ ['a scheme with no build action', '<?xml version="1.0"?><Scheme></Scheme>'],
78
+ ])('%s yields nothing rather than an answer', (_label, xml) => {
79
+ expect(runnableAppProducts(xml)).toEqual([]);
80
+ });
81
+ });
82
+ describe('chooseAppProduct', () => {
83
+ const IOS = 'generic/platform=iOS Simulator';
84
+ // The defect, in one assertion.
85
+ test('takes the scheme’s own app over a companion built beside it', () => {
86
+ const built = [
87
+ app('Debug-watchsimulator', 'Companion Watch App.app'),
88
+ app('Debug-iphonesimulator', 'MyApp.app'),
89
+ ];
90
+ expect(chooseAppProduct(built, ['MyApp.app'], IOS)?.name).toBe('MyApp.app');
91
+ });
92
+ // And it must not depend on scan order, which is the property the old code
93
+ // had no opinion about at all.
94
+ test('takes it whichever order the products were found in', () => {
95
+ const built = [
96
+ app('Debug-iphonesimulator', 'MyApp.app'),
97
+ app('Debug-watchsimulator', 'Companion Watch App.app'),
98
+ ];
99
+ expect(chooseAppProduct(built, ['MyApp.app'], IOS)?.name).toBe('MyApp.app');
100
+ });
101
+ // The single-target shape, which must not regress.
102
+ test('takes the only app there is', () => {
103
+ const built = [app('Debug-iphonesimulator', 'Contributions.app')];
104
+ expect(chooseAppProduct(built, ['Contributions.app'], IOS)?.name).toBe('Contributions.app');
105
+ });
106
+ // Absence of evidence is not evidence: a project with no shared scheme is
107
+ // ordinary, and those builds worked before this resolver existed.
108
+ test('still resolves an app when the scheme says nothing', () => {
109
+ const built = [
110
+ app('Debug-watchsimulator', 'Watch.app'),
111
+ app('Debug-iphonesimulator', 'MyApp.app'),
112
+ ];
113
+ expect(chooseAppProduct(built, [], IOS)?.name).toBe('MyApp.app');
114
+ });
115
+ test('falls back to any platform when none matches the destination', () => {
116
+ const built = [app('Debug-maccatalyst', 'MyApp.app')];
117
+ expect(chooseAppProduct(built, [], IOS)?.name).toBe('MyApp.app');
118
+ });
119
+ // The scheme outranks the platform directory, not the other way round: a
120
+ // watchOS-only scheme built for watchOS still gets its own product.
121
+ test('honours the scheme when the destination is the companion’s', () => {
122
+ const built = [
123
+ app('Debug-iphonesimulator', 'MyApp.app'),
124
+ app('Debug-watchsimulator', 'Watch.app'),
125
+ ];
126
+ const watch = 'generic/platform=watchOS Simulator';
127
+ expect(chooseAppProduct(built, ['Watch.app'], watch)?.name).toBe('Watch.app');
128
+ });
129
+ test('has no answer when nothing was built', () => {
130
+ expect(chooseAppProduct([], ['MyApp.app'], IOS)).toBeNull();
131
+ });
132
+ });
133
+ /**
134
+ * The script the step actually ships, run against a real tree.
135
+ *
136
+ * The rules above are a description of what should happen; this is the thing
137
+ * that happens. A resolver that is correct and unshippable — a syntax error, a
138
+ * heredoc that swallows a character, a path assumption that only holds in the
139
+ * unit test — passes every assertion above and delivers the watch app.
140
+ */
141
+ describe('the resolver script, as the build machine runs it', () => {
142
+ let root;
143
+ beforeEach(() => {
144
+ root = mkdtempSync(join(tmpdir(), 'cibuild-app-product-'));
145
+ });
146
+ afterEach(() => {
147
+ rmSync(root, { recursive: true, force: true });
148
+ });
149
+ const products = () => join(root, 'build', 'DerivedData', 'Build', 'Products');
150
+ function builtApp(platform, name) {
151
+ const dir = join(products(), platform, name);
152
+ mkdirSync(dir, { recursive: true });
153
+ writeFileSync(join(dir, 'Info.plist'), '<plist/>\n');
154
+ }
155
+ /** A shared scheme where Xcode puts one, inside the project bundle. */
156
+ function sharedScheme(project, name, xml) {
157
+ const dir = join(root, project, 'xcshareddata', 'xcschemes');
158
+ mkdirSync(dir, { recursive: true });
159
+ writeFileSync(join(dir, `${name}.xcscheme`), xml);
160
+ }
161
+ function resolve(scheme, destination = 'generic/platform=iOS Simulator') {
162
+ const script = join(root, 'resolver.cjs');
163
+ writeFileSync(script, APP_PRODUCT_RESOLVER_SOURCE);
164
+ try {
165
+ return execFileSync(process.execPath, [script, products(), join(root, 'MyApp.xcodeproj'), scheme, destination], { encoding: 'utf-8' });
166
+ }
167
+ catch {
168
+ return '';
169
+ }
170
+ }
171
+ test('exports the app under test, not the watch companion beside it', () => {
172
+ builtApp('Debug-watchsimulator', 'MyApp Watch App.app');
173
+ builtApp('Debug-iphonesimulator', 'MyApp.app');
174
+ sharedScheme('MyApp.xcodeproj', 'MyApp', schemeXml(['MyApp.app']));
175
+ expect(resolve('MyApp')).toBe(join(products(), 'Debug-iphonesimulator', 'MyApp.app'));
176
+ });
177
+ test('exports the only app of a single-target project', () => {
178
+ builtApp('Debug-iphonesimulator', 'Contributions.app');
179
+ sharedScheme('MyApp.xcodeproj', 'MyApp', schemeXml(['Contributions.app']));
180
+ expect(resolve('MyApp')).toBe(join(products(), 'Debug-iphonesimulator', 'Contributions.app'));
181
+ });
182
+ test('still resolves an app for a project with no shared scheme at all', () => {
183
+ builtApp('Debug-iphonesimulator', 'MyApp.app');
184
+ expect(resolve('MyApp')).toBe(join(products(), 'Debug-iphonesimulator', 'MyApp.app'));
185
+ });
186
+ test('reads a scheme shared by a sibling project in the same directory', () => {
187
+ builtApp('Debug-iphonesimulator', 'MyApp.app');
188
+ builtApp('Debug-watchsimulator', 'MyApp Watch App.app');
189
+ sharedScheme('Other.xcodeproj', 'MyApp', schemeXml(['MyApp.app']));
190
+ expect(resolve('MyApp')).toBe(join(products(), 'Debug-iphonesimulator', 'MyApp.app'));
191
+ });
192
+ // Nothing built is the caller's guard to report, not this script's to guess
193
+ // at: it must say nothing rather than print a path that does not exist.
194
+ test('prints nothing when the build produced no app', () => {
195
+ mkdirSync(products(), { recursive: true });
196
+ expect(resolve('MyApp')).toBe('');
197
+ });
198
+ test('prints nothing when there is no Products directory', () => {
199
+ expect(resolve('MyApp')).toBe('');
200
+ });
201
+ });
202
+ //# sourceMappingURL=xcode-app-product.test.js.map
@@ -28,6 +28,7 @@
28
28
  */
29
29
  import { describe, test, expect } from '@jest/globals';
30
30
  import { XcodeBuildStepExecutor, XcodeBuildForTestStepExecutor, XcodeBuildForSimulatorStepExecutor, } from './xcode.js';
31
+ import { APP_PRODUCT_RESOLVER_SOURCE } from './xcode-app-product.js';
31
32
  import { testConfig } from './test-config.js';
32
33
  const DERIVED = '-derivedDataPath "$OUTPUT_DIR/DerivedData"';
33
34
  const scriptFor = async (executor, inputs) => {
@@ -70,7 +71,8 @@ describe('xcode-build-for-simulator finds the .app where it now lands', () => {
70
71
  const build = (inputs = {}) => scriptFor(new XcodeBuildForSimulatorStepExecutor(), inputs);
71
72
  test('searches under the per-platform Products directory', async () => {
72
73
  const script = await build();
73
- expect(script).toContain('find "$OUTPUT_DIR/DerivedData/Build/Products"');
74
+ expect(script).toContain('PRODUCTS_DIR="$OUTPUT_DIR/DerivedData/Build/Products"');
75
+ expect(script).toContain('find "$PRODUCTS_DIR"');
74
76
  });
75
77
  test('searches deep enough to reach <config>-<platform>/Foo.app', async () => {
76
78
  // Products/Debug-iphonesimulator/MyApp.app is two levels down. maxdepth 1
@@ -88,6 +90,69 @@ describe('xcode-build-for-simulator finds the .app where it now lands', () => {
88
90
  test('still exports the app path for later steps', async () => {
89
91
  expect(await build()).toContain('envman add --key CIBUILD_APP_DIR_PATH');
90
92
  });
93
+ // Following the products is not enough where a scheme builds a companion
94
+ // alongside the app: two bundles land and the scan cannot tell them apart.
95
+ // The scheme is asked first, and it is asked with the scheme and destination
96
+ // this step was actually given — a resolver told the wrong scheme name
97
+ // answers confidently and wrongly.
98
+ test('asks the scheme before falling back to a scan', async () => {
99
+ const script = await build({ scheme: 'MyApp', destination: 'generic/platform=iOS Simulator' });
100
+ const call = script.split('\n').find((l) => l.includes('node "$APP_RESOLVER"'));
101
+ expect(call).toBeDefined();
102
+ expect(call).toContain("'MyApp'");
103
+ expect(call).toContain("'generic/platform=iOS Simulator'");
104
+ // The scan is the fallback now, not the first answer.
105
+ expect(script.indexOf('node "$APP_RESOLVER"')).toBeLessThan(script.indexOf('-name "*.app"'));
106
+ });
107
+ // set -e is on, so a machine without a usable node must not fail the step
108
+ // before the scan it would have fallen back to has run.
109
+ test('a resolver that cannot run falls through instead of failing the build', async () => {
110
+ const call = (await build())
111
+ .split('\n')
112
+ .find((l) => l.includes('node "$APP_RESOLVER"'));
113
+ expect(call).toContain('|| true');
114
+ });
115
+ test('the resolver it writes is the one under test', async () => {
116
+ expect(await build()).toContain(APP_PRODUCT_RESOLVER_SOURCE.trim());
117
+ });
118
+ });
119
+ /**
120
+ * Delivery belonged to the pipeline and should have belonged to the build.
121
+ *
122
+ * gradle-build copies its own .apk into the artifacts directory. This step
123
+ * located the bundle, exported the path, and left it in DerivedData — which
124
+ * artifact discovery excludes. So an iOS proof could go green and hand the user
125
+ * nothing to install, and only the pipelines that added a collecting step of
126
+ * their own delivered anything.
127
+ */
128
+ describe('xcode-build-for-simulator delivers the app it built', () => {
129
+ const build = (inputs = {}) => scriptFor(new XcodeBuildForSimulatorStepExecutor(), inputs);
130
+ test('copies its own output into the artifacts directory', async () => {
131
+ const script = await build();
132
+ expect(script).toContain('ARTIFACTS_DIR="${CIBUILD_ARTIFACTS_DIR:-.ci/artifacts}"');
133
+ expect(script).toContain('mkdir -p "$ARTIFACTS_DIR"');
134
+ });
135
+ // A .app is a directory. Staged as one it becomes an artifact per file
136
+ // inside it, and an internal symlink pointing outside the workspace fails
137
+ // the stage. A .zip is a plain file and stages as exactly one artifact.
138
+ test('delivers one zip rather than the bundle directory', async () => {
139
+ const script = await build();
140
+ expect(script).toContain('ditto -c -k --keepParent "$APP_PATH" "$ARTIFACTS_DIR/$APP_NAME.zip"');
141
+ });
142
+ // The app it exported and the app it delivers are the same one, or the
143
+ // manifest disagrees with CIBUILD_APP_DIR_PATH and the wrong bundle ships
144
+ // under a right-looking name.
145
+ test('delivers the bundle it exported, not another scan of the tree', async () => {
146
+ const script = await build();
147
+ expect(script).toContain('APP_NAME="$(basename "$APP_PATH")"');
148
+ expect(script.indexOf('envman add --key CIBUILD_APP_DIR_PATH')).toBeLessThan(script.indexOf('ditto -c -k'));
149
+ });
150
+ // A pipeline that already collects the app runs after this and writes the
151
+ // same path; re-running the build must not leave two zips either.
152
+ test('replaces its own previous zip rather than appending to it', async () => {
153
+ const script = await build();
154
+ expect(script.indexOf('rm -f "$ARTIFACTS_DIR/$APP_NAME.zip"')).toBeLessThan(script.indexOf('ditto -c -k'));
155
+ });
91
156
  });
92
157
  describe('the output_dir input still steers where everything goes', () => {
93
158
  // The template gives "Build App" its own output_dir, so derived data has to
@@ -1 +1 @@
1
- {"version":3,"file":"xcode.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/xcode.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"xcode.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/xcode.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,7 @@
4
4
  import { existsSync } from 'node:fs';
5
5
  import { BaseStepExecutor } from './base.js';
6
6
  import { resolveDestinationInput } from './xcode-destination.js';
7
+ import { APP_PRODUCT_RESOLVER_SOURCE } from './xcode-app-product.js';
7
8
  import { refusedFetchHelpers, retryOnRefusedFetch } from './git-fetch-retry.js';
8
9
  /**
9
10
  * Xcodebuild step executor
@@ -1097,10 +1098,27 @@ export class XcodeBuildForSimulatorStepExecutor extends BaseStepExecutor {
1097
1098
  // rather than flat in $OUTPUT_DIR, so the guard has to follow it — at
1098
1099
  // maxdepth 1 this found nothing and the step would fail every build with
1099
1100
  // ".app not found" while the app had compiled perfectly.
1100
- commands.push('# Locate generated .app');
1101
- commands.push('APP_PATH=$(find "$OUTPUT_DIR/DerivedData/Build/Products" -maxdepth 2 -name "*.app" -type d 2>/dev/null | head -1)');
1101
+ //
1102
+ // Following the products is not enough on its own, though, because a scheme
1103
+ // that builds a watchOS or tvOS companion produces more than one bundle and
1104
+ // a scan has no way to tell them apart. Ask the scheme, which knows: see
1105
+ // xcode-app-product.ts. The scan below is what runs when the scheme cannot
1106
+ // answer — a project with no shared scheme is ordinary and builds fine, so
1107
+ // this resolves the same bundle it always did there.
1108
+ commands.push('# Locate generated .app — the one this scheme builds');
1109
+ commands.push('PRODUCTS_DIR="$OUTPUT_DIR/DerivedData/Build/Products"');
1110
+ commands.push('APP_RESOLVER="$(mktemp -t cibuild-app-product).cjs"');
1111
+ commands.push(`cat > "$APP_RESOLVER" << 'CIBUILD_APP_PRODUCT_EOF'`);
1112
+ commands.push(APP_PRODUCT_RESOLVER_SOURCE.trim());
1113
+ commands.push('CIBUILD_APP_PRODUCT_EOF');
1114
+ commands.push('APP_PATH=$(node "$APP_RESOLVER" "$PRODUCTS_DIR"' +
1115
+ ` '${escapedPath}' '${escapedScheme}' '${this.escapeBash(destination)}' 2>/dev/null || true)`);
1116
+ commands.push('rm -f "$APP_RESOLVER"');
1117
+ commands.push('if [ -z "$APP_PATH" ]; then');
1118
+ commands.push(' APP_PATH=$(find "$PRODUCTS_DIR" -maxdepth 2 -name "*.app" -type d 2>/dev/null | head -1)');
1119
+ commands.push('fi');
1102
1120
  commands.push('if [ -z "$APP_PATH" ]; then');
1103
- commands.push(' echo "❌ Error: .app not found under $OUTPUT_DIR/DerivedData/Build/Products"');
1121
+ commands.push(' echo "❌ Error: .app not found under $PRODUCTS_DIR"');
1104
1122
  commands.push(' exit 1');
1105
1123
  commands.push('fi');
1106
1124
  commands.push('');
@@ -1109,6 +1127,35 @@ export class XcodeBuildForSimulatorStepExecutor extends BaseStepExecutor {
1109
1127
  commands.push('');
1110
1128
  // Export
1111
1129
  commands.push('envman add --key CIBUILD_APP_DIR_PATH --value "$APP_PATH"');
1130
+ commands.push('');
1131
+ // Deliver it, the way gradle-build delivers its .apk.
1132
+ //
1133
+ // gradle-build copies its own output into the artifacts directory; this
1134
+ // step located the bundle, exported the path and left it in DerivedData —
1135
+ // which the runner excludes from artifact discovery. So the app was
1136
+ // findable and deliberately not found, and an iOS proof could go green
1137
+ // handing the user nothing to install. Pipelines that added a collecting
1138
+ // step of their own delivered; every other one did not, which made
1139
+ // delivery a property of the pipeline rather than of the build.
1140
+ //
1141
+ // It zips rather than copying the bundle in, and that is not cosmetic. A
1142
+ // .app is a directory: staged as a directory it becomes one artifact per
1143
+ // file inside it — every asset, nib and framework separately — and an
1144
+ // internal symlink pointing outside the workspace fails the stage
1145
+ // outright. A .zip is a plain file, stages as exactly one artifact, and is
1146
+ // what someone downloading it wants anyway. ditto is the macOS-native
1147
+ // archiver and preserves the bundle's permissions and symlinks, which a
1148
+ // plain recursive zip does not do faithfully.
1149
+ commands.push('# Deliver the app as one artifact');
1150
+ commands.push('ARTIFACTS_DIR="${CIBUILD_ARTIFACTS_DIR:-.ci/artifacts}"');
1151
+ commands.push('mkdir -p "$ARTIFACTS_DIR"');
1152
+ commands.push('APP_NAME="$(basename "$APP_PATH")"');
1153
+ commands.push('rm -f "$ARTIFACTS_DIR/$APP_NAME.zip"');
1154
+ commands.push('ditto -c -k --keepParent "$APP_PATH" "$ARTIFACTS_DIR/$APP_NAME.zip"');
1155
+ commands.push('echo " → Collected: $APP_NAME.zip"');
1156
+ commands.push('echo "✅ Artifacts copied to: $ARTIFACTS_DIR"');
1157
+ commands.push('ls -lh "$ARTIFACTS_DIR"');
1158
+ commands.push('');
1112
1159
  commands.push('rm -f "$XCCONFIG_FILE"');
1113
1160
  const script = this.createBashScriptFromCommands(commands, stepName);
1114
1161
  return this.createScriptStep(script, stepName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "2.7.9",
3
+ "version": "2.8.0",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",