Skip to content

Commit b2265d5

Browse files
committed
[GR-43971] Support native-image environment variable capturing in bundles.
PullRequest: graal/13914
2 parents 6219844 + 61d617b commit b2265d5

File tree

5 files changed

+229
-91
lines changed

5 files changed

+229
-91
lines changed

substratevm/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ This changelog summarizes major changes to GraalVM Native Image.
2626
* (GR-40641) Dynamic linking of AWT libraries on Linux.
2727
* (GR-40463) Red Hat added experimental support for JMX, which can be enabled with the `--enable-monitoring` option (e.g. `--enable-monitoring=jmxclient,jmxserver`).
2828
* (GR-44110) Native Image now targets `x86-64-v3` by default on AMD64 and supports a new `-march` option. Use `-march=compatibility` for best compatibility (previous default) or `-march=native` for best performance if the native executable is deployed on the same machine or on a machine with the same CPU features. To list all available machine types, use `-march=list`.
29+
* (GR-43971) Add native-image option `-E<env-var-key>[=<env-var-value>]` and support environment variable capturing in bundles. Previously almost all environment variables were available in the builder. To temporarily revert back to the old behaviour, env setting `NATIVE_IMAGE_SLOPPY_BUILDER_SANITATION=true` can be used. The old behaviour will be removed in a future release.
2930

3031
## Version 22.3.0
3132
* (GR-35721) Remove old build output style and the `-H:±BuildOutputUseNewStyle` option.

substratevm/src/com.oracle.svm.driver/resources/HelpExtra.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,11 @@ Non-standard options help:
3434
creates a new bundle app_dbg.nib based on the given app.nib bundle.
3535
Both bundles are the same except the new one also uses the -g option.
3636

37+
-E<env-var-key>[=<env-var-value>]
38+
allow native-image to access the given environment variable during
39+
image build. If the optional <env-var-value> is not given, the value
40+
of the environment variable will be taken from the environment
41+
native-image was invoked from.
42+
3743
-V<key>=<value> provide values for placeholders in native-image.properties files
3844

substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/BundleSupport.java

Lines changed: 102 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@
4040
import java.time.format.FormatStyle;
4141
import java.util.ArrayList;
4242
import java.util.Arrays;
43-
import java.util.Collection;
4443
import java.util.Collections;
4544
import java.util.HashMap;
4645
import java.util.HashSet;
4746
import java.util.List;
47+
import java.util.ListIterator;
4848
import java.util.Map;
4949
import java.util.Objects;
5050
import java.util.Optional;
@@ -86,8 +86,8 @@ final class BundleSupport {
8686
Map<Path, Path> pathCanonicalizations = new HashMap<>();
8787
Map<Path, Path> pathSubstitutions = new HashMap<>();
8888

89-
private final List<String> buildArgs;
90-
private Collection<String> updatedBuildArgs;
89+
private final List<String> nativeImageArgs;
90+
private List<String> updatedNativeImageArgs;
9191

9292
boolean loadBundle;
9393
boolean writeBundle;
@@ -110,9 +110,13 @@ final class BundleSupport {
110110
static final String BUNDLE_OPTION = "--bundle";
111111
static final String BUNDLE_FILE_EXTENSION = ".nib";
112112

113-
private enum BundleOptionVariants {
113+
enum BundleOptionVariants {
114114
create(),
115-
apply()
115+
apply();
116+
117+
String optionName() {
118+
return BUNDLE_OPTION + "-" + this;
119+
}
116120
}
117121

118122
static BundleSupport create(NativeImage nativeImage, String bundleArg, NativeImage.ArgumentQueue args) {
@@ -121,10 +125,6 @@ static BundleSupport create(NativeImage nativeImage, String bundleArg, NativeIma
121125
"Bundle support is still experimental and needs to be unlocked with '" + UNLOCK_BUNDLE_SUPPORT_OPTION + "'. The unlock option must precede '" + bundleArg + "'.");
122126
}
123127

124-
if (!nativeImage.userConfigProperties.isEmpty()) {
125-
throw NativeImage.showError("Bundle support cannot be combined with " + NativeImage.CONFIG_FILE_ENV_VAR_KEY + " environment variable use.");
126-
}
127-
128128
try {
129129
String variant = bundleArg.substring(BUNDLE_OPTION.length() + 1);
130130
String bundleFilename = null;
@@ -133,31 +133,31 @@ static BundleSupport create(NativeImage nativeImage, String bundleArg, NativeIma
133133
variant = variantParts[0];
134134
bundleFilename = variantParts[1];
135135
}
136-
String applyOptionStr = BUNDLE_OPTION + "-" + BundleOptionVariants.apply;
137-
String createOptionStr = BUNDLE_OPTION + "-" + BundleOptionVariants.create;
136+
String applyOptionName = BundleOptionVariants.apply.optionName();
137+
String createOptionName = BundleOptionVariants.create.optionName();
138138
BundleSupport bundleSupport;
139139
switch (BundleOptionVariants.valueOf(variant)) {
140140
case apply:
141141
if (nativeImage.useBundle()) {
142142
if (nativeImage.bundleSupport.loadBundle) {
143-
throw NativeImage.showError(String.format("native-image allows option %s to be specified only once.", applyOptionStr));
143+
throw NativeImage.showError(String.format("native-image allows option %s to be specified only once.", applyOptionName));
144144
}
145145
if (nativeImage.bundleSupport.writeBundle) {
146-
throw NativeImage.showError(String.format("native-image option %s is not allowed to be used after option %s.", applyOptionStr, createOptionStr));
146+
throw NativeImage.showError(String.format("native-image option %s is not allowed to be used after option %s.", applyOptionName, createOptionName));
147147
}
148148
}
149149
if (bundleFilename == null) {
150-
throw NativeImage.showError(String.format("native-image option %s requires a bundle file argument. E.g. %s=bundle-file.nib.", applyOptionStr, applyOptionStr));
150+
throw NativeImage.showError(String.format("native-image option %s requires a bundle file argument. E.g. %s=bundle-file.nib.", applyOptionName, applyOptionName));
151151
}
152152
bundleSupport = new BundleSupport(nativeImage, bundleFilename);
153153
/* Inject the command line args from the loaded bundle in-place */
154-
List<String> buildArgs = bundleSupport.getBuildArgs();
154+
List<String> buildArgs = bundleSupport.getNativeImageArgs();
155155
for (int i = buildArgs.size() - 1; i >= 0; i--) {
156156
args.push(buildArgs.get(i));
157157
}
158158
nativeImage.showVerboseMessage(nativeImage.isVerbose(), BUNDLE_INFO_MESSAGE_PREFIX + "Inject args: '" + String.join(" ", buildArgs) + "'");
159159
/* Snapshot args after in-place expansion (includes also args after this one) */
160-
bundleSupport.updatedBuildArgs = args.snapshot();
160+
bundleSupport.updatedNativeImageArgs = args.snapshot();
161161
break;
162162
case create:
163163
if (nativeImage.useBundle()) {
@@ -209,7 +209,7 @@ private BundleSupport(NativeImage nativeImage) {
209209
} catch (IOException e) {
210210
throw NativeImage.showError("Unable to create bundle directory layout", e);
211211
}
212-
this.buildArgs = Collections.unmodifiableList(nativeImage.config.getBuildArgs());
212+
this.nativeImageArgs = nativeImage.getNativeImageArgs();
213213
}
214214

215215
private BundleSupport(NativeImage nativeImage, String bundleFilenameArg) {
@@ -279,18 +279,25 @@ private BundleSupport(NativeImage nativeImage, String bundleFilenameArg) {
279279
} catch (IOException e) {
280280
throw NativeImage.showError("Failed to read bundle-file " + pathSubstitutionsFile, e);
281281
}
282+
Path environmentFile = stageDir.resolve("environment.json");
283+
try (Reader reader = Files.newBufferedReader(environmentFile)) {
284+
new EnvironmentParser(nativeImage.imageBuilderEnvironment).parseAndRegister(reader);
285+
} catch (IOException e) {
286+
throw NativeImage.showError("Failed to read bundle-file " + environmentFile, e);
287+
}
288+
282289
Path buildArgsFile = stageDir.resolve("build.json");
283290
try (Reader reader = Files.newBufferedReader(buildArgsFile)) {
284291
List<String> buildArgsFromFile = new ArrayList<>();
285292
new BuildArgsParser(buildArgsFromFile).parseAndRegister(reader);
286-
buildArgs = Collections.unmodifiableList(buildArgsFromFile);
293+
nativeImageArgs = Collections.unmodifiableList(buildArgsFromFile);
287294
} catch (IOException e) {
288-
throw NativeImage.showError("Failed to read bundle-file " + pathSubstitutionsFile, e);
295+
throw NativeImage.showError("Failed to read bundle-file " + buildArgsFile, e);
289296
}
290297
}
291298

292-
public List<String> getBuildArgs() {
293-
return buildArgs;
299+
public List<String> getNativeImageArgs() {
300+
return nativeImageArgs;
294301
}
295302

296303
Path recordCanonicalization(Path before, Path after) {
@@ -409,8 +416,8 @@ private Path substitutePath(Path origPath, Path destinationDir) {
409416
return origPath;
410417
}
411418

412-
// TODO Report error if overlapping dir-trees are passed in
413-
// TODO add .endsWith(ClasspathUtils.cpWildcardSubstitute) handling (copy whole directory)
419+
// TODO: Report error if overlapping dir-trees are passed in
420+
414421
String origFileName = origPath.getFileName().toString();
415422
int extensionPos = origFileName.lastIndexOf('.');
416423
String baseName;
@@ -440,6 +447,16 @@ private Path substitutePath(Path origPath, Path destinationDir) {
440447
return substitutedPath;
441448
}
442449

450+
Path originalPath(Path substitutedPath) {
451+
Path relativeSubstitutedPath = rootDir.relativize(substitutedPath);
452+
for (Map.Entry<Path, Path> entry : pathSubstitutions.entrySet()) {
453+
if (entry.getValue().equals(relativeSubstitutedPath)) {
454+
return entry.getKey();
455+
}
456+
}
457+
return null;
458+
}
459+
443460
private void copyFiles(Path source, Path target, boolean overwrite) {
444461
nativeImage.showVerboseMessage(nativeImage.isVVerbose(), "> Copy files from " + source + " to " + target);
445462
if (Files.isDirectory(source)) {
@@ -567,39 +584,36 @@ private Path writeBundle() {
567584
} catch (IOException e) {
568585
throw NativeImage.showError("Failed to write bundle-file " + pathSubstitutionsFile, e);
569586
}
587+
Path environmentFile = stageDir.resolve("environment.json");
588+
try (JsonWriter writer = new JsonWriter(environmentFile)) {
589+
/* Printing as list with defined sort-order ensures useful diffs are possible */
590+
JsonPrinter.printCollection(writer, nativeImage.imageBuilderEnvironment.entrySet(), Map.Entry.comparingByKey(), BundleSupport::printEnvironmentVariable);
591+
} catch (IOException e) {
592+
throw NativeImage.showError("Failed to write bundle-file " + environmentFile, e);
593+
}
570594

571595
Path buildArgsFile = stageDir.resolve("build.json");
572596
try (JsonWriter writer = new JsonWriter(buildArgsFile)) {
573-
ArrayList<String> cleanBuildArgs = new ArrayList<>();
574-
for (String buildArg : updatedBuildArgs != null ? updatedBuildArgs : buildArgs) {
575-
if (buildArg.equals(UNLOCK_BUNDLE_SUPPORT_OPTION)) {
576-
continue;
577-
}
578-
if (buildArg.startsWith(BUNDLE_OPTION)) {
579-
continue;
580-
}
581-
if (buildArg.startsWith(nativeImage.oHPath)) {
582-
continue;
583-
}
584-
if (buildArg.equals(CmdLineOptionHandler.VERBOSE_OPTION)) {
585-
continue;
586-
}
587-
if (buildArg.equals(CmdLineOptionHandler.DRY_RUN_OPTION)) {
588-
continue;
589-
}
590-
if (buildArg.startsWith("-Dllvm.bin.dir=")) {
591-
Optional<String> existing = nativeImage.config.getBuildArgs().stream().filter(arg -> arg.startsWith("-Dllvm.bin.dir=")).findFirst();
592-
if (existing.isPresent() && !existing.get().equals(buildArg)) {
593-
throw NativeImage.showError("Bundle native-image argument '" + buildArg + "' conflicts with existing '" + existing.get() + "'.");
597+
List<String> equalsNonBundleOptions = List.of(UNLOCK_BUNDLE_SUPPORT_OPTION, CmdLineOptionHandler.VERBOSE_OPTION, CmdLineOptionHandler.DRY_RUN_OPTION);
598+
List<String> startsWithNonBundleOptions = List.of(BUNDLE_OPTION, DefaultOptionHandler.ADD_ENV_VAR_OPTION, nativeImage.oHPath);
599+
ArrayList<String> bundleArgs = new ArrayList<>(updatedNativeImageArgs != null ? updatedNativeImageArgs : nativeImageArgs);
600+
ListIterator<String> bundleArgsIterator = bundleArgs.listIterator();
601+
while (bundleArgsIterator.hasNext()) {
602+
String arg = bundleArgsIterator.next();
603+
if (equalsNonBundleOptions.contains(arg) || startsWithNonBundleOptions.stream().anyMatch(arg::startsWith)) {
604+
bundleArgsIterator.remove();
605+
} else if (arg.startsWith("-Dllvm.bin.dir=")) {
606+
Optional<String> existing = nativeImage.config.getBuildArgs().stream().filter(a -> a.startsWith("-Dllvm.bin.dir=")).findFirst();
607+
if (existing.isPresent() && !existing.get().equals(arg)) {
608+
throw NativeImage.showError("Bundle native-image argument '" + arg + "' conflicts with existing '" + existing.get() + "'.");
594609
}
595-
continue;
610+
bundleArgsIterator.remove();
596611
}
597-
cleanBuildArgs.add(buildArg);
598612
}
599613
/* Printing as list with defined sort-order ensures useful diffs are possible */
600-
JsonPrinter.printCollection(writer, cleanBuildArgs, null, BundleSupport::printBuildArg);
614+
JsonPrinter.printCollection(writer, bundleArgs, null, BundleSupport::printBuildArg);
601615
} catch (IOException e) {
602-
throw NativeImage.showError("Failed to write bundle-file " + pathSubstitutionsFile, e);
616+
throw NativeImage.showError("Failed to write bundle-file " + buildArgsFile, e);
603617
}
604618

605619
bundleProperties.write();
@@ -639,7 +653,7 @@ private static Manifest createManifest() {
639653
private static final String substitutionMapDstField = "dst";
640654

641655
private static void printPathMapping(Map.Entry<Path, Path> entry, JsonWriter w) throws IOException {
642-
w.append('{').quote(substitutionMapSrcField).append(" : ").quote(entry.getKey());
656+
w.append('{').quote(substitutionMapSrcField).append(':').quote(entry.getKey());
643657
w.append(',').quote(substitutionMapDstField).append(':').quote(entry.getValue());
644658
w.append('}');
645659
}
@@ -648,6 +662,18 @@ private static void printBuildArg(String entry, JsonWriter w) throws IOException
648662
w.quote(entry);
649663
}
650664

665+
private static final String environmentKeyField = "key";
666+
private static final String environmentValueField = "val";
667+
668+
private static void printEnvironmentVariable(Map.Entry<String, String> entry, JsonWriter w) throws IOException {
669+
if (entry.getValue() == null) {
670+
throw NativeImage.showError("Storing environment variable '" + entry.getKey() + "' in bundle requires to have its value defined.");
671+
}
672+
w.append('{').quote(environmentKeyField).append(':').quote(entry.getKey());
673+
w.append(',').quote(environmentValueField).append(':').quote(entry.getValue());
674+
w.append('}');
675+
}
676+
651677
private static final class PathMapParser extends ConfigurationParser {
652678

653679
private final Map<Path, Path> pathMap;
@@ -674,6 +700,33 @@ public void parseAndRegister(Object json, URI origin) {
674700
}
675701
}
676702

703+
private static final class EnvironmentParser extends ConfigurationParser {
704+
705+
private final Map<String, String> environment;
706+
707+
private EnvironmentParser(Map<String, String> environment) {
708+
super(true);
709+
environment.clear();
710+
this.environment = environment;
711+
}
712+
713+
@Override
714+
public void parseAndRegister(Object json, URI origin) {
715+
for (var rawEntry : asList(json, "Expected a list of environment variable objects")) {
716+
var entry = asMap(rawEntry, "Expected a environment variable object");
717+
Object envVarKeyString = entry.get(environmentKeyField);
718+
if (envVarKeyString == null) {
719+
throw new JSONParserException("Expected " + environmentKeyField + "-field in environment variable object");
720+
}
721+
Object envVarValueString = entry.get(environmentValueField);
722+
if (envVarValueString == null) {
723+
throw new JSONParserException("Expected " + environmentValueField + "-field in environment variable object");
724+
}
725+
environment.put(envVarKeyString.toString(), envVarValueString.toString());
726+
}
727+
}
728+
}
729+
677730
private static final class BuildArgsParser extends ConfigurationParser {
678731

679732
private final List<String> args;

substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/DefaultOptionHandler.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ class DefaultOptionHandler extends NativeImage.OptionHandler<NativeImage> {
4545
static final String addModulesOption = "--add-modules";
4646
private static final String addModulesErrorMessage = " requires modules to be specified";
4747

48+
static final String ADD_ENV_VAR_OPTION = "-E";
49+
4850
/* Defunct legacy options that we have to accept to maintain backward compatibility */
4951
private static final String noServerOption = "--no-server";
5052

@@ -166,6 +168,14 @@ public boolean consume(ArgumentQueue args) {
166168
nativeImage.addOptionKeyValue(keyValue[0], keyValue[1]);
167169
return true;
168170
}
171+
if (headArg.startsWith(ADD_ENV_VAR_OPTION)) {
172+
args.poll();
173+
String envVarSetting = headArg.substring(ADD_ENV_VAR_OPTION.length());
174+
String[] keyValue = envVarSetting.split("=", 2);
175+
String valueDefinedOrInherited = keyValue.length > 1 ? keyValue[1] : null;
176+
nativeImage.imageBuilderEnvironment.put(keyValue[0], valueDefinedOrInherited);
177+
return true;
178+
}
169179
if (headArg.startsWith("-J")) {
170180
args.poll();
171181
if (headArg.equals("-J")) {
@@ -191,7 +201,7 @@ public boolean consume(ArgumentQueue args) {
191201
Path origArgFile = Paths.get(headArg);
192202
Path argFile = nativeImage.bundleSupport != null ? nativeImage.bundleSupport.substituteAuxiliaryPath(origArgFile, BundleMember.Role.Input) : origArgFile;
193203
NativeImage.NativeImageArgsProcessor processor = nativeImage.new NativeImageArgsProcessor(OptionOrigin.argFilePrefix + argFile);
194-
readArgFile(argFile).forEach(processor::accept);
204+
readArgFile(argFile).forEach(processor);
195205
List<String> leftoverArgs = processor.apply(false);
196206
if (leftoverArgs.size() > 0) {
197207
NativeImage.showError(String.format("Found unrecognized options while parsing argument file '%s':%n%s", argFile, String.join(System.lineSeparator(), leftoverArgs)));
@@ -211,7 +221,7 @@ enum PARSER_STATE {
211221
IN_TOKEN
212222
}
213223

214-
class CTX_ARGS {
224+
static class CTX_ARGS {
215225
PARSER_STATE state;
216226
int cptr;
217227
int eob;
@@ -221,7 +231,7 @@ class CTX_ARGS {
221231
}
222232

223233
// Ported from JDK11's java.base/share/native/libjli/args.c
224-
private List<String> readArgFile(Path file) {
234+
private static List<String> readArgFile(Path file) {
225235
List<String> arguments = new ArrayList<>();
226236
// Use of the at sign (@) to recursively interpret files isn't supported.
227237
arguments.add("--disable-@files");

0 commit comments

Comments
 (0)