Supporting Multiple LilyPond Versions

LilyPond’s input language occasionally evolves to accomodate new features or to simplify how things can be expressed. Sometimes these syntax changes can break compatibility of existing documents with newer versions of LilyPond, but upgrading is generally a painless process thanks to the convert-ly script that in most cases can automatically update input files. However, sometimes it is necessary to write LilyPond files that are compatible with multiple LilyPond versions, for example when creating libraries like openLilyLib. The key to this is writing conditional code that depends on the currently executed LilyPond version, and in this post I will describe how this has just become easier with LilyPond.

Breaking Changes

Occasionally improvements in LilyPond development require changes in the input syntax that don’t allow an input file to be compiled with LilyPond versions both before and after a certain version. If you are working with the development versions this can occur more often, as trying and changing new syntax is usually the most “unstable” aspect of the so-called unstable version. I will give just one recent example.

For ages you had to provide two obscure arguments parser and location when defining music functions

oldStyleRepeat =
#(define-music-function (parser location my-music)(ly:music?)
  #{
    #my-music #my-music
  #})

This function simply returns the given music expression twice, and the parser and location arguments don’t do anything here. In fact – and this had always confused me – you could switch the two arguments or even use totally different names. This is because LilyPond implicitly passes two arguments to the function, an object representing the current input parser and one representing the input location from which the function is called. These are then bound to the names given in the function definition, which by convention are the two obvious names for these arguments. But as long as the arguments are not actually used in the function body they and their names are basically irrelevant. Two use cases where they are used are shown in the following two functions:

oldStyleMessage =
#(define-void-function (parser location msg)(string?)
  (ly:input-message location msg))

oldStyleInclude =
#(define-void-function (parser location filename)(string?)
   (ly:parser-include-string parser (format "\\include \"~a\"" filename)))

\oldStyleMessage prints the given text and provides a link to the input location, the place in the file where the function is called from, \oldStyleInclude tries to include a file with the given name. Each function makes use of one of the two arguments.

With LilyPond 2.19.22 David Kastrup introduced *location* and *parser* that allow you to obtain these two objects directly from within any Scheme function. (Technically speaking these are Guile “fluids“.) As a result the corresponding arguments in the function signature are not mandatory anymore and don’t have to clutter each and every function definition. The first of the two functions can now be rewritten as

% LilyPond 2.19.22 and later
newStyleMessage =
#(define-void-function (msg)(string?)
  (ly:input-message (*location*) msg))

while for the second one it’s the function ly:parser-include-string that has been changed to not expect a parser argument anymore, leading to the simplified definition

% LilyPond 2.19.22 and later
newStyleInclude =
#(define-void-function (filename)(string?)
  (ly:parser-include-string (format "\\include \"~a\"" filename)))

This is much clearer to read or write as only the arguments that are actually needed for the purpose of the function are present. It is noteworthy that for now the old syntax will still work with newer LilyPond versions. The parser and location arguments are still passed implicitly to each music-, scheme- or void-function, so \oldStyleMessage will still work (because location still works). However, \oldStyleInclude will fail because ly:parser-include-string doesn’t expect the parser argument anymore. This is a typical case where convert-ly will properly handle the breaking syntax change, leaving you with a file that can only be compiled with current LilyPond, or more concretely: with LilyPond >= 2.19.22. So for supporting both stable and development versions an alternative approach is required.

Conditional Execution Based On LilyPond Version

If LilyPond is executed it can (of course) tell which version it is, and this information is not only available through the command line switch

~$ lilypond --version
GNU LilyPond 2.19.57

Copyright (c) 1996--2017 by
  Han-Wen Nienhuys 
  Jan Nieuwenhuizen 
  and others.

This program is free software.  It is covered by the GNU General Public
License and you are welcome to change it and/or distribute copies of it
under certain conditions.  Invoke as `lilypond --warranty' for more
information.

but also from within LilyPond through the ly:version function which returns a list with three numbers representing the currently executed version:

#(display (ly:version))
=> (2 19 57)

From here it is not too difficult to create functionality that tests a given reference version against the currently executed LilyPond version. I will not go into any detail about it (because this is not a Scheme tutorial), but if you want you can inspect for yourself how this is implemented in openLilyLib. It includes lilypond-version-predicates which are used for example in the code that loads module files (see this piece of code in the module-handling.ily file):

#(if (lilypond-greater-than? "2.19.21")
     (ly:parser-include-string arg)
     (ly:parser-include-string parser arg)))))

This way LilyPond uses the correct syntax to include an external file, depending on the version of LilyPond that is currently running.

Version Comparison Now Built Into LilyPond

I have come to love this possibility because it makes it possible to write library functions that support multiple LilyPond versions, a differentiation which is usually between the current stable and the current development version, but sometimes one also has to consider whether to support previous stable versions. So it was natural that I wanted to integrate the functionality into LilyPond itself – to make it independent from openLilyLib – and since LilyPond 2.19.57 the new function ly:version? is available. (Note that this functionality is only available to test for LilyPond versions starting with 2.19.57, so if you need to support older versions from the 2.18/2.19 line you will have to look into the openLilyLib implementation.)

ly:version? op ver

is the signature of this function, where op is an arithmetic operator (=, <, >, <= or >=) and ver a list of up to three numbers representing the LilyPond version you want to compare to. So assuming you run LilyPond 2.21.13 (which is a future thing while writing this post) the expression (ly:version? > '(2 21 12) would return #t, as would (ly:version? <= '(2 22 0).

One interesting thing about the new function is its behaviour when it comes to incomplete reference version lists. As you will know all the following version statements are valid in LilyPond:

\version "2.21.12"
\version "2.21"
\version "2"

each giving less specific information. ly:version? handles this correctly in all cases, so the following cases are properly evaluated:

2.21.12 = 2.21
2.21.12 < 2.22
2.21 > 2.20.2
2 < 3
2.99 < 3
2.19.15 >= 2.19

So the above example could now be rewritten without depending on openLilyLib (but of course this doesn’t work for this example because – as I said – the new function can’t compare LilyPond versions earlier than 2.19.57):

#(if (ly:version? > '(2 19 21))
     (ly:parser-include-string arg)
     (ly:parser-include-string parser arg)))))

One thought on “Supporting Multiple LilyPond Versions

  1. David Kastrup

    Well, you have one major blunder written up here:

    oldStyleRepeat =
    #(define-music-function (parser location my-music)(ly:music?)
    #{
    #my-music #my-music
    #})

    This function simply returns the given music expression twice, and the parser and location arguments don’t do anything here. In fact – and this had always confused me – you could switch the two arguments or even use totally different names.

    I dare you to try. One _major_ use case of the arguments literally named ‘parser’ and ‘location’ was that they were used for passing the requisite arguments to the #{…#} construct. So your particular example (which actually also suffers from the problem of using #my-music instead of $my-music twice without creating a copy, a big no-no as you’ll find out when transposing the result) will not tolerate swapping parser and location arguments.

    #{…#} now takes its required information from the %parser and %location fluids: that made several things work in a nicer manner.

    By the way: replies on this blog really want a “preview” button since it’s otherwise nigh impossible to get the markup (or markdown?) right. I didn’t even try.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *