This is the third entry in my series of blog posts about adding Parameterized Packages to GNU Guix for my Google Summer of Code project. Parameterization here refers to making it possible for packages to be built with compile-time options, and I have gone over the reasons and benefits for doing so in detail in the first post about Parameterized Packages.
In the last update I talked about implementing support for boolean, non-negative parameters. In this update, I've implemented negation, enumeration and a few other features that will make parameters considerably more powerful!
What's New
A brief summary of additions is
- Parametric Variants
- Parameter Dependencies
- 'Negation' for parameter types
- Enumerated Types
Parametric Variants
To make global parameters useful, it is necessary to be able to change the contents of a package in ways that Package Transformations might not be able to by themselves. Additionally, users might find themselves in situations where they wish to perform different operations for different values of an enumerated type.
"Parametric Variants" refers to matching against enumerated values and using methods of Defining Package Variants, such as package transforms, modify-inputs and procedures that return packages. Parameters now can use any of these methods instead of just using transformations.
For a better understanding of them, please look at any of the examples using the variant-match macro.
Parameter Dependencies
It's possible that enabling a parameter might require enabling another parameter or a package.
For such situations, I've added a new dependencies field to the parameter record that lets users specify parameters or packages a given parameter depends on. You can also fine-tune values for the parameter and the parameters in dependencies.
Negation and Enumeration
Enumeration
In the previous version, parameters could only be in two states: on and off.
This version makes it possible for parameters to take multiple states, as long as the user specifies all the possible states.
This has a huge number of uses- for example, here's a parameter type for locales:
(define locale-parameter-type (parameter-type (name 'locale-type) (accepted-values '(ca_ES cs_CZ da_DK de_DE el_GR en_AU en_CA en_GB en_US es_AR es_CL es_ES es_MX fi_FI fr_BE fr_CA fr_CH fr_FR ga_IE it_IT ja_JP ko_KR nb_NO nl_NL pl_PL pt_PT ro_RO ru_RU sv_SE tr_TR uk_UA vi_VN zh_CN)) (negation #f) (default 'en_US) (description "Type for Locales")))
- Explanation
parameter-typeis the record type for parameter types. Please note that parameter types are different from parameters. Parameter types are similar to variable types (character, boolean, symbol etc.) and parameters are similar to variables as they possess a type. The fields of this record arenamewhich must be a symbol Unlike parameters, parameter-types are identified by the symbol bound bydefine(herelocale-parameter-type) instead of theirnamefield. Thenamefield is purely for the sake of the Guix UI, while the bound name is what is used to utilize a record in a parameter. So to use this record in a parameter record, you would putlocale-parameter-typein itstypefield.accepted-valueswhich must be a list of symbols with at least two elements. This represents the entire set of values parameters belonging to this parameter type can take.negationwhich returns the 'negative' element. By default this is the first element ofaccepted-values, and if it is set to#flike here then negation is not supported for that parameter. It will throw an error if a user tries negating the parameter. Users can negate a parameter in a parameter list by using the#:offkeyword:(list ... (parameter-name #:off) ...)
Normally, you would place a value from
accepted-valuesin the right part of the cell, however,#:offgives the 'negative' value for all parameters that support negation.defaultis a value taken by the parameter if the keyword#:defaultis used on it. This works similar to negation, but you put#:defaultinstead of a#:negationin the right part of the cell. In the case ofdefault, its default value is the second element ofaccepted-valuesifnegationis not set to#f, and the first element otherwise. In our example, it is set to'en_US.descriptionwhich provides a description of the parameter type.
Negation
Negation refers to being able to specify the opposite value for a parameter.
If it is set to anything but #f for a given parameter-type record, any package record belonging to that parameter-type takes on that value when set to the special keyword #:off.
Here's an example that uses it to run the without-tests transform on a package whenever the boolean parameter tests is set to #:off.
(define-global-parameter (package-parameter (name 'tests) (variants (parameter-variant-match (#:off #:transform (without-tests #:package-name)))) (description "Toggle for tests") (predicate #t)))
- Explanation
define-global-parameteris a macro that takes a parameter record and makes it global. This means that it can be referred to in any parameter definition, and that itsnameis guaranteed to be unique among all global parameters.package-parameteris the record type for parameters. Please note that the record is not calledparameter, as it refers to something else entirely in Guile Scheme. The record accepts values for the fieldsname,type,variants,predicateanddescription.nameis a symbol, similar toparameter-type'snamefield. However unlikeparameter-type'sname,package-parameter'snameis very significant. We do not refer to parameters by any Scheme binding (through something likedefineorlet), as it could lead to unexpected errors in logic validation fields. Parameters are only referred to by theirnamefield, which is always unique for global parameters. If a global and local parameter share names, the local parameter is given preference. If two local parameters with the same name are added, an error will be signaled. The user can hence rest assured that in the context of any given package, eachnamehas a unique meaning.typeis theparameter-typeto use as the basis for the parameter. By default, it is set tobooleanwhich consists of the statesonandoff.variantsis an associative list that assigns transforms, procedures, valid build systems etc. to parameter values. This replaces thetransformsfield from the last post. Users are expected not to write the alist themselves, but to instead use theparameter-variant-matchmacro that generates an alist based on a specification as seen here. This macro is somewhat similar to thebuild-system/transform-matchmacro from the last post. Users can also useparameter-variantif they want to match a single value.#:offmatches the 'negative' value for any parameter, and_matches all non-negative values. It is possible to match multiple values by putting them in a list like(_ #:off). Note that here it would have been possible to use the symboloffinstead of the special keyword#:off, as our parameter belongs to thebooleantype and its negative value isoff. But it is a good idea to use#:offas it always matches against the negative symbol, regardless of theparameter-type's accepted values. Users can also specify the build system the value should match, as seen in thegcc-oflagparameter in the Bonus Examples section. This is not all there is to the magic ofparameter-variant-match; to make parameterization more useful, it lets users get the package name, the package and the value of the parameter the statement matched against. These are accessed through keywords, such as the#:package-namekeyword in this argument. Have a look at thegcc-oflagandstatic-libparameters in Bonus Examples to learn more!predicateis set to#fby default.#fmeans that the given parameter can only be used when its been mentioned in a package'sparameter-spec. Setting this to#tmeans that a global parameter can be applied to packages that do not have it in their spec. This is extremely dangerous and should only be used for extremely generic parameters. Otherwise, the user may set this to any lambda that takes apackagerecord as its argument and returns#tor#f. The global parameter will be applied if this lambda returns#t.dependenciesis a list of parameters and packages that a given parameter depends on. The list is punctuated by keywords to indicate parameter and package dependencies, with#:parameterand#:packagerespectively. If no keywords are given, the arguments are assumed to be parameters. Package dependencies have not been implemented yet.(dependencies `(#:parameter a b ... #:package git ,(package (name "some-package") ...) ...))
descriptionis a simple description of the parameter.
What does using parameters look like?
Here is an example use-case for parameterization, which packages Emacs' next, pgtk, xwidgets, wide-int and no-x variants in one package and also makes it possible to mix and match compatible variants.
Usage
The usage format for parameters is the same as that for other package transforms- you specify them through the CLI. In the future, it will also be possible to have a global set of transforms.
guix install emacs-parameterized \ --with-parameter=emacs-parameterized=pgtk=on \ --with-parameter=emacs-parameterized=tree-sitter=on \
Underlying Code
EDIT 9/18/2023: as the package record's source field is not thunked, I'm instead using a #:lambda inside the next package parameter to change it.
Under the hood, this is what the implementation looks like.
(package-with-parameters [parameter-spec (local (list (package-parameter (name 'next) (variants (parameter-variant-match (_ #:lambda (lambda (pkg) (package (inherit pkg) (version "29.0.92") (source (origin (inherit (package-source pkg)) (method git-fetch) (uri (git-reference (url "https://git.savannah.gnu.org/git/emacs.git/") (commit (string-append "emacs-" version)))) (file-name (git-file-name (package-name pkg) version)) (patches (parameter-if #:package pkg (pgtk) (search-patches "emacs-exec-path.patch" "emacs-fix-scheme-indent-function.patch" "emacs-native-comp-driver-options.patch" "emacs-pgtk-super-key-fix.patch") (search-patches "emacs-exec-path.patch" "emacs-fix-scheme-indent-function.patch" "emacs-native-comp-driver-options.patch"))) (sha256 (base32 "1h3p325859svcy43iv7wr27dp68049j9d44jq5akcynqdkxz4jjn")))))))))) (package-parameter (name 'tree-sitter) (dependencies '(next))) (package-parameter (name 'pgtk) (variants (parameter-variant-match (_ #:transform (with-configure-flag #:package-name "=--with-pgtk")))) (dependencies '(tree-sitter x11))) (package-parameter (name 'xwidgets) (variants (parameter-variant-match (_ #:transform (with-configure-flag #:package-name "=--with-xwidgets"))))) (package-parameter (name 'wide-int) (variants (parameter-variant-match (_ #:transform (with-configure-flag #:package-name "=--with-wide-int"))))))) (one-of '((_ (x11 #:off) pgtk) (_ (x11 #:off) xwidgets)))] (inherit emacs) (name "emacs-parameterized") (arguments (parameter-substitute-keyword-arguments (package-arguments emacs) [((x11 #:off)) '(((#:configure-flags flags #~'()) #~(delete "--with-cairo" #$flags)) ((#:modules _) (%emacs-modules build-system)) ((#:phases phases) #~(modify-phases #$phases (delete 'restore-emacs-pdmp) (delete 'strip-double-wrap))))] [(#:all (xwidgets on) (pgtk #:off)) '(((#:configure-flags flags #~'()) #~(cons "--with-xwidgets" #$flags)) ((#:modules _) (%emacs-modules build-system)) ((#:phases phases) #~(modify-phases #$phases (delete 'restore-emacs-pdmp) (delete 'strip-double-wrap))))])) (inputs (parameter-modify-inputs [(next) (prepend sqlite)] [(tree-sitter) (prepend tree-sitter)] [(xwidgets) (prepend gsettings-desktop-schemas webkitgtk-with-libsoup2)] [((x11 #:off)) (delete "libx11" "gtk+" "libxft" "libtiff" "giflib" "libjpeg" "imagemagick" "libpng" "librsvg" "libxpm" "libice" "libsm" "cairo" "pango" "harfbuzz" "libotf" "m17n-lib" "dbus")])))
Step-by-step Explanation
package-with-parameters: This macro takes aparameter-specas its first argument and applies the parameter specification to the package in its body. The default parameters are then activated within the package.parameter-spec: This record type contains all of the logic necessary to declare and resolve parameters for a package. This normally goes inside thepropertiesfield of thepackagerecord. In the previous post, it was necessary to put this record inside the properties, but nowpackage-with-parametershandles that for us. The parameter specification record contains various fields, all of which are optional. I have gone over the fields in detail in the previous blog post, hence I will not explain all of them in detail here. The only big change is thatone-ofnow has a functionality wherein if you start a list within it with_, you can have a case where none of the values in it are positive. Otherwise, it throws an error as one and only one value is expected to be positive. Also notice the usage of#:offto indicate negation. We have also not declaredx11, which will hence be treated as a global parameter. In general global parameters must either have theirpredicateset to something that returns#tor be present anywhere in theparameter-specto be applicable. Users are advised to put them in theoptionalfield, as it was created with this use case in mind.- local parameter
next: The local parameternexthas an interesting#:lambdastatement inside it. This statement takes the currentpackagerecord as an argument and returns a newpackagerecord, which is then used in its place. This is an extremely powerful method for changing otherwise unchangeable options, such as the ones here; because theversionandsourcefields are not thunked, this is the only way of modifying them. The#:lambda's functions can be passed 0, 1 or 2 arguments. In the case that it asks for 1 argument, the currentpackagerecord is passed. If it asks for 2, the currentpackagerecord along with the given parameter's value are passed. In all cases, the function is expected to return apackagerecord that will then be taken as the new currentpackagerecord. - package body:
Within the package body, we have the usual fields you would expect.
(inherit emacs)signifies that this package inherits all of emacs' base fields, and the rest of the fields are overrides of that. Please note that thenamefield cannot be influenced by parameters as it is notthunked. parameter-match: note: this has been substituted by parameter-substitute-keyword-arguments in the latest edit This is an extremely useful macro that matches all the parameter lists that has any positive parameters. It is also possible to require all the parameters in a list to be positive by using#:all. Please keep in mind that it does not short-circuit by default likecond. It will keep matching parameters until all the lists have been combed through. A short-circuiting version exists in the form ofparameter-match-case. I've gone over the functionality offered by this macro in detail in the previous blog post, however it has one small improvement: all conditionals now support checking if a parameter is set to a particular value instead of just checking if it is positive or not. This is very useful for enumerated types, where you might for example want to disable some features if and only if a parameter is set to the second positive value. To illustrate this, if you wanted to check whether a parameteryis set tov1or if the parameter listzis non-negative, the list would be((y v1) z). You can also use this to check for the default or negative value, with(parameter-name #:default)and(parameter-name #:off)respectively._is a similar special symbol which matches all non-negative values, but it is not necessary to use it since the parameter name by itself, sayparameter-nameis the same as(parameter-name _). We can see this in the((y v1) z)example above, wherezis matching all non-negative values ofzeven though we did not specify it as(z _). You can also use this in all of the fields inparameter-specthat require you to specify parameters. The parameter value list syntax is the same everywhere.parameter-substitute-keyword-argumentsandparameter-modify-inputs: As explained in the previous blog post, becausemodify-inputsis a macro, we cannot useparameter-matchwithin it. To counter this, I have writtenparameter-modify-inputsthat behaves a lot like ifmodify-inputshad aparameter-matchmacro within it. I have similarly writtenparameter-substitute-keyword-argumentsfor another commonly used macro calledsubstitute-keyword-arguments.
I have gone over the rest of the conditionals in the previous blog post too, they remain more or less the same with the exception that we use #:all inside lists instead of all like last time. This is to make it obvious at a glance that #:all is not a parameter like the rest of the list.
Bonus Examples
Here are some bonus examples for enumerated parameters:
GCC Optimization Flags
gcc has a set of optimization flags that can be used to make programs faster or smaller at the expense of stability.
This is a very basic attempt at adding that functionality to the gnu-build-system through the CFLAGS make-flag.
(package-parameter (name 'gcc-oflag) (type (parameter-type (name '_) (accepted-values '(-O0 -O1 -O2 -O3 -Os -Ofast -Og -Oz)) (negation #f))) (variants (parameter-variant-match (_ #:build-system gnu-build-system #:lambda (lambda (pkg parameter-value) (package (inherit pkg) (arguments (substitute-keyword-arguments (package-arguments package) ((#:make-flags flags #~'()) #~(append #$flags (list (string-append "CFLAGS=" parameter-value))))))))))))
Static Libraries
In High-Performance Computing, it's often necessary to produce static builds of packages to share them with others. This parameter is a basic attempt at making it possible to do so with any given library.
(package-parameter (name 'static-lib) (variants (parameter-variant-match (_ #:transform (with-configure-flag #:package-name "=--disable-shared") (with-configure-flag #:package-name "=--enable-static")))))
Sneak Peak: A RESTful API for Parameterization
I recently made a post on Mastodon that claimed that the real advantage of Guix is that it's extensible with Guile Scheme. To back up this claim, once parameters have been merged to trunk I'll be writing a set of tutorials on hacking Guix with Guile Scheme.
One of these planned tutorials is going to be about writing a RESTful API using Guile that'll allow users to request a package with specific parameters.
Here is what the POST request for this API may look like:
POST /test HTTP/1.1
Host: guix.example
Accept: application/json
Content-Type: application/json
Content-Length: 194
{
"User" : "guix-hacker",
"Package" : "emacs",
"Parameters" : [
{ "Parameter" : "next",
"Value" : "on"},
{ "Parameter" : "tree-sitter",
"Value" : "off"}
]
}
Future work
Here I have demonstrated a basic DSL that is more-or-less just S-expressions. There is however scope for making it a lot more convenient to use parameters, and thus there are plans on building a convenience syntax on top of this simple one.
One example is using ~parameter-name to indicate the negation of a parameter. However, syntax like this may not be obvious to everyone at a glance, which is why we have decided to make a convenience DSL with these features only after heavy deliberation and discussion.
The next few updates will focus on the UI for Parameterization. The primary goals for the UI are to make it easy to discover parameterization options, tell what type a parameter is and to figure out parameter combinations that work for a given package.
Closing Thoughts
As can be seen with the Parameterized Emacs example in this post, parameterization will make it possible to join a large number of variations of packages and reduce the amount of code requiring maintenance. One of the aims of this project is to also create procedures that test parameter combinations and measure the combinatorial complexity brought about by parameterization, which should make testing parameteric variants easy too.
I expect parameterization to be particularly useful for running Guix on exotic hardware (such as static minimalistic targets) or on High-Performance Computing Systems (specific architecture optimizations) and make it generally easy to tailor a lot of packages for a particular system's requirements.
This update marks the completion of this Google Summer of Code project's midterms. I'd like to thank my mentors Pjotr Prins and Gábor Boskovit as well as Ludovic Courtès, Arun Isaac and Efraim Flashner for their guidance and help, without which I don't think I'd have been able to reach this milestone. I'm also very grateful to the many wonderful people in the Guix community that provided me with a lot of useful advice and suggestions.
Stay tuned for updates, and happy hacking!