Releases: svandragt/vivace
Release list
v0.13.0
viv installs PHP dependencies from a composer.lock and writes the same vendor/ Composer would, byte for byte. This release is about trust: no viv command reports success for work it did not do, viv's licence now matches the code it ships, and the speed table was measured again rather than carried forward.
Highlights
- viv is now licensed GPL-3.0-or-later, not MIT: three of its plugin adapters are ports of GPL code, so MIT was never a licence it could ship under.
- Commands stop claiming success for work they skipped:
rmnames a package that was never required,runerrors without a script name,cache cleanworks on a cache viv made,validatereports the publish errors Composer does. - A mistyped flag exits 1; exit 2 now means only "the resolver found no solution", as the stability contract always said.
--ignore-platform-reqsworks onupdate,requireandremove, andconfig.bump-after-updaterewritescomposer.jsonas Composer does.dump-autoload --apcu-autoloaderreuses the prefix already invendor/, so a rebuild is a no-op and a deploy keeps its warm APCu cache.- The speed table was re-measured with all four tools, including vivacity, the other Rust reimplementation of Composer.
Upgrade
cargo binstall --git https://github.com/svandragt/vivace vivaceOr from a checkout: make install, then make install-shim for the drop-in composer shim. The .deb, the static musl and glibc tarballs and SHA256SUMS are attached; the container image is ghcr.io/svandragt/vivace:0.13.0.
Three things to know after upgrading:
- The licence changed. If your organisation reviews the licences of build tools, viv is now GPL-3.0-or-later. Running it to build your own project is not distribution and needs nothing from you. Details below.
- Exit codes moved. A usage error (unknown flag, missing argument, unknown command) now exits 1. It exited 2 before, which collided with the resolver's own code. A script that treated 2 as "resolver failure" now gets what it asked for.
- One metadata refetch. The repository metadata cache moved from
repo/torepo-v0/, matching every other bucket's name.viv cache prunecollects the old directory as stale, and the nextupdaterefetches metadata once.
Details
Why GPL. src/plugins/drupal_scaffold.rs is a class-by-class port of drupal/core-composer-scaffold, and src/plugins/wordpress_core.rs ports both WordPress core installers. All three upstreams are GPL-2.0-or-later, and a port of GPL source is a derivative work. cargo deny checks the licences of dependencies, not the provenance of hand-written code, so this passed every gate from the day the drupal adapter landed. It was found by reading the notices of another project that had made the same port and removed it. viv keeps every adapter and takes the licence instead; the or later in those upstreams is what allows GPL-3.0. NOTICE.md lists every adapter, its upstream and its licence, and the weekly drift check now fails if the two disagree or a new adapter has no entry. The vendored Composer files stay MIT, which permits their use here. The default licence viv init writes into a new composer.json is unchanged.
The CLI tells the truth. Each of these printed something that looked like success:
viv rm not/thereprinted "Writing lock file" and exited 0. It now saysnot/there is not required in your composer.json and has not been removed, and if nothing was removed it leavescomposer.jsonuntouched, so a typo shows up ingit statustoo. Composer rewrites the file anyway; viv does not.viv runwith no script name behaved as--list, and on a project with no scripts printed nothing and exited 0 — in CI, a step that ran nothing and passed. It now errors naming the missing argument;--liston an empty project says so.viv cache cleanrefused a cache containing only viv's own metadata bucket, andviv cache prunedeleted that bucket every run, throwing away the cache that makes a warmupdatefast. The bucket list had two hard-coded copies and neither knew aboutrepo/or the git-mirror bucket. One list now, and it knows all of them.viv validateexited 0 on a manifest missingnameordescription, which Composer rejects for publishing with exit 2. A CI gate oncomposer validatesilently stopped gating when a project switched. viv now reports both publish errors in Composer's wording and exits 2;--no-check-publishsuppresses them as before.viv auditand a stale-lockviv installtold you to runcomposer installandcomposer update. They namevivnow.validate's stdout keeps Composer's wording, because that stream is contractual.- The
composershim, on an argument it did not understand, ran the real Composer without a word. It now says which argument, andVIV_SHIM_STRICT=1makes that a hard error for a CI job that wants to know its migration is still real.
A resolver failure that explained itself wrongly. When every version of a package was filtered out by a security advisory, viv said the package could not be found and suggested a typo. It now says the versions were blocked, names the advisories, and names both ways out: require a patched version, or --no-blocking as the deliberate override. The same plumbing ports the "found but conflicts with your root requirement" branch of Composer's problem messages.
bump-after-update. With config.bump-after-update: true (or --bump-after-update), viv update now raises each changed requirement in composer.json to a caret constraint on the locked version and writes the file before computing the lock's content-hash, so the two tools produce the same lock. The ordering is the whole fix: hash first and the lock differs even with the rewrite in place.
Platform requirements on update, require, remove. The flags reach the autoload write on all three, and the shim passes them through instead of dropping them with a note. They do not yet reach the solve: an update cannot resolve a package pinned to a PHP this interpreter lacks. The help text says so, and #242 tracks it.
vivacity in the bench. Adelagric/vivacity is a second Rust reimplementation of Composer with the same byte-identical promise, started four days after viv under viv's name; its maintainer renamed it on finding this project and later freed the crate name. It is now the fourth tool in bench/corpus.sh, run with --no-fallback so a run it hands to Composer can never count as its own. On the six corpus projects it installs, viv is 2.0× faster cold and warm, 6.8× on a no-op and 1.5× on a warm update. It refuses the other four — the WordPress, Drupal, Yii and Craft projects — because their locks name a plugin outside its list. viv installs all ten.
How we checked
| Check | Result |
|---|---|
| Compatibility sweep (v0.13.0) | 40 rows: 34 identical (2 under --no-plugins, for symfony/flex), 0 differ, 6 skipped |
| Lock compare, 10 pinned projects | 10 identical, 0 differ |
| Bench gate (monolog against the baseline) | passed |
| Test suite | 795 tests, 6 skipped without PHP |
| Corpus bench, 4 tools, 10 projects, 3 runs, local mirror | bench/results/corpus.md, section 2026-09-15T06:54:06Z |
| Adapter licence check against Packagist | 15 of 15 rows match NOTICE.md |
The speed table in the README is from this release's run, not carried forward. Full sweep results, including every skip and why, are in compat/results/v0.13.0.md.
Known gaps
--ignore-platform-reqsonupdateaffects the autoload write, not the solve (#242).validatestill passes a manifest with an unknown top-level key, which Composer rejects (additionalProperties); noted insrc/validate.rs.cargo binstall vivacewithout--gitdoes not work until the crate is published; the name was freed on 2026-09-14 and publishing follows this release (#166).symfony/flexstays refused by design. Use Composer forcomposer requireon a Symfony project.
Issues closed
- #152 (Problem messages: port getMissingPackageReason's remaining branches)
- #162 (Adapter drift: native adapters behind upstream)
- #192 (Setup dependabot)
- #205 (update: config.bump-after-update is ignored, so composer.json and the lock's content-hash diverge from Composer's)
- #217 (Flaky: store ceiling test fails under parallel cargo test, passes alone)
- #219 (tests: nothing caps nextest or cargo parallelism, so concurrent gates exhaust memory)
- #220 (bench: a corpus run exits non-zero for known riff failures, with viv clean)
- #221 (bench: a mirror recorded before the dev-branch fix never self-heals)
- #225 (compat: a refused-plugin row reports 'identical' without saying both sides ran --no-plugins)
- #230 (composer shim: falling back to real Composer is silent, so a broken translation looks like success)
- #231 (update/require/remove: --ignore-platform-reqs still unreachable)
- #232 (CI: actions/checkout, download-artifact and github-script are a major behind)
- #233 (validate: a manifest Composer rejects for publishing passes with exit 0, so a CI gate silently weakens)
- [#234](https://gith...
v0.12.0
viv installs PHP dependencies from a composer.lock and writes the same vendor/ Composer would, byte for byte. This release is about getting a real project onto viv without editing it: a container image for your Dockerfile's vendor stage, a shim that understands the flags your scripts already pass, and two fixes for cases where a "migrated" build was quietly still running Composer.
Highlights
- A container image at
ghcr.io/svandragt/vivacereplacescomposer:2in a multi-stage build's vendor stage — 16.6 MB, no PHP runtime. - The
composershim understands-d /app, not just-d=/app. It previously fell back to the real Composer, so a migrated CI job kept using Composer with nothing saying so. - The shim stops dropping
--no-plugins, which made a plugin refusal tell you to pass the flag you had just passed. viv installaccepts--prefer-dist --no-interaction --no-progressinstead of rejecting them, so a CI script can point at viv without being rewritten.- An install with no PHP on
PATHno longer reports success over avendor/where phpcs cannot find its standards. --ignore-platform-reqsworks, so a build stage stops getting aplatform_check.phpComposer omits.roots/bedrockinstalls without--no-pluginsfor the first time.
Upgrade
cargo binstall --git https://github.com/svandragt/vivace vivaceOr from a checkout:
make install # puts viv on your PATH
make install-shim # adds the drop-in composer shim next to itIn a Dockerfile:
FROM ghcr.io/svandragt/vivace:0 AS vendor
COPY composer.json composer.lock ./
RUN ["viv", "install", "--no-dev"]
FROM php:8.4-fpm
COPY --from=vendor /app/vendor /app/vendorThe .deb and the static musl and glibc tarballs are attached below, with SHA256SUMS. Nothing to do after upgrading: no cache format changed and no flags were removed.
Details
A container image. Without one, a Dockerfile that wants viv has to pick a PHP base, fetch the right musl tarball for the build architecture, verify it against SHA256SUMS and untar it onto the PATH before running a single command — then every project that migrates copies that snippet and rots its own pin. The image is gcr.io/distroless/static with viv and the composer shim on the PATH, built for amd64 and arm64, tagged :0.12.0, :0.12 and :0.
It is not FROM scratch, despite the binary being static musl. viv reads the system CA store, so on a base with no certificates every network command fails with No CA certificates were loaded from the system. Static linking removes the libc dependency, not the need for trust anchors.
Two things differ from the composer:2 stage it replaces. Write RUN in exec form — RUN ["viv", "install"] — because the image has no shell. And there is no :latest: a moving tag that silently resolves to nothing is what kept releases/latest returning 404 for ten releases, so every tag this release publishes is pulled and run on both architectures before the job passes.
A shim that was not always a shim. The composer shim translates the Composer commands it understands into viv and execs the real Composer for the rest. It classified each argument on its own, so -d /app read -d as a flag it knew and /app as something it did not — and handed the whole command to Composer. Only the = spelling ever reached viv.
The fallback is what made this expensive. On any machine that still has Composer installed it succeeds, the output looks normal, and nothing indicates viv was not used. A CI job or Dockerfile "migrated" to viv kept paying for Composer while everyone believed otherwise. -d /app is what people write in a Dockerfile.
Separately, the shim dropped --no-plugins before calling viv. That is the one flag a user reaches for because viv told them to, so a project with an unadapted plugin refused, said "pass --no-plugins", and refused again when they did.
Flags your scripts already pass. --prefer-dist --no-interaction --no-progress is the idiom every CI guide teaches, and all three already describe what viv does: it is dist-only and never prompts. install and dump-autoload now accept and ignore them rather than failing, so you can point an existing script at viv — or alias composer to the shim and run your scripts unedited, which is the cheapest way to find out whether a project migrates cleanly.
phpcs no longer needs PHP. The adapter for dealerdirect/phpcodesniffer-composer-installer ran php vendor/bin/phpcs --config-set installed_paths so phpcs would write its own config file. With no php on PATH it warned and carried on, so the install reported success over a vendor/ where phpcs could not find its standards — and the failure surfaced much later, in whatever next ran phpcs. Anyone with viv on the host and PHP inside a container or devbox hit this every time. viv now writes the file itself.
--ignore-platform-reqs. viv install and viv dump-autoload take it, along with --ignore-platform-req=<name>, and the shim passes both through. This matters beyond the flag: Composer under --ignore-platform-reqs omits vendor/composer/platform_check.php, and viv wrote it either way, so a build stage whose target PHP does not satisfy a declared requirement got a vendor/ Composer would run and viv fatals on.
A refusal that answers its own question. When viv cannot run a plugin it says so and points at --no-plugins. What it did not say is whether taking that costs anything, which is the question you actually have. It now distinguishes a plugin refused by design — symfony/flex does its work in composer require, so a committed lock has it already applied — from one with no adapter yet, where real work is skipped. A plugin with no record gets the cautious wording; it never claims safety it cannot support.
A Pest adapter. pestphp/pest-plugin writes vendor/pest-plugins.json on install, so viv used to refuse it. It is now ported, which is what lets roots/bedrock install without --no-plugins.
How we checked
| Check | Result |
|---|---|
| Compatibility sweep (v0.12.0) | 44 rows identical, 0 differ, 6 skipped |
| Lock compare, 10 pinned projects | 10 identical, 0 differ |
| Bench gate (monolog) | passed |
| Container image | built for amd64 and arm64; every published tag pulled and run on both |
| Vendor stage, end to end | roots/bedrock and a psr/log project install and hand on a complete vendor/ with no PHP or Composer in the image |
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 are in compat/results/v0.12.0.md.
The speed table in the README still reports 0.11.0 numbers and says so. The corpus bench runs --no-plugins --no-scripts, so an adapter, a flag, a shim fix and an image are all off the path it measures; restamping the version onto numbers nobody re-took would be a claim we cannot support.
Known gaps
--ignore-platform-reqsreachesinstallanddump-autoloadonly.update,requireandremovestill drop it (#231).- The shim still falls back to the real Composer silently.
-d /appis fixed, but the next gap will fail the same invisible way (#230). - No corpus project exercises the phpcs adapter, so its output is pinned by a unit test rather than by the sweep (#226).
symfony/flexstays refused by design. Use Composer forcomposer requireon a Symfony project.
Issues closed
- #131 (Adapter or inert classification for pestphp/pest-plugin)
- #151 (Plugins refused on popular skeletons: TYPO3, CakePHP, Contao, Silverstripe, Bolt) — closed as not planned
- #211 (Document the composer shim as the migration path)
- #213 (Publish a container image, so a Dockerfile vendor stage doesn't reimplement the installer)
- #214 (install: --ignore-platform-reqs is unreachable)
- #218 (install: the phpcs adapter skips registering standards when php is not on PATH)
- #224 (install: the plugin refusal names the remedy but not whether taking it is safe)
- #227 (composer shim: --no-plugins is dropped)
- #228 (composer shim: space-separated flag values fall back to real Composer)
v0.11.0
viv installs PHP dependencies from a composer.lock and writes the same vendor/ Composer would, byte for byte. This is the first release that GitHub does not mark as a pre-release, which is a smaller change than it sounds and a bigger one than it reads: releases/latest now answers, so a CI step or a Dockerfile can install viv without pinning a version by hand.
Highlights
- Releases are no longer flagged as pre-releases, so
curl .../releases/latestresolves the current version instead of returning 404. viv updatematches versions against a compiled constraint rather than walking a boxed tree: constraint matching drops from about 27 ms to 4 or 5 ms.- The bench corpus mirror records a v1 repository's dev branches properly, so
update --offlinemeasuresyiisoft/yii2-app-basicinstead of failing on it. - A bench scenario the mirror cannot serve now fails the run, rather than passing with a missing cell nobody sees.
Upgrade
cargo binstall --git https://github.com/svandragt/vivace vivaceOr from a checkout:
make install # puts viv on your PATH
make install-shim # adds the drop-in composer shim next to itThe .deb and the static musl and glibc tarballs are attached below, with SHA256SUMS. Nothing else to do after upgrading: no cache format changed, no flags were removed.
Details
Releases are real releases now. Every previous release passed --prerelease to gh release create, so ten of them existed and GitHub's "latest release" endpoint returned 404 to all of them. Anyone scripting an install had to pin a version and update it by hand, or parse the release list and filter it themselves. That flag is gone. Note that v0.10.0 and earlier stay flagged, so releases/latest resolves to v0.11.0 and will not walk backwards.
A faster update. Testing one version against one constraint used to allocate and walk a tree of boxed trait objects. It now compiles the constraint once and matches against that. On Laravel this is about 27 ms across 33,000 calls falling to 4 or 5 ms, with the warm metadata closure going from roughly 143 ms to 114 to 120 ms.
The bench mirror was lying by omission. A mirror recorded from a v1 repository such as asset-packagist wrote every version into one file, but the replayed packages.json always advertises a v2 metadata URL, so a client asks for a separate ~dev.json that was never recorded. Offline mode then had no cache entry to revalidate and failed hard. Recorded versions are now split into the pair a real v2 repository would serve. Separately, a scenario the mirror cannot serve fails the bench run instead of leaving a full row of numbers and no footnote.
How we checked
| Check | Result |
|---|---|
| Compatibility sweep (v0.11.0) | 44 rows identical, 0 differ, 6 skipped |
| Lock compare, 10 pinned projects | 10 identical, 0 differ |
| Bench gate (monolog) | passed |
| Corpus bench, 10 projects, mirrored | no viv failures |
| CI | green on check, plugins, coverage, check-macos and bench |
The README speed table is measured again rather than carried over. It had been taken on 0.9.0 and carried a note saying cold was faster than the table showed — the release checklist warns about exactly that, having shipped a stale caveat twice. Measured on 0.11.0 from a local mirror, as the geometric mean across the ten corpus projects:
| Scenario | viv vs Composer | viv vs riff |
|---|---|---|
| Cold | 6.1× (2.7 to 10.9) | 2.0× (0.9 to 127.2) |
| Warm | 19.4× (5.8 to 104.0) | 5.5× (2.1 to 147.4) |
| No-op | 43.3× (19.3 to 109.6) | 12.6× (2.5 to 52.1) |
| Update-warm | 1.8× (1.4 to 4.2) | n/a |
Cold and warm both moved up because this is the first table to measure 0.10.0's parallel archive extraction and parallel classmap scan, not because of anything in this release.
The 6 skips are packages Composer itself refuses to resolve — a dev-only package under the default minimum-stability, a private package, one unmet platform requirement — not something viv got wrong. Full results are in compat/results/v0.11.0.md.
Known gaps
- 2 of the 18 swept projects need
--no-plugins, and both install byte-identically to Composer with it:roots/bedrock(pestphp/pest-plugin, an adapter is planned) andsymfony/demo(symfony/flex, which stays refused by design — flex's work is incomposer require, where Symfony users should keep using Composer). What--no-pluginscosts is the plugin's own work outsidevendor/, not the install. --ignore-platform-reqsis accepted by the shim but does nothing, and viv still writes aplatform_check.phpthat Composer would omit under that flag. A multi-stage Dockerfile whose target PHP differs from the build stage is the case this bites.cargo binstall vivacewithout--gitstill fails: the crate is not on crates.io, and that is held until 1.0.- Windows is unsupported.
Issues closed
- #212 (Every release is flagged prerelease, so releases/latest 404s and scripted installs break)
- #208 (update: find what makes one version-versus-constraint test cost microseconds)
- #216 (bench: the yii2-app-basic mirror lacks bower-asset metadata, so update-offline yields a gap not a number)
- #210 (update: chain.expand is ~38 ms of the warm closure and has never been examined)
- #206 (autoload: platform_check compiles ten regexes even when a lock has no platform requirement)
v0.10.0
viv installs PHP dependencies from a composer.lock and writes the same vendor/ Composer would, byte for byte. This release makes cold installs faster, gives update a way to skip work it already did, and adds a check that viv's resolver agrees with Composer's across the whole test corpus rather than on fixtures alone.
Highlights
- Cold installs extract archives and scan for classes across all your cores, instead of one at a time.
- A warm
updatecan reuse recent registry metadata and skip the network entirely, with--metadata-ttl. - The
composershim now sendsupdate,requireandremoveto viv, so daily use exercises viv's resolver. - The compatibility sweep now compares the lock viv resolves against Composer's, not only the
vendor/it installs. - The nightly fuzz run works again. It had failed on every run since it was added, and was hiding a fuzz target that had never run at all.
Upgrade
cargo install --git https://github.com/svandragt/vivace --tag v0.10.0 vivace --forceOr download a binary from the assets below; .deb packages are there for amd64 and arm64. If you use the composer shim, reinstall it too (make install-shim), because this release changes which commands it handles.
Nothing to do afterwards. No config changes, no cache invalidation, and every new behaviour is off by default.
Details
Cold installs use more than one core. Extracting a large archive now splits across threads, and the classmap scan that follows walks package directories in parallel instead of one after another. On a 101-package project with nothing cached, generating the autoload files went from 60-68 ms to 52 ms, and the directory walk inside it from 39-45 ms to 29-31 ms. The scan speedup is about 1.4x rather than anything near the core count, because one large package in a project sets the floor on its own.
update can skip revalidating fresh metadata. By default, a warm update asks the registry about every package again, even when nothing has changed. --metadata-ttl <seconds> (or VIV_METADATA_TTL) treats cached metadata younger than that window as current, so a second update shortly after the first makes no metadata requests at all. The security-advisories feed now honours the same window. Off by default, so you opt in to a slightly stale registry view in exchange for the speed. --offline always wins, and viv audit always asks the network, because answering "am I affected right now" from a cached feed would be wrong.
The shim covers more commands. composer update, composer require and composer remove now run viv, alongside install, dump-autoload, normalize and create-project. Each command translates only the flags viv actually supports; anything unrecognised still hands the whole invocation to the real Composer, so the failure mode stays "you got Composer's behaviour", not "viv guessed".
The sweep checks the resolver. Until now the sweep proved viv installs what Composer installs, starting from a lock Composer wrote — it could not catch viv resolving a different lock in the first place. COMPAT_LOCKS=1 resolves each corpus project with both tools and compares the results.
How we checked
| Check | Result |
|---|---|
| Pinned corpus, install | 20 of 20 rows identical |
| Pinned corpus, lock compare | 10 of 10 projects identical |
| Random Packagist sample | 14 identical, 0 differ, 6 skipped |
| Test suite | 748 passing |
| Nightly fuzz, 5 targets | all green, 120 s each |
Every skip in the random sample is a package 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. None is a viv difference. 2 of 18 projects still need --no-plugins.
Full results, including which projects and what was skipped, are in compat/results/v0.10.0.md.
Measured differently from last time: the bench gate's baseline is now the median of at least ten CI runs rather than one. A single-run baseline sat at the edge of its own distribution, so the gate failed about one push in three on unchanged code.
Known gaps
- The speed table in the README is still 0.9.0's corpus run. Cold installs are faster than it shows, because it predates this release's parallel extraction and classmap scan.
- Two corpus projects need
--no-plugins, for Composer plugins viv does not adapt yet. - crates.io and Homebrew are gated on a 1.0 release.
cargo binstall vivacedoes not work yet; use the--gitform above. No release attaches a Homebrew formula. - The metadata closure spends about 80 ms of a 143 ms warm update in a walk that cannot run in parallel. That is now the largest single slice left and is not addressed here.
Issues closed
- #179 (craft adapter: dev install takes 11 s with the native adapter, 3.6 s without)
- #180 (compat sweep: compare the lock viv resolves against Composer's, not only vendor/ from Composer's lock)
- #183 (bench: the Laravel no-op cell is noisier than the 5 ms slack)
- #184 (adapter-drift: the fixture column names the wrong directory for symfony/runtime)
- #185 (tests: fixture where a later requirer widens a package's constraints)
- #186 (shim: map update, require and remove to viv)
- #189 (update: fetch security advisories concurrently with the metadata closure)
- #190 (update: packages.json fetch serialises 117 ms ahead of the closure walk)
- #191 (update: opt-in metadata freshness window so back-to-back updates make no requests)
- #193 (cold install: drupal/core extraction re-creates and re-chmods every parent directory per file)
- #194 (fuzz workflow: every target exits 127 before fuzzing, red daily since it was added)
- #197 (update: give the security-advisories feed the same freshness window as metadata)
- #198 (cold install: the autoload classmap scan walks packages serially)
v0.9.0
Highlights
viv updateis now faster than Composer's on every project in the corpus; on a 101-package Laravel lock it went from 607 ms to 230 ms against Composer's 311 ms.viv updateasks for security advisories only from repositories that offer them, as Composer does, so a Satis-only project makes no request to Packagist.- With no
phpon your PATH,viv updatetells you which platform it assumed instead of blaming a missing extension. - The php-http/discovery adapter now writes the same autoloader as the real plugin.
- Linux x86_64 releases ship a glibc build next to the static musl one, and the .deb uses it; the musl build measured up to a third slower.
Upgrade
cargo binstall --git https://github.com/svandragt/vivace vivace
# or
cargo install --git https://github.com/svandragt/vivace --tag v0.9.0 --force vivace
# or from a checkout
make install && make install-shimOn Linux x86_64 prefer the x86_64-unknown-linux-gnu tarball or the .deb; the musl tarball still works everywhere but is slower. Nothing to do after upgrading.
Details
Where the update time went
Packagist serves package metadata in a minified form: each version lists only the fields that changed from the previous one. viv expanded that by rebuilding the full object for every version, 14,000 times on Laravel, when the resolver goes on to consider about 3,000 of them. It now expands a version only when the resolver accepts it. Three smaller changes landed with it: the platform detection shell-out is cached, the metadata cache and pool are no longer freed just before exit, and the pool indexes packages by name. Locks are byte-identical before and after, checked on every recorded fixture and on the corpus.
| Laravel, warm update, local mirror | |
|---|---|
| viv 0.8.0 | 607 ms |
| viv 0.9.0 | 230 ms |
| Composer 2.10.2 | 311 ms |
Advisories from the right place
0.8.0 added Composer's default blocking of versions with a known security advisory, but asked Packagist for advisories on every update. Composer reads each repository's packages.json and asks only repositories that advertise an advisory endpoint. viv now does the same, so a project with only a Satis or local repository gets no advisory request and no blocking, exactly as with Composer.
Two things you would have hit
Running viv update on a machine without PHP used to fail with a message about a missing extension. It now warns once, up front, that no php was found and which version it assumed, and names the two ways out. Separately, the resolver's message stops calling a non-matching platform version "satisfiable by" and uses Composer's "found php[8.3.0] but it does not match the constraint" wording.
How we checked
| Check | Result |
|---|---|
| Compat sweep, 40 rows across 15 projects, adapters native where viv has them | 26 identical, 0 differ, 14, all on Composer's side in the random sample skipped |
| Update locks, viv against Composer on Laravel and monolog, fresh caches and from the mirror | byte-identical |
| Test suite, network on | 715 pass, 6 skip by design |
| Bench gate, monolog and Laravel, viv relative to Composer on the same runner, median run | pass; Laravel update-offline ratio 0.496 to 0.227 |
| Corpus, ten projects from a local mirror, viv vs Composer geometric mean | cold 4.0×, warm 14.0×, no-op 45.0×, update 1.6× (1.1 to 3.9) |
New since 0.8.0: the gate compares the median of five runs rather than the mean, after one stalled run failed a commit that made the code faster.
Known gaps
- Cold installs of Drupal-shaped projects, where packages install outside
vendor/, are still slower than riff's (#179 tracks the adapter cost). - Composer plugins without a native adapter are still refused; symfony/flex and pestphp/pest-plugin are the ones the corpus hits.
- Two problem messages still differ from Composer's (#152).
- Windows is not supported.
Issues closed
- #157 (php-http/discovery adapter: generated strategy is not added to the autoload classmap)
- #167 (README: install section claims gnu Linux builds that the release does not ship)
- #168 (bench: measure the shipped musl binary against the gnu build)
- #169 (update: unclear resolver error when no php binary is on PATH)
- #176 (update: cache parsed metadata so a warm update skips JSON parsing)
- #177 (update: skip dropping the pool and closure at exit)
- #178 (cache platform detection: the php shell-out is 24 ms on every update)
- #181 (update: profile and trim the solve phase (pool build, optimiser, two solves))
- #182 (update: ask only repositories that advertise security-advisories, as Composer does)
v0.8.0
Highlights
viv updatenow refuses versions with a known security advisory, the way Composer does, so the two write the same lock.viv updateworks on projects whose repository redirects, and on Yii projects whose only match is a branch alias.viv addandviv rmuse your project's own repositories, stay offline when asked, and keep yourcomposer.jsonindentation.--link-mode clonegives writablevendor/files at hardlink cost on btrfs, XFS and APFS.- The speed table now covers ten real projects from a local mirror, and says where Composer and riff are ahead.
- Plugin adapters sit behind one seam, with their own CI job and a weekly check against upstream.
Upgrade
cargo install --git https://github.com/svandragt/vivace --tag v0.8.0 --force vivace
# or from a checkout
make install && make install-shimPrebuilt binaries for Linux (static musl, x86_64 and aarch64, also as a .deb) and macOS (x86_64 and aarch64) are attached below.
Nothing to do after upgrading. If you rely on installing a version that has a security advisory, pass --no-blocking or set config.audit.block-insecure to false, as with Composer.
Details
Security advisories block a version, as in Composer
Composer 2.10 removes versions covered by a known advisory before it resolves. viv did not, so on a project whose constraint spanned an advisory the two tools could write different locks. viv now applies the same filter, honours config.audit.block-insecure, block-abandoned and ignore, and accepts --no-blocking (--no-security-blocking as a hidden alias) and COMPOSER_NO_SECURITY_BLOCKING=1. Offline or with the advisory service unreachable it warns and continues, as Composer does. This adds one request to an update; Composer makes the same one.
Two update failures fixed
A repository that answers with a redirect, as asset-packagist.org does, made viv update fail before resolving. Metadata requests now follow redirects with the same rules as downloads.
Under minimum-stability: dev, a root constraint that only a branch alias satisfies, such as yiisoft/yii2 ~2.0.54 met by dev-master's 2.0.x-dev alias, resolved in Composer and failed in viv, because viv's second, require-only solve dropped every alias. It keeps them now, and the resulting lock is byte-identical to Composer's.
add, rm and init behave like the rest of viv
They used to resolve against Packagist alone and reach the network even with --offline. They now build the same repository set as update, so a package that only exists in your own Satis or file:// repository can be added. They also detect and keep the file's indentation, tabs included, instead of rewriting it at four spaces, and they print Composer's "Lock file operations" summary of what changed.
Reflinks
--link-mode clone copies-on-write from viv's store, so a project that patches vendor/ gets writable files without the extra copy. Where the filesystem cannot reflink it falls back to hardlinks, then copies, with one warning.
Speed, measured honestly
The README's table is now ten corpus projects, three tools, three runs each, served from a local mirror so no network is in the numbers. Install is faster than Composer on every project and than riff on most; on a warm update Composer is faster than viv, and the table says so. Profiling shows half of viv's update time is parsing cached metadata and a quarter is freeing memory at exit; both have issues in 0.9.
How we checked
| Check | Result |
|---|---|
| Compat sweep, 40 rows across 20 projects, plugins native where adapted | 36 identical, 0 differ, 4 skipped |
| Test suite, network on | 704 tests, 698 pass, 6 skip by design |
| Line coverage | 87.8 percent |
| Bench gate, monolog and Laravel, viv relative to Composer on the same runner | pass |
| Update lock on yii2-app-basic and monolog against live Composer 2.10.2 | byte-identical |
New since 0.7.0: the bench runs cold and update scenarios from a local mirror, gates two projects, and compares viv to Composer measured in the same job so runner speed cancels out.
Known gaps
- Warm
viv updateis about half Composer's speed on a real-sized lock. #176, #177 and #178 hold the measured fixes. - Composer plugins without a native adapter are still refused; symfony/flex and pestphp/pest-plugin are the ones the corpus hits.
- Windows is not supported.
- The Linux binaries are static musl builds; their speed against the glibc build we measure is not yet checked (#168).
Issues closed
- #19 (Clone (reflink) link mode on btrfs, XFS and APFS)
- #122 (Install hot path: composer.json parsed three times and plan.keep deep-clones before the no-op check)
- #127 (Isolate plugin adapters from the core: one seam per module, separate CI job, upstream drift check)
- #139 (viv new: start a project in a new directory, empty or from a package)
- #141 (Replace the README's single-project speed table with corpus-wide figures)
- #143 (viv init: create a composer.json for a new project)
- #144 (Delete pool_optimizer's optimizeImpossiblePackagesAway if Request never carries locked packages)
- #145 (Is JsonManipulator still needed now that require and remove always normalise?)
- #146 (Store extraction opens each file about 1.6 times; riff opens once)
- #147 (Fuzz targets no longer compile; the scheduled fuzz workflow has been red since 2026-09-08)
- #148 (Compat sweep aborts on a failed clone instead of skipping the row)
- #149 (Metapackage with no dist and no source fails install: shopware/conflicts)
- #153 (phpstan adapter: PHPSTAN_VERSION_CONSTRAINT differs from the real plugin on wp-cli/wp-cli-bundle)
- #154 (viv run: silence Composer\Config::disableProcessTimeout and print skip notices as plain warnings)
- #155 (update, add and rm write the lock's content-hash before normalising composer.json)
- #156 (update, add and rm: print Composer's lock file operations (what changed, old => new))
- #158 (add and rm: the solve ignores --offline and the project's repositories)
- #159 (Warm update metadata: profile parse vs network before any cache-format change; drop the Composer-shaped cache layout)
- #160 (add: bare-name constraint synthesis still resolves against Packagist only and ignores --offline)
- #161 (update, add, rm: composer-type repositories with a file:// URL are rejected)
- #163 (add, rm, init: keep composer.json's existing indentation instead of forcing four spaces)
- #164 (bench: gate a second, larger project and refresh the baseline at every release)
- #165 (bench: offline scenarios so extraction and solving are gated, not just linking)
- #171 (bench mirror: record metadata from every composer repository, not only Packagist)
- #172 (update: yii2-app-basic fails to resolve, viv sees only dev-master for yiisoft/yii2)
- #173 (bench mirror: dists without a reference field are not recorded)
- #174 (update: follow redirects when fetching repository metadata)
- #175 (update: block versions with security advisories and abandoned packages by default, as Composer does)
v0.7.0
Highlights
viv installnow works on a project that Composer installed. No flag needed.- Six more Composer plugins work: Yii2, Craft, private-composer-installer, codeception/c3, Drupal scaffold, Symfony runtime.
- The root package's version is read from git, the same way Composer does it.
viv update --minimal-changesnow keeps locked versions where it can.- Conflict error messages match Composer's wording.
- Every pinned test project installs byte-identical to Composer, with plugins turned on.
Upgrade
make install # viv
make install-shim # the composer drop-in
Or use the Debian package, Homebrew formula or tarball attached below. Reinstall the local binary after upgrading. An older viv refuses plugins this release supports.
Details
Installing over a Composer-written vendor/
If vendor/ was created by Composer, the first viv install relinks every package from viv's store. You no longer need --adopt.
If one package can't be downloaded, for example a paid plugin that needs a licence key, viv keeps Composer's copy of that package, prints a warning, and adopts the rest.
When you run composer install through the shim in a terminal, viv asks before it changes the tree. viv install and scripts do not ask.
viv writes installed.json and its own state file last, after every package is linked. If a run is interrupted, Composer can still use the tree.
Plugins
New native adapters: yiisoft/yii2-composer, craftcms/plugin-installer, ffraenz/private-composer-installer, codeception/c3, drupal/core-composer-scaffold and symfony/runtime.
drupal/core-project-message and drupal/core-recipe-unpack do nothing during install, so viv now ignores them instead of refusing.
The private-composer-installer adapter fills {%KEY} placeholders in download URLs from your environment or a .env file. The lock file keeps the placeholder. A missing variable stops the install with an error that names it.
Root package version
A project without a version in composer.json now gets its version from the git branch or tag, plus the commit hash, in installed.php. Detached checkouts and extra.branch-alias are handled. Before this release every such project got 1.0.0+no-version-set.
Resolver
viv update --minimal-changes was accepted but did nothing. It now prefers the locked version of every package you did not name, where the constraints allow it.
When a conflict has several causes, the error text is now sorted, grouped and shortened the way Composer does it.
How we checked
The compatibility sweep compares vendor/ from viv against vendor/ from Composer on 19 pinned projects and 10 random Packagist packages, in both dev and no-dev mode. For this release the sweep ran with plugins turned on wherever viv has an adapter, and with .git kept in each checkout. Earlier releases ran with plugins off and .git removed, which hid the root-version bug.
| Rows | Result |
|---|---|
| 36 | identical |
| 0 | differ |
| 4 | skipped, Composer itself failed to resolve or authenticate |
Two pinned projects still need --no-plugins: symfony/demo (symfony/flex) and roots/bedrock (pestphp/pest-plugin). Full table: compat/results/v0.7.0.md.
The speed gate against the stored baseline passes. Warm and no-op installs are as fast as before.
Known gaps
- Yii2 and Craft write their plugin maps in the order downloads finish, so Composer's own output varies from run to run. viv writes a fixed dependency order. The sweep compares those two files ignoring order.
- Conflict messages still lack Composer's "found X but it conflicts with your root require" case.
- symfony/flex is not supported. Use Composer for
requireon Symfony projects. - Windows is not supported.
Issues closed
#61 (Wire --minimal-changes into viv update), #62 (Problem messages: port formatDeduplicatedRules and condenseVersionList), #92 (Plugin adapters: yiisoft/yii2-composer and craftcms/plugin-installer), #93 (Drupal support: core-composer-scaffold and core-project-message), #98 (Adapter for ffraenz/private-composer-installer), #111 (Collapse pool_builder::build onto build_partial), #123 (Adopt a Composer-written vendor/ by default), #124 (Compat sweep: run native-adapter projects with plugins on and count refusals), #125 (Root package version: guess from git), #126 (Adapter for codeception/c3), #128 (Root version differs on composer/composer and phpunit checkouts), #129 (phpstan adapter differs on Slim-Skeleton), #130 (yii2 extensions.php differs on yii2-app-basic), plus #81, #94, #107, #108, #109, #110, #112.
v0.6.0: update at Composer's speed
[0.6.0] - 2026-09-07
Added
- CHANGELOG.md generated from closed milestone issues at release time (#80)
- Homebrew tap and Debian package built from the release tarballs (#82)
- Stability policy: what viv promises and how it versions (#83)
- Run the compat sweep on a schedule, not only on tags (#84)
- Add riff to the update-warm benchmark (#87)
- 0.6 target: faster than riff on every scenario (#88)
- Cache parsed constraints when building pool packages (#89)
- Metadata closure fetch dominates warm update at ~1.5 s (#90)
- Resolver at Composer's speed on large locks (#91)
- Normalise composer.json on update, require and remove, not on install (#95)
- viv update rejects --no-plugins and --no-scripts that install accepts (#96)
- viv update refuses a composer.json with a vcs repository (#97)
- Aliases: viv add for require, viv rm for remove (#99)
- Adapter for php-http/discovery (#101)
- Rerun the client corpus sweep and bench before tagging 0.6 (#102)
- viv update fails on wpackagist metadata: provider entry is not a list (#105)
- Bench the public compat corpus, not only the Laravel lock (#106)
Changed
- make install should not put the composer shim on PATH by default (#103)
- update, require and remove should install after writing the lock, as Composer does (#104)
Fixed
- update: self.version in a dependency's require breaks the closure walk (bedrock, drupal) (#115)
- update: pool optimizer leaves alias_of unremapped, panics on phpunit/phpunit (#116)
- update: root replace/provide ignored, symfony/demo lock gains four polyfills (#117)
- update: php-64bit and lib-* platform packages missing from the solver (#118)
- update: honour available-package-patterns so wpackagist isn't asked about every name (#119)
- update: write the lock's time field as RFC 3339 with +00:00 like ArrayDumper (#121)
Performance
- update: cap closure fetch concurrency under the h2 stream limit and take the version scan off the fetch loop (#120)
v0.5.0: reach
Proof of concept, not production ready.
Reach release. New commands, all byte-matched to Composer's plain output on the fixtures: show, tree, outdated, why, audit, validate, update-lock, run, exec. Private Composer repositories: Satis and Private Packagist (v1 and v2 protocols), multiple repositories with canonical ordering. Beyond Composer: viv x runs a Packagist tool without installing it into the project, the way uvx does.
Solver: PoolOptimizer ported and constraints compiled once; viv update on the 101-package Laravel lock goes from 8.7 s to 4.9 s with locks byte-identical (Composer: 1.2 s; the remaining hotspots are #89 and #90). Install warm 42 ms, no-op 7 ms, install -o warm 61 ms.
Sweep: 8 of 10 pinned popular projects byte-identical to Composer in dev and no-dev, 2 skipped for unsupported plugins. Report: compat/results/v0.5.0.md.
Closed in milestone 0.5 reach:
- #86 viv update-lock and viv tree as first-class commands
- #85 viv x: run a tool from Packagist without installing it into the project (uvx equivalent)
- #78 link_tree's create_dir_all issues far more mkdir than needed
- #77 Classmap-scan cache hits still cost ~40-50ms on install -o
- #76 Prune the solver pool before rule generation (no PoolOptimizer)
- #70 viv validate
- #69 viv show and viv outdated
- #68 viv audit: security advisories from Packagist
- #67 Composer-type repositories beyond Packagist: Satis, Private Packagist, GitLab and GitHub package registries
- #65 Release workflow: prebuilt binaries for Linux and macOS on tag push
Install: cargo binstall vivace, or download a tarball below once the release workflow finishes, or cargo install --git https://github.com/svandragt/vivace --tag v0.5.0 --locked.
v0.4.0: hardening and reach
Proof of concept, not production ready.
Hardening release. Warm install on the 101-package Laravel lock drops from 140 ms to 42 ms (parallel linking), install -o from 370 ms to 124 ms (classmap cache). New: --offline, preferred-install: source, tar.bz2, extraction size caps, streaming downloads, viv cache size and gc, Bitbucket OAuth, native adapters for the phpcs installer, phpstan extension-installer, spi and composer-patches, fuzz targets and coverage in CI, a profile of where the time goes (bench/results/profile.md), and a README statement of which Composer commands viv covers.
Sweep: 8 of 10 pinned popular projects byte-identical to Composer in dev and no-dev, 2 skipped for unsupported plugins. Report: compat/results/v0.4.0.md.
Known: viv update is correct but about seven times slower than Composer on a large lock until the pool is pruned (#76).
Closed in milestone 0.4 hardening and reach:
- #79 Partial update of a transitively-required package fails: not found in any version
- #73 parse_constraint panics on multi-byte input (semver-php slices at a non-char boundary)
- #72 macOS CI: snapshot filters must match the canonical /private/var temp path
- #71 Classmap differs from Composer on choks/password-policy-bundle
- #64 compat sweep: keep the last lines of composer's output for skipped rows
- #59 Git source checkouts: match Composer's .git/config
- #56 Prune empty parent directories after removing a package
- #55 Profile viv update and viv require once the resolver lands
- #54 Profile viv install and record where the time goes
- #53 Plugin adapter: cweagans/composer-patches
- #52 Plugin adapters: phpcodesniffer-composer-installer, tbachert/spi, phpstan/extension-installer
- #48 Deny rustdoc warnings in make check and CI
- #47 Silence cargo-deny licence warnings by allowing them explicitly
- #46 Add cargo-machete to make check
- #45 Publish test coverage from CI with cargo-llvm-cov
- #44 Fuzz the hand-written parsers with cargo-fuzz
- #43 Honour preferred-install: source packages record installation-source source and keep a git checkout
- #37 Small drop-in differences: target-dir removal, stale sh proxies, empty vendor/bin, absolute vendor-dir depth, summary wording
- #29 Small ponytail ceilings: sh proxy marker, adopt confirmation, content-hash slash escaping, pointer temp names
- #27 tar.bz2 dists
- #26 Release engineering: binaries, cargo-binstall, checksums
- #25 Cache scanned classmaps per archive for -o installs
- #24 Bitbucket OAuth token exchange and remaining auth.json types
- #23 Offline mode and --prefer-dist-cache behaviour
- #22 Windows support
- #21 Limits on extraction: size caps and entry counts
- #20 viv cache: prune, clean, size, gc of unreferenced archives
Build: cargo install --git https://github.com/svandragt/vivace --tag v0.4.0 --locked, or download a prebuilt tarball below once the release workflow finishes.