Skip to content

Commit b06fc18

Browse files
committed
Compiled Android builds name the TypeScript line of each Kotlin compile error
Gradle's output streams as before and is kept; when the build fails, the compiler's ns-native-retrace --build-log maps each Kotlin error in a generated file to the TypeScript line it came from (source-lines.json).
1 parent 857e0bb commit b06fc18

2 files changed

Lines changed: 104 additions & 10 deletions

File tree

‎lib/services/compiled-release-service.ts‎

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,35 @@ export class CompiledReleaseService {
414414
return ipaPath;
415415
}
416416

417+
/** Kotlin's errors in a failed build, at the TypeScript lines the compiled statements came from. */
418+
private async printKotlinErrorsInSource(
419+
projectData: IProjectData,
420+
projectRoot: string,
421+
log: string,
422+
): Promise<void> {
423+
const errors = log.split("\n").filter((line) => /^e: /.test(line));
424+
if (!errors.length || !this.$fs.exists(path.join(projectRoot, "source-lines.json"))) {
425+
return;
426+
}
427+
const logFile = path.join(projectRoot, "build", "compile-errors.log");
428+
this.$fs.writeFile(logFile, errors.join("\n") + "\n");
429+
try {
430+
const result = await this.$childProcess.spawnFromEvent(
431+
process.execPath,
432+
[this.getCompilerBin(projectData, "ns-native-retrace"), "--build-log", projectRoot, logFile],
433+
"close",
434+
{ cwd: projectData.projectDir },
435+
{ throwError: false },
436+
);
437+
const mapped = result.stdout.split("\n").filter((line, k) => line && line !== errors[k]);
438+
if (mapped.length) {
439+
this.$logger.error(`Kotlin errors in your source:\n${mapped.join("\n")}`);
440+
}
441+
} catch (err) {
442+
this.$logger.trace(`Mapping Kotlin errors to source lines failed: ${err.message}`);
443+
}
444+
}
445+
417446
private async buildAndroid(
418447
projectData: IProjectData,
419448
buildData: IAndroidBuildData,
@@ -437,13 +466,32 @@ export class CompiledReleaseService {
437466
);
438467
}
439468

440-
await this.$gradleCommandService.executeCommand(args, {
441-
cwd: projectRoot,
442-
message: "Gradle build (compiled release)...",
443-
gradlePath:
444-
buildData.gradlePath ??
445-
path.join(this.getPackageDir(projectData), "kit-android", "gradlew"),
446-
});
469+
// Gradle's output is shown as it comes and kept: Kotlin's errors point into
470+
// the generated files, and the compiler's line table names the TypeScript
471+
// line each came from.
472+
const output: string[] = [];
473+
const eventName = "compiledReleaseGradleOutput";
474+
const onOutput = ({ data, pipe }: { data: Buffer | string; pipe: string }) => {
475+
output.push(data.toString());
476+
(pipe === "stderr" ? process.stderr : process.stdout).write(data);
477+
};
478+
this.$childProcess.on(eventName, onOutput);
479+
try {
480+
await this.$gradleCommandService.executeCommand(args, {
481+
cwd: projectRoot,
482+
message: "Gradle build (compiled release)...",
483+
gradlePath:
484+
buildData.gradlePath ??
485+
path.join(this.getPackageDir(projectData), "kit-android", "gradlew"),
486+
stdio: "pipe",
487+
spawnOptions: { throwError: true, emitOptions: { eventName } },
488+
});
489+
} catch (err) {
490+
await this.printKotlinErrorsInSource(projectData, projectRoot, output.join(""));
491+
throw err;
492+
} finally {
493+
this.$childProcess.removeListener(eventName, onOutput);
494+
}
447495

448496
// The generated project is the app module itself, so AGP names its
449497
// outputs after the root project, which the compiler names --name.

‎test/services/compiled-release-service.ts‎

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { assert } from "chai";
2+
import { EventEmitter } from "events";
23
import * as fs from "fs";
34
import * as os from "os";
45
import * as path from "path";
@@ -20,6 +21,9 @@ let xcodebuildCalls: string[][];
2021
let writtenFiles: { [path: string]: string };
2122
let warnings: string[];
2223
let hookCalls: { name: string; args: any }[];
24+
let errorsLogged: string[];
25+
let gradleOutput: { data: string; pipe: string }[] | null;
26+
let retraceOutput: string;
2327

2428
function createProjectData(nsConfig: any = {}): any {
2529
return {
@@ -36,17 +40,18 @@ function createProjectData(nsConfig: any = {}): any {
3640

3741
function createService(nsConfig: any = {}): CompiledReleaseService {
3842
const injector: IInjector = new Yok();
39-
injector.register("childProcess", {
43+
const childProcess = Object.assign(new EventEmitter(), {
4044
spawnFromEvent: async (
4145
command: string,
4246
args: string[],
4347
_: string,
4448
options: any,
4549
) => {
4650
spawnCalls.push({ command, args, options });
47-
return { stdout: "", stderr: "", exitCode: 0 };
51+
return { stdout: retraceOutput, stderr: "", exitCode: 0 };
4852
},
4953
});
54+
injector.register("childProcess", childProcess);
5055
injector.register("errors", {
5156
fail: (message: string) => {
5257
throw new Error(message);
@@ -69,6 +74,12 @@ function createService(nsConfig: any = {}): CompiledReleaseService {
6974
injector.register("gradleCommandService", {
7075
executeCommand: async (args: string[], options: IGradleCommandOptions) => {
7176
gradleCalls.push({ args, options });
77+
if (gradleOutput) {
78+
for (const chunk of gradleOutput) {
79+
childProcess.emit(options.spawnOptions.emitOptions.eventName, chunk);
80+
}
81+
throw new Error("Command gradlew failed with exit code 1");
82+
}
7283
},
7384
});
7485
injector.register("iOSProvisionService", {
@@ -80,6 +91,8 @@ function createService(nsConfig: any = {}): CompiledReleaseService {
8091
injector.register("logger", {
8192
info: (): void => undefined,
8293
warn: (message: string) => warnings.push(message),
94+
error: (message: string) => errorsLogged.push(message),
95+
trace: (): void => undefined,
8396
});
8497
injector.register("mobileHelper", {
8598
isAndroidPlatform: (platform: string) =>
@@ -120,7 +133,10 @@ function installCompilerPackage(): string {
120133
path.join(packageDir, "package.json"),
121134
JSON.stringify({
122135
name: "@nativescript/compiler",
123-
bin: { "ns-native": "bin/ns-native.js" },
136+
bin: {
137+
"ns-native": "bin/ns-native.js",
138+
"ns-native-retrace": "bin/ns-native-retrace.js",
139+
},
124140
}),
125141
);
126142
return fs.realpathSync(packageDir);
@@ -138,6 +154,9 @@ describe("compiledReleaseService", () => {
138154
);
139155
spawnCalls = [];
140156
gradleCalls = [];
157+
errorsLogged = [];
158+
gradleOutput = null;
159+
retraceOutput = "";
141160
xcodebuildCalls = [];
142161
writtenFiles = {};
143162
warnings = [];
@@ -325,6 +344,33 @@ describe("compiledReleaseService", () => {
325344
);
326345
});
327346

347+
it("names the TypeScript line of each Kotlin error when the Gradle build fails", async () => {
348+
const packageDir = installCompilerPackage();
349+
const service = createService();
350+
const projectRoot = path.join(projectDir, "platforms", "compiled", "android");
351+
const kotlinError = `e: file://${projectRoot}/src/main/kotlin/org/example/demo/Home.kt:14:9 Unresolved reference 'x'.`;
352+
gradleOutput = [
353+
{ data: "> Task :compileReleaseKotlin FAILED\n", pipe: "stdout" },
354+
{ data: `${kotlinError}\n`, pipe: "stderr" },
355+
];
356+
retraceOutput = "e: app/home.ts:7: Unresolved reference 'x'. (Home.kt:14:9)\n";
357+
358+
let failure: Error;
359+
try {
360+
await service.build(platformData("Android"), createProjectData(), <any>{ release: true, ...keyStore });
361+
} catch (err) {
362+
failure = err;
363+
}
364+
365+
assert.match(failure.message, /gradlew failed/);
366+
assert.equal(gradleCalls[0].options.stdio, "pipe");
367+
const retrace = spawnCalls.find((c) => c.args[0].endsWith("ns-native-retrace.js"));
368+
assert.deepStrictEqual(retrace.args.slice(1, 3), ["--build-log", projectRoot]);
369+
assert.equal(retrace.args[0], path.join(packageDir, "bin", "ns-native-retrace.js"));
370+
assert.equal(writtenFiles[retrace.args[3]], `${kotlinError}\n`);
371+
assert.deepStrictEqual(errorsLogged, ["Kotlin errors in your source:\ne: app/home.ts:7: Unresolved reference 'x'. (Home.kt:14:9)"]);
372+
});
373+
328374
it("builds an App Bundle for --aab", async () => {
329375
installCompilerPackage();
330376
const service = createService();

0 commit comments

Comments
 (0)