Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tuist generate regression after 4.31.0 with RevenueCat: The file “App” couldn’t be opened because there is no such file. #7145

Open
KaiOelfke opened this issue Dec 7, 2024 · 9 comments · May be fixed by tuist/FileSystem#98
Labels
domain:projects Issues related to the project-generation features type:bug Something isn't working

Comments

@KaiOelfke
Copy link
Contributor

KaiOelfke commented Dec 7, 2024

What happened?

I have a project, which generates with 4.31.0, but any later version fails. First with some CDEEventStoreModel.xcdatamodeld from a closed-source SPM package. And from Tuist 4.33.0 it fails for RevenueCat's purchases-ios-spm repo.

I guess something is wrong with the file system changes and symlinks. In the RevenueCat project I can see that CustomEntitlementComputation is a symlink to the Sources folder.

https://github.com/RevenueCat/purchases-ios-spm/blob/main/CustomEntitlementComputation

Appears to be similar to this issue: #6981

How do we reproduce it?

I'm still working on it, but my guess would be setting up a sample project with https://github.com/RevenueCat/purchases-ios-spm as dependency and running tuist install and tuist generate with a Tuist version from 4.33.0 or higher.

Error log

We received an error that we couldn't handle:
    - Localized description: The file “App” couldn’t be opened because there is no such file.
    - Error: Error Domain=NSCocoaErrorDomain Code=260 "The file “App” couldn’t be opened because there is no such file." UserInfo={NSURL=/Users/kaioelfke/Developer/MyProject/Tuist/.build/checkouts/purchases-ios-spm/CustomEntitlementComputation/App, NSFilePath=/Users/kaioelfke/Developer/MyProject/Tuist/.build/checkouts/purchases-ios-spm/CustomEntitlementComputation/App, NSUnderlyingError=0x600003b41260 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}

macOS version

15.1.1

Tuist version

4.36.0

Xcode version

16.1

@KaiOelfke KaiOelfke added the type:bug Something isn't working label Dec 7, 2024
Copy link

dosubot bot commented Dec 7, 2024

Hey @KaiOelfke! I'm here to help you with any bugs, questions, or contributions. Let's work together to solve this issue.

The issue you're encountering seems to be related to how Tuist handles symbolic links in newer versions, specifically with the CustomEntitlementComputation symlink in the RevenueCat's purchases-ios-spm repository. In Tuist versions 4.33.0 and above, there might be changes in file system handling that affect symlink resolution, leading to the "App" file not being found.

Tuist's project generation process resolves symbolic links to their target paths. However, there is a known issue with symlink resolution, particularly with the relative implementation, which can produce incorrect paths when using symbolic links. This might be causing the path resolution problem you're experiencing [1].

To address this, you can try the following:

  1. Verify Symlink Resolution: Ensure that the symlink CustomEntitlementComputation correctly points to the Sources folder. You can manually check this by navigating to the symlink path and verifying its target.

  2. Check for Path Adjustments: Tuist makes adjustments to align paths from workspace-state.json with the /var temporary directory by replacing occurrences of /private/var with /var. Ensure that your setup aligns with this adjustment [1].

  3. Test with Different Tuist Versions: Since the issue does not occur in version 4.31.0, you might want to compare the behavior between versions to identify specific changes that could be affecting symlink handling.

  4. Create a Minimal Reproduction: Set up a minimal project that includes the purchases-ios-spm dependency and test it with different Tuist versions to isolate the problem.

These steps should help you diagnose and potentially resolve the symlink issue in your project.

To continue talking to Dosu, mention @dosu.


Help Dosu learn! Give it feedback: Great Response | Irrelevant Answer | Incorrect Sources | Too Verbose | Hallucination | Other

@dosubot dosubot bot added the domain:projects Issues related to the project-generation features label Dec 7, 2024
@KaiOelfke
Copy link
Contributor Author

When I run tuist via Xcode locally I don't run into the RevenueCat issue but with the closed source package.

In CoreDataModel+ManifestMapper.swift it appears the line let versions = try await fileSystem.glob(directory: modelPath, include: ["*.xcdatamodel"]).collect() should return the versions (should be four), but it's empty.

So I guess that function maybe doesn't work with the symlink (modelPath).

extension XcodeGraph.CoreDataModel {
    /// Maps a `.xcdatamodeld` package into a XcodeGraph.CoreDataModel instance.
    /// - Parameters:
    ///   - path: The path for a `.xcdatamodeld` package.
    static func from(
        path modelPath: AbsolutePath,
        fileSystem: FileSysteming
    ) async throws -> XcodeGraph.CoreDataModel {

        let versions = try await fileSystem.glob(directory: modelPath, include: ["*.xcdatamodel"]).collect()
        let currentVersion: String = try await {
            if try await CoreDataVersionExtractor.isVersioned(at: modelPath) {
                return try CoreDataVersionExtractor.version(fromVersionFileAtPath: modelPath)
            } else {
                return (versions.count == 1 ? versions[0] : modelPath).basenameWithoutExt
            }
        }()
        return CoreDataModel(path: modelPath, versions: versions, currentVersion: currentVersion)
    }
}

@KaiOelfke
Copy link
Contributor Author

Resolving the symlink by doing some local changes in the Tuist codebase indeed gets the four versions. However, in BuildPhaseGenerator.swift the currentVersionPath is still the unresolved path, which doesn't match the resolved paths of the versions.

I'm not really familiar with how Tuist's file system works and when symlinks are resolved or if the file system glob is supposed to return the versions as paths including the symlink so that it stays unresolved.

private func generateCoreDataModel(
        coreDataModel: CoreDataModel,
        fileElements: ProjectFileElements,
        pbxproj _: PBXProj
    ) -> PBXBuildFile {
        let currentVersion = coreDataModel.currentVersion
        let path = coreDataModel.path
        let currentVersionPath = path.appending(component: "\(currentVersion).xcdatamodel")
        // swiftlint:disable:next force_cast
        let modelReference = fileElements.group(path: path)! as! XCVersionGroup
        if fileElements.file(path: currentVersionPath) == nil {
            print(fileElements.elements)
        }
        let currentVersionReference = fileElements.file(path: currentVersionPath)!
        modelReference.currentVersion = currentVersionReference

        let pbxBuildFile = PBXBuildFile(file: modelReference)
        return pbxBuildFile
    }

@KaiOelfke
Copy link
Contributor Author

I removed the problematic dependency and then it works in Xcode (main branch). But running 4.36.0 via mise still fails with the RevenueCat error.

So I have no idea why I when run as CLI installed via mise gave me the error regarding RevenueCat.

With Tuist 4.32 I actually get the errors regarding the closed source xcdatamodel, which seems more correct.

@KaiOelfke
Copy link
Contributor Author

cc @fortmarek maybe you know when and where symlinks are resolved for such a versioned core data model? I could try making a PR, but I'm not sure design / requirement wise how it should be handled.

@fortmarek
Copy link
Member

So I have no idea why I when run as CLI installed via mise gave me the error regarding RevenueCat.

We have a fixture with RevenueCat that we run as part of our acceptance tests, so I'd be surprised if the integration of it doesn't work: https://github.com/tuist/tuist/tree/main/fixtures/app_with_revenue_cat

I'm not really familiar with how Tuist's file system works and when symlinks are resolved or if the file system glob is supposed to return the versions as paths including the symlink so that it stays unresolved.

We should follow symlinks. We have a unit test for that in our FileSystem library. Maybe you can try to reproduce the directory structure you have in one of the tests and see how the globbing behaves?

Resolving the symlink by doing some local changes in the Tuist codebase indeed gets the four versions.

What changes did you do? Have you been able to find the exact glob that we run and that doesn't return the expected results?

I'd love to help more but without a reproducible sample, I can only provide guidance on how to further debug this, sorry!

@KaiOelfke
Copy link
Contributor Author

Thanks for the hints. I've figured it out I think.

func test_glob_with_symlink_new2() async throws {
      
    let versionPaths = [
      "CDEEventStoreModel_0.xcdatamodel",
      "CDEEventStoreModel_1.xcdatamodel",
      "CDEEventStoreModel_2.xcdatamodel",
      "CDEEventStoreModel_3.xcdatamodel",
    ]
    
    let absolutePath = try AbsolutePath(validating: "/Users/kaioelfke/Developer/ensembles-next/Framework/Source/SwiftPackageResources/CDEEventStoreModel.xcdatamodeld")
    // When
    let got = try await subject.glob(
      directory: absolutePath,
        include: ["*.xcdatamodel"]
    )
    .collect()
    .sorted()
  
    // Then
    XCTAssertEqual(got.count, 4)
    XCTAssertEqual(got.map(\.basename), versionPaths)
  }

This test case fails. But this is a hardcoded path only working on my machine. The thing is that I don't know how to create a relative sym link with FileSystem. And this seems to be the root issue.

The symlink is defined as relative path:

ls
CDEEventStoreModel.xcdatamodeld -> ../../Resources/CDEEventStoreModel.xcdatamodeld

I think in GlobSearch.swift around line 87 let symbolicLinkDestinationAbsoluteString = try? FileManager.default .destinationOfSymbolicLink(atPath: path) this isn't handled properly for relative symlinks.

Documentation:

An NSString object containing the path of the directory or file to which the symbolic link path refers. If the symbolic link is specified as a relative path, that relative path is returned.

With these changes it works:

let path = baseURL.absoluteString.removingPercentEncoding ?? baseURL.absoluteString
                    let symbolicLinkDestination: URL = URL(filePath: path).resolvingSymlinksInPath()
                    var isDirectory: ObjCBool = false
                    guard FileManager.default.fileExists(
                        atPath: symbolicLinkDestination.path(),
                        isDirectory: &isDirectory
                    ),
                        isDirectory.boolValue
                    else { continue }

I can either provide you a sample with these specific files or you could tell me how I can make a unit test with relative sym links instead of an absolute one.

E.g. a unit test I made using the createSymbolicLink(from:to:) function creates a symlink like this:

CDEEventStoreModel.xcdatamodeld -> /private/var/folders/mq/4mfbx0tn4bb93_nmh7lsscdr0000gn/T/FileSystem-F9F37546-F60C-4198-9535-650DA0BFB4AC/Framework/Resources/CDEEventStoreModel.xcdatamodeld

@haifengkao
Copy link
Contributor

haifengkao commented Dec 14, 2024

The problem appears at [email protected]
It runs fine at [email protected]

[email protected] has other bugs to prevent my project from being compiled.

@fortmarek
Copy link
Member

I can either provide you a sample with these specific files or you could tell me how I can make a unit test with relative sym links instead of an absolute one.

A unit test in FileSystem would be amazing. You will need to create a new createSymbolicLink method where to is a RelativePath instead of AbsolutePath.

@KaiOelfke KaiOelfke linked a pull request Dec 18, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
domain:projects Issues related to the project-generation features type:bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants