TM-SGNL-iOS/Scripts/sds_codegen/sds_parse_swift_bridging.py
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

222 lines
6.1 KiB
Python
Executable file

#!/usr/bin/env python3
import os
import sys
import subprocess
import datetime
import argparse
import re
import json
import sds_common
from sds_common import fail
import tempfile
import shutil
# We need to generate fake -Swift.h bridging headers that declare the Swift
# types that our Objective-C files might use. This script does that.
def ows_getoutput(cmd):
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
stdout, stderr = proc.communicate()
return proc.returncode, stdout, stderr
class Namespace:
def __init__(self):
self.swift_protocol_names = []
self.swift_class_names = []
def parse_swift_ast(file_path, namespace, ast):
json_data = json.loads(ast)
json_maps = json_data.get("key.substructure")
if json_maps is None:
return
for json_map in json_maps:
kind = json_map.get("key.kind")
if kind is None:
continue
elif kind == "source.lang.swift.decl.protocol":
# "key.kind" : "source.lang.swift.decl.protocol",
# "key.length" : 1067,
# "key.name" : "TypingIndicators",
# "key.namelength" : 16,
# "key.nameoffset" : 135,
# "key.offset" : 126,
# "key.runtime_name" : "OWSTypingIndicators",
name = json_map.get("key.runtime_name")
if name is None or len(name) < 1 or name.startswith("_"):
name = json_map.get("key.name")
if name is None or len(name) < 1:
fail("protocol is missing name.")
continue
if name.startswith("_"):
continue
namespace.swift_protocol_names.append(name)
elif kind == "source.lang.swift.decl.class":
# "key.kind" : "source.lang.swift.decl.class",
# "key.length" : 15057,
# "key.name" : "TypingIndicatorsImpl",
# "key.namelength" : 20,
# "key.nameoffset" : 1251,
# "key.offset" : 1245,
# "key.runtime_name" : "OWSTypingIndicatorsImpl",
name = json_map.get("key.runtime_name")
if name is None or len(name) < 1 or name.startswith("_"):
name = json_map.get("key.name")
if name is None or len(name) < 1:
fail("class is missing name.")
continue
if name.startswith("_"):
continue
namespace.swift_class_names.append(name)
def process_file(file_path, namespace):
filename = os.path.basename(file_path)
if not filename.endswith(".swift"):
return
if filename == "EmojiWithSkinTones+String.swift":
return
command = ["sourcekitten", "structure", "--file", file_path]
# for part in command:
# print '\t', part
# command = ' '.join(command).strip()
# print 'command', command
# output = commands.getoutput(command)
# command = ' '.join(command).strip()
# print 'command', command
exit_code, output, error_output = ows_getoutput(command)
if exit_code != 0:
print("exit_code:", exit_code)
fail("Are you missing sourcekitten? Install with homebrew?")
if len(error_output.strip()) > 0:
print("error_output:", error_output)
# print 'output:', len(output)
# exit(1)
output = output.strip()
# print 'output', output
parse_swift_ast(file_path, namespace, output)
def generate_swift_bridging_header(namespace, swift_bridging_path):
output = []
for name in namespace.swift_protocol_names:
output.append(
"""
@protocol %s
@end
"""
% (name,)
)
for name in namespace.swift_class_names:
output.append(
"""
@interface %s : NSObject
@end
"""
% (name,)
)
output = "\n".join(output).strip()
if len(output) < 1:
return
header = """//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
#import <Foundation/Foundation.h>
// NOTE: This file is generated by %s.
// Do not manually edit it, instead run `sds_codegen.sh`.
""" % (
sds_common.pretty_module_path(__file__),
)
output = (header + output).strip()
# print 'output:', output[:500]
output = sds_common.clean_up_generated_swift(output)
# print 'output:', output[:500]
# print 'output', output
parent_dir_path = os.path.dirname(swift_bridging_path)
# print 'parent_dir_path', parent_dir_path
if not os.path.exists(parent_dir_path):
os.makedirs(parent_dir_path)
print("Writing:", swift_bridging_path)
with open(swift_bridging_path, "wt") as f:
f.write(output)
# ---
def process_dir(src_dir_path, dir_name, dst_dir_path):
namespace = Namespace()
dir_path = os.path.abspath(os.path.join(src_dir_path, dir_name))
file_paths = []
for rootdir, dirnames, filenames in os.walk(dir_path):
for filename in filenames:
file_path = os.path.abspath(os.path.join(rootdir, filename))
file_paths.append(file_path)
print(f"Found {len(file_paths)} files in {dir_path}")
for idx, file_path in enumerate(file_paths):
process_file(file_path, namespace)
if idx % 100 == 99:
print(f"... {idx+1} / {len(file_paths)}")
bridging_header_path = os.path.abspath(
os.path.join(dst_dir_path, dir_name, dir_name + "-Swift.h")
)
generate_swift_bridging_header(namespace, bridging_header_path)
# ---
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Parse Objective-C AST.")
parser.add_argument(
"--src-path", required=True, help="used to specify a path to process."
)
parser.add_argument(
"--swift-bridging-path",
required=True,
help="used to specify a path to process.",
)
args = parser.parse_args()
src_dir_path = os.path.abspath(args.src_path)
swift_bridging_path = os.path.abspath(args.swift_bridging_path)
if os.path.exists(swift_bridging_path):
shutil.rmtree(swift_bridging_path)
process_dir(src_dir_path, "SignalServiceKit", swift_bridging_path)