at master 10 kB view raw
1(defpackage org.lispbuilds.nix/repository/quicklisp 2 (:use :cl) 3 (:import-from :dex) 4 (:import-from :alexandria :read-file-into-string :ensure-list) 5 (:import-from :arrow-macros :->>) 6 (:import-from :str) 7 (:import-from 8 :org.lispbuilds.nix/database/sqlite 9 :sqlite-database 10 :init-db 11 :database-url 12 :init-file) 13 (:import-from 14 :org.lispbuilds.nix/api 15 :import-lisp-packages) 16 (:import-from 17 :org.lispbuilds.nix/util 18 :replace-regexes) 19 (:export :quicklisp-repository) 20 (:local-nicknames 21 (:json :com.inuoe.jzon))) 22 23(in-package org.lispbuilds.nix/repository/quicklisp) 24 25(defclass quicklisp-repository () 26 ((dist-url :initarg :dist-url 27 :reader dist-url 28 :initform (error "dist url required")))) 29 30(defun clear-line () 31 (write-char #\Return *error-output*) 32 (write-char #\Escape *error-output*) 33 (write-char #\[ *error-output*) 34 (write-char #\K *error-output*)) 35 36(defun status (&rest format-args) 37 (clear-line) 38 (apply #'format (list* *error-output* format-args)) 39 (force-output *error-output*)) 40 41;; TODO: This should not know about the imported.nix file. 42(defun init-tarball-hashes (database) 43 (status "no packages.sqlite - will pre-fill tarball hashes from ~A to save time~%" 44 (truename "imported.nix")) 45 (let* ((lines (uiop:read-file-lines "imported.nix")) 46 (lines (remove-if-not 47 (lambda (line) 48 (let ((trimmed (str:trim-left line))) 49 (or (str:starts-with-p "url = " trimmed) 50 (str:starts-with-p "sha256 = " trimmed)))) 51 lines)) 52 (lines (mapcar 53 (lambda (line) 54 (multiple-value-bind (whole groups) 55 (ppcre:scan-to-strings "\"\(.*\)\"" line) 56 (declare (ignore whole)) 57 (svref groups 0))) 58 lines))) 59 (sqlite:with-open-database (db (database-url database)) 60 (init-db db (init-file database)) 61 (sqlite:with-transaction db 62 (loop while lines do 63 (sqlite:execute-non-query db 64 "insert or ignore into sha256(url,hash) values (?,?)" 65 (prog1 (first lines) (setf lines (rest lines))) 66 (prog1 (first lines) (setf lines (rest lines)))))) 67 (status "OK, imported ~A hashes into DB.~%" 68 (sqlite:execute-single db 69 "select count(*) from sha256"))))) 70 71(defparameter *broken-systems* 72 '( 73 ;; Infinite recursion through dependencies in 2024-10-12 dist 74 "cl-quil" "qvm" 75 ) 76 "List of broken systems, which should be omitted from the package graph") 77 78(defmethod import-lisp-packages ((repository quicklisp-repository) 79 (database sqlite-database)) 80 81 ;; If packages.sqlite is missing, we should populate the sha256 82 ;; table to speed things up. 83 (unless (probe-file (database-url database)) 84 (init-tarball-hashes database)) 85 86 (let* ((db (sqlite:connect (database-url database))) 87 (systems-url (str:concat (dist-url repository) "systems.txt")) 88 (releases-url (str:concat (dist-url repository) "releases.txt")) 89 (systems-lines (rest (butlast (str:split #\Newline (dex:get systems-url))))) 90 (releases-lines (rest (butlast (str:split #\Newline (dex:get releases-url)))))) 91 92 (flet ((sql-query (sql &rest params) 93 (apply #'sqlite:execute-to-list (list* db sql params)))) 94 95 ;; Ensure database schema 96 (init-db db (init-file database)) 97 98 ;; Prepare temporary tables for efficient access 99 (sql-query "create temp table if not exists quicklisp_system 100 (project, asd, name unique, deps)") 101 102 (sql-query "create temp table if not exists quicklisp_release 103 (project unique, url, size, md5, sha1, prefix not null, asds)") 104 105 (sqlite:with-transaction db 106 (dolist (line systems-lines) 107 (destructuring-bind (project asd name &rest deps) 108 (str:words line) 109 (sql-query 110 "insert or ignore into quicklisp_system values(?,?,?,?)" 111 project asd name (json:stringify (coerce deps 'vector)))))) 112 113 (sqlite:with-transaction db 114 (dolist (line releases-lines) 115 (destructuring-bind (project http-url size md5 sha1 prefix &rest asds) 116 (str:words line) 117 ;; quicklisp does not support TLS 118 ;; https://github.com/quicklisp/quicklisp-client/issues/167 119 ;; but since we fetch systems using nix we can adapt the url. 120 (let ((url (str:replace-first "http://" "https://" http-url))) 121 (sql-query 122 "insert or ignore into quicklisp_release values(?,?,?,?,?,?,?)" 123 project url size md5 sha1 prefix (json:stringify (coerce 124 asds 125 'vector))))))) 126 127 ;; Weed out circular dependencies from the package graph. 128 (sqlite:with-transaction db 129 (sql-query "create temp table will_delete (root,name)") 130 (loop for (system) in (sql-query "select name from quicklisp_system") do 131 (when (sql-query 132 "with recursive dep(root, name) as ( 133 select s.name, d.value 134 from quicklisp_system s 135 cross join json_each(s.deps) d 136 where s.name = ? 137 union 138 select dep.root, d.value 139 from quicklisp_system s, dep 140 cross join json_each(s.deps) d 141 where s.name = dep.name 142 ) select 1 from dep where name = root" 143 system) 144 (sql-query 145 "with recursive broken(name) as ( 146 select ? 147 union 148 select s.name from quicklisp_system s, broken b 149 where b.name in (select value from json_each(s.deps)) 150 ) insert into will_delete select ?, name from broken" 151 system system))) 152 (loop for (root name) in (sql-query "select root, name from will_delete") do 153 (warn "Circular dependency in '~a': Omitting '~a'" root name) 154 (sql-query "delete from quicklisp_system where name = ?" name))) 155 156 (sqlite:with-transaction db 157 ;; Should these be temp tables, that then get queried by 158 ;; system name? This looks like it uses a lot of memory. 159 (let ((systems 160 (sql-query 161 "with pkgs as ( 162 select 163 name, asd, url, deps, 164 ltrim(replace(prefix, r.project, ''), '-_') as version 165 from quicklisp_system s, quicklisp_release r 166 where s.project = r.project 167 ) 168 select 169 name, version, asd, url, 170 (select json_group_array( 171 json_array(value, (select version from pkgs where name=value)) 172 ) 173 from json_each(deps) 174 where value <> 'asdf') as deps 175 from pkgs" 176 ))) 177 178 ;; First pass: insert system and source tarball informaton. 179 ;; Can't insert dependency information, because this works 180 ;; on system ids in the database and they don't exist 181 ;; yet. Could it be better to just base dependencies on 182 ;; names? But then ACID is lost. 183 (dolist (system systems) 184 (destructuring-bind (name version asd url deps) system 185 (declare (ignore deps)) 186 (status "importing system '~a-~a'" name version) 187 (let ((hash (nix-prefetch-tarball url db))) 188 (sql-query 189 "insert or ignore into system(name,version,asd) values (?,?,?)" 190 name version asd) 191 (sql-query 192 "insert or ignore into sha256(url,hash) values (?,?)" 193 url hash) 194 (sql-query 195 "insert or ignore into src values 196 ((select id from sha256 where url=?), 197 (select id from system where name=? and version=?))" 198 url name version)))) 199 200 ;; Second pass: connect the in-database systems with 201 ;; dependency information 202 (dolist (system systems) 203 (destructuring-bind (name version asd url deps) system 204 (declare (ignore asd url)) 205 (dolist (dep (coerce (json:parse deps) 'list)) 206 (destructuring-bind (dep-name dep-version) (coerce dep 'list) 207 (if (eql dep-version 'NULL) 208 (warn "Bad data in Quicklisp: ~a has no version" dep-name) 209 (sql-query 210 "insert or ignore into dep values 211 ((select id from system where name=? and version=?), 212 (select id from system where name=? and version=?))" 213 name version 214 dep-name dep-version)))))))))) 215 216 (write-char #\Newline *error-output*)) 217 218(defun shell-command-to-string (cmd) 219 ;; Clearing the library path is needed to prevent a bug, where the 220 ;; called subprocess uses a different glibc than the SBCL process 221 ;; is. In that case, the call to execve attempts to load the 222 ;; libraries used by SBCL from LD_LIBRARY_PATH using a different 223 ;; glibc than they expect, which errors out. 224 (let ((ld-library-path (uiop:getenv "LD_LIBRARY_PATH"))) 225 (setf (uiop:getenv "LD_LIBRARY_PATH") "") 226 (unwind-protect 227 (uiop:run-program cmd :output '(:string :stripped t)) 228 (setf (uiop:getenv "LD_LIBRARY_PATH") ld-library-path)))) 229 230(defun nix-prefetch-tarball (url db) 231 (restart-case 232 (compute-sha256 url db) 233 (try-again () 234 :report "Try downloading again" 235 (nix-prefetch-tarball url db)))) 236 237(defun compute-sha256 (url db) 238 (or (sqlite:execute-single db "select hash from sha256 where url=?" url) 239 (let ((sha256 (shell-command-to-string (str:concat "nix-prefetch-url --unpack " url)))) 240 sha256)))