blob: acad6efdb9d87b7395891b273c8a0a66eb20d1cf [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.nbbuild;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.FileScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.Ant;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.util.FileUtils;
/** Assistent in changing of build scripts.
*
* @author Jaroslav Tulach
*/
public class FixDependencies extends Task {
/** Replace*/
private List<Replace> replaces = new ArrayList<>();
/** files to fix */
private FileSet set;
/** verify target */
private String tgt;
/** clean target */
private String clean;
/** relative path from module file to build script to use for verification */
private String ant;
/** trip only changed */
private boolean onlyChanged;
/** fail on error */
private boolean fail;
private boolean doSanity = true;
private boolean strip = true;
/**
* Only update dependencies for the codebase(s), which match the filter.
*/
private Pattern moduleFilter;
/** tasks to be executed */
/** Initialize. */
public FixDependencies() {
}
public Replace createReplace () {
Replace r = new Replace ();
replaces.add (r);
return r;
}
public FileSet createFileset() throws BuildException {
if (this.set != null) throw new BuildException ("Only one file set is allowed");
this.set = new FileSet();
return this.set;
}
public void setSanityCheck(boolean s) {
doSanity = s;
}
public void setBuildTarget (String s) {
tgt = s;
}
public void setCleanTarget (String s) {
clean = s;
}
public void setAntFile (String r) {
ant = r;
}
public void setStripOnlyChanged (boolean b) {
onlyChanged = b;
}
public void setFailOnError (boolean b) {
fail = b;
}
public void setStrip(boolean b) {
this.strip = b;
}
public void setModuleFilter(String s) {
if (s == null || "".equals(s)) {
return;
}
moduleFilter = Pattern.compile(s, Pattern.CASE_INSENSITIVE);
}
@Override
public void execute () throws org.apache.tools.ant.BuildException {
FileScanner scan = this.set.getDirectoryScanner(getProject());
File dir = scan.getBasedir();
for (String kid : scan.getIncludedFiles()) {
File xml = new File(dir, kid);
if (!xml.exists()) throw new BuildException("File does not exist: " + xml, getLocation());
log ("Fixing " + xml, Project.MSG_INFO);
File script = null;
Ant task = null;
Ant cleanTask = null;
boolean compiled = !doSanity;
if (ant != null && tgt != null) {
task = (org.apache.tools.ant.taskdefs.Ant)getProject ().createTask ("ant");
script = FileUtils.getFileUtils().resolveFile(xml, ant);
if (!script.exists ()) {
String msg = "Skipping. Cannot find file " + ant + " from + " + xml;
if (fail) {
throw new BuildException (msg);
}
log(msg, Project.MSG_ERR);
continue;
}
task.setAntfile (script.getPath ());
task.setDir (script.getParentFile ());
task.setTarget (tgt);
if (clean != null) {
cleanTask = (Ant) getProject().createTask("ant");
cleanTask.setAntfile (script.getPath ());
cleanTask.setDir (script.getParentFile ());
cleanTask.setTarget (clean);
}
try {
if (doSanity || strip) {
// before we do anything else, let's verify that we build
if (cleanTask != null) {
log ("Cleaning " + clean + " in " + script, org.apache.tools.ant.Project.MSG_INFO);
cleanTask.execute ();
compiled = false;
}
if (doSanity) {
log ("Sanity check executes " + tgt + " in " + script, org.apache.tools.ant.Project.MSG_INFO);
task.execute ();
compiled = true;
}
} else {
compiled = true; // just for the case - fix will clean up.
}
} catch (BuildException ex) {
if (fail) {
throw ex;
}
log("Skipping. Could not execute " + tgt + " in " + script, org.apache.tools.ant.Project.MSG_ERR);
continue;
}
}
try {
boolean change = fix (xml, script, task, cleanTask, compiled);
if (!strip || onlyChanged && !change) {
continue;
}
simplify (xml, script, task, cleanTask);
} catch (IOException ex) {
throw new BuildException (ex, getLocation ());
}
}
}
/** Modifies the xml file to replace dependencies wiht new ones.
* @return true if there was a change in the file
*/
private boolean fix (File file, File script, org.apache.tools.ant.taskdefs.Ant task, org.apache.tools.ant.taskdefs.Ant cleanTask, boolean compiled) throws IOException, BuildException {
int s = (int)file.length ();
byte[] data = new byte[s];
try (InputStream is = new FileInputStream(file)) {
if (s != is.read (data)) {
throw new BuildException ("Cannot read " + file);
}
}
String stream = new String (data);
String old = stream;
data = null;
try {
DEPS: for (Replace r : replaces) {
int md = stream.indexOf("<module-dependencies");
if (md == -1) {
throw new BuildException("No module dependencies in " + file);
}
int ed = stream.indexOf ("</module-dependencies>", md);
ed = ed == -1 ? stream.indexOf ("<module-dependencies/>", md) : ed;
if (ed == -1) {
ed = stream.length();
}
if (moduleFilter != null && !moduleFilter.matcher(r.codeNameBase).matches()) {
continue;
}
String alldeps = stream.substring(md, ed);
int idx = stream.indexOf ("<code-name-base>" + r.codeNameBase + "</code-name-base>", md);
if (idx == -1 || idx > ed) continue;
int from = stream.lastIndexOf ("<dependency>", idx);
if (from == -1) throw new BuildException ("No <dependency> tag before index " + idx + " in " + file);
int after = stream.indexOf ("</dependency", idx);
if (after == -1) throw new BuildException ("No </dependency> tag after index " + idx + " in " + file);
after = after + "</dependency>".length ();
String remove = stream.substring (from, after);
if (r.addCompileTime && remove.indexOf ("compile-dependency") == -1) {
int fromAfter = "<dependency".length();
int nonSpace = findNonSpace (remove, fromAfter);
String spaces = remove.substring (fromAfter, nonSpace);
remove = remove.substring (0, fromAfter) + spaces + "<compile-dependency/>" + remove.substring (fromAfter);
}
StringBuffer sb = new StringBuffer ();
sb.append (stream.substring (0, from));
boolean prefix = false;
StringBuffer save = new StringBuffer();
int mods = r.modules.size();
int changed = 0;
// first check whether the module's own version is sufficient:
Module triggerModule = null;
boolean specVersionMissing = false;
for (Module m : r.modules) {
if (m.codeNameBase.equals(r.codeNameBase)) {
log ("Checking dependency: " + r.codeNameBase, Project.MSG_INFO);
if (remove.contains("<implementation-version/>")) {
continue DEPS;
}
String b = "<specification-version>";
int specBeg = remove.indexOf(b);
int specEnd = remove.indexOf("</specification-version>");
if (specBeg != -1 && specEnd != -1) {
String v = remove.substring(specBeg + b.length(), specEnd);
if (olderThanOrEqual(m.specVersion, v)) {
continue DEPS;
}
} else {
log("No specification version present for dependency: " + m.codeNameBase, Project.MSG_WARN);
specVersionMissing = true;
}
triggerModule = m;
break;
}
}
// reached only if the dependencies include OLDER or NONE specification version of the r.codeNameBase
for (Module m : r.modules) {
// check if the dependencies already contain the injected module; if so, rather
if (m != triggerModule && alldeps.indexOf ("<code-name-base>" + m.codeNameBase + "</code-name-base>") != -1) {
continue;
}
if (prefix) {
sb.append('\n');
for (int i = from - 1; stream.charAt(i) == ' '; i--) {
sb.append(' ');
}
}
changed++;
log ("Adding dependency: " + m, Project.MSG_INFO);
int beg = remove.indexOf (r.codeNameBase);
int aft = beg + r.codeNameBase.length ();
sb.append (remove.substring (0, beg));
sb.append (m.codeNameBase);
String a = remove.substring (aft);
if (specVersionMissing) {
int rd = a.indexOf("<run-dependency");
StringBuilder rep = new StringBuilder("<run-dependency>");
if (m.releaseVersion != null) {
rep.append("<release-version>").append(m.releaseVersion).append("</release-version>");
}
if (m.specVersion != null) {
rep.append("<specification-version>").append(m.specVersion).append("</specification-version>");
}
if (rd != -1) {
int end = a.indexOf("</run-dependency>");
String newA = a.substring(0, rd) + rep.toString();
if (end != -1) {
newA = newA + a.substring(end);
} else {
end = a.indexOf("<run-dependency/>");
if (end != -1) {
newA = newA + "</run-dependency>" + a.substring(end + 17);
}
}
a = newA;
} else {
rep.append("</run-dependency>");
a = a + rep.toString();
}
} else {
if (m.specVersion != null) {
a = a.replaceAll (
"<specification-version>[0-9\\.]*</specification-version>",
"<specification-version>" + m.specVersion + "</specification-version>"
);
}
if (m.releaseVersion == null) {
a = a.replaceAll (
"<release-version>[0-9]*</release-version>[\n\r ]*",
""
);
}
}
sb.append (a);
prefix = true;
// check whether the dependency is sufficient, or replacements must be made
if (remove.contains("<compile-dependency")) {
save = new StringBuffer(sb.toString());
save.append(stream.substring(after));
String x = save.toString();
if (!old.equals (x)) {
FileWriter fw = new FileWriter (file);
fw.write (x);
fw.close ();
try {
if (compiled && cleanTask != null) {
log ("Cleaning " + clean + " in " + script, Project.MSG_INFO);
cleanTask.execute ();
compiled = false;
}
if (!compiled && task != null) {
log ("Executing target " + tgt + " in " + script, Project.MSG_INFO);
task.execute ();
log ("Dependency on " + m + " is sufficient, skipping the rest", Project.MSG_INFO);
compiled = true;
stream = x;
continue DEPS;
} else {
log ("Cannot verify dependency on " + m + " no build target, clean target or build script set.", Project.MSG_INFO);
}
} catch (BuildException ex) {
log ("Compilation failed: ", ex, Project.MSG_INFO);
fw = new FileWriter (file);
fw.write (old);
fw.close ();
if (changed == mods) {
throw new BuildException("Could not fix dependencies.");
}
}
}
}
}
sb.append (stream.substring (after));
stream = sb.toString ();
}
if (!old.equals (stream)) {
try (FileWriter fw = new FileWriter (file)) {
fw.write (stream);
}
return true;
} else {
return false;
}
} finally {
// leave compiled so other modules may benefit from the module-auto-deps
if (!compiled && task != null) {
log ("Executing target " + tgt + " in " + script, Project.MSG_INFO);
task.execute ();
}
}
} // end of fix
private void simplify (
File file, File script, org.apache.tools.ant.taskdefs.Ant task, org.apache.tools.ant.taskdefs.Ant cleanTask
) throws IOException, BuildException {
if (ant == null || tgt == null) {
return;
}
int s = (int)file.length ();
byte[] data = new byte[s];
try (InputStream is = new FileInputStream(file)) {
if (s != is.read (data)) {
throw new BuildException ("Cannot read " + file);
}
}
String stream = new String (data);
String old = stream;
int first = -1;
int last = -1;
int begin = -1;
StringBuffer success = new StringBuffer ();
StringBuffer sb = new StringBuffer ();
for (;;) {
if (cleanTask != null) {
log ("Cleaning " + clean + " in " + script, Project.MSG_INFO);
cleanTask.execute ();
}
int from = stream.indexOf ("<dependency>", begin);
if (from == -1) {
break;
}
if (first == -1) {
first = from;
}
int after = stream.indexOf ("</dependency", from);
if (after == -1) throw new BuildException ("No </dependency> tag after index " + from + " in " + file);
after = findNonSpace (stream, after + "</dependency".length ());
last = after;
begin = last;
// write the file without the
try (FileWriter fw = new FileWriter (file)) {
fw.write (stream.substring (0, from) + stream.substring (after));
}
String dep = stream.substring (from, after);
if (dep.indexOf ("compile-dependency") == -1) {
// skip non-compile dependencies
sb.append (stream.substring (from, after));
continue;
}
if (dep.indexOf("org.netbeans.libs.javacapi") != -1) {
// should be kept even if can compile using rt.jar version
sb.append (stream.substring (from, after));
continue;
}
int cnbBeg = dep.indexOf ("<code-name-base>");
int cnbEnd = dep.indexOf ("</code-name-base>");
if (cnbBeg != -1 && cnbEnd != -1) {
dep = dep.substring (cnbBeg + "<code-name-base>".length (), cnbEnd);
}
String result;
try {
log ("Executing target " + tgt + " in " + script, Project.MSG_INFO);
task.execute ();
result = "Ok";
success.append (dep);
success.append ("\n");
} catch (BuildException ex) {
result = "Failure";
// ok, this is needed dependency
sb.append (stream.substring (from, after));
}
log ("Removing dependency " + dep + ": " + result, Project.MSG_INFO);
}
if (first != -1) {
// write the file without the
try (FileWriter fw = new FileWriter (file)) {
fw.write (stream.substring (0, first) + sb.toString () + stream.substring (last));
}
}
log ("Final verification runs " + tgt + " in " + script, Project.MSG_INFO);
// now verify, if there is a failure then something is wrong now
task.execute ();
if (success.length () == 0) {
log ("No dependencies removed from " + script);
} else {
log ("Removed dependencies from " + script + ":\n" + success);
}
} // end of simplify
/**
* Finds first non-whitespace after the tag. The tag may be commented out, sometimes
* the comment appear as &lt;tagname-->. So if > appears right after tagname, consume it
* first, then search for non-whitespaces. Otherwise, break on the nearest nonwhitespace
* @param where
* @param from
* @return
*/
private static int findNonSpace (String where, int from) {
if (from < where.length() && where.charAt(from) == '>') {
from++;
}
while (from < where.length () && Character.isWhitespace (where.charAt (from))) {
from++;
}
return from;
}
private static boolean olderThanOrEqual(String v1, String v2) {
String[] arr1 = v1.split("\\.");
String[] arr2 = v2.split("\\.");
int min = Math.min(arr1.length, arr2.length);
for (int i = 0; i < min; i++) {
int i1 = Integer.parseInt(arr1[i]);
int i2 = Integer.parseInt(arr2[i]);
if (i1 == i2) {
continue;
}
return i1 < i2;
}
return arr1.length <= arr2.length;
}
public static final class Replace extends Object {
String codeNameBase;
List<Module> modules = new ArrayList<>();
boolean addCompileTime;
public void setCodeNameBase (String s) {
codeNameBase = s;
}
public void setAddCompileTime (boolean b) {
addCompileTime = b;
}
public Module createModule () {
Module m = new Module ();
modules.add (m);
return m;
}
}
public static final class Module extends Object {
String codeNameBase;
String specVersion;
String releaseVersion;
public void setCodeNameBase (String s) {
codeNameBase = s;
}
public void setSpec (String s) {
specVersion = s;
}
public void setRelease (String r) {
releaseVersion = r;
}
public String toString() {
return codeNameBase + (releaseVersion != null ? releaseVersion : "") + " > " + specVersion;
}
}
}