← all projects

vivace

It is to Composer what uv was to pip
repo MIT

vivace

viv installs PHP dependencies from a Composer composer.lock file and writes a vendor/ directory that matches what Composer would write, byte for byte. It's a proof of concept at v0.10.0, tested in CI on Linux and macOS, and not yet at 1.0.

Try it

Download a prebuilt binary from the releases page (Linux x86_64 as glibc and static musl builds, aarch64 as static musl, also packaged as a .deb; macOS x86_64 and aarch64), or install it another way:

cargo binstall --git https://github.com/svandragt/vivace vivace
# or build from source:
cargo install --git https://github.com/svandragt/vivace --tag v0.10.0 vivace

Both commands also upgrade an existing install; add --force to cargo install when the version has not changed.

Then run it in a project that already has a composer.json and composer.lock:

viv install

If Composer already wrote the vendor/ directory, viv adopts it automatically, no flag needed.[^1] Only composer install run through the shim asks for confirmation first, and only in a terminal. If a package can't be downloaded (a private package behind a licence key, for example), viv keeps Composer's copy of that package, prints a warning, and adopts the rest. To force a fresh relink of a vendor/ that viv itself wrote, run viv install --adopt.

Starting from nothing

No composer.json yet? viv init writes one and stops, with no prompts: the package name is guessed from git config user.name and the directory, type is project, license is MIT, and autoload.psr-4 points at src/ when that directory exists. Pass --name, --license or --type to override a default, or --require/--require-dev to add dependencies in the same command:

mkdir demo && cd demo && viv init --require psr/log

That resolves psr/log, writes composer.lock, and installs vendor/, the same as viv add would on an existing project (--no-install opts out). Run it again with --force to start over.

viv init is for the directory you're already in; viv new is for one that doesn't exist yet. A bare name creates it and runs init's own defaults inside:

viv new demo

vendor/package[:constraint] downloads that package's dist as a project skeleton (constraint defaults to the newest stable version), drops its own VCS metadata, and installs it, running the post-root-package-install/ post-create-project-cmd scripts a skeleton like Laravel's relies on (--no-scripts opts out):

viv new laravel/laravel:^11 my-app

create-project is Composer's own name for this, kept as an alias.

Stopping

You can stop using viv at any point and go back to Composer with no clean-up. A vendor/ that viv wrote is a valid Composer install: installed.json and the autoload files are the same bytes Composer would have written, so composer install on it is a no-op and composer update replaces packages as usual. The only extra file is a small state file in vendor/composer/, which Composer ignores.

The links from vendor/ into viv's store are hardlinks, not symlinks: each file in vendor/ is a real file that shares its data with the store copy, so deleting the store (viv cache clean) leaves vendor/ complete and working. If you would rather reinstall it with Composer anyway:

rm -rf vendor && composer install

To remove viv itself:

viv cache clean            # deletes viv's store under ~/.cache/vivace
rm ~/.cargo/bin/viv ~/.cargo/bin/composer   # the binary and the shim

Use apt remove vivace if you installed the .deb instead. Nothing else is written outside the project and the cache.

Is it safe to try

viv's contract is that its output matches Composer's byte for byte. Before every release, a compatibility sweep installs a mix of pinned popular projects and a random sample of Packagist packages with both Composer and viv, then compares the results.[^2] The v0.10.0 sweep: 34 rows identical, 0 differ, 6 skipped, and all 10 pinned projects resolve the same composer.lock as Composer as well as installing the same vendor/.[^3]

Two of the pinned projects still need --no-plugins to install with viv, both for a Composer plugin viv doesn't yet support.[^4] See Plugins below.

Speed

10 projects from viv's compatibility corpus, 2026-09-10, from a local mirror so no network is measured, AMD Ryzen 9 7900X3D (24 threads) on ext4, viv 0.9.0, composer 2.10.2, riff 0.0.7, --no-plugins --no-scripts, three runs each. Measured on 0.9.0: the cold column predates 0.10.0's parallel archive extraction and parallel classmap scan, so cold is faster than this table shows, not slower. Each cell is how many times faster viv is, with the range across projects; below 1× viv is slower. Per-project numbers are in bench/results/corpus.md. riff's column measures the same job as viv and Composer (checksums, platform_check.php, proxies, installed.*); the cosmetic differences are listed in bench/results/README.md.

| Scenario | viv vs Composer | viv vs riff | |---|---|---| | Cold | 4.0× (1.6 to 10.7) | 1.7× (0.6 to 69.4)[^5][^6] | | Warm | 14.0× (5.9 to 40.0) | 6.5× (2.0 to 114.2)[^6] | | No-op | 45.0× (23.0 to 104.4) | 22.8× (2.8 to 111.7) | | Update-warm | 1.6× (1.1 to 3.9) | n/a |

A warm update still revalidates every package's metadata with the registry, one conditional request each, even when nothing changed. --metadata-ttl <seconds> on update, add and rm (or VIV_METADATA_TTL; the flag wins) skips that revalidation for a package whose cached metadata is younger than the window, so a second update run shortly after the first makes no metadata requests at all. It's off by default (0, always revalidate, matching Composer), so turn it on only where a slightly stale registry view for a few minutes is an acceptable trade for the extra speed. --offline always wins over a configured window.

Everyday commands

viv init                  # write a composer.json for a new project and stop
viv new demo               # same, in a directory that doesn't exist yet
viv new laravel/laravel:^11 my-app   # download a package skeleton and install it
viv install            # in a project with composer.json and composer.lock
viv install --no-dev
viv install --dry-run  # show the plan, change nothing
viv install --link-mode copy
viv install --no-progress  # skip the fetch/link progress line on a terminal
viv update                # resolves, writes composer.lock and installs
viv update psr/log -w     # partial update with dependencies, then installs
viv update --no-install   # resolve and write the lock only
viv update --no-blocking  # allow versions with a security advisory, as Composer's flag does
viv add psr/container      # edits composer.json, updates the lock and installs (alias: require)
viv rm psr/container       # same, minus the package (--no-install opts out too, alias: remove)
viv dump-autoload -o
viv normalize --check     # add/rm/init also normalise when they write
viv cache prune
viv diagnose              # environment/config report to paste into a bug report

install runs your project's setup scripts, the same way Composer does, unless you pass --no-scripts.[^10]

add, rm and init also tidy up composer.json when they write it, so two branches that each add a dependency merge cleanly instead of fighting over ordering. install and update never touch composer.json.[^11]

Using viv as composer

make install-shim installs a composer binary next to viv (plain make install leaves your real Composer untouched). Put it on PATH ahead of the real Composer, or symlink it as composer in CI: everyday install, dump-autoload, normalize and create-project run through viv; every other command falls through to your real Composer install.[^12]

Plugins

Composer plugins are PHP code that hooks into Composer's own process; viv has no PHP runtime, so it can't run one as written. Instead:

The full list of which plugin falls into which category is documented separately.[^13]

Reasons to use viv

Besides matching Composer's output faster, viv does three things Composer does not.

Running tools without installing them

viv x vendor/package[:constraint] installs the package into an isolated, cached environment and runs its binary, the way uvx and npx do. Nothing is added to your composer.json or vendor/:

viv x phpunit/phpunit:^11 tests      # 27 packages, 0.07 s on a warm cache
viv x friendsofphp/php-cs-fixer fix src
viv x --list                         # environments in the cache

Automatic normalisation

Every command that writes composer.json (add, rm, init) also normalises it: stable key order and whitespace, the same result as running composer normalize. You never commit a diff that is only reordering.[^11] Run viv normalize --check for an explicit run that only reports without writing.

One cache for every project

viv keeps every package it downloads in one store and links from there, so a second project with the same dependencies installs in milliseconds and without touching the network.[^14]

Reasons not to use viv

Scope

viv covers the Composer commands you run every day. Everything else stays with Composer, and the composer shim passes those commands through.

Commands viv runs itself, with the same output as Composer:

Commands that stay with Composer: search, config, global, self-update, licenses, depends and the rest.

Works with

Development

Tooling comes from devbox: PHP, Composer and hyperfine for the fixtures and benchmarks.

make install    # put viv on your PATH (~/.cargo/bin); make install-shim adds the composer drop-in
make check      # fmt, clippy, tests, cargo deny, cargo machete, cargo doc
make test
make bench      # composer vs riff vs viv on bench/laravel
VIVACE_TEST_NETWORK=1 make test   # includes the end-to-end install

There's an engineering log of what was measured and decided along the way, including the study of prior art that shaped viv's design.[^15]

Licence

MIT. A few files are vendored from other MIT-licensed projects and keep their original copyright notices.[^16]

[^1]: viv relinks every package from its own content-addressed store into vendor/, using hardlinks so files aren't copied or re-extracted. [^2]: See compat/README.md for how the sweep works. [^3]: The skips are packages Composer itself refuses to resolve — security advisories blocking every matching version, a dev-master-only package under the default minimum-stability, one unmet platform requirement — not something viv got wrong. Full results, including which projects and what was skipped, are in compat/results/v0.10.0.md. [^4]: Of viv's 20 pinned compatibility projects, the two that need --no-plugins are symfony/demo and roots/bedrock. [^5]: The ratio's range crosses 1×: within noise on some projects. [^6]: riff's phpunit/phpunit cold and warm times (8.2 s and 8.6 s) are an outlier against its other rows in this corpus; kept in the range, not dropped; see bench/results/corpus.md. [^10]: install runs the root's lifecycle scripts (pre-install-cmd, post-autoload-dump, post-install-cmd) as Composer does. [^11]: Normalisation follows ergebnis/composer-normalize's rules, so a project already using that plugin sees no change. [^12]: The shim maps install, dump-autoload, normalize, create-project, update, require and remove with their supported flags (update's partial-update package arguments and -w/-W included) to viv; everything else, search, an unrecognised flag, it hands through to the real Composer binary unchanged. Point VIV_COMPOSER_PATH at the real binary if it isn't first on PATH. The shim only has something to hand through to if a real Composer is on PATH in the first place: if there isn't one, those commands fail rather than silently falling back to viv. [^13]: docs/plugin-strategy.md lists which plugin falls into which category. [^14]: The store lives under $XDG_CACHE_HOME/vivace/, keyed by the sha256 hash of each package archive, and installing hardlinks files from it into vendor/ instead of extracting them again. Store files are read-only, so an accidental edit to a vendor file fails instead of silently changing every project that shares that file on disk; projects that patch their vendor files should use --link-mode copy instead. A no-op install compares the lock against vendor/composer/installed.json and a small state file, without spawning PHP or making a network request. The autoloader files (vendor/autoload.php, vendor/composer/*.php, installed.json, installed.php, platform_check.php) are generated by a port of Composer's own generator, tested against Composer's own golden test cases, and checked end to end by byte-diffing vendor/ against real Composer 2.10.2 output. See ARCHITECTURE.md, docs/composer-contract.md and docs/stability.md for the detail. [^15]: JOURNAL.md, including the study of Riff and Presto that preceded the code. [^16]: src/autoload/templates/ contains Composer's ClassLoader.php, InstalledVersions.php and licence, copied verbatim under Composer's MIT licence. src/spdx-licenses.json is composer/spdx-licenses' own resource file, also copied verbatim under its MIT licence. Test fixtures under tests/fixtures/composer/ are Composer's own, also MIT. [^17]: docs/stability.md states what a minor release may and may not change.