495 lines
16 KiB
Python
495 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"),
|
|
("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_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()
|