All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.xolstice.maven.plugin.protobuf.AbstractProtocMojo Maven / Gradle / Ivy

package org.xolstice.maven.plugin.protobuf;

/*
 * Copyright (c) 2019 Maven Protocol Buffers Plugin Authors. All rights reserved.
 *
 * 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 org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolutionRequest;
import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.artifact.resolver.ResolutionErrorHandler;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.maven.repository.RepositorySystem;
import org.apache.maven.toolchain.Toolchain;
import org.apache.maven.toolchain.ToolchainManager;
import org.apache.maven.toolchain.java.DefaultJavaToolChain;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.Os;
import org.codehaus.plexus.util.SelectorUtils;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.io.RawInputStreamFacade;
import org.sonatype.plexus.build.incremental.BuildContext;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import static java.lang.Math.max;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singleton;
import static org.codehaus.plexus.util.FileUtils.cleanDirectory;
import static org.codehaus.plexus.util.FileUtils.copyStreamToFile;
import static org.codehaus.plexus.util.FileUtils.getDefaultExcludesAsString;
import static org.codehaus.plexus.util.FileUtils.getFileNames;
import static org.codehaus.plexus.util.FileUtils.getFiles;
import static org.codehaus.plexus.util.StringUtils.join;

/**
 * Abstract Mojo implementation.
 *
 * 

This class is extended by {@link ProtocCompileMojo} and * {@link ProtocTestCompileMojo} in order to override the specific configuration for * compiling the main or test classes respectively.

*/ abstract class AbstractProtocMojo extends AbstractMojo { private static final String DEFAULT_INCLUDES = "**/*.proto*"; /** * The current Maven project. */ @Parameter(defaultValue = "${project}", readonly = true) protected MavenProject project; /** * The current Maven Session Object. * * @since 0.2.0 */ @Parameter(defaultValue = "${session}", readonly = true) protected MavenSession session; /** * Build context that tracks changes to the source and target files. * * @since 0.3.0 */ @Component protected BuildContext buildContext; /** * An optional tool chain manager. * * @since 0.2.0 */ @Component protected ToolchainManager toolchainManager; /** * A helper used to add resources to the project. */ @Component protected MavenProjectHelper projectHelper; /** * A factory for Maven artifact definitions. * * @since 0.3.1 */ @Component private ArtifactFactory artifactFactory; /** * A component that implements resolution of Maven artifacts from repositories. * * @since 0.3.1 */ @Component private ArtifactResolver artifactResolver; /** * A component that handles resolution of Maven artifacts. * * @since 0.4.0 */ @Component private RepositorySystem repositorySystem; /** * A component that handles resolution errors. * * @since 0.4.0 */ @Component private ResolutionErrorHandler resolutionErrorHandler; /** * This is the path to the local maven {@code repository}. */ @Parameter( required = true, readonly = true, property = "localRepository" ) private ArtifactRepository localRepository; /** * Remote repositories for artifact resolution. * * @since 0.3.0 */ @Parameter( required = true, readonly = true, defaultValue = "${project.remoteArtifactRepositories}" ) private List remoteRepositories; /** * A directory where temporary files will be generated. * * @since 0.6.0 */ @Parameter( required = true, readonly = true, defaultValue = "${project.build.directory}" ) private File tempDirectory; /** * A directory where native launchers for java protoc plugins will be generated. * * @since 0.3.0 */ @Parameter( required = false, defaultValue = "${project.build.directory}/protoc-plugins" ) private File protocPluginDirectory; /** * This is the path to the {@code protoc} executable. * When this parameter is not set, the plugin attempts to load * a {@code protobuf} toolchain and use it locate {@code protoc} executable. * If no {@code protobuf} toolchain is defined in the project, * the {@code protoc} executable in the {@code PATH} is used. */ @Parameter( required = false, property = "protocExecutable" ) private String protocExecutable; /** * Protobuf compiler artifact specification, in {@code groupId:artifactId:version[:type[:classifier]]} format. * When this parameter is set, the plugin attempts to resolve the specified artifact as {@code protoc} executable. * * @since 0.4.1 */ @Parameter( required = false, property = "protocArtifact" ) private String protocArtifact; /** * Whether to use the shared repository for binary executables. * The same binary, for example the resolved {@code protoc} binary, will only be copied * and made executable once and kept around in this directory. * Defaults to {@code true}, means to use the shared repository. */ @Parameter( required = false, property = "useProtocBinaryRepository", defaultValue = "true" ) private boolean useProtocBinaryRepository; /** * Additional source paths for {@code .proto} definitions. */ @Parameter( required = false ) private File[] additionalProtoPathElements = {}; /** * Since {@code protoc} cannot access jars, proto files in dependencies are extracted to this location * and deleted on exit. This directory is always cleaned during execution. */ @Parameter( required = true, defaultValue = "${project.build.directory}/protoc-dependencies" ) private File temporaryProtoFileDirectory; /** * Set this to {@code false} to disable hashing of dependent jar paths. *

* This plugin expands jars on the classpath looking for embedded {@code .proto} files. * Normally these paths are hashed (MD5) to avoid issues with long file names on windows. * However if this property is set to {@code false} longer paths will be used. */ @Parameter( required = true, defaultValue = "true" ) private boolean hashDependentPaths; /** * A list of <include> elements specifying the protobuf definition files (by pattern) * that should be included in compilation. * When not specified, the default includes will be: *
* <includes>
*  <include>**/*.proto</include>
* </includes>
*
*/ @Parameter( required = false ) private String[] includes = {DEFAULT_INCLUDES}; /** * A list of <exclude> elements specifying the protobuf definition files (by pattern) * that should be excluded from compilation. * When not specified, the default excludes will be empty: *
* <excludes>
* </excludes>
*
*/ @Parameter( required = false ) private String[] excludes = {}; /** * If set to {@code true}, then the specified protobuf source files from this project will be attached * as resources to the build, for subsequent inclusion into the final artifact. * This is the default behaviour, as it allows downstream projects to import protobuf definitions * from the upstream projects, and those imports are automatically resolved at build time. * *

If distribution of {@code .proto} source files is undesirable for security reasons * or because of other considerations, then this parameter should be set to {@code false}.

* * @since 0.4.1 */ @Parameter( required = true, defaultValue = "true" ) protected boolean attachProtoSources; /** * If set to {@code true}, all command line arguments to protoc will be written to a file, * and only a path to that file will be passed to protoc on the command line. * This helps prevent Command line is too long errors when the number of {@code .proto} files is large. * *

NOTE: This is only supported for protoc 3.5.0 and higher.

* * @since 0.6.0 */ @Parameter( required = false, defaultValue = "false" ) protected boolean useArgumentFile; /** * If set to {@code true}, experimental optional feature will be enabled. * *

NOTE: This is only supported for protoc 3.12.0 and higher.

* * @since 0.7.0 */ @Parameter( required = false, defaultValue = "false" ) protected boolean useExperimentalOptional; /** * Specifies one of more custom protoc plugins, written in Java * and available as Maven artifacts. An executable plugin will be created * at execution time. On UNIX the executable is a shell script and on * Windows it is a WinRun4J .exe and .ini. */ @Parameter( required = false ) private List protocPlugins; /** * Sets the granularity in milliseconds of the last modification date * for testing whether source protobuf definitions need recompilation. * *

This parameter is only used when {@link #checkStaleness} parameter is set to {@code true}. * *

If the project is built on NFS it's recommended to set this parameter to {@code 10000}. */ @Parameter( required = false, defaultValue = "0" ) private long staleMillis; /** * Normally {@code protoc} is invoked on every execution of the plugin. * Setting this parameter to {@code true} will enable checking * timestamps of source protobuf definitions vs. generated sources. * * @see #staleMillis */ @Parameter( required = false, defaultValue = "false" ) private boolean checkStaleness; /** * When {@code true}, skip the execution. * * @since 0.2.0 */ @Parameter( required = false, property = "protoc.skip", defaultValue = "false" ) private boolean skip; /** * Usually most of protobuf mojos will not get executed on parent poms * (i.e. projects with packaging type 'pom'). * Setting this parameter to {@code true} will force * the execution of this mojo, even if it would usually get skipped in this case. * * @since 0.2.0 */ @Parameter( required = false, property = "protoc.force", defaultValue = "false" ) private boolean forceMojoExecution; /** * When {@code true}, the output directory will be cleared out prior to code generation. * With the latest versions of protoc (2.5.0 or later) this is generally not required, * although some earlier versions reportedly had issues with running * two code generations in a row without clearing out the output directory in between. * * @since 0.4.0 */ @Parameter( required = false, property = "protoc.clearOutputDirectory", defaultValue = "false" ) private boolean clearOutputDirectory; /** * Executes the mojo. */ @Override public void execute() throws MojoExecutionException, MojoFailureException { if (skipMojo()) { return; } try { checkParameters(); } catch (final MojoConfigurationException e) { throw new MojoExecutionException("Configuration error: " + e.getMessage(), e); } catch (final MojoInitializationException e) { throw new MojoExecutionException(e.getMessage(), e); } final File protoSourceRoot = getProtoSourceRoot(); if (protoSourceRoot.exists()) { try { final List protoFiles = findProtoFilesInDirectory(protoSourceRoot); final File outputDirectory = getOutputDirectory(); final List outputFiles = findGeneratedFilesInDirectory(getOutputDirectory()); if (protoFiles.isEmpty()) { getLog().info("No proto files to compile."); } else if (!hasDelta(protoFiles)) { getLog().info("Skipping compilation because build context has no changes."); doAttachFiles(); } else if (checkStaleness && checkFilesUpToDate(protoFiles, outputFiles)) { getLog().info("Skipping compilation because target directory newer than sources."); doAttachFiles(); } else { final List derivedProtoPathElements = makeProtoPathFromJars(temporaryProtoFileDirectory, getDependencyArtifactFiles()); FileUtils.mkdir(outputDirectory.getAbsolutePath()); if (clearOutputDirectory) { try { cleanDirectory(outputDirectory); } catch (final IOException e) { throw new MojoInitializationException("Unable to clean output directory", e); } } createProtocPlugins(); //get toolchain from context final Toolchain tc = toolchainManager.getToolchainFromBuildContext("protobuf", session); //NOI18N if (tc != null) { getLog().info("Toolchain in protobuf-maven-plugin: " + tc); //when the executable to use is explicitly set by user in mojo's parameter, ignore toolchains. if (protocExecutable != null) { getLog().warn( "Toolchains are ignored, 'protocExecutable' parameter is set to " + protocExecutable); } else { //assign the path to executable from toolchains protocExecutable = tc.findTool("protoc"); //NOI18N } } if (protocExecutable == null && protocArtifact != null) { final Artifact artifact = createDependencyArtifact(protocArtifact); final File file = resolveBinaryArtifact(artifact); protocExecutable = file.getAbsolutePath(); } if (protocExecutable == null) { // Try to fall back to 'protoc' in $PATH getLog().warn("No 'protocExecutable' parameter is configured, using the default: 'protoc'"); protocExecutable = "protoc"; } final Protoc.Builder protocBuilder = new Protoc.Builder(protocExecutable) .addProtoPathElement(protoSourceRoot) .addProtoPathElements(derivedProtoPathElements) .addProtoPathElements(asList(additionalProtoPathElements)) .addProtoFiles(protoFiles); addProtocBuilderParameters(protocBuilder); final Protoc protoc = protocBuilder.build(); if (getLog().isDebugEnabled()) { getLog().debug("Proto source root:"); getLog().debug(" " + protoSourceRoot); if (derivedProtoPathElements != null && !derivedProtoPathElements.isEmpty()) { getLog().debug("Derived proto paths:"); for (final File path : derivedProtoPathElements) { getLog().debug(" " + path); } } if (additionalProtoPathElements != null && additionalProtoPathElements.length > 0) { getLog().debug("Additional proto paths:"); for (final File path : additionalProtoPathElements) { getLog().debug(" " + path); } } } protoc.logExecutionParameters(getLog()); getLog().info(format("Compiling %d proto file(s) to %s", protoFiles.size(), outputDirectory)); final int exitStatus = protoc.execute(getLog()); if (StringUtils.isNotBlank(protoc.getOutput())) { getLog().info("PROTOC: " + protoc.getOutput()); } if (exitStatus != 0) { getLog().error("PROTOC FAILED: " + protoc.getError()); for (File pf : protoFiles) { buildContext.removeMessages(pf); buildContext.addMessage(pf, 0, 0, protoc.getError(), BuildContext.SEVERITY_ERROR, null); } throw new MojoFailureException( "protoc did not exit cleanly. Review output for more information."); } else if (StringUtils.isNotBlank(protoc.getError())) { getLog().warn("PROTOC: " + protoc.getError()); } doAttachFiles(); } } catch (final MojoConfigurationException e) { throw new MojoExecutionException("Configuration error: " + e.getMessage(), e); } catch (final MojoInitializationException e) { throw new MojoExecutionException(e.getMessage(), e); } catch (final CommandLineException e) { throw new MojoExecutionException("An error occurred while invoking protoc: " + e.getMessage(), e); } catch (final InterruptedException e) { getLog().info("Process interrupted"); } } else { getLog().info(format("%s does not exist. Review the configuration or consider disabling the plugin.", protoSourceRoot)); } } /** * Generates native launchers for java protoc plugins. * These launchers will later be added as parameters for protoc compiler. * * @since 0.3.0 */ protected void createProtocPlugins() { if (protocPlugins == null) { return; } final String javaHome = detectJavaHome(); for (final ProtocPlugin plugin : protocPlugins) { if (plugin.getJavaHome() != null) { getLog().debug("Using javaHome defined in plugin definition: " + plugin.getJavaHome()); } else { getLog().debug("Setting javaHome for plugin: " + javaHome); plugin.setJavaHome(javaHome); } getLog().info("Building protoc plugin: " + plugin.getId()); final ProtocPluginAssembler assembler = new ProtocPluginAssembler( plugin, session, project.getArtifact(), artifactFactory, repositorySystem, resolutionErrorHandler, localRepository, remoteRepositories, protocPluginDirectory, getLog()); assembler.execute(); } } /** * Attempts to detect java home directory, using {@code jdk} toolchain if available, * with a fallback to {@code java.home} system property. * * @return path to java home directory. * * @since 0.3.0 */ protected String detectJavaHome() { String javaHome = null; final Toolchain tc = toolchainManager.getToolchainFromBuildContext("jdk", session); if (tc != null) { if (tc instanceof DefaultJavaToolChain) { javaHome = ((DefaultJavaToolChain) tc).getJavaHome(); if (javaHome != null) { getLog().debug("Using javaHome from toolchain: " + javaHome); } } else { // Try to infer JAVA_HOME from location of 'java' tool in toolchain, if available. // We don't use 'java' directly because for Windows we need to find the path to // jvm.dll instead, which the assembler tries to figure out relative to JAVA_HOME. final String javaExecutable = tc.findTool("java"); if (javaExecutable != null) { File parent = new File(javaExecutable).getParentFile(); if (parent != null) { parent = parent.getParentFile(); if (parent != null && parent.isDirectory()) { javaHome = parent.getAbsolutePath(); getLog().debug( "Using javaHome based on 'java' location returned by toolchain: " + javaHome); } } } } } if (javaHome == null) { // Default location is the current JVM's JAVA_HOME. javaHome = System.getProperty("java.home"); getLog().debug("Using javaHome from java.home system property: " + javaHome); } return javaHome; } /** * Adds mojo-specific parameters to the protoc builder. * * @param protocBuilder the builder to be modified. */ protected void addProtocBuilderParameters(final Protoc.Builder protocBuilder) { if (protocPlugins != null) { for (final ProtocPlugin plugin : protocPlugins) { protocBuilder.addPlugin(plugin); } protocPluginDirectory.mkdirs(); protocBuilder.setPluginDirectory(protocPluginDirectory); } protocBuilder.setTempDirectory(tempDirectory); protocBuilder.useArgumentFile(useArgumentFile); protocBuilder.useExperimentalOptional(useExperimentalOptional); } /** *

Determine if the mojo execution should get skipped.

* This is the case if: *
    *
  • {@link #skip} is true
  • *
  • if the mojo gets executed on a project with packaging type 'pom' and * {@link #forceMojoExecution} is false
  • *
* * @return true if the mojo execution should be skipped. * * @since 0.2.0 */ protected boolean skipMojo() { if (skip) { getLog().info("Skipping mojo execution"); return true; } if (!forceMojoExecution && "pom".equals(this.project.getPackaging())) { getLog().info("Skipping mojo execution for project with packaging type 'pom'"); return true; } return false; } protected static List findGeneratedFilesInDirectory(final File directory) { if (directory == null || !directory.isDirectory()) { return emptyList(); } final List generatedFilesInDirectory; try { generatedFilesInDirectory = getFiles(directory, "**/*", getDefaultExcludesAsString()); } catch (final IOException e) { throw new MojoInitializationException("Unable to scan output directory", e); } return generatedFilesInDirectory; } /** * Returns timestamp for the most recently modified file in the given set. * * @param files a collection of file descriptors. * @return timestamp of the most recently modified file. */ protected static long lastModified(final Iterable files) { long result = 0L; for (final File file : files) { result = max(result, file.lastModified()); } return result; } /** * Checks that the source files don't have modification time that is later than the target files. * * @param sourceFiles a collection of source files. * @param targetFiles a collection of target files. * @return {@code true}, if source files are not later than the target files; {@code false}, otherwise. */ protected boolean checkFilesUpToDate(final Iterable sourceFiles, final Iterable targetFiles) { return lastModified(sourceFiles) + staleMillis < lastModified(targetFiles); } /** * Checks if the injected build context has changes in any of the specified files. * * @param files files to be checked for changes. * @return {@code true}, if at least one file has changes; {@code false}, if no files have changes. * * @since 0.3.0 */ protected boolean hasDelta(final Iterable files) { for (final File file : files) { if (buildContext.hasDelta(file)) { return true; } } return false; } protected void checkParameters() { if (project == null) { throw new MojoConfigurationException("'project' is null"); } if (projectHelper == null) { throw new MojoConfigurationException("'projectHelper' is null"); } final File protoSourceRoot = getProtoSourceRoot(); if (protoSourceRoot == null) { throw new MojoConfigurationException("'protoSourceRoot' is null"); } if (protoSourceRoot.isFile()) { throw new MojoConfigurationException("'protoSourceRoot' is a file, not a directory"); } if (temporaryProtoFileDirectory == null) { throw new MojoConfigurationException("'temporaryProtoFileDirectory' is null"); } if (temporaryProtoFileDirectory.isFile()) { throw new MojoConfigurationException("'temporaryProtoFileDirectory' is a file, not a directory"); } final File outputDirectory = getOutputDirectory(); if (outputDirectory == null) { throw new MojoConfigurationException("'outputDirectory' is null"); } if (outputDirectory.isFile()) { throw new MojoConfigurationException("'outputDirectory' is a file, not a directory"); } } protected abstract File getProtoSourceRoot(); protected String[] getIncludes() { return includes; } protected String[] getExcludes() { return excludes; } // TODO add artifact filtering (inclusions and exclusions) // TODO add filtering for proto definitions in included artifacts protected abstract List getDependencyArtifacts(); /** * Returns the output directory for generated sources. Depends on build phase so must * be defined in concrete implementation. * * @return output directory for generated sources. */ protected abstract File getOutputDirectory(); protected void doAttachFiles() { if (attachProtoSources) { doAttachProtoSources(); } doAttachGeneratedFiles(); } protected abstract void doAttachProtoSources(); protected abstract void doAttachGeneratedFiles(); /** * Gets the {@link File} for each dependency artifact. * * @return A list of all dependency artifacts. */ protected List getDependencyArtifactFiles() { final List dependencyArtifacts = getDependencyArtifacts(); if (dependencyArtifacts.isEmpty()) { return emptyList(); } final List dependencyArtifactFiles = new ArrayList<>(dependencyArtifacts.size()); for (final Artifact artifact : dependencyArtifacts) { dependencyArtifactFiles.add(artifact.getFile()); } return dependencyArtifactFiles; } /** * Unpacks proto descriptors that are bundled inside dependent artifacts into a temporary directory. * This is needed because protobuf compiler cannot handle imported descriptors that are packed inside jar files. * * @param temporaryProtoFileDirectory temporary directory to serve as root for unpacked structure. * @param classpathElementFiles classpath elements, can be either jar files or directories. * @return a list of import roots for protobuf compiler * (these will all be subdirectories of the temporary directory). */ protected List makeProtoPathFromJars( final File temporaryProtoFileDirectory, final Iterable classpathElementFiles ) { if (classpathElementFiles == null) { throw new MojoConfigurationException("'classpathElementFiles' is null"); } if (!classpathElementFiles.iterator().hasNext()) { return emptyList(); } // clean the temporary directory to ensure that stale files aren't used if (temporaryProtoFileDirectory.exists()) { try { cleanDirectory(temporaryProtoFileDirectory); } catch (IOException e) { throw new MojoInitializationException("Unable to clean up temporary proto file directory", e); } } final List protoDirectories = new ArrayList<>(); for (final File classpathElementFile : classpathElementFiles) { // for some reason under IAM, we receive poms as dependent files // I am excluding .xml rather than including .jar as there may be other extensions in use (sar, har, zip) if (classpathElementFile.isFile() && classpathElementFile.canRead() && !classpathElementFile.getName().endsWith(".xml")) { // create the jar file. the constructor validates. try (final JarFile classpathJar = new JarFile(classpathElementFile)) { final Enumeration jarEntries = classpathJar.entries(); while (jarEntries.hasMoreElements()) { final JarEntry jarEntry = jarEntries.nextElement(); final String jarEntryName = jarEntry.getName(); if (!jarEntry.isDirectory() && SelectorUtils.matchPath(DEFAULT_INCLUDES, jarEntryName, "/", true)) { final File jarDirectory; try { jarDirectory = new File(temporaryProtoFileDirectory, truncatePath(classpathJar.getName())); // Check for Zip Slip vulnerability // https://snyk.io/research/zip-slip-vulnerability final String canonicalJarDirectoryPath = jarDirectory.getCanonicalPath(); final File uncompressedCopy = new File(jarDirectory, jarEntryName); final String canonicalUncompressedCopyPath = uncompressedCopy.getCanonicalPath(); if (!canonicalUncompressedCopyPath.startsWith(canonicalJarDirectoryPath + File.separator)) { throw new MojoInitializationException( "ZIP SLIP: Entry " + jarEntry.getName() + " in " + classpathJar.getName() + " is outside of the target dir"); } FileUtils.mkdir(uncompressedCopy.getParentFile().getAbsolutePath()); copyStreamToFile( new RawInputStreamFacade(classpathJar.getInputStream(jarEntry)), uncompressedCopy); } catch (final IOException e) { throw new MojoInitializationException("Unable to unpack proto files", e); } protoDirectories.add(jarDirectory); } } } catch (final IOException e) { throw new MojoInitializationException( "Not a readable JAR artifact: " + classpathElementFile.getAbsolutePath(), e); } } else if (classpathElementFile.isDirectory()) { final List protoFiles; try { protoFiles = getFileNames(classpathElementFile, DEFAULT_INCLUDES, null, true); } catch (final IOException e) { throw new MojoInitializationException( "Unable to scan for proto files in: " + classpathElementFile.getAbsolutePath(), e); } if (!protoFiles.isEmpty()) { protoDirectories.add(classpathElementFile); } } } return protoDirectories; } protected List findProtoFilesInDirectory(final File directory) { if (directory == null) { throw new MojoConfigurationException("'directory' is null"); } if (!directory.isDirectory()) { throw new MojoConfigurationException(format("%s is not a directory", directory)); } final List protoFilesInDirectory; try { final String includes = join(getIncludes(), ","); final String excludes = join(getExcludes(), ","); protoFilesInDirectory = getFiles(directory, includes, excludes); } catch (IOException e) { throw new MojoInitializationException("Unable to retrieve the list of files: " + e.getMessage(), e); } return protoFilesInDirectory; } protected List findProtoFilesInDirectories(final Iterable directories) { if (directories == null) { throw new MojoConfigurationException("'directories' is null"); } final List protoFiles = new ArrayList<>(); for (final File directory : directories) { protoFiles.addAll(findProtoFilesInDirectory(directory)); } return protoFiles; } /** * Truncates the path of jar files so that they are relative to the local repository. * * @param jarPath the full path of a jar file. * @return the truncated path relative to the local repository or root of the drive. */ protected String truncatePath(final String jarPath) { if (hashDependentPaths) { return md5Hash(jarPath); } String repository = localRepository.getBasedir().replace('\\', '/'); if (!repository.endsWith("/")) { repository += "/"; } String path = jarPath.replace('\\', '/'); final int repositoryIndex = path.indexOf(repository); if (repositoryIndex != -1) { path = path.substring(repositoryIndex + repository.length()); } // By now the path should be good, but do a final check to fix windows machines. final int colonIndex = path.indexOf(':'); if (colonIndex != -1) { // 2 = :\ in C:\ path = path.substring(colonIndex + 2); } return path; } private static String md5Hash(final String string) { final MessageDigest digest; try { digest = MessageDigest.getInstance("MD5"); } catch (final NoSuchAlgorithmException e) { throw new MojoInitializationException("Unable to create MD5 digest", e); } final byte[] input = string.getBytes(Charset.forName("UTF-8")); return toHexString(digest.digest(input)); } private static final char[] HEX_CHARS = "0123456789abcdef".toCharArray(); protected static String toHexString(final byte[] byteArray) { final StringBuilder hexString = new StringBuilder(2 * byteArray.length); for (final byte b : byteArray) { hexString.append(HEX_CHARS[(b & 0xF0) >> 4]).append(HEX_CHARS[b & 0x0F]); } return hexString.toString(); } protected File resolveBinaryArtifact(final Artifact artifact) { final ArtifactResolutionResult result; final ArtifactResolutionRequest request = new ArtifactResolutionRequest() .setArtifact(project.getArtifact()) .setResolveRoot(false) .setResolveTransitively(false) .setArtifactDependencies(singleton(artifact)) .setManagedVersionMap(emptyMap()) .setLocalRepository(localRepository) .setRemoteRepositories(remoteRepositories) .setOffline(session.isOffline()) .setForceUpdate(session.getRequest().isUpdateSnapshots()) .setServers(session.getRequest().getServers()) .setMirrors(session.getRequest().getMirrors()) .setProxies(session.getRequest().getProxies()); result = repositorySystem.resolve(request); try { resolutionErrorHandler.throwErrors(request, result); } catch (final ArtifactResolutionException e) { throw new MojoInitializationException("Unable to resolve artifact: " + e.getMessage(), e); } final Set artifacts = result.getArtifacts(); if (artifacts == null || artifacts.isEmpty()) { throw new MojoInitializationException("Unable to resolve artifact"); } final Artifact resolvedBinaryArtifact = artifacts.iterator().next(); if (getLog().isDebugEnabled()) { getLog().debug("Resolved artifact: " + resolvedBinaryArtifact); } // Copy the file to the project build directory and make it executable final File sourceFile = resolvedBinaryArtifact.getFile(); final String sourceFileName = sourceFile.getName(); final String targetFileName; if (Os.isFamily(Os.FAMILY_WINDOWS) && !sourceFileName.endsWith(".exe")) { targetFileName = sourceFileName + ".exe"; } else { targetFileName = sourceFileName; } File repositoryDirectory = protocPluginDirectory; if (useProtocBinaryRepository) { File repoBaseDir; for (MavenProject p = project; ; p = p.getParent()) { repoBaseDir = p.getBasedir(); if (!p.hasParent()) { break; } } repositoryDirectory = new File(repoBaseDir, ".mvn/protobuf-binaries"); getLog().info("Using shared repository for protobuf binaries: " + repositoryDirectory); } else { getLog().warn("Consider setting 'useProtocBinaryRepository' to 'true' to avoid sporadic build failures on Linux."); } final File targetFile = new File(repositoryDirectory, targetFileName); if (targetFile.exists()) { // The file must have already been copied in a prior plugin execution/invocation getLog().debug("Executable file already exists: " + targetFile.getAbsolutePath()); return targetFile; } try { FileUtils.forceMkdir(repositoryDirectory); } catch (final IOException e) { throw new MojoInitializationException("Unable to create directory " + repositoryDirectory, e); } final File copyTempFile; try { copyTempFile = File.createTempFile("protoc", ".tmp", repositoryDirectory); } catch (final IOException e) { throw new MojoInitializationException("Unable to copy the protoc binary to " + repositoryDirectory, e); } try { FileUtils.copyFile(sourceFile, copyTempFile); } catch (final IOException e) { throw new MojoInitializationException("Unable to copy the protoc binary to " + repositoryDirectory, e); } if (!Os.isFamily(Os.FAMILY_WINDOWS)) { copyTempFile.setExecutable(true); } if (!targetFile.exists()) { // safe to ignore the return value copyTempFile.renameTo(targetFile); } // Delete the temp file, if it still exists. A "copy race" may have happened. copyTempFile.delete(); if (getLog().isDebugEnabled()) { getLog().debug("Executable file: " + targetFile.getAbsolutePath()); } return targetFile; } /** * Creates a dependency artifact from a specification in * {@code groupId:artifactId:version[:type[:classifier]]} format. * * @param artifactSpec artifact specification. * @return artifact object instance. */ protected Artifact createDependencyArtifact(final String artifactSpec) { final String[] parts = artifactSpec.split(":"); if (parts.length < 3 || parts.length > 5) { throw new MojoConfigurationException( "Invalid artifact specification format" + ", expected: groupId:artifactId:version[:type[:classifier]]" + ", actual: " + artifactSpec); } final String type = parts.length >= 4 ? parts[3] : "exe"; final String classifier = parts.length == 5 ? parts[4] : null; return createDependencyArtifact(parts[0], parts[1], parts[2], type, classifier); } protected Artifact createDependencyArtifact( final String groupId, final String artifactId, final String version, final String type, final String classifier ) { final VersionRange versionSpec; try { versionSpec = VersionRange.createFromVersionSpec(version); } catch (final InvalidVersionSpecificationException e) { throw new MojoConfigurationException("Invalid version specification", e); } return artifactFactory.createDependencyArtifact( groupId, artifactId, versionSpec, type, classifier, Artifact.SCOPE_RUNTIME); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy