jdk-24/test/jdk/tools/jlink/runtimeImage/AbstractLinkableRuntimeTest.java
Severin Gehwolf 2ec358082f 8311302: Implement JEP 493: Linking Run-Time Images without JMODs
Co-authored-by: Mandy Chung <mchung@openjdk.org>
Reviewed-by: mchung, alanb, erikj, ihse
2024-11-11 13:35:25 +00:00

706 lines
26 KiB
Java

/*
* Copyright (c) 2024, Red Hat, Inc.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Scanner;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;
import jdk.tools.jlink.internal.LinkableRuntimeImage;
import tests.Helper;
import tests.JImageGenerator;
import tests.JImageGenerator.JLinkTask;
import tests.JImageValidator;
public abstract class AbstractLinkableRuntimeTest {
protected static final boolean DEBUG = true;
public void run() throws Exception {
boolean isLinkableRuntime = LinkableRuntimeImage.isLinkableRuntime();
Helper helper = Helper.newHelper(isLinkableRuntime);
if (helper == null) {
System.err.println(AbstractLinkableRuntimeTest.class.getSimpleName() +
": Test not run");
return;
}
runTest(helper, isLinkableRuntime);
System.out.println(getClass().getSimpleName() + " PASSED!");
}
/**
* Main test entry point that actual tests ought to override.
*
* @param helper The jlink helper
* @param isLinkableRuntime {@code true} iff the JDK build under test already
* includes the linkable runtime capability in jlink.
* @throws Exception
*/
abstract void runTest(Helper helper, boolean isLinkableRuntime) throws Exception;
/**
* Ensure 'java --list-modules' lists the correct set of modules in the given
* image.
*
* @param jlinkImage
* @param expectedModules
*/
protected void verifyListModules(Path image,
List<String> expectedModules) throws Exception {
OutputAnalyzer out = runJavaCmd(image, List.of("--list-modules"));
List<String> actual = parseListMods(out.getStdout());
Collections.sort(actual);
if (!expectedModules.equals(actual)) {
throw new AssertionError("Different modules! Expected " + expectedModules + " got: " + actual);
}
}
protected OutputAnalyzer runJavaCmd(Path image, List<String> options) throws Exception {
Path targetJava = image.resolve("bin").resolve(getJava());
List<String> cmd = new ArrayList<>();
cmd.add(targetJava.toString());
for (String opt: options) {
cmd.add(opt);
}
List<String> javaCmd = Collections.unmodifiableList(cmd);
OutputAnalyzer out;
try {
out = ProcessTools.executeCommand(javaCmd.toArray(new String[0]));
} catch (Throwable e) {
throw new Exception("Process failed to execute", e);
}
if (out.getExitValue() != 0) {
if (DEBUG) {
System.err.println("Process stdout was: ");
System.err.println(out.getStdout());
System.err.println("Process stderr was: ");
System.err.println(out.getStderr());
}
throw new AssertionError("'" + javaCmd.stream().collect(Collectors.joining(" ")) + "'"
+ " expected to succeed!");
}
return out;
}
protected Path createJavaImageRuntimeLink(BaseJlinkSpec baseSpec) throws Exception {
return createJavaImageRuntimeLink(baseSpec, Collections.emptySet() /* exclude all jmods */);
}
protected Path createJavaImageRuntimeLink(BaseJlinkSpec baseSpec, Set<String> excludedJmods) throws Exception {
// Be sure we have a JDK without JMODs
Path runtimeJlinkImage = createRuntimeLinkImage(baseSpec, excludedJmods);
// On Windows jvm.dll is in 'bin' after the jlink
Path libjvm = Path.of((isWindows() ? "bin" : "lib"), "server", System.mapLibraryName("jvm"));
JlinkSpecBuilder builder = new JlinkSpecBuilder();
// And expect libjvm (not part of the jimage) to be present in the resulting image
builder.expectedFile(libjvm.toString())
.helper(baseSpec.getHelper())
.name(baseSpec.getName())
.validatingModule(baseSpec.getValidatingModule())
.imagePath(runtimeJlinkImage)
.expectedLocation("/java.base/java/lang/String.class");
for (String m: baseSpec.getModules()) {
builder.addModule(m);
}
for (String extra: baseSpec.getExtraOptions()) {
builder.extraJlinkOpt(extra);
}
return jlinkUsingImage(builder.build());
}
protected Path jlinkUsingImage(JlinkSpec spec) throws Exception {
return jlinkUsingImage(spec, new RuntimeLinkOutputAnalyzerHandler());
}
protected Path jlinkUsingImage(JlinkSpec spec, OutputAnalyzerHandler handler) throws Exception {
return jlinkUsingImage(spec, handler, new DefaultSuccessExitPredicate());
}
protected Path jlinkUsingImage(JlinkSpec spec, OutputAnalyzerHandler handler, Predicate<OutputAnalyzer> exitChecker) throws Exception {
String generatedImage = "target-run-time-" + spec.getName();
Path targetImageDir = spec.getHelper().createNewImageDir(generatedImage);
Path targetJlink = spec.getImageToUse().resolve("bin").resolve(getJlink());
String[] jlinkCmdArray = new String[] {
targetJlink.toString(),
"--output", targetImageDir.toString(),
"--verbose",
"--add-modules", spec.getModules().stream().collect(Collectors.joining(","))
};
List<String> jlinkCmd = new ArrayList<>();
jlinkCmd.addAll(Arrays.asList(jlinkCmdArray));
if (spec.getExtraJlinkOpts() != null && !spec.getExtraJlinkOpts().isEmpty()) {
jlinkCmd.addAll(spec.getExtraJlinkOpts());
}
if (spec.getModulePath() != null) {
for (String mp: spec.getModulePath()) {
jlinkCmd.add("--module-path");
jlinkCmd.add(mp);
}
}
jlinkCmd = Collections.unmodifiableList(jlinkCmd); // freeze
System.out.println("DEBUG: run-time image based jlink command: " +
jlinkCmd.stream().collect(Collectors.joining(" ")));
OutputAnalyzer analyzer = null;
try {
analyzer = ProcessTools.executeProcess(jlinkCmd.toArray(new String[0]));
} catch (Throwable t) {
throw new AssertionError("Executing process failed!", t);
}
if (!exitChecker.test(analyzer)) {
if (DEBUG) {
System.err.println("Process stdout was: ");
System.err.println(analyzer.getStdout());
System.err.println("Process stderr was: ");
System.err.println(analyzer.getStderr());
}
// if the exit checker failed, we expected the other outcome
// i.e. fail for success and success for fail.
boolean successExit = analyzer.getExitValue() == 0;
String msg = String.format("Expected jlink to %s given a jmodless image. Exit code was: %d",
(successExit ? "fail" : "pass"), analyzer.getExitValue());
throw new AssertionError(msg);
}
handler.handleAnalyzer(analyzer); // Give tests a chance to process in/output
// validate the resulting image; Includes running 'java -version', only do this
// if the jlink succeeded.
if (analyzer.getExitValue() == 0) {
JImageValidator validator = new JImageValidator(spec.getValidatingModule(), spec.getExpectedLocations(),
targetImageDir.toFile(), spec.getUnexpectedLocations(), Collections.emptyList(), spec.getExpectedFiles());
validator.validate(); // This doesn't validate locations
if (!spec.getExpectedLocations().isEmpty() || !spec.getUnexpectedLocations().isEmpty()) {
JImageValidator.validate(targetImageDir.resolve("lib").resolve("modules"), spec.getExpectedLocations(), spec.getUnexpectedLocations());
}
}
return targetImageDir;
}
/**
* Prepares the test for execution. This assumes the current runtime
* supports linking from it. However, since the 'jmods' dir might be present
* (default jmods module path), the 'jmods' directory needs to get removed
* to provoke actual linking from the run-time image.
*
* @param baseSpec
* @return A path to a JDK that is capable for linking from the run-time
* image.
* @throws Exception
*/
protected Path createRuntimeLinkImage(BaseJlinkSpec baseSpec) throws Exception {
return createRuntimeLinkImage(baseSpec, Collections.emptySet() /* exclude all jmods */);
}
/**
* Prepares the test for execution. Creates a JDK with a jlink that has the
* capability to link from the run-time image (if needed). It further
* ensures that if packaged modules ('jmods' dir) are present, to remove
* them entirely or as specified in the {@link excludedJmodFiles} set. If
* that set is empty, all packaged modules will be removed. Note that with
* packaged modules present no run-time image based linking would be done.
*
* @param baseSpec
* The specification for the custom - run-time image link capable
* - JDK to create via jlink (if any)
* @param excludedJmods
* The set of jmod files to exclude in the base JDK. Empty set if
* all JMODs should be removed.
* @return A path to a JDK, including jdk.jlink, that has the run-time image
* link capability.
*
* @throws Exception
*/
protected Path createRuntimeLinkImage(BaseJlinkSpec baseSpec,
Set<String> excludedJmodFiles) throws Exception {
// Depending on the shape of the JDK under test, we either only filter
// jmod files or create a run-time image link capable JDK on-the-fly.
Path from = null;
Path runtimeJlinkImage = null;
String finalName = baseSpec.getName() + "-jlink";
if (baseSpec.isLinkableRuntime()) {
// The build is already run-time image link capable
String javaHome = System.getProperty("java.home");
from = Path.of(javaHome);
} else {
// Create a run-time image capable JDK using --generate-linkable-runtime
Path tempRuntimeImage = Path.of(finalName + "-tmp");
JLinkTask task = JImageGenerator.getJLinkTask();
task.output(tempRuntimeImage)
.addMods("jdk.jlink") // jdk.jlink module is always needed for the test
.option("--generate-linkable-runtime");
if (baseJDKhasPackagedModules()) {
Path jmodsPath = tempRuntimeImage.resolve("jmods");
task.option("--keep-packaged-modules=" + jmodsPath);
}
for (String module: baseSpec.getModules()) {
task.addMods(module);
}
task.call().assertSuccess();
from = tempRuntimeImage;
}
// Create the target directory
runtimeJlinkImage = baseSpec.getHelper().createNewImageDir(finalName);
// Remove JMODs as needed for the test
copyJDKTreeWithoutSpecificJmods(from, runtimeJlinkImage, excludedJmodFiles);
// Verify the base image is actually without desired packaged modules
if (excludedJmodFiles.isEmpty()) {
if (Files.exists(runtimeJlinkImage.resolve("jmods"))) {
throw new AssertionError("Must not contain 'jmods' directory");
}
} else {
Path basePath = runtimeJlinkImage.resolve("jmods");
for (String jmodFile: excludedJmodFiles) {
Path unexpectedFile = basePath.resolve(Path.of(jmodFile));
if (Files.exists(unexpectedFile)) {
throw new AssertionError("Must not contain jmod: " + unexpectedFile);
}
}
}
return runtimeJlinkImage;
}
private boolean baseJDKhasPackagedModules() {
Path jmodsPath = Path.of(System.getProperty("java.home"), "jmods");
return jmodsPath.toFile().exists();
}
private void copyJDKTreeWithoutSpecificJmods(Path from,
Path to,
Set<String> excludedJmods) throws Exception {
if (Files.exists(to)) {
throw new AssertionError("Expected target dir '" + to + "' to exist");
}
FileVisitor<Path> fileVisitor = null;
if (excludedJmods.isEmpty()) {
fileVisitor = new ExcludeAllJmodsFileVisitor(from, to);
} else {
fileVisitor = new FileExcludingFileVisitor(excludedJmods,
from,
to);
}
Files.walkFileTree(from, fileVisitor);
}
private List<String> parseListMods(String output) throws Exception {
List<String> outputLines = new ArrayList<>();
try (Scanner lineScan = new Scanner(output)) {
while (lineScan.hasNextLine()) {
outputLines.add(lineScan.nextLine());
}
}
return outputLines.stream()
.map(a -> { return a.split("@", 2)[0];})
.filter(a -> !a.isBlank())
.collect(Collectors.toList());
}
private String getJlink() {
return getBinary("jlink");
}
private String getJava() {
return getBinary("java");
}
private String getBinary(String binary) {
return isWindows() ? binary + ".exe" : binary;
}
protected static boolean isWindows() {
return System.getProperty("os.name").startsWith("Windows");
}
static class ExcludeAllJmodsFileVisitor extends SimpleFileVisitor<Path> {
private final Path root;
private final Path destination;
private ExcludeAllJmodsFileVisitor(Path root,
Path destination) {
this.destination = destination;
this.root = root;
}
@Override
public FileVisitResult preVisitDirectory(Path dir,
BasicFileAttributes attrs) throws IOException {
Objects.requireNonNull(dir);
Path relative = root.relativize(dir);
if (relative.getFileName().equals(Path.of("jmods"))) {
return FileVisitResult.SKIP_SUBTREE;
}
// Create dir in destination location
Path targetDir = destination.resolve(relative);
if (!Files.exists(targetDir)) {
Files.createDirectory(targetDir);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Path relative = root.relativize(file);
Files.copy(file, destination.resolve(relative), StandardCopyOption.REPLACE_EXISTING);
return FileVisitResult.CONTINUE;
}
}
static class FileExcludingFileVisitor extends SimpleFileVisitor<Path> {
private final Set<String> filesToExclude;
private final Path root;
private final Path destination;
private FileExcludingFileVisitor(Set<String> filesToExclude,
Path root,
Path destination) {
this.filesToExclude = filesToExclude;
this.destination = destination;
this.root = root;
}
@Override
public FileVisitResult preVisitDirectory(Path dir,
BasicFileAttributes attrs) throws IOException {
Objects.requireNonNull(dir);
Path relative = root.relativize(dir);
// Create dir in destination location
Path targetDir = destination.resolve(relative);
if (!Files.exists(targetDir)) {
Files.createDirectory(targetDir);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
Path relative = root.relativize(file);
// Skip files as determined by the exclude set
String fileName = file.getFileName().toString();
if (!filesToExclude.contains(fileName)) {
Files.copy(file, destination.resolve(relative), StandardCopyOption.REPLACE_EXISTING);
}
return FileVisitResult.CONTINUE;
}
}
static class BaseJlinkSpec {
final Helper helper;
final String name;
final String validatingModule;
final List<String> modules;
final List<String> extraOptions;
final boolean isLinkableRuntime;
BaseJlinkSpec(Helper helper, String name, String validatingModule,
List<String> modules, List<String> extraOptions, boolean isLinkableRuntime) {
this.helper = helper;
this.name = name;
this.modules = modules;
this.extraOptions = extraOptions;
this.validatingModule = validatingModule;
this.isLinkableRuntime = isLinkableRuntime;
}
public String getValidatingModule() {
return validatingModule;
}
public Helper getHelper() {
return helper;
}
public String getName() {
return name;
}
public List<String> getModules() {
return modules;
}
public List<String> getExtraOptions() {
return extraOptions;
}
public boolean isLinkableRuntime() {
return isLinkableRuntime;
}
}
static class BaseJlinkSpecBuilder {
Helper helper;
String name;
String validatingModule;
List<String> modules = new ArrayList<>();
List<String> extraOptions = new ArrayList<>();
boolean isLinkableRuntime;
BaseJlinkSpecBuilder addModule(String module) {
modules.add(module);
return this;
}
BaseJlinkSpecBuilder addExtraOption(String option) {
extraOptions.add(option);
return this;
}
BaseJlinkSpecBuilder setLinkableRuntime() {
isLinkableRuntime = true;
return this;
}
BaseJlinkSpecBuilder helper(Helper helper) {
this.helper = helper;
return this;
}
BaseJlinkSpecBuilder name(String name) {
this.name = name;
return this;
}
BaseJlinkSpecBuilder validatingModule(String module) {
this.validatingModule = module;
return this;
}
BaseJlinkSpec build() {
if (name == null) {
throw new IllegalStateException("Name must be set");
}
if (helper == null) {
throw new IllegalStateException("helper must be set");
}
if (modules.isEmpty()) {
throw new IllegalStateException("modules must be set");
}
if (validatingModule == null) {
throw new IllegalStateException("the module which should get validated must be set");
}
return new BaseJlinkSpec(helper, name, validatingModule, modules, extraOptions, isLinkableRuntime);
}
}
static class JlinkSpec {
final Path imageToUse;
final Helper helper;
final String name;
final List<String> modules;
final String validatingModule;
final List<String> expectedLocations;
final List<String> unexpectedLocations;
final String[] expectedFiles;
final List<String> extraJlinkOpts;
final List<String> modulePath;
JlinkSpec(Path imageToUse, Helper helper, String name, List<String> modules,
String validatingModule, List<String> expectedLocations,
List<String> unexpectedLocations, String[] expectedFiles,
List<String> extraJlinkOpts,
List<String> modulePath) {
this.imageToUse = imageToUse;
this.helper = helper;
this.name = name;
this.modules = modules;
this.validatingModule = validatingModule;
this.expectedLocations = expectedLocations;
this.unexpectedLocations = unexpectedLocations;
this.expectedFiles = expectedFiles;
this.extraJlinkOpts = extraJlinkOpts;
this.modulePath = modulePath;
}
public Path getImageToUse() {
return imageToUse;
}
public Helper getHelper() {
return helper;
}
public String getName() {
return name;
}
public List<String> getModules() {
return modules;
}
public String getValidatingModule() {
return validatingModule;
}
public List<String> getExpectedLocations() {
return expectedLocations;
}
public List<String> getUnexpectedLocations() {
return unexpectedLocations;
}
public String[] getExpectedFiles() {
return expectedFiles;
}
public List<String> getExtraJlinkOpts() {
return extraJlinkOpts;
}
public List<String> getModulePath() {
return modulePath;
}
}
static class JlinkSpecBuilder {
Path imageToUse;
Helper helper;
String name;
List<String> modules = new ArrayList<>();
String validatingModule;
List<String> expectedLocations = new ArrayList<>();
List<String> unexpectedLocations = new ArrayList<>();
List<String> expectedFiles = new ArrayList<>();
List<String> extraJlinkOpts = new ArrayList<>();
List<String> modulePath = new ArrayList<>();
JlinkSpec build() {
if (imageToUse == null) {
throw new IllegalStateException("No image to use for jlink specified!");
}
if (helper == null) {
throw new IllegalStateException("No helper specified!");
}
if (name == null) {
throw new IllegalStateException("No name for the image location specified!");
}
if (validatingModule == null) {
throw new IllegalStateException("No module specified for after generation validation!");
}
return new JlinkSpec(imageToUse,
helper,
name,
modules,
validatingModule,
expectedLocations,
unexpectedLocations,
expectedFiles.toArray(new String[0]),
extraJlinkOpts,
modulePath);
}
JlinkSpecBuilder imagePath(Path image) {
this.imageToUse = image;
return this;
}
JlinkSpecBuilder helper(Helper helper) {
this.helper = helper;
return this;
}
JlinkSpecBuilder name(String name) {
this.name = name;
return this;
}
JlinkSpecBuilder addModule(String module) {
modules.add(module);
return this;
}
JlinkSpecBuilder validatingModule(String module) {
this.validatingModule = module;
return this;
}
JlinkSpecBuilder addModulePath(String modulePath) {
this.modulePath.add(modulePath);
return this;
}
JlinkSpecBuilder expectedLocation(String location) {
expectedLocations.add(location);
return this;
}
JlinkSpecBuilder unexpectedLocation(String location) {
unexpectedLocations.add(location);
return this;
}
JlinkSpecBuilder expectedFile(String file) {
expectedFiles.add(file);
return this;
}
JlinkSpecBuilder extraJlinkOpt(String opt) {
extraJlinkOpts.add(opt);
return this;
}
}
static abstract class OutputAnalyzerHandler {
public abstract void handleAnalyzer(OutputAnalyzer out);
}
static class RuntimeLinkOutputAnalyzerHandler extends OutputAnalyzerHandler {
@Override
public void handleAnalyzer(OutputAnalyzer out) {
out.shouldContain("Linking based on the current run-time image");
}
}
static class DefaultSuccessExitPredicate implements Predicate<OutputAnalyzer> {
@Override
public boolean test(OutputAnalyzer t) {
return t.getExitValue() == 0;
}
}
}