@lynxship/cli 0.1.7 → 0.1.10
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 +175 -19
- package/dist/android-toolchain.d.ts +23 -0
- package/dist/android-toolchain.d.ts.map +1 -0
- package/dist/android-toolchain.js +319 -0
- package/dist/artifact-build.d.ts +17 -0
- package/dist/artifact-build.d.ts.map +1 -0
- package/dist/artifact-build.js +56 -0
- package/dist/artifact-name.d.ts +1 -1
- package/dist/artifact-name.d.ts.map +1 -1
- package/dist/autolink.d.ts +3 -2
- package/dist/autolink.d.ts.map +1 -1
- package/dist/autolink.js +130 -6
- package/dist/bundle-build.d.ts +2 -0
- package/dist/bundle-build.d.ts.map +1 -1
- package/dist/bundle-build.js +11 -3
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/desktop-build.d.ts +22 -0
- package/dist/desktop-build.d.ts.map +1 -0
- package/dist/desktop-build.js +161 -0
- package/dist/desktop-signing.d.ts +14 -0
- package/dist/desktop-signing.d.ts.map +1 -0
- package/dist/desktop-signing.js +200 -0
- package/dist/guidance.d.ts.map +1 -1
- package/dist/guidance.js +113 -4
- package/dist/harmony-build.d.ts +22 -0
- package/dist/harmony-build.d.ts.map +1 -0
- package/dist/harmony-build.js +194 -0
- package/dist/index.js +333 -73
- package/dist/ios-toolchain.d.ts +19 -0
- package/dist/ios-toolchain.d.ts.map +1 -0
- package/dist/ios-toolchain.js +303 -0
- package/dist/lynx-devtool.d.ts +18 -0
- package/dist/lynx-devtool.d.ts.map +1 -0
- package/dist/lynx-devtool.js +125 -0
- package/dist/process-runner.d.ts +11 -0
- package/dist/process-runner.d.ts.map +1 -1
- package/dist/process-runner.js +29 -0
- package/dist/prompt.d.ts +1 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +6 -0
- package/dist/remote.d.ts +3 -3
- package/dist/remote.d.ts.map +1 -1
- package/dist/target-toolchain.d.ts +17 -0
- package/dist/target-toolchain.d.ts.map +1 -0
- package/dist/target-toolchain.js +122 -0
- package/dist/ui/components.d.ts.map +1 -1
- package/dist/ui/components.js +8 -2
- package/dist/web-build.d.ts +16 -0
- package/dist/web-build.d.ts.map +1 -0
- package/dist/web-build.js +116 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Build, sign, store, submit and update LynxJS applications from the terminal.
|
|
4
4
|
|
|
5
|
-
LynxShip connects a real Rspeedy bundle to the
|
|
6
|
-
toolchain, verifies the
|
|
7
|
-
|
|
8
|
-
code.
|
|
5
|
+
LynxShip connects a real Rspeedy bundle to the selected Lynx target, runs the
|
|
6
|
+
official target toolchain, verifies the artifact where the platform exposes an
|
|
7
|
+
official verifier, uploads immutable artifacts to Cloudflare R2 and exposes an
|
|
8
|
+
expiring download URL with a compact terminal QR code.
|
|
9
9
|
|
|
10
|
-
> LynxShip
|
|
11
|
-
>
|
|
12
|
-
> the
|
|
10
|
+
> LynxShip never fabricates a target artifact. Android and iOS require their
|
|
11
|
+
> native hosts; HarmonyOS requires the official Hvigor/DevEco host; Web and
|
|
12
|
+
> Desktop require the project's official Rspeedy/Lynxtron configuration.
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
@@ -33,8 +33,29 @@ The package exposes the `lynxship` executable.
|
|
|
33
33
|
- Android builds on Windows, macOS or Linux: JDK 17, Android SDK command-line
|
|
34
34
|
tools, `adb`, build tools and an executable project Gradle wrapper.
|
|
35
35
|
- iOS builds: macOS, Xcode and Xcode command-line tools.
|
|
36
|
+
- HarmonyOS builds: an official Lynx Harmony host, DevEco/OpenHarmony SDK,
|
|
37
|
+
`ohpm`, the project `hvigorw` wrapper, Java and the official HAP signing tool.
|
|
38
|
+
- Web builds: the project's Web environment (`environments.web`) or an
|
|
39
|
+
explicit `build:web` script that produces one `*.web.bundle` in `dist/`,
|
|
40
|
+
`build/web/` or `build/`.
|
|
41
|
+
- Desktop builds: the official Lynxtron host/builder, or an Electron Builder
|
|
42
|
+
host with `pack`, `build:desktop` or `build:app`, on a supported target OS.
|
|
36
43
|
- Cloudflare R2 credentials for artifact storage.
|
|
37
44
|
|
|
45
|
+
After a native host exists, inspect and repair the Android toolchain with:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
lynxship doctor --project-dir ./my-lynx-app --platform android
|
|
49
|
+
lynxship doctor --project-dir ./my-lynx-app --platform android --fix
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The Android doctor reads the project's wrapper and Android Gradle Plugin before
|
|
53
|
+
checking Java, the SDK path, `compileSdk`, Build Tools, `adb`, `apksigner` and
|
|
54
|
+
licenses. The wrapper is authoritative; a machine-wide Gradle installation is
|
|
55
|
+
not required. `--fix` may install missing SDK packages with `sdkmanager` only
|
|
56
|
+
after confirmation. It never silently installs Android Studio or a JDK, edits
|
|
57
|
+
Gradle files, changes the wrapper or touches signing credentials.
|
|
58
|
+
|
|
38
59
|
## First configuration
|
|
39
60
|
|
|
40
61
|
Run these once on each development or CI machine:
|
|
@@ -119,8 +140,11 @@ init Initialize or link a project
|
|
|
119
140
|
doctor Check the local toolchain and project
|
|
120
141
|
dev Run Rspeedy dev with Lynx Explorer QR/HMR
|
|
121
142
|
preview Preview the production bundle locally
|
|
143
|
+
devtool doctor Check Lynx DevTool and development runtime
|
|
144
|
+
trace doctor Check Lynx Trace prerequisites
|
|
145
|
+
recorder doctor Check Lynx Recorder prerequisites
|
|
122
146
|
build create Build, sign and upload an artifact
|
|
123
|
-
build all Build Android
|
|
147
|
+
build all Build Android, iOS, HarmonyOS, Web and Desktop
|
|
124
148
|
build list List build jobs
|
|
125
149
|
build status <id> Inspect one build job
|
|
126
150
|
build cancel <id> Cancel a build job
|
|
@@ -129,8 +153,8 @@ submit Submit the latest successful artifact
|
|
|
129
153
|
update Publish a signed OTA update
|
|
130
154
|
update rollback Roll back an OTA channel to a previous release
|
|
131
155
|
rollback Compatibility alias for update rollback
|
|
132
|
-
run Install an
|
|
133
|
-
logs Stream native logs
|
|
156
|
+
run Install an Android, iOS or HarmonyOS artifact
|
|
157
|
+
logs Stream Android, iOS or HarmonyOS native logs
|
|
134
158
|
autolink check Check Lynx native-library wiring
|
|
135
159
|
autolink codegen Run native-module codegen
|
|
136
160
|
ota doctor Check native OTA host integration
|
|
@@ -161,8 +185,11 @@ Next steps
|
|
|
161
185
|
```
|
|
162
186
|
|
|
163
187
|
`lynxship dev` is for Lynx Explorer and live QR/HMR development. The Android
|
|
164
|
-
host command creates the native Gradle project needed for a real APK/AAB.
|
|
165
|
-
|
|
188
|
+
host command creates the native Gradle project needed for a real APK/AAB. An
|
|
189
|
+
interactive real build creates a missing host after asking for the application
|
|
190
|
+
ID; CI must pass `--application-id`. Existing native directories are never
|
|
191
|
+
overwritten. `--local` remains a contract-test mode and never creates a fake
|
|
192
|
+
artifact.
|
|
166
193
|
|
|
167
194
|
To build both native platforms in one local workflow, use the multi-platform
|
|
168
195
|
selector:
|
|
@@ -180,10 +207,110 @@ signing setup. Windows and Linux can still run
|
|
|
180
207
|
`lynxship build --platform android`; use a macOS CI worker for the iOS half.
|
|
181
208
|
`--local` can exercise both contract paths without creating APK or IPA files.
|
|
182
209
|
|
|
183
|
-
The same guidance covers missing R2 or signing setup,
|
|
184
|
-
Autolink/codegen,
|
|
185
|
-
|
|
186
|
-
|
|
210
|
+
The same guidance covers missing R2 or signing setup, native SDK tools,
|
|
211
|
+
Autolink/codegen, target packaging, device tools, store credentials and OTA
|
|
212
|
+
compatibility. In automation, use `--json`; failures include a machine-readable
|
|
213
|
+
`nextSteps` array and optional `note` field.
|
|
214
|
+
|
|
215
|
+
## Web, HarmonyOS and Desktop targets
|
|
216
|
+
|
|
217
|
+
These adapters use existing official host contracts; they do not generate a
|
|
218
|
+
fictional native runtime.
|
|
219
|
+
|
|
220
|
+
### Web
|
|
221
|
+
|
|
222
|
+
Configure Rspeedy with the official Web environment and build the Web bundle:
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"build": {
|
|
227
|
+
"production": {
|
|
228
|
+
"web": {
|
|
229
|
+
"environment": "web",
|
|
230
|
+
"artifact": "dist/main.web.bundle"
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
lynxship doctor --platform web
|
|
239
|
+
lynxship build --platform web --profile production
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The adapter invokes the profile's `web.script`, then an existing project
|
|
243
|
+
`build:web` script, and only otherwise uses `rspeedy build --environment web`.
|
|
244
|
+
It then requires exactly one Web bundle output unless the profile declares an
|
|
245
|
+
explicit artifact path.
|
|
246
|
+
|
|
247
|
+
### HarmonyOS
|
|
248
|
+
|
|
249
|
+
Use the official Lynx Harmony integration under `harmony/`. The project must
|
|
250
|
+
provide `hvigorw`, `hvigorfile.ts`, `build-profile.json5` and
|
|
251
|
+
`oh-package.json5`. LynxShip runs `ohpm install`, the pinned wrapper in release
|
|
252
|
+
mode, then verifies the signed HAP with the official `hap-sign-tool.jar`.
|
|
253
|
+
|
|
254
|
+
Optional profile overrides are explicit and remain project-owned:
|
|
255
|
+
|
|
256
|
+
```json
|
|
257
|
+
{
|
|
258
|
+
"build": {
|
|
259
|
+
"production": {
|
|
260
|
+
"harmony": {
|
|
261
|
+
"mode": "module",
|
|
262
|
+
"product": "default",
|
|
263
|
+
"module": "entry@default",
|
|
264
|
+
"buildMode": "release",
|
|
265
|
+
"task": "assembleHap"
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
lynxship doctor --platform harmony
|
|
274
|
+
lynxship build --platform harmony --profile production --no-upload
|
|
275
|
+
lynxship run --platform harmony --artifact .lynxship/artifacts/<uuid>.hap
|
|
276
|
+
lynxship logs --platform harmony --device <device-id>
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Set `LYNXSHIP_HAP_SIGN_TOOL` or `build.<profile>.harmony.signTool`; HAP signing
|
|
280
|
+
keys and profiles remain owned by the Harmony project and are never generated
|
|
281
|
+
or printed by LynxShip.
|
|
282
|
+
|
|
283
|
+
### Desktop
|
|
284
|
+
|
|
285
|
+
Use the official Lynxtron host and builder, or an Electron Builder host. The
|
|
286
|
+
adapter invokes the configured `desktop.script`, then `pack`, `build:desktop`,
|
|
287
|
+
or `build:app`, and otherwise runs `lynxtron-builder --publish never`. It then
|
|
288
|
+
requires one distributable such as `.dmg`, `.exe`, `.appimage` or `.zip`.
|
|
289
|
+
|
|
290
|
+
```json
|
|
291
|
+
{
|
|
292
|
+
"build": {
|
|
293
|
+
"production": {
|
|
294
|
+
"desktop": {
|
|
295
|
+
"script": "pack"
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
lynxship doctor --platform desktop
|
|
304
|
+
lynxship build --platform desktop --profile production --no-upload
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Desktop signing is checked before and after packaging. Windows builds require
|
|
308
|
+
a valid Authenticode signature; macOS builds require a valid Apple code
|
|
309
|
+
signature when the artifact exposes an app bundle. Configure the Lynxtron/
|
|
310
|
+
`electron-builder` signing inputs (`WIN_CSC_LINK`/`CSC_LINK`, protected
|
|
311
|
+
passwords, or the platform identity). Production and uploaded artifacts stop
|
|
312
|
+
when verification fails. Use `--allow-unsigned --no-upload` only for local
|
|
313
|
+
packaging tests.
|
|
187
314
|
|
|
188
315
|
## OTA rollback
|
|
189
316
|
|
|
@@ -228,9 +355,10 @@ but no `android/` directory.
|
|
|
228
355
|
|
|
229
356
|
Production APK/AAB builds require a native Android host. The host is the
|
|
230
357
|
Android application that initializes Lynx, creates `LynxView`, loads the bundle
|
|
231
|
-
and contains the Gradle wrapper. `lynxship build`
|
|
232
|
-
|
|
233
|
-
contract state machine and never
|
|
358
|
+
and contains the Gradle wrapper. `lynxship build` creates the host when
|
|
359
|
+
`android/` is absent, or fails with a repair instruction when an existing host
|
|
360
|
+
is incomplete; `--local` only tests LynxShip's contract state machine and never
|
|
361
|
+
fabricates an APK.
|
|
234
362
|
|
|
235
363
|
To create a minimal host for a pure project:
|
|
236
364
|
|
|
@@ -258,6 +386,34 @@ script. On macOS, `lynxship build --platform ios` installs CocoaPods before
|
|
|
258
386
|
archiving. Xcode, CocoaPods and real Apple signing credentials are still
|
|
259
387
|
required for a signed IPA; the CLI never fabricates them.
|
|
260
388
|
|
|
389
|
+
Run the complete first-use diagnosis before building:
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
lynxship doctor --project-dir ./my-lynx-app --platform ios
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
The iOS doctor checks the active macOS developer directory, Xcode and
|
|
396
|
+
`xcodebuild`, `xcrun`, `codesign`, IPA verification tools, the host and scheme,
|
|
397
|
+
Xcode build settings, Apple team, valid Apple Distribution/Development
|
|
398
|
+
identity, CocoaPods when a `Podfile` exists, export method and provisioning
|
|
399
|
+
profile validity. It does not print certificate or provisioning-profile
|
|
400
|
+
contents. Xcode-managed signing is reported as a warning when profiles are not
|
|
401
|
+
cached locally; manual signing without a valid matching profile fails before
|
|
402
|
+
the archive starts.
|
|
403
|
+
|
|
404
|
+
### Lynx DevTool, Trace and Recorder
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
lynxship devtool doctor --platform android
|
|
408
|
+
lynxship trace doctor --platform android
|
|
409
|
+
lynxship recorder doctor --platform android
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
These checks validate the project development script, native DevTool/Trace
|
|
413
|
+
dependencies and USB transport. Trace and Recorder require Lynx's matching
|
|
414
|
+
`-dev` runtime; the official Lynx DevTool Desktop application performs the
|
|
415
|
+
actual recording and analysis.
|
|
416
|
+
|
|
261
417
|
## Package layout
|
|
262
418
|
|
|
263
419
|
The CLI is backed by the public `@lynxship/*` runtime packages in this
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type ToolchainCheckStatus = "pass" | "warn" | "fail";
|
|
2
|
+
export interface ToolchainCheck {
|
|
3
|
+
name: string;
|
|
4
|
+
status: ToolchainCheckStatus;
|
|
5
|
+
ok: boolean;
|
|
6
|
+
value: string;
|
|
7
|
+
fix?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface AndroidToolchainReport {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
checks: ToolchainCheck[];
|
|
12
|
+
sdkPath?: string;
|
|
13
|
+
compileSdk?: number;
|
|
14
|
+
buildToolsVersion?: string;
|
|
15
|
+
agpVersion?: string;
|
|
16
|
+
gradleVersion?: string;
|
|
17
|
+
javaVersion?: number;
|
|
18
|
+
sdkPackages: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare function inspectAndroidToolchain(root: string): Promise<AndroidToolchainReport>;
|
|
21
|
+
export declare function fixAndroidToolchain(root: string, report: AndroidToolchainReport, confirm: (message: string) => Promise<boolean>, onOutput?: (line: string) => void): Promise<void>;
|
|
22
|
+
export declare function formatAndroidToolchainFailure(report: AndroidToolchainReport): string;
|
|
23
|
+
//# sourceMappingURL=android-toolchain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"android-toolchain.d.ts","sourceRoot":"","sources":["../src/android-toolchain.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,oBAAoB,CAAC;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAgQD,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,sBAAsB,CAAC,CA2LjC;AAED,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,sBAAsB,EAC9B,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAC9C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAChC,OAAO,CAAC,IAAI,CAAC,CAef;AAED,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,sBAAsB,GAC7B,MAAM,CAQR"}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { homedir, platform } from "node:os";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join, normalize } from "node:path";
|
|
4
|
+
import { captureProcess, commandExists, executableExists, runProcess, } from "./process-runner.js";
|
|
5
|
+
const agpMinimumGradle = [
|
|
6
|
+
["9.2", "9.4.1"],
|
|
7
|
+
["9.1", "9.3.1"],
|
|
8
|
+
["9.0", "9.1.0"],
|
|
9
|
+
["8.13", "8.13"],
|
|
10
|
+
["8.12", "8.13"],
|
|
11
|
+
["8.11", "8.13"],
|
|
12
|
+
["8.10", "8.11.1"],
|
|
13
|
+
["8.9", "8.11.1"],
|
|
14
|
+
["8.8", "8.10.2"],
|
|
15
|
+
["8.7", "8.9"],
|
|
16
|
+
["8.6", "8.7"],
|
|
17
|
+
["8.5", "8.7"],
|
|
18
|
+
["8.4", "8.6"],
|
|
19
|
+
["8.3", "8.4"],
|
|
20
|
+
["8.2", "8.2"],
|
|
21
|
+
["8.1", "8.0"],
|
|
22
|
+
["8.0", "8.0"],
|
|
23
|
+
["7.4", "7.5"],
|
|
24
|
+
["7.3", "7.4"],
|
|
25
|
+
["7.2", "7.3.3"],
|
|
26
|
+
["7.1", "7.2"],
|
|
27
|
+
["7.0", "7.0"],
|
|
28
|
+
];
|
|
29
|
+
function readProjectFiles(root) {
|
|
30
|
+
const files = [
|
|
31
|
+
"android/build.gradle",
|
|
32
|
+
"android/build.gradle.kts",
|
|
33
|
+
"android/settings.gradle",
|
|
34
|
+
"android/settings.gradle.kts",
|
|
35
|
+
"android/app/build.gradle",
|
|
36
|
+
"android/app/build.gradle.kts",
|
|
37
|
+
"android/gradle/libs.versions.toml",
|
|
38
|
+
];
|
|
39
|
+
return files
|
|
40
|
+
.filter((file) => existsSync(join(root, file)))
|
|
41
|
+
.map((file) => readFileSync(join(root, file), "utf8"))
|
|
42
|
+
.join("\n");
|
|
43
|
+
}
|
|
44
|
+
function readWrapperVersion(root) {
|
|
45
|
+
const file = join(root, "android", "gradle", "wrapper", "gradle-wrapper.properties");
|
|
46
|
+
if (!existsSync(file))
|
|
47
|
+
return undefined;
|
|
48
|
+
const content = readFileSync(file, "utf8");
|
|
49
|
+
return content.match(/gradle-([0-9]+(?:\.[0-9]+){1,2})-(?:bin|all)\.zip/)?.[1];
|
|
50
|
+
}
|
|
51
|
+
function readAndroidInputs(root) {
|
|
52
|
+
const content = readProjectFiles(root);
|
|
53
|
+
const agpVersion = content.match(/com\.android\.(?:application|library)["']?\)?\s+version\s+["']([0-9]+(?:\.[0-9]+){1,2})["']/)?.[1] ??
|
|
54
|
+
content.match(/com\.android\.tools\.build:gradle:([0-9]+(?:\.[0-9]+){1,2})/)?.[1];
|
|
55
|
+
const compileSdk = content.match(/compileSdk(?:Version)?\s*(?:=|\s)\s*["']?(\d+)/)?.[1];
|
|
56
|
+
const buildToolsVersion = content.match(/buildToolsVersion\s*(?:=|\s)\s*["']([^"']+)["']/)?.[1];
|
|
57
|
+
return {
|
|
58
|
+
agpVersion,
|
|
59
|
+
compileSdk: compileSdk ? Number(compileSdk) : undefined,
|
|
60
|
+
buildToolsVersion,
|
|
61
|
+
gradleVersion: readWrapperVersion(root),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function versionParts(version) {
|
|
65
|
+
return version.split(".").map((part) => Number(part) || 0);
|
|
66
|
+
}
|
|
67
|
+
function compareVersions(left, right) {
|
|
68
|
+
const a = versionParts(left);
|
|
69
|
+
const b = versionParts(right);
|
|
70
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
71
|
+
const difference = (a[index] ?? 0) - (b[index] ?? 0);
|
|
72
|
+
if (difference)
|
|
73
|
+
return difference;
|
|
74
|
+
}
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
function requiredGradleForAgp(agp) {
|
|
78
|
+
if (!agp)
|
|
79
|
+
return undefined;
|
|
80
|
+
return agpMinimumGradle.find(([minimum]) => compareVersions(agp, minimum) >= 0)?.[1];
|
|
81
|
+
}
|
|
82
|
+
function requiredJavaForAgp(agp) {
|
|
83
|
+
if (!agp)
|
|
84
|
+
return 17;
|
|
85
|
+
if (compareVersions(agp, "8.0") >= 0)
|
|
86
|
+
return 17;
|
|
87
|
+
if (compareVersions(agp, "7.0") >= 0)
|
|
88
|
+
return 11;
|
|
89
|
+
return 8;
|
|
90
|
+
}
|
|
91
|
+
function sdkCandidates() {
|
|
92
|
+
const values = [process.env.ANDROID_SDK_ROOT, process.env.ANDROID_HOME];
|
|
93
|
+
if (platform() === "win32") {
|
|
94
|
+
values.push(process.env.LOCALAPPDATA
|
|
95
|
+
? join(process.env.LOCALAPPDATA, "Android", "Sdk")
|
|
96
|
+
: undefined);
|
|
97
|
+
}
|
|
98
|
+
else if (platform() === "darwin") {
|
|
99
|
+
values.push(join(homedir(), "Library", "Android", "sdk"));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
values.push(join(homedir(), "Android", "Sdk"));
|
|
103
|
+
}
|
|
104
|
+
return [
|
|
105
|
+
...new Set(values.filter((value) => Boolean(value)).map(normalize)),
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
function sdkExecutable(sdkPath, name) {
|
|
109
|
+
const extension = platform() === "win32" ? ".exe" : "";
|
|
110
|
+
const candidates = [
|
|
111
|
+
join(sdkPath, "platform-tools", `${name}${extension}`),
|
|
112
|
+
...readDirectories(join(sdkPath, "build-tools")).map((version) => join(sdkPath, "build-tools", version, `${name}${extension}`)),
|
|
113
|
+
];
|
|
114
|
+
return candidates.find(executableExists);
|
|
115
|
+
}
|
|
116
|
+
function readDirectories(directory) {
|
|
117
|
+
try {
|
|
118
|
+
return readdirSync(directory);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function sdkManagerPath(sdkPath) {
|
|
125
|
+
const extension = platform() === "win32" ? ".bat" : "";
|
|
126
|
+
const candidates = [
|
|
127
|
+
join(sdkPath, "cmdline-tools", "latest", "bin", `sdkmanager${extension}`),
|
|
128
|
+
...readDirectories(join(sdkPath, "cmdline-tools"))
|
|
129
|
+
.filter((name) => name !== "latest")
|
|
130
|
+
.map((name) => join(sdkPath, "cmdline-tools", name, "bin", `sdkmanager${extension}`)),
|
|
131
|
+
join(sdkPath, "tools", "bin", `sdkmanager${extension}`),
|
|
132
|
+
];
|
|
133
|
+
return (candidates.find(executableExists) ??
|
|
134
|
+
(commandExists("sdkmanager") ? "sdkmanager" : undefined));
|
|
135
|
+
}
|
|
136
|
+
async function javaVersion(javaPath, cwd) {
|
|
137
|
+
try {
|
|
138
|
+
const result = await captureProcess(javaPath, ["-version"], { cwd });
|
|
139
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
140
|
+
const match = output.match(/version\s+["'](\d+)(?:\.(\d+))?/i);
|
|
141
|
+
if (!match)
|
|
142
|
+
return undefined;
|
|
143
|
+
return Number(match[1]) === 1 ? Number(match[2]) : Number(match[1]);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function javaCandidates() {
|
|
150
|
+
const executable = platform() === "win32" ? "java.exe" : "java";
|
|
151
|
+
const values = [
|
|
152
|
+
process.env.JAVA_HOME
|
|
153
|
+
? join(process.env.JAVA_HOME, "bin", executable)
|
|
154
|
+
: undefined,
|
|
155
|
+
commandExists("java") ? "java" : undefined,
|
|
156
|
+
platform() === "win32"
|
|
157
|
+
? join(process.env.LOCALAPPDATA ?? "", "Programs", "Android", "Android Studio", "jbr", "bin", executable)
|
|
158
|
+
: undefined,
|
|
159
|
+
platform() === "win32"
|
|
160
|
+
? join(process.env.PROGRAMFILES ?? "", "Android", "Android Studio", "jbr", "bin", executable)
|
|
161
|
+
: undefined,
|
|
162
|
+
platform() === "win32"
|
|
163
|
+
? join(process.env.PROGRAMFILES ?? "", "Java", "jdk-17", "bin", executable)
|
|
164
|
+
: undefined,
|
|
165
|
+
platform() === "win32"
|
|
166
|
+
? join(process.env.PROGRAMFILES ?? "", "Eclipse Adoptium", "jdk-17", "bin", executable)
|
|
167
|
+
: undefined,
|
|
168
|
+
platform() === "darwin"
|
|
169
|
+
? "/Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java"
|
|
170
|
+
: undefined,
|
|
171
|
+
platform() === "darwin"
|
|
172
|
+
? "/Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java"
|
|
173
|
+
: undefined,
|
|
174
|
+
platform() === "linux" ? "/opt/android-studio/jbr/bin/java" : undefined,
|
|
175
|
+
platform() === "linux"
|
|
176
|
+
? join(homedir(), "android-studio", "jbr", "bin", "java")
|
|
177
|
+
: undefined,
|
|
178
|
+
];
|
|
179
|
+
return values.filter((value) => Boolean(value));
|
|
180
|
+
}
|
|
181
|
+
function check(name, status, value, fix) {
|
|
182
|
+
return { name, status, ok: status !== "fail", value, fix };
|
|
183
|
+
}
|
|
184
|
+
export async function inspectAndroidToolchain(root) {
|
|
185
|
+
const inputs = readAndroidInputs(root);
|
|
186
|
+
const checks = [];
|
|
187
|
+
const wrapper = join(root, "android", platform() === "win32" ? "gradlew.bat" : "gradlew");
|
|
188
|
+
const wrapperExists = existsSync(wrapper) &&
|
|
189
|
+
(platform() === "win32" || executableExists(wrapper));
|
|
190
|
+
checks.push(check("gradle-wrapper", wrapperExists ? "pass" : "fail", wrapperExists
|
|
191
|
+
? inputs.gradleVersion
|
|
192
|
+
? `Gradle ${inputs.gradleVersion}`
|
|
193
|
+
: "found · version unknown"
|
|
194
|
+
: "missing", wrapperExists
|
|
195
|
+
? undefined
|
|
196
|
+
: "lynxship android host init --application-id com.example.myapp"));
|
|
197
|
+
const minimumGradle = requiredGradleForAgp(inputs.agpVersion);
|
|
198
|
+
const gradleCompatible = Boolean(inputs.gradleVersion &&
|
|
199
|
+
minimumGradle &&
|
|
200
|
+
compareVersions(inputs.gradleVersion, minimumGradle) >= 0);
|
|
201
|
+
const gradleStatus = !inputs.gradleVersion || !inputs.agpVersion
|
|
202
|
+
? "warn"
|
|
203
|
+
: gradleCompatible
|
|
204
|
+
? "pass"
|
|
205
|
+
: "fail";
|
|
206
|
+
checks.push(check("agp-gradle-compatibility", gradleStatus, inputs.agpVersion && inputs.gradleVersion
|
|
207
|
+
? `AGP ${inputs.agpVersion} · Gradle ${inputs.gradleVersion}${minimumGradle ? ` · minimum Gradle ${minimumGradle}` : ""}`
|
|
208
|
+
: "AGP or wrapper version not detected", gradleStatus === "fail" && minimumGradle
|
|
209
|
+
? `Update the project wrapper to Gradle ${minimumGradle}; do not use a global Gradle installation`
|
|
210
|
+
: undefined));
|
|
211
|
+
const requiredJava = requiredJavaForAgp(inputs.agpVersion);
|
|
212
|
+
let detectedJava;
|
|
213
|
+
let javaPath;
|
|
214
|
+
for (const candidate of javaCandidates()) {
|
|
215
|
+
const version = await javaVersion(candidate, root);
|
|
216
|
+
if (version !== undefined) {
|
|
217
|
+
detectedJava = version;
|
|
218
|
+
javaPath = candidate;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const javaStatus = detectedJava === undefined
|
|
223
|
+
? "fail"
|
|
224
|
+
: detectedJava >= requiredJava
|
|
225
|
+
? "pass"
|
|
226
|
+
: "fail";
|
|
227
|
+
checks.push(check("java", javaStatus, detectedJava === undefined
|
|
228
|
+
? "missing"
|
|
229
|
+
: `JDK ${detectedJava}${javaPath === "java" ? " · PATH" : ""}`, javaStatus === "fail"
|
|
230
|
+
? `Install/configure JDK ${requiredJava} and set JAVA_HOME, then run lynxship doctor --platform android`
|
|
231
|
+
: undefined));
|
|
232
|
+
const sdkPath = sdkCandidates().find((candidate) => existsSync(candidate));
|
|
233
|
+
const compileSdk = inputs.compileSdk;
|
|
234
|
+
const buildTools = inputs.buildToolsVersion;
|
|
235
|
+
const platformPackage = compileSdk
|
|
236
|
+
? `platforms;android-${compileSdk}`
|
|
237
|
+
: undefined;
|
|
238
|
+
const buildToolsPackage = buildTools
|
|
239
|
+
? `build-tools;${buildTools}`
|
|
240
|
+
: undefined;
|
|
241
|
+
const missingPackages = [
|
|
242
|
+
platformPackage &&
|
|
243
|
+
sdkPath &&
|
|
244
|
+
!existsSync(join(sdkPath, "platforms", `android-${compileSdk}`))
|
|
245
|
+
? platformPackage
|
|
246
|
+
: undefined,
|
|
247
|
+
buildTools &&
|
|
248
|
+
sdkPath &&
|
|
249
|
+
!existsSync(join(sdkPath, "build-tools", buildTools))
|
|
250
|
+
? buildToolsPackage
|
|
251
|
+
: undefined,
|
|
252
|
+
].filter((value) => Boolean(value));
|
|
253
|
+
const sdkStatus = !sdkPath
|
|
254
|
+
? "fail"
|
|
255
|
+
: missingPackages.length
|
|
256
|
+
? "fail"
|
|
257
|
+
: "pass";
|
|
258
|
+
checks.push(check("android-sdk", sdkStatus, !sdkPath
|
|
259
|
+
? "missing · set ANDROID_HOME or ANDROID_SDK_ROOT"
|
|
260
|
+
: missingPackages.length
|
|
261
|
+
? `missing: ${missingPackages.join(", ")}`
|
|
262
|
+
: sdkPath, sdkStatus === "fail"
|
|
263
|
+
? "Install Android Studio or the Android command-line tools, then run lynxship doctor --platform android --fix"
|
|
264
|
+
: undefined));
|
|
265
|
+
const manager = sdkPath ? sdkManagerPath(sdkPath) : undefined;
|
|
266
|
+
checks.push(check("sdkmanager", manager ? "pass" : sdkPath ? "warn" : "fail", manager
|
|
267
|
+
? "available"
|
|
268
|
+
: sdkPath
|
|
269
|
+
? "missing · use Android Studio SDK Manager"
|
|
270
|
+
: "unavailable until Android SDK is installed", manager && missingPackages.length
|
|
271
|
+
? `sdkmanager ${missingPackages.map((value) => `"${value}"`).join(" ")}`
|
|
272
|
+
: undefined));
|
|
273
|
+
const adb = sdkPath ? sdkExecutable(sdkPath, "adb") : undefined;
|
|
274
|
+
checks.push(check("adb", adb || commandExists("adb") ? "pass" : "warn", adb || commandExists("adb")
|
|
275
|
+
? "Android Platform-Tools available"
|
|
276
|
+
: "missing · required for lynxship run/logs", !adb && !commandExists("adb")
|
|
277
|
+
? "Install Android SDK Platform-Tools, then run adb devices"
|
|
278
|
+
: undefined));
|
|
279
|
+
const apksigner = sdkPath ? sdkExecutable(sdkPath, "apksigner") : undefined;
|
|
280
|
+
checks.push(check("apksigner", apksigner || commandExists("apksigner") ? "pass" : "warn", apksigner || commandExists("apksigner")
|
|
281
|
+
? "Android Build Tools available"
|
|
282
|
+
: "missing · required to verify APK signatures", !apksigner && !commandExists("apksigner")
|
|
283
|
+
? "Install Android SDK Build Tools, then run lynxship doctor --platform android"
|
|
284
|
+
: undefined));
|
|
285
|
+
checks.push(check("android-sdk-licenses", sdkPath && existsSync(join(sdkPath, "licenses")) ? "pass" : "warn", sdkPath && existsSync(join(sdkPath, "licenses"))
|
|
286
|
+
? "license directory found"
|
|
287
|
+
: "not verified", sdkPath ? "sdkmanager --licenses" : undefined));
|
|
288
|
+
const failures = checks.filter((item) => item.status === "fail");
|
|
289
|
+
return {
|
|
290
|
+
ok: failures.length === 0,
|
|
291
|
+
checks,
|
|
292
|
+
sdkPath,
|
|
293
|
+
compileSdk,
|
|
294
|
+
buildToolsVersion: buildTools,
|
|
295
|
+
agpVersion: inputs.agpVersion,
|
|
296
|
+
gradleVersion: inputs.gradleVersion,
|
|
297
|
+
javaVersion: detectedJava,
|
|
298
|
+
sdkPackages: missingPackages,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
export async function fixAndroidToolchain(root, report, confirm, onOutput) {
|
|
302
|
+
const managerCheck = report.checks.find((item) => item.name === "sdkmanager");
|
|
303
|
+
const manager = report.sdkPath ? sdkManagerPath(report.sdkPath) : undefined;
|
|
304
|
+
if (!manager || !report.sdkPackages.length)
|
|
305
|
+
return;
|
|
306
|
+
const packages = report.sdkPackages;
|
|
307
|
+
if (!(await confirm(`Install missing Android SDK packages (${packages.join(", ")}) now?`)))
|
|
308
|
+
return;
|
|
309
|
+
await runProcess(manager, packages, { cwd: root, onOutput });
|
|
310
|
+
if (managerCheck && !(await confirm("Accept Android SDK licenses now?")))
|
|
311
|
+
return;
|
|
312
|
+
await runProcess(manager, ["--licenses"], { cwd: root, onOutput });
|
|
313
|
+
}
|
|
314
|
+
export function formatAndroidToolchainFailure(report) {
|
|
315
|
+
return report.checks
|
|
316
|
+
.filter((item) => item.status === "fail")
|
|
317
|
+
.map((item) => `${item.name}: ${item.value}${item.fix ? ` · fix: ${item.fix}` : ""}`)
|
|
318
|
+
.join("; ");
|
|
319
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type BuildJob, type Platform } from "@lynxship/contracts";
|
|
2
|
+
import { type NativeArtifactExtension } from "./artifact-name.js";
|
|
3
|
+
export interface ArtifactBuildOptions {
|
|
4
|
+
root: string;
|
|
5
|
+
job: BuildJob;
|
|
6
|
+
platform: Platform;
|
|
7
|
+
artifactPath: string;
|
|
8
|
+
extension: NativeArtifactExtension;
|
|
9
|
+
contentType: string;
|
|
10
|
+
uploadArtifacts: boolean;
|
|
11
|
+
verificationMessage: string;
|
|
12
|
+
quiet?: boolean;
|
|
13
|
+
onEvent?: (message: string) => void;
|
|
14
|
+
onProgress?: (value?: number, label?: string) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function publishBuiltArtifact(options: ArtifactBuildOptions): Promise<BuildJob>;
|
|
17
|
+
//# sourceMappingURL=artifact-build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifact-build.d.ts","sourceRoot":"","sources":["../src/artifact-build.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,oBAAoB,CAAC;AAG5B,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,QAAQ,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,uBAAuB,CAAC;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,OAAO,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,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,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,QAAQ,CAAC,CA0EnB"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { copyFile, mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { transitionBuild } from "@lynxship/build-orchestrator";
|
|
4
|
+
import { assert, sha256, } from "@lynxship/contracts";
|
|
5
|
+
import { nativeArtifactName, } from "./artifact-name.js";
|
|
6
|
+
import { loadR2, uploadR2Artifact } from "./r2.js";
|
|
7
|
+
export async function publishBuiltArtifact(options) {
|
|
8
|
+
const { root, job, platform, artifactPath, extension, contentType, uploadArtifacts, onEvent, onProgress, } = options;
|
|
9
|
+
const artifactName = nativeArtifactName(extension);
|
|
10
|
+
const artifactDirectory = join(root, ".lynxship", "artifacts");
|
|
11
|
+
const artifactPathCopy = join(artifactDirectory, artifactName);
|
|
12
|
+
await mkdir(artifactDirectory, { recursive: true });
|
|
13
|
+
await copyFile(artifactPath, artifactPathCopy);
|
|
14
|
+
const content = await readFile(artifactPathCopy);
|
|
15
|
+
const hash = sha256(content);
|
|
16
|
+
job.attempts += 1;
|
|
17
|
+
transitionBuild(job, "signing", options.verificationMessage);
|
|
18
|
+
transitionBuild(job, "uploading_artifacts", `${platform} artifact collected`);
|
|
19
|
+
const step = (message, value) => {
|
|
20
|
+
onEvent?.(message);
|
|
21
|
+
onProgress?.(value, message);
|
|
22
|
+
};
|
|
23
|
+
if (!uploadArtifacts) {
|
|
24
|
+
step("Artifact collected locally; R2 upload skipped", 100);
|
|
25
|
+
job.artifact = {
|
|
26
|
+
name: artifactName,
|
|
27
|
+
hash,
|
|
28
|
+
path: artifactPathCopy,
|
|
29
|
+
size: content.length,
|
|
30
|
+
contentType,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
await loadR2(root);
|
|
35
|
+
step("Uploading artifact to Cloudflare R2…", 80);
|
|
36
|
+
const uploaded = await uploadR2Artifact(root, job.projectId, job.id, artifactPathCopy, contentType, undefined, {
|
|
37
|
+
onProgress: (uploadedBytes, totalBytes) => {
|
|
38
|
+
const transfer = totalBytes === 0 ? 1 : uploadedBytes / totalBytes;
|
|
39
|
+
onProgress?.(80 + transfer * 19, `Uploading artifact to Cloudflare R2… ${Math.round(transfer * 10000) / 100}%`);
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
assert(uploaded.hash === hash, "BUILD_ARTIFACT_HASH", "R2 artifact hash mismatch");
|
|
43
|
+
job.artifact = {
|
|
44
|
+
name: artifactName,
|
|
45
|
+
hash,
|
|
46
|
+
path: artifactPathCopy,
|
|
47
|
+
key: uploaded.key,
|
|
48
|
+
size: uploaded.size,
|
|
49
|
+
contentType: uploaded.contentType,
|
|
50
|
+
url: uploaded.url,
|
|
51
|
+
expiresAt: uploaded.expiresAt,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
step(`Artifact ready: ${artifactName}`, 100);
|
|
55
|
+
return transitionBuild(job, "success", `real ${platform} artifact created`);
|
|
56
|
+
}
|