My composer.json:
{
"name": "my/project",
"repositories": [
{
"type": "artifact",
"url": "artifacts/"
}
],
"require": {
"company/auth-sdk": "^1.0"
}
}
Output of composer diagnose:
Checking Composer version: OK
Composer version: 2.11.999-dev+source
PHP version: 8.4.21
PHP binary path: /opt/homebrew/Cellar/[email protected]/8.4.21/bin/php
OpenSSL version: OpenSSL 3.6.2 7 Apr 2026
curl version: 8.20.0 libz 1.2.12 brotli brotli/1.2.0 zstd supported ssl OpenSSL/3.6.3 HTTP 1.0, 1.1, 2, 3
zip: extension present, unzip present, 7-Zip not available
Checking composer.json: OK
Checking platform settings: OK
Checking git settings: OK git version 2.39.5
Checking http connectivity to packagist: OK
Checking https connectivity to packagist: OK
Checking disk free space: OK
When I run this command:
I get the following output:
The package archive artifacts/company-auth-sdk-1.0.0.tar.gz contains a package directory auth-sdk/ alongside an incidental root file (such as README.md or LICENSE).
During repository initialization, ArtifactRepository inspects the archive via Composer\Util\Tar::getComposerJson() and outputs:
Loading composer repositories with package information
Found package company/auth-sdk (1.0.0) in file company-auth-sdk-1.0.0.tar.gz
...
Package operations: 1 install, 0 updates, 0 removals
Installs: company/auth-sdk:1.0.0
- Downloading company/auth-sdk (1.0.0)
- Installing company/auth-sdk (1.0.0): Extracting archive
Generating autoload files
The command finishes with exit code 0, but ArchiveDownloader fails to unwrap auth-sdk/ because of the top-level README.md. Inside vendor/company/auth-sdk/:
vendor/company/auth-sdk/
├── README.md
└── auth-sdk/
├── composer.json
└── src/
└── Client.php
Because composer.json and src/ remain nested in auth-sdk/ instead of the package root, running the project fails with a fatal error:
Fatal error: Uncaught Error: Class "Company\AuthSdk\Client" not found in ...
And I expected this to happen:
Composer\Util\Tar::getComposerJson() should exhibit parity with Composer\Util\Zip::locateFile and ArchiveDownloader:
- In
Composer\Util\Zip::locateFile, any non-metadata root entry (file or directory) is tracked via $dirname === '.'. When an archive contains a top-level directory alongside a file and lacks a root composer.json, Zip correctly rejects it:
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: README.md,auth-sdk/
- But in
Composer\Util\Tar::extractComposerJsonFromFolder (src/Composer/Util/Tar.php:51), entries are filtered with if ($folderFile->isDir()). Non-directory entries at the root of a TAR archive are completely ignored.
- This creates a mismatch between archive discovery (
Tar.php claims the package is wrapped in a single directory) and archive installation (ArchiveDownloader refuses to unwrap because sibling files exist), leading to broken autoloading.
Additionally, Tar::extractComposerJsonFromFolder should verify that composer.json is a file ($phar['composer.json']->isFile()) before calling getContent(), preventing BadMethodCallException if a directory is named composer.json.