Skip to content

Commit

Permalink
Adapt locales support for GraalVM >= 24.2
Browse files Browse the repository at this point in the history
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
  • Loading branch information
zakkak committed Oct 22, 2024
1 parent 7273685 commit 7f7d3d0
Show file tree
Hide file tree
Showing 24 changed files with 358 additions and 41 deletions.
2 changes: 1 addition & 1 deletion .github/native-tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
{
"category": "Misc2",
"timeout": 75,
"test-modules": "hibernate-validator, test-extension/tests, logging-gelf, mailer, native-config-profile, locales/all, locales/some",
"test-modules": "hibernate-validator, test-extension/tests, logging-gelf, mailer, native-config-profile, locales/all, locales/some, locales/default",
"os-name": "ubuntu-latest"
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ public interface NativeConfig {

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

/**
* Defines the user country used for building the native executable.
* It also serves as the default Locale country for the native executable application runtime.
* With GraalVM versions prior to GraalVM for JDK 24 it also serves as the default Locale country for the native executable
* application runtime.
* e.g. US or FR as defined by ISO 3166-1 alpha-2 codes.
* <p>
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ public static final class Version extends io.quarkus.runtime.graal.GraalVM.Versi
public static final Version VERSION_24_0_0 = new Version("GraalVM 24.0.0", "24.0.0", "22", Distribution.GRAALVM);
public static final Version VERSION_24_0_999 = new Version("GraalVM 24.0.999", "24.0.999", "22", Distribution.GRAALVM);
public static final Version VERSION_24_1_0 = new Version("GraalVM 24.1.0", "24.1.0", "23", Distribution.GRAALVM);
public static final Version VERSION_24_1_999 = new Version("GraalVM 24.1.999", "24.1.999", "23", Distribution.GRAALVM);
public static final Version VERSION_24_2_0 = new Version("GraalVM 24.2.0", "24.2.0", "24", Distribution.GRAALVM);

/**
* The minimum version of GraalVM supported by Quarkus.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -746,15 +746,6 @@ public NativeImageInvokerInfo build() {
}
}
}

final String userLanguage = LocaleProcessor.nativeImageUserLanguage(nativeConfig, localesBuildTimeConfig);
if (!userLanguage.isEmpty()) {
nativeImageArgs.add("-J-Duser.language=" + userLanguage);
}
final String userCountry = LocaleProcessor.nativeImageUserCountry(nativeConfig, localesBuildTimeConfig);
if (!userCountry.isEmpty()) {
nativeImageArgs.add("-J-Duser.country=" + userCountry);
}
final String includeLocales = LocaleProcessor.nativeImageIncludeLocales(nativeConfig, localesBuildTimeConfig);
if (!includeLocales.isEmpty()) {
if ("all".equals(includeLocales)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import io.quarkus.deployment.builditem.GeneratedResourceBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.pkg.NativeConfig;
import io.quarkus.deployment.pkg.steps.NativeBuild;
Expand Down Expand Up @@ -59,6 +60,23 @@ void servicesResource(BuildProducer<NativeImageResourceBuildItem> nativeImageRes
"sun.util.resources.provider.LocaleDataProvider".getBytes(StandardCharsets.UTF_8)));
}

/**
* These exports are only required for GraalVM for JDK < 24, but don't cause any issues for newer versions.
* To be removed once we drop support for GraalVM for JDK < 24.
*/
@BuildStep(onlyIf = NativeBuild.class)
void setDefaults(BuildProducer<NativeImageSystemPropertyBuildItem> buildtimeSystemProperties,
NativeConfig nativeConfig, LocalesBuildTimeConfig localesBuildTimeConfig) {
String language = nativeImageUserLanguage(nativeConfig, localesBuildTimeConfig);
if (!language.isEmpty()) {
buildtimeSystemProperties.produce(new NativeImageSystemPropertyBuildItem("user.language", language));
}
String country = nativeImageUserCountry(nativeConfig, localesBuildTimeConfig);
if (!country.isEmpty()) {
buildtimeSystemProperties.produce(new NativeImageSystemPropertyBuildItem("user.country", country));
}
}

/**
* We activate additional resources in native-image executable only if user opts
* for anything else than what is already the system default.
Expand All @@ -80,7 +98,8 @@ public boolean getAsBoolean() {
(nativeConfig.userCountry().isPresent()
&& !Locale.getDefault().getCountry().equals(nativeConfig.userCountry().get()))
||
!Locale.getDefault().equals(localesBuildTimeConfig.defaultLocale)
(localesBuildTimeConfig.defaultLocale.isPresent() &&
!Locale.getDefault().equals(localesBuildTimeConfig.defaultLocale.get()))
||
localesBuildTimeConfig.locales.stream().anyMatch(l -> !Locale.getDefault().equals(l));
}
Expand All @@ -93,9 +112,14 @@ public boolean getAsBoolean() {
* @param localesBuildTimeConfig
* @return User language set by 'quarkus.default-locale' or by deprecated 'quarkus.native.user-language' or
* effectively LocalesBuildTimeConfig.DEFAULT_LANGUAGE if none of the aforementioned is set.
* @Deprecated
*/
@Deprecated
public static String nativeImageUserLanguage(NativeConfig nativeConfig, LocalesBuildTimeConfig localesBuildTimeConfig) {
String language = localesBuildTimeConfig.defaultLocale.getLanguage();
String language = System.getProperty("user.language", "en");
if (localesBuildTimeConfig.defaultLocale.isPresent()) {
language = localesBuildTimeConfig.defaultLocale.get().getLanguage();
}
if (nativeConfig.userLanguage().isPresent()) {
log.warn(DEPRECATED_USER_LANGUAGE_WARNING);
// The deprecated option takes precedence for users who are already using it.
Expand All @@ -112,9 +136,14 @@ public static String nativeImageUserLanguage(NativeConfig nativeConfig, LocalesB
* @return User country set by 'quarkus.default-locale' or by deprecated 'quarkus.native.user-country' or
* effectively LocalesBuildTimeConfig.DEFAULT_COUNTRY (could be an empty string) if none of the aforementioned is
* set.
* @Deprecated
*/
@Deprecated
public static String nativeImageUserCountry(NativeConfig nativeConfig, LocalesBuildTimeConfig localesBuildTimeConfig) {
String country = localesBuildTimeConfig.defaultLocale.getCountry();
String country = System.getProperty("user.country", "");
if (localesBuildTimeConfig.defaultLocale.isPresent()) {
country = localesBuildTimeConfig.defaultLocale.get().getCountry();
}
if (nativeConfig.userCountry().isPresent()) {
log.warn(DEPRECATED_USER_COUNTRY_WARNING);
// The deprecated option takes precedence for users who are already using it.
Expand All @@ -124,7 +153,7 @@ public static String nativeImageUserCountry(NativeConfig nativeConfig, LocalesBu
}

/**
* Additional locales to be included in native-image executable.
* Locales to be included in native-image executable.
*
* @param nativeConfig
* @param localesBuildTimeConfig
Expand All @@ -139,17 +168,18 @@ public static String nativeImageIncludeLocales(NativeConfig nativeConfig, Locale
return "all";
}

// We subtract what we already declare for native-image's user.language or user.country.
// Note the deprecated options still count.
additionalLocales.remove(localesBuildTimeConfig.defaultLocale);
// GraalVM for JDK 24 doesn't include the default locale used at build time. We must explicitly include the
// specified locales - including the build-time locale if set by the user.
// Note the deprecated options still count and take precedence.
if (nativeConfig.userCountry().isPresent() && nativeConfig.userLanguage().isPresent()) {
additionalLocales.remove(new Locale(nativeConfig.userLanguage().get(), nativeConfig.userCountry().get()));
additionalLocales.add(new Locale(nativeConfig.userLanguage().get(), nativeConfig.userCountry().get()));
} else if (nativeConfig.userLanguage().isPresent()) {
additionalLocales.remove(new Locale(nativeConfig.userLanguage().get()));
additionalLocales.add(new Locale(nativeConfig.userLanguage().get()));
} else if (localesBuildTimeConfig.defaultLocale.isPresent()) {
additionalLocales.add(localesBuildTimeConfig.defaultLocale.get());
}

return additionalLocales.stream()
.filter(l -> !Locale.getDefault().equals(l))
.map(l -> l.getLanguage() + (l.getCountry().isEmpty() ? "" : "-" + l.getCountry()))
.collect(Collectors.joining(","));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.graalvm.nativeimage.ImageSingletons;
import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.RuntimeClassInitialization;
import org.graalvm.nativeimage.hosted.RuntimeSystemProperties;

import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
Expand All @@ -18,13 +19,17 @@
import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedPackageBuildItem;
import io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.UnsafeAccessedFieldBuildItem;
import io.quarkus.deployment.pkg.NativeConfig;
import io.quarkus.gizmo.BranchResult;
import io.quarkus.gizmo.BytecodeCreator;
import io.quarkus.gizmo.CatchBlockCreator;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.ClassOutput;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.gizmo.TryBlock;
import io.quarkus.runtime.LocalesBuildTimeConfig;
import io.quarkus.runtime.graal.GraalVM;

public class NativeImageFeatureStep {
Expand All @@ -35,6 +40,12 @@ public class NativeImageFeatureStep {
Class.class);
private static final MethodDescriptor BUILD_TIME_INITIALIZATION = ofMethod(RuntimeClassInitialization.class,
"initializeAtBuildTime", void.class, String[].class);
private static final MethodDescriptor REGISTER_RUNTIME_SYSTEM_PROPERTIES = ofMethod(RuntimeSystemProperties.class,
"register", void.class, String.class, String.class);
private static final MethodDescriptor GRAALVM_VERSION_GET_CURRENT = ofMethod(GraalVM.Version.class, "getCurrent",
GraalVM.Version.class);
private static final MethodDescriptor GRAALVM_VERSION_COMPARE_TO = ofMethod(GraalVM.Version.class, "compareTo", int.class,
int[].class);
private static final MethodDescriptor INITIALIZE_CLASSES_AT_RUN_TIME = ofMethod(RuntimeClassInitialization.class,
"initializeAtRunTime", void.class, Class[].class);
private static final MethodDescriptor INITIALIZE_PACKAGES_AT_RUN_TIME = ofMethod(RuntimeClassInitialization.class,
Expand All @@ -58,11 +69,12 @@ void addExportsToNativeImage(BuildProducer<JPMSExportBuildItem> features) {

@BuildStep
void generateFeature(BuildProducer<GeneratedNativeImageClassBuildItem> nativeImageClass,
BuildProducer<JPMSExportBuildItem> exports,
List<RuntimeInitializedClassBuildItem> runtimeInitializedClassBuildItems,
List<RuntimeInitializedPackageBuildItem> runtimeInitializedPackageBuildItems,
List<RuntimeReinitializedClassBuildItem> runtimeReinitializedClassBuildItems,
List<UnsafeAccessedFieldBuildItem> unsafeAccessedFields) {
List<UnsafeAccessedFieldBuildItem> unsafeAccessedFields,
NativeConfig nativeConfig,
LocalesBuildTimeConfig localesBuildTimeConfig) {
ClassCreator file = new ClassCreator(new ClassOutput() {
@Override
public void write(String s, byte[] bytes) {
Expand All @@ -81,6 +93,38 @@ public void write(String s, byte[] bytes) {
overallCatch.invokeStaticMethod(BUILD_TIME_INITIALIZATION,
overallCatch.marshalAsArray(String.class, overallCatch.load(""))); // empty string means initialize everything

// Set the user.language and user.country system properties to the default locale
// The deprecated option takes precedence for users who are already using it.
if (nativeConfig.userLanguage().isPresent()) {
overallCatch.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES,
overallCatch.load("user.language"), overallCatch.load(nativeConfig.userLanguage().get()));
if (nativeConfig.userCountry().isPresent()) {
overallCatch.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES,
overallCatch.load("user.country"), overallCatch.load(nativeConfig.userCountry().get()));
}
} else if (localesBuildTimeConfig.defaultLocale.isPresent()) {
overallCatch.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES,
overallCatch.load("user.language"),
overallCatch.load(localesBuildTimeConfig.defaultLocale.get().getLanguage()));
overallCatch.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES,
overallCatch.load("user.country"),
overallCatch.load(localesBuildTimeConfig.defaultLocale.get().getCountry()));
} else {
ResultHandle graalVMVersion = overallCatch.invokeStaticMethod(GRAALVM_VERSION_GET_CURRENT);
BranchResult graalVm24_2Test = overallCatch
.ifGreaterEqualZero(overallCatch.invokeVirtualMethod(GRAALVM_VERSION_COMPARE_TO, graalVMVersion,
overallCatch.marshalAsArray(int.class, overallCatch.load(24), overallCatch.load(2))));
/* GraalVM >= 24.2 */
try (BytecodeCreator greaterEqual24_2 = graalVm24_2Test.trueBranch()) {
greaterEqual24_2.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES,
greaterEqual24_2.load("user.language"),
greaterEqual24_2.load("en"));
greaterEqual24_2.invokeStaticMethod(REGISTER_RUNTIME_SYSTEM_PROPERTIES,
greaterEqual24_2.load("user.country"),
greaterEqual24_2.load("US"));
}
}

if (!runtimeInitializedClassBuildItems.isEmpty()) {
// Class[] runtimeInitializedClasses()
MethodCreator runtimeInitializedClasses = file
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkus.runtime;

import java.util.Locale;
import java.util.Optional;
import java.util.Set;

import io.quarkus.runtime.annotations.ConfigDocPrefix;
Expand Down Expand Up @@ -44,8 +45,10 @@ public class LocalesBuildTimeConfig {
* For instance, the Hibernate Validator extension makes use of it.
* <p>
* Native-image build uses this property to derive {@code user.language} and {@code user.country} for the application's
* runtime.
* runtime. Starting with GraalVM for JDK 24 {@code user.language} and {@code user.country} can also be overridden at
* runtime, provided the selected locale was included at image build time.
*/
@ConfigItem(defaultValue = DEFAULT_LANGUAGE + "-" + DEFAULT_COUNTRY, defaultValueDocumentation = "Build system locale")
public Locale defaultLocale;
@ConfigItem(defaultValueDocumentation = "Defaults to the JVM's default locale if not set. "
+ "Starting with GraalVM for JDK 24, it defaults to en-US for native executables.")
public Optional<Locale> defaultLocale;
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public void created(BeanContainer container) {
// Locales, Locale ROOT means all locales in this setting.
.locales(localesBuildTimeConfig.locales.contains(Locale.ROOT) ? Set.of(Locale.getAvailableLocales())
: localesBuildTimeConfig.locales)
.defaultLocale(localesBuildTimeConfig.defaultLocale)
.defaultLocale(localesBuildTimeConfig.defaultLocale.orElse(Locale.getDefault()))
.beanMetaDataClassNormalizer(new ArcProxyBeanMetaDataClassNormalizer());

if (hibernateValidatorBuildTimeConfig.expressionLanguage().constraintExpressionFeatureLevel().isPresent()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
Expand Down Expand Up @@ -1417,7 +1418,7 @@ private String getDefaultLocale(AnnotationInstance bundleAnnotation, LocalesBuil
AnnotationValue localeValue = bundleAnnotation.value(BUNDLE_LOCALE);
String defaultLocale;
if (localeValue == null || localeValue.asString().equals(MessageBundle.DEFAULT_LOCALE)) {
defaultLocale = locales.defaultLocale.toLanguageTag();
defaultLocale = locales.defaultLocale.orElse(Locale.getDefault()).toLanguageTag();
} else {
defaultLocale = localeValue.asString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig
this.templateContents = Map.copyOf(context.getTemplateContents());
this.tags = context.getTags();
this.templatePathExclude = config.templatePathExclude;
this.defaultLocale = locales.defaultLocale;
this.defaultLocale = locales.defaultLocale.orElse(Locale.getDefault());
this.defaultCharset = config.defaultCharset;
this.container = Arc.container();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;

import java.util.Locale;

import org.apache.http.HttpStatus;
import org.jboss.logging.Logger;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import io.quarkus.test.junit.DisableIfBuiltWithGraalVMNewerThan;
import io.quarkus.test.junit.DisableIfBuiltWithGraalVMOlderThan;
import io.quarkus.test.junit.GraalVMVersion;
import io.quarkus.test.junit.QuarkusIntegrationTest;
import io.restassured.RestAssured;

Expand Down Expand Up @@ -125,4 +130,31 @@ public void testValidationMessageLocale(String acceptLanguage, String expectedMe
.then()
.body(containsString(expectedMessage));
}

// This test works best in a non-english locale.
@Test
@DisableIfBuiltWithGraalVMNewerThan(value = GraalVMVersion.GRAALVM_24_1_999)
public void testDefaultLocalePre24_2() {
RestAssured.given().when()
.get("/default/de-CH")
.then()
.statusCode(HttpStatus.SC_OK)
// With GraalVM < 24.2, the default locale is picked up by the build system when not set by the user.
.body(is(Locale.forLanguageTag("de-CH").getDisplayCountry()))
.log().all();
}

// This test works best in a non-english locale.
@Test
@DisableIfBuiltWithGraalVMOlderThan(value = GraalVMVersion.GRAALVM_24_2_0)
public void testDefaultLocalePost24_1() {
RestAssured.given().when()
.get("/default/de-CH")
.then()
.statusCode(HttpStatus.SC_OK)
// Starting with GraalVM 24.2, the default locale is en-US when not set by the user.
.body(is("Switzerland"))
.log().all();
}

}
Loading

0 comments on commit 7f7d3d0

Please sign in to comment.