diff --git a/.gitignore b/.gitignore index 3ecbdbef..691c7386 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ dist **/.checkstyle test-resources/ **/.gradle +jdtls.ext/com.microsoft.buildserver.adapter/bsp diff --git a/.vscode/launch.json b/.vscode/launch.json index 2f331a2d..6d344592 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,8 @@ "name": "Attach to Plugin", "request": "attach", "hostName": "localhost", - "port": 1044 + "port": 1044, + "projectName": "com.microsoft.buildserver.adapter" }, { "name": "Extension Tests - General", diff --git a/.vscode/settings.json b/.vscode/settings.json index 56a9603b..ef22afb0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,6 @@ "**/.metadata/**", "**/archetype-resources/**", "**/META-INF/maven/**", - ] + ], + "java.compile.nullAnalysis.mode": "disabled" } diff --git a/javaConfig.json b/javaConfig.json index 08c7530f..3a13d1f7 100644 --- a/javaConfig.json +++ b/javaConfig.json @@ -1,6 +1,7 @@ { "projects": [ - "./jdtls.ext/com.microsoft.jdtls.ext.core" + "./jdtls.ext/com.microsoft.jdtls.ext.core", + "./com.microsoft.buildserver.adapter" ], "targetPlatform": "./jdtls.ext/com.microsoft.jdtls.ext.target/com.microsoft.jdtls.ext.tp.target" } diff --git a/jdtls.ext/com.microsoft.buildserver.adapter/META-INF/MANIFEST.MF b/jdtls.ext/com.microsoft.buildserver.adapter/META-INF/MANIFEST.MF new file mode 100644 index 00000000..acb92f37 --- /dev/null +++ b/jdtls.ext/com.microsoft.buildserver.adapter/META-INF/MANIFEST.MF @@ -0,0 +1,22 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Build Server Adapter +Bundle-SymbolicName: com.microsoft.buildserver.adapter;singleton:=true +Bundle-Version: 0.23.0 +Bundle-Activator: com.microsoft.buildserver.adapter.BuildServerAdapter +Bundle-RequiredExecutionEnvironment: JavaSE-11 +Bundle-ActivationPolicy: lazy +Import-Package: org.eclipse.jdt.core, + org.eclipse.jdt.launching, + org.osgi.framework;version="1.3.0" +Require-Bundle: org.eclipse.core.runtime, + org.eclipse.jdt.ls.core, + org.eclipse.jdt.core, + org.eclipse.core.resources, + org.eclipse.lsp4j.jsonrpc, + org.eclipse.lsp4j, + org.apache.commons.lang3, + org.eclipse.buildship.core, + com.google.gson +Bundle-ClassPath: lib/bsp4j-2.1.0-M4.jar, + . diff --git a/jdtls.ext/com.microsoft.buildserver.adapter/build.properties b/jdtls.ext/com.microsoft.buildserver.adapter/build.properties new file mode 100644 index 00000000..0cb55427 --- /dev/null +++ b/jdtls.ext/com.microsoft.buildserver.adapter/build.properties @@ -0,0 +1,7 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + plugin.xml,\ + lib/bsp4j-2.1.0-M4.jar,\ + bsp/ diff --git a/jdtls.ext/com.microsoft.buildserver.adapter/plugin.xml b/jdtls.ext/com.microsoft.buildserver.adapter/plugin.xml new file mode 100644 index 00000000..e49fb0d6 --- /dev/null +++ b/jdtls.ext/com.microsoft.buildserver.adapter/plugin.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/jdtls.ext/com.microsoft.buildserver.adapter/pom.xml b/jdtls.ext/com.microsoft.buildserver.adapter/pom.xml new file mode 100644 index 00000000..0645b0d8 --- /dev/null +++ b/jdtls.ext/com.microsoft.buildserver.adapter/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + com.microsoft.jdtls.ext + jdtls-ext-parent + 0.23.0 + + com.microsoft.buildserver.adapter + eclipse-plugin + ${base.name} :: Build Server Adapter + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + org.apache.maven.plugins + maven-dependency-plugin + + + + ch.epfl.scala + bsp4j + 2.1.0-M4 + + + + + + + + diff --git a/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BspClient.java b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BspClient.java new file mode 100644 index 00000000..9180b3d5 --- /dev/null +++ b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BspClient.java @@ -0,0 +1,122 @@ +package com.microsoft.buildserver.adapter; + +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.buildship.core.internal.util.gradle.GradleVersion; +import org.eclipse.jdt.ls.core.internal.EventNotification; +import org.eclipse.jdt.ls.core.internal.EventType; +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.jdt.ls.core.internal.ProgressReport; +import org.eclipse.lsp4j.ExecuteCommandParams; + +import ch.epfl.scala.bsp4j.BuildClient; +import ch.epfl.scala.bsp4j.DidChangeBuildTarget; +import ch.epfl.scala.bsp4j.LogMessageParams; +import ch.epfl.scala.bsp4j.PublishDiagnosticsParams; +import ch.epfl.scala.bsp4j.ShowMessageParams; +import ch.epfl.scala.bsp4j.TaskDataKind; +import ch.epfl.scala.bsp4j.TaskFinishParams; +import ch.epfl.scala.bsp4j.TaskProgressParams; +import ch.epfl.scala.bsp4j.TaskStartParams; + +public class BspClient implements BuildClient { + + private ConcurrentHashMap taskMap = new ConcurrentHashMap<>(); + + @Override + public void onBuildShowMessage(ShowMessageParams params) { + // TODO: BSP does not support additional data in ShowMessageParams, + // as a workaround, we put [Error Code] as a suffix in the message. + if (params.getMessage().endsWith("[-1]")) { + String argString = params.getMessage().substring(0, params.getMessage().length() - 5); + String[] args = argString.split(","); + String projectUri = args[0]; + String highestJdk = args[1]; + GradleCompatibilityInfo info = new GradleCompatibilityInfo( + projectUri, + "Gradle version is not compatible with JDK version. Please update the Gradle wrapper.", + highestJdk, + GradleVersion.current().getVersion() + ); + EventNotification notification = new EventNotification().withType(EventType.IncompatibleGradleJdkIssue).withData(info); + JavaLanguageServerPlugin.getProjectsManager().getConnection().sendEventNotification(notification); + } + } + + @Override + public void onBuildLogMessage(LogMessageParams params) { + } + + @Override + public void onBuildTaskStart(TaskStartParams params) { + if (Objects.equals(params.getDataKind(), TaskDataKind.COMPILE_TASK)) { + ExecuteCommandParams clientCommand = new ExecuteCommandParams("_java.buildServer.gradle.buildStart", Arrays.asList(params.getMessage())); + JavaLanguageServerPlugin.getProjectsManager().getConnection().sendNotification(clientCommand); + } else { + ProgressReport progressReport = new ProgressReport(params.getTaskId().getId()); + progressReport.setTask("Build Server Task"); + progressReport.setStatus(params.getMessage()); + progressReport.setComplete(false); + taskMap.put(params.getTaskId().getId(), progressReport); + JavaLanguageServerPlugin.getProjectsManager().getConnection().sendProgressReport(progressReport); + } + } + + @Override + public void onBuildTaskProgress(TaskProgressParams params) { + if (Objects.equals(params.getDataKind(), TaskDataKind.COMPILE_TASK)) { + ExecuteCommandParams clientCommand = new ExecuteCommandParams("_java.buildServer.gradle.buildProgress", Arrays.asList(params.getMessage())); + JavaLanguageServerPlugin.getProjectsManager().getConnection().sendNotification(clientCommand); + } else { + ProgressReport progressReport = taskMap.get(params.getTaskId().getId()); + if (progressReport == null) { + return; + } + progressReport.setStatus(params.getMessage()); + JavaLanguageServerPlugin.getProjectsManager().getConnection().sendProgressReport(progressReport); + } + } + + @Override + public void onBuildTaskFinish(TaskFinishParams params) { + if (Objects.equals(params.getDataKind(), TaskDataKind.COMPILE_REPORT)) { + ExecuteCommandParams clientCommand = new ExecuteCommandParams("_java.buildServer.gradle.buildComplete", Arrays.asList(params.getMessage())); + JavaLanguageServerPlugin.getProjectsManager().getConnection().sendNotification(clientCommand); + } else { + ProgressReport progressReport = taskMap.get(params.getTaskId().getId()); + if (progressReport == null) { + return; + } + progressReport.setComplete(true); + progressReport.setStatus(params.getMessage()); + JavaLanguageServerPlugin.getProjectsManager().getConnection().sendProgressReport(progressReport); + + taskMap.remove(params.getTaskId().getId()); + } + } + + @Override + public void onBuildPublishDiagnostics(PublishDiagnosticsParams params) { + } + + @Override + public void onBuildTargetDidChange(DidChangeBuildTarget params) { + } + + private class GradleCompatibilityInfo { + + private String projectUri; + private String message; + private String highestJavaVersion; + private String recommendedGradleVersion; + + public GradleCompatibilityInfo(String projectPath, String message, String highestJavaVersion, String recommendedGradleVersion) { + this.projectUri = projectPath; + this.message = message; + this.highestJavaVersion = highestJavaVersion; + this.recommendedGradleVersion = recommendedGradleVersion; + } + } +} diff --git a/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BspGradleBuildSupport.java b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BspGradleBuildSupport.java new file mode 100644 index 00000000..00e2bbbd --- /dev/null +++ b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BspGradleBuildSupport.java @@ -0,0 +1,488 @@ +package com.microsoft.buildserver.adapter; + +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.buildship.core.internal.workspace.EclipseVmUtil; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.jdt.core.IClasspathAttribute; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.internal.core.ClasspathEntry; +import org.eclipse.jdt.launching.IVMInstall; +import org.eclipse.jdt.launching.JavaRuntime; +import org.eclipse.jdt.ls.core.internal.JSONUtility; +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.jdt.ls.core.internal.ProjectUtils; +import org.eclipse.jdt.ls.core.internal.ResourceUtils; +import org.eclipse.jdt.ls.core.internal.managers.IBuildSupport; +import org.eclipse.jdt.ls.core.internal.managers.ProjectsManager.CHANGE_TYPE; + +import com.microsoft.buildserver.adapter.bsp4j.extended.JvmBuildTargetExt; + +import ch.epfl.scala.bsp4j.BuildServer; +import ch.epfl.scala.bsp4j.BuildTarget; +import ch.epfl.scala.bsp4j.BuildTargetIdentifier; +import ch.epfl.scala.bsp4j.BuildTargetTag; +import ch.epfl.scala.bsp4j.DependencyModule; +import ch.epfl.scala.bsp4j.DependencyModulesItem; +import ch.epfl.scala.bsp4j.DependencyModulesParams; +import ch.epfl.scala.bsp4j.DependencyModulesResult; +import ch.epfl.scala.bsp4j.MavenDependencyModule; +import ch.epfl.scala.bsp4j.MavenDependencyModuleArtifact; +import ch.epfl.scala.bsp4j.OutputPathItem; +import ch.epfl.scala.bsp4j.OutputPathsParams; +import ch.epfl.scala.bsp4j.OutputPathsResult; +import ch.epfl.scala.bsp4j.ResourcesItem; +import ch.epfl.scala.bsp4j.ResourcesParams; +import ch.epfl.scala.bsp4j.ResourcesResult; +import ch.epfl.scala.bsp4j.SourceItem; +import ch.epfl.scala.bsp4j.SourcesItem; +import ch.epfl.scala.bsp4j.SourcesParams; +import ch.epfl.scala.bsp4j.SourcesResult; +import ch.epfl.scala.bsp4j.WorkspaceBuildTargetsResult; + +/** + * @author Fred Bricon + * + */ +public class BspGradleBuildSupport implements IBuildSupport { + + public static final Pattern GRADLE_FILE_EXT = Pattern.compile("^.*\\.gradle(\\.kts)?$"); + public static final String GRADLE_PROPERTIES = "gradle.properties"; + + private static final IClasspathAttribute testAttribute = JavaCore.newClasspathAttribute(IClasspathAttribute.TEST, "true"); + private static final IClasspathAttribute optionalAttribute = JavaCore.newClasspathAttribute(IClasspathAttribute.OPTIONAL, "true"); + + @Override + public boolean applies(IProject project) { + return BspUtils.isBspGradleProject(project); + } + + @Override + public void update(IProject project, boolean force, IProgressMonitor monitor) throws CoreException { + if (!applies(project)) { + return; + } + JavaLanguageServerPlugin.logInfo("Starting Gradle update for " + project.getName()); + + File buildFile = project.getFile(BspGradleProjectImporter.BUILD_GRADLE_DESCRIPTOR).getLocation().toFile(); + File settingsFile = project.getFile(BspGradleProjectImporter.SETTINGS_GRADLE_DESCRIPTOR).getLocation().toFile(); + File buildKtsFile = project.getFile(BspGradleProjectImporter.BUILD_GRADLE_KTS_DESCRIPTOR).getLocation().toFile(); + File settingsKtsFile = project.getFile(BspGradleProjectImporter.SETTINGS_GRADLE_KTS_DESCRIPTOR).getLocation().toFile(); + boolean shouldUpdate = force || (buildFile.exists() && BuildServerAdapter.getDigestStore().updateDigest(buildFile.toPath())) + || (settingsFile.exists() && BuildServerAdapter.getDigestStore().updateDigest(settingsFile.toPath())) + || (buildKtsFile.exists() && BuildServerAdapter.getDigestStore().updateDigest(buildKtsFile.toPath())) + || (settingsKtsFile.exists() && BuildServerAdapter.getDigestStore().updateDigest(settingsKtsFile.toPath())); + if (shouldUpdate) { + BuildServer buildServer = BuildServerAdapter.getBuildServer(); + if (buildServer == null) { + return; + } + buildServer.workspaceReload().join(); + WorkspaceBuildTargetsResult workspaceBuildTargetsResult = buildServer.workspaceBuildTargets().join(); + List buildTargets = workspaceBuildTargetsResult.getTargets(); + Map> buildTargetMap = BspUtils.mapBuildTargetsByUri(buildTargets); + for (Entry> entrySet : buildTargetMap.entrySet()) { + String baseDir = entrySet.getKey(); + if (baseDir == null) { + JavaLanguageServerPlugin.logError("The base directory of the build target is null."); + continue; + } + List targets = entrySet.getValue(); + if (targets == null || targets.isEmpty()) { + continue; + } + URI Uri = null; + try { + Uri = new URI(baseDir); + } catch (URISyntaxException e) { + JavaLanguageServerPlugin.logException(e); + continue; + } + IProject prj = ProjectUtils.getProjectFromUri(Uri.toString()); + BuildServerTargetsManager.getInstance().setBuildTargets(prj, targets); + } + updateClassPath(project, true, monitor); + } + } + + // todo: refactor the method + public void updateClassPath(IProject project, boolean updateProjectDependency, IProgressMonitor monitor) throws CoreException { + BuildServer buildServer = BuildServerAdapter.getBuildServer(); + if (buildServer == null) { + return; + } + + IJavaProject javaProject = JavaCore.create(project); + List classpath = new LinkedList<>(); + + Set mainDependencies = new HashSet<>(); + Set testDependencies = new HashSet<>(); + + List buildTargets = BuildServerTargetsManager.getInstance().getBuildTargets(project); + if (buildTargets == null) { + JavaLanguageServerPlugin.logError("Cannot find build targets for project " + project.getName()); + return; + } + // Sort build targets so that test targets are last, this is to avoid duplicated source roots. + buildTargets.sort((bt1, bt2) -> { + if (bt1.getTags().contains(BuildTargetTag.TEST) && !bt2.getTags().contains(BuildTargetTag.TEST)) { + return 1; + } + if (!bt1.getTags().contains(BuildTargetTag.TEST) && bt2.getTags().contains(BuildTargetTag.TEST)) { + return -1; + } + return 0; + }); + for (BuildTarget buildTarget : buildTargets) { + boolean isTest = buildTarget.getTags().contains(BuildTargetTag.TEST); + + OutputPathsResult outputResult = buildServer.buildTargetOutputPaths(new OutputPathsParams(Arrays.asList(buildTarget.getId()))).join(); + List outputPaths = outputResult.getItems().get(0).getOutputPaths(); + String sourceOutputUriString = outputPaths.get(0).getUri(); + IPath sourceOutputPath = ResourceUtils.filePathFromURI(sourceOutputUriString); + File outputDirectory = sourceOutputPath.toFile(); + if (!outputDirectory.exists()) { + outputDirectory.mkdirs(); + } + IPath relativeSourceOutputPath = sourceOutputPath.makeRelativeTo(project.getLocation()); + IPath sourceOutputFullPath = project.getFolder(relativeSourceOutputPath).getFullPath(); + + SourcesResult sourcesResult = buildServer.buildTargetSources(new SourcesParams(Arrays.asList(buildTarget.getId()))).join(); + for (SourcesItem item : sourcesResult.getItems()) { + if (!Objects.equals(buildTarget.getId(), item.getTarget())) { + continue; + } + for (SourceItem source : item.getSources()) { + IPath sourcePath = ResourceUtils.filePathFromURI(source.getUri()); + IPath projectLocation = project.getLocation(); + IPath sourceFullPath; + if (projectLocation.isPrefixOf(sourcePath)) { + IPath relativeSourcePath = sourcePath.makeRelativeTo(project.getLocation()); + sourceFullPath = project.getFolder(relativeSourcePath).getFullPath(); + + } else { + // if the source path is not relative to the project location, we need to create a linked folder for it. + IPath baseDirectory = ResourceUtils.filePathFromURI(buildTarget.getBaseDirectory()); + IPath relativeSourcePath = sourcePath.makeRelativeTo(baseDirectory); + if (relativeSourcePath.isAbsolute()) { + JavaLanguageServerPlugin.logError("The source path is not relative to the workspace root: " + relativeSourcePath); + continue; + } + IFolder linkFolder = project.getFolder(relativeSourcePath.toString().replace(IPath.SEPARATOR, '_')); + if (!linkFolder.exists()) { + linkFolder.createLink(sourcePath, IResource.REPLACE, monitor); + } + sourceFullPath = linkFolder.getFullPath(); + } + // skip the duplicated source root. Since main source set comes first than test + // source set, skipping here is safe. + if (classpath.stream().anyMatch(entry -> entry.getPath().equals(sourceFullPath))) { + continue; + } + List classpathAttributes = new LinkedList<>(); + if (isTest) { + classpathAttributes.add(testAttribute); + } + + if (!sourcePath.toFile().exists() || source.getGenerated() || + // if the source path is in the build directory, adding optional attribute to it + // because it might be cleaned. + Arrays.stream(sourceFullPath.segments()).anyMatch(segment -> segment.equals("build"))) { + classpathAttributes.add(optionalAttribute); + } + classpath.add(JavaCore.newSourceEntry(sourceFullPath, null, null, sourceOutputFullPath, classpathAttributes.toArray(new IClasspathAttribute[0]))); + } + } + + if (!classpath.isEmpty()) { + addJavaNature(project, monitor); + } + + if (outputPaths.size() > 1) { + // TODO: should iterate over all items + // handle resource output + String resourceOutputUriString = outputResult.getItems().get(0).getOutputPaths().get(1).getUri(); + IPath resourceOutputPath = ResourceUtils.filePathFromURI(resourceOutputUriString); + File resourceOutputDirectory = resourceOutputPath.toFile(); + if (!resourceOutputDirectory.exists()) { + resourceOutputDirectory.mkdirs(); + } + IPath relativeResourceOutputPath = resourceOutputPath.makeRelativeTo(project.getLocation()); + IPath resourceOutputFullPath = project.getFolder(relativeResourceOutputPath).getFullPath(); + + ResourcesResult resourcesResult = buildServer.buildTargetResources(new ResourcesParams(Arrays.asList(buildTarget.getId()))).join(); + for (ResourcesItem item : resourcesResult.getItems()) { + if (!Objects.equals(buildTarget.getId(), item.getTarget())) { + continue; + } + + for (String resourceUri : item.getResources()) { + IPath resourcePath = ResourceUtils.filePathFromURI(resourceUri); + if (!resourcePath.toFile().exists()) { + continue; + } + IPath relativeResourcePath = resourcePath.makeRelativeTo(project.getLocation()); + IPath resourceFullPath = project.getFolder(relativeResourcePath).getFullPath(); + List classpathAttributes = new LinkedList<>(); + if (isTest) { + classpathAttributes.add(testAttribute); + } + classpathAttributes.add(optionalAttribute); + classpath.add(JavaCore.newSourceEntry(resourceFullPath, null, null, resourceOutputFullPath, classpathAttributes.toArray(new IClasspathAttribute[0]))); + } + } + } + + DependencyModulesResult dependencyModuleResult = buildServer.buildTargetDependencyModules(new DependencyModulesParams(Arrays.asList(buildTarget.getId()))).join(); + for (DependencyModulesItem item : dependencyModuleResult.getItems()) { + if (!Objects.equals(buildTarget.getId(), item.getTarget())) { + continue; + } + for (DependencyModule module : item.getModules()) { + MavenDependencyModule mavenModule = JSONUtility.toModel(module.getData(), MavenDependencyModule.class); + if (isTest) { + testDependencies.add(mavenModule); + } else { + mainDependencies.add(mavenModule); + } + } + } + } + + JvmBuildTargetExt jvmBuildTarget = getHighestJavaVersion(buildTargets); + String javaVersion = getEclipseCompatibleVersion(jvmBuildTarget.getTargetBytecodeVersion()); + IVMInstall vm = EclipseVmUtil.findOrRegisterStandardVM(javaVersion, new File(jvmBuildTarget.getJavaHome())); + classpath.add(JavaCore.newContainerEntry(JavaRuntime.newJREContainerPath(vm))); + + testDependencies = testDependencies.stream().filter(t -> { + return !mainDependencies.contains(t); + }).collect(Collectors.toSet()); + + addModuleDependenciesToClasspath(classpath, mainDependencies, false); + addModuleDependenciesToClasspath(classpath, testDependencies, true); + + // dedup the classpath which which has the same path, this happens when two dependency points to the same jar but with different name. + List dedupedClasspaths = classpath.stream() + .collect(Collectors.toMap(IClasspathEntry::getPath, Function.identity(), (a, b) -> a)) + .values() + .stream() + .collect(Collectors.toList()); + + javaProject.setRawClasspath(dedupedClasspaths.toArray(IClasspathEntry[]::new), javaProject.getOutputLocation(), monitor); + // refresh to let JDT be aware of the output folders. + project.refreshLocal(IResource.DEPTH_INFINITE, monitor); + + if (updateProjectDependency) { + addProjectDependencies(project, monitor); + } + } + + public void addProjectDependencies(IProject project, IProgressMonitor monitor) throws JavaModelException { + BuildServer buildServer = BuildServerAdapter.getBuildServer(); + if (buildServer == null) { + return; + } + + IJavaProject javaProject = JavaCore.create(project); + List classpath = new LinkedList<>(Arrays.asList(javaProject.getRawClasspath())); + List buildTargets = BuildServerTargetsManager.getInstance().getBuildTargets(project); + if (buildTargets == null) { + JavaLanguageServerPlugin.logError("Cannot find build targets for project " + project.getName()); + return; + } + Set projectDependencies = new HashSet<>(); + for (BuildTarget buildTarget : buildTargets) { + projectDependencies.addAll(buildTarget.getDependencies()); + } + + addProjectDependenciesToClasspath(classpath, projectDependencies); + javaProject.setRawClasspath(classpath.toArray(IClasspathEntry[]::new), javaProject.getOutputLocation(), monitor); + } + + private JvmBuildTargetExt getHighestJavaVersion(List buildTargets) throws CoreException { + List jvmTargets = new ArrayList<>(); + for (BuildTarget buildTarget : buildTargets) { + JvmBuildTargetExt model = JSONUtility.toModel(buildTarget.getData(), JvmBuildTargetExt.class); + if (StringUtils.isBlank(model.getTargetBytecodeVersion()) || StringUtils.isBlank(model.getJavaHome())) { + continue; + } + jvmTargets.add(JSONUtility.toModel(buildTarget.getData(), JvmBuildTargetExt.class)); + } + + if (jvmTargets.isEmpty()) { + throw new CoreException(new Status(IStatus.ERROR, BuildServerAdapter.PLUGIN_ID, "Failed to get Java version.")); + } + + jvmTargets.sort((j1, j2) -> { + String[] components1 = j1.getTargetBytecodeVersion().split("\\."); + String[] components2 = j2.getTargetBytecodeVersion().split("\\."); + + int length = Math.max(components1.length, components2.length); + for (int i = 0; i < length; i++) { + int version1 = i < components1.length ? Integer.parseInt(components1[i]) : 0; + int version2 = i < components2.length ? Integer.parseInt(components2[i]) : 0; + + if (version1 != version2) { + return Integer.compare(version1, version2); + } + } + + return 0; // versions are equal + }); + + return jvmTargets.get(jvmTargets.size() - 1); + } + + @Override + public boolean fileChanged(IResource resource, CHANGE_TYPE changeType, IProgressMonitor monitor) throws CoreException { + if (resource == null || !applies(resource.getProject())) { + return false; + } + return IBuildSupport.super.fileChanged(resource, changeType, monitor) || isBuildFile(resource); + } + + @Override + public boolean isBuildFile(IResource resource) { + if (resource != null && resource.getType() == IResource.FILE && isBuildLikeFileName(resource.getName()) + && ProjectUtils.hasNature(resource.getProject(), BspGradleProjectNature.NATURE_ID)) { + try { + if (!ProjectUtils.isJavaProject(resource.getProject())) { + return true; + } + IJavaProject javaProject = JavaCore.create(resource.getProject()); + IPath outputLocation = javaProject.getOutputLocation(); + return outputLocation == null || !outputLocation.isPrefixOf(resource.getFullPath()); + } catch (JavaModelException e) { + JavaLanguageServerPlugin.logException(e.getMessage(), e); + } + } + return false; + } + + @Override + public boolean isBuildLikeFileName(String fileName) { + return GRADLE_FILE_EXT.matcher(fileName).matches() || fileName.equals(GRADLE_PROPERTIES); + } + + /** + * Get the Eclipse compatible Java version string. + *
+     * See: 
+       org.eclipse.buildship.core.internal.util.gradle.JavaVersionUtil
+     * 
+ */ + String getEclipseCompatibleVersion(String javaVersion) { + if ("1.9".equals(javaVersion)) { + return "9"; + } else if ("1.10".equals(javaVersion)) { + return "10"; + } + + return javaVersion; + } + + private void addProjectDependenciesToClasspath(List classpath, Set projectDependencies) { + for (BuildTargetIdentifier dependency : projectDependencies) { + String uriString = dependency.getUri(); + URI uri = null; + // TODO: extract to util + try { + uri = new URI(uriString); + uri = new URI(uri.getScheme(), uri.getHost(), uri.getPath(), null, uri.getFragment()); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + if (uri != null) { + IProject dependencyProject = ProjectUtils.getProjectFromUri(uri.toString()); + if (dependencyProject != null) { + classpath.add(JavaCore.newProjectEntry(dependencyProject.getFullPath())); + } + } + } + } + + private void addModuleDependenciesToClasspath(List classpath, Set modules, boolean isTest) { + for (MavenDependencyModule mainDependency : modules) { + File artifact = null; + File sourceArtifact = null; + for (MavenDependencyModuleArtifact a : mainDependency.getArtifacts()) { + try { + if (a.getClassifier() == null) { + artifact = new File(new URI(a.getUri())); + } else if ("sources".equals(a.getClassifier())) { + sourceArtifact = new File(new URI(a.getUri())); + } + } catch (URISyntaxException e) { + e.printStackTrace(); + } + } + + List attributes = new LinkedList<>(); + if (isTest) { + attributes.add(testAttribute); + } + if (!artifact.exists()) { + attributes.add(optionalAttribute); + } + + classpath.add(JavaCore.newLibraryEntry( + new org.eclipse.core.runtime.Path(artifact.getAbsolutePath()), + sourceArtifact == null ? null : new org.eclipse.core.runtime.Path(sourceArtifact.getAbsolutePath()), + null, + ClasspathEntry.NO_ACCESS_RULES, + attributes.size() == 0 ? ClasspathEntry.NO_EXTRA_ATTRIBUTES : attributes.toArray(new IClasspathAttribute[attributes.size()]), + false + )); + } + } + + private void addJavaNature(IProject project, IProgressMonitor monitor) throws CoreException { + SubMonitor progress = SubMonitor.convert(monitor, 1); + // get the description + IProjectDescription description = project.getDescription(); + + // abort if the project already has the nature applied or the nature is not defined + List currentNatureIds = Arrays.asList(description.getNatureIds()); + if (currentNatureIds.contains(JavaCore.NATURE_ID)) { + return; + } + + // add the nature to the project + List newIds = new LinkedList<>(); + newIds.addAll(currentNatureIds); + newIds.add(0, JavaCore.NATURE_ID); + description.setNatureIds(newIds.toArray(new String[newIds.size()])); + + // save the updated description + project.setDescription(description, IResource.AVOID_NATURE_CONFIG, progress.newChild(1)); + } +} + diff --git a/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BspGradleProjectImporter.java b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BspGradleProjectImporter.java new file mode 100644 index 00000000..df468fd2 --- /dev/null +++ b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BspGradleProjectImporter.java @@ -0,0 +1,285 @@ +package com.microsoft.buildserver.adapter; + +import static org.eclipse.jdt.ls.core.internal.handlers.MapFlattener.getValue; + +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; + +import org.eclipse.core.resources.ICommand; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.ls.core.internal.AbstractProjectImporter; +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.jdt.ls.core.internal.ProjectUtils; +import org.eclipse.jdt.ls.core.internal.managers.BasicFileDetector; +import org.eclipse.jdt.ls.core.internal.preferences.Preferences; + +import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.ExecuteCommandParams; +import org.eclipse.lsp4j.MessageType; + +import com.microsoft.buildserver.adapter.builder.BspBuilder; + +import ch.epfl.scala.bsp4j.BuildClientCapabilities; +import ch.epfl.scala.bsp4j.BuildServer; +import ch.epfl.scala.bsp4j.BuildTarget; +import ch.epfl.scala.bsp4j.InitializeBuildParams; +import ch.epfl.scala.bsp4j.InitializeBuildResult; +import ch.epfl.scala.bsp4j.WorkspaceBuildTargetsResult; + +@SuppressWarnings("restriction") +public class BspGradleProjectImporter extends AbstractProjectImporter { + + private static final String JAVA_BUILD_SERVER_GRADLE_ENABLED = "java.buildServer.gradle.enabled"; + public static final String BUILD_GRADLE_DESCRIPTOR = "build.gradle"; + public static final String BUILD_GRADLE_KTS_DESCRIPTOR = "build.gradle.kts"; + public static final String SETTINGS_GRADLE_DESCRIPTOR = "settings.gradle"; + public static final String SETTINGS_GRADLE_KTS_DESCRIPTOR = "settings.gradle.kts"; + + /* (non-Javadoc) + * @see org.eclipse.jdt.ls.core.internal.managers.IProjectImporter#applies(org.eclipse.core.runtime.IProgressMonitor) + */ + @Override + public boolean applies(IProgressMonitor monitor) throws CoreException { + if (rootFolder == null) { + return false; + } + + Preferences preferences = getPreferences(); + if (!preferences.isImportGradleEnabled()) { + return false; + } + + Object bspImporterEnabled = getValue(preferences.asMap(), JAVA_BUILD_SERVER_GRADLE_ENABLED); + if (bspImporterEnabled == null) { + return false; + } + + if (!(boolean) bspImporterEnabled) { + return false; + } + + if (directories == null) { + BasicFileDetector gradleDetector = new BasicFileDetector(rootFolder.toPath(), BUILD_GRADLE_DESCRIPTOR, + SETTINGS_GRADLE_DESCRIPTOR, BUILD_GRADLE_KTS_DESCRIPTOR, SETTINGS_GRADLE_KTS_DESCRIPTOR) + .includeNested(false) + .addExclusions("**/build") //default gradle build dir + .addExclusions("**/bin"); + directories = gradleDetector.scan(monitor); + } + + for (Path directory : directories) { + IProject project = ProjectUtils.getProjectFromUri(directory.toUri().toString()); + if (project == null) { + return true; + } + + if (BspUtils.isBspGradleProject(project)) { + return true; + } + } + return false; + } + + /* (non-Javadoc) + * @see org.eclipse.jdt.ls.core.internal.managers.IProjectImporter#importToWorkspace(org.eclipse.core.runtime.IProgressMonitor) + */ + @Override + public void importToWorkspace(IProgressMonitor monitor) throws CoreException { + BuildServer buildServer = BuildServerAdapter.getBuildServer(); + if (buildServer == null) { + return; + } + + if (BuildServerTargetsManager.getInstance().getInitializeBuildResult(rootFolder) == null) { + InitializeBuildParams params = new InitializeBuildParams( + Constant.CLIENT_NAME, + Constant.CLIENT_VERSION, + Constant.BSP_VERSION, + rootFolder.toPath().toUri().toString(), + new BuildClientCapabilities(java.util.Collections.singletonList("java")) + ); + BuildServerPreferences data = getBuildServerPreferences(); + params.setData(data); + try { + InitializeBuildResult initializeResult = buildServer.buildInitialize(params).join(); + buildServer.onBuildInitialized(); + BuildServerTargetsManager.getInstance().setInitializeBuildResult(rootFolder, initializeResult); + } catch (Exception e) { + JavaLanguageServerPlugin.getInstance().getClientConnection().sendActionableNotification( + MessageType.Error, + "Failed to initialize the build server: " + e.getMessage(), + null, + Arrays.asList(new Command("Open Log", "java.buildServer.openLogs")) + ); + } + } + + WorkspaceBuildTargetsResult workspaceBuildTargetsResult = buildServer.workspaceBuildTargets().join(); + List buildTargets = workspaceBuildTargetsResult.getTargets(); + + List projects = createProjectsIfNotExist(buildTargets, monitor); + if (projects.isEmpty()) { + return; + } + + // store the digest for the imported gradle projects. + ProjectUtils.getGradleProjects().forEach(p -> { + File buildFile = p.getFile(BUILD_GRADLE_DESCRIPTOR).getLocation().toFile(); + File settingsFile = p.getFile(SETTINGS_GRADLE_DESCRIPTOR).getLocation().toFile(); + File buildKtsFile = p.getFile(BUILD_GRADLE_KTS_DESCRIPTOR).getLocation().toFile(); + File settingsKtsFile = p.getFile(SETTINGS_GRADLE_KTS_DESCRIPTOR).getLocation().toFile(); + try { + if (buildFile.exists()) { + BuildServerAdapter.getDigestStore().updateDigest(buildFile.toPath()); + } else if (buildKtsFile.exists()) { + BuildServerAdapter.getDigestStore().updateDigest(buildKtsFile.toPath()); + } + if (settingsFile.exists()) { + BuildServerAdapter.getDigestStore().updateDigest(settingsFile.toPath()); + } else if (settingsKtsFile.exists()) { + BuildServerAdapter.getDigestStore().updateDigest(settingsKtsFile.toPath()); + } + } catch (CoreException e) { + JavaLanguageServerPlugin.logException("Failed to update digest for gradle build file", e); + } + }); + + BspGradleBuildSupport bs = new BspGradleBuildSupport(); + + + if (!projects.isEmpty()) { + Preferences preferences = getPreferences(); + if (preferences.isAutobuildEnabled()) { + JavaLanguageServerPlugin.getInstance().getClientConnection().sendNotification("_java.buildServer.configAutoBuild", Collections.emptyList()); + } + } + + for (IProject project : projects) { + // separate the classpath update and project dependency update to avoid + // having Java project 'xxx' does not exists. + // TODO: consider to use a better way to handle this: i.e. + // add java nature for all java projects first. + bs.updateClassPath(project, false, monitor); + } + for (IProject project : projects) { + bs.addProjectDependencies(project, monitor); + } + } + + private List createProjectsIfNotExist(List buildTargets, IProgressMonitor monitor) throws CoreException { + List projects = new LinkedList<>(); + Map> buildTargetMap = BspUtils.mapBuildTargetsByUri(buildTargets); + for (Entry> entrySet : buildTargetMap.entrySet()) { + String baseDir = entrySet.getKey(); + if (baseDir == null) { + JavaLanguageServerPlugin.logError("The base directory of the build target is null."); + continue; + } + File projectDirectory; + try { + projectDirectory = new File(new URI(baseDir)); + } catch (URISyntaxException e) { + JavaLanguageServerPlugin.logException(e); + continue; + } + IProject[] allProjects = ProjectUtils.getAllProjects(); + Optional projectOrNull = Arrays.stream(allProjects).filter(p -> { + File loc = p.getLocation().toFile(); + return loc.equals(projectDirectory); + }).findFirst(); + + IProject project; + if (projectOrNull.isPresent()) { + project = projectOrNull.get(); + } else { + String projectName = findFreeProjectName(projectDirectory.getName()); + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IProjectDescription projectDescription = workspace.newProjectDescription(projectName); + projectDescription.setLocation(org.eclipse.core.runtime.Path.fromOSString(projectDirectory.getPath())); + projectDescription.setNatureIds(new String[]{BspGradleProjectNature.NATURE_ID}); + ICommand buildSpec = projectDescription.newCommand(); + buildSpec.setBuilderName(BspBuilder.BUILDER_ID); + projectDescription.setBuildSpec(new ICommand[]{buildSpec}); + + project = workspace.getRoot().getProject(projectName); + project.create(projectDescription, monitor); + + // open the project + project.open(IResource.NONE, monitor); + + } + + if (project == null || !project.isAccessible()) { + continue; + } + + project.refreshLocal(IResource.DEPTH_INFINITE, monitor); + projects.add(project); + BuildServerTargetsManager.getInstance().setBuildTargets(project, entrySet.getValue()); + } + return projects; + } + + @Override + public void reset() { + // do nothing. + } + + private BuildServerPreferences getBuildServerPreferences() { + BuildServerPreferences data = new BuildServerPreferences(); + Preferences jdtlsPreferences = getPreferences(); + data.setGradleArguments(jdtlsPreferences.getGradleArguments()); + data.setGradleHome(jdtlsPreferences.getGradleHome()); + data.setGradleJavaHome(jdtlsPreferences.getGradleJavaHome()); + data.setGradleJvmArguments(jdtlsPreferences.getGradleJvmArguments()); + data.setGradleUserHome(jdtlsPreferences.getGradleUserHome()); + data.setGradleVersion(jdtlsPreferences.getGradleVersion()); + data.setGradleWrapperEnabled(jdtlsPreferences.isGradleWrapperEnabled()); + return data; + } + + private String findFreeProjectName(String baseName) { + IProject project = Arrays.stream(ProjectUtils.getAllProjects()) + .filter(p -> p.getName().equals(baseName)).findFirst().orElse(null); + return project != null ? findFreeProjectName(baseName + "_") : baseName; + } + + private void addJavaNature(IProject project, IProgressMonitor monitor) throws CoreException { + SubMonitor progress = SubMonitor.convert(monitor, 1); + // get the description + IProjectDescription description = project.getDescription(); + + // abort if the project already has the nature applied or the nature is not defined + List currentNatureIds = Arrays.asList(description.getNatureIds()); + if (currentNatureIds.contains(JavaCore.NATURE_ID)) { + return; + } + + // add the nature to the project + List newIds = new LinkedList<>(); + newIds.addAll(currentNatureIds); + newIds.add(0, JavaCore.NATURE_ID); + description.setNatureIds(newIds.toArray(new String[newIds.size()])); + + // save the updated description + project.setDescription(description, IResource.AVOID_NATURE_CONFIG, progress.newChild(1)); + } +} + diff --git a/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BspGradleProjectNature.java b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BspGradleProjectNature.java new file mode 100644 index 00000000..b7108952 --- /dev/null +++ b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BspGradleProjectNature.java @@ -0,0 +1,32 @@ +package com.microsoft.buildserver.adapter; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectNature; +import org.eclipse.core.runtime.CoreException; + +public class BspGradleProjectNature implements IProjectNature { + public static final String NATURE_ID = "com.microsoft.buildserver.adapter.bspGradleProjectNature"; + + private IProject project; + + @Override + public void configure() throws CoreException { + + } + + @Override + public void deconfigure() throws CoreException { + + } + + @Override + public IProject getProject() { + return project; + } + + @Override + public void setProject(IProject project) { + this.project = project; + } +} + diff --git a/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BspUtils.java b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BspUtils.java new file mode 100644 index 00000000..0afae370 --- /dev/null +++ b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BspUtils.java @@ -0,0 +1,29 @@ +package com.microsoft.buildserver.adapter; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.eclipse.core.resources.IProject; +import org.eclipse.jdt.ls.core.internal.ProjectUtils; + +import ch.epfl.scala.bsp4j.BuildTarget; + +public class BspUtils { + private BspUtils() {} + + public static Map> mapBuildTargetsByUri(List buildTargets) { + return buildTargets.stream().collect(Collectors.groupingBy(target -> { + String uri = target.getId().getUri(); + int indexOfQuery = uri.indexOf("?"); + if (indexOfQuery != -1) { + uri = uri.substring(0, indexOfQuery); + } + return uri; + })); + } + + public static boolean isBspGradleProject(IProject project) { + return ProjectUtils.hasNature(project, BspGradleProjectNature.NATURE_ID); + } +} diff --git a/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BuildServerAdapter.java b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BuildServerAdapter.java new file mode 100644 index 00000000..723367a1 --- /dev/null +++ b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BuildServerAdapter.java @@ -0,0 +1,135 @@ +package com.microsoft.buildserver.adapter; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Plugin; +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.jdt.ls.core.internal.managers.DigestStore; +import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.osgi.framework.BundleContext; + +import ch.epfl.scala.bsp4j.BuildServer; + +public class BuildServerAdapter extends Plugin { + + public static final String PLUGIN_ID = "com.microsoft.buildserver.adapter"; + + private static BuildServerAdapter adapterInstance; + + private DigestStore digestStore; + + private BuildServer buildServer; + private BspClient buildClient; + + @Override + public void start(BundleContext context) throws Exception { + BuildServerAdapter.adapterInstance = this; + digestStore = new DigestStore(getStateLocation().toFile()); + } + + @Override + public void stop(BundleContext context) throws Exception { + BuildServerAdapter.adapterInstance = null; + } + + public static BuildServerAdapter getInstance() { + return BuildServerAdapter.adapterInstance; + } + + public static DigestStore getDigestStore() { + return adapterInstance.digestStore; + } + + public static BuildServer getBuildServer() { + if (adapterInstance.buildServer == null) { + String javaExecutablePath = getJavaExecutablePath(); + if (javaExecutablePath == null || javaExecutablePath.isEmpty()) { + JavaLanguageServerPlugin.logError("Failed to get Java executable path."); + return null; + } + + String[] classpaths = getBuildServerRuntimeClasspath(); + if (classpaths.length == 0) { + JavaLanguageServerPlugin.logError("Failed to get required runtime classpaths for build server."); + return null; + } + String storagePath = ResourcesPlugin.getWorkspace().getRoot().getLocation() + .removeLastSegments(2).append("build-server").toFile().getAbsolutePath(); + + ProcessBuilder build = new ProcessBuilder( + javaExecutablePath, + // "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8989", + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.base/java.io=ALL-UNNAMED", + "--add-opens=java.base/java.util=ALL-UNNAMED", + "-DbuildServerStorage=" + storagePath, + "-cp", + String.join(getClasspathSplitor(), classpaths), + "com.microsoft.java.bs.core.JavaBspLauncher" + ); + + try { + Process process = build.start(); + ExecutorService fixedThreadPool = Executors.newCachedThreadPool(); + adapterInstance.buildClient = new BspClient(); + Launcher launcher = new Launcher.Builder() + .setOutput(process.getOutputStream()) + .setInput(process.getInputStream()) + .setLocalService(adapterInstance.buildClient) + .setExecutorService(fixedThreadPool) + .setRemoteInterface(BuildServer.class) + .create(); + + launcher.startListening(); + adapterInstance.buildServer = launcher.getRemoteProxy(); + adapterInstance.buildClient.onConnectWithServer(adapterInstance.buildServer); + } catch (IOException e) { + JavaLanguageServerPlugin.logException("Failed to start build server", e); + return null; + } + } + return adapterInstance.buildServer; + } + + private static String getJavaExecutablePath() { + Optional command = ProcessHandle.current().info().command(); + if (command.isPresent()) { + return command.get(); + } + + return ""; + } + + private static String[] getBuildServerRuntimeClasspath() { + try { + URL fileURL = FileLocator.toFileURL(BuildServerAdapter.class.getResource("/bsp")); + File file = new File(fileURL.getPath()); + return new String[]{ + Paths.get(file.getAbsolutePath(), "server.jar").toString(), + Paths.get(file.getAbsolutePath(), "libs").toString() + Path.SEPARATOR + "*" + }; + } catch (Exception e) { + JavaLanguageServerPlugin.logException("Unable to get build server runtime classpath", e); + return new String[0]; + } + } + + private static String getClasspathSplitor() { + String os = System.getProperty("os.name").toLowerCase(); + + if (os.contains("win")) { + return ";"; + } + + return ":"; // Linux or Mac + } +} diff --git a/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BuildServerPreferences.java b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BuildServerPreferences.java new file mode 100644 index 00000000..ab4bbf8a --- /dev/null +++ b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BuildServerPreferences.java @@ -0,0 +1,78 @@ +package com.microsoft.buildserver.adapter; + +import java.util.Collections; +import java.util.List; + +/** + * The data used in 'build/initialize' request. + */ +public class BuildServerPreferences { + private String gradleJavaHome; + private String gradleVersion; + private String gradleHome; + private String gradleUserHome; + private boolean gradleWrapperEnabled; + private List gradleArguments; + private List gradleJvmArguments; + + public BuildServerPreferences() { + gradleArguments = Collections.emptyList(); + gradleJvmArguments = Collections.emptyList(); + } + + public String getGradleJavaHome() { + return gradleJavaHome; + } + + public void setGradleJavaHome(String gradleJavaHome) { + this.gradleJavaHome = gradleJavaHome; + } + + public String getGradleVersion() { + return gradleVersion; + } + + public void setGradleVersion(String gradleVersion) { + this.gradleVersion = gradleVersion; + } + + public String getGradleHome() { + return gradleHome; + } + + public void setGradleHome(String gradleHome) { + this.gradleHome = gradleHome; + } + + public String getGradleUserHome() { + return gradleUserHome; + } + + public void setGradleUserHome(String gradleUserHome) { + this.gradleUserHome = gradleUserHome; + } + + public boolean isGradleWrapperEnabled() { + return gradleWrapperEnabled; + } + + public void setGradleWrapperEnabled(boolean gradleWrapperEnabled) { + this.gradleWrapperEnabled = gradleWrapperEnabled; + } + + public List getGradleArguments() { + return gradleArguments; + } + + public void setGradleArguments(List gradleArguments) { + this.gradleArguments = gradleArguments; + } + + public List getGradleJvmArguments() { + return gradleJvmArguments; + } + + public void setGradleJvmArguments(List gradleJvmArguments) { + this.gradleJvmArguments = gradleJvmArguments; + } +} diff --git a/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BuildServerTargetsManager.java b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BuildServerTargetsManager.java new file mode 100644 index 00000000..ee9e1f4c --- /dev/null +++ b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/BuildServerTargetsManager.java @@ -0,0 +1,47 @@ +package com.microsoft.buildserver.adapter; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.resources.IProject; + +import ch.epfl.scala.bsp4j.BuildTarget; +import ch.epfl.scala.bsp4j.InitializeBuildResult; + +public class BuildServerTargetsManager { + private BuildServerTargetsManager() { + } + + private static class BuildServerTargetsManagerHolder { + private static final BuildServerTargetsManager INSTANCE = new BuildServerTargetsManager(); + } + + public static BuildServerTargetsManager getInstance() { + return BuildServerTargetsManagerHolder.INSTANCE; + } + + private Map> cache = new HashMap<>(); + private Map initializeBuildResultCache = new HashMap<>(); + + public void reset() { + cache.clear(); + } + + public List getBuildTargets(IProject project) { + return cache.get(project); + } + + public void setBuildTargets(IProject project, List targets) { + cache.put(project, targets); + } + + public InitializeBuildResult getInitializeBuildResult(File path) { + return initializeBuildResultCache.get(path); + } + + public void setInitializeBuildResult(File path, InitializeBuildResult result) { + initializeBuildResultCache.put(path, result); + } +} diff --git a/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/Constant.java b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/Constant.java new file mode 100644 index 00000000..bb64753f --- /dev/null +++ b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/Constant.java @@ -0,0 +1,9 @@ +package com.microsoft.buildserver.adapter; + +public final class Constant { + private Constant() {} + + public static final String CLIENT_NAME = "jdtls"; + public static final String CLIENT_VERSION = "1.0.0"; + public static final String BSP_VERSION = "2.1.0-M4"; +} diff --git a/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/bsp4j/extended/JvmBuildTargetExt.java b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/bsp4j/extended/JvmBuildTargetExt.java new file mode 100644 index 00000000..a7b67941 --- /dev/null +++ b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/bsp4j/extended/JvmBuildTargetExt.java @@ -0,0 +1,61 @@ +package com.microsoft.buildserver.adapter.bsp4j.extended; + +import ch.epfl.scala.bsp4j.JvmBuildTarget; + +public class JvmBuildTargetExt extends JvmBuildTarget { + + String sourceLanguageLevel; + + String targetBytecodeVersion; + + public JvmBuildTargetExt(String javaHome, String javaVersion) { + super(javaHome, javaVersion); + } + + public String getSourceLanguageLevel() { + return sourceLanguageLevel; + } + + public void setSourceLanguageLevel(String sourceLanguageLevel) { + this.sourceLanguageLevel = sourceLanguageLevel; + } + + public String getTargetBytecodeVersion() { + return targetBytecodeVersion; + } + + public void setTargetBytecodeVersion(String targetBytecodeVersion) { + this.targetBytecodeVersion = targetBytecodeVersion; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((sourceLanguageLevel == null) ? 0 : sourceLanguageLevel.hashCode()); + result = prime * result + ((targetBytecodeVersion == null) ? 0 : targetBytecodeVersion.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + JvmBuildTargetExt other = (JvmBuildTargetExt) obj; + if (sourceLanguageLevel == null) { + if (other.sourceLanguageLevel != null) + return false; + } else if (!sourceLanguageLevel.equals(other.sourceLanguageLevel)) + return false; + if (targetBytecodeVersion == null) { + if (other.targetBytecodeVersion != null) + return false; + } else if (!targetBytecodeVersion.equals(other.targetBytecodeVersion)) + return false; + return true; + } +} diff --git a/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/builder/BspBuilder.java b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/builder/BspBuilder.java new file mode 100644 index 00000000..526fd5d9 --- /dev/null +++ b/jdtls.ext/com.microsoft.buildserver.adapter/src/com/microsoft/buildserver/adapter/builder/BspBuilder.java @@ -0,0 +1,65 @@ +package com.microsoft.buildserver.adapter.builder; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; + +import com.microsoft.buildserver.adapter.BuildServerAdapter; +import com.microsoft.buildserver.adapter.BuildServerTargetsManager; + +import ch.epfl.scala.bsp4j.BuildServer; +import ch.epfl.scala.bsp4j.BuildTarget; +import ch.epfl.scala.bsp4j.BuildTargetIdentifier; +import ch.epfl.scala.bsp4j.CleanCacheParams; +import ch.epfl.scala.bsp4j.CompileParams; +import ch.epfl.scala.bsp4j.CompileResult; +import ch.epfl.scala.bsp4j.StatusCode; + +/** + * BspBuilder + */ +public class BspBuilder extends IncrementalProjectBuilder { + + public static final String BUILDER_ID = "com.microsoft.buildserver.adapter.builder.bspBuilder"; + + @Override + protected IProject[] build(int kind, Map args, IProgressMonitor monitor) throws CoreException { + // TODO: how to avoid build from the root project when the root project does not contain java files. + // building root project will cause all sub-modules being built. + BuildServer buildServer = BuildServerAdapter.getBuildServer(); + if (buildServer != null) { + List targets = BuildServerTargetsManager.getInstance().getBuildTargets(this.getProject()); + List ids = targets.stream().map(BuildTarget::getId).collect(Collectors.toList()); + if (ids != null) { + if (requiresClean(kind)) { + buildServer.buildTargetCleanCache(new CleanCacheParams(ids)).join(); + } + + if (requiresBuild(kind)) { + CompileResult result = buildServer.buildTargetCompile(new CompileParams(ids)).join(); + if (Objects.equals(result.getStatusCode(), StatusCode.ERROR)) { + throw new CoreException(new Status(IStatus.ERROR, "testtest", "Build Failed.")); + } + } + } + } + return null; + } + + private boolean requiresClean(int kind) { + // TODO: Currently clean is not supported. + return false; + } + + private boolean requiresBuild(int kind) { + return kind == FULL_BUILD || kind == INCREMENTAL_BUILD; + } +} diff --git a/jdtls.ext/com.microsoft.jdtls.ext.target/com.microsoft.jdtls.ext.tp.target b/jdtls.ext/com.microsoft.jdtls.ext.target/com.microsoft.jdtls.ext.tp.target index ad9d88b6..db61f7c9 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.target/com.microsoft.jdtls.ext.tp.target +++ b/jdtls.ext/com.microsoft.jdtls.ext.target/com.microsoft.jdtls.ext.tp.target @@ -25,8 +25,12 @@ - + + + + + diff --git a/jdtls.ext/pom.xml b/jdtls.ext/pom.xml index 09874c64..4cfa8270 100644 --- a/jdtls.ext/pom.xml +++ b/jdtls.ext/pom.xml @@ -24,6 +24,7 @@ com.microsoft.jdtls.ext.core com.microsoft.jdtls.ext.target + com.microsoft.buildserver.adapter @@ -107,6 +108,27 @@ + + org.apache.maven.plugins + maven-dependency-plugin + + + + get-libs + + copy + + validate + + + + false + ${basedir}/lib/ + + true + + diff --git a/language-support/gradle-output/GradleOutput.tmLanguage.json b/language-support/gradle-output/GradleOutput.tmLanguage.json new file mode 100644 index 00000000..e872d71f --- /dev/null +++ b/language-support/gradle-output/GradleOutput.tmLanguage.json @@ -0,0 +1,35 @@ +{ + "name": "Gradle Output", + "scopeName": "source.gradle-output", + "fileTypes": ["gradle-output"], + "patterns": [ + { + "name": "FAILURE", + "match": "(^FAILURE: .+$)", + "captures": { + "1": {"name": "invalid.illegal.failure"} + } + }, + { + "name": "error", + "match": "(^\\d+\\serror$)", + "captures": { + "1": {"name": "invalid.illegal.error"} + } + }, + { + "name": "error.description", + "match": "(error:\\s.+$)", + "captures": { + "1": {"name": "invalid.illegal.error.description"} + } + }, + { + "name": "BUILD FAILED", + "match": "(^BUILD FAILED)", + "captures": { + "1": {"name": "invalid.illegal.bold.buildFailed"} + } + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 1502dbdc..090b95a0 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,8 @@ "main": "./main.js", "contributes": { "javaExtensions": [ - "./server/com.microsoft.jdtls.ext.core-0.23.0.jar" + "./server/com.microsoft.jdtls.ext.core-0.23.0.jar", + "./server/com.microsoft.buildserver.adapter-0.23.0.jar" ], "commands": [ { @@ -223,6 +224,11 @@ "command": "java.view.package.renameFile", "title": "%contributes.commands.java.view.package.renameFile%", "category": "Java" + }, + { + "command": "java.buildServer.openLogs", + "title": "Open Build Server Log", + "category": "Java" } ], "configuration": { @@ -280,6 +286,11 @@ "type": "boolean", "description": "%configuration.java.project.explorer.showNonJavaResources%", "default": true + }, + "java.buildServer.gradle.enabled": { + "type": "boolean", + "description": "[Experimental] Use build server to synchronize Gradle project when enabled.", + "default": false } } }, @@ -862,6 +873,18 @@ } } } + ], + "languages": [ + { + "id": "gradle-output" + } + ], + "grammars": [ + { + "language": "gradle-output", + "scopeName": "source.gradle-output", + "path": "./language-support/gradle-output/GradleOutput.tmLanguage.json" + } ] }, "scripts": { @@ -870,6 +893,7 @@ "test": "tsc -p . && node ./dist/test/index.js", "test-ui": "tsc -p . && node ./dist/test/ui/index.js", "build-server": "node scripts/buildJdtlsExt.js", + "build-bsp": "node scripts/buildBuildServer.js", "vscode:prepublish": "tsc -p ./ && webpack --mode production", "tslint": "tslint -t verbose --project tsconfig.json" }, diff --git a/scripts/buildBuildServer.js b/scripts/buildBuildServer.js new file mode 100644 index 00000000..21e72ef9 --- /dev/null +++ b/scripts/buildBuildServer.js @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +const cp = require('child_process'); +const fse = require('fs-extra'); +const path = require('path'); + +const server_dir = path.resolve('build-server-for-java'); +if (!fse.existsSync(server_dir)) { + cp.execSync('git clone https://github.com/microsoft/build-server-for-java.git', {stdio:[0,1,2]} ); +} + +cp.execSync(gradlew() + ' build -x test', {cwd:server_dir, stdio:[0,1,2]} ); +cp.execSync(gradlew() + ' copyRuntimeLibs', {cwd:server_dir, stdio:[0,1,2]} ); + +copy(path.join(server_dir, 'server/build/libs'), path.resolve('jdtls.ext/com.microsoft.buildserver.adapter/bsp'), (file) => { + return file.endsWith('.jar'); +}); + +copy(path.join(server_dir, 'server/build/runtime-libs'), path.resolve('jdtls.ext/com.microsoft.buildserver.adapter/bsp/libs'), (file) => { + return file.endsWith('.jar'); +}); + +fse.removeSync(server_dir); + +function copy(sourceFolder, targetFolder, fileFilter) { + const jars = fse.readdirSync(sourceFolder).filter(file => fileFilter(file)); + fse.ensureDirSync(targetFolder); + for (const jar of jars) { + fse.copyFileSync(path.join(sourceFolder, jar), path.join(targetFolder, path.basename(jar))); + } +} + +function isWin() { + return /^win/.test(process.platform); +} + +function gradlew() { + return isWin()?"gradlew.bat":"./gradlew"; +} \ No newline at end of file diff --git a/scripts/buildJdtlsExt.js b/scripts/buildJdtlsExt.js index c6623dbf..57d9f25e 100644 --- a/scripts/buildJdtlsExt.js +++ b/scripts/buildJdtlsExt.js @@ -11,6 +11,9 @@ cp.execSync(mvnw()+ ' clean package', {cwd:server_dir, stdio:[0,1,2]} ); copy(path.join(server_dir, 'com.microsoft.jdtls.ext.core/target'), path.resolve('server'), (file) => { return /^com.microsoft.jdtls.ext.core.*.jar$/.test(file); }); +copy(path.join(server_dir, 'com.microsoft.buildserver.adapter/target'), path.resolve('server'), (file) => { + return /^com.microsoft.buildserver.adapter.*.jar$/.test(file); +}); function copy(sourceFolder, targetFolder, fileFilter) { const jars = fse.readdirSync(sourceFolder).filter(file => fileFilter(file)); diff --git a/src/bsp/GradleOutputLinkProvider.ts b/src/bsp/GradleOutputLinkProvider.ts new file mode 100644 index 00000000..756485d6 --- /dev/null +++ b/src/bsp/GradleOutputLinkProvider.ts @@ -0,0 +1,26 @@ +import {DocumentLink, DocumentLinkProvider, ProviderResult, Range, TextDocument, Uri } from "vscode"; + +export class GradleOutputLinkProvider implements DocumentLinkProvider { + provideDocumentLinks(document: TextDocument): ProviderResult { + const links: DocumentLink[] = []; + const content = document.getText(); + let searchPosition = 0; + const lines = content.split(/\r?\n/g); + for (const line of lines) { + const match = line.match(/(.*\.java):(\d+)/); + if (match) { + const startOffset = content.indexOf(match[0], searchPosition); + const start = document.positionAt(startOffset); + const endOffset = startOffset + match[0].length; + const end = document.positionAt(endOffset); + searchPosition += endOffset; + + const file = match[1]; + const line = parseInt(match[2]); + const uri = Uri.file(file).with({ fragment: `L${line}` }); + links.push(new DocumentLink(new Range(start, end), uri)); + } + } + return links; + } +} diff --git a/src/bsp/bspController.ts b/src/bsp/bspController.ts new file mode 100644 index 00000000..8e01d606 --- /dev/null +++ b/src/bsp/bspController.ts @@ -0,0 +1,53 @@ +import { ConfigurationTarget, Disposable, ExtensionContext, OutputChannel, Uri, commands, languages, window, workspace } from "vscode"; +import { GradleOutputLinkProvider } from "./GradleOutputLinkProvider"; +import * as path from "path"; +import * as fse from "fs-extra"; + +export class BspController implements Disposable { + + private disposable: Disposable; + private bsOutputChannel: OutputChannel; + + public constructor(public readonly context: ExtensionContext) { + this.bsOutputChannel = window.createOutputChannel("Build Output", "gradle-output"); + this.disposable = Disposable.from( + commands.registerCommand("java.buildServer.openLogs", async () => { + const storagePath: string | undefined = context.storageUri?.fsPath; + if (storagePath) { + const logFile = path.join(storagePath, "..", "build-server", "bs.log"); + if (await fse.pathExists(logFile)) { + await window.showTextDocument(Uri.file(logFile)); + } else { + window.showErrorMessage("Failed to find build server log file."); + } + } + }), + commands.registerCommand("_java.buildServer.gradle.buildStart", (msg: string) => { + this.bsOutputChannel.appendLine(`> Build started at ${ new Date().toLocaleString()} <\n`); + this.bsOutputChannel.appendLine(msg); + }), + commands.registerCommand("_java.buildServer.gradle.buildProgress", (msg: string) => { + this.bsOutputChannel.appendLine(msg); + }), + commands.registerCommand("_java.buildServer.gradle.buildComplete", (msg: string) => { + if (msg) { + this.bsOutputChannel.appendLine(`\n${msg}`); + this.bsOutputChannel.appendLine('------\n'); + if (msg.includes("BUILD FAILED in")) { + this.bsOutputChannel.show(true); + } + } + }), + commands.registerCommand("_java.buildServer.configAutoBuild", async () => { + await workspace.getConfiguration("java").update("autobuild.enabled", false, ConfigurationTarget.Workspace); + window.showInformationMessage("Auto build is turned off to get the best experience. You can reset it later in Settings."); + }), + this.bsOutputChannel, + languages.registerDocumentLinkProvider({ language: "gradle-output", scheme: 'output' }, new GradleOutputLinkProvider()), + ); + } + + public dispose() { + this.disposable.dispose(); + } +} diff --git a/src/extension.ts b/src/extension.ts index 4f4ad709..93d2fc3d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -20,6 +20,7 @@ import { DiagnosticProvider } from "./tasks/buildArtifact/migration/DiagnosticPr import { setContextForDeprecatedTasks, updateExportTaskType } from "./tasks/buildArtifact/migration/utils"; import { CodeActionProvider } from "./tasks/buildArtifact/migration/CodeActionProvider"; import { newJavaClass } from "./explorerCommands/new"; +import { BspController } from "./bsp/bspController"; export async function activate(context: ExtensionContext): Promise { contextManager.initialize(context); @@ -41,6 +42,7 @@ async function activateExtension(_operationId: string, context: ExtensionContext context.subscriptions.push(new ProjectController(context)); Settings.initialize(context); context.subscriptions.push(new LibraryController(context)); + context.subscriptions.push(new BspController(context)); context.subscriptions.push(DependencyExplorer.getInstance(context)); context.subscriptions.push(contextManager); context.subscriptions.push(syncHandler);