@invarn/cibuild 2.7.0 → 2.7.2
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 +10 -10
- package/dist/src/commands/ios-scanner.d.ts +44 -8
- package/dist/src/commands/ios-scanner.d.ts.map +1 -1
- package/dist/src/commands/ios-scanner.js +52 -10
- package/dist/src/commands/ios-scheme-ranking.test.js +109 -2
- package/dist/src/yaml/a-declared-secret-reaches-the-file-step.test.d.ts +20 -0
- package/dist/src/yaml/a-declared-secret-reaches-the-file-step.test.d.ts.map +1 -0
- package/dist/src/yaml/a-declared-secret-reaches-the-file-step.test.js +201 -0
- package/dist/src/yaml/env-resolver.d.ts +33 -0
- package/dist/src/yaml/env-resolver.d.ts.map +1 -1
- package/dist/src/yaml/env-resolver.js +54 -2
- package/dist/src/yaml/steps/git-fetch-retry-exec.test.d.ts +10 -0
- package/dist/src/yaml/steps/git-fetch-retry-exec.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/git-fetch-retry-exec.test.js +156 -0
- package/dist/src/yaml/steps/git-fetch-retry.d.ts +62 -0
- package/dist/src/yaml/steps/git-fetch-retry.d.ts.map +1 -0
- package/dist/src/yaml/steps/git-fetch-retry.js +155 -0
- package/dist/src/yaml/steps/git-fetch-retry.test.d.ts +5 -0
- package/dist/src/yaml/steps/git-fetch-retry.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/git-fetch-retry.test.js +86 -0
- package/dist/src/yaml/steps/ios-deps.d.ts.map +1 -1
- package/dist/src/yaml/steps/ios-deps.js +14 -4
- package/dist/src/yaml/steps/xcode-resolve-retry.test.d.ts +9 -0
- package/dist/src/yaml/steps/xcode-resolve-retry.test.d.ts.map +1 -0
- package/dist/src/yaml/steps/xcode-resolve-retry.test.js +111 -0
- package/dist/src/yaml/steps/xcode.d.ts.map +1 -1
- package/dist/src/yaml/steps/xcode.js +28 -0
- package/package.json +1 -1
|
@@ -44,18 +44,54 @@ export interface IosScanResult {
|
|
|
44
44
|
*
|
|
45
45
|
* 1. named after the project or workspace — the commonest case, and
|
|
46
46
|
* unambiguous;
|
|
47
|
-
* 2. launches the product named after the project, and is not
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* 3. launches an `.app`, declares tests (the template runs them), and is not
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* 4. launches an `.app` and is not named like a companion;
|
|
47
|
+
* 2. launches the product named after the project, and is not a companion.
|
|
48
|
+
* `thebaselab/codeapp` is why this outranks the next one: its `Code App`
|
|
49
|
+
* scheme launches `Code.app` and declares no tests at all, while `Code UI`
|
|
50
|
+
* has two test bundles and launches `CodeUI.app`;
|
|
51
|
+
* 3. launches an `.app`, declares tests (the template runs them), and is not a
|
|
52
|
+
* companion; among several, the iOS or Debug one, then the shortest;
|
|
53
|
+
* 4. launches an `.app` and is not a companion;
|
|
55
54
|
* 5. whatever order it came in.
|
|
56
55
|
*
|
|
57
56
|
* Every scheme stays in the list — this reorders, it does not filter, so a
|
|
58
57
|
* caller wanting the extension can still find it.
|
|
58
|
+
*
|
|
59
|
+
* **What "companion" has to mean, and why a name is not enough.**
|
|
60
|
+
* `mozilla-mobile/firefox-ios` broke tiers 1-4 without touching the directory
|
|
61
|
+
* order this function was written for. Its project is `Client.xcodeproj`, so
|
|
62
|
+
* `projectName` is `Client`. An app extension is launched *through its host
|
|
63
|
+
* app*, so the `Today` widget scheme's LaunchAction runnable is `Client.app` —
|
|
64
|
+
* identical to `Fennec`'s. Both landed in tier 1, neither name carries an `iOS`
|
|
65
|
+
* or `Debug` token, and the tie fell to name length:
|
|
66
|
+
*
|
|
67
|
+
* "Today" -> 5 characters <- won
|
|
68
|
+
* "Fennec" -> 6 characters
|
|
69
|
+
*
|
|
70
|
+
* The build then failed compiling `ActionExtension` rather than the browser
|
|
71
|
+
* (`bld_b1b38dda58f4111080415283`).
|
|
72
|
+
*
|
|
73
|
+
* Launching the app binary is therefore not sufficient, and the answer is not a
|
|
74
|
+
* longer name blocklist either — `Today` names nothing. Xcode states the fact
|
|
75
|
+
* outright: it writes `wasCreatedForAppExtension = "YES"` onto the `<Scheme>`
|
|
76
|
+
* element of a scheme it created for an extension target, so that is what is
|
|
77
|
+
* read.
|
|
78
|
+
*
|
|
79
|
+
* Measured over all 125 shared schemes in the thirty-app study's clones: 29
|
|
80
|
+
* carry the attribute, and **all 29 are extension schemes** — no false
|
|
81
|
+
* positive. It is also the only signal that would have worked. Two that were
|
|
82
|
+
* considered and are wrong on this same evidence:
|
|
83
|
+
*
|
|
84
|
+
* - *require an `.app` among the BuildAction entries.* 27 of the 29 extension
|
|
85
|
+
* schemes build the host app too — `Today` builds
|
|
86
|
+
* `["Today.appex", "Client.app"]` — so this demotes 2 of 29 and leaves
|
|
87
|
+
* firefox-ios picking `Today`.
|
|
88
|
+
* - *demote anything building an `.appex`.* `chenqi92/primuse`'s `Primuse`
|
|
89
|
+
* scheme is the real app scheme and builds two widget extensions alongside
|
|
90
|
+
* `Primuse.app`.
|
|
91
|
+
*
|
|
92
|
+
* The attribute is a sufficient signal, not a necessary one: a hand-written or
|
|
93
|
+
* pre-Xcode-10 scheme may omit it, and its absence changes nothing, so nothing
|
|
94
|
+
* that ranked before can regress on a scheme that does not carry it.
|
|
59
95
|
*/
|
|
60
96
|
export declare function rankSchemes(root: string, projectPath: string, schemes: string[]): string[];
|
|
61
97
|
/** The ranking, plus the sentence explaining its first entry. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ios-scanner.d.ts","sourceRoot":"","sources":["../../../src/commands/ios-scanner.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,kBAAkB,GAAG,cAAc,GAAG,SAAS,GAAG,gBAAgB,GAAG,WAAW,CAAC;AAE7F,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,MAAM,CAAC;AAEpD,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB;;;OAGG;IACH,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oDAAoD;IACpD,YAAY,EAAE,OAAO,CAAC;IACtB,0DAA0D;IAC1D,MAAM,EAAE,OAAO,CAAC;IAChB,+DAA+D;IAC/D,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iFAAiF;IACjF,eAAe,EAAE,MAAM,CAAC;CACzB;
|
|
1
|
+
{"version":3,"file":"ios-scanner.d.ts","sourceRoot":"","sources":["../../../src/commands/ios-scanner.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,kBAAkB,GAAG,cAAc,GAAG,SAAS,GAAG,gBAAgB,GAAG,WAAW,CAAC;AAE7F,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,MAAM,CAAC;AAEpD,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB;;;OAGG;IACH,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oDAAoD;IACpD,YAAY,EAAE,OAAO,CAAC;IACtB,0DAA0D;IAC1D,MAAM,EAAE,OAAO,CAAC;IAChB,+DAA+D;IAC/D,WAAW,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iFAAiF;IACjF,eAAe,EAAE,MAAM,CAAC;CACzB;AAuFD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EAAE,GAChB,MAAM,EAAE,CAEV;AAED,iEAAiE;AACjE,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EAAE,GAChB;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CA8CxC;AAmLD,wBAAsB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAgGhF;AAaD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAkDjE"}
|
|
@@ -60,9 +60,13 @@ function readSchemeFacts(root, name) {
|
|
|
60
60
|
if (!xml)
|
|
61
61
|
continue;
|
|
62
62
|
const launch = /<LaunchAction[\s\S]*?<\/LaunchAction>/.exec(xml)?.[0] ?? "";
|
|
63
|
+
// Attribute values carry no `>`, so the opening tag alone is a safe scope
|
|
64
|
+
// and no attribute deeper in the document can be mistaken for it.
|
|
65
|
+
const schemeTag = /<Scheme\b[^>]*>/.exec(xml)?.[0] ?? "";
|
|
63
66
|
return {
|
|
64
67
|
runnable: /BuildableName\s*=\s*"([^"]+)"/.exec(launch)?.[1],
|
|
65
68
|
testable: /<TestableReference/.test(xml),
|
|
69
|
+
appExtension: /wasCreatedForAppExtension\s*=\s*"YES"/.test(schemeTag),
|
|
66
70
|
};
|
|
67
71
|
}
|
|
68
72
|
return undefined;
|
|
@@ -84,18 +88,54 @@ function readSchemeFacts(root, name) {
|
|
|
84
88
|
*
|
|
85
89
|
* 1. named after the project or workspace — the commonest case, and
|
|
86
90
|
* unambiguous;
|
|
87
|
-
* 2. launches the product named after the project, and is not
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* 3. launches an `.app`, declares tests (the template runs them), and is not
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* 4. launches an `.app` and is not named like a companion;
|
|
91
|
+
* 2. launches the product named after the project, and is not a companion.
|
|
92
|
+
* `thebaselab/codeapp` is why this outranks the next one: its `Code App`
|
|
93
|
+
* scheme launches `Code.app` and declares no tests at all, while `Code UI`
|
|
94
|
+
* has two test bundles and launches `CodeUI.app`;
|
|
95
|
+
* 3. launches an `.app`, declares tests (the template runs them), and is not a
|
|
96
|
+
* companion; among several, the iOS or Debug one, then the shortest;
|
|
97
|
+
* 4. launches an `.app` and is not a companion;
|
|
95
98
|
* 5. whatever order it came in.
|
|
96
99
|
*
|
|
97
100
|
* Every scheme stays in the list — this reorders, it does not filter, so a
|
|
98
101
|
* caller wanting the extension can still find it.
|
|
102
|
+
*
|
|
103
|
+
* **What "companion" has to mean, and why a name is not enough.**
|
|
104
|
+
* `mozilla-mobile/firefox-ios` broke tiers 1-4 without touching the directory
|
|
105
|
+
* order this function was written for. Its project is `Client.xcodeproj`, so
|
|
106
|
+
* `projectName` is `Client`. An app extension is launched *through its host
|
|
107
|
+
* app*, so the `Today` widget scheme's LaunchAction runnable is `Client.app` —
|
|
108
|
+
* identical to `Fennec`'s. Both landed in tier 1, neither name carries an `iOS`
|
|
109
|
+
* or `Debug` token, and the tie fell to name length:
|
|
110
|
+
*
|
|
111
|
+
* "Today" -> 5 characters <- won
|
|
112
|
+
* "Fennec" -> 6 characters
|
|
113
|
+
*
|
|
114
|
+
* The build then failed compiling `ActionExtension` rather than the browser
|
|
115
|
+
* (`bld_b1b38dda58f4111080415283`).
|
|
116
|
+
*
|
|
117
|
+
* Launching the app binary is therefore not sufficient, and the answer is not a
|
|
118
|
+
* longer name blocklist either — `Today` names nothing. Xcode states the fact
|
|
119
|
+
* outright: it writes `wasCreatedForAppExtension = "YES"` onto the `<Scheme>`
|
|
120
|
+
* element of a scheme it created for an extension target, so that is what is
|
|
121
|
+
* read.
|
|
122
|
+
*
|
|
123
|
+
* Measured over all 125 shared schemes in the thirty-app study's clones: 29
|
|
124
|
+
* carry the attribute, and **all 29 are extension schemes** — no false
|
|
125
|
+
* positive. It is also the only signal that would have worked. Two that were
|
|
126
|
+
* considered and are wrong on this same evidence:
|
|
127
|
+
*
|
|
128
|
+
* - *require an `.app` among the BuildAction entries.* 27 of the 29 extension
|
|
129
|
+
* schemes build the host app too — `Today` builds
|
|
130
|
+
* `["Today.appex", "Client.app"]` — so this demotes 2 of 29 and leaves
|
|
131
|
+
* firefox-ios picking `Today`.
|
|
132
|
+
* - *demote anything building an `.appex`.* `chenqi92/primuse`'s `Primuse`
|
|
133
|
+
* scheme is the real app scheme and builds two widget extensions alongside
|
|
134
|
+
* `Primuse.app`.
|
|
135
|
+
*
|
|
136
|
+
* The attribute is a sufficient signal, not a necessary one: a hand-written or
|
|
137
|
+
* pre-Xcode-10 scheme may omit it, and its absence changes nothing, so nothing
|
|
138
|
+
* that ranked before can regress on a scheme that does not carry it.
|
|
99
139
|
*/
|
|
100
140
|
export function rankSchemes(root, projectPath, schemes) {
|
|
101
141
|
return rankSchemesWithReason(root, projectPath, schemes).schemes;
|
|
@@ -107,10 +147,12 @@ export function rankSchemesWithReason(root, projectPath, schemes) {
|
|
|
107
147
|
const projectName = projectPath.replace(/\.(xcodeproj|xcworkspace)$/, "").split("/").pop();
|
|
108
148
|
const scored = schemes.map((name, index) => {
|
|
109
149
|
const facts = readSchemeFacts(root, name);
|
|
110
|
-
|
|
150
|
+
// Two ways to be a companion: the name says so, or Xcode does. Xcode's
|
|
151
|
+
// word outranks a name match, so it disqualifies tier 0 as well.
|
|
152
|
+
const companion = COMPANION_SCHEME.test(name) || (facts?.appExtension ?? false);
|
|
111
153
|
const app = facts?.runnable?.endsWith(".app") ?? false;
|
|
112
154
|
let tier = 4;
|
|
113
|
-
if (name === projectName)
|
|
155
|
+
if (name === projectName && !facts?.appExtension)
|
|
114
156
|
tier = 0;
|
|
115
157
|
else if (facts?.runnable === `${projectName}.app` && !companion)
|
|
116
158
|
tier = 1;
|
|
@@ -27,11 +27,11 @@ afterEach(() => {
|
|
|
27
27
|
rmSync(root, { recursive: true, force: true });
|
|
28
28
|
});
|
|
29
29
|
/** An `.xcscheme` with the parts the ranking reads, in Xcode's own shape. */
|
|
30
|
-
function scheme(project, name, { runnable, testable }) {
|
|
30
|
+
function scheme(project, name, { runnable, testable, appExtension }) {
|
|
31
31
|
const dir = join(root, `${project}.xcodeproj`, "xcshareddata", "xcschemes");
|
|
32
32
|
mkdirSync(dir, { recursive: true });
|
|
33
33
|
writeFileSync(join(dir, `${name}.xcscheme`), `<?xml version="1.0" encoding="UTF-8"?>
|
|
34
|
-
<Scheme LastUpgradeVersion = "1600" version = "1.7">
|
|
34
|
+
<Scheme LastUpgradeVersion = "1600"${appExtension ? `\n wasCreatedForAppExtension = "YES"` : ""} version = "1.7">
|
|
35
35
|
<TestAction buildConfiguration = "Debug">
|
|
36
36
|
<Testables>${testable
|
|
37
37
|
? `
|
|
@@ -149,4 +149,111 @@ describe("rankSchemes", () => {
|
|
|
149
149
|
expect(pick("Solo.xcodeproj", ["Solo", "Other"])).toBe("Solo");
|
|
150
150
|
});
|
|
151
151
|
});
|
|
152
|
+
/**
|
|
153
|
+
* An app extension is launched *through its host app*, so an extension scheme's
|
|
154
|
+
* LaunchAction runnable is the host `.app` — the same value the app's own
|
|
155
|
+
* scheme carries. Launching the app binary is therefore not sufficient to be
|
|
156
|
+
* the app scheme, and no name blocklist reaches the case: `Today` names nothing.
|
|
157
|
+
*
|
|
158
|
+
* Xcode states the fact itself, on the `<Scheme>` element of any scheme it
|
|
159
|
+
* created for an extension target. Measured across all 125 shared schemes in
|
|
160
|
+
* the thirty-app study's clones: 29 carry `wasCreatedForAppExtension`, all 29
|
|
161
|
+
* are extension schemes, and 27 of them build the host `.app` too — which is
|
|
162
|
+
* why "require an `.app` among the BuildAction entries" does not separate them.
|
|
163
|
+
*/
|
|
164
|
+
describe("rankSchemes and app-extension schemes", () => {
|
|
165
|
+
/**
|
|
166
|
+
* `mozilla-mobile/firefox-ios`, all thirteen schemes of
|
|
167
|
+
* `firefox-ios/Client.xcodeproj`, read off the clone. The project is
|
|
168
|
+
* `Client.xcodeproj`, so five schemes launch `Client.app` and land in the
|
|
169
|
+
* same tier; none carries an `iOS` or `Debug` token, so the tie fell to name
|
|
170
|
+
* length and `Today` (5) beat `Fennec` (6). The build then failed compiling
|
|
171
|
+
* `ActionExtension` — `bld_b1b38dda58f4111080415283`.
|
|
172
|
+
*/
|
|
173
|
+
const FIREFOX_IOS = [
|
|
174
|
+
["Account", { runnable: "libAccount.a", testable: true }],
|
|
175
|
+
["Fennec", { runnable: "Client.app", testable: true }],
|
|
176
|
+
["Fennec_Enterprise", { runnable: "Client.app", testable: true }],
|
|
177
|
+
["Firefox", { runnable: "Client.app", testable: true }],
|
|
178
|
+
["FirefoxBeta", { runnable: "Client.app", testable: true }],
|
|
179
|
+
["FirefoxStaging", { runnable: "Client.app", testable: true }],
|
|
180
|
+
["L10nSnapshotTests", { runnable: "L10nSnapshotTests.xctest", testable: true }],
|
|
181
|
+
["Periphery", {}],
|
|
182
|
+
["ShareTo", { runnable: "ShareTo.appex", appExtension: true }],
|
|
183
|
+
["Storage", { runnable: "libStorage.a", testable: true }],
|
|
184
|
+
["Sync", { runnable: "Sync.framework", testable: true }],
|
|
185
|
+
["Telemetry", { runnable: "libSyncTelemetry.a", testable: true }],
|
|
186
|
+
["Today", { runnable: "Client.app", testable: true, appExtension: true }],
|
|
187
|
+
];
|
|
188
|
+
function firefoxIos() {
|
|
189
|
+
for (const [name, shape] of FIREFOX_IOS)
|
|
190
|
+
scheme("Client", name, shape);
|
|
191
|
+
return FIREFOX_IOS.map(([name]) => name);
|
|
192
|
+
}
|
|
193
|
+
test("picks the browser, not its widget", () => {
|
|
194
|
+
const names = firefoxIos();
|
|
195
|
+
expect(pick("Client.xcodeproj", names)).toBe("Fennec");
|
|
196
|
+
});
|
|
197
|
+
// The defect only became visible because a five-character extension name won
|
|
198
|
+
// a length tiebreak. The answer must not depend on the order either.
|
|
199
|
+
test("picks the browser whatever order the filesystem lists", () => {
|
|
200
|
+
const names = firefoxIos();
|
|
201
|
+
const orders = [
|
|
202
|
+
names,
|
|
203
|
+
[...names].reverse(),
|
|
204
|
+
["Today", ...names.filter((n) => n !== "Today")],
|
|
205
|
+
["ShareTo", "Today", ...names.filter((n) => n !== "Today" && n !== "ShareTo")],
|
|
206
|
+
];
|
|
207
|
+
for (const order of orders) {
|
|
208
|
+
expect([order[0], pick("Client.xcodeproj", order)]).toEqual([order[0], "Fennec"]);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
test("still keeps every scheme in the list", () => {
|
|
212
|
+
const names = firefoxIos();
|
|
213
|
+
expect([...rankSchemes(root, "Client.xcodeproj", names)].sort()).toEqual([...names].sort());
|
|
214
|
+
});
|
|
215
|
+
// The narrow statement, without the other twelve schemes around it.
|
|
216
|
+
test("an extension scheme launching the host app is not the app scheme", () => {
|
|
217
|
+
scheme("Client", "Fennec", { runnable: "Client.app", testable: true });
|
|
218
|
+
scheme("Client", "Today", { runnable: "Client.app", testable: true, appExtension: true });
|
|
219
|
+
expect(pick("Client.xcodeproj", ["Today", "Fennec"])).toBe("Fennec");
|
|
220
|
+
});
|
|
221
|
+
// Xcode's own word about the target outranks a name coincidence, so it
|
|
222
|
+
// disqualifies the name-match tier too.
|
|
223
|
+
test("outranks a scheme named exactly after the project", () => {
|
|
224
|
+
scheme("Widget", "Widget", { runnable: "Host.app", appExtension: true });
|
|
225
|
+
scheme("Widget", "HostApp", { runnable: "Host.app", testable: true });
|
|
226
|
+
expect(pick("Widget.xcodeproj", ["Widget", "HostApp"])).toBe("HostApp");
|
|
227
|
+
});
|
|
228
|
+
/**
|
|
229
|
+
* The attribute is a sufficient signal, not a necessary one — a hand-written
|
|
230
|
+
* or pre-Xcode-10 scheme may omit it. Its absence must change nothing, or
|
|
231
|
+
* every scheme that ranked correctly before this could regress.
|
|
232
|
+
*
|
|
233
|
+
* `chenqi92/primuse` is the row that makes the point concrete: its `Primuse`
|
|
234
|
+
* scheme is the real app scheme and builds two widget `.appex` bundles
|
|
235
|
+
* alongside `Primuse.app`, so "demote anything that builds an extension"
|
|
236
|
+
* would have taken the wrong one.
|
|
237
|
+
*/
|
|
238
|
+
test("an untagged app scheme ranks exactly as it did before", () => {
|
|
239
|
+
scheme("Primuse", "Primuse", { runnable: "Primuse.app", testable: true });
|
|
240
|
+
scheme("Primuse", "PrimuseWidgets", { runnable: "Primuse.app" });
|
|
241
|
+
expect(pick("Primuse.xcodeproj", ["PrimuseWidgets", "Primuse"])).toBe("Primuse");
|
|
242
|
+
});
|
|
243
|
+
// When every candidate is an extension scheme there is no app scheme to
|
|
244
|
+
// find. Both land in the bottom tier, nothing is filtered out, and the
|
|
245
|
+
// pre-existing within-tier rule decides — so the caller still gets a
|
|
246
|
+
// deterministic answer rather than one that depends on the directory order.
|
|
247
|
+
test("still answers deterministically when every scheme is an extension", () => {
|
|
248
|
+
scheme("Client", "ShareTo", { runnable: "ShareTo.appex", appExtension: true });
|
|
249
|
+
scheme("Client", "Today", { runnable: "Client.app", appExtension: true });
|
|
250
|
+
for (const order of [
|
|
251
|
+
["Today", "ShareTo"],
|
|
252
|
+
["ShareTo", "Today"],
|
|
253
|
+
]) {
|
|
254
|
+
expect([order[0], pick("Client.xcodeproj", order)]).toEqual([order[0], "Today"]);
|
|
255
|
+
}
|
|
256
|
+
expect(rankSchemes(root, "Client.xcodeproj", ["ShareTo", "Today"])).toHaveLength(2);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
152
259
|
//# sourceMappingURL=ios-scheme-ranking.test.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A credential the pipeline declares and the dispatch delivers reaches the
|
|
3
|
+
* step that writes it.
|
|
4
|
+
*
|
|
5
|
+
* `file@1.0.0` decides at generation time, from the env map `EnvResolver`
|
|
6
|
+
* hands it: a value means a write script, no value means
|
|
7
|
+
* `echo "⚠️ X not set — skipping <file>"` and a green step. That map had
|
|
8
|
+
* four sources — built-ins, `.cibuild-secrets.json`, `app.envs`,
|
|
9
|
+
* `workflow.envs` — and a dispatched build supplies none of them. The runner
|
|
10
|
+
* merges the dispatch's secrets into the environment of the `ci` invocation
|
|
11
|
+
* and writes no secrets file, so every credential declared as an empty slot
|
|
12
|
+
* was dropped, on every dispatched build, and the step reported success.
|
|
13
|
+
*
|
|
14
|
+
* The two runs that make it unambiguous are here as the first two tests: the
|
|
15
|
+
* same pipeline, the same step, the same value, delivered once through the
|
|
16
|
+
* process environment and once through the local store. Before the fifth
|
|
17
|
+
* resolution step the first wrote nothing and the second wrote the file.
|
|
18
|
+
*/
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=a-declared-secret-reaches-the-file-step.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"a-declared-secret-reaches-the-file-step.test.d.ts","sourceRoot":"","sources":["../../../src/yaml/a-declared-secret-reaches-the-file-step.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A credential the pipeline declares and the dispatch delivers reaches the
|
|
3
|
+
* step that writes it.
|
|
4
|
+
*
|
|
5
|
+
* `file@1.0.0` decides at generation time, from the env map `EnvResolver`
|
|
6
|
+
* hands it: a value means a write script, no value means
|
|
7
|
+
* `echo "⚠️ X not set — skipping <file>"` and a green step. That map had
|
|
8
|
+
* four sources — built-ins, `.cibuild-secrets.json`, `app.envs`,
|
|
9
|
+
* `workflow.envs` — and a dispatched build supplies none of them. The runner
|
|
10
|
+
* merges the dispatch's secrets into the environment of the `ci` invocation
|
|
11
|
+
* and writes no secrets file, so every credential declared as an empty slot
|
|
12
|
+
* was dropped, on every dispatched build, and the step reported success.
|
|
13
|
+
*
|
|
14
|
+
* The two runs that make it unambiguous are here as the first two tests: the
|
|
15
|
+
* same pipeline, the same step, the same value, delivered once through the
|
|
16
|
+
* process environment and once through the local store. Before the fifth
|
|
17
|
+
* resolution step the first wrote nothing and the second wrote the file.
|
|
18
|
+
*/
|
|
19
|
+
import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync } from 'node:fs';
|
|
22
|
+
import { tmpdir } from 'node:os';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { convertYAMLToPipelineDef } from './converter.js';
|
|
25
|
+
import { EnvResolver } from './env-resolver.js';
|
|
26
|
+
import { clearRegistry, registerStep } from './steps/registry.js';
|
|
27
|
+
import { FileStepExecutor } from './steps/file.js';
|
|
28
|
+
const mockConfig = {
|
|
29
|
+
artifactsDir: '/test/artifacts',
|
|
30
|
+
maxConcurrentJobs: 1,
|
|
31
|
+
paths: {
|
|
32
|
+
buildsDir: '/test/builds',
|
|
33
|
+
cacheDir: '/test/cache',
|
|
34
|
+
derivedDataDir: '/test/derived-data',
|
|
35
|
+
},
|
|
36
|
+
interpreters: {
|
|
37
|
+
bash: '/bin/bash',
|
|
38
|
+
python: 'python3',
|
|
39
|
+
ruby: 'ruby',
|
|
40
|
+
node: 'node',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
/** The 61-byte document from the PRD's reproduction. Synthetic. */
|
|
44
|
+
const JSON_CREDENTIAL = '{"project_info":{"project_id":"synthetic-probe"},"client":[]}';
|
|
45
|
+
const TARGET = 'app/google-services.json';
|
|
46
|
+
let workdir;
|
|
47
|
+
let previousCwd;
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
clearRegistry();
|
|
50
|
+
registerStep('file', new FileStepExecutor());
|
|
51
|
+
previousCwd = process.cwd();
|
|
52
|
+
workdir = mkdtempSync(join(tmpdir(), 'cibuild-declared-secret-'));
|
|
53
|
+
process.chdir(workdir);
|
|
54
|
+
// `workdir` is the pre-symlink path on macOS; the steps resolve paths from
|
|
55
|
+
// CIBUILD_SOURCE_DIR, which is cwd, so read files back the same way.
|
|
56
|
+
workdir = process.cwd();
|
|
57
|
+
delete process.env.PLAIN_SECRET;
|
|
58
|
+
delete process.env.B64_SECRET;
|
|
59
|
+
});
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
process.chdir(previousCwd);
|
|
62
|
+
rmSync(workdir, { recursive: true, force: true });
|
|
63
|
+
clearRegistry();
|
|
64
|
+
delete process.env.PLAIN_SECRET;
|
|
65
|
+
delete process.env.B64_SECRET;
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* The shape the Android generator emits: an empty slot under `app.envs` and a
|
|
69
|
+
* skippable `file@1.0.0` step naming it.
|
|
70
|
+
*/
|
|
71
|
+
function probePipeline(declared, fileInputs) {
|
|
72
|
+
const app = declared
|
|
73
|
+
? { app: { envs: Object.entries(declared).map(([k, v]) => ({ [k]: v })) } }
|
|
74
|
+
: {};
|
|
75
|
+
return {
|
|
76
|
+
format_version: '1',
|
|
77
|
+
...app,
|
|
78
|
+
workflows: {
|
|
79
|
+
probe: {
|
|
80
|
+
steps: [
|
|
81
|
+
{
|
|
82
|
+
'file@1.0.0': {
|
|
83
|
+
title: 'Setup credential',
|
|
84
|
+
is_skippable: true,
|
|
85
|
+
inputs: fileInputs,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function writeSecretStore(global) {
|
|
94
|
+
writeFileSync(join(workdir, '.cibuild-secrets.json'), JSON.stringify({ global, workflows: {} }), { mode: 0o600 });
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Converts the pipeline and runs the generated steps the way the runner does,
|
|
98
|
+
* then reads back whatever landed at `target`.
|
|
99
|
+
*/
|
|
100
|
+
async function land(pipeline, target = TARGET) {
|
|
101
|
+
const result = await convertYAMLToPipelineDef(pipeline, mockConfig, 'probe');
|
|
102
|
+
const chunks = [...result.warnings];
|
|
103
|
+
for (const step of result.pipeline.steps) {
|
|
104
|
+
const scriptPath = join(workdir, `__step_${step.id}.sh`);
|
|
105
|
+
writeFileSync(scriptPath, step.script, 'utf-8');
|
|
106
|
+
const proc = spawnSync('bash', [scriptPath], {
|
|
107
|
+
cwd: workdir,
|
|
108
|
+
encoding: 'utf-8',
|
|
109
|
+
env: { ...process.env },
|
|
110
|
+
});
|
|
111
|
+
// A step that cannot even run would make every other assertion here
|
|
112
|
+
// vacuous, so fail on it directly rather than through the file contents.
|
|
113
|
+
expect(proc.status).toBe(0);
|
|
114
|
+
chunks.push(proc.stdout ?? '', proc.stderr ?? '');
|
|
115
|
+
}
|
|
116
|
+
const landedAt = join(workdir, target);
|
|
117
|
+
return {
|
|
118
|
+
output: chunks.join('\n'),
|
|
119
|
+
bytes: existsSync(landedAt) ? readFileSync(landedAt) : undefined,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
describe('a declared credential delivered through the process environment', () => {
|
|
123
|
+
test('reaches the file step and lands byte-exact — the dispatch path', async () => {
|
|
124
|
+
process.env.PLAIN_SECRET = JSON_CREDENTIAL;
|
|
125
|
+
const { output, bytes } = await land(probePipeline({ PLAIN_SECRET: '' }, { target_path: TARGET, var_name: 'PLAIN_SECRET' }));
|
|
126
|
+
expect(output).toContain('✓ Written google-services.json');
|
|
127
|
+
expect(output).not.toContain('not set');
|
|
128
|
+
expect(bytes?.toString('utf-8')).toBe(JSON_CREDENTIAL);
|
|
129
|
+
expect(bytes).toHaveLength(61);
|
|
130
|
+
});
|
|
131
|
+
test('still lands when the value comes from the local store instead', async () => {
|
|
132
|
+
writeSecretStore({ PLAIN_SECRET: JSON_CREDENTIAL });
|
|
133
|
+
const { output, bytes } = await land(probePipeline({ PLAIN_SECRET: '' }, { target_path: TARGET, var_name: 'PLAIN_SECRET' }));
|
|
134
|
+
expect(output).toContain('✓ Written google-services.json');
|
|
135
|
+
expect(bytes?.toString('utf-8')).toBe(JSON_CREDENTIAL);
|
|
136
|
+
});
|
|
137
|
+
test('the local store wins when both carry the key', async () => {
|
|
138
|
+
writeSecretStore({ PLAIN_SECRET: 'from-the-local-store' });
|
|
139
|
+
process.env.PLAIN_SECRET = 'from-the-inherited-environment';
|
|
140
|
+
const { bytes } = await land(probePipeline({ PLAIN_SECRET: '' }, { target_path: TARGET, var_name: 'PLAIN_SECRET' }));
|
|
141
|
+
expect(bytes?.toString('utf-8')).toBe('from-the-local-store');
|
|
142
|
+
});
|
|
143
|
+
test('a value the YAML itself supplies is not replaced by the environment', async () => {
|
|
144
|
+
process.env.PLAIN_SECRET = 'from-the-inherited-environment';
|
|
145
|
+
const { bytes } = await land(probePipeline({ PLAIN_SECRET: 'from-the-pipeline' }, { target_path: TARGET, var_name: 'PLAIN_SECRET' }));
|
|
146
|
+
expect(bytes?.toString('utf-8')).toBe('from-the-pipeline');
|
|
147
|
+
});
|
|
148
|
+
test('arrives verbatim — a $ in a credential is not a variable reference', async () => {
|
|
149
|
+
// A PEM passphrase, a Play service-account key, any credential at all may
|
|
150
|
+
// hold a `$`. Interpolating one would either paste a pipeline value into
|
|
151
|
+
// the middle of a secret or refuse the build over a name nothing declares.
|
|
152
|
+
const withDollars = 'prefix $NOT_A_DECLARED_VARIABLE ${ALSO_NOT_ONE} suffix';
|
|
153
|
+
process.env.PLAIN_SECRET = withDollars;
|
|
154
|
+
const { bytes } = await land(probePipeline({ PLAIN_SECRET: '' }, { target_path: TARGET, var_name: 'PLAIN_SECRET' }));
|
|
155
|
+
expect(bytes?.toString('utf-8')).toBe(withDollars);
|
|
156
|
+
});
|
|
157
|
+
test('base64_encoded round-trips bytes a text channel cannot carry', async () => {
|
|
158
|
+
// NUL and a newline together: the two bytes a keystore has and an
|
|
159
|
+
// environment variable cannot hold, which is why the slot carries base64.
|
|
160
|
+
const payload = Buffer.from([
|
|
161
|
+
0x50, 0x4b, 0x03, 0x04, 0x00, 0x0a, 0x00, 0xff, 0x00, 0x0d, 0x0a, 0x7f,
|
|
162
|
+
]);
|
|
163
|
+
process.env.B64_SECRET = payload.toString('base64');
|
|
164
|
+
const { output, bytes } = await land(probePipeline({ B64_SECRET: '' }, { target_path: 'release.keystore', var_name: 'B64_SECRET', base64_encoded: true }), 'release.keystore');
|
|
165
|
+
expect(output).toContain('✓ Written release.keystore');
|
|
166
|
+
expect(bytes?.equals(payload)).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
test('the value never appears in what ci prints', async () => {
|
|
169
|
+
process.env.PLAIN_SECRET = JSON_CREDENTIAL;
|
|
170
|
+
const { output, bytes } = await land(probePipeline({ PLAIN_SECRET: '' }, { target_path: TARGET, var_name: 'PLAIN_SECRET' }));
|
|
171
|
+
// Asserted on the captured output, not read off a transcript.
|
|
172
|
+
expect(bytes?.toString('utf-8')).toBe(JSON_CREDENTIAL);
|
|
173
|
+
expect(output).not.toContain(JSON_CREDENTIAL);
|
|
174
|
+
expect(output).not.toContain('synthetic-probe');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
describe('the host environment is not a source of pipeline variables', () => {
|
|
178
|
+
test('an undeclared key is not imported, even where a step reads it', async () => {
|
|
179
|
+
// HOME exists in every environment this runs in. If step 5 copied
|
|
180
|
+
// `process.env` in bulk this step would write the user's home directory
|
|
181
|
+
// to a file instead of skipping.
|
|
182
|
+
expect(process.env.HOME).toBeTruthy();
|
|
183
|
+
process.env.PLAIN_SECRET = JSON_CREDENTIAL;
|
|
184
|
+
// PLAIN_SECRET is declared, so the import runs; HOME is not.
|
|
185
|
+
const { output, bytes } = await land(probePipeline({ PLAIN_SECRET: '' }, { target_path: 'home.txt', var_name: 'HOME' }), 'home.txt');
|
|
186
|
+
expect(output).toContain('HOME not set — skipping home.txt');
|
|
187
|
+
expect(bytes).toBeUndefined();
|
|
188
|
+
});
|
|
189
|
+
test('the resolver does not hold an undeclared host variable', () => {
|
|
190
|
+
const pipeline = probePipeline({ PLAIN_SECRET: '' }, {
|
|
191
|
+
target_path: TARGET,
|
|
192
|
+
var_name: 'PLAIN_SECRET',
|
|
193
|
+
});
|
|
194
|
+
process.env.PLAIN_SECRET = JSON_CREDENTIAL;
|
|
195
|
+
const resolver = new EnvResolver(pipeline, pipeline.workflows.probe, 'probe', 'linux');
|
|
196
|
+
expect(resolver.get('PLAIN_SECRET')).toBe(JSON_CREDENTIAL);
|
|
197
|
+
expect(resolver.has('HOME')).toBe(false);
|
|
198
|
+
expect(resolver.getAll()).not.toHaveProperty('HOME');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
//# sourceMappingURL=a-declared-secret-reaches-the-file-step.test.js.map
|
|
@@ -30,6 +30,11 @@ export declare class EnvResolver {
|
|
|
30
30
|
private envVars;
|
|
31
31
|
private referencedVars;
|
|
32
32
|
private secretsManager;
|
|
33
|
+
/**
|
|
34
|
+
* Every key the pipeline names in `app.envs` or `workflow.envs`, whether or
|
|
35
|
+
* not it gave one a value. Read by `importDeclaredFromProcessEnv`.
|
|
36
|
+
*/
|
|
37
|
+
private declaredVars;
|
|
33
38
|
constructor(pipeline: YAMLPipeline, workflow: YAMLWorkflow, workflowName: string, platform: Platform, stack?: string, yamlFilePath?: string);
|
|
34
39
|
/**
|
|
35
40
|
* Gets the value of an environment variable
|
|
@@ -107,6 +112,34 @@ export declare class EnvResolver {
|
|
|
107
112
|
* @param envArray Array of environment variable objects
|
|
108
113
|
*/
|
|
109
114
|
private mergeEnvVars;
|
|
115
|
+
/**
|
|
116
|
+
* Fills a declared-but-still-empty variable from the process environment.
|
|
117
|
+
*
|
|
118
|
+
* This is how a credential dispatched by a build service arrives. The
|
|
119
|
+
* dispatch hands the value to the runner, the runner merges it into the
|
|
120
|
+
* environment of the `ci` invocation, and nothing else copies it anywhere —
|
|
121
|
+
* it writes no `.cibuild-secrets.json`, and that file is gitignored so the
|
|
122
|
+
* clone has none either. Without this step the value sits in the shell and
|
|
123
|
+
* is invisible to the steps that read this map: `file@1.0.0` reads
|
|
124
|
+
* `env[var_name]`, finds nothing, prints "not set — skipping" and reports
|
|
125
|
+
* success. Every credential a pipeline declared as an empty slot was
|
|
126
|
+
* dropped that way, on every dispatched build, with a green step.
|
|
127
|
+
*
|
|
128
|
+
* Scoped to keys the pipeline declares, deliberately. Copying `process.env`
|
|
129
|
+
* wholesale would turn arbitrary host environment into pipeline values and
|
|
130
|
+
* would change what every existing pipeline resolves. The declaration is
|
|
131
|
+
* the opt-in: `- GOOGLE_SERVICES_JSON: ""` is the pipeline saying "I read
|
|
132
|
+
* this key and I have no value for it", which is exactly the invitation the
|
|
133
|
+
* dispatched environment needs. A key nothing declares stays out, so `HOME`
|
|
134
|
+
* and the rest of the host's environment are not pipeline variables.
|
|
135
|
+
*
|
|
136
|
+
* Last, so anything an earlier source did supply wins — a developer's
|
|
137
|
+
* `.cibuild-secrets.json` still beats an inherited environment. The value
|
|
138
|
+
* is taken verbatim, with no interpolation, for the same reason step 2
|
|
139
|
+
* takes secrets verbatim: a credential is bytes, and a `$` inside one is
|
|
140
|
+
* not a variable reference.
|
|
141
|
+
*/
|
|
142
|
+
private importDeclaredFromProcessEnv;
|
|
110
143
|
/**
|
|
111
144
|
* Resolves $SECRET{secret_id} references to actual secret values
|
|
112
145
|
* @param value Value that may contain a secret reference
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env-resolver.d.ts","sourceRoot":"","sources":["../../../src/yaml/env-resolver.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAc,QAAQ,EAAE,MAAM,YAAY,CAAC;AAiEnF,qBAAa,+BAAgC,SAAQ,KAAK;aAEtC,YAAY,EAAE,MAAM;aACpB,QAAQ,CAAC,EAAE,MAAM;aACjB,IAAI,CAAC,EAAE,MAAM;gBAFb,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE,MAAM,YAAA,EACjB,IAAI,CAAC,EAAE,MAAM,YAAA;CAUhC;AAED;;;;;;;GAOG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;aAE/B,QAAQ,EAAE,MAAM;aAChB,QAAQ,CAAC,EAAE,MAAM;gBADjB,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,YAAA;CAKpC;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAsB;IACrC,OAAO,CAAC,cAAc,CAA0B;IAChD,OAAO,CAAC,cAAc,CAAiB;
|
|
1
|
+
{"version":3,"file":"env-resolver.d.ts","sourceRoot":"","sources":["../../../src/yaml/env-resolver.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAc,QAAQ,EAAE,MAAM,YAAY,CAAC;AAiEnF,qBAAa,+BAAgC,SAAQ,KAAK;aAEtC,YAAY,EAAE,MAAM;aACpB,QAAQ,CAAC,EAAE,MAAM;aACjB,IAAI,CAAC,EAAE,MAAM;gBAFb,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE,MAAM,YAAA,EACjB,IAAI,CAAC,EAAE,MAAM,YAAA;CAUhC;AAED;;;;;;;GAOG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;aAE/B,QAAQ,EAAE,MAAM;aAChB,QAAQ,CAAC,EAAE,MAAM;gBADjB,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,YAAA;CAKpC;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAsB;IACrC,OAAO,CAAC,cAAc,CAA0B;IAChD,OAAO,CAAC,cAAc,CAAiB;IACvC;;;OAGG;IACH,OAAO,CAAC,YAAY,CAA0B;gBAG5C,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,YAAY,EACtB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,QAAQ,EAClB,KAAK,CAAC,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,MAAM;IAkCvB;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAKrC;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B;;;;OAIG;IACH,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAMtC;;;OAGG;IACH,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAQhC;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM;IAqCnD;;;;;OAKG;IACH,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,GAAG;IAoBnD;;;OAGG;IACH,sBAAsB,IAAI,IAAI;IAQ9B;;;;;;OAMG;IACH,OAAO,CAAC,UAAU;IAalB;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IA0BvB;;;;;OAKG;IACH,OAAO,CAAC,cAAc;IA4FtB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAoBpB;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,OAAO,CAAC,4BAA4B;IAUpC;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IAuB9B;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;CAS5B;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,YAAY,EACtB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,QAAQ,EAClB,KAAK,CAAC,EAAE,MAAM,GACb,WAAW,CAOb"}
|