;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018 Julien Lepiller
;;;
;;; 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 import quicklisp)
; #:use-module (ice-9 ftw)
#:use-module (ice-9 match)
#:use-module (ice-9 peg)
; #:use-module (ice-9 receive)
#:use-module ((ice-9 rdelim) #:select (read-line))
#:use-module (ice-9 textual-ports)
#:use-module (sxml simple)
#:use-module (sxml xpath)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-2)
#:use-module (web uri)
; #:use-module (guix http-client)
#:use-module ((guix build download) #:prefix build:)
; #:use-module (guix git)
; #:use-module (guix ui)
#:use-module ((guix utils) #:select (call-with-temporary-output-file))
; #:use-module (guix import utils)
; #:use-module ((guix licenses) #:prefix license:)
#:export (test))
;; Define a PEG parser for the quicklisp format
(define-peg-pattern SP none (or " " "\n"))
(define-peg-pattern NL none "\n")
(define-peg-pattern COLON none ":")
(define-peg-pattern RELEASE none "release/")
(define-peg-pattern HASH none "#")
(define-peg-pattern IGNORE none (peg "(! SP .)*"))
(define-peg-pattern IGNORE-UNTIL-NL none (peg "(! NL .)*"))
(define-peg-pattern text all (+ (or (range #\a #\z) "-")))
(define-peg-pattern text-until-sp all (peg "(! SP .)*"))
(define-peg-pattern text-until-colon all (peg "(! COLON .)*"))
(define-peg-pattern text-until-nl all (peg "(! NL .)*"))
;; Meta and distinfo files use COLON as separator:
(define-peg-pattern test all
text-until-colon)
(define-peg-pattern record all
(and text COLON (* SP) text-until-nl))
(define-peg-pattern records body (* (and record (* SP))))
;; Release index has no colons between the values:
(define-peg-pattern record-index all (and text-until-sp (* SP) text-until-nl))
;; Field no. 2 is tarball url
(define-peg-pattern field-2 all
(and text-until-sp SP text-until-sp IGNORE-UNTIL-NL))
(define-peg-pattern 2s body
(* (and (or header-hash field-2) (* SP))))
;; Field no. 6 is version
(define-peg-pattern version all
(and text-until-sp SP IGNORE SP IGNORE SP IGNORE SP
IGNORE SP text-until-sp IGNORE-UNTIL-NL))
(define-peg-pattern versions body
(* (and (or header-hash version) (* SP))))
;; Field no. 7-N is system files which we need.
(define-peg-pattern system-files all
(and text-until-sp SP IGNORE SP IGNORE SP IGNORE SP
IGNORE SP IGNORE SP text-until-nl))
(define-peg-pattern system-files-list body
(* (and (or header-hash system-files) (* SP))))
;; Release hashfile has no colons between the values:
;; Names contain numbers, get everything exept space.
(define-peg-pattern record-hash all
(and
;; This matches the prefix "release/"
RELEASE
text-until-sp (* SP) text-until-nl))
(define-peg-pattern header-hash all
(and
(and HASH (* SP))
text-until-sp (* SP) text-until-nl))
(define-peg-pattern records-hash body (* (and (or header-hash record-hash) (* SP))))
;; Systems.txt with dependencies
;; This parsing results in a lot of duplicates, but we ignore that because our
;; match just picks the first and returns happy and the overhead is neglible.
;; Field no. 3 is system-name
(define-peg-pattern field-3 all
(and text-until-sp SP IGNORE SP text-until-sp IGNORE-UNTIL-NL))
(define-peg-pattern 3s body
(* (and (or header-hash field-3) (* SP))))
(define-peg-pattern record-sys all
(and
;; We ignore the second and third field for now.
text-until-sp (* SP) IGNORE SP IGNORE SP text-until-nl))
(define-peg-pattern records-sys body (* (and (or header-hash record-sys) (* SP))))
;;; QL= QuickLisp
;;; cl= Common Lisp
(define (url-fetch url file-name)
"Save the contents of URL to FILE-NAME. Return #f on failure."
(parameterize ((current-output-port (current-error-port)))
(build:url-fetch url file-name)))
(define (ql-meta-file)
"Get the latest meta release file. From the links in this we extract all
other information we need."
(call-with-temporary-output-file
(lambda (temp port)
(and (url-fetch "https://beta.quicklisp.org/dist/quicklisp.txt" temp)
(peg:tree (match-pattern test (get-string-all port)))))))
;;(display (metadata-ref (ql-meta-file) "release-index-url"))
(define (ql-latest-index-file)
"Get the latest release index file content. This contains: name tarball-url
file-md5 content-sha1-hash version etc. Space separated."
(let ((latest (metadata-ref (ql-meta-file) "release-index-url")))
(call-with-temporary-output-file
(lambda (temp port)
(and (url-fetch latest temp)
(get-string-all port))))))
(define (ql-distinfo-file)
"Get the latest distinfo file. Colon separated. From this we only get the link to system-index-url."
(let ((latest (metadata-ref (ql-meta-file) "canonical-distinfo-url")))
(call-with-temporary-output-file
(lambda (temp port)
(and (url-fetch latest temp)
(peg:tree (match-pattern records (get-string-all port))))))))
(define (ql-hash-file)
"Get the latest hashfile."
(let* ((uri (string->uri (metadata-ref (ql-meta-file) "release-index-url")))
(host (uri-host uri))
(path (string-drop-right (uri-path uri) 12))
(url (string-append "https://" host path "digests.txt")))
(pk 'url url)
(call-with-temporary-output-file
(lambda (temp port)
(and (url-fetch url temp)
(peg:tree (match-pattern records-hash (get-string-all port))))))))
(define (ql-systems-file)
"Get the latest file with dependency information for each package. Spaceseparated list of dependencies."
(let ((latest (metadata-ref (ql-distinfo-file) "system-index-url")))
(call-with-temporary-output-file
(lambda (temp port)
(and (url-fetch latest temp)
(peg:tree (match-pattern records-sys (get-string-all port))))))))
;;(display (ql-systems-file)) works.
(define (metadata-ref file lookup)
"Lookup metadata for FILE and LOOKUP."
;; (pk 'file file 'lookup lookup)
(fold (lambda (record acc)
(match record
;; Output from parser looks like this:
((record (_ key) (_ val))
;; Find our key
(if (equal? key lookup)
;; return val
val
;; else return acc
acc))))
#f file))
;;(metadata-ref (ql-latest-index-file) "1am")
; returns url of the tarball -
; works!
;;(metadata-ref (ql-hash-file) "1am")
; returns sha256 hash of the tarball -
; works!
;;(metadata-ref (ql-systems-file) "able")
; returns string with dependencies
; space separated - works!
(define (ql-extract field name)
"Helper to read the right field from (ql-latest-index-file). Field is one of
url, system-file, system-name, version. Name is the package name."
(let* ((release (ql-latest-index-file))
(systems (ql-systems-file))
(system-files (peg:tree (match-pattern system-files-list release)))
(system-name (peg:tree (match-pattern 3s systems)))
(url (peg:tree (match-pattern 2s release)))
(version (peg:tree (match-pattern versions release))))
(cond
((equal? field 'version)
(let ((str (metadata-ref version name)))
(if (string-prefix? name str)
;; Drop "name-" from version-string.
(string-drop str (+ 1 (string-length name)))
str)))
;; FIXME these ONLY extract the first match
((equal? field 'system-files)
(metadata-ref system-files name))
((equal? field 'system-name)
(metadata-ref system-name name)))))
;;(display (peg:tree (match-pattern versions (ql-latest-index-file))))
#;
(display
(peg:tree (match-pattern versions (ql-latest-index-file))))
;;(display (ql-extract 'version "1am"))
;;(display (ql-extract 'system-files "1am"))
;;(display (ql-latest-index-file))
;; Guess the homepage of the package
(define (homepage name)
(string-append "http://quickdocs.org/" name "/"))
(define (get-homepage name)
"Get the latest meta release file. From the links in this we extract all
other information we need."
(call-with-temporary-output-file
(lambda (temp port)
(and (url-fetch (homepage name) temp)
(xml->sxml (get-string-all port))))))
(display (get-homepage "1am"))
#;
(define (synopsis name)
;;extract from homepage: section class="readme"
)
;; fetcher from texlive importer:
#;
(define (fetch-sxml name)
"Return an sxml representation of the package information contained in the
XML description of the CTAN package or #f in case of failure."
;; This API always returns the latest release of the module.
(let ((url (string-append "http://www.ctan.org/xml/1.2/pkg/" name)))
(guard (c ((http-get-error? c)
(format (current-error-port)
"error: failed to retrieve package information \
from ~s: ~a (~s)~%"
(uri->string (http-get-error-uri c))
(http-get-error-code c)
(http-get-error-reason c))
#f))
(xml->sxml (http-fetch url)
#:trim-whitespace? #t))))
(define (cl-name->guix-name name)
(cond
;; TODO: Any special cases?
((string-prefix? "cl-" name) name)
(else (string-append "cl-" name))))
;; Native dependency information is not available in QL.
;; (define (dependency->input dependency)
;; (match dependency
;; ))
(define (dependency-list->inputs lst)
(map
(lambda (dependency)
(list dependency (list 'unquote (string->symbol dependency))))
(cl-names->guix-names lst)))
(define (sxml->guix-package name)
(define (sxml-value path)
(match ((sxpath path) sxml)
(() #f)
((val) val)))
(and-let* (
(sxml (get-homepage name))
(version (find-latest-version name))
(file '())
;;(home-page '())
(hash (metadata-ref (ql-hash-file) name))
(source-url (metadata-ref url-dict "src"))
(inputs
(dependency-list->inputs
(string-split (metadata-ref (ql-systems-file) name) " "))))
(call-with-temporary-output-file
(lambda (temp port)
(and (url-fetch source-url temp)
`(package
(name ,(cl-name->guix-name name))
(version ,(metadata-ref opam-content "version"))
(source
(origin
(method url-fetch)
(uri ,source-url)
;; TODO chech hash
(sha256 (base32 ,(guix-hash-url temp)))))
(build-system ocaml-build-system)
,@(if (null? inputs)
'()
`((inputs ,(list 'quasiquote inputs))))
(home-page ,(metadata-ref opam-content "homepage"))
(synopsis ,(metadata-ref opam-content "synopsis"))
(description ,(metadata-ref opam-content "description"))
(license #f)))))))
;; opam stuff:
;; (define (find-latest-version package repository)
;; "Get the latest version of a package as described in the given repository."
;; (let* ((dir (string-append repository "/packages/" package))
;; (versions (scandir dir (lambda (name) (not (string-prefix? "." name))))))
;; (if versions
;; (let ((versions (map
;; (lambda (dir)
;; (string-join (cdr (string-split dir #\.)) "."))
;; versions)))
;; (latest-version versions))
;; (begin
;; (format #t (G_ "Package not found in opam repository: ~a~%") package)
;; #f))))
;; (define (get-metadata opam-file)
;; (with-input-from-file opam-file
;; (lambda _
;; (peg:tree (match-pattern records (get-string-all (current-input-port)))))))
;; (define (ocaml-name->guix-name name)
;; (cond
;; ((equal? name "ocamlfind") "ocaml-findlib")
;; ((string-prefix? "ocaml" name) name)
;; ((string-prefix? "conf-" name) (substring name 5))
;; (else (string-append "ocaml-" name))))
;; (define (metadata-ref file lookup)
;; (pk 'file file 'lookup lookup)
;; (fold (lambda (record acc)
;; (match record
;; ((record key val)
;; (if (equal? key lookup)
;; (match val
;; (('list-pat . stuff) stuff)
;; (('string-pat stuff) stuff)
;; (('multiline-string stuff) stuff)
;; (('dict records ...) records))
;; acc))))
;; #f file))
;; (define (native? condition)
;; (match condition
;; (('condition-var var)
;; (match var
;; ("with-test" #t)
;; ("test" #t)
;; ("build" #t)
;; (_ #f)))
;; ((or ('condition-or cond-left cond-right) ('condition-and cond-left cond-right))
;; (or (native? cond-left)
;; (native? cond-right)))
;; (_ #f)))
;; (define (dependency->input dependency)
;; (match dependency
;; (('string-pat str) str)
;; (('conditional-value val condition)
;; (if (native? condition) "" (dependency->input val)))))
;; (define (dependency->native-input dependency)
;; (match dependency
;; (('string-pat str) "")
;; (('conditional-value val condition)
;; (if (native? condition) (dependency->input val) ""))))
;; (define (ocaml-names->guix-names names)
;; (map ocaml-name->guix-name
;; (remove (lambda (name)
;; (or (equal? "" name))
;; (equal? "ocaml" name))
;; names)))
;; (define (depends->inputs depends)
;; (filter (lambda (name)
;; (and (not (equal? "" name))
;; (not (equal? "ocaml" name))
;; (not (equal? "ocamlfind" name))))
;; (map dependency->input depends)))
;; (define (depends->native-inputs depends)
;; (filter (lambda (name) (not (equal? "" name)))
;; (map dependency->native-input depends)))
;; (define (dependency-list->inputs lst)
;; (map
;; (lambda (dependency)
;; (list dependency (list 'unquote (string->symbol dependency))))
;; (ocaml-names->guix-names lst)))