mirror of
https://github.com/jenkinsci/jenkins.git
synced 2026-11-13 12:02:20 +00:00
[JENKINS-20280] Use topological sorting to decide the order to override variables.
This commit is contained in:
1 parent
562c64bbdb
commit
6d56de71cb
2 files changed
+106
-120
No files matched your search
@@ -26,6 +26,8 @@ package hudson;
|
||||
import hudson.remoting.Callable;
|
||||
import hudson.remoting.VirtualChannel;
|
||||
import hudson.util.CaseInsensitiveComparator;
|
||||
import hudson.util.CyclicGraphDetector;
|
||||
import hudson.util.CyclicGraphDetector.CycleDetectedException;
|
||||
import hudson.util.VariableResolver;
|
||||
|
||||
import java.io.File;
|
||||
@@ -40,6 +42,7 @@ import java.util.TreeMap;
|
||||
import java.util.Arrays;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Environment variables.
|
||||
@@ -69,6 +72,7 @@ import java.util.UUID;
|
||||
* @author Kohsuke Kawaguchi
|
||||
*/
|
||||
public class EnvVars extends TreeMap<String,String> {
|
||||
private static Logger LOGGER = Logger.getLogger(EnvVars.class.getName());
|
||||
/**
|
||||
* If this {@link EnvVars} object represents the whole environment variable set,
|
||||
* not just a partial list used for overriding later, then we need to know
|
||||
@@ -158,21 +162,8 @@ public class EnvVars extends TreeMap<String,String> {
|
||||
/**
|
||||
* Calculates the order to override variables.
|
||||
*
|
||||
* We should override variables in a following order:
|
||||
* <ol>
|
||||
* <li>variables that does not contain variable expressions.</li>
|
||||
* <li>variables that refers variables overridden in 1.</li>
|
||||
* <li>variables that refers variables overridden in 2.</li>
|
||||
* <li>...</li>
|
||||
* <li>(last) variables contains '+' (as PATH+MAVEN)</li>
|
||||
* </ol>
|
||||
* Sort variables with topological sort with their reference graph.
|
||||
*
|
||||
* This class orders variables in a following way:
|
||||
* <ol>
|
||||
* <li>scan each overriding variables and list all referred variables (includes indirect references).</li>
|
||||
* <li>sort variables with a number of referring variables (ascending order).</li>
|
||||
* </ol>
|
||||
*
|
||||
* This is package accessible for testing purpose.
|
||||
*/
|
||||
static class OverrideOrderCalculator {
|
||||
@@ -198,143 +189,133 @@ public class EnvVars extends TreeMap<String,String> {
|
||||
}
|
||||
}
|
||||
|
||||
private static class VariableReferenceSorter extends CyclicGraphDetector<String> {
|
||||
// map from a variable to a set of variables that variable refers.
|
||||
private final Map<String, Set<String>> refereeSetMap;
|
||||
|
||||
public VariableReferenceSorter(Map<String, Set<String>> refereeSetMap) {
|
||||
this.refereeSetMap = refereeSetMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Iterable<? extends String> getEdges(String n) {
|
||||
// return variables referred from the variable.
|
||||
if (!refereeSetMap.containsKey(n)) {
|
||||
// there is a case a non-existing variable is referred...
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return refereeSetMap.get(n);
|
||||
}
|
||||
};
|
||||
|
||||
private final Comparator<? super String> comparator;
|
||||
|
||||
private final EnvVars target;
|
||||
private final Map<String,String> overrides;
|
||||
/**
|
||||
* set of variables that a variable is REFERRING.
|
||||
* When A=${B}, refereeSetMap.get("A").contains("B").
|
||||
* Also contains indirect references, when A=${B}, B=${C},
|
||||
* refereeSetMap.get("A").contains("C").
|
||||
*/
|
||||
private Map<String, Set<String>> refereeSetMap;
|
||||
|
||||
/**
|
||||
* set of variables that a variable is REFERRED BY.
|
||||
* When A=${B}, referrerSetMap.get("B").contains("A").
|
||||
* Also contains indirect references, when A=${B}, B=${C},
|
||||
* referrerSetMap.get("C").contains("A").
|
||||
*/
|
||||
private Map<String, Set<String>> referrerSetMap;
|
||||
private Map<String, Set<String>> refereeSetMap;
|
||||
private List<String> orderedVariableNames;
|
||||
|
||||
public OverrideOrderCalculator(EnvVars target, Map<String,String> overrides) {
|
||||
comparator = target.comparator();
|
||||
this.target = target;
|
||||
this.overrides = overrides;
|
||||
refereeSetMap = new TreeMap<String, Set<String>>(comparator);
|
||||
referrerSetMap = new TreeMap<String, Set<String>>(comparator);
|
||||
scan();
|
||||
}
|
||||
|
||||
private int getRefereeNum(String variable) {
|
||||
if (refereeSetMap.containsKey(variable)) {
|
||||
return refereeSetMap.get(variable).size();
|
||||
}
|
||||
return 0;
|
||||
public List<String> getOrderedVariableNames() {
|
||||
return orderedVariableNames;
|
||||
}
|
||||
|
||||
public List<String> getOrderedVariableNames() {
|
||||
List<String> nameList = new ArrayList<String>(overrides.keySet());
|
||||
Collections.sort(nameList, new Comparator<String>() {
|
||||
@Override
|
||||
public int compare(String o1, String o2) {
|
||||
if (o1.indexOf('+') > -1) {
|
||||
if (o2.indexOf('+') > -1) {
|
||||
// ABC+FOO == XYZ+BAR
|
||||
return 0;
|
||||
}
|
||||
// ABC+FOO > BAR
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (o2.indexOf('+') > -1) {
|
||||
// FOO < ABC+BAR
|
||||
return -1;
|
||||
}
|
||||
|
||||
// depends on the number of variables each variable refers.
|
||||
return getRefereeNum(o1) - getRefereeNum(o2);
|
||||
// Cut the reference to the variable in a cycle.
|
||||
private void cutCycleAt(String referee, List<String> cycle) {
|
||||
// cycle contains variables in referrer-to-referee order.
|
||||
// This should not be negative, for the first and last one is same.
|
||||
int refererIndex = cycle.lastIndexOf(referee) - 1;
|
||||
|
||||
assert(refererIndex >= 0);
|
||||
String referrer = cycle.get(refererIndex);
|
||||
boolean removed = refereeSetMap.get(referrer).remove(referee);
|
||||
assert(removed);
|
||||
LOGGER.warning(String.format("Cyclic reference detected: %s", Util.join(cycle," -> ")));
|
||||
LOGGER.warning(String.format("Cut the reference %s -> %s", referrer, referee));
|
||||
}
|
||||
|
||||
// Cut the variable reference in a cycle.
|
||||
private void cutCycle(List<String> cycle) {
|
||||
// if an existing variable is contained in that cycle,
|
||||
// cut the cycle with that variable:
|
||||
// existing:
|
||||
// PATH=/usr/bin
|
||||
// overriding:
|
||||
// PATH1=/usr/local/bin:${PATH}
|
||||
// PATH=/opt/something/bin:${PATH1}
|
||||
// then consider reference PATH1 -> PATH can be ignored.
|
||||
for (String referee: cycle) {
|
||||
if (target.containsKey(referee)) {
|
||||
cutCycleAt(referee, cycle);
|
||||
return;
|
||||
}
|
||||
});
|
||||
return nameList;
|
||||
}
|
||||
|
||||
// if not, cut the reference to the first one.
|
||||
cutCycleAt(cycle.get(0), cycle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan all variables and list all referring variables.
|
||||
*/
|
||||
public void scan() {
|
||||
refereeSetMap = new TreeMap<String, Set<String>>(comparator);
|
||||
List<String> extendingVariableNames = new ArrayList<String>();
|
||||
|
||||
TraceResolver resolver = new TraceResolver(comparator);
|
||||
|
||||
for (String currentVar: overrides.keySet()) {
|
||||
if (currentVar.indexOf('+') > 0) {
|
||||
for (Map.Entry<String, String> entry: overrides.entrySet()) {
|
||||
if (entry.getKey().indexOf('+') > 0) {
|
||||
// XYZ+AAA variables should be always processed in last.
|
||||
extendingVariableNames.add(entry.getKey());
|
||||
continue;
|
||||
}
|
||||
resolver.clear();
|
||||
Util.replaceMacro(overrides.get(currentVar), resolver);
|
||||
Util.replaceMacro(entry.getValue(), resolver);
|
||||
|
||||
// Variables directly referred from the current scanning variable.
|
||||
Set<String> refereeSet = resolver.referredVariables;
|
||||
|
||||
if (refereeSet.isEmpty()) {
|
||||
// nothing to do if this variables does not refer other variables.
|
||||
// Ignore self reference.
|
||||
refereeSet.remove(entry.getKey());
|
||||
refereeSetMap.put(entry.getKey(), refereeSet);
|
||||
}
|
||||
|
||||
VariableReferenceSorter sorter;
|
||||
while(true) {
|
||||
sorter = new VariableReferenceSorter(refereeSetMap);
|
||||
try {
|
||||
sorter.run(refereeSetMap.keySet());
|
||||
} catch(CycleDetectedException e) {
|
||||
// cyclic reference found.
|
||||
// cut the cycle and retry.
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> cycle = e.cycle;
|
||||
cutCycle(cycle);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find indirect referred variables:
|
||||
// A=${B}
|
||||
// CurrentVar=${A}
|
||||
// -> CurrentVar refers B.
|
||||
Set<String> indirectRefereeSet = new TreeSet<String>(comparator);
|
||||
for (String referee: refereeSet) {
|
||||
if (refereeSetMap.containsKey(referee)) {
|
||||
indirectRefereeSet.addAll(refereeSetMap.get(referee));
|
||||
}
|
||||
}
|
||||
|
||||
// now contains variables referred both directly and indirectly.
|
||||
refereeSet.addAll(indirectRefereeSet);
|
||||
|
||||
// Variables refers the current scanning variable.
|
||||
// this contains both direct and indirect reference.
|
||||
Set<String> referrerSet = referrerSetMap.get(currentVar);
|
||||
|
||||
// what I have to do:
|
||||
// 1. Create a link between the current scanning variable and referred variables.
|
||||
// 1-a. register the current variable as a referrer of referred variables.
|
||||
// 1-b. register referred variables as a referee of the current variable.
|
||||
// 2. Create links between referring variables and referred variables.
|
||||
// 2-a. register referring variables as referrers of referred variables.
|
||||
// 2-b. register referred variables as referees of referring variables.
|
||||
//
|
||||
// Links between referring variables and the current scanning variable
|
||||
// is already created from referring variables.
|
||||
for (String referee: refereeSet) {
|
||||
if (!referrerSetMap.containsKey(referee)) {
|
||||
referrerSetMap.put(referee, new TreeSet<String>(comparator));
|
||||
}
|
||||
// 1-a. register the current variable as a referrer of referred variables.
|
||||
referrerSetMap.get(referee).add(currentVar);
|
||||
// 2-b. register referred variables as referees of referring variables.
|
||||
if (referrerSet != null) {
|
||||
referrerSetMap.get(referee).addAll(referrerSet);
|
||||
}
|
||||
}
|
||||
|
||||
if (!refereeSetMap.containsKey(currentVar)) {
|
||||
refereeSetMap.put(currentVar, new TreeSet<String>(comparator));
|
||||
}
|
||||
// 1-b. register referred variables as a referee of the current variable.
|
||||
refereeSetMap.get(currentVar).addAll(refereeSet);
|
||||
|
||||
if (referrerSet != null) {
|
||||
for (String referer: referrerSet) {
|
||||
// 2-b. register referred variables as referees of referring variables.
|
||||
// For referrer refers the current scanning variable,
|
||||
// refereeSetMap.get(referer) always exists.
|
||||
refereeSetMap.get(referer).addAll(refereeSet);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// When A refers B, the last appearance of B always comes after
|
||||
// the last appearance of A.
|
||||
List<String> reversedDuplicatedOrder = new ArrayList<String>(sorter.getSorted());
|
||||
Collections.reverse(reversedDuplicatedOrder);
|
||||
|
||||
orderedVariableNames = new ArrayList<String>(overrides.size());
|
||||
for(String key: reversedDuplicatedOrder) {
|
||||
if(overrides.containsKey(key) && !orderedVariableNames.contains(key)) {
|
||||
orderedVariableNames.add(key);
|
||||
}
|
||||
}
|
||||
Collections.reverse(orderedVariableNames);
|
||||
orderedVariableNames.addAll(extendingVariableNames);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user