This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
"""Generate a dependency-free Xcode project. Run with Python 3 from any directory."""
|
||||
import hashlib
|
||||
import json
|
||||
import pathlib
|
||||
import plistlib
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
objects = {}
|
||||
|
||||
def uid(name):
|
||||
return hashlib.sha256(name.encode()).hexdigest()[:24].upper()
|
||||
|
||||
def add(key_name, isa, **fields):
|
||||
key = uid(key_name)
|
||||
objects[key] = dict(isa=isa, **fields)
|
||||
return key
|
||||
|
||||
def configurations(name, settings):
|
||||
configs = []
|
||||
for mode in ("Debug", "Release"):
|
||||
values = dict(settings)
|
||||
values.update(SWIFT_OPTIMIZATION_LEVEL="-Onone" if mode == "Debug" else "-O")
|
||||
if mode == "Debug":
|
||||
values.update(ENABLE_TESTABILITY="YES", SWIFT_ACTIVE_COMPILATION_CONDITIONS="DEBUG")
|
||||
configs.append(add(name + mode, "XCBuildConfiguration", name=mode, buildSettings=values))
|
||||
return add(name + "configs", "XCConfigurationList", buildConfigurations=configs,
|
||||
defaultConfigurationIsVisible=0, defaultConfigurationName="Release")
|
||||
|
||||
def source_group(folder):
|
||||
refs, builds = [], []
|
||||
for path in sorted((ROOT / folder).glob("*.swift")):
|
||||
ref = add(path.relative_to(ROOT).as_posix(), "PBXFileReference", lastKnownFileType="sourcecode.swift", path=path.name, sourceTree="<group>")
|
||||
refs.append(ref)
|
||||
builds.append(add("build/" + path.relative_to(ROOT).as_posix(), "PBXBuildFile", fileRef=ref))
|
||||
return add(folder, "PBXGroup", path=folder, sourceTree="<group>", children=refs), builds
|
||||
|
||||
app_group, app_sources = source_group("MusicBridge")
|
||||
test_group, test_sources = source_group("MusicBridgeTests")
|
||||
fixture = add("fixture", "PBXFileReference", lastKnownFileType="text.json", path="state-v1.json", sourceTree="<group>")
|
||||
objects[test_group]["children"].append(fixture)
|
||||
fixture_build = add("fixture-build", "PBXBuildFile", fileRef=fixture)
|
||||
products = []
|
||||
targets = []
|
||||
for name, sources, is_test in [("MusicBridge", app_sources, False), ("MusicBridgeTests", test_sources, True)]:
|
||||
product = add(name + "product", "PBXFileReference", explicitFileType="wrapper.cfbundle" if is_test else "wrapper.application",
|
||||
includeInIndex=0, path=name + (".xctest" if is_test else ".app"), sourceTree="BUILT_PRODUCTS_DIR")
|
||||
products.append(product)
|
||||
phases = [add(name + "sources", "PBXSourcesBuildPhase", buildActionMask=2147483647, files=sources, runOnlyForDeploymentPostprocessing=0),
|
||||
add(name + "frameworks", "PBXFrameworksBuildPhase", buildActionMask=2147483647, files=[], runOnlyForDeploymentPostprocessing=0),
|
||||
add(name + "resources", "PBXResourcesBuildPhase", buildActionMask=2147483647, files=[fixture_build] if is_test else [], runOnlyForDeploymentPostprocessing=0)]
|
||||
settings = dict(PRODUCT_NAME="$(TARGET_NAME)", PRODUCT_BUNDLE_IDENTIFIER="ru.yukinoki.musicbridge" + (".tests" if is_test else ""),
|
||||
SWIFT_VERSION="5.0", IPHONEOS_DEPLOYMENT_TARGET="17.0", SDKROOT="iphoneos",
|
||||
SUPPORTED_PLATFORMS="iphoneos iphonesimulator", TARGETED_DEVICE_FAMILY="1", CODE_SIGN_STYLE="Automatic",
|
||||
GENERATE_INFOPLIST_FILE="YES" if is_test else "NO")
|
||||
deps = []
|
||||
if is_test:
|
||||
settings.update(TEST_HOST="$(BUILT_PRODUCTS_DIR)/MusicBridge.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MusicBridge", BUNDLE_LOADER="$(TEST_HOST)")
|
||||
proxy = add("test-proxy", "PBXContainerItemProxy", containerPortal=uid("project"), proxyType=1, remoteGlobalIDString=uid("MusicBridgetarget"), remoteInfo="MusicBridge")
|
||||
deps = [add("test-dependency", "PBXTargetDependency", target=uid("MusicBridgetarget"), targetProxy=proxy)]
|
||||
else:
|
||||
settings.update(INFOPLIST_FILE="MusicBridge/Info.plist", CURRENT_PROJECT_VERSION="1", MARKETING_VERSION="0.1.0")
|
||||
targets.append(add(name + "target", "PBXNativeTarget", name=name, productName=name, productReference=product,
|
||||
productType="com.apple.product-type.bundle.unit-test" if is_test else "com.apple.product-type.application",
|
||||
buildConfigurationList=configurations(name, settings), buildPhases=phases, buildRules=[], dependencies=deps))
|
||||
|
||||
product_group = add("products", "PBXGroup", name="Products", sourceTree="<group>", children=products)
|
||||
main_group = add("main", "PBXGroup", sourceTree="<group>", children=[app_group, test_group, product_group])
|
||||
project = add("project", "PBXProject", attributes=dict(LastUpgradeCheck="1600", BuildIndependentTargetsInParallel="YES"),
|
||||
buildConfigurationList=configurations("project", dict(CLANG_ENABLE_MODULES="YES", CLANG_ENABLE_OBJC_ARC="YES")),
|
||||
compatibilityVersion="Xcode 14.0", developmentRegion="ru", knownRegions=["ru", "en", "Base"],
|
||||
mainGroup=main_group, productRefGroup=product_group, projectDirPath="", projectRoot="", targets=targets)
|
||||
|
||||
def encode(value, indent=0):
|
||||
if isinstance(value, dict):
|
||||
return "{\n" + "".join("\t" * (indent + 1) + json.dumps(k) + " = " + encode(v, indent + 1) + ";\n" for k, v in value.items()) + "\t" * indent + "}"
|
||||
if isinstance(value, list):
|
||||
return "(" + ", ".join(encode(v, indent) for v in value) + ("," if value else "") + ")"
|
||||
return json.dumps(value, ensure_ascii=True)
|
||||
|
||||
project_dir = ROOT / "MusicBridge.xcodeproj"
|
||||
project_dir.mkdir(exist_ok=True)
|
||||
(project_dir / "project.pbxproj").write_bytes(("// !$*UTF8*$!\n" + encode(dict(archiveVersion=1, classes={}, objectVersion=56, objects=objects, rootObject=project)) + "\n").encode("utf-8"))
|
||||
scheme_dir = project_dir / "xcshareddata" / "xcschemes"
|
||||
scheme_dir.mkdir(parents=True, exist_ok=True)
|
||||
def reference(target, product):
|
||||
return f'<BuildableReference BuildableIdentifier="primary" BlueprintIdentifier="{uid(target + "target")}" BuildableName="{product}" BlueprintName="{target}" ReferencedContainer="container:MusicBridge.xcodeproj"/>'
|
||||
app_ref = reference("MusicBridge", "MusicBridge.app")
|
||||
test_ref = reference("MusicBridgeTests", "MusicBridgeTests.xctest")
|
||||
(scheme_dir / "MusicBridge.xcscheme").write_bytes(f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme LastUpgradeVersion="1600" version="1.3">
|
||||
<BuildAction parallelizeBuildables="YES" buildImplicitDependencies="YES"><BuildActionEntries>
|
||||
<BuildActionEntry buildForTesting="YES" buildForRunning="YES" buildForProfiling="YES" buildForArchiving="YES" buildForAnalyzing="YES">{app_ref}</BuildActionEntry>
|
||||
</BuildActionEntries></BuildAction>
|
||||
<TestAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.IDEFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv="YES"><Testables><TestableReference skipped="NO">{test_ref}</TestableReference></Testables></TestAction>
|
||||
<LaunchAction buildConfiguration="Debug" selectedDebuggerIdentifier="Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier="Xcode.IDEFoundation.Launcher.LLDB" launchStyle="0" useCustomWorkingDirectory="NO" ignoresPersistentStateOnLaunch="NO" debugDocumentVersioning="YES" debugServiceExtension="internal" allowLocationSimulation="YES"><BuildableProductRunnable runnableDebuggingMode="0">{app_ref}</BuildableProductRunnable></LaunchAction>
|
||||
<ProfileAction buildConfiguration="Release" shouldUseLaunchSchemeArgsEnv="YES" savedToolIdentifier="" useCustomWorkingDirectory="NO" debugDocumentVersioning="YES"><BuildableProductRunnable runnableDebuggingMode="0">{app_ref}</BuildableProductRunnable></ProfileAction>
|
||||
<AnalyzeAction buildConfiguration="Debug"/><ArchiveAction buildConfiguration="Release" revealArchiveInOrganizer="YES"/>
|
||||
</Scheme>'''.encode("utf-8"))
|
||||
|
||||
info = dict(CFBundleDevelopmentRegion="ru", CFBundleDisplayName="MusicBridge", CFBundleExecutable="$(EXECUTABLE_NAME)",
|
||||
CFBundleIdentifier="$(PRODUCT_BUNDLE_IDENTIFIER)", CFBundleInfoDictionaryVersion="6.0", CFBundleName="$(PRODUCT_NAME)",
|
||||
CFBundlePackageType="APPL", CFBundleShortVersionString="$(MARKETING_VERSION)", CFBundleVersion="$(CURRENT_PROJECT_VERSION)",
|
||||
LSRequiresIPhoneOS=True, UILaunchScreen={},
|
||||
UISupportedInterfaceOrientations=["UIInterfaceOrientationPortrait"],
|
||||
NSLocalNetworkUsageDescription="MusicBridge подключается к вашему ПК, чтобы показывать музыку и управлять воспроизведением.",
|
||||
NSAppTransportSecurity=dict(NSAllowsLocalNetworking=True))
|
||||
(ROOT / "MusicBridge" / "Info.plist").write_bytes(plistlib.dumps(info, sort_keys=False))
|
||||
print("Generated Xcode project, shared scheme and Info.plist.")
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Select an available iPhone from simctl JSON; print a GitHub environment entry."""
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
with open(sys.argv[1], encoding="utf-8") as stream:
|
||||
runtimes = json.load(stream)["devices"]
|
||||
for runtime in sorted(runtimes, reverse=True):
|
||||
if ".iOS-" not in runtime:
|
||||
continue
|
||||
for device in runtimes[runtime]:
|
||||
if device.get("isAvailable") and device.get("name", "").startswith("iPhone"):
|
||||
print("SIMULATOR_ID=" + str(uuid.UUID(device["udid"])))
|
||||
sys.exit(0)
|
||||
sys.exit("No available iPhone simulator in selected Xcode runtime.")
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Cross-platform structural checks only; this does not compile or test Swift."""
|
||||
import hashlib
|
||||
import json
|
||||
import pathlib
|
||||
import plistlib
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
root = pathlib.Path(__file__).resolve().parents[1]
|
||||
generated = [root / "MusicBridge.xcodeproj/project.pbxproj",
|
||||
root / "MusicBridge.xcodeproj/xcshareddata/xcschemes/MusicBridge.xcscheme",
|
||||
root / "MusicBridge/Info.plist"]
|
||||
before = [p.read_bytes() for p in generated]
|
||||
subprocess.run([sys.executable, str(root / "tools/generate_project.py")], check=True)
|
||||
assert before == [p.read_bytes() for p in generated], "Generated project is stale; review and commit regeneration."
|
||||
project = generated[0].read_text(encoding="utf-8")
|
||||
for folder in ("MusicBridge", "MusicBridgeTests"):
|
||||
for source in (root / folder).glob("*.swift"):
|
||||
key = hashlib.sha256(source.relative_to(root).as_posix().encode()).hexdigest()[:24].upper()
|
||||
assert key in project, f"Missing source: {source.name}"
|
||||
defined = set(re.findall(r'"([A-F0-9]{24})" = \{', project))
|
||||
referenced = set(re.findall(r'"([A-F0-9]{24})"', project))
|
||||
assert defined == referenced, "Project has dangling object references."
|
||||
info = plistlib.loads(generated[2].read_bytes())
|
||||
assert info["NSLocalNetworkUsageDescription"]
|
||||
assert info["NSAppTransportSecurity"] == {"NSAllowsLocalNetworking": True}
|
||||
ET.parse(generated[1])
|
||||
fixture = json.loads((root / "MusicBridgeTests/state-v1.json").read_text(encoding="utf-8"))
|
||||
assert fixture["protocolVersion"] == 1 and fixture["positionSeconds"] == 42.5
|
||||
assert set(fixture["capabilities"]) == {"play", "pause", "toggle", "next", "previous", "seek"}
|
||||
print("PASS: deterministic project, source membership, object references, plist, scheme XML, protocol fixture.")
|
||||
print("Swift compilation and XCTest must run on macOS/Xcode.")
|
||||
Reference in New Issue
Block a user