{"record":{"id":"3c5e01b20a400ddb","repo":"composer/composer","slug":"archive-has-more-than-one-top-level-directories-a-3c5e01","errorCode":null,"errorMessage":"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}","messagePattern":"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: (.+?)","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"src/Composer/Util/Zip.php","lineNumber":81,"sourceCode":"        if (false !== ($index = $zip->locateName($filename)) && $zip->getFromIndex($index) !== false) {\n            return $index;\n        }\n\n        $topLevelPaths = [];\n        for ($i = 0; $i < $zip->numFiles; $i++) {\n            $name = $zip->getNameIndex($i);\n            $dirname = dirname($name);\n\n            // ignore OSX specific resource fork folder\n            if (strpos($name, '__MACOSX') !== false) {\n                continue;\n            }\n\n            // handle archives with proper TOC\n            if ($dirname === '.') {\n                $topLevelPaths[$name] = true;\n                if (\\count($topLevelPaths) > 1) {\n                    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)));\n                }\n                continue;\n            }\n\n            // handle archives which do not have a TOC record for the directory itself\n            if (false === strpos($dirname, '\\\\') && false === strpos($dirname, '/')) {\n                $topLevelPaths[$dirname.'/'] = true;\n                if (\\count($topLevelPaths) > 1) {\n                    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)));\n                }\n            }\n        }\n\n        if ($topLevelPaths && false !== ($index = $zip->locateName(key($topLevelPaths).$filename)) && $zip->getFromIndex($index) !== false) {\n            return $index;\n        }\n\n        throw new \\RuntimeException('No composer.json found either at the top level or within the topmost directory');","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/composer/composer/blob/6ffc1177404d0c50119c22dde6564a380f4a82c9/src/Composer/Util/Zip.php#L63-L99","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nzip -r dist.zip .          # many entries at root -> error\n\n// after\nzip -r dist.zip mypkg-1.0.0  # single top-level dir with composer.json","handlingStrategy":"validation","validationCode":"// Validate the zip layout (proper-TOC branch) before calling Zip::getComposerJson\n$zip = new \\ZipArchive();\nif ($zip->open($path) !== true) { throw new \\RuntimeException('cannot open zip'); }\n$root = [];\nfor ($i = 0; $i < $zip->numFiles; $i++) {\n    $name = $zip->getNameIndex($i);\n    if (strpos($name, '__MACOSX') !== false) continue;\n    if (dirname($name) === '.') $root[$name] = true;\n    if (count($root) > 1) {\n        $zip->close();\n        throw new \\InvalidArgumentException('Zip has multiple root entries: '.implode(',', array_keys($root)));\n    }\n}\n$zip->close();\n$json = Zip::getComposerJson($path);","typeGuard":null,"tryCatchPattern":"try {\n    $json = Zip::getComposerJson($path);\n} catch (\\RuntimeException $e) {\n    if (str_contains($e->getMessage(), 'more than one top level')) {\n        return $this->installFromSource($package); // dist is malformed, use VCS\n    }\n    throw $e;\n}","preventionTips":["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"],"tags":["archive","zip","packaging","dist"],"analyzedSha":"6ffc1177404d0c50119c22dde6564a380f4a82c9","analyzedAt":"2026-08-07T00:01:08.491Z","schemaVersion":2},"datasetVersion":"2026-08-07T02:17:10.218Z"}