#!/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 = ""; }};', 2) L(f'{asset_ref} /* Assets.xcassets */ = {{isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }};', 2) L(f'{info_ref} /* Info.plist */ = {{isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }};', 2) L(f'{ent_ref} /* Bugger.entitlements */ = {{isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Bugger.entitlements; sourceTree = ""; }};', 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 = "";', 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 = "";', 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 = "";', 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 = "";', 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 = "";', 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""" """ 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( '\n' '\n' ' \n' ' \n' '\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()