001package com.labun.buildnumber; 002 003import static com.labun.buildnumber.BuildNumberExtractor.propertyNames; 004 005import java.io.File; 006import java.util.Arrays; 007import java.util.List; 008import java.util.Map; 009import java.util.Objects; 010import java.util.Properties; 011import java.util.TreeMap; 012 013import org.apache.maven.plugin.AbstractMojo; 014import org.apache.maven.plugin.MojoExecutionException; 015import org.apache.maven.plugin.MojoFailureException; 016import org.apache.maven.plugins.annotations.Component; 017import org.apache.maven.plugins.annotations.LifecyclePhase; 018import org.apache.maven.plugins.annotations.Mojo; 019import org.apache.maven.plugins.annotations.Parameter; 020import org.apache.maven.project.MavenProject; 021import org.sonatype.plexus.build.incremental.BuildContext; 022 023import lombok.Getter; 024import lombok.Setter; 025 026/** Extracts Git metadata and creates build number. Publishes them as project properties. */ 027@Getter 028@Setter 029@Mojo(name = "extract-buildnumber", defaultPhase = LifecyclePhase.VALIDATE, threadSafe = true) 030public class JGitBuildNumberMojo extends AbstractMojo implements Parameters { 031 032 @Component 033 private BuildContext buildContext; 034 035 // ---------- parameters (user configurable) ---------- 036 037 private @Parameter String namespace; 038 private @Parameter String dirtyValue; 039 private @Parameter Integer shortRevisionLength; 040 private @Parameter String gitDateFormat; 041 private @Parameter String buildDateFormat; 042 private @Parameter String dateFormatTimeZone; 043 private @Parameter String countCommitsSinceInclusive; 044 private @Parameter String countCommitsSinceExclusive; 045 private @Parameter String countCommitsInPath; 046 private @Parameter String buildNumberFormat; 047 private @Parameter File repositoryDirectory; 048 private @Parameter Boolean runOnlyAtExecutionRoot; 049 private @Parameter Boolean skip; 050 private @Parameter Boolean verbose; 051 052 // ---------- parameters (read only) ---------- 053 054 @Parameter(property = "project.basedir", readonly = true, required = true) 055 private File baseDirectory; 056 057 @Parameter(property = "session.executionRootDirectory", readonly = true, required = true) 058 private File executionRootDirectory; 059 060 /** The maven project. */ 061 @Parameter(property = "project", readonly = true) 062 private MavenProject project; 063 064 /** The maven parent project. */ 065 @Parameter(property = "project.parent", readonly = true) 066 private MavenProject parentProject; 067 068 // ---------- implementation ---------- 069 070 /** Extracts buildnumber fields from git repository and publishes them as maven properties. 071 * Executes only once per build. Return default (unknown) buildnumber fields on error. */ 072 @Override 073 public void execute() throws MojoExecutionException, MojoFailureException { 074 long start = System.currentTimeMillis(); 075 076 // set some parameters to Maven specific values 077 if (getRepositoryDirectory() == null) setRepositoryDirectory(project.getBasedir()); // ${project.basedir} 078 079 validateAndSetParameterValues(); 080 081 if (skip) { 082 getLog().info("Execution is skipped by configuration."); 083 return; 084 } 085 086 if (verbose) getLog().info("JGit BuildNumber Maven Plugin - start"); 087 if (verbose) getLog().info("executionRootDirectory: " + executionRootDirectory + ", baseDirectory: " + baseDirectory); 088 089 try { 090 // accesses Git repo only once per build 091 // http://www.sonatype.com/people/2009/05/how-to-make-a-plugin-run-once-during-a-build/ 092 if (!runOnlyAtExecutionRoot || executionRootDirectory.equals(baseDirectory)) { 093 094 BuildNumberExtractor extractor = new BuildNumberExtractor(this, msg -> getLog().info(msg)); 095 096 String headSha1 = extractor.getHeadSha1(); 097 String dirty = extractor.isGitStatusDirty() ? dirtyValue : null; 098 099 List<Object> params = Arrays.asList(headSha1, dirty, shortRevisionLength, gitDateFormat, buildDateFormat, dateFormatTimeZone, 100 countCommitsSinceInclusive, countCommitsSinceExclusive, countCommitsInPath, buildNumberFormat); 101 String paramsKey = "jgitParams" + namespace; 102 String resultKey = "jgitResult" + namespace; 103 104 // note: saving/loading custom classes doesn't work (due to different classloaders?, "cannot be cast" error); 105 // when saving Properties object, our values don't survive; therefore we use a Map here 106 Map<String, String> result = getCachedResultFromBuildConext(paramsKey, params, resultKey); 107 if (result != null) { 108 if (verbose) getLog().info("using cached result: " + result); 109 } else { 110 result = extractor.extract(); 111 saveResultToBuildContext(paramsKey, params, resultKey, result); 112 } 113 setProperties(result, project.getProperties()); 114 115 } else if ("pom".equals(parentProject.getPackaging())) { 116 // build started from parent, we are in subproject, lets provide parent properties to our project 117 Properties parentProps = parentProject.getProperties(); 118 String revision = parentProps.getProperty(namespace + "." + "revision"); 119 if (revision == null) { 120 // we are in subproject, but parent project wasn't build this time, 121 // maybe build is running from parent with custom module list - 'pl' argument 122 getLog().warn("Cannot extract Git info, maybe custom build with 'pl' argument is running"); 123 fillPropsUnknown(); // TODO: throw exception instead? 124 return; 125 } 126 if (verbose) getLog().info("using already extracted properties from parent module: " + toMap(parentProps)); 127 setProperties(parentProps, project.getProperties()); 128 129 } else { 130 // should not happen 131 getLog().warn("Cannot extract JGit version: something wrong with build process, we're not in parent, not in subproject!"); 132 fillPropsUnknown(); // TODO: throw exception instead? 133 } 134 } catch (Exception e) { 135 String message = e.getMessage() != null ? e.getMessage() : /* e.g. NPE */ e.getClass().getSimpleName(); 136 getLog().error(message); 137 // if (verbose) getLog().error(e); // stacktrace (can be printed by Maven with debug output) 138 // fillPropsUnknown(); 139 throw new MojoFailureException(message, e); 140 } finally { 141 long duration = System.currentTimeMillis() - start; 142 if (verbose) getLog().info(String.format("JGit BuildNumber Maven Plugin - end (execution time: %d ms)", duration)); 143 } 144 } 145 146 // m2e build? => save extracted values to BuildContext 147 private void saveResultToBuildContext(String paramsKey, List<Object> currentParams, String resultKey, Map<String, String> result) { 148 if (buildContext != null) { 149 buildContext.setValue(paramsKey, currentParams); 150 buildContext.setValue(resultKey, result); 151 } 152 } 153 154 // m2e incremental build and input params (HEAD, etc.) not changed? => try to get previously extracted values from BuildContext 155 // note: buildContext != null only in m2e builds in Eclipse 156 private Map<String, String> getCachedResultFromBuildConext(String paramsKey, List<Object> currentParams, String resultKey) { 157 if (buildContext != null && buildContext.isIncremental()) { 158 if (verbose) getLog().info("m2e incremental build detected"); 159 // getLog().info("buildContext.getClass(): " + buildContext.getClass()); // org.eclipse.m2e.core.internal.embedder.EclipseBuildContext 160 List<Object> cachedParams = (List<Object>) buildContext.getValue(paramsKey); 161 // getLog().info("cachedParams: " + cachedParams); 162 if (Objects.equals(cachedParams, currentParams)) { 163 Map<String, String> cachedResult = (Map<String, String>) buildContext.getValue(resultKey); 164 // getLog().info("cachedResult: " + cachedResult); 165 return cachedResult; 166 } 167 } 168 return null; 169 } 170 171 private Map<String, String> toMap(Properties props) { 172 Map<String, String> map = new TreeMap<>(); 173 for (String propertyName : propertyNames) 174 map.put(propertyName, props.getProperty(namespace + "." + propertyName)); 175 176 return map; 177 } 178 179 private void setProperties(Map<String, String> source, Properties target) { 180 for (Map.Entry<String, String> e : source.entrySet()) 181 target.setProperty(namespace + "." + e.getKey(), e.getValue()); 182 } 183 184 private void setProperties(Properties source, Properties target) { 185 for (String propertyName : propertyNames) { 186 String prefixedName = namespace + "." + propertyName; 187 target.setProperty(prefixedName, source.getProperty(prefixedName)); 188 } 189 } 190 191 private void fillPropsUnknown() { 192 Properties props = project.getProperties(); 193 for (String propertyName : propertyNames) 194 props.setProperty(namespace + "." + propertyName, "UNKNOWN-" + propertyName); 195 } 196}