/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Brian Westrich, Jean-Baptiste Quenot
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.tasks;
import hudson.FilePath;
import jenkins.MasterToSlaveFileCallable;
import hudson.Launcher;
import hudson.Util;
import hudson.Extension;
import jenkins.util.SystemProperties;
import hudson.model.AbstractProject;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.listeners.ItemListener;
import hudson.remoting.VirtualChannel;
import hudson.util.FormValidation;
import java.io.File;
import org.apache.tools.ant.types.FileSet;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.QueryParameter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import net.sf.json.JSONObject;
import javax.annotation.Nonnull;
import jenkins.model.BuildDiscarder;
import jenkins.model.Jenkins;
import jenkins.tasks.SimpleBuildStep;
import jenkins.util.BuildListenerAdapter;
import org.kohsuke.stapler.DataBoundSetter;
/**
* Copies the artifacts into an archive directory.
*
* @author Kohsuke Kawaguchi
*/
public class ArtifactArchiver extends Recorder implements SimpleBuildStep {
private static final Logger LOG = Logger.getLogger(ArtifactArchiver.class.getName());
/**
* Comma- or space-separated list of patterns of files/directories to be archived.
*/
private String artifacts;
/**
* Possibly null 'excludes' pattern as in Ant.
*/
private String excludes;
@Deprecated
private Boolean latestOnly;
/**
* Fail (or not) the build if archiving returns nothing.
*/
@Nonnull
private Boolean allowEmptyArchive;
/**
* Archive only if build is successful, skip archiving on failed builds.
*/
private boolean onlyIfSuccessful;
private boolean fingerprint;
/**
* Default ant exclusion
*/
@Nonnull
private Boolean defaultExcludes = true;
/**
* Indicate whether include and exclude patterns should be considered as case sensitive
*/
@Nonnull
private Boolean caseSensitive = true;
@DataBoundConstructor public ArtifactArchiver(String artifacts) {
this.artifacts = artifacts.trim();
allowEmptyArchive = false;
}
@Deprecated
public ArtifactArchiver(String artifacts, String excludes, boolean latestOnly) {
this(artifacts, excludes, latestOnly, false, false);
}
@Deprecated
public ArtifactArchiver(String artifacts, String excludes, boolean latestOnly, boolean allowEmptyArchive) {
this(artifacts, excludes, latestOnly, allowEmptyArchive, false);
}
@Deprecated
public ArtifactArchiver(String artifacts, String excludes, boolean latestOnly, boolean allowEmptyArchive, boolean onlyIfSuccessful) {
this(artifacts, excludes , latestOnly , allowEmptyArchive, onlyIfSuccessful , true);
}
@Deprecated
public ArtifactArchiver(String artifacts, String excludes, boolean latestOnly, boolean allowEmptyArchive, boolean onlyIfSuccessful, Boolean defaultExcludes) {
this(artifacts);
setExcludes(excludes);
this.latestOnly = latestOnly;
setAllowEmptyArchive(allowEmptyArchive);
setOnlyIfSuccessful(onlyIfSuccessful);
setDefaultExcludes(defaultExcludes);
}
// Backwards compatibility for older builds
public Object readResolve() {
if (allowEmptyArchive == null) {
this.allowEmptyArchive = SystemProperties.getBoolean(ArtifactArchiver.class.getName()+".warnOnEmpty");
}
if (defaultExcludes == null){
defaultExcludes = true;
}
if (caseSensitive == null) {
caseSensitive = true;
}
return this;
}
public String getArtifacts() {
return artifacts;
}
public @CheckForNull String getExcludes() {
return excludes;
}
@DataBoundSetter public final void setExcludes(@CheckForNull String excludes) {
this.excludes = Util.fixEmptyAndTrim(excludes);
}
@Deprecated
public boolean isLatestOnly() {
return latestOnly != null ? latestOnly : false;
}
public boolean isOnlyIfSuccessful() {
return onlyIfSuccessful;
}
@DataBoundSetter public final void setOnlyIfSuccessful(boolean onlyIfSuccessful) {
this.onlyIfSuccessful = onlyIfSuccessful;
}
public boolean isFingerprint() {
return fingerprint;
}
/** Whether to fingerprint the artifacts after we archive them. */
@DataBoundSetter public void setFingerprint(boolean fingerprint) {
this.fingerprint = fingerprint;
}
public boolean getAllowEmptyArchive() {
return allowEmptyArchive;
}
@DataBoundSetter public final void setAllowEmptyArchive(boolean allowEmptyArchive) {
this.allowEmptyArchive = allowEmptyArchive;
}
public boolean isDefaultExcludes() {
return defaultExcludes;
}
@DataBoundSetter public final void setDefaultExcludes(boolean defaultExcludes) {
this.defaultExcludes = defaultExcludes;
}
public boolean isCaseSensitive() {
return caseSensitive;
}
@DataBoundSetter public final void setCaseSensitive(boolean caseSensitive) {
this.caseSensitive = caseSensitive;
}
private void listenerWarnOrError(TaskListener listener, String message) {
if (allowEmptyArchive) {
listener.getLogger().println(String.format("WARN: %s", message));
} else {
listener.error(message);
}
}
@Override
public void perform(Run<?,?> build, FilePath ws, Launcher launcher, TaskListener listener) throws InterruptedException {
if(artifacts.length()==0) {
listener.error(Messages.ArtifactArchiver_NoIncludes());
build.setResult(Result.FAILURE);
return;
}
if (onlyIfSuccessful && build.getResult() != null && build.getResult().isWorseThan(Result.UNSTABLE)) {
listener.getLogger().println(Messages.ArtifactArchiver_SkipBecauseOnlyIfSuccessful());
return;
}
listener.getLogger().println(Messages.ArtifactArchiver_ARCHIVING_ARTIFACTS());
try {
String artifacts = build.getEnvironment(listener).expand(this.artifacts);
Map<String,String> files = ws.act(new ListFiles(artifacts, excludes, defaultExcludes, caseSensitive));
if (!files.isEmpty()) {
build.pickArtifactManager().archive(ws, launcher, BuildListenerAdapter.wrap(listener), files);
if (fingerprint) {
new Fingerprinter(artifacts).perform(build, ws, launcher, listener);
}
} else {
Result result = build.getResult();
if (result != null && result.isBetterOrEqualTo(Result.UNSTABLE)) {
// If the build failed, don't complain that there was no matching artifact.
// The build probably didn't even get to the point where it produces artifacts.
listenerWarnOrError(listener, Messages.ArtifactArchiver_NoMatchFound(artifacts));
String msg = null;
try {
msg = ws.validateAntFileMask(artifacts, FilePath.VALIDATE_ANT_FILE_MASK_BOUND, caseSensitive);
} catch (Exception e) {
listenerWarnOrError(listener, e.getMessage());
}
if(msg!=null)
listenerWarnOrError(listener, msg);
}
if (!allowEmptyArchive) {
build.setResult(Result.FAILURE);
}
return;
}
} catch (IOException e) {
Util.displayIOException(e,listener);
e.printStackTrace(listener.error(
Messages.ArtifactArchiver_FailedToArchive(artifacts)));
build.setResult(Result.FAILURE);
return;
}
}
private static final class ListFiles extends MasterToSlaveFileCallable<Map<String,String>> {
private static final long serialVersionUID = 1;
private final String includes, excludes;
private final boolean defaultExcludes;
private final boolean caseSensitive;
ListFiles(String includes, String excludes, boolean defaultExcludes, boolean caseSensitive) {
this.includes = includes;
this.excludes = excludes;
this.defaultExcludes = defaultExcludes;
this.caseSensitive = caseSensitive;
}
@Override public Map<String,String> invoke(File basedir, VirtualChannel channel) throws IOException, InterruptedException {
Map<String,String> r = new HashMap<String,String>();
FileSet fileSet = Util.createFileSet(basedir, includes, excludes);
fileSet.setDefaultexcludes(defaultExcludes);
fileSet.setCaseSensitive(caseSensitive);
for (String f : fileSet.getDirectoryScanner().getIncludedFiles()) {
f = f.replace(File.separatorChar, '/');
r.put(f, f);
}
return r;
}
}
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}
/**
* @deprecated as of 1.286
* Some plugin depends on this, so this field is left here and points to the last created instance.
* Use {@link jenkins.model.Jenkins#getDescriptorByType(Class)} instead.
*/
@Deprecated
public static volatile DescriptorImpl DESCRIPTOR;
@Extension @Symbol("archiveArtifacts")
public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {
public DescriptorImpl() {
DESCRIPTOR = this; // backward compatibility
}
public String getDisplayName() {
return Messages.ArtifactArchiver_DisplayName();
}
/**
* Performs on-the-fly validation of the file mask wildcard, when the artifacts
* textbox or the caseSensitive checkbox are modified
*/
public FormValidation doCheckArtifacts(@AncestorInPath AbstractProject project,
@QueryParameter String value,
@QueryParameter(value = "caseSensitive") String caseSensitive)
throws IOException {
if (project == null) {
return FormValidation.ok();
}
// defensive approach to remain case sensitive in doubtful situations
boolean bCaseSensitive = caseSensitive == null || !"false".equals(caseSensitive);
return FilePath.validateFileMask(project.getSomeWorkspace(), value, bCaseSensitive);
}
@Override
public ArtifactArchiver newInstance(StaplerRequest req, JSONObject formData) throws FormException {
return req.bindJSON(ArtifactArchiver.class,formData);
}
public boolean isApplicable(Class<? extends AbstractProject> jobType) {
return true;
}
}
@Extension public static final class Migrator extends ItemListener {
@SuppressWarnings("deprecation")
@Override public void onLoaded() {
for (AbstractProject<?,?> p : Jenkins.getInstance().getAllItems(AbstractProject.class)) {
try {
ArtifactArchiver aa = p.getPublishersList().get(ArtifactArchiver.class);
if (aa != null && aa.latestOnly != null) {
if (aa.latestOnly) {
BuildDiscarder bd = p.getBuildDiscarder();
if (bd instanceof LogRotator) {
LogRotator lr = (LogRotator) bd;
if (lr.getArtifactNumToKeep() == -1) {
p.setBuildDiscarder(new LogRotator(lr.getDaysToKeep(), lr.getNumToKeep(), lr.getArtifactDaysToKeep(), 1));
} else {
LOG.log(Level.WARNING, "will not clobber artifactNumToKeep={0} in {1}", new Object[] {lr.getArtifactNumToKeep(), p});
}
} else if (bd == null) {
p.setBuildDiscarder(new LogRotator(-1, -1, -1, 1));
} else {
LOG.log(Level.WARNING, "unrecognized BuildDiscarder {0} in {1}", new Object[] {bd, p});
}
}
aa.latestOnly = null;
p.save();
}
Fingerprinter f = p.getPublishersList().get(Fingerprinter.class);
if (f != null && f.getRecordBuildArtifacts()) {
f.recordBuildArtifacts = null;
if (aa != null) {
aa.setFingerprint(true);
}
if (f.getTargets().isEmpty()) { // no other reason to be here
p.getPublishersList().remove(f);
}
p.save();
}
} catch (IOException x) {
LOG.log(Level.WARNING, "could not migrate " + p, x);
}
}
}
}
}