execute method

  1. @override
Future<NativeStepResult> execute(
  1. NativeBuildContext context,
  2. ResolvedSource source
)
override

Execute this build step.

The context provides access to the build configuration and logger. Returns a NativeStepResult containing any artifacts produced by this step.

Implementation

@override
Future<NativeStepResult> execute(
  NativeBuildContext context,
  ResolvedSource source,
) async {
  final logger = context.logger;
  final r = runner ?? ProcessRunner(logger: logger);
  final expandedSourceDirectory = expandRecipeValue(
    sourceDirectory,
    context,
    source,
  );
  final srcDir = p.isAbsolute(expandedSourceDirectory)
      ? expandedSourceDirectory
      : p.join(source.directory.path, expandedSourceDirectory);
  final buildDir = buildDirectory != null
      ? (() {
          final expandedBuildDirectory = expandRecipeValue(
            buildDirectory!,
            context,
            source,
          );
          return p.isAbsolute(expandedBuildDirectory)
              ? expandedBuildDirectory
              : p.join(source.directory.path, expandedBuildDirectory);
        })()
      : p.join(srcDir, 'build');

  final buildDirEntity = Directory(buildDir);
  buildDirEntity.createSync(recursive: true);

  // Detect stale CMakeCache.txt from a previous source path
  final cacheFile = File(p.join(buildDir, 'CMakeCache.txt'));
  if (cacheFile.existsSync()) {
    final cacheContent = cacheFile.readAsStringSync();
    final homeDirMatch = RegExp(
      r'CMAKE_HOME_DIRECTORY:INTERNAL=(.+)',
    ).firstMatch(cacheContent);
    if (homeDirMatch != null) {
      final cachedSource = homeDirMatch.group(1);
      if (cachedSource != srcDir) {
        logger?.info(
          '[cmake_configure] Stale cache detected '
          '(cached source: $cachedSource, current: $srcDir). '
          'Cleaning build directory.',
        );
        buildDirEntity.deleteSync(recursive: true);
        buildDirEntity.createSync(recursive: true);
      }
    }
  }

  final args = <String>['-S', srcDir, '-B', buildDir];

  if (generator != null) {
    args.addAll(['-G', expandRecipeValue(generator!, context, source)]);
  }
  String? effectiveToolchain = toolchainFile;
  // Host execution must not use target toolchain
  final isHost = execution == 'host';
  if (effectiveToolchain == null && !isHost) {
    final resolver = const NativeToolchainResolver();
    effectiveToolchain = resolver.cmakeToolchainFile(context.target);
    if (effectiveToolchain != null) {
      logger?.info('[$id] Auto toolchain: $effectiveToolchain');
    }
  }
  if (effectiveToolchain != null) {
    final expandedToolchain = expandRecipeValue(
      effectiveToolchain,
      context,
      source,
    );
    args.addAll(['-DCMAKE_TOOLCHAIN_FILE=$expandedToolchain']);
  }
  // Auto-inject Android defaults when target is Android and not overridden.
  // Skip for host execution — host code generators run on the host compiler.
  if (!isHost && context.target.os.name == 'android') {
    final arch = context.target.architecture;
    final resolver = const NativeToolchainResolver();
    final abi = NativeToolchainResolver.androidAbiFor(arch);
    if (!defines.containsKey('ANDROID_ABI')) {
      args.add('-DANDROID_ABI=$abi');
    }
    if (!defines.containsKey('ANDROID_PLATFORM')) {
      args.add('-DANDROID_PLATFORM=android-24');
    }
    if (!defines.containsKey('ANDROID_STL')) {
      args.add('-DANDROID_STL=c++_static');
    }
    // OPENSSL_ROOT_DIR auto if not set and resolver knows NDK layout?
    if (!defines.containsKey('OPENSSL_ROOT_DIR') && resolver.hasAndroidNdk) {
      // Leave to recipe's {{ dependencies.openssl.prefix }} if present;
      // no default injection to avoid false paths.
    }
  }
  for (final entry in defines.entries) {
    args.add(
      '-D${entry.key}=${expandRecipeValue(entry.value, context, source)}',
    );
  }

  logger?.info('[$id] Running: cmake ${args.join(' ')}');
  await r.runStreaming('cmake', args, workingDirectory: Directory(buildDir));

  // Validate expected targets immediately after configure
  if (expectTargets.isNotEmpty) {
    // Check that CMakeCache mentions those targets or that the build files were generated
    // We do a lightweight check: look for build.ninja or Makefile that would contain the target
    final buildNinja = File(p.join(buildDir, 'build.ninja'));
    final makefile = File(p.join(buildDir, 'Makefile'));
    String? buildFileContent;
    if (buildNinja.existsSync()) {
      buildFileContent = buildNinja.readAsStringSync();
    } else if (makefile.existsSync()) {
      buildFileContent = makefile.readAsStringSync();
    }
    if (buildFileContent != null) {
      for (final t in expectTargets) {
        if (!buildFileContent.contains(t)) {
          throw StateError(
            'CMake configuration completed but required target "$t" was not generated.\n'
            'Relevant dependency errors may be in the log above.\n'
            'Possible missing configuration: OPENSSL_ROOT_DIR or toolchain.',
          );
        }
      }
      logger?.info(
        '[$id] Verified expected targets: ${expectTargets.join(', ')}',
      );
    } else {
      logger?.warning(
        '[$id] Could not verify expected targets ${expectTargets.join(', ')}: no build.ninja/Makefile found at $buildDir',
      );
    }
  }

  return const NativeStepResult();
}