Cache the helpers module

This commit is contained in:
Pedro Piñera 2019-11-15 09:19:33 +01:00 committed by Pedro Piñera
parent 371a7f0d6f
commit 57b98579a5
6 changed files with 141 additions and 42 deletions

View File

@ -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
}
}

View File

@ -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])
}
}

View File

@ -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 {

View File

@ -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")

View File

@ -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)
}

View File

@ -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!))
}
}