Skip to content

Resolve #2347 Add -H:ExcludeResources option to native-image #2912

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions substratevm/Resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,28 @@ To make calls such as `Class.getResource()` or `Class.getResourceAsStream()` (or

```json
{
"resources": [
{"pattern": "<Java regexp that matches resource(s) to be included in the image>"},
{"pattern": "<another regexp>"},
...
]
"resources": {
"includes": [
{"pattern": "<Java regexp that matches resource(s) to be included in the image>"},
{"pattern": "<another regexp>"},
...
],
"excludes": [
{"pattern": "<Java regexp that matches resource(s) to be excluded from the image>"},
{"pattern": "<another regexp>"},
...
]
}
}
```

The configuration file's path must be provided to `native-image` with `-H:ResourceConfigurationFiles=/path/to/resource-config.json`. Alternatively, individual resource paths can also be specified directly to `native-image`:
```shell
The configuration file's path must be provided to the native image builder with `-H:ResourceConfigurationFiles=/path/to/resource-config.json`. Alternatively, individual resource paths can also be specified directly to `native-image`:
```
native-image -H:IncludeResources=<Java regexp that matches resources to be included in the image> ...
```
The `-H:IncludeResources` option can be passed several times to define more than one regexp to match resources.
The `-H:IncludeResources` and `-H:ExcludeResources` options can be passed several times to define more than one regexp to match or exclude resources, respectively.

To see which resources get included into the image, you can enable the related logging info with `-H:Log=registerResource:`.
To see which resources get ultimately included into the image, you can enable the related logging info with `-H:Log=registerResource:`.

### Example Usage

Expand All @@ -41,6 +48,7 @@ Then:
* `Resource0.txt` can be loaded with `.*/Resource0.txt$`.
* `Resource0.txt` and `Resource1.txt` can be loaded with `.*/Resource0.txt$` and `.*/Resource1.txt$`
(or alternatively with a single `.*/(Resource0|Resource1).txt$`).
* Also, if we want to include everything except the `Resource2.txt` file, we can simply exclude it with `-H:IncludeResources='.*/Resource.*txt$'` followed by `-H:ExcludeResources='.*/Resource2.txt$'`.

See also the [guide on assisted configuration of Java resources and other dynamic features](Configuration.md#assisted-configuration-of-native-image-builds).

Expand Down
20 changes: 20 additions & 0 deletions substratevm/mx.substratevm/suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,24 @@
"testProject": True,
},

"com.oracle.svm.configure.test": {
"subDir": "src",
"sourceDirs": ["src"],
"dependencies": [
"mx:JUNIT_TOOL",
"sdk:GRAAL_SDK",
"com.oracle.svm.configure",
],
"checkstyle": "com.oracle.svm.core",
"workingSets": "SVM",
"annotationProcessors": [
"compiler:GRAAL_PROCESSOR",
],
"javaCompliance": "8+",
"spotbugs": "false",
"testProject": True,
},

"com.oracle.svm.reflect": {
"subDir": "src",
"sourceDirs": ["src"],
Expand Down Expand Up @@ -1126,10 +1144,12 @@
"dependencies" : [
"com.oracle.svm.test",
"com.oracle.svm.test.jdk11",
"com.oracle.svm.configure.test",
],
"distDependencies": [
"mx:JUNIT_TOOL",
"sdk:GRAAL_SDK",
"SVM_CONFIGURE",
],
"testDistribution" : True,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/

package com.oracle.svm.configure.test.config;

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
import java.util.LinkedList;
import java.util.List;

import org.junit.Assert;
import org.junit.Test;

import com.oracle.svm.configure.config.ResourceConfiguration;
import com.oracle.svm.configure.json.JsonWriter;
import com.oracle.svm.core.configure.ResourceConfigurationParser;
import com.oracle.svm.core.configure.ResourcesRegistry;

public class ResourceConfigurationTest {

@Test
public void anyResourceMatches() {
ResourceConfiguration rc = new ResourceConfiguration();
rc.addResourcePattern(".*/Resource.*txt$");

Assert.assertTrue(rc.anyResourceMatches("com/my/app/Resource0.txt"));
Assert.assertTrue(rc.anyResourceMatches("com/my/app/Resource1.txt"));
Assert.assertTrue(rc.anyResourceMatches("/Resource2.txt"));
Assert.assertTrue(rc.anyResourceMatches("/Resource3.txt"));

rc.ignoreResourcePattern(".*/Resource2.txt$");

Assert.assertTrue(rc.anyResourceMatches("com/my/app/Resource0.txt"));
Assert.assertTrue(rc.anyResourceMatches("com/my/app/Resource1.txt"));
Assert.assertFalse(rc.anyResourceMatches("/Resource2.txt"));
Assert.assertTrue(rc.anyResourceMatches("/Resource3.txt"));
}

@Test
public void printJson() {
ResourceConfiguration rc = new ResourceConfiguration();
rc.addResourcePattern(".*/Resource.*txt$");
rc.ignoreResourcePattern(".*/Resource2.txt$");
PipedWriter pw = new PipedWriter();
JsonWriter jw = new JsonWriter(pw);

try (PipedReader pr = new PipedReader()) {
pr.connect(pw);

Thread writerThread = new Thread(new Runnable() {

@Override
public void run() {
try {
rc.printJson(jw);
} catch (IOException e) {
Assert.fail(e.getMessage());
} finally {
try {
jw.close();
} catch (IOException e) {
}
}
}
});

List<String> addedResources = new LinkedList<>();
List<String> ignoredResources = new LinkedList<>();

ResourcesRegistry registry = new ResourcesRegistry() {

@Override
public void addResources(String pattern) {
addedResources.add(pattern);
}

@Override
public void ignoreResources(String pattern) {
ignoredResources.add(pattern);
}

@Override
public void addResourceBundles(String name) {
}
};

ResourceConfigurationParser rcp = new ResourceConfigurationParser(registry);
writerThread.start();
rcp.parseAndRegister(pr);

writerThread.join();

Assert.assertTrue(addedResources.contains(".*/Resource.*txt$"));
Assert.assertTrue(ignoredResources.contains(".*/Resource2.txt$"));
} catch (IOException | InterruptedException e) {
e.printStackTrace();
Assert.fail(e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,27 @@ public void addResources(String pattern) {
configuration.addResourcePattern(pattern);
}

@Override
public void ignoreResources(String pattern) {
configuration.ignoreResourcePattern(pattern);
}

@Override
public void addResourceBundles(String name) {
configuration.addBundle(name);
}
}

private final ConcurrentMap<String, Pattern> resources = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Pattern> addedResources = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Pattern> ignoredResources = new ConcurrentHashMap<>();
private final ConcurrentHashMap.KeySetView<String, Boolean> bundles = ConcurrentHashMap.newKeySet();

public void addResourcePattern(String pattern) {
resources.computeIfAbsent(pattern, Pattern::compile);
addedResources.computeIfAbsent(pattern, Pattern::compile);
}

public void ignoreResourcePattern(String pattern) {
ignoredResources.computeIfAbsent(pattern, Pattern::compile);
}

public void addBundle(String bundle) {
Expand All @@ -71,7 +81,12 @@ public boolean anyResourceMatches(String s) {
* Naive -- if the need arises, we could match in the order of most frequently matched
* patterns, or somehow merge the patterns into a single big pattern.
*/
for (Pattern pattern : resources.values()) {
for (Pattern pattern : ignoredResources.values()) {
if (pattern.matcher(s).matches()) {
return false;
}
}
for (Pattern pattern : addedResources.values()) {
if (pattern.matcher(s).matches()) {
return true;
}
Expand All @@ -86,9 +101,15 @@ public boolean anyBundleMatches(String s) {
@Override
public void printJson(JsonWriter writer) throws IOException {
writer.append('{').indent().newline();
writer.quote("resources").append(':');
JsonPrinter.printCollection(writer, resources.keySet(), Comparator.naturalOrder(), (String p, JsonWriter w) -> w.append('{').quote("pattern").append(':').quote(p).append('}'));
writer.append(',').newline();
writer.quote("resources").append(':').append('{').newline();
writer.quote("includes").append(':');
JsonPrinter.printCollection(writer, addedResources.keySet(), Comparator.naturalOrder(), (String p, JsonWriter w) -> w.append('{').quote("pattern").append(':').quote(p).append('}'));
if (!ignoredResources.isEmpty()) {
writer.append(',').newline();
writer.quote("excludes").append(':');
JsonPrinter.printCollection(writer, ignoredResources.keySet(), Comparator.naturalOrder(), (String p, JsonWriter w) -> w.append('{').quote("pattern").append(':').quote(p).append('}'));
}
writer.append('}').append(',').newline();
writer.quote("bundles").append(':');
JsonPrinter.printCollection(writer, bundles, Comparator.naturalOrder(), (String p, JsonWriter w) -> w.append('{').quote("name").append(':').quote(p).append('}'));
writer.unindent().newline().append('}').newline();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,35 @@ private void parseTopLevelObject(Map<String, Object> obj) {
}
}
if (resourcesObject != null) {
List<Object> resources = asList(resourcesObject, "Attribute 'resources' must be a list of resources");
for (Object object : resources) {
parseEntry(object, "pattern", registry::addResources, "resource descriptor object", "'resources' list");
if (resourcesObject instanceof Map) { // New format
Object includesObject = null;
Object excludesObject = null;
for (Map.Entry<String, Object> pair : ((Map<String, Object>) resourcesObject).entrySet()) {
if ("includes".equals(pair.getKey())) {
includesObject = pair.getValue();
} else if ("excludes".equals(pair.getKey())) {
excludesObject = pair.getValue();
} else {
throw new JSONParserException("Unknown attribute '" + pair.getKey() + "' (supported attributes: name) in resource definition");
}
}

List<Object> includes = asList(includesObject, "Attribute 'includes' must be a list of resources");
for (Object object : includes) {
parseEntry(object, "pattern", registry::addResources, "resource descriptor object", "'includes' list");
}

if (excludesObject != null) {
List<Object> excludes = asList(excludesObject, "Attribute 'excludes' must be a list of resources");
for (Object object : excludes) {
parseEntry(object, "pattern", registry::ignoreResources, "resource descriptor object", "'excludes' list");
}
}
} else { // Old format: may be deprecated in future versions
List<Object> resources = asList(resourcesObject, "Attribute 'resources' must be a list of resources");
for (Object object : resources) {
parseEntry(object, "pattern", registry::addResources, "resource descriptor object", "'resources' list");
}
}
}
if (bundlesObject != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,7 @@
public interface ResourcesRegistry {
void addResources(String pattern);

void ignoreResources(String pattern);

void addResourceBundles(String name);
}
Loading