From e5a1154ba554bc9744b3fca4ec38bb47f5e0e6c9 Mon Sep 17 00:00:00 2001 From: Leonid Shalupov Date: Fri, 3 Dec 2021 19:54:41 +0100 Subject: [PATCH] jps-bootstrap: download classes from compile inc configuration GitOrigin-RevId: 56393e4b6dc38f085b49f0114ea7d445da100183 --- platform/jps-bootstrap/pom.xml | 20 ++ .../BuildDependenciesDownloader.java | 220 +++++++++++++++ .../jpsBootstrap/BuildDependenciesUtil.java | 77 ++++++ .../jpsBootstrap/ClassesFromCompileInc.java | 137 +++++++++ .../jpsBootstrap/ClassesFromJpsBuild.java | 107 ++++++++ .../jpsBootstrap/JpsBootstrapMain.java | 259 +++++------------- .../jpsBootstrap/JpsBootstrapUtil.java | 90 +++++- .../jpsBootstrap/JpsProjectUtils.java | 103 +++++++ 8 files changed, 820 insertions(+), 193 deletions(-) create mode 100644 platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/BuildDependenciesDownloader.java create mode 100644 platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/BuildDependenciesUtil.java create mode 100644 platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/ClassesFromCompileInc.java create mode 100644 platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/ClassesFromJpsBuild.java create mode 100644 platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/JpsProjectUtils.java diff --git a/platform/jps-bootstrap/pom.xml b/platform/jps-bootstrap/pom.xml index cd0b1ca82332..f7e4623ad6d1 100644 --- a/platform/jps-bootstrap/pom.xml +++ b/platform/jps-bootstrap/pom.xml @@ -15,11 +15,31 @@ + + com.google.guava + guava + 31.0.1-jre + commons-cli commons-cli 1.5.0 + + commons-codec + commons-codec + 1.15 + + + org.apache.commons + commons-compress + 1.21 + + + com.google.code.gson + gson + 2.8.8 + com.jetbrains.intellij.platform jps-model diff --git a/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/BuildDependenciesDownloader.java b/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/BuildDependenciesDownloader.java new file mode 100644 index 000000000000..6aced626016e --- /dev/null +++ b/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/BuildDependenciesDownloader.java @@ -0,0 +1,220 @@ +// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.jpsBootstrap; + +import org.apache.commons.codec.digest.DigestUtils; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.Arrays; +import java.util.stream.Stream; + +// This currently a copy of BuildDependenciesDownloader.groovy. In the feature both places will share one java implementation +final class BuildDependenciesDownloader { + // Add something to file name computation to make a different name than BuildDependenciesDownloader.groovy + private static final String JPS_BOOTSTRAP_SALT = "jps-bootstrap"; + + private static final String HTTP_HEADER_CONTENT_LENGTH = "Content-Length"; + private static final HttpClient httpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build(); + + static void debug(String message) { + JpsBootstrapUtil.verbose(message); + } + + static void info(String message) { + JpsBootstrapUtil.info(message); + } + + static void checkCommunityRoot(Path communityRoot) { + if (communityRoot == null) { + throw new IllegalStateException("passed community root is null"); + } + + Path probeFile = communityRoot.resolve("intellij.idea.community.main.iml"); + if (!Files.exists(probeFile)) { + throw new IllegalStateException("community root was not found at " + communityRoot); + } + } + + private static Path getProjectLocalDownloadCache(Path communityRoot) { + Path projectLocalDownloadCache = communityRoot.resolve("build").resolve("download"); + + try { + Files.createDirectories(projectLocalDownloadCache); + } + catch (IOException e) { + throw new RuntimeException(e); + } + + return projectLocalDownloadCache; + } + + private static Path getDownloadCachePath(Path communityRoot) throws IOException { + checkCommunityRoot(communityRoot); + + Path path; + if (JpsBootstrapUtil.underTeamCity) { + String persistentCachePath = JpsBootstrapUtil.getTeamCitySystemProperties().getProperty("agent.persistent.cache"); + if (persistentCachePath == null || persistentCachePath.isBlank()) { + throw new IllegalStateException("'agent.persistent.cache' system property is required under TeamCity"); + } + path = Paths.get(persistentCachePath); + } + else { + path = getProjectLocalDownloadCache(communityRoot); + } + + Files.createDirectories(path); + return path; + } + + static synchronized Path downloadFileToCacheLocation(Path communityRoot, URI uri) throws IOException, InterruptedException { + String uriString = uri.toString(); + String lastNameFromUri = uriString.substring(uriString.lastIndexOf('/') + 1); + String fileName = DigestUtils.sha256Hex(JPS_BOOTSTRAP_SALT + uriString).substring(0, 10) + "-" + lastNameFromUri; + Path targetFile = getDownloadCachePath(communityRoot).resolve(fileName); + + downloadFile(uri, targetFile); + return targetFile; + } + + static synchronized Path extractFileToCacheLocation(Path communityRoot, Path archiveFile) { + try { + String directoryName = DigestUtils.sha256Hex(JPS_BOOTSTRAP_SALT + archiveFile).substring(0, 6) + "-" + archiveFile.getFileName().toString(); + Path cacheDirectory = getDownloadCachePath(communityRoot).resolve(directoryName + ".d"); + + // Maintain one top-level directory (cacheDirectory) under persistent cache directory, since + // TeamCity removes whole top-level directories upon cleanup, so both flag and extract directory + // will be deleted at the same time + Path flagFile = cacheDirectory.resolve(".flag.jps-bootstrap"); + Path extractDirectory = cacheDirectory.resolve(archiveFile.getFileName().toString() + ".d"); + extractFileWithFlagFileLocation(archiveFile, extractDirectory, flagFile); + + // Update file modification time to maintain FIFO caches i.e. + // in persistent cache folder on TeamCity agent + Files.setLastModifiedTime(cacheDirectory, FileTime.from(Instant.now())); + + return extractDirectory; + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static byte[] getExpectedFlagFileContent(Path archiveFile, Path targetDirectory) throws IOException { + // Increment this number to force all clients to extract content again + // e.g. when some issues in extraction code were fixed + int codeVersion = 2; + + long numberOfTopLevelEntries; + try (Stream pathStream = Files.list(targetDirectory)) { + numberOfTopLevelEntries = pathStream.count(); + } + + return (codeVersion + "\n" + archiveFile.toString() + "\n" + + "topLevelDirectoryEntries:" + numberOfTopLevelEntries).getBytes(StandardCharsets.UTF_8); + } + + private static boolean checkFlagFile(Path archiveFile, Path flagFile, Path targetDirectory) throws IOException { + if (!Files.isRegularFile(flagFile) || !Files.isDirectory(targetDirectory)) { + return false; + } + + byte[] existingContent = Files.readAllBytes(flagFile); + return Arrays.equals(existingContent, getExpectedFlagFileContent(archiveFile, targetDirectory)); + } + + // assumes file at `archiveFile` is immutable + private static void extractFileWithFlagFileLocation(Path archiveFile, Path targetDirectory, Path flagFile) throws IOException { + if (checkFlagFile(archiveFile, flagFile, targetDirectory)) { + debug("Skipping extract to " + targetDirectory + " since flag file " + flagFile + " is correct"); + + // Update file modification time to maintain FIFO caches i.e. + // in persistent cache folder on TeamCity agent + Files.setLastModifiedTime(targetDirectory, FileTime.from(Instant.now())); + + return; + } + + if (Files.exists(targetDirectory)) { + if (!Files.isDirectory(targetDirectory)) { + throw new IllegalStateException("Target '" + targetDirectory + "' exists, but it's not a directory. Please delete it manually"); + } + + BuildDependenciesUtil.cleanDirectory(targetDirectory); + } + + info(" * Extracting " + archiveFile + " to " + targetDirectory); + + Files.createDirectories(targetDirectory); + BuildDependenciesUtil.extractZip(archiveFile, targetDirectory); + + Files.write(flagFile, getExpectedFlagFileContent(archiveFile, targetDirectory)); + if (!checkFlagFile(archiveFile, flagFile, targetDirectory)) { + throw new IllegalStateException("checkFlagFile must be true right after extracting the archive. " + + "flagFile:" + flagFile + + "archiveFile:" + archiveFile + + "target:" + targetDirectory); + } + } + + private static void downloadFile(URI uri, Path target) throws IOException, InterruptedException { + Instant now = Instant.now(); + if (Files.exists(target)) { + // Update file modification time to maintain FIFO caches i.e. + // in persistent cache folder on TeamCity agent + Files.setLastModifiedTime(target, FileTime.from(now)); + return; + } + + // save to the same disk to ensure that move will be atomic and not as a copy + String tempFileName = target.getFileName() + "-" + + Long.toString(now.getEpochSecond() - 1634886185, 36) + "-" + + Integer.toString(now.getNano(), 36); + + if (tempFileName.length() > 255) { + tempFileName = tempFileName.substring(tempFileName.length() - 255); + } + Path tempFile = target.getParent().resolve(tempFileName); + + try { + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(uri) + .setHeader("User-Agent", "Build Script Downloader") + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(tempFile)); + if (response.statusCode() != 200) { + throw new IllegalStateException("Error downloading " + uri + ": non-200 http status code " + response.statusCode()); + } + + long contentLength = response.headers().firstValueAsLong(HTTP_HEADER_CONTENT_LENGTH).orElse(-1); + if (contentLength <= 0) { + throw new IllegalStateException("Header '" + HTTP_HEADER_CONTENT_LENGTH + "' is missing or zero for " + uri); + } + + long fileSize = Files.size(tempFile); + if (fileSize != contentLength) { + throw new IllegalStateException("Wrong file length after downloading uri '" + uri + + "' to '" + tempFile + + "': expected length " + contentLength + + "from Content-Length header, but got " + fileSize + " on disk"); + } + + Files.move(tempFile, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } + finally { + Files.deleteIfExists(tempFile); + } + } +} diff --git a/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/BuildDependenciesUtil.java b/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/BuildDependenciesUtil.java new file mode 100644 index 000000000000..346333156f9d --- /dev/null +++ b/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/BuildDependenciesUtil.java @@ -0,0 +1,77 @@ +// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.jpsBootstrap; + +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.stream.Stream; + +@SuppressWarnings("UnstableApiUsage") +final class BuildDependenciesUtil { + static boolean isPosix = FileSystems.getDefault().supportedFileAttributeViews().contains("posix"); + + static void extractZip(Path archiveFile, Path target) throws IOException { + try (ZipFile zipFile = new ZipFile(archiveFile.toFile(), "UTF-8")) { + for (final Enumeration en = zipFile.getEntries(); en.hasMoreElements(); ) { + ZipArchiveEntry entry = en.nextElement(); + + Path entryPath = entryFile(target, entry.getName()); + if (entry.isDirectory()) { + Files.createDirectories(entryPath); + } + else { + try (InputStream is = zipFile.getInputStream(entry)) { + Files.copy(is, entryPath); + } + + //noinspection OctalInteger + if (isPosix && (entry.getUnixMode() & 0111) != 0) { + //noinspection SpellCheckingInspection + Files.setPosixFilePermissions(entryPath, PosixFilePermissions.fromString("rwxr-xr-x")); + } + } + } + } + } + + static Path entryFile(Path outputDir, String entryName) throws IOException { + ensureValidPath(entryName); + return outputDir.resolve(trimStart(entryName, '/')); + } + + private static void ensureValidPath(String entryName) throws IOException { + if (entryName.contains("..") && Arrays.asList(entryName.split("[/\\\\]")).contains("..")) { + throw new IOException("Invalid entry name: " + entryName); + } + } + + private static String trimStart(String s, char charToTrim) { + int index = 0; + while (s.charAt(index) == charToTrim) index++; + return s.substring(index); + } + + static void cleanDirectory(Path directory) throws IOException { + Files.createDirectories(directory); + try (Stream stream = Files.list(directory)) { + stream.forEach(path -> { + try { + MoreFiles.deleteRecursively(path, RecursiveDeleteOption.ALLOW_INSECURE); + } + catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + } +} diff --git a/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/ClassesFromCompileInc.java b/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/ClassesFromCompileInc.java new file mode 100644 index 000000000000..97650dced62c --- /dev/null +++ b/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/ClassesFromCompileInc.java @@ -0,0 +1,137 @@ +// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.jpsBootstrap; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import com.intellij.openapi.util.Pair; +import groovy.transform.CompileStatic; +import org.jetbrains.jps.model.JpsProject; +import org.jetbrains.jps.model.java.*; +import org.jetbrains.jps.model.module.JpsModule; +import org.jetbrains.jps.model.module.JpsModuleSourceRoot; +import org.jetbrains.jps.util.JpsPathUtil; + +import java.io.BufferedReader; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.*; +import java.util.stream.Collectors; + +import static org.jetbrains.jpsBootstrap.JpsBootstrapUtil.verbose; + +public class ClassesFromCompileInc { + public final static String MANIFEST_JSON_URL_ENV_NAME = "JPS_BOOTSTRAP_MANIFEST_JSON_URL"; + + public static void downloadProjectClasses(JpsProject project, Path communityRoot) throws IOException, InterruptedException { + String manifestUrl = System.getenv(MANIFEST_JSON_URL_ENV_NAME); + if (manifestUrl == null || manifestUrl.isBlank()) { + throw new IllegalStateException("Env variable '" + MANIFEST_JSON_URL_ENV_NAME + "' is missing or empty"); + } + verbose("Got manifest json url '" + manifestUrl + "' from $" + MANIFEST_JSON_URL_ENV_NAME); + + final Path manifest = BuildDependenciesDownloader.downloadFileToCacheLocation(communityRoot, URI.create(manifestUrl)); + Map parts = downloadPartsFromMetadataJson(manifest, communityRoot); + assignModuleOutputs(project, parts); + } + + private static void assignModuleOutputs(JpsProject project, Map parts) { + Map partsCopy = new HashMap<>(parts); + + for (JpsModule module : project.getModules()) { + boolean hasProduction = false; + boolean hasTests = false; + + for (JpsModuleSourceRoot sourceRoot : module.getSourceRoots()) { + if (sourceRoot.getRootType() == JavaSourceRootType.SOURCE || sourceRoot.getRootType() == JavaResourceRootType.RESOURCE) { + hasProduction = true; + } else if (sourceRoot.getRootType() == JavaSourceRootType.TEST_SOURCE || sourceRoot.getRootType() == JavaResourceRootType.TEST_RESOURCE) { + hasTests = true; + } else { + throw new IllegalStateException("Unsupported source root type " + sourceRoot + " for module " + module.getName()); + } + } + + final JpsJavaModuleExtension javaExtension = JpsJavaExtensionService.getInstance().getOrCreateModuleExtension(module); + if (hasTests) { + final String key = "test/" + module.getName(); + Path testOutputPath = partsCopy.remove(key); + if (testOutputPath == null) { + throw new IllegalStateException("Output for " + key + " was not found"); + } + + javaExtension.setTestOutputUrl(JpsPathUtil.pathToUrl(testOutputPath.toString())); + } + + if (hasProduction) { + final String key = "production/" + module.getName(); + Path productionOutputPath = partsCopy.remove(key); + if (productionOutputPath == null) { + throw new IllegalStateException("Output for " + key + " was not found"); + } + + javaExtension.setOutputUrl(JpsPathUtil.pathToUrl(productionOutputPath.toString())); + } + } + + if (!partsCopy.isEmpty()) { + throw new IllegalStateException("After processing all project modules some entries left: " + + String.join(" ", partsCopy.keySet())); + } + } + + private static Map downloadPartsFromMetadataJson(Path metadataJson, Path communityRoot) throws InterruptedException, IOException { + CompilationPartsMetadata partsMetadata; + try (BufferedReader manifestReader = Files.newBufferedReader(metadataJson, StandardCharsets.UTF_8)) { + partsMetadata = new Gson().fromJson(manifestReader, CompilationPartsMetadata.class); + } + + if (partsMetadata.files.isEmpty()) { + throw new IllegalStateException("partsMetadata.files is empty, check " + metadataJson); + } + + List>> tasks = new ArrayList<>(); + for (final Map.Entry entry : partsMetadata.files.entrySet()) { + Callable> c = () -> { + String modulePrefix = entry.getKey(); + String hash = entry.getValue(); + + URI outputPartUri = URI.create(partsMetadata.serverUrl + "/" + partsMetadata.prefix + "/" + modulePrefix + "/" + hash + ".jar"); + final Path outputPart = BuildDependenciesDownloader.downloadFileToCacheLocation(communityRoot, outputPartUri); + final Path outputPartExtracted = BuildDependenciesDownloader.extractFileToCacheLocation(communityRoot, outputPart); + + System.out.println(modulePrefix + " = " + outputPartExtracted); + + return Pair.pair(modulePrefix, outputPartExtracted); + }; + tasks.add(c); + } + + return JpsBootstrapUtil.executeTasksInParallel(tasks) + .stream().collect(Collectors.toUnmodifiableMap(pair -> pair.getFirst(), pair -> pair.getSecond())); + } + + public static void main(String[] args) throws IOException, InterruptedException { + downloadPartsFromMetadataJson(Paths.get("D:\\temp\\metadata.json"), Paths.get("D:\\Work\\intellij\\community")); + } + + @CompileStatic + private static final class CompilationPartsMetadata { + @SerializedName("server-url") + public String serverUrl; + public String prefix; + + /** + * Map compilation part path to a hash, for now SHA-256 is used. + * sha256(file) == hash, though that may be changed in the future. + */ + public Map files; + } +} diff --git a/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/ClassesFromJpsBuild.java b/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/ClassesFromJpsBuild.java new file mode 100644 index 000000000000..76729d6cc872 --- /dev/null +++ b/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/ClassesFromJpsBuild.java @@ -0,0 +1,107 @@ +// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.jpsBootstrap; + +import com.intellij.openapi.application.PathManager; +import com.intellij.openapi.util.io.FileUtilRt; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.groovy.compiler.rt.GroovyRtConstants; +import org.jetbrains.jps.api.GlobalOptions; +import org.jetbrains.jps.build.Standalone; +import org.jetbrains.jps.incremental.groovy.JpsGroovycRunner; +import org.jetbrains.jps.incremental.messages.BuildMessage; +import org.jetbrains.jps.model.JpsModel; +import org.jetbrains.jps.model.JpsNamedElement; +import org.jetbrains.jps.model.java.JpsJavaExtensionService; +import org.jetbrains.jps.model.module.JpsModule; + +import java.nio.file.Path; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import static org.jetbrains.jpsBootstrap.JpsBootstrapUtil.*; +import static org.jetbrains.jpsBootstrap.JpsBootstrapUtil.fatal; + +public class ClassesFromJpsBuild { + public static final String CLASSES_FROM_JPS_BUILD_ENV_NAME = "JPS_BOOTSTRAP_CLASSES_FROM_JPS_BUILD"; + + public static void buildModule(JpsModule module, Path projectHome, JpsModel model, Path jpsBootstrapWorkDir) throws Exception { + // Workaround for KTIJ-19065 + System.setProperty(PathManager.PROPERTY_HOME_PATH, projectHome.toString()); + + System.setProperty("kotlin.incremental.compilation", "true"); + System.setProperty("kotlin.daemon.enabled", "false"); + System.setProperty(GlobalOptions.COMPILE_PARALLEL_OPTION, "true"); + + System.setProperty(JpsGroovycRunner.GROOVYC_IN_PROCESS, "true"); + System.setProperty(GroovyRtConstants.GROOVYC_ASM_RESOLVING_ONLY, "false"); + System.setProperty(GlobalOptions.USE_DEFAULT_FILE_LOGGING_OPTION, "true"); + System.setProperty(GlobalOptions.LOG_DIR_OPTION, jpsBootstrapWorkDir.resolve("log").toString()); + + String url = "file://" + FileUtilRt.toSystemIndependentName(jpsBootstrapWorkDir.resolve("out").toString()); + JpsJavaExtensionService.getInstance().getOrCreateProjectExtension(model.getProject()).setOutputUrl(url); + + info("Compilation log directory: " + System.getProperty(GlobalOptions.LOG_DIR_OPTION)); + + // kotlin.util.compiler-dependencies downloads all dependencies required for running Kotlin JPS compiler + // see org.jetbrains.kotlin.idea.artifacts.KotlinArtifactsFromSources + runBuild(model, jpsBootstrapWorkDir, "kotlin.util.compiler-dependencies"); + + runBuild(model, jpsBootstrapWorkDir, module.getName()); + } + + private static void runBuild(JpsModel model, Path workDir, String moduleName) throws Exception { + final long buildStart = System.currentTimeMillis(); + final AtomicReference firstError = new AtomicReference<>(); + + Path dataStorageRoot = workDir.resolve("jps-build-data"); + final Set moduleNames = model.getProject().getModules().stream().map(JpsNamedElement::getName).collect(Collectors.toUnmodifiableSet()); + Standalone.runBuild( + () -> model, + dataStorageRoot.toFile(), + false, + ContainerUtil.set(moduleName), + false, + Collections.emptyList(), + false, + msg -> { + BuildMessage.Kind kind = msg.getKind(); + String text = msg.toString(); + + switch (kind) { + case PROGRESS: + verbose(text); + break; + case WARNING: + warn(text); + case ERROR: + case INTERNAL_BUILDER_ERROR: + error(text); + break; + default: + if (!msg.getMessageText().isBlank()) { + if (moduleNames.contains(msg.getMessageText())) { + verbose(text); + } + else { + info(text); + } + } + break; + } + + if ((kind == BuildMessage.Kind.ERROR || kind == BuildMessage.Kind.INTERNAL_BUILDER_ERROR)) { + firstError.compareAndSet(null, text); + } + } + ); + + System.out.println("Finished building '" + moduleName + "' in " + (System.currentTimeMillis() - buildStart) + " ms"); + + String firstErrorText = firstError.get(); + if (firstErrorText != null) { + fatal("Build finished with errors. First error:\n" + firstErrorText); + } + } +} diff --git a/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/JpsBootstrapMain.java b/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/JpsBootstrapMain.java index e65674ce35e7..49fc6ee70947 100644 --- a/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/JpsBootstrapMain.java +++ b/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/JpsBootstrapMain.java @@ -2,42 +2,21 @@ package org.jetbrains.jpsBootstrap; -import com.intellij.openapi.application.PathManager; -import com.intellij.openapi.util.io.FileUtil; -import com.intellij.openapi.util.io.FileUtilRt; -import com.intellij.openapi.util.text.StringUtil; -import com.intellij.util.containers.ContainerUtil; -import com.intellij.util.io.URLUtil; import org.apache.commons.cli.*; import org.jetbrains.annotations.Contract; -import org.jetbrains.groovy.compiler.rt.GroovyRtConstants; -import org.jetbrains.jps.api.GlobalOptions; -import org.jetbrains.jps.build.Standalone; -import org.jetbrains.jps.incremental.groovy.JpsGroovycRunner; -import org.jetbrains.jps.incremental.messages.BuildMessage; -import org.jetbrains.jps.model.JpsElementFactory; import org.jetbrains.jps.model.JpsModel; -import org.jetbrains.jps.model.JpsNamedElement; -import org.jetbrains.jps.model.java.JpsJavaDependenciesEnumerator; -import org.jetbrains.jps.model.java.JpsJavaExtensionService; -import org.jetbrains.jps.model.library.JpsLibrary; -import org.jetbrains.jps.model.library.JpsOrderRootType; import org.jetbrains.jps.model.module.JpsModule; -import org.jetbrains.jps.model.serialization.JpsModelSerializationDataService; -import org.jetbrains.jps.model.serialization.JpsPathVariablesConfiguration; -import org.jetbrains.jps.model.serialization.JpsProjectLoader; -import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; -import java.util.concurrent.atomic.AtomicReference; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; import java.util.stream.Collectors; import static org.jetbrains.jpsBootstrap.JpsBootstrapUtil.*; @@ -62,7 +41,7 @@ public class JpsBootstrapMain { public static void main(String[] args) { try { - mainImpl(args); + new JpsBootstrapMain(args).main(); System.exit(0); } catch (Throwable t) { @@ -72,8 +51,14 @@ public class JpsBootstrapMain { } } - @SuppressWarnings("ConfusingArgumentToVarargsMethod") - private static void mainImpl(String[] args) throws Throwable { + private final Path projectHome; + private final Path communityHome; + private final String moduleNameToRun; + private final String classNameToRun; + private final Path jpsBootstrapWorkDir; + private final String[] mainArgsToRun; + + public JpsBootstrapMain(String[] args) throws IOException { CommandLine cmdline; try { cmdline = (new DefaultParser()).parse(createCliOptions(), args, true); @@ -84,28 +69,24 @@ public class JpsBootstrapMain { throw new IllegalStateException("NOT_REACHED"); } - final String[] freeArgs = cmdline.getArgs(); - if (cmdline.hasOption(ARG_HELP) || freeArgs.length < 2) { + final List freeArgs = Arrays.asList(cmdline.getArgs()); + if (cmdline.hasOption(ARG_HELP) || freeArgs.size() < 2) { showUsagesAndExit(); } + moduleNameToRun = freeArgs.get(0); + classNameToRun = freeArgs.get(1); + JpsBootstrapUtil.setVerboseEnabled(cmdline.hasOption(ARG_VERBOSE)); - long startTime = System.currentTimeMillis(); - - String moduleName = freeArgs[0]; - String className = freeArgs[1]; - String communityHomeString = System.getenv(COMMUNITY_HOME_ENV); if (communityHomeString == null) fatal("Please set " + COMMUNITY_HOME_ENV + " environment variable"); - Path communityHome = Path.of(communityHomeString); + communityHome = Path.of(communityHomeString); Path communityCheckFile = communityHome.resolve("intellij.idea.community.main.iml"); if (!Files.exists(communityCheckFile)) fatal(COMMUNITY_HOME_ENV + " is incorrect: " + communityCheckFile + " is missing"); - Path projectHome; - Path ultimateCheckFile = communityHome.getParent().resolve("intellij.idea.ultimate.main.iml"); if (Files.exists(ultimateCheckFile)) { projectHome = communityHome.getParent(); @@ -115,100 +96,77 @@ public class JpsBootstrapMain { projectHome = communityHome; } - // Workaround for KTIJ-19065 - System.setProperty(PathManager.PROPERTY_HOME_PATH, projectHome.toString()); - - Path workDir; - if (System.getenv(JPS_BOOTSTRAP_WORK_DIR_ENV) != null) { - workDir = Path.of(System.getenv(JPS_BOOTSTRAP_WORK_DIR_ENV)); + jpsBootstrapWorkDir = Path.of(System.getenv(JPS_BOOTSTRAP_WORK_DIR_ENV)); } else { - workDir = communityHome.resolve("out").resolve("jps-bootstrap"); + jpsBootstrapWorkDir = communityHome.resolve("out").resolve("jps-bootstrap"); } - info("Working directory: " + workDir); + info("Working directory: " + jpsBootstrapWorkDir); + Files.createDirectories(jpsBootstrapWorkDir); - Files.createDirectories(workDir); + mainArgsToRun = freeArgs.subList(2, freeArgs.size()).toArray(new String[0]); + } - Path m2LocalRepository = Path.of(System.getProperty("user.home"), ".m2", "repository"); - JpsModel model = JpsElementFactory.getInstance().createModel(); - JpsPathVariablesConfiguration pathVariablesConfiguration = - JpsModelSerializationDataService.getOrCreatePathVariablesConfiguration(model.getGlobal()); - pathVariablesConfiguration.addPathVariable( - "MAVEN_REPOSITORY", FileUtilRt.toSystemIndependentName(m2LocalRepository.toAbsolutePath().toString())); - - System.setProperty("kotlin.incremental.compilation", "true"); - System.setProperty("kotlin.daemon.enabled", "false"); - System.setProperty(GlobalOptions.COMPILE_PARALLEL_OPTION, "true"); - - Map pathVariables = JpsModelSerializationDataService.computeAllPathVariables(model.getGlobal()); - JpsProjectLoader.loadProject(model.getProject(), pathVariables, projectHome.toString()); - System.out.println( - "Loaded project " + projectHome + ": " + - model.getProject().getModules().size() + " modules, " + - model.getProject().getLibraryCollection().getLibraries().size() + " libraries in " + - (System.currentTimeMillis() - startTime) + " ms"); - - addSdk(model, "corretto-11", System.getProperty("java.home")); - - String url = "file://" + FileUtilRt.toSystemIndependentName(workDir.resolve("out").toString()); - JpsJavaExtensionService.getInstance().getOrCreateProjectExtension(model.getProject()).setOutputUrl(url); - - System.setProperty(JpsGroovycRunner.GROOVYC_IN_PROCESS, "true"); - System.setProperty(GroovyRtConstants.GROOVYC_ASM_RESOLVING_ONLY, "false"); - System.setProperty(GlobalOptions.USE_DEFAULT_FILE_LOGGING_OPTION, "true"); - System.setProperty(GlobalOptions.LOG_DIR_OPTION, workDir.resolve("log").toString()); - System.out.println("Log: " + System.getProperty(GlobalOptions.LOG_DIR_OPTION)); - - // kotlin.util.compiler-dependencies downloads all dependencies required for running Kotlin JPS compiler - // see org.jetbrains.kotlin.idea.artifacts.KotlinArtifactsFromSources - runBuild(model, workDir, "kotlin.util.compiler-dependencies"); - - runBuild(model, workDir, moduleName); - - JpsModule module = model.getProject().getModules() - .stream() - .filter(m -> moduleName.equals(m.getName())) - .findFirst().orElseThrow(); - JpsJavaDependenciesEnumerator enumerator = JpsJavaExtensionService - .dependencies(module) - .runtimeOnly() - .productionOnly() - .recursively() - .withoutSdk(); - - List roots = new ArrayList<>(); - for (File file : enumerator.classes().getRoots()) { - URL toURL = file.toURI().toURL(); - roots.add(toURL); - } - roots.sort(Comparator.comparing(URL::toString)); - - for (URL rootUrl : roots) { - verbose(" CLASSPATH " + rootUrl); - } + private void main() throws Throwable { + JpsModel model = JpsProjectUtils.loadJpsProject(projectHome); + JpsModule module = JpsProjectUtils.getModuleByName(model, moduleNameToRun); + loadClasses(module, model); setSystemPropertiesFromTeamCityBuild(); + runMainFromModuleRuntimeClasspath(classNameToRun, mainArgsToRun, module); + } - info("Running class " + className + " from module " + moduleName); + private void loadClasses(JpsModule module, JpsModel model) throws Throwable { + String fromJpsBuildEnvValue = System.getenv(ClassesFromJpsBuild.CLASSES_FROM_JPS_BUILD_ENV_NAME); + boolean jpsBuild = fromJpsBuildEnvValue != null && JpsBootstrapUtil.toBooleanChecked(fromJpsBuildEnvValue); - try (URLClassLoader classloader = new URLClassLoader(roots.toArray(new URL[0]), ClassLoader.getPlatformClassLoader())) { + String manifestJsonUrl = System.getenv(ClassesFromCompileInc.MANIFEST_JSON_URL_ENV_NAME); + + if (jpsBuild && manifestJsonUrl != null) { + throw new IllegalStateException("Both env. variables are set, choose only one: " + + ClassesFromJpsBuild.CLASSES_FROM_JPS_BUILD_ENV_NAME + " " + + ClassesFromCompileInc.MANIFEST_JSON_URL_ENV_NAME); + } + + if (!jpsBuild && manifestJsonUrl == null) { + // Nothing specified. It's ok locally, but on buildserver we must be sure + if (underTeamCity) { + throw new IllegalStateException("On buildserver one of the following env. variables must be set: " + + ClassesFromJpsBuild.CLASSES_FROM_JPS_BUILD_ENV_NAME + " " + + ClassesFromCompileInc.MANIFEST_JSON_URL_ENV_NAME); + } + } + + if (manifestJsonUrl != null) { + info("Downloading project classes from " + manifestJsonUrl); + ClassesFromCompileInc.downloadProjectClasses(model.getProject(), communityHome); + } else { + ClassesFromJpsBuild.buildModule(module, projectHome, model, jpsBootstrapWorkDir); + } + } + + private static void runMainFromModuleRuntimeClasspath(String className, String[] args, JpsModule module) throws Throwable { + List moduleRuntimeClasspath = JpsProjectUtils.getModuleRuntimeClasspath(module); + verbose("Module " + module.getName() + " classpath:\n " + moduleRuntimeClasspath.stream().map(URL::toString).collect(Collectors.joining("\n "))); + + info("Running class " + className + " from module " + module.getName()); + try (URLClassLoader classloader = new URLClassLoader(moduleRuntimeClasspath.toArray(new URL[0]), ClassLoader.getPlatformClassLoader())) { Class mainClass; try { mainClass = classloader.loadClass(className); } catch (ClassNotFoundException ex) { - for (URL rootUrl : roots) { - info(" CLASSPATH " + rootUrl); - } - - throw new IllegalStateException("Class '" + className + "' was not found. See the class path above"); + final String message = "Class '" + className + "' was not found in runtime classpath of module " + module.getName(); + info(message + ":\n " + moduleRuntimeClasspath.stream().map(URL::toString).collect(Collectors.joining("\n "))); + throw new IllegalStateException(message + ". See the class path above"); } + //noinspection ConfusingArgumentToVarargsMethod MethodHandles.lookup() .findStatic(mainClass, "main", MethodType.methodType(Void.TYPE, String[].class)) - .invokeExact(Arrays.copyOfRange(freeArgs, 2, freeArgs.length)); + .invokeExact(args); } } @@ -224,85 +182,6 @@ public class JpsBootstrapMain { } } - private static void runBuild(JpsModel model, Path workDir, String moduleName) throws Exception { - final long buildStart = System.currentTimeMillis(); - final AtomicReference firstError = new AtomicReference<>(); - - Path dataStorageRoot = workDir.resolve("jps-build-data"); - final Set moduleNames = model.getProject().getModules().stream().map(JpsNamedElement::getName).collect(Collectors.toUnmodifiableSet()); - Standalone.runBuild( - () -> model, - dataStorageRoot.toFile(), - false, - ContainerUtil.set(moduleName), - false, - Collections.emptyList(), - false, - msg -> { - BuildMessage.Kind kind = msg.getKind(); - String text = msg.toString(); - - switch (kind) { - case PROGRESS: - verbose(text); - break; - case WARNING: - warn(text); - case ERROR: - case INTERNAL_BUILDER_ERROR: - error(text); - break; - default: - if (!msg.getMessageText().isBlank()) { - if (moduleNames.contains(msg.getMessageText())) { - verbose(text); - } - else { - info(text); - } - } - break; - } - - if ((kind == BuildMessage.Kind.ERROR || kind == BuildMessage.Kind.INTERNAL_BUILDER_ERROR)) { - firstError.compareAndSet(null, text); - } - } - ); - - System.out.println("Finished building '" + moduleName + "' in " + (System.currentTimeMillis() - buildStart) + " ms"); - - String firstErrorText = firstError.get(); - if (firstErrorText != null) { - fatal("Build finished with errors. First error:\n" + firstErrorText); - } - } - - private static List readModulesFromReleaseFile(Path jdkDir) throws IOException { - Path releaseFile = jdkDir.resolve("release"); - Properties p = new Properties(); - try (InputStream is = Files.newInputStream(releaseFile)) { - p.load(is); - } - String jbrBaseUrl = URLUtil.JRT_PROTOCOL + URLUtil.SCHEME_SEPARATOR + - FileUtil.toSystemIndependentName(jdkDir.toFile().getAbsolutePath()) + - URLUtil.JAR_SEPARATOR; - String modules = p.getProperty("MODULES"); - return ContainerUtil.map(StringUtil.split(StringUtil.unquoteString(modules), " "), s -> jbrBaseUrl + s); - } - - private static void addSdk(JpsModel model, String sdkName, String sdkHome) throws IOException { - JpsJavaExtensionService.getInstance().addJavaSdk(model.getGlobal(), sdkName, sdkHome); - JpsLibrary additionalSdk = model.getGlobal().getLibraryCollection().findLibrary(sdkName); - if (additionalSdk == null) { - throw new IllegalStateException("SDK " + sdkHome + " was not found"); - } - - for (String moduleUrl : readModulesFromReleaseFile(Path.of(sdkHome))) { - additionalSdk.addRoot(moduleUrl, JpsOrderRootType.COMPILED); - } - } - @Contract("->fail") private static void showUsagesAndExit() { HelpFormatter formatter = new HelpFormatter(); diff --git a/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/JpsBootstrapUtil.java b/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/JpsBootstrapUtil.java index dbced2a82069..5f4dcbe5e856 100644 --- a/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/JpsBootstrapUtil.java +++ b/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/JpsBootstrapUtil.java @@ -10,12 +10,12 @@ import java.io.BufferedReader; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; +import java.util.*; +import java.util.concurrent.*; public class JpsBootstrapUtil { public static final String TEAMCITY_BUILD_PROPERTIES_FILE_ENV = "TEAMCITY_BUILD_PROPERTIES_FILE"; + public static final String TEAMCITY_CONFIGURATION_PROPERTIES_SYSTEM_PROPERTY = "teamcity.configuration.properties.file"; public static final boolean underTeamCity = System.getenv("TEAMCITY_VERSION") != null; @@ -74,6 +74,15 @@ public class JpsBootstrapUtil { JpsBootstrapUtil.verboseEnabled = verboseEnabled; } + public static boolean toBooleanChecked(String s) { + switch (s) { + case "true": return true; + case "false": return false; + default: + throw new IllegalArgumentException("Could not convert '" + s + "' to boolean. Only 'true' or 'false' values are accepted"); + } + } + public static Properties getTeamCitySystemProperties() throws IOException { if (!underTeamCity) { throw new IllegalStateException("Not under TeamCity"); @@ -91,4 +100,79 @@ public class JpsBootstrapUtil { return properties; } + + public static Properties getTeamCityConfigProperties() throws IOException { + Properties systemProperties = getTeamCitySystemProperties(); + + final String configPropertiesFile = systemProperties.getProperty(TEAMCITY_CONFIGURATION_PROPERTIES_SYSTEM_PROPERTY); + if (configPropertiesFile == null || configPropertiesFile.length() == 0) { + throw new IllegalStateException("TeamCity system property '" + TEAMCITY_CONFIGURATION_PROPERTIES_SYSTEM_PROPERTY + "' is missing under TeamCity build"); + } + + Properties properties = new Properties(); + try (BufferedReader reader = Files.newBufferedReader(Path.of(configPropertiesFile))) { + properties.load(reader); + } + + return properties; + } + + public static String getTeamCityConfigPropertyOrThrow(String configProperty) throws IOException { + final Properties properties = getTeamCityConfigProperties(); + final String value = properties.getProperty(configProperty); + if (value == null) { + throw new IllegalStateException("TeamCity config property " + configProperty + " was not found"); + } + return value; + } + + static List executeTasksInParallel(List> tasks) throws InterruptedException { + ExecutorService executorService = Executors.newFixedThreadPool(5); + long start = System.currentTimeMillis(); + + try { + info("Executing " + tasks.size() + " in parallel"); + + List> futures = executorService.invokeAll(tasks); + + List errors = new ArrayList<>(); + List results = new ArrayList<>(); + for (Future future : futures) { + try { + T r = future.get(10, TimeUnit.MINUTES); + results.add(r); + } + catch (ExecutionException e) { + errors.add(e.getCause()); + if (errors.size() > 4) { + executorService.shutdownNow(); + break; + } + } + catch (TimeoutException e) { + throw new IllegalStateException("Timeout waiting for results, exiting"); + } + } + + if (errors.size() > 0) { + RuntimeException t = new RuntimeException("Unable to execute all targets, " + errors.size() + " error(s)"); + for (Throwable err : errors) { + t.addSuppressed(err); + } + throw t; + } + + if (results.size() != tasks.size()) { + throw new IllegalStateException("received results size != tasks size (" + results.size() + " != " + tasks.size() + ")"); + } + + return results; + } finally { + JpsBootstrapUtil.info("Finished all tasks in " + (System.currentTimeMillis() - start) + " ms"); + + if (!executorService.isShutdown()) { + executorService.shutdownNow(); + } + } + } } diff --git a/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/JpsProjectUtils.java b/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/JpsProjectUtils.java new file mode 100644 index 000000000000..1147844f9ecb --- /dev/null +++ b/platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/JpsProjectUtils.java @@ -0,0 +1,103 @@ +// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.jpsBootstrap; + +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.io.FileUtilRt; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.io.URLUtil; +import org.jetbrains.jps.model.JpsElementFactory; +import org.jetbrains.jps.model.JpsModel; +import org.jetbrains.jps.model.java.JpsJavaDependenciesEnumerator; +import org.jetbrains.jps.model.java.JpsJavaExtensionService; +import org.jetbrains.jps.model.library.JpsLibrary; +import org.jetbrains.jps.model.library.JpsOrderRootType; +import org.jetbrains.jps.model.module.JpsModule; +import org.jetbrains.jps.model.serialization.JpsModelSerializationDataService; +import org.jetbrains.jps.model.serialization.JpsPathVariablesConfiguration; +import org.jetbrains.jps.model.serialization.JpsProjectLoader; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +@SuppressWarnings("SameParameterValue") +public class JpsProjectUtils { + public static JpsModel loadJpsProject(Path projectHome) throws Exception { + long startTime = System.currentTimeMillis(); + + Path m2LocalRepository = Path.of(System.getProperty("user.home"), ".m2", "repository"); + JpsModel model = JpsElementFactory.getInstance().createModel(); + JpsPathVariablesConfiguration pathVariablesConfiguration = + JpsModelSerializationDataService.getOrCreatePathVariablesConfiguration(model.getGlobal()); + pathVariablesConfiguration.addPathVariable( + "MAVEN_REPOSITORY", FileUtilRt.toSystemIndependentName(m2LocalRepository.toAbsolutePath().toString())); + + Map pathVariables = JpsModelSerializationDataService.computeAllPathVariables(model.getGlobal()); + JpsProjectLoader.loadProject(model.getProject(), pathVariables, projectHome.toString()); + System.out.println( + "Loaded project " + projectHome + ": " + + model.getProject().getModules().size() + " modules, " + + model.getProject().getLibraryCollection().getLibraries().size() + " libraries in " + + (System.currentTimeMillis() - startTime) + " ms"); + + addSdk(model, "corretto-11", System.getProperty("java.home")); + + return model; + } + + public static JpsModule getModuleByName(JpsModel model, String moduleName) { + return model.getProject().getModules() + .stream() + .filter(m -> moduleName.equals(m.getName())) + .findFirst().orElseThrow(() -> new IllegalStateException("Module " + moduleName + " is not found")); + } + + public static List getModuleRuntimeClasspath(JpsModule module) throws MalformedURLException { + JpsJavaDependenciesEnumerator enumerator = JpsJavaExtensionService + .dependencies(module) + .runtimeOnly() + .productionOnly() + .recursively() + .withoutSdk(); + + List roots = new ArrayList<>(); + for (File file : enumerator.classes().getRoots()) { + URL toURL = file.toURI().toURL(); + roots.add(toURL); + } + roots.sort(Comparator.comparing(URL::toString)); + + return roots; + } + + private static void addSdk(JpsModel model, String sdkName, String sdkHome) throws IOException { + JpsJavaExtensionService.getInstance().addJavaSdk(model.getGlobal(), sdkName, sdkHome); + JpsLibrary additionalSdk = model.getGlobal().getLibraryCollection().findLibrary(sdkName); + if (additionalSdk == null) { + throw new IllegalStateException("SDK " + sdkHome + " was not found"); + } + + for (String moduleUrl : readModulesFromReleaseFile(Path.of(sdkHome))) { + additionalSdk.addRoot(moduleUrl, JpsOrderRootType.COMPILED); + } + } + + private static List readModulesFromReleaseFile(Path jdkDir) throws IOException { + Path releaseFile = jdkDir.resolve("release"); + Properties p = new Properties(); + try (InputStream is = Files.newInputStream(releaseFile)) { + p.load(is); + } + String jbrBaseUrl = URLUtil.JRT_PROTOCOL + URLUtil.SCHEME_SEPARATOR + + FileUtil.toSystemIndependentName(jdkDir.toFile().getAbsolutePath()) + + URLUtil.JAR_SEPARATOR; + String modules = p.getProperty("MODULES"); + return ContainerUtil.map(StringUtil.split(StringUtil.unquoteString(modules), " "), s -> jbrBaseUrl + s); + } +}