@@ -16,12 +16,68 @@ def file_hash(file):
1616 return h .hexdigest ()
1717
1818
19- def make_archive (file , directory , verbose = False ):
19+ def should_ignore (path , patterns ):
20+ """Check if a path matches any of the ignore patterns using glob-style matching."""
21+ path_obj = Path (path )
22+
23+ for pattern in patterns :
24+ # Handle ** patterns for recursive matching
25+ if pattern .startswith ("**/" ):
26+ # Match pattern anywhere in the path
27+ pattern_without_prefix = pattern [3 :]
28+ if any (part == pattern_without_prefix or Path (part ).match (pattern_without_prefix ) for part in path_obj .parts ):
29+ return True
30+ # Also try matching the full pattern
31+ if path_obj .match (pattern ):
32+ return True
33+ elif "**" in pattern :
34+ # Use pathlib match for patterns with **
35+ if path_obj .match (pattern ):
36+ return True
37+ else :
38+ # For simple patterns, check if any path component matches
39+ if any (Path (part ).match (pattern ) for part in path_obj .parts ):
40+ return True
41+
42+ return False
43+
44+
45+ def make_archive (file , directory , verbose = False , ignore_patterns = None ):
2046 archived_files = []
21- for dirname , _ , files in os .walk (directory ):
47+ for dirname , subdirs , files in os .walk (directory ):
48+ rel_dirname = os .path .relpath (dirname , directory )
49+
50+ # Skip directories that match ignore patterns
51+ if ignore_patterns and rel_dirname != "." and should_ignore (rel_dirname , ignore_patterns ):
52+ if verbose :
53+ print (f"Skipping directory: { rel_dirname } " )
54+ # Clear subdirs to prevent os.walk from descending into this directory
55+ subdirs .clear ()
56+ continue
57+
58+ # Filter out subdirectories that match ignore patterns to prevent descending
59+ if ignore_patterns :
60+ filtered_subdirs = []
61+ for subdir in subdirs :
62+ rel_subdir = os .path .relpath (os .path .join (dirname , subdir ), directory )
63+ if should_ignore (rel_subdir , ignore_patterns ):
64+ if verbose :
65+ print (f"Skipping subdirectory: { rel_subdir } " )
66+ else :
67+ filtered_subdirs .append (subdir )
68+ subdirs [:] = filtered_subdirs
69+
2270 for filename in files :
2371 path = os .path .join (dirname , filename )
24- archived_files .append ((path , os .path .relpath (path , directory )))
72+ archive_path = os .path .relpath (path , directory )
73+
74+ # Skip files that match ignore patterns
75+ if ignore_patterns and should_ignore (archive_path , ignore_patterns ):
76+ if verbose :
77+ print (f"Skipping file: { archive_path } " )
78+ continue
79+
80+ archived_files .append ((path , archive_path ))
2581
2682 with zipfile .ZipFile (file , "w" ) as zf :
2783 for path , archive_path in sorted (archived_files ):
@@ -59,49 +115,181 @@ def make_archive(file, directory, verbose=False):
59115 final_archive = args .output_folder / f"python{ version_nodot } .zip"
60116 temp_archive = args .output_folder / f"temp{ version_nodot } .zip"
61117
62- base_python : Path = args .root_folder
118+ python_root : Path = args .root_folder
119+ python_lib_name = f"python{ version } "
120+
121+ # Determine the structure based on what we're given
122+ # Case 1: We're given a path that ends with /lib (like Homebrew on macOS)
123+ if python_root .name == "lib" :
124+ # Look for python{version} directly in this directory
125+ base_python_lib = python_root / python_lib_name
126+ if not base_python_lib .exists ():
127+ # Try without the dot (e.g., python313)
128+ python_lib_name = f"python{ version_nodot } "
129+ base_python_lib = python_root / python_lib_name
130+
131+ if base_python_lib .exists ():
132+ lib_dir = python_root
133+ bin_dir = python_root .parent / "bin"
134+ include_dir = python_root .parent / "include"
135+ else :
136+ print (f"Error: Could not find Python library directory in { python_root } " )
137+ print (f"Expected: { python_root } /python{ version } /" )
138+ exit (1 )
139+ else :
140+ # Case 2: We're given a parent path, look for lib/python{version}
141+ lib_dir = python_root / "lib"
142+ bin_dir = python_root / "bin"
143+ include_dir = python_root / "include"
144+
145+ base_python_lib = lib_dir / python_lib_name
146+ if not base_python_lib .exists ():
147+ # Try without the dot (e.g., python313)
148+ python_lib_name = f"python{ version_nodot } "
149+ base_python_lib = lib_dir / python_lib_name
150+
151+ if not base_python_lib .exists ():
152+ # Case 3: Maybe python_root itself is the python library directory
153+ if (python_root / "os.py" ).exists ():
154+ base_python_lib = python_root
155+ lib_dir = python_root .parent
156+ bin_dir = lib_dir .parent / "bin" if lib_dir .name == "lib" else lib_dir / "bin"
157+ include_dir = lib_dir .parent / "include" if lib_dir .name == "lib" else lib_dir / "include"
158+ else :
159+ print (f"Error: Could not find Python library directory in { python_root } " )
160+ print (f"Expected structure: { python_root } /lib/python{ version } /" )
161+ exit (1 )
162+
163+ print (f"Python root: { python_root } " )
164+ print (f"Python lib directory: { base_python_lib } " )
165+ print (f"Bin directory: { bin_dir if bin_dir .exists () else 'not found' } " )
166+ print (f"Include directory: { include_dir if include_dir .exists () else 'not found' } " )
63167
64168 base_patterns = [
65169 "**/*.pyc" ,
66170 "**/__pycache__" ,
67171 "**/__phello__" ,
68- "**/*config-3*" ,
69- "**/*tcl*" ,
70- "**/*tdbc*" ,
71- "**/*tk*" ,
72- "**/Tk*" ,
73172 "**/_tk*" ,
74173 "**/_test*" ,
75- "**/libpython *" ,
76- "**/pkgconfig " ,
174+ "**/_xxtestfuzz *" ,
175+ "**/*config-3* " ,
77176 "**/idlelib" ,
177+ "**/pkgconfig" ,
178+ "**/pydoc_data" ,
78179 "**/site-packages" ,
180+ "**/*tcl*" ,
181+ "**/*tdbc*" ,
182+ "**/temp_*.txt" ,
79183 "**/test" ,
80184 "**/turtledemo" ,
81- "**/temp_*.txt" ,
185+ "**/*tk*" ,
186+ "**/Tk*" ,
82187 "**/.DS_Store" ,
83188 "**/EXTERNALLY-MANAGED" ,
84189 "**/LICENSE.txt" ,
85190 ]
86191
192+ # Add custom patterns from cmake (like "test", "lib2to3", etc.)
87193 if args .exclude_patterns :
88- custom_patterns = [x .strip () for x in args .exclude_patterns .replace ('"' , '' ).split (";" )]
89- base_patterns += custom_patterns
194+ custom_patterns = [x .strip () for x in args .exclude_patterns .replace ('"' , '' ).split (";" ) if x .strip ()]
195+ # Convert simple patterns to ** patterns for recursive matching
196+ for pattern in custom_patterns :
197+ if not pattern .startswith ("**/" ) and "**" not in pattern and "/" not in pattern :
198+ base_patterns .append (f"**/{ pattern } " )
199+ base_patterns .append (f"**/{ pattern } /**" )
200+ else :
201+ base_patterns .append (pattern )
90202
91- ignored_files = shutil .ignore_patterns (* base_patterns )
203+ # Create a custom ignore function that uses glob matching for the lib directory
204+ def ignore_lib_files (directory , files ):
205+ ignored = []
206+ for name in files :
207+ full_path = os .path .join (directory , name )
208+ rel_path = os .path .relpath (full_path , base_python_lib )
209+ if should_ignore (rel_path , base_patterns ):
210+ ignored .append (name )
211+ if args .verbose :
212+ print (f"Ignoring during lib copy: { rel_path } " )
213+ return ignored
214+
215+ # Custom ignore function for bin directory (keep python binaries only)
216+ def ignore_bin_files (directory , files ):
217+ _ = directory # Required by shutil.copytree signature but not used
218+ ignored = []
219+ for name in files :
220+ # Keep only python* executables
221+ if not (name .startswith ("python" ) and not name .endswith (".pyc" )):
222+ ignored .append (name )
223+ if args .verbose :
224+ print (f"Ignoring during bin copy: { name } " )
225+ return ignored
92226
93227 print (f"cleaning up { final_location } ..." )
94228 if final_location .exists ():
95229 shutil .rmtree (final_location )
96230
97- print (f"copying library from { base_python } to { final_location } ..." )
98- shutil .copytree (base_python , final_location , ignore = ignored_files , dirs_exist_ok = True )
231+ # Create the lib/python{version} structure
232+ final_lib_location = final_location / "lib" / python_lib_name
233+ print (f"copying library from { base_python_lib } to { final_lib_location } ..." )
234+ shutil .copytree (base_python_lib , final_lib_location , ignore = ignore_lib_files , dirs_exist_ok = True )
235+
236+ # Copy bin directory if it exists
237+ if bin_dir .exists ():
238+ final_bin_location = final_location / "bin"
239+ print (f"copying binaries from { bin_dir } to { final_bin_location } ..." )
240+ shutil .copytree (bin_dir , final_bin_location , ignore = ignore_bin_files , dirs_exist_ok = True )
241+ else :
242+ print (f"Warning: bin directory not found at { bin_dir } " )
243+
244+ # Copy include directory if it exists
245+ if include_dir .exists ():
246+ # Look for include/python{version} subdirectory
247+ include_python_subdir = include_dir / python_lib_name
248+ if include_python_subdir .exists ():
249+ # Copy include/python{version} to final_location/include/python{version}
250+ final_include_location = final_location / "include" / python_lib_name
251+ print (f"copying headers from { include_python_subdir } to { final_include_location } ..." )
252+ shutil .copytree (include_python_subdir , final_include_location , dirs_exist_ok = True )
253+ else :
254+ # Copy entire include directory
255+ final_include_location = final_location / "include"
256+ print (f"copying headers from { include_dir } to { final_include_location } ..." )
257+ shutil .copytree (include_dir , final_include_location , dirs_exist_ok = True )
258+ else :
259+ print (f"Warning: include directory not found at { include_dir } " )
260+
261+ # Update site_packages path to the new location
262+ site_packages = final_lib_location / "site-packages"
99263 os .makedirs (site_packages , exist_ok = True )
100264
265+ # Move libpython library files to lib/ directory
266+ # These are typically in config-3* directories but should be in lib/
267+ print (f"searching for libpython library files to move to lib/..." )
268+ lib_extensions = [".dylib" , ".dll" , ".a" , ".lib" , ".so" ]
269+ final_lib_root = final_location / "lib"
270+
271+ moved_files = []
272+ for root , dirs , files in os .walk (final_lib_location ):
273+ for file in files :
274+ if file .startswith ("libpython" ) and any (file .endswith (ext ) for ext in lib_extensions ):
275+ source_path = Path (root ) / file
276+ # Move to lib/ directory (not lib/python{version}/)
277+ dest_path = final_lib_root / file
278+
279+ # Only move if it's not already in the lib/ directory
280+ if source_path .parent != final_lib_root :
281+ shutil .move (str (source_path ), str (dest_path ))
282+ moved_files .append (file )
283+ if args .verbose :
284+ print (f"Moved { file } from { source_path .parent } to { dest_path .parent } " )
285+
286+ if moved_files :
287+ print (f"Moved { len (moved_files )} libpython library file(s) to lib/" )
288+
101289 print (f"making archive { temp_archive } to { final_archive } ..." )
102290 if os .path .exists (final_archive ):
103- make_archive (temp_archive , final_location , verbose = args .verbose )
291+ make_archive (temp_archive , final_location , verbose = args .verbose , ignore_patterns = base_patterns )
104292 if file_hash (temp_archive ) != file_hash (final_archive ):
105293 shutil .copy (temp_archive , final_archive )
106294 else :
107- make_archive (final_archive , final_location )
295+ make_archive (final_archive , final_location , ignore_patterns = base_patterns )
0 commit comments