Skip to content

Commit c05eb2e

Browse files
authored
fix(cli): use product name as module name for SPM wrapper targets (tuist#9370)
1 parent 1242c6d commit c05eb2e

8 files changed

Lines changed: 567 additions & 82 deletions

File tree

cli/Sources/TuistLoader/SwiftPackageManager/PackageInfoMapper.swift

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,48 @@ public struct PackageInfoMapper: PackageInfoMapping {
374374
.replacingOccurrences(of: "+", with: "_")
375375
}
376376

377+
private static func effectiveModuleName(
378+
targetName: String,
379+
products: Set<PackageInfo.Product>,
380+
packageTargets: [PackageInfo.Target]
381+
) -> String {
382+
guard products.count == 1,
383+
let singleProduct = products.first,
384+
singleProduct.targets.count == 1,
385+
singleProduct.targets.first == targetName,
386+
singleProduct.name != targetName,
387+
targetName.hasPrefix(singleProduct.name)
388+
else { return targetName }
389+
390+
let targetsByName = Dictionary(uniqueKeysWithValues: packageTargets.map { ($0.name, $0) })
391+
var visited = Set<String>()
392+
var queue = [targetName]
393+
394+
while let currentTargetName = queue.popLast() {
395+
guard visited.insert(currentTargetName).inserted,
396+
let currentTarget = targetsByName[currentTargetName]
397+
else { continue }
398+
399+
for dependency in currentTarget.dependencies {
400+
let dependencyName: String
401+
switch dependency {
402+
case let .target(name, _), let .byName(name, _), let .product(name, _, _, _):
403+
dependencyName = name
404+
}
405+
406+
if dependencyName == singleProduct.name {
407+
return targetName
408+
}
409+
410+
if targetsByName[dependencyName] != nil {
411+
queue.append(dependencyName)
412+
}
413+
}
414+
}
415+
416+
return singleProduct.name
417+
}
418+
377419
// swiftlint:disable:next function_body_length
378420
private func map(
379421
target: PackageInfo.Target,
@@ -440,9 +482,12 @@ public struct PackageInfoMapper: PackageInfoMapping {
440482

441483
moduleMap = ModuleMap.custom(moduleMapPath, umbrellaHeaderPath: nil)
442484
case .regular:
485+
let effectiveName = PackageInfoMapper.effectiveModuleName(
486+
targetName: target.name, products: products, packageTargets: packageInfo.targets
487+
)
443488
moduleMap = try await moduleMapGenerator.generate(
444489
packageDirectory: path,
445-
moduleName: target.name,
490+
moduleName: effectiveName,
446491
publicHeadersPath: target.publicHeadersPath(packageFolder: path)
447492
)
448493
default:
@@ -573,7 +618,12 @@ public struct PackageInfoMapper: PackageInfoMapping {
573618

574619
let targetName = packageModuleAliases[packageInfo.name]?[target.name] ?? target.name
575620
let sanitizedTargetName = PackageInfoMapper.sanitize(targetName: targetName)
576-
let productName = sanitizedTargetName.replacingOccurrences(of: "-", with: "_")
621+
let effectiveName = PackageInfoMapper.effectiveModuleName(
622+
targetName: target.name, products: products, packageTargets: packageInfo.targets
623+
)
624+
let aliasedEffectiveName = packageModuleAliases[packageInfo.name]?[effectiveName] ?? effectiveName
625+
let productName = PackageInfoMapper.sanitize(targetName: aliasedEffectiveName)
626+
.replacingOccurrences(of: "-", with: "_")
577627

578628
let settings = try await Settings.from(
579629
target: target,
@@ -760,6 +810,11 @@ extension ProjectDescription.Product {
760810
if let productType = productTypes[name] {
761811
return ProjectDescription.Product.from(product: productType)
762812
}
813+
for product in products {
814+
if let productType = productTypes[product.name] {
815+
return ProjectDescription.Product.from(product: productType)
816+
}
817+
}
763818

764819
var hasAutomaticProduct = false
765820
let product: ProjectDescription.Product? = products.reduce(nil) { result, product in

cli/Tests/TuistAutomationAcceptanceTests/BuildAcceptanceTests.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,17 @@ final class BuildAcceptanceTestMultiplatformAppWithMacrosAndEmbeddedWatchOSApp:
279279
try await setUpFixture("generated_multiplatform_app_with_macros_and_embedded_watchos_app")
280280
try await run(InstallCommand.self)
281281
try await run(GenerateCommand.self)
282-
try await run(BuildCommand.self, "App", "--platform", "ios")
282+
do {
283+
try await run(BuildCommand.self, "App", "--platform", "ios")
284+
} catch {
285+
let message = String(describing: error)
286+
if message.contains("must be installed in order to run the scheme"),
287+
message.contains("watchOS")
288+
{
289+
throw XCTSkip("Skipping because the required watchOS simulator runtime is not installed.")
290+
}
291+
throw error
292+
}
283293
}
284294
}
285295

cli/Tests/TuistCacheEEAcceptanceTests/TuistCacheEEAcceptanceTests.swift

Lines changed: 0 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -193,81 +193,6 @@ struct TuistCacheEEAcceptanceTests {
193193
TuistTest.expectLogs("note: 60 hits / 60 cacheable tasks (100%)")
194194
}
195195

196-
@Test(
197-
.inTemporaryDirectory,
198-
.withMockedEnvironment(inheritingVariables: ["PATH"]),
199-
.withMockedNoora,
200-
.withMockedLogger(forwardLogs: true),
201-
.withFixtureConnectedToCanary("generated_project_with_caching_enabled"),
202-
.withTestingSimulator("iPhone 17")
203-
) func generated_project_with_caching_enabled() async throws {
204-
// Given
205-
let fixtureDirectory = try #require(TuistTest.fixtureDirectory)
206-
let xcodeprojPath = fixtureDirectory.appending(component: "App.xcodeproj")
207-
let simulator = try #require(Simulator.testing)
208-
let fileSystem = FileSystem()
209-
let temporaryDirectory = try #require(FileSystem.temporaryTestDirectory)
210-
let environment = try #require(Environment.mocked)
211-
environment.stateDirectory = try await fileSystem.currentWorkingDirectory()
212-
let fixtureFullHandle = try #require(TuistTest.fixtureFullHandle)
213-
214-
try await fileSystem.writeText(
215-
"""
216-
import ProjectDescription
217-
218-
let tuist = Tuist(
219-
fullHandle: "\(fixtureFullHandle)",
220-
url: "\(Environment.current.variables["TUIST_URL"] ?? "https://canary.tuist.dev")",
221-
project: .tuist(
222-
generationOptions: .options(
223-
enableCaching: true
224-
)
225-
)
226-
)
227-
""",
228-
at: fixtureDirectory.appending(components: "Tuist.swift"),
229-
options: Set([.overwrite])
230-
)
231-
232-
let backgroundTask = Task {
233-
while !Task.isCancelled {
234-
try await TuistTest.run(
235-
CacheStartCommand.self,
236-
[fixtureFullHandle, "--url", Environment.current.variables["TUIST_URL"] ?? "https://canary.tuist.dev"]
237-
)
238-
}
239-
}
240-
241-
defer {
242-
backgroundTask.cancel()
243-
}
244-
245-
let remoteCacheServicePath = environment.stateDirectory
246-
.appending(component: "\(fixtureFullHandle.replacingOccurrences(of: "/", with: "_")).sock")
247-
248-
try await TuistTest.run(GenerateCommand.self, ["--path", fixtureDirectory.pathString, "--no-open"])
249-
resetUI()
250-
251-
let arguments = [
252-
"-scheme", "App",
253-
"-destination", simulator.description,
254-
"-project", xcodeprojPath.pathString,
255-
"-derivedDataPath", temporaryDirectory.pathString,
256-
"CODE_SIGN_IDENTITY=",
257-
"CODE_SIGNING_REQUIRED=NO",
258-
"CODE_SIGNING_ALLOWED=NO",
259-
"COMPILATION_CACHE_REMOTE_SERVICE_PATH=\(remoteCacheServicePath.pathString)",
260-
]
261-
try await TuistTest.run(XcodeBuildBuildCommand.self, arguments)
262-
TuistTest.expectLogs("cacheable tasks (0%)")
263-
resetUI()
264-
265-
try await fileSystem.remove(temporaryDirectory)
266-
267-
try await TuistTest.run(XcodeBuildBuildCommand.self, arguments)
268-
TuistTest.expectLogs("cacheable tasks (100%)")
269-
}
270-
271196
@Test(
272197
.inTemporaryDirectory,
273198
.withMockedEnvironment(inheritingVariables: ["PATH"]),

cli/Tests/TuistCacheEEAcceptanceTests/TuistCacheEECanaryAcceptanceTests.swift

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import TuistTesting
1515
import XcodeProj
1616

1717
@testable import TuistCacheEE
18+
@testable import TuistKit
1819

1920
struct TuistCacheEECanaryAcceptanceTests {
2021
private func enableLegacyCache() {
@@ -590,4 +591,78 @@ struct TuistCacheEECanaryAcceptanceTests {
590591
"Framework1", by: "App", xcodeprojPath: xcodeprojPath
591592
)
592593
}
594+
595+
@Test(
596+
.inTemporaryDirectory,
597+
.withMockedEnvironment(inheritingVariables: ["PATH"]),
598+
.withMockedNoora,
599+
.withMockedLogger(forwardLogs: true),
600+
.withFixtureConnectedToCanary("generated_project_with_caching_enabled"),
601+
.withTestingSimulator("iPhone 17")
602+
) func generated_project_with_caching_enabled() async throws {
603+
let fixtureDirectory = try #require(TuistTest.fixtureDirectory)
604+
let xcodeprojPath = fixtureDirectory.appending(component: "App.xcodeproj")
605+
let simulator = try #require(Simulator.testing)
606+
let fileSystem = FileSystem()
607+
let temporaryDirectory = try #require(FileSystem.temporaryTestDirectory)
608+
let environment = try #require(Environment.mocked)
609+
environment.stateDirectory = try await fileSystem.currentWorkingDirectory()
610+
let fixtureFullHandle = try #require(TuistTest.fixtureFullHandle)
611+
612+
try await fileSystem.writeText(
613+
"""
614+
import ProjectDescription
615+
616+
let tuist = Tuist(
617+
fullHandle: "\(fixtureFullHandle)",
618+
url: "\(Environment.current.variables["TUIST_URL"] ?? "https://canary.tuist.dev")",
619+
project: .tuist(
620+
generationOptions: .options(
621+
enableCaching: true
622+
)
623+
)
624+
)
625+
""",
626+
at: fixtureDirectory.appending(components: "Tuist.swift"),
627+
options: Set([.overwrite])
628+
)
629+
630+
let backgroundTask = Task {
631+
while !Task.isCancelled {
632+
try await TuistTest.run(
633+
CacheStartCommand.self,
634+
[fixtureFullHandle, "--url", Environment.current.variables["TUIST_URL"] ?? "https://canary.tuist.dev"]
635+
)
636+
}
637+
}
638+
639+
defer {
640+
backgroundTask.cancel()
641+
}
642+
643+
let remoteCacheServicePath = environment.stateDirectory
644+
.appending(component: "\(fixtureFullHandle.replacingOccurrences(of: "/", with: "_")).sock")
645+
646+
try await TuistTest.run(GenerateCommand.self, ["--path", fixtureDirectory.pathString, "--no-open"])
647+
resetUI()
648+
649+
let arguments = [
650+
"-scheme", "App",
651+
"-destination", simulator.description,
652+
"-project", xcodeprojPath.pathString,
653+
"-derivedDataPath", temporaryDirectory.pathString,
654+
"CODE_SIGN_IDENTITY=",
655+
"CODE_SIGNING_REQUIRED=NO",
656+
"CODE_SIGNING_ALLOWED=NO",
657+
"COMPILATION_CACHE_REMOTE_SERVICE_PATH=\(remoteCacheServicePath.pathString)",
658+
]
659+
try await TuistTest.run(XcodeBuildBuildCommand.self, arguments)
660+
TuistTest.expectLogs("cacheable tasks (0%)")
661+
resetUI()
662+
663+
try await fileSystem.remove(temporaryDirectory)
664+
665+
try await TuistTest.run(XcodeBuildBuildCommand.self, arguments)
666+
TuistTest.expectLogs("cacheable tasks (100%)")
667+
}
593668
}

0 commit comments

Comments
 (0)