planet2: versions for dependencies

This commit is contained in:
Matthew Flatt 2012-12-20 11:07:37 -07:00
parent bfe9548aa6
commit 0e140a8173
7 changed files with 319 additions and 72 deletions

View File

@ -23,6 +23,7 @@
setup/getinfo
setup/dirs
racket/format
version/utils
"name.rkt"
"util.rkt")
@ -153,12 +154,35 @@
(define (check-dependencies deps)
(unless (and (list? deps)
(for/and ([dep (in-list deps)])
(and (string? dep)
(package-source->name dep))))
(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))))))
(pkg-error (~a "invalid `deps' specification\n"
" specification: ~e")
deps)))
(define (dependency->name dep)
(package-source->name
(dependency->source dep)))
(define (dependency->source dep)
(if (string? dep)
dep
(car dep)))
(define (dependency->version dep)
(if (string? dep)
#f
(cadr dep)))
(define (with-package-lock* t)
(make-directory* (pkg-dir))
(call-with-file-lock/timeout
@ -187,10 +211,11 @@
(define (package-index-lookup pkg)
(or
(for/or ([i (in-list (read-pkg-cfg/def "indexes"))])
(define addr (combine-url/relative (string->url i)
(format "pkg/~a" pkg)))
(log-planet2-debug "resolving via ~a" (url->string addr))
(call/input-url+200
(combine-url/relative
(string->url i)
(format "/pkg/~a" pkg))
addr
read))
(pkg-error (~a "cannot find package on indexes\n"
" package: ~a")
@ -631,7 +656,8 @@
(when clean?
(delete-directory/files pkg-dir)))
(define simultaneous-installs
(list->set (map install-info-name infos)))
(for/hash ([i (in-list infos)])
(values (install-info-name i) (install-info-directory i))))
(cond
[(and (not updating?) (package-info pkg-name #f))
(clean!)
@ -683,32 +709,38 @@
'deps (lambda () empty)
#:checker check-dependencies))
(define unsatisfied-deps
(filter-not (λ (dep)
(or (set-member? simultaneous-installs
(package-source->name dep))
(hash-has-key? db dep)))
deps))
(map dependency->source
(filter-not (λ (dep)
(define name (dependency->name dep))
(or (equal? name "racket")
(hash-ref simultaneous-installs name #f)
(hash-has-key? db name)))
deps)))
(and (not (empty? unsatisfied-deps))
unsatisfied-deps)))
=>
(λ (unsatisfied-deps)
(match
(match
(or dep-behavior
(if name?
'search-ask
'fail))
['fail
(clean!)
(pkg-error "missing dependencies\n missing packages:~a" (format-list unsatisfied-deps))]
(pkg-error (~a "missing dependencies\n"
" for package: ~a\n"
" missing packages:~a")
pkg
(format-list unsatisfied-deps))]
['search-auto
(printf (string-append
"The following packages are listed as dependencies, but are not currently installed,\n"
"so we will automatically install them:\n"))
"so they will be automatically installed:\n"))
(printf "\t")
(for ([p (in-list unsatisfied-deps)])
(printf "~a " p))
(printf "\n")
(raise (vector infos unsatisfied-deps))]
(raise (vector updating? infos unsatisfied-deps void))]
['search-ask
(printf "The following packages are listed as dependencies, but are not currently installed:\n")
(printf "\t")
@ -720,13 +752,110 @@
(flush-output)
(match (read-line)
[(or "y" "Y" "")
(raise (vector infos unsatisfied-deps))]
(raise (vector updating? infos unsatisfied-deps void))]
[(or "n" "N")
(clean!)
(pkg-error "missing dependencies\n missing packages:~a" (format-list unsatisfied-deps))]
[x
(eprintf "Invalid input: ~e\n" x)
(loop)]))]))]
[(and
(not (eq? dep-behavior 'force))
(let ()
(define deps (get-metadata metadata-ns pkg-dir
'deps (lambda () empty)
#:checker check-dependencies))
(define update-deps
(filter-map (λ (dep)
(define name (dependency->name dep))
(define req-vers (dependency->version dep))
(define-values (inst-vers* can-try-update?)
(cond
[(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 (package-directory name)
'version (lambda () "0.0"))
#t)]))
(define inst-vers (if (and req-vers
(not (and (string? inst-vers*)
(valid-version? inst-vers*))))
(begin
(log-planet2-error
"bad verson specification for ~a: ~e"
name
inst-vers*)
"0.0")
inst-vers*))
(and 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)))
(define (format-deps update-deps)
(format-list (for/list ([ud (in-list update-deps)])
(format "~a (have ~a, need ~a)"
(car ud)
(caddr ud)
(cadddr ud)))))
;; 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)
(let ([to-update (filter-map update-package update-pkgs)])
(log-error "to update ~s" to-update)
(λ () (for-each (compose remove-package pkg-desc-name) to-update))))
(match (or dep-behavior
(if name?
'search-ask
'fail))
['fail
(clean!)
(report-mismatch update-deps)]
['search-auto
(printf (string-append
"The following packages are listed as dependencies, but are not at the required\n"
"version, so they will be automatically updated:~a\n")
(format-deps update-deps))
(raise (vector #t infos update-pkgs (make-pre-succeed)))]
['search-ask
(printf (~a "The following packages are listed as dependencies, but are not at the required\n"
"versions:~a\n")
(format-deps update-deps))
(let loop ()
(printf "Would you like to update them via your package indices? [Yn] ")
(flush-output)
(match (read-line)
[(or "y" "Y" "")
(raise (vector #t infos update-pkgs (make-pre-succeed)))]
[(or "n" "N")
(clean!)
(report-mismatch update-deps)]
[x
(eprintf "Invalid input: ~e\n" x)
(loop)]))]))]
[else
(λ ()
(define final-pkg-dir
@ -788,14 +917,14 @@
#:updating? [updating? #f])
(with-handlers* ([vector?
(match-lambda
[(vector new-infos deps)
[(vector updating? new-infos deps more-pre-succeed)
(install-cmd
#:old-infos new-infos
#:old-auto+pkgs (append old-descs descs)
#:force? force
#:ignore-checksums? ignore-checksums
#:dep-behavior dep-behavior
#:pre-succeed pre-succeed
#:pre-succeed (lambda () (pre-succeed) (more-pre-succeed))
#:updating? updating?
(for/list ([dep (in-list deps)])
(pkg-desc dep #f #f #t)))])])
@ -841,7 +970,7 @@
(and new-checksum
(not (equal? checksum new-checksum))
;; FIXME: the type shouldn't be #f here; it should be
;; preseved form instal time:
;; preseved from install time:
(pkg-desc orig-pkg-source #f pkg-name auto?))]))
(define ((package-dependencies metadata-ns) pkg-name)

View File

@ -39,26 +39,38 @@ metadata}.
@deftech{Package metadata} is:
@itemlist[
@item{a @deftech{package name} -- a string made of the characters @|package-name-chars|.}
@item{a list of dependencies -- a list of strings that name other packages that must be installed simultaneously.}
@item{a checksum -- a string that identifies different releases of a package.}
@item{a @deftech{package name} --- a string made of the characters @|package-name-chars|.}
@item{a @deftech{version} --- a string of the form @nonterm{maj}@litchar{.}@nonterm{min},
@nonterm{maj}@litchar{.}@nonterm{min}@litchar{.}@nonterm{sub}, or
@nonterm{maj}@litchar{.}@nonterm{min}@litchar{.}@nonterm{sub}@litchar{.}@nonterm{rel},
where @nonterm{maj}, @nonterm{min}, @nonterm{sub}, and @nonterm{rel} are
all canonical decimal representations of natural numbers, @nonterm{min} has no more
than two digits, and @nonterm{sub} and @nonterm{rel} has no more than
three digits. A version is intended to reflect available features of
a package, and should not be confused with different releases of
a package as indicated by the @tech{checksum}.}
@item{a list of dependencies --- a list of packages to be installed simultaneously, optionally
with a lower bound on each package's version.}
@item{a @deftech{checksum} --- a string that identifies different releases of a package. A
package can be updated when its @tech{checksum} changes
whether or not its @tech{version} changes.}
]
A @tech{package} is typically represented by a directory with the same
name as the package. The checksum is typically left implicit.
If the package depends on other packages, the directory can
contain a file named @filepath{info.rkt} (see @secref["metadata"]).
name as the package. The @tech{checksum} is typically left implicit.
The package directory can contain a file named @filepath{info.rkt}
to declare other metadata (see @secref["metadata"]).
A @deftech{package source} identifies a @tech{package}
representation. Each package source type has a different way of
storing the checksum. The valid package source types are:
storing the @tech{checksum}. The valid package source types are:
@itemlist[
@item{a local file path naming an archive -- The name of the package
is the basename of the archive file. The checksum for archive
is the basename of the archive file. The @tech{checksum} for archive
@filepath{f.@nonterm{ext}} is given by the file @filepath{f.@nonterm{ext}.CHECKSUM}. For
example, @filepath{~/tic-tac-toe.zip}'s checksum would be inside
example, @filepath{~/tic-tac-toe.zip}'s @tech{checksum} would be inside
@filepath{~/tic-tac-toe.zip.CHECKSUM}. The valid archive formats
are (currently) @filepath{.zip}, @filepath{.tar}, @filepath{.tgz},
@filepath{.tar.gz}, and
@ -71,7 +83,7 @@ with alphabetic characters followed by @litchar{://}. The inferred
package name is the filename without its suffix.}
@item{a local directory -- The name of the package is the name of the
directory. The checksum is not present. For example,
directory. The @tech{checksum} is not present. For example,
@filepath{~/tic-tac-toe/} is directory package source.
A package source is inferred to refer
@ -81,10 +93,10 @@ with alphabetic characters followed by @litchar{://}. The inferred
package name is the directory name.}
@item{a remote URL naming an archive -- This type follows the same
rules as a local file path, but the archive and checksum files are
rules as a local file path, but the archive and @tech{checksum} files are
accessed via HTTP(S). For example,
@filepath{http://game.com/tic-tac-toe.zip} is a remote URL package
source whose checksum is found at
source whose @tech{checksum} is found at
@filepath{http://game.com/tic-tac-toe.zip.CHECKSUM}.
A package source is inferred to be a URL only when it
@ -99,9 +111,9 @@ contain a file named @filepath{MANIFEST} that lists all the contingent
files. These are downloaded into a local directory and then the rules
for local directory paths are followed. However, if the remote
directory contains a file named @filepath{.CHECKSUM}, then it is used
to determine the checksum. For example,
to determine the @tech{checksum}. For example,
@filepath{http://game.com/tic-tac-toe/} is a directory URL package
source whose checksum is found at
source whose @tech{checksum} is found at
@filepath{http://game.com/tic-tac-toe/.CHECKSUM}.
A package source is inferred to be a URL the same for a directory or
@ -120,7 +132,7 @@ is a GitHub package source.
The @exec{zip}-formatted archive for the repository (generated by GitHub for
every branch) is used as a remote URL archive path, except the
checksum is the hash identifying the branch.
@tech{checksum} is the hash identifying the branch.
A package source is inferred to be a GitHub reference when it
starts with @litchar{github://}; a package source that is otherwise
@ -130,7 +142,7 @@ is the last element of @nonterm{optional-subpath} if it is
non-empty, otherwise the inferred name is @nonterm{repository}.}
@item{a @tech{package name} -- A @tech{package name resolver} is
consulted to determine the source and checksum for the package. For
consulted to determine the source and @tech{checksum} for the package. For
example, @exec{tic-tac-toe} is a package name that can be used as a
package source.
@ -143,10 +155,10 @@ means that it has only the characters @|package-name-chars|.}
A @deftech{package name resolver} (@deftech{PNR}) is a server that
converts package names to other package sources. A PNR is identified
by a string representing a URL, such that appending
@exec{/pkg/}@nonterm{package} forms a URL that refers to a
@racket[read]-able hash table with the keys: @racket['source] bound to
the source and @racket['checksum] bound to the checksum. Typically,
the source will be a remote URL string.
@exec{pkg/}@nonterm{package} forms a URL that refers to a
@racket[read]-able hash table with the keys: @racket['source] mapped to
the @tech{package source} string and @racket['checksum] mapped to the
@tech{checksum} value. Typically, the source will be a remote URL.
PLT supports two @tech{package name resolvers} that are enabled by
default: @url{https://plt-etc.byu.edu:9004} for new
@ -175,8 +187,11 @@ the purposes of conflicts, a module is a file that ends in
@filepath{.rkt} or @filepath{.ss}.
Package A is a @deftech{package update} of Package B if (1) B is
installed, (2) A and B have the same name, and (3) A's checksum is
different than B's.
installed, (2) A and B have the same name, and (3) A's @tech{checksum} is
different than B's. Note that a package @tech{version} is not taken
into account when determining a @tech{package update}, although a change
in a package's @tech{version} (in either direction) should normally
imply a change in the @tech{checksum}.
@; ----------------------------------------
@ -212,20 +227,20 @@ sub-sub-commands:
environment variable @envvar{PLT_PLANET2_NOSETUP} is set to any non-empty value.}
@item{@DFlag{installation} or @Flag{i} --- Install system-wide, rather than user-local.}
@item{@DFlag{shared} or @Flag{s} --- Install for all versions, rather than user-local and version-specific.}
@item{@DFlag{shared} or @Flag{s} --- Install for all Racket versions, rather than user-local and version-specific.}
@item{@DFlag{deps} @nonterm{behavior} --- Selects the behavior for dependencies, where @nonterm{behavior} is one of
@itemlist[
@item{@exec{fail} --- Cancels the installation if dependencies are unmet (default for most packages)}
@item{@exec{force} --- Installs the package(s) despite missing dependencies (unsafe)}
@item{@exec{search-ask} --- Looks for the dependencies via the configured @tech{package name resolvers}
(default if the dependency is an indexed name) but asks if you would like it installed.}
@item{@exec{search-auto} --- Like @exec{search-ask}, but does not ask for permission to install.}
@item{@exec{fail} --- Cancels the installation if dependencies are version requirements are unmet (default for most packages)}
@item{@exec{force} --- Installs the package(s) despite missing dependencies or version requirements (unsafe)}
@item{@exec{search-ask} --- Looks for the dependencies or updates via the configured @tech{package name resolvers}
(default if the dependency is an indexed name) but asks if you would like it installed or updated.}
@item{@exec{search-auto} --- Like @exec{search-ask}, but does not ask for permission to install or update.}
]}
@item{@DFlag{force} --- Ignores conflicts (unsafe)}
@item{@DFlag{ignore-checksums} --- Ignores errors verifying package checksums (unsafe.)}
@item{@DFlag{ignore-checksums} --- Ignores errors verifying package @tech{checksums} (unsafe).}
@item{@DFlag{link} --- Implies @exec{--type dir} (and overrides any specified type),
and links the existing directory as an installed package.}
@ -259,13 +274,14 @@ listed, this command fails atomically. It accepts the following @nonterm{option}
@item{@DFlag{installation} or @Flag{i} --- Same as for @exec{install}.}
@item{@DFlag{shared} or @Flag{s} --- Same as for @exec{install}.}
@item{@DFlag{force} --- Ignore dependencies when removing packages.}
@item{@DFlag{auto} --- Remove packages that were installed by the @exec{search-auto} and @exec{search-ask} dependency behavior that are no longer required.}
@item{@DFlag{auto} --- Remove packages that were installed by the @exec{search-auto} or @exec{search-ask}
dependency behavior and are no longer required.}
]
}
@item{@exec{show} @nonterm{option} ... --- Print information about currently installed packages.
By default, packages are shown for all installation modes (installation-wide,
user- and version-specific, and user-specific all-version).
user- and Racket-version-specific, and user-specific all-version).
The command accepts the following @nonterm{option}s:
@itemlist[
@ -398,7 +414,7 @@ first:
@commandline{raco pkg create @nonterm{package}}
And then upload the archive and its checksum to your site:
And then upload the archive and its @tech{checksum} to your site:
@commandline{scp @nonterm{package}.zip @nonterm{package}.zip.CHECKSUM your-host:public_html/}
@ -406,8 +422,8 @@ Now, publish your package source as:
@inset{@exec{http://your-host/~@nonterm{user}/@nonterm{package}.zip}}
Whenever you want to release a new version, recreate and reupload the
package archive (and checksum). Your changes will automatically be
Whenever you want to provide a new release of a package, recreate and reupload the
package archive (and @tech{checksum}). Your changes will automatically be
discovered by those who used your package source when they use
@exec{raco pkg update}.
@ -459,6 +475,16 @@ when backwards incompatible changes are necessary. For example,
present interfaces to external, versioned things, such as
@pkgname{sqlite3} or @pkgname{libgtk2}.}
@item{A @tech{version} declaration for a package is used only by other
package implementors to effectively declare dependencies on provided
features. Such declarations allow @exec{raco pkg install} and
@exec{raco pkg update} to help check dependencies. Declaring and
changing a version is optional, and @tech{package name resolvers}
ignore version declarations; in particular, a package is a candidate
for updating when its @tech{checksum} changes, independent of whether
the package's version changes or in which direction the version
changes.}
@item{Packages should not include large sets of utilities libraries
that are likely to cause conflicts. For example, packages should not
contain many extensions to the @filepath{racket} collection, like
@ -491,11 +517,26 @@ The following fields are used by the package manager:
@itemlist[
@item{@racketidfont{deps} --- a list of @tech{package source} strings.
Each string determines a dependency on the @tech{package} whose name
is inferred from the @tech{package source} (i.e., dependencies are
on package names, not package sources), while the @tech{package source} indicates
where to get the package if needed to satisfy the dependency.}
@item{@racketidfont{version} --- a @tech{version} string. The default
@tech{version} of a package is @racket["0.0"].}
@item{@racketidfont{deps} --- a list of dependencies, where each
dependency is either a @tech{package source} strings or a list
containing a @tech{package source} string and a
@tech{version} string.
Each elements of the @racketidfont{deps} list determines a
dependency on the @tech{package} whose name is inferred from
the @tech{package source} (i.e., dependencies are on package
names, not package sources), while the @tech{package source}
indicates where to get the package if needed to satisfy the
dependency.
When provided, a @tech{version} string specifies a lower bound
on an acceptable @tech{version} of the package.
Use the package name @racket["racket"] to specify a dependency
on the version of the Racket installation.}
@item{@racketidfont{setup-collects} --- a list of path strings and/or
lists of path strings, which are used as collection names to
@ -511,6 +552,7 @@ For example, a basic @filepath{info.rkt} file might be
@codeblock{
#lang setup/infotab
(define version "1.0")
(define deps (list _package-source-string ...))
}
@ -578,7 +620,7 @@ requires reinstallation of all packages every version change.)
@subsection{Where and how are packages installed?}
User-local and version-specific packages are in @racket[(build-path
User-local and Racket-version-specific packages are in @racket[(build-path
(find-system-path 'addon-dir) (version) "pkgs")], user-local and
all-version packages are in @racket[(build-path (find-system-path
'addon-dir) "pkgs")], and installation-wide packages are in
@ -597,18 +639,20 @@ conflict checks for user-specific packages. Similarly, new
user-specific but all-version packages can invalidate previous
user-specific conflict checks for a different Racket version.
@subsection{If packages have no version numbers, how can I update
packages with error fixes, etc?}
@subsection{Do I need to change a package's version when I update a package with error fixes, @|etc|?}
If you have a new version of the code for a package, then it will have
a new checksum. When package updates are searched for, the checksum of
the installed package is compared with the checksum of the source, if
they are different, then the source is re-installed. This allows code
changes to be distributed.
If you have new code for a package, then it should have a new
@tech{checksum}. When package updates are searched for, the checksum
of the installed package is compared with the checksum of the source,
if they are different, then the source is re-installed. This allows
code changes to be distributed. You do not need to declare an update a
version number, except to allow other package implementors to indicate
a dependency on particular features (where a bug fix might be
considered a feature, but it is not usually necessary to consider it
that way).
@subsection{If packages have no version numbers, how can I specify
which version of a package I depend on if its interface has changed
and I need an old version?}
@subsection{How can I specify which version of a package I depend on
if its interface has changed and I need an @emph{old} version?}
In such a situation, the author of the package has released a
backwards incompatible edition of a package. The package manager provides
@ -646,9 +690,7 @@ flexible---so that code can migrate in and out of the core, packages
can easily be split up, combined, or taken over by other authors, etc.
This change is bad because it makes the meaning of your program
dependent on the state of the system. (This is already true of Racket
code in general, because there's no way to make the required core
version explicit, but the problem will be exacerbated by the package manager.)
dependent on the state of the system.
The second major difference is that @|Planet1| is committed to
guaranteeing that packages that never conflict with one another, so

View File

@ -0,0 +1,3 @@
#lang setup/infotab
(define version "1.0")

View File

@ -0,0 +1,3 @@
#lang setup/infotab
(define version "2.1")

View File

@ -0,0 +1,4 @@
#lang setup/infotab
(define deps '(("pkg-v" "2.0")
("racket" "5.3.1.10")))

View File

@ -39,5 +39,6 @@
"planet"
"update-deps"
"update-auto"
"versions"
"raco"
"main-server")

View File

@ -0,0 +1,65 @@
#lang racket/base
(require rackunit
racket/system
unstable/debug
racket/match
(for-syntax racket/base
syntax/parse)
racket/file
racket/runtime-path
racket/path
racket/list
planet2/util
"shelly.rkt"
"util.rkt")
(pkg-tests
(shelly-begin
(initialize-indexes)
(shelly-case
"create packages"
$ "raco pkg create --format zip test-pkgs/pkg-v-one"
$ "raco pkg create --format zip test-pkgs/pkg-v-two"
$ "raco pkg create --format zip test-pkgs/pkg-w-one")
(hash-set! *index-ht-1* "pkg-v"
(hasheq 'checksum
(file->string "test-pkgs/pkg-v-one.zip.CHECKSUM")
'source
"http://localhost:9999/pkg-v-one.zip"))
(hash-set! *index-ht-1* "pkg-w"
(hasheq 'checksum
(file->string "test-pkgs/pkg-w-one.zip.CHECKSUM")
'source
"http://localhost:9999/pkg-w-one.zip"))
$ "raco pkg config --set indexes http://localhost:9990"
(shelly-case
"update"
(shelly-begin "install pkg-v version 1.0"
$ "raco pkg install pkg-v")
(shelly-begin "fail on install pkg-w, bad version for pkg-v"
$ "raco pkg install --deps fail pkg-w"
=exit> 1
=stderr> #rx".*version mismatch for dependency.*for package: pkg-w.*pkg-v [(]have 1[.]0, need 2[.]0[)]")
(shelly-begin "auto-update still fails"
$ "raco pkg install --deps search-auto pkg-w"
=exit> 1
=stderr> #rx".*version mismatch for dependency.*for package: pkg-w.*pkg-v [(]have 1[.]0, need 2[.]0[)]"))
(hash-set! *index-ht-1* "pkg-v"
(hasheq 'checksum
(file->string "test-pkgs/pkg-v-two.zip.CHECKSUM")
'source
"http://localhost:9999/pkg-v-two.zip"))
(shelly-case
"update"
(shelly-begin "auto-update now succeeds (installs and version matches)"
$ "raco pkg install --deps search-auto pkg-w"))
(initialize-indexes)))