Skip to content

Releases: svandragt/vivace

v0.13.0

Choose a tag to compare

@github-actions github-actions released this 15 Sep 08:18
v0.13.0
ae73707

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: rm names a package that was never required, run errors without a script name, cache clean works on a cache viv made, validate reports 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-reqs works on update, require and remove, and config.bump-after-update rewrites composer.json as Composer does.
  • dump-autoload --apcu-autoloader reuses the prefix already in vendor/, 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 vivace

Or 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/ to repo-v0/, matching every other bucket's name. viv cache prune collects the old directory as stale, and the next update refetches 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/there printed "Writing lock file" and exited 0. It now says not/there is not required in your composer.json and has not been removed, and if nothing was removed it leaves composer.json untouched, so a typo shows up in git status too. Composer rewrites the file anyway; viv does not.
  • viv run with 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; --list on an empty project says so.
  • viv cache clean refused a cache containing only viv's own metadata bucket, and viv cache prune deleted that bucket every run, throwing away the cache that makes a warm update fast. The bucket list had two hard-coded copies and neither knew about repo/ or the git-mirror bucket. One list now, and it knows all of them.
  • viv validate exited 0 on a manifest missing name or description, which Composer rejects for publishing with exit 2. A CI gate on composer validate silently stopped gating when a project switched. viv now reports both publish errors in Composer's wording and exits 2; --no-check-publish suppresses them as before.
  • viv audit and a stale-lock viv install told you to run composer install and composer update. They name viv now. validate's stdout keeps Composer's wording, because that stream is contractual.
  • The composer shim, on an argument it did not understand, ran the real Composer without a word. It now says which argument, and VIV_SHIM_STRICT=1 makes 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-reqs on update affects the autoload write, not the solve (#242).
  • validate still passes a manifest with an unknown top-level key, which Composer rejects (additionalProperties); noted in src/validate.rs.
  • cargo binstall vivace without --git does not work until the crate is published; the name was freed on 2026-09-14 and publishing follows this release (#166).
  • symfony/flex stays refused by design. Use Composer for composer require on 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...
Read more

v0.12.0

Choose a tag to compare

@github-actions github-actions released this 14 Sep 18:58
v0.12.0
3173be8

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/vivace replaces composer:2 in a multi-stage build's vendor stage — 16.6 MB, no PHP runtime.
  • The composer shim 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 install accepts --prefer-dist --no-interaction --no-progress instead of rejecting them, so a CI script can point at viv without being rewritten.
  • An install with no PHP on PATH no longer reports success over a vendor/ where phpcs cannot find its standards.
  • --ignore-platform-reqs works, so a build stage stops getting a platform_check.php Composer omits.
  • roots/bedrock installs without --no-plugins for the first time.

Upgrade

cargo binstall --git https://github.com/svandragt/vivace vivace

Or from a checkout:

make install        # puts viv on your PATH
make install-shim   # adds the drop-in composer shim next to it

In 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/vendor

The .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-reqs reaches install and dump-autoload only. update, require and remove still drop it (#231).
  • The shim still falls back to the real Composer silently. -d /app is 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/flex stays refused by design. Use Composer for composer require on 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

Choose a tag to compare

@github-actions github-actions released this 14 Sep 07:59
v0.11.0
2fe2b2d

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/latest resolves the current version instead of returning 404.
  • viv update matches 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 --offline measures yiisoft/yii2-app-basic instead 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 vivace

Or from a checkout:

make install        # puts viv on your PATH
make install-shim   # adds the drop-in composer shim next to it

The .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) and symfony/demo (symfony/flex, which stays refused by design — flex's work is in composer require, where Symfony users should keep using Composer). What --no-plugins costs is the plugin's own work outside vendor/, not the install.
  • --ignore-platform-reqs is accepted by the shim but does nothing, and viv still writes a platform_check.php that 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 vivace without --git still 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

v0.10.0 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 13 Sep 10:09
v0.10.0
38b1a48

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 update can reuse recent registry metadata and skip the network entirely, with --metadata-ttl.
  • The composer shim now sends update, require and remove to 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 --force

Or 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 vivace does not work yet; use the --git form 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

v0.9.0 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 10 Sep 10:23
v0.9.0
85dab82

Highlights

  • viv update is 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 update asks for security advisories only from repositories that offer them, as Composer does, so a Satis-only project makes no request to Packagist.
  • With no php on your PATH, viv update tells 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-shim

On 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

v0.8.0 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 09 Sep 17:25
v0.8.0
74c5023

Highlights

  • viv update now refuses versions with a known security advisory, the way Composer does, so the two write the same lock.
  • viv update works on projects whose repository redirects, and on Yii projects whose only match is a branch alias.
  • viv add and viv rm use your project's own repositories, stay offline when asked, and keep your composer.json indentation.
  • --link-mode clone gives writable vendor/ 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-shim

Prebuilt 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 update is 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

v0.7.0 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 08 Sep 13:26
v0.7.0
770ee88

Highlights

  • viv install now 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-changes now 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 require on 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

Pre-release

Choose a tag to compare

@github-actions github-actions released this 07 Sep 21:21
v0.6.0
c3505f0

[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

v0.5.0: reach Pre-release
Pre-release

Choose a tag to compare

@svandragt svandragt released this 07 Sep 03:52
v0.5.0
94cf0ab

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

Pre-release

Choose a tag to compare

@svandragt svandragt released this 06 Sep 23:54
v0.4.0
8d71612

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.