Cache the helpers module
This commit is contained in:
parent
371a7f0d6f
commit
57b98579a5
|
@ -185,12 +185,12 @@ class GraphManifestLoader: GraphManifestLoading {
|
|||
]
|
||||
|
||||
// Helpers
|
||||
let projectDesciptionHelpersModule = try projectDescriptionHelpersBuilder.build(at: path, projectDescriptionPath: projectDescriptionPath)
|
||||
if let projectDesciptionHelpersModule = projectDesciptionHelpersModule {
|
||||
let projectDesciptionHelpersModulePath = try projectDescriptionHelpersBuilder.build(at: path, projectDescriptionPath: projectDescriptionPath)
|
||||
if let projectDesciptionHelpersModulePath = projectDesciptionHelpersModulePath {
|
||||
arguments.append(contentsOf: [
|
||||
"-I", projectDesciptionHelpersModule.path.parentDirectory.pathString,
|
||||
"-L", projectDesciptionHelpersModule.path.parentDirectory.pathString,
|
||||
"-F", projectDesciptionHelpersModule.path.parentDirectory.pathString,
|
||||
"-I", projectDesciptionHelpersModulePath.parentDirectory.pathString,
|
||||
"-L", projectDesciptionHelpersModulePath.parentDirectory.pathString,
|
||||
"-F", projectDesciptionHelpersModulePath.parentDirectory.pathString,
|
||||
"-lProjectDescriptionHelpers",
|
||||
])
|
||||
}
|
||||
|
@ -203,8 +203,6 @@ class GraphManifestLoader: GraphManifestLoading {
|
|||
throw GraphManifestLoaderError.unexpectedOutput(path)
|
||||
}
|
||||
|
||||
try projectDesciptionHelpersModule?.cleanup()
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,29 +2,6 @@ import Basic
|
|||
import Foundation
|
||||
import TuistSupport
|
||||
|
||||
/// This struct represents a project description helpers
|
||||
/// module that has been created temporarily to load the manifests.
|
||||
class ProjectDescriptionHelpersModule {
|
||||
/// Path to the module.
|
||||
let path: AbsolutePath
|
||||
|
||||
/// Function to clean up the temporary module.
|
||||
let cleanup: () throws -> Void
|
||||
|
||||
/// Initializes an instance with the given attributes.
|
||||
/// - Parameters:
|
||||
/// - path: Path to the module.
|
||||
/// - cleanup: Function to clean up the temporary module.
|
||||
init(path: AbsolutePath, cleanup: @escaping () throws -> Void) {
|
||||
self.path = path
|
||||
self.cleanup = cleanup
|
||||
}
|
||||
|
||||
deinit {
|
||||
try? cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
/// This protocol defines the interface to compile a temporary module with the
|
||||
/// helper files under /Tuist/ProjectDescriptionHelpers that can be imported
|
||||
/// from any manifest being loaded.
|
||||
|
@ -33,7 +10,7 @@ protocol ProjectDescriptionHelpersBuilding: AnyObject {
|
|||
/// - Parameters:
|
||||
/// - at: Path to the directory that contains the manifest being loaded.
|
||||
/// - projectDescriptionPath: Path to the project description module.
|
||||
func build(at: AbsolutePath, projectDescriptionPath: AbsolutePath) throws -> ProjectDescriptionHelpersModule?
|
||||
func build(at: AbsolutePath, projectDescriptionPath: AbsolutePath) throws -> AbsolutePath?
|
||||
}
|
||||
|
||||
final class ProjectDescriptionHelpersBuilder: ProjectDescriptionHelpersBuilding {
|
||||
|
@ -46,29 +23,65 @@ final class ProjectDescriptionHelpersBuilder: ProjectDescriptionHelpersBuilding
|
|||
self.rootDirectoryLocator = rootDirectoryLocator
|
||||
}
|
||||
|
||||
func build(at: AbsolutePath, projectDescriptionPath: AbsolutePath) throws -> ProjectDescriptionHelpersModule? {
|
||||
// We return if the helpers directory doesn't exist at /Tuist/ProjectDesciptionHelpers
|
||||
func build(at: AbsolutePath, projectDescriptionPath: AbsolutePath) throws -> AbsolutePath? {
|
||||
guard let helpersDirectory = self.helpersDirectory(at: at) else { return nil }
|
||||
let hash = try self.hash(helpersDirectory: helpersDirectory)
|
||||
let prefixHash = self.prefixHash(helpersDirectory: helpersDirectory)
|
||||
|
||||
// Get paths
|
||||
let cachePath = Environment.shared.projectDescriptionHelpersCacheDirectory
|
||||
let helpersCachePath = cachePath.appending(component: prefixHash)
|
||||
let helpersModuleCachePath = helpersCachePath.appending(component: hash)
|
||||
let dylibName = "libProjectDescriptionHelpers.dylib"
|
||||
|
||||
if FileHandler.shared.exists(helpersModuleCachePath) {
|
||||
return helpersModuleCachePath.appending(component: dylibName)
|
||||
}
|
||||
|
||||
// If the same helpers directory has been previously compiled
|
||||
// we delete it before compiling the new changes.
|
||||
if FileHandler.shared.exists(helpersCachePath) {
|
||||
try FileHandler.shared.delete(helpersCachePath)
|
||||
}
|
||||
try FileHandler.shared.createFolder(helpersModuleCachePath)
|
||||
|
||||
let command = self.command(outputDirectory: helpersModuleCachePath,
|
||||
helpersDirectory: helpersDirectory,
|
||||
projectDescriptionPath: projectDescriptionPath)
|
||||
try System.shared.runAndPrint(command)
|
||||
|
||||
return helpersModuleCachePath.appending(component: dylibName)
|
||||
}
|
||||
|
||||
// MARK: - Fileprivate
|
||||
|
||||
/// Returns the path to the helpers directory if it exists.
|
||||
/// - Parameter at: Path from which we traverse the hierarchy to obtain the helpers directory.
|
||||
fileprivate func helpersDirectory(at: AbsolutePath) -> AbsolutePath? {
|
||||
guard let rootDirectory = self.rootDirectoryLocator.locate(from: at) else { return nil }
|
||||
let helpersDirectory = rootDirectory
|
||||
.appending(component: Constants.tuistDirectoryName)
|
||||
.appending(component: Constants.helpersDirectoryName)
|
||||
if !FileHandler.shared.exists(helpersDirectory) { return nil }
|
||||
return helpersDirectory
|
||||
}
|
||||
|
||||
fileprivate func command(outputDirectory: AbsolutePath,
|
||||
helpersDirectory: AbsolutePath,
|
||||
projectDescriptionPath: AbsolutePath) -> [String] {
|
||||
let files = FileHandler.shared.glob(helpersDirectory, glob: "**/*.swift")
|
||||
let temporaryDirectory = try TemporaryDirectory()
|
||||
let outputPath = temporaryDirectory.path.appending(component: "libProjectDescriptionHelpers.dylib")
|
||||
var command: [String] = [
|
||||
"/usr/bin/xcrun", "swiftc",
|
||||
"-module-name", "ProjectDescriptionHelpers",
|
||||
"-emit-module",
|
||||
"-emit-module-path", temporaryDirectory.path.appending(component: "ProjectDescriptionHelpers.swiftmodule").pathString,
|
||||
"-emit-module-path", outputDirectory.appending(component: "ProjectDescriptionHelpers.swiftmodule").pathString,
|
||||
"-parse-as-library",
|
||||
"-emit-library",
|
||||
"-suppress-warnings",
|
||||
"-I", projectDescriptionPath.parentDirectory.pathString,
|
||||
"-L", projectDescriptionPath.parentDirectory.pathString,
|
||||
"-F", projectDescriptionPath.parentDirectory.pathString,
|
||||
"-working-directory", temporaryDirectory.path.pathString,
|
||||
"-working-directory", outputDirectory.pathString,
|
||||
]
|
||||
if projectDescriptionPath.extension == "dylib" {
|
||||
command.append(contentsOf: ["-lProjectDescription"])
|
||||
|
@ -77,10 +90,31 @@ final class ProjectDescriptionHelpersBuilder: ProjectDescriptionHelpersBuilding
|
|||
}
|
||||
|
||||
command.append(contentsOf: files.map { $0.pathString })
|
||||
return command
|
||||
}
|
||||
|
||||
try System.shared.runAndPrint(command)
|
||||
/// This method returns a hash based on the content in the helpers directory
|
||||
/// and the Swift version used to compile the module.
|
||||
/// - Parameter helpersDirectory: Path to the helpers directory.
|
||||
fileprivate func hash(helpersDirectory: AbsolutePath) throws -> String {
|
||||
let fileHashes = FileHandler.shared
|
||||
.glob(helpersDirectory, glob: "**/*.swift")
|
||||
.compactMap { $0.sha256() }
|
||||
.compactMap { $0.compactMap { byte in String(format: "%02x", byte) }.joined() }
|
||||
let swiftVersion = try System.shared.swiftVersion() ?? ""
|
||||
let tuistVersion = Constants.version
|
||||
|
||||
let cleanup = { try FileHandler.shared.delete(temporaryDirectory.path) }
|
||||
return ProjectDescriptionHelpersModule(path: outputPath, cleanup: cleanup)
|
||||
let identifiers = [swiftVersion, tuistVersion] + fileHashes
|
||||
|
||||
return identifiers.joined(separator: "-").md5
|
||||
}
|
||||
|
||||
/// Gets the prefix hash for the given helpers directory.
|
||||
/// This is useful to uniquely identify a helpers directory in the cache.
|
||||
/// - Parameter helpersDirectory: Path to the helpers directory.
|
||||
fileprivate func prefixHash(helpersDirectory: AbsolutePath) -> String {
|
||||
let pathString = helpersDirectory.pathString
|
||||
let index = pathString.index(pathString.startIndex, offsetBy: 7)
|
||||
return String(helpersDirectory.pathString.md5[..<index])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Basic
|
||||
import CommonCrypto
|
||||
import Darwin
|
||||
import Foundation
|
||||
|
||||
|
@ -55,6 +56,49 @@ extension AbsolutePath {
|
|||
}
|
||||
return ancestorPath
|
||||
}
|
||||
|
||||
/// Returns the hash of the file the path points to.
|
||||
public func sha256() -> Data? {
|
||||
do {
|
||||
let bufferSize = 1024 * 1024
|
||||
// Open file for reading:
|
||||
let file = try FileHandle(forReadingFrom: url)
|
||||
defer {
|
||||
file.closeFile()
|
||||
}
|
||||
|
||||
// Create and initialize SHA256 context:
|
||||
var context = CC_SHA256_CTX()
|
||||
CC_SHA256_Init(&context)
|
||||
|
||||
// Read up to `bufferSize` bytes, until EOF is reached, and update SHA256 context:
|
||||
while autoreleasepool(invoking: {
|
||||
// Read up to `bufferSize` bytes
|
||||
let data = file.readData(ofLength: bufferSize)
|
||||
if data.count > 0 {
|
||||
data.withUnsafeBytes {
|
||||
_ = CC_SHA256_Update(&context, $0, numericCast(data.count))
|
||||
}
|
||||
// Continue
|
||||
return true
|
||||
} else {
|
||||
// End of file
|
||||
return false
|
||||
}
|
||||
}) {}
|
||||
|
||||
// Compute the SHA256 digest:
|
||||
var digest = Data(count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
digest.withUnsafeMutableBytes {
|
||||
_ = CC_SHA256_Final($0, &context)
|
||||
}
|
||||
|
||||
return digest
|
||||
} catch {
|
||||
print(error)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AbsolutePath: ExpressibleByStringLiteral {
|
||||
|
|
|
@ -16,6 +16,12 @@ public protocol Environmenting: AnyObject {
|
|||
|
||||
/// Returns true if the output of Tuist should be coloured.
|
||||
var shouldOutputBeColoured: Bool { get }
|
||||
|
||||
/// Returns the cache directory
|
||||
var cacheDirectory: AbsolutePath { get }
|
||||
|
||||
/// Returns the directory where the project description helper modules are cached.
|
||||
var projectDescriptionHelpersCacheDirectory: AbsolutePath { get }
|
||||
}
|
||||
|
||||
/// Local environment controller.
|
||||
|
@ -81,6 +87,16 @@ public class Environment: Environmenting {
|
|||
return directory.appending(component: "Versions")
|
||||
}
|
||||
|
||||
/// Returns the directory where the project description helper modules are cached.
|
||||
public var projectDescriptionHelpersCacheDirectory: AbsolutePath {
|
||||
return cacheDirectory.appending(component: "ProjectDescriptionHelpers")
|
||||
}
|
||||
|
||||
/// Returns the cache directory
|
||||
public var cacheDirectory: AbsolutePath {
|
||||
return directory.appending(component: "Cache")
|
||||
}
|
||||
|
||||
/// Returns the directory where all the derived projects are generated.
|
||||
public var derivedProjectsDirectory: AbsolutePath {
|
||||
return directory.appending(component: "DerivedProjects")
|
||||
|
|
|
@ -30,6 +30,14 @@ public class MockEnvironment: Environmenting {
|
|||
return directory.path.appending(component: "settings.json")
|
||||
}
|
||||
|
||||
public var cacheDirectory: AbsolutePath {
|
||||
return directory.path.appending(component: "Cache")
|
||||
}
|
||||
|
||||
public var projectDescriptionHelpersCacheDirectory: AbsolutePath {
|
||||
return cacheDirectory.appending(component: "ProjectDescriptionHelpers")
|
||||
}
|
||||
|
||||
func path(version: String) -> AbsolutePath {
|
||||
return versionsDirectory.appending(component: version)
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@ final class ProjectDescriptionHelpersBuilderIntegrationTests: TuistTestCase {
|
|||
|
||||
// Then
|
||||
XCTAssertNotNil(got)
|
||||
XCTAssertTrue(FileHandler.shared.exists(got!.path))
|
||||
try got!.cleanup()
|
||||
XCTAssertTrue(FileHandler.shared.exists(got!))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue