From 85ca4adf45a67c8494606981d6d3d19fcd0a4057 Mon Sep 17 00:00:00 2001 From: Peter Kriens Date: Thu, 30 Jun 2022 12:26:35 +0200 Subject: [PATCH] [resolve] Add cache mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cache – Will use a cache file in the workspace cache. If that file is stale relative to the workspace or project or it does not exist, then the bnd(run) file will be resolved and the result is stored in the cache file. Signed-off-by: Peter Kriens --- .../instructions/ResolutionInstructions.java | 5 + .../bnd/help/instructions/package-info.java | 2 +- .../src/biz/aQute/resolve/Bndrun.java | 105 ++++++++++++++++++ .../biz/aQute/resolve/RunResolutionTest.java | 43 +++++++ docs/_instructions/resolve.md | 4 +- 5 files changed, 157 insertions(+), 2 deletions(-) diff --git a/biz.aQute.bndlib/src/aQute/bnd/help/instructions/ResolutionInstructions.java b/biz.aQute.bndlib/src/aQute/bnd/help/instructions/ResolutionInstructions.java index 6a44734d7e0..52cd3eed155 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/help/instructions/ResolutionInstructions.java +++ b/biz.aQute.bndlib/src/aQute/bnd/help/instructions/ResolutionInstructions.java @@ -78,6 +78,11 @@ enum ResolveMode { @SyntaxAnnotation(lead = "A resolve will take place before launching when in batch mode (e.g. Gradle) but not in IDE mode (e.g. Eclipse)") batch, + /** + * Run the resolver before launching during batch mode + */ + @SyntaxAnnotation(lead = "Resolve when the runbundles are needed unless there is a cache file that is newer than the bndrun/project & workspace. The cache file has the same name as the project/bndrun file but starts with a '.'") + cache, } @SyntaxAnnotation(lead = "Resolve mode defines when resolving takes place. The default, manual, requires a manual step in bndtools. Auto will resolve on save, and beforelaunch runs the resolver before being launched, batchlaunch is like beforelaunch but only in batch mode", example = "'-resolve manual", pattern = "(manual|auto|beforelaunch|batch)") diff --git a/biz.aQute.bndlib/src/aQute/bnd/help/instructions/package-info.java b/biz.aQute.bndlib/src/aQute/bnd/help/instructions/package-info.java index ae16697ce3a..7fb18f56f00 100644 --- a/biz.aQute.bndlib/src/aQute/bnd/help/instructions/package-info.java +++ b/biz.aQute.bndlib/src/aQute/bnd/help/instructions/package-info.java @@ -1,2 +1,2 @@ -@org.osgi.annotation.versioning.Version("1.4.0") +@org.osgi.annotation.versioning.Version("1.5.0") package aQute.bnd.help.instructions; diff --git a/biz.aQute.resolve/src/biz/aQute/resolve/Bndrun.java b/biz.aQute.resolve/src/biz/aQute/resolve/Bndrun.java index c848a53ee06..cf0e5b18200 100644 --- a/biz.aQute.resolve/src/biz/aQute/resolve/Bndrun.java +++ b/biz.aQute.resolve/src/biz/aQute/resolve/Bndrun.java @@ -1,8 +1,10 @@ package biz.aQute.resolve; import java.io.File; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import org.osgi.resource.Requirement; @@ -19,6 +21,7 @@ import aQute.bnd.build.model.conversions.CollectionFormatter; import aQute.bnd.build.model.conversions.Converter; import aQute.bnd.build.model.conversions.HeaderClauseFormatter; +import aQute.bnd.exceptions.Exceptions; import aQute.bnd.header.Parameters; import aQute.bnd.help.Syntax; import aQute.bnd.help.instructions.ResolutionInstructions; @@ -27,6 +30,9 @@ import aQute.bnd.osgi.Processor; import aQute.bnd.osgi.resource.FilterParser; import aQute.bnd.osgi.resource.FilterParser.Expression; +import aQute.lib.io.IO; +import aQute.lib.utf8properties.UTF8Properties; +import aQute.libg.cryptography.SHA1; /** * This is a resolving version of the Run class. The name of this class is known @@ -171,6 +177,9 @@ public Collection getRunbundles() throws Exception { default : break; + case cache : + return cache(); + case batch : if (!gestalt.containsKey(Constants.GESTALT_BATCH) && !super.getRunbundles().isEmpty()) break; @@ -185,4 +194,100 @@ public Collection getRunbundles() throws Exception { return super.getRunbundles(); } + private Collection cache() throws Exception { + File ours = getPropertiesFile(); + File cache = getCacheFile(ours); + File workspace = getWorkspace().getPropertiesFile(); + if (workspace == null) + workspace = ours; + + String force; + if (!cache.isFile()) + force = "no cache file"; + else if (cache.lastModified() <= ours.lastModified()) + force = "cache file is older than our properties"; + else if (cache.lastModified() <= workspace.lastModified()) + force = "cache file is older than our workspace"; + else + force = null; + trace("force = %s", force); + + boolean tryCache = force == null; + + try (Processor p = new Processor()) { + + if (cache.isFile()) + p.setProperties(cache); + + if (tryCache) { + trace("attempting to use cache"); + String runbundles = p.getProperty(Constants.RUNBUNDLES); + Collection containers = parseRunbundles(runbundles); + if (isAllOk(containers)) { + return containers; + } else { + trace("the cached bundles were not ok, will resolve"); + } + } + trace("resolving"); + + IO.delete(cache); + if (cache.isFile()) { + throw new IllegalStateException("cannot delete cache file " + cache); + } + + RunResolution resolved = RunResolution.resolve(this, Collections.emptyList()); + if (!resolved.isOK()) { + throw new IllegalStateException(resolved.report(false)); + } + + String spec = resolved.getRunBundlesAsString(); + + List containers = parseRunbundles(spec); + if (isAllOk(containers)) { + UTF8Properties props = new UTF8Properties(p.getProperties()); + props.setProperty(Constants.RUNBUNDLES, spec); + cache.getParentFile() + .mkdirs(); + props.store(cache); + } + return containers; + } + } + + /** + * Return the file used to cache the resolved solution for the given file + * + * @param file the file to find the cached file for + * @return the cached file + */ + public File getCacheFile(File file) { + try { + String path = file.getAbsolutePath(); + String digest = SHA1.digest(path.getBytes(StandardCharsets.UTF_8)) + .asHex(); + return getWorkspace().getCache("resolved-cache/".concat(digest) + .concat(".resolved")); + } catch (Exception e) { + // not gonna happen + throw Exceptions.duck(e); + } + } + + boolean testIgnoreDownloadErrors = false; + + private boolean isAllOk(Collection containers) { + for (Container c : containers) { + if (c.getError() != null) { + if (testIgnoreDownloadErrors) { + if (c.getError() + .contains("FileNotFoundException")) + continue; + } + return false; + } + } + return true; + } + } diff --git a/biz.aQute.resolve/test/biz/aQute/resolve/RunResolutionTest.java b/biz.aQute.resolve/test/biz/aQute/resolve/RunResolutionTest.java index eed9436f272..f2755d2fe30 100644 --- a/biz.aQute.resolve/test/biz/aQute/resolve/RunResolutionTest.java +++ b/biz.aQute.resolve/test/biz/aQute/resolve/RunResolutionTest.java @@ -1,7 +1,9 @@ package biz.aQute.resolve; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertTrue; +import java.io.File; import java.nio.file.Path; import java.util.Collection; import java.util.Collections; @@ -118,6 +120,47 @@ public void testCachingOfResult() throws Exception { // assertThat(runbundles).hasSize(22); } + @Test + public void testResolveCached() throws Exception { + File file = IO.getFile(tmp.toFile(), "resolver.bndrun"); + Bndrun bndrun = Bndrun.createBndrun(workspace, file); + bndrun.testIgnoreDownloadErrors = true; + assertTrue(bndrun.check()); + File cache = bndrun.getCacheFile(file); + + System.out.println("get the embedded list of runbundles"); + bndrun.setProperty("-resolve", "manual"); + Collection manual = bndrun.getRunbundles(); + + System.out.println("remove the embedded list and set mode to 'cache'"); + bndrun.setProperty("-resolve", "cache"); + bndrun.unsetProperty("-runbundles"); + + assertThat(cache).doesNotExist(); + + System.out.println("First time we should resolve & create a cache file"); + Collection cached = bndrun.getRunbundles(); + assertThat(cache).isFile(); + assertThat(cached).containsAll(manual); + assertThat(cached).hasSize(manual.size()); + assertThat(cache.lastModified()).isGreaterThan(file.lastModified()); + + System.out + .println("Second time, the cache file should used, so make it valid but empty "); + long lastModified = cache.lastModified(); + IO.store("-runbundles ", cache); + cache.setLastModified(file.lastModified() + 1000); + cached = bndrun.getRunbundles(); + assertThat(cached).isEmpty(); + + System.out.println("Now make cache invalid, which be ignored"); + IO.store("-runbundles is not a valid file", cache); + cache.setLastModified(file.lastModified() + 1000); + cached = bndrun.getRunbundles(); + assertThat(manual).containsAll(cached); + + } + @Test public void testNotCachingOfResultForOtherResolveOption() throws Exception { Bndrun bndrun = Bndrun.createBndrun(workspace, IO.getFile(tmp.toFile(), "resolver.bndrun")); diff --git a/docs/_instructions/resolve.md b/docs/_instructions/resolve.md index 9e14ea861ba..f8d17d024e1 100644 --- a/docs/_instructions/resolve.md +++ b/docs/_instructions/resolve.md @@ -1,7 +1,7 @@ --- layout: default class: Workspace -title: -resolve (manual|auto|beforelaunch|batch) +title: -resolve (manual|auto|beforelaunch|batch|cache) summary: Defines when/how resolving is done to calculate the -runbundles --- @@ -13,6 +13,8 @@ The values are: * `auto` – Whenever the initial requirements are saved, the resolver will be used to set new `-runbundles` * `beforelaunch` – Calculate the `-runbundles` on demand. This ignores the value of the `-runbundles` and runs the resolver. The results of the resolver are cached. This cache works by creating a checksum over all the properties of the project. * `batch` – When running in batch mode, the run bundles will be resolved. In all other modes this will only resolve when the `-runbundles` are empty. +* `cache` – Will use a cache file in the workspace cache. If that file is stale relative to the workspace or project or it does not exist, then the bnd(run) file will be resolved and the result is stored in the cache file. + ## Example