99import sys
1010import warnings
1111from collections .abc import Callable , Iterable , Mapping , Sequence
12- from contextlib import suppress
12+ from contextlib import nullcontext , suppress
1313from contextvars import ContextVar
1414from dataclasses import field , replace
1515from filecmp import dircmp
@@ -332,9 +332,13 @@ def _render(path: str) -> str:
332332 return self ._render_string (path )
333333
334334 def _load_external_data (path : str ) -> Any :
335+ # NOTE: Use the absolute destination path instead of `self.dst_path`,
336+ # which may be relative to the original working directory, because
337+ # data may be loaded lazily while rendering, when the working
338+ # directory is the destination path.
335339 if (
336340 not (
337- (self .dst_path / (path := _render (path )))
341+ (self .subproject . local_abspath / (path := _render (path )))
338342 .resolve ()
339343 .is_relative_to (self .subproject .local_abspath )
340344 )
@@ -348,7 +352,9 @@ def _load_external_data(path: str) -> Any:
348352 " - API: `trust=True`"
349353 ),
350354 )
351- return load_answersfile_data (self .dst_path , path , warn_on_missing = True )
355+ return load_answersfile_data (
356+ self .subproject .local_abspath , path , warn_on_missing = True
357+ )
352358
353359 # Given those values are lazily rendered on 1st access then cached
354360 # the phase value is irrelevant and could be misleading.
@@ -777,47 +783,58 @@ def _render_template(self) -> None:
777783 """Render the template in the subproject root."""
778784 follow_symlinks = not self .template .preserve_symlinks
779785 dst_root = self .dst_path .resolve ()
780- for src in scantree (str (self .template_copy_root ), follow_symlinks ):
781- src_abspath = Path (src .path )
782- # If the source is a symlink, we are not preserving symlinks, and the
783- # symlink target is outside the template root, this means that we are
784- # copying a file/directory from outside the template, which is
785- # forbidden, so raise an error.
786- if (
787- src_abspath .is_symlink ()
788- and not self .template .preserve_symlinks
789- and not (src_abspath .resolve ()).is_relative_to (
790- self .template .local_abspath
791- )
792- ):
793- raise ForbiddenPathError (
794- path = src_abspath .relative_to (self .template_copy_root )
786+ if not self .pretend :
787+ dst_root .mkdir (parents = True , exist_ok = True )
788+ # Render with the subproject root as the working directory, so that
789+ # filesystem-related Jinja filters and extensions (e.g. `fileglob`)
790+ # operate relative to the destination path. In pretend mode, a missing
791+ # destination folder is not created, and the working directory is left
792+ # unchanged.
793+ with local .cwd (dst_root ) if dst_root .is_dir () else nullcontext ():
794+ for src in scantree (str (self .template_copy_root ), follow_symlinks ):
795+ src_abspath = Path (src .path )
796+ # If the source is a symlink, we are not preserving symlinks, and the
797+ # symlink target is outside the template root, this means that we are
798+ # copying a file/directory from outside the template, which is
799+ # forbidden, so raise an error.
800+ if (
801+ src_abspath .is_symlink ()
802+ and not self .template .preserve_symlinks
803+ and not (src_abspath .resolve ()).is_relative_to (
804+ self .template .local_abspath
805+ )
806+ ):
807+ raise ForbiddenPathError (
808+ path = src_abspath .relative_to (self .template_copy_root )
809+ )
810+ src_relpath = Path (src_abspath ).relative_to (self .template .local_abspath )
811+ dst_relpaths_ctxs = self ._render_path (
812+ Path (src_abspath ).relative_to (self .template_copy_root )
795813 )
796- src_relpath = Path (src_abspath ).relative_to (self .template .local_abspath )
797- dst_relpaths_ctxs = self ._render_path (
798- Path (src_abspath ).relative_to (self .template_copy_root )
799- )
800- for dst_relpath , ctx in dst_relpaths_ctxs :
801- dst_abspath = dst_root / dst_relpath
802- if dst_abspath .is_symlink () and self .template .preserve_symlinks :
803- # If destination path is a symlink, it can safely point outside the
804- # subproject dir, while still itself existing within the subproject.
805- # (So long as nothing is templated into it (if it is a directory),
806- # which would be caught by that path's own check.)
807- # Therefore avoid resolving the symlink itself:
808- dst_realpath = dst_abspath .parent .resolve () / dst_abspath .name
809- else :
810- dst_realpath = dst_abspath .resolve ()
811- if not dst_realpath .is_relative_to (dst_root ):
812- raise ForbiddenPathError (path = dst_relpath )
813- if self .match_exclude (dst_relpath ):
814- continue
815- if src .is_symlink () and self .template .preserve_symlinks :
816- self ._render_symlink (src_relpath , dst_relpath )
817- elif src .is_dir (follow_symlinks = follow_symlinks ):
818- self ._render_folder (dst_relpath )
819- else :
820- self ._render_file (src_relpath , dst_relpath , extra_context = ctx or {})
814+ for dst_relpath , ctx in dst_relpaths_ctxs :
815+ dst_abspath = dst_root / dst_relpath
816+ if dst_abspath .is_symlink () and self .template .preserve_symlinks :
817+ # If destination path is a symlink, it can safely point
818+ # outside the subproject dir, while still itself existing
819+ # within the subproject. (So long as nothing is templated
820+ # into it (if it is a directory), which would be caught by
821+ # that path's own check.) Therefore avoid resolving the
822+ # symlink itself:
823+ dst_realpath = dst_abspath .parent .resolve () / dst_abspath .name
824+ else :
825+ dst_realpath = dst_abspath .resolve ()
826+ if not dst_realpath .is_relative_to (dst_root ):
827+ raise ForbiddenPathError (path = dst_relpath )
828+ if self .match_exclude (dst_relpath ):
829+ continue
830+ if src .is_symlink () and self .template .preserve_symlinks :
831+ self ._render_symlink (src_relpath , dst_relpath )
832+ elif src .is_dir (follow_symlinks = follow_symlinks ):
833+ self ._render_folder (dst_relpath )
834+ else :
835+ self ._render_file (
836+ src_relpath , dst_relpath , extra_context = ctx or {}
837+ )
821838
822839 def _render_file ( # noqa: C901
823840 self ,
0 commit comments