publish.scm 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2015 David Thompson <davet@gnu.org>
  3. ;;; Copyright © 2020 by Amar M. Singh <nly@disroot.org>
  4. ;;; Copyright © 2015, 2016, 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
  5. ;;; Copyright © 2020 Maxim Cournoyer <maxim.cournoyer@gmail.com>
  6. ;;; Copyright © 2021 Maxime Devos <maximedevos@telenet.be>
  7. ;;;
  8. ;;; This file is part of GNU Guix.
  9. ;;;
  10. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  11. ;;; under the terms of the GNU General Public License as published by
  12. ;;; the Free Software Foundation; either version 3 of the License, or (at
  13. ;;; your option) any later version.
  14. ;;;
  15. ;;; GNU Guix is distributed in the hope that it will be useful, but
  16. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  17. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. ;;; GNU General Public License for more details.
  19. ;;;
  20. ;;; You should have received a copy of the GNU General Public License
  21. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  22. (define-module (guix scripts publish)
  23. #:use-module ((system repl server) #:prefix repl:)
  24. #:use-module (ice-9 binary-ports)
  25. #:use-module (ice-9 format)
  26. #:use-module (ice-9 match)
  27. #:use-module (ice-9 regex)
  28. #:use-module (ice-9 rdelim)
  29. #:use-module (ice-9 threads)
  30. #:use-module (rnrs bytevectors)
  31. #:use-module (srfi srfi-1)
  32. #:use-module (srfi srfi-2)
  33. #:use-module (srfi srfi-9)
  34. #:use-module (srfi srfi-9 gnu)
  35. #:use-module (srfi srfi-19)
  36. #:use-module (srfi srfi-26)
  37. #:use-module (srfi srfi-34)
  38. #:use-module (srfi srfi-37)
  39. #:use-module (web http)
  40. #:use-module (web request)
  41. #:use-module (web response)
  42. #:use-module (web server)
  43. #:use-module (web uri)
  44. #:autoload (sxml simple) (sxml->xml)
  45. #:autoload (guix avahi) (avahi-publish-service-thread)
  46. #:use-module (guix base32)
  47. #:use-module (guix base64)
  48. #:use-module (guix config)
  49. #:use-module (guix derivations)
  50. #:use-module (gcrypt hash)
  51. #:use-module (guix pki)
  52. #:use-module (gcrypt pk-crypto)
  53. #:use-module (guix workers)
  54. #:use-module (guix store)
  55. #:use-module ((guix serialization) #:select (write-file))
  56. #:use-module (zlib)
  57. #:autoload (lzlib) (call-with-lzip-output-port
  58. make-lzip-output-port)
  59. #:autoload (zstd) (call-with-zstd-output-port
  60. make-zstd-output-port)
  61. #:use-module (guix cache)
  62. #:use-module (guix ui)
  63. #:use-module (guix scripts)
  64. #:use-module ((guix utils)
  65. #:select (with-atomic-file-output compressed-file?))
  66. #:use-module ((guix build utils)
  67. #:select (dump-port mkdir-p find-files))
  68. #:use-module ((guix build syscalls) #:select (set-thread-name))
  69. #:export (%default-gzip-compression
  70. %public-key
  71. %private-key
  72. signed-string
  73. open-server-socket
  74. publish-service-type
  75. run-publish-server
  76. guix-publish
  77. when-bound))
  78. (define (show-help)
  79. (format #t (G_ "Usage: guix publish [OPTION]...
  80. Publish ~a over HTTP.\n") %store-directory)
  81. (display (G_ "
  82. -p, --port=PORT listen on PORT"))
  83. (display (G_ "
  84. --listen=HOST listen on the network interface for HOST"))
  85. (display (G_ "
  86. -u, --user=USER change privileges to USER as soon as possible"))
  87. (display (G_ "
  88. -a, --advertise advertise on the local network"))
  89. (display (G_ "
  90. -C, --compression[=METHOD:LEVEL]
  91. compress archives with METHOD at LEVEL"))
  92. (display (G_ "
  93. -c, --cache=DIRECTORY cache published items to DIRECTORY"))
  94. (display (G_ "
  95. --cache-bypass-threshold=SIZE
  96. serve store items below SIZE even when not cached"))
  97. (display (G_ "
  98. --workers=N use N workers to bake items"))
  99. (display (G_ "
  100. --ttl=TTL announce narinfos can be cached for TTL seconds"))
  101. (display (G_ "
  102. --nar-path=PATH use PATH as the prefix for nar URLs"))
  103. (display (G_ "
  104. --public-key=FILE use FILE as the public key for signatures"))
  105. (display (G_ "
  106. --private-key=FILE use FILE as the private key for signatures"))
  107. (display (G_ "
  108. -r, --repl[=PORT] spawn REPL server on PORT"))
  109. (newline)
  110. (display (G_ "
  111. -h, --help display this help and exit"))
  112. (display (G_ "
  113. -V, --version display version information and exit"))
  114. (newline)
  115. (show-bug-report-information))
  116. ;; When testing, ideally the server isn't explicitly bound to
  117. ;; any particular port, to avoid conflicts with other software,
  118. ;; and to be able "make check" multiple Guix checkouts in parallel.
  119. ;;
  120. ;; While these conflicts won't appear in the build container,
  121. ;; they can still be annoying when a developer is testing
  122. ;; something *outside* a network container.
  123. ;;
  124. ;; When this parameter's value is not false, it is a procedure
  125. ;; accepting the port number the server was (implicitly) bound
  126. ;; to (by the kernel).
  127. (define when-bound (make-parameter #f))
  128. (define (getaddrinfo* host)
  129. "Like 'getaddrinfo', but properly report errors."
  130. (catch 'getaddrinfo-error
  131. (lambda ()
  132. (getaddrinfo host))
  133. (lambda (key error)
  134. (leave (G_ "lookup of host '~a' failed: ~a~%")
  135. host (gai-strerror error)))))
  136. ;; Nar compression parameters.
  137. (define-record-type <compression>
  138. (compression type level)
  139. compression?
  140. (type compression-type)
  141. (level compression-level))
  142. (define %no-compression
  143. (compression 'none 0))
  144. (define %default-gzip-compression
  145. ;; Since we compress on the fly, default to fast compression.
  146. (compression 'gzip 3))
  147. (define (default-compression type)
  148. (compression type 3))
  149. (define (actual-compressions item requested)
  150. "Return the actual compressions used for ITEM, which may be %NO-COMPRESSION
  151. if ITEM is already compressed."
  152. (if (compressed-file? item)
  153. (list %no-compression)
  154. requested))
  155. (define (low-compression c)
  156. "Return <compression> of the same type as C, but optimized for low CPU
  157. usage."
  158. (compression (compression-type c)
  159. (min (compression-level c) 2)))
  160. (define %options
  161. (list (option '(#\h "help") #f #f
  162. (lambda _
  163. (show-help)
  164. (exit 0)))
  165. (option '(#\V "version") #f #f
  166. (lambda _
  167. (show-version-and-exit "guix publish")))
  168. (option '(#\a "advertise") #f #f
  169. (lambda (opt name arg result)
  170. (alist-cons 'advertise? #t result)))
  171. (option '(#\u "user") #t #f
  172. (lambda (opt name arg result)
  173. (alist-cons 'user arg result)))
  174. (option '(#\p "port") #t #f
  175. (lambda (opt name arg result)
  176. (alist-cons 'port (string->number* arg) result)))
  177. (option '("listen") #t #f
  178. (lambda (opt name arg result)
  179. (match (getaddrinfo* arg)
  180. ((info _ ...)
  181. (alist-cons 'address (addrinfo:addr info)
  182. result))
  183. (()
  184. (leave (G_ "lookup of host '~a' returned nothing")
  185. name)))))
  186. (option '(#\C "compression") #f #t
  187. (lambda (opt name arg result)
  188. (let* ((colon (string-index arg #\:))
  189. (type (cond
  190. (colon (string-take arg colon))
  191. ((string->number arg) "gzip")
  192. (else arg)))
  193. (level (if colon
  194. (string->number*
  195. (string-drop arg (+ 1 colon)))
  196. (or (string->number arg) 3))))
  197. (match level
  198. (0
  199. (alist-cons 'compression %no-compression result))
  200. (level
  201. (match (string->compression-type type)
  202. ((? symbol? type)
  203. (alist-cons 'compression
  204. (compression type level)
  205. result))
  206. (_
  207. (warning (G_ "~a: unsupported compression type~%")
  208. type)
  209. result)))))))
  210. (option '(#\c "cache") #t #f
  211. (lambda (opt name arg result)
  212. (alist-cons 'cache arg result)))
  213. (option '("cache-bypass-threshold") #t #f
  214. (lambda (opt name arg result)
  215. (alist-cons 'cache-bypass-threshold (size->number arg)
  216. result)))
  217. (option '("workers") #t #f
  218. (lambda (opt name arg result)
  219. (alist-cons 'workers (string->number* arg)
  220. result)))
  221. (option '("ttl") #t #f
  222. (lambda (opt name arg result)
  223. (let ((duration (string->duration arg)))
  224. (unless duration
  225. (leave (G_ "~a: invalid duration~%") arg))
  226. (alist-cons 'narinfo-ttl (time-second duration)
  227. result))))
  228. (option '("nar-path") #t #f
  229. (lambda (opt name arg result)
  230. (alist-cons 'nar-path arg result)))
  231. (option '("public-key") #t #f
  232. (lambda (opt name arg result)
  233. (alist-cons 'public-key-file arg result)))
  234. (option '("private-key" "secret-key") #t #f
  235. (lambda (opt name arg result)
  236. (alist-cons 'private-key-file arg result)))
  237. (option '(#\r "repl") #f #t
  238. (lambda (opt name arg result)
  239. ;; If port unspecified, use default Guile REPL port.
  240. (let ((port (and arg (string->number* arg))))
  241. (alist-cons 'repl (or port 37146) result))))))
  242. (define %default-options
  243. `((port . 8080)
  244. ;; By default, serve nars under "/nar".
  245. (nar-path . "nar")
  246. (public-key-file . ,%public-key-file)
  247. (private-key-file . ,%private-key-file)
  248. ;; Default number of workers when caching is enabled.
  249. (workers . ,(current-processor-count))
  250. (address . ,(make-socket-address AF_INET INADDR_ANY 0))
  251. (repl . #f)))
  252. ;; The key pair used to sign narinfos.
  253. (define %private-key
  254. (make-parameter #f))
  255. (define %public-key
  256. (make-parameter #f))
  257. (define %nix-cache-info
  258. `(("StoreDir" . ,%store-directory)
  259. ("WantMassQuery" . 0)
  260. ("Priority" . 100)))
  261. ;;; A common buffer size value used for the TCP socket SO_SNDBUF option and
  262. ;;; the gzip compressor buffer size.
  263. (define %default-buffer-size
  264. (* 208 1024))
  265. (define %default-socket-options
  266. ;; List of options passed to 'setsockopt' when transmitting files.
  267. (list (list SO_SNDBUF %default-buffer-size)))
  268. (define* (configure-socket socket #:key (level SOL_SOCKET)
  269. (options %default-socket-options))
  270. "Apply multiple option tuples in OPTIONS to SOCKET, using LEVEL."
  271. (for-each (cut apply setsockopt socket level <>)
  272. options))
  273. (define (signed-string s)
  274. "Sign the hash of the string S with the daemon's key. Return a canonical
  275. sexp for the signature."
  276. (let* ((public-key (%public-key))
  277. (hash (bytevector->hash-data (sha256 (string->utf8 s))
  278. #:key-type (key-type public-key))))
  279. (signature-sexp hash (%private-key) public-key)))
  280. (define base64-encode-string
  281. (compose base64-encode string->utf8))
  282. (define* (store-item->recutils store-item
  283. #:key
  284. (nar-path "nar")
  285. (compression %no-compression)
  286. file-size)
  287. "Return the 'Compression' and 'URL' fields of the narinfo for STORE-ITEM,
  288. with COMPRESSION, starting at NAR-PATH."
  289. (let ((url (encode-and-join-uri-path
  290. `(,@(split-and-decode-uri-path nar-path)
  291. ,@(match compression
  292. (($ <compression> 'none)
  293. '())
  294. (($ <compression> type)
  295. (list (symbol->string type))))
  296. ,(basename store-item)))))
  297. (format #f "URL: ~a~%Compression: ~a~%~@[FileSize: ~a~%~]"
  298. url (compression-type compression) file-size)))
  299. (define* (narinfo-string store store-path key
  300. #:key (compressions (list %no-compression))
  301. (nar-path "nar") (file-sizes '()))
  302. "Generate a narinfo key/value string for STORE-PATH; an exception is raised
  303. if STORE-PATH is invalid. Produce a URL that corresponds to COMPRESSION. The
  304. narinfo is signed with KEY. NAR-PATH specifies the prefix for nar URLs.
  305. Optionally, FILE-SIZES is a list of compression/integer pairs, where the
  306. integer is size in bytes of the compressed NAR; it informs the client of how
  307. much needs to be downloaded."
  308. (let* ((path-info (query-path-info store store-path))
  309. (compressions (actual-compressions store-path compressions))
  310. (hash (bytevector->nix-base32-string
  311. (path-info-hash path-info)))
  312. (size (path-info-nar-size path-info))
  313. (file-sizes `((,%no-compression . ,size) ,@file-sizes))
  314. (references (string-join
  315. (map basename (path-info-references path-info))
  316. " "))
  317. (deriver (path-info-deriver path-info))
  318. (base-info (format #f
  319. "\
  320. StorePath: ~a
  321. ~{~a~}\
  322. NarHash: sha256:~a
  323. NarSize: ~d
  324. References: ~a~%"
  325. store-path
  326. (map (lambda (compression)
  327. (let ((size (assoc-ref file-sizes
  328. compression)))
  329. (store-item->recutils store-path
  330. #:file-size size
  331. #:nar-path nar-path
  332. #:compression
  333. compression)))
  334. compressions)
  335. hash size references))
  336. ;; Do not render a "Deriver" or "System" line if we are rendering
  337. ;; info for a derivation.
  338. (info (if (not deriver)
  339. base-info
  340. (catch 'system-error
  341. (lambda ()
  342. (let ((drv (read-derivation-from-file deriver)))
  343. (format #f "~aSystem: ~a~%Deriver: ~a~%"
  344. base-info (derivation-system drv)
  345. (basename deriver))))
  346. (lambda args
  347. ;; DERIVER might be missing, but that's fine:
  348. ;; it's only used for <substitutable> where it's
  349. ;; optional. 'System' is currently unused.
  350. (if (= ENOENT (system-error-errno args))
  351. base-info
  352. (apply throw args))))))
  353. (signature (base64-encode-string
  354. (canonical-sexp->string (signed-string info)))))
  355. (format #f "~aSignature: 1;~a;~a~%" info (gethostname) signature)))
  356. (define* (not-found request
  357. #:key (phrase "Resource not found")
  358. ttl)
  359. "Render 404 response for REQUEST."
  360. (values (build-response #:code 404
  361. #:headers (if ttl
  362. `((cache-control (max-age . ,ttl)))
  363. '()))
  364. (string-append phrase ": "
  365. (uri-path (request-uri request)))))
  366. (define (render-nix-cache-info)
  367. "Render server information."
  368. (values '((content-type . (text/plain)))
  369. (lambda (port)
  370. (for-each (match-lambda
  371. ((key . value)
  372. (format port "~a: ~a~%" key value)))
  373. %nix-cache-info))))
  374. (define* (render-narinfo store request hash
  375. #:key ttl (compressions (list %no-compression))
  376. (nar-path "nar"))
  377. "Render metadata for the store path corresponding to HASH. If TTL is true,
  378. advertise it as the maximum validity period (in seconds) via the
  379. 'Cache-Control' header. This allows 'guix substitute' to cache it for an
  380. appropriate duration. NAR-PATH specifies the prefix for nar URLs."
  381. (let ((store-path (hash-part->path store hash)))
  382. (if (string-null? store-path)
  383. (not-found request #:phrase "")
  384. (values `((content-type . (application/x-nix-narinfo))
  385. ,@(if ttl
  386. `((cache-control (max-age . ,ttl)))
  387. '()))
  388. (cut display
  389. (narinfo-string store store-path (%private-key)
  390. #:nar-path nar-path
  391. #:compressions compressions)
  392. <>)))))
  393. (define* (nar-cache-file directory item
  394. #:key (compression %no-compression))
  395. (string-append directory "/"
  396. (symbol->string (compression-type compression))
  397. "/" (basename item) ".nar"))
  398. (define* (narinfo-cache-file directory item
  399. #:key (compression %no-compression))
  400. (string-append directory "/"
  401. (symbol->string (compression-type compression))
  402. "/" (basename item)
  403. ".narinfo"))
  404. (define (hash-part-mapping-cache-file directory hash)
  405. (string-append directory "/hashes/" hash))
  406. (define run-single-baker
  407. (let ((baking (make-weak-value-hash-table))
  408. (mutex (make-mutex)))
  409. (lambda (item thunk)
  410. "Run THUNK, which is supposed to bake ITEM, but make sure only one
  411. thread is baking ITEM at a given time."
  412. (define selected?
  413. (with-mutex mutex
  414. (and (not (hash-ref baking item))
  415. (begin
  416. (hash-set! baking item (current-thread))
  417. #t))))
  418. (when selected?
  419. (dynamic-wind
  420. (const #t)
  421. thunk
  422. (lambda ()
  423. (with-mutex mutex
  424. (hash-remove! baking item))))))))
  425. (define-syntax-rule (single-baker item exp ...)
  426. "Bake ITEM by evaluating EXP, but make sure there's only one baker for ITEM
  427. at a time."
  428. (run-single-baker item (lambda () exp ...)))
  429. (define (narinfo-files cache)
  430. "Return the list of .narinfo files under CACHE."
  431. (if (file-is-directory? cache)
  432. (find-files cache
  433. (lambda (file stat)
  434. (string-suffix? ".narinfo" file)))
  435. '()))
  436. (define (nar-expiration-time ttl)
  437. "Return the narinfo expiration time (in seconds since the Epoch). The
  438. expiration time is +inf.0 when passed an item that is still in the store; in
  439. other cases, it is the last-access time of the item plus TTL.
  440. This policy allows us to keep cached nars that correspond to valid store
  441. items. Failing that, we could eventually have to recompute them and return
  442. 404 in the meantime."
  443. (let ((expiration-time (file-expiration-time ttl)))
  444. (lambda (file)
  445. (let ((item (string-append (%store-prefix) "/"
  446. (basename file ".narinfo"))))
  447. ;; Note: We don't need to use 'valid-path?' here because FILE would
  448. ;; not exist if ITEM were not valid in the first place.
  449. (if (file-exists? item)
  450. +inf.0
  451. (expiration-time file))))))
  452. (define (hash-part->path* store hash cache)
  453. "Like 'hash-part->path' but cache results under CACHE. This ensures we can
  454. still map HASH to the corresponding store file name, even if said store item
  455. vanished from the store in the meantime."
  456. (let ((cached (hash-part-mapping-cache-file cache hash)))
  457. (catch 'system-error
  458. (lambda ()
  459. (call-with-input-file cached read-string))
  460. (lambda args
  461. (if (= ENOENT (system-error-errno args))
  462. (match (hash-part->path store hash)
  463. ("" "")
  464. (result
  465. (mkdir-p (dirname cached))
  466. (call-with-output-file (string-append cached ".tmp")
  467. (lambda (port)
  468. (display result port)))
  469. (rename-file (string-append cached ".tmp") cached)
  470. result))
  471. (apply throw args))))))
  472. (define cache-bypass-threshold
  473. ;; Maximum size of a store item that may be served by the '/cached' handlers
  474. ;; below even when not in cache.
  475. (make-parameter (* 10 (expt 2 20))))
  476. (define (bypass-cache? store item)
  477. "Return true if we allow ITEM to be downloaded before it is cached. ITEM is
  478. interpreted as the basename of a store item."
  479. (guard (c ((store-error? c) #f))
  480. (< (path-info-nar-size (query-path-info store item))
  481. (cache-bypass-threshold))))
  482. (define* (render-narinfo/cached store request hash
  483. #:key ttl (compressions (list %no-compression))
  484. (nar-path "nar")
  485. cache pool)
  486. "Respond to the narinfo request for REQUEST. If the narinfo is available in
  487. CACHE, then send it; otherwise, return 404 and \"bake\" that nar and narinfo
  488. requested using POOL."
  489. (define (delete-entry narinfo)
  490. ;; Delete NARINFO and the corresponding nar from CACHE.
  491. (let* ((nar (string-append (string-drop-right narinfo
  492. (string-length ".narinfo"))
  493. ".nar"))
  494. (base (basename narinfo ".narinfo"))
  495. (hash (string-take base (string-index base #\-)))
  496. (mapping (hash-part-mapping-cache-file cache hash)))
  497. (delete-file* narinfo)
  498. (delete-file* nar)
  499. (delete-file* mapping)))
  500. (let* ((item (hash-part->path* store hash cache))
  501. (compressions (actual-compressions item compressions))
  502. (cached (and (not (string-null? item))
  503. (narinfo-cache-file cache item
  504. #:compression
  505. (first compressions)))))
  506. (cond ((string-null? item)
  507. (not-found request))
  508. ((file-exists? cached)
  509. ;; Narinfo is in cache, send it.
  510. (values `((content-type . (application/x-nix-narinfo))
  511. ,@(if ttl
  512. `((cache-control (max-age . ,ttl)))
  513. '()))
  514. (lambda (port)
  515. (display (call-with-input-file cached
  516. read-string)
  517. port))))
  518. ((and (file-exists? item) ;cheaper than the 'valid-path?' RPC
  519. (valid-path? store item))
  520. ;; Nothing in cache: bake the narinfo and nar in the background and
  521. ;; return 404.
  522. (eventually pool
  523. (single-baker item
  524. ;; Check whether CACHED has been produced in the meantime.
  525. (unless (file-exists? cached)
  526. ;; (format #t "baking ~s~%" item)
  527. (bake-narinfo+nar cache item
  528. #:ttl ttl
  529. #:compressions compressions
  530. #:nar-path nar-path)))
  531. (when ttl
  532. (single-baker 'cache-cleanup
  533. (maybe-remove-expired-cache-entries cache
  534. narinfo-files
  535. #:entry-expiration
  536. (nar-expiration-time ttl)
  537. #:delete-entry delete-entry
  538. #:cleanup-period ttl))))
  539. ;; If ITEM passes 'bypass-cache?', render a temporary narinfo right
  540. ;; away, with a short TTL. The narinfo is temporary because it
  541. ;; lacks 'FileSize', for instance, which the cached narinfo will
  542. ;; have. Chances are that the nar will be baked by the time the
  543. ;; client asks for it.
  544. (if (bypass-cache? store item)
  545. (render-narinfo store request hash
  546. #:ttl 300 ;temporary
  547. #:nar-path nar-path
  548. #:compressions compressions)
  549. (not-found request
  550. #:phrase "We're baking it"
  551. #:ttl 300))) ;should be available within 5m
  552. (else
  553. (not-found request #:phrase "")))))
  554. (define (compress-nar cache item compression)
  555. "Save in directory CACHE the nar for ITEM compressed with COMPRESSION."
  556. (define nar
  557. (nar-cache-file cache item #:compression compression))
  558. (define (write-compressed-file call-with-compressed-output-port)
  559. ;; Note: the file port gets closed along with the compressed port.
  560. (call-with-compressed-output-port (open-output-file (string-append nar ".tmp"))
  561. (lambda (port)
  562. (write-file item port))
  563. #:level (compression-level compression))
  564. (rename-file (string-append nar ".tmp") nar))
  565. (mkdir-p (dirname nar))
  566. (match (compression-type compression)
  567. ('gzip
  568. (write-compressed-file call-with-gzip-output-port))
  569. ('lzip
  570. (write-compressed-file call-with-lzip-output-port))
  571. ('zstd
  572. (write-compressed-file call-with-zstd-output-port))
  573. ('none
  574. ;; Cache nars even when compression is disabled so that we can
  575. ;; guarantee the TTL (see <https://bugs.gnu.org/28664>.)
  576. (with-atomic-file-output nar
  577. (lambda (port)
  578. (write-file item port)
  579. ;; Make the file world-readable, contrary to what
  580. ;; 'with-atomic-file-output' does.
  581. (chmod port (logand #o644 (lognot (umask)))))))))
  582. (define* (bake-narinfo+nar cache item
  583. #:key ttl (compressions (list %no-compression))
  584. (nar-path "/nar"))
  585. "Write the narinfo and nar for ITEM to CACHE."
  586. (define (compressed-nar-size compression)
  587. (let* ((nar (nar-cache-file cache item #:compression compression))
  588. (stat (stat nar #f)))
  589. (and stat
  590. (cons compression (stat:size stat)))))
  591. (let ((compression (actual-compressions item compressions)))
  592. (for-each (cut compress-nar cache item <>) compressions)
  593. (match compressions
  594. ((main others ...)
  595. (let ((narinfo (narinfo-cache-file cache item
  596. #:compression main)))
  597. (with-atomic-file-output narinfo
  598. (lambda (port)
  599. ;; Open a new connection to the store. We cannot reuse the main
  600. ;; thread's connection to the store since we would end up sending
  601. ;; stuff concurrently on the same channel.
  602. (with-store store
  603. (let ((sizes (filter-map compressed-nar-size compression)))
  604. (display (narinfo-string store item
  605. (%private-key)
  606. #:nar-path nar-path
  607. #:compressions compressions
  608. #:file-sizes sizes)
  609. port)))
  610. ;; Make the cached narinfo world-readable, contrary to what
  611. ;; 'with-atomic-file-output' does, so that other users can rsync
  612. ;; the whole cache.
  613. (chmod port (logand #o644 (lognot (umask))))))
  614. ;; Make narinfo files for OTHERS hard links to NARINFO such that the
  615. ;; atime-based cache eviction considers either all the nars or none
  616. ;; of them as candidates.
  617. (for-each (lambda (other)
  618. (let ((other (narinfo-cache-file cache item
  619. #:compression other)))
  620. (link narinfo other)))
  621. others))))))
  622. ;; XXX: Declare the 'X-Nar-Compression' HTTP header, which is in fact for
  623. ;; internal consumption: it allows us to pass the compression info to
  624. ;; 'http-write', as part of the workaround to <http://bugs.gnu.org/21093>.
  625. (declare-header! "X-Nar-Compression"
  626. (lambda (str)
  627. (match (call-with-input-string str read)
  628. (('compression type level)
  629. (compression type level))))
  630. compression?
  631. (lambda (compression port)
  632. (match compression
  633. (($ <compression> type level)
  634. (write `(compression ,type ,level) port)))))
  635. (define* (render-nar store request store-item
  636. #:key (compression %no-compression))
  637. "Render archive of the store path corresponding to STORE-ITEM."
  638. (let ((store-path (string-append %store-directory "/" store-item)))
  639. ;; The ISO-8859-1 charset *must* be used otherwise HTTP clients will
  640. ;; interpret the byte stream as UTF-8 and arbitrarily change invalid byte
  641. ;; sequences.
  642. (if (valid-path? store store-path)
  643. (values `((content-type . (application/x-nix-archive
  644. (charset . "ISO-8859-1")))
  645. (x-nar-compression . ,compression))
  646. ;; XXX: We're not returning the actual contents, deferring
  647. ;; instead to 'http-write'. This is a hack to work around
  648. ;; <http://bugs.gnu.org/21093>.
  649. store-path)
  650. (not-found request))))
  651. (define* (render-nar/cached store cache request store-item
  652. #:key ttl (compression %no-compression))
  653. "Respond to REQUEST with a nar for STORE-ITEM. If the nar is in CACHE,
  654. return it; otherwise, return 404. When TTL is true, use it as the
  655. 'Cache-Control' expiration time."
  656. (let ((cached (nar-cache-file cache store-item
  657. #:compression compression)))
  658. (cond ((file-exists? cached)
  659. (values `((content-type . (application/octet-stream
  660. (charset . "ISO-8859-1")))
  661. ,@(if ttl
  662. `((cache-control (max-age . ,ttl)))
  663. '())
  664. ;; XXX: We're not returning the actual contents, deferring
  665. ;; instead to 'http-write'. This is a hack to work around
  666. ;; <http://bugs.gnu.org/21093>.
  667. (x-raw-file . ,cached))
  668. #f))
  669. ((let* ((hash (and=> (string-index store-item #\-)
  670. (cut string-take store-item <>)))
  671. (item (and hash
  672. (guard (c ((store-error? c) #f))
  673. (hash-part->path store hash)))))
  674. (and item (not (string-null? item))
  675. (bypass-cache? store item)))
  676. ;; Render STORE-ITEM live. We reach this because STORE-ITEM is
  677. ;; being baked but clients are already asking for it. Thus, we're
  678. ;; duplicating work, but doing so allows us to reduce delays.
  679. (render-nar store request store-item
  680. #:compression (low-compression compression)))
  681. (else
  682. (not-found request)))))
  683. (define (render-content-addressed-file store request
  684. name algo hash)
  685. "Return the content of the result of the fixed-output derivation NAME that
  686. has the given HASH of type ALGO."
  687. ;; TODO: Support other hash algorithms.
  688. (if (and (eq? algo 'sha256) (= 32 (bytevector-length hash)))
  689. (let ((item (fixed-output-path name hash
  690. #:hash-algo algo
  691. #:recursive? #f)))
  692. (if (valid-path? store item)
  693. (values `((content-type . (application/octet-stream
  694. (charset . "ISO-8859-1")))
  695. ;; XXX: We're not returning the actual contents,
  696. ;; deferring instead to 'http-write'. This is a hack to
  697. ;; work around <http://bugs.gnu.org/21093>.
  698. (x-raw-file . ,item))
  699. #f)
  700. (not-found request)))
  701. (not-found request)))
  702. (define (render-log-file store request name)
  703. "Render the log file for NAME, the base name of a store item. Don't attempt
  704. to compress or decompress the log file; just return it as-is."
  705. (define (response-headers file)
  706. ;; XXX: We're not returning the actual contents, deferring instead to
  707. ;; 'http-write'. This is a hack to work around
  708. ;; <http://bugs.gnu.org/21093>.
  709. (cond ((string-suffix? ".gz" file)
  710. `((content-type . (text/plain (charset . "UTF-8")))
  711. (content-encoding . (gzip))
  712. (x-raw-file . ,file)))
  713. ((string-suffix? ".bz2" file)
  714. `((content-type . (application/x-bzip2
  715. (charset . "ISO-8859-1")))
  716. (x-raw-file . ,file)))
  717. (else ;uncompressed
  718. `((content-type . (text/plain (charset . "UTF-8")))
  719. (x-raw-file . ,file)))))
  720. (let ((log (log-file store
  721. (string-append (%store-prefix) "/" name))))
  722. (if log
  723. (values (response-headers log) log)
  724. (not-found request))))
  725. (define (render-signing-key)
  726. "Render signing key."
  727. (let ((file %public-key-file))
  728. (values `((content-type . (text/plain (charset . "UTF-8")))
  729. (x-raw-file . ,file))
  730. file)))
  731. (define (render-home-page request)
  732. "Render the home page."
  733. (values `((content-type . (text/html (charset . "UTF-8"))))
  734. (call-with-output-string
  735. (lambda (port)
  736. (sxml->xml '(html
  737. (head (title "GNU Guix Substitute Server"))
  738. (body
  739. (h1 "GNU Guix Substitute Server")
  740. (p "Hi, "
  741. (a (@ (href
  742. "https://guix.gnu.org/manual/en/html_node/Invoking-guix-publish.html"))
  743. (tt "guix publish"))
  744. " speaking. Welcome!")
  745. (p "Here is the "
  746. (a (@ (href
  747. "signing-key.pub"))
  748. (tt "signing key"))
  749. " for this server. Knock yourselves out!")))
  750. port)))))
  751. (define (extract-narinfo-hash str)
  752. "Return the hash within the narinfo resource string STR, or false if STR
  753. is invalid."
  754. (and (string-suffix? ".narinfo" str)
  755. (let ((base (string-drop-right str 8)))
  756. (and (string-every %nix-base32-charset base)
  757. base))))
  758. (define (get-request? request)
  759. "Return #t if REQUEST uses the GET method."
  760. (eq? (request-method request) 'GET))
  761. (define (request-path-components request)
  762. "Split the URI path of REQUEST into a list of component strings. For
  763. example: \"/foo/bar\" yields '(\"foo\" \"bar\")."
  764. (split-and-decode-uri-path (uri-path (request-uri request))))
  765. ;;;
  766. ;;; Server.
  767. ;;;
  768. (define %http-write
  769. (@@ (web server http) http-write))
  770. (define (strip-headers response)
  771. "Return RESPONSE's headers minus 'Content-Length' and our internal headers."
  772. (fold alist-delete
  773. (response-headers response)
  774. '(content-length x-raw-file x-nar-compression)))
  775. (define (sans-content-length response)
  776. "Return RESPONSE without its 'content-length' header."
  777. (set-field response (response-headers)
  778. (strip-headers response)))
  779. (define (with-content-length response length)
  780. "Return RESPONSE with a 'content-length' header set to LENGTH."
  781. (set-field response (response-headers)
  782. (alist-cons 'content-length length
  783. (strip-headers response))))
  784. (define-syntax-rule (swallow-EPIPE exp ...)
  785. "Swallow EPIPE errors raised by EXP..."
  786. (catch 'system-error
  787. (lambda ()
  788. exp ...)
  789. (lambda args
  790. (if (= EPIPE (system-error-errno args))
  791. (values)
  792. (apply throw args)))))
  793. (define-syntax-rule (swallow-zlib-error exp ...)
  794. "Swallow 'zlib-error' exceptions raised by EXP..."
  795. (catch 'zlib-error
  796. (lambda ()
  797. exp ...)
  798. (const #f)))
  799. (define (nar-response-port response compression)
  800. "Return a port on which to write the body of RESPONSE, the response of a
  801. /nar request, according to COMPRESSION."
  802. (match compression
  803. (($ <compression> 'gzip level)
  804. ;; Note: We cannot used chunked encoding here because
  805. ;; 'make-gzip-output-port' wants a file port.
  806. (make-gzip-output-port (response-port response)
  807. #:level level
  808. #:buffer-size %default-buffer-size))
  809. (($ <compression> 'lzip level)
  810. (make-lzip-output-port (response-port response)
  811. #:level level))
  812. (($ <compression> 'zstd level)
  813. (make-zstd-output-port (response-port response)
  814. #:level level))
  815. (($ <compression> 'none)
  816. (response-port response))
  817. (#f
  818. (response-port response))))
  819. (define (http-write server client response body)
  820. "Write RESPONSE and BODY to CLIENT, possibly in a separate thread to avoid
  821. blocking."
  822. (match (response-content-type response)
  823. (('application/x-nix-archive . _)
  824. ;; Sending the the whole archive can take time so do it in a separate
  825. ;; thread so that the main thread can keep working in the meantime.
  826. (call-with-new-thread
  827. (lambda ()
  828. (set-thread-name "publish nar")
  829. (let* ((compression (assoc-ref (response-headers response)
  830. 'x-nar-compression))
  831. (response (write-response (sans-content-length response)
  832. client))
  833. (port (begin
  834. (force-output client)
  835. (configure-socket client)
  836. (nar-response-port response compression))))
  837. ;; XXX: Given our ugly workaround for <http://bugs.gnu.org/21093> in
  838. ;; 'render-nar', BODY here is just the file name of the store item.
  839. ;; We call 'write-file' from here because we know that's the only
  840. ;; way to avoid building the whole nar in memory, which could
  841. ;; quickly become a real problem. As a bonus, we even do
  842. ;; sendfile(2) directly from the store files to the socket.
  843. (swallow-zlib-error
  844. (swallow-EPIPE
  845. (write-file (utf8->string body) port)))
  846. (swallow-zlib-error
  847. (close-port port))
  848. (values)))))
  849. (_
  850. (match (assoc-ref (response-headers response) 'x-raw-file)
  851. ((? string? file)
  852. ;; Send a raw file in a separate thread.
  853. (call-with-new-thread
  854. (lambda ()
  855. (set-thread-name "publish file")
  856. (catch 'system-error
  857. (lambda ()
  858. (call-with-input-file file
  859. (lambda (input)
  860. (let* ((size (stat:size (stat input)))
  861. (response (write-response (with-content-length response
  862. size)
  863. client))
  864. (output (response-port response)))
  865. (configure-socket client)
  866. (if (file-port? output)
  867. (sendfile output input size)
  868. (dump-port input output))
  869. (close-port output)
  870. (values)))))
  871. (lambda args
  872. ;; If the file was GC'd behind our back, that's fine. Likewise if
  873. ;; the client closes the connection.
  874. (unless (memv (system-error-errno args)
  875. (list ENOENT EPIPE ECONNRESET))
  876. (apply throw args))
  877. (values))))))
  878. (#f
  879. ;; Handle other responses sequentially.
  880. (%http-write server client response body))))))
  881. (define-server-impl concurrent-http-server
  882. ;; A variant of Guile's built-in HTTP server that offloads possibly long
  883. ;; responses to a different thread.
  884. (@@ (web server http) http-open)
  885. (@@ (web server http) http-read)
  886. http-write
  887. (@@ (web server http) http-close))
  888. (define (string->compression-type string)
  889. "Return a symbol denoting the compression method expressed by STRING; return
  890. #f if STRING doesn't match any supported method."
  891. (match string
  892. ("gzip" 'gzip)
  893. ("lzip" 'lzip)
  894. ("zstd" 'zstd)
  895. (_ #f)))
  896. (define (effective-compression requested-type compressions)
  897. "Given the REQUESTED-TYPE for compression and the set of chosen COMPRESSION
  898. methods, return the applicable compression."
  899. (or (find (match-lambda
  900. (($ <compression> type)
  901. (and (eq? type requested-type)
  902. compression)))
  903. compressions)
  904. (default-compression requested-type)))
  905. (define* (make-request-handler store
  906. #:key
  907. cache pool
  908. narinfo-ttl
  909. (nar-path "nar")
  910. (compressions (list %no-compression)))
  911. (define compression-type?
  912. string->compression-type)
  913. (define nar-path?
  914. (let ((expected (split-and-decode-uri-path nar-path)))
  915. (cut equal? expected <>)))
  916. (lambda (request body)
  917. (format #t "~a ~a~%"
  918. (request-method request)
  919. (uri-path (request-uri request)))
  920. (if (get-request? request) ;reject POST, PUT, etc.
  921. (match (request-path-components request)
  922. ;; /nix-cache-info
  923. (("nix-cache-info")
  924. (render-nix-cache-info))
  925. ;; /
  926. ((or () ("index.html"))
  927. (render-home-page request))
  928. ;; guix signing-key
  929. (("signing-key.pub")
  930. (render-signing-key))
  931. ;; /<hash>.narinfo
  932. (((= extract-narinfo-hash (? string? hash)))
  933. (if cache
  934. (render-narinfo/cached store request hash
  935. #:cache cache
  936. #:pool pool
  937. #:ttl narinfo-ttl
  938. #:nar-path nar-path
  939. #:compressions compressions)
  940. (render-narinfo store request hash
  941. #:ttl narinfo-ttl
  942. #:nar-path nar-path
  943. #:compressions compressions)))
  944. ;; /nar/file/NAME/sha256/HASH
  945. (("file" name "sha256" hash)
  946. (guard (c ((invalid-base32-character? c)
  947. (not-found request)))
  948. (let ((hash (nix-base32-string->bytevector hash)))
  949. (render-content-addressed-file store request
  950. name 'sha256 hash))))
  951. ;; /log/OUTPUT
  952. (("log" name)
  953. (render-log-file store request name))
  954. ;; Use different URLs depending on the compression type. This
  955. ;; guarantees that /nar URLs remain valid even when 'guix publish'
  956. ;; is restarted with different compression parameters.
  957. ;; /nar/gzip/<store-item>
  958. ((components ... (? compression-type? type) store-item)
  959. (if (nar-path? components)
  960. (let* ((compression-type (string->compression-type type))
  961. (compression (effective-compression compression-type
  962. compressions)))
  963. (if cache
  964. (render-nar/cached store cache request store-item
  965. #:ttl narinfo-ttl
  966. #:compression compression)
  967. (render-nar store request store-item
  968. #:compression compression)))
  969. (not-found request)))
  970. ;; /nar/<store-item>
  971. ((components ... store-item)
  972. (if (nar-path? components)
  973. (if cache
  974. (render-nar/cached store cache request store-item
  975. #:ttl narinfo-ttl
  976. #:compression %no-compression)
  977. (render-nar store request store-item
  978. #:compression %no-compression))
  979. (not-found request)))
  980. (x (not-found request)))
  981. (not-found request))))
  982. (define (service-name)
  983. "Return the Avahi service name of the server."
  984. (string-append "guix-publish-" (gethostname)))
  985. (define publish-service-type
  986. ;; Return the Avahi service type of the server.
  987. "_guix_publish._tcp")
  988. (define* (run-publish-server socket store
  989. #:key
  990. advertise? port
  991. (compressions (list %no-compression))
  992. (nar-path "nar") narinfo-ttl
  993. cache pool)
  994. (when advertise?
  995. (let ((name (service-name)))
  996. ;; XXX: Use a callback from Guile-Avahi here, as Avahi can pick a
  997. ;; different name to avoid name clashes.
  998. (info (G_ "Advertising ~a~%.") name)
  999. (avahi-publish-service-thread name
  1000. #:type publish-service-type
  1001. #:port port)))
  1002. (run-server (make-request-handler store
  1003. #:cache cache
  1004. #:pool pool
  1005. #:nar-path nar-path
  1006. #:narinfo-ttl narinfo-ttl
  1007. #:compressions compressions)
  1008. concurrent-http-server
  1009. `(#:socket ,socket)))
  1010. (define (open-server-socket address)
  1011. "Return a TCP socket bound to ADDRESS, a socket address."
  1012. (let ((sock (socket (sockaddr:fam address) SOCK_STREAM 0)))
  1013. (configure-socket sock #:options (cons (list SO_REUSEADDR 1)
  1014. %default-socket-options))
  1015. (bind sock address)
  1016. sock))
  1017. (define (gather-user-privileges user)
  1018. "Switch to the identity of USER, a user name."
  1019. (catch 'misc-error
  1020. (lambda ()
  1021. (let ((user (getpw user)))
  1022. (setgroups #())
  1023. (setgid (passwd:gid user))
  1024. (setuid (passwd:uid user))))
  1025. (lambda (key proc message args . rest)
  1026. (leave (G_ "user '~a' not found: ~a~%")
  1027. user (apply format #f message args)))))
  1028. ;;;
  1029. ;;; Entry point.
  1030. ;;;
  1031. (define-command (guix-publish . args)
  1032. (category packaging)
  1033. (synopsis "publish build results over HTTP")
  1034. (with-error-handling
  1035. (let* ((opts (args-fold* args %options
  1036. (lambda (opt name arg result)
  1037. (leave (G_ "~A: unrecognized option~%") name))
  1038. (lambda (arg result)
  1039. (leave (G_ "~A: extraneous argument~%") arg))
  1040. %default-options))
  1041. (advertise? (assoc-ref opts 'advertise?))
  1042. (user (assoc-ref opts 'user))
  1043. (requested-port (assoc-ref opts 'port))
  1044. (ttl (assoc-ref opts 'narinfo-ttl))
  1045. (compressions (match (filter-map (match-lambda
  1046. (('compression . compression)
  1047. compression)
  1048. (_ #f))
  1049. opts)
  1050. (()
  1051. ;; Default to fast & low compression.
  1052. (list %default-gzip-compression))
  1053. (lst (reverse lst))))
  1054. (address (let ((addr (assoc-ref opts 'address)))
  1055. (make-socket-address (sockaddr:fam addr)
  1056. (sockaddr:addr addr)
  1057. requested-port)))
  1058. (socket (open-server-socket address))
  1059. ;; If requested-port = 0, then the kernel
  1060. ;; will automatically assign a free port number.
  1061. (port (if (= 0 requested-port)
  1062. (sockaddr:port (getsockname socket))
  1063. requested-port))
  1064. (nar-path (assoc-ref opts 'nar-path))
  1065. (repl-port (assoc-ref opts 'repl))
  1066. (cache (assoc-ref opts 'cache))
  1067. (workers (assoc-ref opts 'workers))
  1068. ;; Read the key right away so that (1) we fail early on if we can't
  1069. ;; access them, and (2) we can then drop privileges.
  1070. (public-key (read-file-sexp (assoc-ref opts 'public-key-file)))
  1071. (private-key (read-file-sexp (assoc-ref opts 'private-key-file))))
  1072. ;; Inform tests/publish.scm about the port number used.
  1073. (let ((proc (when-bound)))
  1074. (when proc (proc port)))
  1075. (when user
  1076. ;; Now that we've read the key material and opened the socket, we can
  1077. ;; drop privileges.
  1078. (gather-user-privileges user))
  1079. (when (zero? (getuid))
  1080. (warning (G_ "server running as root; \
  1081. consider using the '--user' option!~%")))
  1082. (parameterize ((%public-key public-key)
  1083. (%private-key private-key)
  1084. (cache-bypass-threshold
  1085. (or (assoc-ref opts 'cache-bypass-threshold)
  1086. (cache-bypass-threshold))))
  1087. (info (G_ "publishing ~a on ~a, port ~d~%")
  1088. %store-directory
  1089. (inet-ntop (sockaddr:fam address) (sockaddr:addr address)) port)
  1090. (for-each (lambda (compression)
  1091. (info (G_ "using '~a' compression method, level ~a~%")
  1092. (compression-type compression)
  1093. (compression-level compression)))
  1094. compressions)
  1095. (when repl-port
  1096. (repl:spawn-server (repl:make-tcp-server-socket #:port repl-port)))
  1097. ;; Set the name of the main thread.
  1098. (set-thread-name "guix publish")
  1099. (with-store store
  1100. (run-publish-server socket store
  1101. #:advertise? advertise?
  1102. #:port port
  1103. #:cache cache
  1104. #:pool (and cache (make-pool workers
  1105. #:thread-name
  1106. "publish worker"))
  1107. #:nar-path nar-path
  1108. #:compressions compressions
  1109. #:narinfo-ttl ttl))))))
  1110. ;;; Local Variables:
  1111. ;;; eval: (put 'single-baker 'scheme-indent-function 1)
  1112. ;;; End: