fontconfig: Branch 'main' - 3 commits

GitLab Mirror gitlab-mirror at kemper.freedesktop.org
Mon Jul 28 10:32:17 UTC 2025


 configure.ac              |   10 ++++-
 meson.build               |    6 ++-
 src/fcarch.c              |    2 -
 src/fccache.c             |   39 +++++++++++++++++++++
 src/fcint.h               |    3 +
 src/meson.build           |    6 +++
 test/fctest/__init__.py   |    4 ++
 test/meson.build          |   15 +++++---
 test/test-gen-testcache.c |   45 ++++++++++++++++++++++++
 test/test_sandbox.py      |   83 ++++++++++++++++++++++++++++++++++++++++++++++
 10 files changed, 204 insertions(+), 9 deletions(-)

New commits:
commit ce174010e814be20e5c74141b2a86f986d89cb75
Merge: 8f169b6 59da606
Author: Akira TAGOH <akira at tagoh.org>
Date:   Mon Jul 28 10:32:14 2025 +0000

    Merge branch 'fc-version-in-cache' into 'main'
    
    Add fontconfig version in FcCache
    
    See merge request fontconfig/fontconfig!452

commit 59da606145558a0041eb90d9c80a26a6f0c1d348
Author: Akira TAGOH <akira at tagoh.org>
Date:   Fri Jul 18 19:19:50 2025 +0900

    Add fontconfig version in FcCache
    
    Even if cache files needs to be updated, do not re-generate them
    if it was generated by newer version of fontconfig.
    
    Fontconfig cache files are basically compatible with older versions
    of fontconfig unless any drastic changes happens there and bump a
    cache version. older versions of fontconfig simply ignore unknown
    objects in caches.
    However, a troublesome situation is coming by sharing caches between
    host and containers. A situation of updating caches is likely to
    happens on even containers when mtime of font directories are updated.
    And it may drops such unknown objects. So this aims to avoid such
    situation and keep not updating and warn to encourage users to update
    caches at host.
    
    Also, added FONTCONFIG_NO_CHECK_CACHE_VERSION environment variable to
    revert this behavior for contigency plan. This will be removed in
    the future.
    
    Changelog: changed

diff --git a/configure.ac b/configure.ac
index 2d48096..578a894 100644
--- a/configure.ac
+++ b/configure.ac
@@ -68,8 +68,16 @@ dnl Initialize libtool
 LT_PREREQ([2.2])
 LT_INIT([disable-static win32-dll])
 
+dnl fc version
+eval `echo $VERSION |
+	awk -F. '{ printf ("major=%d\nminor=%d\nrevision=%d\n",
+			   $1, $2, $3); }'`
+AC_DEFINE_UNQUOTED([FC_VERSION_MAJOR], [$major], [major version])
+AC_DEFINE_UNQUOTED([FC_VERSION_MINOR], [$minor], [minor version])
+AC_DEFINE_UNQUOTED([FC_VERSION_MICRO], [$revision], [revision])
+
 dnl cache version
-CACHE_VERSION=9
+CACHE_VERSION=10
 AC_SUBST(CACHE_VERSION)
 
 dnl libtool versioning
diff --git a/meson.build b/meson.build
index e742ea6..63a2ca8 100644
--- a/meson.build
+++ b/meson.build
@@ -25,7 +25,7 @@ curversion = fc_version_minor - 1
 libversion = '@0 at .@1 at .0'.format(soversion, curversion)
 defversion = '@0 at .@1@'.format(curversion, fc_version_micro)
 osxversion = curversion + 1
-cacheversion = '9'
+cacheversion = '10'
 
 freetype_req = '>= 21.0.15'
 freetype_req_cmake = '>= 2.8.1'
@@ -35,6 +35,10 @@ math_dep = cc.find_library('m', required: false)
 
 conf = configuration_data()
 
+conf.set('FC_VERSION_MAJOR', fc_version_major)
+conf.set('FC_VERSION_MINOR', fc_version_minor)
+conf.set('FC_VERSION_MICRO', fc_version_micro)
+
 freetype_dep = dependency('freetype2', method: 'pkg-config', version: freetype_req, required: false)
 
 # Give another shot using CMake
diff --git a/src/fcarch.c b/src/fcarch.c
index 66a0240..b1882d0 100644
--- a/src/fcarch.c
+++ b/src/fcarch.c
@@ -50,7 +50,7 @@ FC_ASSERT_STATIC (0x08 + 1 * FC_MAX (SIZEOF_VOID_P, ALIGNOF_DOUBLE) == sizeof (F
 FC_ASSERT_STATIC (0x00 + 2 * SIZEOF_VOID_P == sizeof (FcPatternElt));
 FC_ASSERT_STATIC (0x08 + 2 * SIZEOF_VOID_P == sizeof (FcPattern));
 FC_ASSERT_STATIC (0x08 + 2 * SIZEOF_VOID_P == sizeof (FcCharSet));
-FC_ASSERT_STATIC (0x10 + 6 * SIZEOF_VOID_P == sizeof (FcCache));
+FC_ASSERT_STATIC (0x28 + 4 * SIZEOF_VOID_P == sizeof (FcCache));
 
 int
 main (int argc FC_UNUSED, char **argv FC_UNUSED)
diff --git a/src/fccache.c b/src/fccache.c
index 15791e3..5847c8e 100644
--- a/src/fccache.c
+++ b/src/fccache.c
@@ -770,6 +770,33 @@ FcCacheFini (void)
 	free_lock();
 }
 
+static FcBool
+FcCacheIsNewVersion (FcConfig *config, FcCache *cache)
+{
+    int64_t version = (FC_VERSION_MAJOR << 24) +
+                      (FC_VERSION_MINOR << 12) +
+                      FC_VERSION_MICRO;
+    static FcBool warn = FcFalse;
+    static FcBool flag = FcFalse, is_initialized = FcFalse;
+
+    /* To revert the behavior for some emergency. This will be removed in the future */
+    if (!is_initialized) {
+	const char *env = getenv ("FONTCONFIG_NO_CHECK_CACHE_VERSION");
+
+	is_initialized = FcTrue;
+	if (env)
+	    FcNameBool ((const FcChar8 *)env, &flag);
+    }
+    if (flag)
+	return !flag;
+    if (cache->fc_version > version && !warn) {
+	warn = FcTrue;
+	fprintf (stderr, "Fontconfig warning: Some cache files was generated by the newer version (0x%lx) of Fontconfig but caches is outdated. We won't regenearate it to avoid an unexpected behavior. Please regenerate caches with the latest version of Fontconfig. (current: 0x%lx)\n", cache->fc_version, version);
+    }
+
+    return cache->fc_version > version;
+}
+
 static FcBool
 FcCacheTimeValid (FcConfig *config, FcCache *cache, struct stat *dir_stat)
 {
@@ -968,6 +995,12 @@ FcDirCacheMapFd (FcConfig *config, int fd, struct stat *fd_stat, struct stat *di
     if (cache) {
 	if (FcCacheTimeValid (config, cache, dir_stat))
 	    return cache;
+	else if (FcCacheIsNewVersion (config, cache)) {
+	    /* Re-use if cache was generated by newer version of fontconfig
+	     * Do not trigger regenerating
+	     */
+	    return cache;
+	}
 	FcDirCacheUnload (cache);
 	cache = NULL;
     }
@@ -1014,7 +1047,8 @@ FcDirCacheMapFd (FcConfig *config, int fd, struct stat *fd_stat, struct stat *di
         cache->version < FC_CACHE_VERSION_NUMBER ||
         cache->size != (intptr_t)fd_stat->st_size ||
         !FcCacheOffsetsValid (cache) ||
-        !FcCacheTimeValid (config, cache, dir_stat) ||
+        (!FcCacheTimeValid (config, cache, dir_stat) &&
+         !FcCacheIsNewVersion (config, cache)) ||
         !FcCacheInsert (cache, fd_stat)) {
 	if (allocated)
 	    free (cache);
@@ -1284,6 +1318,9 @@ FcDirCacheBuild (FcFontSet *set, const FcChar8 *dir, struct stat *dir_stat, FcSt
     cache->size = serialize->size;
     cache->checksum = FcDirChecksum (dir_stat);
     cache->checksum_nano = FcDirChecksumNano (dir_stat);
+    cache->fc_version = (FC_VERSION_MAJOR << 24) +
+	               (FC_VERSION_MINOR << 12) +
+	               FC_VERSION_MICRO;
 
     /*
      * Serialize directory name
diff --git a/src/fcint.h b/src/fcint.h
index 8046cc8..9f03329 100644
--- a/src/fcint.h
+++ b/src/fcint.h
@@ -455,9 +455,12 @@ struct _FcCache {
     intptr_t     dir;           /* offset to dir name */
     intptr_t     dirs;          /* offset to subdirs */
     int          dirs_count;    /* number of subdir strings */
+    int          pad1;
     intptr_t     set;           /* offset to font set */
     int          checksum;      /* checksum of directory state */
+    int          pad2;
     int64_t      checksum_nano; /* checksum of directory state */
+    int64_t      fc_version;    /* fontconfig version */
 };
 
 #undef FcCacheDir
diff --git a/src/meson.build b/src/meson.build
index 5a93687..c4d65d6 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -73,5 +73,6 @@ lib_fontconfig_kwargs = {
 fcarch = executable('fcarch', ['fcarch.c', 'fcarch.h', fcstdint_h, fclang_h],
   include_directories: [incbase, incsrc],
   c_args: c_args,
+  dependencies: [libintl_dep],
   install: false,
   install_tag: 'runtime')
diff --git a/test/fctest/__init__.py b/test/fctest/__init__.py
index cbc3782..4abab2e 100644
--- a/test/fctest/__init__.py
+++ b/test/fctest/__init__.py
@@ -116,6 +116,10 @@ class FcTest:
         self._extra = [x for x in self._extra if not re.search(r'\b<remap-dir\b', x)]
         self._extra += [f'<remap-dir as-path="{self.fontdir.name}">{v}</remap-dir>']
 
+    @property
+    def env(self):
+        return self._env
+
     def config(self) -> str:
         return self.__conf_templ.format(fontdir=self.convert_path(self.fontdir.name),
                                         cachedir=self.convert_path(self.cachedir.name),
diff --git a/test/meson.build b/test/meson.build
index 188c8ce..f26b252 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -23,6 +23,9 @@ tests = [
   ['test-family-matching.c'],
   ['test-ptrlist.c', {'include_directories': include_directories('../src'), 'dependencies': libintl_dep}],
 ]
+tests_build_only = [
+  ['test-gen-testcache.c', {'include_directories': include_directories('../src'), 'dependencies': libintl_dep}],
+]
 tests_not_parallel = []
 
 if host_machine.system() != 'windows'
@@ -50,7 +53,7 @@ if get_option('fontations').enabled()
   link_with_libs += [fc_fontations]
 endif
 
-foreach test_data : tests + tests_not_parallel
+foreach test_data : tests + tests_not_parallel + tests_build_only
   fname = test_data[0]
   opts = test_data.length() > 1 ? test_data[1] : {}
   extra_c_args = opts.get('c_args', [])
@@ -66,10 +69,12 @@ foreach test_data : tests + tests_not_parallel
     dependencies: extra_deps,
   )
 
-  if test_data in tests
-    test(test_name, exe, timeout: 600, is_parallel: true)
-  else
-    test(test_name, exe, timeout: 600, is_parallel: false)
+  if test_data not in tests_build_only
+    if test_data in tests
+      test(test_name, exe, timeout: 600, is_parallel: true)
+    else
+      test(test_name, exe, timeout: 600, is_parallel: false)
+    endif
   endif
 endforeach
 
diff --git a/test/test-gen-testcache.c b/test/test-gen-testcache.c
new file mode 100644
index 0000000..06d8333
--- /dev/null
+++ b/test/test-gen-testcache.c
@@ -0,0 +1,45 @@
+/* Copyright (C) 2025 fontconfig Authors */
+/* SPDX-License-Identifier: HPND */
+#include "fcint.h"
+#include "src/fcint.h"
+
+int
+main (int argc, char **argv)
+{
+    FcConfig  *config;
+    FcFontSet *fs;
+    FcStrSet  *dirs;
+    FcCache   *cache;
+    struct stat st;
+    int ret = -1;
+
+    if (argc <= 1) {
+	fprintf (stderr, "Usage: %s <font path>\n", argv[0]);
+	return 1;
+    }
+    FcInit();
+    config = FcConfigGetCurrent();
+    fs = FcFontSetCreate();
+    dirs = FcStrSetCreateEx (FCSS_GROW_BY_64);
+    if (FcStatChecksum ((const FcChar8 *)argv[1], &st) < 0)
+	goto bail;
+    if (!FcDirScanConfig (fs, dirs, (const FcChar8 *)argv[1], FcTrue, config))
+	goto bail2;
+    cache = FcDirCacheBuild (fs, (const FcChar8 *)argv[1], &st, dirs);
+    if (!cache)
+	goto bail2;
+    cache->fc_version = ((FC_VERSION_MAJOR + 1) << 24) +
+                       (FC_VERSION_MINOR << 12) +
+                       FC_VERSION_MICRO;
+    FcDirCacheWrite (cache, config);
+    ret = 0;
+ bail2:
+    FcStrSetDestroy (dirs);
+ bail:
+    FcFontSetDestroy (fs);
+    FcConfigDestroy (config);
+
+    FcFini();
+
+    return ret;
+}
diff --git a/test/test_sandbox.py b/test/test_sandbox.py
index 4732fb6..2283dde 100644
--- a/test/test_sandbox.py
+++ b/test/test_sandbox.py
@@ -164,3 +164,86 @@ def test_md5_consistency(fctest, fcfont):
     # Make sure they are totally different but same filename
     assert cache_files_before == cache_files_after
     assert cmp_cache_before != cmp_cache_after
+
+
+ at pytest.mark.skipif(not not os.getenv('EXEEXT'), reason='not working on Win32')
+ at pytest.mark.skipif(not shutil.which('bwrap'), reason='No bwrap installed')
+def test_gen_testcache(fctest, fcfont):
+    testexe = Path(fctest.builddir) / 'test' / ('test-gen-testcache' + fctest._exeext)
+    if not testexe.exists():
+        testexe = Path(fctest.builddir) / 'test' / ('test_gen_testcache' + fctest._exeext)
+        if not testexe.exists():
+            raise RuntimeError('No test case for gen-testcache')
+
+    fctest.setup()
+    fctest.install_font(fcfont.fonts, '.')
+    for ret, stdout, stderr in fctest.run(testexe, [fctest.fontdir.name]):
+        assert ret == 0, stderr
+        cache_files1 = [f for f in fctest.cache_files()]
+        assert len(cache_files1) == 1, cache_files1
+        time.sleep(1)
+        # Update mtime
+        Path(fctest.fontdir.name).touch()
+        cache_stat1 = [f.stat() for f in fctest.cache_files()]
+        fctest.logger.info(cache_files1)
+
+    time.sleep(1)
+    basedir = tempfile.TemporaryDirectory(prefix='fontconfig.',
+                                          suffix='.base')
+    with fctest.sandboxed(basedir.name) as f:
+        for ret, stdout, stderr in f.run_match([]):
+            assert ret == 0, stderr
+            assert "Fontconfig warning" in stderr, stderr
+            f.logger.info(stdout)
+            f.logger.info(stderr)
+
+    cache_stat2 = [f.stat() for f in fctest.cache_files()]
+    cache_files2 = [f for f in fctest.cache_files()]
+    assert len(cache_stat2) == 1, cache_stat2
+    assert cache_files1 == cache_files2, cache_files2
+    # ignore st_atime
+    cmp_cache_before = [attrgetter('st_mode', 'st_ino', 'st_dev', 'st_nlink', 'st_uid', 'st_gid', 'st_size', 'st_mtime', 'st_ctime')(st) for st in cache_stat1]
+    cmp_cache_after = [attrgetter('st_mode', 'st_ino', 'st_dev', 'st_nlink', 'st_uid', 'st_gid', 'st_size', 'st_mtime', 'st_ctime')(st) for st in cache_stat2]
+    assert cmp_cache_before == cmp_cache_after
+
+
+ at pytest.mark.skipif(not not os.getenv('EXEEXT'), reason='not working on Win32')
+ at pytest.mark.skipif(not shutil.which('bwrap'), reason='No bwrap installed')
+def test_gen_testcache_no_check(fctest, fcfont):
+    testexe = Path(fctest.builddir) / 'test' / ('test-gen-testcache' + fctest._exeext)
+    if not testexe.exists():
+        testexe = Path(fctest.builddir) / 'test' / ('test_gen_testcache' + fctest._exeext)
+        if not testexe.exists():
+            raise RuntimeError('No test case for gen-testcache')
+
+    fctest.env['FONTCONFIG_NO_CHECK_CACHE_VERSION'] = '1'
+    fctest.setup()
+    fctest.install_font(fcfont.fonts, '.')
+    for ret, stdout, stderr in fctest.run(testexe, [fctest.fontdir.name]):
+        assert ret == 0, stderr
+        cache_files1 = [f for f in fctest.cache_files()]
+        assert len(cache_files1) == 1, cache_files1
+        time.sleep(1)
+        # Update mtime
+        Path(fctest.fontdir.name).touch()
+        cache_stat1 = [f.stat() for f in fctest.cache_files()]
+        fctest.logger.info(cache_files1)
+
+    time.sleep(1)
+    basedir = tempfile.TemporaryDirectory(prefix='fontconfig.',
+                                          suffix='.base')
+    with fctest.sandboxed(basedir.name) as f:
+        for ret, stdout, stderr in f.run_match([]):
+            assert ret == 0, stderr
+            assert "Fontconfig warning" not in stderr, stderr
+            f.logger.info(stdout)
+            f.logger.info(stderr)
+
+    cache_stat2 = [f.stat() for f in fctest.cache_files()]
+    cache_files2 = [f for f in fctest.cache_files()]
+    assert len(cache_stat2) == 1, cache_stat2
+    assert cache_files1 == cache_files2, cache_files2
+    # ignore st_atime
+    cmp_cache_before = [attrgetter('st_mode', 'st_ino', 'st_dev', 'st_nlink', 'st_uid', 'st_gid', 'st_size', 'st_mtime', 'st_ctime')(st) for st in cache_stat1]
+    cmp_cache_after = [attrgetter('st_mode', 'st_ino', 'st_dev', 'st_nlink', 'st_uid', 'st_gid', 'st_size', 'st_mtime', 'st_ctime')(st) for st in cache_stat2]
+    assert cmp_cache_before != cmp_cache_after
commit 5b040c8224798586beca65d39ea3384309790a5b
Author: Akira TAGOH <akira at tagoh.org>
Date:   Fri Jul 18 18:27:36 2025 +0900

    meson: Add a missing fontconfig architecture test case

diff --git a/src/meson.build b/src/meson.build
index bf5a781..5a93687 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -70,3 +70,8 @@ lib_fontconfig_kwargs = {
   'link_with': [pattern_lib],
 }
 
+fcarch = executable('fcarch', ['fcarch.c', 'fcarch.h', fcstdint_h, fclang_h],
+  include_directories: [incbase, incsrc],
+  c_args: c_args,
+  install: false,
+  install_tag: 'runtime')


More information about the Fontconfig mailing list