build-system

This commit is contained in:
Isaac 2025-12-01 22:13:16 +08:00
parent 0055396d4d
commit 557af859bb
10 changed files with 871 additions and 1 deletions

View file

@ -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": []
}

1
.gitignore vendored
View file

@ -74,4 +74,5 @@ buildServer.json
Telegram.LSP.json
**/.build/**
spm-files
xcode-files
.bsp/**

View file

@ -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
}

View file

@ -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"),
]
)
]
)

View file

@ -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)
}

View file

@ -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)")
}
}

View file

@ -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)
}
}

View file

@ -0,0 +1,114 @@
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() {
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
}
}
}

View file

@ -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"
}
}
}

View file

@ -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()