@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.
- package/dist/cli.cjs +143 -10
- package/dist/src/commands/android-java-version.test.js +93 -10
- package/dist/src/commands/android-scanner.d.ts +87 -8
- package/dist/src/commands/android-scanner.d.ts.map +1 -1
- package/dist/src/commands/android-scanner.js +133 -27
- package/dist/src/commands/build.d.ts +8 -0
- package/dist/src/commands/build.d.ts.map +1 -1
- package/dist/src/commands/build.js +17 -3
- package/dist/src/yaml/steps/xcode-app-product.d.ts +90 -0
- package/dist/src/yaml/steps/xcode-app-product.d.ts.map +1 -0
- package/dist/src/yaml/steps/xcode-app-product.js +247 -0
- package/dist/src/yaml/steps/xcode-app-product.test.d.ts +2 -0
- package/dist/src/yaml/steps/xcode-app-product.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/xcode-app-product.test.js +202 -0
- package/dist/src/yaml/steps/xcode-derived-data.test.js +66 -1
- package/dist/src/yaml/steps/xcode.d.ts.map +1 -1
- package/dist/src/yaml/steps/xcode.js +50 -3
- package/package.json +1 -1
|
@@ -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 @@
|
|
|
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('
|
|
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":"AAAA;;GAEG;AAGH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;
|
|
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
|
-
|
|
1101
|
-
|
|
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 $
|
|
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);
|