provisionJavaRuntime function
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);
}
}