@@ -16,6 +16,99 @@ def file_hash(file):
1616 return h .hexdigest ()
1717
1818
19+ def should_exclude (path , name , exclude_patterns ):
20+ """Check if a path should be excluded based on patterns."""
21+ # Check if the name itself matches any pattern
22+ for pattern in exclude_patterns :
23+ # Exact name match
24+ if name == pattern :
25+ return True
26+
27+ # Directory patterns (e.g., __pycache__, test)
28+ if pattern .startswith ('**/' ):
29+ pattern_name = pattern [3 :]
30+ if name == pattern_name :
31+ return True
32+
33+ # Extension patterns (e.g., *.pyc)
34+ if pattern .startswith ('**/*.' ):
35+ ext = pattern [4 :] # Remove **/*
36+ if name .endswith (ext ):
37+ return True
38+
39+ # Wildcard patterns
40+ if '*' in pattern and not pattern .startswith ('**/' ):
41+ import fnmatch
42+ if fnmatch .fnmatch (name , pattern ):
43+ return True
44+
45+ return False
46+
47+
48+ def copy_filtered_tree (src , dst , exclude_patterns , verbose = False ):
49+ """Recursively copy directory tree with filtering."""
50+ if not os .path .exists (dst ):
51+ os .makedirs (dst )
52+
53+ for item in os .listdir (src ):
54+ src_path = os .path .join (src , item )
55+ dst_path = os .path .join (dst , item )
56+
57+ # Check if should exclude
58+ if should_exclude (src_path , item , exclude_patterns ):
59+ if verbose :
60+ print (f"Excluding: { os .path .relpath (src_path , src )} " )
61+ continue
62+
63+ if os .path .isdir (src_path ):
64+ copy_filtered_tree (src_path , dst_path , exclude_patterns , verbose )
65+ else :
66+ shutil .copy2 (src_path , dst_path )
67+ if verbose :
68+ print (f"Copied: { os .path .relpath (src_path , src )} " )
69+
70+
71+ def clean_duplicate_libraries (directory , verbose = False ):
72+ """Keep only one of each dynamic library type."""
73+ lib_files = {}
74+
75+ for root , dirs , files in os .walk (directory ):
76+ for file in files :
77+ file_path = os .path .join (root , file )
78+
79+ # Group by base name without version numbers
80+ # e.g., libpython3.13.dylib -> libpython, libpython3.13.a -> libpython
81+ if any (ext in file for ext in ['.dylib' , '.dll' , '.so' , '.a' , '.lib' ]):
82+ # Extract base name and extension type
83+ base_name = file .split ('.' )[0 ]
84+
85+ # Determine library type
86+ if '.dylib' in file :
87+ lib_type = 'dylib'
88+ elif '.dll' in file :
89+ lib_type = 'dll'
90+ elif '.so' in file :
91+ lib_type = 'so'
92+ elif '.a' in file :
93+ lib_type = 'static'
94+ elif '.lib' in file :
95+ lib_type = 'lib'
96+ else :
97+ continue
98+
99+ key = (base_name , lib_type )
100+
101+ if key not in lib_files :
102+ lib_files [key ] = file_path
103+ if verbose :
104+ print (f"Keeping library: { file } " )
105+ else :
106+ # Remove duplicate
107+ if verbose :
108+ print (f"Removing duplicate library: { file } " )
109+ os .remove (file_path )
110+
111+
19112def make_archive (file , directory , verbose = False ):
20113 archived_files = []
21114 for dirname , _ , files in os .walk (directory ):
@@ -42,7 +135,7 @@ def make_archive(file, directory, verbose=False):
42135 print (f"starting python standard lib archiving tool..." )
43136
44137 parser = ArgumentParser ()
45- parser .add_argument ("-r" , "--root-folder" , type = Path , help = "Path to the python root folder." )
138+ parser .add_argument ("-r" , "--root-folder" , type = Path , help = "Path to the python base folder." )
46139 parser .add_argument ("-o" , "--output-folder" , type = Path , help = "Path to the output folder." )
47140 parser .add_argument ("-M" , "--version-major" , type = int , help = "Major version number (integer)." )
48141 parser .add_argument ("-m" , "--version-minor" , type = int , help = "Minor version number (integer)." )
@@ -53,55 +146,163 @@ def make_archive(file, directory, verbose=False):
53146
54147 version = f"{ args .version_major } .{ args .version_minor } "
55148 version_nodot = f"{ args .version_major } { args .version_minor } "
149+ python_folder_name = f"python{ version } "
56150
57151 final_location : Path = args .output_folder / "python"
58- site_packages = final_location / "site-packages"
59152 final_archive = args .output_folder / f"python{ version_nodot } .zip"
60153 temp_archive = args .output_folder / f"temp{ version_nodot } .zip"
61154
62155 base_python : Path = args .root_folder
63156
64157 base_patterns = [
65- "**/*.pyc" ,
66- "**/__pycache__" ,
67- "**/__phello__" ,
68- "**/*config-3*" ,
69- "**/*tcl*" ,
70- "**/*tdbc*" ,
71- "**/*tk*" ,
72- "**/Tk*" ,
73- "**/_tk*" ,
74- "**/_test*" ,
75- "**/libpython*" ,
76- "**/pkgconfig" ,
77- "**/idlelib" ,
78- "**/site-packages" ,
79- "**/test" ,
80- "**/turtledemo" ,
81- "**/temp_*.txt" ,
82- "**/.DS_Store" ,
83- "**/EXTERNALLY-MANAGED" ,
84- "**/LICENSE.txt" ,
158+ "*.pyc" ,
159+ "__pycache__" ,
160+ "__phello__" ,
161+ "_tk*" ,
162+ "_test*" ,
163+ "_xxtestfuzz*" ,
164+ "config-3*" ,
165+ "idlelib" ,
166+ "pkgconfig" ,
167+ "pydoc_data" ,
168+ "test" ,
169+ "*tcl*" ,
170+ "*tdbc*" ,
171+ "*tk*" ,
172+ "Tk*" ,
173+ "turtledemo" ,
174+ ".DS_Store" ,
175+ "EXTERNALLY-MANAGED" ,
176+ "LICENSE.txt" ,
85177 ]
86178
87179 if args .exclude_patterns :
88180 custom_patterns = [x .strip () for x in args .exclude_patterns .replace ('"' , '' ).split (";" )]
89181 base_patterns += custom_patterns
90182
91- ignored_files = shutil .ignore_patterns (* base_patterns )
92-
93183 print (f"cleaning up { final_location } ..." )
94184 if final_location .exists ():
95185 shutil .rmtree (final_location )
96186
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 )
99- os .makedirs (site_packages , exist_ok = True )
187+ final_location .mkdir (parents = True , exist_ok = True )
100188
189+ # Copy lib folder structure
190+ lib_src = base_python / "lib"
191+ if lib_src .exists ():
192+ lib_dst = final_location / "lib"
193+ lib_dst .mkdir (parents = True , exist_ok = True )
194+
195+ # Copy python version folder
196+ python_lib_src = lib_src / python_folder_name
197+ if python_lib_src .exists ():
198+ python_lib_dst = lib_dst / python_folder_name
199+ print (f"copying library from { python_lib_src } to { python_lib_dst } ..." )
200+ copy_filtered_tree (python_lib_src , python_lib_dst , base_patterns , verbose = args .verbose )
201+
202+ # Create site-packages directory
203+ site_packages = python_lib_dst / "site-packages"
204+ site_packages .mkdir (parents = True , exist_ok = True )
205+ else :
206+ print (f"Warning: Python library path { python_lib_src } does not exist" )
207+
208+ # Copy dynamic libraries from lib root (e.g., libpython3.13.dylib)
209+ print (f"copying dynamic libraries from { lib_src } ..." )
210+ for item in lib_src .iterdir ():
211+ if item .is_file ():
212+ if any (ext in item .name for ext in ['.dylib' , '.dll' , '.so' , '.a' , '.lib' ]):
213+ if not should_exclude (str (item ), item .name , base_patterns ):
214+ shutil .copy2 (item , lib_dst / item .name )
215+ if args .verbose :
216+ print (f"Copied library: { item .name } " )
217+ else :
218+ print (f"Warning: Library path { lib_src } does not exist" )
219+
220+ # Copy bin folder
221+ bin_src = base_python / "bin"
222+ if bin_src .exists ():
223+ bin_dst = final_location / "bin"
224+ bin_dst .mkdir (parents = True , exist_ok = True )
225+
226+ print (f"copying binaries from { bin_src } to { bin_dst } ..." )
227+
228+ # Copy python executables and symlinks (deduplicate with set)
229+ executables = list (set ([
230+ "python3" ,
231+ "python" ,
232+ f"python{ version } " ,
233+ f"python{ args .version_major } " ,
234+ f"python{ args .version_major } .{ args .version_minor } " ,
235+ ]))
236+
237+ for executable in executables :
238+ exe_path = bin_src / executable
239+ dst_path = bin_dst / executable
240+
241+ if exe_path .exists ():
242+ # Skip if destination already exists
243+ if dst_path .exists ():
244+ if args .verbose :
245+ print (f"Skipping existing binary: { executable } " )
246+ continue
247+
248+ if exe_path .is_symlink ():
249+ # Preserve symlinks
250+ link_target = os .readlink (exe_path )
251+ os .symlink (link_target , dst_path )
252+ else :
253+ shutil .copy2 (exe_path , dst_path )
254+ if args .verbose :
255+ print (f"Copied binary: { executable } " )
256+ else :
257+ print (f"Warning: Binary path { bin_src } does not exist" )
258+
259+ # Copy include folder
260+ include_src = base_python / "include"
261+ if include_src .exists ():
262+ include_dst = final_location / "include"
263+ include_dst .mkdir (parents = True , exist_ok = True )
264+
265+ print (f"copying include files from { include_src } to { include_dst } ..." )
266+
267+ # Copy the python version include folder
268+ python_include_src = include_src / python_folder_name
269+ if python_include_src .exists ():
270+ python_include_dst = include_dst / python_folder_name
271+ shutil .copytree (python_include_src , python_include_dst , dirs_exist_ok = True )
272+ else :
273+ # Try copying the whole include folder
274+ for item in include_src .iterdir ():
275+ if item .is_dir ():
276+ shutil .copytree (item , include_dst / item .name , dirs_exist_ok = True )
277+ else :
278+ shutil .copy2 (item , include_dst / item .name )
279+
280+ if args .verbose :
281+ print (f"Copied include files" )
282+ else :
283+ print (f"Warning: Include path { include_src } does not exist" )
284+
285+ # Clean up duplicate libraries
286+ print (f"cleaning up duplicate libraries..." )
287+ clean_duplicate_libraries (final_location , verbose = args .verbose )
288+
289+ # Create archive from final_location contents (not including the python/ wrapper)
101290 print (f"making archive { temp_archive } to { final_archive } ..." )
102291 if os .path .exists (final_archive ):
103292 make_archive (temp_archive , final_location , verbose = args .verbose )
104293 if file_hash (temp_archive ) != file_hash (final_archive ):
105294 shutil .copy (temp_archive , final_archive )
295+ os .remove (temp_archive )
296+ print (f"Archive updated" )
297+ else :
298+ os .remove (temp_archive )
299+ print (f"Archive unchanged" )
106300 else :
107- make_archive (final_archive , final_location )
301+ make_archive (final_archive , final_location , verbose = args .verbose )
302+ print (f"Archive created" )
303+
304+ # Clean up temporary directory
305+ print (f"cleaning up { final_location } ..." )
306+ shutil .rmtree (final_location )
307+
308+ print ("Done!" )
0 commit comments