|
#!/usr/bin/env -S java --source=24 --enable-preview -DshowCommand=true |
|
|
|
import java.util.Map.Entry; |
|
|
|
import static java.lang.Boolean.getBoolean; |
|
import static java.lang.constant.ConstantDescs.CD_String; |
|
import static java.lang.constant.ConstantDescs.CD_void; |
|
import static java.lang.Integer.max; |
|
import static java.lang.Integer.MAX_VALUE; |
|
import static java.lang.reflect.AccessFlag.ABSTRACT; |
|
import static java.lang.reflect.AccessFlag.STATIC; |
|
import static java.lang.System.err; |
|
import static java.lang.System.exit; |
|
import static java.lang.System.getProperty; |
|
import static java.lang.System.out; |
|
import static java.util.Comparator.comparing; |
|
import static java.util.FormattableFlags.ALTERNATE; |
|
import static java.util.Locale.ROOT; |
|
import static java.util.stream.Collectors.groupingByConcurrent; |
|
import static java.util.stream.Collectors.toCollection; |
|
import static java.util.stream.Gatherers.mapConcurrent; |
|
|
|
import module java.base; |
|
|
|
/// Run: |
|
/// <pre>java --show-version -DshowCommand=true MainMethodFinderModern.jsh</pre> |
|
/// |
|
/// Run with different Java Home: |
|
/// <pre>java -DshowCommand=true MainMethodFinderModern.jsh /Library/Java/JavaVirtualMachines/jdk-24.jdk/Contents/Home</pre> |
|
/// |
|
/// Compile with GraalVM Native Image: |
|
/// <pre>native-image -cp target/classes wb.java24.MainMethodFinderModern MainMethodFinderModern</pre> |
|
/// |
|
/// Run GraalVM Native Image |
|
/// <pre>./MainMethodFinderModern -DshowCommand=true /Library/Java/JavaVirtualMachines/jdk-24.jdk/Contents/Home</pre> |
|
sealed interface JArchive extends Entry<Path, String>, Formattable permits JArchive.Jmod, JArchive.Jar { |
|
boolean SHOW_COMMAND = getBoolean("showCommand"); |
|
|
|
@Override default void formatTo(Formatter f, int flags, int w, int p) { f.format("%s %s", getKey(), getValue()); } |
|
|
|
record Jmod(Path getKey/* moduleName */, String getValue, Path cmd, Path modulePath, Path jmod) implements JArchive { |
|
Jmod(boolean isCustomJdk, Path home, Path jmod, String fqn) { |
|
this(Path.of(jmod.getFileName().toString().replace(".jmod", "")),fqn, |
|
isCustomJdk ? home.resolve("bin", "java") : Path.of("java"), |
|
(getBoolean("showCommand") && isCustomJdk) ? home.resolve("jmods") : Path.of(""), jmod); |
|
} |
|
@Override public void formatTo(Formatter f, int flags, int w, int p) { |
|
if ((flags & ALTERNATE) == 0) JArchive.super.formatTo(f, flags, w, p); |
|
else f.format("%s%s -m %s/%s", cmd(), modulePath().toString().isEmpty() ? "" : " --module-path " + modulePath(), getKey(), getValue()); |
|
} |
|
} |
|
|
|
record Jar(Path getKey/* classPath */, String getValue, Path cmd, Path classPath) implements JArchive { |
|
Jar(boolean isCustomJdk, Path home, Path jar, String fqn) { |
|
this(home.relativize(jar), fqn, |
|
isCustomJdk ? home.resolve("bin", "java") : Path.of("java"), jar); |
|
} |
|
@Override public void formatTo(Formatter f, int flags, int w, int p) { |
|
if ((flags & ALTERNATE) == 0) JArchive.super.formatTo(f, flags, w, p); |
|
else f.format("%s -cp %s %s", cmd(), classPath(), getValue()); |
|
} |
|
} |
|
|
|
@Override default String setValue(String ignored) { throw new UnsupportedOperationException("Immutable!"); } |
|
|
|
static void main(String... args) throws IOException { |
|
final var isCustomJdkSupplied = args.length > 0; |
|
if (!isCustomJdkSupplied && getProperty("org.graalvm.nativeimage.imagecode") != null) { |
|
err.println(""" |
|
MainMethodFinderModern: Missing path operand. |
|
Usage: MainMethodFinderModern path/to/jdk"""); |
|
exit(1); |
|
return; |
|
} |
|
final var javaHome = isCustomJdkSupplied |
|
? Path.of(args[0]) |
|
: ProcessHandle.current().info().command().map(Path::of).map(Path::getParent).map(Path::getParent).orElseThrow(); |
|
|
|
try (var javaArchives = Files.find(javaHome, MAX_VALUE, (p, attrs) -> attrs.isRegularFile() && (p.toString().endsWith(".jar") || p.toString().endsWith(".jmod")))) { |
|
javaArchives |
|
.gather(mapConcurrent(max(1, Runtime.getRuntime().availableProcessors() - 1), javaArchive -> { |
|
try (var zfs = FileSystems.newFileSystem(javaArchive); |
|
var root = Files.find(zfs.getRootDirectories().iterator().next(), MAX_VALUE, (p, _) -> p.toString().endsWith(".class"))) { |
|
return root |
|
.map(classFilePath -> { |
|
try (var is = zfs.provider().newInputStream(classFilePath)) { return is.readAllBytes(); } |
|
catch (IOException e) { err.println("Error scanning " + javaArchive + ": " + e.getMessage()); return null; } |
|
}).filter(Objects::nonNull) |
|
.map(ClassFile.of()::parse) |
|
.filter(c -> |
|
c.methods().stream() |
|
.anyMatch(it -> |
|
!it.flags().has(ABSTRACT) && |
|
it.flags().has(STATIC) && |
|
CD_void.equals(it.methodTypeSymbol().returnType()) && |
|
(MethodTypeDesc.of(CD_void).equals(it.methodTypeSymbol()) || MethodTypeDesc.of(CD_void, CD_String.arrayType()).equals(it.methodTypeSymbol())) && |
|
("main".equalsIgnoreCase(it.methodName().stringValue()) || "_main".equalsIgnoreCase(it.methodName().stringValue())) |
|
) |
|
).map(ClassModel::thisClass) |
|
.collect(groupingByConcurrent(_ -> javaArchive, toCollection(ConcurrentHashMap::newKeySet))); |
|
} catch (IOException e) { throw new RuntimeException(e); } |
|
})).map(Map::entrySet) |
|
.flatMap(Collection::stream) |
|
.sorted(Entry.comparingByKey(comparing(Iterable::iterator, comparing(Iterator::next)))/* .thenComparing(Entry.comparingByValue()) */) |
|
.<Formattable>mapMulti((mains, acc) -> { |
|
mains.getValue().stream() |
|
.map(ClassEntry::asInternalName) |
|
.map(it -> it.replace('/', '.')) |
|
.map(main -> switch (mains.getKey().getFileName().toString().toLowerCase(ROOT)) { |
|
case String s when s.endsWith(".jmod") -> new JArchive.Jmod(isCustomJdkSupplied, javaHome, mains.getKey(), main); |
|
case String s when s.endsWith(".jar") -> new JArchive.Jar(isCustomJdkSupplied, javaHome, mains.getKey(), main); |
|
default -> throw new IllegalStateException("Could not generate launch command for: %s %s".formatted(mains.getKey().getFileName(), main)); // You can create an "UnknownRes" record for default if you wish |
|
}).filter(Objects::nonNull) |
|
.forEach(acc); |
|
}).forEach(it -> out.format("%" + (SHOW_COMMAND ? "#" : "") + "s%n", it)); |
|
} |
|
} |
|
} |