Conversation
Composer path repositories install a package as a symlink, so a source directory reached through one contributed no files at all: PHP's RecursiveDirectoryIterator does not descend into symlinked directories unless asked, because RecursiveIteratorIterator calls hasChildren() and its $allowLinks argument defaults to false. The build still reported success, and the missing classes only surfaced at runtime as `class 'X' is undefined`. Scan with FilesystemIterator::FOLLOW_SYMLINKS, visiting each real directory at most once so a link pointing at an ancestor cannot recurse forever. Add `follow-symlinks: false` and --no-follow-symlinks for trees that deliberately link to something that must not be compiled. Both are read before sources are scanned, since that is what consumes them, with the command line taking precedence over the project file.
matyhtf
left a comment
There was a problem hiding this comment.
Thanks for tracking down the missing Composer path-repository sources. Following linked source directories is the behavior we want, but I am requesting changes before merging this PR.
-
Please treat symlinks to files and directories as ordinary source entries, without adding a
follow-symlinksconfiguration setting or--no-follow-symlinksCLI option. Source selection should remain controlled by the existingsourcesandignoresettings. Please remove the option, its completion entry, and the related documentation and tests. -
Please fix
ignorefor paths reached through a symlink. The scanner returns a path such asproject/vendor/pkg/src/Foo.php, whileparseProjectYaml()resolvesignore: vendor/pkgto the target's real path.shouldIgnoreFile()compares those strings, so the ignored package is still compiled. Match exclusions against the path the user specified and the scanner traversed; keep the real path separately for identity and cycle detection. -
Please avoid a global visited-directory set before filtering. If two links point to the same directory and the first path is excluded by
FileScanner::addExcludePattern(), the current callback marks the real directory visited and the second, allowed path is never scanned. Detect cycles against the real directories on the current traversal branch, then deduplicate accepted source files by real path after exclusions. Keep the resulting file order deterministic.
Please add regression tests for a linked directory excluded by YAML ignore, two aliases to one directory with only one excluded, a link cycle, and a symlinked source file. I reproduced both exclusion failures with the scanner from this PR's head commit.
Follow symlinked source directories
A source directory reached through a symlink contributed no files at all, and
the build still reported success. The missing classes only surfaced at runtime.
How to reproduce
This is the ordinary Composer layout for developing a library alongside an app.
The app requires the library through a path repository:
{ "repositories": [{ "type": "path", "url": "../mylib" }], "require": { "vendor/mylib": "@dev" } }Composer installs a path repository by symlinking it, which is what makes
edits to the library show up in the app immediately:
A compiled binary has no autoloader, so every class it uses has to be compiled
in, and the project file says so:
Compiling then succeeds:
but nothing under
vendor/vendor/mylibwas compiled, and the binary dies on thefirst request that reaches the library:
Cause
FileScanner::scan()builds aRecursiveDirectoryIteratorwithSKIP_DOTSonly.
RecursiveIteratorIteratordescends by callinghasChildren(), whose$allowLinksargument defaults tofalse, so symlinked directories are neverentered. Passing a symlink as the source root works, because that path is
resolved directly; only symlinks found during a scan are skipped.
FOLLOW_SYMLINKSdoes not appear anywhere in the tree, and nothing in the docsor tests describes source scanning as excluding symlinks, so this looks like an
inherited default rather than a decision. For contrast,
PrecompiledHeaderManagerchecks
isLink()deliberately, twice, where it matters for cache cleanup.Change
Scan with
FilesystemIterator::FOLLOW_SYMLINKS, and visit each real directoryat most once, so a link pointing at one of its own ancestors cannot recurse
forever.
Opting out, for a tree that deliberately links to something that must not be
compiled:
Both are read before sources are scanned, since that is what consumes them.
The command line wins over the project file, matching
php-version.Tests
phpunit/src/Build/FileScannerTest.phpcovers the three behaviours: a symlinkeddirectory is scanned by default, is skipped when disabled, and a symlink cycle
is visited once rather than recursing.
Full suite before and after this branch fails the same 7 pre-existing tests
(
ExitCodeTest,PythonModuleTest), 2289 → 2292 tests with the three added.completions/tpc.bashis regenerated for the new flag.Additional notes
Chinese documentation and code comments were created with the help of AI, I'm not able to verify if their quality is good enough. Those texts might be rough or need polishing, let me know if that's the case and I'll happily fix it.