From 359c74f1ba9e51c72bdf3eedae310622c85af43a Mon Sep 17 00:00:00 2001 From: Ashley Scopes <73482956+ascopes@users.noreply.github.com> Date: Sat, 30 Aug 2025 14:15:31 +0100 Subject: [PATCH 1/5] GH-782: Implement ability to use sanctioned executable paths. 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. --- pom.xml | 2 +- protobuf-maven-plugin/pom.xml | 2 +- .../invoker.properties | 17 ++ .../pom.xml | 129 +++++++++++++++ .../postbuild.groovy | 38 +++++ .../prebuild.groovy | 25 +++ .../ReactorGreetingServiceImpl.java | 27 ++++ .../src/main/protobuf/helloworld.proto | 35 ++++ .../ReactorGreetingServiceImplTest.java | 55 +++++++ .../test.groovy | 84 ++++++++++ .../fs/AbstractTemporaryLocationProvider.java | 72 +++++++++ .../fs/TemporarySpace.java | 45 +----- .../generation/GenerationRequest.java | 10 ++ .../generation/ProtobufBuildOrchestrator.java | 1 + .../mojo/AbstractGenerateMojo.java | 41 +++-- .../protoc/ProtocExecutor.java | 17 +- .../protoc/ProtocInvocation.java | 16 +- .../protoc/targets/PluginProtocTarget.java | 1 - .../SanctionedExecutableTransformer.java | 150 ++++++++++++++++++ ...AbstractTemporaryLocationProviderTest.java | 114 +++++++++++++ .../fs/TemporarySpaceTest.java | 2 +- 21 files changed, 812 insertions(+), 71 deletions(-) create mode 100644 protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/invoker.properties create mode 100644 protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/pom.xml create mode 100644 protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/postbuild.groovy create mode 100644 protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/prebuild.groovy create mode 100644 protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/src/main/java/org/example/helloworld/ReactorGreetingServiceImpl.java create mode 100644 protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/src/main/protobuf/helloworld.proto create mode 100644 protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/src/test/java/org/example/helloworld/ReactorGreetingServiceImplTest.java create mode 100644 protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/test.groovy create mode 100644 protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/fs/AbstractTemporaryLocationProvider.java create mode 100644 protobuf-maven-plugin/src/main/java/io/github/ascopes/protobufmavenplugin/protoc/targets/SanctionedExecutableTransformer.java create mode 100644 protobuf-maven-plugin/src/test/java/io/github/ascopes/protobufmavenplugin/fs/AbstractTemporaryLocationProviderTest.java diff --git a/pom.xml b/pom.xml index 921e0e081..5963b26a4 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ io.github.ascopes protobuf-maven-plugin-parent - 3.8.3-SNAPSHOT + 3.9.0-SNAPSHOT Protobuf Maven Plugin Parent Parent POM for the Protobuf Maven Plugin. diff --git a/protobuf-maven-plugin/pom.xml b/protobuf-maven-plugin/pom.xml index 38f013431..278be77d8 100644 --- a/protobuf-maven-plugin/pom.xml +++ b/protobuf-maven-plugin/pom.xml @@ -22,7 +22,7 @@ io.github.ascopes protobuf-maven-plugin-parent - 3.8.3-SNAPSHOT + 3.9.0-SNAPSHOT protobuf-maven-plugin diff --git a/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/invoker.properties b/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/invoker.properties new file mode 100644 index 000000000..0cd6971ec --- /dev/null +++ b/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/invoker.properties @@ -0,0 +1,17 @@ +# +# 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. +# + +invoker.goals = clean package diff --git a/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/pom.xml b/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/pom.xml new file mode 100644 index 000000000..976ec9afd --- /dev/null +++ b/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/pom.xml @@ -0,0 +1,129 @@ + + + + 4.0.0 + + + @project.groupId@.it + integration-test-parent + @project.version@ + ../setup/pom.xml + + + gh-782-sanctioned-executable-paths + gh-782-sanctioned-executable-paths + + + 4.28.2 + + + + + + com.google.protobuf + protobuf-java + + + + com.salesforce.servicelibs + reactor-grpc-stub + + + + io.grpc + grpc-protobuf + + + + io.grpc + grpc-netty + + + + io.grpc + grpc-stub + + + + io.projectreactor + reactor-core + + + + + javax.annotation + javax.annotation-api + + + + org.junit.jupiter + junit-jupiter + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + @project.groupId@ + @project.artifactId@ + + + + + io.grpc + protoc-gen-grpc-java + ${grpc.version} + + + + + + com.salesforce.servicelibs + reactor-grpc + ${reactor-grpc.version} + + + + ${java.io.tmpdir}/pmp-gh-782-sanctioned-executable-paths + + + + + + generate + + + + + + + diff --git a/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/postbuild.groovy b/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/postbuild.groovy new file mode 100644 index 000000000..3a4064486 --- /dev/null +++ b/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/postbuild.groovy @@ -0,0 +1,38 @@ +/* + * 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. + */ +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor + +Path tempDir = Path.of(System.getProperty("java.io.tmpdir")) +Path sanctionedDir = tempDir.resolve("pmp-gh-782-sanctioned-executable-paths") + +Files.walkFileTree(sanctionedDir, new SimpleFileVisitor() { + FileVisitResult visitFile(Path file, BasicFileAttributes _) { + println("Dropping file ${file}") + Files.delete(file) + return FileVisitResult.CONTINUE + } + + FileVisitResult postVisitDirectory(Path dir, IOException _) { + println("Dropping dir ${dir}") + Files.delete(dir) + return FileVisitResult.CONTINUE + } +}) + +return true diff --git a/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/prebuild.groovy b/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/prebuild.groovy new file mode 100644 index 000000000..c46df0b70 --- /dev/null +++ b/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/prebuild.groovy @@ -0,0 +1,25 @@ +/* + * 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. + */ +import java.nio.file.Files +import java.nio.file.Path + +Path tempDir = Path.of(System.getProperty("java.io.tmpdir")) + +// We use /tmp for this to work around Windows problems +// with https://github.com/ascopes/protobuf-maven-plugin/pull/786 +Files.createDirectories(tempDir.resolve("pmp-gh-782-sanctioned-executable-paths")) + +return true diff --git a/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/src/main/java/org/example/helloworld/ReactorGreetingServiceImpl.java b/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/src/main/java/org/example/helloworld/ReactorGreetingServiceImpl.java new file mode 100644 index 000000000..2d9ab46e5 --- /dev/null +++ b/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/src/main/java/org/example/helloworld/ReactorGreetingServiceImpl.java @@ -0,0 +1,27 @@ +/* + * 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 org.example.helloworld; + +import reactor.core.publisher.Mono; + +public class ReactorGreetingServiceImpl extends ReactorGreetingServiceGrpc.GreetingServiceImplBase { + @Override + public Mono greet(Mono request) { + return request.map(body -> GreetingResponse.newBuilder() + .setText("Hello, " + body.getName() + "!") + .build()); + } +} diff --git a/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/src/main/protobuf/helloworld.proto b/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/src/main/protobuf/helloworld.proto new file mode 100644 index 000000000..6fc29269b --- /dev/null +++ b/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/src/main/protobuf/helloworld.proto @@ -0,0 +1,35 @@ +// +// 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. +// + +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "org.example.helloworld"; + +package org.example.helloworld; + +message GreetingRequest { + string name = 1; +} + +message GreetingResponse { + string text = 1; +} + +service GreetingService { + rpc Greet(GreetingRequest) returns (GreetingResponse); +} + diff --git a/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/src/test/java/org/example/helloworld/ReactorGreetingServiceImplTest.java b/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/src/test/java/org/example/helloworld/ReactorGreetingServiceImplTest.java new file mode 100644 index 000000000..a7950d97d --- /dev/null +++ b/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/src/test/java/org/example/helloworld/ReactorGreetingServiceImplTest.java @@ -0,0 +1,55 @@ +/* + * 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 org.example.helloworld; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.grpc.ManagedChannelBuilder; +import io.grpc.ServerBuilder; +import org.junit.jupiter.api.Test; + +class ReactorGreetingServiceImplTest { + @Test + void greetingServiceWorksAsExpected() throws Throwable { + // Given + var service = new ReactorGreetingServiceImpl(); + var server = ServerBuilder + .forPort(8084) + .addService(service) + .build(); + var channel = ManagedChannelBuilder.forAddress("localhost", 8084) + .usePlaintext() + .build(); + var stub = ReactorGreetingServiceGrpc.newReactorStub(channel); + + try { + server.start(); + + // When + var request = GreetingRequest + .newBuilder() + .setName("Dave") + .build(); + var response = stub.greet(request).block(); + + // Then + assertEquals("Hello, Dave!", response.getText()); + } finally { + server.shutdown(); + server.awaitTermination(); + } + } +} diff --git a/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/test.groovy b/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/test.groovy new file mode 100644 index 000000000..784b65aad --- /dev/null +++ b/protobuf-maven-plugin/src/it/gh-782-sanctioned-executable-paths/test.groovy @@ -0,0 +1,84 @@ +/* + * 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. + */ + + +import java.nio.file.Files +import java.nio.file.Path +import java.util.function.Consumer +import java.util.stream.Collectors +import java.util.stream.Stream + +import static org.assertj.core.api.Assertions.assertThat + +// Type hint to work around method ambiguity in groovy. +@SuppressWarnings("all") + Consumer consumer(Closure consumer) { consumer } + + +Path baseDirectory = basedir.toPath().toAbsolutePath() +Path generatedSourcesDir = baseDirectory.resolve("target/generated-sources/protobuf") +Path classesDir = baseDirectory.resolve("target/classes") +Path tempDir = Path.of(System.getProperty("java.io.tmpdir")) +List expectedGeneratedFiles = [ + "org/example/helloworld/Helloworld", + "org/example/helloworld/GreetingRequest", + "org/example/helloworld/GreetingRequestOrBuilder", + "org/example/helloworld/GreetingServiceGrpc", + "org/example/helloworld/ReactorGreetingServiceGrpc", +] + +Path sanctionedExecutableDir = tempDir + .resolve("pmp-gh-782-sanctioned-executable-paths") + .resolve("gh-782-sanctioned-executable-paths") // groupId + .resolve("gh-782-sanctioned-executable-paths") // artifactId + .resolve("protobuf-maven-plugin") // frag + .resolve("generate") // goal + .resolve("default") // goal ID + +List expectedSanctionedFilePatterns = [ + "plugin-[0-9]-invoke.(bat|sh)", + "plugin-[0-9]-protoc-gen-grpc-java-.+\\.exe", + "protoc-protoc-.+\\.exe", +] + + +assertThat(generatedSourcesDir).isDirectory() +assertThat(classesDir).isDirectory() + +expectedGeneratedFiles.forEach { + assertThat(generatedSourcesDir.resolve("${it}.java")) + .exists() + .isNotEmptyFile() + assertThat(classesDir.resolve("${it}.class")) + .exists() + .isNotEmptyFile() +} + +assertThat(sanctionedExecutableDir).isDirectory() + +try (Stream list = Files.list(sanctionedExecutableDir)) { + List 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 @@ + + + - + + - - - + + - - + + - +