jps-bootstrap: download classes from compile inc configuration
authorLeonid Shalupov <leonid@shalupov.com>
Fri, 3 Dec 2021 18:54:41 +0000 (19:54 +0100)
committerintellij-monorepo-bot <intellij-monorepo-bot-no-reply@jetbrains.com>
Fri, 3 Dec 2021 18:55:05 +0000 (18:55 +0000)
GitOrigin-RevId: 56393e4b6dc38f085b49f0114ea7d445da100183

platform/jps-bootstrap/pom.xml
platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/BuildDependenciesDownloader.java [new file with mode: 0644]
platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/BuildDependenciesUtil.java [new file with mode: 0644]
platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/ClassesFromCompileInc.java [new file with mode: 0644]
platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/ClassesFromJpsBuild.java [new file with mode: 0644]
platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/JpsBootstrapMain.java
platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/JpsBootstrapUtil.java
platform/jps-bootstrap/src/main/java/org/jetbrains/jpsBootstrap/JpsProjectUtils.java [new file with mode: 0644]

index cd0b1ca82332f010b967c291e1ed732bf1ace44e..f7e4623ad6d1b9227386237a0da6b9c83a1d06aa 100644 (file)
   </properties>
 
   <dependencies>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+      <version>31.0.1-jre</version>
+    </dependency>
     <dependency>
       <groupId>commons-cli</groupId>
       <artifactId>commons-cli</artifactId>
       <version>1.5.0</version>
     </dependency>
+    <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+      <version>1.15</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-compress</artifactId>
+      <version>1.21</version>
+    </dependency>
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+      <version>2.8.8</version>
+    </dependency>
     <dependency>
       <groupId>com.jetbrains.intellij.platform</groupId>
       <artifactId>jps-model</artifactId>
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 (file)
index 0000000..6aced62
--- /dev/null
@@ -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<Path> 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<Path> 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 (file)
index 0000000..3463331
--- /dev/null
@@ -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<ZipArchiveEntry> 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<Path> 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 (file)
index 0000000..97650dc
--- /dev/null
@@ -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<String, Path> parts = downloadPartsFromMetadataJson(manifest, communityRoot);
+    assignModuleOutputs(project, parts);
+  }
+
+  private static void assignModuleOutputs(JpsProject project, Map<String, Path> parts) {
+    Map<String, Path> 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<String, Path> 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<Callable<Pair<String, Path>>> tasks = new ArrayList<>();
+    for (final Map.Entry<String, String> entry : partsMetadata.files.entrySet()) {
+      Callable<Pair<String, Path>> 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<String, String> 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 (file)
index 0000000..76729d6
--- /dev/null
@@ -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<String> firstError = new AtomicReference<>();
+
+    Path dataStorageRoot = workDir.resolve("jps-build-data");
+    final Set<String> 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);
+    }
+  }
+}
index e65674ce35e73ac18e1f2077c5e145110cdbcd8d..49fc6ee7094716c7d4d3937e29cf08ef1d3ffa5a 100644 (file)
@@ -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<String> freeArgs = Arrays.asList(cmdline.getArgs());
+    if (cmdline.hasOption(ARG_HELP) || freeArgs.size() < 2) {
       showUsagesAndExit();
     }
 
-    JpsBootstrapUtil.setVerboseEnabled(cmdline.hasOption(ARG_VERBOSE));
-
-    long startTime = System.currentTimeMillis();
+    moduleNameToRun = freeArgs.get(0);
+    classNameToRun = freeArgs.get(1);
 
-    String moduleName = freeArgs[0];
-    String className = freeArgs[1];
+    JpsBootstrapUtil.setVerboseEnabled(cmdline.hasOption(ARG_VERBOSE));
 
     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);
-
-    Files.createDirectories(workDir);
-
-    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<String, String> 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<URL> roots = new ArrayList<>();
-    for (File file : enumerator.classes().getRoots()) {
-      URL toURL = file.toURI().toURL();
-      roots.add(toURL);
+    info("Working directory: " + jpsBootstrapWorkDir);
+    Files.createDirectories(jpsBootstrapWorkDir);
+
+    mainArgsToRun = freeArgs.subList(2, freeArgs.size()).toArray(new String[0]);
+  }
+
+  private void main() throws Throwable {
+    JpsModel model = JpsProjectUtils.loadJpsProject(projectHome);
+    JpsModule module = JpsProjectUtils.getModuleByName(model, moduleNameToRun);
+
+    loadClasses(module, model);
+    setSystemPropertiesFromTeamCityBuild();
+    runMainFromModuleRuntimeClasspath(classNameToRun, mainArgsToRun, module);
+  }
+
+  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);
+
+    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);
     }
-    roots.sort(Comparator.comparing(URL::toString));
 
-    for (URL rootUrl : roots) {
-      verbose("  CLASSPATH " + rootUrl);
+    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);
+      }
     }
 
-    setSystemPropertiesFromTeamCityBuild();
+    if (manifestJsonUrl != null) {
+      info("Downloading project classes from " + manifestJsonUrl);
+      ClassesFromCompileInc.downloadProjectClasses(model.getProject(), communityHome);
+    } else {
+      ClassesFromJpsBuild.buildModule(module, projectHome, model, jpsBootstrapWorkDir);
+    }
+  }
 
-    info("Running class " + className + " from module " + moduleName);
+  private static void runMainFromModuleRuntimeClasspath(String className, String[] args, JpsModule module) throws Throwable {
+    List<URL> moduleRuntimeClasspath = JpsProjectUtils.getModuleRuntimeClasspath(module);
+    verbose("Module " + module.getName() + " classpath:\n  " + moduleRuntimeClasspath.stream().map(URL::toString).collect(Collectors.joining("\n  ")));
 
-    try (URLClassLoader classloader = new URLClassLoader(roots.toArray(new URL[0]), ClassLoader.getPlatformClassLoader())) {
+    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<String> firstError = new AtomicReference<>();
-
-    Path dataStorageRoot = workDir.resolve("jps-build-data");
-    final Set<String> 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<String> 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();
index dbced2a82069e403f08807a0cf8f8330f61fe23c..5f4dcbe5e85665fee8a11a2fdf859e954187e767 100644 (file)
@@ -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 <T> List<T> executeTasksInParallel(List<Callable<T>> tasks) throws InterruptedException {
+    ExecutorService executorService = Executors.newFixedThreadPool(5);
+    long start = System.currentTimeMillis();
+
+    try {
+      info("Executing " + tasks.size() + " in parallel");
+
+      List<Future<T>> futures = executorService.invokeAll(tasks);
+
+      List<Throwable> errors = new ArrayList<>();
+      List<T> results = new ArrayList<>();
+      for (Future<T> 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 (file)
index 0000000..1147844
--- /dev/null
@@ -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<String, String> 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<URL> getModuleRuntimeClasspath(JpsModule module) throws MalformedURLException {
+    JpsJavaDependenciesEnumerator enumerator = JpsJavaExtensionService
+      .dependencies(module)
+      .runtimeOnly()
+      .productionOnly()
+      .recursively()
+      .withoutSdk();
+
+    List<URL> 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<String> 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);
+  }
+}