Dependency Hell: Jak různé ekosystémy řeší konflikt závislostí - srovnání (2026)
Diamond dependency problem: mám knihovny A1 a B1, obě závisí na C, ale každá chce jinou verzi.
Můj projekt
/ \
A1 B1
\ /
C v1.x C v2.x ← problém
Klíčová otázka je kdy se o konfliktu dozvím. Buď hned při instalaci (fail fast) — vzteká se vývojář, ale produkce je čistá. Nebo build projde, systém si vybere verzi sám, a chyba se projeví až za běhu u uživatele (silent failure) — to fakt nechci!
PHP — Composer
Funguje správně od výchozího nastavení. Při konfliktu okamžitě odmítne instalaci:
Your requirements could not be resolved to an installable set of packages.
Problem 1
- knihovna-b1 v1.0.0 requires knihovna-c ^2.0 -> satisfiable by knihovna-c[2.0.0].
- knihovna-a1 v1.0.0 requires knihovna-c ^1.0 -> satisfiable by knihovna-c[1.0.0, 1.1.0, 1.2.0].
- Root composer.json requires knihovna-a1 ^1.0 -> satisfiable by knihovna-a1[1.0.0].
- Root composer.json requires knihovna-b1 ^1.0 -> satisfiable by knihovna-b1[1.0.0].
Dobře, ale co s tím?
1. Nejdřív zkontrolovat jestli je konflikt skutečný — podívat se do changelogu C, možná stačí upgradovat A1 na verzi co podporuje C ^2.0.
composer show knihovna-a1 # dostupné verze
composer why-not knihovna-c 2.0.0 # proč nejde nainstalovat
2. Pokud jedna z knihoven není udržovaná — lze použít replace (říkám tím "tohle je kompatibilní náhrada", takže opatrně):
{
"replace": {
"vendor/knihovna-c": "1.*"
}
}
3. Opravdu neřešitelný konflikt — forknout. Vytvořit vlastní kopii a tu přidat do projektu jako kód do libs/app. Nebo najít alternativu.
Java — Maven
Výchozí chování je špatné. Maven si tiše vybere verzi podle pravidla "nearest definition wins" — vyhraje ta verze C, která je v dependency tree blíže kořeni.
Build projde, ale za běhu může spadnout NoSuchMethodError nebo ClassNotFoundException.
Obtížně se debuguje. Obtížně se vysvětluje klientovi.
Alespoň se podívám co se děje:
mvn dependency:tree
Jak Maven opravit — Enforcer Plugin
Přidat do pom.xml, pak build selže při konfliktu stejně jako Composer:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<id>enforce</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<dependencyConvergence/>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Výstup při konfliktu pak vypadá takhle:
[ERROR] Rule 0: org.apache.maven.plugins.enforcer.DependencyConvergence failed with message:
Failed while enforcing releasability the error(s) are [
Dependency convergence error for knihovna-c:1.0.0 paths to dependency are:
+-knihovna-a1:1.0.0
+-knihovna-c:1.0.0
and
+-knihovna-b1:1.0.0
+-knihovna-c:2.0.0
]
Explicitní určení verze
Pokud vím která verze oběma funguje, explicitně určit přes <dependencyManagement>:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>knihovna-c</artifactId>
<version>1.5.0</version> <!-- tato verze vyhraje vždy -->
</dependency>
</dependencies>
</dependencyManagement>
Enforcer Plugin dávat do každého nového projektu. Výchozí chování Mavenu je chyba v designu.
.NET — NuGet
Podobně jako Maven — vybere nejnižší verzi splňující všechny požadavky (lowest applicable version). Deterministické, ale bez záruky že to bude fungovat.
Explicitní verze v .csproj
<ItemGroup>
<PackageReference Include="KnihovnaA1" Version="1.0.0" />
<PackageReference Include="KnihovnaB1" Version="1.0.0" />
<!-- explicitní určení tranzitivní závislosti -->
<PackageReference Include="KnihovnaC" Version="1.5.0" />
</ItemGroup>
Užitečné příkazy
dotnet list package --include-transitive # celý strom závislostí
dotnet list package --vulnerable # bezpečnostní audit
dotnet list package --outdated # zastaralé balíčky
Central Package Management — pro monorepa
Od NuGet 6.2. Centrální správa verzí přes Directory.Packages.props v kořeni repozitáře:
<!-- Directory.Packages.props -->
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="KnihovnaC" Version="1.5.0" />
</ItemGroup>
</Project>
Pak v jednotlivých projektech bez verze:
<ItemGroup>
<PackageReference Include="KnihovnaC" />
</ItemGroup>
Pozor: NuGet nemá nativní fail-fast jako Composer nebo Maven+Enforcer.
Nejlepší co jde udělat je kombinace CPM + pravidelný dotnet list package --outdated.
Python — uv
pip konflikty historicky tiše ignoroval, nepoužívat na nic vážného.
Poetry byl dlouho nejlepší volba — striktní resolver (odmítne instalaci při konfliktu), automatická správa virtualenv, pyproject.toml
jako jediný config soubor. Pokud ho někde používám a funguje, není důvod měnit.
uv je ale lepší pro nové projekty ze dvou důvodů: je 10–100× rychlejší (napsaný v Rustu, znát hlavně v CI/CD) a postupně sjednocuje celý Python toolchain — nahrazuje pip, pip-tools, virtualenv, pyenv i Poetry. Jeden nástroj místo pěti. Striktnost resolveru mají oba stejnou.
Instalace
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Základní použití
uv init muj-projekt
cd muj-projekt
uv add knihovna-a1
uv add knihovna-b1 # selže pokud je konflikt
uv sync # instalace ze souboru
uv run python script.py
pyproject.toml
[project]
name = "muj-projekt"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"knihovna-a1>=1.0.0",
"knihovna-b1>=1.0.0",
]
[tool.uv]
resolution = "lowest-direct" # nebo "highest" (výchozí)
index-url = "https://pypi.org/simple"
Lockfile
uv.lock commitovat do gitu:
uv lock --upgrade # aktualizace
uv sync --frozen # instalace přesně podle lockfile (CI/CD)
Explicitní uvedení tranzitivní závislosti
[dependency-overrides]
"knihovna-c" = ">=1.5.0,<2.0.0"
JavaScript — npm
S JavaScriptem je to těžký. Prakticky vzato to dělá velmi dobře. Sice trochu na sílu tím, že všechno duplikuje, ale funguje to.
npm nainstaluje obě verze C současně — každá knihovna dostane svou kopii v node_modules:
node_modules/
├── knihovna-a1/
│ └── node_modules/
│ └── knihovna-c/ ← verze 1.x
├── knihovna-b1/
│ └── node_modules/
│ └── knihovna-c/ ← verze 2.x
└── knihovna-c/ ← hoisted (sdílená kde to jde)
Build nikdy neselže kvůli konfliktu. Problém nastane jen pokud C drží globální stav — dvě verze jsou dva různé objekty, instanceof vrátí false (pochopitelně), singletony nefungují.
Klasicky React (nesmí existovat dvě instance) nebo ORM.
npm ls knihovna-c # zobrazit duplicity
npm dedupe # pokus o sjednocení verzí
Explicitní uvedení verze (npm 8.3+)
{
"dependencies": {
"knihovna-a1": "^1.0.0",
"knihovna-b1": "^1.0.0"
},
"overrides": {
"knihovna-c": "1.5.0"
}
}
yarn má resolutions, pnpm má overrides ve stejném formátu.
pnpm je přísnější v tom co balíčky "vidí" (symlinky do centrálního store), ale konflikty řeší stejně — duplikací, ne odmítnutím.
Rust — Cargo
Rust čerpá ze zkušeností předchůdců a ze svého striktního typování. Výsledek je nejelegantnější, kde nejsou skoro žádná ale.
Cargo taky nainstaluje více verzí C současně jako npm.
Jenže na rozdíl od npm to není riskantní kompromis — je to bezpečná vlastnost.
Důvod: Rust má silný statický typový systém a žádný sdílený globální stav. Foo z C v1.x a Foo z C v2.x jsou
pro kompilátor dva zcela odlišné typy. Pokud si A1 a B1 nepředávají typy z C, obě verze klidně koexistují.
Když si je předávat musí — kompilátor odmítne build:
error[E0308]: mismatched types
--> src/main.rs:12:20
|
12 | b1::process(conn);
| ^^^^ expected `c_v2::Connection`, found `c_v1::Connection`
Žádný NoSuchMethodError za běhu, žádný záhadný crash na produkci. Problém je vidět před spuštěním.
Cargo.toml vs Cargo.lock
Cargo tohle pěkně odděluje — v Cargo.toml deklaruji záměr (rozsah verzí), Cargo.lock určí přesné verze všech tranzitivních závislostí:
# Cargo.toml
[dependencies]
knihovna-a1 = "1.0"
knihovna-b1 = "1.0"
Pro aplikace Cargo.lock commitovat do gitu — build je pak reprodukovatelný byte-for-byte.
Pro knihovny lock necommitovat — uživatelé knihovny potřebují flexibilitu. (Ve skutečnosti tato podmínka už není nutná, při použití balíčku jako knihovny cargo lock souboru ignoruje)
cargo update # aktualizace (respektuje semver z Cargo.toml)
cargo update -p knihovna-c # aktualizace konkrétní knihovny
cargo tree --duplicates # zobrazit duplicitní verze
Proč to jinde nejde snadno napodobit: JavaScript je dynamicky typovaný, takže dvě verze mohou tiše šlapat po sdíleném stavu. JVM a CLR mají sdílený class loader, který z více verzi dělá noční můru. V Rustu tyto problémy strukturálně neexistují — typový systém je záchranná síť.
Závěr
Vždy je lepší, když chybu odstraní vývojář. Klient by měl žít v představě, že jsme kouzelníci, kteří nedělají chyby. Proto je Fail fast vždy lepší strategie. Na produkci se navíc chyba hledá mnohem hůř, a mnohem déle, a mnohem nervózněji.