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" } }

yarnresolutions, pnpmoverrides 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.

Také píšou