mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-09-22 12:20:43 +02:00
chore: migrate to new version + fixed several critical bugs
- Migrated project to latest Telegram iOS base (v12.3.2+) - Fixed circular dependency between GhostModeManager and MiscSettingsManager - Fixed multiple Bazel build configuration errors (select() default conditions) - Fixed duplicate type definitions in PeerInfoScreen - Fixed swiftmodule directory resolution in build scripts - Added Ghostgram Settings tab in main Settings menu with all 5 features - Cleared sensitive credentials from config.json (template-only now) - Excluded bazel-cache from version control
This commit is contained in:
@@ -5,40 +5,165 @@ import shutil
|
||||
import tempfile
|
||||
import plistlib
|
||||
import argparse
|
||||
import subprocess
|
||||
import base64
|
||||
|
||||
from BuildEnvironment import run_executable_with_output, check_run_system
|
||||
|
||||
|
||||
def get_certificate_base64():
|
||||
certificate_data = run_executable_with_output('security', arguments=['find-certificate', '-c', 'Apple Distribution: Telegram FZ-LLC (C67CF9S4VU)', '-p'])
|
||||
certificate_data = certificate_data.replace('-----BEGIN CERTIFICATE-----', '')
|
||||
certificate_data = certificate_data.replace('-----END CERTIFICATE-----', '')
|
||||
certificate_data = certificate_data.replace('\n', '')
|
||||
return certificate_data
|
||||
def setup_temp_keychain(p12_path, p12_password=''):
|
||||
"""Create a temporary keychain and import the p12 certificate."""
|
||||
keychain_name = 'generate-profiles-temp.keychain'
|
||||
keychain_password = 'temp123'
|
||||
|
||||
# Delete if exists
|
||||
run_executable_with_output('security', arguments=['delete-keychain', keychain_name], check_result=False)
|
||||
|
||||
# Create keychain
|
||||
run_executable_with_output('security', arguments=[
|
||||
'create-keychain', '-p', keychain_password, keychain_name
|
||||
], check_result=True)
|
||||
|
||||
# Add to search list
|
||||
existing = run_executable_with_output('security', arguments=['list-keychains', '-d', 'user'])
|
||||
run_executable_with_output('security', arguments=[
|
||||
'list-keychains', '-d', 'user', '-s', keychain_name, existing.replace('"', '')
|
||||
], check_result=True)
|
||||
|
||||
# Unlock and set settings
|
||||
run_executable_with_output('security', arguments=['set-keychain-settings', keychain_name])
|
||||
run_executable_with_output('security', arguments=[
|
||||
'unlock-keychain', '-p', keychain_password, keychain_name
|
||||
])
|
||||
|
||||
# Import p12
|
||||
run_executable_with_output('security', arguments=[
|
||||
'import', p12_path, '-k', keychain_name, '-P', p12_password,
|
||||
'-T', '/usr/bin/codesign', '-T', '/usr/bin/security'
|
||||
], check_result=True)
|
||||
|
||||
# Set partition list for access
|
||||
run_executable_with_output('security', arguments=[
|
||||
'set-key-partition-list', '-S', 'apple-tool:,apple:', '-k', keychain_password, keychain_name
|
||||
], check_result=True)
|
||||
|
||||
return keychain_name
|
||||
|
||||
|
||||
def process_provisioning_profile(source, destination, certificate_data):
|
||||
def cleanup_temp_keychain(keychain_name):
|
||||
"""Remove the temporary keychain."""
|
||||
run_executable_with_output('security', arguments=['delete-keychain', keychain_name], check_result=False)
|
||||
|
||||
|
||||
def get_signing_identity_from_p12(p12_path, p12_password=''):
|
||||
"""Extract the common name (signing identity) from the p12 certificate."""
|
||||
proc = subprocess.Popen(
|
||||
['openssl', 'pkcs12', '-in', p12_path, '-passin', 'pass:' + p12_password, '-nokeys', '-legacy'],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
cert_pem, _ = proc.communicate()
|
||||
|
||||
proc2 = subprocess.Popen(
|
||||
['openssl', 'x509', '-noout', '-subject', '-nameopt', 'oneline,-esc_msb'],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
subject, _ = proc2.communicate(cert_pem)
|
||||
subject = subject.decode('utf-8').strip()
|
||||
|
||||
# Parse CN from subject line like: subject= C = AE, O = ..., CN = Some Name
|
||||
if 'CN = ' in subject:
|
||||
cn = subject.split('CN = ')[-1].split(',')[0].strip()
|
||||
return cn
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_certificate_base64_from_p12(p12_path, p12_password=''):
|
||||
"""Extract the certificate as base64 from p12 file."""
|
||||
# Extract certificate in PEM format
|
||||
proc = subprocess.Popen(
|
||||
['openssl', 'pkcs12', '-in', p12_path, '-passin', 'pass:' + p12_password, '-nokeys', '-legacy'],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
cert_pem, _ = proc.communicate()
|
||||
|
||||
# Convert to DER format
|
||||
proc2 = subprocess.Popen(
|
||||
['openssl', 'x509', '-outform', 'DER'],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
cert_der, _ = proc2.communicate(cert_pem)
|
||||
|
||||
return base64.b64encode(cert_der).decode('utf-8')
|
||||
|
||||
|
||||
def process_provisioning_profile(source, destination, certificate_data, signing_identity, keychain_name):
|
||||
parsed_plist = run_executable_with_output('security', arguments=['cms', '-D', '-i', source], check_result=True)
|
||||
parsed_plist_file = tempfile.mktemp()
|
||||
with open(parsed_plist_file, 'w+') as file:
|
||||
file.write(parsed_plist)
|
||||
|
||||
run_executable_with_output('plutil', arguments=['-remove', 'DeveloperCertificates.0', parsed_plist_file])
|
||||
# Remove all existing developer certificates
|
||||
while True:
|
||||
result = run_executable_with_output('plutil', arguments=['-remove', 'DeveloperCertificates.0', parsed_plist_file], check_result=False)
|
||||
if result is None or 'Could not' in str(result) or result == '':
|
||||
# Check if the removal actually failed by trying to extract
|
||||
check = run_executable_with_output('plutil', arguments=['-extract', 'DeveloperCertificates.0', 'raw', parsed_plist_file], check_result=False)
|
||||
if check is None or 'Could not' in str(check):
|
||||
break
|
||||
|
||||
# Insert the new certificate
|
||||
run_executable_with_output('plutil', arguments=['-insert', 'DeveloperCertificates.0', '-data', certificate_data, parsed_plist_file])
|
||||
|
||||
# Remove the DER-Encoded-Profile (signature)
|
||||
run_executable_with_output('plutil', arguments=['-remove', 'DER-Encoded-Profile', parsed_plist_file])
|
||||
|
||||
run_executable_with_output('security', arguments=['cms', '-S', '-N', 'Apple Distribution: Telegram FZ-LLC (C67CF9S4VU)', '-i', parsed_plist_file, '-o', destination])
|
||||
# Sign with the certificate from the temporary keychain
|
||||
run_executable_with_output('security', arguments=[
|
||||
'cms', '-S', '-k', keychain_name, '-N', signing_identity, '-i', parsed_plist_file, '-o', destination
|
||||
], check_result=True)
|
||||
|
||||
os.unlink(parsed_plist_file)
|
||||
|
||||
|
||||
def generate_provisioning_profiles(source_path, destination_path):
|
||||
certificate_data = get_certificate_base64()
|
||||
def generate_provisioning_profiles(source_path, destination_path, certs_path):
|
||||
p12_path = os.path.join(certs_path, 'SelfSigned.p12')
|
||||
|
||||
if not os.path.exists(destination_path):
|
||||
print('{} does not exits'.format(destination_path))
|
||||
if not os.path.exists(p12_path):
|
||||
print('{} does not exist'.format(p12_path))
|
||||
sys.exit(1)
|
||||
|
||||
for file_name in os.listdir(source_path):
|
||||
if file_name.endswith('.mobileprovision'):
|
||||
process_provisioning_profile(source=source_path + '/' + file_name, destination=destination_path + '/' + file_name, certificate_data=certificate_data)
|
||||
if not os.path.exists(destination_path):
|
||||
print('{} does not exist'.format(destination_path))
|
||||
sys.exit(1)
|
||||
|
||||
# Extract certificate info from p12
|
||||
p12_password = '' # fake-codesigning uses empty password
|
||||
certificate_data = get_certificate_base64_from_p12(p12_path, p12_password)
|
||||
signing_identity = get_signing_identity_from_p12(p12_path, p12_password)
|
||||
|
||||
if not signing_identity:
|
||||
print('Could not extract signing identity from {}'.format(p12_path))
|
||||
sys.exit(1)
|
||||
|
||||
print('Using signing identity: {}'.format(signing_identity))
|
||||
|
||||
# Setup temporary keychain with the certificate
|
||||
keychain_name = setup_temp_keychain(p12_path, p12_password)
|
||||
|
||||
try:
|
||||
for file_name in os.listdir(source_path):
|
||||
if file_name.endswith('.mobileprovision'):
|
||||
print('Processing {}'.format(file_name))
|
||||
process_provisioning_profile(
|
||||
source=os.path.join(source_path, file_name),
|
||||
destination=os.path.join(destination_path, file_name),
|
||||
certificate_data=certificate_data,
|
||||
signing_identity=signing_identity,
|
||||
keychain_name=keychain_name
|
||||
)
|
||||
print('Done. Generated {} profiles.'.format(
|
||||
len([f for f in os.listdir(destination_path) if f.endswith('.mobileprovision')])
|
||||
))
|
||||
finally:
|
||||
cleanup_temp_keychain(keychain_name)
|
||||
|
||||
+28
-19
@@ -46,6 +46,7 @@ class BazelCommandLine:
|
||||
self.show_actions = False
|
||||
self.enable_sandbox = False
|
||||
self.disable_provisioning_profiles = False
|
||||
self.profile_swift = False
|
||||
|
||||
self.common_args = [
|
||||
# https://docs.bazel.build/versions/master/command-line-reference.html
|
||||
@@ -143,6 +144,9 @@ class BazelCommandLine:
|
||||
def set_disable_provisioning_profiles(self):
|
||||
self.disable_provisioning_profiles = True
|
||||
|
||||
def set_profile_swift(self, value):
|
||||
self.profile_swift = value
|
||||
|
||||
def set_configuration(self, configuration):
|
||||
if configuration == 'debug_arm64':
|
||||
self.configuration_args = [
|
||||
@@ -300,6 +304,8 @@ class BazelCommandLine:
|
||||
]
|
||||
|
||||
combined_arguments += self.configuration_args
|
||||
if self.profile_swift:
|
||||
combined_arguments += ['--config=swift_profile']
|
||||
|
||||
print('TelegramBuild: running')
|
||||
print(subprocess.list2cmdline(combined_arguments))
|
||||
@@ -369,17 +375,15 @@ class BazelCommandLine:
|
||||
print(subprocess.list2cmdline(combined_arguments))
|
||||
call_executable(combined_arguments)
|
||||
|
||||
def get_spm_aspect_invocation(self):
|
||||
def invoke_spm_build(self):
|
||||
combined_arguments = [
|
||||
self.build_environment.bazel_path
|
||||
]
|
||||
combined_arguments += self.get_startup_bazel_arguments()
|
||||
combined_arguments += ['build']
|
||||
|
||||
if self.custom_target is not None:
|
||||
combined_arguments += [self.custom_target]
|
||||
else:
|
||||
combined_arguments += ['Telegram/Telegram']
|
||||
# Build the generate_spm target directly to get the dependency tree JSON
|
||||
combined_arguments += ['//Telegram:spm_build_root']
|
||||
|
||||
if self.continue_on_error:
|
||||
combined_arguments += ['--keep_going']
|
||||
@@ -409,8 +413,6 @@ class BazelCommandLine:
|
||||
|
||||
combined_arguments += self.configuration_args
|
||||
|
||||
combined_arguments += ['--aspects', '//build-system/bazel-utils:spm.bzl%spm_text_aspect']
|
||||
|
||||
print(subprocess.list2cmdline(combined_arguments))
|
||||
call_executable(combined_arguments)
|
||||
|
||||
@@ -624,6 +626,7 @@ def build(bazel, arguments):
|
||||
bazel_command_line.set_continue_on_error(arguments.continueOnError)
|
||||
bazel_command_line.set_show_actions(arguments.showActions)
|
||||
bazel_command_line.set_enable_sandbox(arguments.sandbox)
|
||||
bazel_command_line.set_profile_swift(arguments.profileSwift)
|
||||
|
||||
bazel_command_line.set_split_swiftmodules(arguments.enableParallelSwiftmoduleGeneration)
|
||||
|
||||
@@ -719,7 +722,7 @@ def query(bazel, arguments):
|
||||
bazel_command_line.invoke_query(query_args)
|
||||
|
||||
|
||||
def get_spm_aspect_invocation(bazel, arguments):
|
||||
def build_spm(bazel, arguments):
|
||||
bazel_command_line = BazelCommandLine(
|
||||
bazel=bazel,
|
||||
override_bazel_version=arguments.overrideBazelVersion,
|
||||
@@ -741,13 +744,12 @@ def get_spm_aspect_invocation(bazel, arguments):
|
||||
|
||||
bazel_command_line.set_configuration(arguments.configuration)
|
||||
bazel_command_line.set_build_number(arguments.buildNumber)
|
||||
bazel_command_line.set_custom_target(arguments.target)
|
||||
bazel_command_line.set_continue_on_error(False)
|
||||
bazel_command_line.set_show_actions(False)
|
||||
bazel_command_line.set_enable_sandbox(False)
|
||||
bazel_command_line.set_split_swiftmodules(False)
|
||||
|
||||
bazel_command_line.get_spm_aspect_invocation()
|
||||
bazel_command_line.invoke_spm_build()
|
||||
|
||||
def add_codesigning_common_arguments(current_parser: argparse.ArgumentParser):
|
||||
configuration_group = current_parser.add_mutually_exclusive_group(required=True)
|
||||
@@ -977,6 +979,12 @@ if __name__ == '__main__':
|
||||
help='Generate .swiftmodule files in parallel to building modules, can speed up compilation on multi-core '
|
||||
'systems. '
|
||||
)
|
||||
buildParser.add_argument(
|
||||
'--profileSwift',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Enable single-core Swift compile profiling flags.'
|
||||
)
|
||||
buildParser.add_argument(
|
||||
'--target',
|
||||
type=str,
|
||||
@@ -1068,6 +1076,13 @@ if __name__ == '__main__':
|
||||
type=str,
|
||||
help='Path to the destination directory.'
|
||||
)
|
||||
generate_profiles_build_parser.add_argument(
|
||||
'--certsPath',
|
||||
required=False,
|
||||
type=str,
|
||||
default='build-system/fake-codesigning/certs',
|
||||
help='Path to the directory containing SelfSigned.p12 certificate.'
|
||||
)
|
||||
|
||||
remote_upload_testflight_parser = subparsers.add_parser('remote-deploy-testflight', help='Build the app using a remote environment.')
|
||||
remote_upload_testflight_parser.add_argument(
|
||||
@@ -1188,13 +1203,7 @@ if __name__ == '__main__':
|
||||
metavar='query_string'
|
||||
)
|
||||
|
||||
spm_parser = subparsers.add_parser('spm', help='Generate SPM package')
|
||||
spm_parser.add_argument(
|
||||
'--target',
|
||||
type=str,
|
||||
help='A custom bazel target name to build.',
|
||||
metavar='target_name'
|
||||
)
|
||||
spm_parser = subparsers.add_parser('spm', help='Generate SPM package (outputs bazel-bin/Telegram/spm_build_root_modules.json)')
|
||||
spm_parser.add_argument(
|
||||
'--buildNumber',
|
||||
required=False,
|
||||
@@ -1315,7 +1324,7 @@ if __name__ == '__main__':
|
||||
additional_codesigning_output_path=remote_input_path
|
||||
)
|
||||
|
||||
GenerateProfiles.generate_provisioning_profiles(source_path=remote_input_path + '/profiles', destination_path=args.destination)
|
||||
GenerateProfiles.generate_provisioning_profiles(source_path=remote_input_path + '/profiles', destination_path=args.destination, certs_path=args.certsPath)
|
||||
elif args.commandName == 'remote-deploy-testflight':
|
||||
env = os.environ
|
||||
if 'APPSTORE_CONNECT_USERNAME' not in env:
|
||||
@@ -1351,7 +1360,7 @@ if __name__ == '__main__':
|
||||
elif args.commandName == 'query':
|
||||
query(bazel=bazel_path, arguments=args)
|
||||
elif args.commandName == 'spm':
|
||||
get_spm_aspect_invocation(bazel=bazel_path, arguments=args)
|
||||
build_spm(bazel=bazel_path, arguments=args)
|
||||
else:
|
||||
raise Exception('Unknown command')
|
||||
except KeyboardInterrupt:
|
||||
|
||||
@@ -604,7 +604,7 @@ def remote_build_tart(macos_version, bazel_cache_host, configuration, build_inpu
|
||||
else:
|
||||
guest_build_sh += '--cacheHost="$CACHE_HOST" \\'
|
||||
guest_build_sh += 'build \\'
|
||||
guest_build_sh += '--lock \\'
|
||||
#guest_build_sh += '--lock \\'
|
||||
guest_build_sh += '--buildNumber={} \\'.format(build_number)
|
||||
guest_build_sh += '--configuration={} \\'.format(configuration)
|
||||
guest_build_sh += '--configurationPath=$HOME/telegram-build-input/configuration.json \\'
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "aexml",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/tadija/AEXML.git",
|
||||
"state" : {
|
||||
"revision" : "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3",
|
||||
"version" : "4.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "pathkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kylef/PathKit.git",
|
||||
"state" : {
|
||||
"revision" : "3bfd2737b700b9a36565a8c94f4ad2b050a5e574",
|
||||
"version" : "1.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "spectre",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kylef/Spectre.git",
|
||||
"state" : {
|
||||
"revision" : "26cc5e9ae0947092c7139ef7ba612e34646086c7",
|
||||
"version" : "0.10.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-argument-parser",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-argument-parser",
|
||||
"state" : {
|
||||
"revision" : "309a47b2b1d9b5e991f36961c983ecec72275be3",
|
||||
"version" : "1.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "xcodeproj",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/tuist/XcodeProj.git",
|
||||
"state" : {
|
||||
"revision" : "dc3b87a4e69f9cd06c6cb16199f5d0472e57ef6b",
|
||||
"version" : "8.24.3"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// swift-tools-version:5.8
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "MakeProject",
|
||||
platforms: [
|
||||
.macOS(.v13)
|
||||
],
|
||||
products: [
|
||||
.executable(name: "MakeProject", targets: ["MakeProject"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/tuist/XcodeProj.git", from: "8.15.0"),
|
||||
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"),
|
||||
],
|
||||
targets: [
|
||||
.executableTarget(
|
||||
name: "MakeProject",
|
||||
dependencies: [
|
||||
.product(name: "XcodeProj", package: "XcodeProj"),
|
||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
import Foundation
|
||||
|
||||
struct ModuleDefinition: Codable {
|
||||
let name: String
|
||||
let moduleName: String?
|
||||
let type: String
|
||||
let path: String
|
||||
let sources: [String]
|
||||
let deps: [String]?
|
||||
let copts: [String]?
|
||||
let cxxopts: [String]?
|
||||
let defines: [String]?
|
||||
let includes: [String]?
|
||||
let sdkFrameworks: [String]?
|
||||
let sdkDylibs: [String]?
|
||||
let hdrs: [String]?
|
||||
let textualHdrs: [String]?
|
||||
let weakSdkFrameworks: [String]?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case moduleName = "module_name"
|
||||
case type
|
||||
case path
|
||||
case sources
|
||||
case deps
|
||||
case copts
|
||||
case cxxopts
|
||||
case defines
|
||||
case includes
|
||||
case sdkFrameworks = "sdk_frameworks"
|
||||
case sdkDylibs = "sdk_dylibs"
|
||||
case hdrs
|
||||
case textualHdrs = "textual_hdrs"
|
||||
case weakSdkFrameworks = "weak_sdk_frameworks"
|
||||
}
|
||||
}
|
||||
|
||||
enum ModuleType: String {
|
||||
case swiftLibrary = "swift_library"
|
||||
case objcLibrary = "objc_library"
|
||||
case ccLibrary = "cc_library"
|
||||
case xcframework = "apple_static_xcframework_import"
|
||||
|
||||
init?(from definition: ModuleDefinition) {
|
||||
self.init(rawValue: definition.type)
|
||||
}
|
||||
}
|
||||
|
||||
func loadModules(from path: String) throws -> [String: ModuleDefinition] {
|
||||
let url = URL(fileURLWithPath: path)
|
||||
let data = try Data(contentsOf: url)
|
||||
return try JSONDecoder().decode([String: ModuleDefinition].self, from: data)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
import XcodeProj
|
||||
|
||||
class ProjectGenerator {
|
||||
let modulesPath: Path
|
||||
let outputDir: Path
|
||||
let projectRoot: Path
|
||||
|
||||
init(modulesPath: Path, outputDir: Path, projectRoot: Path) {
|
||||
self.modulesPath = modulesPath
|
||||
self.outputDir = outputDir
|
||||
self.projectRoot = projectRoot
|
||||
}
|
||||
|
||||
func generate() throws {
|
||||
print("Loading modules from \(modulesPath)...")
|
||||
let modules = try loadModules(from: modulesPath.string)
|
||||
print("Loaded \(modules.count) modules")
|
||||
|
||||
// Filter out empty modules, but keep:
|
||||
// - Modules with source files (excluding .a)
|
||||
// - Static library modules (only .a files)
|
||||
// - XCFramework imports
|
||||
let validModules = modules.filter { name, module in
|
||||
let nonStaticSources = module.sources.filter { !$0.hasSuffix(".a") }
|
||||
let staticLibs = module.sources.filter { $0.hasSuffix(".a") }
|
||||
return !nonStaticSources.isEmpty ||
|
||||
!staticLibs.isEmpty ||
|
||||
module.type == "apple_static_xcframework_import"
|
||||
}
|
||||
print("Processing \(validModules.count) non-empty modules")
|
||||
|
||||
// Setup output directory
|
||||
try outputDir.mkpath()
|
||||
|
||||
// Create symlink manager
|
||||
let symlinkManager = SymlinkManager(outputDir: outputDir, projectRoot: projectRoot)
|
||||
symlinkManager.scanExistingFiles()
|
||||
|
||||
// Create project
|
||||
let projectPath = outputDir + "Telegram.xcodeproj"
|
||||
let pbxproj = PBXProj()
|
||||
|
||||
// Create main group
|
||||
let mainGroup = PBXGroup(children: [], sourceTree: .group)
|
||||
pbxproj.add(object: mainGroup)
|
||||
|
||||
// Create project-level build configurations
|
||||
let projectDebugSettings: BuildSettings = [
|
||||
"ALWAYS_SEARCH_USER_PATHS": "NO",
|
||||
"CLANG_CXX_LANGUAGE_STANDARD": "c++17",
|
||||
"CLANG_ENABLE_MODULES": "YES",
|
||||
"CLANG_ENABLE_OBJC_ARC": "YES",
|
||||
"CLANG_ENABLE_EXPLICIT_MODULES": "NO", // Disable explicit module builds for ObjC-Swift interop
|
||||
"SWIFT_ENABLE_EXPLICIT_MODULES": "NO", // Disable explicit module builds for Swift
|
||||
"ENABLE_STRICT_OBJC_MSGSEND": "YES",
|
||||
"GCC_NO_COMMON_BLOCKS": "YES",
|
||||
"IPHONEOS_DEPLOYMENT_TARGET": "13.0",
|
||||
"MTL_ENABLE_DEBUG_INFO": "INCLUDE_SOURCE",
|
||||
"ONLY_ACTIVE_ARCH": "YES",
|
||||
"SDKROOT": "iphoneos",
|
||||
"SWIFT_VERSION": "5.0",
|
||||
"TARGETED_DEVICE_FAMILY": "1,2",
|
||||
"DEBUG_INFORMATION_FORMAT": "dwarf",
|
||||
"ENABLE_BITCODE": "NO",
|
||||
]
|
||||
|
||||
var projectReleaseSettings = projectDebugSettings
|
||||
projectReleaseSettings["DEBUG_INFORMATION_FORMAT"] = "dwarf-with-dsym"
|
||||
projectReleaseSettings["MTL_ENABLE_DEBUG_INFO"] = "NO"
|
||||
projectReleaseSettings["ONLY_ACTIVE_ARCH"] = "NO"
|
||||
|
||||
let projectDebugConfig = XCBuildConfiguration(name: "Debug", buildSettings: projectDebugSettings)
|
||||
let projectReleaseConfig = XCBuildConfiguration(name: "Release", buildSettings: projectReleaseSettings)
|
||||
pbxproj.add(object: projectDebugConfig)
|
||||
pbxproj.add(object: projectReleaseConfig)
|
||||
|
||||
let projectConfigList = XCConfigurationList(
|
||||
buildConfigurations: [projectDebugConfig, projectReleaseConfig],
|
||||
defaultConfigurationName: "Release"
|
||||
)
|
||||
pbxproj.add(object: projectConfigList)
|
||||
|
||||
// Create project
|
||||
let project = PBXProject(
|
||||
name: "Telegram",
|
||||
buildConfigurationList: projectConfigList,
|
||||
compatibilityVersion: "Xcode 14.0",
|
||||
preferredProjectObjectVersion: 56,
|
||||
minimizedProjectReferenceProxies: 0,
|
||||
mainGroup: mainGroup
|
||||
)
|
||||
pbxproj.add(object: project)
|
||||
|
||||
// Create target builder
|
||||
let targetBuilder = TargetBuilder(
|
||||
project: project,
|
||||
pbxproj: pbxproj,
|
||||
mainGroup: mainGroup,
|
||||
outputDir: outputDir,
|
||||
symlinkManager: symlinkManager
|
||||
)
|
||||
|
||||
// Build targets
|
||||
print("Creating targets...")
|
||||
var builtCount = 0
|
||||
for (name, module) in validModules.sorted(by: { $0.key < $1.key }) {
|
||||
do {
|
||||
if let _ = try targetBuilder.buildTarget(for: module, allModules: validModules) {
|
||||
builtCount += 1
|
||||
if builtCount % 50 == 0 {
|
||||
print(" Created \(builtCount) targets...")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Warning: Failed to build target \(name): \(error)")
|
||||
}
|
||||
}
|
||||
print("Created \(builtCount) targets")
|
||||
|
||||
// Wire up dependencies
|
||||
print("Wiring up dependencies...")
|
||||
try targetBuilder.wireUpDependencies(modules: validModules)
|
||||
|
||||
// Write project
|
||||
print("Writing project to \(projectPath)...")
|
||||
pbxproj.rootObject = project
|
||||
let xcodeproj = XcodeProj(workspace: XCWorkspace(), pbxproj: pbxproj)
|
||||
try xcodeproj.write(path: projectPath)
|
||||
|
||||
// Generate scheme for main target
|
||||
if let telegramTarget = targetBuilder.getTarget(named: "TelegramUI") {
|
||||
print("Generating scheme...")
|
||||
let schemeGenerator = SchemeGenerator(projectPath: projectPath, pbxproj: pbxproj)
|
||||
try schemeGenerator.generateScheme(for: telegramTarget, named: "TelegramUI")
|
||||
} else {
|
||||
print("Warning: Could not find TelegramUI target for scheme")
|
||||
}
|
||||
|
||||
// Clean up stale files
|
||||
print("Cleaning up stale symlinks...")
|
||||
symlinkManager.cleanupStaleFiles()
|
||||
|
||||
print("Done! Project written to \(projectPath)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
import XcodeProj
|
||||
|
||||
class SchemeGenerator {
|
||||
let projectPath: Path
|
||||
let pbxproj: PBXProj
|
||||
|
||||
init(projectPath: Path, pbxproj: PBXProj) {
|
||||
self.projectPath = projectPath
|
||||
self.pbxproj = pbxproj
|
||||
}
|
||||
|
||||
func generateScheme(for target: PBXNativeTarget, named schemeName: String) throws {
|
||||
let schemesDir = projectPath + "xcshareddata" + "xcschemes"
|
||||
try schemesDir.mkpath()
|
||||
|
||||
let buildableReference = XCScheme.BuildableReference(
|
||||
referencedContainer: "container:Telegram.xcodeproj",
|
||||
blueprint: target,
|
||||
buildableName: "\(target.name).framework",
|
||||
blueprintName: target.name
|
||||
)
|
||||
|
||||
let buildAction = XCScheme.BuildAction(
|
||||
buildActionEntries: [
|
||||
XCScheme.BuildAction.Entry(
|
||||
buildableReference: buildableReference,
|
||||
buildFor: [.running, .testing, .profiling, .archiving, .analyzing]
|
||||
)
|
||||
],
|
||||
parallelizeBuild: true,
|
||||
buildImplicitDependencies: true
|
||||
)
|
||||
|
||||
let launchAction = XCScheme.LaunchAction(
|
||||
runnable: nil,
|
||||
buildConfiguration: "Debug"
|
||||
)
|
||||
|
||||
let testAction = XCScheme.TestAction(
|
||||
buildConfiguration: "Debug",
|
||||
macroExpansion: buildableReference
|
||||
)
|
||||
|
||||
let profileAction = XCScheme.ProfileAction(
|
||||
runnable: nil,
|
||||
buildConfiguration: "Release",
|
||||
macroExpansion: buildableReference
|
||||
)
|
||||
|
||||
let analyzeAction = XCScheme.AnalyzeAction(buildConfiguration: "Debug")
|
||||
|
||||
let archiveAction = XCScheme.ArchiveAction(
|
||||
buildConfiguration: "Release",
|
||||
revealArchiveInOrganizer: true
|
||||
)
|
||||
|
||||
let scheme = XCScheme(
|
||||
name: schemeName,
|
||||
lastUpgradeVersion: nil,
|
||||
version: nil,
|
||||
buildAction: buildAction,
|
||||
testAction: testAction,
|
||||
launchAction: launchAction,
|
||||
profileAction: profileAction,
|
||||
analyzeAction: analyzeAction,
|
||||
archiveAction: archiveAction
|
||||
)
|
||||
|
||||
let schemePath = schemesDir + "\(schemeName).xcscheme"
|
||||
try scheme.write(path: schemePath, override: true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
|
||||
class SymlinkManager {
|
||||
let outputDir: Path
|
||||
let projectRoot: Path
|
||||
private var previousFiles: Set<Path> = []
|
||||
private var currentFiles: Set<Path> = []
|
||||
|
||||
init(outputDir: Path, projectRoot: Path) {
|
||||
self.outputDir = outputDir
|
||||
self.projectRoot = projectRoot
|
||||
}
|
||||
|
||||
func scanExistingFiles() {
|
||||
previousFiles = []
|
||||
scanDirectory(outputDir)
|
||||
}
|
||||
|
||||
private func scanDirectory(_ path: Path) {
|
||||
guard path.exists else { return }
|
||||
do {
|
||||
for item in try path.children() {
|
||||
let name = item.lastComponent
|
||||
// Skip build artifacts and xcodeproj bundles
|
||||
if name == ".build" || name.hasSuffix(".xcodeproj") { continue }
|
||||
previousFiles.insert(item)
|
||||
if item.isDirectory && !item.isSymlink {
|
||||
scanDirectory(item)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Warning: Could not scan \(path): \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func createDirectory(_ path: Path) throws {
|
||||
currentFiles.insert(path)
|
||||
var parent = path.parent()
|
||||
while parent != outputDir && parent.string.count > outputDir.string.count {
|
||||
currentFiles.insert(parent)
|
||||
parent = parent.parent()
|
||||
}
|
||||
if !path.exists {
|
||||
try path.mkpath()
|
||||
}
|
||||
}
|
||||
|
||||
func createSymlink(from source: Path, to target: Path) throws {
|
||||
currentFiles.insert(target)
|
||||
var parent = target.parent()
|
||||
while parent != outputDir && parent.string.count > outputDir.string.count {
|
||||
currentFiles.insert(parent)
|
||||
parent = parent.parent()
|
||||
}
|
||||
|
||||
// Calculate relative path from target back to source
|
||||
let targetDir = target.parent()
|
||||
let depth = targetDir.components.count - outputDir.components.count + 1
|
||||
let relativePrefix = Array(repeating: "..", count: depth).joined(separator: "/")
|
||||
let relativePath = Path(relativePrefix) + source
|
||||
|
||||
if target.isSymlink {
|
||||
let existingTarget = try? target.symlinkDestination()
|
||||
if existingTarget == relativePath {
|
||||
return // Already correct
|
||||
}
|
||||
try target.delete()
|
||||
} else if target.exists {
|
||||
try target.delete()
|
||||
}
|
||||
|
||||
try targetDir.mkpath()
|
||||
try FileManager.default.createSymbolicLink(
|
||||
atPath: target.string,
|
||||
withDestinationPath: relativePath.string
|
||||
)
|
||||
}
|
||||
|
||||
func cleanupStaleFiles() {
|
||||
let staleFiles = previousFiles.subtracting(currentFiles)
|
||||
let sortedStale = staleFiles.sorted { $0.components.count > $1.components.count }
|
||||
|
||||
for path in sortedStale {
|
||||
do {
|
||||
if path.isSymlink || path.isFile {
|
||||
try path.delete()
|
||||
} else if path.isDirectory {
|
||||
if (try? path.children().isEmpty) == true {
|
||||
try path.delete()
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Warning: Could not remove \(path): \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func markFile(_ path: Path) {
|
||||
currentFiles.insert(path)
|
||||
}
|
||||
}
|
||||
|
||||
extension Path {
|
||||
var isSymlink: Bool {
|
||||
var isDir: ObjCBool = false
|
||||
let exists = FileManager.default.fileExists(atPath: self.string, isDirectory: &isDir)
|
||||
guard exists else { return false }
|
||||
do {
|
||||
let attrs = try FileManager.default.attributesOfItem(atPath: self.string)
|
||||
return attrs[.type] as? FileAttributeType == .typeSymbolicLink
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,792 @@
|
||||
import Foundation
|
||||
import PathKit
|
||||
import XcodeProj
|
||||
|
||||
class TargetBuilder {
|
||||
let project: PBXProject
|
||||
let pbxproj: PBXProj
|
||||
let mainGroup: PBXGroup
|
||||
let outputDir: Path
|
||||
let symlinkManager: SymlinkManager
|
||||
|
||||
private var targetsByName: [String: PBXNativeTarget] = [:]
|
||||
private var groupsByPath: [String: PBXGroup] = [:]
|
||||
|
||||
init(project: PBXProject, pbxproj: PBXProj, mainGroup: PBXGroup, outputDir: Path, symlinkManager: SymlinkManager) {
|
||||
self.project = project
|
||||
self.pbxproj = pbxproj
|
||||
self.mainGroup = mainGroup
|
||||
self.outputDir = outputDir
|
||||
self.symlinkManager = symlinkManager
|
||||
}
|
||||
|
||||
// Track which modules are header-only (have no linkable code)
|
||||
private var headerOnlyModules: Set<String> = []
|
||||
// Track which modules are static library collections (.a files)
|
||||
private var staticLibraryModules: [String: [String]] = [:] // module name -> list of .a file paths
|
||||
|
||||
func isHeaderOnlyModule(_ name: String) -> Bool {
|
||||
return headerOnlyModules.contains(name)
|
||||
}
|
||||
|
||||
func isStaticLibraryModule(_ name: String) -> Bool {
|
||||
return staticLibraryModules[name] != nil
|
||||
}
|
||||
|
||||
func getStaticLibraries(for name: String) -> [String] {
|
||||
return staticLibraryModules[name] ?? []
|
||||
}
|
||||
|
||||
func buildTarget(for module: ModuleDefinition, allModules: [String: ModuleDefinition]) throws -> PBXNativeTarget? {
|
||||
guard let moduleType = ModuleType(from: module) else {
|
||||
print("Warning: Unknown module type \(module.type) for \(module.name)")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for static library modules (only .a files)
|
||||
let staticLibs = module.sources.filter { $0.hasSuffix(".a") }
|
||||
let nonStaticLibs = module.sources.filter { !$0.hasSuffix(".a") }
|
||||
let isStaticLibOnly = !staticLibs.isEmpty && nonStaticLibs.allSatisfy { $0.hasSuffix(".h") || $0.hasSuffix(".hpp") }
|
||||
|
||||
if isStaticLibOnly && moduleType != .xcframework {
|
||||
// This is a static library module - track its .a files but don't create a framework
|
||||
staticLibraryModules[module.name] = staticLibs
|
||||
return nil // Don't create a target, just track the static libs
|
||||
}
|
||||
|
||||
// Check if module is header-only (only header files, no real sources)
|
||||
let sourceFiles = module.sources.filter { source in
|
||||
!source.hasSuffix(".a") && !source.hasSuffix(".h") && !source.hasSuffix(".hpp")
|
||||
}
|
||||
let isHeaderOnly = sourceFiles.isEmpty && moduleType != .xcframework
|
||||
|
||||
// Skip modules with no sources at all
|
||||
if module.sources.isEmpty && moduleType != .xcframework {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch moduleType {
|
||||
case .xcframework:
|
||||
return try buildXCFrameworkTarget(for: module)
|
||||
case .swiftLibrary, .objcLibrary, .ccLibrary:
|
||||
if isHeaderOnly {
|
||||
headerOnlyModules.insert(module.name)
|
||||
return try buildHeaderOnlyFrameworkTarget(for: module)
|
||||
} else {
|
||||
return try buildFrameworkTarget(for: module, moduleType: moduleType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func buildXCFrameworkTarget(for module: ModuleDefinition) throws -> PBXNativeTarget {
|
||||
// For xcframeworks, we create a reference but no build phases
|
||||
let target = PBXNativeTarget(
|
||||
name: module.name,
|
||||
buildConfigurationList: createConfigurationList(for: module, isXCFramework: true),
|
||||
buildPhases: [],
|
||||
productName: module.name,
|
||||
productType: .framework
|
||||
)
|
||||
pbxproj.add(object: target)
|
||||
project.targets.append(target)
|
||||
targetsByName[module.name] = target
|
||||
return target
|
||||
}
|
||||
|
||||
private func buildFrameworkTarget(for module: ModuleDefinition, moduleType: ModuleType) throws -> PBXNativeTarget {
|
||||
// Create group for module
|
||||
let moduleGroup = try getOrCreateGroup(for: module.path)
|
||||
|
||||
// Create symlinks and file references
|
||||
var sourceRefs: [PBXFileReference] = []
|
||||
var publicHeaderRefs: [PBXFileReference] = []
|
||||
var seenPublicHeaderNames: Set<String> = [] // Track header filenames to avoid duplicates in headers build phase
|
||||
|
||||
// Determine public header detection:
|
||||
// 1. If hdrs is provided, use those (explicit public headers)
|
||||
// 2. Otherwise, headers in includes directories are public
|
||||
let explicitPublicHeaders = Set(module.hdrs ?? [])
|
||||
|
||||
let allSourceFiles = module.sources + (module.hdrs ?? []) + (module.textualHdrs ?? [])
|
||||
|
||||
for source in allSourceFiles {
|
||||
if source.hasSuffix(".a") { continue }
|
||||
|
||||
let sourcePath = Path(source)
|
||||
let fileName = sourcePath.lastComponent
|
||||
|
||||
// Calculate relative path within module
|
||||
let relativeToModule: String
|
||||
if source.hasPrefix(module.path + "/") {
|
||||
relativeToModule = String(source.dropFirst(module.path.count + 1))
|
||||
} else if source.hasPrefix("bazel-out/") {
|
||||
// Generated file
|
||||
if let range = source.range(of: module.path + "/") {
|
||||
relativeToModule = String(source[range.upperBound...])
|
||||
} else {
|
||||
relativeToModule = fileName
|
||||
}
|
||||
} else {
|
||||
relativeToModule = fileName
|
||||
}
|
||||
|
||||
// Create symlink
|
||||
let symlinkPath = outputDir + module.path + relativeToModule
|
||||
try symlinkManager.createDirectory(symlinkPath.parent())
|
||||
try symlinkManager.createSymlink(from: Path(source), to: symlinkPath)
|
||||
|
||||
// Create file reference
|
||||
let fileRef = try getOrCreateFileReference(
|
||||
path: relativeToModule,
|
||||
in: moduleGroup,
|
||||
modulePath: module.path,
|
||||
fileName: fileName
|
||||
)
|
||||
|
||||
let isHeader = source.hasSuffix(".h") || source.hasSuffix(".hpp")
|
||||
|
||||
// Determine if this is a public header:
|
||||
// 1. Explicitly in hdrs array
|
||||
// 2. Located in an includes directory
|
||||
let isPublicHeader: Bool
|
||||
if !isHeader {
|
||||
isPublicHeader = false
|
||||
} else if !explicitPublicHeaders.isEmpty {
|
||||
// If hdrs is provided, only those are public
|
||||
isPublicHeader = explicitPublicHeaders.contains(source)
|
||||
} else {
|
||||
// Headers in include directories are public (handles bazel-out paths)
|
||||
isPublicHeader = isInIncludesDirectory(source: source, modulePath: module.path, includes: module.includes)
|
||||
}
|
||||
|
||||
if isPublicHeader {
|
||||
// Skip duplicate header filenames to avoid "multiple commands produce" errors
|
||||
if !seenPublicHeaderNames.contains(fileName) {
|
||||
seenPublicHeaderNames.insert(fileName)
|
||||
publicHeaderRefs.append(fileRef)
|
||||
}
|
||||
} else if !source.hasSuffix(".inc") && !isHeader {
|
||||
// Source files (not headers)
|
||||
sourceRefs.append(fileRef)
|
||||
}
|
||||
// Private headers are not added to any build phase
|
||||
}
|
||||
|
||||
// Build phases
|
||||
var buildPhases: [PBXBuildPhase] = []
|
||||
|
||||
// Sources build phase
|
||||
let sourcesBuildPhase = PBXSourcesBuildPhase(
|
||||
files: sourceRefs.map { ref in
|
||||
let buildFile = PBXBuildFile(file: ref)
|
||||
pbxproj.add(object: buildFile)
|
||||
return buildFile
|
||||
}
|
||||
)
|
||||
pbxproj.add(object: sourcesBuildPhase)
|
||||
buildPhases.append(sourcesBuildPhase)
|
||||
|
||||
// Generate modulemap for ObjC/C++ modules (SPM-style explicit headers)
|
||||
var modulemapPath: Path? = nil
|
||||
if moduleType == .objcLibrary || moduleType == .ccLibrary {
|
||||
modulemapPath = try generateModulemap(for: module)
|
||||
|
||||
// Headers build phase with public headers - needed for ObjC #import to work
|
||||
if !publicHeaderRefs.isEmpty {
|
||||
let headersBuildPhase = PBXHeadersBuildPhase(
|
||||
files: publicHeaderRefs.map { ref in
|
||||
let buildFile = PBXBuildFile(file: ref, settings: ["ATTRIBUTES": ["Public"]])
|
||||
pbxproj.add(object: buildFile)
|
||||
return buildFile
|
||||
}
|
||||
)
|
||||
pbxproj.add(object: headersBuildPhase)
|
||||
buildPhases.append(headersBuildPhase)
|
||||
}
|
||||
}
|
||||
|
||||
// Frameworks build phase
|
||||
let frameworksBuildPhase = PBXFrameworksBuildPhase(files: [])
|
||||
pbxproj.add(object: frameworksBuildPhase)
|
||||
buildPhases.append(frameworksBuildPhase)
|
||||
|
||||
// Create target with custom modulemap if generated
|
||||
let configList = createConfigurationList(for: module, isXCFramework: false, modulemapPath: modulemapPath)
|
||||
|
||||
let target = PBXNativeTarget(
|
||||
name: module.name,
|
||||
buildConfigurationList: configList,
|
||||
buildPhases: buildPhases,
|
||||
productName: module.name,
|
||||
productType: .framework
|
||||
)
|
||||
pbxproj.add(object: target)
|
||||
project.targets.append(target)
|
||||
targetsByName[module.name] = target
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
/// Build a framework target for header-only modules (just headers + modulemap, no sources)
|
||||
private func buildHeaderOnlyFrameworkTarget(for module: ModuleDefinition) throws -> PBXNativeTarget {
|
||||
// Create group for module
|
||||
let moduleGroup = try getOrCreateGroup(for: module.path)
|
||||
|
||||
// Symlink header files and create file references
|
||||
var publicHeaderRefs: [PBXFileReference] = []
|
||||
var seenPublicHeaderNames: Set<String> = [] // Track header filenames to avoid duplicates
|
||||
let allHeaders = module.sources.filter { $0.hasSuffix(".h") || $0.hasSuffix(".hpp") } + (module.hdrs ?? []) + (module.textualHdrs ?? [])
|
||||
|
||||
for source in allHeaders {
|
||||
let sourcePath = Path(source)
|
||||
let fileName = sourcePath.lastComponent
|
||||
let relativeToModule = relativePathInModule(source: source, modulePath: module.path)
|
||||
|
||||
// Create parent group
|
||||
let parentPath = (Path(module.path) + Path(relativeToModule).parent()).string
|
||||
let parentGroup = try getOrCreateGroup(for: parentPath)
|
||||
|
||||
// Create symlink
|
||||
let symlinkPath = outputDir + module.path + relativeToModule
|
||||
try symlinkManager.createDirectory(symlinkPath.parent())
|
||||
try symlinkManager.createSymlink(from: Path(source), to: symlinkPath)
|
||||
|
||||
// Create file reference
|
||||
let fileType = lastKnownFileType(for: fileName)
|
||||
let fileRef = PBXFileReference(
|
||||
sourceTree: .group,
|
||||
name: fileName,
|
||||
lastKnownFileType: fileType,
|
||||
path: fileName
|
||||
)
|
||||
pbxproj.add(object: fileRef)
|
||||
parentGroup.children.append(fileRef)
|
||||
|
||||
// Check if it's a public header (in includes directories)
|
||||
// Use helper that properly handles bazel-out paths
|
||||
let isPublic = isInIncludesDirectory(source: source, modulePath: module.path, includes: module.includes)
|
||||
if isPublic {
|
||||
// Skip duplicate header filenames to avoid "multiple commands produce" errors
|
||||
if !seenPublicHeaderNames.contains(fileName) {
|
||||
seenPublicHeaderNames.insert(fileName)
|
||||
publicHeaderRefs.append(fileRef)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate modulemap for this header-only module
|
||||
let modulemapPath = try generateModulemap(for: module)
|
||||
|
||||
// Create build phases
|
||||
var buildPhases: [PBXBuildPhase] = []
|
||||
|
||||
// Headers build phase with public headers - needed for ObjC #import to work
|
||||
if !publicHeaderRefs.isEmpty {
|
||||
let headersBuildPhase = PBXHeadersBuildPhase(
|
||||
files: publicHeaderRefs.map { ref in
|
||||
let buildFile = PBXBuildFile(file: ref, settings: ["ATTRIBUTES": ["Public"]])
|
||||
pbxproj.add(object: buildFile)
|
||||
return buildFile
|
||||
}
|
||||
)
|
||||
pbxproj.add(object: headersBuildPhase)
|
||||
buildPhases.append(headersBuildPhase)
|
||||
}
|
||||
|
||||
// Empty frameworks phase (needed for Xcode, but won't link anything)
|
||||
let frameworksBuildPhase = PBXFrameworksBuildPhase(files: [])
|
||||
pbxproj.add(object: frameworksBuildPhase)
|
||||
buildPhases.append(frameworksBuildPhase)
|
||||
|
||||
// Create target with custom modulemap
|
||||
let configList = createConfigurationList(for: module, isXCFramework: false, modulemapPath: modulemapPath)
|
||||
|
||||
let target = PBXNativeTarget(
|
||||
name: module.name,
|
||||
buildConfigurationList: configList,
|
||||
buildPhases: buildPhases,
|
||||
productName: module.name,
|
||||
productType: .framework
|
||||
)
|
||||
pbxproj.add(object: target)
|
||||
project.targets.append(target)
|
||||
targetsByName[module.name] = target
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
/// Collect all transitive dependencies for a module
|
||||
private func collectAllDependencies(for moduleName: String, modules: [String: ModuleDefinition], visited: inout Set<String>) -> Set<String> {
|
||||
guard !visited.contains(moduleName) else { return [] }
|
||||
visited.insert(moduleName)
|
||||
|
||||
guard let module = modules[moduleName], let deps = module.deps else {
|
||||
return []
|
||||
}
|
||||
|
||||
var allDeps = Set(deps)
|
||||
for depName in deps {
|
||||
allDeps.formUnion(collectAllDependencies(for: depName, modules: modules, visited: &visited))
|
||||
}
|
||||
return allDeps
|
||||
}
|
||||
|
||||
func wireUpDependencies(modules: [String: ModuleDefinition]) throws {
|
||||
for (name, module) in modules {
|
||||
guard let target = targetsByName[name],
|
||||
let deps = module.deps else { continue }
|
||||
|
||||
// Find frameworks build phase
|
||||
guard let frameworksPhase = target.buildPhases.compactMap({ $0 as? PBXFrameworksBuildPhase }).first else {
|
||||
continue
|
||||
}
|
||||
|
||||
// Track all frameworks we've added to avoid duplicates
|
||||
var linkedFrameworks: Set<String> = []
|
||||
// Track library search paths for static libraries
|
||||
var staticLibSearchPaths: Set<String> = []
|
||||
|
||||
// Add target dependency (only for direct deps)
|
||||
for depName in deps {
|
||||
if let depTarget = targetsByName[depName] {
|
||||
let dependency = PBXTargetDependency(target: depTarget)
|
||||
pbxproj.add(object: dependency)
|
||||
target.dependencies.append(dependency)
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all transitive dependencies to link
|
||||
var visited = Set<String>()
|
||||
let allDeps = collectAllDependencies(for: name, modules: modules, visited: &visited)
|
||||
|
||||
// Collect header paths from ALL dependencies (direct and transitive)
|
||||
var depHeaderPaths: [String] = []
|
||||
for depName in allDeps {
|
||||
if let depModule = modules[depName] {
|
||||
depHeaderPaths.append(contentsOf: exportedHeaderPaths(for: depModule))
|
||||
}
|
||||
}
|
||||
|
||||
// Link all dependencies (direct and transitive) that have targets
|
||||
for depName in allDeps {
|
||||
// Skip if already linked
|
||||
guard !linkedFrameworks.contains(depName) else { continue }
|
||||
|
||||
// Check if this is a static library module - link .a files directly
|
||||
if isStaticLibraryModule(depName) {
|
||||
// Link static libraries directly
|
||||
for libPath in getStaticLibraries(for: depName) {
|
||||
let libName = Path(libPath).lastComponent
|
||||
// Create an absolute path to the project root then to the static library
|
||||
// SRCROOT is xcode-files, so we need to go up one level to get to telegram-ios
|
||||
let projectRoot = outputDir.parent()
|
||||
let absoluteLibPath = (projectRoot + libPath).string
|
||||
let libRef = PBXFileReference(
|
||||
sourceTree: .absolute,
|
||||
name: libName,
|
||||
lastKnownFileType: "archive.ar",
|
||||
path: absoluteLibPath
|
||||
)
|
||||
pbxproj.add(object: libRef)
|
||||
let buildFile = PBXBuildFile(file: libRef)
|
||||
pbxproj.add(object: buildFile)
|
||||
frameworksPhase.files?.append(buildFile)
|
||||
|
||||
// Add the library's directory to LIBRARY_SEARCH_PATHS
|
||||
let libDir = Path(absoluteLibPath).parent().string
|
||||
if !staticLibSearchPaths.contains(libDir) {
|
||||
staticLibSearchPaths.insert(libDir)
|
||||
}
|
||||
}
|
||||
linkedFrameworks.insert(depName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip header-only modules (no framework to link)
|
||||
if isHeaderOnlyModule(depName) { continue }
|
||||
|
||||
// Regular framework dependency
|
||||
guard targetsByName[depName] != nil else { continue }
|
||||
|
||||
linkedFrameworks.insert(depName)
|
||||
let frameworkRef = PBXFileReference(
|
||||
sourceTree: .buildProductsDir,
|
||||
name: "\(depName).framework",
|
||||
lastKnownFileType: "wrapper.framework",
|
||||
path: "\(depName).framework"
|
||||
)
|
||||
pbxproj.add(object: frameworkRef)
|
||||
let buildFile = PBXBuildFile(file: frameworkRef)
|
||||
pbxproj.add(object: buildFile)
|
||||
frameworksPhase.files?.append(buildFile)
|
||||
}
|
||||
|
||||
// Update build configurations with dependency paths
|
||||
if !depHeaderPaths.isEmpty || !deps.isEmpty || !staticLibSearchPaths.isEmpty {
|
||||
for config in target.buildConfigurationList?.buildConfigurations ?? [] {
|
||||
// Add header search paths
|
||||
if !depHeaderPaths.isEmpty {
|
||||
let existing = config.buildSettings["HEADER_SEARCH_PATHS"] as? String ?? "$(inherited)"
|
||||
config.buildSettings["HEADER_SEARCH_PATHS"] = existing + " " + depHeaderPaths.joined(separator: " ")
|
||||
}
|
||||
// Ensure framework and module search paths include built products
|
||||
config.buildSettings["FRAMEWORK_SEARCH_PATHS"] = "$(inherited) $(BUILT_PRODUCTS_DIR)"
|
||||
config.buildSettings["SWIFT_INCLUDE_PATHS"] = "$(inherited) $(BUILT_PRODUCTS_DIR)"
|
||||
// Add library search paths for static libraries
|
||||
if !staticLibSearchPaths.isEmpty {
|
||||
let existing = config.buildSettings["LIBRARY_SEARCH_PATHS"] as? String ?? "$(inherited)"
|
||||
config.buildSettings["LIBRARY_SEARCH_PATHS"] = existing + " " + staticLibSearchPaths.sorted().joined(separator: " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect SDK frameworks from this module and all transitive dependencies
|
||||
var allSdkFrameworks: Set<String> = []
|
||||
if let sdkFrameworks = module.sdkFrameworks {
|
||||
allSdkFrameworks.formUnion(sdkFrameworks)
|
||||
}
|
||||
for depName in allDeps {
|
||||
if let depModule = modules[depName], let depFrameworks = depModule.sdkFrameworks {
|
||||
allSdkFrameworks.formUnion(depFrameworks)
|
||||
}
|
||||
}
|
||||
|
||||
// Add SDK frameworks
|
||||
for framework in allSdkFrameworks {
|
||||
let fileRef = PBXFileReference(
|
||||
sourceTree: .sdkRoot,
|
||||
name: "\(framework).framework",
|
||||
lastKnownFileType: "wrapper.framework",
|
||||
path: "System/Library/Frameworks/\(framework).framework"
|
||||
)
|
||||
pbxproj.add(object: fileRef)
|
||||
let buildFile = PBXBuildFile(file: fileRef)
|
||||
pbxproj.add(object: buildFile)
|
||||
frameworksPhase.files?.append(buildFile)
|
||||
}
|
||||
|
||||
// Collect SDK dylibs from this module and all transitive dependencies
|
||||
var allSdkDylibs: Set<String> = []
|
||||
if let sdkDylibs = module.sdkDylibs {
|
||||
allSdkDylibs.formUnion(sdkDylibs)
|
||||
}
|
||||
for depName in allDeps {
|
||||
if let depModule = modules[depName], let depDylibs = depModule.sdkDylibs {
|
||||
allSdkDylibs.formUnion(depDylibs)
|
||||
}
|
||||
}
|
||||
|
||||
// Add SDK dylibs (system libraries like libz, libiconv, etc.)
|
||||
for dylib in allSdkDylibs {
|
||||
// Clean up the library name - remove 'lib' prefix if present
|
||||
let libName = dylib.hasPrefix("lib") ? String(dylib.dropFirst(3)) : dylib
|
||||
let fileRef = PBXFileReference(
|
||||
sourceTree: .sdkRoot,
|
||||
name: "lib\(libName).tbd",
|
||||
lastKnownFileType: "sourcecode.text-based-dylib-definition",
|
||||
path: "usr/lib/lib\(libName).tbd"
|
||||
)
|
||||
pbxproj.add(object: fileRef)
|
||||
let buildFile = PBXBuildFile(file: fileRef)
|
||||
pbxproj.add(object: buildFile)
|
||||
frameworksPhase.files?.append(buildFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the header search paths that this module exports to its dependents
|
||||
private func exportedHeaderPaths(for module: ModuleDefinition) -> [String] {
|
||||
var paths: [String] = []
|
||||
if let includes = module.includes, !includes.isEmpty {
|
||||
for inc in includes {
|
||||
if inc == "." {
|
||||
paths.append("$(SRCROOT)/\(module.path)")
|
||||
} else {
|
||||
paths.append("$(SRCROOT)/\(module.path)/\(inc)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No includes specified, export module's own path
|
||||
paths.append("$(SRCROOT)/\(module.path)")
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func getTarget(named name: String) -> PBXNativeTarget? {
|
||||
return targetsByName[name]
|
||||
}
|
||||
|
||||
|
||||
private func createConfigurationList(for module: ModuleDefinition, isXCFramework: Bool, modulemapPath: Path? = nil) -> XCConfigurationList {
|
||||
let debugSettings = createBuildSettings(for: module, isDebug: true, isXCFramework: isXCFramework, modulemapPath: modulemapPath)
|
||||
let releaseSettings = createBuildSettings(for: module, isDebug: false, isXCFramework: isXCFramework, modulemapPath: modulemapPath)
|
||||
|
||||
let debugConfig = XCBuildConfiguration(name: "Debug", buildSettings: debugSettings)
|
||||
let releaseConfig = XCBuildConfiguration(name: "Release", buildSettings: releaseSettings)
|
||||
|
||||
pbxproj.add(object: debugConfig)
|
||||
pbxproj.add(object: releaseConfig)
|
||||
|
||||
let configList = XCConfigurationList(
|
||||
buildConfigurations: [debugConfig, releaseConfig],
|
||||
defaultConfigurationName: "Release"
|
||||
)
|
||||
pbxproj.add(object: configList)
|
||||
|
||||
return configList
|
||||
}
|
||||
|
||||
private func createBuildSettings(for module: ModuleDefinition, isDebug: Bool, isXCFramework: Bool, modulemapPath: Path? = nil) -> BuildSettings {
|
||||
var settings: BuildSettings = [
|
||||
"PRODUCT_NAME": "$(TARGET_NAME)",
|
||||
"PRODUCT_BUNDLE_IDENTIFIER": "org.telegram.\(module.name)",
|
||||
"INFOPLIST_FILE": "",
|
||||
"SKIP_INSTALL": "YES",
|
||||
"GENERATE_INFOPLIST_FILE": "YES",
|
||||
]
|
||||
|
||||
let moduleType = ModuleType(from: module)
|
||||
|
||||
// Swift settings
|
||||
if moduleType == .swiftLibrary {
|
||||
settings["SWIFT_VERSION"] = "5.0"
|
||||
settings["DEFINES_MODULE"] = "YES"
|
||||
|
||||
if let copts = module.copts, !copts.isEmpty {
|
||||
let filtered = copts.filter { !$0.hasPrefix("-warnings") }
|
||||
if !filtered.isEmpty {
|
||||
settings["OTHER_SWIFT_FLAGS"] = "$(inherited) " + filtered.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
|
||||
if let defines = module.defines, !defines.isEmpty {
|
||||
settings["SWIFT_ACTIVE_COMPILATION_CONDITIONS"] = "$(inherited) " + defines.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
|
||||
// C/ObjC settings
|
||||
if moduleType == .objcLibrary || moduleType == .ccLibrary {
|
||||
// Always suppress deprecated warnings (e.g., OSSpinLock) and don't treat warnings as errors
|
||||
var cflags = ["-Wno-deprecated-declarations"]
|
||||
if let copts = module.copts, !copts.isEmpty {
|
||||
let filtered = copts.filter { !$0.hasPrefix("-warnings") && !$0.hasPrefix("-W") }
|
||||
cflags.append(contentsOf: filtered)
|
||||
}
|
||||
settings["OTHER_CFLAGS"] = "$(inherited) " + cflags.joined(separator: " ")
|
||||
settings["GCC_TREAT_WARNINGS_AS_ERRORS"] = "NO"
|
||||
|
||||
if let cxxopts = module.cxxopts, !cxxopts.isEmpty {
|
||||
let filtered = cxxopts.filter { !$0.hasPrefix("-std=") }
|
||||
if !filtered.isEmpty {
|
||||
settings["OTHER_CPLUSPLUSFLAGS"] = "$(inherited) " + filtered.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
|
||||
if let defines = module.defines, !defines.isEmpty {
|
||||
settings["GCC_PREPROCESSOR_DEFINITIONS"] = "$(inherited) " + defines.joined(separator: " ")
|
||||
}
|
||||
|
||||
// Always include module's own path for header search
|
||||
var headerPaths = ["$(SRCROOT)/\(module.path)"]
|
||||
if let includes = module.includes {
|
||||
for inc in includes where inc != "." {
|
||||
headerPaths.append("$(SRCROOT)/\(module.path)/\(inc)")
|
||||
}
|
||||
}
|
||||
settings["HEADER_SEARCH_PATHS"] = "$(inherited) " + headerPaths.joined(separator: " ")
|
||||
|
||||
// Use custom modulemap if provided (SPM-style)
|
||||
if let modmap = modulemapPath {
|
||||
// Get relative path from SRCROOT
|
||||
let relativePath = modmap.string.replacingOccurrences(of: outputDir.string + "/", with: "")
|
||||
settings["MODULEMAP_FILE"] = "$(SRCROOT)/\(relativePath)"
|
||||
}
|
||||
settings["DEFINES_MODULE"] = "YES"
|
||||
}
|
||||
|
||||
return settings
|
||||
}
|
||||
|
||||
private func getOrCreateGroup(for path: String) throws -> PBXGroup {
|
||||
if let existing = groupsByPath[path] {
|
||||
return existing
|
||||
}
|
||||
|
||||
let components = path.split(separator: "/").map(String.init)
|
||||
var currentGroup = mainGroup
|
||||
var currentPath = ""
|
||||
|
||||
for component in components {
|
||||
currentPath = currentPath.isEmpty ? component : currentPath + "/" + component
|
||||
|
||||
if let existing = groupsByPath[currentPath] {
|
||||
currentGroup = existing
|
||||
} else {
|
||||
let newGroup = PBXGroup(children: [], sourceTree: .group, name: component, path: component)
|
||||
pbxproj.add(object: newGroup)
|
||||
currentGroup.children.append(newGroup)
|
||||
groupsByPath[currentPath] = newGroup
|
||||
currentGroup = newGroup
|
||||
}
|
||||
}
|
||||
|
||||
return currentGroup
|
||||
}
|
||||
|
||||
private func getOrCreateFileReference(path: String, in group: PBXGroup, modulePath: String, fileName: String) throws -> PBXFileReference {
|
||||
let pathComponents = path.split(separator: "/").map(String.init)
|
||||
|
||||
var currentGroup = group
|
||||
var currentPath = modulePath
|
||||
|
||||
// Navigate/create intermediate groups
|
||||
for component in pathComponents.dropLast() {
|
||||
currentPath = currentPath + "/" + component
|
||||
if let existing = groupsByPath[currentPath] {
|
||||
currentGroup = existing
|
||||
} else {
|
||||
let newGroup = PBXGroup(children: [], sourceTree: .group, name: component, path: component)
|
||||
pbxproj.add(object: newGroup)
|
||||
currentGroup.children.append(newGroup)
|
||||
groupsByPath[currentPath] = newGroup
|
||||
currentGroup = newGroup
|
||||
}
|
||||
}
|
||||
|
||||
// Create file reference
|
||||
let fileType = lastKnownFileType(for: fileName)
|
||||
let fileRef = PBXFileReference(
|
||||
sourceTree: .group,
|
||||
name: fileName,
|
||||
lastKnownFileType: fileType,
|
||||
path: fileName
|
||||
)
|
||||
pbxproj.add(object: fileRef)
|
||||
currentGroup.children.append(fileRef)
|
||||
|
||||
return fileRef
|
||||
}
|
||||
|
||||
private func lastKnownFileType(for fileName: String) -> String {
|
||||
let ext = (fileName as NSString).pathExtension.lowercased()
|
||||
switch ext {
|
||||
case "swift": return "sourcecode.swift"
|
||||
case "m": return "sourcecode.c.objc"
|
||||
case "mm": return "sourcecode.cpp.objcpp"
|
||||
case "c": return "sourcecode.c.c"
|
||||
case "cc", "cpp", "cxx": return "sourcecode.cpp.cpp"
|
||||
case "h": return "sourcecode.c.h"
|
||||
case "hpp": return "sourcecode.cpp.h"
|
||||
case "metal": return "sourcecode.metal"
|
||||
case "json": return "text.json"
|
||||
case "plist": return "text.plist.xml"
|
||||
default: return "text"
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the relative path within a module from a source path
|
||||
/// Handles both regular paths (submodules/Foo/...) and bazel-out paths (bazel-out/.../bin/submodules/Foo/...)
|
||||
private func relativePathInModule(source: String, modulePath: String) -> String {
|
||||
if source.hasPrefix(modulePath + "/") {
|
||||
return String(source.dropFirst(modulePath.count + 1))
|
||||
} else if source.hasPrefix("bazel-out/") {
|
||||
// Generated file - extract path after the module path portion
|
||||
if let range = source.range(of: modulePath + "/") {
|
||||
return String(source[range.upperBound...])
|
||||
} else {
|
||||
return Path(source).lastComponent
|
||||
}
|
||||
} else {
|
||||
return Path(source).lastComponent
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a source file is in one of the includes directories
|
||||
private func isInIncludesDirectory(source: String, modulePath: String, includes: [String]?) -> Bool {
|
||||
guard let includes = includes, !includes.isEmpty else {
|
||||
return false
|
||||
}
|
||||
let relative = relativePathInModule(source: source, modulePath: modulePath)
|
||||
return includes.contains { inc in
|
||||
if inc == "." {
|
||||
return true // All files in module are public
|
||||
} else {
|
||||
return relative.hasPrefix(inc + "/") || relative == inc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates an explicit module.modulemap for ObjC/C++ modules (SPM-style)
|
||||
/// Returns the path to the modulemap, or nil if no public headers
|
||||
func generateModulemap(for module: ModuleDefinition) throws -> Path? {
|
||||
let moduleType = ModuleType(from: module)
|
||||
guard moduleType == .objcLibrary || moduleType == .ccLibrary else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Determine public header prefix from includes
|
||||
let publicHeaderPrefix: String
|
||||
if let includes = module.includes, !includes.isEmpty {
|
||||
let firstInclude = includes[0]
|
||||
publicHeaderPrefix = firstInclude == "." ? "" : firstInclude
|
||||
} else {
|
||||
publicHeaderPrefix = ""
|
||||
}
|
||||
|
||||
// Determine public headers
|
||||
let explicitPublicHeaders = Set(module.hdrs ?? [])
|
||||
|
||||
let allFiles = module.sources + (module.hdrs ?? []) + (module.textualHdrs ?? [])
|
||||
let allHeaders = allFiles.filter { $0.hasSuffix(".h") || $0.hasSuffix(".hpp") }
|
||||
|
||||
// Determine which headers are public
|
||||
let publicHeaders: [String]
|
||||
if !explicitPublicHeaders.isEmpty {
|
||||
publicHeaders = allHeaders.filter { explicitPublicHeaders.contains($0) }
|
||||
} else if module.includes != nil && !module.includes!.isEmpty {
|
||||
// Use helper that properly handles bazel-out paths
|
||||
publicHeaders = allHeaders.filter { header in
|
||||
isInIncludesDirectory(source: header, modulePath: module.path, includes: module.includes)
|
||||
}
|
||||
} else {
|
||||
publicHeaders = []
|
||||
}
|
||||
|
||||
guard !publicHeaders.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate explicit modulemap content (SPM-style)
|
||||
var content = "// module.modulemap for \(module.name)\n"
|
||||
content += "// Auto-generated - do not edit\n\n"
|
||||
content += "module \(module.name) {\n"
|
||||
|
||||
for header in publicHeaders {
|
||||
// Calculate the symlinked path - matches the symlink creation in buildFrameworkTarget
|
||||
// The symlink is at: outputDir + module.path + relativeToModule
|
||||
let relativeToModule = relativePathInModule(source: header, modulePath: module.path)
|
||||
let symlinkPath = outputDir + module.path + relativeToModule
|
||||
content += " header \"\(symlinkPath.string)\"\n"
|
||||
}
|
||||
|
||||
content += " export *\n"
|
||||
content += "}\n"
|
||||
|
||||
// Write modulemap to the public headers directory
|
||||
let modulemapDir: Path
|
||||
if !publicHeaderPrefix.isEmpty {
|
||||
modulemapDir = outputDir + module.path + publicHeaderPrefix
|
||||
} else {
|
||||
modulemapDir = outputDir + module.path
|
||||
}
|
||||
let modulemapPath = modulemapDir + "module.modulemap"
|
||||
|
||||
try modulemapDir.mkpath()
|
||||
try modulemapPath.write(content)
|
||||
|
||||
// Track this file so it doesn't get cleaned up
|
||||
symlinkManager.markFile(modulemapPath)
|
||||
|
||||
return modulemapPath
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import Foundation
|
||||
import ArgumentParser
|
||||
import PathKit
|
||||
|
||||
struct MakeProject: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "MakeProject",
|
||||
abstract: "Generate Xcode project from Bazel module definitions"
|
||||
)
|
||||
|
||||
@Option(name: .long, help: "Path to modules JSON file")
|
||||
var modulesJson: String = "bazel-bin/Telegram/spm_build_root_modules.json"
|
||||
|
||||
@Option(name: .long, help: "Output directory for generated project")
|
||||
var output: String = "xcode-files"
|
||||
|
||||
func run() throws {
|
||||
// Determine project root (where we find the modules JSON)
|
||||
let currentDir = Path.current
|
||||
var projectRoot = currentDir
|
||||
|
||||
// Walk up to find project root (contains bazel-bin or the modules file)
|
||||
var searchDir = currentDir
|
||||
for _ in 0..<5 {
|
||||
if (searchDir + modulesJson).exists {
|
||||
projectRoot = searchDir
|
||||
break
|
||||
}
|
||||
searchDir = searchDir.parent()
|
||||
}
|
||||
|
||||
let modulesPath = projectRoot + modulesJson
|
||||
let outputDir = projectRoot + output
|
||||
|
||||
guard modulesPath.exists else {
|
||||
print("Error: Modules JSON not found at \(modulesPath)")
|
||||
print("Run 'bazel build //Telegram:spm_build_root' first")
|
||||
throw ExitCode.failure
|
||||
}
|
||||
|
||||
let generator = ProjectGenerator(
|
||||
modulesPath: modulesPath,
|
||||
outputDir: outputDir,
|
||||
projectRoot: projectRoot
|
||||
)
|
||||
|
||||
try generator.generate()
|
||||
}
|
||||
}
|
||||
|
||||
MakeProject.main()
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"bundle_id": "ph.telegra.Telegraph",
|
||||
"api_id": "8",
|
||||
"api_hash": "YOUR_API_HASH",
|
||||
"api_hash": "7245de8e747a0d6fbe11f7cc14fcc0bb",
|
||||
"team_id": "C67CF9S4VU",
|
||||
"app_center_id": "4c816ed0-df83-423c-846b-a0a8467dc7d2",
|
||||
"is_internal_build": "false",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"bundle_id": "ph.telegra.Telegraph",
|
||||
"api_id": "8",
|
||||
"api_hash": "YOUR_API_HASH",
|
||||
"api_hash": "7245de8e747a0d6fbe11f7cc14fcc0bb",
|
||||
"team_id": "C67CF9S4VU",
|
||||
"app_center_id": "0",
|
||||
"is_internal_build": "false",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
|
||||
telegram_bundle_id = "ph.telegra.Telegraph"
|
||||
telegram_api_id = "8"
|
||||
telegram_api_hash = "YOUR_API_HASH"
|
||||
telegram_api_hash = "7245de8e747a0d6fbe11f7cc14fcc0bb"
|
||||
telegram_team_id = "C67CF9S4VU"
|
||||
telegram_app_center_id = "0"
|
||||
telegram_is_internal_build = "false"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,7 @@ export DISTRIBUTION_CODE_SIGN_IDENTITY="iPhone Distribution: Digital Fortress LL
|
||||
export DEVELOPMENT_TEAM="C67CF9S4VU"
|
||||
|
||||
export API_ID="8"
|
||||
export API_HASH="YOUR_API_HASH"
|
||||
export API_HASH="7245de8e747a0d6fbe11f7cc14fcc0bb"
|
||||
|
||||
export BUNDLE_ID="ph.telegra.Telegraph"
|
||||
export APP_CENTER_ID="0"
|
||||
|
||||
Reference in New Issue
Block a user