;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2023 Nicolas Graves ;;; ;;; 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 juliahub) #:use-module (ice-9 textual-ports) #:use-module (ice-9 regex) #:use-module (ice-9 match) #:use-module (ice-9 streams) #:use-module (ice-9 string-fun) #:use-module (srfi srfi-1) #:use-module (srfi srfi-9) #:use-module (srfi srfi-11) #:use-module (guix http-client) #:use-module (guix git) #:use-module (guix import utils) #:use-module (guix import json) #:use-module (guix base32) #:use-module (guix packages) #:use-module (guix upstream) #:use-module (json) #:use-module ((guix licenses) #:prefix license:) #:export (juliahub->guix-package %juliahub-updater juliahub-recursive-import)) ;; Juliahub may be more up-to-date than the General registry or the actual git ;; tag (it seems around 6 hours pass between the time a commit is supplied to ;; JuliaRegistrator as a release, and the time Julia TagBot Github Action makes ;; the git tag). We have no simple way to get the commit of the latest-version. ;; Thus the best simple thing we can do is get the latest-git-version, and ;; import this version instead. We do this by parsing Package.toml in the General ;; registry, and then getting the refs of the git repo supplied by this ;; file. Parsing this file is also necessary if the package is in a subdir of a ;; git repository, because the information isn't present in Juliahub. ;; There's a last case where some Julia packages are not based on a particular ;; git tag, and where looking for the commit is tedious and artificial. In these ;; cases, we introduce the tree-commit which is available in the Versions.toml ;; file in the General repository. This is equivalent to a commit, since we have ;; a unique hash of the listing of files and directories, thus it can be used to ;; identify the state of a repository. (define %general-base-url "https://raw.githubusercontent.com/JuliaRegistries/General/master/") (define (general-url package-name file) (let ((folder (string-capitalize (string-take package-name 1)))) (string-append %general-base-url folder "/" package-name "/" file))) (define (ini-list->alist lst) (match lst ((attribute '= value ooo ...) `((,attribute . ,value) ,@(ini-list->alist ooo))) ('() '()))) (define (ini-fetch url) (let* ((port (http-fetch url #:text? #t)) (ini-list (stream->list (port->stream port read)))) (close-port port) (ini-list->alist ini-list))) (define (latest-git-tag repo) (let* ((last-ref (last (remote-refs repo #:tags? #t))) (last-git-tag (last (string-split last-ref #\/)))) (string-drop last-git-tag 1))) ;; To update, see file sysimg.jl (define %julia-stdlibs (list "julia" "ArgTools" "Artifacts" "Base64" "CRC32c" "FileWatching" "Libdl" "Logging" "Mmap" "NetworkOptions" "SHA" "Serialization" "Sockets" "Unicode" "DelimitedFiles" "LinearAlgebra" "Markdown" "Printf" "Random" "Tar" "Dates" "Distributed" "Future" "InteractiveUtils" "LibGit2" "Profile" "SparseArrays" "UUIDs" "REPL" "SharedArrays" "Statistics" "SuiteSparse" "TOML" "Test" "LibCURL" "Downloads" "Pkg" "LazyArtifacts")) (define (juliahub-redirect-uri name) (let* ((url (string-append "https://docs.juliahub.com/" name "/")) (port (http-fetch url #:text? #t)) (_ (get-line port)) (meta (get-line port)) (regex "url=[a-zA-Z0-9]{5}\\/[0-9\\.]*") (redirect (match:substring (string-match regex meta)))) (close-port port) (string-drop redirect 4))) (define (juliahub-url name) (let* ((url (string-append "https://docs.juliahub.com/" name "/")) (uri (juliahub-redirect-uri name))) (string-append url uri "/"))) (define (juliahub-slug+version name) (let* ((uri (juliahub-redirect-uri name)) (slug (string-take uri 5)) (latest-version (string-drop uri 6))) `(,slug ,latest-version))) (define (json->juliahub-dependencies vector) (if (vector? vector) (filter-map (lambda (el) (let ((dep (json->juliahub-dependency el))) (if (not (member (juliahub-dependency-name dep) %julia-stdlibs)) dep #f))) (vector->list vector)))) (define (parse-test-dependencies directory) (let* ((port (open-input-file (string-append directory "/Project.toml"))) (project.toml (get-string-all port)) (regex "\ntest = \\[.*\\]") (deps (match:substring (string-match regex project.toml))) (pure (string-delete (list->char-set (list #\" #\ )) deps))) (close-port port) (filter (lambda (x) (not (member x %julia-stdlibs))) (string-split (string-drop (string-drop-right pure 1) 7) #\,)))) ;; Julia package. (define-json-mapping make-juliahub-package juliahub-package? json->juliahub-package (homepage juliahub-package-homepage) ;string (readme juliahub-package-readme) ;string (version juliahub-package-version) ;string (description juliahub-package-description) ;string (dependencies juliahub-package-dependencies "deps" json->juliahub-dependencies) ;list of (url juliahub-package-url) ;string (uuid juliahub-package-uuid) ;string (license juliahub-package-license)) ;string (define-json-mapping make-juliahub-dependency juliahub-dependency? json->juliahub-dependency (direct? juliahub-dependency-direct? "direct") ;boolean (name juliahub-dependency-name) ;string (uuid juliahub-dependency-uuid) ;string (versions juliahub-dependency-versions "versions" vector->list)) ;list of strings (define (julia-name->guix-name name) (string-append "julia-" (snake-case name))) (define* (juliahub-fetch name #:key (version #f)) "Return a record for package NAME, or #f on failure." (let ((url (if version (string-append "https://docs.juliahub.com/" name "/" (car (juliahub-slug+version name)) "/" version "/pkg.json") (string-append (juliahub-url name) "pkg.json")))) (and=> (json-fetch url) json->juliahub-package))) (define (make-julia-sexp name source home-page synopsis description direct-dependencies test-dependencies-names licenses) "Return the `package' s-expression for a Julia package with the given NAME, VERSION, URI, HASH, HOME-PAGE, DESCRIPTION, DIRECT-DEPENDENCIES, TEST-DEPENDENCIES-NAMES and LICENSES." `(package (name ,(julia-name->guix-name name)) (version ,version) (source ,source) (build-system julia-build-system) ,@(if (null? direct-dependencies) '() `((propagated-inputs (list ,@(map (compose julia-name->guix-name juliahub-dependency-name) direct-dependencies))))) ,@(if (null? test-dependencies-names) '() `((native-inputs (list ,@(map julia-name->guix-name test-dependencies-names))))) (synopsis ,synopsis) (description ,description) (home-page ,home-page) (license ,(match licenses (() #f) ((license) (license->symbol license)) (_ `(list ,@(map license->symbol licenses))))))) (define* (juliahub->guix-package package-name #:key version #:allow-other-keys) "Fetch the metadata for PACKAGE-NAME from juliahub.org, and return the `package' s-expression corresponding to that package, or #f on failure. Optionally include a VERSION string to fetch a specific version juliahub." (let* ((package.toml (ini-fetch (general-url package-name "Package.toml"))) (subdir (assoc-ref package.toml 'subdir)) (tag (latest-git-tag (assoc-ref package.toml 'repo))) (package (if version (juliahub-fetch package-name #:version version) (if tag (juliahub-fetch package-name #:version tag) (juliahub-fetch package-name))))) (if package (let-values (((source directory) (git->origin+dir (juliahub-package-url package) `(tag-or-commit . ,(string-append "v" (juliahub-package-version package)))))) (let* ((direct-dependencies (filter juliahub-dependency-direct? (juliahub-package-dependencies package))) (dependencies-names (map juliahub-dependency-name direct-dependencies)) (licenses (map spdx-string->license (list (juliahub-package-license package)))) (test-dependencies-names (if subdir (parse-test-dependencies (string-append subdir "/" directory)) (parse-test-dependencies directory)))) (values (make-julia-sexp package-name source (juliahub-package-homepage package) (juliahub-package-description package) (beautify-description (juliahub-package-readme package)) direct-dependencies test-dependencies-names licenses) (append dependencies-names test-dependencies-names)))) (values #f '())))) (define (guix-package->juliahub-name package) (let* ((url (juliahub-package-url package)) (git-name (last (string-split url #\/))) (ungitted-name (if (string-suffix? ".git" git-name) (string-drop-right git-name 4) git-name)) (package-name (if (string-suffix? ".jl" ungitted-name) (string-drop-right ungitted-name 4) ungitted-name))) package-name)) (define* (import-release package #:key (version #f)) "Return an for the latest release of PACKAGE." (let* ((package-name (guix-package->juliahub-name package)) (package (juliahub-fetch package-name)) (version (or version (juliahub-package-version package)))) (upstream-source (package (package-name package)) (version version) (urls (list (juliahub-package-url package)))))) (define %juliahub-updater (upstream-updater (name 'juliahub) (description "Updater for Juliahub packages") (pred juliahub-package?) (import import-release))) (define* (juliahub-recursive-import package-name #:optional version) (recursive-import package-name #:repo '() #:repo->guix-package juliahub->guix-package #:guix-name julia-name->guix-name #:version version))