bugger/scripts/generate_xcode_project.py

491 lines
16 KiB
Python

#!/usr/bin/env python3
"""Generate a valid Bugger.xcodeproj from the 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"
PROJECT_DIR = ROOT / f"{PROJECT_NAME}.xcodeproj"
def uuid(seed: str) -> str:
return hashlib.md5(seed.encode()).hexdigest().upper()[:24]
def quote_path(name: str) -> str:
"""Quote a path value if it contains characters unsafe in NeXTSTEP plist format.
In NeXTSTEP plist, unquoted strings can only contain [a-zA-Z0-9_./$-].
Characters like +, -, *, etc. act as operators and must be quoted.
"""
safe_chars = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_./$-")
if all(c in safe_chars for c in name):
return name
return f'"{name}"'
def collect_swift_files() -> dict[str, str]:
"""Return {relative_path_in_sources: absolute_path} for all .swift files."""
files = {}
for path in sorted(SOURCE_ROOT.rglob("*.swift")):
rel = str(path.relative_to(SOURCE_ROOT))
files[rel] = str(path)
return files
def build_pbxproj() -> str:
swift_files = collect_swift_files()
sorted_files = sorted(swift_files.keys())
# ---- UUIDs ----
# File references
f_uuids = {f: uuid(f"fr-{f}") for f in sorted_files}
b_uuids = {f: uuid(f"bf-{f}") for f in sorted_files}
# Resources
asset_ref = uuid("res-assets")
info_ref = uuid("res-info-plist")
ent_ref = uuid("res-entitlements")
asset_build = uuid("res-build-assets")
# Product
product_ref = uuid("product-ref")
# Build phases
frameworks_phase = uuid("fw-phase")
resources_phase = uuid("res-phase")
sources_phase = uuid("src-phase")
# Target
target_id = uuid("target")
target_cfg_list = uuid("targ-cfg-list")
target_dbg = uuid("targ-dbg")
target_rel = uuid("targ-rel")
# Project
project_id = uuid("project")
project_cfg_list = uuid("proj-cfg-list")
project_dbg = uuid("proj-dbg")
project_rel = uuid("proj-rel")
# Groups
root_group = uuid("root-group")
sources_group = uuid("sources-group")
products_group = uuid("products-group")
resources_group = uuid("resources-group")
# Build group hierarchy from directory structure
all_groups = set()
for f in sorted_files:
parts = f.split("/")[:-1]
for i in range(len(parts)):
all_groups.add("/".join(parts[:i + 1]))
sorted_groups = sorted(all_groups, key=lambda g: (g.count("/"), g))
group_uuids = {g: uuid(f"grp-{g}") for g in sorted_groups}
# Determine which files/children go where
top_files = [f for f in sorted_files if "/" not in f]
top_dirs = {g for g in sorted_groups if "/" not in g}
def dir_files(group: str) -> list[str]:
"""Files directly inside this group directory."""
prefix = group + "/"
return sorted(f for f in sorted_files
if f.startswith(prefix) and "/" not in f[len(prefix):])
def subdirs(group: str) -> list[str]:
"""Subdirectory groups directly under this group."""
prefix = group + "/"
return sorted(sg for sg in sorted_groups
if sg.startswith(prefix) and "/" not in sg[len(prefix):])
# ---- Build output ----
b: list[str] = []
def L(line: str = "", indent: int = 0):
b.append("\t" * indent + line)
L("// !$*UTF8*$!")
L("{")
L("archiveVersion = 1;", 1)
L("classes = {", 1)
L("};", 1)
L("objectVersion = 56;", 1)
L("objects = {", 1)
L()
# -- PBXBuildFile --
L("/* Begin PBXBuildFile section */")
for f in sorted_files:
name = f.split("/")[-1]
L(f'{b_uuids[f]} /* {name} in Sources */ = {{isa = PBXBuildFile; fileRef = {f_uuids[f]}; }};', 2)
L(f'{asset_build} /* Assets.xcassets in Resources */ = {{isa = PBXBuildFile; fileRef = {asset_ref}; }};', 2)
L("/* End PBXBuildFile section */")
L()
# -- PBXFileReference --
L("/* Begin PBXFileReference section */")
L(f'{product_ref} /* {PROJECT_NAME}.app */ = {{isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = {PROJECT_NAME}.app; sourceTree = BUILT_PRODUCTS_DIR; }};', 2)
for f in sorted_files:
name = f.split("/")[-1]
L(f'{f_uuids[f]} /* {name} */ = {{isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = {quote_path(name)}; sourceTree = "<group>"; }};', 2)
L(f'{asset_ref} /* Assets.xcassets */ = {{isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }};', 2)
L(f'{info_ref} /* Info.plist */ = {{isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }};', 2)
L(f'{ent_ref} /* Bugger.entitlements */ = {{isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Bugger.entitlements; sourceTree = "<group>"; }};', 2)
L("/* End PBXFileReference section */")
L()
# -- PBXFrameworksBuildPhase --
L("/* Begin PBXFrameworksBuildPhase section */")
L(f'{frameworks_phase} /* Frameworks */ = {{', 2)
L("isa = PBXFrameworksBuildPhase;", 3)
L("buildActionMask = 2147483647;", 3)
L("files = (", 3)
L(");", 3)
L("runOnlyForDeploymentPostprocessing = 0;", 3)
L("};", 2)
L("/* End PBXFrameworksBuildPhase section */")
L()
# -- PBXGroup --
L("/* Begin PBXGroup section */")
# Root group
L(f'{root_group} /* Root */ = {{', 2)
L("isa = PBXGroup;", 3)
L("children = (", 3)
L(f'{sources_group} /* Sources */,', 4)
L(f'{resources_group} /* Resources */,', 4)
L(f'{products_group} /* Products */,', 4)
L(");", 3)
L('sourceTree = "<group>";', 3)
L("};", 2)
# Sources group
L(f'{sources_group} /* Sources */ = {{', 2)
L("isa = PBXGroup;", 3)
L("children = (", 3)
for f in top_files:
L(f'{f_uuids[f]} /* {f} */,', 4)
for d in sorted(top_dirs):
L(f'{group_uuids[d]} /* {d} */,', 4)
L(");", 3)
L("path = Sources;", 3)
L('sourceTree = "<group>";', 3)
L("};", 2)
# Subdirectory groups
for g in sorted_groups:
name = g.split("/")[-1]
L(f'{group_uuids[g]} /* {name} */ = {{', 2)
L("isa = PBXGroup;", 3)
L("children = (", 3)
for f in dir_files(g):
L(f'{f_uuids[f]} /* {f.split("/")[-1]} */,', 4)
for sg in subdirs(g):
L(f'{group_uuids[sg]} /* {sg.split("/")[-1]} */,', 4)
L(");", 3)
L(f"path = {name};", 3)
L('sourceTree = "<group>";', 3)
L("};", 2)
# Resources group
L(f'{resources_group} /* Resources */ = {{', 2)
L("isa = PBXGroup;", 3)
L("children = (", 3)
L(f'{asset_ref} /* Assets.xcassets */,', 4)
L(f'{info_ref} /* Info.plist */,', 4)
L(f'{ent_ref} /* Bugger.entitlements */,', 4)
L(");", 3)
L("path = Resources;", 3)
L('sourceTree = "<group>";', 3)
L("};", 2)
# Products group
L(f'{products_group} /* Products */ = {{', 2)
L("isa = PBXGroup;", 3)
L("children = (", 3)
L(f'{product_ref} /* {PROJECT_NAME}.app */,', 4)
L(");", 3)
L("name = Products;", 3)
L('sourceTree = "<group>";', 3)
L("};", 2)
L("/* End PBXGroup section */")
L()
# -- PBXNativeTarget --
L("/* Begin PBXNativeTarget section */")
L(f'{target_id} /* {PROJECT_NAME} */ = {{', 2)
L("isa = PBXNativeTarget;", 3)
L(f'buildConfigurationList = {target_cfg_list} /* Build configuration list for PBXNativeTarget "{PROJECT_NAME}" */;', 3)
L("buildPhases = (", 3)
L(f'{sources_phase} /* Sources */,', 4)
L(f'{frameworks_phase} /* Frameworks */,', 4)
L(f'{resources_phase} /* Resources */,', 4)
L(");", 3)
L("buildRules = (", 3)
L(");", 3)
L("dependencies = (", 3)
L(");", 3)
L(f"name = {PROJECT_NAME};", 3)
L(f"productName = {PROJECT_NAME};", 3)
L(f"productReference = {product_ref} /* {PROJECT_NAME}.app */;", 3)
L('productType = "com.apple.product-type.application";', 3)
L("};", 2)
L("/* End PBXNativeTarget section */")
L()
# -- PBXProject --
L("/* Begin PBXProject section */")
L(f'{project_id} /* Project object */ = {{', 2)
L("isa = PBXProject;", 3)
L("attributes = {", 3)
L("BuildIndependentTargetsInParallel = 1;", 4)
L("LastSwiftUpdateCheck = 2600;", 4)
L("LastUpgradeCheck = 2600;", 4)
L(f"TargetAttributes = {{", 4)
L(f"{target_id} = {{", 5)
L("CreatedOnToolsVersion = 26.0;", 6)
L("};", 5)
L("};", 4)
L("};", 3)
L(f'buildConfigurationList = {project_cfg_list} /* Build configuration list for PBXProject "{PROJECT_NAME}" */;', 3)
L('compatibilityVersion = "Xcode 14.0";', 3)
L("developmentRegion = en;", 3)
L("hasScannedForEncodings = 0;", 3)
L("knownRegions = (", 3)
L("en,", 4)
L("Base,", 4)
L(");", 3)
L(f"mainGroup = {root_group};", 3)
L(f"productRefGroup = {products_group} /* Products */;", 3)
L('projectDirPath = "";', 3)
L('projectRoot = "";', 3)
L("targets = (", 3)
L(f'{target_id} /* {PROJECT_NAME} */,', 4)
L(");", 3)
L("};", 2)
L("/* End PBXProject section */")
L()
# -- PBXResourcesBuildPhase --
L("/* Begin PBXResourcesBuildPhase section */")
L(f'{resources_phase} /* Resources */ = {{', 2)
L("isa = PBXResourcesBuildPhase;", 3)
L("buildActionMask = 2147483647;", 3)
L("files = (", 3)
L(f'{asset_build} /* Assets.xcassets in Resources */,', 4)
L(");", 3)
L("runOnlyForDeploymentPostprocessing = 0;", 3)
L("};", 2)
L("/* End PBXResourcesBuildPhase section */")
L()
# -- PBXSourcesBuildPhase --
L("/* Begin PBXSourcesBuildPhase section */")
L(f'{sources_phase} /* Sources */ = {{', 2)
L("isa = PBXSourcesBuildPhase;", 3)
L("buildActionMask = 2147483647;", 3)
L("files = (", 3)
for f in sorted_files:
name = f.split("/")[-1]
L(f'{b_uuids[f]} /* {name} in Sources */,', 4)
L(");", 3)
L("runOnlyForDeploymentPostprocessing = 0;", 3)
L("};", 2)
L("/* End PBXSourcesBuildPhase section */")
L()
# -- XCBuildConfiguration --
L("/* Begin XCBuildConfiguration section */")
# Project Debug
L(f'{project_dbg} /* Debug */ = {{', 2)
L("isa = XCBuildConfiguration;", 3)
L("buildSettings = {", 3)
L("ALWAYS_SEARCH_USER_PATHS = NO;", 4)
L("CLANG_ENABLE_MODULES = YES;", 4)
L("DEBUG_INFORMATION_FORMAT = dwarf;", 4)
L("SDKROOT = macosx;", 4)
L("SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;", 4)
L('SWIFT_OPTIMIZATION_LEVEL = "-Onone";', 4)
L("};", 3)
L("name = Debug;", 3)
L("};", 2)
# Project Release
L(f'{project_rel} /* Release */ = {{', 2)
L("isa = XCBuildConfiguration;", 3)
L("buildSettings = {", 3)
L("ALWAYS_SEARCH_USER_PATHS = NO;", 4)
L("CLANG_ENABLE_MODULES = YES;", 4)
L('DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";', 4)
L("SDKROOT = macosx;", 4)
L("SWIFT_COMPILATION_MODE = wholemodule;", 4)
L("};", 3)
L("name = Release;", 3)
L("};", 2)
# Target Debug
target_settings = [
("ASSETCATALOG_COMPILER_APPICON_NAME", "AppIcon"),
("COMBINE_HIDPI_IMAGES", "YES"),
("CURRENT_PROJECT_VERSION", "1"),
("FEISHU_APP_ID", '"$(FEISHU_APP_ID)"'),
("FEISHU_APP_SECRET", '"$(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"),
]
L(f'{target_dbg} /* Debug */ = {{', 2)
L("isa = XCBuildConfiguration;", 3)
L("buildSettings = {", 3)
for k, v in target_settings:
L(f"{k} = {v};", 4)
L("};", 3)
L("name = Debug;", 3)
L("};", 2)
# Target Release
L(f'{target_rel} /* Release */ = {{', 2)
L("isa = XCBuildConfiguration;", 3)
L("buildSettings = {", 3)
for k, v in target_settings:
L(f"{k} = {v};", 4)
L("};", 3)
L("name = Release;", 3)
L("};", 2)
L("/* End XCBuildConfiguration section */")
L()
# -- XCConfigurationList --
L("/* Begin XCConfigurationList section */")
L(f'{target_cfg_list} /* Build configuration list for PBXNativeTarget "{PROJECT_NAME}" */ = {{', 2)
L("isa = XCConfigurationList;", 3)
L("buildConfigurations = (", 3)
L(f'{target_dbg} /* Debug */,', 4)
L(f'{target_rel} /* Release */,', 4)
L(");", 3)
L("defaultConfigurationIsVisible = 0;", 3)
L("defaultConfigurationName = Release;", 3)
L("};", 2)
L(f'{project_cfg_list} /* Build configuration list for PBXProject "{PROJECT_NAME}" */ = {{', 2)
L("isa = XCConfigurationList;", 3)
L("buildConfigurations = (", 3)
L(f'{project_dbg} /* Debug */,', 4)
L(f'{project_rel} /* Release */,', 4)
L(");", 3)
L("defaultConfigurationIsVisible = 0;", 3)
L("defaultConfigurationName = Release;", 3)
L("};", 2)
L("/* End XCConfigurationList section */")
L()
L("};", 1)
L(f"rootObject = {project_id} /* Project object */;", 1)
L("}")
return "\n".join(b) + "\n"
def build_xcscheme() -> str:
target_id = uuid("target")
return f"""<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "{target_id}"
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 = "{target_id}"
BuildableName = "{PROJECT_NAME}.app"
BlueprintName = "{PROJECT_NAME}"
ReferencedContainer = "container:{PROJECT_NAME}.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
</Scheme>
"""
def main():
PROJECT_DIR.mkdir(parents=True, exist_ok=True)
# pbxproj
pbxproj = build_pbxproj()
(PROJECT_DIR / "project.pbxproj").write_text(pbxproj)
print(f"Wrote {PROJECT_DIR / 'project.pbxproj'} ({len(pbxproj)} bytes)")
# Workspace
ws_dir = PROJECT_DIR / "project.xcworkspace"
ws_dir.mkdir(parents=True, exist_ok=True)
(ws_dir / "contents.xcworkspacedata").write_text(
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<Workspace\n'
' version = "1.0">\n'
' <FileRef\n'
' location = "self:">\n'
' </FileRef>\n'
'</Workspace>\n'
)
# Scheme
scheme_dir = PROJECT_DIR / "xcshareddata" / "xcschemes"
scheme_dir.mkdir(parents=True, exist_ok=True)
(scheme_dir / f"{PROJECT_NAME}.xcscheme").write_text(build_xcscheme())
print("Generated project successfully.")
print(f" Sources: {len(collect_swift_files())} Swift files")
if __name__ == "__main__":
main()