racket/racket/collects/pkg/private/install.rkt
Matthew Flatt 8498eff8ef raco pkg {install,update}: track Git repo source from a catalog
When a package is installed via a catalog, and the source provided
by the catalog is a Git repostory, then allow `--clone` to use
that repository when just the package name is given.
2014-11-23 06:31:01 -07:00

1161 lines
52 KiB
Racket

#lang racket/base
(require racket/file
racket/path
racket/list
racket/format
racket/match
racket/string
racket/set
racket/function
openssl/sha1
compiler/compilation-path
version/utils
setup/link
"../path.rkt"
"../name.rkt"
"stage.rkt"
"remove.rkt"
"desc.rkt"
"path.rkt"
"pkg-db.rkt"
"params.rkt"
"print.rkt"
"metadata.rkt"
"dep.rkt"
"get-info.rkt"
"catalog.rkt"
"dirs.rkt"
"collects.rkt"
"addl-installs.rkt"
"repo-path.rkt"
"orig-pkg.rkt"
"git.rkt")
(provide pkg-install
pkg-update)
(define (checksum-for-pkg-source pkg-source type pkg-name given-checksum download-printf)
(case type
[(file-url dir-url github git clone)
(or given-checksum
(remote-package-checksum `(url ,pkg-source) download-printf pkg-name #:type type))]
[(file)
(define checksum-pth (format "~a.CHECKSUM" pkg-source))
(or (and (file-exists? checksum-pth)
(file->string checksum-pth))
(and (file-exists? pkg-source)
(call-with-input-file* pkg-source sha1)))]
[(name)
(or given-checksum
(remote-package-checksum `(catalog ,pkg-source) download-printf pkg-name #:type type))]
[else given-checksum]))
(define (disallow-package-path-overlaps pkg-name
pkg-path
path-pkg-cache
simultaneous-installs)
(define simple-pkg-path (simple-form-path pkg-path))
(define (one-in-the-other? p1 p2)
(define pe (explode-path p1))
(define e (explode-path p2))
(if ((length e) . < . (length pe))
(equal? (take pe (length e)) e)
(equal? (take e (length pe)) pe)))
;; Check collects:
(for ([c (in-list (current-library-collection-paths))])
(when (one-in-the-other? simple-pkg-path
(simple-form-path c))
(pkg-error (~a "cannot link a directory that overlaps with a collection path\n"
" collection path: ~a\n"
" link path: ~a\n"
" as package: ~a")
c
pkg-path
pkg-name)))
;; Check installed packages:
(for ([f (in-directory simple-pkg-path)])
(define found-pkg (path->pkg f #:cache path-pkg-cache))
(when (and found-pkg
(not (equal? found-pkg pkg-name)))
(pkg-error (~a "cannot link a directory that overlaps with existing packages\n"
" existing package: ~a\n"
" overlapping path: ~a\n"
" a package: ~a")
found-pkg
f
pkg-name)))
;; Check simultaneous installs:
(for ([(other-pkg other-dir) (in-hash simultaneous-installs)])
(unless (equal? other-pkg pkg-name)
(when (one-in-the-other? simple-pkg-path
(simple-form-path other-dir))
(pkg-error (~a "cannot link directories that overlap for different packages\n"
" package: ~a\n"
" path: ~a\n"
" overlapping package: ~a\n"
" overlapping path: ~a")
pkg-name
pkg-path
other-pkg
other-dir)))))
(define (ask question)
(let loop ()
(printf question)
(printf " [Y/n/a/?] ")
(flush-output)
(match (string-trim (read-line (current-input-port) 'any))
[(or "y" "Y" "")
'yes]
[(or "n" "N")
'no]
[(or "a" "A")
'always-yes]
[x
(eprintf "Invalid answer: ~a\n" x)
(eprintf " Answer nothing or `y' or `Y' for \"yes\", `n' or `N' for \"no\", or\n")
(eprintf " `a' or `A' for \"yes for all\".\n")
(loop)])))
(define (format-deps update-deps)
(format-list (for/list ([ud (in-list update-deps)])
(cond
[(pkg-desc? ud)
(pkg-desc-name ud)]
[(string? ud)
ud]
[else
(format "~a (have ~a, need ~a)"
(car ud)
(caddr ud)
(cadddr ud))]))))
(define (install-packages
#:old-infos old-infos
#:old-descs old-descs
#:pre-succeed pre-succeed
#:dep-behavior dep-behavior
#:update-deps? update-deps?
#:update-implies? update-implies?
#:update-cache update-cache
#:updating? updating?
#:ignore-checksums? ignore-checksums?
#:use-cache? use-cache?
#:skip-installed? skip-installed?
#:force? force?
#:all-platforms? all-platforms?
#:quiet? quiet?
#:from-command-line? from-command-line?
#:conversation conversation
#:strip strip-mode
#:force-strip? force-strip?
#:link-dirs? link-dirs?
#:local-docs-ok? local-docs-ok?
#:ai-cache ai-cache
descs)
(define download-printf (if quiet? void printf/flush))
(define check-sums? (not ignore-checksums?))
(define current-scope-db (read-pkg-db))
(define all-db (merge-pkg-dbs))
(define path-pkg-cache (make-hash))
(define (install-package/outer infos desc info)
(match-define (pkg-desc pkg type orig-name given-checksum auto? pkg-extra-path) desc)
(match-define
(install-info pkg-name orig-pkg pkg-dir git-dir clean? checksum module-paths additional-installs)
info)
(define name? (eq? 'catalog (first orig-pkg)))
(define this-dep-behavior (or dep-behavior
(if name?
'search-ask
'fail)))
(define do-update-deps?
(and update-deps?
(member this-dep-behavior '(search-auto search-ask))))
(define (clean!)
(when clean?
(delete-directory/files pkg-dir)))
(define (show-dependencies deps update? auto?)
(unless quiet?
(printf/flush "The following~a packages are listed as dependencies of ~a~a:~a\n"
(if update? " out-of-date" " uninstalled")
pkg-name
(if (or auto? (eq? conversation 'always-yes))
(format "\nand they will be ~a~a"
(if auto? "automatically " "")
(if update? "updated" "installed"))
"")
(if update?
(format-deps deps)
(format-list deps)))))
(define simultaneous-installs
(for/hash ([i (in-list infos)])
(values (install-info-name i) (install-info-directory i))))
(when (and (pair? orig-pkg)
(or (eq? (car orig-pkg) 'link)
(eq? (car orig-pkg) 'static-link)
(eq? (car orig-pkg) 'clone)))
(disallow-package-path-overlaps pkg-name
(if (eq? (car orig-pkg) 'clone)
git-dir
pkg-dir)
path-pkg-cache
simultaneous-installs))
(cond
[(and (not updating?)
(hash-ref all-db pkg-name #f)
;; Already installed, but can force if the install is for
;; a wider scope:
(not (and (not (hash-ref current-scope-db pkg-name #f))
force?)))
(define existing-pkg-info (hash-ref all-db pkg-name #f))
(cond
[(and (pkg-info-auto? existing-pkg-info)
(not (pkg-desc-auto? desc))
;; Don't confuse a promotion request with a different-source install:
(equal? (pkg-info-orig-pkg existing-pkg-info) orig-pkg)
;; Also, make sure it's installed in the scope that we're changing:
(hash-ref current-scope-db pkg-name #f))
;; promote an auto-installed package to a normally installed one
(cons
#f ; no repo change
;; The `do-it` thunk:
(lambda ()
(unless quiet?
(download-printf "Promoting ~a from auto-installed to explicitly installed\n" pkg-name))
(update-pkg-db! pkg-name (update-auto existing-pkg-info #f))))]
[else
;; Fail --- already installed
(clean!)
(cond
[(not (hash-ref current-scope-db pkg-name #f))
(pkg-error (~a "package is currently installed in a wider scope\n"
" package: ~a\n"
" installed scope: ~a\n"
" given scope: ~a")
pkg-name
(find-pkg-installation-scope pkg-name #:next? #t)
(current-pkg-scope))]
[(not (equal? (pkg-info-orig-pkg existing-pkg-info) orig-pkg))
(pkg-error (~a "package is already installed from a different source\n"
" package: ~a\n"
" installed source: ~a\n"
" given source: ~a")
pkg-name
(pkg-info-orig-pkg existing-pkg-info)
orig-pkg)]
[else
(pkg-error "package is already installed\n package: ~a"
pkg-name)])])]
[(and
(not force?)
(for/or ([mp (in-set module-paths)])
;; In an installed collection? Try resolving the path:
(define r (with-handlers ([exn:fail:filesystem:missing-module? (lambda (x) #f)])
((current-module-name-resolver) mp #f #f #f)))
(define f (and r (resolved-module-path-name r)))
(when f
(unless (path? f)
(pkg-error "expected a filesystem path for a resolved module path: ~a" mp)))
;; Check for source or compiled:
(cond
[(and f
(or (file-exists? f)
(file-exists? (path-replace-suffix f #".ss"))
(file-exists? (get-compilation-bytecode-file f))
(file-exists? (get-compilation-bytecode-file (path-replace-suffix f #".ss"))))
(or (not updating?)
(not (equal? pkg-name (path->pkg f #:cache path-pkg-cache)))))
;; This module is already installed
(cons (path->pkg f #:cache path-pkg-cache) mp)]
[else
;; Compare with simultaneous installs
(for/or ([other-pkg-info (in-list infos)]
#:unless (eq? other-pkg-info info))
(and (set-member? (install-info-module-paths other-pkg-info) mp)
(cons (install-info-name other-pkg-info)
mp)))])))
=>
(λ (conflicting-pkg*mp)
(clean!)
(match-define (cons conflicting-pkg mp) conflicting-pkg*mp)
(if conflicting-pkg
(pkg-error (~a "packages ~aconflict\n"
" package: ~a\n"
" package: ~a\n"
" module path: ~s")
(if (equal? conflicting-pkg pkg-name)
"in different scopes "
"")
pkg conflicting-pkg (pretty-module-path mp))
(pkg-error (~a "package conflicts with existing installed module\n"
" package: ~a\n"
" module path: ~s")
pkg (pretty-module-path mp))))]
[(and
(not force?)
(for/or ([ai (in-set additional-installs)])
;; Check for source or compiled:
(cond
;; If `local-docs-ok?`, exempt doc collisions for user-scope install, since
;; user-scope documentation is rendered within the package:
[(and local-docs-ok?
(eq? (car ai) 'doc)
(eq? (current-pkg-scope) 'user))
#f]
[(set-member? (get-additional-installed (car ai)
simultaneous-installs
ai-cache
metadata-ns
path-pkg-cache)
ai)
;; This item is already installed
(cons #f ai)]
[else
;; Compare with simultaneous installs
(for/or ([other-pkg-info (in-list infos)]
#:unless (eq? other-pkg-info info))
(and (set-member? (install-info-additional-installs other-pkg-info) ai)
(cons (install-info-name other-pkg-info)
ai)))])))
=>
(λ (conflicting-pkg*ai)
(clean!)
(match-define (cons conflicting-pkg ai) conflicting-pkg*ai)
(if conflicting-pkg
(pkg-error (~a "packages ~aconflict\n"
" package: ~a\n"
" package: ~a\n"
" item category: ~a\n"
" item name: ~s")
(if (equal? conflicting-pkg pkg-name)
"in different scopes "
"")
pkg conflicting-pkg
(car ai)
(cdr ai))
(pkg-error (~a "package conflicts with existing installed item\n"
" package: ~a\n"
" item category: ~a\n"
" item name: ~s")
pkg
(car ai)
(cdr ai))))]
[(and
(not (eq? dep-behavior 'force))
(let ()
(define deps (get-all-deps metadata-ns pkg-dir))
(define unsatisfied-deps
(map dependency->source
(filter-not (λ (dep)
(define name (dependency->name dep))
(or (equal? name "racket")
(not (or all-platforms?
(dependency-this-platform? dep)))
(hash-ref simultaneous-installs name #f)
(hash-has-key? all-db name)))
deps)))
(and (not (empty? unsatisfied-deps))
unsatisfied-deps)))
=>
(λ (unsatisfied-deps)
(match this-dep-behavior
['fail
(clean!)
(pkg-error (~a "missing dependencies"
(if from-command-line?
(~a ";\n"
" specify `--deps search-auto' to install them, or\n"
" specify `--deps search-ask' to be asked about installing them")
"")
"\n"
" for package: ~a\n"
" missing packages:~a")
pkg
(format-list unsatisfied-deps))]
['search-auto
;; (show-dependencies unsatisfied-deps #f #t)
(raise (vector updating? infos pkg-name unsatisfied-deps void 'always-yes))]
['search-ask
(show-dependencies unsatisfied-deps #f #f)
(case (if (eq? conversation 'always-yes)
'always-yes
(ask "Would you like to install these dependencies?"))
[(yes)
(raise (vector updating? infos pkg-name unsatisfied-deps void 'again))]
[(always-yes)
(raise (vector updating? infos pkg-name unsatisfied-deps void 'always-yes))]
[(no)
(clean!)
(pkg-error "missing dependencies\n missing packages:~a" (format-list unsatisfied-deps))])]))]
[(and
(or do-update-deps?
update-implies?)
(let ()
(define-values (run-deps build-deps) (get-all-deps* metadata-ns pkg-dir))
(define deps (append run-deps build-deps))
(define implies (list->set
(append
(get-all-implies metadata-ns pkg-dir run-deps)
(get-all-update-implies metadata-ns pkg-dir deps))))
(define update-pkgs
(append-map (λ (dep)
(define name (dependency->name dep))
(define this-platform? (or all-platforms?
(dependency-this-platform? dep)))
(or (and this-platform?
(or do-update-deps?
(set-member? implies name))
(not (hash-ref simultaneous-installs name #f))
((packages-to-update download-printf current-scope-db
#:must-update? #f
#:deps? do-update-deps?
#:implies? update-implies?
#:update-cache update-cache
#:namespace metadata-ns
#:all-platforms? all-platforms?
#:ignore-checksums? ignore-checksums?
#:use-cache? use-cache?
#:from-command-line? from-command-line?
#:link-dirs? link-dirs?)
name))
null))
deps))
(and (not (empty? update-pkgs))
update-pkgs
(let ()
(define (continue conversation)
(raise (vector #t infos pkg-name update-pkgs
(λ () (for-each (compose (remove-package quiet?) pkg-desc-name) update-pkgs))
conversation)))
(match (if (andmap (lambda (dep) (set-member? implies (pkg-desc-name dep)))
update-pkgs)
'search-auto
this-dep-behavior)
['search-auto
(show-dependencies update-pkgs #t #t)
(continue conversation)]
['search-ask
(show-dependencies update-pkgs #t #f)
(case (if (eq? conversation 'always-yes)
'always-yes
(ask "Would you like to update these dependencies?"))
[(yes)
(continue 'again)]
[(always-yes)
(continue 'always-yes)]
[(no)
;; Don't fail --- just skip update
#f])])))))
(error "internal error: should have raised an exception")]
[(and
(not (eq? dep-behavior 'force))
(let ()
(define deps (get-all-deps metadata-ns pkg-dir))
(define update-deps
(filter-map (λ (dep)
(define name (dependency->name dep))
(define req-vers (dependency->version dep))
(define this-platform? (or all-platforms?
(dependency-this-platform? dep)))
(define-values (inst-vers* can-try-update?)
(cond
[(not this-platform?)
(values #f #f)]
[(not req-vers)
(values #f #f)]
[(equal? name "racket")
(values (version) #f)]
[(hash-ref simultaneous-installs name #f)
=> (lambda (dir)
(values
(get-metadata metadata-ns dir
'version (lambda () "0.0"))
#f))]
[else
(values (get-metadata metadata-ns (pkg-directory** name)
'version (lambda () "0.0"))
#t)]))
(define inst-vers (if (and this-platform?
req-vers
(not (and (string? inst-vers*)
(valid-version? inst-vers*))))
(begin
(log-pkg-error
"bad verson specification for ~a: ~e"
name
inst-vers*)
"0.0")
inst-vers*))
(and this-platform?
req-vers
((version->integer req-vers)
. > .
(version->integer inst-vers))
(list name can-try-update? inst-vers req-vers)))
deps))
(and (not (empty? update-deps))
update-deps)))
=> (lambda (update-deps)
(define (report-mismatch update-deps)
(define multi? (1 . < . (length update-deps)))
(pkg-error (~a "version mismatch for dependenc~a\n"
" for package: ~a\n"
" mismatch packages:~a")
(if multi? "ies" "y")
pkg
(format-deps update-deps)))
;; If there's a mismatch that we can't attempt to update, complain.
(unless (andmap cadr update-deps)
(report-mismatch (filter (compose not cadr) update-deps)))
;; Try updates:
(define update-pkgs (map car update-deps))
(define (make-pre-succeed)
(define db current-scope-db)
(let ([to-update (append-map (packages-to-update download-printf db
#:deps? update-deps?
#:implies? update-implies?
#:update-cache update-cache
#:namespace metadata-ns
#:all-platforms? all-platforms?
#:ignore-checksums? ignore-checksums?
#:use-cache? use-cache?
#:from-command-line? from-command-line?
#:link-dirs? link-dirs?)
update-pkgs)])
(λ () (for-each (compose (remove-package quiet?) pkg-desc-name) to-update))))
(match this-dep-behavior
['fail
(clean!)
(report-mismatch update-deps)]
['search-auto
(show-dependencies update-deps #t #t)
(raise (vector #t infos pkg-name update-pkgs (make-pre-succeed) 'always-yes))]
['search-ask
(show-dependencies update-deps #t #f)
(case (if (eq? conversation 'always-yes)
'always-yes
(ask "Would you like to update these dependencies?"))
[(yes)
(raise (vector #t infos pkg-name update-pkgs (make-pre-succeed) 'again))]
[(always-yes)
(raise (vector #t infos pkg-name update-pkgs (make-pre-succeed) 'always-yes))]
[(no)
(clean!)
(report-mismatch update-deps)])]))]
[else
(cons
;; The repo to get new commits, if any:
(and git-dir
(list (enclosing-path-for-repo (caddr orig-pkg) git-dir)
checksum))
;; The "do-it" function (see `repos+do-its` below):
(λ ()
(when updating?
(download-printf "Re-installing ~a\n" pkg-name))
(define final-pkg-dir
(cond
[clean?
(define final-pkg-dir (or git-dir
(select-package-directory
(build-path (pkg-installed-dir) pkg-name))))
(unless git-dir
(make-parent-directory* final-pkg-dir)
(copy-directory/files pkg-dir final-pkg-dir #:keep-modify-seconds? #t))
(clean!)
final-pkg-dir]
[else
pkg-dir]))
(define single-collect (pkg-single-collection final-pkg-dir
#:name pkg-name
#:namespace post-metadata-ns))
(log-pkg-debug "creating ~alink to ~e"
(if single-collect "single-collection " "")
final-pkg-dir)
(define scope (current-pkg-scope))
(links final-pkg-dir
#:name single-collect
#:user? (not (or (eq? 'installation scope)
(path? scope)))
#:file (scope->links-file scope)
#:root? (not single-collect)
#:static-root? (and (pair? orig-pkg)
(eq? 'static-link (car orig-pkg))))
(define alt-dir-name
;; If we had to pick an alternate dir name, then record it:
(let-values ([(base name dir?) (split-path final-pkg-dir)])
(and (regexp-match? #rx"[+]" name)
(path->string name))))
(define this-pkg-info
(make-pkg-info orig-pkg checksum auto? single-collect alt-dir-name))
(log-pkg-debug "updating db with ~e to ~e" pkg-name this-pkg-info)
(update-pkg-db! pkg-name this-pkg-info)))]))
(define metadata-ns (make-metadata-namespace))
(define infos
(for/list ([v (in-list descs)])
(stage-package/info (pkg-desc-source v) (pkg-desc-type v) (pkg-desc-name v)
#:at-dir (pkg-desc-extra-path v)
#:given-checksum (pkg-desc-checksum v)
#:use-cache? use-cache?
check-sums? download-printf
metadata-ns
#:strip strip-mode
#:force-strip? force-strip?
#:link-dirs? link-dirs?)))
;; For the top-level call, we need to double-check that all provided packages
;; were distinct:
(for/fold ([ht (hash)]) ([i (in-list infos)]
[desc (in-list descs)])
(define name (install-info-name i))
(when (hash-ref ht name #f)
(pkg-error (~a "given package sources have the same package name\n"
" package name: ~a\n"
" package source: ~a\n"
" package source: ~a")
name
(pkg-desc-source (hash-ref ht name #f))
(pkg-desc-source desc)))
(hash-set ht name desc))
(define all-descs (append old-descs descs))
(define all-infos (append old-infos infos))
(define repo+do-its ; list of (cons #f-or-(list git-dir checksum) do-it-thunk)
(map (curry install-package/outer all-infos)
all-descs
all-infos))
;; collapse planned repo actions, and make sure they don't conflict:
(define repos
(for/fold ([ht (hash)]) ([repo+do-it (in-list repo+do-its)])
(define repo (car repo+do-it))
(cond
[repo
(define git-dir (car repo))
(define checksum (cadr repo))
(define prev-checksum (hash-ref ht git-dir #f))
(when (and prev-checksum
(not (equal? prev-checksum checksum)))
(pkg-error (~a "multiple packages in the same clone have different target commits\n"
" clone: ~a\n"
" commit: ~a\n"
" other commit: ~a")
git-dir
prev-checksum
checksum))
(hash-set ht git-dir checksum)]
[else ht])))
;; relevant commits have been fecthed to the repos, and now we need
;; to check them out; If a checkout fails, then we've left the
;; package installation in no worse shape than if a manual `git
;; pull` failed
(for ([(git-dir checksum) (in-hash repos)])
(parameterize ([current-directory git-dir])
(download-printf "Merging commits at ~a\n"
git-dir)
(git #:status (lambda (s) (download-printf "~a\n" s))
"merge" "--ff-only" checksum)))
;; pre-succeed removes packages that are being updated
(pre-succeed)
(define post-metadata-ns (make-metadata-namespace))
;; moves packages into place and installs links:
(for-each (λ (t) ((cdr t))) repo+do-its)
(define (is-promote? info)
;; if the package name is in `current-scope-db', we must
;; be simply promiting the package, and so it's
;; already set up:
(and (hash-ref current-scope-db (install-info-name info) #f) #t))
(define setup-collects
(let ([db (read-pkg-db)])
(get-setup-collects ((if updating?
(make-close-over-depending (read-pkg-db)
post-metadata-ns
all-platforms?)
values)
(map install-info-name
(if updating?
all-infos
(filter-not is-promote? all-infos))))
db
post-metadata-ns)))
(cond
[(or (null? repo+do-its)
(and (not updating?) (andmap is-promote? all-infos)))
;; No actions, so no setup:
'skip]
[else
setup-collects]))
(define ((make-close-over-depending db metadata-ns all-platforms?) l)
(define setup-pkgs (list->set l))
(define empty-set (set))
(define rev-pkg-deps
(for/fold ([rev (hash)]) ([pkg-name (in-hash-keys db)])
(for/fold ([rev rev]) ([dep (in-list ((package-dependencies metadata-ns db all-platforms?)
pkg-name))])
(hash-update rev dep (lambda (v) (set-add v pkg-name)) empty-set))))
(let loop ([check setup-pkgs] [setup-pkgs setup-pkgs])
;; Find all packages that depend on a package in `check':
(define new-check
(set-subtract (for/fold ([new-check (set)]) ([pkg (in-set check)])
(set-union new-check
(hash-ref rev-pkg-deps pkg empty-set)))
setup-pkgs))
(cond
[(set-empty? new-check)
;; found fixed point:
(set->list setup-pkgs)]
[else
;; more packages to setup and check:
(loop new-check
(set-union setup-pkgs new-check))])))
(define (select-package-directory dir #:counter [counter 0])
(define full-dir (if (zero? counter)
dir
(let-values ([(base name dir?) (split-path dir)])
(define new-name (bytes->path
(bytes-append (path->bytes name)
(string->bytes/utf-8
(~a "+" counter)))))
(if (path? base)
(build-path base new-name)
new-name))))
(cond
[(directory-exists? full-dir)
;; If the directory exists, assume that we'd like to replace it.
;; Maybe the directory couldn't be deleted when a package was
;; uninstalled, and maybe it will work now (because some process
;; has completed on Windows or some other filesystem with locks).
(with-handlers ([exn:fail:filesystem?
(lambda (exn)
(log-pkg-warning "error deleting old directory: ~a"
(exn-message exn))
(select-package-directory dir #:counter (add1 counter)))])
(delete-directory/files full-dir)
;; delete succeeded:
full-dir)]
[else
;; all clear to use the selected name:
full-dir]))
(define (snoc l x)
(append l (list x)))
(define (pkg-install descs
#:old-infos [old-infos empty]
#:old-auto+pkgs [old-descs empty]
#:all-platforms? [all-platforms? #f]
#:force? [force #f]
#:ignore-checksums? [ignore-checksums? #f]
#:strict-doc-conflicts? [strict-doc-conflicts? #f]
#:use-cache? [use-cache? #t]
#:skip-installed? [skip-installed? #f]
#:pre-succeed [pre-succeed void]
#:dep-behavior [dep-behavior #f]
#:update-deps? [update-deps? #f]
#:update-implies? [update-implies? #t]
#:update-cache [update-cache (make-hash)]
#:updating? [updating? #f]
#:quiet? [quiet? #f]
#:from-command-line? [from-command-line? #f]
#:conversation [conversation #f]
#:strip [strip-mode #f]
#:force-strip? [force-strip? #f]
#:link-dirs? [link-dirs? #f]
#:summary-deps [summary-deps empty])
(define new-descs
(remove-duplicates
(if (not skip-installed?)
descs
(let ([db (read-pkg-db)])
(filter (lambda (d)
(define pkg-name
(or (pkg-desc-name d)
(package-source->name (pkg-desc-source d)
(pkg-desc-type d))))
(define i (hash-ref db pkg-name #f))
(or (not i) (pkg-info-auto? i)))
descs)))
pkg-desc=?))
(with-handlers* ([vector?
(match-lambda
[(vector updating? new-infos dep-pkg deps more-pre-succeed conv)
(pkg-install
#:summary-deps (snoc summary-deps (vector dep-pkg deps))
#:old-infos new-infos
#:old-auto+pkgs (append old-descs new-descs)
#:all-platforms? all-platforms?
#:force? force
#:ignore-checksums? ignore-checksums?
#:strict-doc-conflicts? strict-doc-conflicts?
#:use-cache? use-cache?
#:dep-behavior dep-behavior
#:update-deps? update-deps?
#:update-implies? update-implies?
#:update-cache update-cache
#:pre-succeed (lambda () (pre-succeed) (more-pre-succeed))
#:updating? updating?
#:conversation conv
#:strip strip-mode
#:force-strip? force-strip?
(for/list ([dep (in-list deps)])
(if (pkg-desc? dep)
dep
(pkg-desc dep #f #f #f #t #f))))])])
(begin0
(install-packages
#:old-infos old-infos
#:old-descs old-descs
#:all-platforms? all-platforms?
#:force? force
#:ignore-checksums? ignore-checksums?
#:use-cache? use-cache?
#:skip-installed? skip-installed?
#:dep-behavior dep-behavior
#:update-deps? update-deps?
#:update-implies? update-implies?
#:update-cache update-cache
#:pre-succeed pre-succeed
#:updating? updating?
#:quiet? quiet?
#:from-command-line? from-command-line?
#:conversation conversation
#:strip strip-mode
#:force-strip? force-strip?
#:link-dirs? link-dirs?
#:local-docs-ok? (not strict-doc-conflicts?)
#:ai-cache (box #f)
new-descs)
(unless (empty? summary-deps)
(unless quiet?
(printf/flush "The following~a packages were listed as dependencies~a:~a\n"
(if updating? " out-of-date" " uninstalled")
(format "\nand they were ~a~a"
(if (eq? dep-behavior 'search-auto) "automatically " "")
(if updating? "updated" "installed"))
(string-append*
(for/list ([p*ds (in-list summary-deps)])
(match-define (vector n ds) p*ds)
(format "\n dependencies of ~a:~a"
n
(if updating?
(format-deps ds)
(format-list ds)))))))))))
;; Determine packages to update, starting with `pkg-name'. If `pkg-name'
;; needs to be updated, return it in a list. Otherwise, if `deps?',
;; then return a list of dependencies that need to be updated.
;; (If a package needs to be updated, wait until the update
;; has been inspected for further dependencies.)
;; If `must-installed?', then complain if the package is not
;; installed inthe current scope.
;; If `must-update?', then complain if the package is not
;; updatable.
;; The `update-cache' argument is used to cache which packages
;; are already being updated and downloaded checksums.
(define ((packages-to-update download-printf db
#:must-installed? [must-installed? #t]
#:must-update? [must-update? #t]
#:deps? deps?
#:implies? implies?
#:namespace metadata-ns
#:update-cache update-cache
#:all-platforms? all-platforms?
#:ignore-checksums? ignore-checksums?
#:use-cache? use-cache?
#:from-command-line? from-command-line?
#:link-dirs? link-dirs?)
pkg-name)
(cond
[(pkg-desc? pkg-name)
;; Infer the package-source type and name:
(define-values (inferred-name type) (package-source->name+type
(pkg-desc-source pkg-name)
(pkg-desc-type pkg-name)
#:link-dirs? link-dirs?
#:must-infer-name? (not (pkg-desc-name pkg-name))
#:complain complain-about-source))
(define name (or (pkg-desc-name pkg-name)
inferred-name))
;; Check that the package is installed, and get current checksum:
(define info (package-info name #:db db))
(define new-checksum (checksum-for-pkg-source (pkg-desc-source pkg-name)
type
name
(pkg-desc-checksum pkg-name)
download-printf))
(hash-set! update-cache name new-checksum) ; record downloaded checksum
(unless (or ignore-checksums? (not (pkg-desc-checksum pkg-name)))
(unless (equal? (pkg-desc-checksum pkg-name) new-checksum)
(pkg-error (~a "incorrect checksum on package\n"
" package source: ~a\n"
" expected: ~e\n"
" got: ~e")
(pkg-desc-source pkg-name)
(pkg-desc-checksum pkg-name)
new-checksum)))
(if (or (not (equal? (pkg-info-checksum info)
new-checksum))
;; No checksum available => always update
(not new-checksum)
;; Different source => always update
(not (same-orig-pkg? (pkg-info-orig-pkg info)
(desc->orig-pkg type
(pkg-desc-source pkg-name)
(pkg-desc-extra-path pkg-name)))))
;; Update:
(begin
(hash-set! update-cache (pkg-desc-source pkg-name) #t)
(list (pkg-desc (pkg-desc-source pkg-name)
(pkg-desc-type pkg-name)
name
(pkg-desc-checksum pkg-name)
(pkg-desc-auto? pkg-name)
(or (pkg-desc-extra-path pkg-name)
(and (eq? type 'clone)
(current-directory))))))
;; No update needed, but maybe check dependencies:
(if (or deps?
implies?)
((packages-to-update download-printf db
#:must-update? #f
#:deps? deps?
#:implies? implies?
#:update-cache update-cache
#:namespace metadata-ns
#:all-platforms? all-platforms?
#:ignore-checksums? ignore-checksums?
#:use-cache? use-cache?
#:from-command-line? from-command-line?
#:link-dirs? link-dirs?)
name)
null))]
[(eq? #t (hash-ref update-cache pkg-name #f))
;; package is already being updated
null]
;; A string indicates that package source that should be
;; looked up in the installed packages to get the old source
;; for getting the checksum:
[(package-info pkg-name #:db db must-update?)
=>
(lambda (m)
(match-define (pkg-info orig-pkg checksum auto?) m)
(match orig-pkg
[`(,(or 'link 'static-link) ,orig-pkg-dir)
(if must-update?
(pkg-error (~a "cannot update linked packages;\n"
" except with a replacement package source\n"
" package name: ~a\n"
" package source: ~a")
pkg-name
(normalize-path
(path->complete-path orig-pkg-dir (pkg-installed-dir))))
null)]
[`(dir ,_)
(if must-update?
(pkg-error (~a "cannot update packages installed locally;\n"
" except with a replacement package source;\n"
" package was installed via a local directory\n"
" package name: ~a")
pkg-name)
null)]
[`(file ,_)
(if must-update?
(pkg-error (~a "cannot update packages installed locally;\n"
" except with a replacement package source;\n"
" package was installed via a local file\n"
" package name: ~a")
pkg-name)
null)]
[_
(define-values (orig-pkg-source orig-pkg-type orig-pkg-dir)
(if (eq? 'clone (car orig-pkg))
(values (caddr orig-pkg)
'clone
(enclosing-path-for-repo (caddr orig-pkg) (cadr orig-pkg)))
;; It would be better if the type were preseved
;; from install time, but we always make the
;; URL unambigious:
(values (cadr orig-pkg) #f #f)))
(define new-checksum
(or (hash-ref update-cache pkg-name #f)
(remote-package-checksum orig-pkg download-printf pkg-name)))
;; Record downloaded checksum:
(hash-set! update-cache pkg-name new-checksum)
(or (and new-checksum
(not (equal? checksum new-checksum))
(begin
;; Update it:
(hash-set! update-cache pkg-name #t)
;; Flush cache of downloaded checksums, in case
;; there was a race between our checkig and updates on
;; the catalog server:
(clear-checksums-in-cache! update-cache)
(list (pkg-desc orig-pkg-source orig-pkg-type pkg-name #f auto?
orig-pkg-dir))))
(if (or deps? implies?)
;; Check dependencies
(append-map
(packages-to-update download-printf db
#:must-update? #f
#:deps? deps?
#:implies? implies?
#:update-cache update-cache
#:namespace metadata-ns
#:all-platforms? all-platforms?
#:ignore-checksums? ignore-checksums?
#:use-cache? use-cache?
#:from-command-line? from-command-line?
#:link-dirs? link-dirs?)
((package-dependencies metadata-ns db all-platforms?
#:only-implies? (not deps?))
pkg-name))
null))]))]
[else null]))
(define (pkg-update in-pkgs
#:all? [all? #f]
#:dep-behavior [dep-behavior #f]
#:all-platforms? [all-platforms? #f]
#:force? [force? #f]
#:ignore-checksums? [ignore-checksums? #f]
#:strict-doc-conflicts? [strict-doc-conflicts? #f]
#:use-cache? [use-cache? #t]
#:update-deps? [update-deps? #f]
#:update-implies? [update-implies? #t]
#:quiet? [quiet? #f]
#:from-command-line? [from-command-line? #f]
#:strip [strip-mode #f]
#:force-strip? [force-strip? #f]
#:link-dirs? [link-dirs? #f])
(define download-printf (if quiet? void printf))
(define metadata-ns (make-metadata-namespace))
(define db (read-pkg-db))
(define all-mode? (and all? (empty? in-pkgs)))
(define pkgs (cond
[all-mode? (hash-keys db)]
[else in-pkgs]))
(define update-cache (make-hash))
(define to-update (append-map (packages-to-update download-printf db
#:must-update? (not all-mode?)
#:deps? (or update-deps?
all-mode?) ; avoid races
#:implies? update-implies?
#:update-cache update-cache
#:namespace metadata-ns
#:all-platforms? all-platforms?
#:ignore-checksums? ignore-checksums?
#:use-cache? use-cache?
#:from-command-line? from-command-line?
#:link-dirs? link-dirs?)
(map (convert-clone-name-to-clone-repo db)
pkgs)))
(cond
[(empty? pkgs)
(unless quiet?
(printf/flush (~a "No packages given to update"
(if from-command-line?
(~a
";\n use `--all' to update all packages, or run from a package's directory"
"\n to update that package")
"")
"\n")))
'skip]
[(empty? to-update)
(unless quiet?
(printf/flush "No updates available\n"))
'skip]
[else
(unless quiet?
(printf "Updating:\n")
(for ([u (in-list to-update)])
(printf " ~a\n" (pkg-desc-name u)))
(flush-output))
(pkg-install
#:updating? #t
#:pre-succeed (λ () (for-each (compose (remove-package quiet?) pkg-desc-name) to-update))
#:dep-behavior dep-behavior
#:update-deps? update-deps?
#:update-implies? update-implies?
#:update-cache update-cache
#:quiet? quiet?
#:from-command-line? from-command-line?
#:strip strip-mode
#:force-strip? force-strip?
#:all-platforms? all-platforms?
#:force? force?
#:ignore-checksums? ignore-checksums?
#:strict-doc-conflicts? strict-doc-conflicts?
#:use-cache? use-cache?
#:link-dirs? link-dirs?
to-update)]))
;; If `pkg` is a description with the type 'clone, but its syntax
;; matches a ackage name, then infer a repo from the current package
;; installation and return an alternate description.
(define ((convert-clone-name-to-clone-repo db) pkg-name)
(cond
[(and (pkg-desc? pkg-name)
(eq? 'clone (pkg-desc-type pkg-name))
(let-values ([(name type) (package-source->name+type (pkg-desc-source pkg-name) 'name)])
name))
=> (lambda (name)
;; Infer or complain
(define info (package-info name #:db db))
(unless info
(pkg-error (~a "package is not currently installed\n"
" package: ~a")
name))
(define new-pkg-name
(match (pkg-info-orig-pkg info)
[`(clone ,path ,url-str)
(pkg-error (~a "package is already a linked repository clone\n"
" package: ~a")
name)]
[`(catalog ,lookup-name ,url-str)
;; Found a catalog-based installation that can be converted
;; to a clone:
(pkg-desc url-str 'clone name
(pkg-desc-checksum pkg-name)
(pkg-desc-auto? pkg-name)
(pkg-desc-extra-path pkg-name))]
[`(url ,url-str)
(define-values (current-name current-type)
(package-source->name+type url-str #f))
(case current-type
[(git github)
;; found a repo URL
(pkg-desc url-str 'clone name
(pkg-desc-checksum pkg-name)
(pkg-desc-auto? pkg-name)
(pkg-desc-extra-path pkg-name))]
[else #f])]
[else #f]))
(unless new-pkg-name
(pkg-error (~a "package is not currently installed from a repository\n"
" package: ~a\n"
" current installation: ~a")
name
(pkg-info-orig-pkg info)))
new-pkg-name)]
[else pkg-name]))
;; ----------------------------------------
(define (clear-checksums-in-cache! update-cache)
(define l (for/list ([(k v) (in-hash update-cache)]
#:when (string? v))
k))
(for ([k (in-list l)]) (hash-remove! update-cache k)))