diff --git a/pom.xml b/pom.xml
index abcc0fd..8869f6a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -65,6 +65,7 @@ under the License.
3.0
7
2020-04-12T12:45:04Z
+ @
@@ -145,9 +146,33 @@ under the License.
2.2
test
+
+ org.apache.maven.shared
+ maven-invoker
+ 3.1.0
+ test
+
+
+
+ ${basedir}/src/test/resources
+ true
+
+ **/pom.xml
+ **/settings.xml
+
+
+
+ ${basedir}/src/test/resources
+
+ **/pom.xml
+ **/settings.xml
+
+
+
+
@@ -205,6 +230,30 @@ under the License.
run-its
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ true
+
+
+ ${maven.home}
+ ${https.protocols}
+ ${project.build.testOutputDirectory}/gnupg
+ ${project.build.directory}/local-repo
+ /it/settings.xml
+
+
+
org.apache.maven.plugins
maven-invoker-plugin
diff --git a/src/it/sign-release-without-passphrase/invoker.properties b/src/it/sign-release-without-passphrase/invoker.properties
new file mode 100644
index 0000000..f2a7dfb
--- /dev/null
+++ b/src/it/sign-release-without-passphrase/invoker.properties
@@ -0,0 +1,18 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+invoker.buildResult = failure
diff --git a/src/it/sign-release-without-passphrase/pom.xml b/src/it/sign-release-without-passphrase/pom.xml
new file mode 100644
index 0000000..8e279f5
--- /dev/null
+++ b/src/it/sign-release-without-passphrase/pom.xml
@@ -0,0 +1,100 @@
+
+
+
+
+
+ 4.0.0
+
+ org.apache.maven.its.gpg.srwop
+ test
+ 1.0
+ jar
+
+
+ Tests that signing with a missing passphrase in Maven batch mode (non-interactive mode) does not hang.
+
+
+
+ true
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 2.0.2
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ @project.version@
+
+ non-existent
+
+
+
+ sign-artifacts
+
+ sign
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-install-plugin
+ 2.2
+
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 2.1
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 2.2
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.0.4
+
+
+ attach-sources
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.3.1
+
+
+
+
+
diff --git a/src/it/sign-release-without-passphrase/verify.bsh b/src/it/sign-release-without-passphrase/verify.bsh
new file mode 100644
index 0000000..8ff268c
--- /dev/null
+++ b/src/it/sign-release-without-passphrase/verify.bsh
@@ -0,0 +1,38 @@
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+import java.io.*;
+import org.codehaus.plexus.util.FileUtils;
+
+File buildLog = new File( basedir, "build.log" );
+String logContent = FileUtils.fileRead(buildLog);
+
+// assert that the Maven build properly failed and did not time out
+if ( !logContent.contains( "Total time: " ) || !logContent.contains( "Finished at: " ) )
+{
+ throw new Exception( "Maven build did not fail, but timed out" );
+}
+
+// assert that the Maven build failed, because pinentry is not allowed in non-interactive mode
+if ( !logContent.contains( "[GNUPG:] FAILURE sign 67108949" ) )
+{
+ throw new Exception( "Maven build did not fail in consequence of pinentry not being available to GPG" );
+}
+
diff --git a/src/main/java/org/apache/maven/plugins/gpg/GpgSigner.java b/src/main/java/org/apache/maven/plugins/gpg/GpgSigner.java
index 65dd396..3fb0f3e 100644
--- a/src/main/java/org/apache/maven/plugins/gpg/GpgSigner.java
+++ b/src/main/java/org/apache/maven/plugins/gpg/GpgSigner.java
@@ -97,20 +97,25 @@ protected void generateSignatureForFile( File file, File signature )
cmd.createArg().setValue( "--no-use-agent" );
}
}
- else
- {
- cmd.createArg().setValue( "--pinentry-mode" );
- cmd.createArg().setValue( "loopback" );
- }
InputStream in = null;
if ( null != passphrase )
{
- // make --passphrase-fd effective in gpg2
- cmd.createArg().setValue( "--batch" );
+ if ( gpgVersion.isAtLeast( GpgVersion.parse( "2.0" ) ) )
+ {
+ // required for option --passphrase-fd since GPG 2.0
+ cmd.createArg().setValue( "--batch" );
+ }
- cmd.createArg().setValue( "--passphrase-fd" );
+ if ( gpgVersion.isAtLeast( GpgVersion.parse( "2.1" ) ) )
+ {
+ // required for option --passphrase-fd since GPG 2.1
+ cmd.createArg().setValue( "--pinentry-mode" );
+ cmd.createArg().setValue( "loopback" );
+ }
+ // make --passphrase-fd effective in gpg2
+ cmd.createArg().setValue( "--passphrase-fd" );
cmd.createArg().setValue( "0" );
// Prepare the input stream which will be used to pass the passphrase to the executable
@@ -128,9 +133,24 @@ protected void generateSignatureForFile( File file, File signature )
cmd.createArg().setValue( "--detach-sign" );
+ if ( getLog().isDebugEnabled() )
+ {
+ // instruct GPG to write status information to stdout
+ cmd.createArg().setValue( "--status-fd" );
+ cmd.createArg().setValue( "1" );
+ }
+
if ( !isInteractive )
{
+ cmd.createArg().setValue( "--batch" );
cmd.createArg().setValue( "--no-tty" );
+
+ if ( null == passphrase && gpgVersion.isAtLeast( GpgVersion.parse( "2.1" ) ) )
+ {
+ // prevent GPG from spawning input prompts in Maven non-interactive mode
+ cmd.createArg().setValue( "--pinentry-mode" );
+ cmd.createArg().setValue( "error" );
+ }
}
if ( !defaultKeyring )
diff --git a/src/test/java/org/apache/maven/plugins/gpg/it/BuildResult.java b/src/test/java/org/apache/maven/plugins/gpg/it/BuildResult.java
new file mode 100644
index 0000000..3e40566
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/gpg/it/BuildResult.java
@@ -0,0 +1,48 @@
+package org.apache.maven.plugins.gpg.it;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+import java.io.File;
+
+import org.apache.maven.shared.invoker.InvocationResult;
+
+public class BuildResult
+{
+
+ private final File buildLog;
+ private final InvocationResult invocationResult;
+
+ public BuildResult( final File buildLog, final InvocationResult invocationResult )
+ {
+ this.buildLog = buildLog;
+ this.invocationResult = invocationResult;
+ }
+
+ public File getBuildLog()
+ {
+ return buildLog;
+ }
+
+ public InvocationResult getInvocationResult()
+ {
+ return invocationResult;
+ }
+
+}
diff --git a/src/test/java/org/apache/maven/plugins/gpg/it/GpgSignAttachedMojoIT.java b/src/test/java/org/apache/maven/plugins/gpg/it/GpgSignAttachedMojoIT.java
new file mode 100644
index 0000000..ea36ac1
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/gpg/it/GpgSignAttachedMojoIT.java
@@ -0,0 +1,74 @@
+package org.apache.maven.plugins.gpg.it;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
+
+import java.io.File;
+
+import org.apache.maven.shared.invoker.InvocationRequest;
+import org.apache.maven.shared.invoker.InvocationResult;
+import org.codehaus.plexus.util.FileUtils;
+import org.junit.Test;
+
+public class GpgSignAttachedMojoIT
+{
+
+ private final File mavenHome;
+ private final File localRepository;
+ private final File mavenUserSettings;
+ private final File gpgHome;
+
+ public GpgSignAttachedMojoIT() throws Exception
+ {
+ this.mavenHome = new File( System.getProperty( "maven.home" ) );
+ this.localRepository = new File( System.getProperty( "localRepositoryPath" ) );
+ this.mavenUserSettings = InvokerTestUtils.getTestResource( System.getProperty( "settingsFile" ) );
+ this.gpgHome = new File( System.getProperty( "gpg.homedir" ) );
+ }
+
+ @Test
+ public void testInteractiveWithoutPassphrase() throws Exception
+ {
+ // given
+ final File pomFile = InvokerTestUtils.getTestResource( "/it/sign-release-without-passphrase-interactive/pom.xml" );
+ final InvocationRequest request = InvokerTestUtils.createRequest( pomFile, mavenUserSettings, gpgHome );
+
+ // require Maven interactive mode
+ request.setBatchMode( false );
+
+ // when
+ final BuildResult result = InvokerTestUtils.executeRequest( request, mavenHome, localRepository );
+
+ final InvocationResult invocationResult = result.getInvocationResult();
+ final String buildLogContent = FileUtils.fileRead( result.getBuildLog() );
+
+ // then
+ assertThat( "Maven execution must fail", invocationResult.getExitCode(), not( 0 ) );
+ assertThat(
+ "Maven execution failed because no pinentry program is available",
+ buildLogContent,
+ containsString( "[GNUPG:] FAILURE sign 67108949" )
+ );
+ }
+
+}
diff --git a/src/test/java/org/apache/maven/plugins/gpg/it/InvokerTestUtils.java b/src/test/java/org/apache/maven/plugins/gpg/it/InvokerTestUtils.java
new file mode 100644
index 0000000..3dd041d
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/gpg/it/InvokerTestUtils.java
@@ -0,0 +1,106 @@
+package org.apache.maven.plugins.gpg.it;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintStream;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Properties;
+
+import org.apache.commons.io.input.NullInputStream;
+import org.apache.maven.shared.invoker.DefaultInvocationRequest;
+import org.apache.maven.shared.invoker.DefaultInvoker;
+import org.apache.maven.shared.invoker.InvocationOutputHandler;
+import org.apache.maven.shared.invoker.InvocationRequest;
+import org.apache.maven.shared.invoker.InvocationResult;
+import org.apache.maven.shared.invoker.Invoker;
+import org.apache.maven.shared.invoker.InvokerLogger;
+import org.apache.maven.shared.invoker.MavenInvocationException;
+import org.apache.maven.shared.invoker.PrintStreamHandler;
+import org.apache.maven.shared.invoker.PrintStreamLogger;
+
+public class InvokerTestUtils
+{
+
+ public static InvocationRequest createRequest( final File pomFile, final File mavenUserSettings, final File gpgHome )
+ {
+ final InvocationRequest request = new DefaultInvocationRequest();
+ request.setUserSettingsFile( mavenUserSettings );
+ request.setShowVersion( true );
+ request.setDebug( true );
+ request.setShowErrors( true );
+ request.setTimeoutInSeconds( 60 ); // safeguard against GPG freezes
+ request.setGoals( Arrays.asList( "clean", "install" ) );
+ request.setPomFile( pomFile );
+
+ final Properties properties = new Properties();
+ request.setProperties( properties );
+
+ // Required for JRE 7 to connect to Maven Central with TLSv1.2
+ final String httpsProtocols = System.getProperty( "https.protocols" );
+ if ( httpsProtocols != null && !httpsProtocols.isEmpty() ) {
+ properties.setProperty( "https.protocols", httpsProtocols );
+ }
+
+ properties.setProperty( "gpg.homedir", gpgHome.getAbsolutePath() );
+
+ return request;
+ }
+
+ public static BuildResult executeRequest( final InvocationRequest request, final File mavenHome, final File localRepository )
+ throws FileNotFoundException, MavenInvocationException
+ {
+ final InvocationResult result;
+
+ final File buildLog = new File( request.getBaseDirectory( request.getPomFile().getParentFile() ), "build.log" );
+ try ( final PrintStream buildLogStream = new PrintStream( buildLog ) )
+ {
+ final InvocationOutputHandler buildLogOutputHandler = new PrintStreamHandler( buildLogStream, false );
+ final InvokerLogger logger = new PrintStreamLogger( buildLogStream, InvokerLogger.DEBUG );
+
+ final Invoker invoker = new DefaultInvoker();
+ invoker.setMavenHome( mavenHome );
+ invoker.setLocalRepositoryDirectory( localRepository );
+ invoker.setInputStream( new NullInputStream( 0 ) );
+ invoker.setOutputHandler( buildLogOutputHandler );
+ invoker.setErrorHandler( buildLogOutputHandler );
+ invoker.setLogger( logger );
+
+ result = invoker.execute( request );
+ }
+
+ return new BuildResult( buildLog, result );
+ }
+
+ public static File getTestResource( final String path ) throws URISyntaxException, FileNotFoundException
+ {
+ final URL resourceUrl = InvokerTestUtils.class.getResource( path );
+ if ( resourceUrl == null )
+ {
+ throw new FileNotFoundException( "Cannot find file " + path );
+ }
+
+ return new File( resourceUrl.toURI() );
+ }
+
+}
diff --git a/src/test/resources/gnupg/gpg-agent.conf b/src/test/resources/gnupg/gpg-agent.conf
new file mode 100644
index 0000000..00efc6f
--- /dev/null
+++ b/src/test/resources/gnupg/gpg-agent.conf
@@ -0,0 +1,5 @@
+# Prevent gpg-agent from caching the passphrase / unlocked key between integration tests
+ignore-cache-for-signing
+
+# Prevent pinentry input prompts from blocking integration tests
+pinentry-program pinentry-non-existent
diff --git a/src/test/resources/it/settings.xml b/src/test/resources/it/settings.xml
new file mode 100644
index 0000000..bc80ff2
--- /dev/null
+++ b/src/test/resources/it/settings.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+ it-repo
+
+ true
+
+
+
+ local.central
+ file://@settings.localRepository@
+
+ true
+
+
+ true
+
+
+
+
+
+ local.central
+ file://@settings.localRepository@
+
+ true
+
+
+ true
+
+
+
+
+
+
+
diff --git a/src/test/resources/it/sign-release-without-passphrase-interactive/pom.xml b/src/test/resources/it/sign-release-without-passphrase-interactive/pom.xml
new file mode 100644
index 0000000..64de500
--- /dev/null
+++ b/src/test/resources/it/sign-release-without-passphrase-interactive/pom.xml
@@ -0,0 +1,97 @@
+
+
+
+
+
+ 4.0.0
+
+ org.apache.maven.its.gpg.srwopi
+ test
+ 1.0
+ jar
+
+
+ Tests that signing with a missing passphrase in Maven interactive mode does not hang.
+
+
+
+ true
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 2.0.2
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ @project.version@
+
+
+ sign-artifacts
+
+ sign
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-install-plugin
+ 2.2
+
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 2.1
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 2.2
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 2.0.4
+
+
+ attach-sources
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.3.1
+
+
+
+
+