@lynxship/cli 0.1.4 → 0.1.7
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/README.md +90 -2
- package/dist/android-build.d.ts +1 -0
- package/dist/android-build.d.ts.map +1 -1
- package/dist/android-build.js +12 -19
- package/dist/android-host.d.ts +12 -0
- package/dist/android-host.d.ts.map +1 -0
- package/dist/android-host.js +70 -0
- package/dist/bundle-build.d.ts +7 -0
- package/dist/bundle-build.d.ts.map +1 -0
- package/dist/bundle-build.js +24 -0
- package/dist/guidance.d.ts +6 -0
- package/dist/guidance.d.ts.map +1 -0
- package/dist/guidance.js +354 -0
- package/dist/index.js +349 -93
- package/dist/ios-build.d.ts +1 -0
- package/dist/ios-build.d.ts.map +1 -1
- package/dist/ios-build.js +38 -12
- package/dist/ios-host.d.ts +17 -0
- package/dist/ios-host.d.ts.map +1 -0
- package/dist/ios-host.js +93 -0
- package/dist/ui/components.d.ts +2 -0
- package/dist/ui/components.d.ts.map +1 -1
- package/dist/ui/components.js +30 -16
- package/dist/ui/index.d.ts +3 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +6 -1
- package/package.json +5 -4
- package/templates/android-host/app/build.gradle +38 -0
- package/templates/android-host/app/proguard-rules.pro +2 -0
- package/templates/android-host/app/src/main/AndroidManifest.xml +20 -0
- package/templates/android-host/app/src/main/java/template/LynxShipApplication.java +13 -0
- package/templates/android-host/app/src/main/java/template/MainActivity.java +19 -0
- package/templates/android-host/app/src/main/java/template/ProjectTemplateProvider.java +32 -0
- package/templates/android-host/app/src/main/res/values/strings.xml +3 -0
- package/templates/android-host/app/src/main/res/values/themes.xml +6 -0
- package/templates/android-host/build.gradle +3 -0
- package/templates/android-host/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/templates/android-host/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/templates/android-host/gradle.properties +3 -0
- package/templates/android-host/gradlew +185 -0
- package/templates/android-host/gradlew.bat +89 -0
- package/templates/android-host/settings.gradle +20 -0
- package/templates/ios-host/ExportOptions.plist +16 -0
- package/templates/ios-host/Podfile +42 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/AppDelegate.swift +11 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/Assets.xcassets/AppIcon.appiconset/Contents.json +13 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/Assets.xcassets/Contents.json +6 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/Base.lproj/LaunchScreen.storyboard +25 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/Base.lproj/Main.storyboard +24 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/DemoLynxProvider.swift +18 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/Hello-Lynx-Bridging-Header.h +4 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/Info.plist +25 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/SceneDelegate.swift +49 -0
- package/templates/ios-host/__IOS_TARGET_NAME__/ViewController.swift +22 -0
- package/templates/ios-host/__IOS_TARGET_NAME__.xcodeproj/project.pbxproj +452 -0
- package/templates/ios-host/sync-bundle.mjs +11 -0
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
|
-
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
6
6
|
import { BuildOrchestrator } from "@lynxship/build-orchestrator";
|
|
7
7
|
import { JsonRepository } from "@lynxship/db";
|
|
8
8
|
import { assert, createId, sha256, } from "@lynxship/contracts";
|
|
@@ -10,6 +10,8 @@ import { createSigningKey, signManifest, } from "@lynxship/signing";
|
|
|
10
10
|
import { AppStoreConnectApiProvider, GooglePlayApiProvider, SubmissionService, } from "@lynxship/submit";
|
|
11
11
|
import { DEFAULT_CONFIG, loadConfig, platformValue, } from "./config.js";
|
|
12
12
|
import { hasAndroidHost, isSupportedAndroidPlatform, runRealAndroidBuild, } from "./android-build.js";
|
|
13
|
+
import { initializeAndroidHost, suggestedAndroidApplicationId, } from "./android-host.js";
|
|
14
|
+
import { initializeIosHost, suggestedIosBundleIdentifier } from "./ios-host.js";
|
|
13
15
|
import { hasIosHost, runRealIosBuild } from "./ios-build.js";
|
|
14
16
|
import { configureAndroid, configureAppStoreConnect, configureGooglePlay, configureR2, } from "./configure.js";
|
|
15
17
|
import { fetchOtaPublicKey, publishOtaRelease, rollbackOtaRelease, submitRealArtifact, } from "./remote.js";
|
|
@@ -21,7 +23,10 @@ import { inspectAutolink, requireAutolinkReady } from "./autolink.js";
|
|
|
21
23
|
import { assertCompatibleBinaryBuild, inspectRuntimeFingerprint, } from "./runtime-fingerprint.js";
|
|
22
24
|
import { inspectOtaHost } from "./ota-doctor.js";
|
|
23
25
|
import { otaAssetName, otaAssetPaths } from "./ota-assets.js";
|
|
26
|
+
import { prompt } from "./prompt.js";
|
|
24
27
|
import { commandExists, packageManagerCommand, runProcess, runRspeedy, } from "./process-runner.js";
|
|
28
|
+
import { guidanceForError } from "./guidance.js";
|
|
29
|
+
import { buildLynxBundle } from "./bundle-build.js";
|
|
25
30
|
const rawArgs = process.argv.slice(2);
|
|
26
31
|
const args = [...rawArgs];
|
|
27
32
|
const ui = createCliUi(rawArgs);
|
|
@@ -52,6 +57,10 @@ function flag(name, fallback = null) {
|
|
|
52
57
|
const index = args.indexOf(name);
|
|
53
58
|
return index >= 0 ? (args[index + 1] ?? "true") : fallback;
|
|
54
59
|
}
|
|
60
|
+
async function assertInteractivePrompt(label, fallback, optionName) {
|
|
61
|
+
assert(ui.interactive, "CLI_INTERACTIVE_REQUIRED", `Pass ${label.toLowerCase()} with ${optionName} in non-interactive mode.`);
|
|
62
|
+
return prompt(label, fallback);
|
|
63
|
+
}
|
|
55
64
|
function printValue(value, view) {
|
|
56
65
|
if (json) {
|
|
57
66
|
console.log(JSON.stringify(typeof value === "string" ? { result: value } : value));
|
|
@@ -171,9 +180,134 @@ async function loadState() {
|
|
|
171
180
|
return { state, repository, builds, submissions };
|
|
172
181
|
}
|
|
173
182
|
async function saveState(state, repository, builds, submissions) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
183
|
+
const next = stateSaveQueue.then(async () => {
|
|
184
|
+
state.builds = builds.list();
|
|
185
|
+
state.submissions = submissions.list();
|
|
186
|
+
await repository.write(state);
|
|
187
|
+
});
|
|
188
|
+
stateSaveQueue = next.catch(() => undefined);
|
|
189
|
+
await next;
|
|
190
|
+
}
|
|
191
|
+
let stateSaveQueue = Promise.resolve();
|
|
192
|
+
async function executeBuild(options, showResult = true) {
|
|
193
|
+
const { config, profile, platform, skipUpload, wait, local, state, repository, builds, submissions, progress: sharedProgress, progressPrefix, onProgress, onEvent, skipBundleBuild, } = options;
|
|
194
|
+
await requireAutolinkReady(root, platform);
|
|
195
|
+
const runtime = await inspectRuntimeFingerprint(root, platform, config);
|
|
196
|
+
const job = await builds.create({
|
|
197
|
+
projectId: configuredProjectId(config),
|
|
198
|
+
organizationId: "local_org",
|
|
199
|
+
platform,
|
|
200
|
+
profile,
|
|
201
|
+
sourceHash: createHash("sha256").update(root).digest("hex"),
|
|
202
|
+
runtimeVersion: runtime.value,
|
|
203
|
+
runtimeInputs: runtime.inputs,
|
|
204
|
+
});
|
|
205
|
+
ui.info(`Using profile: ${profile} · platform: ${platform}`);
|
|
206
|
+
const ownsProgress = !sharedProgress;
|
|
207
|
+
const progress = sharedProgress ??
|
|
208
|
+
ui.progress(platform === "android" ? "Android build" : "iOS build");
|
|
209
|
+
const prefix = progressPrefix ? `${progressPrefix} · ` : "";
|
|
210
|
+
const reportEvent = (message) => {
|
|
211
|
+
if (onEvent)
|
|
212
|
+
onEvent(message);
|
|
213
|
+
else
|
|
214
|
+
progress.event(`${prefix}${message}`);
|
|
215
|
+
};
|
|
216
|
+
const reportProgress = (value, label) => {
|
|
217
|
+
if (onProgress)
|
|
218
|
+
onProgress(value, label);
|
|
219
|
+
else
|
|
220
|
+
progress.update(value, label ? `${prefix}${label}` : undefined);
|
|
221
|
+
};
|
|
222
|
+
try {
|
|
223
|
+
reportProgress(undefined, "Preparing build pipeline…");
|
|
224
|
+
if (wait) {
|
|
225
|
+
if (platform === "android" && !isSupportedAndroidPlatform() && !local)
|
|
226
|
+
assert(false, "ANDROID_PLATFORM_UNSUPPORTED", "Android builds are supported only on Linux, macOS and Windows.");
|
|
227
|
+
const realAndroid = platform === "android" &&
|
|
228
|
+
isSupportedAndroidPlatform() &&
|
|
229
|
+
(await hasAndroidHost(root));
|
|
230
|
+
const realIos = platform === "ios" && hasIosHost(root, config.build?.[profile]);
|
|
231
|
+
if (platform === "ios" && !realIos && !local)
|
|
232
|
+
assert(false, "IOS_HOST_REQUIRED", "A macOS Xcode host is required for a real iOS build. No local fake iOS build is created.");
|
|
233
|
+
if (platform === "android" && !realAndroid && !local)
|
|
234
|
+
assert(false, "ANDROID_HOST_REQUIRED", "This project has no Android Gradle host. For live development, run `lynxship dev` and scan the QR code with Lynx Explorer; for an APK/AAB, integrate the official Lynx Android host under `android/` (including android/gradlew). `--local` is only for contract tests and does not create an APK.");
|
|
235
|
+
if (realAndroid) {
|
|
236
|
+
await runRealAndroidBuild(job, {
|
|
237
|
+
root,
|
|
238
|
+
profile: config.build?.[profile] ?? {},
|
|
239
|
+
uploadArtifacts: !skipUpload,
|
|
240
|
+
skipBundleBuild,
|
|
241
|
+
quiet: json,
|
|
242
|
+
onEvent: reportEvent,
|
|
243
|
+
onProgress: reportProgress,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
else if (realIos) {
|
|
247
|
+
await runRealIosBuild(job, {
|
|
248
|
+
root,
|
|
249
|
+
profile: config.build?.[profile] ?? {},
|
|
250
|
+
uploadArtifacts: !skipUpload,
|
|
251
|
+
skipBundleBuild,
|
|
252
|
+
quiet: json,
|
|
253
|
+
onEvent: reportEvent,
|
|
254
|
+
onProgress: reportProgress,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
await builds.run(job.id);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
reportProgress(100);
|
|
262
|
+
await saveState(state, repository, builds, submissions);
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
await saveState(state, repository, builds, submissions);
|
|
266
|
+
if (error instanceof Error) {
|
|
267
|
+
Object.assign(error, { buildId: job.id });
|
|
268
|
+
}
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
if (ownsProgress)
|
|
273
|
+
progress.stop();
|
|
274
|
+
}
|
|
275
|
+
const result = builds.get(job.id);
|
|
276
|
+
if (showResult) {
|
|
277
|
+
printValue(result, {
|
|
278
|
+
title: `${platform === "android" ? "Android" : "iOS"} build result`,
|
|
279
|
+
rows: [
|
|
280
|
+
{ label: "Build ID", value: result.id, valueColor: "purple" },
|
|
281
|
+
{ label: "Platform", value: result.platform, valueColor: "blue" },
|
|
282
|
+
{ label: "Profile", value: result.profile, valueColor: "text" },
|
|
283
|
+
{
|
|
284
|
+
label: "Status",
|
|
285
|
+
value: result.state,
|
|
286
|
+
valueColor: result.state === "success" ? "green" : "yellow",
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
done: result.state === "success"
|
|
290
|
+
? "Build complete. Run lynxship submit to publish."
|
|
291
|
+
: "Build queued.",
|
|
292
|
+
});
|
|
293
|
+
if (result.state === "success" && result.artifact?.url)
|
|
294
|
+
ui.downloadArtifact(result.artifact.url, result.artifact.expiresAt);
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
async function buildSharedLynxBundle() {
|
|
299
|
+
const progress = ui.progress("Shared Lynx bundle");
|
|
300
|
+
try {
|
|
301
|
+
progress.update(undefined, "Building Lynx bundle once for Android and iOS…");
|
|
302
|
+
await buildLynxBundle(root, {
|
|
303
|
+
quiet: json,
|
|
304
|
+
onOutput: (message) => progress.event(message),
|
|
305
|
+
});
|
|
306
|
+
progress.update(100, "Shared Lynx bundle ready");
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
progress.stop();
|
|
310
|
+
}
|
|
177
311
|
}
|
|
178
312
|
async function initSelfHost() {
|
|
179
313
|
const directory = join(root, ".lynxship");
|
|
@@ -218,7 +352,7 @@ function helpText() {
|
|
|
218
352
|
Commands:
|
|
219
353
|
init Initialize or link a LynxShip project
|
|
220
354
|
doctor Check the local toolchain and project
|
|
221
|
-
dev Run
|
|
355
|
+
dev Run Rspeedy dev with Lynx Explorer QR/HMR
|
|
222
356
|
preview Preview the production Lynx bundle locally
|
|
223
357
|
inspect Inspect Rspeedy/Rspack configuration
|
|
224
358
|
profile Build with Rspack profiling enabled
|
|
@@ -232,17 +366,20 @@ Commands:
|
|
|
232
366
|
build status <id> Show one build job
|
|
233
367
|
build cancel <id> Cancel a build job
|
|
234
368
|
build retry <id> Retry a failed build job
|
|
369
|
+
build all Build Android and iOS on macOS
|
|
235
370
|
submit Submit the latest successful build
|
|
236
371
|
update Upload and publish a signed OTA update
|
|
237
372
|
update rollback Roll back an OTA channel to a previous release
|
|
238
373
|
rollback Alias for update rollback
|
|
239
374
|
self-host init Generate local self-host credentials
|
|
240
375
|
storage configure Configure Cloudflare R2 and encrypted R2 credentials
|
|
376
|
+
android host init Create a minimal official Lynx Android host
|
|
377
|
+
ios host init Create a minimal official Lynx iOS/Xcode host
|
|
241
378
|
android configure Configure or generate encrypted Android signing credentials
|
|
242
379
|
store configure Configure Google Play or App Store Connect submission
|
|
243
380
|
|
|
244
381
|
Build options:
|
|
245
|
-
--platform <p> Target android or
|
|
382
|
+
--platform <p> Target android, ios or all for build (default: android)
|
|
246
383
|
--profile <name> Build profile (default: production)
|
|
247
384
|
--no-wait Queue the build without executing it locally
|
|
248
385
|
--no-upload Keep the signed artifact local and skip R2 (CI verification)
|
|
@@ -286,6 +423,8 @@ Global options:
|
|
|
286
423
|
--banner Show the Braille LynxShip logo in a TTY
|
|
287
424
|
--project-dir <path> Use a LynxShip project from any working directory
|
|
288
425
|
--project-id <id> Project ID used by init
|
|
426
|
+
--application-id <id> Android package/application ID for host init
|
|
427
|
+
--bundle-identifier <id> iOS bundle identifier for host init
|
|
289
428
|
--library-dir <path> Native library directory for autolink codegen
|
|
290
429
|
--simulator Install an iOS .app on a simulator with simctl
|
|
291
430
|
--help Show this complete command reference
|
|
@@ -365,6 +504,9 @@ async function runRspeedyCommand(subcommand, environment) {
|
|
|
365
504
|
await initializeBuildProject();
|
|
366
505
|
const forwarded = forwardedToolArgs(args);
|
|
367
506
|
ui.info(`Running local Rspeedy ${subcommand}…`);
|
|
507
|
+
if (subcommand === "dev") {
|
|
508
|
+
ui.info("Lynx Explorer mode: no Android or iOS native host is required. Scan the QR code printed by Rspeedy; source changes reload automatically.");
|
|
509
|
+
}
|
|
368
510
|
await runRspeedy(root, subcommand, forwarded, {
|
|
369
511
|
env: environment,
|
|
370
512
|
quiet: json,
|
|
@@ -564,7 +706,7 @@ async function main() {
|
|
|
564
706
|
name: "package-manager-lockfile",
|
|
565
707
|
ok: Boolean(lockfile),
|
|
566
708
|
status: lockfile ? "pass" : "fail",
|
|
567
|
-
value: lockfile ?? "missing · fix:
|
|
709
|
+
value: lockfile ?? "missing · fix: pnpm install (or npm install)",
|
|
568
710
|
},
|
|
569
711
|
{
|
|
570
712
|
name: "lynxship.json",
|
|
@@ -588,7 +730,7 @@ async function main() {
|
|
|
588
730
|
status: androidHost ? "pass" : "fail",
|
|
589
731
|
value: androidHost
|
|
590
732
|
? "Gradle host found"
|
|
591
|
-
: "missing ·
|
|
733
|
+
: "missing · fix: lynxship android host init --application-id com.example.myapp",
|
|
592
734
|
},
|
|
593
735
|
]
|
|
594
736
|
: []),
|
|
@@ -598,7 +740,7 @@ async function main() {
|
|
|
598
740
|
status: configuration.r2 ? "pass" : "fail",
|
|
599
741
|
value: configuration.r2
|
|
600
742
|
? "configured"
|
|
601
|
-
: "
|
|
743
|
+
: "missing · fix: lynxship storage configure",
|
|
602
744
|
},
|
|
603
745
|
{
|
|
604
746
|
name: doctorPlatform === "android" ? "android-signing" : "ios-host",
|
|
@@ -613,10 +755,10 @@ async function main() {
|
|
|
613
755
|
value: doctorPlatform === "android"
|
|
614
756
|
? configuration.android
|
|
615
757
|
? "configured"
|
|
616
|
-
: "
|
|
758
|
+
: "missing · fix: lynxship android configure"
|
|
617
759
|
: process.platform === "darwin" && hasIosHost(root)
|
|
618
760
|
? "Xcode host found"
|
|
619
|
-
: "missing · fix: use macOS
|
|
761
|
+
: "missing · fix: use macOS, then lynxship ios host init --bundle-identifier com.example.myapp",
|
|
620
762
|
},
|
|
621
763
|
{
|
|
622
764
|
name: `lynx-autolink-${doctorPlatform}`,
|
|
@@ -780,9 +922,88 @@ async function main() {
|
|
|
780
922
|
});
|
|
781
923
|
return;
|
|
782
924
|
}
|
|
925
|
+
if (command === "ios") {
|
|
926
|
+
const subcommand = args.shift() ?? "host";
|
|
927
|
+
assert(subcommand === "host" && (args.shift() ?? "init") === "init", "CLI_IOS_HOST_COMMAND", "Use `lynxship ios host init` to create an iOS host.");
|
|
928
|
+
await initializeBuildProject();
|
|
929
|
+
const suggestedId = suggestedIosBundleIdentifier(root);
|
|
930
|
+
const bundleIdentifier = flag("--bundle-identifier") ??
|
|
931
|
+
(await assertInteractivePrompt("iOS bundle identifier", suggestedId, "--bundle-identifier"));
|
|
932
|
+
const result = await initializeIosHost(root, {
|
|
933
|
+
bundleIdentifier,
|
|
934
|
+
appName: basename(root),
|
|
935
|
+
});
|
|
936
|
+
ui.success(`iOS host created: ${result.directory}`);
|
|
937
|
+
printValue({
|
|
938
|
+
status: "created",
|
|
939
|
+
platform: "ios",
|
|
940
|
+
directory: result.directory,
|
|
941
|
+
bundleIdentifier: result.bundleIdentifier,
|
|
942
|
+
project: result.project,
|
|
943
|
+
scheme: result.scheme,
|
|
944
|
+
configUpdated: result.configUpdated,
|
|
945
|
+
}, {
|
|
946
|
+
title: "iOS host",
|
|
947
|
+
rows: [
|
|
948
|
+
{
|
|
949
|
+
label: "Bundle identifier",
|
|
950
|
+
value: result.bundleIdentifier,
|
|
951
|
+
valueColor: "blue",
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
label: "Xcode project",
|
|
955
|
+
value: result.project,
|
|
956
|
+
valueColor: "green",
|
|
957
|
+
},
|
|
958
|
+
{ label: "Scheme", value: result.scheme, valueColor: "purple" },
|
|
959
|
+
{
|
|
960
|
+
label: "CocoaPods",
|
|
961
|
+
value: "Run pod install on macOS before the first build",
|
|
962
|
+
valueColor: "yellow",
|
|
963
|
+
},
|
|
964
|
+
],
|
|
965
|
+
done: "iOS host is ready. Run lynxship doctor --platform ios on macOS.",
|
|
966
|
+
});
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
783
969
|
if (command === "android") {
|
|
970
|
+
const subcommand = args.shift() ?? "configure";
|
|
971
|
+
if (subcommand === "host") {
|
|
972
|
+
assert((args.shift() ?? "init") === "init", "CLI_ANDROID_HOST_COMMAND", "Use `lynxship android host init` to create an Android host.");
|
|
973
|
+
await initializeBuildProject();
|
|
974
|
+
const suggestedId = suggestedAndroidApplicationId(root);
|
|
975
|
+
const applicationId = flag("--application-id") ??
|
|
976
|
+
(await assertInteractivePrompt("Android application ID", suggestedId, "--application-id"));
|
|
977
|
+
const result = await initializeAndroidHost(root, {
|
|
978
|
+
applicationId,
|
|
979
|
+
appName: basename(root),
|
|
980
|
+
});
|
|
981
|
+
ui.success(`Android host created: ${result.directory}`);
|
|
982
|
+
printValue({
|
|
983
|
+
status: "created",
|
|
984
|
+
platform: "android",
|
|
985
|
+
directory: result.directory,
|
|
986
|
+
applicationId: result.applicationId,
|
|
987
|
+
}, {
|
|
988
|
+
title: "Android host",
|
|
989
|
+
rows: [
|
|
990
|
+
{
|
|
991
|
+
label: "Application ID",
|
|
992
|
+
value: result.applicationId,
|
|
993
|
+
valueColor: "blue",
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
label: "Gradle wrapper",
|
|
997
|
+
value: join(result.directory, "gradlew"),
|
|
998
|
+
valueColor: "green",
|
|
999
|
+
},
|
|
1000
|
+
],
|
|
1001
|
+
done: "Android host is ready. Run lynxship doctor, then lynxship build.",
|
|
1002
|
+
});
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
784
1005
|
assert(ui.interactive, "CLI_INTERACTIVE_REQUIRED", "Run `lynxship android configure` in an interactive terminal");
|
|
785
|
-
assert(
|
|
1006
|
+
assert(subcommand === "configure", "CLI_ANDROID_COMMAND", "Only android configure is available");
|
|
786
1007
|
ui.info("Configuring Android signing. Secret fields will stay invisible…");
|
|
787
1008
|
const result = await configureAndroid(root);
|
|
788
1009
|
ui.success(result.generated
|
|
@@ -1120,7 +1341,9 @@ async function main() {
|
|
|
1120
1341
|
}
|
|
1121
1342
|
assert(command === "build", "CLI_COMMAND", `Unknown command: ${command}`);
|
|
1122
1343
|
const subcommand = args[0] && !args[0].startsWith("--") ? args.shift() : "create";
|
|
1123
|
-
const
|
|
1344
|
+
const platformArgument = flag("--platform", "android");
|
|
1345
|
+
const buildAll = subcommand === "all" || platformArgument === "all";
|
|
1346
|
+
const platform = buildAll ? "android" : platformValue(platformArgument);
|
|
1124
1347
|
const skipUpload = args.includes("--no-upload");
|
|
1125
1348
|
await requireOperationalConfiguration(platform, { requireR2: !skipUpload });
|
|
1126
1349
|
if (subcommand === "list") {
|
|
@@ -1144,90 +1367,114 @@ async function main() {
|
|
|
1144
1367
|
printValue(job);
|
|
1145
1368
|
return;
|
|
1146
1369
|
}
|
|
1147
|
-
assert(subcommand === "create", "CLI_BUILD_COMMAND", `Unknown build command: ${subcommand}`);
|
|
1370
|
+
assert(subcommand === "create" || buildAll, "CLI_BUILD_COMMAND", `Unknown build command: ${subcommand}`);
|
|
1148
1371
|
const config = await loadConfig(root);
|
|
1149
1372
|
const profile = flag("--profile", "production");
|
|
1150
|
-
|
|
1151
|
-
const
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
platform
|
|
1156
|
-
profile,
|
|
1157
|
-
sourceHash: createHash("sha256").update(root).digest("hex"),
|
|
1158
|
-
runtimeVersion: runtime.value,
|
|
1159
|
-
runtimeInputs: runtime.inputs,
|
|
1160
|
-
});
|
|
1161
|
-
ui.info(`Using profile: ${profile} · platform: ${platform}`);
|
|
1162
|
-
const progress = ui.progress("Build execution");
|
|
1163
|
-
try {
|
|
1164
|
-
progress.update(undefined, "Preparing build pipeline…");
|
|
1165
|
-
if (!args.includes("--no-wait")) {
|
|
1166
|
-
if (platform === "android" &&
|
|
1167
|
-
!isSupportedAndroidPlatform() &&
|
|
1168
|
-
!args.includes("--local"))
|
|
1169
|
-
assert(false, "ANDROID_PLATFORM_UNSUPPORTED", "Android builds are supported only on Linux, macOS and Windows.");
|
|
1170
|
-
const realAndroid = platform === "android" &&
|
|
1171
|
-
isSupportedAndroidPlatform() &&
|
|
1172
|
-
(await hasAndroidHost(root));
|
|
1173
|
-
const realIos = platform === "ios" && hasIosHost(root, config.build?.[profile]);
|
|
1174
|
-
if (platform === "ios" && !realIos && !args.includes("--local"))
|
|
1175
|
-
assert(false, "IOS_HOST_REQUIRED", "A macOS Xcode host is required for a real iOS build. No local fake iOS build is created.");
|
|
1176
|
-
if (platform === "android" && !realAndroid && !args.includes("--local"))
|
|
1177
|
-
assert(false, "ANDROID_HOST_REQUIRED", "A real Android build requires an Android Gradle host. Add android/gradlew and the native Lynx host, or use --local for contract tests.");
|
|
1178
|
-
if (realAndroid) {
|
|
1179
|
-
await runRealAndroidBuild(job, {
|
|
1180
|
-
root,
|
|
1181
|
-
profile: config.build?.[profile] ?? {},
|
|
1182
|
-
uploadArtifacts: !skipUpload,
|
|
1183
|
-
quiet: json,
|
|
1184
|
-
onEvent: (message) => progress.event(message),
|
|
1185
|
-
onProgress: (value, label) => progress.update(value, label),
|
|
1186
|
-
});
|
|
1187
|
-
}
|
|
1188
|
-
else if (realIos) {
|
|
1189
|
-
await runRealIosBuild(job, {
|
|
1190
|
-
root,
|
|
1191
|
-
profile: config.build?.[profile] ?? {},
|
|
1192
|
-
uploadArtifacts: !skipUpload,
|
|
1193
|
-
quiet: json,
|
|
1194
|
-
onEvent: (message) => progress.event(message),
|
|
1195
|
-
onProgress: (value, label) => progress.update(value, label),
|
|
1196
|
-
});
|
|
1197
|
-
}
|
|
1198
|
-
else {
|
|
1199
|
-
await builds.run(job.id);
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
progress.update(100);
|
|
1203
|
-
await saveState(state, repository, builds, submissions);
|
|
1373
|
+
const wait = !args.includes("--no-wait");
|
|
1374
|
+
const local = args.includes("--local");
|
|
1375
|
+
const platforms = buildAll ? ["android", "ios"] : [platform];
|
|
1376
|
+
if (buildAll && wait && !local) {
|
|
1377
|
+
assert(process.platform === "darwin", "BUILD_ALL_MACOS_REQUIRED", "A real build for both Android and iOS requires macOS locally. Use `lynxship build --platform android` on Windows/Linux, or run this command on a macOS CI worker.");
|
|
1378
|
+
assert(await hasAndroidHost(root), "ANDROID_HOST_REQUIRED", "The Android host is required for `lynxship build --platform all`.");
|
|
1379
|
+
assert(hasIosHost(root, config.build?.[profile]), "IOS_HOST_REQUIRED", "The iOS Xcode host is required for `lynxship build --platform all`.");
|
|
1204
1380
|
}
|
|
1205
|
-
|
|
1206
|
-
await
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1381
|
+
if (!buildAll) {
|
|
1382
|
+
await executeBuild({
|
|
1383
|
+
config,
|
|
1384
|
+
profile,
|
|
1385
|
+
platform,
|
|
1386
|
+
skipUpload,
|
|
1387
|
+
wait,
|
|
1388
|
+
local,
|
|
1389
|
+
state,
|
|
1390
|
+
repository,
|
|
1391
|
+
builds,
|
|
1392
|
+
submissions,
|
|
1393
|
+
});
|
|
1394
|
+
return;
|
|
1211
1395
|
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1396
|
+
if (buildAll && wait && !local)
|
|
1397
|
+
await buildSharedLynxBundle();
|
|
1398
|
+
const progress = ui.progress("Android + iOS build");
|
|
1399
|
+
const progressValues = {
|
|
1400
|
+
android: 0,
|
|
1401
|
+
ios: 0,
|
|
1402
|
+
};
|
|
1403
|
+
const progressLabels = {
|
|
1404
|
+
android: "Starting Android build…",
|
|
1405
|
+
ios: "Starting iOS build…",
|
|
1406
|
+
};
|
|
1407
|
+
const platformName = (target) => target === "android" ? "Android" : "iOS";
|
|
1408
|
+
const outcomes = await Promise.allSettled(platforms.map((target) => executeBuild({
|
|
1409
|
+
config,
|
|
1410
|
+
profile,
|
|
1411
|
+
platform: target,
|
|
1412
|
+
skipUpload,
|
|
1413
|
+
wait,
|
|
1414
|
+
local,
|
|
1415
|
+
state,
|
|
1416
|
+
repository,
|
|
1417
|
+
builds,
|
|
1418
|
+
submissions,
|
|
1419
|
+
progress,
|
|
1420
|
+
progressPrefix: platformName(target),
|
|
1421
|
+
skipBundleBuild: wait && !local,
|
|
1422
|
+
onEvent: (message) => progress.event(`${platformName(target)} · ${message}`),
|
|
1423
|
+
onProgress: (value, label) => {
|
|
1424
|
+
if (value !== undefined)
|
|
1425
|
+
progressValues[target] = value;
|
|
1426
|
+
if (label)
|
|
1427
|
+
progressLabels[target] = label;
|
|
1428
|
+
const average = (progressValues.android + progressValues.ios) / platforms.length;
|
|
1429
|
+
progress.update(average, `${platformName(target)} · ${progressLabels[target]}`);
|
|
1430
|
+
},
|
|
1431
|
+
}, false)));
|
|
1432
|
+
progress.update(100, "Android and iOS builds finished");
|
|
1433
|
+
progress.stop();
|
|
1434
|
+
const summaryBuilds = outcomes.map((outcome, index) => {
|
|
1435
|
+
if (outcome.status === "fulfilled")
|
|
1436
|
+
return outcome.value;
|
|
1437
|
+
const error = outcome.reason;
|
|
1438
|
+
const buildId = error && typeof error === "object" && "buildId" in error
|
|
1439
|
+
? String(error.buildId)
|
|
1440
|
+
: undefined;
|
|
1441
|
+
const job = buildId ? builds.jobs.get(buildId) : undefined;
|
|
1442
|
+
return (job ?? {
|
|
1443
|
+
platform: platforms[index] ?? "android",
|
|
1444
|
+
state: "failed",
|
|
1445
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1446
|
+
});
|
|
1447
|
+
});
|
|
1448
|
+
const failed = summaryBuilds.some((result) => result.state === "failed");
|
|
1449
|
+
printValue({
|
|
1450
|
+
status: failed
|
|
1451
|
+
? "failed"
|
|
1452
|
+
: summaryBuilds.every((result) => result.state === "success")
|
|
1453
|
+
? "success"
|
|
1454
|
+
: "queued",
|
|
1455
|
+
builds: summaryBuilds,
|
|
1456
|
+
}, {
|
|
1457
|
+
title: "Build all result",
|
|
1458
|
+
rows: summaryBuilds.map((result) => ({
|
|
1459
|
+
label: platformName(result.platform),
|
|
1460
|
+
value: `${result.state}${"id" in result ? ` · ${result.id}` : ""}`,
|
|
1461
|
+
valueColor: result.state === "success"
|
|
1462
|
+
? "green"
|
|
1463
|
+
: result.state === "failed"
|
|
1464
|
+
? "red"
|
|
1465
|
+
: "yellow",
|
|
1466
|
+
})),
|
|
1467
|
+
done: failed
|
|
1468
|
+
? "At least one platform build failed. Review its events and retry that platform."
|
|
1469
|
+
: "Android and iOS build jobs completed. Submit each successful artifact separately.",
|
|
1228
1470
|
});
|
|
1229
|
-
|
|
1230
|
-
|
|
1471
|
+
for (const result of summaryBuilds)
|
|
1472
|
+
if (result.state === "success" &&
|
|
1473
|
+
"artifact" in result &&
|
|
1474
|
+
result.artifact?.url)
|
|
1475
|
+
ui.downloadArtifact(result.artifact.url, result.artifact.expiresAt);
|
|
1476
|
+
if (failed)
|
|
1477
|
+
process.exitCode = 5;
|
|
1231
1478
|
}
|
|
1232
1479
|
function exitCode(error) {
|
|
1233
1480
|
const code = error.code;
|
|
@@ -1248,14 +1495,23 @@ function exitCode(error) {
|
|
|
1248
1495
|
void main()
|
|
1249
1496
|
.catch((error) => {
|
|
1250
1497
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1498
|
+
const code = error.code ?? "CLI_ERROR";
|
|
1499
|
+
const nextSteps = guidanceForError(error);
|
|
1251
1500
|
if (json) {
|
|
1252
1501
|
console.log(JSON.stringify({
|
|
1253
1502
|
error: message,
|
|
1254
|
-
code
|
|
1503
|
+
code,
|
|
1504
|
+
...(nextSteps.commands.length > 0
|
|
1505
|
+
? {
|
|
1506
|
+
nextSteps: nextSteps.commands,
|
|
1507
|
+
...(nextSteps.note ? { note: nextSteps.note } : {}),
|
|
1508
|
+
}
|
|
1509
|
+
: {}),
|
|
1255
1510
|
}));
|
|
1256
1511
|
}
|
|
1257
1512
|
else {
|
|
1258
1513
|
ui.error(message);
|
|
1514
|
+
ui.nextSteps(nextSteps);
|
|
1259
1515
|
}
|
|
1260
1516
|
process.exitCode = exitCode(error);
|
|
1261
1517
|
})
|
package/dist/ios-build.d.ts
CHANGED
package/dist/ios-build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ios-build.d.ts","sourceRoot":"","sources":["../src/ios-build.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"ios-build.d.ts","sourceRoot":"","sources":["../src/ios-build.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAMhD,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACvD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAcxE;AAsDD,wBAAsB,eAAe,CACnC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,QAAQ,CAAC,CAuMnB"}
|