| by Arround The Web | No comments

GNU Guix: A New Rust Packaging Model

If you've ever struggled with Rust packaging, here's some good news!

We have changed to a simplified Rust packaging model that is easier to automate
and allows for modification, replacement and deletion of dependencies at the
same time. The new model will significantly reduce our Rust packaging time and
will help us to improve both package availability and quality.

Those changes are currently on the rust-team branch, slated to be merged in
the coming weeks.

How good is the news? Migration of our current Rust package collection, 150+
applications with 3600+ dependency libraries, only took two weeks, all by one
person! 🙂

See #387, if you want to track the
current progress and give feedback. I'll request merging the rust-team branch
when the pull request is merged. After merging the branch, a news entry will be
issued for guix pull.

Upcoming changes

The previous packaging model for Rust in Guix would map one crate (Rust package)
to one Guix package. This seemed to make sense but there's a fundamental
mismatch here: while Guix packages—from applications like GIMP and Inkscape to C
libraries like GnuTLS and Nettle—are meant to be compiled independently, Rust
applications are meant to be compiled as a single unit together with all the
crates they depend on, recursively. That mismatch meant that Guix would build
each crate independently, but that build output was of no use at all.

The new model instead focuses on defining
origins
for crates, with actual builds happening only on the "leaves" of the graph—Rust
applications. This is a major change with many implications, as we will see
below.

  1. Importer

    guix import crate
    will support importing from Cargo.lock using the new --lockfile / -f
    option.

    guix import --insert=gnu/packages/rust-crates.scm \
         crate --lockfile=/path/to/Cargo.lock PACKAGE
    
    guix import -i gnu/packages/rust-crates.scm \
         crate -f /path/to/Cargo.lock PACKAGE

    To avoid conflicts with the new lockfile importer, the
    crates.io importer will be altered so it will no
    longer support importing dependencies.

    A new procedure, cargo-inputs-from-lock-file, will be added for use in the
    guix.scm
    of Rust projects. Note that Cargo workspaces in dependencies require manual
    intervention and are therefore not handled by this procedure.

    (use-modules (guix import crate))
    
    (package
      ...
      (inputs (cargo-inputs-from-lock-file "Cargo.lock")))
  2. Build system

    cargo-build-sytem
    will support directory inputs and Cargo workspaces.

    Build phase check-for-pregenerated-files will scan all unpacked sources
    and print out non-empty binary files.

    We won't accept contributions using the old packaging approach
    (#:cargo-inputs and #:cargo-development-inputs) anymore. Its support is
    deprecated and will be removed after Dec. 31, 2026.

  3. Packages

    Rust libraries will be stored in two new modules and will be hidden from the
    user interface:

    • (gnu packages rust-sources)

      Rust libraries that require a build process or complex modification
      involving external dependencies to unbundle dependencies.

    • (gnu packages rust-crates)

      Rust libraries imported using the lockfile importer. This module
      exports a lookup-cargo-inputs interface, providing an identifier ->
      libraries mapping.

      Libraries defined in this module can be modified via snippets and
      patches
      ,
      replaced by changing their definitions to point to other variables, or
      removed by changing their definitions to #f. The importer will skip
      existing libraries to avoid overwriting modifications.

      A template file for this module will be provided as
      etc/teams/rust/rust-crates.tmpl in Guix source tree, for use in external
      channels.

    All other libraries (those currently in (gnu packages crates-...)) will
    be moved to an external channel
    . If you have packages depending on them,
    please add this
    channel
    and use
    its (past-crates packages crates-io) module to avoid possible breakage. Once
    merged, you can migrate your packages and safely remove the channel.

    (channel
      (name 'guix-rust-past-crates)
      (url "https://codeberg.org/guix/guix-rust-past-crates.git")
      (branch "trunk")
      (introduction
       (make-channel-introduction
        "1db24ca92c28255b28076792b93d533eabb3dc6a"
        (openpgp-fingerprint
         "F4C2D1DF3FDEEA63D1D30776ACC66D09CA528292"))))
  4. Documentation

    API references for
    cargo-build-sytem
    and packaging guidelines for Rust
    crates

    will be updated. A packaging workflow built upon the new features will be
    added under the Packaging
    chapter
    of Guix
    Cookbook
    .

Background

Currently, our Rust packaging uses the traditional approach, treating each
application and library equally.

This brings issues. Firstly on packaging and maintenance, due to the large
number of libraries with limited people working on it, plus we can't reuse those
packaged libraries so instead of the built libraries, their sources are
extracted and used in the build process. As a result, the packaging experience
is not very smooth, although the crates.io importer has helped mitigate this to
some extent.

Secondly on the user interface, thousands of Rust libraries that can't be used
by the user appear in the search result. Documentation can't be taken good care
of for all these packages as well, understandably.

Lastly, the inconsistency in the packaging interface. Our dependency model
cannot perfectly map to Rust's, and circular dependencies are possible. To
solve this, build system arguments #:cargo-inputs and
#:cargo-development-inputs were introduced and used for specifying Rust
libraries, instead of the regular propagated-inputs and native-inputs.
Additionally, inputs propagation logic had to be reimplemented for them, which
resulted in additional performance overhead.

Approaches have been proposed to improve the situation, notably the
antioxidant build
system developed by Maxime Devos, and the
cargo2guix tool developed by Murilo and
Luis Guilherme Coelho:

  1. Antioxidant

    The antioxidant build system builds Rust packages without Cargo, instead the
    build process is fully managed by Guix by invoking rustc directly.

    This build system would allow Guix to produce and share build artifacts for
    Rust libraries. It's a step towards making our work on the current approach
    more reasonable.

    However there's a downside. Since this is not what the Rust community
    expects, we'd also have to heavily patch many Rust packages, which would
    make it even harder for us to move forward.

  2. cargo2guix

    This tool parses Cargo.lock and outputs package definitions. It's more
    reliable than the crates.io importer, since dependencies are already known
    offline. It should be the most efficient improvement for the current
    approach. The upcoming importer update integrates a modified version of
    this tool.

    Murilo also proposes to package Rust applications in self-contained modules,
    each module containing a Rust application with all its dependencies, in
    order to reduce merge conflicts. However, one same library will be defined
    in multiple modules, duplicating the effort to check and manage them.

  3. Let Cargo download dependencies

    This is the "vendoring" approach, used in some distributions and can be
    implemented as a fixed-output derivation.

    We don't use this approach since the dependency information is completely
    hidden from us. We can't locate a library easily when we want to modify or
    replace it. If we made a mistake on checking dependencies, it could be very
    difficult to find out later.

    Another downside is that downloading of a single library can't be
    deduplicated. Since we use an isolated build environment, commonly used
    libraries will be downloaded repeatedly, despite already available in the
    store.

After reading the recent
discussion
,
I thought about these existing approaches in the hope of finding one that does
only the minimum necessary: since users can't use our packaged libraries,
there's no reason to insist on the traditional approach -> libraries can be
hidden from the user interface -> user-facing documentations are not needed ->
since metadata is not used at this stage, why bother defining a
package

for the library?

Actually cargo2guix is more suitable for importing sources rather than packages,
as it has issues handling licenses, and Cargo.lock only contains enough
information to construct the source
representation

in Guix, which has support for simple patching.

Since the vendoring approach exists, packaging all Rust libraries as sources
only has been proven effective. However, we'll lose important information in
our representation when switching from packages to sources: license and
dependency. Thanks to the awesome
cargo-license tool, only the latter
required further consideration.

The implementation has been changed a few times in the review process, but the
idea remains: make automation and manual intervention coexist. As a result, the
importer:

  1. outputs definitions with full versions.
  2. skips existing definitions.
  3. maintains an identifier -> libraries mapping, along with an accessing
    interface that handles modifications made to the libraries.

Despite proposing it, I was a bit worried about the mapping, which references
all dependency libraries directly, but the result went quite well: with compact
source definitions, we reduced 153k lines of definitions for Rust libraries to
42k after this migration.

  • Imported libraries, these are what the importer creates:

    (define rust-unindent-0.2.4
      (crate-source "unindent" "0.2.4"
                    "1wvfh815i6wm6whpdz1viig7ib14cwfymyr1kn3sxk2kyl3y2r3j"))
    
    (define rust-ureq-2.10.0.1cad58f
      (origin
        (method git-fetch)
        (uri (git-reference (url "https://github.com/algesten/ureq")
                            (commit "1cad58f5a4f359e318858810de51666d63de70e8")))
        (file-name (git-file-name "rust-ureq" "2.10.0.1cad58f"))
        (sha256 (base32 "1ryn499kbv44h3lzibk9568ln13yi10frbpjjnrn7dz0lkrdin2w"))))
  • Library with modification:

    (define rust-libmimalloc-sys-0.1.24
      (crate-source "libmimalloc-sys" "0.1.24"
                    "0s8ab4nc33qgk9jybpv0zxcb75jgwwjb7fsab1rkyjgdyr0gq1bp"
                    #:snippet
                    '(begin
                       (delete-file-recursively "c_src")
                       (delete-file "build.rs")
                       (with-output-to-file "build.rs"
                         (lambda _
                           (format #t "fn main() {~@
                            println!(\"cargo:rustc-link-lib=mimalloc\");~@
                            }~%"))))))
  • Library with replacement, for those requiring a build process with
    dependencies.

    (define rust-pipewire-0.8.0.fd3d8f7 rust-pipewire-for-niri)
  • Deleted library:

    (define rust-unrar-0.5.8 #f)
  • Accessing interface and identifier -> libraries mapping:

    (define-cargo-inputs lookup-cargo-inputs
      (rust-deunicode-1
       => (list rust-any-ascii-0.3.2
                rust-emojis-0.6.4
                rust-itoa-1.0.15
                ...))
      (rust-pcre2-utf32-0.2
       => (list rust-bitflags-2.9.0
                rust-cc-1.2.18
                rust-cfg-if-1.0.0
                ...))
      (zoxide
       => (list rust-aho-corasick-1.1.3
                rust-aliasable-0.1.3
                rust-anstream-0.6.18
                ...)))
  • Dependency libraries lookup, module selection is supported:

    (cargo-inputs 'rust-pcre2-utf32-0.2)
    (define (my-cargo-inputs name)
      (cargo-inputs name #:module '(my packages rust-crates)))
    
    (my-cargo-inputs ...)

Since we have all the dependency information, unpacking any libraries we want to
a directory and then running more common tools to check them is possible (some
scripts are provided under etc/teams/rust, yet to be rewritten in Guile).
You're encouraged to share yours and check the libraries after the merge, and
help improve the collection 😉

Next steps

One issue for this model is that all libraries are stored and referenced in one
module, making merge conflicts harder to resolve.

I'm considering creating a separate repository to manage this module. Whenever
there's a change, it will be applied into this repository first and then synced
back to Guix.

We can also store dependency specifications and lockfiles in that separate
repository to make the packaging process, which may require changing the
specifications, more transparent. This may also allow automation in updating
dependency libraries.

Thanks for reading! Happy hacking 🙂

Share Button

Source: Planet GNU