Skip to content

Commit 39a7fb4

Browse files
committed
Improve python packaging
1 parent 320590b commit 39a7fb4

File tree

3 files changed

+232
-31
lines changed

3 files changed

+232
-31
lines changed

cmake/yup_python.cmake

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function (yup_prepare_python_stdlib target_name python_tools_path output_variabl
2626

2727
cmake_parse_arguments (YUP_ARG "${options}" "${one_value_args}" "${multi_value_args}" ${ARGN})
2828

29-
set (default_ignored_library_patterns "lib2to3" "pydoc_data" "_xxtestfuzz*")
29+
set (default_ignored_library_patterns "lib2to3")
3030

3131
set (ignored_library_patterns ${default_ignored_library_patterns})
3232
list (APPEND ignored_library_patterns ${YUP_ARG_IGNORED_LIBRARY_PATTERNS})
@@ -45,7 +45,7 @@ function (yup_prepare_python_stdlib target_name python_tools_path output_variabl
4545

4646
get_filename_component (python_root_path "${python_embed_env_SOURCE_DIR}" REALPATH)
4747
else()
48-
get_filename_component (python_root_path "${Python_LIBRARY_DIRS}" REALPATH)
48+
get_filename_component (python_root_path "${Python_LIBRARY_DIRS}/.." REALPATH)
4949
endif()
5050

5151
_yup_message (STATUS "Executing python stdlib archive generator tool")

modules/yup_python/scripting/yup_ScriptEngine.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ std::unique_ptr<PyConfig> ScriptEngine::prepareScriptingHome (
111111
auto mis = MemoryInputStream (mb.getData(), mb.getSize(), false);
112112

113113
auto zip = ZipFile (mis);
114-
zip.uncompressTo (libFolder.getParentDirectory());
114+
zip.uncompressTo (destinationFolder);
115115
}
116116

117117
//for (auto entry : RangedDirectoryIterator (destinationFolder, true, "*", File::findFiles, File::FollowSymlinks::no))

python/tools/ArchivePythonStdlib.py

Lines changed: 229 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
19112
def 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

Comments
 (0)