From 557af859bb32f10be39d859d94b2468656a2c1a8 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Mon, 1 Dec 2025 22:13:16 +0800 Subject: [PATCH] build-system --- .claude/settings.local.json | 8 +- .gitignore | 1 + build-system/MakeProject/Package.resolved | 50 +++ build-system/MakeProject/Package.swift | 26 ++ .../Sources/MakeProject/ModuleJSON.swift | 54 +++ .../MakeProject/ProjectGenerator.swift | 139 +++++++ .../Sources/MakeProject/SchemeGenerator.swift | 74 ++++ .../Sources/MakeProject/SymlinkManager.swift | 114 ++++++ .../Sources/MakeProject/TargetBuilder.swift | 355 ++++++++++++++++++ .../Sources/MakeProject/main.swift | 51 +++ 10 files changed, 871 insertions(+), 1 deletion(-) create mode 100644 build-system/MakeProject/Package.resolved create mode 100644 build-system/MakeProject/Package.swift create mode 100644 build-system/MakeProject/Sources/MakeProject/ModuleJSON.swift create mode 100644 build-system/MakeProject/Sources/MakeProject/ProjectGenerator.swift create mode 100644 build-system/MakeProject/Sources/MakeProject/SchemeGenerator.swift create mode 100644 build-system/MakeProject/Sources/MakeProject/SymlinkManager.swift create mode 100644 build-system/MakeProject/Sources/MakeProject/TargetBuilder.swift create mode 100644 build-system/MakeProject/Sources/MakeProject/main.swift diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 3a27f2450e..e6863b84f5 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -9,7 +9,13 @@ "Skill(superpowers:brainstorming)", "Bash(cat:*)", "Bash(python3:*)", - "Skill(superpowers:writing-plans)" + "Skill(superpowers:writing-plans)", + "Skill(superpowers:subagent-driven-development)", + "Bash(test:*)", + "Bash(swift package:*)", + "Bash(.build/debug/MakeProject:*)", + "Bash(./build-system/MakeProject/.build/debug/MakeProject)", + "Bash(xcodebuild:*)" ], "deny": [] } diff --git a/.gitignore b/.gitignore index 0e855890d9..bf06cade52 100644 --- a/.gitignore +++ b/.gitignore @@ -74,4 +74,5 @@ buildServer.json Telegram.LSP.json **/.build/** spm-files +xcode-files .bsp/** \ No newline at end of file diff --git a/build-system/MakeProject/Package.resolved b/build-system/MakeProject/Package.resolved new file mode 100644 index 0000000000..d9f1a9dc65 --- /dev/null +++ b/build-system/MakeProject/Package.resolved @@ -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 +} diff --git a/build-system/MakeProject/Package.swift b/build-system/MakeProject/Package.swift new file mode 100644 index 0000000000..8e81f4a781 --- /dev/null +++ b/build-system/MakeProject/Package.swift @@ -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"), + ] + ) + ] +) diff --git a/build-system/MakeProject/Sources/MakeProject/ModuleJSON.swift b/build-system/MakeProject/Sources/MakeProject/ModuleJSON.swift new file mode 100644 index 0000000000..fa47427c75 --- /dev/null +++ b/build-system/MakeProject/Sources/MakeProject/ModuleJSON.swift @@ -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) +} diff --git a/build-system/MakeProject/Sources/MakeProject/ProjectGenerator.swift b/build-system/MakeProject/Sources/MakeProject/ProjectGenerator.swift new file mode 100644 index 0000000000..93d01a4845 --- /dev/null +++ b/build-system/MakeProject/Sources/MakeProject/ProjectGenerator.swift @@ -0,0 +1,139 @@ +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 + let validModules = modules.filter { name, module in + let sources = module.sources.filter { !$0.hasSuffix(".a") } + return !sources.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", + "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)") + } +} diff --git a/build-system/MakeProject/Sources/MakeProject/SchemeGenerator.swift b/build-system/MakeProject/Sources/MakeProject/SchemeGenerator.swift new file mode 100644 index 0000000000..1c1b7632ae --- /dev/null +++ b/build-system/MakeProject/Sources/MakeProject/SchemeGenerator.swift @@ -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) + } +} diff --git a/build-system/MakeProject/Sources/MakeProject/SymlinkManager.swift b/build-system/MakeProject/Sources/MakeProject/SymlinkManager.swift new file mode 100644 index 0000000000..119e894d17 --- /dev/null +++ b/build-system/MakeProject/Sources/MakeProject/SymlinkManager.swift @@ -0,0 +1,114 @@ +import Foundation +import PathKit + +class SymlinkManager { + let outputDir: Path + let projectRoot: Path + private var previousFiles: Set = [] + private var currentFiles: Set = [] + + 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() { + if item.lastComponent == ".build" { 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 + } + } +} diff --git a/build-system/MakeProject/Sources/MakeProject/TargetBuilder.swift b/build-system/MakeProject/Sources/MakeProject/TargetBuilder.swift new file mode 100644 index 0000000000..fc47ecaa1f --- /dev/null +++ b/build-system/MakeProject/Sources/MakeProject/TargetBuilder.swift @@ -0,0 +1,355 @@ +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 + } + + 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 + } + + // Skip empty modules + let sourceFiles = module.sources.filter { !$0.hasSuffix(".a") } + if sourceFiles.isEmpty && moduleType != .xcframework { + return nil + } + + switch moduleType { + case .xcframework: + return try buildXCFrameworkTarget(for: module) + case .swiftLibrary, .objcLibrary, .ccLibrary: + 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 headerRefs: [PBXFileReference] = [] + + 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 + ) + + if source.hasSuffix(".h") || source.hasSuffix(".hpp") { + headerRefs.append(fileRef) + } else if !source.hasSuffix(".inc") { + sourceRefs.append(fileRef) + } + } + + // 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) + + // Headers build phase for ObjC/C++ + if moduleType == .objcLibrary || moduleType == .ccLibrary { + let headersBuildPhase = PBXHeadersBuildPhase( + files: headerRefs.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 + let configList = createConfigurationList(for: module, isXCFramework: false) + + 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 + } + + 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 + } + + for depName in deps { + guard let depTarget = targetsByName[depName] else { continue } + + // Add target dependency + let dependency = PBXTargetDependency(target: depTarget) + pbxproj.add(object: dependency) + target.dependencies.append(dependency) + } + + // Add SDK frameworks + if let sdkFrameworks = module.sdkFrameworks { + for framework in sdkFrameworks { + 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) + } + } + } + } + + func getTarget(named name: String) -> PBXNativeTarget? { + return targetsByName[name] + } + + private func createConfigurationList(for module: ModuleDefinition, isXCFramework: Bool) -> XCConfigurationList { + let debugSettings = createBuildSettings(for: module, isDebug: true, isXCFramework: isXCFramework) + let releaseSettings = createBuildSettings(for: module, isDebug: false, isXCFramework: isXCFramework) + + 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) -> 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" + + 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 { + if let copts = module.copts, !copts.isEmpty { + let filtered = copts.filter { !$0.hasPrefix("-warnings") && !$0.hasPrefix("-W") } + if !filtered.isEmpty { + settings["OTHER_CFLAGS"] = "$(inherited) " + filtered.joined(separator: " ") + } + } + + 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: " ") + } + + if let includes = module.includes, !includes.isEmpty { + let paths = includes.map { inc -> String in + if inc == "." { + return "$(SRCROOT)/\(module.path)" + } + return "$(SRCROOT)/\(module.path)/\(inc)" + } + settings["HEADER_SEARCH_PATHS"] = "$(inherited) " + paths.joined(separator: " ") + } + } + + 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" + } + } +} diff --git a/build-system/MakeProject/Sources/MakeProject/main.swift b/build-system/MakeProject/Sources/MakeProject/main.swift new file mode 100644 index 0000000000..31fd3656c2 --- /dev/null +++ b/build-system/MakeProject/Sources/MakeProject/main.swift @@ -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()