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

  1. 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)
  2. Or put composer.json at the zip root and keep all other entries nested, so the root has exactly one entry
  3. Audit the release/packaging script that produced the dist
  4. 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

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


AI-assisted analysis of composer/composer@6ffc117740 (2026-08-07). Data as JSON: /api/errors/3c5e01b20a400ddb. Report an issue: GitHub.