diff --git a/changelog.html b/changelog.html index 4dc57a0d0e1553d94b87d0277047636f312e631b..07bf978a687ccd2e1a69aefeb5adee0e785eb961 100644 --- a/changelog.html +++ b/changelog.html @@ -67,6 +67,9 @@ Upcoming changes
  • Display the full display name in title for jobs and views. (pull request 884) +
  • + Added a new extension point to control where archived artifacts get stored. + (issue 17236) diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index f2ed61022732e2bd4c399427c8af16277c033f98..652e4f981b3fc97ad2fdee41e16187cc2b06220c 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -53,10 +53,9 @@ import static hudson.FilePath.TarCompression.GZIP; import hudson.org.apache.tools.tar.TarInputStream; import hudson.util.io.Archiver; import hudson.util.io.ArchiverFactory; -import org.apache.tools.ant.BuildException; +import jenkins.util.VirtualFile; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.Project; -import org.apache.tools.ant.taskdefs.Copy; import org.apache.tools.ant.types.FileSet; import org.apache.tools.tar.TarEntry; import org.apache.commons.io.input.CountingInputStream; @@ -98,8 +97,10 @@ import com.jcraft.jzlib.GZIPOutputStream; import com.sun.jna.Native; import hudson.os.PosixException; -import java.io.BufferedInputStream; +import hudson.util.FileVisitor; import java.util.Enumeration; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Logger; import org.apache.tools.ant.taskdefs.Chmod; @@ -197,7 +198,7 @@ public final class FilePath implements Serializable { * that's connected to that machine. If null, that means the local file path. */ public FilePath(VirtualChannel channel, String remote) { - this.channel = channel; + this.channel = channel == Jenkins.MasterComputer.localChannel ? null : channel; this.remote = normalize(remote); } @@ -1041,6 +1042,13 @@ public final class FilePath implements Serializable { }); } + /** + * Gets the {@link VirtualFile} representation of this {@link FilePath} + */ + public VirtualFile toVirtualFile() { + return VirtualFile.forFilePath(this); + } + /** * Creates this directory. */ @@ -1880,6 +1888,18 @@ public final class FilePath implements Serializable { * the number of files copied. */ public int copyRecursiveTo(final String fileMask, final String excludes, final FilePath target) throws IOException, InterruptedException { + return copyRecursiveTo(new DirScanner.Glob(fileMask, excludes), target, fileMask); + } + + /** + * Copies files according to a specified scanner to a target node. + * @param scanner a way of enumerating some files (must be serializable for possible delivery to remote side) + * @param target the destination basedir + * @param description a description of the fileset, for logging purposes + * @return the number of files copied + * @since 1.531 + */ + public int copyRecursiveTo(final DirScanner scanner, final FilePath target, final String description) throws IOException, InterruptedException { if(this.channel==target.channel) { // local to local copy. return act(new FileCallable() { @@ -1887,37 +1907,30 @@ public final class FilePath implements Serializable { public Integer invoke(File base, VirtualChannel channel) throws IOException { if(!base.exists()) return 0; assert target.channel==null; - - try { - class CopyImpl extends Copy { - private int copySize; - - public CopyImpl() { - setProject(new org.apache.tools.ant.Project()); - } - - @Override - protected void doFileOperations() { - copySize = super.fileCopyMap.size(); - super.doFileOperations(); + final File dest = new File(target.remote); + final AtomicInteger count = new AtomicInteger(); + scanner.scan(base, new FileVisitor() { + @Override public void visit(File f, String relativePath) throws IOException { + if (f.isFile()) { + File target = new File(dest, relativePath); + target.getParentFile().mkdirs(); + Util.copyFile(f, target); + count.incrementAndGet(); } - - public int getNumCopied() { - return copySize; + } + @Override public boolean understandsSymlink() { + return true; + } + @Override public void visitSymlink(File link, String target, String relativePath) throws IOException { + try { + Util.createSymlink(dest, target, relativePath, TaskListener.NULL); + } catch (InterruptedException x) { + throw (IOException) new IOException(x.toString()).initCause(x); } + count.incrementAndGet(); } - - CopyImpl copyTask = new CopyImpl(); - copyTask.setTodir(new File(target.remote)); - copyTask.addFileset(Util.createFileSet(base,fileMask,excludes)); - copyTask.setOverwrite(true); - copyTask.setIncludeEmptyDirs(false); - - copyTask.execute(); - return copyTask.getNumCopied(); - } catch (BuildException e) { - throw new IOException2("Failed to copy "+base+"/"+fileMask+" to "+target,e); - } + }); + return count.get(); } }); } else @@ -1929,7 +1942,7 @@ public final class FilePath implements Serializable { private static final long serialVersionUID = 1L; public Void invoke(File f, VirtualChannel channel) throws IOException { try { - readFromTar(remote+'/'+fileMask, f,TarCompression.GZIP.extract(pipe.getIn())); + readFromTar(remote + '/' + description, f,TarCompression.GZIP.extract(pipe.getIn())); return null; } finally { pipe.getIn().close(); @@ -1939,7 +1952,7 @@ public final class FilePath implements Serializable { Future future2 = actAsync(new FileCallable() { private static final long serialVersionUID = 1L; @Override public Integer invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { - return writeToTar(new File(remote),fileMask,excludes,TarCompression.GZIP.compress(pipe.getOut())); + return writeToTar(new File(remote), scanner, TarCompression.GZIP.compress(pipe.getOut())); } }); try { @@ -1957,14 +1970,14 @@ public final class FilePath implements Serializable { private static final long serialVersionUID = 1L; public Integer invoke(File f, VirtualChannel channel) throws IOException { try { - return writeToTar(f,fileMask,excludes,TarCompression.GZIP.compress(pipe.getOut())); + return writeToTar(f, scanner, TarCompression.GZIP.compress(pipe.getOut())); } finally { pipe.getOut().close(); } } }); try { - readFromTar(remote+'/'+fileMask,new File(target.remote),TarCompression.GZIP.extract(pipe.getIn())); + readFromTar(remote + '/' + description,new File(target.remote),TarCompression.GZIP.extract(pipe.getIn())); } catch (IOException e) {// BuildException or IOException try { future.get(3,TimeUnit.SECONDS); @@ -2013,10 +2026,10 @@ public final class FilePath implements Serializable { * @return * number of files/directories that are written. */ - private static Integer writeToTar(File baseDir, String fileMask, String excludes, OutputStream out) throws IOException { + private static Integer writeToTar(File baseDir, DirScanner scanner, OutputStream out) throws IOException { Archiver tw = ArchiverFactory.TAR.create(out); try { - new DirScanner.Glob(fileMask,excludes).scan(baseDir,tw); + scanner.scan(baseDir,tw); } finally { tw.close(); } @@ -2479,4 +2492,35 @@ public final class FilePath implements Serializable { } }); } + + /** + * Helper class to make it easy to send an explicit list of files using {@link FilePath} methods. + * @since 1.531 + */ + public static final class ExplicitlySpecifiedDirScanner extends DirScanner { + + private static final long serialVersionUID = 1; + + private final Map files; + + /** + * Create a “scanner” (it actually does no scanning). + * @param files a map from logical relative paths as per {@link FileVisitor#visit}, to actual relative paths within the scanned directory + */ + public ExplicitlySpecifiedDirScanner(Map files) { + this.files = files; + } + + @Override public void scan(File dir, FileVisitor visitor) throws IOException { + for (Map.Entry entry : files.entrySet()) { + String archivedPath = entry.getKey(); + assert archivedPath.indexOf('\\') == -1; + String workspacePath = entry.getValue(); + assert workspacePath.indexOf('\\') == -1; + scanSingle(new File(dir, workspacePath), archivedPath, visitor); + } + } + + } + } diff --git a/core/src/main/java/hudson/model/DirectoryBrowserSupport.java b/core/src/main/java/hudson/model/DirectoryBrowserSupport.java index d924710c765721eb636d924e43c4a3a350a273ec..3fcac328a988c7266436511242ed013180a47fc2 100644 --- a/core/src/main/java/hudson/model/DirectoryBrowserSupport.java +++ b/core/src/main/java/hudson/model/DirectoryBrowserSupport.java @@ -26,19 +26,13 @@ package hudson.model; import hudson.FilePath; import hudson.Util; import hudson.util.IOException2; -import hudson.FilePath.FileCallable; -import hudson.remoting.VirtualChannel; import jenkins.model.Jenkins; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.HttpResponse; -import org.apache.tools.ant.types.FileSet; -import org.apache.tools.ant.DirectoryScanner; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; -import java.io.File; -import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -53,6 +47,9 @@ import java.util.Locale; import java.util.StringTokenizer; import java.util.logging.Logger; import java.util.logging.Level; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import jenkins.util.VirtualFile; /** * Has convenience methods to serve file system. @@ -69,7 +66,7 @@ public final class DirectoryBrowserSupport implements HttpResponse { public final String title; - private final FilePath base; + private final VirtualFile base; private final String icon; private final boolean serveDirIndex; private String indexFileName = "index.html"; @@ -79,7 +76,7 @@ public final class DirectoryBrowserSupport implements HttpResponse { * Use {@link #DirectoryBrowserSupport(ModelObject, FilePath, String, String, boolean)} */ public DirectoryBrowserSupport(ModelObject owner, String title) { - this(owner,null,title,null,false); + this(owner, (VirtualFile) null, title, null, false); } /** @@ -96,6 +93,24 @@ public final class DirectoryBrowserSupport implements HttpResponse { * False to serve "index.html" */ public DirectoryBrowserSupport(ModelObject owner, FilePath base, String title, String icon, boolean serveDirIndex) { + this(owner, base.toVirtualFile(), title, icon, serveDirIndex); + } + + /** + * @param owner + * The parent model object under which the directory browsing is added. + * @param base + * The root of the directory that's bound to URL. + * @param title + * Used in the HTML caption. + * @param icon + * The icon file name, like "folder.gif" + * @param serveDirIndex + * True to generate the directory index. + * False to serve "index.html" + * @since 1.531 + */ + public DirectoryBrowserSupport(ModelObject owner, VirtualFile base, String title, String icon, boolean serveDirIndex) { this.owner = owner; this.base = base; this.title = title; @@ -133,6 +148,10 @@ public final class DirectoryBrowserSupport implements HttpResponse { * from the {@code doXYZ} method and let Stapler generate a response for you. */ public void serveFile(StaplerRequest req, StaplerResponse rsp, FilePath root, String icon, boolean serveDirIndex) throws IOException, ServletException, InterruptedException { + serveFile(req, rsp, root.toVirtualFile(), icon, serveDirIndex); + } + + private void serveFile(StaplerRequest req, StaplerResponse rsp, VirtualFile root, String icon, boolean serveDirIndex) throws IOException, ServletException, InterruptedException { // handle form submission String pattern = req.getParameter("pattern"); if(pattern==null) @@ -163,7 +182,7 @@ public final class DirectoryBrowserSupport implements HttpResponse { String pathElement = pathTokens.nextToken(); // Treat * and ? as wildcard unless they match a literal filename if((pathElement.contains("?") || pathElement.contains("*")) - && inBase && !(new FilePath(root, (_base.length() > 0 ? _base + "/" : "") + pathElement).exists())) + && inBase && !root.child((_base.length() > 0 ? _base + "/" : "") + pathElement).exists()) inBase = false; if(pathElement.equals("*zip*")) { // the expected syntax is foo/bar/*zip*/bar.zip @@ -189,20 +208,23 @@ public final class DirectoryBrowserSupport implements HttpResponse { String rest = _rest.toString(); // this is the base file/directory - FilePath baseFile = new FilePath(root,base); + VirtualFile baseFile = root.child(base); if(baseFile.isDirectory()) { if(zip) { rsp.setContentType("application/zip"); - baseFile.zip(rsp.getOutputStream(),rest); + zip(rsp.getOutputStream(), baseFile, rest); return; } if (plain) { rsp.setContentType("text/plain;charset=UTF-8"); OutputStream os = rsp.getOutputStream(); try { - for (String kid : baseFile.act(new SimpleChildList())) { - os.write(kid.getBytes("UTF-8")); + for (VirtualFile kid : baseFile.list()) { + os.write(kid.getName().getBytes("UTF-8")); + if (kid.isDirectory()) { + os.write('/'); + } os.write('\n'); } os.flush(); @@ -221,15 +243,15 @@ public final class DirectoryBrowserSupport implements HttpResponse { } } - FileCallable>> glob = null; + List> glob = null; if(rest.length()>0) { // the rest is Ant glob pattern - glob = new PatternScanner(rest,createBackRef(restSize)); + glob = patternScan(baseFile, rest, createBackRef(restSize)); } else if(serveDirIndex) { // serve directory index - glob = new ChildPathBuilder(req.getLocale()); + glob = buildChildPaths(baseFile, req.getLocale()); } if(glob!=null) { @@ -239,7 +261,7 @@ public final class DirectoryBrowserSupport implements HttpResponse { req.setAttribute("parentPath",parentPaths); req.setAttribute("backPath", createBackRef(restSize)); req.setAttribute("topPath", createBackRef(parentPaths.size()+restSize)); - req.setAttribute("files", baseFile.act(glob)); + req.setAttribute("files", glob); req.setAttribute("icon", icon); req.setAttribute("path", path); req.setAttribute("pattern",rest); @@ -262,24 +284,25 @@ public final class DirectoryBrowserSupport implements HttpResponse { boolean view = rest.equals("*view*"); if(rest.equals("*fingerprint*")) { - rsp.forward(Jenkins.getInstance().getFingerprint(baseFile.digest()),"/",req); + rsp.forward(Jenkins.getInstance().getFingerprint(Util.getDigestOf(baseFile.open())), "/", req); return; } - ContentInfo ci = baseFile.act(new ContentInfo()); + long lastModified = baseFile.lastModified(); + long length = baseFile.length(); if(LOGGER.isLoggable(Level.FINE)) - LOGGER.fine("Serving "+baseFile+" with lastModified="+ci.lastModified+", contentLength="+ci.contentLength); + LOGGER.fine("Serving "+baseFile+" with lastModified=" + lastModified + ", length=" + length); - InputStream in = baseFile.read(); + InputStream in = baseFile.open(); if (view) { // for binary files, provide the file name for download rsp.setHeader("Content-Disposition", "inline; filename=" + baseFile.getName()); // pseudo file name to let the Stapler set text/plain - rsp.serveFile(req, in, ci.lastModified, -1, ci.contentLength, "plain.txt"); + rsp.serveFile(req, in, lastModified, -1, length, "plain.txt"); } else { - rsp.serveFile(req, in, ci.lastModified, -1, ci.contentLength, baseFile.getName() ); + rsp.serveFile(req, in, lastModified, -1, length, baseFile.getName() ); } } @@ -290,19 +313,6 @@ public final class DirectoryBrowserSupport implements HttpResponse { return path; } - private static final class ContentInfo implements FileCallable { - long contentLength; - long lastModified; - - public ContentInfo invoke(File f, VirtualChannel channel) throws IOException { - contentLength = f.length(); - lastModified = f.lastModified(); - return this; - } - - private static final long serialVersionUID = 1L; - } - /** * Builds a list of {@link Path} that represents ancestors * from a string like "/foo/bar/zot". @@ -328,6 +338,19 @@ public final class DirectoryBrowserSupport implements HttpResponse { return buf.toString(); } + private static void zip(OutputStream outputStream, VirtualFile dir, String glob) throws IOException { + ZipOutputStream zos = new ZipOutputStream(outputStream); + for (String n : dir.list(glob.length() == 0 ? "**" : glob)) { + ZipEntry e = new ZipEntry(n); + VirtualFile f = dir.child(n); + e.setTime(f.lastModified()); + zos.putNextEntry(e); + Util.copyStream(f.open(), outputStream); + zos.closeEntry(); + } + zos.close(); + } + /** * Represents information about one file or folder. */ @@ -349,7 +372,7 @@ public final class DirectoryBrowserSupport implements HttpResponse { private final long size; /** - * If the current user can read the file. + * If the current user can read the file. */ private final boolean isReadable; @@ -393,14 +416,14 @@ public final class DirectoryBrowserSupport implements HttpResponse { - private static final class FileComparator implements Comparator { + private static final class FileComparator implements Comparator { private Collator collator; public FileComparator(Locale locale) { this.collator = Collator.getInstance(locale); } - public int compare(File lhs, File rhs) { + public int compare(VirtualFile lhs, VirtualFile rhs) { // directories first, files next int r = dirRank(lhs)-dirRank(rhs); if(r!=0) return r; @@ -408,29 +431,13 @@ public final class DirectoryBrowserSupport implements HttpResponse { return this.collator.compare(lhs.getName(), rhs.getName()); } - private int dirRank(File f) { + private int dirRank(VirtualFile f) { + try { if(f.isDirectory()) return 0; else return 1; - } - } - - /** - * Simple list of names of children of a folder. - * Subfolders will have a trailing slash appended. - */ - private static final class SimpleChildList implements FileCallable> { - private static final long serialVersionUID = 1L; - public List invoke(File f, VirtualChannel channel) throws IOException { - List r = new ArrayList(); - String[] kids = f.list(); // no need to sort - for (String kid : kids) { - if (new File(f, kid).isDirectory()) { - r.add(kid + "/"); - } else { - r.add(kid); - } + } catch (IOException ex) { + return 0; } - return r; } } @@ -439,22 +446,14 @@ public final class DirectoryBrowserSupport implements HttpResponse { * list of {@link Path} represents one child item to be shown * (this mechanism is used to skip empty intermediate directory.) */ - private static final class ChildPathBuilder implements FileCallable>> { - private Locale locale; - - public ChildPathBuilder(Locale locale) { - this.locale = locale; - } - - public List> invoke(File cur, VirtualChannel channel) throws IOException { + private static List> buildChildPaths(VirtualFile cur, Locale locale) throws IOException { List> r = new ArrayList>(); - File[] files = cur.listFiles(); - if (files != null) { - Arrays.sort(files,new FileComparator(this.locale)); + VirtualFile[] files = cur.list(); + Arrays.sort(files,new FileComparator(locale)); - for( File f : files ) { - Path p = new Path(Util.rawEncode(f.getName()),f.getName(),f.isDirectory(),f.length(), f.canRead()); + for( VirtualFile f : files ) { + Path p = new Path(Util.rawEncode(f.getName()), f.getName(), f.isDirectory(), f.length(), f.canRead()); if(!f.isDirectory()) { r.add(Collections.singletonList(p)); } else { @@ -464,53 +463,38 @@ public final class DirectoryBrowserSupport implements HttpResponse { String relPath = Util.rawEncode(f.getName()); while(true) { // files that don't start with '.' qualify for 'meaningful files', nor SCM related files - File[] sub = f.listFiles(new FilenameFilter() { - public boolean accept(File dir, String name) { - return !name.startsWith(".") && !name.equals("CVS") && !name.equals(".svn"); + List sub = new ArrayList(); + for (VirtualFile vf : f.list()) { + String name = vf.getName(); + if (!name.startsWith(".") && !name.equals("CVS") && !name.equals(".svn")) { + sub.add(vf); } - }); - if(sub==null || sub.length!=1 || !sub[0].isDirectory()) + } + if (sub.size() !=1 || !sub.get(0).isDirectory()) break; - f = sub[0]; + f = sub.get(0); relPath += '/'+Util.rawEncode(f.getName()); l.add(new Path(relPath,f.getName(),true,0, f.canRead())); } r.add(l); } } - } return r; - } - - private static final long serialVersionUID = 1L; } /** * Runs ant GLOB against the current {@link FilePath} and returns matching * paths. + * @param baseRef String like "../../../" that cancels the 'rest' portion. Can be "./" */ - private static class PatternScanner implements FileCallable>> { - private final String pattern; - /** - * String like "../../../" that cancels the 'rest' portion. Can be "./" - */ - private final String baseRef; - - public PatternScanner(String pattern,String baseRef) { - this.pattern = pattern; - this.baseRef = baseRef; - } - - public List> invoke(File baseDir, VirtualChannel channel) throws IOException { - FileSet fs = Util.createFileSet(baseDir,pattern); - DirectoryScanner ds = fs.getDirectoryScanner(); - String[] files = ds.getIncludedFiles(); + private static List> patternScan(VirtualFile baseDir, String pattern, String baseRef) throws IOException { + String[] files = baseDir.list(pattern); if (files.length > 0) { List> r = new ArrayList>(files.length); for (String match : files) { - List file = buildPathList(baseDir, new File(baseDir,match)); + List file = buildPathList(baseDir, baseDir.child(match), baseRef); r.add(file); } return r; @@ -522,7 +506,7 @@ public final class DirectoryBrowserSupport implements HttpResponse { /** * Builds a path list from the current workspace directory down to the specified file path. */ - private List buildPathList(File baseDir, File filePath) throws IOException { + private static List buildPathList(VirtualFile baseDir, VirtualFile filePath, String baseRef) throws IOException { List pathList = new ArrayList(); StringBuilder href = new StringBuilder(baseRef); @@ -533,8 +517,8 @@ public final class DirectoryBrowserSupport implements HttpResponse { /** * Builds the path list and href recursively top-down. */ - private void buildPathList(File baseDir, File filePath, List pathList, StringBuilder href) throws IOException { - File parent = filePath.getParentFile(); + private static void buildPathList(VirtualFile baseDir, VirtualFile filePath, List pathList, StringBuilder href) throws IOException { + VirtualFile parent = filePath.getParent(); if (!baseDir.equals(parent)) { buildPathList(baseDir, parent, pathList, href); } @@ -548,8 +532,6 @@ public final class DirectoryBrowserSupport implements HttpResponse { pathList.add(path); } - private static final long serialVersionUID = 1L; - } private static final Logger LOGGER = Logger.getLogger(DirectoryBrowserSupport.class.getName()); } diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java index eb2f44d6b105373929cf1e14fa4ecda0718f9900..bdac8d5b141457cfd36ecfd9960151121c5ef7e5 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -52,14 +52,12 @@ import hudson.security.AccessControlled; import hudson.security.Permission; import hudson.security.PermissionGroup; import hudson.tasks.BuildWrapper; -import hudson.tasks.BuildStep; import hudson.tasks.test.AbstractTestResultAction; import hudson.util.FlushProofOutputStream; import hudson.util.FormApply; import hudson.util.IOException2; import hudson.util.LogTaskListener; import hudson.util.XStream2; -import hudson.util.ProcessTree; import java.io.BufferedReader; import java.io.File; @@ -121,8 +119,13 @@ import java.io.StringWriter; import static java.util.logging.Level.*; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; +import jenkins.model.ArtifactManager; +import jenkins.model.ArtifactManagerConfiguration; +import jenkins.model.ArtifactManagerFactory; import jenkins.model.PeepholePermalink; +import jenkins.model.StandardArtifactManager; import jenkins.model.RunAction2; +import jenkins.util.VirtualFile; /** * A particular execution of {@link Job}. @@ -257,6 +260,12 @@ public abstract class Run ,RunT extends Run ID_FORMATTER = new IDFormatterProvider(); private static final class IDFormatterProvider extends ThreadLocal { @@ -326,6 +335,9 @@ public abstract class Run ,RunT extends Run,RunT extends RunThis method should be used when existing artifacts are to be loaded, displayed, or removed. + * If adding artifacts, use {@link #pickArtifactManager} instead. + * @return an appropriate artifact manager + * @since 1.531 + */ + public final @Nonnull ArtifactManager getArtifactManager() { + return artifactManager != null ? artifactManager : new StandardArtifactManager(this); + } + + /** + * Selects an object responsible for storing and retrieving build artifacts. + * The first time this is called on a running build, {@link ArtifactManagerConfiguration} is checked + * to see if one will handle this build. + * If so, that manager is saved in the build and it will be used henceforth. + * If no manager claimed the build, {@link StandardArtifactManager} is used. + *

    This method should be used when a build step expects to archive some artifacts. + * If only displaying existing artifacts, use {@link #getArtifactManager} instead. + * @return an appropriate artifact manager + * @throws IOException if a custom manager was selected but the selection could not be saved + * @since 1.531 + */ + public final synchronized @Nonnull ArtifactManager pickArtifactManager() throws IOException { + if (artifactManager != null) { + return artifactManager; + } else { + for (ArtifactManagerFactory f : ArtifactManagerConfiguration.get().getArtifactManagerFactories()) { + ArtifactManager mgr = f.managerFor(this); + if (mgr != null) { + artifactManager = mgr; + save(); + return mgr; + } + } + return new StandardArtifactManager(this); + } + } + /** * Gets the directory where the artifacts are archived. + * @deprecated Should only be used from {@link StandardArtifactManager} or subclasses. */ + @Deprecated public File getArtifactsDir() { return new File(getRootDir(),"archive"); } @@ -984,7 +1040,11 @@ public abstract class Run ,RunT extends Run getArtifactsUpTo(int n) { ArtifactList r = new ArtifactList(); - addArtifacts(getArtifactsDir(),"","",r,null,n); + try { + addArtifacts(getArtifactManager().root(), "", "", r, null, n); + } catch (IOException x) { + LOGGER.log(Level.WARNING, null, x); + } r.computeDisplayName(); return r; } @@ -999,18 +1059,17 @@ public abstract class Run ,RunT extends Run,RunT extends Run,RunT extends Run,RunT extends Run files = ws.act(new ListFiles(artifacts, excludes)); + if (!files.isEmpty()) { + build.pickArtifactManager().archive(ws, launcher, listener, files); + } 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. @@ -165,6 +169,23 @@ public class ArtifactArchiver extends Recorder { return true; } + private static final class ListFiles implements FilePath.FileCallable> { + private static final long serialVersionUID = 1; + private final String includes, excludes; + ListFiles(String includes, String excludes) { + this.includes = includes; + this.excludes = excludes; + } + @Override public Map invoke(File basedir, VirtualChannel channel) throws IOException, InterruptedException { + Map r = new HashMap(); + for (String f : Util.createFileSet(basedir, includes, excludes).getDirectoryScanner().getIncludedFiles()) { + f = f.replace(File.separatorChar, '/'); + r.put(f, f); + } + return r; + } + } + @Override public boolean prebuild(AbstractBuild build, BuildListener listener) { if(latestOnly) { @@ -176,14 +197,14 @@ public class ArtifactArchiver extends Recorder { bestResultSoFar = b.getResult(); } else { // remove old artifacts - File ad = b.getArtifactsDir(); - if(ad.exists()) { - listener.getLogger().println(Messages.ArtifactArchiver_DeletingOld(b.getDisplayName())); - try { - Util.deleteRecursive(ad); - } catch (IOException e) { - e.printStackTrace(listener.error(e.getMessage())); + try { + if (b.getArtifactManager().delete()) { + listener.getLogger().println(Messages.ArtifactArchiver_DeletingOld(b.getDisplayName())); } + } catch (IOException e) { + e.printStackTrace(listener.error(e.getMessage())); + } catch (InterruptedException x) { + x.printStackTrace(listener.error(x.getMessage())); } } } diff --git a/core/src/main/java/hudson/util/DirScanner.java b/core/src/main/java/hudson/util/DirScanner.java index c11ae9c62295120e1d80312c382288e518c0c6b8..81a19f5b166eda2f6b401e384d34458f6559f3b1 100644 --- a/core/src/main/java/hudson/util/DirScanner.java +++ b/core/src/main/java/hudson/util/DirScanner.java @@ -26,29 +26,36 @@ public abstract class DirScanner implements Serializable { */ public abstract void scan(File dir, FileVisitor visitor) throws IOException; + /** + * @since 1.531 + */ + protected final void scanSingle(File f, String relative, FileVisitor visitor) throws IOException { + if (visitor.understandsSymlink()) { + try { + String target; + try { + target = Util.resolveSymlink(f); + } catch (IOException x) { // JENKINS-13202 + target = null; + } + if (target != null) { + visitor.visitSymlink(f, target, relative); + return; + } + } catch (InterruptedException e) { + throw (IOException) new InterruptedIOException().initCause(e); + } + } + visitor.visit(f, relative); + } + /** * Scans everything recursively. */ public static class Full extends DirScanner { private void scan(File f, String path, FileVisitor visitor) throws IOException { if (f.canRead()) { - if (visitor.understandsSymlink()) { - try { - String target; - try { - target = Util.resolveSymlink(f); - } catch (IOException x) { // JENKINS-13202 - target = null; - } - if (target!=null) { - visitor.visitSymlink(f,target,path+f.getName()); - return; - } - } catch (InterruptedException e) { - throw (IOException)new InterruptedIOException().initCause(e); - } - } - visitor.visit(f,path+f.getName()); + scanSingle(f, path + f.getName(), visitor); if(f.isDirectory()) { for( File child : f.listFiles() ) scan(child,path+f.getName()+'/',visitor); @@ -113,24 +120,7 @@ public abstract class DirScanner implements Serializable { DirectoryScanner ds = fs.getDirectoryScanner(new org.apache.tools.ant.Project()); for( String f : ds.getIncludedFiles()) { File file = new File(dir, f); - - if (visitor.understandsSymlink()) { - try { - String target; - try { - target = Util.resolveSymlink(file); - } catch (IOException x) { // JENKINS-13202 - target = null; - } - if (target!=null) { - visitor.visitSymlink(file,target,f); - continue; - } - } catch (InterruptedException e) { - throw (IOException)new InterruptedIOException().initCause(e); - } - } - visitor.visit(file,f); + scanSingle(file, f, visitor); } } } diff --git a/core/src/main/java/jenkins/model/ArtifactManager.java b/core/src/main/java/jenkins/model/ArtifactManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a93e053e1b894b2f73ff8143eb488f9532b81def --- /dev/null +++ b/core/src/main/java/jenkins/model/ArtifactManager.java @@ -0,0 +1,82 @@ +/* + * The MIT License + * + * Copyright 2013 Jesse Glick. + * + * 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 jenkins.model; + +import hudson.FilePath; +import hudson.Launcher; +import hudson.model.AbstractBuild; +import hudson.model.BuildListener; +import hudson.model.Run; +import hudson.tasks.ArtifactArchiver; +import java.io.IOException; +import java.util.Map; +import jenkins.util.VirtualFile; + +/** + * Manager of artifacts for one build. + * @see ArtifactManagerFactory + * @since 1.531 + */ +public abstract class ArtifactManager { + + /** + * Called when this manager is loaded from disk. + * The selected manager will be persisted inside a build, so the build reference should be {@code transient} (quasi-{@code final}) and restored here. + * @param build a historical build with which this manager was associated + */ + public abstract void onLoad(Run build); + + /** + * Archive all configured artifacts from a build. + *

    If called multiple times for the same build, do not delete the old artifacts but keep them all, unless overwritten. + * For example, the XVNC plugin could use this to save {@code screenshot.jpg} if so configured. + *

    This method is typically invoked on a running build, though e.g. in the case of Maven module builds, + * the build may actually be {@link hudson.model.Run.State#COMPLETED} when this is called + * (since it is the parent build which is still running and performing archiving). + * @param workspace the root directory from which to copy files (typically {@link AbstractBuild#getWorkspace} but not necessarily) + * @param launcher a launcher to use if external processes need to be forked + * @param listener a way to print messages about progress or problems + * @param artifacts map from paths in the archive area to paths relative to {@code workspace} (all paths {@code /}-separated) + * @throws IOException if transfer or copying failed in any way + * @throws InterruptedException if transfer was interrupted + * @see ArtifactArchiver#perform(AbstractBuild, Launcher, BuildListener) + */ + public abstract void archive(FilePath workspace, Launcher launcher, BuildListener listener, Map artifacts) throws IOException, InterruptedException; + + /** + * Delete all artifacts associated with an earlier build (if any). + * @return true if there was actually anything to delete + * @throws IOException if deletion could not be completed + * @throws InterruptedException if deletion was interrupted + */ + public abstract boolean delete() throws IOException, InterruptedException; + + /** + * Returns a representation of the root directory of archived artifacts. + * @return the archive root + */ + public abstract VirtualFile root(); + +} diff --git a/core/src/main/java/jenkins/model/ArtifactManagerConfiguration.java b/core/src/main/java/jenkins/model/ArtifactManagerConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..3aeb3aa128672d7a41a376dbb07c5545b0f649f9 --- /dev/null +++ b/core/src/main/java/jenkins/model/ArtifactManagerConfiguration.java @@ -0,0 +1,68 @@ +/* + * The MIT License + * + * Copyright 2013 Jesse Glick. + * + * 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 jenkins.model; + +import hudson.Extension; +import hudson.util.DescribableList; +import java.io.IOException; +import net.sf.json.JSONObject; +import org.kohsuke.stapler.StaplerRequest; + +/** + * List of configured {@link ArtifactManagerFactory}s. + * @since 1.531 + */ +@Extension +public class ArtifactManagerConfiguration extends GlobalConfiguration { + + public static ArtifactManagerConfiguration get() { + return Jenkins.getInstance().getInjector().getInstance(ArtifactManagerConfiguration.class); + } + + private final DescribableList artifactManagerFactories = new DescribableList(this); + + public ArtifactManagerConfiguration() { + load(); + } + + private Object readResolve() { + artifactManagerFactories.setOwner(this); + return this; + } + + public DescribableList getArtifactManagerFactories() { + return artifactManagerFactories; + } + + @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { + try { + artifactManagerFactories.rebuildHetero(req, json, ArtifactManagerFactoryDescriptor.all(), "artifactManagerFactories"); + return true; + } catch (IOException x) { + throw new FormException(x, "artifactManagerFactories"); + } + } + +} diff --git a/core/src/main/java/jenkins/model/ArtifactManagerFactory.java b/core/src/main/java/jenkins/model/ArtifactManagerFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..48e95516c07949e9bc71ba3bdce7cf3245b0ec36 --- /dev/null +++ b/core/src/main/java/jenkins/model/ArtifactManagerFactory.java @@ -0,0 +1,56 @@ +/* + * The MIT License + * + * Copyright 2013 Jesse Glick. + * + * 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 jenkins.model; + +import hudson.ExtensionPoint; +import hudson.model.AbstractDescribableImpl; +import hudson.model.Run; +import javax.annotation.CheckForNull; +import org.kohsuke.stapler.DataBoundConstructor; + +/** + * Pluggable ability to manage transfer and/or storage of build artifacts. + * The descriptor should specify at least a display name, and optionally a {@code config} view. + * Since the user can configure this class, you must have a {@link DataBoundConstructor}. + * @see ArtifactManagerConfiguration + * @see ArtifactManagerFactoryDescriptor + * @since 1.531 + */ +public abstract class ArtifactManagerFactory extends AbstractDescribableImpl implements ExtensionPoint { + + /** + * Optionally creates a manager for a particular build. + * All configured factories are consulted in sequence; the first manager thus yielded (if any) will be stored in the build. + * {@link StandardArtifactManager} is used as a fallback. + * @param build a running (or recently completed) build ready for {@link ArtifactManager#archive} + * @return a manager, or null if this manager should not handle this kind of project, builds on this kind of slave, etc. + */ + public abstract @CheckForNull ArtifactManager managerFor(Run build); + + @Override public ArtifactManagerFactoryDescriptor getDescriptor() { + return (ArtifactManagerFactoryDescriptor) super.getDescriptor(); + } + +} diff --git a/core/src/main/java/jenkins/model/ArtifactManagerFactoryDescriptor.java b/core/src/main/java/jenkins/model/ArtifactManagerFactoryDescriptor.java new file mode 100644 index 0000000000000000000000000000000000000000..638cb01eaedc55f6913bb7bdf16e98e51d702cf8 --- /dev/null +++ b/core/src/main/java/jenkins/model/ArtifactManagerFactoryDescriptor.java @@ -0,0 +1,41 @@ +/* + * The MIT License + * + * Copyright 2013 Jesse Glick. + * + * 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 jenkins.model; + +import hudson.DescriptorExtensionList; +import hudson.model.Descriptor; + +/** + * Definition of a kind of artifact manager. + * @see ArtifactManagerFactory + * @since 1.531 + */ +public abstract class ArtifactManagerFactoryDescriptor extends Descriptor { + + public static DescriptorExtensionList all() { + return Jenkins.getInstance().getDescriptorList(ArtifactManagerFactory.class); + } + +} diff --git a/core/src/main/java/jenkins/model/StandardArtifactManager.java b/core/src/main/java/jenkins/model/StandardArtifactManager.java new file mode 100644 index 0000000000000000000000000000000000000000..bd2b3d89a0f963ca74c952fdb7d7890adb46d608 --- /dev/null +++ b/core/src/main/java/jenkins/model/StandardArtifactManager.java @@ -0,0 +1,78 @@ +/* + * The MIT License + * + * Copyright 2013 Jesse Glick. + * + * 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 jenkins.model; + +import hudson.FilePath; +import hudson.Launcher; +import hudson.Util; +import hudson.model.BuildListener; +import hudson.model.Run; +import java.io.File; +import java.io.IOException; +import java.util.Map; +import jenkins.util.VirtualFile; + +/** + * Default artifact manager which transfers files over the remoting channel and stores them inside the build directory. + * May be subclassed to provide an artifact manager which uses the standard storage but which only overrides {@link #archive}. + * @since 1.531 + */ +public class StandardArtifactManager extends ArtifactManager { + + protected transient Run build; + + public StandardArtifactManager(Run build) { + onLoad(build); + } + + @Override public final void onLoad(Run build) { + this.build = build; + } + + @Override public void archive(FilePath workspace, Launcher launcher, BuildListener listener, final Map artifacts) throws IOException, InterruptedException { + File dir = getArtifactsDir(); + String description = "transfer of " + artifacts.size() + " files"; // TODO improve when just one file + workspace.copyRecursiveTo(new FilePath.ExplicitlySpecifiedDirScanner(artifacts), new FilePath(dir), description); + } + + @Override public final boolean delete() throws IOException, InterruptedException { + File ad = getArtifactsDir(); + if (!ad.exists()) { + return false; + } + Util.deleteRecursive(ad); + return true; + } + + @Override public VirtualFile root() { + return VirtualFile.forFile(getArtifactsDir()); + } + + @SuppressWarnings("deprecation") + private File getArtifactsDir() { + return build.getArtifactsDir(); + } + +} diff --git a/core/src/main/java/jenkins/util/VirtualFile.java b/core/src/main/java/jenkins/util/VirtualFile.java new file mode 100644 index 0000000000000000000000000000000000000000..2488da2c373c3c8066defdef1e6f4ac3a8965dbd --- /dev/null +++ b/core/src/main/java/jenkins/util/VirtualFile.java @@ -0,0 +1,344 @@ +/* + * The MIT License + * + * Copyright 2013 Jesse Glick. + * + * 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 jenkins.util; + +import hudson.FilePath; +import hudson.model.DirectoryBrowserSupport; +import hudson.remoting.VirtualChannel; +import hudson.util.DirScanner; +import hudson.util.FileVisitor; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nonnull; + +/** + * Abstraction over {@link File}, {@link FilePath}, or other items such as network resources or ZIP entries. + * Assumed to be read-only and makes very limited assumptions, just enough to display content and traverse directories. + * @see DirectoryBrowserSupport + * @since 1.531 + */ +public abstract class VirtualFile implements Comparable { + + /** + * Gets the base name, meaning just the last portion of the path name without any + * directories. + * + * For a “root directory” this may be the empty string. + * @return a simple name (no slashes) + */ + public abstract @Nonnull String getName(); + + /** + * Gets a URI. + * Should at least uniquely identify this virtual file within its root, but not necessarily globally. + * @return a URI (need not be absolute) + */ + public abstract URI toURI(); + + /** + * Gets the parent file. + * Need only operate within the originally given root. + * @return the parent + */ + public abstract VirtualFile getParent(); + + /** + * Checks whether this file exists and is a directory. + * @return true if it is a directory, false if a file or nonexistent + * @throws IOException in case checking status failed + */ + public abstract boolean isDirectory() throws IOException; + + /** + * Checks whether this file exists and is a plain file. + * @return true if it is a file, false if a directory or nonexistent + * @throws IOException in case checking status failed + */ + public abstract boolean isFile() throws IOException; + + /** + * Checks whether this file exists. + * @return true if it is a plain file or directory, false if nonexistent + * @throws IOException in case checking status failed + */ + public abstract boolean exists() throws IOException; + + /** + * Lists children of this directory. + * @return a list of children (files and subdirectories); empty for a file or nonexistent directory + * @throws IOException if this directory exists but listing was not possible for some other reason + */ + public abstract @Nonnull VirtualFile[] list() throws IOException; + + /** + * Lists recursive files of this directory with pattern matching. + * @param glob an Ant-style glob + * @return a list of relative names of children (files directly inside or in subdirectories) + * @throws IOException if this is not a directory, or listing was not possible for some other reason + */ + public abstract @Nonnull String[] list(String glob) throws IOException; + + /** + * Obtains a child file. + * @param name a relative path, possibly including {@code /} (but not {@code ..}) + * @return a representation of that child, whether it actually exists or not + */ + public abstract @Nonnull VirtualFile child(@Nonnull String name); + + /** + * Gets the file length. + * @return a length, or 0 if inapplicable (e.g. a directory) + * @throws IOException if checking the length failed + */ + public abstract long length() throws IOException; + + /** + * Gets the file timestamp. + * @return a length, or 0 if inapplicable + * @throws IOException if checking the timestamp failed + */ + public abstract long lastModified() throws IOException; + + /** + * Checks whether this file can be read. + * @return true normally + * @throws IOException if checking status failed + */ + public abstract boolean canRead() throws IOException; + + /** + * Opens an input stream on the file so its contents can be read. + * @return an open stream + * @throws IOException if it could not be opened + */ + public abstract InputStream open() throws IOException; + + /** + * Does case-insensitive comparison. + * @inheritDoc + */ + @Override public final int compareTo(VirtualFile o) { + return getName().compareToIgnoreCase(o.getName()); + } + + /** + * Compares according to {@link #toURI}. + * @inheritDoc + */ + @Override public final boolean equals(Object obj) { + return obj instanceof VirtualFile && toURI().equals(((VirtualFile) obj).toURI()); + } + + /** + * Hashes according to {@link #toURI}. + * @inheritDoc + */ + @Override public final int hashCode() { + return toURI().hashCode(); + } + + /** + * Displays {@link #toURI}. + * @inheritDoc + */ + @Override public final String toString() { + return toURI().toString(); + } + + /** + * Creates a virtual file wrapper for a local file. + * @param f a disk file (need not exist) + * @return a wrapper + */ + public static VirtualFile forFile(final File f) { + return new VirtualFile() { + @Override public String getName() { + return f.getName(); + } + @Override public URI toURI() { + return f.toURI(); + } + @Override public VirtualFile getParent() { + return forFile(f.getParentFile()); + } + @Override public boolean isDirectory() throws IOException { + return f.isDirectory(); + } + @Override public boolean isFile() throws IOException { + return f.isFile(); + } + @Override public boolean exists() throws IOException { + return f.exists(); + } + @Override public VirtualFile[] list() throws IOException { + File[] kids = f.listFiles(); + if (kids == null) { + return new VirtualFile[0]; + } + VirtualFile[] vfs = new VirtualFile[kids.length]; + for (int i = 0; i < kids.length; i++) { + vfs[i] = forFile(kids[i]); + } + return vfs; + } + @Override public String[] list(String glob) throws IOException { + return new Scanner(glob).invoke(f, null); + } + @Override public VirtualFile child(String name) { + return forFile(new File(f, name)); + } + @Override public long length() throws IOException { + return f.length(); + } + @Override public long lastModified() throws IOException { + return f.lastModified(); + } + @Override public boolean canRead() throws IOException { + return f.canRead(); + } + @Override public InputStream open() throws IOException { + return new FileInputStream(f); + } + }; + } + + /** + * Creates a virtual file wrapper for a remotable file. + * @param f a local or remote file (need not exist) + * @return a wrapper + */ + public static VirtualFile forFilePath(final FilePath f) { + return new VirtualFile() { + @Override public String getName() { + return f.getName(); + } + @Override public URI toURI() { + try { + return f.toURI(); + } catch (Exception x) { + return URI.create(f.getRemote()); + } + } + @Override public VirtualFile getParent() { + return f.getParent().toVirtualFile(); + } + @Override public boolean isDirectory() throws IOException { + try { + return f.isDirectory(); + } catch (InterruptedException x) { + throw (IOException) new IOException(x.toString()).initCause(x); + } + } + @Override public boolean isFile() throws IOException { + // TODO should probably introduce a method for this purpose + return exists() && !isDirectory(); + } + @Override public boolean exists() throws IOException { + try { + return f.exists(); + } catch (InterruptedException x) { + throw (IOException) new IOException(x.toString()).initCause(x); + } + } + @Override public VirtualFile[] list() throws IOException { + try { + List kids = f.list(); + if (kids == null) { + return new VirtualFile[0]; + } + VirtualFile[] vfs = new VirtualFile[kids.size()]; + for (int i = 0; i < vfs.length; i++) { + vfs[i] = forFilePath(kids.get(i)); + } + return vfs; + } catch (InterruptedException x) { + throw (IOException) new IOException(x.toString()).initCause(x); + } + } + @Override public String[] list(String glob) throws IOException { + try { + return f.act(new Scanner(glob)); + } catch (InterruptedException x) { + throw (IOException) new IOException(x.toString()).initCause(x); + } + } + @Override public VirtualFile child(String name) { + return forFilePath(f.child(name)); + } + @Override public long length() throws IOException { + try { + return f.length(); + } catch (InterruptedException x) { + throw (IOException) new IOException(x.toString()).initCause(x); + } + } + @Override public long lastModified() throws IOException { + try { + return f.lastModified(); + } catch (InterruptedException x) { + throw (IOException) new IOException(x.toString()).initCause(x); + } + } + @Override public boolean canRead() throws IOException { + try { + return f.act(new Readable()); + } catch (InterruptedException x) { + throw (IOException) new IOException(x.toString()).initCause(x); + } + } + @Override public InputStream open() throws IOException { + return f.read(); + } + }; + } + private static final class Scanner implements FilePath.FileCallable { + private final String glob; + Scanner(String glob) { + this.glob = glob; + } + @Override public String[] invoke(File f, VirtualChannel channel) throws IOException { + final List paths = new ArrayList(); + new DirScanner.Glob(glob, null).scan(f, new FileVisitor() { + @Override + public void visit(File f, String relativePath) throws IOException { + paths.add(relativePath); + } + }); + return paths.toArray(new String[paths.size()]); + } + + } + private static final class Readable implements FilePath.FileCallable { + @Override public Boolean invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + return f.canRead(); + } + } + +} diff --git a/core/src/main/resources/jenkins/model/ArtifactManagerConfiguration/config.groovy b/core/src/main/resources/jenkins/model/ArtifactManagerConfiguration/config.groovy new file mode 100644 index 0000000000000000000000000000000000000000..da3ab8a04a598ff0829bd0885f8a4aa7a5618979 --- /dev/null +++ b/core/src/main/resources/jenkins/model/ArtifactManagerConfiguration/config.groovy @@ -0,0 +1,35 @@ +/* + * The MIT License + * + * Copyright 2013 Jesse Glick. + * + * 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 jenkins.model.ArtifactManagerConfiguration; + +f = namespace(lib.FormTagLib); + +if (!jenkins.model.ArtifactManagerFactoryDescriptor.all().isEmpty()) { + f.section(title: _("Artifact Management for Builds")) { + f.block() { + f.repeatableHeteroProperty(field: "artifactManagerFactories", hasHeader: true) + } + } +} diff --git a/core/src/test/java/hudson/model/RunTest.java b/core/src/test/java/hudson/model/RunTest.java index b44f7c2ce8ee850292298544493a2f0b069e5b72..8ee207cbd3f742bc49a9884c979e3c13cdfcb286 100644 --- a/core/src/test/java/hudson/model/RunTest.java +++ b/core/src/test/java/hudson/model/RunTest.java @@ -25,18 +25,17 @@ package hudson.model; import hudson.Util; +import hudson.model.Run.Artifact; import hudson.util.StreamTaskListener; -import java.io.ByteArrayOutputStream; import java.io.File; -import java.nio.charset.Charset; import java.util.Date; +import java.util.List; import java.util.TimeZone; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import javax.xml.stream.events.Characters; import static org.junit.Assert.*; import org.junit.Test; @@ -114,4 +113,37 @@ public class RunTest { } } + private List.Artifact> createArtifactList(String... paths) throws Exception { + Run r = new Run(new StubJob(), 0) {}; + Run.ArtifactList list = r.new ArtifactList(); + for (String p : paths) { + list.add(r.new Artifact(p, p, p, String.valueOf(p.length()), "n" + list.size())); // Assuming all test inputs don't need urlencoding + } + list.computeDisplayName(); + return list; + } + + @Test + public void artifactListDisambiguation1() throws Exception { + List.Artifact> a = createArtifactList("a/b/c.xml", "d/f/g.xml", "h/i/j.xml"); + assertEquals(a.get(0).getDisplayPath(), "c.xml"); + assertEquals(a.get(1).getDisplayPath(), "g.xml"); + assertEquals(a.get(2).getDisplayPath(), "j.xml"); + } + + @Test + public void artifactListDisambiguation2() throws Exception { + List.Artifact> a = createArtifactList("a/b/c.xml", "d/f/g.xml", "h/i/g.xml"); + assertEquals(a.get(0).getDisplayPath(), "c.xml"); + assertEquals(a.get(1).getDisplayPath(), "f/g.xml"); + assertEquals(a.get(2).getDisplayPath(), "i/g.xml"); + } + + @Test + public void artifactListDisambiguation3() throws Exception { + List.Artifact> a = createArtifactList("a.xml", "a/a.xml"); + assertEquals(a.get(0).getDisplayPath(), "a.xml"); + assertEquals(a.get(1).getDisplayPath(), "a/a.xml"); + } + } diff --git a/maven-plugin/src/main/java/hudson/maven/MavenBuild.java b/maven-plugin/src/main/java/hudson/maven/MavenBuild.java index ccf65172ff44e0a3f9f2efd83166cd4e957e88ed..9cb8cac5aedef522e912f4acfc1b9aad3a332ce7 100644 --- a/maven-plugin/src/main/java/hudson/maven/MavenBuild.java +++ b/maven-plugin/src/main/java/hudson/maven/MavenBuild.java @@ -25,6 +25,7 @@ package hudson.maven; import hudson.EnvVars; import hudson.FilePath; +import hudson.Launcher; import hudson.maven.reporters.MavenArtifactRecord; import hudson.maven.reporters.SurefireArchiver; import hudson.maven.reporters.TestFailureDetector; @@ -43,6 +44,7 @@ import hudson.model.Run; import hudson.model.TaskListener; import hudson.model.listeners.RunListener; import hudson.remoting.Channel; +import hudson.remoting.VirtualChannel; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.Entry; import hudson.tasks.BuildWrapper; @@ -71,12 +73,15 @@ import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.CheckForNull; +import jenkins.model.ArtifactManager; import jenkins.mvn.SettingsProvider; @@ -402,6 +407,8 @@ public class MavenBuild extends AbstractMavenBuild { class ProxyImpl implements MavenBuildProxy, Serializable { private static final long serialVersionUID = 8865133776526671879L; + private final Map artifacts = new LinkedHashMap(); + public V execute(BuildCallable program) throws T, IOException, InterruptedException { return program.call(MavenBuild.this); } @@ -429,10 +436,47 @@ public class MavenBuild extends AbstractMavenBuild { return new FilePath(MavenBuild.this.getParent().getParent().getRootDir()); } + /** + * @deprecated Does not work with {@link ArtifactManager}. + */ + @Deprecated public FilePath getArtifactsDir() { return new FilePath(MavenBuild.this.getArtifactsDir()); } + @Override public void queueArchiving(String artifactPath, String artifact) { + artifacts.put(artifactPath, artifact); + } + + void performArchiving(Launcher launcher, BuildListener listener) throws IOException, InterruptedException { + for (Map.Entry e : artifacts.entrySet()) { + listener.getLogger().println("[JENKINS] Archiving " + e.getValue() + " to " + e.getKey()); + } + ArtifactManager am = pickArtifactManager(); + FilePath ws = getWorkspace(); + Map artifactsInsideWorkspace = new LinkedHashMap(); + String prefix = ws.act(new CanonicalPath()) + '/'; // try to relativize paths to workspace + Iterator> it = artifacts.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry e = it.next(); + String p = new FilePath(ws, e.getValue()).act(new CanonicalPath()); + if (!p.startsWith(prefix)) { + listener.getLogger().println(p + " is not inside " + prefix + "; will archive in a separate pass"); + continue; + } + artifactsInsideWorkspace.put(e.getKey(), p.substring(prefix.length())); + it.remove(); + } + if (!artifactsInsideWorkspace.isEmpty()) { + am.archive(ws, launcher, listener, artifactsInsideWorkspace); + } + // Now handle other files outside the workspace, if any. + for (Map.Entry e : artifacts.entrySet()) { + FilePath f = new FilePath(ws, e.getValue()); + am.archive(f.getParent(), launcher, listener, Collections.singletonMap(e.getKey(), f.getName())); + } + } + public void setResult(Result result) { MavenBuild.this.setResult(result); } @@ -476,6 +520,13 @@ public class MavenBuild extends AbstractMavenBuild { } } + private static final class CanonicalPath implements FilePath.FileCallable { + private static final long serialVersionUID = 1; + @Override public String invoke(File f, VirtualChannel channel) throws IOException, InterruptedException { + return f.getCanonicalPath().replace(File.separatorChar, '/'); + } + } + public class ProxyImpl2 extends ProxyImpl implements MavenBuildProxy2 { private static final long serialVersionUID = -3377221864644014218L; @@ -761,9 +812,11 @@ public class MavenBuild extends AbstractMavenBuild { { boolean normalExit = false; try { + ProxyImpl proxy = new ProxyImpl(); Result r = process.call(new Builder( - listener,new ProxyImpl(), + listener, proxy, getProject(), margs.toList(), systemProps)); + proxy.performArchiving(launcher, listener); normalExit = true; return r; } finally { diff --git a/maven-plugin/src/main/java/hudson/maven/MavenBuildProxy.java b/maven-plugin/src/main/java/hudson/maven/MavenBuildProxy.java index 395e54ab180b95099a4cce45b1333f0720e6578d..7485a153e72b9e568330313f6f38abe9fbd57a42 100644 --- a/maven-plugin/src/main/java/hudson/maven/MavenBuildProxy.java +++ b/maven-plugin/src/main/java/hudson/maven/MavenBuildProxy.java @@ -25,6 +25,7 @@ package hudson.maven; import hudson.FilePath; import hudson.model.Result; +import hudson.remoting.Asynchronous; import hudson.remoting.Callable; import hudson.remoting.DelegatingCallable; @@ -32,6 +33,7 @@ import java.io.IOException; import java.io.Serializable; import java.util.Calendar; import java.util.List; +import jenkins.model.ArtifactManager; /** * Remoting proxy interface for {@link MavenReporter}s to talk to {@link MavenBuild} @@ -93,10 +95,20 @@ public interface MavenBuildProxy { FilePath getModuleSetRootDir(); /** - * @see MavenBuild#getArtifactsDir() + * @deprecated Does not work with {@link ArtifactManager}. */ + @Deprecated FilePath getArtifactsDir(); + /** + * @param artifactPath a relative {@code /}-separated path + * @param artifact absolute path name on the slave in the workspace + * @see ArtifactManager#archive + * @since 1.531 + */ + @Asynchronous + void queueArchiving(String artifactPath, String artifact); + /** * @see MavenBuild#setResult(Result) */ @@ -211,6 +223,10 @@ public interface MavenBuildProxy { return core.getArtifactsDir(); } + @Override public void queueArchiving(String artifactPath, String artifact) { + core.queueArchiving(artifactPath, artifact); + } + public void setResult(Result result) { core.setResult(result); } diff --git a/maven-plugin/src/main/java/hudson/maven/MavenModuleSetBuild.java b/maven-plugin/src/main/java/hudson/maven/MavenModuleSetBuild.java index 2f0480c2fa6f8de48200696c30e6a245916693d7..948ba4bfc36165a593cc82e86a6afad1a00d82b5 100644 --- a/maven-plugin/src/main/java/hudson/maven/MavenModuleSetBuild.java +++ b/maven-plugin/src/main/java/hudson/maven/MavenModuleSetBuild.java @@ -811,6 +811,9 @@ public class MavenModuleSetBuild extends AbstractMavenBuild ((WrappedArtifactRepository) deploymentRepository).setUniqueVersion(true); } Artifact main = mainArtifact.toArtifact(handlerManager, artifactFactory, parent); - if (!isPOM()) - main.addMetadata(new ProjectArtifactMetadata(main, pomArtifact.getFile(parent))); - + File pomFile = null; + if (!isPOM()) { + pomFile = pomArtifact.getFile(parent); + main.addMetadata(new ProjectArtifactMetadata(main, pomFile)); + } if (main.getType().equals("maven-plugin")) { GroupRepositoryMetadata metadata = new GroupRepositoryMetadata(main.getGroupId()); String goalPrefix = PluginDescriptor.getGoalPrefixFromArtifactId(main.getArtifactId()); @@ -188,30 +189,18 @@ public class MavenArtifactRecord extends MavenAbstractArtifactRecord // deploy the main artifact. This also deploys the POM logger.println(Messages.MavenArtifact_DeployingMainArtifact(main.getFile().getName())); deployer.deploy(main.getFile(), main, deploymentRepository, embedder.getLocalRepository()); + main.getFile().delete(); + if (pomFile != null) { + pomFile.delete(); + } for (MavenArtifact aa : attachedArtifacts) { Artifact a = aa.toArtifact(handlerManager, artifactFactory, parent); logger.println(Messages.MavenArtifact_DeployingMainArtifact(a.getFile().getName())); deployer.deploy(a.getFile(), a, deploymentRepository, embedder.getLocalRepository()); + a.getFile().delete(); } } - - /** - * Installs the artifact to the local Maven repository. - */ - public void install(MavenEmbedder embedder) throws MavenEmbedderException, IOException, ComponentLookupException, ArtifactInstallationException { - ArtifactHandlerManager handlerManager = embedder.lookup(ArtifactHandlerManager.class); - ArtifactInstaller installer = embedder.lookup(ArtifactInstaller.class); - ArtifactFactory factory = embedder.lookup(ArtifactFactory.class); - - Artifact main = mainArtifact.toArtifact(handlerManager,factory,parent); - if(!isPOM()) - main.addMetadata(new ProjectArtifactMetadata(main,pomArtifact.getFile(parent))); - installer.install(mainArtifact.getFile(parent),main,embedder.getLocalRepository()); - - for (MavenArtifact aa : attachedArtifacts) - installer.install(aa.getFile(parent), aa.toArtifact(handlerManager, factory, parent), embedder.getLocalRepository()); - } public void recordFingerprints() throws IOException { // record fingerprints diff --git a/maven-plugin/src/test/java/hudson/maven/reporters/SurefireArchiverUnitTest.java b/maven-plugin/src/test/java/hudson/maven/reporters/SurefireArchiverUnitTest.java index 43cbd79280b251091ab5ce4d1d3f95babf016847..4b6bb8af10403b3282f1f0f4accd01b40f807df2 100644 --- a/maven-plugin/src/test/java/hudson/maven/reporters/SurefireArchiverUnitTest.java +++ b/maven-plugin/src/test/java/hudson/maven/reporters/SurefireArchiverUnitTest.java @@ -246,6 +246,8 @@ public class SurefireArchiverUnitTest { return null; } + @Override public void queueArchiving(String artifactPath, String artifact) {} + @Override public void setResult(Result result) { } diff --git a/test/src/test/java/hudson/maven/MavenMultiModuleTest.java b/test/src/test/java/hudson/maven/MavenMultiModuleTest.java index ae3ed0e97336deeba0fcba7d41f77da1f5b703eb..91137ba10129931e47434cdc410cde14b8989240 100644 --- a/test/src/test/java/hudson/maven/MavenMultiModuleTest.java +++ b/test/src/test/java/hudson/maven/MavenMultiModuleTest.java @@ -1,6 +1,7 @@ package hudson.maven; import com.gargoylesoftware.htmlunit.html.HtmlPage; +import hudson.FilePath; import hudson.Functions; import org.junit.Assert; import org.jvnet.hudson.test.Bug; @@ -17,13 +18,21 @@ import hudson.model.BuildListener; import hudson.model.Job; import hudson.model.PermalinkProjectAction; import hudson.model.Result; +import hudson.model.Run; import hudson.tasks.Fingerprinter.FingerprintAction; import hudson.tasks.Maven.MavenInstallation; import java.io.File; import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; +import jenkins.model.ArtifactManager; import java.util.Set; import java.util.TreeSet; +import jenkins.model.ArtifactManagerConfiguration; +import jenkins.model.ArtifactManagerFactory; +import jenkins.util.VirtualFile; import static org.junit.Assert.*; import org.junit.Assume; import org.junit.Ignore; @@ -464,6 +473,83 @@ public class MavenMultiModuleTest { modulesPage.getAnchorByText(m.getDisplayName()).openLinkInNewWindow(); } + @Bug(17236) + @Test public void artifactArchiving() throws Exception { + ArtifactManagerConfiguration.get().getArtifactManagerFactories().add(new TestAMF()); + j.configureDefaultMaven(); // using Maven 2 so we can test single-module builds + MavenModuleSet mms = j.createMavenProject(); + mms.setScm(new ExtractResourceSCM(getClass().getResource("maven-multimod.zip"))); + mms.setAssignedNode(j.createOnlineSlave()); + j.buildAndAssertSuccess(mms); + // We want all the artifacts in a given module to be archived in one operation. But modules are archived separately. + Map> expected = new TreeMap>(); + FilePath ws = mms.getModule("org.jvnet.hudson.main.test.multimod$multimod-top").getBuildByNumber(1).getWorkspace(); + expected.put("org.jvnet.hudson.main.test.multimod:multimod-top", Collections.singletonMap("org.jvnet.hudson.main.test.multimod/multimod-top/1.0-SNAPSHOT/multimod-top-1.0-SNAPSHOT.pom", new FilePath(ws.getChannel(), "…/org/jvnet/hudson/main/test/multimod/multimod-top/1.0-SNAPSHOT/multimod-top-1.0-SNAPSHOT.pom"))); + for (String module : new String[] {"moduleA", "moduleB", "moduleC"}) { + Map m = new TreeMap(); + ws = mms.getModule("org.jvnet.hudson.main.test.multimod$" + module).getBuildByNumber(1).getWorkspace(); + m.put("org.jvnet.hudson.main.test.multimod/" + module + "/1.0-SNAPSHOT/" + module + "-1.0-SNAPSHOT.pom", ws.child("pom.xml")); + m.put("org.jvnet.hudson.main.test.multimod/" + module + "/1.0-SNAPSHOT/" + module + "-1.0-SNAPSHOT.jar", ws.child("target/" + module + "-1.0-SNAPSHOT.jar")); + expected.put("org.jvnet.hudson.main.test.multimod:" + module, m); + } + assertEquals(expected.toString(), TestAM.archivings.toString()); // easy to read + assertEquals(expected, TestAM.archivings); // compares also FileChannel + // Also check single-module build. + expected.clear(); + TestAM.archivings.clear(); + MavenBuild isolated = j.buildAndAssertSuccess(mms.getModule("org.jvnet.hudson.main.test.multimod$moduleA")); + assertEquals(2, isolated.number); + Map m = new TreeMap(); + ws = isolated.getWorkspace(); + m.put("org.jvnet.hudson.main.test.multimod/moduleA/1.0-SNAPSHOT/moduleA-1.0-SNAPSHOT.pom", ws.child("pom.xml")); + m.put("org.jvnet.hudson.main.test.multimod/moduleA/1.0-SNAPSHOT/moduleA-1.0-SNAPSHOT.jar", ws.child("target/moduleA-1.0-SNAPSHOT.jar")); + expected.put("org.jvnet.hudson.main.test.multimod:moduleA", m); + assertEquals(expected, TestAM.archivings); + } + + public static final class TestAMF extends ArtifactManagerFactory { + @Override public ArtifactManager managerFor(Run build) { + return new TestAM(build); + } + } + public static final class TestAM extends ArtifactManager { + static final Map> archivings = new TreeMap>(); + transient Run build; + TestAM(Run build) { + onLoad(build); + } + @Override public void onLoad(Run build) { + this.build = build; + } + @Override public void archive(FilePath workspace, Launcher launcher, BuildListener listener, Map artifacts) throws IOException, InterruptedException { + String name = build.getParent().getName(); + if (archivings.containsKey(name)) { + // Would be legitimate only if some archived files for a given module were outside workspace, such as repository parent POM, *and* others were inside, which is not the case in this test. + throw new IOException("repeated archiving to " + name); + } + Map m = new TreeMap(); + for (Map.Entry e : artifacts.entrySet()) { + FilePath f = workspace.child(e.getValue()); + if (f.exists()) { + if (f.getRemote().replace('\\', '/').contains("/org/jvnet/hudson/main/test/")) { + // Inside the local repository. Hard to know exactly what that path might be, so just mask it out. + f = new FilePath(f.getChannel(), f.getRemote().replaceFirst("^.+(?=[/\\\\]org[/\\\\]jvnet[/\\\\]hudson[/\\\\]main[/\\\\]test[/\\\\])", "…")); + } + m.put(e.getKey(), f); + } else { + throw new IOException("no such file " + f); + } + } + archivings.put(name, m); + } + @Override public boolean delete() throws IOException, InterruptedException { + throw new IOException(); + } + @Override public VirtualFile root() { + throw new UnsupportedOperationException(); + } + } + /* @Test public void parallelMultiModMavenWsExists() throws Exception { configureDefaultMaven(); diff --git a/test/src/test/java/hudson/model/RunTest.java b/test/src/test/java/hudson/model/RunTest.java index be4f74e8660a316ba25c081a128b0437dc3b7d93..a54a0d3f866ab5e7d78d3381181f2a6980ce8c04 100644 --- a/test/src/test/java/hudson/model/RunTest.java +++ b/test/src/test/java/hudson/model/RunTest.java @@ -26,9 +26,6 @@ package hudson.model; import java.net.HttpURLConnection; import java.util.Collection; import java.util.Collections; -import static org.junit.Assert.*; - -import java.util.List; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.Bug; @@ -41,37 +38,6 @@ public class RunTest { @Rule public JenkinsRule j = new JenkinsRule(); - private List.Artifact> createArtifactList(String... paths) throws Exception { - FreeStyleProject prj = j.createFreeStyleProject(); - FreeStyleBuild r = prj.scheduleBuild2(0).get(); - Run.ArtifactList list = r.new ArtifactList(); - for (String p : paths) { - list.add(r.new Artifact(p,p,p,String.valueOf(p.length()),"n"+list.size())); // Assuming all test inputs don't need urlencoding - } - list.computeDisplayName(); - return list; - } - - @Test public void artifactListDisambiguation1() throws Exception { - List.Artifact> a = createArtifactList("a/b/c.xml", "d/f/g.xml", "h/i/j.xml"); - assertEquals(a.get(0).getDisplayPath(),"c.xml"); - assertEquals(a.get(1).getDisplayPath(),"g.xml"); - assertEquals(a.get(2).getDisplayPath(),"j.xml"); - } - - @Test public void artifactListDisambiguation2() throws Exception { - List.Artifact> a = createArtifactList("a/b/c.xml", "d/f/g.xml", "h/i/g.xml"); - assertEquals(a.get(0).getDisplayPath(),"c.xml"); - assertEquals(a.get(1).getDisplayPath(),"f/g.xml"); - assertEquals(a.get(2).getDisplayPath(),"i/g.xml"); - } - - @Test public void artifactListDisambiguation3() throws Exception { - List.Artifact> a = createArtifactList("a.xml","a/a.xml"); - assertEquals(a.get(0).getDisplayPath(),"a.xml"); - assertEquals(a.get(1).getDisplayPath(),"a/a.xml"); - } - @Bug(17935) @Test public void getDynamicInvisibleTransientAction() throws Exception { TransientBuildActionFactory.all().add(0, new TransientBuildActionFactory() {