GLEGram 12.5 — Initial public release

Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
This commit is contained in:
Leeksov
2026-04-06 09:48:12 +03:00
commit 4647310322
39685 changed files with 11052678 additions and 0 deletions
@@ -0,0 +1,126 @@
import Foundation
import ProjectSpec
import XcodeProj
public class BreakpointGenerator {
let project: Project
public init(project: Project) {
self.project = project
}
func generateBreakpointList() throws -> XCBreakpointList? {
let breakpoints = project.breakpoints
guard !breakpoints.isEmpty else {
return nil
}
return XCBreakpointList(type: "4", version: "2.0", breakpoints: try breakpoints.map({ try generateBreakpointProxy($0) }))
}
private func generateBreakpointProxy(_ breakpoint: Breakpoint) throws -> XCBreakpointList.BreakpointProxy {
let breakpointExtensionID: BreakpointExtensionID
var filePath: String?
var line: String?
var column: String?
var scope: String?
var stopOnStyle: String?
var symbol: String?
var module: String?
switch breakpoint.type {
case let .file(path, lineNumber, columnNumber):
breakpointExtensionID = .file
filePath = path
line = String(lineNumber)
column = columnNumber.map(String.init)
case let .exception(exception):
breakpointExtensionID = .exception
scope = exception.scope.rawValue
stopOnStyle = exception.stopOnStyle.rawValue
case .swiftError:
breakpointExtensionID = .swiftError
case .openGLError:
breakpointExtensionID = .openGLError
case let .symbolic(symbolName, moduleName):
breakpointExtensionID = .symbolic
symbol = symbolName
module = moduleName
case .ideConstraintError:
breakpointExtensionID = .ideConstraintError
case .ideTestFailure:
breakpointExtensionID = .ideTestFailure
case .runtimeIssue:
breakpointExtensionID = .runtimeIssue
}
let xcbreakpoint = XCBreakpointList.BreakpointProxy.BreakpointContent(
enabled: breakpoint.enabled,
ignoreCount: String(breakpoint.ignoreCount),
continueAfterRunningActions: breakpoint.continueAfterRunningActions,
filePath: filePath,
startingColumn: column,
endingColumn: column,
startingLine: line,
endingLine: line,
symbol: symbol,
module: module,
scope: scope,
stopOnStyle: stopOnStyle,
condition: breakpoint.condition,
actions: try breakpoint.actions.map { try generateBreakpointActionProxy($0) }
)
return XCBreakpointList.BreakpointProxy(
breakpointExtensionID: breakpointExtensionID,
breakpointContent: xcbreakpoint
)
}
private func generateBreakpointActionProxy(_ breakpointAction: Breakpoint.Action) throws -> XCBreakpointList.BreakpointProxy.BreakpointContent.BreakpointActionProxy {
let actionExtensionID: BreakpointActionExtensionID
var consoleCommand: String?
var message: String?
var conveyanceType: String?
var command: String?
var arguments: String?
var waitUntilDone: Bool?
var script: String?
var soundName: String?
switch breakpointAction {
case let .debuggerCommand(command):
actionExtensionID = .debuggerCommand
consoleCommand = command
case let .log(log):
actionExtensionID = .log
message = log.message
conveyanceType = log.conveyanceType.rawValue
case let .shellCommand(commandPath, commandArguments, waitUntilCommandDone):
actionExtensionID = .shellCommand
command = commandPath
arguments = commandArguments
waitUntilDone = waitUntilCommandDone
case .graphicsTrace:
actionExtensionID = .graphicsTrace
case let .appleScript(appleScript):
actionExtensionID = .appleScript
script = appleScript
case let .sound(sound):
actionExtensionID = .sound
soundName = sound.rawValue
}
let xcaction = XCBreakpointList.BreakpointProxy.BreakpointContent.BreakpointActionProxy.ActionContent(
consoleCommand: consoleCommand,
message: message,
conveyanceType: conveyanceType,
command: command,
arguments: arguments,
waitUntilDone: waitUntilDone,
script: script,
soundName: soundName
)
return XCBreakpointList.BreakpointProxy.BreakpointContent.BreakpointActionProxy(
actionExtensionID: actionExtensionID,
actionContent: xcaction
)
}
}
@@ -0,0 +1,170 @@
//
// CarthageDependencyResolver.swift
// XcodeGenKit
//
// Created by Rogerio de Paula Assis on 2/4/19.
//
import Foundation
import ProjectSpec
import PathKit
public struct ResolvedCarthageDependency: Equatable, Hashable {
let dependency: Dependency
let isFromTopLevelTarget: Bool
}
public class CarthageDependencyResolver {
static func getBuildPath(_ project: Project) -> String {
return project.options.carthageBuildPath ?? "Carthage/Build"
}
/// Carthage's base build path as specified by the
/// project's `SpecOptions`, or `Carthage/Build` by default
var buildPath: String {
return CarthageDependencyResolver.getBuildPath(project)
}
/// Carthage's executable path as specified by the
/// project's `SpecOptions`, or `carthage` by default
var executable: String {
project.options.carthageExecutablePath ?? "carthage"
}
private let project: Project
let versionLoader: CarthageVersionLoader
init(project: Project) {
self.project = project
versionLoader = CarthageVersionLoader(buildPath: project.basePath + CarthageDependencyResolver.getBuildPath(project))
}
/// Carthage's build path for the given platform
func buildPath(for platform: Platform, linkType: Dependency.CarthageLinkType) -> String {
switch linkType {
case .static:
return "\(buildPath)/\(platform.carthageName)/Static"
case .dynamic:
return "\(buildPath)/\(platform.carthageName)"
}
}
/// Fetches all carthage dependencies for a given target
func dependencies(for topLevelTarget: Target) -> [ResolvedCarthageDependency] {
// this is used to resolve cyclical target dependencies
var visitedTargets: Set<String> = []
var frameworks: Set<ResolvedCarthageDependency> = []
var isTopLevelTarget = true
var queue: [ProjectTarget] = [topLevelTarget]
while !queue.isEmpty {
// projectTarget is not the top level target after the first loop ends
defer { isTopLevelTarget = false }
let projectTarget = queue.removeFirst()
if visitedTargets.contains(projectTarget.name) {
continue
}
if let target = projectTarget as? Target {
for dependency in target.dependencies {
if case (false, false) = (dependency.link, dependency.embed ?? topLevelTarget.shouldEmbedCarthageDependencies) {
continue
}
guard !frameworks.contains(where: { $0.dependency == dependency }) else {
continue
}
switch dependency.type {
case .carthage(let findFrameworks, _):
let findFrameworks = findFrameworks ?? project.options.findCarthageFrameworks
if findFrameworks {
relatedDependencies(for: dependency, in: target.platform)
.filter { dependency in
!frameworks.contains(where: { $0.dependency == dependency })
}
.forEach {
frameworks.insert(.init(
dependency: $0,
isFromTopLevelTarget: isTopLevelTarget
))
}
} else {
frameworks.insert(.init(
dependency: dependency,
isFromTopLevelTarget: isTopLevelTarget
))
}
case .target:
if let projectTarget = project.getProjectTarget(dependency.reference) {
if let dependencyTarget = projectTarget as? Target {
if topLevelTarget.platform == dependencyTarget.platform {
queue.append(projectTarget)
}
} else {
queue.append(projectTarget)
}
}
default:
break
}
}
} else if let aggregateTarget = projectTarget as? AggregateTarget {
for dependencyName in aggregateTarget.targets {
if let projectTarget = project.getProjectTarget(dependencyName) {
queue.append(projectTarget)
}
}
}
visitedTargets.update(with: projectTarget.name)
}
return frameworks.sorted(by: { $0.dependency.reference < $1.dependency.reference })
}
/// Reads the .version file generated for a given Carthage dependency
/// and returns a list of its related dependencies including self
func relatedDependencies(for dependency: Dependency, in platform: Platform) -> [Dependency] {
guard
case .carthage = dependency.type,
let versionFile = try? versionLoader.getVersionFile(for: dependency.reference) else {
// No .version file or we've been unable to parse
// so fail gracefully by returning the main dependency
return [dependency]
}
return versionFile.frameworks(for: platform)
.map { Dependency(
type: dependency.type,
reference: $0,
embed: dependency.embed,
codeSign: dependency.codeSign,
link: dependency.link,
implicit: dependency.implicit,
weakLink: dependency.weakLink
) }
.sorted(by: { $0.reference < $1.reference })
}
}
extension Platform {
public var carthageName: String {
switch self {
case .auto:
// This is a dummy value
return "auto"
case .iOS:
return "iOS"
case .tvOS:
return "tvOS"
case .watchOS:
return "watchOS"
case .macOS:
return "Mac"
case .visionOS:
// This is a dummy value because Carthage doesn't support visionOS.
return "visionOS"
}
}
}
@@ -0,0 +1,87 @@
//
// CarthageVersionLoader.swift
// XcodeGenKit
//
// Created by Yonas Kolb on 24/3/19.
//
import Foundation
import PathKit
import ProjectSpec
class Mutex<T> {
var value: T
var semaphore: DispatchSemaphore = DispatchSemaphore(value: 1)
init(_ value: T) {
self.value = value
}
func get<U>(closure: (inout T) throws -> (U)) rethrows -> U {
semaphore.wait()
defer { semaphore.signal() }
return try closure(&value)
}
func get(closure: (inout T) -> Void) {
semaphore.wait()
closure(&value)
semaphore.signal()
}
}
// Note: this class can be accessed on multiple threads. It must therefore stay thread-safe.
class CarthageVersionLoader {
private let buildPath: Path
private var cachedFilesMutex: Mutex<[String: CarthageVersionFile]> = Mutex([:])
init(buildPath: Path) {
self.buildPath = buildPath
}
func getVersionFile(for dependency: String) throws -> CarthageVersionFile {
return try cachedFilesMutex.get { cachedFiles in
if let versionFile = cachedFiles[dependency] {
return versionFile
}
let filePath = buildPath + ".\(dependency).version"
let data = try filePath.read()
let carthageVersionFile = try JSONDecoder().decode(CarthageVersionFile.self, from: data)
cachedFiles[dependency] = carthageVersionFile
return carthageVersionFile
}
}
}
struct CarthageVersionFile: Decodable {
private struct Reference: Decodable, Equatable {
public let name: String
public let hash: String
}
private let data: [Platform: [String]]
internal init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Platform.self)
data = try Platform.allCases.reduce(into: [:]) { data, platform in
let references = try container.decodeIfPresent([Reference].self, forKey: platform) ?? []
let frameworks = Set(references.map { $0.name }).sorted()
data[platform] = frameworks
}
}
}
extension Platform: Swift.CodingKey {
public var stringValue: String {
carthageName
}
}
extension CarthageVersionFile {
func frameworks(for platform: Platform) -> [String] {
data[platform] ?? []
}
}
@@ -0,0 +1,56 @@
import Foundation
import PathKit
import ProjectSpec
import XcodeProj
public class FileWriter {
let project: Project
public init(project: Project) {
self.project = project
}
public func writeXcodeProject(_ xcodeProject: XcodeProj, to projectPath: Path? = nil) throws {
let projectPath = projectPath ?? project.defaultProjectPath
let tempPath = try Path.processUniqueTemporary() + "XcodeGen"
try? tempPath.delete()
if projectPath.exists {
try projectPath.copy(tempPath)
}
try xcodeProject.write(path: tempPath, override: true)
try? projectPath.delete()
try tempPath.copy(projectPath)
try? tempPath.delete()
}
public func writePlists() throws {
let infoPlistGenerator = InfoPlistGenerator()
for target in project.targets {
// write Info.plist
if let plist = target.info {
let properties = infoPlistGenerator.generateProperties(for: target).merged(plist.properties)
try writePlist(properties, path: plist.path)
}
// write entitlements
if let plist = target.entitlements {
try writePlist(plist.properties, path: plist.path)
}
}
}
private func writePlist(_ plist: [String: Any], path: String) throws {
let path = project.basePath + path
if path.exists, let data: Data = try? path.read(),
let existingPlist = (try? PropertyListSerialization.propertyList(from: data, format: nil)) as? [String: Any], NSDictionary(dictionary: plist).isEqual(to: existingPlist) {
// file is the same
return
}
let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)
try? path.delete()
try path.parent().mkpath()
try path.write(data)
}
}
@@ -0,0 +1,49 @@
import Foundation
import PathKit
import ProjectSpec
public class InfoPlistGenerator {
/**
Default info plist attributes taken from:
/Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/Project Templates/Base/Base_DefinitionsInfoPlist.xctemplate/TemplateInfo.plist
*/
private func generateDefaultInfoPlist(for target: Target) -> [String: Any] {
var dictionary: [String: Any] = [:]
dictionary["CFBundleIdentifier"] = "$(PRODUCT_BUNDLE_IDENTIFIER)"
dictionary["CFBundleInfoDictionaryVersion"] = "6.0"
dictionary["CFBundleName"] = "$(PRODUCT_NAME)"
dictionary["CFBundleDevelopmentRegion"] = "$(DEVELOPMENT_LANGUAGE)"
dictionary["CFBundleShortVersionString"] = "1.0"
dictionary["CFBundleVersion"] = "1"
// Bundles should not contain any CFBundleExecutable otherwise they will be rejected when uploading.
if target.type != .bundle {
dictionary["CFBundleExecutable"] = "$(EXECUTABLE_NAME)"
}
return dictionary
}
public func generateProperties(for target: Target) -> [String: Any] {
var targetInfoPlist = generateDefaultInfoPlist(for: target)
switch target.type {
case .uiTestBundle,
.unitTestBundle:
targetInfoPlist["CFBundlePackageType"] = "BNDL"
case .application,
.watch2App:
targetInfoPlist["CFBundlePackageType"] = "APPL"
case .framework:
targetInfoPlist["CFBundlePackageType"] = "FMWK"
case .bundle:
targetInfoPlist["CFBundlePackageType"] = "BNDL"
case .xpcService,
.appExtension:
targetInfoPlist["CFBundlePackageType"] = "XPC!"
default: break
}
return targetInfoPlist
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,56 @@
import Foundation
import JSONUtilities
import PathKit
import ProjectSpec
import XcodeProj
import Yams
public class ProjectGenerator {
let project: Project
public init(project: Project) {
self.project = project
}
public func generateXcodeProject(in projectDirectory: Path? = nil, userName: String) throws -> XcodeProj {
// generate PBXProj
let pbxProjGenerator = PBXProjGenerator(project: project,
projectDirectory: projectDirectory)
let pbxProj = try pbxProjGenerator.generate()
// generate Workspace
let workspace = try generateWorkspace()
// generate Schemes
let schemeGenerator = SchemeGenerator(project: project, pbxProj: pbxProj)
let (sharedSchemes, userSchemes, schemeManagement) = try schemeGenerator.generateSchemes()
// generate Breakpoints
let breakpointGenerator = BreakpointGenerator(project: project)
let xcbreakpointlist = try breakpointGenerator.generateBreakpointList()
// generate shared data
let sharedData = XCSharedData(schemes: sharedSchemes, breakpoints: xcbreakpointlist)
// generate user data
let userData = userSchemes.isEmpty && schemeManagement == nil ? [] : [
XCUserData(userName: userName, schemes: userSchemes, schemeManagement: schemeManagement)
]
return XcodeProj(
workspace: workspace,
pbxproj: pbxProj,
sharedData: sharedData,
userData: userData
)
}
func generateWorkspace() throws -> XCWorkspace {
let selfReference = XCWorkspaceDataFileRef(location: .current(""))
let dataElement = XCWorkspaceDataElement.file(selfReference)
let workspaceData = XCWorkspaceData(children: [dataElement])
return XCWorkspace(data: workspaceData)
}
}
@@ -0,0 +1,564 @@
import Foundation
import ProjectSpec
import XcodeProj
import PathKit
private func suitableConfig(for type: ConfigType, in project: Project) -> Config {
if let defaultConfig = Config.defaultConfigs.first(where: { $0.type == type }),
project.configs.contains(defaultConfig) {
return defaultConfig
}
return project.configs.first { $0.type == type }!
}
public class SchemeGenerator {
let project: Project
let pbxProj: PBXProj
var defaultDebugConfig: Config {
suitableConfig(for: .debug, in: project)
}
var defaultReleaseConfig: Config {
suitableConfig(for: .release, in: project)
}
public init(project: Project, pbxProj: PBXProj) {
self.project = project
self.pbxProj = pbxProj
}
private var projects: [ProjectReference: PBXProj] = [:]
func getPBXProj(from reference: ProjectReference) throws -> PBXProj {
if let cachedProject = projects[reference] {
return cachedProject
}
let pbxproj = try XcodeProj(path: project.basePath + Path(reference.path)).pbxproj
projects[reference] = pbxproj
return pbxproj
}
public func generateSchemes() throws -> (
shared: [XCScheme],
user: [XCScheme],
management: XCSchemeManagement?
) {
var schemes: [(Scheme, ProjectTarget?)] = []
for scheme in project.schemes {
schemes.append((scheme, nil))
}
for target in project.projectTargets {
if let targetScheme = target.scheme {
if targetScheme.configVariants.isEmpty {
let schemeName = target.name
let debugConfig = suitableConfig(for: .debug, in: project)
let releaseConfig = suitableConfig(for: .release, in: project)
let scheme = Scheme(
name: schemeName,
target: target,
targetScheme: targetScheme,
project: project,
debugConfig: debugConfig.name,
releaseConfig: releaseConfig.name
)
schemes.append((scheme, target))
} else {
for configVariant in targetScheme.configVariants {
let schemeName = "\(target.name) \(configVariant)"
let debugConfig = project.configs
.first(including: configVariant, for: .debug)!
let releaseConfig = project.configs
.first(including: configVariant, for: .release)!
let scheme = Scheme(
name: schemeName,
target: target,
targetScheme: targetScheme,
project: project,
debugConfig: debugConfig.name,
releaseConfig: releaseConfig.name
)
schemes.append((scheme, target))
}
}
}
}
var sharedSchemes: [XCScheme] = []
var userSchemes: [XCScheme] = []
var schemeManagements: [XCSchemeManagement.UserStateScheme] = []
for (scheme, projectTarget) in schemes {
let xcscheme = try generateScheme(scheme, for: projectTarget)
if scheme.management?.shared == false {
userSchemes.append(xcscheme)
} else {
sharedSchemes.append(xcscheme)
}
if let management = scheme.management {
schemeManagements.append(
XCSchemeManagement.UserStateScheme(
name: scheme.name + ".xcscheme",
shared: management.shared,
orderHint: management.orderHint,
isShown: management.isShown
)
)
}
}
return (
shared: sharedSchemes,
user: userSchemes,
management: schemeManagements.isEmpty
? nil
: XCSchemeManagement(schemeUserState: schemeManagements, suppressBuildableAutocreation: nil)
)
}
public func generateScheme(_ scheme: Scheme, for target: ProjectTarget? = nil) throws -> XCScheme {
func getBuildableReference(_ target: TargetReference) throws -> XCScheme.BuildableReference {
let pbxProj: PBXProj
let projectFilePath: String
switch target.location {
case .project(let project):
guard let projectReference = self.project.getProjectReference(project) else {
throw SchemeGenerationError.missingProject(project)
}
pbxProj = try getPBXProj(from: projectReference)
projectFilePath = projectReference.path
case .local:
pbxProj = self.pbxProj
projectFilePath = "\(self.project.name).xcodeproj"
}
guard let pbxTarget = pbxProj.targets(named: target.name).first else {
throw SchemeGenerationError.missingTarget(target, projectPath: projectFilePath)
}
let buildableName: String
switch target.location {
case .project:
buildableName = pbxTarget.productNameWithExtension() ?? pbxTarget.name
case .local:
guard let _buildableName =
project.getTarget(target.name)?.filename ??
project.getAggregateTarget(target.name)?.name else {
fatalError("Unable to determinate \"buildableName\" for build target: \(target)")
}
buildableName = _buildableName
}
return XCScheme.BuildableReference(
referencedContainer: "container:\(projectFilePath)",
blueprint: pbxTarget,
buildableName: buildableName,
blueprintName: target.name
)
}
func getBuildableTestableReference(_ target: TestableTargetReference) throws -> XCScheme.BuildableReference {
switch target.location {
case .package(let packageName):
guard let package = self.project.getPackage(packageName),
case let .local(path, _, _) = package else {
throw SchemeGenerationError.missingPackage(packageName)
}
return XCScheme.BuildableReference(
referencedContainer: "container:\(path)",
blueprintIdentifier: target.name,
buildableName: target.name,
blueprintName: target.name
)
default:
return try getBuildableReference(target.targetReference)
}
}
func getBuildEntry(_ buildTarget: Scheme.BuildTarget) throws -> XCScheme.BuildAction.Entry {
let buildableReference = try getBuildableTestableReference(buildTarget.target)
return XCScheme.BuildAction.Entry(buildableReference: buildableReference, buildFor: buildTarget.buildTypes)
}
let testTargets = scheme.test?.targets ?? []
let testBuildTargets = testTargets.map {
Scheme.BuildTarget(target: $0.targetReference, buildTypes: BuildType.testOnly)
}
let testBuildTargetEntries = try testBuildTargets.map(getBuildEntry)
let buildActionEntries: [XCScheme.BuildAction.Entry] = try scheme.build.targets.map(getBuildEntry)
func getExecutionAction(_ action: Scheme.ExecutionAction) -> XCScheme.ExecutionAction {
// ExecutionActions can require the use of build settings. Xcode allows the settings to come from a build or test target.
let environmentBuildable = action.settingsTarget.flatMap { settingsTarget in
(buildActionEntries + testBuildTargetEntries)
.first { settingsTarget == $0.buildableReference.blueprintName }?
.buildableReference
}
return XCScheme.ExecutionAction(
scriptText: action.script,
title: action.name,
shellToInvoke: action.shell,
environmentBuildable: environmentBuildable
)
}
let schemeTarget: ProjectTarget?
if let targetName = scheme.run?.executable {
schemeTarget = project.getTarget(targetName)
} else {
guard let firstTarget = scheme.build.targets.first else {
throw SchemeGenerationError.missingBuildTargets(scheme.name)
}
let name = scheme.build.targets.first { $0.buildTypes.contains(.running) }?.target.name ?? firstTarget.target.name
schemeTarget = target ?? project.getTarget(name)
}
let shouldExecuteOnLaunch = schemeTarget?.shouldExecuteOnLaunch == true
let buildableReference = buildActionEntries.first(where: { $0.buildableReference.blueprintName == schemeTarget?.name })?.buildableReference ?? buildActionEntries.first!.buildableReference
let runnables = makeProductRunnables(for: schemeTarget, buildableReference: buildableReference)
let buildAction = XCScheme.BuildAction(
buildActionEntries: buildActionEntries,
preActions: scheme.build.preActions.map(getExecutionAction),
postActions: scheme.build.postActions.map(getExecutionAction),
parallelizeBuild: scheme.build.parallelizeBuild,
buildImplicitDependencies: scheme.build.buildImplicitDependencies,
runPostActionsOnFailure: scheme.build.runPostActionsOnFailure
)
let testables: [XCScheme.TestableReference] = zip(testTargets, testBuildTargetEntries).map { testTarget, testBuildEntries in
var locationScenarioReference: XCScheme.LocationScenarioReference?
if var location = testTarget.location {
if location.contains(".gpx") {
var path = Path(components: [project.options.schemePathPrefix, location])
path = path.simplifyingParentDirectoryReferences()
location = path.string
}
let referenceType = location.contains(".gpx") ? "0" : "1"
locationScenarioReference = XCScheme.LocationScenarioReference(identifier: location, referenceType: referenceType)
}
return XCScheme.TestableReference(
skipped: testTarget.skipped,
parallelizable: testTarget.parallelizable,
randomExecutionOrdering: testTarget.randomExecutionOrder,
buildableReference: testBuildEntries.buildableReference,
locationScenarioReference: locationScenarioReference,
skippedTests: testTarget.skippedTests.map(XCScheme.TestItem.init),
selectedTests: testTarget.selectedTests.map(XCScheme.TestItem.init),
useTestSelectionWhitelist: !testTarget.selectedTests.isEmpty ? true : nil
)
}
let coverageBuildableTargets = try scheme.test?.coverageTargets.map {
try getBuildableTestableReference($0)
} ?? []
let testCommandLineArgs = scheme.test.map { XCScheme.CommandLineArguments($0.commandLineArguments) }
let launchCommandLineArgs = scheme.run.map { XCScheme.CommandLineArguments($0.commandLineArguments) }
let profileCommandLineArgs = scheme.profile.map { XCScheme.CommandLineArguments($0.commandLineArguments) }
let testVariables = scheme.test.flatMap { $0.environmentVariables.isEmpty ? nil : $0.environmentVariables }
let launchVariables = scheme.run.flatMap { $0.environmentVariables.isEmpty ? nil : $0.environmentVariables }
let profileVariables = scheme.profile.flatMap { $0.environmentVariables.isEmpty ? nil : $0.environmentVariables }
let defaultTestPlanIndex = scheme.test?.testPlans.firstIndex { $0.defaultPlan } ?? 0
let testPlans = scheme.test?.testPlans.enumerated().map { index, testPlan in
XCScheme.TestPlanReference(reference: "container:\(testPlan.path)", default: defaultTestPlanIndex == index)
} ?? []
let testBuildableEntries = buildActionEntries.filter({ $0.buildFor.contains(.testing) }) + testBuildTargetEntries
let testMacroExpansionBuildableRef = testBuildableEntries.map(\.buildableReference).contains(buildableReference) ? buildableReference : testBuildableEntries.first?.buildableReference
let testMacroExpansion: XCScheme.BuildableReference = buildActionEntries.first(
where: { value in
if let macroExpansion = scheme.test?.macroExpansion {
return value.buildableReference.blueprintName == macroExpansion
}
return false
}
)?.buildableReference ?? testMacroExpansionBuildableRef ?? buildableReference
let testAction = XCScheme.TestAction(
buildConfiguration: scheme.test?.config ?? defaultDebugConfig.name,
macroExpansion: testMacroExpansion,
testables: testables,
testPlans: testPlans.isEmpty ? nil : testPlans,
preActions: scheme.test?.preActions.map(getExecutionAction) ?? [],
postActions: scheme.test?.postActions.map(getExecutionAction) ?? [],
selectedDebuggerIdentifier: (scheme.test?.debugEnabled ?? Scheme.Test.debugEnabledDefault) ? XCScheme.defaultDebugger : "",
selectedLauncherIdentifier: (scheme.test?.debugEnabled ?? Scheme.Test.debugEnabledDefault) ? XCScheme.defaultLauncher : "Xcode.IDEFoundation.Launcher.PosixSpawn",
shouldUseLaunchSchemeArgsEnv: scheme.test?.shouldUseLaunchSchemeArgsEnv ?? true,
codeCoverageEnabled: scheme.test?.gatherCoverageData ?? Scheme.Test.gatherCoverageDataDefault,
codeCoverageTargets: coverageBuildableTargets,
onlyGenerateCoverageForSpecifiedTargets: !coverageBuildableTargets.isEmpty,
disableMainThreadChecker: scheme.test?.disableMainThreadChecker ?? Scheme.Test.disableMainThreadCheckerDefault,
commandlineArguments: testCommandLineArgs,
environmentVariables: testVariables,
language: scheme.test?.language,
region: scheme.test?.region,
systemAttachmentLifetime: scheme.test?.systemAttachmentLifetime,
preferredScreenCaptureFormat: scheme.test?.preferredScreenCaptureFormat,
customLLDBInitFile: scheme.test?.customLLDBInit
)
let allowLocationSimulation = scheme.run?.simulateLocation?.allow ?? true
var locationScenarioReference: XCScheme.LocationScenarioReference?
if let simulateLocation = scheme.run?.simulateLocation, var identifier = simulateLocation.defaultLocation, let referenceType = simulateLocation.referenceType {
if referenceType == .gpx {
var path = Path(components: [project.options.schemePathPrefix, identifier])
path = path.simplifyingParentDirectoryReferences()
identifier = path.string
}
locationScenarioReference = XCScheme.LocationScenarioReference(identifier: identifier, referenceType: referenceType.rawValue)
}
var storeKitConfigurationFileReference: XCScheme.StoreKitConfigurationFileReference?
if let storeKitConfiguration = scheme.run?.storeKitConfiguration {
let storeKitConfigurationPath = Path(components: [project.options.schemePathPrefix, storeKitConfiguration]).simplifyingParentDirectoryReferences()
storeKitConfigurationFileReference = XCScheme.StoreKitConfigurationFileReference(identifier: storeKitConfigurationPath.string)
}
let macroExpansion: XCScheme.BuildableReference?
if let macroExpansionName = scheme.run?.macroExpansion,
let resolvedMacroExpansion = buildActionEntries.first(where: { $0.buildableReference.blueprintName == macroExpansionName })?.buildableReference {
macroExpansion = resolvedMacroExpansion
} else {
macroExpansion = shouldExecuteOnLaunch ? nil : buildableReference
}
let launchAction = XCScheme.LaunchAction(
runnable: shouldExecuteOnLaunch ? runnables.launch : nil,
buildConfiguration: scheme.run?.config ?? defaultDebugConfig.name,
preActions: scheme.run?.preActions.map(getExecutionAction) ?? [],
postActions: scheme.run?.postActions.map(getExecutionAction) ?? [],
macroExpansion: macroExpansion,
selectedDebuggerIdentifier: selectedDebuggerIdentifier(for: schemeTarget, run: scheme.run),
selectedLauncherIdentifier: selectedLauncherIdentifier(for: schemeTarget, run: scheme.run),
askForAppToLaunch: scheme.run?.askForAppToLaunch,
allowLocationSimulation: allowLocationSimulation,
locationScenarioReference: locationScenarioReference,
enableGPUFrameCaptureMode: scheme.run?.enableGPUFrameCaptureMode ?? XCScheme.LaunchAction.defaultGPUFrameCaptureMode,
disableGPUValidationMode: !(scheme.run?.enableGPUValidationMode ?? Scheme.Run.enableGPUValidationModeDefault),
disableMainThreadChecker: scheme.run?.disableMainThreadChecker ?? Scheme.Run.disableMainThreadCheckerDefault,
disablePerformanceAntipatternChecker: scheme.run?.disableThreadPerformanceChecker ?? Scheme.Run.disableThreadPerformanceCheckerDefault,
stopOnEveryMainThreadCheckerIssue: scheme.run?.stopOnEveryMainThreadCheckerIssue ?? Scheme.Run.stopOnEveryMainThreadCheckerIssueDefault,
commandlineArguments: launchCommandLineArgs,
environmentVariables: launchVariables,
language: scheme.run?.language,
region: scheme.run?.region,
launchAutomaticallySubstyle: scheme.run?.launchAutomaticallySubstyle ?? launchAutomaticallySubstyle(for: schemeTarget),
storeKitConfigurationFileReference: storeKitConfigurationFileReference,
customLLDBInitFile: scheme.run?.customLLDBInit
)
let profileAction = XCScheme.ProfileAction(
buildableProductRunnable: shouldExecuteOnLaunch ? runnables.profile : nil,
buildConfiguration: scheme.profile?.config ?? defaultReleaseConfig.name,
preActions: scheme.profile?.preActions.map(getExecutionAction) ?? [],
postActions: scheme.profile?.postActions.map(getExecutionAction) ?? [],
macroExpansion: shouldExecuteOnLaunch ? nil : buildableReference,
shouldUseLaunchSchemeArgsEnv: scheme.profile?.shouldUseLaunchSchemeArgsEnv ?? true,
askForAppToLaunch: scheme.profile?.askForAppToLaunch,
commandlineArguments: profileCommandLineArgs,
environmentVariables: profileVariables
)
let analyzeAction = XCScheme.AnalyzeAction(buildConfiguration: scheme.analyze?.config ?? defaultDebugConfig.name)
let archiveAction = XCScheme.ArchiveAction(
buildConfiguration: scheme.archive?.config ?? defaultReleaseConfig.name,
revealArchiveInOrganizer: scheme.archive?.revealArchiveInOrganizer ?? true,
customArchiveName: scheme.archive?.customArchiveName,
preActions: scheme.archive?.preActions.map(getExecutionAction) ?? [],
postActions: scheme.archive?.postActions.map(getExecutionAction) ?? []
)
let lastUpgradeVersion = project.attributes["LastUpgradeCheck"] as? String ?? project.xcodeVersion
return XCScheme(
name: scheme.name,
lastUpgradeVersion: lastUpgradeVersion,
version: project.schemeVersion,
buildAction: buildAction,
testAction: testAction,
launchAction: launchAction,
profileAction: profileAction,
analyzeAction: analyzeAction,
archiveAction: archiveAction,
wasCreatedForAppExtension: schemeTarget
.flatMap { $0.type.isExtension ? true : nil }
)
}
private func launchAutomaticallySubstyle(for target: ProjectTarget?) -> String? {
if target?.type.isExtension == true {
return "2"
} else {
return nil
}
}
private func makeProductRunnables(for target: ProjectTarget?, buildableReference: XCScheme.BuildableReference) -> (launch: XCScheme.Runnable, profile: XCScheme.BuildableProductRunnable) {
let buildable = XCScheme.BuildableProductRunnable(buildableReference: buildableReference)
if target?.type.isWatchApp == true {
let remote = XCScheme.RemoteRunnable(
buildableReference: buildableReference,
bundleIdentifier: "com.apple.Carousel",
runnableDebuggingMode: "2"
)
return (remote, buildable)
} else {
return (buildable, buildable)
}
}
private func selectedDebuggerIdentifier(for target: ProjectTarget?, run: Scheme.Run?) -> String {
if target?.type.canUseDebugLauncher != false && run?.debugEnabled ?? Scheme.Run.debugEnabledDefault {
return XCScheme.defaultDebugger
} else {
return ""
}
}
private func selectedLauncherIdentifier(for target: ProjectTarget?, run: Scheme.Run?) -> String {
if target?.type.canUseDebugLauncher != false && run?.debugEnabled ?? Scheme.Run.debugEnabledDefault {
return XCScheme.defaultLauncher
} else {
return "Xcode.IDEFoundation.Launcher.PosixSpawn"
}
}
}
enum SchemeGenerationError: Error, CustomStringConvertible {
case missingTarget(TargetReference, projectPath: String)
case missingPackage(String)
case missingProject(String)
case missingBuildTargets(String)
var description: String {
switch self {
case .missingTarget(let target, let projectPath):
return "Unable to find target named \"\(target)\" in \"\(projectPath)\""
case .missingProject(let project):
return "Unable to find project reference named \"\(project)\" in project.yml"
case .missingBuildTargets(let name):
return "Unable to find at least one build target in scheme \"\(name)\""
case .missingPackage(let package):
return "Unable to find swift package named \"\(package)\" in project.yml"
}
}
}
extension Scheme {
public init(name: String, target: ProjectTarget, targetScheme: TargetScheme, project: Project, debugConfig: String, releaseConfig: String) {
self.init(
name: name,
build: .init(
targets: Scheme.buildTargets(for: target, project: project),
buildImplicitDependencies: targetScheme.buildImplicitDependencies,
preActions: targetScheme.preActions,
postActions: targetScheme.postActions
),
run: .init(
config: debugConfig,
commandLineArguments: targetScheme.commandLineArguments,
environmentVariables: targetScheme.environmentVariables,
disableMainThreadChecker: targetScheme.disableMainThreadChecker,
stopOnEveryMainThreadCheckerIssue: targetScheme.stopOnEveryMainThreadCheckerIssue,
disableThreadPerformanceChecker: targetScheme.disableThreadPerformanceChecker,
language: targetScheme.language,
region: targetScheme.region,
storeKitConfiguration: targetScheme.storeKitConfiguration
),
test: .init(
config: debugConfig,
gatherCoverageData: targetScheme.gatherCoverageData,
coverageTargets: targetScheme.coverageTargets,
disableMainThreadChecker: targetScheme.disableMainThreadChecker,
commandLineArguments: targetScheme.commandLineArguments,
targets: targetScheme.testTargets,
environmentVariables: targetScheme.environmentVariables,
testPlans: targetScheme.testPlans,
language: targetScheme.language,
region: targetScheme.region
),
profile: .init(
config: releaseConfig,
commandLineArguments: targetScheme.commandLineArguments,
environmentVariables: targetScheme.environmentVariables
),
analyze: .init(
config: debugConfig
),
archive: .init(
config: releaseConfig
),
management: targetScheme.management
)
}
private static func buildTargets(for target: ProjectTarget, project: Project) -> [BuildTarget] {
let buildTarget = Scheme.BuildTarget(target: TestableTargetReference.local(target.name))
switch target.type {
case .watchApp, .watch2App:
let hostTarget = project.targets
.first { projectTarget in
projectTarget.dependencies.contains { $0.reference == target.name }
}
.map { BuildTarget(target: TestableTargetReference.local($0.name)) }
return hostTarget.map { [buildTarget, $0] } ?? [buildTarget]
default:
return [buildTarget]
}
}
}
extension PBXProductType {
var canUseDebugLauncher: Bool {
// Extensions don't use the lldb launcher
return !isExtension
}
var isWatchApp: Bool {
switch self {
case .watchApp, .watch2App:
return true
default:
return false
}
}
}
extension Scheme.Test {
var systemAttachmentLifetime: XCScheme.TestAction.AttachmentLifetime? {
switch (captureScreenshotsAutomatically, deleteScreenshotsWhenEachTestSucceeds) {
case (false, _):
return .keepNever
case (true, false):
return .keepAlways
case (true, true):
return nil
}
}
}
+1
View File
@@ -0,0 +1 @@
../../SettingPresets/
@@ -0,0 +1,319 @@
import Foundation
import JSONUtilities
import PathKit
import ProjectSpec
import XcodeProj
import Yams
extension Project {
public func getProjectBuildSettings(config: Config) -> BuildSettings {
var buildSettings: BuildSettings = [:]
// set project SDKROOT is a single platform
if let firstPlatform = targets.first?.platform,
targets.allSatisfy({ $0.platform == firstPlatform })
{
buildSettings["SDKROOT"] = firstPlatform.sdkRoot
}
if let type = config.type, options.settingPresets.applyProject {
buildSettings += SettingsPresetFile.base.getBuildSettings()
buildSettings += SettingsPresetFile.config(type).getBuildSettings()
}
// apply custom platform version
for platform in Platform.allCases {
if let version = options.deploymentTarget.version(for: platform) {
buildSettings[platform.deploymentTargetSetting] = version.deploymentTarget
}
}
// Prevent setting presets from overwriting settings in project xcconfig files
if let configPath = configFiles[config.name] {
buildSettings = removeConfigFileSettings(from: buildSettings, configPath: configPath)
}
buildSettings += getBuildSettings(settings: settings, config: config)
return buildSettings
}
public func getTargetBuildSettings(target: Target, config: Config) -> BuildSettings {
var buildSettings = BuildSettings()
// list of supported destination sorted by priority
let specSupportedDestinations = target.supportedDestinations?.sorted(by: { $0.priority < $1.priority }) ?? []
if options.settingPresets.applyTarget {
let platform: Platform
if target.platform == .auto,
let firstDestination = specSupportedDestinations.first,
let firstDestinationPlatform = Platform(rawValue: firstDestination.rawValue) {
platform = firstDestinationPlatform
} else {
platform = target.platform
}
buildSettings += SettingsPresetFile.platform(platform).getBuildSettings()
buildSettings += SettingsPresetFile.product(target.type).getBuildSettings()
buildSettings += SettingsPresetFile.productPlatform(target.type, platform).getBuildSettings()
if target.platform == .auto {
// this fix is necessary because the platform preset overrides the original value
buildSettings["SDKROOT"] = Platform.auto.rawValue
}
}
if !specSupportedDestinations.isEmpty {
var supportedPlatforms: [String] = []
var targetedDeviceFamily: [String] = []
for supportedDestination in specSupportedDestinations {
let supportedPlatformBuildSettings = SettingsPresetFile.supportedDestination(supportedDestination).getBuildSettings()
buildSettings += supportedPlatformBuildSettings
if let value = supportedPlatformBuildSettings?["SUPPORTED_PLATFORMS"] as? String {
supportedPlatforms += value.components(separatedBy: " ")
}
if let value = supportedPlatformBuildSettings?["TARGETED_DEVICE_FAMILY"] as? String {
targetedDeviceFamily += value.components(separatedBy: ",")
}
}
buildSettings["SUPPORTED_PLATFORMS"] = supportedPlatforms.joined(separator: " ")
buildSettings["TARGETED_DEVICE_FAMILY"] = targetedDeviceFamily.joined(separator: ",")
}
// apply custom platform version
if let version = target.deploymentTarget {
if !specSupportedDestinations.isEmpty {
for supportedDestination in specSupportedDestinations {
if let platform = Platform(rawValue: supportedDestination.rawValue) {
buildSettings[platform.deploymentTargetSetting] = version.deploymentTarget
}
}
} else {
buildSettings[target.platform.deploymentTargetSetting] = version.deploymentTarget
}
}
// Prevent setting presets from overrwriting settings in target xcconfig files
if let configPath = target.configFiles[config.name] {
buildSettings = removeConfigFileSettings(from: buildSettings, configPath: configPath)
}
// Prevent setting presets from overrwriting settings in project xcconfig files
if let configPath = configFiles[config.name] {
buildSettings = removeConfigFileSettings(from: buildSettings, configPath: configPath)
}
buildSettings += getBuildSettings(settings: target.settings, config: config)
return buildSettings
}
public func getBuildSettings(settings: Settings, config: Config) -> BuildSettings {
var buildSettings: BuildSettings = [:]
for group in settings.groups {
if let settings = settingGroups[group] {
buildSettings += getBuildSettings(settings: settings, config: config)
}
}
buildSettings += settings.buildSettings
for (configVariant, settings) in settings.configSettings {
let isPartialMatch = config.name.lowercased().contains(configVariant.lowercased())
if isPartialMatch {
let exactConfig = getConfig(configVariant)
let matchesExactlyToOtherConfig = exactConfig != nil && exactConfig?.name != config.name
if !matchesExactlyToOtherConfig {
buildSettings += getBuildSettings(settings: settings, config: config)
}
}
}
return buildSettings
}
// combines all levels of a target's settings: target, target config, project, project config
public func getCombinedBuildSetting(_ setting: String, target: ProjectTarget, config: Config) -> Any? {
if let target = target as? Target,
let value = getTargetBuildSettings(target: target, config: config)[setting] {
return value
}
if let configFilePath = target.configFiles[config.name],
let value = loadConfigFileBuildSettings(path: configFilePath)?[setting] {
return value
}
if let value = getProjectBuildSettings(config: config)[setting] {
return value
}
if let configFilePath = configFiles[config.name],
let value = loadConfigFileBuildSettings(path: configFilePath)?[setting] {
return value
}
return nil
}
public func getBoolBuildSetting(_ setting: String, target: ProjectTarget, config: Config) -> Bool? {
guard let value = getCombinedBuildSetting(setting, target: target, config: config) else { return nil }
if let boolValue = value as? Bool {
return boolValue
} else if let stringValue = value as? String {
return stringValue == "YES"
}
return nil
}
public func targetHasBuildSetting(_ setting: String, target: Target, config: Config) -> Bool {
getCombinedBuildSetting(setting, target: target, config: config) != nil
}
/// Removes values from build settings if they are defined in an xcconfig file
private func removeConfigFileSettings(from buildSettings: BuildSettings, configPath: String) -> BuildSettings {
var buildSettings = buildSettings
if let configSettings = loadConfigFileBuildSettings(path: configPath) {
for key in configSettings.keys {
// FIXME: Catch platform specifier. e.g. LD_RUNPATH_SEARCH_PATHS[sdk=iphone*]
buildSettings.removeValue(forKey: key)
buildSettings.removeValue(forKey: key.quoted)
}
}
return buildSettings
}
/// Returns cached build settings from a config file
private func loadConfigFileBuildSettings(path: String) -> BuildSettings? {
let configFilePath = basePath + path
if let cached = configFileSettings[configFilePath.string] {
return cached.value
} else {
guard let configFile = try? XCConfig(path: configFilePath) else {
configFileSettings[configFilePath.string] = .nothing
return nil
}
let settings = configFile.flattenedBuildSettings()
configFileSettings[configFilePath.string] = .cached(settings)
return settings
}
}
}
private enum Cached<T> {
case cached(T)
case nothing
var value: T? {
switch self {
case let .cached(value): return value
case .nothing: return nil
}
}
}
// cached flattened xcconfig file settings
private var configFileSettings: [String: Cached<BuildSettings>] = [:]
// cached setting preset settings
private var settingPresetSettings: [String: Cached<BuildSettings>] = [:]
extension SettingsPresetFile {
public func getBuildSettings() -> BuildSettings? {
if let cached = settingPresetSettings[path] {
return cached.value
}
let bundlePath = Path(Bundle.main.bundlePath)
let relativePath = Path("SettingPresets/\(path).yml")
var possibleSettingsPaths: [Path] = [
relativePath,
bundlePath + relativePath,
bundlePath + "../share/xcodegen/\(relativePath)",
Path(#file).parent().parent().parent() + relativePath,
]
if let resourcePath = Bundle.main.resourcePath {
possibleSettingsPaths.append(Path(resourcePath) + relativePath)
}
if let symlink = try? (bundlePath + "xcodegen").symlinkDestination() {
possibleSettingsPaths = [
symlink.parent() + relativePath,
] + possibleSettingsPaths
}
if let moduleResourcePath = Bundle.availableModule?.path(forResource: "SettingPresets", ofType: nil) {
possibleSettingsPaths.append(Path(moduleResourcePath) + "\(path).yml")
}
guard let settingsPath = possibleSettingsPaths.first(where: { $0.exists }) else {
switch self {
case .base, .config, .platform, .supportedDestination:
print("No \"\(name)\" settings found")
case .product, .productPlatform:
break
}
settingPresetSettings[path] = .nothing
return nil
}
guard let buildSettings = try? loadYamlDictionary(path: settingsPath) else {
print("Error parsing \"\(name)\" settings")
return nil
}
settingPresetSettings[path] = .cached(buildSettings)
return buildSettings
}
}
private class BundleFinder {}
/// The default SPM generated `Bundle.module` crashes on runtime if there is no .bundle file.
/// Below implementation modified from generated `Bundle.module` code which call `fatalError` if .bundle file not found.
private extension Bundle {
/// Returns the resource bundle associated with the current Swift module.
static let availableModule: Bundle? = {
let bundleName = "XcodeGen_XcodeGenKit"
let overrides: [URL]
#if DEBUG
// The 'PACKAGE_RESOURCE_BUNDLE_PATH' name is preferred since the expected value is a path. The
// check for 'PACKAGE_RESOURCE_BUNDLE_URL' will be removed when all clients have switched over.
// This removal is tracked by rdar://107766372.
if let override = ProcessInfo.processInfo.environment["PACKAGE_RESOURCE_BUNDLE_PATH"]
?? ProcessInfo.processInfo.environment["PACKAGE_RESOURCE_BUNDLE_URL"] {
overrides = [URL(fileURLWithPath: override)]
} else {
overrides = []
}
#else
overrides = []
#endif
let candidates = overrides + [
// Bundle should be present here when the package is linked into an App.
Bundle.main.resourceURL,
// Bundle should be present here when the package is linked into a framework.
Bundle(for: BundleFinder.self).resourceURL,
// For command-line tools.
Bundle.main.bundleURL,
]
for candidate in candidates {
let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")
if let bundle = bundlePath.flatMap(Bundle.init(url:)) {
return bundle
}
}
return nil
}()
}
@@ -0,0 +1,34 @@
import Foundation
import ProjectSpec
import XcodeProj
public enum SettingsPresetFile {
case config(ConfigType)
case platform(Platform)
case supportedDestination(SupportedDestination)
case product(PBXProductType)
case productPlatform(PBXProductType, Platform)
case base
var path: String {
switch self {
case let .config(config): return "Configs/\(config.rawValue)"
case let .platform(platform): return "Platforms/\(platform.rawValue)"
case let .supportedDestination(supportedDestination): return "SupportedDestinations/\(supportedDestination.rawValue)"
case let .product(product): return "Products/\(product.name)"
case let .productPlatform(product, platform): return "Product_Platform/\(product.name)_\(platform.rawValue)"
case .base: return "base"
}
}
var name: String {
switch self {
case let .config(config): return "\(config.rawValue) config"
case let .platform(platform): return platform.rawValue
case let .supportedDestination(supportedDestination): return supportedDestination.rawValue
case let .product(product): return product.name
case let .productPlatform(product, platform): return "\(platform) \(product)"
case .base: return "base"
}
}
}
@@ -0,0 +1,811 @@
import Foundation
import PathKit
import ProjectSpec
import XcodeProj
import XcodeGenCore
struct SourceFile {
let path: Path
let fileReference: PBXFileElement
let buildFile: PBXBuildFile
let buildPhase: BuildPhaseSpec?
}
class SourceGenerator {
var rootGroups: Set<PBXFileElement> = []
private let projectDirectory: Path?
private var fileReferencesByPath: [String: PBXFileElement] = [:]
private var groupsByPath: [Path: PBXGroup] = [:]
private var variantGroupsByPath: [Path: PBXVariantGroup] = [:]
private let project: Project
let pbxProj: PBXProj
private var defaultExcludedFiles = [
".DS_Store",
]
private let defaultExcludedExtensions = [
"orig",
]
private(set) var knownRegions: Set<String> = []
init(project: Project, pbxProj: PBXProj, projectDirectory: Path?) {
self.project = project
self.pbxProj = pbxProj
self.projectDirectory = projectDirectory
}
private func resolveGroupPath(_ path: Path, isTopLevelGroup: Bool) -> String {
if isTopLevelGroup, let relativePath = try? path.relativePath(from: projectDirectory ?? project.basePath).string {
return relativePath
} else {
return path.lastComponent
}
}
@discardableResult
func addObject<T: PBXObject>(_ object: T, context: String? = nil) -> T {
pbxProj.add(object: object)
object.context = context
return object
}
func createLocalPackage(path: Path, group: Path?) throws {
var parentGroup: String = project.options.localPackagesGroup ?? "Packages"
if let group {
parentGroup = group.string
}
let absolutePath = project.basePath + path.normalize()
// Get the local package's relative path from the project root
let fileReferencePath = try? absolutePath.relativePath(from: projectDirectory ?? project.basePath).string
let fileReference = addObject(
PBXFileReference(
sourceTree: .sourceRoot,
name: absolutePath.lastComponent,
lastKnownFileType: "folder",
path: fileReferencePath
)
)
if parentGroup == "" {
rootGroups.insert(fileReference)
} else {
let parentGroups = parentGroup.components(separatedBy: "/")
createParentGroups(parentGroups, for: fileReference)
}
}
/// Collects an array complete of all `SourceFile` objects that make up the target based on the provided `TargetSource` definitions.
///
/// - Parameters:
/// - targetType: The type of target that the source files should belong to.
/// - sources: The array of sources defined as part of the targets spec.
/// - buildPhases: A dictionary containing any build phases that should be applied to source files at specific paths in the event that the associated `TargetSource` didn't already define a `buildPhase`. Values from this dictionary are used in cases where the project generator knows more about a file than the spec/filesystem does (i.e if the file should be treated as the targets Info.plist and so on).
func getAllSourceFiles(targetType: PBXProductType, sources: [TargetSource], buildPhases: [Path : BuildPhaseSpec]) throws -> [SourceFile] {
try sources.flatMap { try getSourceFiles(targetType: targetType, targetSource: $0, buildPhases: buildPhases) }
}
// get groups without build files. Use for Project.fileGroups
func getFileGroups(path: String) throws {
_ = try getSourceFiles(targetType: .none, targetSource: TargetSource(path: path), buildPhases: [:])
}
func getFileType(path: Path) -> FileType? {
if let fileExtension = path.extension {
return project.options.fileTypes[fileExtension] ?? FileType.defaultFileTypes[fileExtension]
} else {
return nil
}
}
private func makeDestinationFilters(for path: Path, with filters: [SupportedDestination]?, or inferDestinationFiltersByPath: Bool?) -> [String]? {
if let filters = filters, !filters.isEmpty {
return filters.map { $0.string }
} else if inferDestinationFiltersByPath == true {
for supportedDestination in SupportedDestination.allCases {
let regex1 = try? NSRegularExpression(pattern: "\\/\(supportedDestination)\\/", options: .caseInsensitive)
let regex2 = try? NSRegularExpression(pattern: "\\_\(supportedDestination)\\.swift$", options: .caseInsensitive)
if regex1?.isMatch(to: path.string) == true || regex2?.isMatch(to: path.string) == true {
return [supportedDestination.string]
}
}
}
return nil
}
func generateSourceFile(targetType: PBXProductType, targetSource: TargetSource, path: Path, fileReference: PBXFileElement? = nil, buildPhases: [Path: BuildPhaseSpec]) -> SourceFile {
let fileReference = fileReference ?? fileReferencesByPath[path.string.lowercased()]!
var settings: [String: Any] = [:]
let fileType = getFileType(path: path)
var attributes: [String] = targetSource.attributes + (fileType?.attributes ?? [])
var chosenBuildPhase: BuildPhaseSpec?
var compilerFlags: String = ""
let assetTags: [String] = targetSource.resourceTags + (fileType?.resourceTags ?? [])
let headerVisibility = targetSource.headerVisibility ?? .public
if let buildPhase = targetSource.buildPhase {
chosenBuildPhase = buildPhase
} else if resolvedTargetSourceType(for: targetSource, at: path) == .folder {
chosenBuildPhase = .resources
} else if let buildPhase = buildPhases[path] {
chosenBuildPhase = buildPhase
} else {
chosenBuildPhase = getDefaultBuildPhase(for: path, targetType: targetType)
}
if chosenBuildPhase == .headers && targetType == .staticLibrary {
// Static libraries don't support the header build phase
// For public headers they need to be copied
if headerVisibility == .public {
chosenBuildPhase = .copyFiles(BuildPhaseSpec.CopyFilesSettings(
destination: .productsDirectory,
subpath: "include/$(PRODUCT_NAME)",
phaseOrder: .preCompile
))
} else {
chosenBuildPhase = nil
}
}
if chosenBuildPhase == .headers {
if headerVisibility != .project {
// Xcode doesn't write the default of project
attributes.append(headerVisibility.settingName)
}
}
if let flags = fileType?.compilerFlags {
compilerFlags += flags.joined(separator: " ")
}
if !targetSource.compilerFlags.isEmpty {
if !compilerFlags.isEmpty {
compilerFlags += " "
}
compilerFlags += targetSource.compilerFlags.joined(separator: " ")
}
if chosenBuildPhase == .sources && !compilerFlags.isEmpty {
settings["COMPILER_FLAGS"] = compilerFlags
}
if !attributes.isEmpty {
settings["ATTRIBUTES"] = attributes
}
if chosenBuildPhase == .resources && !assetTags.isEmpty {
settings["ASSET_TAGS"] = assetTags
}
let platforms = makeDestinationFilters(for: path, with: targetSource.destinationFilters, or: targetSource.inferDestinationFiltersByPath)
let buildFile = PBXBuildFile(file: fileReference, settings: settings.isEmpty ? nil : settings, platformFilters: platforms)
return SourceFile(
path: path,
fileReference: fileReference,
buildFile: buildFile,
buildPhase: chosenBuildPhase
)
}
func getContainedFileReference(path: Path) -> PBXFileElement {
let createIntermediateGroups = project.options.createIntermediateGroups
let parentPath = path.parent()
let fileReference = getFileReference(path: path, inPath: parentPath)
let parentGroup = getGroup(
path: parentPath,
mergingChildren: [fileReference],
createIntermediateGroups: createIntermediateGroups,
hasCustomParent: false,
isBaseGroup: true
)
if createIntermediateGroups {
createIntermediaGroups(for: parentGroup, at: parentPath)
}
return fileReference
}
func getFileReference(path: Path, inPath: Path, name: String? = nil, sourceTree: PBXSourceTree = .group, lastKnownFileType: String? = nil) -> PBXFileElement {
let fileReferenceKey = path.string.lowercased()
if let fileReference = fileReferencesByPath[fileReferenceKey] {
return fileReference
} else {
let fileReferencePath = (try? path.relativePath(from: inPath)) ?? path
var fileReferenceName: String? = name ?? fileReferencePath.lastComponent
if fileReferencePath.string == fileReferenceName {
fileReferenceName = nil
}
let lastKnownFileType = lastKnownFileType ?? Xcode.fileType(path: path)
if path.extension == "xcdatamodeld" {
let versionedModels = (try? path.children()) ?? []
// Sort the versions alphabetically
let sortedPaths = versionedModels
.filter { $0.extension == "xcdatamodel" }
.sorted { $0.string.localizedStandardCompare($1.string) == .orderedAscending }
let modelFileReferences =
sortedPaths.map { path in
addObject(
PBXFileReference(
sourceTree: .group,
lastKnownFileType: "wrapper.xcdatamodel",
path: path.lastComponent
)
)
}
// If no current version path is found we fall back to alphabetical
// order by taking the last item in the sortedPaths array
let currentVersionPath = findCurrentCoreDataModelVersionPath(using: versionedModels) ?? sortedPaths.last
let currentVersion: PBXFileReference? = {
guard let indexOf = sortedPaths.firstIndex(where: { $0 == currentVersionPath }) else { return nil }
return modelFileReferences[indexOf]
}()
let versionGroup = addObject(XCVersionGroup(
currentVersion: currentVersion,
path: fileReferencePath.string,
sourceTree: sourceTree,
versionGroupType: "wrapper.xcdatamodel",
children: modelFileReferences
))
fileReferencesByPath[fileReferenceKey] = versionGroup
return versionGroup
} else {
// For all extensions other than `xcdatamodeld`
let fileReference = addObject(
PBXFileReference(
sourceTree: sourceTree,
name: fileReferenceName,
lastKnownFileType: lastKnownFileType,
path: fileReferencePath.string
)
)
fileReferencesByPath[fileReferenceKey] = fileReference
return fileReference
}
}
}
/// returns a default build phase for a given path. This is based off the filename
private func getDefaultBuildPhase(for path: Path, targetType: PBXProductType) -> BuildPhaseSpec? {
if let buildPhase = getFileType(path: path)?.buildPhase {
return buildPhase
}
if let fileExtension = path.extension {
switch fileExtension {
case "modulemap":
guard targetType == .staticLibrary else { return nil }
return .copyFiles(BuildPhaseSpec.CopyFilesSettings(
destination: .productsDirectory,
subpath: "include/$(PRODUCT_NAME)",
phaseOrder: .preCompile
))
case "swiftcrossimport":
guard targetType == .framework else { return nil }
return .copyFiles(BuildPhaseSpec.CopyFilesSettings(
destination: .productsDirectory,
subpath: "$(PRODUCT_NAME).framework/Modules",
phaseOrder: .preCompile
))
default:
return .resources
}
}
return nil
}
/// Create a group or return an existing one at the path.
/// Any merged children are added to a new group or merged into an existing one.
private func getGroup(path: Path, name: String? = nil, mergingChildren children: [PBXFileElement], createIntermediateGroups: Bool, hasCustomParent: Bool, isBaseGroup: Bool) -> PBXGroup {
let groupReference: PBXGroup
if let cachedGroup = groupsByPath[path] {
var cachedGroupChildren = cachedGroup.children
for child in children {
// only add the children that aren't already in the cachedGroup
// Check equality by path and sourceTree because XcodeProj.PBXObject.== is very slow.
if !cachedGroupChildren.contains(where: { $0.name == child.name && $0.path == child.path && $0.sourceTree == child.sourceTree }) {
cachedGroupChildren.append(child)
child.parent = cachedGroup
}
}
cachedGroup.children = cachedGroupChildren
groupReference = cachedGroup
} else {
// lives outside the project base path
let isOutOfBasePath = !path.absolute().string.contains(project.basePath.absolute().string)
// whether the given path is a strict parent of the project base path
// e.g. foo/bar is a parent of foo/bar/baz, but not foo/baz
let isParentOfBasePath = isOutOfBasePath && ((try? path.isParent(of: project.basePath)) == true)
// has no valid parent paths
let isRootPath = (isBaseGroup && isOutOfBasePath && isParentOfBasePath) || path.parent() == project.basePath
// is a top level group in the project
let isTopLevelGroup = !hasCustomParent && ((isBaseGroup && !createIntermediateGroups) || isRootPath || isParentOfBasePath)
let groupName = name ?? path.lastComponent
let groupPath = resolveGroupPath(path, isTopLevelGroup: hasCustomParent || isTopLevelGroup)
let group = PBXGroup(
children: children,
sourceTree: .group,
name: groupName != groupPath ? groupName : nil,
path: groupPath
)
groupReference = addObject(group)
groupsByPath[path] = groupReference
if isTopLevelGroup {
rootGroups.insert(groupReference)
}
}
return groupReference
}
/// Creates a variant group or returns an existing one at the path
private func getVariantGroup(path: Path, inPath: Path) -> PBXVariantGroup {
let variantGroup: PBXVariantGroup
if let cachedGroup = variantGroupsByPath[path] {
variantGroup = cachedGroup
} else {
let group = PBXVariantGroup(
sourceTree: .group,
name: path.lastComponent
)
variantGroup = addObject(group)
variantGroupsByPath[path] = variantGroup
}
return variantGroup
}
/// Collects all the excluded paths within the targetSource
private func getSourceMatches(targetSource: TargetSource, patterns: [String]) -> Set<Path> {
let rootSourcePath = project.basePath + targetSource.path
return Set(
patterns.parallelMap { pattern in
guard !pattern.isEmpty else { return [] }
return Glob(pattern: "\(rootSourcePath)/\(pattern)")
.map { Path($0) }
.map {
guard $0.isDirectory else {
return [$0]
}
return (try? $0.recursiveChildren()) ?? []
}
.reduce([], +)
}
.reduce([], +)
)
}
/// Checks whether the path is not in any default or TargetSource excludes
func isIncludedPath(_ path: Path, excludePaths: Set<Path>, includePaths: SortedArray<Path>?) -> Bool {
return !defaultExcludedFiles.contains(where: { path.lastComponent == $0 })
&& !(path.extension.map(defaultExcludedExtensions.contains) ?? false)
&& !excludePaths.contains(path)
// If includes is empty, it's included. If it's not empty, the path either needs to match exactly, or it needs to be a direct parent of an included path.
&& (includePaths.flatMap { _isIncludedPathSorted(path, sortedPaths: $0) } ?? true)
}
private func _isIncludedPathSorted(_ path: Path, sortedPaths: SortedArray<Path>) -> Bool {
guard let idx = sortedPaths.firstIndex(where: { $0 >= path }) else { return false }
let foundPath = sortedPaths.value[idx]
return foundPath.description.hasPrefix(path.description)
}
/// Gets all the children paths that aren't excluded
private func getSourceChildren(targetSource: TargetSource, dirPath: Path, excludePaths: Set<Path>, includePaths: SortedArray<Path>?) throws -> [Path] {
try dirPath.children()
.filter {
if $0.isDirectory {
let children = try $0.children()
if children.isEmpty {
return project.options.generateEmptyDirectories
}
return !children
.filter { self.isIncludedPath($0, excludePaths: excludePaths, includePaths: includePaths) }
.isEmpty
} else if $0.isFile {
return self.isIncludedPath($0, excludePaths: excludePaths, includePaths: includePaths)
} else {
return false
}
}
}
/// creates all the source files and groups they belong to for a given targetSource
private func getGroupSources(
targetType: PBXProductType,
targetSource: TargetSource,
path: Path,
isBaseGroup: Bool,
hasCustomParent: Bool,
excludePaths: Set<Path>,
includePaths: SortedArray<Path>?,
buildPhases: [Path: BuildPhaseSpec]
) throws -> (sourceFiles: [SourceFile], groups: [PBXGroup]) {
let children = try getSourceChildren(targetSource: targetSource, dirPath: path, excludePaths: excludePaths, includePaths: includePaths)
let createIntermediateGroups = targetSource.createIntermediateGroups ?? project.options.createIntermediateGroups
let nonLocalizedChildren = children.filter { $0.extension != "lproj" }
let stringCatalogChildren = children.filter { $0.extension == "xcstrings" }
let directories = nonLocalizedChildren
.filter {
if let fileType = getFileType(path: $0) {
return !fileType.file
} else {
return $0.isDirectory && !Xcode.isDirectoryFileWrapper(path: $0)
}
}
let filePaths = nonLocalizedChildren
.filter {
if let fileType = getFileType(path: $0) {
return fileType.file
} else {
return $0.isFile || $0.isDirectory && Xcode.isDirectoryFileWrapper(path: $0)
}
}
let localisedDirectories = children
.filter { $0.extension == "lproj" }
var groupChildren: [PBXFileElement] = filePaths.map { getFileReference(path: $0, inPath: path) }
var allSourceFiles: [SourceFile] = filePaths.map {
generateSourceFile(targetType: targetType, targetSource: targetSource, path: $0, buildPhases: buildPhases)
}
var groups: [PBXGroup] = []
for path in directories {
let subGroups = try getGroupSources(
targetType: targetType,
targetSource: targetSource,
path: path,
isBaseGroup: false,
hasCustomParent: false,
excludePaths: excludePaths,
includePaths: includePaths,
buildPhases: buildPhases
)
guard !subGroups.sourceFiles.isEmpty || project.options.generateEmptyDirectories else {
continue
}
allSourceFiles += subGroups.sourceFiles
if let firstGroup = subGroups.groups.first {
groupChildren.append(firstGroup)
groups += subGroups.groups
} else if project.options.generateEmptyDirectories {
groups += subGroups.groups
}
}
// find the base localised directory
let baseLocalisedDirectory: Path? = {
func findLocalisedDirectory(by languageId: String) -> Path? {
localisedDirectories.first { $0.lastComponent == "\(languageId).lproj" }
}
return findLocalisedDirectory(by: "Base") ??
findLocalisedDirectory(by: NSLocale.canonicalLanguageIdentifier(from: project.options.developmentLanguage ?? "en"))
}()
knownRegions.formUnion(localisedDirectories.map { $0.lastComponentWithoutExtension })
// XCode 15 - Detect known regions from locales present in string catalogs
let stringCatalogsLocales = stringCatalogChildren
.compactMap { StringCatalog(from: $0) }
.reduce(Set<String>(), { partialResult, stringCatalog in
partialResult.union(stringCatalog.includedLocales)
})
knownRegions.formUnion(stringCatalogsLocales)
// create variant groups of the base localisation first
var baseLocalisationVariantGroups: [PBXVariantGroup] = []
if let baseLocalisedDirectory = baseLocalisedDirectory {
let filePaths = try baseLocalisedDirectory.children()
.filter { self.isIncludedPath($0, excludePaths: excludePaths, includePaths: includePaths) }
.sorted()
for filePath in filePaths {
let variantGroup = getVariantGroup(path: filePath, inPath: path)
groupChildren.append(variantGroup)
baseLocalisationVariantGroups.append(variantGroup)
let sourceFile = generateSourceFile(targetType: targetType,
targetSource: targetSource,
path: filePath,
fileReference: variantGroup,
buildPhases: buildPhases)
allSourceFiles.append(sourceFile)
}
}
// add references to localised resources into base localisation variant groups
for localisedDirectory in localisedDirectories {
let localisationName = localisedDirectory.lastComponentWithoutExtension
let filePaths = try localisedDirectory.children()
.filter { self.isIncludedPath($0, excludePaths: excludePaths, includePaths: includePaths) }
.sorted { $0.lastComponent < $1.lastComponent }
for filePath in filePaths {
// find base localisation variant group
// ex: Foo.strings will be added to Foo.strings or Foo.storyboard variant group
let variantGroup = baseLocalisationVariantGroups
.first {
Path($0.name!).lastComponent == filePath.lastComponent
} ?? baseLocalisationVariantGroups.first {
Path($0.name!).lastComponentWithoutExtension == filePath.lastComponentWithoutExtension
}
let fileReference = getFileReference(
path: filePath,
inPath: path,
name: variantGroup != nil ? localisationName : filePath.lastComponent
)
if let variantGroup = variantGroup {
if !variantGroup.children.contains(fileReference) {
variantGroup.children.append(fileReference)
}
} else {
// add SourceFile to group if there is no Base.lproj directory
let sourceFile = generateSourceFile(targetType: targetType,
targetSource: targetSource,
path: filePath,
fileReference: fileReference,
buildPhases: buildPhases)
allSourceFiles.append(sourceFile)
groupChildren.append(fileReference)
}
}
}
let group = getGroup(
path: path,
mergingChildren: groupChildren,
createIntermediateGroups: createIntermediateGroups,
hasCustomParent: hasCustomParent,
isBaseGroup: isBaseGroup
)
if createIntermediateGroups {
createIntermediaGroups(for: group, at: path)
}
groups.insert(group, at: 0)
return (allSourceFiles, groups)
}
/// creates source files
private func getSourceFiles(targetType: PBXProductType, targetSource: TargetSource, buildPhases: [Path: BuildPhaseSpec]) throws -> [SourceFile] {
// generate excluded paths
let path = project.basePath + targetSource.path
let excludePaths = getSourceMatches(targetSource: targetSource, patterns: targetSource.excludes)
// generate included paths. Excluded paths will override this.
let includePaths = targetSource.includes.isEmpty ? nil : getSourceMatches(targetSource: targetSource, patterns: targetSource.includes)
let type = resolvedTargetSourceType(for: targetSource, at: path)
let customParentGroups = (targetSource.group ?? "").split(separator: "/").map { String($0) }
let hasCustomParent = !customParentGroups.isEmpty
let createIntermediateGroups = targetSource.createIntermediateGroups ?? project.options.createIntermediateGroups
var sourceFiles: [SourceFile] = []
let sourceReference: PBXFileElement
var sourcePath = path
switch type {
case .folder:
let fileReference = getFileReference(
path: path,
inPath: project.basePath,
name: targetSource.name ?? path.lastComponent,
sourceTree: .sourceRoot,
lastKnownFileType: "folder"
)
if !(createIntermediateGroups || hasCustomParent) || path.parent() == project.basePath {
rootGroups.insert(fileReference)
}
let sourceFile = generateSourceFile(targetType: targetType, targetSource: targetSource, path: path, buildPhases: buildPhases)
sourceFiles.append(sourceFile)
sourceReference = fileReference
case .file:
let parentPath = path.parent()
let fileReference = getFileReference(path: path, inPath: parentPath, name: targetSource.name)
let sourceFile = generateSourceFile(targetType: targetType, targetSource: targetSource, path: path, buildPhases: buildPhases)
if hasCustomParent {
sourcePath = path
sourceReference = fileReference
} else if parentPath == project.basePath {
sourcePath = path
sourceReference = fileReference
rootGroups.insert(fileReference)
} else {
let parentGroup = getGroup(
path: parentPath,
mergingChildren: [fileReference],
createIntermediateGroups: createIntermediateGroups,
hasCustomParent: hasCustomParent,
isBaseGroup: true
)
sourcePath = parentPath
sourceReference = parentGroup
}
sourceFiles.append(sourceFile)
case .group:
if targetSource.optional && !path.exists {
// This group is missing, so if's optional just return an empty array
return []
}
let (groupSourceFiles, groups) = try getGroupSources(
targetType: targetType,
targetSource: targetSource,
path: path,
isBaseGroup: true,
hasCustomParent: hasCustomParent,
excludePaths: excludePaths,
includePaths: includePaths.flatMap(SortedArray.init(_:)),
buildPhases: buildPhases
)
let group = groups.first!
if let name = targetSource.name {
group.name = name
}
sourceFiles += groupSourceFiles
sourceReference = group
}
if hasCustomParent {
createParentGroups(customParentGroups, for: sourceReference)
try makePathRelative(for: sourceReference, at: path)
} else if createIntermediateGroups {
createIntermediaGroups(for: sourceReference, at: sourcePath)
}
return sourceFiles
}
/// Returns the resolved `SourceType` for a given `TargetSource`.
///
/// While `TargetSource` declares `type`, its optional and in the event that the value is not defined then we must resolve a sensible default based on the path of the source.
private func resolvedTargetSourceType(for targetSource: TargetSource, at path: Path) -> SourceType {
return targetSource.type ?? (path.isFile || path.extension != nil ? .file : .group)
}
private func createParentGroups(_ parentGroups: [String], for fileElement: PBXFileElement) {
guard let parentName = parentGroups.last else {
return
}
let parentPath = project.basePath + Path(parentGroups.joined(separator: "/"))
let parentPathExists = parentPath.exists
let parentGroupAlreadyExists = groupsByPath[parentPath] != nil
let parentGroup = getGroup(
path: parentPath,
mergingChildren: [fileElement],
createIntermediateGroups: false,
hasCustomParent: false,
isBaseGroup: parentGroups.count == 1
)
// As this path is a custom group, remove the path reference
if !parentPathExists {
parentGroup.name = String(parentName)
parentGroup.path = nil
}
if !parentGroupAlreadyExists {
createParentGroups(parentGroups.dropLast(), for: parentGroup)
}
}
// Add groups for all parents recursively
private func createIntermediaGroups(for fileElement: PBXFileElement, at path: Path) {
let parentPath = path.parent()
guard parentPath != project.basePath else {
// we've reached the top
return
}
let hasParentGroup = groupsByPath[parentPath] != nil
if !hasParentGroup {
do {
// if the path is a parent of the project base path (or if calculating that fails)
// do not create a parent group
// e.g. for project path foo/bar/baz
// - create foo/baz
// - create baz/
// - do not create foo
let pathIsParentOfProject = try path.isParent(of: project.basePath)
if pathIsParentOfProject { return }
} catch {
return
}
}
let parentGroup = getGroup(
path: parentPath,
mergingChildren: [fileElement],
createIntermediateGroups: true,
hasCustomParent: false,
isBaseGroup: false
)
if !hasParentGroup {
createIntermediaGroups(for: parentGroup, at: parentPath)
}
}
// Make the fileElement path and name relative to its parents aggregated paths
private func makePathRelative(for fileElement: PBXFileElement, at path: Path) throws {
// This makes the fileElement path relative to its parent and not to the project. Xcode then rebuilds the actual
// path for the file based on the hierarchy this fileElement lives in.
var paths: [String] = []
var element: PBXFileElement = fileElement
while true {
guard let parent = element.parent else { break }
if let path = parent.path {
paths.insert(path, at: 0)
}
element = parent
}
let completePath = project.basePath + Path(paths.joined(separator: "/"))
let relativePath = try path.relativePath(from: completePath)
let relativePathString = relativePath.string
if relativePathString != fileElement.path {
fileElement.path = relativePathString
fileElement.name = relativePath.lastComponent
}
}
private func findCurrentCoreDataModelVersionPath(using versionedModels: [Path]) -> Path? {
// Find and parse the current version model stored in the .xccurrentversion file
guard
let versionPath = versionedModels.first(where: { $0.lastComponent == ".xccurrentversion" }),
let data = try? versionPath.read(),
let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any],
let versionString = plist["_XCCurrentVersionName"] as? String else {
return nil
}
return versionedModels.first(where: { $0.lastComponent == versionString })
}
}
@@ -0,0 +1,82 @@
import Foundation
import JSONUtilities
import PathKit
struct StringCatalog {
/**
* Sample string catalog:
* {
* "sourceLanguage" : "en",
* "strings" : {
* "foo" : {
* "localizations" : {
* "en" : {
* ...
* },
* "es" : {
* ...
* },
* "it" : {
* ...
* }
* }
* }
* }
* }
*/
private struct CatalogItem {
private enum JSONKeys: String {
case localizations
}
private let key: String
let locales: Set<String>
init?(key: String, from jsonDictionary: JSONDictionary) {
guard let localizations = jsonDictionary[JSONKeys.localizations.rawValue] as? JSONDictionary else {
return nil
}
self.key = key
self.locales = Set(localizations.keys)
}
}
private enum JSONKeys: String {
case strings
}
private let strings: [CatalogItem]
init?(from path: Path) {
guard let catalogDictionary = try? JSONDictionary.from(url: path.url),
let catalog = StringCatalog(from: catalogDictionary) else {
return nil
}
self = catalog
}
private init?(from jsonDictionary: JSONDictionary) {
guard let stringsDictionary = jsonDictionary[JSONKeys.strings.rawValue] as? JSONDictionary else {
return nil
}
self.strings = stringsDictionary.compactMap { key, value -> CatalogItem? in
guard let stringDictionary = value as? JSONDictionary else {
return nil
}
return CatalogItem(key: key, from: stringDictionary)
}
}
var includedLocales: Set<String> {
strings.reduce(Set<String>(), { partialResult, catalogItem in
partialResult.union(catalogItem.locales)
})
}
}
+58
View File
@@ -0,0 +1,58 @@
import Foundation
import ProjectSpec
extension Project {
public var xcodeVersion: String {
XCodeVersion.parse(options.xcodeVersion ?? "14.3")
}
var schemeVersion: String {
"1.7"
}
var compatibilityVersion: String {
"Xcode 14.0"
}
var objectVersion: UInt {
54
}
var minimizedProjectReferenceProxies: Int {
1
}
}
public struct XCodeVersion {
public static func parse(_ version: String) -> String {
if version.contains(".") {
let parts = version.split(separator: ".").map(String.init)
var string = ""
let major = parts[0]
if major.count == 1 {
string = "0\(major)"
} else {
string = major
}
let minor = parts[1]
string += minor
if parts.count > 2 {
let patch = parts[2]
string += patch
} else {
string += "0"
}
return string
} else if version.count == 2 {
return "\(version)00"
} else if version.count == 1 {
return "0\(version)00"
} else {
return version
}
}
}
@@ -0,0 +1,76 @@
import Foundation
import PathKit
import XcodeProj
extension PBXFileElement {
public var nameOrPath: String {
return name ?? path ?? ""
}
static func sortByNamePath(_ lhs: PBXFileElement, _ rhs: PBXFileElement) -> Bool {
return lhs.namePathSortString.localizedStandardCompare(rhs.namePathSortString) == .orderedAscending
}
private var namePathSortString: String {
// This string needs to be unique for all combinations of name & path or the order won't be stable.
return "\(name ?? path ?? "")\t\(name ?? "")\t\(path ?? "")"
}
}
extension PBXProj {
public func printGroups() -> String {
guard let project = projects.first,
let mainGroup = project.mainGroup else {
return ""
}
return printGroup(group: mainGroup)
}
public func printGroup(group: PBXGroup) -> String {
var string = group.nameOrPath
for child in group.children {
if let group = child as? PBXGroup {
string += "\n 📁 " + printGroup(group: group).replacingOccurrences(of: "\n ", with: "\n ")
} else if let fileReference = child as? PBXFileReference {
string += "\n 📄 " + fileReference.nameOrPath
} else if let variantGroup = child as? PBXVariantGroup {
string += "\n 🌎 " + variantGroup.nameOrPath
} else if let versionGroup = child as? XCVersionGroup {
string += "\n 🔢 " + versionGroup.nameOrPath
}
}
return string
}
}
extension Dictionary {
public var valueArray: [Value] {
Array(values)
}
}
extension Xcode {
public static func fileType(path: Path, productType: PBXProductType? = nil) -> String? {
guard let fileExtension = path.extension else { return nil }
switch (fileExtension, productType) {
// cases that aren't handled (yet) in XcodeProj.
case ("appex", .extensionKitExtension):
return "wrapper.extensionkit-extension"
case ("swiftcrossimport", _):
return "wrapper.swiftcrossimport"
case ("xcstrings", _):
return "text.json.xcstrings"
default:
// fallback to XcodeProj defaults
return Xcode.filetype(extension: fileExtension)
}
}
public static func isDirectoryFileWrapper(path: Path) -> Bool {
guard path.isDirectory else { return false }
return fileType(path: path) != nil
}
}