- Using ‘swift package fetch’ in an Xcode project
- By Matt Gallagher
- The Nuggets translation Project
- Translator: Gocy
- Proofread by: Atuooo, lovelyCiTY
Until now, Cocoa with Love’s Git repositories have used “Git subtrees” to manage dependencies, all of which are copied and statically stored in the dependent directory. I would like to find an alternative to the existing approach of dependency management that is more dynamic while remaining invisible to library users. (Cocoa with Love)
I want to use Swift Package Manager to solve this problem, but I don’t want all repositories to have to rely on Swift Package Manager to build. The build range supported by the Swift package management tool is quite limited, and I don’t want to shackle my library with these shackles.
In this article, I discuss a hybrid approach, where Swift package management tools will replace the manual configuration of Xcode projects as a behind-the-scenes tool for obtaining dependencies, while maintaining support for the target platform and build structure of the original “subtree” run.
content
- Dependency management
- Looking to the future
- Swift package fetch
- Automated script
- Give it a try
- conclusion
Dependency management
The dependencies for the CwlSignal library I published last year are very simple:
CwlCatchException ← CwlPreconditionTesting ← CwlUtils ← CwlSignal
Until now, I have been using Git subtrees.
In this case, you don’t need to download each dependency individually; If you look at the earlier structure of the CwlSignal repository, it contains CwlUtils, which contains CwlPreconditionTesting, which contains CwlCatchException. This is similar to manually copying every file into the repository, but it has the small advantage that if the dependency changes, I can simply call a Subtree pull to synchronize the update to the dependent.
This approach has its drawbacks. In the inflexible tree structure, you have to pull changes along the file chain. You can’t update all dependencies with a single command. Each repository is bloated with dependencies that need to contain it. If you inadvertently modify the dependency tree of the dependent party, you will also have trouble pulling the dependency to merge.
None of this is a big deal for a CwlSignal library with simple and lightweight dependencies, but I should point out some of the issues and hassles.
Git submodules
I could also try git subModule. In theory, it can solve the problems that Git subtrees solve more dynamically. I thought Git submodules would be an ideal choice, but when it comes to practice, git modules are not transparent in their changes to your repository, and git’s poor handling of them makes everything obscure.
It is possible to get the pull and push instructions out of order and end up losing changes. Changing the dependency target is cumbersome and usually requires you to manually change the contents of the “.git “directory. GitHub’s “Download ZIP” function becomes useless because ZIP files, like most non-Git code management, ignore references to submodules.
Every user of submodules needs to be familiar with the structure of submodules and fine-tune git directives every time to make sure the pull update repository is correct, which is usually not a concern in Git subtrees.
I could also use mature package management tools like CocoaPods or Carthage. I do need to further improve compatibility with these tools to satisfy some users who want to use them, but I don’t want to force all users to use them. For my part, I’d prefer to be able to control the workflow myself, rather than relying on these tools to control the workspace or build Settings.
Swift package management tool
So I chose Swift package management tool; A comprehensive solution with built-in build system and dependency management.
Does the Swift package management tool offer any new features over the options I mentioned earlier? Well, it does provide a whole new build system, but my main goal was dependency management; I wasn’t looking for a build system.
Using Swift package management tools doesn’t have the same weird problems as git submodules, and it’s embedded in Swift, so instead of CocoaPods and Carthage, It’s a more natural choice – although I still don’t want users of my library to have to use Swift package management tools as well.
Is it possible to use only the dependency management capabilities of the Swift package management tool without using its build system?
I just said, “My main goal is dependency management; I’m not looking for a build system. “I lied a little. I’m supposed to like it for its excellent dependency management capabilities, but to be honest, I’d love to try out the Swift package management tool build system.
Like Apache Maven and Rust Cargo, the Swift package management tool includes a build system with convention guidelines. While some metadata is declared in a top-level manifest, the build process itself is determined as much as possible by the organization of files under the directory. I’m a big fan of building systems like this; The build should not require a lot of configuration. If our directory structure follows convention, the system should be able to predict most (or all) of the build parameters, rather than having developers enumerate everything every time.
I look forward to the Swift package Management tool project becoming one of the Xcode template projects. Automating the logic that keeps files in their respective module directories, rather than storing them randomly in the file system for ongoing maintenance. Build parameters are generated by speculation rather than being set in a long list of tags and inspectors. Dependencies are visualized like the Debug Memory Graph in Xcode.
But apparently Xcode doesn’t support Swift package management tools yet (Swift package Management tools support Xcode but I’m more interested in reverse support). And to be honest, before Swift package management tools support building applications, support building on iOS, support building on watchOS, support building on tvOS, support building mixed-language modules, support building with resource packs, and be able to manage independent test dependencies or inline across modules, It doesn’t even meet all the requirements of a library as simple as CwlSignal.
(Translator’s note: Projects generated by Swift package management tool can be converted into ordinary Xcode projects using Swift Package generate-XcodeProj command, but currently Xcode projects cannot be converted into projects in Swift package management tool format.)
But I still support the Swift package management tool as a secondary build option, and hope that in a few years it will be my primary choice.
Swift package fetch
Wanting to use the dependency management functionality of the Swift package management tool without making it the primary build system can cause some problems.
To sum up, we need to accomplish the following things:
- Support for Swift package management tools (including build and dependency fetching).
- Have the Xcode project rely on files downloaded by the Swift package management tool.
- Ensure that the above operations are not visible to the user.
Configuring the package. swift file, adding semantic version labels to the repository, and ensuring proper pulling of dependencies is trivial.
The real focus is on the following tasks (in no order) :
- Adjust the project directory hierarchy to follow the structure specified in the Swift package management tool convention (all projects are involved in this example).
- Split the objective-C and Swift mixed modules into separate modules (in this case, all projects except CwlSignal).
- Ensure that the
#if SWIFT_PACKAGE
Judgment,import
The new module created in Step 2 (all projects except CwlSignal in this case). - Split the “.h “header so that we can import them all together via an Xcode global header or module header in Swift-PM (in this case all projects except CwlSignal).
- Make sure that the modules affected in Step 2
internal
Keyword change topublic
To ensure that they can be accessed normally (CwlCatchException in this case). - Remove all pairs
DEBUG
Or other dependencies that are not set by Swift package management tools (in this case CwlDeferredWork and CwlUtils and CwlSignal test files are involved). - With two-way dependence Objective – C and Swift file, change the Objective – C the dependence to dynamic search to avoid circular dependencies module (in this case, involving the CwlMachBadInstructionHandler).
- Move the info.plist file. These files are automatically generated by the Swift package Management tool, but they must be manually migrated into Xcode – the Swift package Management tool must be set to ignore these files (all projects involved in this example).
It also took some effort to learn about the conventions and working patterns of the Swift package management tool and let it guide some aspects of the build.
2. Make Xcode project rely on files downloaded by Swift package management tool
In Swift 3.0, dependencies are placed in the./Packages/ModuleName-X.Y.Z directory, where X, Y, and Z are the corresponding semantic version numbers. Obviously, this path will change as the dependent version changes.
In a Swift and updated version 3.1, rely on is placed in the “. / build/checkout/ModuleName – XXXXXXXX “directory, which XXXXXXXX is the hash value of the URL in the warehouse. The hash value is subject to change, and as far as I know, the format of the path is not documented and could change at any time.
Therefore, we cannot direct the path in our Xcode project directly to the directory above. We need to create a stable symlink rather than rely on actual paths that may change. This means we need to come up with a simple way to determine the current path.
The best solution to the path problem we can find within the scope of the document definition comes from this instruction:
swift package show-dependencies --format json
Copy the code
This directive outputs a JSON structure containing the module name and the path to checkout. While there is no guarantee that the structure will remain reliable, it works well in versions 3.0 through 3.1 so far, and is much better than traversing the entire directory structure.
We need to refine the symbolic link from a stable path to the concrete path described in the JSON file.
I chose to use the “./. Build /cwl_symlinks/ModuleName “path to store symbolic links containing specific paths used by Swift 3.0 and 3.1 package management tools. Despite the risk of collisions between different sources and versions of a dependency module with the same name, the path should steadily guide Xcode to its dependencies, regardless of the slight possibility. If I update the version of the dependent library or the hash value changes (I often switch between the local and remote branches for testing purposes) or some other factor causes the path to change, we just need to update the symbolic link.
Getting Xcode to reference symbolic links to retrieve paths indirectly, rather than immediately parsing them, is tricky. When using Swift, version 3.1, I actually still in the “. / build/cwl_symlinks ModuleName “directory copy a minute”. / build/checkout/ModuleName – XXXXXXXX “directory, And put all the files into the Xcode, then delete this copy, and in the “. / build/cwl_symlinks/ModuleName “directory to create points to the”. / build/checkout/ModuleName – XXXXXXXX “symbolic links.
Now we have the following two actions visible to the user that need to be eliminated:
- Performed when obtaining a dependency
swift package fetch
The instructions. - Create symbolic link files for all dynamically acquired dependencies in a stable directory.
To achieve this goal, we need to write an automated script.
Automated script
We need to add a “Run Script” build phase for all Xcode projects that have external dependencies.
When you add the “run script” build phase to Xcode, it points to “/bin/sh” by default. Since I’m not familiar with bash instructions, I changed the “Shell” value for the build phase to “/usr/bin/xcrun — SDK macOSx Swift” (this was done because even if the build was for iOS or another platform, I still need to make sure I use the macOS SDK) and add the following code to the script. Some of the parsing and configuration logic may be a bit complicated, but with the comments you should get the general idea.
import Foundation /// Launch a process and run to completion, returning the standard out on success. func launch(_ command: String, _ args: [String], directory: String? = nil) -> String? { let proc = Process() proc.launchPath = command proc.arguments = args _ = directory.map { proc.currentDirectoryPath = $0 } let pipe = Pipe() proc.standardOutput = pipe proc.launch() let result = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)! proc.waitUntilExit() return proc.terminationStatus ! = 0? nil : result } let srcRoot = ProcessInfo.processInfo.environment["SRCROOT"]! // STEP 1: use `swift package fetch` to get all dependencies print(launch("/usr/bin/swift", ["package", "fetch"], directory: srcRoot)!) // Create a symlink only if it is not already present and pointing to the destination let symlinksPath = "\(srcRoot)/.build/cwl_symlinks" func createSymlink(srcRoot: String, name: String, destination: String) throws { let location = "\(symlinksPath)/\(name)" let link = ".. /.. /\(destination)" if (try? FileManager.default.destinationOfSymbolicLink(atPath: location)) ! = link { _ = try? FileManager.default.removeItem(atPath: location) try FileManager.default.createSymbolicLink(atPath: location, withDestinationPath: link) print("Created symbolic link: \(location) -> \(link)") } } // Recursively parse the dependency graph JSON, creating symlinks in our own location func createSymlinks(srcRoot: String, description: Dictionary<String, Any>, topLevelPath: String) throws { guard let dependencies = description["dependencies"] as? [Dictionary<String, Any>] else { return } for dependency in dependencies { let path = dependency["path"] as! String let relativePath = path.substring(from: path.range(of: topLevelPath)! .upperBound) let name = dependency["name"] as! String try createSymlink(srcRoot: srcRoot, name: name, destination: relativePath) try createSymlinks(srcRoot: srcRoot, description: dependency, topLevelPath: topLevelPath) } } // STEP 2: create symlinks from our stable locations to the fetched locations let descriptionString = launch("/usr/bin/swift", ["package", "show-dependencies", "--format", "json"], directory: srcRoot)! let descriptionData = descriptionString.data(using: .utf8)! let description = try JSONSerialization.jsonObject(with: descriptionData, options: []) as! Dictionary<String, Any> let topLevelPath = (description["path"] as! String) + "/" do { try FileManager.default.createDirectory(atPath: symlinksPath, withIntermediateDirectories: true, attributes: nil) try createSymlinks(srcRoot: srcRoot, description: description, topLevelPath: topLevelPath) print("Complete.") } catch { print(error) }
Copy the code
Compiling and running this code takes about a second, which is not long, but it would be nice to save that time after the first run. You can add $(SRCROOT)/ package. swift to the Input Files list where you run the script and add one dependency for each swift Package management tool that gets it $(SRCROOT)/. Build /cwl_symlinks/ModuleName (ModuleName is the obtained ModuleName). This prevents Xcode from running the script repeatedly when “package. swift” has not changed or the module symbolic link has not been removed, thus saving a second of compile time.
Ironically, to avoid statically including dependencies, I included the file statically in each repository instead.
You can view or download CwlCatchException, CwlPreconditionTesting, CwlUtils and CwlSignal projects on GitHub. These projects now support building with Swift package management tools on macOS. In theory, the Swift package management tool makes it possible for some of these libraries to run on Linux, but we’ll explore that next time.
This is an experimental change to these warehouses. For some reason, I may have made some mistakes or neglected to make better choices. If you have any questions or better suggestions, feel free to submit an issue on GitHub.
conclusion
I’m happy to be able to replace the dependency inclusion scheme for Git subtree with a more dynamic scheme.
I’m also grateful for the support of the Swift package management tool. It doesn’t support Linux yet (don’t worry), but it does work smoothly (albeit with a lot of path configuration modifications) and isn’t particularly hard to use.
It would be simpler and more structured if all use cases could be made entirely dependent on the Swift package management tool. Unfortunately, the current version of Swift package management tools can’t handle a large number of different build scenarios (including other apps and build on iOS/watchOS/tvOS), so using Xcode as your preferred build environment is quite necessary, but it means you need to integrate both.
The build phase of “Run the script” nicely hides the process of pulling dependencies. Although the first build will fail when there is no network connection, it should normally be invisible and require no special handling. Setting up “Input Files” and “Output Files” for the build phase of “Run scripts” eliminates the extra cost of the build and run phases in most scenarios, so it doesn’t have much impact.
But I do worry that this build script could easily fail at such a frequent update rate for the Swift package management tool. I know I need to keep an eye out for updates to the Swift package management tool in the future – especially any that might affect the Swift package show-dependencies –format JSON output.