diff --git a/biz.aQute.bndall.tests/resources/ws-stalecheck/p5/bnd.bnd b/biz.aQute.bndall.tests/resources/ws-stalecheck/p5/bnd.bnd new file mode 100644 index 0000000000..e69de29bb2 diff --git a/biz.aQute.bndall.tests/resources/ws-stalecheck/p5/gen/Buildpath.java b/biz.aQute.bndall.tests/resources/ws-stalecheck/p5/gen/Buildpath.java new file mode 100644 index 0000000000..028914e12d --- /dev/null +++ b/biz.aQute.bndall.tests/resources/ws-stalecheck/p5/gen/Buildpath.java @@ -0,0 +1,9 @@ +package foo.bar; + + +public class Buildpath { + public final static String VERSION = "${def;Bundle-Version;1.0.0}"; + public final static String[] BUILDPATH = { + ${template;-buildpath;"${@}"} + }; +} \ No newline at end of file diff --git a/biz.aQute.bndall.tests/test/biz/aQute/bnd/project/ProjectGenerateTest.java b/biz.aQute.bndall.tests/test/biz/aQute/bnd/project/ProjectGenerateTest.java index e6c7442730..795a7e7328 100644 --- a/biz.aQute.bndall.tests/test/biz/aQute/bnd/project/ProjectGenerateTest.java +++ b/biz.aQute.bndall.tests/test/biz/aQute/bnd/project/ProjectGenerateTest.java @@ -108,6 +108,74 @@ public void testSimpleGenerator() throws Exception { } } + @Test + @SuppressWarnings({ + "unchecked", "rawtypes" + }) + public void testSimpleGeneratorDontClearOutput() throws Exception { + try (Workspace ws = getWorkspace("resources/ws-stalecheck")) { + getRepo(ws); + Project project = ws.getProject("p5"); + project.setProperty("-generate", + "gen/**.java;output=src-gen/;generate='javagen -o src-gen/ gen/**.java';clear=false"); + + File outputdir = project.getFile("src-gen"); + + assertThat(project.getGenerate() + .getOutputDirs()).contains(outputdir); + + File in = project.getFile("gen/Buildpath.java"); + in.setLastModified(System.currentTimeMillis() - 2000); + + File out = project.getFile("src-gen/foo/bar/Buildpath.java"); + assertThat(out).doesNotExist(); + + File existing = project.getFile("src-gen/test.txt"); + assertThat(existing).doesNotExist(); + + assertThat(project.getGenerate() + .needsBuild()).isTrue(); + + project.getGenerate() + .generate(true); + assertThat(project.check()).isTrue(); + assertThat(out).isFile(); + + assertThat(project.getGenerate() + .needsBuild()).isFalse(); + + // new we simulate modification of a source by adding a file + existing.createNewFile(); + + // no reason to build + assertThat(project.getGenerate() + .needsBuild()).isFalse(); + + // delete the generated file + IO.delete(out); + // in some rear cases the whole process seamed to be to fast and the + // created file had the exact same timestamp. So we make sure the + // source is newer + in.setLastModified(System.currentTimeMillis() + 1); + + assertThat(out).doesNotExist(); + + assertThat(project.getGenerate() + .needsBuild()).isTrue(); + + project.getGenerate() + .generate(true); + + // check that our test file is still there and does exist + assertThat(existing).isFile(); + assertThat(project.check()).isTrue(); + assertThat(out).isFile(); + + assertThat(project.getGenerate() + .needsBuild()).isFalse(); + } + } + @Test @SuppressWarnings({ "unchecked", "rawtypes" diff --git a/biz.aQute.bndlib/src/aQute/bnd/build/Project.java b/biz.aQute.bndlib/src/aQute/bnd/build/Project.java index 920b456bfe..33b1b1e3fc 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/build/Project.java +++ b/biz.aQute.bndlib/src/aQute/bnd/build/Project.java @@ -2444,8 +2444,7 @@ public void clean() throws Exception { clean(getTargetDir(), "target"); clean(getSrcOutput(), "source output"); clean(getTestOutput(), "test output"); - for (File output : getGenerate().getOutputDirs()) - clean(output, "generate output " + output, false); + getGenerate().clean(); for (File src : getSourcePath()) { IO.mkdirs(src); diff --git a/biz.aQute.bndlib/src/aQute/bnd/build/ProjectGenerate.java b/biz.aQute.bndlib/src/aQute/bnd/build/ProjectGenerate.java index 891e913d4d..d3f0741a9b 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/build/ProjectGenerate.java +++ b/biz.aQute.bndlib/src/aQute/bnd/build/ProjectGenerate.java @@ -4,6 +4,7 @@ import static aQute.bnd.result.Result.ok; import java.io.File; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Collections; @@ -96,12 +97,27 @@ private Result prepare(String sourceWithDuplicate, GeneratorSpec st) { File out = project.getFile(output); if (out.isDirectory()) { - for (File f : out.listFiles()) { - IO.delete(f); + if (st.clear() + .orElseGet(() -> Boolean.TRUE)) { + for (File f : out.listFiles()) { + IO.delete(f); + } } } else { out.mkdirs(); } + + long latestModifiedSource = sourceFiles.stream() + .mapToLong(File::lastModified) + .max() + .getAsLong(); + + // the out folder serves as our trigger if a build is needed. Thus we + // use to output directory timestamp as marker we can compare against + // later. If clear is false a generator might decided to do nothing and + // could cause a build loop. + out.setLastModified(latestModifiedSource); + return Result.ok(null); } @@ -308,9 +324,15 @@ public Result> getInputs() { } public Set getOutputDirs() { + return getOutputDirs(false); + } + + private Set getOutputDirs(boolean toClear) { return project.instructions.generate() .values() .stream() + .filter(spec -> !toClear || spec.clear() + .orElse(Boolean.TRUE)) .map(GeneratorSpec::output) .filter(Objects::nonNull) .map(project::getFile) @@ -349,11 +371,9 @@ public boolean needsBuild() { if (outputFiles.isEmpty()) return true; - long latestModifiedTarget = outputFiles.stream() - .filter(File::isFile) - .mapToLong(File::lastModified) - .min() - .orElse(0); + File out = project.getFile(output); + + long latestModifiedTarget = out.lastModified(); boolean staleFiles = latestModifiedSource > latestModifiedTarget; if (staleFiles) @@ -363,8 +383,12 @@ public boolean needsBuild() { } public void clean() { - getOutputDirs().stream() - .forEach(IO::delete); + for (File output : getOutputDirs(true)) + try { + project.clean(output, "generate output " + output, false); + } catch (IOException e) { + Exceptions.duck(e); + } } @Override diff --git a/biz.aQute.bndlib/src/aQute/bnd/help/instructions/ProjectInstructions.java b/biz.aQute.bndlib/src/aQute/bnd/help/instructions/ProjectInstructions.java index 1b54a4a46a..db7498202c 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/help/instructions/ProjectInstructions.java +++ b/biz.aQute.bndlib/src/aQute/bnd/help/instructions/ProjectInstructions.java @@ -61,6 +61,9 @@ interface GeneratorSpec { @SyntaxAnnotation(lead = "Specify a JAR version for a Main class plugin (name in generate must be a fqn class name)") Optional version(); + + @SyntaxAnnotation(lead = "Determins if the output directory needs to be cleared before the generator runs. The default is true.") + Optional clear(); } } diff --git a/docs/_instructions/generate.md b/docs/_instructions/generate.md index 1faafdb5d7..1ae512b7d3 100644 --- a/docs/_instructions/generate.md +++ b/docs/_instructions/generate.md @@ -9,17 +9,20 @@ since: 5.1 Virtually all the work bnd is concerned about happens in generating the JAR file. The key idea is to _pull_ resources in the JAR, instead of the more traditional _push_ model of other builders. This works well, except for _generating source code_. This generating step must happen before the compiler is called, and the compiler is generally called before bnd becomes active. This `-generate` instruction specifies the code generating steps that must be executed. Source code can be generated by _system_ commands or the bnd _external plugins_. - + -generate ::= clause ( ',' clause )* clause ::= FILESET ';' 'output=' DIR (';' option )* src ::= FILESET option ::= 'system=' STRING | 'generate=' STRING - | 'classpath=' PATH - | 'workingdir=' FILE + | 'classpath=' PATH + | 'workingdir=' FILE + | 'clear=' BOOLEAN For each clause, the key of the clause is used to establish an Ant File Set, e.g. `foo/**.in`. This a glob expression with the exception that the double wildcard ('**') indicates to any depth of directories. The `output` attribute _must_ specify a directory. If the output must be compiled this directory must be on the bnd source path. +The output directory will created if it does not exist. It will be cleared of any previous generate results before a run, except if the option `clear` is set to false. In this case the used Generator needs to deal with the remnants in the directory itself. + If any file in the source is older than any file in the target (to any depth), or the target is empty, the clause is considered _stale_. If the clause is not stale, it is further ignored. If no further options are set on the clause, a warning is generated that some files are out of date. If either a warning or error option is given, these will be executed on the project. @@ -56,7 +59,7 @@ If this example is used, it is necessary to add a new _source folder_. In Eclips src=${^src},src-gen Assuming that the input files are in the `gen` directory, the following can be used to automatically generate the output files based on the input files. - + -generate: \ gen/**.java; \ output='src-gen/' ; \ @@ -128,4 +131,3 @@ If you the plugin source code is in the same workspace as the project using this You can take a look at the [JavaGen](https://github.com/bndtools/bnd/tree/master/biz.aQute.bnd.javagen) project in the bnd build to see how an actual external plugin is made. -