diff --git a/Sources/ProjectSpec/SourceType.swift b/Sources/ProjectSpec/SourceType.swift index 77ce4ffe7..819cf1b1d 100644 --- a/Sources/ProjectSpec/SourceType.swift +++ b/Sources/ProjectSpec/SourceType.swift @@ -11,4 +11,5 @@ public enum SourceType: String { case group case file case folder + case variantGroup } diff --git a/Sources/XcodeGenKit/PBXProjGenerator.swift b/Sources/XcodeGenKit/PBXProjGenerator.swift index 60ebdfca3..6ad5697d6 100644 --- a/Sources/XcodeGenKit/PBXProjGenerator.swift +++ b/Sources/XcodeGenKit/PBXProjGenerator.swift @@ -222,7 +222,8 @@ public class PBXProjGenerator { pbxProject.projects = subprojects } - try project.targets.forEach(generateTarget) + let pbxVariantGroupInfoList = try PBXVariantGroupGenerator(pbxProj: pbxProj, project: project).generate() + try project.targets.forEach { try generateTarget($0, pbxVariantGroupInfoList: pbxVariantGroupInfoList) } try project.aggregateTargets.forEach(generateAggregateTarget) if !carthageFrameworksByPlatform.isEmpty { @@ -648,13 +649,13 @@ public class PBXProjGenerator { return pbxproj } - func generateTarget(_ target: Target) throws { + func generateTarget(_ target: Target, pbxVariantGroupInfoList: [PBXVariantGroupInfo]) throws { let carthageDependencies = carthageResolver.dependencies(for: target) let infoPlistFiles: [Config: String] = getInfoPlists(for: target) let sourceFileBuildPhaseOverrideSequence: [(Path, BuildPhaseSpec)] = Set(infoPlistFiles.values).map({ (project.basePath + $0, .none) }) let sourceFileBuildPhaseOverrides = Dictionary(uniqueKeysWithValues: sourceFileBuildPhaseOverrideSequence) - let sourceFiles = try sourceGenerator.getAllSourceFiles(targetType: target.type, sources: target.sources, buildPhases: sourceFileBuildPhaseOverrides) + let sourceFiles = try sourceGenerator.getAllSourceFiles(targetName: target.name, targetType: target.type, sources: target.sources, buildPhases: sourceFileBuildPhaseOverrides, pbxVariantGroupInfoList: pbxVariantGroupInfoList) .sorted { $0.path.lastComponent < $1.path.lastComponent } var anyDependencyRequiresObjCLinking = false diff --git a/Sources/XcodeGenKit/PBXVariantGroupGenerator.swift b/Sources/XcodeGenKit/PBXVariantGroupGenerator.swift new file mode 100644 index 000000000..347aead65 --- /dev/null +++ b/Sources/XcodeGenKit/PBXVariantGroupGenerator.swift @@ -0,0 +1,155 @@ +import XcodeProj +import ProjectSpec +import PathKit +import XcodeGenCore + +class PBXVariantGroupInfo { + let targetName: String + let variantGroup: PBXVariantGroup + var path: Path + + init(targetName: String, variantGroup: PBXVariantGroup, path: Path) { + self.targetName = targetName + self.variantGroup = variantGroup + self.path = path + } +} + +class PBXVariantGroupGenerator: TargetSourceFilterable { + let pbxProj: PBXProj + let project: Project + + init(pbxProj: PBXProj, project: Project) { + self.pbxProj = pbxProj + self.project = project + } + + var alwaysStoredBaseExtensions: [String] { + [".xib", ".storyboard", ".intentdefinition"] + } + + func generate() throws -> [PBXVariantGroupInfo] { + var variantGroupInfoList: [PBXVariantGroupInfo] = [] + + try project.targets.forEach { target in + try target.sources.forEach { targetSource in + let excludePaths = getSourceMatches(targetSource: targetSource, + patterns: targetSource.excludes) + let includePaths = getSourceMatches(targetSource: targetSource, + patterns: targetSource.includes) + + let path = project.basePath + targetSource.path + + try generateVarientGroup(targetName: target.name, + targetSource: targetSource, + path: path, + excludePaths: excludePaths, + includePaths: SortedArray(includePaths)) + } + } + + func generateVarientGroup(targetName: String, + targetSource: TargetSource, + path: Path, + excludePaths: Set, + includePaths: SortedArray) throws { + guard path.exists && path.isDirectory && !Xcode.isDirectoryFileWrapper(path: path) else { + return + } + + let children = try getSourceChildren(targetSource: targetSource, + dirPath: path, + excludePaths: excludePaths, + includePaths: includePaths) + + try children.forEach { + let excludePaths = getSourceMatches(targetSource: targetSource, + patterns: targetSource.excludes) + let includePaths = getSourceMatches(targetSource: targetSource, + patterns: targetSource.includes) + + try generateVarientGroup(targetName: targetName, + targetSource: targetSource, + path: $0, + excludePaths: excludePaths, + includePaths: SortedArray(includePaths)) + } + + let localizeDirs: [Path] = children + .filter ({ $0.extension == "lproj" }) + .reduce(into: [Path]()) { partialResult, path in + if path.lastComponentWithoutExtension == "Base" { + partialResult.append(path) + } else { + partialResult.insert(path, at: 0) + } + } + + guard localizeDirs.count > 0 else { + return + } + + try localizeDirs.forEach { localizedDir in + try localizedDir.children() + .filter { self.isIncludedPath($0, excludePaths: excludePaths, includePaths: includePaths) } + .sorted() + .forEach { localizedDirChildPath in + let fileReferencePath = try localizedDirChildPath.relativePath(from: path) + let fileRef = PBXFileReference( + sourceTree: .group, + name: localizedDir.lastComponentWithoutExtension, + lastKnownFileType: Xcode.fileType(path: localizedDirChildPath), + path: fileReferencePath.string + ) + pbxProj.add(object: fileRef) + + let variantGroupInfo = getVariantGroupInfo(targetName: targetName, localizedChildPath: localizedDirChildPath) + + if localizedDir.lastComponentWithoutExtension == "Base" || project.options.developmentLanguage == localizedDir.lastComponentWithoutExtension { + + variantGroupInfo.path = localizedDirChildPath + variantGroupInfo.variantGroup.name = localizedDirChildPath.lastComponent + } + + variantGroupInfo.variantGroup.children.append(fileRef) + } + } + } + + func getVariantGroupInfo(targetName: String, localizedChildPath: Path) -> PBXVariantGroupInfo { + let pbxVariantGroupInfo = variantGroupInfoList + .filter { $0.targetName == targetName } + .first { + let existsAlwaysStoredBaseFile = alwaysStoredBaseExtensions + .reduce(into: [Bool]()) { $0.append(localizedChildPath.lastComponent.contains($1)) } + .filter { $0 } + .count > 0 + + if existsAlwaysStoredBaseFile { + return $0.path.lastComponentWithoutExtension == localizedChildPath.lastComponentWithoutExtension + } else { + return $0.path.lastComponent == localizedChildPath.lastComponent + } + } + + if let pbxVariantGroupInfo = pbxVariantGroupInfo { + return pbxVariantGroupInfo + } else { + let variantGroup = PBXVariantGroup( + sourceTree: .group, + name: localizedChildPath.lastComponent + ) + pbxProj.add(object: variantGroup) + + let pbxVariantGroupInfo = PBXVariantGroupInfo(targetName: targetName, + variantGroup: variantGroup, + path: localizedChildPath) + variantGroupInfoList.append(pbxVariantGroupInfo) + + return pbxVariantGroupInfo + } + } + + return variantGroupInfoList + } +} diff --git a/Sources/XcodeGenKit/SourceGenerator.swift b/Sources/XcodeGenKit/SourceGenerator.swift index e788ef09d..9adbbc606 100644 --- a/Sources/XcodeGenKit/SourceGenerator.swift +++ b/Sources/XcodeGenKit/SourceGenerator.swift @@ -11,25 +11,18 @@ struct SourceFile { let buildPhase: BuildPhaseSpec? } -class SourceGenerator { +class SourceGenerator: TargetSourceFilterable { var rootGroups: Set = [] private let projectDirectory: Path? private var fileReferencesByPath: [String: PBXFileElement] = [:] private var groupsByPath: [Path: PBXGroup] = [:] - private var variantGroupsByPath: [Path: PBXVariantGroup] = [:] + private var pbxVariantGroupInfoList: [PBXVariantGroupInfo] = [] private var localPackageGroup: PBXGroup? - private let project: Project + let project: Project let pbxProj: PBXProj - private var defaultExcludedFiles = [ - ".DS_Store", - ] - private let defaultExcludedExtensions = [ - "orig", - ] - private(set) var knownRegions: Set = [] init(project: Project, pbxProj: PBXProj, projectDirectory: Path?) { @@ -90,16 +83,19 @@ class SourceGenerator { /// 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) } + /// - targetName: The name of target that the source files should belong to. + /// - 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). + /// - pbxVariantGroupInfoList: An array of all PBXVariantGroup information expected to be added to the target + func getAllSourceFiles(targetName: String, targetType: PBXProductType, sources: [TargetSource], buildPhases: [Path : BuildPhaseSpec], pbxVariantGroupInfoList: [PBXVariantGroupInfo]) throws -> [SourceFile] { + self.pbxVariantGroupInfoList = pbxVariantGroupInfoList + return try sources.flatMap { try getSourceFiles(targetName: targetName, 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: [:]) + _ = try getSourceFiles(targetName: "", targetType: .none, targetSource: TargetSource(path: path), buildPhases: [:]) } func getFileType(path: Path) -> FileType? { @@ -338,84 +334,9 @@ class SourceGenerator { 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 { - 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, includePaths: SortedArray) -> 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.value.isEmpty || _isIncludedPathSorted(path, sortedPaths: includePaths)) - } - - private func _isIncludedPathSorted(_ path: Path, sortedPaths: SortedArray) -> 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, includePaths: SortedArray) 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( + targetName: String, targetType: PBXProductType, targetSource: TargetSource, path: Path, @@ -449,9 +370,6 @@ class SourceGenerator { } } - 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) @@ -461,6 +379,7 @@ class SourceGenerator { for path in directories { let subGroups = try getGroupSources( + targetName: targetName, targetType: targetType, targetSource: targetSource, path: path, @@ -484,79 +403,36 @@ class SourceGenerator { 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 }) - - // create variant groups of the base localisation first - var baseLocalisationVariantGroups: [PBXVariantGroup] = [] - - if let baseLocalisedDirectory = baseLocalisedDirectory { - let filePaths = try baseLocalisedDirectory.children() + + let localisedDirectories = children + .filter { $0.extension == "lproj" } + + for localizedDir in localisedDirectories { + + let localizedDirChildren = try localizedDir.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) - + + for localizedDirChildPath in localizedDirChildren { + + guard let variantGroupInfo = pbxVariantGroupInfoList + .filter({ $0.targetName == targetName }) + .first(where: { $0.path == localizedDirChildPath }) else { + break + } + groupChildren.append(variantGroupInfo.variantGroup) + let sourceFile = generateSourceFile(targetType: targetType, targetSource: targetSource, - path: filePath, - fileReference: variantGroup, + path: localizedDirChildPath, + fileReference: variantGroupInfo.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) - } - } - } - + + knownRegions.formUnion(localisedDirectories.map { $0.lastComponentWithoutExtension }) + let group = getGroup( path: path, mergingChildren: groupChildren, @@ -573,7 +449,7 @@ class SourceGenerator { } /// creates source files - private func getSourceFiles(targetType: PBXProductType, targetSource: TargetSource, buildPhases: [Path: BuildPhaseSpec]) throws -> [SourceFile] { + private func getSourceFiles(targetName: String, targetType: PBXProductType, targetSource: TargetSource, buildPhases: [Path: BuildPhaseSpec]) throws -> [SourceFile] { // generate excluded paths let path = project.basePath + targetSource.path @@ -642,6 +518,7 @@ class SourceGenerator { } let (groupSourceFiles, groups) = try getGroupSources( + targetName: targetName, targetType: targetType, targetSource: targetSource, path: path, @@ -659,6 +536,19 @@ class SourceGenerator { sourceFiles += groupSourceFiles sourceReference = group + + case .variantGroup: + let variantGroup: PBXVariantGroup? = pbxVariantGroupInfoList + .first { $0.path == path }?.variantGroup + + let sourceFile = generateSourceFile(targetType: targetType, + targetSource: targetSource, + path: path, + fileReference: variantGroup, + buildPhases: buildPhases) + + sourceFiles.append(sourceFile) + sourceReference = variantGroup! } if hasCustomParent { @@ -675,7 +565,11 @@ class SourceGenerator { /// /// 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) + if targetSource.path.contains(".lproj") { + return .variantGroup + } else { + return targetSource.type ?? (path.isFile || path.extension != nil ? .file : .group) + } } private func createParentGroups(_ parentGroups: [String], for fileElement: PBXFileElement) { diff --git a/Sources/XcodeGenKit/TargetSourceFilterable.swift b/Sources/XcodeGenKit/TargetSourceFilterable.swift new file mode 100644 index 000000000..74fb608ed --- /dev/null +++ b/Sources/XcodeGenKit/TargetSourceFilterable.swift @@ -0,0 +1,79 @@ +import XcodeProj +import ProjectSpec +import PathKit +import XcodeGenCore + +protocol TargetSourceFilterable { + var project: Project { get } + var defaultExcludedFiles: [String] { get } + var defaultExcludedExtensions: [String] { get } +} + +extension TargetSourceFilterable { + + var defaultExcludedFiles: [String] { + [".DS_Store"] + } + + var defaultExcludedExtensions: [String] { + ["orig"] + } + + /// Gets all the children paths that aren't excluded + func getSourceChildren(targetSource: TargetSource, dirPath: Path, excludePaths: Set, includePaths: SortedArray) 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 + } + } + } + + /// Checks whether the path is not in any default or TargetSource excludes + func isIncludedPath(_ path: Path, excludePaths: Set, includePaths: SortedArray) -> 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.value.isEmpty || _isIncludedPathSorted(path, sortedPaths: includePaths)) + } + + private func _isIncludedPathSorted(_ path: Path, sortedPaths: SortedArray) -> Bool { + guard let idx = sortedPaths.firstIndex(where: { $0 >= path }) else { return false } + let foundPath = sortedPaths.value[idx] + return foundPath.description.hasPrefix(path.description) + } + + func getSourceMatches(targetSource: TargetSource, patterns: [String]) -> Set { + 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([], +) + ) + } +} diff --git a/Tests/Fixtures/TestProject/App_Clip/Base.lproj/Main.storyboard b/Tests/Fixtures/TestProject/App_Clip/Base.lproj/Storyboard.storyboard similarity index 100% rename from Tests/Fixtures/TestProject/App_Clip/Base.lproj/Main.storyboard rename to Tests/Fixtures/TestProject/App_Clip/Base.lproj/Storyboard.storyboard diff --git a/Tests/Fixtures/TestProject/App_Clip/en.lproj/Localizable.stringsdict b/Tests/Fixtures/TestProject/App_Clip/en.lproj/Localizable.stringsdict new file mode 100644 index 000000000..006d3e9b5 --- /dev/null +++ b/Tests/Fixtures/TestProject/App_Clip/en.lproj/Localizable.stringsdict @@ -0,0 +1,30 @@ + + + + + StringKey + + NSStringLocalizedFormatKey + %#@VARIABLE@ + VARIABLE + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + + zero + + one + + two + + few + + many + + other + + + + + diff --git a/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj b/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj index cc3fa31dc..5c010b572 100644 --- a/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj +++ b/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj @@ -35,7 +35,6 @@ 0AB541AE3163B063E7012877 /* StaticLibrary_ObjC.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5A2B916A11DCC2565241359F /* StaticLibrary_ObjC.h */; }; 0BDA156BEBFCB9E65910F838 /* MyFramework.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A58A16491CDDF968B0D56DE /* MyFramework.h */; settings = {ATTRIBUTES = (Public, ); }; }; 0D0E2466833FC2636B92C43D /* Swinject in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = D7917D10F77DA9D69937D493 /* Swinject */; }; - 0F99AECCB4691803C791CDCE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2FC2A8A829CE71B1CF415FF7 /* Main.storyboard */; }; 15129B8D9ED000BDA1FEEC27 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A2F16890ECF2EE3FED72AE /* AppDelegate.swift */; }; 1551370B0ACAC632E15C853B /* SwiftyJSON.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FF47010E7368583405AA50CB /* SwiftyJSON.framework */; }; 1BC891D89980D82738D963F3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 74FBDFA5CB063F6001AD8ACD /* Main.storyboard */; }; @@ -43,6 +42,7 @@ 1E2A4D61E96521FF7123D7B0 /* XPC Service.xpc in CopyFiles */ = {isa = PBXBuildFile; fileRef = 22237B8EBD9E6BE8EBC8735F /* XPC Service.xpc */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 1E457F55331FD2C3E8E00BE2 /* Result.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0C5AC2545AE4D4F7F44E2E9B /* Result.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 1F9168A43FD8E2FCC2699E14 /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = B5C943D39DD7812CAB94B614 /* Documentation.docc */; settings = {COMPILER_FLAGS = "-Werror"; }; }; + 20B51B15FDAA4B296642DB9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = E8222828982682ACB6942EC9 /* Localizable.stringsdict */; }; 210B49C23B9717C668B40C8C /* FrameworkFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5F527F2590C14956518174 /* FrameworkFile.swift */; }; 2116F89CF5A04EA0EFA30A89 /* TestProjectUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D88C6BF7355702B74396791 /* TestProjectUITests.swift */; }; 212BCB51DAF3212993DDD49E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D51CC8BCCBD68A90E90A3207 /* Assets.xcassets */; }; @@ -62,6 +62,7 @@ 3133B36F3898A27A2B1C56FC /* FrameworkFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5F527F2590C14956518174 /* FrameworkFile.swift */; }; 32956CD11BD6B02E64F5D8D1 /* swift-tagged.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E4841131C451A658AC8596C /* swift-tagged.framework */; }; 3318F40C855184C18197ED30 /* StaticLibrary_ObjC.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D0C79A8C750EC0DE748C463 /* StaticLibrary_ObjC.m */; }; + 3352A6F4623250F4186D49AF /* Storyboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BDF80447206456A66BD42FB7 /* Storyboard.storyboard */; }; 339578307B9266AB3D7722D9 /* File2.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC56891DA7446EAC8C2F27EB /* File2.swift */; }; 3535891EC86283BB5064E7E1 /* Headers in Headers */ = {isa = PBXBuildFile; fileRef = 2E1E747C7BC434ADB80CC269 /* Headers */; settings = {ATTRIBUTES = (Public, ); }; }; 3788E1382B38DF4ACE3D2BB1 /* MyFramework.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A58A16491CDDF968B0D56DE /* MyFramework.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -135,6 +136,7 @@ A90C4C147AD175DB9F7B5114 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CD22B8CD2E91BB97CC534E /* main.swift */; }; A949422315536EACDF8DD78A /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B785B1161553A7DD6DA4255 /* NetworkExtension.framework */; }; A9548E5DCFE92236494164DF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE1F06D99242F4223D081F0D /* LaunchScreen.storyboard */; }; + AC15BD11AE6121DDE7C34A51 /* Storyboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BDF80447206456A66BD42FB7 /* Storyboard.storyboard */; }; AFF19412E9B35635D3AF48CB /* XPC_Service.m in Sources */ = {isa = PBXBuildFile; fileRef = 148B7C933698BCC4F1DBA979 /* XPC_Service.m */; }; B142965C5AE9C6200BF65802 /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C5AC2545AE4D4F7F44E2E9B /* Result.framework */; platformFilter = maccatalyst; }; B18C121B0A4D43ED8149D8E2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 79325B44B19B83EC6CEDBCC5 /* LaunchScreen.storyboard */; }; @@ -157,6 +159,7 @@ CCA17097382757012B58C17C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1BC32A813B80A53962A1F365 /* Assets.xcassets */; }; D5458D67C3596943114C3205 /* Standalone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F5BD97AF0F94A15A5B7DDB7 /* Standalone.swift */; }; D61BEABD5B26B2DE67D0C2EC /* FrameworkFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5F527F2590C14956518174 /* FrameworkFile.swift */; }; + D6312C8EBF458001D43FBDC2 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = E8222828982682ACB6942EC9 /* Localizable.stringsdict */; }; D8ED40ED61AD912385CFF5F0 /* StaticLibrary_ObjC.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D0C79A8C750EC0DE748C463 /* StaticLibrary_ObjC.m */; }; DD5FBFC3C1B2DB3D0D1CF210 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B785B1161553A7DD6DA4255 /* NetworkExtension.framework */; }; E0B27599D701E6BB0223D0A8 /* FilterDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16AA52945B70B1BF9E246350 /* FilterDataProvider.swift */; }; @@ -690,7 +693,6 @@ 2A5F527F2590C14956518174 /* FrameworkFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameworkFile.swift; sourceTree = ""; }; 2E1E747C7BC434ADB80CC269 /* Headers */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Headers; sourceTree = SOURCE_ROOT; }; 2F430AABE04B7499B458D9DB /* SwiftFileInDotPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftFileInDotPath.swift; sourceTree = ""; }; - 3096A0760969873D46F80A92 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 325F18855099386B08DD309B /* Resource.abcd */ = {isa = PBXFileReference; path = Resource.abcd; sourceTree = ""; }; 33F6DCDC37D2E66543D4965D /* App_macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App_macOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 34F13B632328979093CE6056 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; @@ -798,6 +800,7 @@ E43116070AFEF5D8C3A5A957 /* TestFramework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TestFramework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E55F45EACB0F382722D61C8D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; E5E0A80CCE8F8DB662DCD2D0 /* EndpointSecuritySystemExtension.systemextension */ = {isa = PBXFileReference; explicitFileType = "wrapper.system-extension"; includeInIndex = 0; path = EndpointSecuritySystemExtension.systemextension; sourceTree = BUILT_PRODUCTS_DIR; }; + E9182F7632482B8412DC5AE0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; E9672EF8FE1DDC8DE0705129 /* PushNotificationPayload.apns */ = {isa = PBXFileReference; lastKnownFileType = text; path = PushNotificationPayload.apns; sourceTree = ""; }; EDCC70978B8AD49373DA0DE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; EE1343F2238429D4DA9D830B /* File1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = File1.swift; path = Group/File1.swift; sourceTree = ""; }; @@ -806,6 +809,7 @@ F192E783CCA898FBAA5C34EA /* AnotherProject */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = AnotherProject; path = AnotherProject/AnotherProject.xcodeproj; sourceTree = ""; }; F2950763C4C568CC85021D18 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; F2FA55A558627ED576A4AFD6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + FA152C2101E254C78F43CFAE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Storyboard.storyboard; sourceTree = ""; }; FA86D418796C1A6864414460 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; FD05F36F95D6F098A76F220B /* XPC_Service.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = XPC_Service.h; sourceTree = ""; }; FD4A16C7B8FEB7F97F3CBE3F /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; }; @@ -1342,7 +1346,8 @@ 1FA5E208EC184E3030D2A21D /* Clip.entitlements */, 6F165CDD5BCC13AFF50B65E2 /* Info.plist */, 79325B44B19B83EC6CEDBCC5 /* LaunchScreen.storyboard */, - 2FC2A8A829CE71B1CF415FF7 /* Main.storyboard */, + E8222828982682ACB6942EC9 /* Localizable.stringsdict */, + BDF80447206456A66BD42FB7 /* Storyboard.storyboard */, DFE6A6FAAFF701FE729293DE /* ViewController.swift */, ); path = App_Clip; @@ -1941,6 +1946,7 @@ buildConfigurationList = 129D9E77D45A66B1C78578F2 /* Build configuration list for PBXNativeTarget "App_Clip_UITests" */; buildPhases = ( 2E1429F0FB524A2BCFC61DF1 /* Sources */, + 78249BAE6D3EE80459CF1FA9 /* Resources */, 05D615CB74F875917AA8C9B0 /* Embed Frameworks */, ); buildRules = ( @@ -2306,6 +2312,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 78249BAE6D3EE80459CF1FA9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D6312C8EBF458001D43FBDC2 /* Localizable.stringsdict in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 8508BA1B733839E314AF2853 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -2322,6 +2336,7 @@ 61601545B6BE00CA74A4E38F /* SceneKitCatalog.scnassets in Resources */, 28A96EBC76D53817AABDA91C /* Settings.bundle in Resources */, E8A135F768448632F8D77C8F /* StandaloneAssets.xcassets in Resources */, + AC15BD11AE6121DDE7C34A51 /* Storyboard.storyboard in Resources */, 818D448D4DDD6649B5B26098 /* example.mp4 in Resources */, 2C7C03B45571A13D472D6B23 /* iMessageApp.app in Resources */, ); @@ -2341,7 +2356,8 @@ files = ( 61516CAC12B2843FBC4572E6 /* Assets.xcassets in Resources */, B18C121B0A4D43ED8149D8E2 /* LaunchScreen.storyboard in Resources */, - 0F99AECCB4691803C791CDCE /* Main.storyboard in Resources */, + 20B51B15FDAA4B296642DB9D /* Localizable.stringsdict in Resources */, + 3352A6F4623250F4186D49AF /* Storyboard.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3007,14 +3023,6 @@ name = LocalizedStoryboard.storyboard; sourceTree = ""; }; - 2FC2A8A829CE71B1CF415FF7 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 3096A0760969873D46F80A92 /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; 65C8D6D1DDC1512D396C07B7 /* Localizable.stringsdict */ = { isa = PBXVariantGroup; children = ( @@ -3065,6 +3073,14 @@ name = Localizable.strings; sourceTree = ""; }; + BDF80447206456A66BD42FB7 /* Storyboard.storyboard */ = { + isa = PBXVariantGroup; + children = ( + FA152C2101E254C78F43CFAE /* Base */, + ); + name = Storyboard.storyboard; + sourceTree = ""; + }; C872631362DDBAFCE71E5C66 /* Interface.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -3081,6 +3097,14 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; + E8222828982682ACB6942EC9 /* Localizable.stringsdict */ = { + isa = PBXVariantGroup; + children = ( + E9182F7632482B8412DC5AE0 /* en */, + ); + name = Localizable.stringsdict; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ diff --git a/Tests/Fixtures/TestProject/project.yml b/Tests/Fixtures/TestProject/project.yml index fb052d3fa..06d70b85b 100644 --- a/Tests/Fixtures/TestProject/project.yml +++ b/Tests/Fixtures/TestProject/project.yml @@ -121,6 +121,7 @@ targets: resourceTags: - tag1 - tag2 + - path: App_Clip/Base.lproj/Storyboard.storyboard settings: INFOPLIST_FILE: App_iOS/Info.plist PRODUCT_BUNDLE_IDENTIFIER: com.project.app @@ -390,7 +391,9 @@ targets: App_Clip_UITests: type: bundle.ui-testing platform: iOS - sources: App_Clip_UITests + sources: + - path: App_Clip_UITests + - path: App_Clip/en.lproj/Localizable.stringsdict dependencies: - target: App_Clip # https://github.com/yonaskolb/XcodeGen/issues/1232 diff --git a/Tests/XcodeGenKitTests/SourceGeneratorTests.swift b/Tests/XcodeGenKitTests/SourceGeneratorTests.swift index de033f509..460308b28 100644 --- a/Tests/XcodeGenKitTests/SourceGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/SourceGeneratorTests.swift @@ -143,41 +143,77 @@ class SourceGeneratorTests: XCTestCase { let directories = """ Sources: Base.lproj: + - LocalizedXib.xib - LocalizedStoryboard.storyboard + - IntentDefinition.intentdefinition + - BaseLocalizable.strings en.lproj: + - LocalizedXib.strings - LocalizedStoryboard.strings + - IntentDefinition.strings + - BaseLocalizable.strings + - Localizable.strings + ja.lproj: + - LocalizedXib.strings + - LocalizedStoryboard.strings + - IntentDefinition.strings + - BaseLocalizable.strings + - Localizable.strings """ try createDirectories(directories) let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"]) - let project = Project(basePath: directoryPath, name: "Test", targets: [target]) + let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: SpecOptions(developmentLanguage: "en")) let pbxProj = try project.generatePbxProj() - - func getFileReferences(_ path: String) -> [PBXFileReference] { - pbxProj.fileReferences.filter { $0.path == path } - } - - func getVariableGroups(_ name: String?) -> [PBXVariantGroup] { - pbxProj.variantGroups.filter { $0.name == name } - } - - let resourceName = "LocalizedStoryboard.storyboard" - let baseResource = "Base.lproj/LocalizedStoryboard.storyboard" - let localizedResource = "en.lproj/LocalizedStoryboard.strings" - - let variableGroup = try unwrap(getVariableGroups(resourceName).first) - - do { - let refs = getFileReferences(baseResource) - try expect(refs.count) == 1 - try expect(variableGroup.children.filter { $0 == refs.first }.count) == 1 - } - - do { - let refs = getFileReferences(localizedResource) - try expect(refs.count) == 1 - try expect(variableGroup.children.filter { $0 == refs.first }.count) == 1 + + let resourceList: [(name: String, basePath: String, localizedResources: [String])] = [ + // resouce is stored in base localized directory + ( + name: "LocalizedXib.xib", + basePath: "Base.lproj/LocalizedXib.xib", + localizedResources: ["en.lproj/LocalizedXib.strings", "ja.lproj/LocalizedXib.strings"] + ), + ( + name: "LocalizedStoryboard.storyboard", + basePath: "Base.lproj/LocalizedStoryboard.storyboard", + localizedResources: ["en.lproj/LocalizedStoryboard.strings", "ja.lproj/LocalizedStoryboard.strings"] + ), + ( + name: "IntentDefinition.intentdefinition", + basePath: "Base.lproj/IntentDefinition.intentdefinition", + localizedResources: ["en.lproj/IntentDefinition.strings", "ja.lproj/IntentDefinition.strings"] + ), + ( + name: "BaseLocalizable.strings", + basePath: "Base.lproj/BaseLocalizable.strings", + localizedResources: ["en.lproj/BaseLocalizable.strings", "ja.lproj/BaseLocalizable.strings"] + ), + // resouce is not stored in base localized directory + ( + name: "Localizable.strings", + basePath: "en.lproj/Localizable.strings", + localizedResources: ["ja.lproj/Localizable.strings"] + ) + ] + + try resourceList.forEach { resource in + + let variableGroup = try unwrap(pbxProj.variantGroups.filter { $0.name == resource.name }.first) + + do { + let refs = pbxProj.fileReferences.filter { $0.path == resource.basePath } + try expect(refs.count) == 1 + try expect(variableGroup.children.filter { $0 == refs.first }.count) == 1 + } + + try resource.localizedResources.forEach { localizedResource in + do { + let refs = pbxProj.fileReferences.filter { $0.path == localizedResource } + try expect(refs.count) == 1 + try expect(variableGroup.children.filter { $0 == refs.first }.count) == 1 + } + } } }