Skip to content

Commit 55ce91e

Browse files
committed
feat: Multiarch wheel builds
1 parent bc7bdf7 commit 55ce91e

File tree

4 files changed

+158
-75
lines changed

4 files changed

+158
-75
lines changed

.github/workflows/build-wheel.yml

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ jobs:
121121
# Verify wheel structure
122122
twine check dist/*
123123
124-
- name: Build macOS wheel (Universal)
124+
- name: Build macOS wheel
125125
if: runner.os == 'macOS'
126126
run: |
127127
# Create necessary directories
@@ -138,13 +138,28 @@ jobs:
138138
# Download native artifacts
139139
python scripts/download_artifacts.py $C2PA_VERSION
140140
141-
# Build wheel
142-
python setup.py bdist_wheel --plat-name macosx_10_9_universal2
141+
# Determine platform name based on target architecture
142+
if [ "${{ inputs.architecture }}" = "universal2" ]; then
143+
PLATFORM_NAME="macosx_10_9_universal2"
144+
elif [ "${{ inputs.architecture }}" = "arm64" ]; then
145+
PLATFORM_NAME="macosx_11_0_arm64"
146+
elif [ "${{ inputs.architecture }}" = "x86_64" ]; then
147+
PLATFORM_NAME="macosx_10_9_x86_64"
148+
else
149+
echo "Unknown architecture: ${{ inputs.architecture }}"
150+
exit 1
151+
fi
152+
153+
echo "Building wheel for architecture: ${{ inputs.architecture }}"
154+
echo "Using platform name: $PLATFORM_NAME"
155+
156+
# Build wheel with appropriate platform name
157+
python setup.py bdist_wheel --plat-name $PLATFORM_NAME
143158
144159
# Rename wheel to ensure unique filename
145160
cd dist
146161
for wheel in *.whl; do
147-
mv "$wheel" "${wheel/macosx_10_9_universal2/macosx_10_9_universal2}"
162+
mv "$wheel" "${wheel/$PLATFORM_NAME/$PLATFORM_NAME}"
148163
done
149164
cd ..
150165

scripts/download_artifacts.py

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,17 @@ def detect_arch():
4040
else:
4141
raise ValueError(f"Unsupported CPU architecture: {machine}")
4242

43-
def get_platform_identifier():
44-
"""Get the full platform identifier (arch-os) for the current system,
45-
matching the identifiers used by the Github publisher.
43+
def get_platform_identifier(target_arch=None):
44+
"""Get the full platform identifier (arch-os) for the current system or target.
45+
46+
Args:
47+
target_arch: Optional target architecture. If provided, overrides auto-detection.
48+
For macOS: 'universal2', 'arm64', or 'x86_64'
49+
4650
Returns one of:
47-
- universal-apple-darwin (for Mac)
51+
- universal-apple-darwin (for macOS universal)
52+
- aarch64-apple-darwin (for macOS ARM64)
53+
- x86_64-apple-darwin (for macOS x86_64)
4854
- x86_64-pc-windows-msvc (for Windows 64-bit)
4955
- x86_64-unknown-linux-gnu (for Linux x86_64)
5056
- aarch64-unknown-linux-gnu (for Linux ARM64)
@@ -53,11 +59,27 @@ def get_platform_identifier():
5359
machine = platform.machine().lower()
5460

5561
if system == "darwin":
56-
return "universal-apple-darwin"
62+
if target_arch == "arm64":
63+
return "aarch64-apple-darwin"
64+
elif target_arch == "x86_64":
65+
return "x86_64-apple-darwin"
66+
elif target_arch == "universal2":
67+
return "universal-apple-darwin"
68+
else:
69+
# Auto-detect: prefer specific architecture over universal
70+
if machine == "arm64":
71+
return "aarch64-apple-darwin"
72+
elif machine == "x86_64":
73+
return "x86_64-apple-darwin"
74+
else:
75+
return "universal-apple-darwin"
5776
elif system == "windows":
58-
return "x86_64-pc-windows-msvc"
77+
if target_arch == "arm64":
78+
return "aarch64-pc-windows-msvc"
79+
else:
80+
return "x86_64-pc-windows-msvc"
5981
elif system == "linux":
60-
if machine in ["arm64", "aarch64"]:
82+
if target_arch == "aarch64" or machine in ["arm64", "aarch64"]:
6183
return "aarch64-unknown-linux-gnu"
6284
else:
6385
return "x86_64-unknown-linux-gnu"
@@ -87,19 +109,20 @@ def download_and_extract_libs(url, platform_name):
87109
response = requests.get(url, headers=headers)
88110
response.raise_for_status()
89111

112+
print(f"Downloaded zip file, extracting lib files...")
90113
with zipfile.ZipFile(io.BytesIO(response.content)) as zip_ref:
91114
# Extract only files inside the libs/ directory
115+
extracted_count = 0
92116
for member in zip_ref.namelist():
93-
print(f" Processing zip member: {member}")
94117
if member.startswith("lib/") and not member.endswith("/"):
95-
print(f" Processing lib file from downloadedzip: {member}")
96118
target_path = platform_dir / os.path.relpath(member, "lib")
97-
print(f" Moving file to target path: {target_path}")
98119
target_path.parent.mkdir(parents=True, exist_ok=True)
99120
with zip_ref.open(member) as source, open(target_path, "wb") as target:
100121
target.write(source.read())
122+
extracted_count += 1
123+
print(f" Extracted: {member} -> {target_path}")
101124

102-
print(f"Done downloading and extracting libraries for {platform_name}")
125+
print(f"Done downloading and extracting {extracted_count} library files for {platform_name}")
103126

104127
def copy_artifacts_to_root():
105128
"""Copy the artifacts folder from scripts/artifacts to the root of the repository."""
@@ -108,56 +131,77 @@ def copy_artifacts_to_root():
108131
return
109132

110133
print("Copying artifacts from scripts/artifacts to root...")
134+
print("Contents of scripts/artifacts before copying:")
135+
for item in sorted(SCRIPTS_ARTIFACTS_DIR.iterdir()):
136+
print(f" {item.name}")
137+
111138
if ROOT_ARTIFACTS_DIR.exists():
112139
shutil.rmtree(ROOT_ARTIFACTS_DIR)
113140
print(f"Copying from {SCRIPTS_ARTIFACTS_DIR} to {ROOT_ARTIFACTS_DIR}")
114141
shutil.copytree(SCRIPTS_ARTIFACTS_DIR, ROOT_ARTIFACTS_DIR)
115142
print("Done copying artifacts")
116-
print("\nFolder content of artifacts directory:")
143+
print("\nFolder content of root artifacts directory:")
117144
for item in sorted(ROOT_ARTIFACTS_DIR.iterdir()):
118145
print(f" {item.name}")
119146

120147
def main():
121148
if len(sys.argv) < 2:
122-
print("Usage: python download_artifacts.py <release_tag>")
149+
print("Usage: python download_artifacts.py <release_tag> [target_architecture]")
123150
print("Example: python download_artifacts.py c2pa-v0.49.5")
151+
print("Example: python download_artifacts.py c2pa-v0.49.5 arm64")
124152
sys.exit(1)
125153

126154
release_tag = sys.argv[1]
155+
target_arch = sys.argv[2] if len(sys.argv) > 2 else None
156+
127157
try:
158+
# Clean up any existing artifacts before starting
159+
print("Cleaning up existing artifacts...")
160+
if SCRIPTS_ARTIFACTS_DIR.exists():
161+
shutil.rmtree(SCRIPTS_ARTIFACTS_DIR)
128162
SCRIPTS_ARTIFACTS_DIR.mkdir(exist_ok=True)
129163
print(f"Fetching release information for tag {release_tag}...")
130164
release = get_release_by_tag(release_tag)
131165
print(f"Found release: {release['tag_name']} \n")
132166

133-
# Get the platform identifier for the current system
167+
# Get the platform identifier for the target architecture
134168
env_platform = os.environ.get("C2PA_LIBS_PLATFORM")
135169
if env_platform:
136170
print(f"Using platform from environment variable C2PA_LIBS_PLATFORM: {env_platform}")
137-
platform_id = env_platform or get_platform_identifier()
171+
platform_id = env_platform
172+
else:
173+
platform_id = get_platform_identifier(target_arch)
174+
print(f"Using target architecture: {target_arch or 'auto-detected'}")
175+
print(f"Detected machine architecture: {platform.machine()}")
176+
print(f"Detected system: {platform.system()}")
177+
138178
print("Looking up releases for platform id: ", platform_id)
139-
print("Environment variable set for lookup: ", env_platform)
140-
platform_source = "environment variable" if env_platform else "auto-detection"
141-
print(f"Target platform: {platform_id} (set through{platform_source})")
179+
platform_source = "environment variable" if env_platform else "target architecture" if target_arch else "auto-detection"
180+
print(f"Target platform: {platform_id} (set through {platform_source})")
142181

143182
# Construct the expected asset name
144183
expected_asset_name = f"{release_tag}-{platform_id}.zip"
145184
print(f"Looking for asset: {expected_asset_name}")
146185

147186
# Find the matching asset in the release
148187
matching_asset = None
188+
print(f"Looking for asset: {expected_asset_name}")
189+
print("Available assets in release:")
149190
for asset in release['assets']:
191+
print(f" - {asset['name']}")
150192
if asset['name'] == expected_asset_name:
151193
matching_asset = asset
152-
break
194+
print(f"Matching asset: {matching_asset['name']}")
153195

154196
if matching_asset:
155-
print(f"Found matching asset: {matching_asset['name']}")
197+
print(f"\nDownloading asset: {matching_asset['name']}")
156198
download_and_extract_libs(matching_asset['browser_download_url'], platform_id)
157199
print("\nArtifacts have been downloaded and extracted successfully!")
158200
copy_artifacts_to_root()
159201
else:
160-
print(f"\nNo matching asset found: {expected_asset_name}")
202+
print(f"\nNo matching asset found for platform: {platform_id}")
203+
print(f"Expected asset name: {expected_asset_name}")
204+
print("Please check if the asset exists in the release or if the platform identifier is correct.")
161205

162206
except requests.exceptions.RequestException as e:
163207
print(f"Error: {e}")

setup.py

Lines changed: 61 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ def get_version():
2525
# Based on what c2pa-rs repo publishes
2626
PLATFORM_FOLDERS = {
2727
'universal-apple-darwin': 'dylib',
28+
'aarch64-apple-darwin': 'dylib',
29+
'x86_64-apple-darwin': 'dylib',
2830
'x86_64-pc-windows-msvc': 'dll',
2931
'x86_64-unknown-linux-gnu': 'so',
3032
'aarch64-unknown-linux-gnu': 'so',
@@ -35,30 +37,42 @@ def get_version():
3537
PACKAGE_LIBS_DIR = Path('src/c2pa/libs') # Where libraries will be copied for the wheel
3638

3739

38-
def get_platform_identifier() -> str:
39-
"""Get a platform identifier (arch-os) for the current system,
40-
matching downloaded identifiers used by the Github publisher.
40+
def get_platform_identifier(target_arch=None) -> str:
41+
"""Get a platform identifier (arch-os) for the current system or target architecture.
4142
4243
Args:
43-
Only used on macOS systems.:
44-
cpu_arch: Optional CPU architecture for macOS. If not provided, returns universal build.
44+
target_arch: Optional target architecture. If provided, overrides auto-detection.
45+
For macOS: 'universal2', 'arm64', or 'x86_64'
46+
For Linux: 'aarch64' or 'x86_64'
47+
For Windows: 'x64' or 'arm64'
4548
4649
Returns one of:
47-
- universal-apple-darwin (for macOS)
50+
- universal-apple-darwin (for macOS universal)
51+
- aarch64-apple-darwin (for macOS ARM64)
52+
- x86_64-apple-darwin (for macOS x86_64)
4853
- x86_64-pc-windows-msvc (for Windows 64-bit)
4954
- x86_64-unknown-linux-gnu (for Linux 64-bit)
5055
- aarch64-unknown-linux-gnu (for Linux ARM64)
5156
"""
5257
system = platform.system().lower()
5358

5459
if system == "darwin":
55-
return "universal-apple-darwin"
60+
if target_arch == "arm64":
61+
return "aarch64-apple-darwin"
62+
elif target_arch == "x86_64":
63+
return "x86_64-apple-darwin"
64+
else:
65+
return "universal-apple-darwin"
5666
elif system == "windows":
57-
return "x86_64-pc-windows-msvc"
67+
if target_arch == "arm64":
68+
return "aarch64-pc-windows-msvc"
69+
else:
70+
return "x86_64-pc-windows-msvc"
5871
elif system == "linux":
59-
if platform.machine() == "aarch64":
72+
if target_arch == "aarch64" or platform.machine() == "aarch64":
6073
return "aarch64-unknown-linux-gnu"
61-
return "x86_64-unknown-linux-gnu"
74+
else:
75+
return "x86_64-unknown-linux-gnu"
6276
else:
6377
raise ValueError(f"Unsupported operating system: {system}")
6478

@@ -144,60 +158,59 @@ def find_available_platforms():
144158

145159
# For wheel building (both bdist_wheel and build)
146160
if 'bdist_wheel' in sys.argv or 'build' in sys.argv:
147-
available_platforms = find_available_platforms()
148-
if not available_platforms:
149-
print("No platform-specific libraries found. Building wheel without platform-specific libraries.")
161+
# Check if we're building for a specific architecture
162+
target_arch = None
163+
for i, arg in enumerate(sys.argv):
164+
if arg == '--plat-name':
165+
if i + 1 < len(sys.argv):
166+
plat_name = sys.argv[i + 1]
167+
if 'arm64' in plat_name:
168+
target_arch = 'arm64'
169+
elif 'x86_64' in plat_name:
170+
target_arch = 'x86_64'
171+
elif 'universal2' in plat_name:
172+
target_arch = 'universal2'
173+
break
174+
175+
# Get the platform identifier for the target architecture
176+
target_platform = get_platform_identifier(target_arch)
177+
print(f"Building wheel for target platform: {target_platform}")
178+
179+
# Check if we have libraries for this platform
180+
platform_dir = ARTIFACTS_DIR / target_platform
181+
if not platform_dir.exists() or not any(platform_dir.iterdir()):
182+
print(f"Warning: No libraries found for platform {target_platform}")
183+
print("Available platforms:")
184+
for platform_name in find_available_platforms():
185+
print(f" - {platform_name}")
186+
187+
# Copy libraries for the target platform
188+
try:
189+
copy_platform_libraries(target_platform, clean_first=True)
190+
191+
# Build the wheel
150192
setup(
151193
name=PACKAGE_NAME,
152194
version=VERSION,
153195
package_dir={"": "src"},
154196
packages=find_namespace_packages(where="src"),
155197
include_package_data=True,
156198
package_data={
157-
"c2pa": ["libs/*"], # Include all files in libs directory
199+
"c2pa": ["libs/*"],
158200
},
159201
classifiers=[
160202
"Programming Language :: Python :: 3",
161-
get_platform_classifier(get_current_platform()),
203+
get_platform_classifier(target_platform),
162204
],
163205
python_requires=">=3.10",
164206
long_description=open("README.md").read(),
165207
long_description_content_type="text/markdown",
166208
license="MIT OR Apache-2.0",
167209
)
168-
sys.exit(0)
169-
170-
print(f"Found libraries for platforms: {', '.join(available_platforms)}")
171-
172-
for platform_name in available_platforms:
173-
print(f"\nBuilding wheel for {platform_name}...")
174-
try:
175-
# Copy libraries for this platform (cleaning first)
176-
copy_platform_libraries(platform_name, clean_first=True)
177-
178-
# Build the wheel
179-
setup(
180-
name=PACKAGE_NAME,
181-
version=VERSION,
182-
package_dir={"": "src"},
183-
packages=find_namespace_packages(where="src"),
184-
include_package_data=True,
185-
package_data={
186-
"c2pa": ["libs/*"], # Include all files in libs directory
187-
},
188-
classifiers=[
189-
"Programming Language :: Python :: 3",
190-
get_platform_classifier(platform_name),
191-
],
192-
python_requires=">=3.10",
193-
long_description=open("README.md").read(),
194-
long_description_content_type="text/markdown",
195-
license="MIT OR Apache-2.0",
196-
)
197-
finally:
198-
# Clean up by removing the package libs directory
199-
if PACKAGE_LIBS_DIR.exists():
200-
shutil.rmtree(PACKAGE_LIBS_DIR)
210+
finally:
211+
# Clean up
212+
if PACKAGE_LIBS_DIR.exists():
213+
shutil.rmtree(PACKAGE_LIBS_DIR)
201214
sys.exit(0)
202215

203216
# For sdist and development installation

0 commit comments

Comments
 (0)