Skip to content

Commit 6ea8e6c

Browse files
authored
feat(builder): add skipGenerateIfCached parameter to CMakeBuilder (#26)
* feat(builder): add skipGenerateIfCached parameter to CMakeBuilder Add a new optional parameter `skipGenerateIfCached` to the `CMakeBuilder.run` method. When set to `true`, the builder will skip the CMake generation step if a valid CMakeCache.txt exists and the last generation was successful, improving build performance for repeated builds. The last generation status is persisted to a file in the output directory to track success. Add a corresponding unit test to verify the caching behavior. * fix: correct CMake generation skip logic The previous logic incorrectly skipped generation when skipGenerateIfCached was false. The condition was inverted, causing generation to be skipped when it should run. Now properly track skip state and only skip when both conditions are met. * fix: correct logic for skipping CMake generation The condition was inverted, causing generation to be skipped when it should run and vice versa. This prevented CMake projects from being generated when needed.
1 parent f6c8ec4 commit 6ea8e6c

File tree

3 files changed

+140
-7
lines changed

3 files changed

+140
-7
lines changed

lib/src/builder/builder.dart

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,17 @@ class CMakeBuilder implements Builder {
201201
/// Runs the CMake genetate and build process.
202202
///
203203
/// Completes with an error if the build fails.
204+
///
205+
/// - [skipGenerateIfCached]: Whether to skip generating the CMake project if it is already cached, that is,
206+
/// the `CMakeCache.txt` file exists in the output directory
207+
/// **AND** the last generate exit code is 0.
204208
@override
205-
Future<void> run({required BuildInput input, required BuildOutputBuilder output, Logger? logger}) async {
209+
Future<void> run({
210+
required BuildInput input,
211+
required BuildOutputBuilder output,
212+
Logger? logger,
213+
bool skipGenerateIfCached = false,
214+
}) async {
206215
// do not override user specified output directory if they also set buildLocal to true
207216
if (outDir == null && buildLocal) {
208217
final os = input.config.code.targetOS;
@@ -313,7 +322,7 @@ class CMakeBuilder implements Builder {
313322
);
314323
}
315324

316-
await task.run(environment: envVars);
325+
await task.run(environment: envVars, skipGenerateIfCached: skipGenerateIfCached);
317326
}
318327

319328
/// Get environment variables from vcvarsXXX.bat

lib/src/builder/run_builder.dart

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ class RunCMakeBuilder {
6161
/// log level of CMake
6262
final LogLevel logLevel;
6363

64+
/// save the last generate status of native_toolchain_cmake
65+
final String lastGenStatusFile;
66+
6467
RunCMakeBuilder({
6568
required this.input,
6669
required this.codeConfig,
@@ -74,6 +77,7 @@ class RunCMakeBuilder {
7477
this.androidArgs = const AndroidBuilderArgs(),
7578
this.appleArgs = const AppleBuilderArgs(),
7679
this.logLevel = LogLevel.STATUS,
80+
this.lastGenStatusFile = 'ntc_last_generate_status.txt',
7781
Uri? outputDir,
7882
UserConfig? userConfig,
7983
}) : outDir = outputDir ?? input.outputDirectory,
@@ -132,11 +136,30 @@ class RunCMakeBuilder {
132136

133137
Uri androidSysroot(ToolInstance compiler) => compiler.uri.resolve('../sysroot/');
134138

135-
Future<void> run({Map<String, String>? environment}) async {
136-
final result = await _generate(environment: environment);
137-
if (result.exitCode != 0) {
138-
throw Exception('Failed to generate CMake project: ${result.stderr}');
139+
/// Run CMake to generate and build the project.
140+
///
141+
/// - [environment] additional environment variables to pass to CMake.
142+
/// - [skipGenerateIfCached] whether to skip generating the CMake project.
143+
Future<void> run({Map<String, String>? environment, bool skipGenerateIfCached = false}) async {
144+
bool skipGenerate = false;
145+
if (skipGenerateIfCached) {
146+
final cmakeCacheExists = await File.fromUri(outDir.resolve('CMakeCache.txt')).exists();
147+
final lastGenExitCode = await getLastGenExitCode();
148+
if (cmakeCacheExists && lastGenExitCode == 0) {
149+
logger?.warning(
150+
'CMake project is already successfully generated '
151+
'and skipGenerateIfCached is requested, skip generating.',
152+
);
153+
skipGenerate = true;
154+
}
155+
}
156+
if (!skipGenerate) {
157+
final result = await _generate(environment: environment);
158+
if (result.exitCode != 0) {
159+
throw Exception('Failed to generate CMake project: ${result.stderr}');
160+
}
139161
}
162+
140163
final result1 = await _build(environment: environment);
141164
if (result1.exitCode != 0) {
142165
throw Exception('Failed to build CMake project: ${result1.stderr}');
@@ -168,7 +191,7 @@ class RunCMakeBuilder {
168191
}
169192
final _generator = generator.toArgs();
170193

171-
return runProcess(
194+
final results = await runProcess(
172195
executable: await cmakePath(),
173196
arguments: [
174197
'--log-level=${logLevel.name}',
@@ -187,6 +210,17 @@ class RunCMakeBuilder {
187210
throwOnUnexpectedExitCode: false,
188211
environment: environment,
189212
);
213+
214+
// save the last generate status
215+
final msg =
216+
"command: ${results.command}\n"
217+
"exitCode: ${results.exitCode}\n"
218+
// "stdout: ${results.stdout}\n"
219+
// "stderr: ${results.stderr}\n"
220+
;
221+
await File.fromUri(outDir.resolve(lastGenStatusFile)).writeAsString(msg);
222+
223+
return results;
190224
}
191225

192226
Future<RunProcessResult> _build({Map<String, String>? environment}) async {
@@ -306,6 +340,23 @@ class RunCMakeBuilder {
306340
return defs;
307341
}
308342

343+
/// Returns the exit code of the last CMake generate command.
344+
///
345+
/// -1: not exist
346+
/// 0: success
347+
/// other: failed
348+
Future<int> getLastGenExitCode() async {
349+
final statusFile = File.fromUri(outDir.resolve(lastGenStatusFile));
350+
if (!await statusFile.exists()) {
351+
return -1;
352+
}
353+
final content = await statusFile.readAsLines();
354+
final exitCode = int.tryParse(
355+
content.firstWhere((line) => line.startsWith('exitCode: ')).split('exitCode: ')[1],
356+
);
357+
return exitCode ?? -1;
358+
}
359+
309360
static const androidAbis = {
310361
Architecture.arm: 'armeabi-v7a',
311362
Architecture.arm64: 'arm64-v8a',
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
@OnPlatform({'mac-os': Timeout.factor(2), 'windows': Timeout.factor(10)})
2+
import 'dart:io';
3+
4+
import 'package:code_assets/code_assets.dart';
5+
import 'package:hooks/hooks.dart';
6+
import 'package:native_toolchain_cmake/native_toolchain_cmake.dart';
7+
import 'package:test/test.dart';
8+
9+
import '../helpers.dart';
10+
11+
void main() {
12+
const name = 'hello_world';
13+
const sourceDir = 'test/builder/testfiles/hello_world';
14+
15+
test('CMakeBuilder skipGenerateIfCached', () async {
16+
final tempUri = await tempDirForTest();
17+
final tempUri2 = await tempDirForTest();
18+
19+
final logMessages = <String>[];
20+
final logger = createCapturingLogger(logMessages);
21+
22+
final buildInputBuilder = BuildInputBuilder()
23+
..setupShared(
24+
packageName: name,
25+
packageRoot: tempUri,
26+
outputFile: tempUri.resolve('output.json'),
27+
outputDirectoryShared: tempUri2,
28+
)
29+
..config.setupBuild(linkingEnabled: false)
30+
..addExtension(
31+
CodeAssetExtension(
32+
targetOS: OS.current,
33+
macOS: OS.current == OS.macOS ? MacOSCodeConfig(targetVersion: defaultMacOSVersion) : null,
34+
targetArchitecture: Architecture.current,
35+
linkModePreference: LinkModePreference.dynamic,
36+
cCompiler: cCompiler,
37+
),
38+
);
39+
40+
final buildInput = BuildInput(buildInputBuilder.json);
41+
final buildOutput = BuildOutputBuilder();
42+
43+
final builder = CMakeBuilder.create(
44+
name: name,
45+
sourceDir: Directory(sourceDir).absolute.uri,
46+
buildMode: BuildMode.release,
47+
androidArgs: const AndroidBuilderArgs(),
48+
appleArgs: const AppleBuilderArgs(),
49+
);
50+
51+
// First run: Generate and build
52+
await builder.run(input: buildInput, output: buildOutput, logger: logger, skipGenerateIfCached: false);
53+
54+
// Verify first run logs DO NOT contain the skip message
55+
expect(logMessages, isNot(contains(contains('CMake project is already successfully generated'))));
56+
57+
// Clear logs for second run
58+
logMessages.clear();
59+
60+
// Second run: Skip generate if cached
61+
await builder.run(input: buildInput, output: buildOutput, logger: logger, skipGenerateIfCached: true);
62+
63+
// Verify second run logs DO contain the skip message
64+
expect(
65+
logMessages,
66+
contains(
67+
contains(
68+
'CMake project is already successfully generated and skipGenerateIfCached is requested, skip generating.',
69+
),
70+
),
71+
);
72+
});
73+
}

0 commit comments

Comments
 (0)