bugger/scripts/generate_xcode_project.py

473 lines
22 KiB
Python

#!/usr/bin/env python3
"""Generate Bugger.xcodeproj from source tree."""
import hashlib
import os
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
PROJECT_NAME = "Bugger"
BUNDLE_ID = "com.xorbitlab.bugger"
SOURCE_ROOT = ROOT / "Sources"
RESOURCE_ROOT = ROOT / "Resources"
def uuid(seed: str) -> str:
digest = hashlib.md5(seed.encode()).hexdigest().upper()
return digest[:24]
def collect_swift_files() -> list[Path]:
return sorted(SOURCE_ROOT.rglob("*.swift"))
def collect_resources() -> list[Path]:
resources = []
for path in sorted(RESOURCE_ROOT.rglob("*")):
if path.is_file():
resources.append(path)
return resources
def pbxproj() -> str:
swift_files = collect_swift_files()
resources = collect_resources()
project_id = uuid("project")
target_id = uuid("target")
sources_phase_id = uuid("sources-phase")
resources_phase_id = uuid("resources-phase")
frameworks_phase_id = uuid("frameworks-phase")
product_ref_id = uuid("product-ref")
project_config_list_id = uuid("project-config-list")
target_config_list_id = uuid("target-config-list")
debug_config_id = uuid("debug-config")
release_config_id = uuid("release-config")
target_debug_config_id = uuid("target-debug-config")
target_release_config_id = uuid("target-release-config")
main_group_id = uuid("main-group")
products_group_id = uuid("products-group")
sources_group_id = uuid("sources-group")
resources_group_id = uuid("resources-group")
file_refs: dict[Path, str] = {}
build_files: list[tuple[str, str, str]] = []
for path in swift_files:
ref_id = uuid(f"file-{path}")
build_id = uuid(f"build-{path}")
file_refs[path] = ref_id
build_files.append((build_id, ref_id, "Sources"))
resource_refs: dict[Path, str] = {}
for path in resources:
ref_id = uuid(f"res-{path}")
build_id = uuid(f"res-build-{path}")
resource_refs[path] = ref_id
build_files.append((build_id, ref_id, "Resources"))
def group_for(path: Path, parent: str) -> str:
rel = path.relative_to(ROOT)
parts = rel.parts[:-1]
group_ids = [main_group_id]
current = ""
for part in parts:
current = f"{current}/{part}"
group_ids.append(uuid(f"group-{current}"))
return group_ids[-1]
groups: dict[str, list[str]] = {}
child_groups: dict[str, set[str]] = {}
def add_to_group(group_id: str, child_id: str):
groups.setdefault(group_id, []).append(child_id)
add_to_group(main_group_id, sources_group_id)
add_to_group(main_group_id, resources_group_id)
add_to_group(main_group_id, products_group_id)
add_to_group(products_group_id, product_ref_id)
def ensure_group_chain(rel_parts: tuple[str, ...], root_group: str):
current = root_group
built = []
for part in rel_parts:
built.append(part)
gid = uuid(f"group-{'/'.join(built)}")
if gid not in groups and gid != current:
add_to_group(current, gid)
current = gid
return current
for path in swift_files:
rel = path.relative_to(SOURCE_ROOT)
parent = ensure_group_chain(rel.parts[:-1], sources_group_id)
add_to_group(parent, file_refs[path])
for path in resources:
rel = path.relative_to(RESOURCE_ROOT)
if len(rel.parts) > 1:
parent = ensure_group_chain(rel.parts[:-1], resources_group_id)
else:
parent = resources_group_id
add_to_group(parent, resource_refs[path])
lines: list[str] = []
lines.append("// !$*UTF8*$!")
lines.append("{")
lines.append("\tarchiveVersion = 1;")
lines.append("\tclasses = {};")
lines.append("\tobjectVersion = 56;")
lines.append("\tobjects = {")
lines.append(f"\n/* Begin PBXBuildFile section */")
for build_id, ref_id, kind in build_files:
if kind == "Sources":
lines.append(f"\t\t{build_id} /* {Path(ref_id).name} in Sources */ = {{isa = PBXBuildFile; fileRef = {ref_id}; }};")
else:
lines.append(f"\t\t{build_id} /* {ref_id} in Resources */ = {{isa = PBXBuildFile; fileRef = {ref_id}; }};")
lines.append("/* End PBXBuildFile section */\n")
lines.append("/* Begin PBXFileReference section */")
lines.append(f"\t\t{product_ref_id} /* {PROJECT_NAME}.app */ = {{isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = {PROJECT_NAME}.app; sourceTree = BUILT_PRODUCTS_DIR; }};")
for path, ref_id in file_refs.items():
rel = path.relative_to(ROOT)
lines.append(f"\t\t{ref_id} /* {path.name} */ = {{isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = {path.name}; sourceTree = \"<group>\"; }};")
for path, ref_id in resource_refs.items():
rel = path.relative_to(ROOT)
suffix = path.suffix.lower()
if suffix == ".plist":
file_type = "text.plist.xml"
elif suffix == ".entitlements":
file_type = "text.plist.entitlements"
elif path.name == "Contents.json":
file_type = "text.json"
else:
file_type = "folder.assetcatalog" if path.parent.suffix == ".xcassets" and path.name == "Contents.json" and path.parent.parent.name == "Assets.xcassets" else "text.json"
if path.name == "Contents.json" and "Assets.xcassets" in str(path):
if path.parent == RESOURCE_ROOT / "Assets.xcassets":
continue
if path.parent.name.endswith(".appiconset"):
file_type = "text.json"
elif path.parent == RESOURCE_ROOT / "Assets.xcassets":
file_type = "folder.assetcatalog"
# asset catalog handled as single folder reference below
if "Assets.xcassets" in str(path) and path != RESOURCE_ROOT / "Assets.xcassets":
continue
if path == RESOURCE_ROOT / "Assets.xcassets":
lines.append(f"\t\t{ref_id} /* Assets.xcassets */ = {{isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; }};")
continue
lines.append(f"\t\t{ref_id} /* {path.name} */ = {{isa = PBXFileReference; lastKnownFileType = {file_type}; path = {path.name}; sourceTree = \"<group>\"; }};")
lines.append("/* End PBXFileReference section */\n")
# Simpler approach: rewrite resource refs cleanly
resource_file_refs = []
asset_ref = uuid("asset-catalog")
info_ref = uuid("info-plist")
ent_ref = uuid("entitlements")
resource_file_refs = [
(RESOURCE_ROOT / "Assets.xcassets", asset_ref, "folder.assetcatalog", "Assets.xcassets"),
(RESOURCE_ROOT / "Info.plist", info_ref, "text.plist.xml", "Info.plist"),
(RESOURCE_ROOT / "Bugger.entitlements", ent_ref, "text.plist.entitlements", "Bugger.entitlements"),
]
lines = []
lines.append("// !$*UTF8*$!")
lines.append("{")
lines.append("\tarchiveVersion = 1;")
lines.append("\tclasses = {};")
lines.append("\tobjectVersion = 56;")
lines.append("\tobjects = {")
lines.append("\n/* Begin PBXBuildFile section */")
swift_build_entries = []
for path in swift_files:
ref_id = uuid(f"file-{path}")
build_id = uuid(f"build-{path}")
swift_build_entries.append((build_id, ref_id, path.name))
lines.append(f"\t\t{build_id} /* {path.name} in Sources */ = {{isa = PBXBuildFile; fileRef = {ref_id}; }};")
asset_build_id = uuid("asset-build")
lines.append(f"\t\t{asset_build_id} /* Assets.xcassets in Resources */ = {{isa = PBXBuildFile; fileRef = {asset_ref}; }};")
lines.append("/* End PBXBuildFile section */\n")
lines.append("/* Begin PBXFileReference section */")
lines.append(f"\t\t{product_ref_id} /* {PROJECT_NAME}.app */ = {{isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = {PROJECT_NAME}.app; sourceTree = BUILT_PRODUCTS_DIR; }};")
swift_ref_entries = []
for path in swift_files:
ref_id = uuid(f"file-{path}")
swift_ref_entries.append((ref_id, path))
lines.append(f"\t\t{ref_id} /* {path.name} */ = {{isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = {path.name}; sourceTree = \"<group>\"; }};")
lines.append(f"\t\t{asset_ref} /* Assets.xcassets */ = {{isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; }};")
lines.append(f"\t\t{info_ref} /* Info.plist */ = {{isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; }};")
lines.append(f"\t\t{ent_ref} /* Bugger.entitlements */ = {{isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Bugger.entitlements; sourceTree = \"<group>\"; }};")
lines.append("/* End PBXFileReference section */\n")
def render_groups():
group_defs = []
def walk(dir_path: Path, group_name: str, group_id: str, parent_set: list[str]):
children = []
for child in sorted(dir_path.iterdir()):
if child.is_dir() and child.name != ".DS_Store":
child_id = uuid(f"group-{child}")
children.append(child_id)
walk(child, child.name, child_id, [])
elif child.suffix == ".swift":
ref_id = uuid(f"file-{child}")
children.append(ref_id)
if dir_path == RESOURCE_ROOT:
children = [asset_ref, info_ref, ent_ref]
if dir_path == ROOT:
children = [sources_group_id, resources_group_id, products_group_id]
if dir_path == SOURCE_ROOT:
children = []
for child in sorted(dir_path.iterdir()):
if child.is_dir():
children.append(uuid(f"group-{child}"))
elif child.suffix == ".swift":
children.append(uuid(f"file-{child}"))
child_lines = "\n".join([f"\t\t\t\t{id_}," for id_ in children])
path_line = f"path = {group_name};" if group_name not in ("", PROJECT_NAME) else ""
if group_id == main_group_id:
group_defs.append(
f"\t\t{group_id} = {{\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n{child_lines}\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t}};"
)
elif group_id == products_group_id:
group_defs.append(
f"\t\t{group_id} = {{\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t{product_ref_id},\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t}};"
)
elif group_id == sources_group_id:
group_defs.append(
f"\t\t{group_id} = {{\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n{child_lines}\n\t\t\t);\n\t\t\tpath = Sources;\n\t\t\tsourceTree = \"<group>\";\n\t\t}};"
)
elif group_id == resources_group_id:
group_defs.append(
f"\t\t{group_id} = {{\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t{asset_ref},\n\t\t\t\t{info_ref},\n\t\t\t\t{ent_ref},\n\t\t\t);\n\t\t\tpath = Resources;\n\t\t\tsourceTree = \"<group>\";\n\t\t}};"
)
else:
rel = dir_path.relative_to(SOURCE_ROOT) if str(dir_path).startswith(str(SOURCE_ROOT)) else dir_path.name
group_defs.append(
f"\t\t{group_id} = {{\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n{child_lines}\n\t\t\t);\n\t\t\tpath = {dir_path.name};\n\t\t\tsourceTree = \"<group>\";\n\t\t}};"
)
walk(ROOT, PROJECT_NAME, main_group_id, [])
for child in sorted(SOURCE_ROOT.iterdir()):
if child.is_dir():
walk(child, child.name, uuid(f"group-{child}"), [])
return "\n".join(group_defs)
lines.append("/* Begin PBXFrameworksBuildPhase section */")
lines.append(f"\t\t{frameworks_phase_id} /* Frameworks */ = {{")
lines.append("\t\t\tisa = PBXFrameworksBuildPhase;")
lines.append("\t\t\tbuildActionMask = 2147483647;")
lines.append("\t\t\tfiles = (")
lines.append("\t\t\t);")
lines.append("\t\t\trunOnlyForDeploymentPostprocessing = 0;")
lines.append("\t\t};")
lines.append("/* End PBXFrameworksBuildPhase section */\n")
lines.append("/* Begin PBXGroup section */")
lines.append(render_groups())
lines.append("/* End PBXGroup section */\n")
lines.append("/* Begin PBXNativeTarget section */")
lines.append(f"\t\t{target_id} /* {PROJECT_NAME} */ = {{")
lines.append("\t\t\tisa = PBXNativeTarget;")
lines.append(f"\t\t\tbuildConfigurationList = {target_config_list_id} /* Build configuration list for PBXNativeTarget \"{PROJECT_NAME}\" */;")
lines.append("\t\t\tbuildPhases = (")
lines.append(f"\t\t\t\t{sources_phase_id} /* Sources */,")
lines.append(f"\t\t\t\t{frameworks_phase_id} /* Frameworks */,")
lines.append(f"\t\t\t\t{resources_phase_id} /* Resources */,")
lines.append("\t\t\t);")
lines.append("\t\t\tbuildRules = (")
lines.append("\t\t\t);")
lines.append("\t\t\tdependencies = (")
lines.append("\t\t\t);")
lines.append(f"\t\t\tname = {PROJECT_NAME};")
lines.append(f"\t\t\tproductName = {PROJECT_NAME};")
lines.append(f"\t\t\tproductReference = {product_ref_id} /* {PROJECT_NAME}.app */;")
lines.append("\t\t\tproductType = \"com.apple.product-type.application\";")
lines.append("\t\t};")
lines.append("/* End PBXNativeTarget section */\n")
lines.append("/* Begin PBXProject section */")
lines.append(f"\t\t{project_id} /* Project object */ = {{")
lines.append("\t\t\tisa = PBXProject;")
lines.append("\t\t\tattributes = {")
lines.append("\t\t\t\tBuildIndependentTargetsInParallel = 1;")
lines.append("\t\t\t\tLastSwiftUpdateCheck = 1500;")
lines.append("\t\t\t\tLastUpgradeCheck = 1500;")
lines.append("\t\t\t\tTargetAttributes = {")
lines.append(f"\t\t\t\t\t{target_id} = {{")
lines.append("\t\t\t\t\t\tCreatedOnToolsVersion = 15.0;")
lines.append("\t\t\t\t\t};")
lines.append("\t\t\t\t};")
lines.append("\t\t\t};")
lines.append(f"\t\t\tbuildConfigurationList = {project_config_list_id} /* Build configuration list for PBXProject \"{PROJECT_NAME}\" */;")
lines.append("\t\t\tcompatibilityVersion = \"Xcode 14.0\";")
lines.append("\t\t\tdevelopmentRegion = en;")
lines.append("\t\t\thasScannedForEncodings = 0;")
lines.append("\t\t\tknownRegions = (")
lines.append("\t\t\t\ten,")
lines.append("\t\t\t\tBase,")
lines.append("\t\t\t);")
lines.append(f"\t\t\tmainGroup = {main_group_id};")
lines.append(f"\t\t\tproductRefGroup = {products_group_id} /* Products */;")
lines.append("\t\t\tprojectDirPath = \"\";")
lines.append("\t\t\tprojectRoot = \"\";")
lines.append("\t\t\ttargets = (")
lines.append(f"\t\t\t\t{target_id} /* {PROJECT_NAME} */,")
lines.append("\t\t\t);")
lines.append("\t\t};")
lines.append("/* End PBXProject section */\n")
lines.append("/* Begin PBXResourcesBuildPhase section */")
lines.append(f"\t\t{resources_phase_id} /* Resources */ = {{")
lines.append("\t\t\tisa = PBXResourcesBuildPhase;")
lines.append("\t\t\tbuildActionMask = 2147483647;")
lines.append("\t\t\tfiles = (")
lines.append(f"\t\t\t\t{asset_build_id} /* Assets.xcassets in Resources */,")
lines.append("\t\t\t);")
lines.append("\t\t\trunOnlyForDeploymentPostprocessing = 0;")
lines.append("\t\t};")
lines.append("/* End PBXResourcesBuildPhase section */\n")
lines.append("/* Begin PBXSourcesBuildPhase section */")
lines.append(f"\t\t{sources_phase_id} /* Sources */ = {{")
lines.append("\t\t\tisa = PBXSourcesBuildPhase;")
lines.append("\t\t\tbuildActionMask = 2147483647;")
lines.append("\t\t\tfiles = (")
for build_id, _, name in swift_build_entries:
lines.append(f"\t\t\t\t{build_id} /* {name} in Sources */,")
lines.append("\t\t\t);")
lines.append("\t\t\trunOnlyForDeploymentPostprocessing = 0;")
lines.append("\t\t};")
lines.append("/* End PBXSourcesBuildPhase section */\n")
common_settings = [
("ASSETCATALOG_COMPILER_APPICON_NAME", "AppIcon"),
("CODE_SIGN_ENTITLEMENTS", "Resources/Bugger.entitlements"),
("CODE_SIGN_STYLE", "Automatic"),
("COMBINE_HIDPI_IMAGES", "YES"),
("CURRENT_PROJECT_VERSION", "1"),
("DEVELOPMENT_TEAM", ""),
("ENABLE_HARDENED_RUNTIME", "YES"),
("FEISHU_APP_ID", ""),
("FEISHU_APP_SECRET", ""),
("FEISHU_BASE_DOMAIN", "xorbitlab.feishu.cn"),
("GENERATE_INFOPLIST_FILE", "NO"),
("INFOPLIST_FILE", "Resources/Info.plist"),
("LD_RUNPATH_SEARCH_PATHS", "$(inherited) @executable_path/../Frameworks"),
("MACOSX_DEPLOYMENT_TARGET", "14.0"),
("MARKETING_VERSION", "1.0"),
("PRODUCT_BUNDLE_IDENTIFIER", BUNDLE_ID),
("PRODUCT_NAME", "$(TARGET_NAME)"),
("SWIFT_EMIT_LOC_STRINGS", "YES"),
("SWIFT_VERSION", "5.0"),
]
def config_block(config_id: str, name: str, settings: list[tuple[str, str]]) -> str:
setting_lines = "\n".join([f"\t\t\t\t{key} = {value};" for key, value in settings])
return (
f"\t\t{config_id} /* {name} */ = {{\n"
f"\t\t\tisa = XCBuildConfiguration;\n"
f"\t\t\tbuildSettings = {{\n{setting_lines}\n"
f"\t\t\t}};\n"
f"\t\t\tname = {name};\n"
f"\t\t}};"
)
lines.append("/* Begin XCBuildConfiguration section */")
lines.append(config_block(debug_config_id, "Debug", [("ALWAYS_SEARCH_USER_PATHS", "NO"), ("CLANG_ENABLE_MODULES", "YES"), ("COPY_PHASE_STRIP", "NO"), ("DEBUG_INFORMATION_FORMAT", "dwarf"), ("GCC_DYNAMIC_NO_PIC", "NO"), ("GCC_OPTIMIZATION_LEVEL", "0"), ("ONLY_ACTIVE_ARCH", "YES"), ("SDKROOT", "macosx"), ("SWIFT_ACTIVE_COMPILATION_CONDITIONS", "DEBUG"), ("SWIFT_OPTIMIZATION_LEVEL", "-Onone")]))
lines.append(config_block(release_config_id, "Release", [("ALWAYS_SEARCH_USER_PATHS", "NO"), ("CLANG_ENABLE_MODULES", "YES"), ("COPY_PHASE_STRIP", "NO"), ("DEBUG_INFORMATION_FORMAT", "dwarf-with-dsym"), ("SDKROOT", "macosx"), ("SWIFT_COMPILATION_MODE", "wholemodule")]))
lines.append(config_block(target_debug_config_id, "Debug", common_settings))
lines.append(config_block(target_release_config_id, "Release", common_settings))
lines.append("/* End XCBuildConfiguration section */\n")
lines.append("/* Begin XCConfigurationList section */")
lines.append(f"\t\t{project_config_list_id} /* Build configuration list for PBXProject \"{PROJECT_NAME}\" */ = {{")
lines.append("\t\t\tisa = XCConfigurationList;")
lines.append("\t\t\tbuildConfigurations = (")
lines.append(f"\t\t\t\t{debug_config_id} /* Debug */,")
lines.append(f"\t\t\t\t{release_config_id} /* Release */,")
lines.append("\t\t\t);")
lines.append("\t\t\tdefaultConfigurationIsVisible = 0;")
lines.append("\t\t\tdefaultConfigurationName = Release;")
lines.append("\t\t};")
lines.append(f"\t\t{target_config_list_id} /* Build configuration list for PBXNativeTarget \"{PROJECT_NAME}\" */ = {{")
lines.append("\t\t\tisa = XCConfigurationList;")
lines.append("\t\t\tbuildConfigurations = (")
lines.append(f"\t\t\t\t{target_debug_config_id} /* Debug */,")
lines.append(f"\t\t\t\t{target_release_config_id} /* Release */,")
lines.append("\t\t\t);")
lines.append("\t\t\tdefaultConfigurationIsVisible = 0;")
lines.append("\t\t\tdefaultConfigurationName = Release;")
lines.append("\t\t};")
lines.append("/* End XCConfigurationList section */")
lines.append("\t};")
lines.append(f"\trootObject = {project_id} /* Project object */;")
lines.append("}")
return "\n".join(lines)
def main():
project_dir = ROOT / f"{PROJECT_NAME}.xcodeproj"
project_dir.mkdir(exist_ok=True)
(project_dir / "project.pbxproj").write_text(pbxproj())
scheme_dir = project_dir / "xcshareddata" / "xcschemes"
scheme_dir.mkdir(parents=True, exist_ok=True)
(scheme_dir / f"{PROJECT_NAME}.xcscheme").write_text(f"""<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "{uuid('target')}"
BuildableName = "{PROJECT_NAME}.app"
BlueprintName = "{PROJECT_NAME}"
ReferencedContainer = "container:{PROJECT_NAME}.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "{uuid('target')}"
BuildableName = "{PROJECT_NAME}.app"
BlueprintName = "{PROJECT_NAME}"
ReferencedContainer = "container:{PROJECT_NAME}.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
</Scheme>
""")
print(f"Generated {project_dir}")
if __name__ == "__main__":
main()