So, the situation is: * timezone_t exists * The functions tzalloc, tzfree, mktime_z, localtime_rz exist timezone_t _Nullable tzalloc(const char* _Nullable __id) __INTRODUCED_IN(35); void tzfree(timezone_t _Nullable __tz) __INTRODUCED_IN(35); time_t mktime_z(timezone_t _Nonnull __tz, struct tm* _Nonnull __tm) __INTRODUCED_IN(35); struct tm* _Nullable localtime_rz(timezone_t _Nonnull __tz, const time_t* _Nonnull __t, struct tm* _Nonnull __tm) __INTRODUCED_IN(35); (See ~/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/time.h .) * But strftime_z or strftime_lz with timezone_t argument does not exist.
That does not give us the ability to set and restore the global timezone (via the "TZ" environment variable) and call strftime. Therefore in strftime.c we cannot use the 'underlying_strftime' code. I considered 4 approaches to work around this: A) Override timezone_t, tzalloc, tzfree, mktime_z, localtime_rz. This will work because Gnulib's rpl_timezone_t contains textual time zone information, that can be transformed into a TZ variable. B,C,D) Don't use system's strftime, always use gnulib's strftime entirely. #if defined _LIBC || (USE_C_LOCALE && !HAVE_STRFTIME_L) -> #if defined _LIBC || ((USE_C_LOCALE || defined __ANDROID__) && !HAVE_STRFTIME_L) B) Always use the C locale. This is suitable because the C locale is the only one supported on Android. (See bionic/libc/tzcode/strftime.c .) C) Fetch the localized values of this struct via calls to strftime(). Optionally cache them. struct locale_time_data { /* Abbreviated week day names. */ const char *abbr_weekday[7]; /* Full week day names. */ const char *full_weekday[7]; /* Abbreviated month names. */ const char *abbr_month[12]; /* Full month names. */ const char *full_month[12]; /* Abbreviated alternate month names. */ const char *abbr_altmonth[12]; /* Full alternate month names. */ const char *full_altmonth[12]; /* Ante meridiem / Post meridiem indicators. */ const char *am_pm[2]; /* Format string for %c. */ const char *d_t_fmt; /* Format string for %x. */ const char *d_fmt; /* Format string for %X. */ const char *t_fmt; /* Format string for %r. */ const char *t_ampm_fmt; # if 0 /* Not included here: Support for alternate era. */ const char **era; size_t num_era; const char *era_year; const char *era_d_t_fmt; const char *era_d_fmt; const char *era_t_fmt; const char *alt_digits[100]; # endif }; D) Incorporate a read-only database of such 'struct locale_time_data', with lookup rules like dcgettext("gnulib", ..., LC_TIME) would have. Generate this database from the glibc localedata sources. I went for approach B, because it is the simplest one. 2025-05-08 Bruno Haible <br...@clisp.org> nstrftime: Fix a link error on Android API level ≥ 35. Reported by Po Lu <luang...@yahoo.com> in <https://lists.gnu.org/archive/html/bug-gnulib/2025-05/msg00069.html>. * lib/strftime.c (HAVE_ONLY_C_LOCALE): New macro. (REQUIRE_GNUISH_STRFTIME_AM_PM): Set to false if HAVE_ONLY_C_LOCALE is 1. (TOUPPER): Use c_toupper if HAVE_ONLY_C_LOCALE is 1. (c_locale_cache, c_locale): Don't define if HAVE_ONLY_C_LOCALE && HAVE_STRUCT_TM_TM_ZONE. (utc_timezone_cache, utc_timezone): Don't define if (HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L)) && HAVE_STRUCT_TM_TM_ZONE. (c_weekday_names, c_month_names): Define also if HAVE_ONLY_C_LOCALE is 1. (underlying_strftime): Don't define if HAVE_ONLY_C_LOCALE && HAVE_STRUCT_TM_TM_ZONE && !(USE_C_LOCALE && !HAVE_STRFTIME_L). (get_tm_zone): On Android, don't return NULL. (__strftime_internal): If HAVE_ONLY_C_LOCALE is 1, don't invoke underlying_strftime. * modules/nstrftime (Depends-on): Add c-ctype. diff --git a/lib/strftime.c b/lib/strftime.c index 27feb1d728..64a1f0456a 100644 --- a/lib/strftime.c +++ b/lib/strftime.c @@ -14,10 +14,14 @@ You should have received a copy of the GNU Lesser General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. */ +/* If FPRINTFTIME is set to 1, this file defines a function with a + 'FILE *fp' parameter instead of two 'char *s, size_t max' parameters. */ #ifndef FPRINTFTIME # define FPRINTFTIME 0 #endif +/* If USE_C_LOCALE is set to 1, this file defines a function that uses the + "C" locale, regardless of the current locale. */ #ifndef USE_C_LOCALE # define USE_C_LOCALE 0 #endif @@ -38,6 +42,14 @@ # include "time-internal.h" #endif +/* Whether the system supports no localized output at all, that is, whether + strftime's output does not depend on the current locale. */ +#if defined __ANDROID__ +# define HAVE_ONLY_C_LOCALE 1 +#else +# define HAVE_ONLY_C_LOCALE 0 +#endif + /* Whether to require GNU behavior for AM and PM indicators, even on other platforms. This matters only in non-C locales. The default is to require it; you can override this via @@ -46,12 +58,12 @@ #ifndef REQUIRE_GNUISH_STRFTIME_AM_PM # define REQUIRE_GNUISH_STRFTIME_AM_PM true #endif -#if USE_C_LOCALE +#if HAVE_ONLY_C_LOCALE || USE_C_LOCALE # undef REQUIRE_GNUISH_STRFTIME_AM_PM # define REQUIRE_GNUISH_STRFTIME_AM_PM false #endif -#if USE_C_LOCALE +#if HAVE_ONLY_C_LOCALE || USE_C_LOCALE # include "c-ctype.h" #else # include <ctype.h> @@ -302,7 +314,7 @@ enum pad_style # define TOUPPER(Ch, L) __toupper_l (Ch, L) # define TOLOWER(Ch, L) __tolower_l (Ch, L) # else -# if USE_C_LOCALE +# if HAVE_ONLY_C_LOCALE || USE_C_LOCALE # define TOUPPER(Ch, L) c_toupper (Ch) # define TOLOWER(Ch, L) c_tolower (Ch) # else @@ -379,7 +391,8 @@ memcpy_uppcase (CHAR_T *dest, const CHAR_T *src, size_t len LOCALE_PARAM) #define HAVE_NATIVE_TIME_Z \ (USE_C_LOCALE && HAVE_STRFTIME_L ? HAVE_STRFTIME_LZ : HAVE_STRFTIME_Z) -#if USE_C_LOCALE && HAVE_STRFTIME_L +#if (!HAVE_ONLY_C_LOCALE || !HAVE_STRUCT_TM_TM_ZONE) \ + && USE_C_LOCALE && HAVE_STRFTIME_L /* Cache for the C locale object. Marked volatile so that different threads see the same value @@ -398,7 +411,10 @@ c_locale (void) #endif -#if HAVE_NATIVE_TIME_Z +#if !defined _LIBC \ + && (!(HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L)) \ + || !HAVE_STRUCT_TM_TM_ZONE) \ + && HAVE_NATIVE_TIME_Z /* On NetBSD a null tz has undefined behavior, so use a non-null tz. Cache the UTC time zone object in a volatile variable for improved @@ -818,7 +834,7 @@ iso_week_days (int yday, int wday) } -#if !defined _NL_CURRENT && (USE_C_LOCALE && !HAVE_STRFTIME_L) +#if !defined _NL_CURRENT && (HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L)) static CHAR_T const c_weekday_names[][sizeof "Wednesday"] = { L_("Sunday"), L_("Monday"), L_("Tuesday"), L_("Wednesday"), @@ -861,7 +877,8 @@ static size_t __strftime_internal (STREAM_OR_CHAR_T *, STRFTIME_ARG (size_t) extra_args_spec LOCALE_PARAM); #if !defined _LIBC \ - && (!(USE_C_LOCALE && !HAVE_STRFTIME_L) || !HAVE_STRUCT_TM_TM_ZONE) + && (!(HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L)) \ + || !HAVE_STRUCT_TM_TM_ZONE) /* Make sure we're calling the actual underlying strftime. In some cases, time.h contains something like @@ -1028,7 +1045,12 @@ get_tm_zone (timezone_t tz, char *ubuf, int ubufsize, int modifier, *TP is computed with a totally different time zone. This is bogus: though POSIX allows bad behavior like this, POSIX does not require it. Do the right thing instead. */ - return tp->tm_zone; + const char *ret = tp->tm_zone; +# if defined __ANDROID__ + if (!ret) + ret = ""; +# endif + return ret; #else if (!tz) return "UTC"; @@ -1125,7 +1147,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) # define am_len STRLEN (a_month) # define aam_len STRLEN (a_altmonth) # define ap_len STRLEN (ampm) -#elif USE_C_LOCALE && !HAVE_STRFTIME_L +#elif HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L) /* The English abbreviated weekday names are just the first 3 characters of the English full weekday names. */ # define a_wkday \ @@ -1383,7 +1405,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) to_uppcase = true; to_lowcase = false; } -#if defined _NL_CURRENT || (USE_C_LOCALE && !HAVE_STRFTIME_L) +#if defined _NL_CURRENT || HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L) cpy (aw_len, a_wkday); break; #else @@ -1398,7 +1420,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) to_uppcase = true; to_lowcase = false; } -#if defined _NL_CURRENT || (USE_C_LOCALE && !HAVE_STRFTIME_L) +#if defined _NL_CURRENT || HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L) cpy (STRLEN (f_wkday), f_wkday); break; #else @@ -1420,7 +1442,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) else cpy (am_len, a_month); break; -#elif USE_C_LOCALE && !HAVE_STRFTIME_L +#elif HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L) cpy (am_len, a_month); break; #else @@ -1444,7 +1466,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) else cpy (STRLEN (f_month), f_month); break; -#elif USE_C_LOCALE && !HAVE_STRFTIME_L +#elif HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L) cpy (STRLEN (f_month), f_month); break; #else @@ -1461,7 +1483,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) NLW(ERA_D_T_FMT))) != '\0'))) subfmt = (const CHAR_T *) _NL_CURRENT (LC_TIME, NLW(D_T_FMT)); -#elif USE_C_LOCALE && !HAVE_STRFTIME_L +#elif HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L) subfmt = L_("%a %b %e %H:%M:%S %Y"); #elif defined _WIN32 && !defined __CYGWIN__ /* On native Windows, "%c" is "%d/%m/%Y %H:%M:%S" by default. */ @@ -1500,7 +1522,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) } break; -#if !defined _LIBC && !(USE_C_LOCALE && !HAVE_STRFTIME_L) +#if !defined _LIBC && !(HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L)) underlying_strftime: { char ubuf[1024]; /* enough for any single format in practice */ @@ -1600,7 +1622,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) # endif break; } -#elif USE_C_LOCALE && !HAVE_STRFTIME_L +#elif HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L) #else goto underlying_strftime; #endif @@ -1624,7 +1646,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) != L_('\0')))) subfmt = (const CHAR_T *) _NL_CURRENT (LC_TIME, NLW(D_FMT)); goto subformat; -#elif USE_C_LOCALE && !HAVE_STRFTIME_L +#elif HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L) subfmt = L_("%m/%d/%y"); goto subformat; #else @@ -1702,7 +1724,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) break; } } -#elif USE_C_LOCALE && !HAVE_STRFTIME_L +#elif HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L) #else goto underlying_strftime; #endif @@ -1850,7 +1872,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) to_uppcase = false; to_lowcase = true; } -#if defined _NL_CURRENT || (USE_C_LOCALE && !HAVE_STRFTIME_L) +#if defined _NL_CURRENT || HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L) cpy (ap_len, ampm); break; #else @@ -1871,7 +1893,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) == L_('\0')) subfmt = L_("%I:%M:%S %p"); goto subformat; -#elif USE_C_LOCALE && !HAVE_STRFTIME_L +#elif HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L) subfmt = L_("%I:%M:%S %p"); goto subformat; #elif ((defined __APPLE__ && defined __MACH__) || defined __FreeBSD__ \ @@ -1933,7 +1955,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) != L_('\0')))) subfmt = (const CHAR_T *) _NL_CURRENT (LC_TIME, NLW(T_FMT)); goto subformat; -#elif USE_C_LOCALE && !HAVE_STRFTIME_L +#elif HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L) subfmt = L_("%H:%M:%S"); goto subformat; #else @@ -2043,7 +2065,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) pad = yr_spec; goto subformat; } -#elif USE_C_LOCALE && !HAVE_STRFTIME_L +#elif HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L) #else goto underlying_strftime; #endif @@ -2067,7 +2089,7 @@ __strftime_internal (STREAM_OR_CHAR_T *s, STRFTIME_ARG (size_t maxsize) DO_NUMBER (2, (era->offset + delta * era->absolute_direction)); } -#elif USE_C_LOCALE && !HAVE_STRFTIME_L +#elif HAVE_ONLY_C_LOCALE || (USE_C_LOCALE && !HAVE_STRFTIME_L) #else goto underlying_strftime; #endif diff --git a/modules/nstrftime b/modules/nstrftime index 023fea853c..6ff773f741 100644 --- a/modules/nstrftime +++ b/modules/nstrftime @@ -10,6 +10,7 @@ m4/tm_gmtoff.m4 Depends-on: attribute +c-ctype c99 errno-h extensions