diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 13b37ad0d..613c7f898 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -42,7 +42,16 @@ jobs: with: distribution: temurin java-version: 17 + - name: install groovy + shell: bash + run: > + curl -s "https://get.sdkman.io" | bash; + source "$HOME/.sdkman/bin/sdkman-init.sh"; + sdk install groovy ${{ matrix.groovy-version }} - name: full test + shell: bash run: >- + source "$HOME/.sdkman/bin/sdkman-init.sh"; + sdk use groovy ${{ matrix.groovy-version }}; ./mvnw ${MAVEN_ARGS} ${MVN_GROOVY_GROUP_ID} ${MVN_GROOVY_VERSION} clean install invoker:install invoker:run diff --git a/src/it/forkMode/invoker.properties b/src/it/forkMode/invoker.properties new file mode 100644 index 000000000..d3182cae9 --- /dev/null +++ b/src/it/forkMode/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals=clean test -e -ntp -Dmaven.plugin.validation=verbose +#invoker.debug = true diff --git a/src/it/forkMode/pom.xml b/src/it/forkMode/pom.xml new file mode 100644 index 000000000..3ef779d03 --- /dev/null +++ b/src/it/forkMode/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + + org.codehaus.gmavenplus + gmavenplus-plugin-it-root + testing + ../pom.xml + + + gmavenplus-plugin-it-forkCompile + testing + GMavenPlus Plugin Fork Compile Test + The simplest compile with fork. + + + + + junit + junit + + + @groovyGroupId@ + groovy + + + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + true + true + + + + + compile + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + + + diff --git a/src/it/forkMode/src/main/groovy/org/codehaus/gmavenplus/SomeClass.groovy b/src/it/forkMode/src/main/groovy/org/codehaus/gmavenplus/SomeClass.groovy new file mode 100644 index 000000000..6cc5dc909 --- /dev/null +++ b/src/it/forkMode/src/main/groovy/org/codehaus/gmavenplus/SomeClass.groovy @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.codehaus.gmavenplus + + +class SomeClass { + + void someMethod(String param1, String param2) {} + +} diff --git a/src/it/forkMode/src/test/java/org/codehaus/gmavenplus/SomeClassTest.java b/src/it/forkMode/src/test/java/org/codehaus/gmavenplus/SomeClassTest.java new file mode 100644 index 000000000..291397e33 --- /dev/null +++ b/src/it/forkMode/src/test/java/org/codehaus/gmavenplus/SomeClassTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2011 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.codehaus.gmavenplus; + +import groovy.lang.GroovySystem; +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.List; + + +public class SomeClassTest { + + @Test + public void testSomeMethod() throws NoSuchMethodException { + Method method = SomeClass.class.getDeclaredMethod("someMethod", String.class, String.class); + List parameterNames = Arrays.asList(method.getParameters()); + + Assert.assertEquals(2, parameterNames.size()); + + if (GroovySystem.getVersion().startsWith("1.5") + || GroovySystem.getVersion().startsWith("1.6") + || GroovySystem.getVersion().startsWith("1.7") + || GroovySystem.getVersion().startsWith("1.8") + || GroovySystem.getVersion().startsWith("1.9") + || GroovySystem.getVersion().startsWith("2.0") + || GroovySystem.getVersion().startsWith("2.1") + || GroovySystem.getVersion().startsWith("2.2") + || GroovySystem.getVersion().startsWith("2.3") + || GroovySystem.getVersion().startsWith("2.4")) { + Assert.assertEquals("arg0", parameterNames.get(0).getName()); + Assert.assertEquals("arg1", parameterNames.get(1).getName()); + } else { + Assert.assertEquals("param1", parameterNames.get(0).getName()); + Assert.assertEquals("param2", parameterNames.get(1).getName()); + } + } + +} diff --git a/src/main/java/org/codehaus/gmavenplus/mojo/AbstractCompileMojo.java b/src/main/java/org/codehaus/gmavenplus/mojo/AbstractCompileMojo.java index 8c44ab563..a7fff635a 100644 --- a/src/main/java/org/codehaus/gmavenplus/mojo/AbstractCompileMojo.java +++ b/src/main/java/org/codehaus/gmavenplus/mojo/AbstractCompileMojo.java @@ -16,6 +16,16 @@ package org.codehaus.gmavenplus.mojo; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Locale; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.stream.Collectors; +import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Parameter; import org.codehaus.gmavenplus.model.IncludeClasspath; import org.codehaus.gmavenplus.model.internal.Version; @@ -29,7 +39,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.codehaus.gmavenplus.mojo.GroovycLogger.LogTarget; +import static java.util.stream.Collectors.joining; import static org.codehaus.gmavenplus.util.ReflectionUtils.findConstructor; import static org.codehaus.gmavenplus.util.ReflectionUtils.findMethod; import static org.codehaus.gmavenplus.util.ReflectionUtils.invokeConstructor; @@ -312,6 +324,15 @@ public abstract class AbstractCompileMojo extends AbstractGroovySourcesMojo { @Parameter(defaultValue = "false") protected boolean previewFeatures; + /** + * Whether to run the compiler using {@code groovyc} in a separate process. + *

+ * {@code groovyc} will be search in {@code GROOVY_HOME/bin} first and then on the {@code PATH}. + * If no executable was found, the compilation fails. + */ + @Parameter(property = "groovy.fork", defaultValue = "false") + protected boolean fork; + /** * Performs compilation of compile mojos. * @@ -323,15 +344,196 @@ public abstract class AbstractCompileMojo extends AbstractGroovySourcesMojo { * @throws IllegalAccessException when a method needed for compilation cannot be accessed * @throws InvocationTargetException when a reflection invocation needed for compilation cannot be completed * @throws MalformedURLException when a classpath element provides a malformed URL + * @throws MojoExecutionException in case the mojo execution breaks with another reason. */ @SuppressWarnings({"rawtypes"}) protected synchronized void doCompile(final Set sources, final List classpath, final File compileOutputDirectory) - throws ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException, MalformedURLException { + throws ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException, MalformedURLException, MojoExecutionException { if (sources == null || sources.isEmpty()) { getLog().info("No sources specified for compilation. Skipping."); return; } + if (this.fork) { + doCompileProcess(sources, classpath, compileOutputDirectory); + return; + } + + doCompileLibrary(sources, classpath, compileOutputDirectory); + } + + private void doCompileProcess(Set sources, List classpath, File compileOutputDirectory) + throws MojoExecutionException { + Path groovyc = getGroovyc(); + + List args = new ArrayList<>(); + args.add(groovyc.toAbsolutePath().toString()); + final String delimitedCp = getClassPathString(classpath); + args.add("--classpath="+delimitedCp); + + if (this.sourceEncoding != null && !this.sourceEncoding.trim().isEmpty()) { + args.add("--encoding=" + this.sourceEncoding); + } + + if (this.parameters) { + args.add("--parameters"); + } + + if (this.previewFeatures) { + args.add("--enable-preview"); + } + + if (this.targetBytecode != null) { + args.add("-Jtarget=" + this.targetBytecode); + } + + if (this.debug) { + getLog().warn("Option 'debug' is requested but not supported yet with fork=true."); + } + + if (!this.invokeDynamic) { + getLog().warn("Option 'invokeDynamic=false' is requested but not supported yet with fork=true."); + } + + if (!this.skipBytecodeCheck) { + getLog().warn("Option 'skipBytecodeCheck' is requested but not supported yet with fork=true."); + } + + if (this.warningLevel != 1) { + getLog().warn("Option 'warningLevel' is requested but not supported yet with fork=true."); + } + + if (this.tolerance != 0) { + getLog().warn("Option 'tolerance' is requested but not supported yet with fork=true."); + } + + if (this.parallelParsing != null) { + getLog().warn("Option 'parallelParsing' is requested but not supported yet with fork=true."); + } + + if (this.includeClasspath != IncludeClasspath.PROJECT_ONLY) { + getLog().warn("Option 'includeClasspath' is requested but not supported yet with fork=true."); + } + + // missing: + // this.configScript (available as --configscript=) + // this.verbose + // as well as: + // --compile-static + // --type-checked + // --temp= + + final String compileMessage = String.format( + Locale.ROOT, + "Compiling %d source files with groovyc %s to %s", + sources.size(), + args.stream().skip(2).collect(Collectors.toList()), + project.getBasedir().toPath().relativize(compileOutputDirectory.toPath())); + getLog().info(compileMessage); + args.addAll(sources.stream().map(File::getAbsolutePath).collect(Collectors.toList())); + + final ProcessBuilder processBuilder = new ProcessBuilder() + .directory(compileOutputDirectory) + .command(args) + .inheritIO() + ; + + getLog().debug("Running groovyc via: " + args); + + try { + final Path compileOutputDirectoryPath = compileOutputDirectory.toPath(); + if (!Files.exists(compileOutputDirectoryPath)) { + Files.createDirectories(compileOutputDirectoryPath); + } + if (!Files.isDirectory(compileOutputDirectoryPath)) { + throw new MojoExecutionException( + "Target directory [" + compileOutputDirectoryPath + "] is not a directory."); + } + final Process groovycProcess = processBuilder.start(); + + final GroovycLogger outputLogger = new GroovycLogger(groovycProcess.getInputStream(), + getLog(), LogTarget.INFO); + final GroovycLogger errorLogger = new GroovycLogger(groovycProcess.getErrorStream(), + getLog(), LogTarget.ERROR); + final Thread outputLoggerThread = new Thread(outputLogger); + outputLoggerThread.start(); + final Thread errorLoggerThread = new Thread(errorLogger); + errorLoggerThread.start(); + + final int groovycRc = groovycProcess.waitFor(); + outputLoggerThread.join(); + errorLoggerThread.join(); + + if (groovycRc != 0) { + throw new MojoExecutionException("Groovy exited with RC=" + groovycRc); + } + } catch (final IOException ioException) { + throw new MojoExecutionException("Error compiling", ioException); + } catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + throw new MojoExecutionException("Compilation interrupted", interruptedException); + } + } + + private static String getClassPathString(List classpath) { + for (Object cp : classpath) { + if (!(cp instanceof String)) { + throw new IllegalArgumentException("Classpath element [" + cp + "] is not a string!"); + } + } + + return classpath.stream() + .map(obj -> (String) obj) + .collect(joining(File.pathSeparator)); + } + + private Path getGroovyc() { + final Optional groovyHomeGroovyc = getGroovyHomeGroovyc(); + + if (groovyHomeGroovyc.isPresent()) { + return groovyHomeGroovyc.orElseThrow(NoSuchElementException::new); + } + + final Optional pathGroovyc = getPathGroovyc(); + + return pathGroovyc.orElseThrow(() -> new IllegalStateException("No groovyc found in GROOVY_HOME or PATH!")); + } + + private Optional getPathGroovyc() { + for (String dirname : System.getenv("PATH").split(File.pathSeparator)) { + final Path groovyc = Paths.get(dirname, "groovyc"); + if (Files.isRegularFile(groovyc) && Files.isExecutable(groovyc)) { + return Optional.of(groovyc); + } + } + + return Optional.empty(); + } + + private Optional getGroovyHomeGroovyc() { + final String groovyHome = System.getenv("GROOVY_HOME"); + + if (groovyHome == null || groovyHome.trim().isEmpty()) { + return Optional.empty(); + } + + final Path groovyHomePath = Paths.get(groovyHome); + + if (!Files.isDirectory(groovyHomePath)) { + return Optional.empty(); + } + + final Path groovyc = groovyHomePath.resolve("bin/groovyc"); + + if (Files.exists(groovyc) && Files.isExecutable(groovyc) && Files.isRegularFile(groovyc)) { + return Optional.of(groovyc); + } + + return Optional.empty(); + } + + private void doCompileLibrary(Set sources, List classpath, File compileOutputDirectory) + throws MalformedURLException, ClassNotFoundException, InvocationTargetException, IllegalAccessException, InstantiationException { setupClassWrangler(classpath, includeClasspath); logPluginClasspath(); diff --git a/src/main/java/org/codehaus/gmavenplus/mojo/GroovycLogger.java b/src/main/java/org/codehaus/gmavenplus/mojo/GroovycLogger.java new file mode 100644 index 000000000..4791ee7b3 --- /dev/null +++ b/src/main/java/org/codehaus/gmavenplus/mojo/GroovycLogger.java @@ -0,0 +1,43 @@ +package org.codehaus.gmavenplus.mojo; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import org.apache.maven.plugin.logging.Log; + +public class GroovycLogger implements Runnable { + + private final InputStream inputStream; + private final Log log; + private final LogTarget level; + + public GroovycLogger(InputStream inputStream, Log log, LogTarget level) { + this.inputStream = inputStream; + this.log = log; + this.level = level; + } + + @Override + public void run() { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(this.inputStream))) { + String line; + while ((line = reader.readLine()) != null) { + System.out.println(line); + if (this.level == LogTarget.ERROR) { + this.log.error(line); + + } else { + this.log.info(line); + } + } + } catch (IOException ioException) { + this.log.error(ioException); + } + } + + public enum LogTarget { + INFO, + ERROR; + } +}