From 66c244f5401c12dbb2e174f7f595aa36ae611fe2 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Sun, 30 Mar 2025 02:06:26 +0400 Subject: [PATCH] Add LSP support --- .gitignore | 1 + build-system/Make/Make.py | 18 +- build-system/Make/ProjectGeneration.py | 6 +- build-system/XcodeParse/.gitignore | 8 + .../xcschemes/XcodeParse.xcscheme | 88 ++++++ build-system/XcodeParse/Package.resolved | 51 ++++ build-system/XcodeParse/Package.swift | 22 ++ build-system/XcodeParse/Sources/main.swift | 288 ++++++++++++++++++ 8 files changed, 478 insertions(+), 4 deletions(-) create mode 100644 build-system/XcodeParse/.gitignore create mode 100644 build-system/XcodeParse/.swiftpm/xcode/xcshareddata/xcschemes/XcodeParse.xcscheme create mode 100644 build-system/XcodeParse/Package.resolved create mode 100644 build-system/XcodeParse/Package.swift create mode 100644 build-system/XcodeParse/Sources/main.swift diff --git a/.gitignore b/.gitignore index b74f0cf625..b761e03cb0 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,4 @@ submodules/OpenSSLEncryptionProvider/SharedHeaders/* submodules/TelegramCore/FlatSerialization/Sources/* buildServer.json .build/** +Telegram.LSP.json diff --git a/build-system/Make/Make.py b/build-system/Make/Make.py index e9403e27d5..d1078efef7 100644 --- a/build-system/Make/Make.py +++ b/build-system/Make/Make.py @@ -547,7 +547,7 @@ def generate_project(bazel, arguments): call_executable(['killall', 'Xcode'], check_result=False) - generate( + xcodeproj_path = generate( build_environment=bazel_command_line.build_environment, disable_extensions=disable_extensions, disable_provisioning_profiles=disable_provisioning_profiles, @@ -558,6 +558,22 @@ def generate_project(bazel, arguments): target_name=target_name ) + if target_name == "Telegram": + run_executable_with_output('swift', arguments=[ + 'run', + '-c', + 'release', + '--package-path', + 'build-system/XcodeParse', + 'XcodeParse', + '--project-path', + xcodeproj_path, + '--output-path', + 'Telegram/Telegram.LSP.json' + ], check_result=True) + + call_executable(['open', xcodeproj_path]) + def build(bazel, arguments): bazel_command_line = BazelCommandLine( diff --git a/build-system/Make/ProjectGeneration.py b/build-system/Make/ProjectGeneration.py index a8759a89e2..17d436a33d 100644 --- a/build-system/Make/ProjectGeneration.py +++ b/build-system/Make/ProjectGeneration.py @@ -51,8 +51,8 @@ def generate_xcodeproj(build_environment: BuildEnvironment, disable_extensions, call_executable(bazel_generate_arguments) xcodeproj_path = '{}.xcodeproj'.format(app_target_spec.replace(':', '/')) - call_executable(['open', xcodeproj_path]) + return xcodeproj_path -def generate(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, include_release, generate_dsym, configuration_path, bazel_app_arguments, target_name): - generate_xcodeproj(build_environment, disable_extensions, disable_provisioning_profiles, include_release, generate_dsym, configuration_path, bazel_app_arguments, target_name) +def generate(build_environment: BuildEnvironment, disable_extensions, disable_provisioning_profiles, include_release, generate_dsym, configuration_path, bazel_app_arguments, target_name) -> str: + return generate_xcodeproj(build_environment, disable_extensions, disable_provisioning_profiles, include_release, generate_dsym, configuration_path, bazel_app_arguments, target_name) diff --git a/build-system/XcodeParse/.gitignore b/build-system/XcodeParse/.gitignore new file mode 100644 index 0000000000..0023a53406 --- /dev/null +++ b/build-system/XcodeParse/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/build-system/XcodeParse/.swiftpm/xcode/xcshareddata/xcschemes/XcodeParse.xcscheme b/build-system/XcodeParse/.swiftpm/xcode/xcshareddata/xcschemes/XcodeParse.xcscheme new file mode 100644 index 0000000000..a390e9a0b5 --- /dev/null +++ b/build-system/XcodeParse/.swiftpm/xcode/xcshareddata/xcschemes/XcodeParse.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build-system/XcodeParse/Package.resolved b/build-system/XcodeParse/Package.resolved new file mode 100644 index 0000000000..602d5d029e --- /dev/null +++ b/build-system/XcodeParse/Package.resolved @@ -0,0 +1,51 @@ +{ + "originHash" : "17beff37a9aac4bf93a9e4b944ed029007695fe907b5a4eaa87576689f695b1b", + "pins" : [ + { + "identity" : "aexml", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tadija/AEXML.git", + "state" : { + "revision" : "db806756c989760b35108146381535aec231092b", + "version" : "4.7.0" + } + }, + { + "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.git", + "state" : { + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" + } + }, + { + "identity" : "xcodeproj", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/XcodeProj.git", + "state" : { + "revision" : "128d90e4633a8e6941586dea75426e177dfb92e6", + "version" : "9.0.0" + } + } + ], + "version" : 3 +} diff --git a/build-system/XcodeParse/Package.swift b/build-system/XcodeParse/Package.swift new file mode 100644 index 0000000000..20207cc8f9 --- /dev/null +++ b/build-system/XcodeParse/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "XcodeParse", + platforms: [.macOS(.v11)], + dependencies: [ + .package(url: "https://github.com/tuist/XcodeProj.git", exact: "9.0.0"), + .package(url: "https://github.com/apple/swift-argument-parser.git", exact: "1.5.0"), + ], + targets: [ + .executableTarget( + name: "XcodeParse", + dependencies: [ + "XcodeProj", + .product(name: "ArgumentParser", package: "swift-argument-parser") + ] + ), + ] +) diff --git a/build-system/XcodeParse/Sources/main.swift b/build-system/XcodeParse/Sources/main.swift new file mode 100644 index 0000000000..4dba653ce6 --- /dev/null +++ b/build-system/XcodeParse/Sources/main.swift @@ -0,0 +1,288 @@ +import Foundation +import Darwin +import XcodeProj +import PathKit +import ArgumentParser + +// Custom error types for the command +enum XcodeParseError: Error, LocalizedError { + case missingBuildSetting(String) + case unresolvableBuildSetting(String, String) + case swiftFlagProcessingError(String, Error) + case unresolvableSwiftFlag(String, String) + + var errorDescription: String? { + switch self { + case .missingBuildSetting(let setting): + return "Project does not contain required build setting: \(setting)" + case .unresolvableBuildSetting(let name, let value): + return "Could not resolve build setting value: \(name) = \(value)" + case .swiftFlagProcessingError(let target, let error): + return "Error processing swift flags for \(target): \(error)" + case .unresolvableSwiftFlag(let target, let flag): + return "Unresolved variable in swift flags for \(target): \(flag)" + } + } +} + +struct XcodeParse: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "xcodeparse", + abstract: "Extract file sets and Swift flags from an Xcode project", + version: "1.0.0" + ) + + // Extract all variable references like $(VARIABLE_NAME) + // private static let variablePatternRegex = try! NSRegularExpression(pattern: "\\$\\(([^)]+)\\)", options: []) + + // Update the class-level regex to handle simple variables + private static let simpleVariableRegex = try! NSRegularExpression(pattern: "\\$\\(([^$)]+)\\)", options: []) + + // Add another regex for variables that contain exactly one nested variable + private static let nestedVariableRegex = try! NSRegularExpression(pattern: "\\$\\(([^$)]*\\$\\([^)]+\\)[^)]*)\\)", options: []) + + @Option(name: .shortAndLong, help: "Path to the Xcode project (.xcodeproj) file") + var projectPath: String + + @Option(name: .shortAndLong, help: "Output path for the JSON file") + var outputPath: String + + func run() throws { + let projectPathObj = Path(projectPath) + + let xcodeproj = try XcodeProj(path: projectPathObj) + + func absolutePath(file: PBXFileElement) -> String? { + guard let path = file.path else { + return nil + } + if let parent = file.parent { + if let parentPath = absolutePath(file: parent) { + return parentPath + "/" + path + } else { + return path + } + } else { + return path + } + } + + func localPath(projectRoot: String, file: PBXFileElement) -> String? { + guard let path = absolutePath(file: file) else { + return nil + } + if path.hasPrefix(projectRoot) { + return String(path[path.index(path.startIndex, offsetBy: projectRoot.count)...]) + } else { + return path + } + } + + var rawVariables: [String: String] = [:] + let requiredBuildSettings: [String] = ["SRCROOT", "PROJECT_DIR", "BAZEL_OUT"] + for buildConfiguration in xcodeproj.pbxproj.buildConfigurations { + if buildConfiguration.name == "Debug" { + for name in requiredBuildSettings { + if let value = buildConfiguration.buildSettings[name]?.stringValue { + rawVariables[name] = value + } + } + } + } + + for name in requiredBuildSettings { + if rawVariables[name] == nil { + throw XcodeParseError.missingBuildSetting(name) + } + } + + while true { + var hasSubstitutions: Bool = false + inner: for (name, value) in rawVariables { + for (otherName, otherValue) in rawVariables { + if name == otherName { + continue + } + if value.contains("$(\(otherName))") { + rawVariables[name] = value.replacingOccurrences(of: "$(\(otherName))", with: otherValue) + hasSubstitutions = true + break inner + } + } + } + if !hasSubstitutions { + break + } + } + + for (name, value) in rawVariables { + if value.contains("$(") { + throw XcodeParseError.unresolvableBuildSetting(name, value) + } + } + + rawVariables["ENABLE_PREVIEWS"] = "" + + let variables = rawVariables + + let projectRoot = variables["SRCROOT"]! + "/" + + enum ShlexError: Error { + case unmatchedQuote + } + + func shlexSplit(_ input: String) throws -> [String] { + var tokens = [String]() + var current = "" + var inSingleQuote = false + var inDoubleQuote = false + var escapeNext = false + + for char in input { + if escapeNext { + current.append(char) + escapeNext = false + continue + } + + if char == "\\" { + // In single quotes, backslashes are taken literally. + if inSingleQuote { + current.append(char) + } else { + escapeNext = true + } + } else if char == "'" && !inDoubleQuote { + inSingleQuote.toggle() + } else if char == "\"" && !inSingleQuote { + inDoubleQuote.toggle() + } else if char.isWhitespace && !inSingleQuote && !inDoubleQuote { + if !current.isEmpty { + tokens.append(current) + current = "" + } + } else { + current.append(char) + } + } + + if escapeNext { + // A trailing backslash is taken as a literal backslash. + current.append("\\") + } + + if inSingleQuote || inDoubleQuote { + throw ShlexError.unmatchedQuote + } + + if !current.isEmpty { + tokens.append(current) + } + + return tokens + } + + struct FileSet { + var files: [String] + var swiftFlags: [String] + } + + var fileSets: [FileSet] = [] + + for target in xcodeproj.pbxproj.nativeTargets { + var files: [String] = [] + for sourceFile in try target.sourceFiles() { + if let path = localPath(projectRoot: projectRoot, file: sourceFile) { + files.append(path) + } + } + + var swiftFlags: [String] = [] + if let buildConfigurationList = target.buildConfigurationList { + for buildConfiguration in buildConfigurationList.buildConfigurations { + if buildConfiguration.name == "Debug" { + if let swiftFlagsString = buildConfiguration.buildSettings["OTHER_SWIFT_FLAGS[sdk=iphonesimulator*]"]?.stringValue { + do { + swiftFlags = try shlexSplit(swiftFlagsString) + } catch let error { + throw XcodeParseError.swiftFlagProcessingError(target.name, error) + } + } + + for i in 0 ..< swiftFlags.count { + if swiftFlags[i].contains("$(") { + var flag = swiftFlags[i] + var madeProgress = true + + // Keep resolving variables until no more progress can be made + while flag.contains("$(") && madeProgress { + madeProgress = false + + let nsString = flag as NSString + let matches = XcodeParse.simpleVariableRegex.matches(in: flag, options: [], range: NSRange(location: 0, length: nsString.length)) + + // Try to resolve variables that don't contain other variables + for match in matches.reversed() { + let variableRange = match.range(at: 1) + let variableName = nsString.substring(with: variableRange) + + // Skip this variable if it contains another variable reference + // (we'll get it in a later iteration after inner variables are resolved) + if variableName.contains("$(") { + continue + } + + // Look up the variable directly by name + var variableValue: String? = variables[variableName] + + // If not found in variables, check build settings + if variableValue == nil, let value = buildConfiguration.buildSettings[variableName]?.stringValue { + variableValue = value + } + + // If variable found, do the replacement + if let value = variableValue { + let fullRange = match.range(at: 0) // The full $(VARIABLE_NAME) pattern + flag = (flag as NSString).replacingCharacters(in: fullRange, with: value) + madeProgress = true + } + } + } + + // Check if there are still unresolved variables + if flag.contains("$(") { + throw XcodeParseError.unresolvableSwiftFlag(target.name, flag) + } + + swiftFlags[i] = flag + } + } + } + } + } + + if !files.isEmpty && !swiftFlags.isEmpty { + fileSets.append(FileSet( + files: files, + swiftFlags: swiftFlags + )) + } + } + + do { + let fileSetDicts = fileSets.map { fileSet -> [String: Any] in + return [ + "files": fileSet.files, + "swiftFlags": fileSet.swiftFlags + ] + } + let jsonData = try JSONSerialization.data(withJSONObject: fileSetDicts, options: .prettyPrinted) + try jsonData.write(to: URL(fileURLWithPath: outputPath)) + print("Successfully wrote output to \(outputPath)") + } catch let error { + throw error + } + } +} + +XcodeParse.main()