| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.maven.buildcache; |
| |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.FileVisitResult; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.nio.file.SimpleFileVisitor; |
| import java.nio.file.attribute.BasicFileAttributes; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.TreeSet; |
| import java.util.UUID; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ConcurrentMap; |
| import java.util.concurrent.Future; |
| import java.util.concurrent.FutureTask; |
| import java.util.regex.Pattern; |
| import javax.annotation.Nonnull; |
| import javax.inject.Inject; |
| import javax.inject.Named; |
| import org.apache.commons.io.FileUtils; |
| import org.apache.commons.io.FilenameUtils; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.commons.lang3.mutable.MutableBoolean; |
| import org.apache.maven.SessionScoped; |
| import org.apache.maven.artifact.handler.ArtifactHandler; |
| import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager; |
| import org.apache.maven.buildcache.artifact.RestoredArtifact; |
| import org.apache.maven.buildcache.hash.HashAlgorithm; |
| import org.apache.maven.buildcache.hash.HashFactory; |
| import org.apache.maven.buildcache.xml.Build; |
| import org.apache.maven.buildcache.xml.CacheConfig; |
| import org.apache.maven.buildcache.xml.CacheSource; |
| import org.apache.maven.buildcache.xml.DtoUtils; |
| import org.apache.maven.buildcache.xml.XmlService; |
| import org.apache.maven.buildcache.xml.build.Artifact; |
| import org.apache.maven.buildcache.xml.build.CompletedExecution; |
| import org.apache.maven.buildcache.xml.build.DigestItem; |
| import org.apache.maven.buildcache.xml.build.ProjectsInputInfo; |
| import org.apache.maven.buildcache.xml.build.Scm; |
| import org.apache.maven.buildcache.xml.config.PropertyName; |
| import org.apache.maven.buildcache.xml.config.TrackedProperty; |
| import org.apache.maven.buildcache.xml.diff.Diff; |
| import org.apache.maven.buildcache.xml.report.CacheReport; |
| import org.apache.maven.buildcache.xml.report.ProjectReport; |
| import org.apache.maven.execution.MavenSession; |
| import org.apache.maven.execution.MojoExecutionEvent; |
| import org.apache.maven.plugin.Mojo; |
| import org.apache.maven.plugin.MojoExecution; |
| import org.apache.maven.plugin.descriptor.Parameter; |
| import org.apache.maven.project.MavenProject; |
| import org.apache.maven.project.MavenProjectHelper; |
| import org.apache.maven.repository.RepositorySystem; |
| import org.codehaus.plexus.util.ReflectionUtils; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import static java.nio.file.StandardOpenOption.CREATE; |
| import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; |
| import static org.apache.commons.lang3.StringUtils.isNotBlank; |
| import static org.apache.commons.lang3.StringUtils.replace; |
| import static org.apache.commons.lang3.StringUtils.split; |
| import static org.apache.maven.buildcache.CacheResult.empty; |
| import static org.apache.maven.buildcache.CacheResult.failure; |
| import static org.apache.maven.buildcache.CacheResult.partialSuccess; |
| import static org.apache.maven.buildcache.CacheResult.rebuilded; |
| import static org.apache.maven.buildcache.CacheResult.success; |
| import static org.apache.maven.buildcache.HttpCacheRepositoryImpl.BUILDINFO_XML; |
| import static org.apache.maven.buildcache.checksum.KeyUtils.getVersionlessProjectKey; |
| import static org.apache.maven.buildcache.checksum.MavenProjectInput.CACHE_IMPLEMENTATION_VERSION; |
| |
| /** |
| * CacheControllerImpl |
| */ |
| @SessionScoped |
| @Named |
| @SuppressWarnings( "unused" ) |
| public class CacheControllerImpl implements CacheController |
| { |
| |
| public static final String FILE_SEPARATOR_SUBST = "_"; |
| |
| /** |
| * Prefix for generated sources stored as a separate artifact in cache |
| */ |
| private static final String BUILD_PREFIX = "build" + FILE_SEPARATOR_SUBST; |
| |
| private static final Logger LOGGER = LoggerFactory.getLogger( CacheControllerImpl.class ); |
| |
| private final MavenProjectHelper projectHelper; |
| |
| private final ArtifactHandlerManager artifactHandlerManager; |
| |
| private final XmlService xmlService; |
| |
| private final CacheConfig cacheConfig; |
| |
| private final LocalCacheRepository localCache; |
| |
| private final RemoteCacheRepository remoteCache; |
| |
| private final ConcurrentMap<String, CacheResult> cacheResults = new ConcurrentHashMap<>(); |
| |
| private final LifecyclePhasesHelper lifecyclePhasesHelper; |
| |
| private volatile Map<String, MavenProject> projectIndex; |
| |
| private final ProjectInputCalculator projectInputCalculator; |
| |
| private final RestoredArtifactHandler restoreArtifactHandler; |
| |
| private volatile Scm scm; |
| |
| @Inject |
| @SuppressWarnings( "checkstyle:ParameterNumber" ) |
| public CacheControllerImpl( |
| MavenProjectHelper projectHelper, |
| RepositorySystem repoSystem, |
| ArtifactHandlerManager artifactHandlerManager, |
| XmlService xmlService, |
| LocalCacheRepository localCache, |
| RemoteCacheRepository remoteCache, |
| CacheConfig cacheConfig, |
| ProjectInputCalculator projectInputCalculator, |
| RestoredArtifactHandler restoreArtifactHandler, |
| LifecyclePhasesHelper lifecyclePhasesHelper, |
| MavenSession session ) |
| { |
| this.projectHelper = projectHelper; |
| this.localCache = localCache; |
| this.remoteCache = remoteCache; |
| this.cacheConfig = cacheConfig; |
| this.artifactHandlerManager = artifactHandlerManager; |
| this.xmlService = xmlService; |
| this.lifecyclePhasesHelper = lifecyclePhasesHelper; |
| this.projectInputCalculator = projectInputCalculator; |
| this.restoreArtifactHandler = restoreArtifactHandler; |
| } |
| |
| @Override |
| @Nonnull |
| public CacheResult findCachedBuild( MavenSession session, MavenProject project, |
| List<MojoExecution> mojoExecutions ) |
| { |
| final String highestRequestPhase = CacheUtils.getLast( mojoExecutions ).getLifecyclePhase(); |
| if ( !lifecyclePhasesHelper.isLaterPhaseThanClean( highestRequestPhase ) ) |
| { |
| return empty(); |
| } |
| |
| LOGGER.info( "Attempting to restore project from build cache" ); |
| |
| ProjectsInputInfo inputInfo = projectInputCalculator.calculateInput( project ); |
| |
| final CacheContext context = new CacheContext( project, inputInfo, session ); |
| // remote build first |
| CacheResult result = findCachedBuild( mojoExecutions, context ); |
| |
| if ( !result.isSuccess() && result.getContext() != null ) |
| { |
| LOGGER.debug( "Remote cache is incomplete or missing, trying local build" ); |
| |
| CacheResult localBuild = findLocalBuild( mojoExecutions, context ); |
| |
| if ( localBuild.isSuccess() || ( localBuild.isPartialSuccess() && !result.isPartialSuccess() ) ) |
| { |
| result = localBuild; |
| } |
| else |
| { |
| LOGGER.info( "Local build was not found by checksum " + inputInfo.getChecksum() ); |
| } |
| } |
| cacheResults.put( getVersionlessProjectKey( project ), result ); |
| |
| return result; |
| } |
| |
| private CacheResult findCachedBuild( List<MojoExecution> mojoExecutions, CacheContext context ) |
| { |
| Optional<Build> cachedBuild = Optional.empty(); |
| try |
| { |
| cachedBuild = localCache.findBuild( context ); |
| if ( cachedBuild.isPresent() ) |
| { |
| return analyzeResult( context, mojoExecutions, cachedBuild.get() ); |
| } |
| } |
| catch ( Exception e ) |
| { |
| LOGGER.error( "Cannot read cached remote build", e ); |
| } |
| return cachedBuild.map( build -> failure( build, context ) ) |
| .orElseGet( () -> empty( context ) ); |
| } |
| |
| private CacheResult findLocalBuild( List<MojoExecution> mojoExecutions, CacheContext context ) |
| { |
| Optional<Build> localBuild = Optional.empty(); |
| try |
| { |
| localBuild = localCache.findLocalBuild( context ); |
| if ( localBuild.isPresent() ) |
| { |
| return analyzeResult( context, mojoExecutions, localBuild.get() ); |
| } |
| } |
| catch ( Exception e ) |
| { |
| LOGGER.error( "Cannot read local build", e ); |
| } |
| return localBuild.map( build -> failure( build, context ) ) |
| .orElseGet( () -> empty( context ) ); |
| } |
| |
| private CacheResult analyzeResult( CacheContext context, List<MojoExecution> mojoExecutions, Build build ) |
| { |
| try |
| { |
| final ProjectsInputInfo inputInfo = context.getInputInfo(); |
| |
| LOGGER.info( "Found cached build, restoring from cache {}", inputInfo.getChecksum() ); |
| LOGGER.debug( "Cached build details: {}", build ); |
| |
| final String cacheImplementationVersion = build.getCacheImplementationVersion(); |
| if ( !CACHE_IMPLEMENTATION_VERSION.equals( cacheImplementationVersion ) ) |
| { |
| LOGGER.warn( |
| "Maven and cached build implementations mismatch, caching might not work correctly. " |
| + "Implementation version: " + CACHE_IMPLEMENTATION_VERSION |
| + ", cached build: {}", |
| build.getCacheImplementationVersion() ); |
| } |
| |
| List<MojoExecution> cachedSegment = lifecyclePhasesHelper.getCachedSegment( mojoExecutions, build ); |
| List<MojoExecution> missingMojos = build.getMissingExecutions( cachedSegment ); |
| if ( !missingMojos.isEmpty() ) |
| { |
| LOGGER.warn( "Cached build doesn't contains all requested plugin executions " |
| + "(missing: {}), cannot restore", missingMojos ); |
| return failure( build, context ); |
| } |
| |
| if ( !isCachedSegmentPropertiesPresent( context.getProject(), build, cachedSegment ) ) |
| { |
| LOGGER.info( "Cached build violates cache rules, cannot restore" ); |
| return failure( build, context ); |
| } |
| |
| final String highestRequestPhase = CacheUtils.getLast( mojoExecutions ).getLifecyclePhase(); |
| if ( lifecyclePhasesHelper.isLaterPhaseThanBuild( highestRequestPhase, build ) |
| && !canIgnoreMissingSegment( build, mojoExecutions ) ) |
| { |
| LOGGER.info( "Project restored partially. Highest cached goal: {}, requested: {}", |
| build.getHighestCompletedGoal(), highestRequestPhase ); |
| return partialSuccess( build, context ); |
| } |
| |
| return success( build, context ); |
| |
| } |
| catch ( Exception e ) |
| { |
| LOGGER.error( "Failed to restore project", e ); |
| localCache.clearCache( context ); |
| return failure( build, context ); |
| } |
| } |
| |
| private boolean canIgnoreMissingSegment( Build info, List<MojoExecution> mojoExecutions ) |
| { |
| final List<MojoExecution> postCachedSegment = lifecyclePhasesHelper.getPostCachedSegment( mojoExecutions, |
| info ); |
| for ( MojoExecution mojoExecution : postCachedSegment ) |
| { |
| if ( !cacheConfig.canIgnore( mojoExecution ) ) |
| { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean restoreProjectArtifacts( CacheResult cacheResult ) |
| { |
| final Build build = cacheResult.getBuildInfo(); |
| final CacheContext context = cacheResult.getContext(); |
| final MavenProject project = context.getProject(); |
| |
| try |
| { |
| RestoredArtifact restoredProjectArtifact = null; |
| List<RestoredArtifact> restoredAttachedArtifacts = new ArrayList<>(); |
| |
| if ( build.getArtifact() != null && isNotBlank( build.getArtifact().getFileName() ) ) |
| { |
| final Artifact artifactInfo = build.getArtifact(); |
| String originalVersion = artifactInfo.getVersion(); |
| artifactInfo.setVersion( project.getVersion() ); |
| // TODO if remote is forced, probably need to refresh or reconcile all files |
| final Future<File> downloadTask = createDownloadTask( |
| cacheResult, |
| context, |
| project, |
| artifactInfo, |
| originalVersion ); |
| restoredProjectArtifact = restoredArtifact( project.getArtifact(), artifactInfo.getType(), |
| artifactInfo.getClassifier(), |
| downloadTask ); |
| } |
| |
| for ( Artifact attachedArtifactInfo : build.getAttachedArtifacts() ) |
| { |
| String originalVersion = attachedArtifactInfo.getVersion(); |
| attachedArtifactInfo.setVersion( project.getVersion() ); |
| if ( isNotBlank( attachedArtifactInfo.getFileName() ) ) |
| { |
| if ( StringUtils.startsWith( attachedArtifactInfo.getClassifier(), BUILD_PREFIX ) ) |
| { |
| // restoring generated sources might be unnecessary in CI, could be disabled for |
| // performance reasons |
| if ( cacheConfig.isRestoreGeneratedSources() ) |
| { |
| // generated sources artifact |
| final Path attachedArtifactFile = localCache.getArtifactFile( context, |
| cacheResult.getSource(), attachedArtifactInfo ); |
| restoreGeneratedSources( attachedArtifactInfo, attachedArtifactFile, project ); |
| } |
| } |
| else |
| { |
| Future<File> downloadTask = createDownloadTask( |
| cacheResult, |
| context, |
| project, |
| attachedArtifactInfo, |
| originalVersion ); |
| final RestoredArtifact restoredAttachedArtifact = restoredArtifact( |
| restoredProjectArtifact == null ? project.getArtifact() |
| : restoredProjectArtifact, |
| attachedArtifactInfo.getType(), |
| attachedArtifactInfo.getClassifier(), |
| downloadTask ); |
| restoredAttachedArtifacts.add( restoredAttachedArtifact ); |
| } |
| } |
| } |
| // Actually modify project at the end in case something went wrong during restoration, |
| // in which case, the project is unmodified and we continue with normal build. |
| if ( restoredProjectArtifact != null ) |
| { |
| project.setArtifact( restoredProjectArtifact ); |
| } |
| restoredAttachedArtifacts.forEach( project::addAttachedArtifact ); |
| return true; |
| } |
| catch ( Exception e ) |
| { |
| LOGGER.debug( "Cannot restore cache, continuing with normal build.", e ); |
| return false; |
| } |
| } |
| |
| /** |
| * Helper method similar to {@link org.apache.maven.project.MavenProjectHelper#attachArtifact} to work specifically |
| * with restored from cache artifacts |
| */ |
| private RestoredArtifact restoredArtifact( org.apache.maven.artifact.Artifact parent, String artifactType, |
| String artifactClassifier, |
| Future<File> artifactFile ) |
| { |
| ArtifactHandler handler = null; |
| |
| if ( artifactType != null ) |
| { |
| handler = artifactHandlerManager.getArtifactHandler( artifactType ); |
| } |
| |
| if ( handler == null ) |
| { |
| handler = artifactHandlerManager.getArtifactHandler( "jar" ); |
| } |
| |
| // todo: probably need update download url to cache |
| RestoredArtifact artifact = new RestoredArtifact( parent, artifactFile, artifactType, artifactClassifier, |
| handler ); |
| artifact.setResolved( true ); |
| |
| return artifact; |
| } |
| |
| private Future<File> createDownloadTask( CacheResult cacheResult, CacheContext context, MavenProject project, |
| Artifact artifact, String originalVersion ) |
| { |
| final FutureTask<File> downloadTask = new FutureTask<>( () -> |
| { |
| LOGGER.debug( "Downloading artifact {}", artifact.getArtifactId() ); |
| final Path artifactFile = localCache.getArtifactFile( context, cacheResult.getSource(), |
| artifact ); |
| if ( !Files.exists( artifactFile ) ) |
| { |
| throw new FileNotFoundException( |
| "Missing file for cached build, cannot restore. File: " + artifactFile ); |
| } |
| LOGGER.debug( "Downloaded artifact " + artifact.getArtifactId() + " to: " + artifactFile ); |
| return restoreArtifactHandler.adjustArchiveArtifactVersion( |
| project, |
| originalVersion, |
| artifactFile ).toFile(); |
| } ); |
| if ( !cacheConfig.isLazyRestore() ) |
| { |
| downloadTask.run(); |
| } |
| return downloadTask; |
| } |
| |
| @Override |
| public void save( CacheResult cacheResult, List<MojoExecution> mojoExecutions, |
| Map<String, MojoExecutionEvent> executionEvents ) |
| { |
| CacheContext context = cacheResult.getContext(); |
| |
| if ( context == null || context.getInputInfo() == null ) |
| { |
| LOGGER.info( "Cannot save project in cache, skipping" ); |
| return; |
| } |
| |
| final MavenProject project = context.getProject(); |
| final MavenSession session = context.getSession(); |
| try |
| { |
| final HashFactory hashFactory = cacheConfig.getHashFactory(); |
| final org.apache.maven.artifact.Artifact projectArtifact = project.getArtifact(); |
| final List<org.apache.maven.artifact.Artifact> attachedArtifacts; |
| final List<Artifact> attachedArtifactDtos; |
| final Artifact projectArtifactDto; |
| if ( project.hasLifecyclePhase( "package" ) ) |
| { |
| final HashAlgorithm algorithm = hashFactory.createAlgorithm(); |
| attachGeneratedSources( project ); |
| attachOutputs( project ); |
| attachedArtifacts = project.getAttachedArtifacts() != null |
| ? project.getAttachedArtifacts() : Collections.emptyList(); |
| attachedArtifactDtos = artifactDtos( attachedArtifacts, algorithm ); |
| projectArtifactDto = artifactDto( project.getArtifact(), algorithm ); |
| } |
| else |
| { |
| attachedArtifacts = Collections.emptyList(); |
| attachedArtifactDtos = new ArrayList<>(); |
| projectArtifactDto = null; |
| } |
| |
| List<CompletedExecution> completedExecution = buildExecutionInfo( mojoExecutions, executionEvents ); |
| |
| final Build build = new Build( session.getGoals(), projectArtifactDto, attachedArtifactDtos, |
| context.getInputInfo(), completedExecution, hashFactory.getAlgorithm() ); |
| populateGitInfo( build, session ); |
| build.getDto().set_final( cacheConfig.isSaveFinal() ); |
| cacheResults.put( getVersionlessProjectKey( project ), rebuilded( cacheResult, build ) ); |
| |
| // if package phase presence means new artifacts were packaged |
| if ( project.hasLifecyclePhase( "package" ) ) |
| { |
| localCache.beforeSave( context ); |
| localCache.saveBuildInfo( cacheResult, build ); |
| if ( projectArtifact.getFile() != null ) |
| { |
| localCache.saveArtifactFile( cacheResult, projectArtifact ); |
| } |
| for ( org.apache.maven.artifact.Artifact attachedArtifact : attachedArtifacts ) |
| { |
| if ( attachedArtifact.getFile() != null && isOutputArtifact( |
| attachedArtifact.getFile().getName() ) ) |
| { |
| localCache.saveArtifactFile( cacheResult, attachedArtifact ); |
| } |
| } |
| } |
| else |
| { |
| localCache.saveBuildInfo( cacheResult, build ); |
| } |
| |
| if ( cacheConfig.isBaselineDiffEnabled() ) |
| { |
| produceDiffReport( cacheResult, build ); |
| } |
| |
| } |
| catch ( Exception e ) |
| { |
| LOGGER.error( "Failed to save project, cleaning cache. Project: {}", project, e ); |
| try |
| { |
| localCache.clearCache( context ); |
| } |
| catch ( Exception ex ) |
| { |
| LOGGER.error( "Failed to clean cache due to unexpected error:", ex ); |
| } |
| } |
| } |
| |
| public void produceDiffReport( CacheResult cacheResult, Build build ) |
| { |
| MavenProject project = cacheResult.getContext().getProject(); |
| Optional<Build> baselineHolder = remoteCache.findBaselineBuild( project ); |
| if ( baselineHolder.isPresent() ) |
| { |
| Build baseline = baselineHolder.get(); |
| String outputDirectory = project.getBuild().getDirectory(); |
| Path reportOutputDir = Paths.get( outputDirectory, "incremental-maven" ); |
| LOGGER.info( "Saving cache builds diff to: {}", reportOutputDir ); |
| Diff diff = new CacheDiff( build.getDto(), baseline.getDto(), cacheConfig ).compare(); |
| try |
| { |
| Files.createDirectories( reportOutputDir ); |
| final ProjectsInputInfo baselineInputs = baseline.getDto().getProjectsInputInfo(); |
| final String checksum = baselineInputs.getChecksum(); |
| Files.write( reportOutputDir.resolve( "buildinfo-baseline-" + checksum + ".xml" ), |
| xmlService.toBytes( baseline.getDto() ), TRUNCATE_EXISTING, CREATE ); |
| Files.write( reportOutputDir.resolve( "buildinfo-" + checksum + ".xml" ), |
| xmlService.toBytes( build.getDto() ), TRUNCATE_EXISTING, CREATE ); |
| Files.write( reportOutputDir.resolve( "buildsdiff-" + checksum + ".xml" ), |
| xmlService.toBytes( diff ), TRUNCATE_EXISTING, CREATE ); |
| final Optional<DigestItem> pom = CacheDiff.findPom( build.getDto().getProjectsInputInfo() ); |
| if ( pom.isPresent() ) |
| { |
| Files.write( reportOutputDir.resolve( "effective-pom-" + checksum + ".xml" ), |
| pom.get().getValue().getBytes( StandardCharsets.UTF_8 ), |
| TRUNCATE_EXISTING, CREATE ); |
| } |
| final Optional<DigestItem> baselinePom = CacheDiff.findPom( baselineInputs ); |
| if ( baselinePom.isPresent() ) |
| { |
| Files.write( reportOutputDir.resolve( |
| "effective-pom-baseline-" + baselineInputs.getChecksum() + ".xml" ), |
| baselinePom.get().getValue().getBytes( StandardCharsets.UTF_8 ), |
| TRUNCATE_EXISTING, CREATE ); |
| } |
| } |
| catch ( IOException e ) |
| { |
| LOGGER.error( "Cannot produce build diff for project", e ); |
| } |
| } |
| else |
| { |
| LOGGER.info( "Cannot find project in baseline build, skipping diff" ); |
| } |
| } |
| |
| private List<Artifact> artifactDtos( List<org.apache.maven.artifact.Artifact> attachedArtifacts, |
| HashAlgorithm digest ) throws IOException |
| { |
| List<Artifact> result = new ArrayList<>(); |
| for ( org.apache.maven.artifact.Artifact attachedArtifact : attachedArtifacts ) |
| { |
| if ( attachedArtifact.getFile() != null && isOutputArtifact( attachedArtifact.getFile().getName() ) ) |
| { |
| result.add( artifactDto( attachedArtifact, digest ) ); |
| } |
| } |
| return result; |
| } |
| |
| private Artifact artifactDto( org.apache.maven.artifact.Artifact projectArtifact, |
| HashAlgorithm algorithm ) throws IOException |
| { |
| final Artifact dto = DtoUtils.createDto( projectArtifact ); |
| if ( projectArtifact.getFile() != null && projectArtifact.getFile().isFile() ) |
| { |
| final Path file = projectArtifact.getFile().toPath(); |
| dto.setFileHash( algorithm.hash( file ) ); |
| dto.setFileSize( Files.size( file ) ); |
| } |
| return dto; |
| } |
| |
| private List<CompletedExecution> buildExecutionInfo( List<MojoExecution> mojoExecutions, |
| Map<String, MojoExecutionEvent> executionEvents ) |
| { |
| List<CompletedExecution> list = new ArrayList<>(); |
| for ( MojoExecution mojoExecution : mojoExecutions ) |
| { |
| final String executionKey = CacheUtils.mojoExecutionKey( mojoExecution ); |
| final MojoExecutionEvent executionEvent = executionEvents != null ? executionEvents.get( executionKey ) |
| : null; |
| CompletedExecution executionInfo = new CompletedExecution(); |
| executionInfo.setExecutionKey( executionKey ); |
| executionInfo.setMojoClassName( mojoExecution.getMojoDescriptor().getImplementation() ); |
| if ( executionEvent != null ) |
| { |
| recordMojoProperties( executionInfo, executionEvent ); |
| } |
| list.add( executionInfo ); |
| } |
| return list; |
| } |
| |
| private void recordMojoProperties( CompletedExecution execution, MojoExecutionEvent executionEvent ) |
| { |
| final MojoExecution mojoExecution = executionEvent.getExecution(); |
| |
| final boolean logAll = cacheConfig.isLogAllProperties( mojoExecution ); |
| List<TrackedProperty> trackedProperties = cacheConfig.getTrackedProperties( mojoExecution ); |
| List<PropertyName> noLogProperties = cacheConfig.getNologProperties( mojoExecution ); |
| List<PropertyName> forceLogProperties = cacheConfig.getLoggedProperties( mojoExecution ); |
| final Mojo mojo = executionEvent.getMojo(); |
| |
| final File baseDir = executionEvent.getProject().getBasedir(); |
| final String baseDirPath = FilenameUtils.normalizeNoEndSeparator( baseDir.getAbsolutePath() ) + File.separator; |
| |
| final List<Parameter> parameters = mojoExecution.getMojoDescriptor().getParameters(); |
| for ( Parameter parameter : parameters ) |
| { |
| // editable parameters could be configured by user |
| if ( !parameter.isEditable() ) |
| { |
| continue; |
| } |
| |
| final String propertyName = parameter.getName(); |
| final boolean tracked = isTracked( propertyName, trackedProperties ); |
| if ( !tracked && isExcluded( propertyName, logAll, noLogProperties, forceLogProperties ) ) |
| { |
| continue; |
| } |
| |
| try |
| { |
| final Object value = ReflectionUtils.getValueIncludingSuperclasses( propertyName, mojo ); |
| DtoUtils.addProperty( execution, propertyName, value, baseDirPath, tracked ); |
| } |
| catch ( IllegalAccessException e ) |
| { |
| LOGGER.info( "Cannot get property {} value from {}: {}", propertyName, mojo, e.getMessage() ); |
| if ( tracked ) |
| { |
| throw new IllegalArgumentException( "Property configured in cache introspection config " |
| + "for " + mojo + " is not accessible: " + propertyName ); |
| } |
| } |
| } |
| } |
| |
| private boolean isExcluded( String propertyName, boolean logAll, List<PropertyName> excludedProperties, |
| List<PropertyName> forceLogProperties ) |
| { |
| if ( !forceLogProperties.isEmpty() ) |
| { |
| for ( PropertyName logProperty : forceLogProperties ) |
| { |
| if ( StringUtils.equals( propertyName, logProperty.getPropertyName() ) ) |
| { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| if ( !excludedProperties.isEmpty() ) |
| { |
| for ( PropertyName excludedProperty : excludedProperties ) |
| { |
| if ( StringUtils.equals( propertyName, excludedProperty.getPropertyName() ) ) |
| { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| return !logAll; |
| } |
| |
| private boolean isTracked( String propertyName, List<TrackedProperty> trackedProperties ) |
| { |
| for ( TrackedProperty trackedProperty : trackedProperties ) |
| { |
| if ( StringUtils.equals( propertyName, trackedProperty.getPropertyName() ) ) |
| { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean isCachedSegmentPropertiesPresent( MavenProject project, Build build, |
| List<MojoExecution> mojoExecutions ) |
| { |
| for ( MojoExecution mojoExecution : mojoExecutions ) |
| { |
| // completion of all mojos checked above, so we expect tp have execution info here |
| final List<TrackedProperty> trackedProperties = cacheConfig.getTrackedProperties( mojoExecution ); |
| final CompletedExecution cachedExecution = build.findMojoExecutionInfo( mojoExecution ); |
| |
| if ( cachedExecution == null ) |
| { |
| LOGGER.info( "Execution is not cached. Plugin: {}, goal {}", |
| mojoExecution.getExecutionId(), mojoExecution.getGoal() ); |
| return false; |
| } |
| |
| if ( !DtoUtils.containsAllProperties( cachedExecution, trackedProperties ) ) |
| { |
| LOGGER.info( "Build info doesn't match rules. Plugin: {}", |
| mojoExecution.getExecutionId() ); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean isForcedExecution( MavenProject project, MojoExecution execution ) |
| { |
| if ( cacheConfig.isForcedExecution( execution ) ) |
| { |
| return true; |
| } |
| |
| if ( StringUtils.isNotBlank( cacheConfig.getAlwaysRunPlugins() ) ) |
| { |
| String[] alwaysRunPluginsList = split( cacheConfig.getAlwaysRunPlugins(), "," ); |
| for ( String pluginAndGoal : alwaysRunPluginsList ) |
| { |
| String[] tokens = pluginAndGoal.split( ":" ); |
| String alwaysRunPlugin = tokens[0]; |
| String alwaysRunGoal = tokens.length == 1 ? "*" : tokens[1]; |
| if ( Objects.equals( execution.getPlugin().getArtifactId(), alwaysRunPlugin ) |
| && ( "*".equals( alwaysRunGoal ) |
| || Objects.equals( execution.getGoal(), alwaysRunGoal ) ) ) |
| { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public void saveCacheReport( MavenSession session ) |
| { |
| try |
| { |
| CacheReport cacheReport = new CacheReport(); |
| for ( CacheResult result : cacheResults.values() ) |
| { |
| ProjectReport projectReport = new ProjectReport(); |
| CacheContext context = result.getContext(); |
| MavenProject project = context.getProject(); |
| projectReport.setGroupId( project.getGroupId() ); |
| projectReport.setArtifactId( project.getArtifactId() ); |
| projectReport.setChecksum( context.getInputInfo().getChecksum() ); |
| boolean checksumMatched = result.getStatus() != RestoreStatus.EMPTY; |
| projectReport.setChecksumMatched( checksumMatched ); |
| projectReport.setLifecycleMatched( checksumMatched && result.isSuccess() ); |
| projectReport.setSource( String.valueOf( result.getSource() ) ); |
| if ( result.getSource() == CacheSource.REMOTE ) |
| { |
| projectReport.setUrl( remoteCache.getResourceUrl( context, BUILDINFO_XML ) ); |
| } |
| else if ( result.getSource() == CacheSource.BUILD && cacheConfig.isSaveToRemote() ) |
| { |
| projectReport.setSharedToRemote( true ); |
| projectReport.setUrl( remoteCache.getResourceUrl( context, BUILDINFO_XML ) ); |
| } |
| cacheReport.addProject( projectReport ); |
| } |
| |
| String buildId = UUID.randomUUID().toString(); |
| localCache.saveCacheReport( buildId, session, cacheReport ); |
| } |
| catch ( Exception e ) |
| { |
| LOGGER.error( "Cannot save incremental build aggregated report", e ); |
| } |
| } |
| |
| private void populateGitInfo( Build build, MavenSession session ) |
| { |
| if ( scm == null ) |
| { |
| synchronized ( this ) |
| { |
| if ( scm == null ) |
| { |
| try |
| { |
| scm = CacheUtils.readGitInfo( session ); |
| } |
| catch ( IOException e ) |
| { |
| scm = new Scm(); |
| LOGGER.error( "Cannot populate git info", e ); |
| } |
| } |
| } |
| } |
| build.getDto().setScm( scm ); |
| } |
| |
| private void zipAndAttachArtifact( MavenProject project, Path dir, String classifier ) throws IOException |
| { |
| Path temp = Files.createTempFile( "maven-incremental", project.getArtifactId() ); |
| temp.toFile().deleteOnExit(); |
| CacheUtils.zip( dir, temp ); |
| projectHelper.attachArtifact( project, "zip", classifier, temp.toFile() ); |
| } |
| |
| private String pathToClassifier( Path relative ) |
| { |
| final int nameCount = relative.getNameCount(); |
| List<String> segments = new ArrayList<>( nameCount + 1 ); |
| for ( int i = 0; i < nameCount; i++ ) |
| { |
| segments.add( relative.getName( i ).toFile().getName() ); |
| } |
| // todo handle _ in file names |
| return BUILD_PREFIX + StringUtils.join( segments.iterator(), FILE_SEPARATOR_SUBST ); |
| } |
| |
| private Path classifierToPath( Path outputDir, String classifier ) |
| { |
| classifier = StringUtils.removeStart( classifier, BUILD_PREFIX ); |
| final String relPath = replace( classifier, FILE_SEPARATOR_SUBST, File.separator ); |
| return outputDir.resolve( relPath ); |
| } |
| |
| private void restoreGeneratedSources( Artifact artifact, Path artifactFilePath, MavenProject project ) |
| throws IOException |
| { |
| final Path targetDir = Paths.get( project.getBuild().getDirectory() ); |
| final Path outputDir = classifierToPath( targetDir, artifact.getClassifier() ); |
| if ( Files.exists( outputDir ) ) |
| { |
| FileUtils.cleanDirectory( outputDir.toFile() ); |
| } |
| else |
| { |
| Files.createDirectories( outputDir ); |
| } |
| CacheUtils.unzip( artifactFilePath, outputDir ); |
| } |
| |
| // TODO: move to config |
| public void attachGeneratedSources( MavenProject project ) throws IOException |
| { |
| final Path targetDir = Paths.get( project.getBuild().getDirectory() ); |
| |
| final Path generatedSourcesDir = targetDir.resolve( "generated-sources" ); |
| attachDirIfNotEmpty( generatedSourcesDir, targetDir, project ); |
| |
| final Path generatedTestSourcesDir = targetDir.resolve( "generated-test-sources" ); |
| attachDirIfNotEmpty( generatedTestSourcesDir, targetDir, project ); |
| |
| Set<String> sourceRoots = new TreeSet<>(); |
| if ( project.getCompileSourceRoots() != null ) |
| { |
| sourceRoots.addAll( project.getCompileSourceRoots() ); |
| } |
| if ( project.getTestCompileSourceRoots() != null ) |
| { |
| sourceRoots.addAll( project.getTestCompileSourceRoots() ); |
| } |
| |
| for ( String sourceRoot : sourceRoots ) |
| { |
| final Path sourceRootPath = Paths.get( sourceRoot ); |
| if ( Files.isDirectory( sourceRootPath ) |
| && sourceRootPath.startsWith( targetDir ) |
| && !( sourceRootPath.startsWith( generatedSourcesDir ) |
| || sourceRootPath.startsWith( generatedTestSourcesDir ) ) ) |
| { // dir within target |
| attachDirIfNotEmpty( sourceRootPath, targetDir, project ); |
| } |
| } |
| } |
| |
| private void attachOutputs( MavenProject project ) throws IOException |
| { |
| final List<String> attachedDirs = cacheConfig.getAttachedOutputs(); |
| for ( String dir : attachedDirs ) |
| { |
| final Path targetDir = Paths.get( project.getBuild().getDirectory() ); |
| final Path outputDir = targetDir.resolve( dir ); |
| attachDirIfNotEmpty( outputDir, targetDir, project ); |
| } |
| } |
| |
| private void attachDirIfNotEmpty( Path candidateSubDir, Path parentDir, MavenProject project ) throws IOException |
| { |
| if ( Files.isDirectory( candidateSubDir ) && hasFiles( candidateSubDir ) ) |
| { |
| final Path relativePath = parentDir.relativize( candidateSubDir ); |
| final String classifier = pathToClassifier( relativePath ); |
| zipAndAttachArtifact( project, candidateSubDir, classifier ); |
| LOGGER.debug( "Attached directory: {}", candidateSubDir ); |
| } |
| } |
| |
| private boolean hasFiles( Path candidateSubDir ) throws IOException |
| { |
| final MutableBoolean hasFiles = new MutableBoolean(); |
| Files.walkFileTree( candidateSubDir, new SimpleFileVisitor<Path>() |
| { |
| |
| @Override |
| public FileVisitResult visitFile( Path path, BasicFileAttributes basicFileAttributes ) |
| { |
| hasFiles.setTrue(); |
| return FileVisitResult.TERMINATE; |
| } |
| } ); |
| return hasFiles.booleanValue(); |
| } |
| |
| private boolean isOutputArtifact( String name ) |
| { |
| List<Pattern> excludePatterns = cacheConfig.getExcludePatterns(); |
| for ( Pattern pattern : excludePatterns ) |
| { |
| if ( pattern.matcher( name ).matches() ) |
| { |
| return false; |
| } |
| } |
| return true; |
| } |
| } |