Skip to content

Instantly share code, notes, and snippets.

@btn0s
Created October 4, 2024 09:45
Show Gist options
  • Select an option

  • Save btn0s/04b5f0096f6ca81e26a67af5014122ff to your computer and use it in GitHub Desktop.

Select an option

Save btn0s/04b5f0096f6ca81e26a67af5014122ff to your computer and use it in GitHub Desktop.
generate-swift-package.py
import os
import xml.etree.ElementTree as ET
import re
import glob
import plistlib
import json
def parse_pbxproj(pbxproj_path):
"""
Parse the Xcode project file (.pbxproj) to extract target names, source files, frameworks, and build settings.
"""
try:
with open(pbxproj_path, 'r') as file:
content = file.read()
plist = plistlib.loads(bytes(content, 'UTF-8'))
except Exception as e:
print(f"Error reading pbxproj file: {e}")
return [], [], [], {}
targets = []
source_files = []
frameworks = []
build_settings = {}
# Extract target and build settings information from the plist dictionary
for key, value in plist.items():
if isinstance(value, dict) and value.get('isa') == 'XCBuildConfiguration':
build_settings.update(value.get('buildSettings', {}))
for key, value in plist.items():
if isinstance(value, dict) and value.get('isa') == 'PBXNativeTarget':
targets.append(value.get('name', ''))
for key, value in plist.items():
if isinstance(value, dict) and value.get('isa') == 'PBXFileReference':
file_path = value.get('path')
if file_path and file_path.endswith(('.m', '.swift', '.c', '.cpp')):
source_files.append(file_path)
if file_path and file_path.endswith('.framework'):
frameworks.append(file_path)
return targets, source_files, frameworks, build_settings
def find_all_source_files(root_dir):
"""
Recursively find all source files within the specified directory.
"""
return glob.glob(f"{root_dir}/**/*.[mc]", recursive=True) + \
glob.glob(f"{root_dir}/**/*.swift", recursive=True) + \
glob.glob(f"{root_dir}/**/*.cpp", recursive=True)
def analyze_source_files(source_files):
"""
Analyze source files to determine dependencies.
"""
dependencies = set()
for source_file in source_files:
try:
with open(source_file, 'r') as file:
content = file.read()
includes = re.findall(r'#include [<"](.*?)[>"]', content)
imports = re.findall(r'@import (.*?);', content)
dependencies.update(includes + imports)
except Exception as e:
print(f"Error reading source file {source_file}: {e}")
return list(dependencies)
def find_resources(root_dir):
"""
Find resource files such as images, storyboards, and other assets.
"""
resource_extensions = ['.png', '.jpg', '.jpeg', '.storyboard', '.xib', '.xcassets', '.json', '.plist']
resources = []
for ext in resource_extensions:
resources.extend(glob.glob(f"{root_dir}/**/*{ext}", recursive=True))
return resources
def find_submodules(root_dir):
"""
Find git submodules defined in the .gitmodules file.
"""
gitmodules_path = os.path.join(root_dir, '.gitmodules')
submodules = []
if os.path.exists(gitmodules_path):
with open(gitmodules_path, 'r') as f:
content = f.read()
submodules = re.findall(r'path = (.+)', content)
return submodules
def find_script_phases(pbxproj_path):
"""
Extract shell script build phases from the Xcode project file.
"""
try:
with open(pbxproj_path, 'r') as file:
content = file.read()
plist = plistlib.loads(bytes(content, 'UTF-8'))
except Exception as e:
print(f"Error reading pbxproj file: {e}")
return []
script_phases = []
for key, value in plist.items():
if isinstance(value, dict) and value.get('isa') == 'PBXShellScriptBuildPhase':
script_phases.append(value.get('shellScript', ''))
return script_phases
def generate_package_swift(targets, source_files, frameworks, dependencies, resources, submodules, build_settings, script_phases):
"""
Generate the Package.swift file content based on the extracted project data.
"""
source_files_str = ', '.join(f'"{file}"' for file in source_files)
frameworks_str = ', '.join(f'.linkedFramework("{framework}")' for framework in frameworks)
dependencies_str = ', '.join(f'"{dep}"' for dep in dependencies)
resources_str = ', '.join(f'"{res}"' for res in resources)
submodules_str = ', '.join(f'"{sub}"' for sub in submodules)
deployment_target = build_settings.get('IPHONEOS_DEPLOYMENT_TARGET', '13.0')
c_flags = build_settings.get('OTHER_CFLAGS', '').split()
cxx_flags = build_settings.get('OTHER_CPLUSPLUSFLAGS', '').split()
c_settings = [f'.define("{flag}")' for flag in c_flags]
cxx_settings = [f'.define("{flag}")' for flag in cxx_flags]
header_search_paths = build_settings.get('HEADER_SEARCH_PATHS', '').split()
for path in header_search_paths:
c_settings.append(f'.headerSearchPath("{path}")')
cxx_settings.append(f'.headerSearchPath("{path}")')
package_swift = f"""
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "PPSSPPIOS",
platforms: [
.iOS(.v{deployment_target})
],
products: [
.library(
name: "PPSSPPIOS",
targets: ["PPSSPPIOS"]),
],
dependencies: [
// Add external dependencies here
{submodules_str}
],
targets: [
.target(
name: "PPSSPPIOS",
dependencies: [],
path: "ios",
exclude: ["README.md"],
sources: [{source_files_str}],
resources: [{resources_str}],
publicHeadersPath: "include",
cSettings: [
{', '.join(c_settings)}
],
cxxSettings: [
{', '.join(cxx_settings)}
],
linkerSettings: [
{frameworks_str}
]
)
],
cLanguageStandard: .gnu11,
cxxLanguageStandard: .gnucxx14
)
"""
return package_swift
def main():
project_path = "path/to/your/PPSSPP.xcodeproj/project.pbxproj"
ios_dir = "path/to/your/ios/directory"
root_dir = "path/to/your/project/root"
targets, project_source_files, frameworks, build_settings = parse_pbxproj(project_path)
all_source_files = find_all_source_files(ios_dir)
dependencies = analyze_source_files(all_source_files)
resources = find_resources(ios_dir)
submodules = find_submodules(root_dir)
script_phases = find_script_phases(project_path)
# Combine and deduplicate source files
source_files = list(set(project_source_files + all_source_files))
package_swift = generate_package_swift(targets, source_files, frameworks, dependencies, resources, submodules, build_settings, script_phases)
with open("Package.swift", "w") as f:
f.write(package_swift)
print("Package.swift has been generated.")
# Generate additional configuration files if needed
if script_phases:
with open("build_phases.json", "w") as f:
json.dump(script_phases, f, indent=2)
print("build_phases.json has been generated for custom build phases.")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment