-
Notifications
You must be signed in to change notification settings - Fork 30
[JENKINS-70870] Save libraries as JAR files rather than unpacked #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
62562ac
c02ed47
e5e95d0
8598f5e
10155ea
0a2ed44
fec1f4e
3cd3102
e2b6276
f9be590
a14ed45
1783fb0
3062a97
859dad1
6ba9d43
fa82612
75f2cae
5bbb1ed
0a7ccb4
20729d2
29a8848
af8414f
894fbbb
38e609a
2c181cc
f5b0866
80395ee
c2c711e
81ca5a6
d1f3aca
840cd60
0aa22f4
743db17
444bde7
992eeaa
b7bd774
10623dd
698ae68
77aea3e
2fb8ed9
6acba02
23125c9
68d2a33
d2c5eff
157d6f0
d608664
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,12 +35,128 @@ | |
import hudson.util.FormValidation; | ||
import edu.umd.cs.findbugs.annotations.CheckForNull; | ||
import edu.umd.cs.findbugs.annotations.NonNull; | ||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; | ||
import hudson.slaves.WorkspaceList; | ||
import hudson.util.DirScanner; | ||
import hudson.util.FileVisitor; | ||
import hudson.util.io.ArchiverFactory; | ||
import java.io.File; | ||
import java.io.IOException; | ||
import java.io.OutputStream; | ||
import java.nio.file.Path; | ||
import java.util.concurrent.atomic.AtomicBoolean; | ||
import java.util.jar.Attributes; | ||
import java.util.jar.JarFile; | ||
import java.util.jar.Manifest; | ||
|
||
/** | ||
* A way in which a library can be physically obtained for use in a build. | ||
*/ | ||
public abstract class LibraryRetriever extends AbstractDescribableImpl<LibraryRetriever> implements ExtensionPoint { | ||
|
||
/** | ||
* JAR manifest attribute giving original library name. | ||
*/ | ||
static final String ATTR_LIBRARY_NAME = "Jenkins-Library-Name"; | ||
|
||
@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Non-final for write access via the Script Console") | ||
public static boolean INCLUDE_SRC_TEST_IN_LIBRARIES = Boolean.getBoolean(SCMSourceRetriever.class.getName() + ".INCLUDE_SRC_TEST_IN_LIBRARIES"); | ||
|
||
/** | ||
* Obtains library sources. | ||
* @param name the {@link LibraryConfiguration#getName} | ||
* @param version the version of the library, such as from {@link LibraryConfiguration#getDefaultVersion} or an override | ||
* @param changelog whether to include changesets in the library in jobs using it from {@link LibraryConfiguration#getIncludeInChangesets} | ||
* @param target a JAR file in which to stash sources; should contain {@code **}{@code /*.groovy} (sources at top level will be considered vars), and optionally also {@code resources/} | ||
* @param run a build which will use the library | ||
* @param listener a way to report progress | ||
* @throws Exception if there is any problem (use {@link AbortException} for user errors) | ||
*/ | ||
public void retrieveJar(@NonNull String name, @NonNull String version, boolean changelog, @NonNull FilePath target, @NonNull Run<?,?> run, @NonNull TaskListener listener) throws Exception { | ||
if (Util.isOverridden(LibraryRetriever.class, getClass(), "retrieve", String.class, String.class, boolean.class, FilePath.class, Run.class, TaskListener.class)) { | ||
FilePath tmp = target.sibling(target.getBaseName() + "-checkout"); | ||
if (tmp == null) { | ||
throw new IOException(); | ||
} | ||
try { | ||
retrieve(name, version, changelog, tmp, run, listener); | ||
dir2Jar(name, tmp, target, listener); | ||
} finally { | ||
tmp.deleteRecursive(); | ||
FilePath tmp2 = WorkspaceList.tempDir(tmp); | ||
if (tmp2 != null) { | ||
tmp2.deleteRecursive(); | ||
} | ||
} | ||
} else { | ||
throw new AbstractMethodError("Implement retrieveJar"); | ||
} | ||
} | ||
|
||
/** | ||
* Translates a historical directory with {@code src/} and/or {@code vars/} and/or {@code resources/} subdirectories | ||
* into a JAR file with Groovy in classpath orientation and {@code resources/} as a ZIP folder. | ||
*/ | ||
static void dir2Jar(@NonNull String name, @NonNull FilePath dir, @NonNull FilePath jar, @NonNull TaskListener listener) throws IOException, InterruptedException { | ||
lookForBadSymlinks(dir, dir); | ||
FilePath mf = jar.withSuffix(".mf"); | ||
try { | ||
try (OutputStream os = mf.write()) { | ||
Manifest m = new Manifest(); | ||
m.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); | ||
// Informational debugging aid, since the hex JAR basename will be meaningless: | ||
m.getMainAttributes().putValue(ATTR_LIBRARY_NAME, name); | ||
m.write(os); | ||
} | ||
try (OutputStream os = jar.write()) { | ||
dir.archive(ArchiverFactory.ZIP, os, new DirScanner() { | ||
@Override public void scan(File dir, FileVisitor visitor) throws IOException { | ||
scanSingle(new File(mf.getRemote()), JarFile.MANIFEST_NAME, visitor); | ||
String excludes; | ||
if (!INCLUDE_SRC_TEST_IN_LIBRARIES && new File(dir, "src/test").isDirectory()) { | ||
excludes = "test/"; | ||
listener.getLogger().println("Excluding src/test/ so that library test code cannot be accessed by Pipelines."); | ||
listener.getLogger().println("To remove this log message, move the test code outside of src/. To restore the previous behavior that allowed access to files in src/test/, pass -D" + SCMSourceRetriever.class.getName() + ".INCLUDE_SRC_TEST_IN_LIBRARIES=true to the java command used to start Jenkins."); | ||
} else { | ||
excludes = null; | ||
} | ||
AtomicBoolean found = new AtomicBoolean(); | ||
FileVisitor verifyingVisitor = visitor.with(pathname -> { | ||
if (pathname.getName().endsWith(".groovy")) { | ||
found.set(true); | ||
} | ||
return true; | ||
}); | ||
new DirScanner.Glob("**/*.groovy", excludes).scan(new File(dir, "src"), verifyingVisitor); | ||
new DirScanner.Glob("*.groovy,*.txt", null).scan(new File(dir, "vars"), verifyingVisitor); | ||
if (!found.get()) { | ||
throw new AbortException("Library " + name + " expected to contain at least one of src or vars directories"); | ||
} | ||
new DirScanner.Glob("resources/", null).scan(dir, visitor); | ||
} | ||
}); | ||
} | ||
} finally { | ||
mf.delete(); | ||
} | ||
} | ||
|
||
private static void lookForBadSymlinks(FilePath root, FilePath dir) throws IOException, InterruptedException { | ||
for (FilePath child : dir.list()) { | ||
if (child.isDirectory()) { | ||
lookForBadSymlinks(root, child); | ||
} else { | ||
String link = child.readLink(); | ||
if (link != null) { | ||
Path target = Path.of(dir.getRemote(), link).toRealPath(); | ||
if (!target.startsWith(Path.of(root.getRemote()))) { | ||
Comment on lines
+151
to
+152
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this behave as expected if JENKINS_HOME itself is a symlink? See for example jenkinsci/workflow-cps-global-lib-plugin#139. Also, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No idea offhand; I suppose we have no test coverage for such cases.
I think it is fine. If there is no such target, we want to fail one way or the other. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't think so. Based on the number of reports I got from the related security fix though, it is a surprisingly common scenario. Safest to use
Yeah, but the interesting thing about |
||
throw new SecurityException(child + " → " + target + " is not inside " + root); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Obtains library sources. | ||
* @param name the {@link LibraryConfiguration#getName} | ||
|
@@ -51,7 +167,14 @@ public abstract class LibraryRetriever extends AbstractDescribableImpl<LibraryRe | |
* @param listener a way to report progress | ||
* @throws Exception if there is any problem (use {@link AbortException} for user errors) | ||
*/ | ||
public abstract void retrieve(@NonNull String name, @NonNull String version, boolean changelog, @NonNull FilePath target, @NonNull Run<?,?> run, @NonNull TaskListener listener) throws Exception; | ||
@Deprecated | ||
public void retrieve(@NonNull String name, @NonNull String version, boolean changelog, @NonNull FilePath target, @NonNull Run<?,?> run, @NonNull TaskListener listener) throws Exception { | ||
if (Util.isOverridden(LibraryRetriever.class, getClass(), "retrieve", String.class, String.class, FilePath.class, Run.class, TaskListener.class)) { | ||
retrieve(name, version, target, run, listener); | ||
} else { | ||
throw new AbstractMethodError("Implement retrieveJar"); | ||
} | ||
} | ||
|
||
/** | ||
* Obtains library sources. | ||
|
@@ -62,8 +185,10 @@ public abstract class LibraryRetriever extends AbstractDescribableImpl<LibraryRe | |
* @param listener a way to report progress | ||
* @throws Exception if there is any problem (use {@link AbortException} for user errors) | ||
*/ | ||
// TODO this should have been made nonabstract and deprecated and delegated to the new version; may be able to use access-modifier to help | ||
public abstract void retrieve(@NonNull String name, @NonNull String version, @NonNull FilePath target, @NonNull Run<?,?> run, @NonNull TaskListener listener) throws Exception; | ||
@Deprecated | ||
public void retrieve(@NonNull String name, @NonNull String version, @NonNull FilePath target, @NonNull Run<?,?> run, @NonNull TaskListener listener) throws Exception { | ||
throw new AbstractMethodError("Implement retrieveJar"); | ||
} | ||
|
||
@Deprecated | ||
public FormValidation validateVersion(@NonNull String name, @NonNull String version) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(suppress whitespace changes when reviewing)