#lang racket/base
(require net/url
         json
         openssl/sha1
         racket/contract
         racket/match
         racket/path
         racket/file
         setup/link
         setup/pack
         setup/unpack
         setup/dirs
         setup/collection-name
         racket/port
         racket/list
         racket/function
         racket/dict
         racket/set
         racket/string
         file/untgz
         file/tar
         file/zip
         file/unzip
         file/cache
         setup/getinfo
         setup/dirs
         racket/format
         version/utils
         syntax/modcollapse
         syntax/modread
         "name.rkt"
         "util.rkt"
         "strip.rkt"
         "path.rkt"
         (prefix-in db: "db.rkt"))

(define current-pkg-scope
  (make-parameter 'user (lambda (p)
                          (if (path? p)
                              (simple-form-path p)
                              p))))
(define current-pkg-scope-version
  (make-parameter (get-installation-name)))
(define current-pkg-error
  (make-parameter (lambda args (apply error 'pkg args))))
(define current-no-pkg-db
  (make-parameter #f))
(define current-pkg-catalogs
  (make-parameter #f))

(define current-pkg-download-cache-dir
  (make-parameter #f))
(define current-pkg-download-cache-max-files
  (make-parameter #f))
(define current-pkg-download-cache-max-bytes
  (make-parameter #f))

(define (pkg-error . rest)
  (apply (current-pkg-error) rest))

(define (format-list l)
  (if (null? l)
      " [none]"
      (apply string-append
             (for/list ([v (in-list l)])
               (format "\n   ~a" v)))))

(define (log-exn x what)
  (log-pkg-error (~a "failure ~a\n"
                         "  error: ~s")
                     what
                     (exn-message x)))

(define (printf/flush fmt . args)
  ;; For status reporting, flush immediately after printing
  (apply printf fmt args)
  (flush-output))

(struct pkg-desc (source type name checksum auto?))
(define (pkg-desc=? a b)
  (define (->list a)
    (list (pkg-desc-source a)
          (pkg-desc-type a)
          (pkg-desc-name a)
          (pkg-desc-checksum a)
          (pkg-desc-auto? a)))
  (equal? (->list a) (->list b)))

(define (path->bytes* pkg)
  (cond
    [(path? pkg)
     (path->bytes pkg)]
    [(string? pkg)
     (path->bytes (string->path pkg))]
    [(bytes? pkg)
     pkg]))

(define (directory-path-no-slash pkg)
  (bytes->path (regexp-replace* #rx#"/$" (path->bytes* pkg) #"")))

(define (directory-list* d)
  (append-map
   (λ (pp)
     (define p (build-path d pp))
     (if (directory-exists? p)
       (map (curry build-path pp)
            (directory-list* p))
       (list pp)))
   (directory-list d)))

(define (simple-form-path* p)
  (path->string (simple-form-path p)))

(define (pretty-module-path mod)
  (if (and (list? mod)
           (= 2 (length mod))
           (eq? (car mod) 'lib)
           (regexp-match? #rx"[.]rkt$" (cadr mod)))
      (string->symbol (regexp-replace #rx"[.]rkt$" (cadr mod) ""))
      mod))

(define (untar pkg pkg-dir #:strip-components [strip-components 0])
  (make-directory* pkg-dir)
  (untgz pkg #:dest pkg-dir #:strip-count strip-components))

(define (download-file! url file checksum
                        #:download-printf [download-printf #f]
                        #:use-cache? [use-cache? #t]
                        #:fail-okay? [fail-okay? #f])
  (with-handlers ([exn:fail?
                   (λ (x)
                     (unless fail-okay?
                       (raise x)))])
    (make-parent-directory* file)
    (log-pkg-debug "\t\tDownloading ~a to ~a" (url->string url) file)
    (define (download!)
      (when download-printf
        (download-printf "Downloading ~a\n" (url->string url)))
      (call-with-output-file file
        (λ (op)
          (call/input-url+200
           url
           (λ (ip) (copy-port ip op))))))
    (cond
     [(and checksum use-cache?)
      (cache-file file
                  (list (url->string url) checksum)
                  (get-download-cache-dir)
                  download!
                  #:log-error-string (lambda (s) (log-pkg-error s))
                  #:log-debug-string (lambda (s) (log-pkg-debug s))
                  #:notify-cache-use (lambda (s)
                                       (when download-printf
                                         (download-printf "Using ~a for ~a\n"
                                                          s
                                                          (url->string url))))
                  #:max-cache-files (get-download-cache-max-files)
                  #:max-cache-size (get-download-cache-max-bytes))]
     [else (download!)])))

(define (clean-cache pkg-url checksum)
  (when pkg-url
    ;; Something failed after download, so remove cached file (if any):
    (with-handlers ([exn:fail? void]) ; any error is logged already
      (cache-remove (list (url->string pkg-url) checksum)
                    (get-download-cache-dir)
                    #:log-error-string (lambda (s) (log-pkg-error s))
                    #:log-debug-string (lambda (s) (log-pkg-debug s))))))

(define (pkg-dir config?)
  (define scope (current-pkg-scope))
  (if (and config?
           (eq? scope 'installation))
      (find-config-dir)
      (get-pkgs-dir scope (current-pkg-scope-version))))
(define (pkg-config-file)
  (build-path (pkg-dir #t) "config.rktd"))
(define (pkg-db-file)
  (build-path (pkg-dir #f) "pkgs.rktd"))
(define (pkg-installed-dir)
  (pkg-dir #f))
(define (pkg-lock-file)
  (make-lock-file-name (pkg-db-file)))

(define (get-download-cache-dir)
  (or (current-pkg-download-cache-dir)
      (read-pkg-cfg/def 'download-cache-dir)))
(define (get-download-cache-max-files)
  (or (current-pkg-download-cache-max-files)
      (read-pkg-cfg/def 'download-cache-max-files)))
(define (get-download-cache-max-bytes)
  (or (current-pkg-download-cache-max-bytes)
      (read-pkg-cfg/def 'download-cache-max-bytes)))

(define (make-metadata-namespace)
  (make-base-empty-namespace))

(define (get-pkg-info pkg-dir metadata-ns)
  (with-handlers ([exn:fail? (λ (x)
                                (log-exn x "getting info")
                                #f)])
    (get-info/full pkg-dir 
                   #:namespace metadata-ns
                   #:bootstrap? #t)))

(define (get-metadata metadata-ns pkg-dir key get-default
                      #:checker [checker void])
  (define get-info (get-pkg-info pkg-dir metadata-ns))
  (define v
    (if get-info
        (get-info key get-default)
        (get-default)))
  (checker v)
  v)

(define (package-collections pkg-dir metadata-ns)
  (for/list ([d (directory-list pkg-dir)]
             #:when (directory-exists? (build-path pkg-dir d))
             #:when (std-filter d))
    d))

(define (package-collection-directories pkg-dir metadata-ns)
  (for/list ([c (in-list (package-collections pkg-dir metadata-ns))])
    (build-path pkg-dir c)))

(define (collection-equal? a b)
  (equal? (if (path? a) a (string->path a))
          (if (path? b) b (string->path b))))

(define ((check-dependencies which) deps)
  (unless (and (list? deps)
               (for/and ([dep (in-list deps)])
                 (define (package-source? dep)
                   (and (string? dep)
                        (package-source->name dep)))
                 (define (version? s)
                   (and (string? s)
                        (valid-version? s)))
                 (or (package-source? dep)
                     (and (list? dep)
                          (= 2 (length dep))
                          (package-source? (car dep))
                          (version? (cadr dep)))
                     (and (list? dep)
                          ((length dep) . >= . 1)
                          (odd? (length dep))
                          (package-source? (car dep))
                          (let loop ([saw (hash)] [dep (cdr dep)])
                            (cond 
                             [(null? dep) #t]
                             [(hash-ref saw (car dep) #f) #f]
                             [else
                              (define kw (car dep))
                              (define val (cadr dep))
                              (and
                               (cond
                                [(eq? kw '#:version) (version? val)]
                                [(eq? kw '#:platform)
                                 (or (string? val)
                                     (regexp? val)
                                     (memq val '(unix windows macosx)))]
                                [else #f])
                               (loop (hash-set saw (car dep) #t)
                                     (cddr dep)))]))))))
    (pkg-error (~a "invalid `" which "' specification\n"
                   "  specification: ~e")
               deps)))

(define (get-all-deps metadata-ns pkg-dir)
  (append
   (get-metadata metadata-ns pkg-dir 
                 'deps (lambda () empty)
                 #:checker (check-dependencies 'deps))
   (get-metadata metadata-ns pkg-dir 
                 'build-deps (lambda () empty)
                 #:checker (check-dependencies 'build-deps))))

(define (get-all-implies metadata-ns pkg-dir deps)
  (get-metadata metadata-ns pkg-dir 
                'implies (lambda () empty)
                #:checker (lambda (l)
                            (unless (null? l)
                              (define deps-set (list->set
                                                (map dependency->name deps)))
                              (unless (and (list? l)
                                           (andmap (lambda (v)
                                                     (or (string? v)
                                                         (eq? v 'core)))
                                                   l))
                                (pkg-error (~a "invalid `implies' specification\n"
                                               "  specification: ~e")
                                           l))
                              (unless (andmap (lambda (i)
                                                (or (eq? i 'core)
                                                    (set-member? deps-set i)))
                                              l)
                                (pkg-error (~a "`implies' is not a subset of dependencies\n"
                                               "  specification: ~e")
                                           l))))))

(define (dependency->name dep)
  (package-source->name
   (dependency->source dep)))

(define (dependency->source dep)
  (if (string? dep)
      dep
      (car dep)))

(define (dependency->version dep)
  (cond
   [(string? dep) #f]
   [(null? (cdr dep)) #f]
   [(keyword? (cadr dep))
    (dependency-lookup '#:version dep)]
   [else (cadr dep)]))

(define (dependency-lookup kw dep)
  (cond
   [(string? dep) #f]
   [(null? (cdr dep)) #f]
   [(keyword? (cadr dep))
    (define p (member kw (cdr dep)))
    (and p (cadr p))]
   [else #f]))

(define (dependency-this-platform? dep)
  (define p (dependency-lookup '#:platform dep))
  (if p
      (if (symbol? p)
          (eq? p (system-type))
          (let ([s (path->string (system-library-subpath #f))])
            (if (regexp? p)
                (regexp-match? p s)
                (equal? p s))))
      #t))

(define pkg-lock-held (make-parameter #f))
(define pkg-lock-scope (make-parameter #f))

;; Call `t' with lock held for the current scope. The intent is that
;; `t' reads and writes package information in the curent scope. It
;; may also *read* package information for wider package scopes
;; without a further lock --- which is questionable, but modification
;; of a shared scope while others are running can create trouble,
;; anyway.
(define (with-pkg-lock* read-only? t)
  (define mode (if read-only? 'shared 'exclusive))
  (define held-mode (pkg-lock-held))
  (define now-scope (current-pkg-scope))
  (define held-scope (pkg-lock-scope))
  (when (and held-scope
             (not (eq? held-scope now-scope)))
    (pkg-error "lock mismatch\n  held scope: ~a\n  requested scope: ~a"
               held-scope
               now-scope))
  (if (or (eq? mode held-mode)
          (eq? 'exclusive held-mode))
      (t)
      (let ([d (pkg-dir #f)])
        (unless read-only? (make-directory* d))
        (if (directory-exists? d)
            ;; If the directory exists, assume that a lock file is
            ;; available or creatable:
            (call-with-file-lock/timeout
             #f 
             mode
             (lambda ()
               (parameterize ([pkg-lock-held mode]
                              [pkg-lock-scope now-scope]
                              [current-no-pkg-db #f])
                 (t)))
             (λ () (pkg-error  (~a "could not acquire package lock\n"
                                   "  lock file: ~a")
                               (pkg-lock-file)))
             #:lock-file (pkg-lock-file))
            ;; Directory does not exist; we must be in read-only mode.
            ;; Run `t' under the claim that no database is available
            ;; (in case the database is created concurrently):
            (parameterize ([current-no-pkg-db now-scope])
              (parameterize ([pkg-lock-held mode])
                (t)))))))
(define-syntax-rule (with-pkg-lock e ...)
  (with-pkg-lock* #f (λ () e ...)))
(define-syntax-rule (with-pkg-lock/read-only e ...)
  (with-pkg-lock* #t (λ () e ...)))

(define (maybe-append lists)
  (and (for/and ([v (in-list lists)]) (not (eq? v 'all)))
       (apply append lists)))

(define (db-path? p)
  (regexp-match? #rx"[.]sqlite$" (path->bytes p)))

(define (catalog-dispatch i server db dir)
  (cond
   [(equal? "file" (url-scheme i))
    (define path (url->path i))
    (cond
     [(db-path? path)
      (parameterize ([db:current-pkg-catalog-file path])
        (db))]
     [(directory-exists? path) (dir path)]
     [else #f])]
   [else (server i)]))

;; Add current package version to a URL:
(define (add-version-query addr/no-query)
  (struct-copy url addr/no-query
               [query (append
                       (url-query addr/no-query)
                       (list
                        (cons 'version (current-pkg-scope-version))))]))

;; Take a package-info hash table and lift any version-specific
;; information in 'versions.
(define (select-info-version ht)
  (and ht
       (let ([v (hash-ref ht 'versions #f)])
         (cond
          [(hash? v)
           (or (for/or ([vers (in-list (list (current-pkg-scope-version)
                                             'default))])
                 (define ht2 (hash-ref v vers #f))
                 (and ht2
                      ;; Override fields of `ht' with values from `ht2':
                      (for/fold ([ht ht]) ([(k v) (in-hash ht2)])
                        (hash-set ht k v))))
               ;; Keep ht as-is:
               ht)]
          [else ht]))))

(define (package-catalog-lookup pkg details? download-printf)
  (or
   (for/or ([i (in-list (pkg-catalogs))])
     (if download-printf
         (download-printf "Resolving ~s via ~a\n" pkg (url->string i))
         (log-pkg-debug "consulting catalog ~a" (url->string i)))
     (select-info-version
      (catalog-dispatch
       i
       ;; Server:
       (lambda (i)
         (define addr (add-version-query
                       (combine-url/relative i (format "pkg/~a" pkg))))
         (log-pkg-debug "resolving via ~a" (url->string addr))
         (read-from-server
          'package-catalog-lookup
          addr
          (lambda (v) (and (hash? v)
                           (for/and ([k (in-hash-keys v)])
                             (symbol? k))))
          (lambda (s) #f)))
       ;; Local database:
       (lambda ()
         (define pkgs (db:get-pkgs #:name pkg))
         (and (pair? pkgs)
              (db-pkg-info (car pkgs) details?)))
       ;; Local directory:
       (lambda (path)
         (define pkg-path (build-path path "pkg" pkg))
         (and (file-exists? pkg-path)
              (call-with-input-file* pkg-path read))))))
   (pkg-error (~a "cannot find package on catalogs\n"
                  "  package: ~a")
              pkg)))

(define (db-pkg-info pkg details?)
  (if details?
      (let ([tags (db:get-pkg-tags (db:pkg-name pkg)
                                   (db:pkg-catalog pkg))]
            [mods (db:get-pkg-modules (db:pkg-name pkg)
                                      (db:pkg-catalog pkg)
                                      (db:pkg-checksum pkg))]
            [deps (db:get-pkg-dependencies (db:pkg-name pkg)
                                           (db:pkg-catalog pkg)
                                           (db:pkg-checksum pkg))])
        (hash 'name (db:pkg-name pkg)
              'author (db:pkg-author pkg)
              'source (db:pkg-source pkg)
              'checksum (db:pkg-checksum pkg)
              'description (db:pkg-desc pkg)
              'tags tags
              'modules mods
              'dependencies deps))
      (hash 'source (db:pkg-source pkg)
            'checksum (db:pkg-checksum pkg))))

(define (remote-package-checksum pkg download-printf pkg-name)
  (match pkg
    [`(catalog ,pkg-name)
     (hash-ref (package-catalog-lookup pkg-name #f download-printf) 'checksum)]
    [`(url ,pkg-url-str)
     (package-url->checksum pkg-url-str 
                            #:download-printf download-printf
                            #:pkg-name pkg-name)]))

(define (checksum-for-pkg-source pkg-source type pkg-name given-checksum download-printf)
  (case type
    [(file-url dir-url github)
     (or given-checksum
	 (remote-package-checksum `(url ,pkg-source) download-printf pkg-name))]
    [(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)))]
    [else given-checksum]))

(define (write-file-hash! file new-db)
  (unless (eq? (pkg-lock-held) 'exclusive)
    (pkg-error "attempt to write package database without write lock"))
  (make-parent-directory* file)
  (call-with-atomic-output-file
   file
   (λ (o tmp-path) (write new-db o) (newline o))))

(define (read-pkg-db)
  (unless (pkg-lock-held)
    (pkg-error "attempt to read package database without lock"))
  (define scope (current-pkg-scope))
  (if (eq? (current-no-pkg-db) scope)
      #hash()
      (read-pkgs-db scope (current-pkg-scope-version))))

;; read all packages in this scope or wider
(define (merge-pkg-dbs [scope (current-pkg-scope)])
  (define (merge-next-pkg-dbs scope)
    (parameterize ([current-pkg-scope scope])
      (merge-pkg-dbs scope)))
  (if (path? scope)
      (read-pkg-db)
      (case scope
        [(installation)
         (for*/hash ([dir (in-list (get-pkgs-search-dirs))]
                     [(k v) (read-pkgs-db dir)])
           (values k v))]
        [(user)
         (define db (read-pkgs-db 'user (current-pkg-scope-version)))
         (for/fold ([ht (merge-next-pkg-dbs 'installation)]) ([(k v) (in-hash db)])
           (hash-set ht k v))])))    

;; Finds the scope, in which `pkg-name' is installed; returns 'dir,
;; 'installation, a path, or #f (where #f means "not installed").  If
;; `next?' is true, search only scopes wider than the current one.
(define (find-pkg-installation-scope pkg-name #:next? [next? #f])
  (case (current-pkg-scope)
    [(user)
     (or (and (not next?)
              (hash-ref (read-pkg-db) pkg-name #f)
              'user)
         (parameterize ([current-pkg-scope 'installation])
           (find-pkg-installation-scope pkg-name)))]
    [(installation)
     (or (and (not next?)
              (hash-ref (read-pkg-db) pkg-name #f)
              'installation)
         (for/or ([dir (in-list (get-pkgs-search-dirs))])
           (and (hash-ref (read-pkgs-db dir) pkg-name #f)
                dir)))]
    [else
     (and (not next?)
          (and (hash-ref (read-pkgs-db (current-pkg-scope)) pkg-name #f)
               (current-pkg-scope)))]))

(define (package-info pkg-name [fail? #t] #:db [given-db #f])
  (define db (or given-db (read-pkg-db)))
  (define pi (hash-ref db pkg-name #f))
  (cond
    [pi
     pi]
    [(not fail?)
     #f]
    [else
     (pkg-not-installed pkg-name db)]))

;; return the current scope as a string
(define (current-scope->string)
  (define scope (current-pkg-scope))
  (cond
   [(path? scope) (path->string scope)]
   [else (symbol->string scope)]))

;; prints an error for packages that are not installed
;; pkg-name db -> void
(define (pkg-not-installed pkg-name db)
  ;; This may read narrower package scopes without holding the
  ;; lock, but maybe that's ok for mere error reporting:
  (define s (parameterize ([current-pkg-scope 'user])
              (find-pkg-installation-scope pkg-name)))

  (define not-installed-msg
   (cond [s "package installed in a different scope"]
         [else "package not currently installed"]))

  (apply pkg-error (~a not-installed-msg
                       "\n  package: ~a"
                       "\n  current scope: ~a"
                       (if s
                           "\n  installed in scope: ~a"
                           "")
                       ;; Probably too much information:
                       #;
                       "\n  packages in current scope:~a")
         (append
          (list
           pkg-name
           (current-scope->string))
          (if s (list s) null)
          #;
          (list
           (format-list (hash-keys db))))))
         
(define (update-pkg-db! pkg-name info)
  (write-file-hash!
   (pkg-db-file)
   (hash-set (read-pkg-db) pkg-name info)))
(define (remove-from-pkg-db! pkg-name)
  (write-file-hash!
   (pkg-db-file)
   (hash-remove (read-pkg-db) pkg-name)))

(define (read-pkg-cfg/def k)
  ;; Lock is held for the current scope, but if
  ;; the key is not found in the current scope,
  ;; get the next scope's lock and try there,
  ;; etc.
  (define (get-default)
    (match k
      ['catalogs
       (list "http://pkgs.racket-lang.org"
             "http://planet-compats.racket-lang.org")]
      ['default-scope "user"]
      ['installation-name (version)]
      ['download-cache-dir (build-path (find-system-path 'addon-dir)
                                       "download-cache")]
      ['download-cache-max-files 1024]
      ['download-cache-max-bytes (* 64 1024 1024)]
      [_ #f]))
  (define c (read-pkg-file-hash (pkg-config-file)))
  (define v (hash-ref c k 'none))
  (cond
   [(eq? v 'none)
    ;; Default from enclosing scope or hard-wired default:
    (define s (current-pkg-scope))
    (if (eq? s 'installation)
        ;; Hard-wided:
        (get-default)
        ;; Enclosing:
        (parameterize ([current-pkg-scope 'installation])
          (read-pkg-cfg/def k)))]
   [else
    (match k
      ['catalogs
       (if (member #f v)
           ;; Replace #f with default URLs:
           (apply append (for/list ([i (in-list v)])
                           (if (not i)
                               (get-default)
                               (list i))))
           v)]
      [_ v])]))

(define (update-pkg-cfg! key val)
  (define f (pkg-config-file))
  (write-file-hash! 
   f
   (hash-set (read-pkg-file-hash f) key val)))

(define (default-pkg-scope)
  (match (default-pkg-scope-as-string)
    ["installation" 'installation]
    [else 'user]))
(define (default-pkg-scope-as-string)
  (read-pkg-cfg/def 'default-scope))

(define (pkg-config-catalogs)
  (with-pkg-lock/read-only
   (read-pkg-cfg/def 'catalogs)))

(define (pkg-catalogs)
  (or (current-pkg-catalogs)
      (map string->url (read-pkg-cfg/def 'catalogs))))

(struct install-info (name orig-pkg directory clean? checksum module-paths))

(define (update-install-info-orig-pkg if op)
  (struct-copy install-info if
               [orig-pkg op]))
(define (update-install-info-checksum if op)
  (struct-copy install-info if
               [checksum op]))

(define (scope->links-file scope)
  (and (path? scope)
       (build-path scope "links.rktd")))

(define (get-scope-list)
  ;; Get a list of scopes suitable for searches with respect to
  ;; the current scope
  (define current-scope (current-pkg-scope))
  (if (path? current-scope)
      (list current-scope)
      (member current-scope
              (append '(user)
                      (let ([main (find-pkgs-dir)])
                        (for/list ([d (get-pkgs-search-dirs)])
                          (if (equal? d main)
                              'installation
                              d)))))))

(define (pkg-directory pkg-name)
  ;; Warning: takes locks individually.
  (pkg-directory** pkg-name 
                   (lambda (f)
                     (with-pkg-lock/read-only
                      (f)))))

(define (pkg-directory** pkg-name [call-with-pkg-lock (lambda (f) (f))])
  (for/or ([scope (in-list (get-scope-list))])
    (parameterize ([current-pkg-scope scope])
      (call-with-pkg-lock
       (lambda ()
         (pkg-directory* pkg-name))))))

(define (pkg-directory* pkg-name #:db [db #f])
  (define info (package-info pkg-name #f #:db db))
  (and info
       (let ()
         (match-define (pkg-info orig-pkg checksum _) info)
         (match orig-pkg
           [`(,(or 'link 'static-link) ,orig-pkg-dir)
            (path->complete-path orig-pkg-dir (pkg-installed-dir))]
           [_
            (build-path (pkg-installed-dir) 
                        (or (cond
                             [(pkg-info/alt? info)
                              (pkg-info/alt-dir-name info)]
                             [(sc-pkg-info/alt? info)
                              (sc-pkg-info/alt-dir-name info)]
                             [else #f])
                            pkg-name))]))))

(define (make-pkg-info orig-pkg checksum auto? single-collect alt-dir-name)
  ;; Picks the right structure subtype
  (if single-collect
      (if alt-dir-name
          (sc-pkg-info/alt orig-pkg checksum auto? single-collect alt-dir-name)
          (sc-pkg-info orig-pkg checksum auto? single-collect))
      (if alt-dir-name
          (pkg-info/alt orig-pkg checksum auto? alt-dir-name)
          (pkg-info orig-pkg checksum auto?))))

(define (update-auto this-pkg-info auto?)
  (match-define (pkg-info orig-pkg checksum _) this-pkg-info)
  (make-pkg-info orig-pkg checksum auto?
                 (and (sc-pkg-info? this-pkg-info)
                      (sc-pkg-info-collect this-pkg-info))
                 (or (and (sc-pkg-info/alt? this-pkg-info)
                          (sc-pkg-info/alt-dir-name this-pkg-info))
                     (and (pkg-info/alt? this-pkg-info)
                          (pkg-info/alt-dir-name this-pkg-info)))))

(define (demote-packages quiet? pkg-names)
  (define db (read-pkg-db))
  (for ([pkg-name (in-list pkg-names)])
    (define pi (package-info pkg-name #:db db))
    (unless (pkg-info-auto? pi)
      (unless quiet?
        (printf/flush "Demoting ~a to auto-installed\n" pkg-name))
      (update-pkg-db! pkg-name (update-auto pi #t)))))

(define ((remove-package quiet?) pkg-name)
  (unless quiet?
    (printf/flush "Removing ~a\n" pkg-name))
  (define db (read-pkg-db))
  (define pi (package-info pkg-name #:db db))
  (match-define (pkg-info orig-pkg checksum _) pi)
  (define pkg-dir (pkg-directory* pkg-name #:db db))
  (remove-from-pkg-db! pkg-name)
  (define scope (current-pkg-scope))
  (define user? (not (or (eq? scope 'installation)
                         (path? scope))))
  (match orig-pkg
    [`(,(or 'link 'static-link) ,_)
     (links pkg-dir
            #:remove? #t
            #:user? user?
            #:file (scope->links-file scope)
            #:root? (not (sc-pkg-info? pi)))]
    [_
     (links pkg-dir
            #:remove? #t
            #:user? user?
            #:file (scope->links-file scope)
            #:root? (not (sc-pkg-info? pi)))
     (delete-directory/files pkg-dir)]))

      

(define (pkg-remove given-pkgs
                    #:demote? [demote? #f]
                    #:force? [force? #f]
                    #:auto? [auto? #f]
                    #:quiet? [quiet? #f]
                    #:from-command-line? [from-command-line? #f])
  (define db (read-pkg-db))
  (define all-pkgs
    (hash-keys db))
  (define all-pkgs-set
    (list->set all-pkgs))
  (define metadata-ns (make-metadata-namespace))
  (define in-pkgs (remove-duplicates given-pkgs))
  (define remove-pkgs
    (if auto?
        ;; compute fixpoint:
        (let ([init-drop (set-union
                          (list->set
                           (filter
                            (λ (p) (pkg-info-auto? (hash-ref db p)))
                            all-pkgs))
                          (list->set in-pkgs))])
          (let loop ([drop init-drop]
                     [keep (set-subtract
                            (list->set all-pkgs)
                            init-drop)])
            (define deps
              (list->set
               (append-map (package-dependencies metadata-ns db #t)
                           (set->list keep))))
            (define still-drop (set-subtract drop deps))
            (define delta (set-subtract drop still-drop))
            (if (set-empty? delta)
                (set->list drop)
                (loop still-drop
                      (set-union keep delta)))))
        ;; just given pkgs:
        (if demote?
            null
            in-pkgs)))
  (define setup-collects
    (get-setup-collects remove-pkgs
                        db
                        metadata-ns))
  (unless (or force? demote?)
    ;; Check dependencies on `in-pkgs' (not `pkgs', which has already
    ;; been filtered to remove package with dependencies if `auto?' is
    ;; true).
    (define pkgs-set (list->set in-pkgs))
    (define remaining-pkg-db-set
      (set-subtract all-pkgs-set
                    (if auto?
                        (list->set remove-pkgs)
                        pkgs-set)))
    (define deps-to-be-removed
      (set-intersect
       pkgs-set
       (list->set
        (append-map (package-dependencies metadata-ns db #t)
                    (set->list
                     remaining-pkg-db-set)))))    
    (unless (set-empty? deps-to-be-removed)
      (pkg-error (~a "cannot remove packages that are dependencies of other packages\n"
                     "  dependencies:~a")
                 (format-list 
                  (map
                   (λ (p)
                     (define ds
                       (filter (λ (dp)
                                 (member p ((package-dependencies metadata-ns db #t) dp)))
                               (set->list
                                remaining-pkg-db-set)))
                     (~a p " (required by: " ds ")"))
                   (set->list deps-to-be-removed))))))

  (when demote?
    ;; Demote any package that is not going to be removed:
    (demote-packages
     quiet?
     (set->list (set-subtract (list->set in-pkgs)
                              (list->set remove-pkgs)))))

  (for-each (remove-package quiet?)
            remove-pkgs)

  (cond
   [(or (null? remove-pkgs) demote?)
    ;; Did nothing, so no setup:
    'skip]
   [else
    ;; setup only collections that still exist:
    (and setup-collects
         (for/list ([c (in-list setup-collects)]
                    #:when (apply collection-path
                                  (if (path-string? c) (list c) c)
                                  #:fail (lambda (s) #f)))
           c))]))

(define (complain-about-source s reason)
  (pkg-error (~a "invalid package source;\n"
                 " ~a\n"
                 "  given: ~a")
             reason
             s))

(define (check-checksum given-checksum checksum what pkg-src cached-url)
  (when (and given-checksum
             checksum
             (not (equal? given-checksum checksum)))
    (clean-cache cached-url checksum)
    (pkg-error (~a "~a checksum on package\n"
                   "  package source: ~a\n"
                   "  expected: ~e\n"
                   "  got: ~e")
               what
               pkg-src
               given-checksum
               checksum)))

(define (drop-redundant-files pkg-dir)
  ;; Ad hoc space-saving rule: for an installation-wide package, remove
  ;; any redundant "COPYING.txt" or "COPYING_LESSER.txt" files.
  (when (and (eq? 'installation (current-pkg-scope))
             (find-share-dir))
    (for ([i (in-list '("COPYING.txt" "COPYING_LESSER.txt"))])
      (define pkg-file (build-path pkg-dir i))
      (define share-file (build-path (find-share-dir) i))
      (when (and (file-exists? pkg-file)
                 (file-exists? share-file)
                 (equal? (file->bytes pkg-file)
                         (file->bytes share-file)))
        ;; This file would be redundant, so drop it
        (delete-file pkg-file)))))

;; Downloads a package (if needed) and unpacks it (if needed) into a  
;; temporary directory.
(define (stage-package/info pkg
                            given-type
                            given-pkg-name
                            #:given-checksum [given-checksum #f]
                            #:cached-url [cached-url #f]
                            #:use-cache? use-cache?
                            check-sums?
                            download-printf
                            metadata-ns
                            #:strip [strip-mode #f]
                            #:in-place? [in-place? #f]
                            #:in-place-clean? [in-place-clean? #f]
                            #:link-dirs? [link-dirs? #f])
  (define-values (inferred-pkg-name type) 
    (if (path? pkg)
        (package-source->name+type (path->string pkg)
                                   (or given-type
                                       (if (directory-exists? pkg)
                                           (if link-dirs?
                                               'link
                                               'dir)
                                           'file))
                                   #:must-infer-name? (not given-pkg-name)
                                   #:complain complain-about-source)
        (package-source->name+type pkg given-type
                                   #:link-dirs? link-dirs?
                                   #:must-infer-name? (not given-pkg-name)
                                   #:complain complain-about-source)))
  (define pkg-name (or given-pkg-name inferred-pkg-name))
  (when (and type (not pkg-name))
    (pkg-error (~a "could not infer package name from source\n"
                   "  source: ~a")
               pkg))
  (cond
   [(and (eq? type 'github)
         (not (regexp-match? #rx"^git(?:hub)?://" pkg)))
    ;; Add "git://github.com/"
    (stage-package/info (string-append "git://github.com/" pkg) type 
                        pkg-name 
                        #:given-checksum given-checksum
                        #:use-cache? use-cache?
                        check-sums? download-printf
                        metadata-ns
                        #:strip strip-mode)]
   [(or (eq? type 'file-url) (eq? type 'dir-url) (eq? type 'github))
    (define pkg-url (string->url pkg))
    (define scheme (url-scheme pkg-url))

    (define orig-pkg `(url ,pkg))
    (define found-checksum
      ;; If a checksum is given, use that. In the case of a non-github
      ;; source, we could try to get the checksum from the source, and
      ;; then check whether it matches the expected one, but we choose
      ;; to avoid an extra trip to the server.
      (or given-checksum
          (remote-package-checksum orig-pkg download-printf pkg-name)))
    (when check-sums?
      (check-checksum given-checksum found-checksum "unexpected" pkg #f))
    (define checksum (or found-checksum given-checksum))
    (define downloaded-info
       (match type
         ['github
          (unless checksum
            (pkg-error 
             (~a "could not find checksum for github package source, which implies it doesn't exist\n"
                 "  source: ~a")
             pkg))
          (when (equal? checksum "")
            (pkg-error 
             (~a "cannot use empty checksum for github package source\n"
                 "  source: ~a")
             pkg))
          (match-define (list* user repo branch path)
                        (split-github-url pkg-url))
          (define new-url
            (url "https" #f "github.com" #f #t
                 (map (λ (x) (path/param x empty))
                      (list user repo "tarball" checksum))
                 empty
                 #f))
          (define tmp.tgz
            (make-temporary-file
             (string-append
              "~a-"
              (format "~a.~a.tgz" repo branch))
             #f))
          (delete-file tmp.tgz)
          (define tmp-dir
            (make-temporary-file
             (string-append
              "~a-"
              (format "~a.~a" repo branch))
             'directory))
          (define package-path
            (apply build-path tmp-dir path))

          (dynamic-wind
              void
              (λ ()
                 (download-file! new-url tmp.tgz checksum
                                 #:use-cache? use-cache?
                                 #:download-printf download-printf)
                 (define staged? #f)
                 (dynamic-wind
                     void
                     (λ ()
                        (untar tmp.tgz tmp-dir #:strip-components 1)
                        (begin0
                         (stage-package/info (path->string package-path)
                                             'dir
                                             pkg-name
                                             #:given-checksum checksum
                                             #:cached-url new-url
                                             #:use-cache? use-cache?
                                             check-sums?
                                             download-printf
                                             metadata-ns
                                             #:strip strip-mode
                                             #:in-place? (not strip-mode)
                                             #:in-place-clean? #t)
                         (set! staged? #t)))
                     (λ ()
                       (when (and use-cache? (not staged?))
                         (clean-cache new-url checksum))
                       (unless (and staged? (not strip-mode))
                         (delete-directory/files tmp-dir)))))
              (λ ()
                 (delete-directory/files tmp.tgz)))]
         [_
          (define url-last-component
            (path/param-path (last (url-path pkg-url))))
          (define url-looks-like-directory? (eq? type 'dir-url))
          (define-values
            (package-path download-type download-package!)
            (cond
             [url-looks-like-directory?
              (define package-path
                (make-temporary-file
                 (string-append
                  "~a-"
                  pkg-name)
                 'directory))
              (define (path-like f)
                (build-path package-path f))
              (define (url-like f)
                (if (and (pair? (url-path pkg-url))
                         (equal? "" (path/param-path (last (url-path pkg-url)))))
                    ;; normal relative path:
                    (combine-url/relative pkg-url f)
                    ;; we're assuming that the last path element is
                    ;; a directory, so just add f:
                    (struct-copy url pkg-url [path
                                              (append
                                               (url-path pkg-url)
                                               (list (path/param f null)))])))
              (values package-path
                      'dir
                      (λ ()
                         (download-printf "Cloning remote directory ~a\n"
                                          (url->string pkg-url))
                         (make-directory* package-path)
                         (define manifest
                           (call/input-url+200
                            (url-like "MANIFEST")
                            port->lines))
                         (unless manifest
                           (pkg-error (~a "could not find MANIFEST for package source\n"
                                          "  source: ~a")
                                      pkg))
                         (for ([f (in-list manifest)])
                           (download-file! (url-like f)
                                           (path-like f)
                                           #f
                                           #:use-cache? use-cache?))))]
             [else
              (define package-path
                (make-temporary-file
                 (string-append
                  "~a-"
                  url-last-component)
                 #f))
              (delete-file package-path)
              (values package-path
                      'file
                      (λ ()
                        (log-pkg-debug "\tAssuming URL names a file")
                        (download-file! pkg-url package-path checksum
                                        #:use-cache? use-cache?
                                        #:download-printf download-printf)))]))
          (define staged? #f)
          (dynamic-wind
              void
              (λ ()
                 (download-package!)
                 (log-pkg-debug "\tDownloading done, installing ~a as ~a"
                                package-path pkg-name)
                 (begin0
                  (stage-package/info package-path
                                      download-type
                                      pkg-name
                                      #:given-checksum checksum
                                      #:cached-url pkg-url
                                      #:use-cache? use-cache?
                                      check-sums?
                                      download-printf
                                      metadata-ns
                                      #:strip strip-mode)
                  (set! staged? #t)))
              (λ ()
                 (when (or (file-exists? package-path)
                           (directory-exists? package-path))
                   (when (and use-cache? (not staged?))
                     (clean-cache pkg-url checksum))
                   (delete-directory/files package-path))))]))
    (define info (update-install-info-orig-pkg downloaded-info
                                               orig-pkg))
    (when (and check-sums?
               (install-info-checksum info)
               (not checksum))
      (pkg-error (~a "remote package had no checksum\n"
                     "  package: ~a")
                 pkg))
    (when check-sums?
      (check-checksum checksum (install-info-checksum info)
                      "mismatched"
                      pkg
                      (and use-cache? cached-url)))
    (update-install-info-checksum
     info
     checksum)]
   [(eq? type 'file)
    (unless (file-exists? pkg)
      (pkg-error "no such file\n  path: ~a" pkg))
    (define checksum-pth (format "~a.CHECKSUM" pkg))
    (define expected-checksum
      (and (file-exists? checksum-pth)
           check-sums?
           (file->string checksum-pth)))
    (check-checksum given-checksum expected-checksum "unexpected" pkg #f)
    (define actual-checksum
      (with-input-from-file pkg
        (λ ()
           (sha1 (current-input-port)))))
    (check-checksum expected-checksum actual-checksum "mismatched" pkg
                    (and use-cache? cached-url))
    (define checksum
      actual-checksum)
    (define pkg-format (filename-extension pkg))
    (define pkg-dir
      (make-temporary-file (string-append "~a-" pkg-name)
                           'directory))
    (define staged? #t)
    (dynamic-wind
        void
        (λ ()
           (make-directory* pkg-dir)

           (match pkg-format
             [#"tgz"
              (untar pkg pkg-dir)]
             [#"tar"
              (untar pkg pkg-dir)]
             [#"gz" ; assuming .tar.gz
              (untar pkg pkg-dir)]
             [#"zip"
              (unzip pkg (make-filesystem-entry-reader #:dest pkg-dir)
                     #:preserve-timestamps? #t)]
             [#"plt"
              (make-directory* pkg-dir)
              (unpack pkg pkg-dir
                      (lambda (x) (log-pkg-debug "~a" x))
                      (lambda () pkg-dir)
                      #f
                      (lambda (auto-dir main-dir file) pkg-dir))
              (define info-path (build-path pkg-dir "info.rkt"))
              (unless (file-exists? info-path)
                ;; Add in "info.rkt" file to make it multi-collection,
                ;; since a ".plt" archive is never single-collection. This
                ;; is needed for supporting old ".plt" archives as packages.
                (call-with-output-file info-path
                  (lambda (o)
                    (fprintf o "#lang setup/infotab\n")
                    (write '(define collection 'multi) o)
                    (newline o))))]
             [x
              (pkg-error "invalid package format\n  given: ~a" x)])

           (begin0
            (update-install-info-checksum
             (update-install-info-orig-pkg
              (stage-package/info pkg-dir
                                  'dir
                                  pkg-name
                                  #:given-checksum checksum
                                  #:cached-url cached-url
                                  #:use-cache? use-cache?
                                  check-sums?
                                  download-printf
                                  metadata-ns
                                  #:strip strip-mode
                                  #:in-place? (not strip-mode)
                                  #:in-place-clean? #t)
              `(file ,(simple-form-path* pkg)))
             checksum)
            (unless strip-mode
              (set! staged? #t))))
        (λ ()
           (unless staged?
             (delete-directory/files pkg-dir))))]
   [(or (eq? type 'dir)
        (eq? type 'link)
        (eq? type 'static-link))
    (unless (directory-exists? pkg)
      (pkg-error "no such directory\n  path: ~a" pkg))
    (let ([pkg (directory-path-no-slash pkg)])
      (cond
       [(or (eq? type 'link)
            (eq? type 'static-link))
        (install-info pkg-name
                      `(,type ,(path->string
                                (find-relative-path (pkg-installed-dir)
                                                    (simple-form-path pkg)
                                                    #:more-than-root? #t)))
                      pkg
                      #f
                      given-checksum ; if a checksum is provided, just use it
                      (directory->module-paths pkg pkg-name metadata-ns))]
       [else
        (define pkg-dir
          (if in-place?
              (if strip-mode
                  (pkg-error "cannot strip directory in place")
                  pkg)
              (let ([pkg-dir (make-temporary-file "pkg~a" 'directory)])
                (delete-directory pkg-dir)
                (if strip-mode
                    (begin
                      (make-directory* pkg-dir)
                      (generate-stripped-directory strip-mode pkg pkg-dir))
                    (begin
                      (make-parent-directory* pkg-dir)
                      (copy-directory/files pkg pkg-dir #:keep-modify-seconds? #t)))
                pkg-dir)))
        (when (or (not in-place?)
                  in-place-clean?)
          (drop-redundant-files pkg-dir))
        (install-info pkg-name
                      `(dir ,(simple-form-path* pkg))
                      pkg-dir
                      (or (not in-place?) in-place-clean?) 
                      given-checksum ; if a checksum is provided, just use it
                      (directory->module-paths pkg-dir pkg-name metadata-ns))]))]
   [(eq? type 'name)
    (define catalog-info (package-catalog-lookup pkg #f download-printf))
    (log-pkg-debug "catalog response: ~s" catalog-info)
    (define source (hash-ref catalog-info 'source))
    (define checksum (hash-ref catalog-info 'checksum))
    (define info (stage-package/info source
                                     #f
                                     pkg-name
                                     #:given-checksum checksum
                                     #:use-cache? use-cache?
                                     check-sums?
                                     download-printf
                                     metadata-ns
                                     #:strip strip-mode))
    (when check-sums?
      (check-checksum given-checksum checksum "unexpected" pkg #f)
      (check-checksum checksum (install-info-checksum info) "incorrect" pkg #f))
    (update-install-info-orig-pkg
     (update-install-info-checksum
      info
      checksum)
     `(catalog ,pkg))]
   [else
    (pkg-error "cannot infer package source type\n  source: ~a" pkg)]))

(define (pkg-stage desc
                   #:namespace [metadata-ns (make-metadata-namespace)] 
                   #:in-place? [in-place? #f]
                   #:strip [strip-mode #f])
  (define i (stage-package/info (pkg-desc-source desc)
                                (pkg-desc-type desc)
                                (pkg-desc-name desc)
                                #:given-checksum (pkg-desc-checksum desc)
                                #:use-cache? #f
                                #t
                                void
                                metadata-ns
                                #:in-place? in-place?
                                #:strip strip-mode))
  (values (install-info-name i)
          (install-info-directory i)
          (install-info-checksum i)
          (install-info-clean? i)
          (install-info-module-paths i)))

(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
         #:link-dirs? link-dirs?
         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?) desc)
    (match-define
     (install-info pkg-name orig-pkg pkg-dir clean? checksum module-paths)
     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))))
    (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
         (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"))
                     (let-values ([(base name dir?) (split-path f)])
                       (or (file-exists? (build-path base "compiled" (path-add-suffix name #".zo")))
                           (file-exists? (build-path base "compiled" (path-add-suffix
                                                                      (path-replace-suffix name #".ss")
                                                                      #".zo"))))))
                 (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 (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 deps (get-all-deps metadata-ns pkg-dir))
          (define implies (list->set
                           (get-all-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?)
                                     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?)
                                           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
       (λ ()
         (when updating?
           (download-printf "Re-installing ~a\n" pkg-name))
         (define final-pkg-dir
           (cond
             [clean?
              (define final-pkg-dir (select-package-directory
                                     (build-path (pkg-installed-dir) pkg-name)))
              (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)
                          #:given-checksum (pkg-desc-checksum v)
                          #:use-cache? use-cache?
                          check-sums? download-printf
                          metadata-ns
                          #:strip strip-mode
                          #: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 do-its
    (map (curry install-package/outer all-infos)
         all-descs
         all-infos))
  (pre-succeed)

  (define post-metadata-ns (make-metadata-namespace))
  (for-each (λ (t) (t)) 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? do-its)
        (and (not updating?) (andmap is-promote? all-infos)))
    ;; No actions, so no setup:
    'skip]
   [else
    setup-collects]))

(define (pkg-single-collection dir 
                               #:name [pkg-name (let-values ([(base name dir?) (split-path dir)])
                                                  (path-element->string name))]
                               #:namespace [metadata-ns (make-metadata-namespace)])
  (define i (get-pkg-info dir metadata-ns))
  (if (not i)
      pkg-name
      (let ([s (i 'collection (lambda () 'use-pkg-name))])
        (unless (or (collection-name-element? s)
                    (eq? s 'multi)
                    (eq? s 'use-pkg-name))
          (log-error (format (~a "bad `collection' definition in \"info.rkt\";\n"
                                 " definition will be ignored\n"
                                 "  path: ~a\n"
                                 "  found: ~e\n"
                                 "  expected: (or/c collection-name-element? 'multi 'use-pkg-name)")
                             (build-path dir "info.rkt")
                             s)))
        (or (and (collection-name-element? s)
                 s)
            (and (eq? s 'use-pkg-name)
                 pkg-name)))))

(define (get-setup-collects pkg-names db metadata-ns)
  (maybe-append
   (for/list ([pkg-name (in-list pkg-names)])
     (define pkg-dir (pkg-directory* pkg-name #:db db))
     (define single-collect
       (and pkg-dir
            (pkg-single-collection pkg-dir #:name pkg-name #:namespace metadata-ns)))
     (or (and (not pkg-dir) null)
         (and single-collect (list single-collect))
         (get-metadata metadata-ns pkg-dir
                       'setup-collects (lambda () (package-collections
                                                   pkg-dir
                                                   metadata-ns))
                       #:checker (lambda (v)
                                   (unless (or (eq? v 'all)
                                               (and (list? v)
                                                    (for ([c (in-list v)])
                                                      (or (path-string? c)
                                                          (and (list? c)
                                                               (pair? c)
                                                               (andmap path-string? c))))))
                                     (pkg-error "bad 'setup-collects value\n  value: ~e"
                                                v))))))))

(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]
                     #: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]
                     #: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?
                       #: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
                       (for/list ([dep (in-list deps)])
                         (if (pkg-desc? dep)
                             dep
                             (pkg-desc dep #f #f #f #t))))])])
    (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
       #:link-dirs? link-dirs?
       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?)
         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)
                                         #: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))
    (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))
        ;; 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))))
        ;; 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?)
             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~a\n"
                            "  package name: ~a\n"
                            "  package source: ~a")
                        (if from-command-line?
                            " without `--link'"
                            " without new link")
                        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"
                            " 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"
                            " package was installed via a local file\n"
                            "  package name: ~a")
                        pkg-name)
             null)]
        [`(,_ ,orig-pkg-source)
         (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)
                    ;; FIXME: the type shouldn't be #f here; it should be
                    ;; preseved from install time:
                    (list (pkg-desc orig-pkg-source #f pkg-name #f auto?))))
             (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?)
                  ((package-dependencies metadata-ns db all-platforms? 
                                         #:only-implies? (not deps?))
                   pkg-name))
                 null))]))]
   [else null]))

(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)))
                  

(define ((package-dependencies metadata-ns db all-platforms?
                               #:only-implies? [only-implies? #f]) 
         pkg-name)
  (define pkg-dir (pkg-directory* pkg-name #:db db))
  (define deps
    (map dependency->name 
         (let ([l (get-all-deps metadata-ns pkg-dir)])
           (if all-platforms?
               l
               (filter dependency-this-platform? l)))))
  (if only-implies?
      (let ([implies (list->set (get-all-implies metadata-ns pkg-dir deps))])
        (filter (lambda (dep)
                  (set-member? implies dep))
                deps))
      deps))

(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]
                    #: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]
                    #: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?)
                                pkgs))
  (cond
    [(empty? pkgs)
     (unless quiet?
       (printf/flush (~a "No packages given to update"
                         (if from-command-line?
                             ";\n use `--all' to update all packages"
                             "")
                         "\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
      #:all-platforms? all-platforms?
      #:force? force?
      #:ignore-checksums? ignore-checksums?
      #:use-cache? use-cache?
      #:link-dirs? link-dirs?
      to-update)]))

(define (pkg-show indent 
                  #:directory? [dir? #f]
                  #:auto? [show-auto? #f])
  (let ()
    (define db (read-pkg-db))
    (define pkgs (sort (hash-keys db) string-ci<=?))
    (if (null? pkgs)
        (printf " [none]\n")
        (begin
          (table-display
           (list*
            (append
             (list (format "~aPackage~a"
                           indent 
                           (if show-auto? "[*=auto]" ""))
                   "Checksum"
                   "Source")
             (if dir?
                 (list "Directory")
                 empty))
            (for/list ([pkg (in-list pkgs)]
                       #:when (or show-auto?
                                  (not (pkg-info-auto? (hash-ref db pkg)))))
              (match-define (pkg-info orig-pkg checksum auto?) (hash-ref db pkg))
              (append
               (list (format "~a~a~a"
                             indent
                             pkg
                             (if auto? "*" ""))
                     (format "~a" checksum)
                     (format "~a" orig-pkg))
               (if dir?
                   (list (~a (pkg-directory* pkg #:db db)))
                   empty)))))
          (unless show-auto?
            (define n (for/sum ([pkg (in-list pkgs)] 
                                #:when (pkg-info-auto? (hash-ref db pkg)))
                               1))
            (unless (zero? n)
              (printf "~a[~a auto-installed package~a not shown]\n"
                      indent
                      n
                      (if (= n 1) "" "s"))))))))

(define (installed-pkg-table #:scope [given-scope #f])
  (parameterize ([current-pkg-scope 
                  (or given-scope (default-pkg-scope))])
    (with-pkg-lock/read-only
     (read-pkg-db))))

(define (installed-pkg-names #:scope [given-scope #f])
  (sort (hash-keys (installed-pkg-table #:scope given-scope))
        string-ci<=?))
  
(define (pkg-migrate from-version
                     #:all-platforms? [all-platforms? #f]
                     #:force? [force? #f]
                     #:quiet? [quiet? #f]
                     #:from-command-line? [from-command-line? #f]
                     #:ignore-checksums? [ignore-checksums? #f]
                     #:use-cache? [use-cache? #t]
                     #:dep-behavior [dep-behavior #f]
                     #:strip [strip-mode #f])
  (define from-db
    (parameterize ([current-pkg-scope-version from-version])
      (installed-pkg-table #:scope 'user)))
  (define to-install
    (sort
     (for/list ([(name info) (in-hash from-db)]
                #:unless (pkg-info-auto? info))
       (define-values (source type)
         (match (pkg-info-orig-pkg info)
           [(list 'catalog name) (values name 'name)]
           [(list 'url url) (values url #f)]
           [(list 'link path) (values path 'link)]
           [(list 'static-link path) (values path 'static-link)]))
       (pkg-desc source type name #f #f))
     string<?
     #:key pkg-desc-name))
  (unless quiet?
    (cond
     [(null? to-install)
      (printf "No packages from ~s to install\n" from-version)]
     [else
      (printf "Packages to install:\n")
      (for ([d (in-list to-install)])
        (define t (pkg-desc-type d))
        (define n (pkg-desc-name d))
        (case t
          [(name) (printf "  ~a\n" n)]
          [(link static-link)
           (printf "  ~a ~aed from ~a\n" n t (pkg-desc-source d))]
          [else
           (printf "  ~a from ~a\n" n (pkg-desc-source d))]))]))
  (if (null? to-install)
      'skip
      (begin0
       (pkg-install to-install
                    #:all-platforms? all-platforms?
                    #:force? force?
                    #:ignore-checksums? ignore-checksums?
                    #:use-cache? use-cache?
                    #:skip-installed? #t
                    #:dep-behavior (or dep-behavior 'search-auto)
                    #:quiet? quiet? 
                    #:from-command-line? from-command-line?
                    #:strip strip-mode)
       (unless quiet?
         (printf "Packages migrated\n")))))

(define (pkg-config config:set key+vals
                    #:from-command-line? [from-command-line? #f])
  (cond
    [config:set
     (match key+vals
       [(list)
        (pkg-error "no config key given")]
       [(list (and key
                   (or "default-scope"
                       "name"
                       "download-cache-max-files"
                       "download-cache-max-bytes"
                       "download-cache-dir")))
        (pkg-error (~a "missing value for config key\n"
                       "  config key: ~a")
                   key)]
       [(list* (and key
                    (or "default-scope"
                        "name"
                        "download-cache-max-files"
                        "download-cache-max-bytes"
                        "download-cache-dir"))
               val
               another-val
               more-vals)
        (pkg-error (~a "too many values provided for config key\n"
                       "  config key: ~a\n"
                       "  given values:~a")
                   key
                   (format-list (cons val more-vals)))]
       [(list* (and key "catalogs") val)
        (update-pkg-cfg! 'catalogs val)]
       [(list (and key "default-scope") val)
        (unless (member val '("installation" "user"))
          (pkg-error (~a "invalid value for config key\n"
                         "  config key: ~a\n"
                         "  given value: ~a\n"
                         "  valid values: installation, user")
                     key
                     val))
        (update-pkg-cfg! 'default-scope val)]
       [(list (and key "name") val)
        (unless (eq? 'installation (current-pkg-scope))
          (pkg-error (~a "setting `name' makes sense only in `installation' scope\n"
                         "  current package scope: ~a")
                     (current-pkg-scope)))
        (update-pkg-cfg! 'installation-name val)]
       [(list (and key "download-cache-dir")
              val)
        (unless (complete-path? val)
          (pkg-error (~a "invalid value for config key\n"
                         " not an absolute path\n"
                         "  config key: ~a\n"
                         "  given value: ~a")
                     key
                     val))
        (update-pkg-cfg! (string->symbol key) val)]
       [(list (and key (or "download-cache-max-files"
                           "download-cache-max-bytes"))
              val)
        (unless (real? (string->number val))
          (pkg-error (~a "invalid value for config key\n"
                         "  config key: ~a\n"
                         "  given value: ~a\n"
                         "  valid values: real numbers")
                     key
                     val))
        (update-pkg-cfg! (string->symbol key) (string->number val))]
       [(list* key args)
        (pkg-error "unsupported config key\n  key: ~a" key)])]
    [else
     (define (show key+vals indent)
       (match key+vals
         [(list key)
          (match key
            ["catalogs"
             (for ([s (in-list (read-pkg-cfg/def 'catalogs))])
               (printf "~a~a\n" indent s))]
            ["default-scope"
             (printf "~a~a\n" indent (read-pkg-cfg/def 'default-scope))]
            ["name"
             (printf "~a~a\n" indent (read-pkg-cfg/def 'installation-name))]
            [(or "download-cache-dir"
                 "download-cache-max-files"
                 "download-cache-max-bytes")
             (printf "~a~a\n" indent (read-pkg-cfg/def (string->symbol key)))]
            [_
             (pkg-error "unsupported config key\n  key: ~e" key)])]
         [(list)
          (pkg-error "config key not provided")]
         [_
          (pkg-error (~a "multiple config keys provided"
                         (if from-command-line?
                             ";\n supply `--set' to set a config key's value"
                             "")))]))
     (match key+vals
       [(list)
        (for ([key (in-list '("name"
                              "catalogs"
                              "default-scope"
                              "download-cache-dir"
                              "download-cache-max-files"
                              "download-cache-max-bytes"))])
          (printf "~a:\n" key)
          (show (list key) "  "))]
       [_ (show key+vals "")])]))

(define (create-as-is create:format pkg-name dir orig-dir
                      #:quiet? [quiet? #f]
                      #:from-command-line? [from-command-line? #f]
                      #:hide-src? [hide-src? #f]
                      #:dest [dest-dir #f])
  (begin
    (unless (directory-exists? dir)
      (pkg-error "directory does not exist\n  path: ~a" dir))
    (match create:format
      ['MANIFEST
       (unless quiet?
         (printf/flush "creating manifest for ~a\n"
                       orig-dir))
       (with-output-to-file (build-path (or dest-dir dir) "MANIFEST")
         #:exists 'replace
         (λ ()
           (for ([f (in-list (parameterize ([current-directory dir])
                               (find-files file-exists?)))])
             (display f)
             (newline))))]
      [else
       (define pkg (format "~a.~a" pkg-name create:format))
       (define actual-dest-dir (or dest-dir 
                                   (let-values ([(base name dir?) (split-path dir)])
                                     (cond
                                      [(path? base) (path->complete-path base)]
                                      [else (current-directory)]))))
       (define pkg/complete (path->complete-path pkg actual-dest-dir))
       ;; To make checksums more consistent, set a directory's timestamp to
       ;; the latest time of any of its source files.
       (define (use-real-timestamp? p)
         (and (file-exists? p)
              (regexp-match? #rx"[.](?:rkt|ss|scrbl|txt)$" p)))
       (define latest-timestamp
         (for/fold ([ts #f]) ([f (in-directory dir)])
           (define fts (and (use-real-timestamp? f)
                            (file-or-directory-modify-seconds f)))
           (if (and fts (or (not ts) (fts . > . ts)))
               fts
               ts)))
       (define (file-or-directory-timestamp p)
         (or (and (not (use-real-timestamp? p))
                  latest-timestamp)
             (file-or-directory-modify-seconds p)))
       (unless quiet?
         (printf/flush "packing~a into ~a\n"
                       (if hide-src? "" (format " ~a" dir))
                       (if dest-dir
                           pkg/complete
                           pkg)))
       (match create:format
         ['tgz
          (when (file-exists? pkg/complete)
            (delete-file pkg/complete))
          (parameterize ([current-directory dir])
            (with-handlers ([exn? (lambda (exn)
                                    (when (file-exists? pkg/complete)
                                      (delete-file pkg/complete))
                                    (raise exn))])
              (apply tar-gzip pkg/complete (directory-list)
                     #:get-timestamp file-or-directory-timestamp)))]
         ['zip
          (when (file-exists? pkg/complete)
            (delete-file pkg/complete))
          (parameterize ([current-directory dir])
            (with-handlers ([exn? (lambda (exn)
                                    (when (file-exists? pkg/complete)
                                      (delete-file pkg/complete))
                                    (raise exn))])
              (apply zip pkg/complete (directory-list)
                     #:get-timestamp file-or-directory-timestamp)))]
         ['plt
          (define dest pkg/complete)
          (when (pkg-single-collection #:name pkg-name dir)
            (pkg-error (~a "single-collection package not supported in .plt format\n"
                           "  directory: ~a")
                       dir))
          (parameterize ([current-directory dir])
            (define names (filter std-filter (directory-list)))
            (define dirs (filter directory-exists? names))
            (pack-plt dest pkg-name 
                      names
                      #:plt-relative? #t
                      #:as-paths (map (lambda (v) (build-path "collects" v)) names)
                      #:collections (map list (map path->string dirs))))]
         [x
          (pkg-error "invalid package format\n  format: ~a" x)])
       (define chk (format "~a.CHECKSUM" pkg))
       (define chk/complete (path->complete-path chk actual-dest-dir))
       (unless quiet?
         (printf/flush "writing package checksum to ~a\n"
                       (if dest-dir
                           chk/complete
                           chk)))
       (with-output-to-file chk/complete
         #:exists 'replace
         (λ () (display (call-with-input-file pkg/complete sha1))))])))

(define (stripped-create mode name dir
                        #:format [create:format 'zip]
                        #:quiet? [quiet? #f]
                        #:from-command-line? [from-command-line? #f]
                        #:dest [archive-dest-dir #f])
  (define tmp-dir (make-temporary-file "create-binary-~a" 'directory))
  (dynamic-wind
      void
      (lambda ()
        (define dest-dir (build-path tmp-dir name))
        (make-directory dest-dir)
        (generate-stripped-directory mode dir dest-dir)
        (create-as-is create:format name dest-dir dir 
                      #:hide-src? #t 
                      #:quiet? quiet?
                      #:from-command-line? from-command-line?
                      #:dest (if archive-dest-dir
                                 (path->complete-path archive-dest-dir)
                                 (current-directory))))
      (lambda ()
        (delete-directory/files tmp-dir))))

(define (pkg-create create:format dir-or-name
                    #:dest [dest-dir #f]
                    #:source [source 'dir]
                    #:mode [mode 'as-is]
                    #:quiet? [quiet? #f]
                    #:from-command-line? [from-command-line? #f])
  (define pkg-name
    (if (eq? source 'dir)
        (path->string (let-values ([(base name dir?) (split-path dir-or-name)])
                        name))
        dir-or-name))
  (define dir
    (if (eq? source 'dir)
        dir-or-name
        (let ()
          (define (get-dir scope)
            (parameterize ([current-pkg-scope scope])
              (with-pkg-lock/read-only
               (pkg-directory* dir-or-name))))
          (define dir (get-dir 'user))
          (unless dir
            (pkg-error (~a "package not installed in user scope\n"
                           "  package name: ~a"
                           (if (get-dir 'installation)
                               "\n  installed in scope: installation"
                               ""))
                       dir-or-name))
          dir)))
  (case mode
    [(as-is)
     (create-as-is create:format pkg-name dir dir
                   #:dest dest-dir
                   #:quiet? quiet?
                   #:from-command-line? from-command-line?)]
    [else (stripped-create mode pkg-name dir
                           #:dest dest-dir
                           #:format create:format
                           #:quiet? quiet?
                           #:from-command-line? from-command-line?)]))

(define (pkg-catalog-copy srcs dest
                        #:from-config? [from-config? #f]
                        #:merge? [merge? #f]
                        #:force? [force? #f]
                        #:override? [override? #f])
  (define src-paths
    (for/list ([src (in-list (append srcs
                                     (if from-config?
                                         (pkg-config-catalogs)
                                         null)))])
      (define src-path
        (cond
         [(path? src) (path->complete-path src)]
         [(regexp-match? #rx"^https?://" src)
          (string->url src)]
         [(regexp-match? #rx"^file://" src)
          (url->path (string->url src))]
         [(regexp-match? #rx"^[a-zA-Z]*://" src)
          (pkg-error (~a "unrecognized URL scheme for an catalog\n"
                         "  URL: ~a")
                     src)]
         [else (path->complete-path src)]))
      (when (path? src-path)
        (cond
         [(db-path? src-path)
          (void)]
         [(directory-exists? src-path)
          (void)]
         [(let-values ([(base name dir?) (split-path src-path)]) dir?)
          (void)]
         [else
          (pkg-error (~a "bad source catalog path\n"
                         "  path: ~a\n"
                         "  expected: directory or path with \".sqlite\" extension")
                     src)]))
      src-path))
  (define dest-path
    (cond
     [(path? dest) (path->complete-path dest)]
     [(regexp-match? #rx"^file://" dest)
      (url->path (string->url dest))]
     [(regexp-match? #rx"^[a-zA-Z]*://" dest)
      (pkg-error (~a "cannot copy to a non-file destination catalog\n"
                     "  given URL: ~a")
                 dest)]
     [else (path->complete-path dest)]))

  (unless (or force? merge?)
    (when (or (file-exists? dest-path)
              (directory-exists? dest-path)
              (link-exists? dest-path))
      (pkg-error (~a "destination exists\n"
                     "  path: ~a")
                 dest-path)))

  (define details
    (let ([src-paths (if (and merge?
                              (or (file-exists? dest-path)
                                  (directory-exists? dest-path)))
                         (if override?
                             (append src-paths
                                     (list dest-path))
                             (cons dest-path
                                   src-paths))
                         src-paths)])
      (parameterize ([current-pkg-catalogs (for/list ([src-path src-paths])
                                            (if (path? src-path)
                                                (path->url src-path)
                                                src-path))])
        (get-all-pkg-details-from-catalogs))))

  (when (and force? (not merge?))
    (cond
     [(file-exists? dest-path)
      (delete-file dest-path)]
     [(directory-exists? dest-path)
      (if (db-path? dest-path)
          (delete-directory/files dest-path)
          (for ([i (directory-list dest-path)])
            (delete-directory/files (build-path dest-path i))))]
     [(link-exists? dest-path)
      (delete-file dest-path)]))

  (cond
   [(db-path? dest-path)
    (define vers-details
      (for/hash ([(k v) (in-hash details)])
        (values k (select-info-version v))))
    (parameterize ([db:current-pkg-catalog-file dest-path])
      (db:set-catalogs! '("local"))
      (db:set-pkgs! "local"
                    (for/list ([(k v) (in-hash vers-details)])
                      (db:pkg k "local"
                              (hash-ref v 'author "")
                              (hash-ref v 'source "")
                              (hash-ref v 'checksum "")
                              (hash-ref v 'description ""))))
      (for ([(k v) (in-hash vers-details)])
        (define t (hash-ref v 'tags '()))
        (unless (null? t)
          (db:set-pkg-tags! k "local" t)))
      (for ([(k v) (in-hash vers-details)])
        (define mods (hash-ref v 'modules '()))
        (unless (null? mods)
          (define cs (hash-ref v 'checksum ""))
          (db:set-pkg-modules! k "local" cs mods)))
      (for ([(k v) (in-hash vers-details)])
        (define deps (hash-ref v 'dependencies '()))
        (unless (null? deps)
          (define cs (hash-ref v 'checksum ""))
          (db:set-pkg-dependencies! k "local" cs deps))))]
   [else
    (define pkg-path (build-path dest-path "pkg"))
    (make-directory* pkg-path)
    (for ([(k v) (in-hash details)])
      (call-with-output-file*
       #:exists 'truncate/replace
       (build-path pkg-path k)
       (lambda (o) (write v o))))
    (call-with-output-file*
     #:exists 'truncate/replace
     (build-path dest-path "pkgs")
     (lambda (o) (write (hash-keys details) o)))
    (call-with-output-file*
     #:exists 'truncate/replace
     (build-path dest-path "pkgs-all")
     (lambda (o) (write details o)))]))

(define (pkg-catalog-show names 
                        #:all? [all? #f]
                        #:only-names? [only-names? #f]
                        #:modules? [modules? #f])
  (for ([name (in-list names)])
    (define-values (parsed-name type)
      (package-source->name+type name #f))
    (unless (eq? type 'name)
      (pkg-error (~a "incorrect syntax for a package name\n"
                     "  given: ~a")
                 name)))
  
  (cond
   [only-names?
    (define all-names (if all?
                          (get-all-pkg-names-from-catalogs)
                          names))
    (for ([name (in-list all-names)])
      (unless all?
        ;; Make sure it's available:
        (get-pkg-details-from-catalogs name))
      (printf "~a\n" name))]
   [else
    (define all-details (and all?
                             (get-all-pkg-details-from-catalogs)))
    (for ([name (in-list (if all?
                             (hash-keys all-details)
                             names))])
      (define details (select-info-version
                       (if all?
                           (hash-ref all-details name)
                           (get-pkg-details-from-catalogs name))))
      (printf "Package name: ~a\n" name)
      (for ([key '(author source checksum tags description)])
        (define v (hash-ref details key #f))
        (when v
          (printf " ~a: ~a\n"
                  (string-titlecase (symbol->string key))
                  (if (list? v)
                      (apply ~a #:separator ", " v)
                      v))))
      (for ([key '(dependencies)])
        (define v (hash-ref details key null))
        (unless (null? v)
          (printf " Dependencies:\n")
          (for ([dep (in-list v)])
            (define vers (dependency->version dep))
            (define plat (dependency-lookup '#:platform dep))
            (printf "  ~a~a~a\n"
                    (dependency->name dep)
                    (if vers
                        (format " version ~a" vers)
                        "")
                    (if plat
                        (format " on platform ~v" plat)
                        "")))))
      (when modules?
        (printf "Modules:")
        (for/fold ([col 72]) ([mod (in-list (hash-ref details 'modules null))])
          (define pretty-mod (pretty-module-path mod))
          (define mod-str (~a " " pretty-mod))
          (define new-col (if ((+ col (string-length mod-str)) . > . 72)
                              (begin
                                (newline)
                                0)
                              col))
          (display mod-str)
          (+ new-col (string-length mod-str)))
        (newline)))]))
  
(define (get-all-pkg-names-from-catalogs)
  (define ht
    (for*/hash ([i (in-list (pkg-catalogs))]
                [name
                 (catalog-dispatch
                  i
                  ;; Server:
                  (lambda (i)
                    (read-from-server 
                     'get-all-pkg-names-from-catalogs
                     (add-version-query
                      (combine-url/relative i "pkgs"))
                     (lambda (l) (and (list? l) 
                                      (andmap string? l)))))
                  ;; Local database:
                  (lambda ()
                    (map db:pkg-name (db:get-pkgs)))
                  ;; Local directory:
                  (lambda (path)
                    (define pkgs-path (build-path path "pkgs"))
                    (cond
                     [(file-exists? pkgs-path)
                      (call-with-input-file* pkgs-path read)]
                     [else
                      (define pkg-path (build-path path "pkg"))
                      (for/list ([i (directory-list pkg-path)]
                                 #:when (file-exists? (build-path pkg-path i)))
                        (path-element->string i))])))])
      (values name #t)))
  (sort (hash-keys ht) string<?))

(define (get-pkg-details-from-catalogs name)
  (for/or ([i (in-list (pkg-catalogs))])
    (package-catalog-lookup name #t #f)))

(define (get-all-pkg-details-from-catalogs)
  (for/fold ([ht (hash)]) ([i (in-list (pkg-catalogs))])
    (define one-ht
      (catalog-dispatch
       i
       ;; Server:
       (lambda (i)
         (read-from-server
          'get-all-pkg-details-from-catalogs
          (add-version-query
           (combine-url/relative i "pkgs-all"))
          (lambda (v)
            (and (hash? v)
                 (for/and ([(k v) (in-hash v)])
                   (and (string? k)
                        (hash? v)
                        (for/and ([k (in-hash-keys v)])
                          (symbol? k))))))))
       ;; Local database:
       (lambda ()
         (define pkgs (db:get-pkgs))
         (for/fold ([ht (hash)]) ([p (in-list pkgs)])
           (if (hash-ref ht (db:pkg-name p) #f)
               ht
               (hash-set ht
                         (db:pkg-name p)
                         (db-pkg-info p #t)))))
       ;; Local directory:
       (lambda (path)
         (define pkgs-all-path (build-path path "pkgs-all"))
         (cond
          [(file-exists? pkgs-all-path)
           (call-with-input-file* pkgs-all-path read)]
          [else
           (define pkg-path (build-path path "pkg"))
           (for/hash ([i (directory-list pkg-path)]
                      #:when (file-exists? (build-path pkg-path i)))
             (values (path-element->string i)
                     (call-with-input-file* (build-path pkg-path i)
                                            read)))]))))
    (for/fold ([ht ht]) ([(k v) (in-hash one-ht)])
      (if (hash-ref ht k #f)
          ht
          (hash-set ht k v)))))

(define (extract-pkg-dependencies get-info
                                  #:build-deps? [build-deps? #t]
                                  #:filter? [filter? #f])
  (define v (if get-info
                (get-info 'deps (lambda () empty))
                empty))
  ((check-dependencies 'deps) v)
  (define v2 (if (and get-info build-deps?)
                 (get-info 'build-deps (lambda () empty))
                 empty))
  ((check-dependencies 'build-deps) v2)
  (define all-v (append v v2))
  (if filter?
      (for/list ([dep (in-list all-v)]
                 #:when (dependency-this-platform? dep))
        (if (pair? dep)
            (car dep)
            dep))
      all-v))

(define (get-pkg-content desc 
                         #:namespace [metadata-ns (make-metadata-namespace)]
                         #:extract-info [extract-info extract-pkg-dependencies])
  (define-values (pkg-name dir cksum clean? module-paths) 
    (pkg-stage desc #:in-place? #t #:namespace metadata-ns))
  (define get-info (get-info/full dir #:namespace metadata-ns))
  (begin0
   (values cksum
           (set->list module-paths)
           (extract-info get-info))
   (when clean?
     (delete-directory/files dir))))

(define (pkg-directory->module-paths dir pkg-name 
                                     #:namespace [metadata-ns (make-metadata-namespace)])
  (set->list (directory->module-paths dir pkg-name metadata-ns)))

(define (directory->module-paths dir pkg-name metadata-ns)
  (define dummy (build-path dir "dummy.rkt"))
  (define compiled (string->path-element "compiled"))
  (define single-collect (pkg-single-collection dir #:name pkg-name #:namespace metadata-ns))
  (define (try-path s f)
    (define mp
      `(lib ,(apply ~a
                    #:separator "/"
                    (let ([l (map path-element->string 
                                  (explode-path f))])
                      (if single-collect
                          (if (eq? 'relative (car l))
                              (cons single-collect (cdr l))
                              (cons single-collect l))
                          l)))))
    (if (module-path? mp)
        (set-add s (collapse-module-path mp dummy))
        s))
  (parameterize ([current-directory dir])
    (let loop ([s (set)] [f 'init] [check-zo? #f])
      (cond
       [(eq? f 'init)
        (for/fold ([s s]) ([f (directory-list)])
          (loop s f check-zo?))]
       [(directory-exists? f)
        ;; Count ".zo" files toward the set of module paths only
        ;; if an "info.rkt" in an enclosing directory says to
        ;; assume virtual sources. Otherwise, the ".zo" file will
        ;; be discarded by `raco setup'.
        (define sub-check-zo?
          (or check-zo?
              (let ([i (get-pkg-info f metadata-ns)])
                (and i
                     (i 'assume-virtual-sources (lambda () #f))))))
        (for/fold ([s s]) ([f (directory-list f #:build? #t)])
          (loop s f sub-check-zo?))]
       [(not (file-exists? f)) s]
       [else
        (define-values (base name dir?) (split-path f))
        (cond
         [(and (eq? 'relative base) (not single-collect)) s]
         [else
          (define bstr (path-element->bytes name))
          (cond
           [(or (equal? #"info.rkt" bstr)
                (equal? #"info.ss" bstr))
            ;; don't count "info.rkt" as a conflict, because
            ;; splices may need their own "info.rkt"s, and
            ;; `raco setup' can handle that
            s]
           [(regexp-match? #rx#"[.](?:rkt|ss|scrbl)$" bstr)
            (try-path s f)]
           [(and check-zo?
                 (regexp-match? #rx#"_(?:rkt|ss|scrbl)[.]zo$" (path-element->bytes name)))
            (define-values (dir-base dir-name dir?) (split-path base))
            (cond
             [(eq? 'relative dir-base) s]
             [(equal? dir-name compiled)
              (define bstr2 (regexp-replace
                             #rx#"_(?:rkt|ss|scrbl)[.]zo$"
                             (path-element->bytes name)
                             #".rkt"))
              (if (equal? #"info.rkt" bstr2)
                  s
                  (try-path s (build-path dir-base
                                          (bytes->path-element
                                           bstr2))))]
             [else s])]
           [else s])])]))))

(define (pkg-catalog-update-local #:catalog-file [catalog-file (db:current-pkg-catalog-file)]
                                  #:quiet? [quiet? #f]
                                  #:consult-packages? [consult-packages? #f])
  (parameterize ([db:current-pkg-catalog-file catalog-file])
    (define catalogs (pkg-config-catalogs))
    (db:set-catalogs! catalogs)

    (for ([catalog (in-list catalogs)])
      (parameterize ([current-pkg-catalogs (list (string->url catalog))])
        (define details (get-all-pkg-details-from-catalogs))
        ;; set packages:
        (db:set-pkgs! catalog (for/list ([(name ht) (in-hash details)])
                                (db:pkg name
                                        catalog
                                        (hash-ref ht 'author "")
                                        (hash-ref ht 'source "")
                                        (hash-ref ht 'checksum "")
                                        (hash-ref ht 'description ""))))
        ;; Add available module and dependency info:
        (for/list ([(name ht) (in-hash details)])
          (define checksum (hash-ref ht 'checksum ""))
          (define mods (hash-ref ht 'modules #f))
          (when mods
            (db:set-pkg-modules! name catalog checksum mods))
          (define deps (hash-ref ht 'dependencies #f))
          (when deps
            (db:set-pkg-dependencies! name catalog checksum deps)))
        (when consult-packages?
          ;; If module information isn't available for a package, download
          ;; the package to fill in that information:
          (define need-modules (db:get-pkgs-without-modules #:catalog catalog))
          (for ([(pkg) (in-list need-modules)])
            (define name (db:pkg-name pkg))
            (define ht (hash-ref details name))
            (define source (hash-ref ht 'source))
            (unless quiet?
              (printf/flush "Downloading ~s\n" source))
            (define-values (checksum modules deps)
              (get-pkg-content (pkg-desc source
                                         #f
                                         name
                                         (hash-ref ht 'checksum #f)
                                         #f)))
            (db:set-pkg-modules! name catalog checksum modules)
            (db:set-pkg-dependencies! name catalog checksum deps)))))))

(define (choose-catalog-file)
  (define default (db:current-pkg-catalog-file))
  (if (file-exists? default)
      default
      (let ([installation (build-path (find-share-dir) "pkgs" (file-name-from-path default))])
        (if (file-exists? installation)
            installation
            default))))

(define (pkg-catalog-suggestions-for-module module-path
                                            #:catalog-file [catalog-file (choose-catalog-file)])
  (if (file-exists? catalog-file)
      (parameterize ([db:current-pkg-catalog-file catalog-file])
        (let* ([mod (collapse-module-path 
                     module-path
                     (lambda () (build-path (current-directory) "dummy.rkt")))]
               [pkgs (db:get-module-pkgs mod)]
               [more-pkgs (let ([rx:reader #rx"/lang/reader[.]rkt$"])
                            (if (and (pair? mod)
                                     (eq? (car mod) 'lib)
                                     (regexp-match rx:reader (cadr mod)))
                                (db:get-module-pkgs `(lib ,(regexp-replace rx:reader (cadr mod) "/main.rkt")))
                                null))])
          (sort (set->list
                 (list->set
                  (map db:pkg-name (append pkgs more-pkgs)))) 
                string<?)))
      null))
  
(define dep-behavior/c
  (or/c #f 'fail 'force 'search-ask 'search-auto))

(define package-scope/c
  (or/c 'installation 'user
        (and/c path? complete-path?)))

(provide
 (all-from-out "path.rkt")
 with-pkg-lock
 with-pkg-lock/read-only
 pkg-desc?
 (contract-out
  [current-pkg-scope
   (parameter/c package-scope/c)]
  [current-pkg-scope-version
   (parameter/c string?)]
  [current-pkg-error 
   (parameter/c procedure?)]
  [current-pkg-catalogs
   (parameter/c (or/c #f (listof url?)))]
  [current-pkg-download-cache-dir
   (parameter/c (or/c #f (and path-string? complete-path?)))]
  [current-pkg-download-cache-max-files
   (parameter/c (or/c #f real?))]
  [current-pkg-download-cache-max-bytes
   (parameter/c (or/c #f real?))]
  [pkg-directory
   (-> string? (or/c path-string? #f))]
  [pkg-desc 
   (-> string? 
       (or/c #f 'file 'dir 'link 'static-link 'file-url 'dir-url 'github 'name) 
       (or/c string? #f)
       (or/c string? #f)
       boolean?
       pkg-desc?)]
  [pkg-config
   (->* (boolean? (listof string?))
        (#:from-command-line? boolean?)
        void?)]
  [pkg-create
   (->* ((or/c 'zip 'tgz 'plt 'MANIFEST)
         path-string?)
        (#:source (or/c 'dir 'name)
                  #:mode (or/c 'as-is 'source 'binary 'built)
                  #:quiet? boolean?
                  #:from-command-line? boolean?
                  #:dest (or/c (and/c path-string? complete-path?) #f))
        void?)]
  [pkg-update
   (->* ((listof (or/c string? pkg-desc?)))
        (#:dep-behavior dep-behavior/c
                        #:all? boolean?
                        #:update-deps? boolean?
                        #:update-implies? boolean?
                        #:quiet? boolean?
                        #:from-command-line? boolean?
                        #:all-platforms? boolean?
                        #:force? boolean?
                        #:ignore-checksums? boolean?
                        #:use-cache? boolean?
                        #:strip (or/c #f 'source 'binary)
                        #:link-dirs? boolean?)
        (or/c #f 'skip (listof (or/c path-string? (non-empty-listof path-string?)))))]
  [pkg-remove
   (->* ((listof string?))
        (#:auto? boolean?
                 #:force? boolean?
                 #:quiet? boolean?
                 #:from-command-line? boolean?
                 #:demote? boolean?)
        (or/c #f 'skip (listof (or/c path-string? (non-empty-listof path-string?)))))]
  [pkg-show
   (->* (string?)
        (#:directory? boolean?
                      #:auto? boolean?)
        void?)]
  [pkg-install
   (->* ((listof pkg-desc?))
        (#:dep-behavior dep-behavior/c
                        #:update-deps? boolean?
                        #:update-implies? boolean?
                        #:all-platforms? boolean?
                        #:force? boolean?
                        #:ignore-checksums? boolean?
                        #:use-cache? boolean?
                        #:skip-installed? boolean?
                        #:quiet? boolean?
                        #:from-command-line? boolean?
                        #:strip (or/c #f 'source 'binary)
                        #:link-dirs? boolean?)
        (or/c #f 'skip (listof (or/c path-string? (non-empty-listof path-string?)))))]
  [pkg-migrate
   (->* (string?)
        (#:dep-behavior dep-behavior/c
                        #:all-platforms? boolean?
                        #:force? boolean?
                        #:ignore-checksums? boolean?
                        #:use-cache? boolean?
                        #:quiet? boolean?
                        #:from-command-line? boolean?
                        #:strip (or/c #f 'source 'binary))
        (or/c #f 'skip (listof (or/c path-string? (non-empty-listof path-string?)))))]
  [pkg-catalog-show
   (->* ((listof string?))
        (#:all? boolean?
                #:only-names? boolean?
                #:modules? boolean?)
        void?)]
  [pkg-catalog-copy
   (->* ((listof path-string?) path-string?)
        (#:from-config? any/c
                        #:merge? boolean?
                        #:force? boolean?
                        #:override? boolean?)
        void?)]
  [default-pkg-scope
   (-> package-scope/c)]
  [installed-pkg-names
   (->* ()
        (#:scope (or/c #f package-scope/c))
        (listof string?))]
  [installed-pkg-table
   (->* ()
        (#:scope (or/c #f package-scope/c))
        (hash/c string? pkg-info?))]
  [pkg-stage (->* (pkg-desc?)
                  (#:namespace namespace?
                               #:in-place? boolean?
                               #:strip (or/c #f 'source 'binary))
                  (values string?
                          path?
                          (or/c #f string?)
                          boolean?
                          (listof module-path?)))]
  [pkg-config-catalogs
   (-> (listof string?))]
  [pkg-catalog-update-local
   (->* ()
        (#:catalog-file path-string?
         #:quiet? boolean?
         #:consult-packages? boolean?)
        void?)]
  [pkg-catalog-suggestions-for-module
   (->* (module-path?)
        (#:catalog-file path-string?)
        (listof string?))]
  [get-all-pkg-names-from-catalogs
   (-> (listof string?))]
  [get-all-pkg-details-from-catalogs
   (-> (hash/c string? (hash/c symbol? any/c)))]
  [get-pkg-details-from-catalogs
   (-> string?
       (or/c #f (hash/c symbol? any/c)))]
  [get-pkg-content
   (->* (pkg-desc?)
        (#:extract-info (-> (or/c #f
                                  ((symbol?) ((-> any)) . ->* . any))
                            any/c)
                        #:namespace namespace?)
        (values (or/c #f string?)
                (listof module-path?)
                any/c))]
  [extract-pkg-dependencies
   (->* ((symbol? (-> any/c) . -> . any/c))
        (#:build-deps? boolean?
                       #:filter? boolean?)
        (listof (or/c string? (cons/c string? list?))))]
  [pkg-single-collection
   (->* (path-string?)
        (#:name string?
                #:namespace namespace?)
        (or/c #f string?))]
  [find-pkg-installation-scope (->* (string?)
                                    (#:next? boolean?)
                                    (or/c #f package-scope/c))]
  [pkg-directory->module-paths (->* (path-string? string?)
                                    (#:namespace namespace?)
                                    (listof module-path?))]))

