Subject: guix substitute crashes, instead of printing a useful error message, when the only substitutes on offer use unsupported compression

From: "Zack Weinberg" <[email-masked]>
Date: Sat, 11 Mar 2023 19:55:31 -0500

`narinfo-best-uri` ends with

```
 (match (sort choices (if fast-decompression? (negate speed<?) file-size<?))
    (((uri compression file-size) _ ...)
     (values uri compression file-size)))
```

This will throw an error if the `choices` list is empty.  Nothing catches this error, so, if the choices list should happen to be empty for some package, you will get a giant, unhelpful backtrace:

```
$ guix package -i [PACKAGE]
...
substitute: Backtrace:
substitute: In ice-9/boot-9.scm:
substitute:   1752:10 17 (with-exception-handler _ _ #:unwind? _ # _)
substitute: In unknown file:
substitute:           16 (apply-smob/0 #<thunk 7f93056f32e0>)
substitute: In ice-9/boot-9.scm:
substitute:     724:2 15 (call-with-prompt _ _ #<procedure default-prompt-handle…>)
substitute: In ice-9/eval.scm:
substitute:     619:8 14 (_ #(#(#<directory (guile-user) 7f93056f8c80>)))
substitute: In guix/ui.scm:
substitute:    2275:7 13 (run-guix . _)
substitute:   2238:10 12 (run-guix-command _ . _)
substitute: In ice-9/boot-9.scm:
substitute:   1752:10 11 (with-exception-handler _ _ #:unwind? _ # _)
substitute:   1752:10 10 (with-exception-handler _ _ #:unwind? _ # _)
substitute: In guix/scripts/substitute.scm:
substitute:    842:18  9 (_)
substitute:     352:7  8 (process-query #<output: file 4> _ #:cache-urls _ #:acl _)
substitute: In srfi/srfi-1.scm:
substitute:     634:9  7 (for-each #<procedure 7f9305761040 at guix/scripts/sub…> …)
substitute: In guix/scripts/substitute.scm:
substitute:    297:13  6 (_ #<<narinfo> path: "/gnu/store/…>)
substitute: In guix/narinfo.scm:
substitute:     346:2  5 (narinfo-best-uri _ #:fast-decompression? _)
substitute: In ice-9/boot-9.scm:
substitute:   1685:16  4 (raise-exception _ #:continuable? _)
substitute:   1685:16  3 (raise-exception _ #:continuable? _)
substitute:   1780:13  2 (_ #<&compound-exception components: (#<&error> #<&orig…>)
substitute:   1685:16  1 (raise-exception _ #:continuable? _)
substitute:   1685:16  0 (raise-exception _ #:continuable? _)
substitute:
substitute: ice-9/boot-9.scm:1685:16: In procedure raise-exception:
substitute: Throw to key `match-error' with args `("match" "no matching pattern" ())'.
guix package: error: `/usr/bin/guix substitute' died unexpectedly
```

The strongest clue to what is actually wrong is

```
Throw to key `match-error' with args `("match" "no matching pattern" ())
```

and you have to know that that () at the end of the error message means `match` was passed an empty list.

OK, so when can the choices list be empty for a package?  Simply, when the only substitutes on offer for that package have been compressed with compression methods that the *Guix daemon*'s Guile installation does not support -- for instance, zstd, if the (zstd) module is not available.  narinfo-best-uri intentionally filters those URIs out of the choices list, but is not prepared for the possibility that nothing will be left.

I tripped over this with a somewhat heterodox configuration; a third-party substitute server that only offers zstd-compressed nars, plus Guix as foreign package manager on Debian, with the daemon installed from *Debian's* package, which doesn't have a dependency on guile-zstd (*Guix's* package of guix does depend on guile-zstd).  Still, I feel that the principle "externally supplied data, no matter how malformed, should never cause a crash" applies.

The requested change, then, is for `guix substitute` to *not* crash if the filtered choices list is empty.  Probably narinfo-best-uri should return (values nil nil nil) in that case, and then scripts/substitute.scm should also be changed to error out gracefully and/or fall back to local building when it gets that result from narinfo-best-uri.

zw