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

🛑 Prevent overwriting identical workspace data #607

Merged
merged 6 commits into from
Apr 27, 2021

Conversation

ferologics
Copy link
Contributor

@ferologics ferologics commented Apr 21, 2021

Partially resolves tuist/tuist#2809 for projects using App.xcodeproj -- without App.xcworkspace at project root. I haven't investigated the issue beyond the scope of .xcodeproj generation.

Edit: The implementation has been updated to prevent overwriting identical workspace data (regardless of whether it's a project or a workspace)

Short description 📝

Prevent overwriting identical workspace data

Solution 📦

Check if previous workspace data is equal to newly generated workspace data.

Implementation 👩‍💻👨‍💻

  • Add a check in XCWorkspace that prevents overwriting of identical data

How can I test this change @fortmarek? I ran into complications when trying with 1.40.0 tag via swift build -c release --product tuist and using that with Xcode 12.4 version:

Users/ferologics/Dev/tuist/.build/x86_64-apple-macosx/release/tuist generate --project-only                                     ✔
The 'swiftc' was interrupted with a signal 11 and message:
Stack dump:
0.      Program arguments: /Applications/Xcode-12.4.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -interpret /Users/ferologics/Globekeeper/Connect/Tuist/Config.swift -enable-objc-interop -sdk /Applications/Xcode-12.4.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk -I /Users/ferologics/Dev/tuist/.build/x86_64-apple-macosx/release -F /Users/ferologics/Dev/tuist/.build/x86_64-apple-macosx/release -suppress-warnings -target-sdk-version 11.1 -module-name Config -lProjectDescription -framework ProjectDescription -- --tuist-dump
1.      Apple Swift version 5.3.2 (swiftlang-1200.0.45 clang-1200.0.32.28)
2.      While running user code "/Users/ferologics/Globekeeper/Connect/Tuist/Config.swift"
0  swift                       0x0000000111d7c615 llvm::sys::PrintStackTrace(llvm::raw_ostream&) + 37
1  swift                       0x0000000111d7b615 llvm::sys::RunSignalHandlers() + 85
2  swift                       0x0000000111d7cbcf SignalHandler(int) + 111
3  libsystem_platform.dylib    0x00007fff206a8d7d _sigtramp + 29
4  libsystem_platform.dylib    0x00007fdedd704ef0 _sigtramp + 18446743935146901904
5  libProjectDescription.dylib 0x0000000117195165 $s18ProjectDescription6ConfigV17GenerationOptionsOSEAASE6encode2toys7Encoder_p_tKFTW + 21
6  libswiftCore.dylib          0x00007fff2ccae3a7 $sSE6encode2toys7Encoder_p_tKFTj + 7
7  libswiftFoundation.dylib    0x00007fff306b17bf $s10Foundation13__JSONEncoder33_12768CA107A31EF2DCE034FD75B541C9LLC4box_ySo8NSObjectCSgSE_pKF + 2591
8  libswiftFoundation.dylib    0x00007fff3076af76 $s10Foundation29_JSONUnkeyedEncodingContainer33_12768CA107A31EF2DCE034FD75B541C9LLV6encodeyyxKSERzlF + 550
9  libswiftFoundation.dylib    0x00007fff3076bf49 $s10Foundation29_JSONUnkeyedEncodingContainer33_12768CA107A31EF2DCE034FD75B541C9LLVs07UnkeyedcD0AAsAEP6encodeyyqd__KSERd__lFTW + 9
10 libswiftCore.dylib          0x00007fff2ca3872b $sSasSERzlE6encode2toys7Encoder_p_tKF + 507
11 libswiftCore.dylib          0x00007fff2ca38828 $sSayxGSEsSERzlSE6encode2toys7Encoder_p_tKFTW + 24
12 libswiftCore.dylib          0x00007fff2ccae3a7 $sSE6encode2toys7Encoder_p_tKFTj + 7
13 libswiftFoundation.dylib    0x00007fff306b17bf $s10Foundation13__JSONEncoder33_12768CA107A31EF2DCE034FD75B541C9LLC4box_ySo8NSObjectCSgSE_pKF + 2591
14 libswiftFoundation.dylib    0x00007fff306b60ae $s10Foundation27_JSONKeyedEncodingContainer33_12768CA107A31EF2DCE034FD75B541C9LLV6encode_6forKeyyqd___xtKSERd__lF + 2734
15 libswiftFoundation.dylib    0x00007fff306b55f5 $s10Foundation27_JSONKeyedEncodingContainer33_12768CA107A31EF2DCE034FD75B541C9LLVyxGs05KeyedcD8ProtocolAAsAFP6encode_6forKeyyqd___0P0QztKSERd__lFTW + 21
16 libswiftCore.dylib          0x00007fff2ca21dce $ss26_KeyedEncodingContainerBoxC6encode_6forKeyyqd___qd_0_tKSERd__s06CodingG0Rd_0_r0_lF + 206
17 libswiftCore.dylib          0x00007fff2ca19c34 $ss22KeyedEncodingContainerV6encode_6forKeyyqd___xtKSERd__lF + 36
18 libProjectDescription.dylib 0x0000000117196fa7 $s18ProjectDescription6ConfigV6encode2toys7Encoder_p_tKF + 247
19 libProjectDescription.dylib 0x0000000117197417 $s18ProjectDescription6ConfigVSEAASE6encode2toys7Encoder_p_tKFTW + 39
20 libswiftCore.dylib          0x00007fff2ccae3a7 $sSE6encode2toys7Encoder_p_tKFTj + 7
21 libswiftFoundation.dylib    0x00007fff306b17bf $s10Foundation13__JSONEncoder33_12768CA107A31EF2DCE034FD75B541C9LLC4box_ySo8NSObjectCSgSE_pKF + 2591
22 libswiftFoundation.dylib    0x00007fff3075f8dc $s10Foundation11JSONEncoderC6encodeyAA4DataVxKSERzlF + 460
23 libswiftFoundation.dylib    0x00007fff3077c84e $s10Foundation11JSONEncoderC6encodeyAA4DataVxKSERzlFTj + 14
24 libProjectDescription.dylib 0x000000011719580a $s18ProjectDescription12dumpIfNeededyyxSERzlFAA6ConfigV_Tg5 + 298
25 libProjectDescription.dylib 0x00000001171955ec $s18ProjectDescription6ConfigV23compatibleXcodeVersions5cloud5cache7plugins17generationOptionsAcA010CompatibleeF0O_AA5CloudVSgAA5CacheVSgSayAA14PluginLocationVGSayAC010GenerationK0OGtcfC + 412
26 libProjectDescription.dylib 0x000000011743b077 $s18ProjectDescription6ConfigV23compatibleXcodeVersions5cloud5cache7plugins17generationOptionsAcA010CompatibleeF0O_AA5CloudVSgAA5CacheVSgSayAA14PluginLocationVGSayAC010GenerationK0OGtcfC + 2776103
27 swift                       0x000000010d937a6f llvm::orc::runAsMain(int (*)(int, char**), llvm::ArrayRef<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >, llvm::Optional<llvm::StringRef>) + 2495
28 swift                       0x000000010d914e5b swift::RunImmediately(swift::CompilerInstance&, std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > const&, swift::IRGenOptions const&, swift::SILOptions const&, std::__1::unique_ptr<swift::SILModule, std::__1::default_delete<swift::SILModule> >&&) + 6747
29 swift                       0x000000010d8eff7a performCompileStepsPostSILGen(swift::CompilerInstance&, swift::CompilerInvocation const&, std::__1::unique_ptr<swift::SILModule, std::__1::default_delete<swift::SILModule> >, llvm::PointerUnion<swift::ModuleDecl*, swift::SourceFile*>, swift::PrimarySpecificPaths const&, int&, swift::FrontendObserver*) + 2426
30 swift                       0x000000010d8dff4e swift::performFrontend(llvm::ArrayRef<char const*>, char const*, void*, swift::FrontendObserver*) + 21134
31 swift                       0x000000010d860c27 main + 1255
32 libdyld.dylib               0x00007fff2067ef3d start + 1
33 libdyld.dylib               0x0000000000000015 start + 18446603339972481241

@codecov
Copy link

codecov bot commented Apr 21, 2021

Codecov Report

Merging #607 (69c7894) into main (0c88990) will decrease coverage by 0.08%.
The diff coverage is 38.46%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #607      +/-   ##
==========================================
- Coverage   84.28%   84.20%   -0.09%     
==========================================
  Files         154      154              
  Lines        8688     8700      +12     
==========================================
+ Hits         7323     7326       +3     
- Misses       1365     1374       +9     
Impacted Files Coverage Δ
Sources/XcodeProj/Workspace/XCWorkspace.swift 45.16% <0.00%> (-15.71%) ⬇️
Sources/XcodeProj/Workspace/XCWorkspaceData.swift 90.90% <100.00%> (+0.43%) ⬆️
...ources/XcodeProj/Utils/BuildSettingsProvider.swift 54.15% <0.00%> (-0.20%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 0c88990...69c7894. Read the comment docs.

@fortmarek
Copy link
Member

@ferologics to resolve the error, could you try running swift build -c release --product ProjectDescription first?

@ferologics ferologics marked this pull request as ready for review April 22, 2021 07:36
Copy link
Collaborator

@kwridan kwridan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ferologics! This has been one annoying quirk of regenerating projects that would be great to solve.

@yonaskolb would this have any negative side effects for XcodeGen?

From my testing, I noticed if you have a workspace with several projects open, Xcode seems to tolerate regenerating the individual projects, however if you regenerate the entire workspace (including the .xcworkspace) that's when we start hitting the issue of needing to close and re-open Xcode.

Prototyping locally something like this may do the trick:

  • extracted a rawContents() method in XCWorkspaceData which can be used by its write method
// XCWorkspaceData.swift

   func rawContents() -> String {
        let document = AEXMLDocument()
        let workspace = document.addChild(name: "Workspace", value: nil, attributes: ["version": "1.0"])
        _ = children
            .map { $0.xmlElement() }
            .map(workspace.addChild)
        return document.xmlXcodeFormat
    }

    // MARK: - <Writable>

    public func write(path: Path, override: Bool = true) throws {
        let rawXml = rawContents()
        if override, path.exists {
            try path.delete()
        }
        try path.write(rawXml)
    }
  • Updated XCWorkspace.write to check the raw contents before deleting
// XCWorkspace.swift

   public func write(path: Path, override: Bool = true) throws {
        let dataPath = path + "contents.xcworkspacedata"
        if override, dataPath.exists {
            if let existingContent: String = try? dataPath.read(),
               existingContent == data.rawContents() {
                // Raw data matches what's on disk
                // there's no need to overwrite the contents
                // this mitigates Xcode forcing users to
                // close and re-open projects/workspaces
                // on regneration.
                return
            }
            try dataPath.delete()
        }
        try dataPath.mkpath()
        try data.write(path: dataPath)
    }

Benchmarking reading the raw contents vs deserialisation they were roughly the same - not sure now which is better 🤷‍♂️

@@ -130,6 +130,7 @@ extension XcodeProj: Writable {
/// - Parameter override: if workspace should be overridden. Default is true.
/// If false will throw error if workspace already exists at the given path.
public func writeWorkspace(path: Path, override: Bool = true) throws {
guard let p = path.glob("*.xcworkspacedata").first, workspace.data != (try? XCWorkspaceData(path: p)) else { return }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a few caveats we should be mindful here:

  • The xcworkspacedata serialisation is a responsibility of the XCWorkspace class, a check of this nature can potentially go there, as that component knows the exact path and there wouldn't be a need to search / glob for it :)
  • XCWorkspace deletes and recreates the underlying contents.xcworkspacedata file when override is set, which I believe may be the source of the issue - this is also the same when viewing a Workspace rather than a standalone Project
if override, dataPath.exists {
   try dataPath.delete()
}
  • We need to mindful of not reloading and deserialising content from disk to verify equality as that could potentially be expensive for large workspaces (needs benchmarking), we may be able to compare the raw data without deserialisation - From my quick test locally on a test fixture with 100 projects it didn't have as big of an impact as I feared it would <1%

@ferologics
Copy link
Contributor Author

Thanks for giving this a thorough look @kwridan 🙇 I agree with your observations about both the responsibility and the root cause. I adjusted the code according to your suggestion.

@ferologics
Copy link
Contributor Author

ferologics commented Apr 26, 2021

After spending more time debugging this with a project I still got the occasional "The file “project.xcworkspace” has been modified by another application." when regenerating. A closer look at the contents.xcworkspacedata shows that this data is slightly different from the "clean slate" data that we use to verify the equality:

Clean slate:

<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
</Workspace>

Existing:

<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
   <FileRef
      location = "self:">
   </FileRef>
</Workspace>

The problematic <FileRef/> is added at some point in the lifetime of the project. I am not exactly sure what it's doing there, or what it's purpose is @kwridan @fortmarek, but perhaps it can be added to the "clean slate" xml data?

Copy link
Member

@fortmarek fortmarek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problematic is added at some point in the lifetime of the project. I am not exactly sure what it's doing there, or what it's purpose is

This comes from XCWorkspaceDataElementLocationType that has a case self(String) // Single project workspace in xcodeproj directory. I have not dived deeper into why this one is added as I don't really follow what's the issue with it - the generation should be deterministic, so what exact modification / command creates this change in the data? But I believe we are fine with the fact that this data changes as long as it happens only when the generated .xcworkspace is different as well.

Sources/XcodeProj/Workspace/XCWorkspace.swift Outdated Show resolved Hide resolved
Sources/XcodeProj/Workspace/XCWorkspace.swift Show resolved Hide resolved
@kwridan
Copy link
Collaborator

kwridan commented Apr 27, 2021

I believe what you're witnessing is an internal Xcode behaviour:

My current understanding of it based on past observations is as follows:

  • Projects need to always operate with a workspace
  • When launching a standalone .xcodeproj an implicit workspace is used that is nested within the .xcodeproj container
    • App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata
    • The implicit workspace needs to reference the project and as such adds the following on launch
   <FileRef
      location = "self:">
   </FileRef>
  • When launching within a dedicated workspace (e.g. when hosting one or more projects), Xcode doesn't use the implicit workspace
    • App.xcworkspace/contents.xcworkspacedata
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
   <FileRef
      location = "group:App/App.xcodeproj">
   </FileRef>
   <FileRef
      location = "group:Modules/MyFramework.xcodeproj">
   </FileRef>
</Workspace>

As such we'd need to figure out which XCWorkspace we're dealing with if we were to modify the clean slate default - not sure XcodeProj has enough information to decide that at the library level, possibly something we can infer at the Tuist level?

Copy link
Collaborator

@kwridan kwridan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates

@pepicrft pepicrft merged commit 3076b6d into tuist:main Apr 27, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Development/watch command for re-generating resources on the fly
4 participants