Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Bugfixes:
* Preserve TTY properties for child processes in `spago run` (#1341)
* Do not repeatedly request the same log line from the Registry server (#1381)
* Fix solver failing for local packages in `extraPackages` with version constraints (#1338)
- Solver now widens ranges for non-registry extra packages so they are always accepted
- Warns when a local package's `publish.version` doesn't match the declared constraint

## [1.0.3] - 2026-02-01

Expand Down
43 changes: 40 additions & 3 deletions src/Spago/Command/Fetch.purs
Original file line number Diff line number Diff line change
Expand Up @@ -782,13 +782,21 @@ getTransitiveDeps workspacePackage = do
getTransitiveDepsFromRegistry :: forall a. Map PackageName Range -> PackageMap -> Spago (FetchEnv a) (Map PackageName Version)
getTransitiveDepsFromRegistry depsRanges extraPackages = do
let
-- Widen ranges for non-registry extra packages (local/git/workspace overrides).
widenIfExtra :: Map PackageName Range -> Map PackageName Range
widenIfExtra = mapWithIndex \name range ->
case Map.lookup name extraPackages of
Just (RegistryVersion _) -> range
Just _ -> Config.widestRange
Nothing -> range

loader :: PackageName -> Spago (FetchEnv a) (Map Version (Map PackageName Range))
loader packageName = do
-- First look up in the extra packages, as they are the workspace ones, and overrides
case Map.lookup packageName extraPackages of
Just p -> do
deps <- getPackageDependencies packageName p
let coreDeps = deps <#> _.core # fromMaybe Map.empty
let coreDeps = widenIfExtra $ deps <#> _.core # fromMaybe Map.empty
pure $ Map.singleton (getVersionFromPackage p) coreDeps
Nothing -> do
maybeMetadata <- Registry.getMetadata packageName
Expand All @@ -798,10 +806,26 @@ getTransitiveDepsFromRegistry depsRanges extraPackages = do
Left _err -> []
map (Map.fromFoldable :: Array _ -> Map _ _) $ for versions \v -> do
maybeManifest <- Registry.getManifestFromIndex packageName v
let deps = fromMaybe Map.empty $ map (_.dependencies <<< unwrap) maybeManifest
let deps = widenIfExtra $ fromMaybe Map.empty $ map (_.dependencies <<< unwrap) maybeManifest
pure (Tuple v deps)

maybePlan <- Registry.Solver.loadAndSolve loader depsRanges
-- Warn when a non-registry extra package's version doesn't match the declared constraint
for_ (Map.toUnfoldable (Map.intersectionWith Tuple depsRanges extraPackages) :: Array _)
\(Tuple name (Tuple range pkg)) -> case pkg of
RegistryVersion _ -> pure unit
_ -> do
maybeVersion <- extraPackageVersion pkg
case maybeVersion of
Just version | not (Range.includes range version) ->
logWarn $ "Extra package " <> PackageName.print name <> " has version "
<> Version.print version
<> ", which doesn't satisfy constraint "
<> Range.print range
_ -> pure unit

let widenedDepsRanges = widenIfExtra depsRanges

maybePlan <- Registry.Solver.loadAndSolve loader widenedDepsRanges

case maybePlan of
Left errs -> die
Expand Down Expand Up @@ -901,6 +925,19 @@ getVersionFromPackage = case _ of
RegistryVersion v -> v
_ -> unsafeFromRight $ Version.parse "0.0.0"

-- | Extract the version from a non-registry package's config if available.
extraPackageVersion :: forall a. Package -> Spago (FetchEnv a) (Maybe Version)
extraPackageVersion = case _ of
RegistryVersion v -> pure $ Just v
WorkspacePackage wp -> pure $ wp.package.publish <#> _.version
LocalPackage p -> do
result <- Config.readConfig (Path.global p.path </> "spago.yaml")
pure $ case result of
Right { yaml: { package: Just { publish: Just { version } } } } -> Just version
_ -> Nothing
-- Git packages would require cloning to read their config, so no version warning is possible.
GitPackage _ -> pure Nothing

notInPackageSetError :: PackageName -> TransitiveDepsResult -> TransitiveDepsResult
notInPackageSetError dep result = result
{ errors { notInPackageSet = Set.insert dep result.errors.notInPackageSet } }
Expand Down
15 changes: 15 additions & 0 deletions test-fixtures/1338-extra-packages-version/consumer/spago.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package:
name: consumer
dependencies:
- local-lib-match: ">=1.0.0 <2.0.0"
- local-lib-mismatch: ">=2.0.0 <3.0.0"
- local-lib-no-version: ">=5.0.0 <6.0.0"

workspace:
extraPackages:
local-lib-match:
path: ../local-lib-match
local-lib-mismatch:
path: ../local-lib-mismatch
local-lib-no-version:
path: ../local-lib-no-version
14 changes: 14 additions & 0 deletions test-fixtures/1338-extra-packages-version/consumer/src/Main.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Main where

import LocalLibMatch as Match
import LocalLibMismatch as Mismatch
import LocalLibNoVersion as NoVersion

match :: String
match = Match.hello

mismatch :: String
mismatch = Mismatch.hello

noVersion :: String
noVersion = NoVersion.hello
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package:
name: local-lib-match
dependencies: []
publish:
version: 1.0.0
license: MIT

workspace: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module LocalLibMatch where

hello :: String
hello = "Hello from local-lib-match"
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package:
name: local-lib-mismatch
dependencies: []
publish:
version: 1.0.0
license: MIT

workspace: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module LocalLibMismatch where

hello :: String
hello = "Hello from local-lib-mismatch"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package:
name: local-lib-no-version
dependencies: []

workspace: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module LocalLibNoVersion where

hello :: String
hello = "Hello from local-lib-no-version"
16 changes: 15 additions & 1 deletion test/Spago/Install.purs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,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)
import Test.Spec.Assertions.String (shouldContain, shouldNotContain)

spec :: Spec Unit
spec = Spec.around withTempDir do
Expand Down Expand Up @@ -340,6 +340,20 @@ spec = Spec.around withTempDir do
spago [ "install", "either" ] >>= shouldBeSuccess
checkFixture (testCwd </> "spago.yaml") (fixture "spago-install-solver-ranges.yaml")

Spec.it "widens solver ranges for non-registry extra packages (#1338)" \{ spago, fixture, testCwd } -> do
FS.copyTree { src: fixture "1338-extra-packages-version", dst: testCwd }
Paths.chdir $ testCwd </> "consumer"
-- Three local extra packages in one workspace:
-- local-lib-match: version 1.0.0, constraint >=1.0.0 <2.0.0 (satisfies, no warning)
-- local-lib-mismatch: version 1.0.0, constraint >=2.0.0 <3.0.0 (warns, still builds)
-- local-lib-no-version: no publish.version, constraint >=5.0.0 <6.0.0 (widened, no warning)
result <- spago [ "build" ]
result # shouldBeSuccess
let stderr = either _.stderr _.stderr result
stderr `shouldContain` "Extra package local-lib-mismatch has version 1.0.0, which doesn't satisfy constraint >=2.0.0 <3.0.0"
stderr `shouldNotContain` "local-lib-match"
stderr `shouldNotContain` "local-lib-no-version"

insertConfigDependencies :: Config -> Dependencies -> Dependencies -> Config
insertConfigDependencies config core test =
( config
Expand Down
Loading