composer/composer · error · RuntimeException
Archive has more than one top level directories, and no comp
Error message
Archive has more than one top level directories, and no composer.json was found on the top level, so it's an invalid archive. Top level paths found were: {paths} What it means
Zip::locateFile() handles archives with a proper table of contents. When iterating entries whose dirname is '.' (root), it expects at most one such entry (a single top-level file/dir) before composer.json is located at root. A second root entry triggers the 'invalid archive' error listing the paths.
Source
Thrown at src/Composer/Util/Zip.php:81
if (false !== ($index = $zip->locateName($filename)) && $zip->getFromIndex($index) !== false) {
return $index;
}
$topLevelPaths = [];
for ($i = 0; $i < $zip->numFiles; $i++) {
$name = $zip->getNameIndex($i);
$dirname = dirname($name);
// ignore OSX specific resource fork folder
if (strpos($name, '__MACOSX') !== false) {
continue;
}
// handle archives with proper TOC
if ($dirname === '.') {
$topLevelPaths[$name] = true;
if (\count($topLevelPaths) > 1) {
throw new \RuntimeException('Archive has more than one top level directories, and no composer.json was found on the top level, so it\'s an invalid archive. Top level paths found were: '.implode(',', array_keys($topLevelPaths)));
}
continue;
}
// handle archives which do not have a TOC record for the directory itself
if (false === strpos($dirname, '\\') && false === strpos($dirname, '/')) {
$topLevelPaths[$dirname.'/'] = true;
if (\count($topLevelPaths) > 1) {
throw new \RuntimeException('Archive has more than one top level directories, and no composer.json was found on the top level, so it\'s an invalid archive. Top level paths found were: '.implode(',', array_keys($topLevelPaths)));
}
}
}
if ($topLevelPaths && false !== ($index = $zip->locateName(key($topLevelPaths).$filename)) && $zip->getFromIndex($index) !== false) {
return $index;
}
throw new \RuntimeException('No composer.json found either at the top level or within the topmost directory');View on GitHub (pinned to 6ffc117740)
Solutions
- Re-zip so contents live under one top-level directory containing composer.json (cd parent && zip -r pkg-1.0.0.zip pkg-1.0.0)
- Or put composer.json at the zip root and keep all other entries nested, so the root has exactly one entry
- Audit the release/packaging script that produced the dist
- Re-tag and republish the package with a correctly structured dist
Example fix
// before zip -r dist.zip . # many entries at root -> error // after zip -r dist.zip mypkg-1.0.0 # single top-level dir with composer.json
Defensive patterns
Strategy: validation
Validate before calling
// Validate the zip layout (proper-TOC branch) before calling Zip::getComposerJson
$zip = new \ZipArchive();
if ($zip->open($path) !== true) { throw new \RuntimeException('cannot open zip'); }
$root = [];
for ($i = 0; $i < $zip->numFiles; $i++) {
$name = $zip->getNameIndex($i);
if (strpos($name, '__MACOSX') !== false) continue;
if (dirname($name) === '.') $root[$name] = true;
if (count($root) > 1) {
$zip->close();
throw new \InvalidArgumentException('Zip has multiple root entries: '.implode(',', array_keys($root)));
}
}
$zip->close();
$json = Zip::getComposerJson($path); Try / catch
try {
$json = Zip::getComposerJson($path);
} catch (\RuntimeException $e) {
if (str_contains($e->getMessage(), 'more than one top level')) {
return $this->installFromSource($package); // dist is malformed, use VCS
}
throw $e;
} Prevention
- Zip the project directory itself (zip -r out.zip mypkg-x.y.z), not its contents
- Add a release CI job that runs Zip::getComposerJson against each published dist
- Reject multi-root zips at publish time with a clear error
- Prefer `git archive` for dists; it produces a clean single-top-level layout
When it happens
Trigger: Opening a .zip dist with Zip::getComposerJson() whose TOC lists multiple entries directly at the archive root (e.g. files 'composer.json' was not located at root and 'src/...', 'README' both appear at '/'). The second root-level entry throws.
Common situations: A zip was created by zipping the contents of a folder rather than the folder itself, putting many files at the zip root; a build pipeline flattened the structure; a repackaged artifact lost its single top-level directory.
Related errors
- No composer.json found either at the top level or within the
- Archive has more than one top level directories, and no comp
- No composer.json found either at the top level or within the
- The zip extension and unzip/7z commands are both missing, sk
- Invalid zip file for "{package name}" with compression ratio
AI-assisted analysis of composer/composer@6ffc117740 (2026-08-07).
Data as JSON: /api/errors/3c5e01b20a400ddb.
Report an issue: GitHub.