diff --git a/CHANGELOG.md b/CHANGELOG.md index c1239f3c5..34d0b7295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ Other improvements: - When the `publish.location` field is missing, `spago publish` will attempt to figure out the location from Git remotes and write it back to `spago.yaml`. - Internally Spago uses stricter-typed file paths. +- `spago install` warns the user when the installed versions of packages are outside + their specified dependency ranges. - `spago publish` no longer tries to validate all workspace dependencies, but only the (transitive) dependencies of the project being published. diff --git a/README.md b/README.md index 992c228b2..38b21275f 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,14 @@ You can ask Spago to come up with a good set of bounds for you by running: $ spago install --ensure-ranges ``` +You can specify your version ranges manually in the `spago.yaml` configuration file too: + +```yaml +package: + dependencies: + - lists: ">=7.0.0 <8.0.0" +``` + ### Install a direct dependency To add a dependency to your project you can run: diff --git a/src/Spago/Command/Fetch.purs b/src/Spago/Command/Fetch.purs index bea158293..768f21e32 100644 --- a/src/Spago/Command/Fetch.purs +++ b/src/Spago/Command/Fetch.purs @@ -53,6 +53,7 @@ import Spago.FS as FS import Spago.Git as Git import Spago.Lock (LockEntry(..)) import Spago.Lock as Lock +import Spago.Log as Log import Spago.Path as Path import Spago.Paths as Paths import Spago.Purs as Purs @@ -83,6 +84,12 @@ type FetchOpts = , isRepl :: Boolean } +type VersionResolution = + { name :: PackageName + , requested :: Range + , resolved :: Version + } + run :: forall a. FetchOpts -> Spago (FetchEnv a) PackageTransitiveDeps run { packages: packagesRequestedToInstall, ensureRanges, isTest, isRepl } = do logDebug $ "Requested to install these packages: " <> printJson (CJ.array PackageName.codec) packagesRequestedToInstall @@ -620,9 +627,50 @@ getTransitiveDeps workspacePackage = do <$> forEnv "core" depsRanges.core <*> forEnv "test" depsRanges.test - PackageSetBuild _info set -> do - depsRanges # onEachEnvM \depsRanges' -> - getTransitiveDepsFromPackageSet set $ (Array.fromFoldable $ Map.keys depsRanges') + PackageSetBuild _info set -> + do + packages <- depsRanges # onEachEnvM \depsRanges' -> + getTransitiveDepsFromPackageSet set $ (Array.fromFoldable $ Map.keys depsRanges') + + let + mergeEnvs :: ∀ k v. Ord k => ByEnv (Map k v) -> Map k v + mergeEnvs { core, test } = Map.union core test + + resolvePackageVersionsToRanges :: Map PackageName Package -> Map PackageName Range -> Array VersionResolution + resolvePackageVersionsToRanges registry = + Array.fromFoldable + <<< Map.values + <<< Map.mapMaybeWithKey \name requested -> + Map.lookup name registry >>= case _ of + RegistryVersion resolved -> Just { name, requested, resolved } + _ -> Nothing + + itemisePackages :: String -> Array (Tuple PackageName String) -> Array Docc + itemisePackages heading pairs = + Array.cons (toDoc heading) $ pairs <#> \(Tuple name version) -> + Log.indent <<< toDoc + $ "- " + <> PackageName.print name + <> ": " + <> version + + missingVersions = + Array.filter + (\{ requested, resolved } -> not $ Range.includes requested resolved) + $ resolvePackageVersionsToRanges (mergeEnvs packages) (mergeEnvs depsRanges) + + when (Array.length missingVersions > 0) do + logWarn + [ itemisePackages "The following package versions do not exist in your package set:" + $ (\{ name, requested } -> Tuple name (Range.print requested)) + <$> missingVersions + , [ Log.break ] + , itemisePackages "Proceeding with the latest available versions instead:" + $ (\{ name, resolved } -> Tuple name (Version.print resolved)) + <$> missingVersions + ] + + pure packages where -- Note: here we can safely discard the dependencies because we don't need to bother about building a build plan, @@ -784,3 +832,4 @@ onEachEnv f e = e { core = f e.core, test = f e.test } onEachEnvM :: ∀ m a b. Apply m => (a -> m b) -> ByEnv a -> m (ByEnv b) onEachEnvM f e = e { core = _, test = _ } <$> f e.core <*> f e.test + diff --git a/test-fixtures/missing-versions.txt b/test-fixtures/missing-versions.txt new file mode 100644 index 000000000..34f8b9acb --- /dev/null +++ b/test-fixtures/missing-versions.txt @@ -0,0 +1,8 @@ +‼ The following package versions do not exist in your package set: + - lists: >=1000.0.0 <1000.0.1 + - maybe: >=1000.0.0 <1000.0.1 + + +Proceeding with the latest available versions instead: + - lists: 7.0.0 + - maybe: 6.0.0 diff --git a/test/Prelude.purs b/test/Prelude.purs index 9df8d5d59..1048b52a4 100644 --- a/test/Prelude.purs +++ b/test/Prelude.purs @@ -18,6 +18,7 @@ import Node.Platform as Platform import Node.Process as Process import Record (merge) import Registry.PackageName as PackageName +import Registry.Range as Range import Registry.Version as Version import Spago.Cmd (ExecResult, StdinConfig(..)) import Spago.Cmd (ExecResult, StdinConfig(..)) as X @@ -250,6 +251,9 @@ mkPackageName = unsafeFromRight <<< PackageName.parse mkVersion :: String -> Version mkVersion = unsafeFromRight <<< Version.parse +mkRange :: String -> Range +mkRange = unsafeFromRight <<< Range.parse + writeMain :: Array String -> String writeMain rest = writePursFile { moduleName: "Main", rest } diff --git a/test/Spago/Install.purs b/test/Spago/Install.purs index c911d44da..a2eb2b044 100644 --- a/test/Spago/Install.purs +++ b/test/Spago/Install.purs @@ -7,6 +7,7 @@ import Data.Map as Map import Effect.Now as Now import Registry.Version as Version import Spago.Command.Init as Init +import Spago.Core.Config (Dependencies(..), Config) import Spago.Core.Config as Config import Spago.FS as FS import Spago.Log (LogVerbosity(..)) @@ -17,6 +18,7 @@ import Test.Spec (Spec) import Test.Spec as Spec import Test.Spec.Assertions as Assert import Test.Spec.Assertions as Assertions +import Test.Spec.Assertions.String (shouldContain) spec :: Spec Unit spec = Spec.around withTempDir do @@ -61,6 +63,32 @@ spec = Spec.around withTempDir do spago [ "install", "foo-foo-foo", "bar-bar-bar", "effcet", "arrys" ] >>= shouldBeFailureErr (fixture "missing-dependencies.txt") checkFixture (testCwd "spago.yaml") (fixture "spago-install-failure.yaml") + Spec.it "warns when specified dependency versions do not exist" \{ spago, fixture, testCwd } -> do + spago [ "init", "--package-set", "29.3.0" ] >>= shouldBeSuccess + + FS.writeYamlFile Config.configCodec (testCwd "spago.yaml") + $ insertConfigDependencies + ( Init.defaultConfig + { name: mkPackageName "aaa" + , withWorkspace: Just { setVersion: Just $ unsafeFromRight $ Version.parse "0.0.1" } + , testModuleName: "Test.Main" + } + ) + ( Dependencies $ Map.fromFoldable + [ Tuple (mkPackageName "prelude") (Just $ mkRange ">=6.0.0 <7.0.0") + , Tuple (mkPackageName "lists") (Just $ mkRange ">=1000.0.0 <1000.0.1") + ] + ) + ( Dependencies $ Map.fromFoldable + [ Tuple (mkPackageName "spec") (Just $ mkRange ">=7.0.0 <8.0.0") + , Tuple (mkPackageName "maybe") (Just $ mkRange ">=1000.0.0 <1000.0.1") + ] + ) + + warning <- FS.readTextFileSync $ fixture "missing-versions.txt" + outputs <- spago [ "install" ] + either _.stderr _.stderr outputs `shouldContain` warning + Spec.it "does not allow circular dependencies" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess let @@ -234,6 +262,19 @@ spec = Spec.around withTempDir do -- Check that the lockfile is back to the original checkFixture (testCwd "spago.lock") (fixture "spago.lock") + +insertConfigDependencies :: Config -> Dependencies -> Dependencies -> Config +insertConfigDependencies config core test = + ( config + { package = config.package # map + ( \package' -> package' + { dependencies = core + , test = package'.test # map ((_ { dependencies = test })) + } + ) + } + ) + writeConfigWithEither :: RootPath -> Aff Unit writeConfigWithEither root = do -- The commit for `either` is for the `v6.1.0` release