provisionJavaRuntime function

Future<ProvisionResult> provisionJavaRuntime(
  1. JavaRuntimeSpec spec, {
  2. String? cacheDir,
  3. String? maven,
  4. bool offline = false,
  5. bool force = false,
  6. void log(
    1. String line
    )?,
  7. Map<String, String>? environment,
})

Resolves spec's jars with Maven into its runtime directory, writes classpath.txt and manifest.json, and verifies the result.

Already provisioned — classpath.txt and manifest.json both there — and not forced: only verifies. cacheDir overrides the cache base; maven is the mvn to run; offline runs it with -o. log receives progress lines.

The runtime is built in a staging directory beside the real one and renamed into place only when complete, with manifest.json written last. An interrupted run leaves at most a staging directory, which the next run discards — never a half-filled runtime that looks provisioned — and --force replaces the previous runtime only once the new one exists. A runtime that fails verification is still put in place, for inspection, but its manifest says "verified": false and resolveClassPath skips it.

Maven runs dependency:copy-dependencies with -DincludeScope=runtime — the default copies every scope, test included. Files are named artifactId-version.jar, so two artifacts that share both from different groups would collide; keep such a pair out of the POM. classpath.txt is written from the directory listing with relative paths, not from dependency:build-classpath, whose paths point into ~/.m2 and make the cache impossible to relocate.

Throws MavenNotFound, MavenFailed, or a StateError when the POM cannot be found or Maven copied nothing.

Implementation

Future<ProvisionResult> provisionJavaRuntime(
  JavaRuntimeSpec spec, {
  String? cacheDir,
  String? maven,
  bool offline = false,
  bool force = false,
  void Function(String line)? log,
  Map<String, String>? environment,
}) async {
  final runtimeDir = spec.runtimeDir(
    override: cacheDir,
    environment: environment,
  );

  if (!force && _isComplete(runtimeDir)) {
    log?.call('already provisioned in ${runtimeDir.path}');
    return ProvisionResult(
      runtimeDir: runtimeDir,
      verification: verifyJars(spec, recordedClassPath(runtimeDir)!),
      provisioned: false,
    );
  }

  final mvn = await locateMaven(explicit: maven, environment: environment);
  log?.call('maven: $mvn');

  final staging = Directory('${runtimeDir.path}.partial');
  if (staging.existsSync()) staging.deleteSync(recursive: true);
  staging.createSync(recursive: true);
  try {
    final pom = File(joinPath([staging.path, 'pom.xml']))
      ..writeAsStringSync(await readPackagePom(spec));

    log?.call('resolving dependencies...');
    final result = await _run(mvn, [
      '-B',
      '-f',
      pom.path,
      if (offline) '-o',
      '-DincludeScope=runtime',
      '-DoutputDirectory=${JavaRuntimeSpec.jarsDirName}',
      'dependency:copy-dependencies',
    ], workingDirectory: staging.path);
    if (result.exitCode != 0) {
      throw MavenFailed(result.exitCode, '${result.stdout}${result.stderr}');
    }

    final jarsDir = Directory(
      joinPath([staging.path, JavaRuntimeSpec.jarsDirName]),
    );
    final jars = jarsDir.existsSync()
        ? (jarsDir
              .listSync()
              .whereType<File>()
              .where((f) => f.path.endsWith('.jar'))
              .toList()
            ..sort((a, b) => a.path.compareTo(b.path)))
        : <File>[];
    if (jars.isEmpty) {
      throw StateError('Maven copied no jars into ${jarsDir.path}');
    }
    String nameOf(File f) => f.uri.pathSegments.last;

    File(
      joinPath([staging.path, JavaRuntimeSpec.classPathFileName]),
    ).writeAsStringSync(
      '${['# Written by provisionJavaRuntime. Paths are relative to this directory.', for (final jar in jars) '${JavaRuntimeSpec.jarsDirName}/${nameOf(jar)}'].join('\n')}\n',
    );

    final logging = spec.loggingProperties;
    if (logging != null) {
      File(
        joinPath([staging.path, JavaRuntimeSpec.loggingPropertiesFileName]),
      ).writeAsStringSync(logging);
    }

    final totalBytes = jars.fold<int>(0, (sum, f) => sum + f.lengthSync());
    final verification = verifyJars(spec, recordedClassPath(staging)!);

    // Last: its presence is what makes the runtime complete.
    File(
      joinPath([staging.path, JavaRuntimeSpec.manifestFileName]),
    ).writeAsStringSync(
      '${const JsonEncoder.withIndent('  ').convert({
        'runtimeId': spec.runtimeId,
        ...spec.manifestExtras,
        'jars': [
          for (final jar in jars) {'name': nameOf(jar), 'size': jar.lengthSync()},
        ],
        'verified': verification.ok,
      })}\n',
    );

    _replace(runtimeDir, replacement: staging);

    log?.call(
      '${jars.length} jars (${(totalBytes / (1024 * 1024)).toStringAsFixed(1)} '
      'MB) in ${runtimeDir.path}',
    );
    return ProvisionResult(
      runtimeDir: runtimeDir,
      // Checked again where it now lives, so any path it reports is real.
      verification: verifyJars(spec, recordedClassPath(runtimeDir)!),
      provisioned: true,
      jarCount: jars.length,
      totalBytes: totalBytes,
    );
  } finally {
    if (staging.existsSync()) staging.deleteSync(recursive: true);
  }
}