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.
-
Importer
guix import crate
will support importing fromCargo.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")))
-
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. -
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 alookup-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"))))
-
-
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:
-
The antioxidant build system builds Rust packages without Cargo, instead the
build process is fully managed by Guix by invokingrustc
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. -
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. -
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:
- outputs definitions with full versions.
- skips existing definitions.
- 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 🙂
Source: Planet GNU