Skip to content

Fix directory detection heuristic - #1802

Open
cromedome wants to merge 2 commits into
mainfrom
fix/gh-1781-app-root-detection
Open

Fix directory detection heuristic#1802
cromedome wants to merge 2 commits into
mainfrom
fix/gh-1781-app-root-detection

Conversation

@cromedome

Copy link
Copy Markdown
Contributor

Fixes #1781. Supersedes #1782.

Problem

_build_location walks up from the caller's directory looking for the application
root. Its test for "is this an app root?" was "does it hold both lib/ and bin/?"
— which also describes every Perl distribution root, most home directories, /usr,
and /usr/local.

This was harmless while the upward walk was effectively stalling, since it rarely
left the script's own directory. 2.1.0 made the walk work, and the weak test
immediately started selecting directories far above the application. Apps then
resolved config.yml, views/, public/ and logs/ against the wrong root, which
is the template and public-file failures in #1781.

Reproducing

The trigger is any directory holding both lib/ and bin/ above the app. A
Dancer2 checkout already has lib/:

mkdir bin
prove -l t/auto_page.t     # fails
rmdir bin
prove -l t/auto_page.t     # passes

That's why a plain cpanm Dancer2 fails for some people and not others: ~/bin +
~/lib is enough, and the walk climbs out of the extracted tarball into the home
directory. Reconstructing that:

mkdir -p /tmp/fakehome/bin /tmp/fakehome/lib
git archive HEAD --prefix=Dancer2-2.1.0/ | tar -x -C /tmp/fakehome
cd /tmp/fakehome/Dancer2-2.1.0 && prove -Ilib -r t/

11 failing test files on main, 0 on this branch.

Fix

Recognise an app root by what it actually holds, closest-first as the walk climbs:

  1. a .dancer file — the explicit marker
  2. environments/, views/ or public/
  3. config.<ext> for any extension Config::Any handles
  4. lib/ + bin/ — kept for apps with no Dancer2 artifacts, but checked last so it
    only wins when found closer than real evidence. Still skips blib/.

Because t/ holds config.yml, views/ and public/, it now matches immediately
and the walk never approaches the distribution root or ~.

A second commit stops the walk at a checkout or distribution boundary (.git,
.hg, .svn, Makefile.PL, Build.PL, dist.ini, cpanfile) rather than
escaping above it. That only applies to directories already rejected as app roots,
so a dancer2 gen app — which ships a cpanfile and a Makefile.PL alongside
.dancer and config.yml — is accepted several tiers earlier.

The walk is also canonicalised with realpath up front, and now stops when
parent() reaches its own fixed point instead of comparing against rootdir. A
relative $subdir became a chain of .. segments, which made the blib/ regex
meaningless and the root comparison non-portable. That's the real issue behind
#1782, whose stated premise — path('.')->parent being a fixed point — doesn't hold
on current Path::Tiny.

Note on the test change

t/issues/memleak/die_in_hooks.t asserts on stderr and was relying on detection
failing: it previously found no app root, fell back to its own directory, and so
never loaded t/issues/config.yml with its logger: "Note". It now has a config of
its own stating that requirement.

The upward walk in _build_location was effectively stalling before 2.1.0,
so it rarely left the script's own directory. Now that it walks properly,
the heuristic it walks with turns out to be far too weak: "holds both lib/
and bin/" matches every Perl distribution root, most home directories,
/usr and /usr/local. Applications were resolving their config, views and
public directories against a directory far above themselves.

Recognise a directory as an application root by what it actually holds,
checked closest-first as the walk climbs:

  1. a .dancer file, the explicit marker
  2. environments/, views/ or public/
  3. config.<ext> for any extension Config::Any handles, so anything
     ConfigReader would load counts
  4. lib/ + bin/, kept for apps carrying no Dancer2 artifacts at all, but
     checked last so it only wins when found closer than real evidence

Also canonicalise the walk with realpath up front and stop on parent()
reaching its own fixed point. A relative $subdir became a chain of '..'
segments, which made the blib/ regex meaningless and the rootdir string
comparison non-portable.

t/issues/memleak/die_in_hooks.t asserts on stderr and had been relying on
detection failing: it previously found no app root, fell back to its own
directory, and so never loaded t/issues/config.yml with its logger: Note.
Give it a config of its own that states the requirement explicitly.

Fixes GH #1781
A directory holding .git, .hg, .svn, Makefile.PL, Build.PL, dist.ini or
cpanfile is the root of a checkout or a distribution, and so a ceiling:
whatever the application root is, it sits at or below that line, never in
the home directory or build directory above it.

Halt the upward walk there rather than escaping past it, falling back to
the script's own directory as before. This closes the case GH #1781 leaves
open, where an application carrying no Dancer2 artifacts anywhere above the
script is run from somewhere under a home directory that happens to hold
lib/ and bin/.

The check runs only for directories _is_app_root has already rejected, so a
project root that is also an application root - the usual case, since
'dancer2 gen' writes both a cpanfile and a Makefile.PL - has been accepted
well before this is reached.
@cromedome
cromedome requested a review from xsawyerx August 9, 2026 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Test failures with 2.1.0

1 participant