From aff8ce7c742443eaab0b0c6b6f27e6539f3af85f Mon Sep 17 00:00:00 2001 From: David Thompson Date: Tue, 17 Mar 2015 10:21:31 -0400 Subject: scripts: Add 'publish' command. * guix/scripts/publish.scm: New file. * po/guix/POTFILES.in: Add it. * tests/publish.scm: New file. * Makefile.am (MODULES): Add script module. (SCM_TESTS): Add test module. * doc/guix.texi ("Invoking guix publish"): New node. --- guix/scripts/publish.scm | 243 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 guix/scripts/publish.scm (limited to 'guix/scripts') diff --git a/guix/scripts/publish.scm b/guix/scripts/publish.scm new file mode 100644 index 0000000000..c7c66fefbe --- /dev/null +++ b/guix/scripts/publish.scm @@ -0,0 +1,243 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2015 David Thompson +;;; +;;; This file is part of GNU Guix. +;;; +;;; GNU Guix is free software; you can redistribute it and/or modify it +;;; under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 3 of the License, or (at +;;; your option) any later version. +;;; +;;; GNU Guix is distributed in the hope that it will be useful, but +;;; WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;; GNU General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Guix. If not, see . + +(define-module (guix scripts publish) + #:use-module ((system repl server) #:prefix repl:) + #:use-module (ice-9 binary-ports) + #:use-module (ice-9 format) + #:use-module (ice-9 match) + #:use-module (ice-9 regex) + #:use-module (rnrs io ports) + #:use-module (rnrs bytevectors) + #:use-module (srfi srfi-1) + #:use-module (srfi srfi-2) + #:use-module (srfi srfi-26) + #:use-module (srfi srfi-37) + #:use-module (web http) + #:use-module (web request) + #:use-module (web response) + #:use-module (web server) + #:use-module (web uri) + #:use-module (guix base32) + #:use-module (guix base64) + #:use-module (guix config) + #:use-module (guix derivations) + #:use-module (guix hash) + #:use-module (guix pki) + #:use-module (guix pk-crypto) + #:use-module (guix store) + #:use-module (guix serialization) + #:use-module (guix ui) + #:export (guix-publish)) + +(define (show-help) + (format #t (_ "Usage: guix publish [OPTION]... +Publish ~a over HTTP.\n") %store-directory) + (display (_ " + -p, --port=PORT listen on PORT")) + (display (_ " + -r, --repl[=PORT] spawn REPL server on PORT")) + (newline) + (display (_ " + -h, --help display this help and exit")) + (display (_ " + -V, --version display version information and exit")) + (newline) + (show-bug-report-information)) + +(define %options + (list (option '(#\h "help") #f #f + (lambda _ + (show-help) + (exit 0))) + (option '(#\V "version") #f #f + (lambda _ + (show-version-and-exit "guix publish"))) + (option '(#\p "port") #t #f + (lambda (opt name arg result) + (alist-cons 'port (string->number* arg) result))) + (option '(#\r "repl") #f #t + (lambda (opt name arg result) + ;; If port unspecified, use default Guile REPL port. + (let ((port (and arg (string->number* arg)))) + (alist-cons 'repl (or port 37146) result)))))) + +(define %default-options + '((port . 8080) + (repl . #f))) + +(define (lazy-read-file-sexp file) + "Return a promise to read the canonical sexp from FILE." + (delay + (call-with-input-file file + (compose string->canonical-sexp + get-string-all)))) + +(define %private-key + (lazy-read-file-sexp %private-key-file)) + +(define %public-key + (lazy-read-file-sexp %public-key-file)) + +(define %nix-cache-info + `(("StoreDir" . ,%store-directory) + ("WantMassQuery" . 0) + ("Priority" . 100))) + +(define (load-derivation file) + "Read the derivation from FILE." + (call-with-input-file file read-derivation)) + +(define (signed-string s) + "Sign the hash of the string S with the daemon's key." + (let* ((public-key (force %public-key)) + (hash (bytevector->hash-data (sha256 (string->utf8 s)) + #:key-type (key-type public-key)))) + (signature-sexp hash (force %private-key) public-key))) + +(define base64-encode-string + (compose base64-encode string->utf8)) + +(define (narinfo-string store-path path-info key) + "Generate a narinfo key/value string for STORE-PATH using the details in +PATH-INFO. The narinfo is signed with KEY." + (let* ((url (string-append "nar/" (basename store-path))) + (hash (bytevector->base32-string + (path-info-hash path-info))) + (size (path-info-nar-size path-info)) + (references (string-join + (map basename (path-info-references path-info)) + " ")) + (deriver (path-info-deriver path-info)) + (base-info (format #f + "StorePath: ~a +URL: ~a +Compression: none +NarHash: sha256:~a +NarSize: ~d +References: ~a~%" + store-path url hash size references)) + ;; Do not render a "Deriver" or "System" line if we are rendering + ;; info for a derivation. + (info (if (string-null? deriver) + base-info + (let ((drv (load-derivation deriver))) + (format #f "~aSystem: ~a~%Deriver: ~a~%" + base-info (derivation-system drv) + (basename deriver))))) + (signature (base64-encode-string + (canonical-sexp->string (signed-string info))))) + (format #f "~aSignature: 1;~a;~a~%" info (gethostname) signature))) + +(define (not-found request) + "Render 404 response for REQUEST." + (values (build-response #:code 404) + (string-append "Resource not found: " + (uri-path (request-uri request))))) + +(define (render-nix-cache-info) + "Render server information." + (values '((content-type . (text/plain))) + (lambda (port) + (for-each (match-lambda + ((key . value) + (format port "~a: ~a~%" key value))) + %nix-cache-info)))) + +(define (render-narinfo store request hash) + "Render metadata for the store path corresponding to HASH." + (let* ((store-path (hash-part->path store hash)) + (path-info (and (not (string-null? store-path)) + (query-path-info store store-path)))) + (if path-info + (values '((content-type . (application/x-nix-narinfo))) + (cut display + (narinfo-string store-path path-info (force %private-key)) + <>)) + (not-found request)))) + +(define (render-nar request store-item) + "Render archive of the store path corresponding to STORE-ITEM." + (let ((store-path (string-append %store-directory "/" store-item))) + ;; The ISO-8859-1 charset *must* be used otherwise HTTP clients will + ;; interpret the byte stream as UTF-8 and arbitrarily change invalid byte + ;; sequences. + (if (file-exists? store-path) + (values '((content-type . (application/x-nix-archive + (charset . "ISO-8859-1")))) + (lambda (port) + (write-file store-path port))) + (not-found request)))) + +(define extract-narinfo-hash + (let ((regexp (make-regexp "^([a-df-np-sv-z0-9]{32}).narinfo$"))) + (lambda (str) + "Return the hash within the narinfo resource string STR, or false if STR +is invalid." + (and=> (regexp-exec regexp str) + (cut match:substring <> 1))))) + +(define (get-request? request) + "Return #t if REQUEST uses the GET method." + (eq? (request-method request) 'GET)) + +(define (request-path-components request) + "Split the URI path of REQUEST into a list of component strings. For +example: \"/foo/bar\" yields '(\"foo\" \"bar\")." + (split-and-decode-uri-path (uri-path (request-uri request)))) + +(define (make-request-handler store) + (lambda (request body) + (format #t "~a ~a~%" + (request-method request) + (uri-path (request-uri request))) + (if (get-request? request) ; reject POST, PUT, etc. + (match (request-path-components request) + ;; /nix-cache-info + (("nix-cache-info") + (render-nix-cache-info)) + ;; /.narinfo + (((= extract-narinfo-hash (? string? hash))) + (render-narinfo store request hash)) + ;; /nar/ + (("nar" store-item) + (render-nar request store-item)) + (_ (not-found request))) + (not-found request)))) + +(define (run-publish-server port store) + (run-server (make-request-handler store) + 'http + `(#:addr ,INADDR_ANY + #:port ,port))) + +(define (guix-publish . args) + (with-error-handling + (let* ((opts (args-fold* args %options + (lambda (opt name arg result) + (leave (_ "~A: unrecognized option~%") name)) + (lambda (arg result) + (leave (_ "~A: extraneuous argument~%") arg)) + %default-options)) + (port (assoc-ref opts 'port)) + (repl-port (assoc-ref opts 'repl))) + (format #t (_ "publishing ~a on port ~d~%") %store-directory port) + (when repl-port + (repl:spawn-server (repl:make-tcp-server-socket #:port repl-port))) + (with-store store + (run-publish-server (assoc-ref opts 'port) store))))) -- cgit v1.2.3 From 65d428d8f4bd6bf05dde428ce51a3ce04bd3aad3 Mon Sep 17 00:00:00 2001 From: Ludovic Courtès Date: Mon, 6 Apr 2015 20:02:22 +0200 Subject: guix package: Move generation deletion to its own procedure. * guix/scripts/package.scm (delete-matching-generations): New procedure, with code formerly found... (guix-package)[process-actions]: ... here. Use it. Remove 'current-generation-number'. --- guix/scripts/package.scm | 56 +++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 27 deletions(-) (limited to 'guix/scripts') diff --git a/guix/scripts/package.scm b/guix/scripts/package.scm index 3cc7ae760f..7074243ed9 100644 --- a/guix/scripts/package.scm +++ b/guix/scripts/package.scm @@ -232,6 +232,34 @@ DURATION-RELATION with the current time." filter-by-duration) (else #f))) +(define (delete-matching-generations store profile pattern) + "Delete from PROFILE all the generations matching PATTERN. PATTERN must be +a string denoting a set of generations: the empty list means \"all generations +but the current one\", a number designates a generation, and other patterns +denote ranges as interpreted by 'matching-derivations'." + (let ((current (generation-number profile))) + (cond ((not (file-exists? profile)) ; XXX: race condition + (raise (condition (&profile-not-found-error + (profile profile))))) + ((string-null? pattern) + (delete-generations (%store) profile + (delv current (profile-generations profile)))) + ;; Do not delete the zeroth generation. + ((equal? 0 (string->number pattern)) + (exit 0)) + + ;; If PATTERN is a duration, match generations that are + ;; older than the specified duration. + ((matching-generations pattern profile + #:duration-relation >) + => + (lambda (numbers) + (if (null-list? numbers) + (exit 1) + (delete-generations (%store) profile numbers)))) + (else + (leave (_ "invalid syntax: ~a~%") pattern))))) + ;;; ;;; Package specifications. @@ -751,9 +779,6 @@ more information.~%")) (define dry-run? (assoc-ref opts 'dry-run?)) (define profile (assoc-ref opts 'profile)) - (define current-generation-number - (generation-number profile)) - ;; First roll back if asked to. (cond ((and (assoc-ref opts 'roll-back?) (not dry-run?)) @@ -782,30 +807,7 @@ more information.~%")) (for-each (match-lambda (('delete-generations . pattern) - (cond ((not (file-exists? profile)) ; XXX: race condition - (raise (condition (&profile-not-found-error - (profile profile))))) - ((string-null? pattern) - (delete-generations - (%store) profile - (delete current-generation-number - (profile-generations profile)))) - ;; Do not delete the zeroth generation. - ((equal? 0 (string->number pattern)) - (exit 0)) - - ;; If PATTERN is a duration, match generations that are - ;; older than the specified duration. - ((matching-generations pattern profile - #:duration-relation >) - => - (lambda (numbers) - (if (null-list? numbers) - (exit 1) - (delete-generations (%store) profile numbers)))) - (else - (leave (_ "invalid syntax: ~a~%") - pattern))) + (delete-matching-generations (%store) profile pattern) (process-actions (alist-delete 'delete-generations opts))) -- cgit v1.2.3 From d26eb84d140af8d2119509d7da440b4f035608c5 Mon Sep 17 00:00:00 2001 From: Ludovic Courtès Date: Mon, 6 Apr 2015 21:26:12 +0200 Subject: guix package: Never remove the current generation and warn about it. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes . Reported by taylanbayirli@gmail.com (Taylan Ulrich Bayırlı/Kammer). * guix/scripts/package.scm (delete-matching-generations): Warn when CURRENT is in NUMBERS, and always remove it before calling 'delete-generations'. * tests/guix-package.sh: Add --switch-generation=2 invocation before --delete-generations=3 invocation. Add --delete-generations=1.. test case. --- guix/scripts/package.scm | 13 ++++++++++--- tests/guix-package.sh | 11 +++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) (limited to 'guix/scripts') diff --git a/guix/scripts/package.scm b/guix/scripts/package.scm index 7074243ed9..3a7afb724b 100644 --- a/guix/scripts/package.scm +++ b/guix/scripts/package.scm @@ -254,9 +254,16 @@ denote ranges as interpreted by 'matching-derivations'." #:duration-relation >) => (lambda (numbers) - (if (null-list? numbers) - (exit 1) - (delete-generations (%store) profile numbers)))) + (when (memv current numbers) + (warning (_ "not removing generation ~a, which is current~%") + current)) + + ;; Make sure we don't inadvertently remove the current + ;; generation. + (let ((numbers (delv current numbers))) + (if (null-list? numbers) + (exit 1) + (delete-generations (%store) profile numbers))))) (else (leave (_ "invalid syntax: ~a~%") pattern))))) diff --git a/tests/guix-package.sh b/tests/guix-package.sh index 94cf927420..a4c04255ba 100644 --- a/tests/guix-package.sh +++ b/tests/guix-package.sh @@ -161,6 +161,9 @@ then guix package --bootstrap -p "$profile" -i guile-bootstrap -i gcc-bootstrap guix package --search-paths -p "$profile" | grep LIBRARY_PATH + # Roll back so we can delete #3 below. + guix package -p "$profile" --switch-generation=2 + # Delete the third generation and check that it was actually deleted. guix package -p "$profile" --delete-generations=3 test -z "`guix package -p "$profile" -l 3`" @@ -212,6 +215,14 @@ if guix package -p "$profile" --delete-generations=12m; then false; else true; fi test "`readlink_base "$profile"`" = "$generation" +# The following command should not delete the current generation, even though +# it matches the given pattern (see .) And since +# there's nothing else to delete, it should just fail. +guix package --list-generations -p "$profile" +if guix package --bootstrap -p "$profile" --delete-generations=1.. +then false; else true; fi +test "`readlink_base "$profile"`" = "$generation" + # Make sure $profile is a GC root at this point. real_profile="`readlink -f "$profile"`" if guix gc -d "$real_profile" -- cgit v1.2.3 From 250bc998ac9d456f70c675579020d7173f87f3c3 Mon Sep 17 00:00:00 2001 From: Ludovic Courtès Date: Mon, 6 Apr 2015 21:27:03 +0200 Subject: guix package: Avoid 'exit' calls in 'delete-matching-generations'. * guix/scripts/package.scm (delete-matching-generations): Remove call to 'exit' when PATTERN is "0". Call 'leave' instead of 'exit' when (null-list? number). --- guix/scripts/package.scm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'guix/scripts') diff --git a/guix/scripts/package.scm b/guix/scripts/package.scm index 3a7afb724b..6190f3286d 100644 --- a/guix/scripts/package.scm +++ b/guix/scripts/package.scm @@ -246,7 +246,7 @@ denote ranges as interpreted by 'matching-derivations'." (delv current (profile-generations profile)))) ;; Do not delete the zeroth generation. ((equal? 0 (string->number pattern)) - (exit 0)) + #t) ;; If PATTERN is a duration, match generations that are ;; older than the specified duration. @@ -261,9 +261,9 @@ denote ranges as interpreted by 'matching-derivations'." ;; Make sure we don't inadvertently remove the current ;; generation. (let ((numbers (delv current numbers))) - (if (null-list? numbers) - (exit 1) - (delete-generations (%store) profile numbers))))) + (when (null-list? numbers) + (leave (_ "no matching generation~%"))) + (delete-generations (%store) profile numbers)))) (else (leave (_ "invalid syntax: ~a~%") pattern))))) -- cgit v1.2.3