From 3201f1330a68f800f6fdc804f0c44f926153b018 Mon Sep 17 00:00:00 2001 From: Matthew Flatt Date: Tue, 20 Aug 2013 18:05:43 -0600 Subject: [PATCH] raco pkg install: make `--force` override scope-conflict errors It's possible that an installation will have a package X already and a user wants to install a different X. To make it all work out, the user may have to also install a new Y for every installation-scoped Y that refers to X --- maybe not easy, but at least possible as a last resort. --- .../racket-doc/pkg/scribblings/pkg.scrbl | 30 ++++++--- .../racket-doc/scribblings/raco/config.scrbl | 3 +- .../racket-test/tests/pkg/test.rkt | 3 +- .../racket-test/tests/pkg/tests-raco.rkt | 6 +- .../racket-test/tests/pkg/tests-scope.rkt | 67 +++++++++++++++++++ .../racket-test/tests/pkg/tests-update.rkt | 4 +- .../racket-test/tests/pkg/util.rkt | 61 ++++++++++++++--- racket/collects/pkg/lib.rkt | 60 ++++++++++------- 8 files changed, 180 insertions(+), 54 deletions(-) create mode 100644 pkgs/racket-pkgs/racket-test/tests/pkg/tests-scope.rkt diff --git a/pkgs/racket-pkgs/racket-doc/pkg/scribblings/pkg.scrbl b/pkgs/racket-pkgs/racket-doc/pkg/scribblings/pkg.scrbl index 582a9e115a..34bb5592d4 100644 --- a/pkgs/racket-pkgs/racket-doc/pkg/scribblings/pkg.scrbl +++ b/pkgs/racket-pkgs/racket-doc/pkg/scribblings/pkg.scrbl @@ -253,8 +253,16 @@ specific to both the current user and the installation's name/version all users of the Racket installation. Finally, a directory path can be used as a @tech{package scope}, in which case package operations affect the set of packages installations in the -directory (and an installation can be configured to include the -directory in its search path for installed packages). +directory; an installation can be configured to include the +directory in its search path for installed packages (see +@secref["config-file" #:doc '(lib "scribblings/raco/raco.scrbl")]). +Conflict checking disallows installation of the same or conflicting +package in different scopes, but if such a configuration is forced, +collections are found first in packages with @exec{user} @tech{package +scope}; search then proceeds in a configured order, where +@exec{installation} @tech{package scope} typically precedes other +directory @tech{package scopes}. + @; ---------------------------------------- @@ -285,7 +293,8 @@ sub-commands. @itemlist[ @item{@exec{fail} --- Cancels the installation if dependencies are uninstalled or version requirements are unmet. This behavior is the default for a @nonterm{pkg-source} that is not a @tech{package name}.} - @item{@exec{force} --- Installs the package(s) despite missing dependencies or version requirements (unsafe).} + @item{@exec{force} --- Installs the package(s) despite missing dependencies or version requirements. + Forcing an installation may leave package content in an inconsistent state.} @item{@exec{search-ask} --- Looks for dependencies (when uninstalled) or updates (when version requirements are unmet) via the configured @tech{package catalogs}, but asks if you would like the packages installed or updated. This behavior is the default for a @@ -342,7 +351,9 @@ sub-commands. whose name corresponds to an already-installed package, except for promoting auto-installed packages to explicitly installed.} - @item{@DFlag{force} --- Ignores conflicts (unsafe).} + @item{@DFlag{force} --- Ignores module conflicts, including conflicts due to installing a single + package in multiple scopes. Forcing an installation may leave package content in an + inconsistent state.} @item{@DFlag{ignore-checksums} --- Ignores errors verifying package @tech{checksums} (unsafe).} @@ -799,13 +810,12 @@ multi-collection packages) with @exec{raco link}. related for conflict checking?} User-specific packages are checked against installation-wide packages -for conflicts. Installation-wide packages are checked only against -other installation-wide packages. +for package-name conflicts and provided-module +conflicts. Installation-wide packages are checked against +user-specific packages only for provided-module conflicts. -Beware that a new installation-wide package can invalidate previous -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. +Beware that a conflict-free, installation-wide change by one user can +create conflicts for a different user. @subsection{Do I need to change a package's version when I update a package with error fixes, @|etc|?} diff --git a/pkgs/racket-pkgs/racket-doc/scribblings/raco/config.scrbl b/pkgs/racket-pkgs/racket-doc/scribblings/raco/config.scrbl index d927a9a284..35c57dd16a 100644 --- a/pkgs/racket-pkgs/racket-doc/scribblings/raco/config.scrbl +++ b/pkgs/racket-pkgs/racket-doc/scribblings/raco/config.scrbl @@ -52,8 +52,7 @@ directory: @item{@indexed-racket['links-file] --- a path, string, or byte string for the @tech[#:doc reference-doc]{collection links file}; it defaults - to a @filepath{links.rktd} file in the @filepath{share} sibling - of the main collection directory.} + to a @filepath{links.rktd} file in the main shared-file directory.} @item{@indexed-racket['links-search-files] --- like @racket['lib-search-dirs], but for @tech[#:doc reference-doc]{collection links file}.} diff --git a/pkgs/racket-pkgs/racket-test/tests/pkg/test.rkt b/pkgs/racket-pkgs/racket-test/tests/pkg/test.rkt index c75fb02184..bf18c471f4 100644 --- a/pkgs/racket-pkgs/racket-test/tests/pkg/test.rkt +++ b/pkgs/racket-pkgs/racket-test/tests/pkg/test.rkt @@ -52,7 +52,8 @@ ;; "main-server" "update-deps" "update-auto" - "migrate" + "scope" + "migrate" "versions" "platform" "raco" diff --git a/pkgs/racket-pkgs/racket-test/tests/pkg/tests-raco.rkt b/pkgs/racket-pkgs/racket-test/tests/pkg/tests-raco.rkt index e2e5cd14e6..3c3a0d5006 100644 --- a/pkgs/racket-pkgs/racket-test/tests/pkg/tests-raco.rkt +++ b/pkgs/racket-pkgs/racket-test/tests/pkg/tests-raco.rkt @@ -18,12 +18,12 @@ $ "raco pkg create --format plt test-pkgs/raco-pkg" $ "raco raco-pkg" =exit> 1 $ "raco pkg install --no-setup test-pkgs/raco-pkg.plt" - $ "raco raco-pkg" =exit> 1 - (putenv "PLT_PKG_NOSETUP" ""))) + $ "raco raco-pkg" =exit> 1)) (with-fake-root (shelly-case "raco install/update uses raco setup" + (putenv "PLT_PKG_NOSETUP" "") $ "raco pkg create --format plt test-pkgs/raco-pkg" $ "raco raco-pkg" =exit> 1 $ "raco pkg install test-pkgs/raco-pkg.plt" @@ -32,6 +32,7 @@ (with-fake-root (shelly-case "raco install uses raco setup with single collect" + (putenv "PLT_PKG_NOSETUP" "") $ "raco pkg install --copy test-pkgs/pkg-test3-v3" =exit> 0)) (shelly-begin @@ -39,6 +40,7 @@ (shelly-case "update of package runs setup on package with dependency" + (putenv "PLT_PKG_NOSETUP" "") (shelly-wind $ "mkdir -p test-pkgs/update-test" $ "cp -f test-pkgs/pkg-test1.zip test-pkgs/update-test/pkg-test1.zip" diff --git a/pkgs/racket-pkgs/racket-test/tests/pkg/tests-scope.rkt b/pkgs/racket-pkgs/racket-test/tests/pkg/tests-scope.rkt new file mode 100644 index 0000000000..9855d8a048 --- /dev/null +++ b/pkgs/racket-pkgs/racket-test/tests/pkg/tests-scope.rkt @@ -0,0 +1,67 @@ +#lang racket/base +(require "shelly.rkt" + "util.rkt") + +(pkg-tests + (with-fake-installation + (with-fake-root + + (shelly-case + "default scope in different scopes" + $ "raco pkg show" + $ "raco pkg config --set -i default-scope installation" + $ "raco pkg config -u default-scope" =stdout> "installation\n" ; inherited + $ "raco pkg config --set default-scope user" ; set user at installation scope + $ "raco pkg config -u default-scope" =stdout> "user\n" ; inherited + $ "raco pkg config -i default-scope" =stdout> "user\n" + + $ "raco pkg config --set default-scope installation" ; set installation at user scope + $ "raco pkg config -u default-scope" =stdout> "installation\n" + $ "raco pkg config -i default-scope" =stdout> "user\n" + $ "raco pkg config default-scope" =stdout> "user\n" ; misleading! + + $ "raco pkg config --set default-scope user" ; still sets user in installation + $ "raco pkg config -i default-scope" =stdout> "user\n" + $ "raco pkg config -u default-scope" =stdout> "installation\n" + + $ "raco pkg config -u --set default-scope user" + $ "raco pkg config default-scope" =stdout> "user\n") + + (shelly-case + "conflict due to installations in different scopes: installation-wide first" + $ "raco pkg install -i test-pkgs/pkg-test1" + $ "racket -l pkg-test1" =stdout> #rx"main loaded" + $ "raco pkg install -u test-pkgs/pkg-test1" =exit> 1 =stderr> #rx"installed in a wider scope" + $ "raco pkg install -u --name base test-pkgs/pkg-test1" =exit> 1 =stderr> #rx"installed in a wider scope" + $ "raco pkg remove pkg-test1" =stdout> #rx"Inferred package scope: installation") + + (shelly-case + "conflict due to installations in different scopes: user-specific first" + $ "raco pkg install -u test-pkgs/pkg-test1" + $ "racket -l pkg-test1" =stdout> #rx"main loaded" + $ "raco pkg install -i test-pkgs/pkg-test1" =exit> 1 =stderr> #rx"packages in different scopes conflict" + $ "raco pkg remove pkg-test1" =stdout> "Removing pkg-test1\n") + + (shelly-case + "override conflist, installation first" + $ "raco pkg install -i test-pkgs/pkg-test1" + $ "racket -l racket/base -l pkg-test1/number -e '(number)'" =stdout> "1\n" + $ "raco pkg install -u --name pkg-test1 test-pkgs/pkg-test1-v2" =exit> 1 + $ "raco pkg install --force -u --name pkg-test1 test-pkgs/pkg-test1-v2" + $ "racket -l racket/base -l pkg-test1/number -e '(number)'" =stdout> "2\n" + $ "raco pkg remove pkg-test1" =stdout> "Removing pkg-test1\n" + $ "racket -l racket/base -l pkg-test1/number -e '(number)'" =stdout> "1\n" + $ "raco pkg remove pkg-test1" =stdout> #rx"Inferred package scope: installation") + + (shelly-case + "override conflist, user first" + $ "raco pkg install -u test-pkgs/pkg-test1" + $ "racket -l racket/base -l pkg-test1/number -e '(number)'" =stdout> "1\n" + $ "raco pkg install -i --name pkg-test1 test-pkgs/pkg-test1-v2" =exit> 1 + $ "raco pkg install --force -i --name pkg-test1 test-pkgs/pkg-test1-v2" + $ "racket -l racket/base -l pkg-test1/number -e '(number)'" =stdout> "1\n" + $ "raco pkg remove pkg-test1" =stdout> "Removing pkg-test1\n" + $ "racket -l racket/base -l pkg-test1/number -e '(number)'" =stdout> "2\n" + $ "raco pkg remove pkg-test1" =stdout> #rx"Inferred package scope: installation") + + ))) diff --git a/pkgs/racket-pkgs/racket-test/tests/pkg/tests-update.rkt b/pkgs/racket-pkgs/racket-test/tests/pkg/tests-update.rkt index c202d364f1..908ce2d01c 100644 --- a/pkgs/racket-pkgs/racket-test/tests/pkg/tests-update.rkt +++ b/pkgs/racket-pkgs/racket-test/tests/pkg/tests-update.rkt @@ -138,7 +138,7 @@ $ "cp -f test-pkgs/pkg-test1-v2.zip.CHECKSUM test-pkgs/update-test/pkg-test1.zip.CHECKSUM" $ "cp -f test-pkgs/pkg-test3-v2.zip test-pkgs/update-test/pkg-test3.zip" $ "cp -f test-pkgs/pkg-test3-v2.zip.CHECKSUM test-pkgs/update-test/pkg-test3.zip.CHECKSUM" - $ "raco pkg update --update-deps pkg-test3" =exit> 0 + $ "raco pkg update --update-deps --deps search-auto pkg-test3" =exit> 0 $ "racket -e '(require pkg-test1/update)'" =exit> 43 $ "racket -e '(require pkg-test3)'" =stdout> #rx"version 2 loaded" $ "raco pkg remove pkg-test3") @@ -189,7 +189,7 @@ $ "cp -f test-pkgs/pkg-test1-v2.zip.CHECKSUM test-pkgs/update-test/pkg-test1.zip.CHECKSUM" $ "cp -f test-pkgs/pkg-test3.zip test-pkgs/update-test/pkg-test3.zip" $ "cp -f test-pkgs/pkg-test3.zip.CHECKSUM test-pkgs/update-test/pkg-test3.zip.CHECKSUM" - $ "raco pkg update --update-deps pkg-test3" =exit> 0 + $ "raco pkg update --update-deps --deps search-auto pkg-test3" =exit> 0 $ "racket -e '(require pkg-test1/update)'" =exit> 43 $ "racket -e '(require pkg-test3)'" =stdout> #rx"main loaded" $ "raco pkg remove pkg-test3") diff --git a/pkgs/racket-pkgs/racket-test/tests/pkg/util.rkt b/pkgs/racket-pkgs/racket-test/tests/pkg/util.rkt index f6e3d1bf05..c83d0ab210 100644 --- a/pkgs/racket-pkgs/racket-test/tests/pkg/util.rkt +++ b/pkgs/racket-pkgs/racket-test/tests/pkg/util.rkt @@ -9,6 +9,7 @@ racket/runtime-path racket/path racket/list + setup/dirs pkg/util "shelly.rkt") @@ -20,26 +21,64 @@ (and (file-exists? p) p)) +(define (with-fake-installation* t) + (define tmp-dir + (make-temporary-file ".racket.fake-installation~a" 'directory + (find-system-path 'temp-dir))) + (make-directory* tmp-dir) + (dynamic-wind + void + (λ () + (define ->s path->string) + (define config + (hash + ;; redirect main installation via "share" to + ;; our temporary directory: + 'share-dir + (->s (build-path tmp-dir)) + + ;; Find existing links and packages from the + ;; old configuration: + 'links-search-files + (cons #f + (map ->s (get-links-search-files))) + 'pkgs-search-dirs + (cons #f + (map ->s (get-pkgs-search-dirs))))) + (call-with-output-file* + (build-path tmp-dir "config.rktd") + (lambda (o) + (write config o) + (newline o))) + (define tmp-dir-s + (path->string tmp-dir)) + (parameterize ([current-environment-variables + (environment-variables-copy + (current-environment-variables))]) + (putenv "PLTCONFIGDIR" tmp-dir-s) + (t))) + (λ () + (delete-directory/files tmp-dir)))) +(define-syntax-rule (with-fake-installation e ...) + (with-fake-installation* (λ () e ...))) + (define (with-fake-root* t) (define tmp-dir (make-temporary-file ".racket.fake-root~a" 'directory (find-system-path 'home-dir))) (make-directory* tmp-dir) - (define tmp-dir-s - (path->string tmp-dir)) - (define before - (or (getenv "PLTADDONDIR") - (path->string (find-system-path 'addon-dir)))) (dynamic-wind void (λ () - (putenv "PLTADDONDIR" - tmp-dir-s) - (t)) + (define tmp-dir-s + (path->string tmp-dir)) + (parameterize ([current-environment-variables + (environment-variables-copy + (current-environment-variables))]) + (putenv "PLTADDONDIR" tmp-dir-s) + (t))) (λ () - (delete-directory/files tmp-dir) - (putenv "PLTADDONDIR" - before)))) + (delete-directory/files tmp-dir)))) (define-syntax-rule (with-fake-root e ...) (with-fake-root* (λ () e ...))) diff --git a/racket/collects/pkg/lib.rkt b/racket/collects/pkg/lib.rkt index c63d558255..d12da87333 100644 --- a/racket/collects/pkg/lib.rkt +++ b/racket/collects/pkg/lib.rkt @@ -1227,43 +1227,48 @@ (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)) - (define this-pkg-info (hash-ref all-db pkg-name #f)) + [(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? this-pkg-info) + [(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 this-pkg-info) orig-pkg) + (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 this-pkg-info #f)))] + (update-pkg-db! pkg-name (update-auto existing-pkg-info #f)))] [else ;; Fail --- already installed (clean!) - (if (and (pkg-info-auto? this-pkg-info) - (not (pkg-desc-auto? desc))) - ;; It failed either due to scope or source: - (if (equal? (pkg-info-orig-pkg this-pkg-info) orig-pkg) - (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)) - (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 this-pkg-info) - orig-pkg)) - (pkg-error "package is already installed\n package: ~a" - pkg-name))])] + (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)]) @@ -1300,10 +1305,13 @@ (clean!) (match-define (cons conflicting-pkg mp) conflicting-pkg*mp) (if conflicting-pkg - (pkg-error (~a "packages conflict\n" + (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\n" " package: ~a\n"