Fix UnsupportedClassVersionError: ship Java 8 SonicMfContainerTool.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-24 09:33:35 +02:00
co-authored by Cursor
parent 22b8ff4e85
commit d6cf49532b
3 changed files with 251 additions and 74 deletions
@@ -210,24 +210,28 @@ public sealed class SonicMfApiExecutor
return (false, null, null, null, libError);
}
// Bevorzugt: vorcompilierte .jar/.class aus Tools\ (kein javac nötig).
if (TryResolvePrebuiltTool(out string? prebuiltDir, out string? prebuiltEntry, out string? prebuiltError)
int runtimeMajor = await GetJavaClassMajorAsync(javaExe, cancellationToken);
// Sonic/SMC nutzt typisch Java 8 (major 52). Nie mit höherer Class-Version starten.
// Vorcompilierte .class/.jar nur nutzen, wenn Class-Version zur Runtime passt.
if (TryResolvePrebuiltTool(runtimeMajor, out string? prebuiltDir, out string? prebuiltEntry, out string? prebuiltError)
&& prebuiltDir is not null && prebuiltEntry is not null)
{
string classpath = BuildClasspath(libDir, prebuiltEntry);
return (true, javaExe, prebuiltDir, classpath, null);
}
// Fallback: zur Laufzeit aus .java kompilieren (braucht javac + Sonic-Libs).
// Zur Laufzeit aus .java für Java 8 kompilieren (--release 8 / -source 1.8 -target 1.8).
string? sourcePath = ResolveToolSourcePath();
if (sourcePath is null)
{
return (false, null, null, null,
(prebuiltError ?? "Kein vorcompilierter SonicMfContainerTool gefunden.") +
" Tools\\SonicMfContainerTool.java/.class/.jar fehlen im Ausgabeverzeichnis.");
(prebuiltError ?? "Kein SonicMfContainerTool gefunden.") +
" Tools\\SonicMfContainerTool.java/.class fehlen.\n" +
$"Runtime-Java: {javaExe} (class major max={runtimeMajor}).");
}
string workDir = Path.Combine(Path.GetTempPath(), "esb-sonic-mf");
string workDir = Path.Combine(Path.GetTempPath(), "esb-sonic-mf-j8");
Directory.CreateDirectory(workDir);
string javaTarget = Path.Combine(workDir, "SonicMfContainerTool.java");
@@ -237,24 +241,33 @@ public sealed class SonicMfApiExecutor
string compileClasspath = BuildClasspath(libDir, null);
string runtimeClasspath = BuildClasspath(libDir, workDir);
bool needsCompile = !File.Exists(classFile)
|| File.GetLastWriteTimeUtc(classFile) < File.GetLastWriteTimeUtc(sourcePath);
if (needsCompile)
bool classOk = File.Exists(classFile)
&& ReadClassFileMajorVersion(classFile) is int existingMajor
&& existingMajor <= runtimeMajor
&& File.GetLastWriteTimeUtc(classFile) >= File.GetLastWriteTimeUtc(sourcePath);
if (!classOk)
{
string? javac = ResolveJavacExecutable(javaExe);
if (javac is null)
{
return (false, null, null, null,
"Vorcompilierter SonicMfContainerTool fehlt und javac wurde nicht gefunden. " +
"JDK installieren oder Tools\\SonicMfContainerTool.jar/.class mit ausliefern. " +
"SonicMfContainerTool muss für Java 8 gebaut werden, aber javac fehlt.\n" +
"Tools\\SonicMfContainerTool.class (major<=52) mit ausliefern oder JDK installieren.\n" +
$"Gefundene java.exe: {javaExe}");
}
// Wichtig: ohne --release 8 erzeugt modernes javac major 65 → UnsupportedClassVersionError auf Sonic-JRE 8.
string releaseArgs = SupportsJavacRelease8(javac)
? "--release 8"
: "-source 1.8 -target 1.8";
ProcessStartInfo compilePsi = new()
{
FileName = javac,
Arguments = $"-encoding UTF-8 -cp {Quote(compileClasspath)} -d {Quote(workDir)} {Quote(javaTarget)}",
Arguments =
$"{releaseArgs} -encoding UTF-8 -cp {Quote(compileClasspath)} -d {Quote(workDir)} {Quote(javaTarget)}",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
@@ -273,19 +286,25 @@ public sealed class SonicMfApiExecutor
string cErr = await compile.StandardError.ReadToEndAsync(cancellationToken);
await compile.WaitForExitAsync(cancellationToken);
if (compile.ExitCode != 0 || !File.Exists(classFile))
int? builtMajor = File.Exists(classFile) ? ReadClassFileMajorVersion(classFile) : null;
if (compile.ExitCode != 0 || builtMajor is null || builtMajor > runtimeMajor)
{
return (false, null, null, null,
"Kompilieren von SonicMfContainerTool fehlgeschlagen.\n" +
"Kompilieren von SonicMfContainerTool für Java 8 fehlgeschlagen.\n" +
Truncate((cErr + "\n" + cOut).Trim()) +
$"\nClasspath-Lib: {libDir}");
$"\nErzeugt major={builtMajor?.ToString() ?? "?"}, Runtime erlaubt <={runtimeMajor}.\n" +
$"Classpath-Lib: {libDir}");
}
}
return (true, javaExe, workDir, runtimeClasspath, null);
}
private bool TryResolvePrebuiltTool(out string? workDir, out string? classpathEntry, out string? error)
private bool TryResolvePrebuiltTool(
int runtimeMajor,
out string? workDir,
out string? classpathEntry,
out string? error)
{
workDir = null;
classpathEntry = null;
@@ -300,27 +319,133 @@ public sealed class SonicMfApiExecutor
foreach (string dir in searchDirs.Where(Directory.Exists))
{
string jar = Path.Combine(dir, "SonicMfContainerTool.jar");
if (File.Exists(jar))
{
workDir = dir;
classpathEntry = jar;
return true;
}
string classFile = Path.Combine(dir, "SonicMfContainerTool.class");
if (File.Exists(classFile))
{
workDir = dir;
classpathEntry = dir;
return true;
int? major = ReadClassFileMajorVersion(classFile);
if (major is not null && major <= runtimeMajor)
{
workDir = dir;
classpathEntry = dir;
return true;
}
error =
$"Tools\\SonicMfContainerTool.class ist zu neu (major={major}, braucht <={runtimeMajor} / Java 8).";
continue;
}
string jar = Path.Combine(dir, "SonicMfContainerTool.jar");
if (File.Exists(jar))
{
// JAR ohne Version-Check nur wenn Runtime >= 52; bei Unsicherheit neu kompilieren.
if (runtimeMajor >= 52)
{
workDir = dir;
classpathEntry = jar;
return true;
}
}
}
error = "Tools\\SonicMfContainerTool.jar/.class nicht gefunden.";
error ??= "Tools\\SonicMfContainerTool.class (Java 8) nicht gefunden.";
return false;
}
private static int? ReadClassFileMajorVersion(string classFile)
{
try
{
byte[] header = new byte[8];
using FileStream fs = File.OpenRead(classFile);
if (fs.Read(header, 0, 8) < 8)
{
return null;
}
// CA FE BA BE | minor | major
if (header[0] != 0xCA || header[1] != 0xFE || header[2] != 0xBA || header[3] != 0xBE)
{
return null;
}
return (header[6] << 8) | header[7];
}
catch
{
return null;
}
}
private static async Task<int> GetJavaClassMajorAsync(string javaExe, CancellationToken cancellationToken)
{
try
{
ProcessStartInfo psi = new()
{
FileName = javaExe,
Arguments = "-version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using Process p = new() { StartInfo = psi };
if (!p.Start())
{
return 52; // Sonic-Default Java 8
}
string err = await p.StandardError.ReadToEndAsync(cancellationToken);
string output = await p.StandardOutput.ReadToEndAsync(cancellationToken);
await p.WaitForExitAsync(cancellationToken);
string text = (err + "\n" + output).ToLowerInvariant();
if (text.Contains("version \"1.8") || text.Contains("version \"8"))
{
return 52;
}
Match m = Regex.Match(text, @"version ""(\d+)");
if (m.Success && int.TryParse(m.Groups[1].Value, out int major) && major >= 9)
{
// Java 9+ class major = 44 + version => 9→53, 11→55, 17→61, 21→65
return 44 + major;
}
}
catch
{
// ignore
}
return 52;
}
private static bool SupportsJavacRelease8(string javacPath)
{
try
{
using Process p = Process.Start(new ProcessStartInfo
{
FileName = javacPath,
Arguments = "-version",
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
CreateNoWindow = true
})!;
string text = (p.StandardError.ReadToEnd() + p.StandardOutput.ReadToEnd()).ToLowerInvariant();
p.WaitForExit(5000);
// javac 9+ supports --release
return !text.Contains("1.8") && !text.Contains("1.7") && !text.Contains("1.6");
}
catch
{
return false;
}
}
private (string? Dir, string? Error) ResolveLibDirectoryDetailed()
{
List<(string Path, string Label)> candidates = [];