@limrun/api 0.41.1 → 0.42.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/CHANGELOG.md +8 -0
- package/client.d.mts +4 -0
- package/client.d.mts.map +1 -1
- package/client.d.ts +4 -0
- package/client.d.ts.map +1 -1
- package/client.js +3 -0
- package/client.js.map +1 -1
- package/client.mjs +3 -0
- package/client.mjs.map +1 -1
- package/exec-client.d.mts +34 -1
- package/exec-client.d.mts.map +1 -1
- package/exec-client.d.ts +34 -1
- package/exec-client.d.ts.map +1 -1
- package/exec-client.js +18 -4
- package/exec-client.js.map +1 -1
- package/exec-client.mjs +18 -4
- package/exec-client.mjs.map +1 -1
- package/folder-sync-ignore.d.mts +8 -0
- package/folder-sync-ignore.d.mts.map +1 -1
- package/folder-sync-ignore.d.ts +8 -0
- package/folder-sync-ignore.d.ts.map +1 -1
- package/folder-sync-ignore.js +12 -4
- package/folder-sync-ignore.js.map +1 -1
- package/folder-sync-ignore.mjs +12 -4
- package/folder-sync-ignore.mjs.map +1 -1
- package/index.d.mts +2 -1
- package/index.d.mts.map +1 -1
- package/index.d.ts +2 -1
- package/index.d.ts.map +1 -1
- package/index.js.map +1 -1
- package/index.mjs.map +1 -1
- package/package.json +1 -1
- package/resources/daemon-client-shared.d.mts +37 -0
- package/resources/daemon-client-shared.d.mts.map +1 -0
- package/resources/daemon-client-shared.d.ts +37 -0
- package/resources/daemon-client-shared.d.ts.map +1 -0
- package/resources/daemon-client-shared.js +51 -0
- package/resources/daemon-client-shared.js.map +1 -0
- package/resources/daemon-client-shared.mjs +45 -0
- package/resources/daemon-client-shared.mjs.map +1 -0
- package/resources/gradle-instances-helpers.d.mts +54 -0
- package/resources/gradle-instances-helpers.d.mts.map +1 -0
- package/resources/gradle-instances-helpers.d.ts +54 -0
- package/resources/gradle-instances-helpers.d.ts.map +1 -0
- package/resources/gradle-instances-helpers.js +98 -0
- package/resources/gradle-instances-helpers.js.map +1 -0
- package/resources/gradle-instances-helpers.mjs +94 -0
- package/resources/gradle-instances-helpers.mjs.map +1 -0
- package/resources/gradle-instances.d.mts +149 -0
- package/resources/gradle-instances.d.mts.map +1 -0
- package/resources/gradle-instances.d.ts +149 -0
- package/resources/gradle-instances.d.ts.map +1 -0
- package/resources/gradle-instances.js +41 -0
- package/resources/gradle-instances.js.map +1 -0
- package/resources/gradle-instances.mjs +37 -0
- package/resources/gradle-instances.mjs.map +1 -0
- package/resources/index.d.mts +2 -0
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +2 -0
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js +3 -1
- package/resources/index.js.map +1 -1
- package/resources/index.mjs +1 -0
- package/resources/index.mjs.map +1 -1
- package/resources/xcode-instances-helpers.d.mts +2 -11
- package/resources/xcode-instances-helpers.d.mts.map +1 -1
- package/resources/xcode-instances-helpers.d.ts +2 -11
- package/resources/xcode-instances-helpers.d.ts.map +1 -1
- package/resources/xcode-instances-helpers.js +5 -41
- package/resources/xcode-instances-helpers.js.map +1 -1
- package/resources/xcode-instances-helpers.mjs +3 -38
- package/resources/xcode-instances-helpers.mjs.map +1 -1
- package/src/client.ts +27 -0
- package/src/exec-client.ts +55 -5
- package/src/folder-sync-ignore.ts +22 -6
- package/src/index.ts +10 -0
- package/src/resources/daemon-client-shared.ts +71 -0
- package/src/resources/gradle-instances-helpers.ts +155 -0
- package/src/resources/gradle-instances.ts +206 -0
- package/src/resources/index.ts +13 -0
- package/src/resources/xcode-instances-helpers.ts +13 -59
- 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/client.ts
CHANGED
|
@@ -54,6 +54,19 @@ import {
|
|
|
54
54
|
XcodeInstanceListParams,
|
|
55
55
|
XcodeInstancesItems,
|
|
56
56
|
} from './resources/xcode-instances';
|
|
57
|
+
import {
|
|
58
|
+
GradleInstance,
|
|
59
|
+
GradleInstanceCreateParams,
|
|
60
|
+
GradleInstanceListParams,
|
|
61
|
+
GradleInstancesItems,
|
|
62
|
+
} from './resources/gradle-instances';
|
|
63
|
+
import {
|
|
64
|
+
GradleInstances,
|
|
65
|
+
GradleCreateClientParams,
|
|
66
|
+
GradleClient,
|
|
67
|
+
GradleSyncOptions,
|
|
68
|
+
GradleBuildOptions,
|
|
69
|
+
} from './resources/gradle-instances-helpers';
|
|
57
70
|
import {
|
|
58
71
|
XcodeInstances,
|
|
59
72
|
XcodeCreateClientParams,
|
|
@@ -822,6 +835,7 @@ export class Limrun {
|
|
|
822
835
|
assets: API.Assets = new API.Assets(this);
|
|
823
836
|
iosInstances: API.IosInstances = new API.IosInstances(this);
|
|
824
837
|
xcodeInstances: API.XcodeInstances = new API.XcodeInstances(this);
|
|
838
|
+
gradleInstances: API.GradleInstances = new API.GradleInstances(this);
|
|
825
839
|
analytics: API.Analytics = new API.Analytics(this);
|
|
826
840
|
}
|
|
827
841
|
|
|
@@ -829,6 +843,7 @@ Limrun.AndroidInstances = AndroidInstances;
|
|
|
829
843
|
Limrun.Assets = Assets;
|
|
830
844
|
Limrun.IosInstances = IosInstances;
|
|
831
845
|
Limrun.XcodeInstances = XcodeInstances;
|
|
846
|
+
Limrun.GradleInstances = GradleInstances;
|
|
832
847
|
Limrun.Analytics = Analytics;
|
|
833
848
|
|
|
834
849
|
export declare namespace Limrun {
|
|
@@ -883,6 +898,18 @@ export declare namespace Limrun {
|
|
|
883
898
|
type SimulatorInstallState as SimulatorInstallState,
|
|
884
899
|
};
|
|
885
900
|
|
|
901
|
+
export {
|
|
902
|
+
GradleInstances as GradleInstances,
|
|
903
|
+
type GradleInstance as GradleInstance,
|
|
904
|
+
type GradleInstancesItems as GradleInstancesItems,
|
|
905
|
+
type GradleInstanceCreateParams as GradleInstanceCreateParams,
|
|
906
|
+
type GradleInstanceListParams as GradleInstanceListParams,
|
|
907
|
+
type GradleCreateClientParams as GradleCreateClientParams,
|
|
908
|
+
type GradleClient as GradleClient,
|
|
909
|
+
type GradleSyncOptions as GradleSyncOptions,
|
|
910
|
+
type GradleBuildOptions as GradleBuildOptions,
|
|
911
|
+
};
|
|
912
|
+
|
|
886
913
|
export {
|
|
887
914
|
Analytics as Analytics,
|
|
888
915
|
type AnalyticsInstancesResponse as AnalyticsInstancesResponse,
|
package/src/exec-client.ts
CHANGED
|
@@ -12,7 +12,9 @@ import { nodeProxyTransport } from './internal/proxy-transport';
|
|
|
12
12
|
// Types
|
|
13
13
|
// =============================================================================
|
|
14
14
|
|
|
15
|
-
export type ExecRequest =
|
|
15
|
+
export type ExecRequest = XcodeBuildExecRequest | GradleBuildExecRequest;
|
|
16
|
+
|
|
17
|
+
export type XcodeBuildExecRequest = {
|
|
16
18
|
command: 'xcodebuild';
|
|
17
19
|
xcodebuild?: {
|
|
18
20
|
workspace?: string;
|
|
@@ -43,6 +45,41 @@ export type ExecRequest = {
|
|
|
43
45
|
};
|
|
44
46
|
};
|
|
45
47
|
|
|
48
|
+
/** Android ABIs the gradle daemon accepts; 'all' keeps the project's own configuration. */
|
|
49
|
+
export type GradleAndroidABI = 'armeabi-v7a' | 'arm64-v8a' | 'x86' | 'x86_64' | 'all';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* React Native / Expo tuning for gradle builds. The server detects Expo
|
|
53
|
+
* managed-workflow projects automatically when the workspace has no
|
|
54
|
+
* Gradle root; setting this forces the React Native pipeline (dependency
|
|
55
|
+
* install, expo prebuild) and is an error for projects with no detected
|
|
56
|
+
* Expo app.
|
|
57
|
+
*/
|
|
58
|
+
export type GradleReactNativeConfig = {
|
|
59
|
+
/** Relative path to the Expo app directory in a monorepo. Omit to auto-detect. */
|
|
60
|
+
expoAppDir?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Android ABIs to build. The server defaults to x86_64 (what Limrun
|
|
63
|
+
* Android instances run) except for release and bundle tasks, which
|
|
64
|
+
* keep the project's own ABI configuration; pass ['all'] to always
|
|
65
|
+
* keep it.
|
|
66
|
+
*/
|
|
67
|
+
architectures?: GradleAndroidABI[];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type GradleBuildExecRequest = {
|
|
71
|
+
command: 'gradlebuild';
|
|
72
|
+
/** Gradle tasks to run. Omit for the server default (assembleDebug). */
|
|
73
|
+
tasks?: string[];
|
|
74
|
+
/** Relative path to the Gradle root when auto-discovery is ambiguous. */
|
|
75
|
+
projectPath?: string;
|
|
76
|
+
reactNative?: GradleReactNativeConfig;
|
|
77
|
+
signedUploadUrl?: string;
|
|
78
|
+
additionalMetadata?: {
|
|
79
|
+
signedDownloadUrl?: string;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
46
83
|
export type ExecOptions = {
|
|
47
84
|
apiUrl: string;
|
|
48
85
|
token: string;
|
|
@@ -232,7 +269,12 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
|
|
|
232
269
|
const { log } = this;
|
|
233
270
|
const { apiUrl, token } = this.options;
|
|
234
271
|
|
|
235
|
-
// 1. Trigger the build via POST /exec
|
|
272
|
+
// 1. Trigger the build via POST /exec.
|
|
273
|
+
// additionalMetadata is a client-only carrier (no daemon reads it; it is
|
|
274
|
+
// spread into ExecResult below so callers can surface the download URL), so
|
|
275
|
+
// it is stripped from the wire body: the daemon OpenAPI schemas do not
|
|
276
|
+
// declare it, and sending it would 400 under strict request validation.
|
|
277
|
+
const { additionalMetadata: _clientOnly, ...wireRequest } = request;
|
|
236
278
|
let execRes: Response;
|
|
237
279
|
try {
|
|
238
280
|
execRes = await nodeProxyTransport.fetch(`${apiUrl}/exec`, {
|
|
@@ -241,7 +283,7 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
|
|
|
241
283
|
'Content-Type': 'application/json',
|
|
242
284
|
Authorization: `Bearer ${token}`,
|
|
243
285
|
},
|
|
244
|
-
body: JSON.stringify(
|
|
286
|
+
body: JSON.stringify(wireRequest),
|
|
245
287
|
signal: this.abortController.signal,
|
|
246
288
|
});
|
|
247
289
|
} catch (err) {
|
|
@@ -259,7 +301,15 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
|
|
|
259
301
|
|
|
260
302
|
if (!execRes.ok) {
|
|
261
303
|
const text = await execRes.text();
|
|
262
|
-
|
|
304
|
+
let message = text;
|
|
305
|
+
try {
|
|
306
|
+
// The daemon returns an APIError JSON body; surface its message
|
|
307
|
+
// instead of the raw escaped JSON.
|
|
308
|
+
message = (JSON.parse(text) as { message?: string }).message || text;
|
|
309
|
+
} catch {
|
|
310
|
+
// Not JSON; keep the raw body.
|
|
311
|
+
}
|
|
312
|
+
throw new Error(`exec failed: ${execRes.status} ${message}`);
|
|
263
313
|
}
|
|
264
314
|
|
|
265
315
|
const execData = (await execRes.json()) as { execId: string };
|
|
@@ -273,7 +323,7 @@ export class ExecChildProcess implements PromiseLike<ExecResult> {
|
|
|
273
323
|
// budget by its server-side verdict watch plus upload headroom so a long
|
|
274
324
|
// build is not force-failed client-side while the server still succeeds.
|
|
275
325
|
let timeoutMs = 3600 * 1000;
|
|
276
|
-
if (request.testflight) {
|
|
326
|
+
if (request.command === 'xcodebuild' && request.testflight) {
|
|
277
327
|
timeoutMs += (Math.max(0, request.testflight.waitTimeoutSeconds ?? 120) + 900) * 1000;
|
|
278
328
|
}
|
|
279
329
|
let exitCode: number;
|
|
@@ -31,6 +31,14 @@ export type IgnoreFnOptions = {
|
|
|
31
31
|
* in it.
|
|
32
32
|
*/
|
|
33
33
|
xcodeDefaults?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Honor .gitignore files in nested directories, not just the project root.
|
|
36
|
+
* Defaults to whatever `xcodeDefaults` is, so existing callers are unchanged;
|
|
37
|
+
* a caller can enable it independently (e.g. gradle sync, whose nested module
|
|
38
|
+
* build/ dirs are excluded by their own app/.gitignore) without opting into
|
|
39
|
+
* the Xcode-specific default excludes and force-includes.
|
|
40
|
+
*/
|
|
41
|
+
nestedGitignore?: boolean;
|
|
34
42
|
basisCacheDir: string;
|
|
35
43
|
log?: IgnoreLogger;
|
|
36
44
|
};
|
|
@@ -43,6 +51,14 @@ function normalizeRelativePath(relativePath: string): string {
|
|
|
43
51
|
.replace(/\/+/g, '/');
|
|
44
52
|
}
|
|
45
53
|
|
|
54
|
+
// Linear-time replacement for `.replace(/\/+$/, '')`: that regex backtracks
|
|
55
|
+
// quadratically on long runs of '/' (flagged by CodeQL as polynomial ReDoS).
|
|
56
|
+
function stripTrailingSlashes(s: string): string {
|
|
57
|
+
let end = s.length;
|
|
58
|
+
while (end > 0 && s[end - 1] === '/') end--;
|
|
59
|
+
return end === s.length ? s : s.slice(0, end);
|
|
60
|
+
}
|
|
61
|
+
|
|
46
62
|
type GitTrackedSets = {
|
|
47
63
|
tracked: Set<string>;
|
|
48
64
|
prefixes: Set<string>;
|
|
@@ -138,7 +154,7 @@ export async function createIgnoreFn(rootDir: string, options: IgnoreFnOptions):
|
|
|
138
154
|
// install sync) keep the legacy behavior: root-only .gitignore and no
|
|
139
155
|
// project force-includes, so a build artifact isn't reshaped by gitignore
|
|
140
156
|
// files or Xcode-specific rules that happen to sit inside it.
|
|
141
|
-
const nestedGitignore = !!options.xcodeDefaults;
|
|
157
|
+
const nestedGitignore = options.nestedGitignore ?? !!options.xcodeDefaults;
|
|
142
158
|
|
|
143
159
|
// Evaluates the .gitignore chain for a path (trailing slash preserved so
|
|
144
160
|
// dir-only rules like `build/` match the directory itself). Files are
|
|
@@ -146,7 +162,7 @@ export async function createIgnoreFn(rootDir: string, options: IgnoreFnOptions):
|
|
|
146
162
|
// files naturally override shallower ones. When nestedGitignore is off,
|
|
147
163
|
// only the root .gitignore is consulted.
|
|
148
164
|
const gitignoreDecision = (testPath: string): { ignored: boolean; rule?: string } | undefined => {
|
|
149
|
-
const withoutTrailingSlash = testPath
|
|
165
|
+
const withoutTrailingSlash = stripTrailingSlashes(testPath);
|
|
150
166
|
const trailingSlash = testPath.endsWith('/') ? '/' : '';
|
|
151
167
|
const parts = withoutTrailingSlash.split('/');
|
|
152
168
|
let decision: { ignored: boolean; rule?: string } | undefined;
|
|
@@ -164,9 +180,9 @@ export async function createIgnoreFn(rootDir: string, options: IgnoreFnOptions):
|
|
|
164
180
|
return decision;
|
|
165
181
|
};
|
|
166
182
|
|
|
167
|
-
const basisCacheRelative =
|
|
168
|
-
path.relative(rootResolved, options.basisCacheDir),
|
|
169
|
-
)
|
|
183
|
+
const basisCacheRelative = stripTrailingSlashes(
|
|
184
|
+
normalizeRelativePath(path.relative(rootResolved, options.basisCacheDir)),
|
|
185
|
+
);
|
|
170
186
|
const shouldIgnoreBasisCache =
|
|
171
187
|
basisCacheRelative &&
|
|
172
188
|
basisCacheRelative !== '.' &&
|
|
@@ -181,7 +197,7 @@ export async function createIgnoreFn(rootDir: string, options: IgnoreFnOptions):
|
|
|
181
197
|
return (relativePath: string) => {
|
|
182
198
|
const normalized = normalizeRelativePath(relativePath);
|
|
183
199
|
if (!normalized) return false;
|
|
184
|
-
const withoutTrailingSlash = normalized
|
|
200
|
+
const withoutTrailingSlash = stripTrailingSlashes(normalized);
|
|
185
201
|
|
|
186
202
|
// 1. The basis cache must not sync itself, even when --include matches.
|
|
187
203
|
if (
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,10 @@ export { buildSettingKeyPattern, parseBuildSettingEntries, validateBuildSettings
|
|
|
13
13
|
export {
|
|
14
14
|
exec,
|
|
15
15
|
type ExecRequest,
|
|
16
|
+
type XcodeBuildExecRequest,
|
|
17
|
+
type GradleAndroidABI,
|
|
18
|
+
type GradleBuildExecRequest,
|
|
19
|
+
type GradleReactNativeConfig,
|
|
16
20
|
type ExecOptions,
|
|
17
21
|
type ExecResult,
|
|
18
22
|
type ExecChildProcess,
|
|
@@ -46,6 +50,12 @@ export {
|
|
|
46
50
|
type SimulatorDeviceInfo,
|
|
47
51
|
type SimulatorInstallState,
|
|
48
52
|
} from './resources/xcode-instances-helpers';
|
|
53
|
+
export {
|
|
54
|
+
type GradleCreateClientParams,
|
|
55
|
+
type GradleClient,
|
|
56
|
+
type GradleSyncOptions,
|
|
57
|
+
type GradleBuildOptions,
|
|
58
|
+
} from './resources/gradle-instances-helpers';
|
|
49
59
|
export {
|
|
50
60
|
LIMRUN_DIR,
|
|
51
61
|
TRY_IMPORT_LINE,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Helpers and platform-neutral types shared by the per-instance build-daemon
|
|
2
|
+
// clients (xcode, gradle).
|
|
3
|
+
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
|
|
8
|
+
export type LogLevel = 'none' | 'error' | 'warn' | 'info' | 'debug';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Derives the client-side folder-sync cache location for a local project.
|
|
12
|
+
* The key format is a compatibility contract: changing it orphans every
|
|
13
|
+
* user's existing basis cache, so both daemon clients must share this one
|
|
14
|
+
* derivation.
|
|
15
|
+
*/
|
|
16
|
+
export function deriveBasisCache(
|
|
17
|
+
localCodePath: string,
|
|
18
|
+
override?: string,
|
|
19
|
+
): { cacheKey: string; basisCacheDir: string } {
|
|
20
|
+
const resolvedPath = path.resolve(localCodePath);
|
|
21
|
+
const folderName = path.basename(resolvedPath);
|
|
22
|
+
const hash = crypto.createHash('sha1').update(resolvedPath).digest('hex').slice(0, 8);
|
|
23
|
+
const cacheKey = `limsync-cache-${folderName}-${hash}`;
|
|
24
|
+
return { cacheKey, basisCacheDir: override ?? path.join(os.tmpdir(), cacheKey) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type SyncResult = {
|
|
28
|
+
/**
|
|
29
|
+
* Number of bytes transmitted to the server for this sync (full uploads plus
|
|
30
|
+
* delta patches, before transport compression). In watch mode, this reflects
|
|
31
|
+
* the initial sync only.
|
|
32
|
+
*/
|
|
33
|
+
bytesSent?: number;
|
|
34
|
+
/** Present only when watch=true; call to stop watching */
|
|
35
|
+
stopWatching?: () => Promise<void>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type AssetUploadUrls = { signedUploadUrl: string; signedDownloadUrl: string };
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Mints presigned upload/download URLs for a named asset via assets.getOrCreate,
|
|
42
|
+
* wrapping failures with the asset name (and the original error as cause).
|
|
43
|
+
*/
|
|
44
|
+
export function mintAssetUploadUrls(
|
|
45
|
+
assets: { getOrCreate: (body: { name: string; ttl?: string }) => Promise<AssetUploadUrls> },
|
|
46
|
+
name: string,
|
|
47
|
+
ttl?: string,
|
|
48
|
+
): Promise<AssetUploadUrls> {
|
|
49
|
+
return assets.getOrCreate({ name, ...(ttl && { ttl }) }).catch((err) => {
|
|
50
|
+
const message = `Failed to create upload URL for asset '${name}': ${
|
|
51
|
+
err instanceof Error ? err.message : err
|
|
52
|
+
}`;
|
|
53
|
+
// @ts-ignore - not all envs have native support for cause yet
|
|
54
|
+
throw new Error(message, { cause: err });
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const logLevels: LogLevel[] = ['none', 'error', 'warn', 'info', 'debug'];
|
|
59
|
+
|
|
60
|
+
export function createDaemonLogger(prefix: string, logLevel: LogLevel) {
|
|
61
|
+
const threshold = logLevels.indexOf(logLevel);
|
|
62
|
+
const shouldLog = (level: LogLevel) => threshold >= logLevels.indexOf(level);
|
|
63
|
+
return (level: 'debug' | 'info' | 'warn' | 'error', msg: string) => {
|
|
64
|
+
if (!shouldLog(level)) return;
|
|
65
|
+
if (level === 'error' || level === 'warn') {
|
|
66
|
+
console[level](prefix, msg);
|
|
67
|
+
} else {
|
|
68
|
+
console.log(prefix, msg);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { GradleInstances as GeneratedGradleInstances, type GradleInstance } from './gradle-instances';
|
|
2
|
+
import {
|
|
3
|
+
exec,
|
|
4
|
+
type ExecChildProcess,
|
|
5
|
+
type GradleBuildExecRequest,
|
|
6
|
+
type GradleReactNativeConfig,
|
|
7
|
+
} from '../exec-client';
|
|
8
|
+
import { syncFolder as syncFolderImpl, type FolderSyncOptions } from '../folder-sync';
|
|
9
|
+
import { createIgnoreFn } from '../folder-sync-ignore';
|
|
10
|
+
import {
|
|
11
|
+
createDaemonLogger,
|
|
12
|
+
deriveBasisCache,
|
|
13
|
+
mintAssetUploadUrls,
|
|
14
|
+
type LogLevel,
|
|
15
|
+
type SyncResult,
|
|
16
|
+
} from './daemon-client-shared';
|
|
17
|
+
|
|
18
|
+
export type GradleCreateClientParams = { logLevel?: LogLevel } & (
|
|
19
|
+
| { instance: GradleInstance }
|
|
20
|
+
| { apiUrl: string; token: string }
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export type GradleSyncOptions = {
|
|
24
|
+
/**
|
|
25
|
+
* Directory for the client-side folder-sync cache.
|
|
26
|
+
* Defaults to a temporary directory under the OS temp directory.
|
|
27
|
+
*/
|
|
28
|
+
basisCacheDir?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Optional predicate for ignoring files and directories during sync.
|
|
31
|
+
* Called with the relative path from the sync root (using forward slashes).
|
|
32
|
+
* For directories, the path ends with '/'.
|
|
33
|
+
* Return true to ignore, false to keep.
|
|
34
|
+
*/
|
|
35
|
+
ignore?: (relativePath: string) => boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Optional predicate for force-including paths that would otherwise be
|
|
38
|
+
* dropped. Same calling convention as `ignore`; return true to force-sync.
|
|
39
|
+
*/
|
|
40
|
+
include?: (relativePath: string) => boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type GradleBuildOptions = {
|
|
44
|
+
/** Gradle tasks to run. Omit for the server default (assembleDebug). */
|
|
45
|
+
tasks?: string[];
|
|
46
|
+
/** Relative path to the Gradle root when auto-discovery is ambiguous. */
|
|
47
|
+
projectPath?: string;
|
|
48
|
+
/** Upload the built APK as a named org asset, or to a presigned URL. */
|
|
49
|
+
upload?: { assetName: string; signedUploadUrl?: never } | { signedUploadUrl: string; assetName?: never };
|
|
50
|
+
/** React Native / Expo tuning. */
|
|
51
|
+
reactNative?: GradleReactNativeConfig;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type GradleClient = {
|
|
55
|
+
sync(localCodePath: string, opts?: GradleSyncOptions): Promise<SyncResult>;
|
|
56
|
+
gradlebuild(options?: GradleBuildOptions): ExecChildProcess;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Machine-local or regenerable files that must never reach the build
|
|
60
|
+
// sandbox: local.properties points at the developer's own SDK and would
|
|
61
|
+
// shadow the image's. Only ROOT-level cache/output dirs are defaulted;
|
|
62
|
+
// nested module build/ dirs are the project .gitignore's job (dropping any
|
|
63
|
+
// 'build' segment would silently exclude legitimate packages named build).
|
|
64
|
+
function gradleDefaultIgnore(relativePath: string): boolean {
|
|
65
|
+
const trimmed = relativePath.endsWith('/') ? relativePath.slice(0, -1) : relativePath;
|
|
66
|
+
const segments = trimmed.split('/');
|
|
67
|
+
if (segments[segments.length - 1] === 'local.properties') {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return segments.length === 1 && (trimmed === '.gradle' || trimmed === '.kotlin' || trimmed === 'build');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class GradleInstances extends GeneratedGradleInstances {
|
|
74
|
+
async createClient(params: GradleCreateClientParams): Promise<GradleClient> {
|
|
75
|
+
let apiUrl: string;
|
|
76
|
+
let token: string;
|
|
77
|
+
if ('instance' in params) {
|
|
78
|
+
if (!params.instance.status.apiUrl) {
|
|
79
|
+
throw new Error('Instance not ready: apiUrl is not available');
|
|
80
|
+
}
|
|
81
|
+
apiUrl = params.instance.status.apiUrl;
|
|
82
|
+
token = params.instance.status.token;
|
|
83
|
+
} else {
|
|
84
|
+
apiUrl = params.apiUrl;
|
|
85
|
+
token = params.token;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const log = createDaemonLogger('[GradleInstance]', params.logLevel ?? 'info');
|
|
89
|
+
const client = this._client;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
async sync(localCodePath: string, opts?: GradleSyncOptions): Promise<SyncResult> {
|
|
93
|
+
const { cacheKey, basisCacheDir } = deriveBasisCache(localCodePath, opts?.basisCacheDir);
|
|
94
|
+
const userIgnore = opts?.ignore;
|
|
95
|
+
const codeSyncOpts: FolderSyncOptions = {
|
|
96
|
+
apiUrl,
|
|
97
|
+
token,
|
|
98
|
+
udid: cacheKey,
|
|
99
|
+
install: false,
|
|
100
|
+
ignoreFn: await createIgnoreFn(localCodePath, {
|
|
101
|
+
basisCacheDir,
|
|
102
|
+
log,
|
|
103
|
+
xcodeDefaults: false,
|
|
104
|
+
// Honor nested module .gitignore files (app/.gitignore's /build,
|
|
105
|
+
// etc.); gradleDefaultIgnore only covers root-level build/.gradle/
|
|
106
|
+
// .kotlin, so without this a locally-built project would sync its
|
|
107
|
+
// nested build/ artifact trees.
|
|
108
|
+
nestedGitignore: true,
|
|
109
|
+
additional: userIgnore ? (p) => gradleDefaultIgnore(p) || userIgnore(p) : gradleDefaultIgnore,
|
|
110
|
+
...(opts?.include ? { include: opts.include } : {}),
|
|
111
|
+
}),
|
|
112
|
+
basisCacheDir,
|
|
113
|
+
// Gradle builds are one-shot; no dev-loop watch like xcode.
|
|
114
|
+
watch: false,
|
|
115
|
+
launchMode: 'ForegroundIfRunning',
|
|
116
|
+
log,
|
|
117
|
+
syncSymlinks: true,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const result = await syncFolderImpl(localCodePath, codeSyncOpts);
|
|
121
|
+
const out: SyncResult = {};
|
|
122
|
+
if (result.bytesSent !== undefined) {
|
|
123
|
+
out.bytesSent = result.bytesSent;
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
gradlebuild(options?: GradleBuildOptions): ExecChildProcess {
|
|
129
|
+
const request: GradleBuildExecRequest = {
|
|
130
|
+
command: 'gradlebuild',
|
|
131
|
+
...(options?.tasks?.length && { tasks: options.tasks }),
|
|
132
|
+
...(options?.projectPath && { projectPath: options.projectPath }),
|
|
133
|
+
...(options?.reactNative && { reactNative: options.reactNative }),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (options?.upload && 'assetName' in options.upload) {
|
|
137
|
+
const requestPromise = mintAssetUploadUrls(client.assets, options.upload.assetName).then(
|
|
138
|
+
(asset) => {
|
|
139
|
+
request.signedUploadUrl = asset.signedUploadUrl;
|
|
140
|
+
request.additionalMetadata = { signedDownloadUrl: asset.signedDownloadUrl };
|
|
141
|
+
return request;
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
return exec(requestPromise, { apiUrl, token, log });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (options?.upload && 'signedUploadUrl' in options.upload) {
|
|
148
|
+
request.signedUploadUrl = options.upload.signedUploadUrl;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return exec(request, { apiUrl, token, log });
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Hand-written following the Stainless resource pattern; a future generation
|
|
2
|
+
// from the OpenAPI spec reconciles with this file.
|
|
3
|
+
|
|
4
|
+
import { APIResource } from '../core/resource';
|
|
5
|
+
import { APIPromise } from '../core/api-promise';
|
|
6
|
+
import { Items, type ItemsParams, PagePromise } from '../core/pagination';
|
|
7
|
+
import { buildHeaders } from '../internal/headers';
|
|
8
|
+
import { RequestOptions } from '../internal/request-options';
|
|
9
|
+
import { path } from '../internal/utils/path';
|
|
10
|
+
|
|
11
|
+
export class GradleInstances extends APIResource {
|
|
12
|
+
/**
|
|
13
|
+
* Create a gradle instance
|
|
14
|
+
*/
|
|
15
|
+
create(params: GradleInstanceCreateParams, options?: RequestOptions): APIPromise<GradleInstance> {
|
|
16
|
+
const { reuseIfExists, wait, ...body } = params;
|
|
17
|
+
return this._client.post('/v1/gradle_instances', { query: { reuseIfExists, wait }, body, ...options });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* List gradle instances
|
|
22
|
+
*/
|
|
23
|
+
list(
|
|
24
|
+
query: GradleInstanceListParams | null | undefined = {},
|
|
25
|
+
options?: RequestOptions,
|
|
26
|
+
): PagePromise<GradleInstancesItems, GradleInstance> {
|
|
27
|
+
return this._client.getAPIList('/v1/gradle_instances', Items<GradleInstance>, { query, ...options });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Delete gradle instance with given name
|
|
32
|
+
*/
|
|
33
|
+
delete(id: string, options?: RequestOptions): APIPromise<void> {
|
|
34
|
+
return this._client.delete(path`/v1/gradle_instances/${id}`, {
|
|
35
|
+
...options,
|
|
36
|
+
headers: buildHeaders([{ Accept: '*/*' }, options?.headers]),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get gradle instance with given ID
|
|
42
|
+
*/
|
|
43
|
+
get(id: string, options?: RequestOptions): APIPromise<GradleInstance> {
|
|
44
|
+
return this._client.get(path`/v1/gradle_instances/${id}`, options);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type GradleInstancesItems = Items<GradleInstance>;
|
|
49
|
+
|
|
50
|
+
export interface GradleInstance {
|
|
51
|
+
metadata: GradleInstance.Metadata;
|
|
52
|
+
|
|
53
|
+
spec: GradleInstance.Spec;
|
|
54
|
+
|
|
55
|
+
status: GradleInstance.Status;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export namespace GradleInstance {
|
|
59
|
+
export interface Metadata {
|
|
60
|
+
id: string;
|
|
61
|
+
|
|
62
|
+
createdAt: string;
|
|
63
|
+
|
|
64
|
+
organizationId: string;
|
|
65
|
+
|
|
66
|
+
displayName?: string;
|
|
67
|
+
|
|
68
|
+
labels?: { [key: string]: string };
|
|
69
|
+
|
|
70
|
+
terminatedAt?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface Spec {
|
|
74
|
+
inactivityTimeout: string;
|
|
75
|
+
|
|
76
|
+
region: string;
|
|
77
|
+
|
|
78
|
+
hardTimeout?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface Status {
|
|
82
|
+
token: string;
|
|
83
|
+
|
|
84
|
+
state: 'unknown' | 'creating' | 'assigned' | 'ready' | 'terminated';
|
|
85
|
+
|
|
86
|
+
apiUrl?: string;
|
|
87
|
+
|
|
88
|
+
errorMessage?: string;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Machine-readable reason the instance was terminated. Always present once state
|
|
92
|
+
* is "terminated", never present before that. New values may be added over time,
|
|
93
|
+
* so treat any unrecognized value as "Unknown". Known values:
|
|
94
|
+
*
|
|
95
|
+
* - "UserRequested": terminated by a delete request to the API.
|
|
96
|
+
* - "InactivityTimeout": the timeout given in spec.inactivityTimeout elapsed.
|
|
97
|
+
* - "HardTimeout": the timeout given in spec.hardTimeout elapsed.
|
|
98
|
+
* - "Unknown": terminated for a cause the platform did not attribute, including
|
|
99
|
+
* instances that failed to get ready during creation. See errorMessage for
|
|
100
|
+
* details when available.
|
|
101
|
+
*/
|
|
102
|
+
terminationReason?: string;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface GradleInstanceCreateParams {
|
|
107
|
+
/**
|
|
108
|
+
* Query param: If there is another instance with given labels and region, return
|
|
109
|
+
* that one instead of creating a new instance.
|
|
110
|
+
*/
|
|
111
|
+
reuseIfExists?: boolean;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Query param: Return after the instance is ready to connect.
|
|
115
|
+
*/
|
|
116
|
+
wait?: boolean;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Body param
|
|
120
|
+
*/
|
|
121
|
+
metadata?: GradleInstanceCreateParams.Metadata;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Body param
|
|
125
|
+
*/
|
|
126
|
+
spec?: GradleInstanceCreateParams.Spec;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export namespace GradleInstanceCreateParams {
|
|
130
|
+
export interface Metadata {
|
|
131
|
+
displayName?: string;
|
|
132
|
+
|
|
133
|
+
labels?: { [key: string]: string };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface Spec {
|
|
137
|
+
clues?: Array<Spec.Clue>;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* After how many minutes should the instance be terminated. Example values 1m,
|
|
141
|
+
* 10m, 3h. Default is "0" which means no hard timeout.
|
|
142
|
+
*/
|
|
143
|
+
hardTimeout?: string;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* After how many minutes of inactivity should the instance be terminated. Example
|
|
147
|
+
* values 1m, 10m, 3h. Default is 5m.
|
|
148
|
+
*/
|
|
149
|
+
inactivityTimeout?: string;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Where the instance will be created. If not given, the region is decided based on
|
|
153
|
+
* scheduling clues (client IP) and availability.
|
|
154
|
+
*
|
|
155
|
+
* A region is a preference, not a hard pin: the request always overflows to every
|
|
156
|
+
* other available region, ordered by proximity, when the preferred ones are full.
|
|
157
|
+
*
|
|
158
|
+
* Accepted values:
|
|
159
|
+
*
|
|
160
|
+
* - A specific region name (e.g. "us-west1"). It is tried first, then the
|
|
161
|
+
* remaining regions in order of proximity to it. Scheduling clues (client IP)
|
|
162
|
+
* are ignored when a region is given.
|
|
163
|
+
* - A region group name (e.g. "us", "eu"). Its member regions are tried first in
|
|
164
|
+
* their listed order, then the remaining regions by proximity to the first
|
|
165
|
+
* member.
|
|
166
|
+
* - A pipe-separated, ordered list of regions (e.g. "us-east1|us-west1"). Those
|
|
167
|
+
* are tried first in the given order, then the remaining regions by proximity to
|
|
168
|
+
* the first.
|
|
169
|
+
*/
|
|
170
|
+
region?: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export namespace Spec {
|
|
174
|
+
export interface Clue {
|
|
175
|
+
kind: 'ClientIP';
|
|
176
|
+
|
|
177
|
+
clientIp?: string;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface GradleInstanceListParams extends ItemsParams {
|
|
183
|
+
/**
|
|
184
|
+
* Labels filter to apply to instances to return. Expects a comma-separated list of
|
|
185
|
+
* key=value pairs (e.g., env=prod,region=us-west).
|
|
186
|
+
*/
|
|
187
|
+
labelSelector?: string;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* State filter to apply to gradle instances to return. Each comma-separated state
|
|
191
|
+
* will be used as part of an OR clause, e.g. "assigned,ready" will return all
|
|
192
|
+
* instances that are either assigned or ready.
|
|
193
|
+
*
|
|
194
|
+
* Valid states: creating, assigned, ready, terminated, unknown
|
|
195
|
+
*/
|
|
196
|
+
state?: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export declare namespace GradleInstances {
|
|
200
|
+
export {
|
|
201
|
+
type GradleInstance as GradleInstance,
|
|
202
|
+
type GradleInstancesItems as GradleInstancesItems,
|
|
203
|
+
type GradleInstanceCreateParams as GradleInstanceCreateParams,
|
|
204
|
+
type GradleInstanceListParams as GradleInstanceListParams,
|
|
205
|
+
};
|
|
206
|
+
}
|