This Time Self-Hosted
dark mode light mode Search

A case study: enchant’s internal Hunspell copy

Okay Ryan, let me see if I can write something up to at least help understanding how to solve the problem (as to why it is a problem, well, that I think I explained already a couple of times 😉

I took enchant as that’s the bug you linked to, and it also seems an easy one.

First off, enchant is an abstraction library for spellchecking. It supports ispell, aspell, hspell (all three of them have a CLI interface, so theyw ork “at an arm’s length”) and MySpell (OpenOffice.org spellchecker). As it turns out, nobody really use original MySpell and rather use the Hunspell fork, even our OpenOffice.org.

As Enchant creates plugins (modules), they have to use PIC libraries; up to an year ago, more or less, Hunspell did not install any PIC compatible library, just static archives, unsuitable for shared linking, and especially unsuitable for building shared objects on PIC architectures like almost everything beside x86. This is probably the reason why an internal copy of it was used up to now.

It turns out nowadays Hunspell does install shared libraries, and even a pkg-config datafile. So it should be easy to use those instead. The first step is to give a way to choose whether to use system hunspell or not. The easiest way is usually to add a new --with-system-foo option during build. I sinerely prefer a different approach when possible: rather than having just --enable-myspell and --disable-myspell, provide a --enable-myspell=external option, that allows to use the system copy of hunspell.

So it’s just a matter of changing the current conditional not to think of the $build_myspell variable as a yes/no one, and add a new conditional:

-AM_CONDITIONAL(WITH_MYSPELL, test "x$build_myspell" = "xyes")
+AM_CONDITIONAL(WITH_SYSTEM_HUNSPELL, test "x$build_myspell" = "xexternal")
+AM_CONDITIONAL(WITH_MYSPELL, test "x$build_myspell" != "xno")

Good. By the way, seems like the build system already supposedly had some support for external myspell, but it’s not actuall used, so let’s remove the old cruft, just to avoid messing with obsolete codepaths, and rename MYSPELL_CFLAGS/MYSPELL_LIBS into HUNSPELL_CFLAGS/HUNSPELL_LIBS, I’ll show in a moment why:

-MYSPELL_CFLAGS="$MYSPELL_CFLAGS -DENCHANT_MYSPELL_DICT_DIR='"$myspell_dir"'"
-if test "x$with_system_myspell" != "xno"; then
-   MYSPELL_CFLAGS="$MYSPELL_CFLAGS -DWITH_SYSTEM_MYSPELL=1"
-fi
-
-AC_SUBST(MYSPELL_CFLAGS)
-AC_SUBST(MYSPELL_LIBS)
+HUNSPELL_CFLAGS="$HUNSPELL_CFLAGS -DENCHANT_MYSPELL_DICT_DIR='"$myspell_dir"'"

Now of course it isn’t nice to ask for system hunspell if it’s not installed, let’s see if it is there. The easiest way is to use pkg-config. As I’m not sure since which version they fixed Hunspell to install shared libraries, I check what I have installed in my system:

flame@enterprise ~ % pkg-config --modversion hunspell
1.1.9

and then add a test for that:

+if test "x$build_myspell" = "xexternal"; then
+   PKG_CHECK_MODULES([HUNSPELL], [hunspell >= 1.1.9])
+fi
+

This shows why I wanted to rename MYSPELL_CFLAGS into HUNSPELL_CFLAGS: the PKG_CHECK_MODULES macro uses the first parameter both as the prefix of the variables and to output “Checking for FOO…” during configure run. I didn’t want users to see “Checking for MYSPELL…” while Hunspell was checked instead.

Now the changes to the configure are finished, it’s time to start with the changes to the Makefiles, in particular src/myspell/Makefile.am.

First off, let’s replace MYSPELL_CFLAGS and MYSPELL_LIBS:

-INCLUDES=-I$(top_srcdir)/src $(ENCHANT_CFLAGS) $(MYSPELL_CFLAGS) -D_ENCHANT_BUILD=1
+INCLUDES=-I$(top_srcdir)/src $(ENCHANT_CFLAGS) $(HUNSPELL_CFLAGS) -D_ENCHANT_BUILD=1

....

-libenchant_myspell_la_LIBADD= $(MYSPELL_LIBS) $(ENCHANT_LIBS) $(top_builddir)/src/libenchant.la
+libenchant_myspell_la_LIBADD= $(HUNSPELL_LIBS) $(ENCHANT_LIBS) $(top_builddir)/src/libenchant.la

Then, let’s not build all the Hunspell sources when we’re using external Hunspell, to do so, we create a commodity variable hunspell_sources to list all the hunspell sources, but only if WITH_EXTERNAL_HUNSPELL is unset:

-libenchant_myspell_la_SOURCES =        
+if !WITH_SYSTEM_HUNSPELL
+hunspell_sources = 
        affentry.hxx            
...
        suggestmgr.cxx
+endif

and then use that variable in the list of sources for the module:

+libenchant_myspell_la_SOURCES =        
+       $(hunspell_sources)     
        myspell_checker.cpp

Nice, let’s try it then 🙂

./configure --disable-myspell:

        Build Myspell/Hunspell backend: no

okay…

./configure --enable-myspell:

        Build Myspell/Hunspell backend: yes

flame@enterprise enchant-1.3.0 % nm -D --defined-only 
  src/myspell/.libs/libenchant_myspell.so | 
  c++filt | fgrep -c Hunspell::
38

okay here too…

./configure --enable-myspell=external:

checking for hunspell >= 1.1.9... yes
checking HUNSPELL_CFLAGS... -I/usr/include/hunspell
checking HUNSPELL_LIBS... -L/usr/lib -lhunspell-1.1
...
        Build Myspell/Hunspell backend: external

flame@enterprise enchant-1.3.0 % nm -D --defined-only 
  src/myspell/.libs/libenchant_myspell.so | 
  c++filt | fgrep -c Hunspell::
0
flame@enterprise enchant-1.3.0 % scanelf -n 
  src/myspell/.libs/libenchant_myspell.so | 
  fgrep -c libhunspell
1

Perfect! Now it’s time for the ebuild. First off, I copy over the enchant ebuild in my overlay, and the patch I’ve just made into files/

I add eutils and autotools eclasses to inherit, as I need epatch to apply the patch, and eautoreconf to re-run autotools. Then I add this to src_unpack before elibtoolize:

        epatch "${FILESDIR}/${P}-external-hunspell.patch"
        AT_M4DIR="ac-helpers" eautoreconf

Now it’s time to add an USE flag. As it is, the dependencies of enchant are a bit broken, as foser wanted to put a || dependency over aspell or ispell or hunspell. Up to now, hunspell was not requested at all, at the very best one had to install the myspell dictionaries.

As now hunspell is linked to, we do NOT want it to not be a non-deterministic dependency, otherwise is as good as automagic, so it has to become an USE flag. There’s no hunspell or myspell USE flag in the use.desc and use.local.desc files, which means that we are free to choose what we like. I think hunspell is the best option.

To avoid depending on spell and the other when we have hunspell enabled, we can just move the rest of the || dependency in !hunspell, this way:

        hunspell? ( >=app-text/hunspell-1.1.9 )
        !hunspell? ( || ( virtual/aspell-dict app-text/ispell ) )"

Now we have to enable/disable hunspell as requested. There’s no src_compile already, so I’m going to add one. --disable-dependency-tracking is just a nice option in general, there’s no reason to enable dependency tracking as ebuilds are one-time builds. In most cases it won’t make much difference anyway, I just always add it whenever available.

src_compile() {
        econf 
                $(use_enable hunspell myspell external) 
                --disable-dependency-tracking 
                || die "econf failed"
        emake || die "emake failed"
}

The three parameters use_enable call states that the hunspell USE flag is related to --enable/--disable-myspell and that when we enable it we have to pass external as value, making the two alternatives --enable-myspell=external and --disable-myspell, justlike we need.

Okay, now it’s emerge time, and let’s see the:

[Original]
flame@enterprise profiles % qsize enchant
app-text/enchant-1.3.0: 36 files, 19 non-files, 1536.433 KB
[USE=-hunspell]
flame@enterprise profiles % qsize enchant
app-text/enchant-1.3.0: 33 files, 19 non-files, 567.91 KB
[USE=hunspell]
flame@enterprise profiles % qsize enchant
app-text/enchant-1.3.0: 37 files, 19 non-files, 719.313 KB
flame@enterprise profiles % qlop -tgH enchant
enchant: Fri Jan 18 14:25:30 2008: 1 minute, 19 seconds [Original]
enchant: Mon Feb  4 10:00:27 2008: 1 minute, 2 seconds  [USE=-hunspell]
enchant: Mon Feb  4 10:02:28 2008: 58 seconds           [USE=hunspell]

As you can see, the size is reduced quite a bit, and the build time also decreased even with the hassle of running autotools. And now hunspell can be shared betwen processes using enchant and OpenOffice, for instance.

The complete patch, and the ebuild, are available as usual on my overlay. I’ll attach them to the bug and open an upstream bug in a few minutes.

Comments 5
  1. Thanks for the ebuild :-)I’ve tried to build enchant with hunspell-1.1.12 (not 1.1.9).Maybe restrict the dep to 1.1.9? (didn’t test 1.1.10)….enchant-1.3.0 prefix: /usr compiler: i686-pc-linux-gnu-gcc Build Aspell backend: yes Build Ispell backend: yes Build Uspell backend: no Build Hspell backend: no Build Myspell/Hunspell backend: external Build a relocatable library: no$ enchant-lsmod** (process:17949): WARNING **: Error loading plugin: /usr/lib/enchant/libenchant_myspell.so: undefined symbol: _ZN8Hunspell5spellEPKcaspell (Aspell Provider)ispell (Ispell Provider)

  2. Hm more like it has to be fixed, I’ll see if in the next days I can get that working. Besides, upstream SVN has a similar thing already, maybe it’s time for them to release 😉

  3. Yehhh, very good message and information Flameeyes !!!a very good raison to love gentoo …

  4. Just a notice for newcomers: keep in mind that every good change should be pushed back to upstream. Gentoo from day one tried to work with upstream in getting issues solved in a proper way…

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.