1 ;; -*- lexical-binding: t -*-
3 ;; This is the updater for recipes-archive-melpa.json
6 (require 'semaphore-promise
)
15 (defun alist-set (key value alist
)
21 (defun alist-update (key f alist
)
22 (let ((value (alist-get key alist
)))
24 (cons key
(funcall f value
))
29 (defun process-promise (semaphore program
&rest args
)
30 "Generate an asynchronous process and
31 return Promise to resolve in that process."
33 (semaphore-promise-gated
35 (lambda (resolve reject
)
36 (funcall resolve
(apply #'promise
:make-process program args
))))
39 (defun mangle-name (s)
40 (if (string-match "^[a-zA-Z].*" s
)
44 ;; ## Shell promise + env
47 (with-output-to-string (princ o
)))
49 (defun assocenv (env &rest namevals
)
50 (let ((process-environment (copy-sequence env
)))
52 (setenv (as-string (car e
))
54 (seq-partition namevals
2))
57 (defun shell-promise (semaphore env script
)
58 (semaphore-promise-gated
60 (lambda (resolve reject
)
61 (let ((process-environment env
))
62 (funcall resolve
(promise:make-shell-command script
))))))
66 ;; ## Previous Archive Reader
68 (defun previous-commit (index ename variant
)
69 (when-let (pdesc (and index
(gethash ename index
)))
70 (when-let (desc (and pdesc
(gethash variant pdesc
)))
71 (gethash 'commit desc
))))
73 (defun previous-sha256 (index ename variant
)
74 (when-let (pdesc (and index
(gethash ename index
)))
75 (when-let (desc (and pdesc
(gethash variant pdesc
)))
76 (gethash 'sha256 desc
))))
78 (defun parse-previous-archive (filename)
79 (let ((idx (make-hash-table :test
'equal
)))
81 (let ((json-object-type 'hash-table
)
82 (json-array-type 'list
)
83 (json-key-type 'symbol
))
84 (json-read-file filename
))
85 do
(puthash (gethash 'ename desc
)
91 ;; (defun latest-git-revision (url)
92 ;; (process-promise "git" "ls-remote" url))
94 (defun prefetch (semaphore fetcher repo commit
)
96 (apply 'process-promise
99 ("github" (list "nix-prefetch-url"
100 "--unpack" (concat "https://github.com/" repo
"/archive/" commit
".tar.gz")))
101 ("gitlab" (list "nix-prefetch-url"
102 "--unpack" (concat "https://gitlab.com/api/v4/projects/"
103 (url-hexify-string repo
)
104 "/repository/archive.tar.gz?ref="
106 ("sourcehut" (list "nix-prefetch-url"
107 "--unpack" (concat "https://git.sr.ht/~" repo
"/archive/" commit
".tar.gz")))
108 ("codeberg" (list "nix-prefetch-url"
109 "--unpack" (concat "https://codeberg.org/" repo
"/archive/" commit
".tar.gz")))
110 ("bitbucket" (list "nix-prefetch-hg"
111 (concat "https://bitbucket.com/" repo
) commit
))
112 ("hg" (list "nix-prefetch-hg"
114 ("git" (list "nix-prefetch-git"
118 (_ (throw 'unknown-fetcher fetcher
))))
121 ("git" (alist-get 'sha256
(json-read-from-string res
)))
122 (_ (car (split-string res
)))))))
124 (defun source-sha (semaphore ename eprops aprops previous variant
)
125 (let* ((fetcher (alist-get 'fetcher eprops
))
126 (url (alist-get 'url eprops
))
127 (repo (alist-get 'repo eprops
))
128 (commit (gethash 'commit aprops
))
129 (prev-commit (previous-commit previous ename variant
))
130 (prev-sha256 (previous-sha256 previous ename variant
)))
131 (if (and commit prev-sha256
132 (equal prev-commit commit
))
134 (message "INFO: %s: re-using %s %s" ename prev-commit prev-sha256
)
135 (promise-resolve `((sha256 .
,prev-sha256
))))
136 (if (and commit
(or repo url
))
138 (prefetch semaphore fetcher
(or repo url
) commit
)
140 (message "INFO: %s: prefetched repository %s %s" ename commit sha256
)
141 `((sha256 .
,sha256
)))
143 (message "ERROR: %s: during prefetch %s" ename err
)
147 (message "ERROR: %s: no commit information" ename
)
149 `((error .
"No commit information"))))))))
151 (defun source-info (recipe archive source-sha
)
152 (let* ((esym (car recipe
))
153 (ename (symbol-name esym
))
154 (eprops (cdr recipe
))
155 (aentry (gethash esym archive
))
156 (version (and aentry
(gethash 'ver aentry
)))
157 (deps (when-let (deps (gethash 'deps aentry
))
158 (remove 'emacs
(hash-table-keys deps
))))
159 (aprops (and aentry
(gethash 'props aentry
)))
160 (commit (gethash 'commit aprops
)))
161 (append `((version .
,version
))
162 (when (< 0 (length deps
))
163 `((deps .
,(sort deps
'string
<))))
164 `((commit .
,commit
))
167 (defun recipe-info (recipe-index ename
)
168 (if-let (desc (gethash ename recipe-index
))
169 (cl-destructuring-bind (rcp-commit . rcp-sha256
) desc
170 `((commit .
,rcp-commit
)
171 (sha256 .
,rcp-sha256
)))
172 `((error .
"No recipe info"))))
174 (defun start-fetch (semaphore recipe-index-promise recipes unstable-archive stable-archive previous
)
176 (mapcar (lambda (entry)
177 (let* ((esym (car entry
))
178 (ename (symbol-name esym
))
180 (fetcher (alist-get 'fetcher eprops
))
181 (url (alist-get 'url eprops
))
182 (repo (alist-get 'repo eprops
))
184 (unstable-aentry (gethash esym unstable-archive
))
185 (unstable-aprops (and unstable-aentry
(gethash 'props unstable-aentry
)))
186 (unstable-commit (and unstable-aprops
(gethash 'commit unstable-aprops
)))
188 (stable-aentry (gethash esym stable-archive
))
189 (stable-aprops (and stable-aentry
(gethash 'props stable-aentry
)))
190 (stable-commit (and stable-aprops
(gethash 'commit stable-aprops
)))
192 (unstable-shap (if unstable-aprops
193 (source-sha semaphore ename eprops unstable-aprops previous
'unstable
)
194 (promise-resolve nil
)))
195 (stable-shap (if (equal unstable-commit stable-commit
)
198 (source-sha semaphore ename eprops stable-aprops previous
'stable
)
199 (promise-resolve nil
)))))
202 (promise-all (list recipe-index-promise unstable-shap stable-shap
))
204 (seq-let [recipe-index unstable-sha stable-sha
] res
205 (append `((ename .
,ename
))
206 (if-let (desc (gethash ename recipe-index
))
207 (cl-destructuring-bind (rcp-commit . rcp-sha256
) desc
208 (append `((commit .
,rcp-commit
)
209 (sha256 .
,rcp-sha256
))
210 (when (not unstable-aprops
)
211 (message "ERROR: %s: not in archive" ename
)
212 `((error .
"Not in archive")))))
213 `((error .
"No recipe info")))
214 `((fetcher .
,fetcher
))
215 (if (or (equal "github" fetcher
)
216 (equal "bitbucket" fetcher
)
217 (equal "gitlab" fetcher
)
218 (equal "sourcehut" fetcher
)
219 (equal "codeberg" fetcher
))
222 (when unstable-aprops
`((unstable .
,(source-info entry unstable-archive unstable-sha
))))
223 (when stable-aprops
`((stable .
,(source-info entry stable-archive stable-sha
))))))))))
228 (defun emit-json (prefetch-semaphore recipe-index-promise recipes archive stable-archive previous
)
233 (sort recipes
(lambda (a b
)
235 (symbol-name (car a
))
236 (symbol-name (car b
)))))
237 archive stable-archive
239 (lambda (descriptors)
240 (message "Finished downloading %d descriptors" (length descriptors
))
241 (let ((buf (generate-new-buffer "*recipes-archive*")))
242 (with-current-buffer buf
243 ;; (switch-to-buffer buf)
246 (let ((json-encoding-pretty-print t
)
247 (json-encoding-default-indentation " "))
248 (json-encode descriptors
)))
253 (defun http-get (url parser
)
255 (lambda (resolve reject
)
258 (funcall resolve
(condition-case err
260 (url-http-parse-headers)
261 (goto-char url-http-end-of-headers
)
262 (message (buffer-substring (point-min) (point)))
264 (funcall reject err
))))))))
266 (defun json-read-buffer (buffer)
267 (with-current-buffer buffer
272 (defun error-count (recipes-archive)
276 (alist-get 'error desc
))
279 ;; (error-count (json-read-buffer "recipes-archive-melpa.json"))
281 (defun latest-recipe-commit (semaphore repo base-rev recipe
)
283 semaphore
(assocenv process-environment
287 "exec git log --first-parent -n1 --pretty=format:%H $BASE_REV -- recipes/$RECIPE"))
289 (defun latest-recipe-sha256 (semaphore repo base-rev recipe
)
292 semaphore
(assocenv process-environment
296 "exec nix-hash --flat --type sha256 --base32 <(
298 git ls-tree $BASE_REV recipes/$RECIPE | cut -f1 | cut -d' ' -f3
303 (split-string res
)))))
305 (defun index-recipe-commits (semaphore repo base-rev recipes
)
308 (mapcar (lambda (recipe)
310 (latest-recipe-commit semaphore repo base-rev recipe
)
311 (let ((sha256p (latest-recipe-sha256 semaphore repo base-rev recipe
)))
313 (promise-then sha256p
315 (message "Indexed Recipe %s %s %s" recipe commit sha256
)
316 (cons recipe
(cons commit sha256
))))))))
318 (lambda (rcp-commits)
319 (let ((idx (make-hash-table :test
'equal
)))
321 (puthash (car rcpc
) (cdr rcpc
) idx
))
325 (defun with-melpa-checkout (resolve)
326 (let ((tmpdir (make-temp-file "melpa-" t
)))
330 (semaphore-create 1 "dummy")
331 (assocenv process-environment
"MELPA_DIR" tmpdir
)
334 git remote add origin https://github.com/melpa/melpa.git
335 git fetch origin) 1>&2
338 (message "Created melpa checkout %s" dir
)
339 (funcall resolve dir
)))
341 (delete-directory tmpdir t
)
342 (message "Deleted melpa checkout %s" tmpdir
)))))
344 (defun list-recipes (repo base-rev
)
346 (shell-promise nil
(assocenv process-environment
349 "git ls-tree --name-only $BASE_REV recipes/")
357 (defvar recipe-indexp
)
360 (defun run-updater ()
361 (message "Turning off logging to *Message* buffer")
362 (setq message-log-max nil
)
363 (setenv "GIT_ASKPASS")
364 (setenv "SSH_ASKPASS")
365 (setq process-adaptive-read-buffering nil
)
367 ;; Indexer and Prefetcher run in parallel
375 (list-recipes repo
"origin/master")
376 (lambda (recipe-names)
377 (promise:make-thread
#'index-recipe-commits
378 ;; The indexer runs on a local git repository,
379 ;; so it is CPU bound.
380 ;; Adjust for core count + 2
381 (semaphore-create 6 "local-indexer")
383 ;; (seq-take recipe-names 20)
386 (message "Indexed Recipes: %d" (hash-table-count res
))
387 (defvar recipe-index res
)
390 (message "ERROR: %s" err
))))))
392 ;; Prefetcher + Emitter
395 (promise-then (promise-all
396 (list (http-get "https://melpa.org/recipes.json"
398 (let ((json-object-type 'alist
)
399 (json-array-type 'list
)
400 (json-key-type 'symbol
))
402 (http-get "https://melpa.org/archive.json"
404 (let ((json-object-type 'hash-table
)
405 (json-array-type 'list
)
406 (json-key-type 'symbol
))
408 (http-get "https://stable.melpa.org/archive.json"
410 (let ((json-object-type 'hash-table
)
411 (json-array-type 'list
)
412 (json-key-type 'symbol
))
415 (message "Finished download")
416 (seq-let [recipes-content archive-content stable-archive-content
] resolved
417 ;; The prefetcher is network bound, so 64 seems a good estimate
418 ;; for parallel network connections
419 (promise:make-thread
#'emit-json
(semaphore-create 64 "prefetch-pool")
423 stable-archive-content
424 (parse-previous-archive "recipes-archive-melpa.json")))))
426 (with-current-buffer buf
427 (write-file "recipes-archive-melpa.json")))
429 (message "ERROR: %s" err
))))
434 (promise-finally archivep
436 ;; (message "Joining threads %s" (all-threads))
437 ;; (mapc (lambda (thr)
438 ;; (when (not (eq thr (current-thread)))
439 ;; (thread-join thr)))