files = list.collect(Collectors.toList());
+
+ for (String fileNamePattern : expectedSanctionedFilePatterns) {
+ assertThat(files).satisfiesOnlyOnce consumer {
+ assertThat(it.getFileName().toString())
+ .matches(fileNamePattern)
+ assertThat(it)
+ .isNotEmptyFile()
+ .isRegularFile()
+ }
+ }
+}
diff --git a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/fs/AbstractTemporaryLocationProvider.java b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/fs/AbstractTemporaryLocationProvider.java
new file mode 100644
index 000000000..900371cc7
--- /dev/null
+++ b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/fs/AbstractTemporaryLocationProvider.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 - 2025, Ashley Scopes.
+ *
+ * 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 io.github.ascopes.protobufmavenplugin.fs;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Objects;
+import org.apache.maven.plugin.MojoExecution;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Base for implementing a temporary location somewhere that has a unique path per goal
+ * invocation.
+ *
+ * @author Ashley Scopes
+ */
+public abstract class AbstractTemporaryLocationProvider {
+ private static final String FRAG = "protobuf-maven-plugin";
+ private static final Logger log = LoggerFactory.getLogger(
+ AbstractTemporaryLocationProvider.class
+ );
+
+ private final MojoExecution mojoExecution;
+
+ protected AbstractTemporaryLocationProvider(MojoExecution mojoExecution) {
+ this.mojoExecution = mojoExecution;
+ }
+
+ protected Path resolveAndCreateDirectory(Path basePath, String... bits) throws IOException {
+ // GH-488: Execution ID and goal can potentially be null, e.g. in Quarkus dev mode, so
+ // default to a semi-sensible value to prevent a NullPointerException.
+ var goal = Objects.requireNonNullElse(
+ mojoExecution.getGoal(),
+ "unknown-goal"
+ );
+ var executionId = Objects.requireNonNullElse(
+ mojoExecution.getExecutionId(),
+ "unknown-execution-id"
+ );
+
+ var dir = basePath.resolve(FRAG)
+ // GH-421: Include the execution ID and goal to keep file paths unique
+ // between invocations in multiple goals.
+ .resolve(goal)
+ .resolve(executionId);
+
+ for (var bit : bits) {
+ dir = dir.resolve(bit);
+ }
+
+ log.trace("Creating temporary location at '{}' if it does not already exist...", dir);
+
+ // This should be concurrent-safe as it will not break if the directory already exists unless
+ // the directory is instead a regular file.
+ return Files.createDirectories(dir);
+ }
+}
diff --git a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/fs/TemporarySpace.java b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/fs/TemporarySpace.java
index 497b52aa5..3cd062bc8 100644
--- a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/fs/TemporarySpace.java
+++ b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/fs/TemporarySpace.java
@@ -17,71 +17,40 @@
import java.io.IOException;
import java.io.UncheckedIOException;
-import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.maven.execution.scope.MojoExecutionScoped;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.project.MavenProject;
import org.eclipse.sisu.Description;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* Helper to provide access to temporary spaces on the file system to use during builds.
*
+ * These temporary spaces reside within the project {@code target/} directory.
+ *
* @author Ashley Scopes
*/
@Description("Manages build-scoped reusable temporary directories for processing")
@MojoExecutionScoped
@Named
-public final class TemporarySpace {
-
- private static final String FRAG = "protobuf-maven-plugin";
- private static final Logger log = LoggerFactory.getLogger(TemporarySpace.class);
+public final class TemporarySpace extends AbstractTemporaryLocationProvider {
private final MavenProject mavenProject;
- private final MojoExecution mojoExecution;
@Inject
public TemporarySpace(MavenProject mavenProject, MojoExecution mojoExecution) {
+ super(mojoExecution);
this.mavenProject = mavenProject;
- this.mojoExecution = mojoExecution;
}
public Path createTemporarySpace(String... bits) {
- // GH-488: Execution ID and goal can potentially be null, e.g. in Quarkus dev mode, so
- // default to a semi-sensible value to prevent a NullPointerException.
- var goal = Objects.requireNonNullElse(
- mojoExecution.getGoal(),
- "unknown-goal"
- );
- var executionId = Objects.requireNonNullElse(
- mojoExecution.getExecutionId(),
- "unknown-execution-id"
- );
-
- var dir = Path.of(mavenProject.getBuild().getDirectory())
- .resolve(FRAG)
- // GH-421: Include the execution ID and goal to keep file paths unique
- // between invocations in multiple goals.
- .resolve(goal)
- .resolve(executionId);
-
- for (var bit : bits) {
- dir = dir.resolve(bit);
- }
-
- log.trace("Creating temporary directory at '{}' if it does not already exist...", dir);
-
- // This should be concurrent-safe as it will not break if the directory already exists unless
- // the directory is instead a regular file.
try {
- return Files.createDirectories(dir);
+ var baseDir = Path.of(mavenProject.getBuild().getDirectory());
+ return resolveAndCreateDirectory(baseDir, bits);
} catch (IOException ex) {
- throw new UncheckedIOException("Failed to create temporary directory!", ex);
+ throw new UncheckedIOException("Failed to create temporary location!", ex);
}
}
}
diff --git a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/generation/GenerationRequest.java b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/generation/GenerationRequest.java
index 10500aa37..7d8ba1c45 100644
--- a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/generation/GenerationRequest.java
+++ b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/generation/GenerationRequest.java
@@ -200,6 +200,16 @@ public interface GenerationRequest {
*/
String getProtocVersion();
+ /**
+ * Sanctioned path to place executables in.
+ *
+ *
Used for corporate environments with overly locked-down policies on where native
+ * executables can be placed.
+ *
+ * @return the sanctioned path, or {@code null} if no movement of resources is desired.
+ */
+ @Nullable Path getSanctionedExecutablePath();
+
/**
* Additional user-defined Maven dependencies to include in the {@code protoc}
* import path, and to compile.
diff --git a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/generation/ProtobufBuildOrchestrator.java b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/generation/ProtobufBuildOrchestrator.java
index d09180dd9..2460a4e44 100644
--- a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/generation/ProtobufBuildOrchestrator.java
+++ b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/generation/ProtobufBuildOrchestrator.java
@@ -393,6 +393,7 @@ private ProtocInvocation createProtocInvocation(
.importPaths(importPaths)
.inputDescriptorFiles(inputDescriptorFiles)
.protocPath(protocPath)
+ .sanctionedExecutablePath(request.getSanctionedExecutablePath())
.sourcePaths(filesToCompile.getProtoSources())
.targets(targets)
.build();
diff --git a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/mojo/AbstractGenerateMojo.java b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/mojo/AbstractGenerateMojo.java
index b1a48fb4b..8f2262003 100644
--- a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/mojo/AbstractGenerateMojo.java
+++ b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/mojo/AbstractGenerateMojo.java
@@ -62,13 +62,7 @@ public abstract class AbstractGenerateMojo extends AbstractMojo {
private static final String DEFAULT_TRUE = "true";
private static final String DEFAULT_TRANSITIVE = "TRANSITIVE";
- private static final String PROTOBUF_COMPILER_DIGEST = "protobuf.compiler.digest";
- private static final String PROTOBUF_COMPILER_EXCLUDES = "protobuf.compiler.excludes";
- private static final String PROTOBUF_COMPILER_INCLUDES = "protobuf.compiler.includes";
- private static final String PROTOBUF_COMPILER_INCREMENTAL = "protobuf.compiler.incremental";
- private static final String PROTOBUF_COMPILER_VERSION = "protobuf.compiler.version";
- private static final String PROTOBUF_SKIP = "protobuf.skip";
- private static final String PROTOC_ALIAS = "protoc";
+ private static final String COMPILER_VERSION_PROPERTY = "protobuf.compiler.version";
private static final Logger log = LoggerFactory.getLogger(AbstractGenerateMojo.class);
@@ -406,7 +400,7 @@ public AbstractGenerateMojo() {
*
* @since 2.2.0
*/
- @Parameter(property = PROTOBUF_COMPILER_EXCLUDES)
+ @Parameter(property = "protobuf.compiler.excludes")
@Nullable List excludes;
/**
@@ -552,7 +546,7 @@ public AbstractGenerateMojo() {
*
* @since 2.2.0
*/
- @Parameter(property = PROTOBUF_COMPILER_INCLUDES)
+ @Parameter(property = "protobuf.compiler.includes")
@Nullable List includes;
/**
@@ -567,7 +561,7 @@ public AbstractGenerateMojo() {
*
* @since 2.7.0
*/
- @Parameter(defaultValue = DEFAULT_TRUE, property = PROTOBUF_COMPILER_INCREMENTAL)
+ @Parameter(defaultValue = DEFAULT_TRUE, property = "protobuf.compiler.incremental")
boolean incrementalCompilation;
/**
@@ -801,7 +795,7 @@ public AbstractGenerateMojo() {
*
* @since 3.5.0
*/
- @Parameter(property = PROTOBUF_COMPILER_DIGEST)
+ @Parameter(property = "protobuf.compiler.digest")
@Nullable Digest protocDigest;
/**
@@ -842,9 +836,9 @@ public AbstractGenerateMojo() {
* @since 0.0.1
*/
@Parameter(
- alias = PROTOC_ALIAS,
+ alias = "protoc",
required = true,
- property = PROTOBUF_COMPILER_VERSION
+ property = COMPILER_VERSION_PROPERTY
)
String protocVersion;
@@ -899,12 +893,28 @@ public AbstractGenerateMojo() {
@Parameter(defaultValue = DEFAULT_FALSE)
boolean rustEnabled;
+ /**
+ * Specify a corporate-sanctioned path to run native executables from.
+ *
+ * Most users SHOULD NOT specify this.
+ *
+ *
If you operate in an overly locked-down corporate environment that disallows running
+ * shell/batch scripts or native executables outside sanctioned locations on your local
+ * file system, you can specify the path here either via this configuration parameter
+ * or via a property such that any executables are first moved to a directory within this
+ * location. This is designed to be able to be used within a Maven profile if desired.
+ *
+ * @since 3.9.0
+ */
+ @Parameter(property = "protobuf.sanctioned-executable-path")
+ @Nullable Path sanctionedExecutablePath;
+
/**
* Whether to skip the plugin execution entirely.
*
* @since 2.0.0
*/
- @Parameter(defaultValue = DEFAULT_FALSE, property = PROTOBUF_SKIP)
+ @Parameter(defaultValue = DEFAULT_FALSE, property = "protobuf.skip")
boolean skip;
/**
@@ -1120,6 +1130,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
.protocDigest(protocDigest)
.protocVersion(protocVersion())
.registerAsCompilationRoot(registerAsCompilationRoot)
+ .sanctionedExecutablePath(sanctionedExecutablePath)
.sourceDependencies(nonNullList(sourceDependencies))
.sourceDescriptorDependencies(nonNullList(sourceDescriptorDependencies))
.sourceDescriptorPaths(determinePaths(sourceDescriptorPaths, List::of))
@@ -1163,7 +1174,7 @@ private Path outputDirectory() {
private String protocVersion() {
// Give precedence to overriding the protobuf.compiler.version via the command line
// in case the Maven binaries are incompatible with the current system.
- var overriddenVersion = System.getProperty(PROTOBUF_COMPILER_VERSION);
+ var overriddenVersion = System.getProperty(COMPILER_VERSION_PROPERTY);
return overriddenVersion == null
? requireNonNull(protocVersion, "protocVersion has not been set")
: overriddenVersion;
diff --git a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/ProtocExecutor.java b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/ProtocExecutor.java
index e4054103f..7a363e8b8 100644
--- a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/ProtocExecutor.java
+++ b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/ProtocExecutor.java
@@ -20,6 +20,7 @@
import io.github.ascopes.protobufmavenplugin.protoc.targets.LanguageProtocTarget;
import io.github.ascopes.protobufmavenplugin.protoc.targets.PluginProtocTarget;
import io.github.ascopes.protobufmavenplugin.protoc.targets.ProtocTarget;
+import io.github.ascopes.protobufmavenplugin.protoc.targets.SanctionedExecutableTransformer;
import io.github.ascopes.protobufmavenplugin.utils.ArgumentFileBuilder;
import io.github.ascopes.protobufmavenplugin.utils.HostSystem;
import io.github.ascopes.protobufmavenplugin.utils.TeeWriter;
@@ -49,14 +50,28 @@ public final class ProtocExecutor {
private static final Logger log = LoggerFactory.getLogger(ProtocExecutor.class);
private final HostSystem hostSystem;
private final TemporarySpace temporarySpace;
+ private final SanctionedExecutableTransformer sanctionedExecutablePathTransformer;
@Inject
- public ProtocExecutor(HostSystem hostSystem, TemporarySpace temporarySpace) {
+ public ProtocExecutor(
+ HostSystem hostSystem,
+ TemporarySpace temporarySpace,
+ SanctionedExecutableTransformer sanctionedExecutablePathTransformer
+ ) {
this.hostSystem = hostSystem;
this.temporarySpace = temporarySpace;
+ this.sanctionedExecutablePathTransformer = sanctionedExecutablePathTransformer;
}
public boolean invoke(ProtocInvocation invocation) throws IOException {
+ // In locked down corporate environments, ensure executables are placed in an allowed location
+ // such that we can invoke them successfully.
+ //
+ // We do this here as usually our sources will be all over the place. Some might be on
+ // the system path, some might be in target/ in the temporary space, some may be in the
+ // local Maven repository.
+ invocation = sanctionedExecutablePathTransformer.transform(invocation);
+
var argumentFileBuilder = createArgumentFileBuilder(invocation);
var argumentFile = writeArgumentFile(argumentFileBuilder);
diff --git a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/ProtocInvocation.java b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/ProtocInvocation.java
index af5db7488..152bf73db 100644
--- a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/ProtocInvocation.java
+++ b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/ProtocInvocation.java
@@ -21,6 +21,7 @@
import java.util.Map;
import java.util.SortedSet;
import org.immutables.value.Value.Immutable;
+import org.jspecify.annotations.Nullable;
/**
* Model that holds information about the exact {@code protoc} invocation to perform,
@@ -32,34 +33,23 @@
@Immutable
public interface ProtocInvocation {
- // The executable protoc binary.
Path getProtocPath();
- // Fail if we get warnings, rather than continuing.
boolean isFatalWarnings();
- // Additional arguments to pass to protoc.
List getArguments();
- // Environment variables to explicitly set.
Map getEnvironmentVariables();
- // Paths to proto source files on the root file system to compile.
List getImportPaths();
- // The physical descriptor files to build.
List getInputDescriptorFiles();
- // The files that are described within the provided input descriptors which
- // we want protoc to generate source code from.
List getDescriptorSourceFiles();
- // Paths to proto source files on the root file system to compile.
List getSourcePaths();
- // "Things" to make or output. This can be built-in language generators,
- // protoc plugins, Java plugins that are decorated in an OS-specific set of
- // scripts to invoke it via the kernel fork/exec mechanism, or descriptor
- // files to generate.
SortedSet getTargets();
+
+ @Nullable Path getSanctionedExecutablePath();
}
diff --git a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/targets/PluginProtocTarget.java b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/targets/PluginProtocTarget.java
index 31948fd5f..d1debd1f5 100644
--- a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/targets/PluginProtocTarget.java
+++ b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/targets/PluginProtocTarget.java
@@ -16,7 +16,6 @@
package io.github.ascopes.protobufmavenplugin.protoc.targets;
import io.github.ascopes.protobufmavenplugin.plugins.ResolvedProtocPlugin;
-import java.nio.file.Path;
import org.immutables.value.Value.Derived;
import org.immutables.value.Value.Immutable;
diff --git a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/targets/SanctionedExecutableTransformer.java b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/targets/SanctionedExecutableTransformer.java
new file mode 100644
index 000000000..c6d1278ba
--- /dev/null
+++ b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/targets/SanctionedExecutableTransformer.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2023 - 2025, Ashley Scopes.
+ *
+ * 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 io.github.ascopes.protobufmavenplugin.protoc.targets;
+
+import io.github.ascopes.protobufmavenplugin.fs.AbstractTemporaryLocationProvider;
+import io.github.ascopes.protobufmavenplugin.plugins.ImmutableResolvedProtocPlugin;
+import io.github.ascopes.protobufmavenplugin.protoc.ImmutableProtocInvocation;
+import io.github.ascopes.protobufmavenplugin.protoc.ProtocInvocation;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Collections;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import javax.inject.Inject;
+import javax.inject.Named;
+import org.apache.maven.execution.scope.MojoExecutionScoped;
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.project.MavenProject;
+import org.eclipse.sisu.Description;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Transformer of {@link ProtocInvocation} requests that moves executables to a
+ * sanctioned user-requested location.
+ *
+ * The use case for this is for users working in overly restrictive corporate
+ * environments with various company-mandated facilities that prevent execution
+ * of binaries and scripts from outside very specific locations.
+ *
+ *
If no sanctioned location has been specified, then nothing is changed.
+ *
+ *
In the event a sanctioned location is specified, then any targets will be
+ * rebuilt with a new executable location, and any respective files will be copied
+ * across to that location.
+ *
+ * @author Ashley Scopes
+ * @since 3.9.0
+ */
+@Description("Moves executable targets to a user-specified location for corporate environments")
+@MojoExecutionScoped
+@Named
+public final class SanctionedExecutableTransformer extends AbstractTemporaryLocationProvider {
+
+ private static final Logger log = LoggerFactory.getLogger(SanctionedExecutableTransformer.class);
+
+ private final MavenProject mavenProject;
+
+ @Inject
+ public SanctionedExecutableTransformer(
+ MavenProject mavenProject,
+ MojoExecution mojoExecution
+ ) {
+ super(mojoExecution);
+ this.mavenProject = mavenProject;
+ }
+
+ public ProtocInvocation transform(ProtocInvocation protocInvocation) throws IOException {
+ var sanctionedPath = protocInvocation.getSanctionedExecutablePath();
+
+ if (sanctionedPath == null) {
+ log.debug(
+ "No sanctioned executable location specified; will not intercept the protoc invocation"
+ );
+ return protocInvocation;
+ }
+
+ sanctionedPath = sanctionedPath
+ .resolve(mavenProject.getGroupId())
+ .resolve(mavenProject.getArtifactId());
+ sanctionedPath = resolveAndCreateDirectory(sanctionedPath);
+
+ Files.createDirectories(sanctionedPath);
+
+ log.warn(
+ "A user-specified sanctioned execution location of \"{}\" was provided. All executables "
+ + "managed by this plugin invocation will be moved to that location. Your "
+ + "mileage may vary, and it will be up to you to manage cleaning up this path.",
+ sanctionedPath
+ );
+
+ return ImmutableProtocInvocation.builder()
+ .from(protocInvocation)
+ .protocPath(transfer(sanctionedPath, "protoc-", protocInvocation.getProtocPath()))
+ .targets(transformTargets(sanctionedPath, protocInvocation))
+ .build();
+ }
+
+ private SortedSet transformTargets(
+ Path sanctionedPath,
+ ProtocInvocation invocation
+ ) throws IOException {
+ var transformedTargets = new TreeSet();
+
+ for (var target : invocation.getTargets()) {
+ if (target instanceof PluginProtocTarget) {
+ var pluginTarget = (PluginProtocTarget) target;
+ var prefix = "plugin-" + transformedTargets.size() + "-";
+
+ target = ImmutablePluginProtocTarget.builder()
+ .from(pluginTarget)
+ .plugin(ImmutableResolvedProtocPlugin.builder()
+ .from(pluginTarget.getPlugin())
+ .path(transfer(
+ sanctionedPath,
+ prefix,
+ pluginTarget.getPlugin().getPath()
+ ))
+ .build())
+ .build();
+ }
+
+ transformedTargets.add(target);
+ }
+
+ return Collections.unmodifiableSortedSet(transformedTargets);
+ }
+
+ private Path transfer(
+ Path sanctionedPath,
+ String prefix,
+ Path existingFile
+ ) throws IOException {
+ var newFile = sanctionedPath.resolve(prefix + existingFile.getFileName().toString());
+
+ log.debug("Copying \"{}\" to \"{}\"", existingFile, newFile);
+
+ return Files.copy(
+ existingFile,
+ newFile,
+ StandardCopyOption.COPY_ATTRIBUTES,
+ StandardCopyOption.REPLACE_EXISTING
+ );
+ }
+}
diff --git a/protobuf-maven-plugin/src/test/java/io/github/ascopes/protobufmavenplugin/fs/AbstractTemporaryLocationProviderTest.java b/protobuf-maven-plugin/src/test/java/io/github/ascopes/protobufmavenplugin/fs/AbstractTemporaryLocationProviderTest.java
new file mode 100644
index 000000000..6d4b11f59
--- /dev/null
+++ b/protobuf-maven-plugin/src/test/java/io/github/ascopes/protobufmavenplugin/fs/AbstractTemporaryLocationProviderTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2023 - 2025, Ashley Scopes.
+ *
+ * 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 io.github.ascopes.protobufmavenplugin.fs;
+
+import static io.github.ascopes.protobufmavenplugin.fixtures.RandomFixtures.someBasicString;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.apache.maven.plugin.MojoExecution;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Answers;
+import org.mockito.quality.Strictness;
+
+@DisplayName("AbstractTemporaryLocationProvider tests")
+class AbstractTemporaryLocationProviderTest {
+
+ @TempDir
+ Path tempDir;
+ MojoExecution mojoExecution;
+ String executionId;
+ String goal;
+ SomeTemporaryLocationProvider provider;
+
+ @BeforeEach
+ void setUp() {
+ goal = "goal-" + someBasicString();
+ executionId = "executionId-" + someBasicString();
+
+ mojoExecution = mock(MojoExecution.class, withSettings()
+ .strictness(Strictness.LENIENT)
+ .defaultAnswer(Answers.RETURNS_SMART_NULLS));
+
+ when(mojoExecution.getExecutionId())
+ .thenReturn(executionId);
+ when(mojoExecution.getGoal())
+ .thenReturn(goal);
+
+ provider = new SomeTemporaryLocationProvider();
+ }
+
+ @DisplayName("temporary locations are created in the expected place")
+ @Test
+ void temporaryLocationsAreCreatedInTheExpectedPlace() throws IOException {
+ // Given
+ var id = someBasicString();
+
+ // When
+ var actualPath = provider.resolveAndCreateDirectory(tempDir, "foo", "bar", "baz", id);
+
+ // Then
+ assertThat(actualPath)
+ .isEqualTo(tempDir
+ .resolve("protobuf-maven-plugin")
+ .resolve(goal)
+ .resolve(executionId)
+ .resolve("foo")
+ .resolve("bar")
+ .resolve("baz")
+ .resolve(id))
+ .isDirectory();
+ }
+
+ @DisplayName("nothing happens if the temporary directory already exists")
+ @Test
+ void nothingHappensIfTheTemporaryDirectoryAlreadyExists() throws IOException {
+ // Given
+ var id = someBasicString();
+ var existingPath = tempDir
+ .resolve("protobuf-maven-plugin")
+ .resolve(goal)
+ .resolve(executionId)
+ .resolve("foo")
+ .resolve("bar")
+ .resolve("baz")
+ .resolve(id);
+ Files.createDirectories(existingPath);
+
+ // When
+ var actualPath = provider.resolveAndCreateDirectory(tempDir, "foo", "bar", "baz", id);
+
+ // Then
+ assertThat(actualPath)
+ .isEqualTo(existingPath)
+ .isDirectory();
+ }
+
+ private final class SomeTemporaryLocationProvider extends AbstractTemporaryLocationProvider {
+
+ SomeTemporaryLocationProvider() {
+ super(mojoExecution);
+ }
+ }
+}
diff --git a/protobuf-maven-plugin/src/test/java/io/github/ascopes/protobufmavenplugin/fs/TemporarySpaceTest.java b/protobuf-maven-plugin/src/test/java/io/github/ascopes/protobufmavenplugin/fs/TemporarySpaceTest.java
index b50bbd4a2..53cdbf23f 100644
--- a/protobuf-maven-plugin/src/test/java/io/github/ascopes/protobufmavenplugin/fs/TemporarySpaceTest.java
+++ b/protobuf-maven-plugin/src/test/java/io/github/ascopes/protobufmavenplugin/fs/TemporarySpaceTest.java
@@ -137,7 +137,7 @@ void directoryCreationFailuresArePropagated() {
// Then
assertThatExceptionOfType(UncheckedIOException.class)
.isThrownBy(() -> temporarySpace.createTemporarySpace("foo", id))
- .withMessage("Failed to create temporary directory!")
+ .withMessage("Failed to create temporary location!")
.withCause(expectedCause);
}
}
From a5642e811079288c47c0051d20c63ae206dff070 Mon Sep 17 00:00:00 2001
From: Ashley Scopes <73482956+ascopes@users.noreply.github.com>
Date: Mon, 1 Sep 2025 06:51:39 +0100
Subject: [PATCH 2/5] GH-782: Fix logged path for sanctioned executable paths
---
.../targets/SanctionedExecutableTransformer.java | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/targets/SanctionedExecutableTransformer.java b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/targets/SanctionedExecutableTransformer.java
index c6d1278ba..b9014a11c 100644
--- a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/targets/SanctionedExecutableTransformer.java
+++ b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/targets/SanctionedExecutableTransformer.java
@@ -80,13 +80,6 @@ public ProtocInvocation transform(ProtocInvocation protocInvocation) throws IOEx
return protocInvocation;
}
- sanctionedPath = sanctionedPath
- .resolve(mavenProject.getGroupId())
- .resolve(mavenProject.getArtifactId());
- sanctionedPath = resolveAndCreateDirectory(sanctionedPath);
-
- Files.createDirectories(sanctionedPath);
-
log.warn(
"A user-specified sanctioned execution location of \"{}\" was provided. All executables "
+ "managed by this plugin invocation will be moved to that location. Your "
@@ -94,6 +87,13 @@ public ProtocInvocation transform(ProtocInvocation protocInvocation) throws IOEx
sanctionedPath
);
+ sanctionedPath = sanctionedPath
+ .resolve(mavenProject.getGroupId())
+ .resolve(mavenProject.getArtifactId());
+ sanctionedPath = resolveAndCreateDirectory(sanctionedPath);
+
+ Files.createDirectories(sanctionedPath);
+
return ImmutableProtocInvocation.builder()
.from(protocInvocation)
.protocPath(transfer(sanctionedPath, "protoc-", protocInvocation.getProtocPath()))
From 980fcbeffe9ba0f1687367305fc5d59665b38478 Mon Sep 17 00:00:00 2001
From: Ashley Scopes <73482956+ascopes@users.noreply.github.com>
Date: Mon, 1 Sep 2025 07:57:21 +0100
Subject: [PATCH 3/5] GH-782: Document that sanctioned executables reside in
subdirectories
---
.../protobufmavenplugin/mojo/AbstractGenerateMojo.java | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/mojo/AbstractGenerateMojo.java b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/mojo/AbstractGenerateMojo.java
index 8f2262003..cd0fa1b33 100644
--- a/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/mojo/AbstractGenerateMojo.java
+++ b/protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/mojo/AbstractGenerateMojo.java
@@ -904,6 +904,10 @@ public AbstractGenerateMojo() {
* or via a property such that any executables are first moved to a directory within this
* location. This is designed to be able to be used within a Maven profile if desired.
*
+ * When specified, any executables will be copied to this directory prior to invoking them.
+ * These executables will be located in a nested sub-directory to allow this setting to be
+ * shared across plugin invocations whilst retaining build reproducibility.
+ *
* @since 3.9.0
*/
@Parameter(property = "protobuf.sanctioned-executable-path")
From 606afc31fc24ec96a1b9551a283a3f4110d032a1 Mon Sep 17 00:00:00 2001
From: Ashley Scopes <73482956+ascopes@users.noreply.github.com>
Date: Mon, 1 Sep 2025 08:04:46 +0100
Subject: [PATCH 4/5] GH-782: Add test for sanctionedExecutablePath to
AbstractGenerateMojo tests
---
.../AbstractGenerateMojoTestTemplate.java | 37 +++++++++++++++++++
1 file changed, 37 insertions(+)
diff --git a/protobuf-maven-plugin/src/test/java/io/github/ascopes/protobufmavenplugin/mojo/AbstractGenerateMojoTestTemplate.java b/protobuf-maven-plugin/src/test/java/io/github/ascopes/protobufmavenplugin/mojo/AbstractGenerateMojoTestTemplate.java
index e4ecb6e15..477041964 100644
--- a/protobuf-maven-plugin/src/test/java/io/github/ascopes/protobufmavenplugin/mojo/AbstractGenerateMojoTestTemplate.java
+++ b/protobuf-maven-plugin/src/test/java/io/github/ascopes/protobufmavenplugin/mojo/AbstractGenerateMojoTestTemplate.java
@@ -927,6 +927,43 @@ void registerAsCompilationRootIsSetToSpecifiedValue(boolean value) throws Throwa
assertThat(actualRequest.isRegisterAsCompilationRoot()).isEqualTo(value);
}
+ @DisplayName("when ssnctionedExecutablePath is provided, expect it to be set on the request")
+ @Test
+ void whenSanctionedExecutablePathIsProvidedExpectItToBeSetOnTheRequest(
+ @TempDir Path tempDir
+ ) throws Throwable {
+ var expectedSanctionedExecutablePath = Files.createDirectories(tempDir.resolve("some-path"));
+ // Given
+ mojo.sanctionedExecutablePath = expectedSanctionedExecutablePath;
+
+ // When
+ mojo.execute();
+
+ // Then
+ var captor = ArgumentCaptor.forClass(GenerationRequest.class);
+ verify(mojo.sourceCodeGenerator).generate(captor.capture());
+ var actualRequest = captor.getValue();
+ assertThat(actualRequest.getSanctionedExecutablePath())
+ .isEqualTo(expectedSanctionedExecutablePath);
+ }
+
+ @DisplayName("when sanctionedExecutablePath is not provided, expect no path to be used")
+ @Test
+ void whenSanctionedExecutablePathNotProvidedExpectNoFileToBeUsed() throws Throwable {
+ // Given
+ mojo.sanctionedExecutablePath = null;
+
+ // When
+ mojo.execute();
+
+ // Then
+ var captor = ArgumentCaptor.forClass(GenerationRequest.class);
+ verify(mojo.sourceCodeGenerator).generate(captor.capture());
+ var actualRequest = captor.getValue();
+ assertThat(actualRequest.getSanctionedExecutablePath())
+ .isNull();
+ }
+
@DisplayName("when sourceDependencies is null, expect an empty list in the request")
@NullAndEmptySource
@ParameterizedTest(name = "when {0}")
From 933c08b524704ae6afe58c6efa281467c073846c Mon Sep 17 00:00:00 2001
From: Ashley Scopes <73482956+ascopes@users.noreply.github.com>
Date: Mon, 1 Sep 2025 08:24:49 +0100
Subject: [PATCH 5/5] GH-782: Update user guide with details on using
sanctioned executable paths
---
.../site/markdown/corporate-environments.md | 114 ++++++++++++++++++
...e-support.md => other-language-support.md} | 0
protobuf-maven-plugin/src/site/site.xml | 17 +--
3 files changed, 124 insertions(+), 7 deletions(-)
create mode 100644 protobuf-maven-plugin/src/site/markdown/corporate-environments.md
rename protobuf-maven-plugin/src/site/markdown/{additional-language-support.md => other-language-support.md} (100%)
diff --git a/protobuf-maven-plugin/src/site/markdown/corporate-environments.md b/protobuf-maven-plugin/src/site/markdown/corporate-environments.md
new file mode 100644
index 000000000..52ac94128
--- /dev/null
+++ b/protobuf-maven-plugin/src/site/markdown/corporate-environments.md
@@ -0,0 +1,114 @@
+# Corporate environments
+
+
+
+Some users may be utilising this plugin within a locked-down corporate environments.
+
+The following documents some usage patterns that may be useful to corporate users.
+
+## Limited executable locations
+
+Some development environments will have corporate-mandated locations that any custom executables
+must be run from. In this case, failing to run executables from said locations often will result in
+builds being forcefully aborted and failing.
+
+To work around this, a "sanctioned executable path" directory can be configured within this plugin.
+When specified, any executables will first be copied to a unique path within this directory. Any
+calls to the original executables will be changed to invoke the executables within the sanctioned
+directory.
+
+This setting is designed to be able to be set within profiles and within parent POMs if
+desired, so a path that is unique to each project will be generated during the build process.
+
+**Note**: it is up to users to periodically clear out such a directory if specified.
+
+To configure this, there are a few options:
+
+- Configure the plugin directly within the project or plugin management of the parent project.
+
+ ```xml
+
+ C:\dev\protobuf-maven-plugin
+
+ ```
+
+- Configure a property within the project or parent plugin management.
+
+ ```xml
+
+ C:\dev\protobuf-maven-plugin
+
+ ```
+
+- Configure Maven properties in the root of your repository.
+
+ ```
+ # .mvn/maven.config
+ -Dprotobuf.sanctioned-executable-path=C:\dev\protobuf-maven-plugin
+ ```
+
+- Configure a global environment variable to propagate this configuration to all
+ invocations of Maven on the current machine.
+
+ ```
+ MAVEN_ARGS=-Dprotobuf.sanctioned-executable-path=C:\dev\protobuf-maven-plugin
+ # or
+ MAVEN_OPTS=-Dprotobuf.sanctioned-executable-path=C:\dev\protobuf-maven-plugin
+ ```
+
+If this solution is not desirable, the other option is to ensure your `.m2` and
+project you are building are located within the sanctioned path.
+
+## No permission to run executables
+
+Corporate users may be forbidden from running downloaded executables.
+
+In order to use this plugin, the ability to run executables is required. This will need to be
+discussed with the user's IT administrator if it is problematic.
+
+Users may also consider having `protoc` and any binary executable plugins shipped on their
+system `$PATH`. Configuring this plugin to read `protoc` and associated plugins from the system
+`$PATH` is supported and documented in the goal documentation.
+
+## No permission to run scripts
+
+Corporate users may be forbidden from running scripts.
+
+To utilise JVM plugins, this plugin needs the ability to run batch scripts on Windows, or shell
+scripts on other operating systems. This is required as `protoc` lacks the ability to run JARs
+directly.
+
+Historically, other protobuf plugins have instead required the presence of a C compiler to generate
+a JNI-integrated entrypoint. In this plugin, host-specific scripts are used instesd due to the
+improved simplicity and lack of additional dependencies.
+
+Users should discuss this with their IT administrator if this is problematic.
+
+## Using package mirrors
+
+Corporate environments often utilise mirrors of Maven Central for package management.
+
+This usually uses a system such as:
+
+- Sonatype Nexus
+- JFrog Artifactory
+- GitLab Packages
+- AWS CodeArtifact
+
+This plugin supports the use of mirrors out of the box, as dependency resolution is peformed using
+the Maven subsystem for artifact resolution. Users must only ensure that their `settings.xml` and
+`settings-security.xml` (for Maven 3) or keychain (for Maven 4) are configured
+appropriately.
+
+## Using authenticated HTTP/FTP endpoints for direct downloads
+
+At this time, the use of authenticated HTTP or FTP endpoints for direct downloads
+of resources specified by URLs is not supported outside encoding credentials within the authority
+section of the URL itself.
+
+Users should consider placing such resources in a Maven package registry, or keeping them on their
+local machine.
+
+## Dependency scanning
+
+Scanning of dependencies is not covered by this plugin, and is considered out of scope.
diff --git a/protobuf-maven-plugin/src/site/markdown/additional-language-support.md b/protobuf-maven-plugin/src/site/markdown/other-language-support.md
similarity index 100%
rename from protobuf-maven-plugin/src/site/markdown/additional-language-support.md
rename to protobuf-maven-plugin/src/site/markdown/other-language-support.md
diff --git a/protobuf-maven-plugin/src/site/site.xml b/protobuf-maven-plugin/src/site/site.xml
index ab2ba6b8f..47044699b 100644
--- a/protobuf-maven-plugin/src/site/site.xml
+++ b/protobuf-maven-plugin/src/site/site.xml
@@ -56,22 +56,25 @@
+
+
+
-