@limrun/api 0.31.3 → 0.31.4
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/CHANGELOG.md +13 -0
- package/client.d.mts.map +1 -1
- package/client.d.ts.map +1 -1
- package/client.js +9 -1
- package/client.js.map +1 -1
- package/client.mjs +9 -1
- package/client.mjs.map +1 -1
- package/index.d.mts +1 -1
- package/index.d.mts.map +1 -1
- package/index.d.ts +1 -1
- package/index.d.ts.map +1 -1
- package/index.js +3 -1
- package/index.js.map +1 -1
- package/index.mjs +1 -1
- package/index.mjs.map +1 -1
- package/package.json +1 -1
- package/rbe-bep.d.mts +47 -1
- package/rbe-bep.d.mts.map +1 -1
- package/rbe-bep.d.ts +47 -1
- package/rbe-bep.d.ts.map +1 -1
- package/rbe-bep.js +131 -40
- package/rbe-bep.js.map +1 -1
- package/rbe-bep.mjs +128 -40
- package/rbe-bep.mjs.map +1 -1
- package/resources/xcode-instances-helpers.d.mts +3 -3
- package/resources/xcode-instances-helpers.d.ts +3 -3
- package/src/client.ts +9 -1
- package/src/index.ts +1 -1
- package/src/rbe-bep.ts +177 -44
- package/src/resources/xcode-instances-helpers.ts +3 -3
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
package/src/rbe-bep.ts
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
* `--remote_download_outputs=minimal` to skip the (unneeded) local download by
|
|
13
13
|
* default; overriding it (e.g. `--remote_download_outputs=toplevel` on the command
|
|
14
14
|
* line) to materialize the .ipa locally does not affect this parser.
|
|
15
|
+
*
|
|
16
|
+
* A fully-cached rebuild (0 actions executed) still emits the bytestream URI and
|
|
17
|
+
* reproduces the same digest, so the auto-install watcher can rely on this parser
|
|
18
|
+
* after a no-op or revert (e.g. `git stash`) rebuild. Verified empirically.
|
|
15
19
|
*/
|
|
16
20
|
|
|
17
21
|
export type BepIpaDigest = {
|
|
@@ -27,7 +31,12 @@ type BepEvent = {
|
|
|
27
31
|
id?: {
|
|
28
32
|
targetCompleted?: { label?: string };
|
|
29
33
|
namedSet?: { id?: string };
|
|
34
|
+
buildFinished?: unknown;
|
|
30
35
|
};
|
|
36
|
+
started?: { uuid?: string };
|
|
37
|
+
finished?: { overallSuccess?: boolean; exitCode?: { name?: string; code?: number } };
|
|
38
|
+
/** Bazel sets this on the final BEP event once the stream is fully flushed. */
|
|
39
|
+
lastMessage?: boolean;
|
|
31
40
|
completed?: {
|
|
32
41
|
success?: boolean;
|
|
33
42
|
outputGroup?: Array<{ name?: string; fileSets?: Array<{ id?: string }> }>;
|
|
@@ -38,17 +47,62 @@ type BepEvent = {
|
|
|
38
47
|
};
|
|
39
48
|
};
|
|
40
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Why a BEP could not yield an .ipa digest for a label.
|
|
52
|
+
*
|
|
53
|
+
* `terminal` failures will not resolve by re-reading the same completed BEP
|
|
54
|
+
* (the build used the wrong flags); `no-build` / `no-output` / `no-ipa` are
|
|
55
|
+
* transient while the build is still flushing and become terminal only once
|
|
56
|
+
* the build is confirmed complete. The watcher uses this to decide whether to
|
|
57
|
+
* retry the read or to log-once and give up.
|
|
58
|
+
*/
|
|
59
|
+
export type RbeBepErrorKind = 'no-build' | 'no-output' | 'no-ipa' | 'local-only' | 'non-sha256';
|
|
60
|
+
|
|
61
|
+
export class RbeBepError extends Error {
|
|
62
|
+
readonly kind: RbeBepErrorKind;
|
|
63
|
+
constructor(kind: RbeBepErrorKind, message: string) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = 'RbeBepError';
|
|
66
|
+
this.kind = kind;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* True when re-reading the same (completed) BEP cannot help — the build was
|
|
71
|
+
* produced with flags the instance cache can't use (local download or a
|
|
72
|
+
* non-SHA256 digest). The watcher logs these once instead of retrying.
|
|
73
|
+
*/
|
|
74
|
+
get terminal(): boolean {
|
|
75
|
+
return this.kind === 'local-only' || this.kind === 'non-sha256';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
41
79
|
/** A SHA-256 digest is exactly 64 lowercase hex chars; the instance CAS is keyed by it. */
|
|
42
80
|
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
43
81
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Extracts (digestFunction, hash, size) from a Bazel/REAPI bytestream blob URI:
|
|
84
|
+
* `bytestream://…/blobs/[<digest_function>/]<hash>/<size>`. The digest-function
|
|
85
|
+
* segment is OMITTED for functions inferable from hash length (SHA-256 among
|
|
86
|
+
* them) and PRESENT otherwise — notably BLAKE3, whose 256-bit output is also 64
|
|
87
|
+
* hex chars and so cannot be told apart from SHA-256 by length. Capturing the
|
|
88
|
+
* function lets the caller reject a non-SHA256 build (e.g. Bazel 9's BLAKE3
|
|
89
|
+
* default) with the right guidance instead of misreading the URI.
|
|
90
|
+
*/
|
|
91
|
+
function parseBytestreamDigest(
|
|
92
|
+
uri: string,
|
|
93
|
+
): { digestFunction?: string; hash: string; sizeBytes: number } | null {
|
|
94
|
+
// Anchor on the /blobs/[func/]<hash>/<size> tail (tolerating an instance-name
|
|
95
|
+
// prefix). The optional function group only matches a real function segment:
|
|
96
|
+
// for the SHA-256 form (`/blobs/<hash>/<size>`) it backtracks to absent, and
|
|
97
|
+
// for `/blobs/blake3/<hash>/<size>` the non-hex 'l'/'k' force it to be captured
|
|
98
|
+
// as the function rather than the hash.
|
|
99
|
+
const m = uri.match(/\/blobs\/(?:([a-z0-9_]+)\/)?([0-9a-fA-F]+)\/(\d+)(?:$|\/)/);
|
|
100
|
+
if (!m || m[2] === undefined || m[3] === undefined) return null;
|
|
101
|
+
return {
|
|
102
|
+
...(m[1] !== undefined ? { digestFunction: m[1] } : {}),
|
|
103
|
+
hash: m[2].toLowerCase(),
|
|
104
|
+
sizeBytes: Number(m[3]),
|
|
105
|
+
};
|
|
52
106
|
}
|
|
53
107
|
|
|
54
108
|
/**
|
|
@@ -67,14 +121,8 @@ function canonicalizeLabel(label: string): string {
|
|
|
67
121
|
return name ? `${pkg}:${name}` : pkg;
|
|
68
122
|
}
|
|
69
123
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
* digest. Throws a descriptive Error when the target/.ipa is absent, its output
|
|
73
|
-
* was materialized locally (a `file://` URI) instead of left in CAS, or it was
|
|
74
|
-
* built with a non-SHA256 digest the instance cache cannot resolve.
|
|
75
|
-
*/
|
|
76
|
-
export function parseTopLevelIpaDigest(bepJson: string, label: string): BepIpaDigest {
|
|
77
|
-
const canonical = canonicalizeLabel(label);
|
|
124
|
+
/** Parses the BEP stream into events, ignoring partial/garbled lines. */
|
|
125
|
+
function parseEvents(bepJson: string): BepEvent[] {
|
|
78
126
|
const events: BepEvent[] = [];
|
|
79
127
|
for (const line of bepJson.split('\n')) {
|
|
80
128
|
const trimmed = line.trim();
|
|
@@ -85,32 +133,83 @@ export function parseTopLevelIpaDigest(bepJson: string, label: string): BepIpaDi
|
|
|
85
133
|
// Ignore partial/garbled lines (BEP is flushed incrementally).
|
|
86
134
|
}
|
|
87
135
|
}
|
|
136
|
+
return events;
|
|
137
|
+
}
|
|
88
138
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
139
|
+
export type BepCompletion = {
|
|
140
|
+
/**
|
|
141
|
+
* True once Bazel has flushed the terminal event (`lastMessage`), so every
|
|
142
|
+
* earlier event — including the .ipa's namedSetOfFiles — is durably on disk.
|
|
143
|
+
* Gate the digest read on this, NOT on the presence of `buildFinished`:
|
|
144
|
+
* `buildFinished` is followed by `buildToolLogs` and `buildMetrics`
|
|
145
|
+
* (the `lastMessage` carrier), so it is not the last line.
|
|
146
|
+
*/
|
|
147
|
+
complete: boolean;
|
|
148
|
+
/** Whether the build itself succeeded (overallSuccess / exit code 0). */
|
|
149
|
+
success: boolean;
|
|
150
|
+
/** Bazel's invocation UUID — distinguishes one build from the next. */
|
|
151
|
+
invocationId?: string;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Inspects a BEP stream for build completion. The watcher reads the BEP on every
|
|
156
|
+
* change but only acts once `complete` is true (the stream is fully flushed) and
|
|
157
|
+
* `success` is true (the build did not fail). `invocationId` lets the watcher act
|
|
158
|
+
* once per build even when consecutive builds reproduce the same .ipa digest
|
|
159
|
+
* (e.g. a `git stash` revert that rebuilds to an earlier digest).
|
|
160
|
+
*/
|
|
161
|
+
export function inspectBuildCompletion(bepJson: string): BepCompletion {
|
|
162
|
+
let complete = false;
|
|
163
|
+
let sawFinished = false;
|
|
164
|
+
let success = false;
|
|
165
|
+
let invocationId: string | undefined;
|
|
166
|
+
for (const e of parseEvents(bepJson)) {
|
|
167
|
+
if (e.started?.uuid) invocationId = e.started.uuid;
|
|
168
|
+
if (e.id?.buildFinished !== undefined || e.finished !== undefined) {
|
|
169
|
+
sawFinished = true;
|
|
170
|
+
// proto3 JSON omits zero-valued scalars, so a SUCCESS exit code serializes
|
|
171
|
+
// as `{name:"SUCCESS"}` with `code` omitted — checking `code === 0` would be
|
|
172
|
+
// a dead branch. Trust overallSuccess (present=true on success, omitted on
|
|
173
|
+
// failure) and fall back to the exit code's symbolic name.
|
|
174
|
+
success = e.finished?.overallSuccess === true || e.finished?.exitCode?.name === 'SUCCESS';
|
|
175
|
+
}
|
|
176
|
+
if (e.lastMessage === true) complete = true;
|
|
98
177
|
}
|
|
178
|
+
return {
|
|
179
|
+
complete,
|
|
180
|
+
success: sawFinished && success,
|
|
181
|
+
...(invocationId !== undefined ? { invocationId } : {}),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
99
184
|
|
|
185
|
+
/** Indexes namedSetOfFiles events by id for transitive fileset resolution. */
|
|
186
|
+
function indexNamedSets(events: BepEvent[]): Map<string, NonNullable<BepEvent['namedSetOfFiles']>> {
|
|
187
|
+
const namedSets = new Map<string, NonNullable<BepEvent['namedSetOfFiles']>>();
|
|
188
|
+
for (const e of events) {
|
|
189
|
+
const id = e.id?.namedSet?.id;
|
|
190
|
+
if (id && e.namedSetOfFiles) namedSets.set(id, e.namedSetOfFiles);
|
|
191
|
+
}
|
|
192
|
+
return namedSets;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Resolves the top-level `.ipa` CAS digest for one successful target's
|
|
197
|
+
* `completed` event, walking its default output group's filesets transitively.
|
|
198
|
+
* Throws a typed RbeBepError when no .ipa is present, it was materialized
|
|
199
|
+
* locally (`file://`), or it carries a non-SHA256 (e.g. BLAKE3) digest.
|
|
200
|
+
*/
|
|
201
|
+
function resolveIpaDigest(
|
|
202
|
+
label: string,
|
|
203
|
+
completed: NonNullable<BepEvent['completed']>,
|
|
204
|
+
namedSets: Map<string, NonNullable<BepEvent['namedSetOfFiles']>>,
|
|
205
|
+
): BepIpaDigest {
|
|
100
206
|
// Default output group -> the fileSet (namedSetOfFiles) ids holding its outputs.
|
|
101
207
|
const rootFileSetIds = (completed.outputGroup ?? [])
|
|
102
208
|
.filter((og) => og.name === 'default' || !og.name)
|
|
103
209
|
.flatMap((og) => (og.fileSets ?? []).map((fs) => fs.id))
|
|
104
210
|
.filter((id): id is string => !!id);
|
|
105
211
|
if (rootFileSetIds.length === 0) {
|
|
106
|
-
throw new
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Index namedSetOfFiles by id and walk transitively (sets can nest).
|
|
110
|
-
const namedSets = new Map<string, NonNullable<BepEvent['namedSetOfFiles']>>();
|
|
111
|
-
for (const e of events) {
|
|
112
|
-
const id = e.id?.namedSet?.id;
|
|
113
|
-
if (id && e.namedSetOfFiles) namedSets.set(id, e.namedSetOfFiles);
|
|
212
|
+
throw new RbeBepError('no-output', `Build of ${label} reported no default output files.`);
|
|
114
213
|
}
|
|
115
214
|
|
|
116
215
|
const seen = new Set<string>();
|
|
@@ -128,15 +227,23 @@ export function parseTopLevelIpaDigest(bepJson: string, label: string): BepIpaDi
|
|
|
128
227
|
if (!f.name || !f.name.endsWith('.ipa')) continue;
|
|
129
228
|
const digest = f.uri ? parseBytestreamDigest(f.uri) : null;
|
|
130
229
|
if (digest) {
|
|
131
|
-
// The instance cache is keyed by SHA-256.
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
230
|
+
// The instance cache is keyed by SHA-256. Reject any other digest
|
|
231
|
+
// function — Bazel 9 defaults to BLAKE3, whose URI carries an explicit
|
|
232
|
+
// `blake3` function segment (and whose 64-hex hash is indistinguishable
|
|
233
|
+
// from SHA-256 by length, so the hash form alone can't catch it). Surface
|
|
234
|
+
// it here with the fix rather than letting it fail later as a cryptic
|
|
235
|
+
// server cache miss.
|
|
236
|
+
const isSha256 =
|
|
237
|
+
digest.digestFunction === undefined ?
|
|
238
|
+
SHA256_HEX.test(digest.hash)
|
|
239
|
+
: digest.digestFunction === 'sha256';
|
|
240
|
+
if (!isSha256) {
|
|
241
|
+
const got = digest.digestFunction ?? `${digest.hash.length} hex chars`;
|
|
242
|
+
throw new RbeBepError(
|
|
243
|
+
'non-sha256',
|
|
244
|
+
`Built ${f.name} with a non-SHA256 digest (${got}; Bazel 9 defaults to BLAKE3). The instance ` +
|
|
245
|
+
`cache is keyed by SHA-256 — rebuild with --digest_function=sha256, e.g. ` +
|
|
246
|
+
`\`bazelisk --digest_function=sha256 build --config=limrun ${label}\`.`,
|
|
140
247
|
);
|
|
141
248
|
}
|
|
142
249
|
return { hash: digest.hash, sizeBytes: digest.sizeBytes, ipaName: f.name };
|
|
@@ -149,11 +256,37 @@ export function parseTopLevelIpaDigest(bepJson: string, label: string): BepIpaDi
|
|
|
149
256
|
}
|
|
150
257
|
|
|
151
258
|
if (localOnlyIpa) {
|
|
152
|
-
throw new
|
|
259
|
+
throw new RbeBepError(
|
|
260
|
+
'local-only',
|
|
153
261
|
`Found ${label}'s .ipa (${localOnlyIpa}) but it has no remote (bytestream) digest — it was ` +
|
|
154
262
|
`built locally, not remotely executed. Ensure you built with --config=limrun so the .ipa is ` +
|
|
155
263
|
`produced in the instance's cache.`,
|
|
156
264
|
);
|
|
157
265
|
}
|
|
158
|
-
throw new
|
|
266
|
+
throw new RbeBepError('no-ipa', `No .ipa output found for ${label} in the build event log.`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Finds the `.ipa` produced for `label` in a BEP stream and returns its CAS
|
|
271
|
+
* digest. Throws a typed RbeBepError when the target/.ipa is absent, its output
|
|
272
|
+
* was materialized locally (a `file://` URI) instead of left in CAS, or it was
|
|
273
|
+
* built with a non-SHA256 digest the instance cache cannot resolve.
|
|
274
|
+
*/
|
|
275
|
+
export function parseTopLevelIpaDigest(bepJson: string, label: string): BepIpaDigest {
|
|
276
|
+
const canonical = canonicalizeLabel(label);
|
|
277
|
+
const events = parseEvents(bepJson);
|
|
278
|
+
|
|
279
|
+
// The successful TargetComplete for the requested label (matched against the
|
|
280
|
+
// canonical //pkg:name form Bazel records, so `//App` matches `//App:App`).
|
|
281
|
+
const completed = events.find((e) => e.id?.targetCompleted?.label === canonical && e.completed?.success)
|
|
282
|
+
?.completed;
|
|
283
|
+
if (!completed) {
|
|
284
|
+
throw new RbeBepError(
|
|
285
|
+
'no-build',
|
|
286
|
+
`No successful build of ${label} found in the build event log. ` +
|
|
287
|
+
`Build it first with --config=limrun (e.g. \`bazelisk --digest_function=sha256 build --config=limrun ${label}\`).`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return resolveIpaDigest(label, completed, indexNamedSets(events));
|
|
159
292
|
}
|
|
@@ -220,9 +220,9 @@ export type XcodeClient = {
|
|
|
220
220
|
|
|
221
221
|
/**
|
|
222
222
|
* Attach a simulator to this xcode instance.
|
|
223
|
-
* After attaching, the latest
|
|
224
|
-
*
|
|
225
|
-
*
|
|
223
|
+
* After attaching, the latest installable build is installed on the simulator:
|
|
224
|
+
* the latest xcodebuild build or the latest RBE build (whichever is newer),
|
|
225
|
+
* unless that build's exact digest is already on the simulator.
|
|
226
226
|
*/
|
|
227
227
|
attachSimulator: (
|
|
228
228
|
simulator: IosInstance | { apiUrl: string; token: string },
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.31.
|
|
1
|
+
export const VERSION = '0.31.4'; // x-release-please-version
|
package/version.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.31.
|
|
1
|
+
export declare const VERSION = "0.31.4";
|
|
2
2
|
//# sourceMappingURL=version.d.mts.map
|
package/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.31.
|
|
1
|
+
export declare const VERSION = "0.31.4";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/version.js
CHANGED
package/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '0.31.
|
|
1
|
+
export const VERSION = '0.31.4'; // x-release-please-version
|
|
2
2
|
//# sourceMappingURL=version.mjs.map
|