From 4a6906aef1474a97ea2cd8163380fba945ba3faa Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Fri, 9 Aug 2024 08:07:35 +0100 Subject: [PATCH] Maven plugin for dumping dependencies (cross-repo navigation) (#735) Additionally: * Add `build` command which packages CLI in a reasonable location * update SBT * move benchmark tests to check job * Add a new maven workflow * Read classpath entries from possibly any number of *dependencies.txt --- .github/workflows/ci.yml | 65 ++++++++--- build.sbt | 41 +++++++ docs/manual-configuration.md | 33 ++++++ examples/maven-example/pom.xml | 101 ++++++++++++++++++ examples/maven-example/src/main/java/App.java | 13 +++ .../maven-example/src/test/java/AppTest.java | 20 ++++ .../maven/DependencyWriterMojo.java | 99 +++++++++++++++++ .../META-INF/maven/plugin.template.xml | 58 ++++++++++ project/build.properties | 2 +- .../scip_java/buildtools/ClasspathEntry.scala | 35 ++++-- 10 files changed, 442 insertions(+), 25 deletions(-) create mode 100644 examples/maven-example/pom.xml create mode 100644 examples/maven-example/src/main/java/App.java create mode 100644 examples/maven-example/src/test/java/AppTest.java create mode 100644 maven-plugin/src/main/java/com/sourcegraph/maven/DependencyWriterMojo.java create mode 100644 maven-plugin/src/main/resources/META-INF/maven/plugin.template.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c642c4356..5ea69719a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,26 +17,15 @@ jobs: java: [8, 11, 17, 21] steps: - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: distribution: "temurin" cache: "sbt" java-version: ${{ matrix.java }} - - name: Main project tests - run: sbt test - benchmarks-test: - runs-on: ubuntu-latest - name: Benchmark tests - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 - with: - distribution: "temurin" - cache: "sbt" - java-version: 17 - - name: Run sample benchmarks - run: sbt 'bench/Jmh/run -i 1 -f1 -t1 -foe true' + - name: Main project tests + run: sbt test docker_test: runs-on: ${{ matrix.os }} @@ -94,8 +83,8 @@ jobs: steps: - uses: actions/checkout@v2 - run: yarn global add @bazel/bazelisk - - run: sbt cli/pack - - run: echo "$PWD/scip-java/target/pack/bin" >> $GITHUB_PATH + - run: sbt build + - run: echo "$PWD/out/bin" >> $GITHUB_PATH - name: Auto-index scip-java codebase run: | scip-java index --build-tool=bazel --bazel-scip-java-binary=$(which scip-java) @@ -116,4 +105,48 @@ jobs: distribution: "temurin" java-version: 17 cache: "sbt" + - run: sbt checkAll + + - name: Run sample benchmarks + run: sbt 'bench/Jmh/run -i 1 -f1 -t1 -foe true' + + + maven: + runs-on: ubuntu-latest + name: Maven tests + strategy: + fail-fast: false + matrix: + java: [8, 11, 17, 21] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: "temurin" + cache: "sbt" + java-version: ${{ matrix.java }} + + + - run: | + sbt build publishM2 publishLocal dumpScipJavaVersion + echo "SCIP_JAVA_VERSION=$(cat VERSION)" >> $GITHUB_ENV + echo "SCIP_JAVA_CLI=$PWD/out/bin/scip-java" >> $GITHUB_ENV + + - run: | + mvn clean verify -DskipTests -Dscip-java.version=$SCIP_JAVA_VERSION sourcegraph:sourcegraphDependencies + working-directory: examples/maven-example + + - run: $SCIP_JAVA_CLI index-semanticdb target/semanticdb-targetroot + working-directory: examples/maven-example + + - run: | + set -e + grep org.hamcrest target/semanticdb-targetroot/*dependencies.txt + grep $PWD/src/main/java target/semanticdb-targetroot/*dependencies.txt + working-directory: examples/maven-example + + - run: du -h index.scip + working-directory: examples/maven-example + diff --git a/build.sbt b/build.sbt index 0c678b4f5..7549aee0e 100644 --- a/build.sbt +++ b/build.sbt @@ -205,6 +205,7 @@ lazy val scipProto = project lazy val scip = project .in(file("scip-semanticdb")) .settings( + publishMavenStyle := true, moduleName := "scip-semanticdb", javaToolchainVersion := "8", javaOnlySettings, @@ -214,6 +215,36 @@ lazy val scip = project ) .dependsOn(semanticdb, scipProto) +lazy val mavenPlugin = project + .in(file("maven-plugin")) + .settings( + moduleName := "maven-plugin", + javaToolchainVersion := "8", + javaOnlySettings, + libraryDependencies ++= + Seq( + "org.apache.maven" % "maven-plugin-api" % "3.6.3", + "org.apache.maven.plugin-tools" % "maven-plugin-annotations" % "3.6.4" % + Provided, + "org.apache.maven" % "maven-project" % "2.2.1" + ), + Compile / resourceGenerators += + Def.task { + val dir = (Compile / managedResourceDirectories).value.head / + "META-INF" / "maven" + IO.createDirectory(dir) + val file = dir / "plugin.xml" + val template = IO.read( + (Compile / resourceDirectory).value / "META-INF" / "maven" / + "plugin.template.xml" + ) + + IO.write(file, template.replace("@VERSION@", version.value)) + + Seq(file) + } + ) + lazy val cli = project .in(file("scip-java")) .settings( @@ -602,3 +633,13 @@ dumpScipJavaVersion := { IO.write((ThisBuild / baseDirectory).value / "VERSION", versionValue) } + +lazy val build = taskKey[Unit]( + "Build `scip-java` CLI and place it in the out/bin/scip-java. " +) + +build := { + val source = (cli / pack).value + val destination = (ThisBuild / baseDirectory).value / "out" + IO.copyDirectory(source, destination) +} diff --git a/docs/manual-configuration.md b/docs/manual-configuration.md index 3b1c1c033..607448837 100644 --- a/docs/manual-configuration.md +++ b/docs/manual-configuration.md @@ -156,6 +156,9 @@ index.scip: JSON data ## Step 5 (optional): Enable cross-repository navigation +Cross-repository navigation is a feature that allows "goto definition" and "find +references" to show results from multiple repositories. + By default, the `index.scip` file only enables navigation within the local repository. You can optionally enable cross-repository navigation by creating one of the following files in the SemanticDB _targetroot_ directory (the path in @@ -193,5 +196,35 @@ one of the following files in the SemanticDB _targetroot_ directory (the path in your Sourcegraph instance has another repository that defines that symbol, the cross-repository navigation should succeed. +### Maven plugin + +To simplify setting up cross-repo navigation for Maven projects, we provide a +plugin that can dump the project's dependencies in a format that scip-java understands. + +You can either use it directly from commandline: + +``` +$ mvn com.sourcegraph:maven-plugin:@STABLE_VERSION@:sourcegraphDependencies +``` + +Or add it to your build like any other maven plugin: + +```xml + + com.sourcegraph + maven-plugin + @STABLE_VERSION@ + + + + sourcegraphDependencies + + + + +``` + +Which allows you to invoke it by simply running `mvn sourcegraph:sourcegraphDependencies`. + Cross-repository navigation is a feature that allows "goto definition" and "find references" to show results from multiple repositories. diff --git a/examples/maven-example/pom.xml b/examples/maven-example/pom.xml new file mode 100644 index 000000000..b61149a71 --- /dev/null +++ b/examples/maven-example/pom.xml @@ -0,0 +1,101 @@ + + + + 4.0.0 + + com.sourcegraph + example + ${revision} + + example + + http://www.example.com + + + UTF-8 + 1.8 + 1.8 + 1.0.0-SNAPSHOT + + + + + junit + junit + 4.11 + test + + + com.sourcegraph + semanticdb-javac + ${scip-java.version} + + + + + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + -Xplugin:semanticdb -sourceroot:${session.executionRootDirectory} -targetroot:${session.executionRootDirectory}/target/semanticdb-targetroot + + + + + maven-surefire-plugin + 2.22.1 + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + com.sourcegraph + maven-plugin + ${scip-java.version} + + + + sourcegraphDependencies + + + + + + + + + + + diff --git a/examples/maven-example/src/main/java/App.java b/examples/maven-example/src/main/java/App.java new file mode 100644 index 000000000..b8e9695d7 --- /dev/null +++ b/examples/maven-example/src/main/java/App.java @@ -0,0 +1,13 @@ +package test; + +/** + * Hello world! + * + */ +public class App +{ + public static void main( String[] args ) + { + System.out.println( "Hello World!" ); + } +} diff --git a/examples/maven-example/src/test/java/AppTest.java b/examples/maven-example/src/test/java/AppTest.java new file mode 100644 index 000000000..fc84e0f6b --- /dev/null +++ b/examples/maven-example/src/test/java/AppTest.java @@ -0,0 +1,20 @@ +package test; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +/** + * Unit test for simple App. + */ +public class AppTest +{ + /** + * Rigorous Test :-) + */ + @Test + public void shouldAnswerWithTrue() + { + assertTrue( true ); + } +} diff --git a/maven-plugin/src/main/java/com/sourcegraph/maven/DependencyWriterMojo.java b/maven-plugin/src/main/java/com/sourcegraph/maven/DependencyWriterMojo.java new file mode 100644 index 000000000..2b1dbd6bc --- /dev/null +++ b/maven-plugin/src/main/java/com/sourcegraph/maven/DependencyWriterMojo.java @@ -0,0 +1,99 @@ +package com.sourcegraph.maven; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.*; +import org.apache.maven.project.MavenProject; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Set; + +@Mojo( + name = "sourcegraphDependencies", + defaultPhase = LifecyclePhase.COMPILE, + requiresDependencyResolution = ResolutionScope.TEST, + requiresProject = true) +public class DependencyWriterMojo extends AbstractMojo { + @Parameter(defaultValue = "${project}", required = true, readonly = true) + MavenProject project; + + @Parameter( + property = "semanticdb.targetRoot", + defaultValue = "${session.executionRootDirectory}/target/semanticdb-targetroot") + private String targetRoot; + + public void execute() throws MojoExecutionException, MojoFailureException { + String sanitisedProjectId = project.getId().replaceAll("[^0-9_a-zA-Z()%\\-.]", "_"); + Set artifacts = project.getArtifacts(); + StringBuilder builder = new StringBuilder(); + + String groupID = project.getGroupId(); + String artifactID = project.getArtifactId(); + String version = project.getVersion(); + List sourceRoots = project.getCompileSourceRoots(); + + if (groupID == null || artifactID == null) { + getLog() + .warn( + "Failed to extract groupID and artifactID from the project.\n" + + "This will not prevent a SCIP index from being created, but the symbols \n" + + "extracted from this project won't be available for cross-repository navigation,\n" + + "as this project doesn't define any Maven coordinates by which it can be referred back to.\n" + + "See here for more details: https://sourcegraph.github.io/scip-java/docs/manual-configuration.html#step-5-optional-enable-cross-repository-navigation\n"); + } else { + for (Object root : sourceRoots) { + if (root instanceof String) { + String rootString = (String) root; + builder.append( + String.format("%s\t%s\t%s\t%s\n", groupID, artifactID, version, rootString)); + } + } + } + + for (Object dep : artifacts) { + if (dep instanceof Artifact) { + Artifact artifact = (Artifact) dep; + if (artifact.getFile() != null) { + builder.append( + String.format( + "%s\t%s\t%s\t%s\n", + artifact.getGroupId(), + artifact.getArtifactId(), + artifact.getVersion(), + artifact.getFile())); + } else { + getLog() + .warn( + "Dependency " + + summariseArtifact(artifact) + + " does not have a resolved file, so it won't be added to the dependencies.txt"); + } + } + } + + Path dependenciesFile = Paths.get(targetRoot).resolve(sanitisedProjectId + ".dependencies.txt"); + + try { + Files.createDirectories(dependenciesFile.getParent()); + try (BufferedWriter writer = Files.newBufferedWriter(dependenciesFile)) { + writer.write(builder.toString()); + } + } catch (IOException e) { + throw new MojoFailureException("Failed to write dependencies to file " + dependenciesFile, e); + } + + getLog().info("Dependencies were written to " + dependenciesFile.toAbsolutePath()); + } + + private String summariseArtifact(Artifact artifact) { + return String.format( + "%s:%s:%s", artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion()); + } +} diff --git a/maven-plugin/src/main/resources/META-INF/maven/plugin.template.xml b/maven-plugin/src/main/resources/META-INF/maven/plugin.template.xml new file mode 100644 index 000000000..e0cb1c27f --- /dev/null +++ b/maven-plugin/src/main/resources/META-INF/maven/plugin.template.xml @@ -0,0 +1,58 @@ + + + + + + Sourcegraph scip-java Maven plugin + A Maven plugin which exports your project's dependencies in a format scip-java can understand + com.sourcegraph + maven-plugin + @VERSION@ + sourcegraph + false + true + 1.8 + 3.9.5 + + + sourcegraphDependencies + false + true + false + false + false + true + generate-resources + com.sourcegraph.maven.DependencyWriterMojo + java + per-lookup + once-per-session + test + true + + + project + org.apache.maven.project.MavenProject + true + false + The maven project. + + + targetRoot + java.lang.String + false + true + Location where `dependencies.txt` file will be written (should match the Semanticdb targetroot option) + + + + ${project} + ${session.executionRootDirectory}/target/semanticdb-targetroot + + + + + + + + diff --git a/project/build.properties b/project/build.properties index 04267b14a..ee4c672cd 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.9 +sbt.version=1.10.1 diff --git a/scip-java/src/main/scala/com/sourcegraph/scip_java/buildtools/ClasspathEntry.scala b/scip-java/src/main/scala/com/sourcegraph/scip_java/buildtools/ClasspathEntry.scala index b7caebcd1..061e5e7f6 100644 --- a/scip-java/src/main/scala/com/sourcegraph/scip_java/buildtools/ClasspathEntry.scala +++ b/scip-java/src/main/scala/com/sourcegraph/scip_java/buildtools/ClasspathEntry.scala @@ -38,6 +38,9 @@ object ClasspathEntry { * - javacopts.txt: line-separated list of Java compiler options. * - dependencies.txt: line-separated list of dependency information. * + * Note that the targetroot can contain several files with names ending in + * "dependencies.txt" - for example if they come from a multi-module build. + * * @param targetroot * @return */ @@ -46,18 +49,34 @@ object ClasspathEntry { sourceroot: Path ): List[ClasspathEntry] = { val javacopts = targetroot.resolve("javacopts.txt") - val dependencies = targetroot.resolve("dependencies.txt") - if (Files.isRegularFile(dependencies)) { - fromDependencies(dependencies) - } else if (Files.isRegularFile(javacopts)) { + if (Files.isRegularFile(javacopts)) fromJavacopts(javacopts, sourceroot) - } else { - Nil - } + else + discoverDependenciesFromFiles(targetroot) + } + + /** + * Discover all files that end in "dependencies.txt" directly under + * targetroot. There can be many files because we will be writing dependencies + * for multiple projects. + * + * @param targetroot + * @return classpath entries read from the discovered files + */ + private def discoverDependenciesFromFiles( + targetroot: Path + ): List[ClasspathEntry] = { + os.list + .stream(os.Path(targetroot)) + .filter(p => os.isFile(p) && p.last.endsWith("dependencies.txt")) + .map(path => fromDependencies(path.toNIO)) + .toList + .flatten + .distinct } /** - * Parses ClasspathEntry from a "dependencies.txt" file in the targetroot. + * Parses ClasspathEntry from a "dependencies.txt" file * * Every line of the file is a tab separated value with the following columns: * groupId, artifactId, version, path to the jar file OR classes directory