m323/Sources/Aufgabe5/Aufgabe5.xcodeproj/rules_xcodeproj/bazel/calculate_output_groups.py
cediackermann b1968074b6
build: generate per-package and aggregate Xcode projects
Add rules_xcodeproj targets to each package BUILD plus a root m323
project, with extra_files filegroups so the full source tree shows in
Xcode. Ignore Scala/Metals build dirs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 14:00:40 +02:00

435 lines
14 KiB
Python
Executable file

#!/usr/bin/python3
import datetime
import glob
import json
import os
import sys
import time
import traceback
# Ordered the same as we order platforms in the generated Xcode project, except
# macOS is last
_DEVICE_PLATFORMS = {
"iphoneos": None,
"appletvos": None,
"watchos": None,
"macosx": None,
}
_SIMULATOR_PLATFORMS = {
"iphonesimulator": None,
"appletvsimulator": None,
"watchsimulator": None,
"macosx": None,
}
def _wait_for_value(calculate_value, value_name):
wait_counter = 0
while True:
value = calculate_value()
if value:
break
if wait_counter == 0:
now = datetime.datetime.now().strftime('%H:%M:%S')
print(
f"note: ({now}) {value_name} not updated yet, waiting...",
file = sys.stderr,
flush = True,
)
if wait_counter == 10:
now = datetime.datetime.now().strftime('%H:%M:%S')
print(
f"""\
warning: ({now}) {value_name} still not updated after 10 seconds. If happens \
frequently, or the cache is never created, please file a bug report here: \
https://github.com/MobileNativeFoundation/rules_xcodeproj/issues/new?template=bug.md""",
file = sys.stderr,
flush = True,
)
time.sleep(1)
wait_counter += 1
if wait_counter > 0:
now = datetime.datetime.now().strftime('%H:%M:%S')
print(
f"""\
note: ({now}) {value_name} updated after {wait_counter} seconds.""",
file = sys.stderr,
flush = True,
)
return value
def _get_build_request(
xcode_version,
objroot,
build_request_min_ctime):
if xcode_version < 1430:
# Before Xcode 14.3
def wait_for_description():
build_description_cache = max(
glob.iglob(
f"{objroot}/XCBuildData/BuildDescriptionCacheIndex-*",
),
key = os.path.getctime,
)
if (os.path.getctime(build_description_cache) >=
build_request_min_ctime):
return build_description_cache
return None
build_description_cache = (
_wait_for_value(wait_for_description, "BuildDescriptionCacheIndex")
)
with open(build_description_cache, 'rb') as f:
f.seek(-32, os.SEEK_END)
build_request_id = f.read().decode('ASCII')
build_request_file = (
f"{objroot}/XCBuildData/{build_request_id}-buildRequest.json"
)
def wait_for_build_request_file():
if os.path.exists(build_request_file):
with open(build_request_file, encoding = "utf-8") as f:
# Parse the build-request.json file
try:
return json.load(f)
except Exception as error:
print(
f"""\
error: Failed to parse '{build_request_file}':
{type(error).__name__}: {error}.
Please file a bug report here: \
https://github.com/MobileNativeFoundation/rules_xcodeproj/issues/new?template=bug.md""",
file = sys.stderr,
)
exit(1)
return None
return _wait_for_value(
wait_for_build_request_file,
f"\"{build_request_file}\"",
)
def wait_for_build_request():
try:
xcbuilddata = max(
glob.iglob(f"{objroot}/XCBuildData/*.xcbuilddata"),
key = os.path.getctime,
)
if os.path.getctime(xcbuilddata) < build_request_min_ctime:
return None
except (ValueError, FileNotFoundError, OSError):
# ValueError: no .xcbuilddata files yet (empty glob)
# FileNotFoundError/OSError: TOCTOU race - file deleted between glob and getctime
return None
build_request_file = f"{xcbuilddata}/build-request.json"
if os.path.exists(build_request_file):
with open(build_request_file, encoding = "utf-8") as f:
# Parse the build-request.json file
try:
return json.load(f)
except Exception as error:
print(
f"""\
error: Failed to parse '{build_request_file}':
{type(error).__name__}: {error}.
Please file a bug report here: \
https://github.com/MobileNativeFoundation/rules_xcodeproj/issues/new?template=bug.md""",
file = sys.stderr,
)
sys.exit(1)
return None
return _wait_for_value(
wait_for_build_request,
"newest 'buildRequest.json' file",
)
def _calculate_label_and_target_ids(
build_request,
guid_labels,
guid_target_ids):
# Xcode gets "stuck" in the `buildFiles` or `build` command for
# top-level targets, so we can't reliably change commands here. Leaving
# the code in place in case this is fixed in the future, or we want to
# do something similar in an XCBBuildService proxy.
#
# command = (
# build_request.get("_buildCommand2", {}).get("command", "build")
# )
command = "build"
parameters = build_request["parameters"]
platform = (
parameters["activeRunDestination"]["platform"]
)
configuration_name = parameters["configurationName"]
labels_and_target_ids = []
for target in build_request["configuredTargets"]:
label = guid_labels.get(target["guid"])
if not label:
# `BazelDependency` and the like
continue
full_target_target_ids = guid_target_ids[target["guid"]]
target_target_ids = (
full_target_target_ids.get(command) or
# Will only be `null` if `command == "buildFiles"` and there
# isn't a different compile target id
full_target_target_ids["build"]
)
target_ids = _select_target_ids(
target_target_ids[configuration_name],
platform,
)
for target_id in target_ids:
labels_and_target_ids.append((label, target_id))
if not labels_and_target_ids:
print(
"""\
error: Failed to determine labels and targets. Note, currently `.xcworkspace`s \
aren't supported. Please make sure you are opening the generated \
`.xcodeproj` file bundle directly. If you are, try using the "Clean Build \
Folder" command instead (⇧ ⌘ K). If you still get this error after that, then \
please file a bug report here: \
https://github.com/MobileNativeFoundation/rules_xcodeproj/issues/new?template=bug.md""",
file = sys.stderr,
)
exit(1)
return labels_and_target_ids
def _calculate_guid_labels_and_target_ids(base_objroot):
pif_cache = f"{base_objroot}/XCBuildData/PIFCache"
project_cache = f"{pif_cache}/project"
target_cache = f"{pif_cache}/target"
# The PIF cache will only be created before the `SetSessionUserInfo`
# command, which normally happens when a project is opened. If Derived Data
# is cleared while the project is open
if not (os.path.exists(project_cache) and os.path.exists(target_cache)):
print(
f"""\
error: PIFCache ({pif_cache}) doesn't exist. If you manually cleared Derived \
Data, you need to close and re-open the project for the PIFCache to be created \
again. Using the "Clean Build Folder" command instead (⇧ ⌘ K) won't trigger \
this error. If this error still happens after re-opening the project, please \
file a bug report here: \
https://github.com/MobileNativeFoundation/rules_xcodeproj/issues/new?template=bug.md""",
file = sys.stderr,
)
sys.exit(1)
project_pif = max(
glob.iglob(f"{project_cache}/*"),
key = os.path.getctime,
)
guid_payload_parent = f"{base_objroot}/guid_payload"
guid_payload_path = f"""\
{guid_payload_parent}/{os.path.basename(project_pif)}_v2.json"""
if os.path.exists(guid_payload_path):
with open(guid_payload_path, encoding = "utf-8") as f:
payload = json.load(f)
return payload["labels"], payload["targetIds"]
with open(project_pif, encoding = "utf-8") as f:
project_pif = json.load(f)
targets = project_pif["targets"]
guid_labels = {}
guid_target_ids = {}
for target_name in targets:
target_file = f"{target_cache}/{target_name}-json"
with open(target_file, encoding = "utf-8") as f:
target_pif = json.load(f)
label = None
build_target_ids = {}
compile_target_ids = {}
for configuration in target_pif["buildConfigurations"]:
config_build_target_ids = {"key": "BAZEL_TARGET_ID"}
config_compile_target_ids = {"key": "BAZEL_COMPILE_TARGET_IDS"}
for key, value in configuration["buildSettings"].items():
if key.startswith("BAZEL_TARGET_ID"):
# This uses a list in the case where the value isn't meant to
# be inherited. Otherwise, `$(BAZEL_TARGET_ID)` is used later
# in this processing to pull from an inherited setting.
if value == "$(BAZEL_TARGET_ID)":
target_ids = value
else:
# This is only a single value but the later parsing of
# these target ids assumes a list.
target_ids = [value]
config_build_target_ids[_platform_from_build_key(key)] = (
target_ids
)
elif key.startswith("BAZEL_COMPILE_TARGET_IDS"):
# This uses a list in the case where the value isn't meant to
# be inherited. Otherwise, `$(BAZEL_COMPILE_TARGET_IDS)` is
# used later in this processing to pull from an inherited
# setting.
if value == "$(BAZEL_COMPILE_TARGET_IDS)":
target_ids = "$(BAZEL_COMPILE_TARGET_IDS)"
else:
# Target identifiers contain a space but are space separated.
# Split on all spaces then rejoin across the identifier pairs.
target_ids = value.split(" ")
target_ids = [
" ".join(target_ids[i:i+2])
for i in range(0, len(target_ids), 2)
]
config_compile_target_ids[_platform_from_compile_key(key)] = (
target_ids
)
elif key == "BAZEL_LABEL":
label = value
configuration_name = configuration["name"]
build_target_ids[configuration_name] = config_build_target_ids
compile_target_ids[configuration_name] = config_compile_target_ids
if not label:
# `BazelDependency` and the like
continue
target_ids = {
"build": build_target_ids,
}
if len(compile_target_ids) > 1:
target_ids["buildFiles"] = compile_target_ids
guid = target_pif["guid"]
guid_labels[guid] = label
guid_target_ids[guid] = target_ids
os.makedirs(guid_payload_parent, exist_ok = True)
with open(guid_payload_path, "w", encoding = "utf-8") as f:
payload = {
"labels": guid_labels,
"targetIds": guid_target_ids,
}
json.dump(payload, f)
return guid_labels, guid_target_ids
def _platform_from_build_key(key):
if key.startswith("BAZEL_TARGET_ID[sdk="):
return key[20:-2]
return ""
def _platform_from_compile_key(key):
if key.startswith("BAZEL_COMPILE_TARGET_IDS[sdk="):
return key[29:-2]
return ""
def _select_target_ids(target_ids, platform):
key = target_ids["key"]
platforms = {platform: None}
# We need to try other similar platforms (i.e. other simulator platforms if
# `platform`` is for a simulator). This is to support schemes with targets
# of multiple platforms in them. Because `dict` is insertion ordered,
# `platform` will be checked first.
platforms.update(_similar_platforms(platform))
for platform in platforms:
platform_target_ids = target_ids.get(platform)
if platform_target_ids:
if platform_target_ids == f"$({key})":
return target_ids[""]
return platform_target_ids
return target_ids[""]
def _similar_platforms(platform):
if platform == "macosx" or "simulator" in platform:
return _SIMULATOR_PLATFORMS
return _DEVICE_PLATFORMS
def _main(
xcode_version,
objroot,
base_objroot,
marker_file,
prefixes_str):
if not os.path.exists(marker_file):
return
build_request_min_ctime = os.path.getctime(marker_file)
try:
xcode_version = int(xcode_version)
except ValueError:
print(
"""
warning: xcode_version was not an integer. Please file a bug report here: \
https://github.com/MobileNativeFoundation/rules_xcodeproj/issues/new?template=bug.md
""",
file = sys.stderr,
)
xcode_version = 9999
prefixes = prefixes_str.split(",")
try:
build_request = _get_build_request(
xcode_version,
objroot,
build_request_min_ctime,
)
guid_labels, guid_target_ids = (
_calculate_guid_labels_and_target_ids(base_objroot)
)
except Exception:
print(
f"""\
error: Failed to calculate labels and target ids from PIFCache:
{traceback.format_exc()}
Please file a bug report here: \
https://github.com/MobileNativeFoundation/rules_xcodeproj/issues/new?template=bug.md""",
file = sys.stderr,
)
sys.exit(1)
labels_and_target_ids = _calculate_label_and_target_ids(
build_request,
guid_labels,
guid_target_ids,
)
print("\n".join(
[
f"{label}\n{prefix} {id}"
for label, id in labels_and_target_ids
for prefix in prefixes
],
))
if __name__ == "__main__":
_main(
# XCODE_VERSION_ACTUAL
sys.argv[1],
# non_preview_objroot
sys.argv[2],
# base_objroot
sys.argv[3],
# build_marker_file
sys.argv[4],
# output_group_prefixes
sys.argv[5],
)