Skip to main content

Dependency Management

This document defines dependency resolution and management for Morphir workspaces.

Overview​

Morphir supports three categories of dependency sources:

SourceUse CaseStatus
PathLocal development, workspace members, monoreposAvailable
RepositoryPre-release code, forks, private packagesAvailable
RegistryPublished packages, stable releasesPlanned

Note: A Morphir package registry is planned for future releases. Until then, path and repository dependencies are the recommended approaches for sharing Morphir packages.

Dependencies can be declared at two levels:

  • Workspace-level: Shared versions defined once, inherited by members
  • Project-level: Specific to individual projects

Dependency Sources​

Path Dependencies​

Path dependencies reference local directories containing Morphir projects. They are resolved directly from the filesystem without version negotiation.

[dependencies]
# Relative path (recommended for workspace members)
"my-org/core" = { path = "../core" }

# Absolute path (for external local projects)
"my-org/shared" = { path = "/path/to/shared" }

Use cases:

  • Workspace member projects depending on each other
  • Local development of multiple related packages
  • Temporary overrides during development (similar to Go's replace)

Behavior:

  • Always uses the current source from the path
  • No version constraint required (implicit "latest")
  • Changes to source are immediately visible
  • Must contain a valid morphir.toml

Repository Dependencies​

Repository dependencies are fetched directly from git repositories, similar to Go modules. This is the recommended approach for sharing packages across organizations until a registry is available.

[dependencies]
# Git repository with tag/version
"acme/experimental" = { git = "https://github.com/acme/experimental.git", tag = "v1.0.0" }

# Git repository with branch
"acme/feature" = { git = "https://github.com/acme/feature.git", branch = "main" }

# Git repository with specific commit
"acme/pinned" = { git = "https://github.com/acme/pinned.git", rev = "a8b3c5d" }

# SSH URL (for private repositories)
"private/internal" = { git = "git@github.com:private/internal.git", tag = "v2.0.0" }

Reference types (mutually exclusive):

FieldDescriptionExample
tagGit tag (recommended for releases)"v1.0.0"
branchGit branch (for tracking development)"main"
revSpecific commit SHA (for pinning)"a8b3c5d82"

Use cases:

  • Sharing packages across organizations
  • Pre-release packages
  • Forked versions with custom modifications
  • Private packages

Behavior:

  • Repository is cloned/fetched to dependency cache
  • Specific ref is checked out
  • Treated as immutable once resolved (stored in lock file)

Registry Dependencies (Planned)​

Status: Registry dependencies are planned for a future release.

Registry dependencies will be published packages fetched from a Morphir package registry:

[dependencies]
# Simple version constraint (planned)
"morphir/sdk" = "3.0.0"

# Semver range (planned)
"some-org/utilities" = "^1.2.0"

# Explicit registry specification (planned)
"finos/morphir-json" = { version = "^1.0.0", registry = "https://registry.morphir.dev" }

Until registry support is available, use repository dependencies with git tags for similar functionality:

# Current recommended approach for external packages
"morphir/sdk" = { git = "https://github.com/finos/morphir-sdk.git", tag = "v3.0.0" }

Workspace Inheritance​

Workspace members can inherit dependency versions from the workspace root, similar to Cargo's workspace inheritance:

# workspace/morphir.toml
[workspace]
members = ["packages/*"]

[workspace.dependencies]
"morphir/sdk" = { git = "https://github.com/finos/morphir-sdk.git", tag = "v3.0.0" }
"acme/shared" = { git = "https://github.com/acme/shared.git", tag = "v2.0.0" }
# workspace/packages/domain/morphir.toml
[project]
name = "my-org/domain"

[dependencies]
# Inherit version from workspace
"morphir/sdk" = { workspace = true }

# Can still add project-specific dependencies
"other/lib" = { path = "../other" }

Benefits:

  • Single source of truth for shared dependency versions
  • Consistent versions across all workspace members
  • Simplified updates (change once, apply everywhere)
  • Reduced configuration duplication

Version Constraints​

Version constraints apply to git tags and future registry dependencies:

SyntaxMeaningExample
"1.2.3"Exact versionOnly 1.2.3
"^1.2.3"Compatible (caret)>=1.2.3, <2.0.0
"~1.2.3"Approximately (tilde)>=1.2.3, <1.3.0
">=1.2.0"Greater than or equal>=1.2.0
"<2.0.0"Less than<2.0.0
">=1.0.0, <2.0.0"Range>=1.0.0, <2.0.0

Notes:

  • Path dependencies ignore version constraints
  • Git dependencies with branch track the branch head
  • Git dependencies with rev ignore semver entirely

Types​

DependencySource​

/// Source of a dependency
pub type DependencySource {
/// Local path dependency
Path(path: String)
/// Git repository dependency
Repository(
url: String,
ref: GitRef,
)
/// Registry dependency (planned)
Registry(
version: SemVer,
registry: Option(String),
)
/// Inherited from workspace
Workspace
}

/// Git reference type
pub type GitRef {
Tag(String)
Branch(String)
Rev(String)
}

DependencySpec​

/// Dependency specification as declared in morphir.toml
pub type DependencySpec {
DependencySpec(
/// Package name
name: PackagePath,
/// Dependency source
source: DependencySource,
)
}

DependencyInfo​

/// Resolved dependency information
pub type DependencyInfo {
DependencyInfo(
/// Package name
name: PackagePath,
/// Original source specification
source: DependencySource,
/// Whether dependency has been resolved
resolved: Bool,
/// Resolved location (path to resolved package)
resolved_path: Option(String),
/// Resolved version (from morphir.toml or git tag)
resolved_version: Option(SemVer),
)
}

DependencyGraph​

/// Resolved dependency graph
pub type DependencyGraph {
DependencyGraph(
/// Root projects being resolved
roots: List(PackagePath),
/// All resolved packages (topologically sorted)
packages: List(ResolvedPackage),
/// Resolution conflicts (if any)
conflicts: List(DependencyConflict),
)
}

/// A resolved package in the graph
pub type ResolvedPackage {
ResolvedPackage(
name: PackagePath,
version: Option(SemVer),
/// Direct dependencies
dependencies: List(PackagePath),
/// Resolved source information
source: ResolvedSource,
)
}

/// Resolved source location
pub type ResolvedSource {
/// Local path (workspace member or path dep)
Local(path: String)
/// Cached from git repository
Cached(
cache_path: String,
url: String,
ref: String,
)
/// Cached from registry (planned)
RegistryCached(
cache_path: String,
registry: String,
)
}

Operations​

Add Dependency​

Adds a dependency to a project.

Behavior​

  1. Validate dependency source specification
  2. Update project's morphir.toml
  3. Mark dependency as unresolved
  4. Optionally trigger resolution

WIT Interface​

/// Dependency source specification
variant dependency-source {
/// Local path dependency
path(string),
/// Git repository dependency
repository(git-dependency),
/// Inherit from workspace
workspace,
}

/// Git repository dependency
record git-dependency {
url: string,
ref: git-ref,
}

/// Git reference
variant git-ref {
tag(string),
branch(string),
rev(string),
}

/// Add a dependency to a project
add-dependency: func(
project: package-path,
dependency: package-path,
source: dependency-source,
) -> result<_, workspace-error>;

JSON-RPC​

Request (path dependency):

{
"method": "workspace/addDependency",
"params": {
"project": "my-org/api",
"dependency": "my-org/core",
"source": { "path": "../core" }
}
}

Request (repository dependency):

{
"method": "workspace/addDependency",
"params": {
"project": "my-org/api",
"dependency": "morphir/sdk",
"source": {
"git": "https://github.com/finos/morphir-sdk.git",
"tag": "v3.0.0"
}
}
}

Request (workspace inheritance):

{
"method": "workspace/addDependency",
"params": {
"project": "my-org/api",
"dependency": "morphir/sdk",
"source": { "workspace": true }
}
}

CLI​

# Path dependency
morphir deps add my-org/core --path ../core

# Repository dependency with tag
morphir deps add morphir/sdk --git https://github.com/finos/morphir-sdk.git --tag v3.0.0

# Repository dependency with branch
morphir deps add acme/feature --git https://github.com/acme/feature.git --branch main

# Repository dependency with commit
morphir deps add acme/pinned --git https://github.com/acme/pinned.git --rev a8b3c5d

# Workspace inheritance
morphir deps add morphir/sdk --workspace

# Specify target project
morphir deps add my-org/core --path ../core --project my-org/api

Remove Dependency​

Removes a dependency from a project.

Behavior​

  1. Verify dependency exists
  2. Check if removal would break other projects
  3. Update project's morphir.toml
  4. Update lock file

WIT Interface​

/// Remove a dependency from a project
remove-dependency: func(
project: package-path,
dependency: package-path,
) -> result<_, workspace-error>;

JSON-RPC​

Request:

{
"method": "workspace/removeDependency",
"params": {
"project": "my-org/api",
"dependency": "some/unused-lib"
}
}

CLI​

morphir deps remove some/unused-lib
morphir deps remove some/unused-lib --project my-org/api

Resolve Dependencies​

Resolves all dependencies for a project or workspace.

Behavior​

  1. Collect all dependency specifications
  2. Build dependency graph
  3. For path dependencies: verify path exists and contains valid project
  4. For repository dependencies: clone/fetch to cache, checkout ref
  5. Resolve any version conflicts
  6. Update lock file
  7. Return resolution result

WIT Interface​

/// Resolve all dependencies for a project
resolve-dependencies: func(
project: package-path,
) -> result<list<dependency-info>, workspace-error>;

/// Resolve all dependencies for entire workspace
resolve-all-dependencies: func() -> result<list<tuple<package-path, list<dependency-info>>>, workspace-error>;

JSON-RPC​

Request (single project):

{
"method": "workspace/resolveDependencies",
"params": {
"project": "my-org/api"
}
}

Request (entire workspace):

{
"method": "workspace/resolveDependencies",
"params": {}
}

Response:

{
"result": [
{
"name": "morphir/sdk",
"source": { "git": "https://github.com/finos/morphir-sdk.git", "tag": "v3.0.0" },
"resolved": true,
"resolvedPath": ".morphir/deps/morphir/sdk/v3.0.0",
"resolvedVersion": "3.0.0"
},
{
"name": "my-org/core",
"source": { "path": "../core" },
"resolved": true,
"resolvedPath": "/workspace/packages/core"
},
{
"name": "acme/shared",
"source": { "git": "https://github.com/acme/shared.git", "branch": "main" },
"resolved": true,
"resolvedPath": ".morphir/deps/acme/shared/main-a8b3c5d",
"resolvedVersion": null
}
]
}

CLI​

morphir deps resolve
morphir deps resolve --project my-org/api
morphir deps list
morphir deps list --resolved # Show resolved paths

Lock File​

The workspace maintains a morphir.lock file with resolved dependency information:

# morphir.lock
# This file is auto-generated. Do not edit.
# It ensures reproducible builds by pinning exact dependency sources.

[[package]]
name = "morphir/sdk"
source = "git"
url = "https://github.com/finos/morphir-sdk.git"
tag = "v3.0.0"
rev = "abc123def456..."
checksum = "sha256:abc123..."
dependencies = []

[[package]]
name = "my-org/core"
source = "path"
path = "packages/core"
# Path dependencies are not version-locked; they use current source

[[package]]
name = "acme/shared"
source = "git"
url = "https://github.com/acme/shared.git"
branch = "main"
rev = "a8b3c5d82e..." # Pinned commit at resolution time
checksum = "sha256:def456..."
dependencies = ["morphir/sdk"]

[[package]]
name = "acme/pinned"
source = "git"
url = "https://github.com/acme/pinned.git"
rev = "deadbeef..."
checksum = "sha256:789abc..."
dependencies = []

Lock File Behavior​

SourceLock Behavior
PathNot locked; always uses current source
Git (tag)Locks tag + resolved commit SHA
Git (branch)Locks branch + resolved commit SHA at resolution time
Git (rev)Locks exact commit SHA

Updating the lock file:

# Resolve and update lock file
morphir deps resolve

# Update a specific dependency to latest
morphir deps update morphir/sdk

# Update all dependencies
morphir deps update --all

# Update git branch dependencies to latest commit
morphir deps update --branches

Resolution Algorithm​

The dependency resolution algorithm uses Minimal Version Selection (MVS), similar to Go modules. This provides reproducible builds without a SAT solver.

Algorithm Overview​

┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Collect │────►│ Resolve │────►│ Verify │
│ Dependencies │ │ Versions │ │ & Lock │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
Direct deps Transitive Lock file
from config resolution generation

1. Dependency Collection​

Gather all dependency specifications from:

  • Workspace-level [workspace.dependencies]
  • Project-level [dependencies]
  • Transitive dependencies (from resolved packages)

2. Workspace Inheritance Resolution​

For dependencies marked { workspace = true }:

  1. Look up the dependency in [workspace.dependencies]
  2. Replace with the workspace-defined source
  3. Error if dependency not found in workspace

3. Graph Construction​

Build a dependency graph:

my-org/api
├── morphir/sdk (git: finos/morphir-sdk.git@v3.0.0)
├── my-org/domain (path: ../domain)
│ ├── morphir/sdk (workspace -> git: finos/morphir-sdk.git@v3.0.0)
│ └── my-org/core (path: ../core)
│ └── morphir/sdk (workspace -> git: finos/morphir-sdk.git@v3.0.0)
└── acme/shared (git: acme/shared.git@main)
└── morphir/sdk (git: finos/morphir-sdk.git@v3.0.0)

4. Source Resolution​

For each dependency in topological order:

Path dependencies:

  1. Resolve path relative to dependent project
  2. Verify directory exists
  3. Verify morphir.toml exists and is valid
  4. Add to resolved graph

Repository dependencies:

  1. Check if already in cache (matching URL + ref)
  2. If not cached: clone repository to .morphir/deps/
  3. Checkout specified ref (tag/branch/rev)
  4. For branches: record resolved commit SHA
  5. Verify morphir.toml exists in repository root
  6. Add to resolved graph with cache path

5. Transitive Dependency Resolution​

Transitive dependencies are resolved recursively. Each resolved package may declare its own dependencies.

Algorithm:

function resolveTransitive(rootDeps):
resolved = {}
queue = rootDeps

while queue is not empty:
dep = queue.pop()

if dep.name in resolved:
# Already resolved - check for version conflict
existing = resolved[dep.name]
resolved[dep.name] = selectVersion(existing, dep)
else:
# New dependency - resolve and add transitives
pkg = fetchAndResolve(dep)
resolved[dep.name] = pkg
queue.addAll(pkg.dependencies)

return topologicalSort(resolved)

Example:

my-org/api
├── morphir/sdk@^3.0.0
├── my-org/domain
│ ├── morphir/sdk@^3.0.0 (transitive)
│ └── morphir/json@^1.0.0 (transitive)
└── acme/utils@^2.0.0
└── morphir/sdk@^3.1.0 (transitive - higher minimum)

Resolution:
morphir/sdk → 3.1.0 (highest minimum requested)
morphir/json → 1.0.0
acme/utils → 2.0.0
my-org/domain → (path)

6. Version Selection (MVS)​

Morphir uses Minimal Version Selection (MVS): select the minimum version that satisfies all constraints.

Rules:

ScenarioResolution
Single requestUse requested version
Multiple requests, same constraint typeUse highest minimum
Caret (^) constraintSelect minimum in range
Exact version conflictError (user must resolve)

Example - Caret Constraints:

A requires morphir/sdk@^3.0.0 → needs ≥3.0.0, <4.0.0
B requires morphir/sdk@^3.2.0 → needs ≥3.2.0, <4.0.0

MVS selects: 3.2.0 (minimum that satisfies both)

Example - Incompatible Constraints:

A requires morphir/sdk@^3.0.0 → needs ≥3.0.0, <4.0.0
B requires morphir/sdk@^2.0.0 → needs ≥2.0.0, <3.0.0

Error: No version satisfies both constraints
morphir/sdk@^3.0.0 (required by A)
morphir/sdk@^2.0.0 (required by B)

Resolution: Update B to use ^3.0.0, or use path override

7. Diamond Dependency Resolution​

Diamond dependencies occur when two packages depend on different versions of the same transitive dependency.

my-org/api
/ \
/ \
lib-a@1.0 lib-b@1.0
\ /
\ /
morphir/sdk@???

lib-a requires morphir/sdk@^3.0.0
lib-b requires morphir/sdk@^3.2.0

Resolution Strategy:

  1. Compatible ranges: Select highest minimum (MVS)

    lib-a: ^3.0.0 (≥3.0.0, <4.0.0)
    lib-b: ^3.2.0 (≥3.2.0, <4.0.0)
    → Select 3.2.0 (satisfies both)
  2. Incompatible ranges: Report error with upgrade path

    lib-a: ^2.0.0 (≥2.0.0, <3.0.0)
    lib-b: ^3.0.0 (≥3.0.0, <4.0.0)
    → Error: Cannot unify versions
    → Suggestion: Update lib-a to version that supports morphir/sdk@^3.0.0
  3. Path override: User can force a specific version

    [dependencies]
    # Force specific version for development
    "morphir/sdk" = { path = "../morphir-sdk" }

8. Conflict Detection and Reporting​

Conflicts are detected and reported clearly:

Source Conflicts:

Error: Conflicting sources for morphir/sdk
my-org/api requires: git@github.com/finos/morphir-sdk@v3.0.0
legacy/lib requires: git@github.com/fork/morphir-sdk@v3.0.0

Different repositories for same package name.

Resolution options:
1. Update legacy/lib to use official repository
2. Rename fork to different package name
3. Use path override for local resolution

Version Conflicts:

Error: Incompatible versions for morphir/sdk
my-org/api requires: ^3.0.0 (via direct dependency)
legacy/lib requires: ^2.0.0 (via lib-old@1.0.0)

No version satisfies both constraints.

Resolution options:
1. Update legacy/lib to version supporting morphir/sdk@^3.0.0
2. Update lib-old to newer version
3. Use path override: "morphir/sdk" = { path = "..." }

Cycle Detection:

Error: Dependency cycle detected
my-org/api → my-org/domain → my-org/core → my-org/api

Cycles are not allowed. Refactor to break the cycle.

10. Path Overrides​

For local development, path dependencies can override other sources:

# morphir.toml
[dependencies]
# Development override - path takes precedence
"morphir/sdk" = { path = "../morphir-sdk" }

# This repository source is ignored when path is also specified:
# "morphir/sdk" = { git = "...", tag = "v3.0.0" }

This is similar to Go's replace directive, allowing local modifications without changing the primary dependency specification.

Dependency Cache​

Repository dependencies can be cached locally (per-workspace) or globally (user-level). Local caching is the default for workspace isolation, but global caching can reduce disk usage and download time across multiple workspaces.

Cache Locations​

CacheLocationUse Case
Local.morphir/deps/Workspace isolation, offline builds
Global$XDG_CACHE_HOME/morphir/deps/Shared across workspaces, reduced duplication

The global cache follows XDG Base Directory conventions:

  • Linux/macOS: ~/.cache/morphir/deps/ (or $XDG_CACHE_HOME/morphir/deps/)
  • Windows: %LOCALAPPDATA%\morphir\cache\deps\

Local Cache (Default)​

workspace/
└── .morphir/
└── deps/
├── morphir/
│ └── sdk/
│ └── v3.0.0-abc123/ # tag + short SHA
│ ├── morphir.toml
│ ├── src/
│ └── .git/ # Shallow clone
└── acme/
└── shared/
└── main-def456/ # branch + short SHA
└── ...

Global Cache​

~/.cache/morphir/
└── deps/
├── github.com/
│ ├── finos/
│ │ └── morphir-sdk/
│ │ ├── v3.0.0/
│ │ └── v3.1.0/
│ └── acme/
│ └── shared/
│ └── main-def456/
└── gitlab.com/
└── ...

Configuring Cache Behavior​

Per-dependency cache location:

[dependencies]
# Use global cache for this dependency
"morphir/sdk" = { git = "https://github.com/finos/morphir-sdk.git", tag = "v3.0.0", cache = "global" }

# Explicit local cache (default)
"acme/experimental" = { git = "https://github.com/acme/experimental.git", branch = "main", cache = "local" }

Workspace-wide cache default:

# morphir.toml
[workspace]
members = ["packages/*"]

[cache]
# Use global cache for all repository dependencies by default
dependencies = "global"

# Or keep default local caching
# dependencies = "local"

User-level default:

# ~/.config/morphir/config.toml
[cache]
# Set global as default for all workspaces
dependencies = "global"

Cache Management​

# Show cache status (local and global)
morphir deps cache status

# Clean unused cached dependencies (local)
morphir deps cache clean

# Clean global cache
morphir deps cache clean --global

# Clean all cached dependencies
morphir deps cache clean --all

# Verify cache integrity
morphir deps cache verify

Best Practices​

  1. Commit Lock Files: Always commit morphir.lock for reproducible builds
  2. Use Tags for Releases: Prefer git tags over branches for stability
  3. Workspace Inheritance: Share common dependencies at workspace level with [workspace.dependencies]
  4. Path for Development: Use path dependencies for local cross-project development
  5. Global Cache for CI: Consider global caching in CI environments to speed up builds across jobs
  6. Pin Branches: When using branch dependencies, run morphir deps resolve regularly to update the lock file
  7. Review Updates: Use morphir deps update --dry-run to preview changes before applying

Migration Guide​

From Direct Git Clones​

If you previously cloned dependencies manually:

# Before: manual clone
git clone https://github.com/finos/morphir-sdk.git deps/morphir-sdk

# After: declare in morphir.toml
[dependencies]
"morphir/sdk" = { git = "https://github.com/finos/morphir-sdk.git", tag = "v3.0.0" }

Preparing for Registry​

When the Morphir registry becomes available, migration will be straightforward:

# Current: repository dependency
"morphir/sdk" = { git = "https://github.com/finos/morphir-sdk.git", tag = "v3.0.0" }

# Future: registry dependency
"morphir/sdk" = "3.0.0"

The lock file format will remain compatible, so existing builds will continue to work.