Composer output contract
What composer install (2.10, plain, no --optimize) writes, and the rules
vivace reproduces byte for byte. Derived from Composer's source
(AutoloadGenerator, FilesystemRepository, PackageSorter, ArrayDumper,
composer/class-map-generator) and checked against the fixtures in
tests/fixtures/*/expected/.
Files
vendor/autoload.php, and in vendor/composer/:
| File | When |
|---|---|
autoload_namespaces.php, autoload_psr4.php, autoload_classmap.php, autoload_static.php, autoload_real.php |
always |
autoload_files.php |
any files entry, else deleted |
platform_check.php |
config.platform-check not false and at least one PHP or ext requirement, else deleted |
ClassLoader.php, InstalledVersions.php, LICENSE |
verbatim copies from Composer (MIT); vivace embeds them from src/autoload/templates/ |
installed.json, installed.php |
always |
vendor/bin/* |
packages with bin, per config.bin-compat (src/bin.rs) |
No .gitignore is written. Files are only rewritten when their bytes change.
Path rendering
Standard layout gives $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir);.
A path under the vendor dir renders as $vendorDir . '/psr/log/src', anything
else relative to the project as $baseDir . '/src'. Paths are normalised
first: // collapsed, ./ and .. resolved, trailing slash stripped, so
"src/" and "src" both give /src. In autoload_static.php the same paths
render as __DIR__ . '/..' . '/psr/log/src' and __DIR__ . '/../..' . '/src'.
Autoloader suffix
config.autoloader-suffix, else the suffix already in vendor/autoload.php
(ComposerAutoloaderInit([^:\s]+)::), else the lock's content-hash, else 32
random hex. Class names: ComposerAutoloaderInit<suffix> and
Composer\Autoload\ComposerStaticInit<suffix>.
APCu prefix
--apcu-autoloader-prefix/config.apcu-autoloader-prefix, else the prefix
already in vendor/composer/autoload_real.php
(setApcuPrefix\('([^']+)'\)), else bin2hex(random_bytes(10)). The prefix
keys every APCu entry the classmap uses, so reusing it is what keeps a dump
from discarding the warm cache.
Ordering
Packages are sorted with PackageSorter: each package's weight is decremented
by 1 - importance(user) for every package that requires it (cycles count
0), then sorted by weight ascending with strnatcasecmp on the name as the
tie-break. Dependencies therefore come before dependants. The root package is
appended last.
files: sorted order (dependencies first, root last). Key ismd5("<package name>:<path as written>").psr-0,psr-4,classmap: reverse sorted order (root first). Thenkrsorton namespace for psr-0/psr-4; path lists keep insertion order.classmapoutput isksorted by class name and always containsComposer\InstalledVersions => $vendorDir . '/composer/InstalledVersions.php'.- In dev mode the root's
autoload-devis merged into itsautoload(array_merge_recursive). With--no-dev, packages indev-package-namesare skipped everywhere, includingplatform_check.php.
autoload_static.php
Properties emitted in ClassLoader declaration order, skipping empty ones:
$files (only if autoload_files.php exists), $prefixLengthsPsr4,
$prefixDirsPsr4, $fallbackDirsPsr4, $prefixesPsr0, $fallbackDirsPsr0,
$classMap. prefixLengthsPsr4 is keyed by first character, then prefix,
value strlen(prefix). Values are PHP var_export output, re-indented by four
spaces per level with trailing spaces stripped (so 'P' => has no trailing
space). The initializer assigns every property except $files, followed by a
blank line before }, null, ClassLoader::class);.
autoload_real.php
Fixed template. require __DIR__ . '/platform_check.php'; only when that file
is written; the $filesToLoad block only when autoload_files.php exists;
$loader->register(true) unless config.prepend-autoloader is false.
Classmap scanning
Only classmap entries are scanned on a plain install (PSR dirs only with
--optimize). Extensions .php, .inc, .hh. Algorithm
(PhpFileParser::findClasses):
- Strip comments, replace string/heredoc/nowdoc bodies with
null, drop anything outside<?php ... ?>. - Regex, case-insensitive:
\b(?<![\\$:>])(class|interface|trait|enum)\s++([a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+)or\b(?<![\\$:>])namespace(\s++<name(\s*\\\s*name)*>)?\s*+[\{;]. - Walk matches:
namespace X\Ysets prefixX\Y\; barenamespace {sets''. Skip namesextends/implements(anonymous classes). Forenum, cut the name at the last:. Emitltrim(prefix.name, '\\').
The lookbehind rejects Foo::class, $class, ->class; \b rejects
subclass. First occurrence of a class wins. vivace visits files in sorted
path order, so which occurrence is first is deterministic; Composer's Finder
order depends on the filesystem.
installed.json
{ "packages": [ ...sorted by name... ], "dev": true, "dev-package-names": [ ...sorted... ] }
Each package is the lock entry re-dumped by ArrayDumper in this key order:
name, version, version_normalized, target-dir, source, dist,
require, conflict, provide, replace, require-dev, suggest, time,
default-branch, bin, type, extra, installation-source,
autoload, autoload-dev, notification-url, include-path, php-ext,
archive, scripts, license, authors, description, homepage,
keywords, repositories, support, funding, abandoned,
transport-options, install-path ("../vendor/name"). Link maps are
ksorted, keywords sorted, empty arrays and nulls dropped, shasum: ""
kept. JSON is pretty-printed with 4 spaces, unescaped slashes and unicode,
trailing newline.
installation-source is "source" when the package has no dist at all, or
config.preferred-install picked source over dist (#43); otherwise "dist".
A metapackage is never downloaded, so Composer leaves the key out
entirely rather than writing it null.
installed.php
<?php return array(
'root' => array(
'name' => 'vendor/root',
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'project',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'foo/bar' => array(
'pretty_version' => '3.11.0',
'version' => '3.11.0.0',
'reference' => '<dist reference>',
'type' => 'library',
'install_path' => __DIR__ . '/../foo/bar',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/log-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '3.0.0',
),
),
),
);
versions is ksorted and includes the root. replace and provide links
(skipping php*, ext-*, lib-*, composer*) add replaced/provided
lists of pretty constraints; self.version becomes the provider's version.
Null renders as lowercase null. Version normalisation: pad to four numeric
components (3.11.0 becomes 3.11.0.0), root without a version is
1.0.0+no-version-set / 1.0.0.0.
aliases combines two independent mechanisms, both of which can apply to the
same package (see tests/fixtures/root-alias): a branch alias
(extra.branch-alias, or default-branch: true falling back to
9999999-dev) and a root alias (the root composer.json's dev-master as
1.0.0, recorded in the lock's top-level aliases array). The combined list
is sorted with SORT_NATURAL (case-sensitive strnatcmp), so a root alias
like 1.0.0 sorts before a branch alias's 9999999-dev. installed.json is
untouched by a root alias — Composer's AliasPackage is skipped when it
dumps real installed packages.
platform_check.php
Inputs are the require links of the root and every installed non-dev
package. For php, the highest lower bound across constraints gives
PHP_VERSION_ID >= 80100 and the message ">= 8.1.0". With
config.platform-check: true (default is "php-only"), ext-* requirements
add extension_loaded('x') || $missingExtensions[] = 'x'; lines, ksorted,
unless another package replaces or provides the extension.
Zip extraction
If the archive has exactly one top-level entry and it is a directory
(.DS_Store ignored), its contents become the package root. Otherwise the
archive root is. Composer extracts with system unzip, so exec bits survive.
Dist cache
Composer: ~/.cache/composer/files/<vendor>/<name>/<sha1 of dist url>.zip.
dist.shasum is the sha1 of the archive and is "" for GitHub zipballs, so
verification is skipped when empty.