|
|
Log in / Subscribe / Register

LWN.net Weekly Edition for December 4, 2025

Welcome to the LWN.net Weekly Edition for December 4, 2025

This edition contains the following feature content:

This week's edition also includes these inner pages:

  • Brief items: Brief news items from throughout the community.
  • Announcements: Newsletters, conferences, security updates, patches, and more.

Please enjoy this week's edition, and, as always, thank you for supporting LWN.net.

Comments (none posted)

APT Rust requirement raises questions

By Joe Brockmeier
November 24, 2025

It is rarely newsworthy when a project or package picks up a new dependency. However, changes in a core tool like Debian's Advanced Package Tool (APT) can have far-reaching effects. For example, Julian Andres Klode's declaration that APT would require Rust in May 2026 means that a few of Debian's unofficial ports must either acquire a working Rust toolchain or depend on an old version of APT. This has raised several questions within the project, particularly about the ability of a single maintainer to make changes that have widespread impact.

On October 31, Klode sent an announcement to the debian-devel mailing list that he intended to introduce Rust dependencies and code into APT as soon as May 2026:

This extends at first to the Rust compiler and standard library, and the Sequoia ecosystem.

In particular, our code to parse .deb, .ar, .tar, and the HTTP signature verification code would strongly benefit from memory safe languages and a stronger approach to unit testing.

If you maintain a port without a working Rust toolchain, please ensure it has one within the next 6 months, or sunset the port.

Klode added this was necessary so that the project as a whole could move forward, rely on modern technologies, "and not be held back by trying to shoehorn modern software on retro computing devices". Some Debian developers have welcomed the news. Paul Tagliamonte acknowledged that it would impact unofficial Debian ports but called the push toward Rust "welcome news".

However, John Paul Adrian Glaubitz complained that Klode's wording was unpleasant and that the approach was confrontational. In another message, he explained that he was not against adoption of Rust; he had worked on enabling Rust on many of the Debian architectures and helped to fix architecture-specific bugs in the Rust toolchain as well as LLVM upstream. However, the message strongly suggested there was no room for a change in plan: Klode had ended his message with "thank you for understanding", which invited no further discussion. Glaubitz was one of a few Debian developers who expressed discomfort with Klode's communication style in the message.

Klode noted, briefly, that Rust was already a hard requirement for all Debian release architectures and ports, except for Alpha (alpha), Motorola 680x0 (m68k), PA-RISC (hppa), and SuperH (sh4), because of APT's use of the Sequoia-PGP project's sqv tool to verify OpenPGP signatures. APT falls back to using the GNU Privacy Guard signature-verification tool, gpgv, on ports that do not have a Rust compiler. By depending directly on Rust, though, APT itself would not be available on ports without a Rust compiler. LWN recently covered the state of Linux architecture support, and the status of Rust support for each one.

None of the ports listed by Klode are among those officially supported by Debian today, or targeted for support in Debian 14 ("forky"). The sh4 port has never been officially supported, and none of the other ports have been supported since Debian 6.0. The actual impact on the ports lacking Rust is also less dramatic than it sounded at first. Glaubitz assured Antoni Boucher that "the ultimatum that Julian set doesn't really exist", but phrasing it that way "gets more attention in the news". Boucher is the maintainer of rust_codegen_gcc, a GCC ahead-of-time code generator for Rust. Nothing, Glaubitz said, stops ports from using a non-Rust version of APT until Boucher and others manage to bootstrap Rust for those ports.

Security theater?

David Kalnischkies, who is also a major contributor to APT, suggested that if the goal is to reduce bugs, it would be better to remove the code that is used to parse the .deb, .ar, and .tar formats that Klode mentioned from APT entirely. It is only needed for two tools, apt-ftparchive and apt-extracttemplates, he said, and the only "serious usage" of apt-ftparchive was by Klode's employer, Canonical, for its Launchpad software-collaboration platform. If those were taken out of the main APT code base, then it would not matter whether they were written in Rust, Python, or another language, since the tools are not directly necessary for any given port.

Kalnischkies also questioned the claim that Rust was necessary to achieve the stronger approach to unit testing that Klode mentioned:

You can certainly do unit tests in C++, we do. The main problem is that someone has to write those tests. Like docs.

Your new solver e.g. has none (apart from our preexisting integration tests). You don't seriously claim that is because of C++ ? If you don't like GoogleTest, which is what we currently have, I could suggest doctest (as I did in previous installments). Plenty other frameworks exist with similar or different styles.

Klode has not responded to those comments yet, which is a bit unfortunate given the fact that introducing hard dependencies on Rust has an impact beyond his own work on APT. It may well be that he has good answers to the questions, but it can also give the impression that Klode is simply embracing a trend toward Rust. He is involved in the Ubuntu work to migrate from GNU Coreutils to the Rust-based uutils. The reasons given for that work, again, are around modernization and better security—but security is not automatically guaranteed simply by switching to Rust, and there are a number of other considerations.

For example, Adrian Bunk pointed out that there are a number of Debian teams, as well as tooling, that will be impacted by writing some of APT in Rust. The release notes for Debian 13 ("trixie") mention that Debian's infrastructure "currently has problems with rebuilding packages of types that systematically use static linking", such as those with code written in Go and Rust. Thus, "these packages will be covered by limited security support until the infrastructure is improved to deal with them maintainably". Limited security support means that updates to Rust libraries are likely to only be released when Debian publishes a point release, which happens about every two months. The security team has specifically stated that sqv is fully supported, but there are still outstanding problems.

Due to the static-linking issue, any time one of sqv's dependencies, currently more than 40 Rust crates, have to be rebuilt due to a security issue, sqv (at least potentially) also needs to be rebuilt. There are also difficulties in tracking CVEs for all of its dependencies, and understanding when a security vulnerability in a Rust crate may require updating a Rust program that depends on it.

Fabian Grünbichler, a maintainer of Debian's Rust toolchain, listed several outstanding problems Debian has with dealing with Rust packages. One of the largest is the need for a consistent Debian policy for declaring statically linked libraries. In 2022, Guillem Jover added a control field for Debian packages called Static-Built-Using (SBU), which would list the source packages used to build a binary package. This would indicate when a binary package needs to be rebuilt due to an update in another source package. For example, sqv depends on more than 40 Rust crates that are packaged for Debian. Without declaring the SBUs, it may not be clear if sqv needs to be updated when one of its dependencies is updated. Debian has been working on a policy requirement for SBU since April 2024, but it is not yet finished or adopted.

The discussion sparked by Grünbichler makes clear that most of Debian's Rust-related problems are in the process of being solved. However, there's no evidence that Klode explored the problems before declaring that APT would depend on Rust, or even asked "is this a reasonable time frame to introduce this dependency?"

Where tradition meets tomorrow

Debian's tagline, or at least one of its taglines, is "the universal operating system", meaning that the project aims to run on a wide variety of hardware (old and new) and be usable on the desktop, server, IoT devices, and more. The "Why Debian" page lists a number of reasons users and developers should choose the distribution: multiple hardware architectures, long-term support, and its democratic governance structure are just a few of the arguments it puts forward in favor of Debian. It also notes that "Debian cannot be controlled by a single company". A single developer employed by a company to work on Debian tools pushing a change that seems beneficial to that company, without discussion or debate, that impacts multiple hardware architectures and that requires other volunteers to do unplanned work or meet an artificial deadline seems to go against many of the project's stated values.

Debian, of course, does have checks and balances that could be employed if other Debian developers feel it necessary. Someone could, for example, appeal to Debian's Technical Committee, or sponsor a general resolution to override a developer if they cannot be persuaded by discussion alone. That happened recently when the committee required systemd maintainers to provide the /var/lock directory "until a satisfactory migration of impacted software has occurred and Policy updated accordingly".

However, it also seems fair to point out that Debian can move slowly, even glacially, at times. APT added support for the DEB822 format for its source information lists in 2015. Despite APT supporting that format for years, Klode faced resistance in 2021, when he pushed for Debian to move to the new format ahead of the Debian 12 ("bookworm") release in 2021, but was unsuccessful. It is now the default for trixie with the move to APT 3.0, though APT will continue to support the old format for years to come.

The fact is, regardless of what Klode does with APT, more and more free software is being written (or rewritten) in Rust. Making it easier to support that software when it is packaged for Debian is to everyone's benefit. Perhaps the project needs some developers who will be aggressive about pushing the project to move more quickly in improving its support for Rust. However, what is really needed is more developers lending a hand to do the work that is needed to support Rust in Debian and elsewhere, such as gccrs. It does not seem in keeping with Debian's community focus for a single developer to simply declare dependencies that other volunteers will have to scramble to support.

Comments (343 posted)

Unpacking for Python comprehensions

By Jake Edge
November 21, 2025

Unpacking Python iterables of various sorts, such as dictionaries or lists, is useful in a number of contexts, including for function arguments, but there has long been a call for extending that capability to comprehensions. PEP 798 ("Unpacking in Comprehensions") was first proposed in June 2025 to fill that gap. In early November, the steering council accepted the PEP, which means that the feature will be coming to Python 3.15 in October 2026. It may be something of a niche feature, but it is an inconsistency that has been apparent for a while—to the point that some Python programmers assume that it is already present in the language.

Unpacking

One of the most common use cases for unpacking is to pass a list or dictionary to a function as a series of its elements rather than as a single object. The "*" unpacking operator (and its companion, "**", for dictionaries) can be used to expand an iterable in that fashion; a simple example might look like the following:

    def foo(a, b):
        a*b

    l = [ 2, 4 ]  # list
    t = ( 3, 5 )  # tuple
    d = { 'a' : 2, 'b' : 9 } # dict
    foo(*l)   # foo(2, 4) == 8
    foo(*t)   # foo(3, 5) == 15
    foo(**d)  # foo(a=2, b=9) == 18
In each case, the unpacking operator extracts the elements of the iterable to turn them into individual arguments to the function—keyword arguments for the dictionary.

Python comprehensions provide a mechanism to build a list or other iterable using a compact syntax, rather than a full loop. A classic example of that appears in the Python documentation linked just above:

    squares = []
    for x in range(10):
        squares.append(x**2)

    # can be replaced with

    squares = [ x**2 for x in range(10) ]
For both of those, the result is a list with the squares of the numbers zero through nine.

The unpacking operator can already be used to create iterables, such as:

    a = [ 1, 2 ]
    b = [ 3, 4 ]
    c = [ *a, *b ]  # [ 1, 2, 3, 4 ]

    # dictionaries can be merged in similar fashion

    newdict = { **d1, **d2, **d3 }
In the latter example, the order of the dictionaries matters, keys that are duplicated will take their value from the last dictionary where they were set (i.e. d3[key] takes precedence over the value for key in d1 or d2).

But what if there is a list of lists with a length that is not known except at run time? Currently, there is no easy way to use a list comprehension to build the flattened list of all of the entries of each list; it can be done using a comprehension with two loops, but that is error prone. There are other possibilities too, as described in the "Motivation" section of the PEP, but all of them suffer from semi-obscurity or complexity. Instead, Python will be allowing unpacking operators in comprehensions:

    # from the PEP
    
    [*it for it in its]  # list with the concatenation of iterables in 'its'
    {*it for it in its}  # set with the union of iterables in 'its'
    {**d for d in dicts} # dict with the combination of dicts in 'dicts'
    (*it for it in its)  # generator of the concatenation of iterables in 'its'

    # a usage example

    a = [ 1, 2 ]
    b = [ 3, 4, 5 ]
    c = [ 6 ]
    its = [ a, b, c ]
    [ *it for it in its ]   #  [ 1, 2, 3, 4, 5, 6 ]

    # current double-loop version

    [ x for it in its for x in it ]  #  [ 1, 2, 3, 4, 5, 6 ]

Discussion

The PEP authors, Adam Hartz and Erik Demaine, actually proposed the idea back in 2021 on the python-ideas mailing list. As noted in that message, though, the idea also came up in 2016 and perhaps even before that. In 2021, the proposal was generally well-received, and reached the pre-PEP stage, but was unable to attract a core developer as a sponsor. In late June, Hartz posted a lengthy pre-PEP message to the ideas category of the Python discussion forum, "hoping to find a sponsor for moving forward with the PEP process if there's still enthusiasm behind this idea".

Hartz noted that the idea had been raised in October 2023, as well, so it is a feature that is frequently brought up—generally to nodding approval. That 2023 message was posted by Alex Prengère, who was quick to reply to the pre-PEP saying that he had been working on unpacking in comprehensions as well. He, along with others, wondered about support for unpacking in asynchronous comprehensions (as described in PEP 530); it was not mentioned in the pre-PEP, but the implementation would allow them, he said. Hartz said that the intent was to support them and that he would update the text to reflect that.

There was also some discussion regarding the syntax for making function calls using a generator comprehension (e.g. f(x for x in it)); there is some ambiguity in the meaning, as Ben Hsing noted. PEP 448 ("Additional Unpacking Generalizations") added the ability to use the unpacking operators in more contexts, including function call arguments, but it explicitly did not extend that to generator comprehensions because it was not clear which meaning should be chosen. As Hsing put it:

That is, which one of these is intended?
    f(*x for x in it) == f((*x for x in it))
or:
    f(*x for x in it) == f(*(x for x in it))

In the thread, Hartz and others argued that, since the language already allows a generator comprehension (without any unpacking operators) as an argument without requiring an extra set of parentheses (e.g. f(x for x in it), the same should be true for those with the unpacking operator. In the reply linked above, Hartz noted that the error message for the syntax error points in that direction as well:

A little bit of support in this direction, perhaps, comes from the way that the syntax error for f(*x for x in it) is reported in 3.13, which suggests that this is interpreted as f(<a single malformed generator expression>) rather than as f(*<something>):
    >>> f(*x for x in its)
      File "<python-input-0>", line 1
        f(*x for x in its)
          ^^
    SyntaxError: iterable unpacking cannot be used in comprehension

The draft PEP continued to be discussed; it was updated a few days after it was first posted, on June 25, and then again on July 3. The latter posting caught the eye of core developer Jelle Zijlstra who said that it "is a nice little feature that I've missed several times in the past" and that he was willing to sponsor it. That started its path into the CPython mainline.

The now-numbered PEP 798 was posted for discussion in the PEPs category of the forum on July 19. Along the way, the PEP had picked up some extra pieces, including a section with examples of where standard library code could be simplified using the feature and an appendix on support in other languages. Most of the comments at that point were about other features that might also be considered, though Hartz and Zijlstra tried to keep things focused on the PEP itself.

One outstanding issue was the treatment of synchronous generator expressions versus the asynchronous variety. The PEP, which will be changing as we will see, currently makes a distinction between the two because "yield from" is not permitted in asynchronous generators. Another appendix goes into more detail; the difference comes down to whether the generator-protocol methods, such as send(), can be used. There are two ways that the semantics of an unpacking generator expression could be defined:

    g = (*x for x in it)

    # could be:

    def gen():
        for x in it:
            yield from x
    g = gen()

    # or:

    def gen():
        for x in it:
            for i in x:
                yield i
    g = gen()
Either of those works for synchronous generators, but:
    g = (*x async for x in ait())

    # must be:

    async def gen()
        async for x in ait():
            for i in x:
                yield i
    g = gen()
So the question is whether it makes sense to define the synchronous semantics differently so that those comprehensions could potentially use the generator-protocol methods. Hartz ran a poll in the thread, with several possibilities for the semantics, but no real consensus was reached—perhaps unsurprising given the esoteric nature of the question and that thread participants had likewise been unable to converge on the semantics.

In mid-September, after more than a month of quiet in the thread, Hartz submitted the PEP to the steering council for consideration. The council started looking at it a month later, with council member Pablo Galindo Salgado noting that the group was uncomfortable with positioning the new syntax "as offering 'much clearer' code compared to existing alternatives (such as itertools.chain.from_iterable, explicit loops, or nested comprehensions)" because readability is in the eye of the beholder. Instead, the council suggested that "the stronger, more objective argument is syntactic consistency as extending Python's existing unpacking patterns naturally into comprehensions". Hartz agreed and adjusted the PEP accordingly.

In the thread, "Nice Zombies" highlighted part of the "Rationale" section of the PEP, which nicely illustrates the argument for syntactic consistency:

This proposal was motivated in part by a written exam in a Python programming class, where several students used the proposed notation (specifically the set version) in their solutions, assuming that it already existed in Python. This suggests that the notation represents a logical, consistent extension to Python's existing syntax. By contrast, the existing double-loop version [x for it in its for x in it] is one that students often get wrong, the natural impulse for many students being to reverse the order of the for clauses.

One of the examples given in the PEP shows how an explicit loop to create a set could be changed in the shutil module of the standard library:

    # current:
    ignored_names = []
    for pattern in patterns:
        ignored_names.extend(fnmatch.filter(names, pattern))
    return set(ignored_names)

    # proposed:
    return {*fnmatch.filter(names, pattern) for pattern in patterns}
Instead of extending the list from the iterable returned by fnmatch.filter(), then converting it to a set, the new syntax allows creating the set directly. The existing code could have taken advantage of set.update() to avoid using the list, but the new syntax is in keeping with the ideas behind comprehensions—and was apparently intuitively obvious, but wrong, to Python students.

In its announcement of the PEP's acceptance, the SC also decided the question about generator comprehensions: "we require that both synchronous and asynchronous generator expressions use explicit loops rather than yield from for unpacking operations". That removes some advanced use cases "that are rarely relevant when writing comprehensions" but simplifies the mental model for the new feature. "We don't believe that developers writing comprehensions should have to think about the differences between sync and async generator semantics or about generator delegation protocols."

While it is certainly useful, the feature is not revolutionary in any sense, it simply fills a fairly longstanding hole that has been noticed and discussed several times over the years. Python is a mature language at this point, so revolutions are likely to be few and far between—if not absent entirely. The whole tale shows, however, that, with some persistence, a well-written PEP, and a well-shepherded discussion (by Hartz joined by Zijlstra, Demaine was absent this time around), changes can be made. Future Python students can rejoice starting next October.

Comments (15 posted)

Zig's new plan for asynchronous programs

By Daroc Alden
December 2, 2025

The designers of the Zig programming language have been working to find a suitable design for asynchronous code for some time. Zig is a carefully minimalist language, and its initial design for asynchronous I/O did not fit well with its other features. Now, the project has announced (in a Zig SHOWTIME video) a new approach to asynchronous I/O that promises to solve the function coloring problem, and allows writing code that will execute correctly using either synchronous or asynchronous I/O.

In many languages (including Python, JavaScript, and Rust), asynchronous code uses special syntax. This can make it difficult to reuse code between synchronous and asynchronous parts of a program, introducing a number of headaches for library authors. Languages that don't make a syntactical distinction (such as Haskell) essentially solve the problem by making everything asynchronous, which typically requires the language's runtime to bake in ideas about how programs are allowed to execute.

Neither of those options was deemed suitable for Zig. Its designers wanted to find an approach that did not add too much complexity to the language, that still permitted fine control over asynchronous operations, and that still made it relatively painless to actually write high-performance event-driven I/O. The new approach solves this by hiding asynchronous operations behind a new generic interface, Io.

Any function that needs to perform an I/O operation will need to have access to an instance of the interface. Typically, that is provided by passing the instance to the function as a parameter, similar to Zig's Allocator interface for memory allocation. The standard library will include two built-in implementations of the interface: Io.Threaded and Io.Evented. The former uses synchronous operations except where explicitly asked to run things in parallel (with a special function; see below), in which case it uses threads. The latter (which is still a work-in-progress) uses an event loop and asynchronous I/O. Nothing in the design prevents a Zig programmer from implementing their own version, however, so Zig's users retain their fine control over how their programs execute.

Loris Cro, one of Zig's community organizers, wrote an explanation of the new behavior to justify the approach. Synchronous code is not much changed, other than using the standard library functions that have moved under Io, he explained. Functions like the example below, which don't involve explicit asynchronicity, will continue to work. This example creates a file, sets the file to close at the end of the function, and then writes a buffer of data to the file. It uses Zig's try keyword to handle errors, and defer to ensure the file is closed. The return type, !void, indicates that it could return an error, but doesn't return any data:

    const std = @import("std");
    const Io = std.Io;

    fn saveFile(io: Io, data: []const u8, name: []const u8) !void {
        const file = try Io.Dir.cwd().createFile(io, name, .{});
        defer file.close(io);
        try file.writeAll(io, data);
    }

If this function is given an instance of Io.Threaded, it will create the file, write data to it, and then close it using ordinary system calls. If it is given an instance of Io.Evented, it will instead use io_uring, kqueue, or some other asynchronous backend suitable to the target operating system. In doing so, it might pause the current execution and go work on a different asynchronous function. Either way, the operation is guaranteed to be complete by the time writeAll() returns. A library author writing a function that involves I/O doesn't need to care about which of these things the ultimate user of the library chooses to do.

On the other hand, suppose that a program wanted to save two files. These operations could profitably be done in parallel. If a library author wanted to enable that, they could use the Io interface's async() function to express that it does not matter which order the two files are saved in:

    fn saveData(io: Io, data: []const u8) !void {
        // Calls saveFile(io, data, "saveA.txt")
        var a_future = io.async(saveFile, .{io, data, "saveA.txt"});
        var b_future = io.async(saveFile, .{io, data, "saveB.txt"});

        const a_result = a_future.await(io);
        const b_result = b_future.await(io);

        try a_result;
        try b_result;

        const out: Io.File = .stdout();
        try out.writeAll(io, "save complete");
    }

When using an Io.Threaded instance, the async() function doesn't actually isn't actually required to do anything asynchronously [although the actual implementation may dispatch the function to a separate thread, depending on how it was configured] — it can just run the provided function right away. So, with that version of the interface, the function first saves file A and then file B. With an Io.Evented instance, the operations are actually asynchronous, and the program can save both files at once.

The real advantage of this approach is that it turns asynchronous code into a performance optimization. The first version of a program or library can write normal straight-line code. Later, if asynchronicity proves to be useful for performance, the author can come back and write it using asynchronous operations. If the ultimate user of the function has not enabled asynchronous execution, nothing changes. If they have, though, the function becomes faster transparently — nothing about the function signature or how it interacts with the rest of the code base changes.

One problem, however, is with programs where two parts are actually required to execute simultaneously for correctness. For example, suppose that a program wants to listen for connections on a port and simultaneously respond to user input. In that scenario, it wouldn't be correct to wait for a connection and only then ask for user input. For that use case, the Io interface provides a separate function, asyncConcurrent()concurrent() [this function was renamed during development; concurrent() is the most recent name] that explicitly asks for the provided function to be run in parallel. Io.Threaded uses a thread in a thread pool to accomplish this. Io.Evented treats it exactly the same as a normal call to async().

    const socket = try openServerSocket(io);
    var server = try io.concurrent(startAccepting, .{io, socket});
    defer server.cancel(io) catch {};

    try handleUserInput(io);

If the programmer uses async() where they should have used concurrent(), that is a bug. Zig's new model does not (and cannot) prevent programmers from writing incorrect code, so there are still some subtleties to keep in mind when adapting existing Zig code to use the new interface.

The style of code that results from this design is a bit more verbose than languages that give asynchronous functions special syntax, but Andrew Kelley, creator of the language, said that "it reads like standard, idiomatic Zig code." In particular, he noted that this approach lets the programmer use all of Zig's typical control-flow primitives, such as try and defer; it doesn't introduce any new language features specific to asynchronous code.

To demonstrate this, Kelley gave an example of using the new interface to implement asynchronous DNS resolution. The standard getaddrinfo() function for querying DNS information falls short because, although it makes requests to multiple servers (for IPv4 and IPv6) in parallel, it waits for all of the queries to complete before returning an answer. Kelley's example Zig code returns the first successful answer, canceling the other inflight requests.

Asynchronous I/O in Zig is far from done, however. Io.Evented is still experimental, and doesn't have implementations for all supported operating systems yet. A third kind of Io, one that is compatible with WebAssembly, is planned (although, as that issue details, implementing it depends on some other new language features). The original pull request for Io lists 24 planned follow-up items, most of which still need work.

Still, the overall design of asynchronous code in Zig appears to be set. Zig has not yet had its 1.0 release, because the community is still experimenting with the correct way to implement many features. Asynchronous I/O was one of the larger remaining priorities (along with native code generation, which was also enabled by default for debug builds on some architectures this year). Zig seems to be steadily working its way toward a finished design — which should decrease the number of times Zig programmers are asked to rewrite their I/O because the interface has changed again.

Comments (20 posted)

BPF and io_uring, two different ways

By Jonathan Corbet
November 20, 2025
BPF allows programs uploaded from user space to be run, safely, within the kernel. The io_uring subsystem, too, can be thought of as a way of loading programs in the kernel, though the programs in question are mostly a sequence of I/O-related system calls. It has sometimes seemed inevitable that io_uring would, like many other parts of the kernel, gain BPF capabilities as a way of providing more flexibility to user space. That has not yet happened, but there are currently two patch sets under consideration that take different approaches to the problem.

An io_uring "program" is built by placing a series of entries in a submission queue managed in a ring buffer shared between the kernel and user space. Each submission-queue entry (SQE) describes a system call to be performed, and may make use of special buffers and file descriptors maintained within io_uring itself. Each SQE is normally executed asynchronously, but it is possible to link a series of SQEs so that each is only executed after the successful completion of the previous one. The result of each operation is stored in a completion-queue entry (CQE) in a second shared ring. Using io_uring, an application can keep many streams of I/O going concurrently with a minimum of system calls.

The io_uring linkage mechanism enables simple sequences of operations, such as creating a file, writing a buffer to that file, and closing the file. It does not offer much flexibility; one operation cannot pass information to the next or change how subsequent operations may execute. So it is not surprising that adding BPF support is seen as a way of filling that gap. So far, though, no attempts at adding that support have been seriously considered for merging into the mainline.

BPF operations

In early November, Ming Lei posted a patch set adding BPF support to io_uring in the form of a new operation, IORING_OP_BPF, that can be placed in the submission queue. The linkage mechanism can be used, for example, to cause a BPF program to be run between two other io_uring operations. The programs themselves can be set up as, essentially, new io_uring operations.

Specifically, the patch creates a new struct ops program type for defining BPF operations. A user-space program will fill in and register a uring_bpf_ops structure:

    struct uring_bpf_ops {
	unsigned short		id;
	uring_io_prep_t		prep_fn;
	uring_io_issue_t	issue_fn;
	uring_io_fail_t		fail_fn;
	uring_io_cleanup_t	cleanup_fn;
    };

The id is an operation ID; only the bottom eight bits are used, meaning that a program can establish up to 256 separate BPF-based operations. The rest of the fields are BPF programs that will implement the functions required by io_uring to set up, execute, and clean up after I/O operations. There are a couple of new kfuncs provided for those programs to obtain the request data from an SQE and to store a result of the operation in the proper CQE.

Once the operation has been set up, io_uring SQEs can make use of it with an IORING_OP_BPF operation specifying the appropriate ID. Two buffers can be passed to a BPF operation in each request; a new kfunc has been added to allow BPF programs to bulk-copy data between buffers. One of the use cases targeted by this work is to make it easy to copy data between user space and in-kernel buffers that are not readily accessible from user space; this feature would evidently be helpful for the increasingly capable ublk io_uring-based block driver subsystem.

The number of review comments on this work has been relatively small. Stefan Metzmacher said: "This sounds useful to me". But Pavel Begunkov was rather more negative, saying that attempts to add BPF operations to io_uring in the past did not work well. The performance of BPF programs in that context, he said, is poor due to the associated io_uring overhead. He has a different approach, he added, that seems more promising.

Hooking into the control loop

Shortly thereafter, Begunkov posted a new version of a series he has been working on sporadically to add BPF support to io_uring in a different way. Rather than add a new operation type, this series adds a new hook into the io_uring completion loop, allowing a BPF program to be run as operations finish. This implementation, he said, can improve performance by moving CQE processing from user space into the kernel. It also, he said, could eventually allow for the removal of the io_uring linkage mechanism, which he called "a large liability" due to the complexity it adds, entirely.

This series, which shows some signs of having been prepared in a hurry, also sets up a struct ops hook. It adds a single callback which, according to the changelog, should be called handle_events(), but is actually:

    int (*loop)(struct io_ring_ctx *ctx, struct iou_loop_state *ls);

The ctx field gives information about the submission and completion queues, while the iou_loop_state parameter can be used to control how often loop() is to be called, determined by the number of available CQEs and a timeout. When this program is called, it can look at the completed operations, if any, and possibly enqueue new operations in response.

There is a pair of new kfuncs to go along with this mechanism. Pointers to the various parts of the ring buffer can be had with bpf_io_uring_get_region() (though Begunkov says that this interface is likely to be replaced in a future version), and bpf_io_uring_submit_sqes() can be used to submit new operations. Using these kfuncs, a BPF program could replace links by waiting for operations of interest to complete, then submitting the next operation that should follow, perhaps using information from the operations that have already completed.

Lei, having looked at Begunkov's patches, said that they do not solve the problem as well as his operation-based approach. The key difference Lei pointed out is that, with IORING_OP_BPF, the bulk of the application logic, including the creation of SQEs, remains in user space. With Begunkov's series, instead, much of the application logic must be pushed into the kernel, necessitating a lot of communication between user space and the kernel that has the potential to hurt performance. Begunkov answered that the communication can be handled efficiently using a BPF arena, and that his approach provides a greater level of flexibility to handle more types of applications.

Neither developer appears to have convinced the other. Lei intends to continue work on IORING_OP_BPF, while Begunkov is likely to do the same with his patch. Both developers have said that there might be room in the kernel for both approaches, though one might reasonably expect resistance from the wider BPF community to adding what appears to be redundant functionality. A third possibility — that io_uring and BPF remain unintegrated as they have for years — remains a possibility as well.

Comments (none posted)

Checked-size array parameters in C

By Jonathan Corbet
December 1, 2025
There are many possible programmer mistakes that are not caught by the minimal checks specified by the C language; among those is passing an array of the wrong size to a function. A recent attempt to add some safety around array parameters within the crypto layer involved the use of some clever tricks, but it turns out that clever tricks are unnecessary in this case. There is an obscure C feature that can cause this checking to happen, and it is already in use in a few places within the kernel.

The discussion started when Ard Biesheuvel sought to improve the safety of the poetically named function xchacha20poly1305_encrypt():

    void xchacha20poly1305_encrypt(u8 *dst, const u8 *src, const size_t src_len,
			           const u8 *ad, const size_t ad_len,
			       	   const u8 nonce[XCHACHA20POLY1305_NONCE_SIZE],
			       	   const u8 key[CHACHA20POLY1305_KEY_SIZE]);

A potential problem with this function is that it takes as parameters several pointers to arrays of type u8. As Biesheuvel pointed out, the size of the nonce and key arrays is not checked by the compiler, even though it is clearly specified in the function prototype. That makes it easy to, for example, give the parameters in the wrong order. The resulting vulnerabilities are generally not the outcome developers have in mind when they write cryptographic code.

Biesheuvel suggested that it was possible to write the prototype this way instead (differences shown in bold):

    void xchacha20poly1305_encrypt(u8 *dst, const u8 *src, const size_t src_len,
			           const u8 *ad, const size_t ad_len,
			       	   const u8 (*nonce)[XCHACHA20POLY1305_NONCE_SIZE],
			       	   const u8 (*key)[CHACHA20POLY1305_KEY_SIZE]);

The types of the last two arguments have changed; there is a new level of pointer indirection, with the argument being a pointer to an array of a given size. Callers must change their calls by adding an additional & operator to obtain the desired pointer type, but the address that is passed is the same. In this case, though, the compiler will check the sizes of the array passed, and will now catch a reordering of the arguments to the function.

Jason Donenfeld was interested by the idea, but he knew of an arguably more straightforward way to address this problem. It seems that, buried deep within the C standard, is a strange usage of the static keyword, making it possible to write the prototype as:

    void xchacha20poly1305_encrypt(u8 *dst, const u8 *src, const size_t src_len,
			           const u8 *ad, const size_t ad_len,
			       	   const u8 nonce[static XCHACHA20POLY1305_NONCE_SIZE],
			       	   const u8 key[static CHACHA20POLY1305_KEY_SIZE]);

This, too, will cause the compiler to check the sizes of the arrays, and it does not require changes on the caller side. Unlike the pointer trick, which requires an exact match on the array size, use of static will only generate a warning if the passed-in array is too small. So it will not catch all mistakes, though it is sufficient to prevent memory-safety and swapped-argument problems.

Eric Biggers pointed out that GCC can often generate "array too small" warnings even without static, but that the kernel currently disables those warnings; that would suppress them when static is used as well. (The warning was disabled in 6.8 due to false positives.) But he thought that adding static was worthwhile to get the warnings in Clang — if Linus Torvalds would be willing to accept use of "this relatively obscure feature of C".

Torvalds, as it turns out, has no objection to this usage; he likes the feature, if not the way it was designed:

The main issue with the whole 'static' thing is just that the syntax is such a horrible hack, where people obviously picked an existing keyword that made absolutely no sense, but was also guaranteed to have no backwards compatibility issues.

He pointed out that there are a number of places in the kernel that are already using it; for a simple example, see getconsxy() in the virtual-terminal driver. He suggested perhaps hiding it with a macro like min_array_size() just to make the usage more obvious, but didn't seem convinced that it was necessary. Donenfeld followed up with a patch to that effect a few days later, but then pivoted to an at_least marker instead.

A lot of work has gone into the kernel to make its use of C as safe as possible, but that does not mean that the low-hanging fruit has all been picked. The language has features, such as static when used to define formal array parameters, that can improve safety, but which are not generally known and not often used. In this particular case, though, it would not be surprising to see this "horrible hack" come into wider use in the near future.

Comments (25 posted)

Some 6.18 development statistics

By Jonathan Corbet
December 1, 2025
Linus Torvalds released the 6.18 kernel as expected on November 30, closing the last full development cycle of 2025. It was another busy cycle, featuring a record number of developers. The time has come for a look at where the code came from for this kernel release, but also for the year-long long-term-support cycle which has also reached its conclusion with this release.

As a reminder: LWN subscribers can find much of the information below — and more — at any time in the LWN Kernel Source Database.

The 6.18 release

The 6.18 development cycle brought in 13,710 commits from 2,134 developers. The commit count is more-or-less average for recent releases, but the number of developers involved is the highest that has yet been seen in the kernel's development history. There were 333 first-time kernel contributors this time around, falling just barely short of the record 335 new developers who contributed to 6.12 one year ago.

The most active contributors this time around were:

Most active 6.18 developers
By changesets
Takashi Iwai 2051.5%
Stefan Metzmacher 1831.3%
Krzysztof Kozlowski 1501.1%
Brian Masney 1451.1%
Laurent Pinchart 1381.0%
Sean Christopherson 1260.9%
Al Viro 1210.9%
Qianfeng Rong 1090.8%
Eric Biggers 1070.8%
Bartosz Golaszewski 1000.7%
Jakub Kicinski 970.7%
Mauro Carvalho Chehab 970.7%
Eric Dumazet 950.7%
Ian Rogers 940.7%
Jani Nikula 890.6%
Russell King 880.6%
Rob Herring 850.6%
Thorsten Blum 850.6%
Thomas Weißschuh 770.6%
Xichao Zhao 730.5%
By changed lines
Linus Torvalds11758114.9%
Przemek Kitszel 165302.1%
Ian Rogers 138691.8%
Takashi Iwai 131471.7%
Taniya Das 120001.5%
Alice Ryhl 118361.5%
Eric Biggers 103491.3%
Abhijit Gangurde 94321.2%
Fan Gong 76641.0%
Jerome Brunet 71810.9%
Jakub Kicinski 71030.9%
Mauro Carvalho Chehab 70620.9%
Tomeu Vizoso 63360.8%
Laura Nao 62410.8%
Dongsheng Yang 54720.7%
Alex Deucher 54470.7%
Dikshita Agarwal 50650.6%
Beleswar Padhi 50010.6%
Hector Martin 48700.6%
Laurent Pinchart 46790.6%

Takashi Iwai, the maintainer of the sound subsystem, was the top contributor of changesets this time around; the bulk of them were bringing scope-based resource management to that subsystem. Stefan Metzmacher worked extensively in the SMB filesystem client and server implementations. Krzysztof Kozlowski, as is often the case, contributed a lot of devicetree (and related) changes. Brian Masney performed a large set of API conversions within the clock-driver subsystem, and Laurent Pinchart made a lot of updates within the media subsystem.

Linus Torvalds makes a rare appearance at the top of the "lines changed" column by virtue of having removed the bcachefs filesystem. Przemek Kitszel performed some significant refactoring of the Intel "ice" Ethernet driver. Ian Rogers worked extensively with the perf tool. Iwai's work has been noted above; Taniya Das added a number of clock drivers. Also worthy of mention is Alice Ryhl who, after years of work, contributed the Rust binder driver.

This time around, 9.0% of the commits to the mainline had Tested-by tags, while 53.6% had Reviewed-by tags — both typical numbers. The top testers and reviewers for 6.18 were:

Test and review credits in 6.18
Tested-by
Dan Wheeler 1348.9%
Neil Armstrong 916.1%
Arnaldo Carvalho de Melo 392.6%
Bryan O'Donoghue 332.2%
Vikash Garodia 261.7%
Chris Packham 251.7%
Rick Edgecombe 251.7%
Venkat Rao Bagalkote 251.7%
Mathias Krause 241.6%
John Allen 241.6%
Rinitha S 221.5%
Hans de Goede 211.4%
Thomas Falcon 201.3%
Ramu R 181.2%
Jayesh Choudhary 171.1%
Reviewed-by
Simon Horman 2272.3%
Dmitry Baryshkov 1992.0%
Konrad Dybcio 1791.8%
Andy Shevchenko 1391.4%
Krzysztof Kozlowski 1371.4%
Geert Uytterhoeven 1321.3%
Lorenzo Stoakes 1291.3%
Frank Li 1201.2%
Linus Walleij 1151.2%
Rob Herring 1111.1%
Alice Ryhl 1061.1%
Andrew Lunn 1041.1%
David Sterba 991.0%
Christian Brauner 971.0%
Alex Deucher 941.0%

These listings change little from one release to the next; the community's most diligent testers and reviewers are seemingly in it for the long term.

The work in 6.18 was supported by 217 employers that we know of; those most active employers were:

Most active 6.18 employers
By changesets
Intel142010.4%
(Unknown)13079.5%
Google10807.9%
Red Hat8666.3%
AMD6825.0%
(None)6334.6%
Linaro5393.9%
SUSE5343.9%
Qualcomm5293.9%
Meta4453.2%
Renesas Electronics3582.6%
Huawei Technologies3562.6%
(Consultant)3092.3%
NVIDIA3002.2%
Oracle2571.9%
vivo Mobile Communication Co2571.9%
NXP Semiconductors2231.6%
Arm2101.5%
Collabora1891.4%
SerNet1831.3%
By lines changed
Linux Foundation11814215.0%
Intel742559.4%
Google678818.6%
Qualcomm564237.2%
(Unknown)553077.0%
AMD359014.6%
(None)338704.3%
Red Hat290833.7%
SUSE260273.3%
Meta246693.1%
Huawei Technologies232503.0%
Linaro194812.5%
(Consultant)152611.9%
NVIDIA143951.8%
Collabora137821.8%
Texas Instruments124341.6%
Renesas Electronics107561.4%
NXP Semiconductors98931.3%
BayLibre90251.1%
Microsoft80651.0%

These numbers, too, tend to be relatively consistent from one release to the next.

Finally, counting the number of Signed-off-by tags applied by developers to patches authored by others gives an insight into patch acceptance; those tags are usually applied when a patch is applied to a subsystem tree on its way toward the mainline. The top appliers of patches in 6.18 were:

Non-author Signed-off-by tags in 6.18
Individual
Jakub Kicinski 10558.1%
Alex Deucher 5444.2%
Andrew Morton5224.0%
Hans Verkuil 4323.3%
Mark Brown 3983.1%
Greg Kroah-Hartman 3632.8%
Bjorn Andersson 3332.6%
Paolo Abeni 3052.3%
Steve French 2652.0%
Jens Axboe 1971.5%
Arnaldo Carvalho de Melo 1921.5%
Jonathan Cameron 1911.5%
Sakari Ailus 1701.3%
Borislav Petkov 1641.3%
Jonathan Corbet 1591.2%
Alexei Starovoitov 1451.1%
Shawn Guo 1411.1%
David Sterba 1321.0%
Martin K. Petersen 1321.0%
Lee Jones 1160.9%
Employers
Meta155112.0%
Google143911.1%
Intel12319.5%
Red Hat10087.8%
AMD9637.4%
Linaro7685.9%
Qualcomm5904.5%
Arm5224.0%
Linux Foundation4383.4%
Microsoft4323.3%
Cisco4323.3%
SUSE4053.1%
(Unknown)3292.5%
NVIDIA2932.3%
Huawei Technologies2862.2%
Oracle2401.8%
(None)1991.5%
Renesas Electronics1751.3%
LWN.net1591.2%
Linutronix1130.9%

It still remains true that (just) over half of the patches landing in the mainline pass through the hands of maintainers employed by just five companies — two hyperscalers, two CPU manufacturers, and Red Hat. The Linux Foundation has moved down on this list, perhaps because the amount of activity in the staging tree has fallen considerably over the years, reducing the number of patches handled by Greg Kroah-Hartman.

The longer cycle

For some years now, the last kernel release of each calendar year is designated a long-term-support (LTS) release, which will receive support for a minimum of two years. Most Linux systems in the wild are not running arbitrary kernel releases; they are, instead, running one of these LTS releases. One can thus think of the kernel development cycle as having a one-year cadence; there are checkpoint releases every nine or ten weeks, but there is one major release at the end of the year that is widely deployed.

Last year's LTS release was 6.12; there have been six releases since then that incorporated 80,035 non-merge changesets from 5,275 developers. The most active developers over the course of the year were:

Most active 6.13-18 developers
By changesets
Kent Overstreet 9011.1%
Krzysztof Kozlowski 7390.9%
Sean Christopherson 7150.9%
Jani Nikula 6530.8%
Thomas Weißschuh 6500.8%
Bartosz Golaszewski 6500.8%
Jakub Kicinski 5790.7%
Takashi Iwai 5710.7%
Ian Rogers 5620.7%
Eric Biggers 5490.7%
Dmitry Baryshkov 5380.7%
Christoph Hellwig 5170.6%
Thomas Zimmermann 4870.6%
Andy Shevchenko 4830.6%
Matthew Wilcox 4780.6%
Rob Herring 4620.6%
Alex Deucher 4550.6%
Ville Syrjälä 4260.5%
Filipe Manana 4150.5%
Al Viro 4120.5%
By changed lines
Linus Torvalds1231752.9%
Wayne Lin 815741.9%
Ian Rogers 807351.9%
Philipp Hortmann 764071.8%
Eric Biggers 499991.2%
Dennis Dalessandro 483571.1%
Johannes Berg 423691.0%
Bitterblue Smith 397880.9%
Jan Kara 369430.9%
Takashi Iwai 358870.8%
Dave Penkler 340470.8%
Rob Herring 338940.8%
Miri Korenblit 337290.8%
Dmitry Baryshkov 314400.7%
Kent Overstreet 295000.7%
Taniya Das 290770.7%
Jani Nikula 280080.7%
Dr. David Alan Gilbert 264380.6%
Andrew Donnellan 258450.6%
Richard Fitzgerald 254020.6%

In 2024, Kent Overstreet topped the by-changesets list with nearly 4,000 commits; there were two other developers (Uwe Kleine-König and Krzysztof Kozlowski) with over 1,000 commits. This year, the curve is somewhat flatter, with nobody reaching 1,000 commits. The same is true of the lines-changed column, most resulting from a slowdown in the addition of massive, machine-generated amdgpu graphics-driver header files.

It is somewhat ironic, though, that the top of the by-changesets column reflects the addition of bcachefs, while the top of the by-lines column reflects its removal.

The top testers and reviewers over the longer cycle were:

Test and review credits in 6.13-18
Tested-by
Dan Wheeler 7608.9%
Neil Armstrong 3293.9%
Arnaldo Carvalho de Melo 1571.8%
Thomas Falcon 1131.3%
Dmitry Osipenko 971.1%
Rinitha S 921.1%
Randy Dunlap 891.0%
Venkat Rao Bagalkote 821.0%
Rafal Romanowski 760.9%
Tomi Valkeinen 690.8%
Mark Pearson 680.8%
Nicolin Chen 680.8%
James Clark 640.8%
Timur Tabi 600.7%
Bryan O'Donoghue 580.7%
Babu Moger 560.7%
Alex Bennée 550.6%
Pucha Himasekhar Reddy 540.6%
Vikash Garodia 520.6%
K Prateek Nayak 510.6%
Reviewed-by
Simon Horman 12552.3%
Dmitry Baryshkov 10331.9%
Krzysztof Kozlowski 9321.7%
Ilpo Järvinen 7621.4%
Christoph Hellwig 7361.3%
Geert Uytterhoeven 7351.3%
Konrad Dybcio 6841.2%
Andy Shevchenko 6331.1%
Andrew Lunn 5971.1%
David Sterba 5961.1%
Rob Herring 5561.0%
Linus Walleij 4670.8%
Neil Armstrong 4560.8%
AngeloGioacchino Del Regno 4470.8%
Frank Li 4310.8%
Laurent Pinchart 4270.8%
Chao Yu 4160.8%
Darrick J. Wong 3830.7%
Christian König 3730.7%
Jeff Layton 3730.7%

The most active employers (out of the 372 total) over the longer cycle were:

Most active 6.13-18 employers
By changesets
Intel881311.0%
(Unknown)67968.5%
Google60377.5%
Red Hat51816.5%
(None)42575.3%
AMD40985.1%
Linaro37014.6%
SUSE27503.4%
Meta27443.4%
Qualcomm25323.2%
Huawei Technologies19172.4%
Oracle18862.4%
NVIDIA18822.4%
Renesas Electronics18582.3%
Arm15471.9%
IBM13451.7%
(Consultant)12521.6%
NXP Semiconductors12181.5%
Linutronix11491.4%
Microsoft8871.1%
By lines changed
Intel43494310.2%
(Unknown)3831819.0%
Google3097677.2%
AMD2773846.5%
Qualcomm2354505.5%
Red Hat2157255.0%
(None)1766864.1%
SUSE1335673.1%
Linaro1299463.0%
Linux Foundation1290583.0%
Meta1249272.9%
NVIDIA1079312.5%
IBM969062.3%
Huawei Technologies821321.9%
Arm772171.8%
Emerson769191.8%
Collabora646291.5%
NXP Semiconductors625491.5%
Oracle605531.4%
Renesas Electronics527321.2%

Now begins the 6.19 development cycle, starting this whole process over again. As of this writing, there are just over 12,500 changesets waiting in linux-next, suggesting that 6.19 will be another busy — but typical — development cycle. In other words, the kernel project will be beginning 2026 at full speed; we'll do everything we can to keep up.

Comments (2 posted)

Just: a command runner

By Joe Brockmeier
December 3, 2025

Over time, many Linux users wind up with a collection of aliases, shell scripts, and makefiles to run simple commands (or a series of commands) that are often used, but challenging to remember and annoying to type out at length. The just command runner is a Rust-based utility that just does one thing and does it well: it reads recipes from a text file (aptly called a "justfile"), and runs the commands from an invoked recipe. Rather than accumulating a library of one-off shell scripts over time, just provides a cross-platform tool with a framework and well-documented syntax for collecting and documenting tasks that makes it useful for solo users and collaborative projects.

just what it is

Using just has a few advantages over a collection of scripts or bending make to uses it may not be intended for. It's certainly easier to get started with than make for newer Linux users who haven't had the need to learn make previously. Generally, just is more ergonomic for what it does; it isn't a complete replacement for shell scripts or scripting, but it provides a better framework, organization, and user experience.

A task is defined by a recipe. A recipe is, basically a series of commands that are to be run by just. This is a simple recipe that tells me a bit about the operating system just is running on:

    os:
        @echo "This is a {{os()}} system running on {{arch()}} with {{num_cpus()}} logical CPUs."

Running "just os" produces the following output on my system:

    This is a linux system running on x86_64 with 16 logical CPUs.

The recipes for tasks are collected in a justfile and stored in the user's home directory or in the root directory of a project. Technically, these can be broken out into multiple files and imported into the main justfile, but let's not go down that rabbit hole just now.

Say an open-source project includes a justfile in its Git repository for doing common tasks, such as updating the changelog, running tests, or publishing a release; all a new contributor needs to do is run "just -l" to discover the tasks that are available, along with their documentation. Assuming, of course, someone has written the documentation—just is useful, but it's not magic.

Another advantage is that just is expressly designed for running tasks across multiple platforms and for collaborative use. For instance, it has built-in functions for determining and acting on system information (such as os() for the operating system), environment variables, error handling, and much more. A task can be written to use sh on Linux, ksh on macOS, and PowerShell on Windows, if needed. Recipes can have shared variables and settings, so one need not redefine $TMPDIR in each shell script for a project. A recipe can also have dependencies, specified similarly to makefile dependencies, which can be reused for multiple recipes as needed.

Users can invoke multiple recipes at once, and a recipe's dependencies can be skipped by using the --no-deps option when a user just wants to execute the primary task. For example, perhaps a task has a dependency on downloading an ISO image or other large file; that can be skipped if it has been run recently and the user knows there is not a newer one.

Project history

Casey Rodarmor created just in 2016. The project is inspired by make; in fact it was originally just a shell script that called make. But just is not meant to be a replacement for make or a build system in general; it is just a command runner. It is licensed, somewhat unusually, under the Creative Commons Zero (CC0) 1.0 Universal deed: essentially, it is public-domain code. Rodarmor wrote that he chose CC0 because he had "no desire to impose any restrictions whatsoever on anyone who wants to use my code." Nearly 200 people have made small contributions to just, but the bulk of development is Rodarmor's work.

Like many maintainers of open-source projects these days, Rodarmor does not tend to do major releases with many new features. Instead, there is a frequent stream of minor releases with a few new features and miscellaneous improvements, along with point releases with bug fixes. The most recent minor release, 1.43.0, came out in September, with the 1.43.1 bug fix release on November 12. According to the Just Programmer's Manual, there will never be a 2.0 release. Any backward-incompatible changes, "will be opt-in on a per-justfile basis, so users may migrate at their leisure". Any recipes written for a version of just since 1.0 should continue to work without breakage indefinitely.

Most popular Linux distributions provide just packages, though the version in a distribution's repository may be somewhat out of date given how frequently the project is released. Users can also snag the latest version using "cargo install just", or use one of the project's pre-built binaries.

Justfiles

The just command looks for a file named justfile with recipes in the current directory or those above it. The idea is that a user might include a justfile with each project, as well as a justfile in their home directory for miscellaneous recipes. So, if the present working directory is ~/src/project, the "just foo" command will tell the utility to look in ~/src/project for a justfile. If it is not found there, then just will look in ~/src, and ~ for a justfile. Technically it will keep looking in /home and such, but one hopes users are not placing their recipe files there. One of the features added in 1.43.0 is a --ceiling option; this lets the user specify the "top" directory where just should stop looking for a justfile.

Note that the name is case-insensitive; just will accept Justfile, justfile, JUSTFILE, and .justfile if one prefers to hide configuration files. Even JuStFiLe will work, though it would be an affront to good taste. Naturally, there is a justfile in the just repository that has recipes for building project documentation, running demos, creating the just man page, updating Rust, and more.

Once just finds a justfile, it stops looking; this means that, for example, if one has a justfile in ~/ and another in ~/project/foo, running just in the ~/project/foo directory will usually not find any recipes above that directory. This can be changed by adding the set fallback directive in a recipe file. That way, if a user runs "just foo" and there is no recipe foo to be found in the first justfile, it will keep looking in the directories above it.

The syntax for just is inspired by make, but Rodarmor wanted to avoid what he calls the idiosyncrasies of make: such as the need to have .PHONY targets when there is a file with the same name as a make target (e.g., "clean") in the directory. Recipe lines must be indented at least one level from the recipe name, but users have a choice of spaces or tabs. Note that each recipe does have to be consistent: just does not allow indenting one line of a recipe with spaces, and another line with tabs.

Here is a simple recipe, called pub I use to update my Hugo-based static site:

    # update dissociatedpress.net
    [no-cd]
    pub:
        hugo
        rsync -avh --progress -e ssh /home/user/src/dissociatedpress/public/ \
        user@dissociatedpress.net:/path/to/www/html
        w3m https://dissociatedpress.net/

When I run "just pub", just steps through the recipe and runs each line in a separate shell. It runs hugo in the directory where the justfile is invoked; the default is for just to run a recipe in the directory where the justfile is stored, but [no-cd] instructs it to run the recipe in the current directory instead. It will rsync the contents to the server, and then open the site in w3m so I can verify that the site was updated.

Running "just -l" will list all of the available recipes in a justfile. If there is a comment above the recipe, it will be displayed:

    $ just -l
    pub #update dissociatedpress.net

The "just --choose" command will display an interactive menu that allows the user to choose the just recipe to run. This makes use of the fzf command-line fuzzy finder by default; it will work without it, but just prints a warning if it cannot find fzf. Most distributions should have fzf available.

Polyglot

Typically, just uses "sh -cu" to execute the commands in a recipe, unless configured otherwise. That can be changed with the "set shell" setting in a recipe, such as this:

    set shell := ["fish", "-c"]

One of just's more interesting features is its support for writing recipes in many programming languages, so users are not limited to shell interpreters; these are called shebang recipes. Basically, a user can write a recipe using JavaScript, Perl, Python, Ruby, and just about any other scripting language. A shebang recipe is saved to a temporary file by just and then run using the interpreter specified:

    python:
        #!/usr/bin/env python3
        print('GNU Terry Pratchett')

Running "just python" saves the Python script to /tmp to be executed by python3. The temporary directory can be specified in several ways if one wants to avoid placing scripts in a world-readable directory. Users can, of course, substitute their favorite languages; a justfile can contain shebang recipes in multiple languages with no problem.

Recipes can use conditional expressions, take variables from the command line, and much more. The programmer's manual does a fine job of laying out all of just's features and how to use them. There is also a cheatsheet for just syntax and grammar; it is a useful thing to have bookmarked if one starts to use just.

Simple and useful

The project has a Discord channel for discussion and support. There is a GitHub issue that Rodarmor uses to solicit user feedback when he is considering major new features, such as new recipe types. He suggests that users subscribe to that issue if they would like to provide input on features in the future. Sadly, GitHub does not offer RSS feeds for issues, so the only way to keep up with that is to have a GitHub account and use its subscribe feature.

I had heard of just but had not bothered to try it out until I started testing the Project Bluefin distribution in 2023. The project uses just, aliased to ujust, to use the system-wide justfiles for a number of administrative tasks or to toggle user features on and off. For example, Bluefin has "ujust benchmark" to run a one-minute benchmark test with the stress-ng utility, and "ujust setup-luks-tpm-unlock" to enable automatic disk unlocking. The justfiles for the distribution can be found on GitHub and the project also maintains a justfile with recipes for building the distribution images. Those files provide some interesting examples of just's capabilities.

Since then, I've been using just rather than shell scripts and such for automating little things (or medium-sized things...), and converting old scripts to recipes. It's one of the first tools I install on a new system, and it makes everything just a bit easier.

Comments (36 posted)

Page editor: Joe Brockmeier

Inside this week's LWN.net Weekly Edition

  • Briefs: Landlock; Let's Encrypt lifetimes; Last 5.4 kernel; TAB election; AlmaLinux 10.1; FreeBSD 15.0; NixOS 25.11; Django 6.0; Home Assistant 2025.12; PHP 8.5.0; Racket 9.0; Quotes; ...
  • Announcements: Newsletters, conferences, security updates, patches, and more.
Next page: Brief items>>

Copyright © 2025, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds