guix-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Integration with Software Heritage


From: Ludovic Courtès
Subject: Integration with Software Heritage
Date: Fri, 16 Nov 2018 17:36:43 +0100
User-agent: Gnus/5.13 (Gnus v5.13) Emacs/26.1 (gnu/linux)

Hello Guix!

I’ve been playing with the HTTP API of Software Heritage to see how we
could use it as a mirror for the source of our packages.  The attached
file provides bindings to that API, which allow you to do things like
looking up an “origin” (source code URL):

  scheme@(guix swh)> (lookup-origin "https://github.com/guix-mirror/guix/";)
  $84 = #<<origin> id: 52181937 visits-url: "/api/1/origin/52181937/visits/" 
type: "git" url: "https://github.com/guix-mirror/guix";>

looking up a tag (which first means looking up its origin and then the
“visits” of that origin):

  scheme@(guix swh)> (lookup-origin-revision 
"https://github.com/guix-mirror/guix/"; "v0.8")
  $85 = #<<revision> id: "44941798d222901b8f381b3210957d880b90a2fc" date: 
#<date nanosecond: 0 second: 38 minute: 9 hour: 22 day: 17 month: 11 year: 2014 
zone-offset: 3600> directory: "f2b347a9eede13c32e298cc694fdfeb51709e065" 
directory-url: "/api/1/directory/f2b347a9eede13c32e298cc694fdfeb51709e065/">

looking up a “revision” (typically a Git commit):

  scheme@(guix swh)> (lookup-revision 
"c8855b991880897b2658dc90164e29c96e2aeb3a")
  $87 = #<<revision> id: "c8855b991880897b2658dc90164e29c96e2aeb3a" date: 
#<date nanosecond: 0 second: 11 minute: 10 hour: 12 day: 4 month: 11 year: 2015 
zone-offset: 3600> directory: "e8e7646053738dac1d94cdf1d47e17cfa460fbbc" 
directory-url: "/api/1/directory/e8e7646053738dac1d94cdf1d47e17cfa460fbbc/">

looking up “content” (such as a Git tree or, in some cases, a tarball):

  scheme@(guile-user)> ,use(guix swh)
  scheme@(guile-user)> ,use(gnu packages base)
  scheme@(guile-user)> ,use(guix)
  scheme@(guile-user)> (lookup-content (origin-sha256
                                        (package-source coreutils))
                                       "sha256")
  $88 = #<<content> checksums: (("sha1" . #vu8(99 213 34 244 48 240 249 252 236 
167 30 75 82 180 20 153 149 49 145 92)) ("sha256" . #vu8(146 208 250 28 49 28 
172 239 168 152 83 189 181 60 98 244 17 12 223 218 56 32 52 107 89 203 208 152 
244 15 149 94)) ("sha1_git" . #vu8(187 96 21 252 21 195 11 238 235 180 110 110 
169 57 214 11 161 46 216 44)) ("blake2s256" . #vu8(251 8 178 16 97 158 173 248 
72 66 132 183 201 41 123 8 162 98 69 4 215 121 223 44 213 83 144 13 35 93 234 
178))) data-url: 
"/api/1/content/sha256:92d0fa1c311cacefa89853bdb53c62f4110cdfda3820346b59cbd098f40f955e/raw/"
 file-type-url: 
"/api/1/content/sha256:92d0fa1c311cacefa89853bdb53c62f4110cdfda3820346b59cbd098f40f955e/filetype/"
 language-url: 
"/api/1/content/sha256:92d0fa1c311cacefa89853bdb53c62f4110cdfda3820346b59cbd098f40f955e/language/"
 length: 5286588 license-url: 
"/api/1/content/sha256:92d0fa1c311cacefa89853bdb53c62f4110cdfda3820346b59cbd098f40f955e/license/">

I haven’t yet implemented “directory” retrieval.  There are several
issues we discussed over the last few days on swh-devel (some of which
were already fixed!) so this is still work in progress:

  https://sympa.inria.fr/sympa/arc/swh-devel/2018-11/

Stefano opened an issue on their tracker to track progress on Guix
package coverage:

  https://forge.softwareheritage.org/T1352

One of the proposal Stefano made is to write a “lister” and/or a
“loader” for Guix packages, which is a snippet of Python code that would
run inside Software Heritage so that it can regularly grab the list of
packages that it needs to archive:

  https://sympa.inria.fr/sympa/arc/swh-devel/2018-11/msg00019.html

If you’re willing to give it a stab or to play with other aspects of the
archive, you’re very much welcome to join!

Ludo’.

;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018 Ludovic Courtès <address@hidden>
;;;
;;; 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 <http://www.gnu.org/licenses/>.

(define-module (guix swh)
  #:use-module (guix http-client)
  #:use-module (guix base16)
  #:use-module (web client)
  #:use-module (web response)
  #:use-module (json)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-9)
  #:use-module (srfi srfi-11)
  #:use-module (ice-9 match)
  #:use-module (ice-9 regex)
  #:export (origin?
            origin-id
            origin-type
            origin-url
            origin-visits
            lookup-origin

            visit?
            visit-date
            visit-origin
            visit-url
            visit-snapshot-url
            visit-status
            visit-number
            visit-snapshot

            branch?
            branch-name
            branch-target

            release?
            release-id
            release-name
            release-message
            release-target

            revision?
            revision-id
            revision-date
            revision-directory
            lookup-revision
            lookup-origin-revision

            content?
            content-checksums
            content-data-url
            content-length
            lookup-content))

;;; Commentary:
;;;
;;; This module provides bindings to the HTTP interface of Software Heritage.
;;; It allows you to browse the archive, look up revisions (such as SHA1
;;; commit IDs), "origins" (code hosting URLs), content (files), etc.  See
;;; <https://archive.softwareheritage.org/api/> for more information.
;;;
;;; Code:

(define %swh-base-url
  ;; Presumably we won't need to change it.
  "https://archive.softwareheritage.org";)

(define (swh-url path . rest)
  (define url
    (string-append %swh-base-url path
                   (string-join rest "/" 'prefix)))

  ;; Ensure there's a trailing slash or we get a redirect.
  (if (string-suffix? "/" url)
      url
      (string-append url "/")))

(define-syntax-rule (define-json-reader json->record ctor spec ...)
  "Define JSON->RECORD as a procedure that converts a JSON representation,
read from a port, string, or hash table, into a record created by CTOR and
following SPEC, a series of field specifications."
  (define (json->record input)
    (let ((table (cond ((port? input)
                        (json->scm input))
                       ((string? input)
                        (json-string->scm input))
                       ((hash-table? input)
                        input))))
      (let-syntax ((extract-field (syntax-rules ()
                                    ((_ table (field key json->value))
                                     (json->value (hash-ref table key)))
                                    ((_ table (field key))
                                     (hash-ref table key))
                                    ((_ table (field))
                                     (hash-ref table
                                               (symbol->string 'field))))))
        (ctor (extract-field table spec) ...)))))

(define-syntax-rule (define-json-mapping rtd ctor pred json->record
                      (field getter spec ...) ...)
  "Define RTD as a record type with the given FIELDs and GETTERs, à la SRFI-9,
and define JSON->RECORD as a conversion from JSON to a record of this type."
  (begin
    (define-record-type rtd
      (ctor field ...)
      pred
      (field getter) ...)

    (define-json-reader json->record ctor
      (field spec ...) ...)))

(define %date-regexp
  ;; Match strings like "2014-11-17T22:09:38+01:00" or
  ;; "2018-09-30T23:20:07.815449+00:00"".
  (make-regexp 
"^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})((\\.[0-9]+)?)([+-][0-9]{2}):([0-9]{2})$"))

(define (string->date* str)
  "Return a SRFI-19 date parsed from STR, a date string as returned by
Software Heritage."
  ;; We can't use 'string->date' because of the timezone format: SWH returns
  ;; "+01:00" when the '~z' template expects "+0100".  So we roll our own!
  (or (and=> (regexp-exec %date-regexp str)
             (lambda (match)
               (define (ref n)
                 (string->number (match:substring match n)))

               (make-date (let ((ns (match:substring match 8)))
                            (if ns
                                (string->number (string-drop ns 1))
                                0))
                          (ref 6) (ref 5) (ref 4)
                          (ref 3) (ref 2) (ref 1)
                          (+ (* 3600 (ref 9))     ;time zone
                             (if (< (ref 9) 0)
                                 (- (ref 10))
                                 (ref 10))))))
      str))                                       ;oops!

(define* (call url decode #:optional (method http-get)
               #:key (false-if-404? #t))
  "Invoke the endpoint at URL using METHOD.  Decode the resulting JSON body
using DECODE, a one-argument procedure that takes an input port.  When
FALSE-IF-404? is true, return #f upon 404 responses."
  (let*-values (((response port)
                 (method url #:streaming? #t)))
    ;; See <https://archive.softwareheritage.org/api/#rate-limiting>.
    (match (assq-ref (response-headers response) 'x-ratelimit-remaining)
      (#f #t)
      ((? (compose zero? string->number))
       (error "rate limit reached!"))
      (_ #t))

    (cond ((= 200 (response-code response))
           (let ((result (decode port)))
             (close-port port)
             result))
          ((and false-if-404?
                (= 404 (response-code response)))
           (close-port port)
           #f)
          (else
           (close-port port)
           (throw 'swh-error url response)))))

(define-syntax define-query
  (syntax-rules (path)
    "Define a procedure that performs a Software Heritage query."
    ((_ (name args ...) docstring (path components ...)
        json->value)
     (define (name args ...)
       docstring
       (call (swh-url components ...) json->value)))))

;; 
<https://archive.softwareheritage.org/api/1/origin/git/url/https://github.com/guix-mirror/guix/>
(define-json-mapping <origin> make-origin origin?
  json->origin
  (id origin-id)
  (visits-url origin-visits-url "origin_visits_url")
  (type origin-type)
  (url origin-url))

;; <https://archive.softwareheritage.org/api/1/origin/52181937/visits/>
(define-json-mapping <visit> make-visit visit?
  json->visit
  (date visit-date "date" string->date*)
  (origin visit-origin)
  (url visit-url "origin_visit_url")
  (snapshot-url visit-snapshot-url "snapshot_url")
  (status visit-status)
  (number visit-number "visit"))

;; 
<https://archive.softwareheritage.org/api/1/snapshot/4334c3ed4bb208604ed780d8687fe523837f1bd1/>
(define-json-mapping <snapshot> make-snapshot snapshot?
  json->snapshot
  (branches snapshot-branches "branches" json->branches))

;; This is used for the "branches" field of snapshots.
(define-record-type <branch>
  (make-branch name target-type target-url)
  branch?
  (name         branch-name)
  (target-type  branch-target-type)               ;release | revision
  (target-url   branch-target-url))

(define (json->branches branches)
  (hash-map->list (lambda (key value)
                    (make-branch key
                                 (string->symbol
                                  (hash-ref value "target_type"))
                                 (hash-ref value "target_url")))
                  branches))

;; 
<https://archive.softwareheritage.org/api/1/release/1f44934fb6e2cefccbecd4fa347025349fa9ff76/>
(define-json-mapping <release> make-release release?
  json->release
  (id          release-id)
  (name        release-name)
  (message     release-message)
  (target-type release-target-type "target_type" string->symbol)
  (target-url  release-target-url "target_url"))

;; 
<https://archive.softwareheritage.org/api/1/revision/359fdda40f754bbf1b5dc261e7427b75463b59be/>
(define-json-mapping <revision> make-revision revision?
  json->revision
  (id            revision-id)
  (date          revision-date "date" string->date*)
  (directory     revision-directory)
  (directory-url revision-directory-url "directory_url"))

;; <https://archive.softwareheritage.org/api/1/content/>
(define-json-mapping <content> make-content content?
  json->content
  (checksums     content-checksums "checksums" json->checksums)
  (data-url      content-data-url "data_url")
  (file-type-url content-file-type-url "filetype_url")
  (language-url  content-language-url "language_url")
  (length        content-length)
  (license-url   content-license-url "license_url"))

(define (json->checksums checksums)
  (hash-map->list (lambda (key value)
                    (cons key (base16-string->bytevector value)))
                  checksums))

;; <https://archive.softwareheritage.org/api/1/origin/save/>
(define-json-mapping <save-reply> make-save-reply save-reply?
  json->save-reply
  (origin-url     save-reply-origin-url "origin_url")
  (origin-type    save-reply-origin-type "origin_type")
  (request-date   save-reply-request-date "save_request_date"
                  string->date*)
  (request-status save-reply-request-status "save_request_status"
                  string->symbol)
  (task-status    save-reply-task-status "save_task_status"
                  (match-lambda
                    ("not created" 'not-created)
                    ((? string? str) (string->symbol str)))))


;;;
;;; RPCs.
;;;

(define-query (lookup-origin url)
  "Return an origin for URL."
  (path "/api/1/origin/git/url" url)
  json->origin)

(define-query (lookup-content hash type)
  "Return a content for HASH, of the given TYPE--e.g., \"sha256\"."
  (path "/api/1/content"
        (string-append type ":"
                       (bytevector->base16-string hash)))
  json->content)

(define-query (lookup-revision id)
  "Return the revision with the given ID, typically a Git commit SHA1."
  (path "/api/1/revision" id)
  json->revision)

(define (origin-visits origin)
  "Return the list of visits of ORIGIN, a record as returned by
'lookup-origin'."
  (map json->visit
       (json->scm (http-fetch (swh-url (origin-visits-url origin))))))

(define (visit-snapshot visit)
  "Return the snapshot corresponding to VISIT."
  (json->snapshot (http-fetch (swh-url (visit-snapshot-url visit)))))

(define (branch-target branch)
  "Return the target of BRANCH, either a <revision> or a <release>."
  (match (branch-target-type branch)
    ('release
     (json->release
      (http-fetch (swh-url (branch-target-url branch)))))
    ('revision
     (json->revision
      (http-fetch (swh-url (branch-target-url branch)))))))

(define (lookup-origin-revision url tag)
  "Return a <revision> corresponding to the given TAG for the repository
coming from URL.  Example:

  (lookup-origin-release \"https://github.com/guix-mirror/guix/\"; \"v0.8\")
  => #<<revision> id: \"44941…\" …>

The information is based on the latest visit of URL available.  Return #f if
URL could not be found."
  (match (lookup-origin url)
    (#f #f)
    (origin
      (match (origin-visits origin)
        ((visit . _)
         (let ((snapshot (visit-snapshot visit)))
           (match (and=> (find (lambda (branch)
                                 (string=? (string-append "refs/tags/" tag)
                                           (branch-name branch)))
                               (snapshot-branches snapshot))
                         branch-target)
             ((? release? release)
              (release-target release))
             ((? revision? revision)
              revision)
             (#f                                  ;tag not found
              #f))))
        (()
         #f)))))

(define (release-target release)
  "Return the revision that is the target of RELEASE."
  (match (release-target-type release)
    ('revision
     (json->revision (http-fetch (swh-url (release-target-url release)))))))

(define (save-origin url #:optional (type "git"))
  "Request URL to be saved."
  (call (swh-url "/api/1/origin/save" type "url" url) json->save-reply
        http-post))

(define-query (save-origin-status url type)
  "Return the status of a /save request for URL and TYPE (e.g., \"git\")."
  (path "/api/origin/save" type "url" url)
  json->save-reply)

Attachment: signature.asc
Description: PGP signature


reply via email to

[Prev in Thread] Current Thread [Next in Thread]