Add native SwiftUI player and macOS CI
iOS build and tests / simulator (push) Has been cancelled

This commit is contained in:
2026-09-10 09:11:55 +03:00
parent 6307070194
commit 6174be5610
20 changed files with 1729 additions and 0 deletions
+108
View File
@@ -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.")
+15
View File
@@ -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.")
+34
View File
@@ -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.")