@@ -132,6 +132,12 @@ def _module_selector(m) -> bool:
132132 self ._blob_url = None
133133 self ._blob = None
134134
135+ # USER_CONTENT files contributed by decorators/mutators via add_to_package.
136+ # Keyed by arcname (path relative to the flow directory) -> absolute file path.
137+ # These are merged with the files discovered by walking the flow directory;
138+ # duplicates (same arcname) are dropped to avoid conflicts.
139+ self ._user_content_from_addl : Dict [str , str ] = {}
140+
135141 # Update package in the system context -- it will be available
136142 # in all hooks going forward including the ones called in the
137143 # thread that is used to create the package asynchronously.
@@ -611,11 +617,33 @@ def _check_tuple(path_tuple):
611617 raise NonUniqueFileNameToFilePathMappingException (
612618 file_name , [deco_module_paths [file_name ], file_path ]
613619 )
620+ elif file_type == ContentType .USER_CONTENT :
621+ # USER_CONTENT files will be merged with the files discovered by
622+ # walking the flow directory. Track them here so we can:
623+ # 1. Include them in the package even if they live outside the
624+ # flow directory (or are excluded by the user_code_filter).
625+ # 2. Avoid duplicating files already picked up by the walker.
626+ real_path = os .path .realpath (path_tuple [0 ])
627+ path_tuple = (real_path , file_name , file_type )
628+ existing = self ._user_content_from_addl .get (file_name )
629+ if existing is None :
630+ self ._user_content_from_addl [file_name ] = real_path
631+ elif existing != real_path :
632+ raise NonUniqueFileNameToFilePathMappingException (
633+ file_name , [existing , real_path ]
634+ )
635+ else :
636+ return None # Already recorded for this arcname
614637 else :
615638 raise ValueError (f"Unknown file type: { file_type } " )
616639 return path_tuple
617640
618641 def _add_tuple (path_tuple ):
642+ # USER_CONTENT is intentionally NOT handled here: those files are
643+ # packaged alongside the user's flow code (see _user_code_tuples)
644+ # rather than under the mfcontent namespace, and are tracked in
645+ # self._user_content_from_addl by _check_tuple above. mfcontent
646+ # owns MODULE/CODE/OTHER only.
619647 file_path , file_name , file_type = path_tuple
620648 if file_type == ContentType .MODULE_CONTENT :
621649 # file_path is actually a module
@@ -625,6 +653,16 @@ def _add_tuple(path_tuple):
625653 elif file_type == ContentType .OTHER_CONTENT :
626654 self ._mfcontent .add_other_file (file_path , file_name )
627655
656+ # flow decorators
657+ for decos in self ._flow ._flow_decorators .values ():
658+ for deco in decos :
659+ for path_tuple in deco .add_to_package ():
660+ path_tuple = _check_tuple (path_tuple )
661+ if path_tuple is None :
662+ continue
663+ _add_tuple (path_tuple )
664+
665+ # step decorators
628666 for step in self ._flow :
629667 for deco in step .decorators :
630668 for path_tuple in deco .add_to_package ():
@@ -640,16 +678,42 @@ def _add_tuple(path_tuple):
640678 continue
641679 _add_tuple (path_tuple )
642680
681+ # flow mutators
682+ for mutator in self ._flow ._flow_mutators :
683+ for path_tuple in mutator .add_to_package ():
684+ path_tuple = _check_tuple (path_tuple )
685+ if path_tuple is None :
686+ continue
687+ _add_tuple (path_tuple )
688+
689+ # step mutators (deduplicated across steps)
690+ seen_step_mutators = set ()
691+ for step in self ._flow :
692+ for mutator in step .config_decorators :
693+ if id (mutator ) in seen_step_mutators :
694+ continue
695+ seen_step_mutators .add (id (mutator ))
696+ for path_tuple in mutator .add_to_package ():
697+ path_tuple = _check_tuple (path_tuple )
698+ if path_tuple is None :
699+ continue
700+ _add_tuple (path_tuple )
701+
643702 def _user_code_tuples (self ):
703+ # Track arcnames yielded by the directory walker so we can detect overlap
704+ # with USER_CONTENT files contributed via add_to_package hooks.
705+ seen_arcnames = set ()
644706 if R .use_r ():
645707 # the R working directory
646708 self ._user_flow_dir = R .working_dir ()
647709 for path_tuple in walk (
648710 "%s/" % R .working_dir (), file_filter = self ._user_code_filter
649711 ):
712+ seen_arcnames .add (path_tuple [1 ])
650713 yield path_tuple
651714 # the R package
652715 for path_tuple in R .package_paths ():
716+ seen_arcnames .add (path_tuple [1 ])
653717 yield path_tuple
654718 else :
655719 # the user's working directory
@@ -660,10 +724,17 @@ def _user_code_tuples(self):
660724 file_filter = self ._user_code_filter ,
661725 exclude_tl_dirs = self ._exclude_tl_dirs ,
662726 ):
663- # TODO: This is where we will check if the file is already included
664- # in the mfcontent portion
727+ seen_arcnames .add (path_tuple [1 ])
665728 yield path_tuple
666729
730+ # Emit USER_CONTENT files contributed by decorators/mutators that were not
731+ # already picked up by the directory walker (either because they live
732+ # outside the flow directory or were filtered out by the suffix/user filter).
733+ for arcname , file_path in self ._user_content_from_addl .items ():
734+ if arcname in seen_arcnames :
735+ continue
736+ yield (file_path , arcname )
737+
667738 def _make (self ):
668739 backend = self ._backend ()
669740 with backend .create () as archive :
0 commit comments