jps-bootstrap: download classes from compile inc configuration

GitOrigin-RevId: 56393e4b6dc38f085b49f0114ea7d445da100183
This commit is contained in:
Leonid Shalupov
2021-12-03 18:55:05 +00:00
committed by intellij-monorepo-bot
parent 4a46b62742
commit e5a1154ba5
8 changed files with 820 additions and 193 deletions
+20
View File
@@ -15,11 +15,31 @@
</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>
@@ -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);
}
}
}
@@ -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);
}
});
}
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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();
}
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<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);
}
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<URL> 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<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();
@@ -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();
}
}
}
}
@@ -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);
}
}