Skip to content

Commit 7f7d3d0

Browse files
committed
Adapt locales support for GraalVM >= 24.2
Starting with GraalVM for JDK 24 (24.2) native image will no longer set the locale default at build time. As a result, the default locale won't be included by default in the native image unless explicitly specified. As discussed in quarkusio#43533 (reply in thread) this patch updates the locales support so that: - if neither `quarkus.locales` nor `quarkus.default-locale` is set, the Quarkus applications should default to English (`en_US`), instead of the build systems locale (which is the current behavior), at run-time. - if `quarkus.default-locale` is set but `quarkus.locales` is not set, then we should only include the locale `quarkus.default-locale` is set to. This is the current behavior with GraalVM for JDK 21. - if both `quarkus.default-locale` and `quarkus.locales` are set, then we should include only the locales from `quarkus.locales` and the one from `quarkus.default-locale` (this is the current behavior). - if `quarkus.locales` is set but `quarkus.default-locale` is not set, then we should include only the locales from `quarkus.locales` and default to English, instead of the build systems locale (which is the current behavior), at run-time (similarly to point 1). - if `quarkus.default-locale` (which is build time fixed) is set, it is used to set the default `user.language` and `user.country` values at run-time, while users may still override them. For points 2 and 3 starting with graalVM for JDK 24 we also include `en_US` which shouldn't be a big issue as mentioned in quarkusio#43533 (reply in thread), CAUTION: Point 1 changes the current behavior, meaning we need to clearly document and communicate it. This patch also updates the Locales integration tests accordingly. See oracle/graal#9694
1 parent 7273685 commit 7f7d3d0

File tree

24 files changed

+358
-41
lines changed

24 files changed

+358
-41
lines changed

.github/native-tests.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
{
106106
"category": "Misc2",
107107
"timeout": 75,
108-
"test-modules": "hibernate-validator, test-extension/tests, logging-gelf, mailer, native-config-profile, locales/all, locales/some",
108+
"test-modules": "hibernate-validator, test-extension/tests, logging-gelf, mailer, native-config-profile, locales/all, locales/some, locales/default",
109109
"os-name": "ubuntu-latest"
110110
},
111111
{

core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ public interface NativeConfig {
8888

8989
/**
9090
* Defines the user language used for building the native executable.
91-
* It also serves as the default Locale language for the native executable application runtime.
91+
* With GraalVM versions prior to GraalVM for JDK 24 it also serves as the default Locale language for the native executable
92+
* application runtime.
9293
* e.g. en or cs as defined by IETF BCP 47 language tags.
9394
* <p>
9495
*
@@ -100,7 +101,8 @@ public interface NativeConfig {
100101

101102
/**
102103
* Defines the user country used for building the native executable.
103-
* It also serves as the default Locale country for the native executable application runtime.
104+
* With GraalVM versions prior to GraalVM for JDK 24 it also serves as the default Locale country for the native executable
105+
* application runtime.
104106
* e.g. US or FR as defined by ISO 3166-1 alpha-2 codes.
105107
* <p>
106108
*

core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ public static final class Version extends io.quarkus.runtime.graal.GraalVM.Versi
191191
public static final Version VERSION_24_0_0 = new Version("GraalVM 24.0.0", "24.0.0", "22", Distribution.GRAALVM);
192192
public static final Version VERSION_24_0_999 = new Version("GraalVM 24.0.999", "24.0.999", "22", Distribution.GRAALVM);
193193
public static final Version VERSION_24_1_0 = new Version("GraalVM 24.1.0", "24.1.0", "23", Distribution.GRAALVM);
194+
public static final Version VERSION_24_1_999 = new Version("GraalVM 24.1.999", "24.1.999", "23", Distribution.GRAALVM);
195+
public static final Version VERSION_24_2_0 = new Version("GraalVM 24.2.0", "24.2.0", "24", Distribution.GRAALVM);
194196

195197
/**
196198
* The minimum version of GraalVM supported by Quarkus.

core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -746,15 +746,6 @@ public NativeImageInvokerInfo build() {
746746
}
747747
}
748748
}
749-
750-
final String userLanguage = LocaleProcessor.nativeImageUserLanguage(nativeConfig, localesBuildTimeConfig);
751-
if (!userLanguage.isEmpty()) {
752-
nativeImageArgs.add("-J-Duser.language=" + userLanguage);
753-
}
754-
final String userCountry = LocaleProcessor.nativeImageUserCountry(nativeConfig, localesBuildTimeConfig);
755-
if (!userCountry.isEmpty()) {
756-
nativeImageArgs.add("-J-Duser.country=" + userCountry);
757-
}
758749
final String includeLocales = LocaleProcessor.nativeImageIncludeLocales(nativeConfig, localesBuildTimeConfig);
759750
if (!includeLocales.isEmpty()) {
760751
if ("all".equals(includeLocales)) {

core/deployment/src/main/java/io/quarkus/deployment/steps/LocaleProcessor.java

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import io.quarkus.deployment.builditem.GeneratedResourceBuildItem;
1515
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem;
1616
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem;
17+
import io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem;
1718
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
1819
import io.quarkus.deployment.pkg.NativeConfig;
1920
import io.quarkus.deployment.pkg.steps.NativeBuild;
@@ -59,6 +60,23 @@ void servicesResource(BuildProducer<NativeImageResourceBuildItem> nativeImageRes
5960
"sun.util.resources.provider.LocaleDataProvider".getBytes(StandardCharsets.UTF_8)));
6061
}
6162

63+
/**
64+
* These exports are only required for GraalVM for JDK < 24, but don't cause any issues for newer versions.
65+
* To be removed once we drop support for GraalVM for JDK < 24.
66+
*/
67+
@BuildStep(onlyIf = NativeBuild.class)
68+
void setDefaults(BuildProducer<NativeImageSystemPropertyBuildItem> buildtimeSystemProperties,
69+
NativeConfig nativeConfig, LocalesBuildTimeConfig localesBuildTimeConfig) {
70+
String language = nativeImageUserLanguage(nativeConfig, localesBuildTimeConfig);
71+
if (!language.isEmpty()) {
72+
buildtimeSystemProperties.produce(new NativeImageSystemPropertyBuildItem("user.language", language));
73+
}
74+
String country = nativeImageUserCountry(nativeConfig, localesBuildTimeConfig);
75+
if (!country.isEmpty()) {
76+
buildtimeSystemProperties.produce(new NativeImageSystemPropertyBuildItem("user.country", country));
77+
}
78+
}
79+
6280
/**
6381
* We activate additional resources in native-image executable only if user opts
6482
* for anything else than what is already the system default.
@@ -80,7 +98,8 @@ public boolean getAsBoolean() {
8098
(nativeConfig.userCountry().isPresent()
8199
&& !Locale.getDefault().getCountry().equals(nativeConfig.userCountry().get()))
82100
||
83-
!Locale.getDefault().equals(localesBuildTimeConfig.defaultLocale)
101+
(localesBuildTimeConfig.defaultLocale.isPresent() &&
102+
!Locale.getDefault().equals(localesBuildTimeConfig.defaultLocale.get()))
84103
||
85104
localesBuildTimeConfig.locales.stream().anyMatch(l -> !Locale.getDefault().equals(l));
86105
}
@@ -93,9 +112,14 @@ public boolean getAsBoolean() {
93112
* @param localesBuildTimeConfig
94113
* @return User language set by 'quarkus.default-locale' or by deprecated 'quarkus.native.user-language' or
95114
* effectively LocalesBuildTimeConfig.DEFAULT_LANGUAGE if none of the aforementioned is set.
115+
* @Deprecated
96116
*/
117+
@Deprecated
97118
public static String nativeImageUserLanguage(NativeConfig nativeConfig, LocalesBuildTimeConfig localesBuildTimeConfig) {
98-
String language = localesBuildTimeConfig.defaultLocale.getLanguage();
119+
String language = System.getProperty("user.language", "en");
120+
if (localesBuildTimeConfig.defaultLocale.isPresent()) {
121+
language = localesBuildTimeConfig.defaultLocale.get().getLanguage();
122+
}
99123
if (nativeConfig.userLanguage().isPresent()) {
100124
log.warn(DEPRECATED_USER_LANGUAGE_WARNING);
101125
// The deprecated option takes precedence for users who are already using it.
@@ -112,9 +136,14 @@ public static String nativeImageUserLanguage(NativeConfig nativeConfig, LocalesB
112136
* @return User country set by 'quarkus.default-locale' or by deprecated 'quarkus.native.user-country' or
113137
* effectively LocalesBuildTimeConfig.DEFAULT_COUNTRY (could be an empty string) if none of the aforementioned is
114138
* set.
139+
* @Deprecated
115140
*/
141+
@Deprecated
116142
public static String nativeImageUserCountry(NativeConfig nativeConfig, LocalesBuildTimeConfig localesBuildTimeConfig) {
117-
String country = localesBuildTimeConfig.defaultLocale.getCountry();
143+
String country = System.getProperty("user.country", "");
144+
if (localesBuildTimeConfig.defaultLocale.isPresent()) {
145+
country = localesBuildTimeConfig.defaultLocale.get().getCountry();
146+
}
118147
if (nativeConfig.userCountry().isPresent()) {
119148
log.warn(DEPRECATED_USER_COUNTRY_WARNING);
120149
// The deprecated option takes precedence for users who are already using it.
@@ -124,7 +153,7 @@ public static String nativeImageUserCountry(NativeConfig nativeConfig, LocalesBu
124153
}
125154

126155
/**
127-
* Additional locales to be included in native-image executable.
156+
* Locales to be included in native-image executable.
128157
*
129158
* @param nativeConfig
130159
* @param localesBuildTimeConfig
@@ -139,17 +168,18 @@ public static String nativeImageIncludeLocales(NativeConfig nativeConfig, Locale
139168
return "all";
140169
}
141170

142-
// We subtract what we already declare for native-image's user.language or user.country.
143-
// Note the deprecated options still count.
144-
additionalLocales.remove(localesBuildTimeConfig.defaultLocale);
171+
// GraalVM for JDK 24 doesn't include the default locale used at build time. We must explicitly include the
172+
// specified locales - including the build-time locale if set by the user.
173+
// Note the deprecated options still count and take precedence.
145174
if (nativeConfig.userCountry().isPresent() && nativeConfig.userLanguage().isPresent()) {
146-
additionalLocales.remove(new Locale(nativeConfig.userLanguage().get(), nativeConfig.userCountry().get()));
175+
additionalLocales.add(new Locale(nativeConfig.userLanguage().get(), nativeConfig.userCountry().get()));
147176
} else if (nativeConfig.userLanguage().isPresent()) {
148-
additionalLocales.remove(new Locale(nativeConfig.userLanguage().get()));
177+
additionalLocales.add(new Locale(nativeConfig.userLanguage().get()));
178+
} else if (localesBuildTimeConfig.defaultLocale.isPresent()) {
179+
additionalLocales.add(localesBuildTimeConfig.defaultLocale.get());
149180
}
150181

151182
return additionalLocales.stream()
152-
.filter(l -> !Locale.getDefault().equals(l))
153183
.map(l -> l.getLanguage() + (l.getCountry().isEmpty() ? "" : "-" + l.getCountry()))
154184
.collect(Collectors.joining(","));
155185
}

core/deployment/src/main/java/io/quarkus/deployment/steps/NativeImageFeatureStep.java

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.graalvm.nativeimage.ImageSingletons;
1010
import org.graalvm.nativeimage.hosted.Feature;
1111
import org.graalvm.nativeimage.hosted.RuntimeClassInitialization;
12+
import org.graalvm.nativeimage.hosted.RuntimeSystemProperties;
1213

1314
import io.quarkus.deployment.annotations.BuildProducer;
1415
import io.quarkus.deployment.annotations.BuildStep;
@@ -18,13 +19,17 @@
1819
import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedPackageBuildItem;
1920
import io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem;
2021
import io.quarkus.deployment.builditem.nativeimage.UnsafeAccessedFieldBuildItem;
22+
import io.quarkus.deployment.pkg.NativeConfig;
23+
import io.quarkus.gizmo.BranchResult;
24+
import io.quarkus.gizmo.BytecodeCreator;
2125
import io.quarkus.gizmo.CatchBlockCreator;
2226
import io.quarkus.gizmo.ClassCreator;
2327
import io.quarkus.gizmo.ClassOutput;
2428
import io.quarkus.gizmo.MethodCreator;
2529
import io.quarkus.gizmo.MethodDescriptor;
2630
import io.quarkus.gizmo.ResultHandle;
2731
import io.quarkus.gizmo.TryBlock;
32+
import io.quarkus.runtime.LocalesBuildTimeConfig;
2833
import io.quarkus.runtime.graal.GraalVM;
2934

3035
public class NativeImageFeatureStep {
@@ -35,6 +40,12 @@ public class NativeImageFeatureStep {
3540
Class.class);
3641
private static final MethodDescriptor BUILD_TIME_INITIALIZATION = ofMethod(RuntimeClassInitialization.class,
3742
"initializeAtBuildTime", void.class, String[].class);
43+
private static final MethodDescriptor REGISTER_RUNTIME_SYSTEM_PROPERTIES = ofMethod(RuntimeSystemProperties.class,
44+
"register", void.class, String.class, String.class);
45+
private static final MethodDescriptor GRAALVM_VERSION_GET_CURRENT = ofMethod(GraalVM.Version.class, "getCurrent",
46+
GraalVM.Version.class);
47+
private static final MethodDescriptor GRAALVM_VERSION_COMPARE_TO = ofMethod(GraalVM.Version.class, "compareTo", int.class,
48+
int[].class);
3849
private static final MethodDescriptor INITIALIZE_CLASSES_AT_RUN_TIME = ofMethod(RuntimeClassInitialization.class,
3950
"initializeAtRunTime", void.class, Class[].class);
4051
private static final MethodDescriptor INITIALIZE_PACKAGES_AT_RUN_TIME = ofMethod(RuntimeClassInitialization.class,
@@ -58,11 +69,12 @@ void addExportsToNativeImage(BuildProducer<JPMSExportBuildItem> features) {
5869

5970
@BuildStep
6071
void generateFeature(BuildProducer<GeneratedNativeImageClassBuildItem> nativeImageClass,
61-
BuildProducer<JPMSExportBuildItem> exports,
6272
List<RuntimeInitializedClassBuildItem> runtimeInitializedClassBuildItems,
6373
List<RuntimeInitializedPackageBuildItem> runtimeInitializedPackageBuildItems,
6474
List<RuntimeReinitializedClassBuildItem> runtimeReinitializedClassBuildItems,
65-
List<UnsafeAccessedFieldBuildItem> unsafeAccessedFields) {
75+
List<UnsafeAccessedFieldBuildItem> unsafeAccessedFields,
76+
NativeConfig nativeConfig,
77+
LocalesBuildTimeConfig localesBuildTimeConfig) {
6678
ClassCreator file = new ClassCreator(new ClassOutput() {
6779
@Override
6880
public void write(String s, byte[] bytes) {
@@ -81,6 +93,38 @@ public void write(String s, byte[] bytes) {
8193
overallCatch.invokeStaticMethod(BUILD_TIME_INITIALIZATION,
8294
overallCatch.marshalAsArray(String.class, overallCatch.load(""))); // empty string means initialize everything
8395

96+
// Set the user.language and user.country system properties to the default locale
97+
// The deprecated option takes precedence for users who are already using it.
98+
if (nativeConfig.userLanguage().isPresent()) {
99+
overallCatch.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES,
100+
overallCatch.load("user.language"), overallCatch.load(nativeConfig.userLanguage().get()));
101+
if (nativeConfig.userCountry().isPresent()) {
102+
overallCatch.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES,
103+
overallCatch.load("user.country"), overallCatch.load(nativeConfig.userCountry().get()));
104+
}
105+
} else if (localesBuildTimeConfig.defaultLocale.isPresent()) {
106+
overallCatch.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES,
107+
overallCatch.load("user.language"),
108+
overallCatch.load(localesBuildTimeConfig.defaultLocale.get().getLanguage()));
109+
overallCatch.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES,
110+
overallCatch.load("user.country"),
111+
overallCatch.load(localesBuildTimeConfig.defaultLocale.get().getCountry()));
112+
} else {
113+
ResultHandle graalVMVersion = overallCatch.invokeStaticMethod(GRAALVM_VERSION_GET_CURRENT);
114+
BranchResult graalVm24_2Test = overallCatch
115+
.ifGreaterEqualZero(overallCatch.invokeVirtualMethod(GRAALVM_VERSION_COMPARE_TO, graalVMVersion,
116+
overallCatch.marshalAsArray(int.class, overallCatch.load(24), overallCatch.load(2))));
117+
/* GraalVM >= 24.2 */
118+
try (BytecodeCreator greaterEqual24_2 = graalVm24_2Test.trueBranch()) {
119+
greaterEqual24_2.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES,
120+
greaterEqual24_2.load("user.language"),
121+
greaterEqual24_2.load("en"));
122+
greaterEqual24_2.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES,
123+
greaterEqual24_2.load("user.country"),
124+
greaterEqual24_2.load("US"));
125+
}
126+
}
127+
84128
if (!runtimeInitializedClassBuildItems.isEmpty()) {
85129
// Class[] runtimeInitializedClasses()
86130
MethodCreator runtimeInitializedClasses = file

core/runtime/src/main/java/io/quarkus/runtime/LocalesBuildTimeConfig.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.quarkus.runtime;
22

33
import java.util.Locale;
4+
import java.util.Optional;
45
import java.util.Set;
56

67
import io.quarkus.runtime.annotations.ConfigDocPrefix;
@@ -44,8 +45,10 @@ public class LocalesBuildTimeConfig {
4445
* For instance, the Hibernate Validator extension makes use of it.
4546
* <p>
4647
* Native-image build uses this property to derive {@code user.language} and {@code user.country} for the application's
47-
* runtime.
48+
* runtime. Starting with GraalVM for JDK 24 {@code user.language} and {@code user.country} can also be overridden at
49+
* runtime, provided the selected locale was included at image build time.
4850
*/
49-
@ConfigItem(defaultValue = DEFAULT_LANGUAGE + "-" + DEFAULT_COUNTRY, defaultValueDocumentation = "Build system locale")
50-
public Locale defaultLocale;
51+
@ConfigItem(defaultValueDocumentation = "Defaults to the JVM's default locale if not set. "
52+
+ "Starting with GraalVM for JDK 24, it defaults to en-US for native executables.")
53+
public Optional<Locale> defaultLocale;
5154
}

extensions/hibernate-validator/runtime/src/main/java/io/quarkus/hibernate/validator/runtime/HibernateValidatorRecorder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public void created(BeanContainer container) {
8686
// Locales, Locale ROOT means all locales in this setting.
8787
.locales(localesBuildTimeConfig.locales.contains(Locale.ROOT) ? Set.of(Locale.getAvailableLocales())
8888
: localesBuildTimeConfig.locales)
89-
.defaultLocale(localesBuildTimeConfig.defaultLocale)
89+
.defaultLocale(localesBuildTimeConfig.defaultLocale.orElse(Locale.getDefault()))
9090
.beanMetaDataClassNormalizer(new ArcProxyBeanMetaDataClassNormalizer());
9191

9292
if (hibernateValidatorBuildTimeConfig.expressionLanguage().constraintExpressionFeatureLevel().isPresent()) {

extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.LinkedHashMap;
1919
import java.util.List;
2020
import java.util.ListIterator;
21+
import java.util.Locale;
2122
import java.util.Map;
2223
import java.util.Map.Entry;
2324
import java.util.Set;
@@ -1417,7 +1418,7 @@ private String getDefaultLocale(AnnotationInstance bundleAnnotation, LocalesBuil
14171418
AnnotationValue localeValue = bundleAnnotation.value(BUNDLE_LOCALE);
14181419
String defaultLocale;
14191420
if (localeValue == null || localeValue.asString().equals(MessageBundle.DEFAULT_LOCALE)) {
1420-
defaultLocale = locales.defaultLocale.toLanguageTag();
1421+
defaultLocale = locales.defaultLocale.orElse(Locale.getDefault()).toLanguageTag();
14211422
} else {
14221423
defaultLocale = localeValue.asString();
14231424
}

extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig
9797
this.templateContents = Map.copyOf(context.getTemplateContents());
9898
this.tags = context.getTags();
9999
this.templatePathExclude = config.templatePathExclude;
100-
this.defaultLocale = locales.defaultLocale;
100+
this.defaultLocale = locales.defaultLocale.orElse(Locale.getDefault());
101101
this.defaultCharset = config.defaultCharset;
102102
this.container = Arc.container();
103103

0 commit comments

Comments
 (0)