diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b668c9e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM swift:4.2.1 +COPY XcodeProject.xcodeproj ./XcodeProject.xcodeproj +COPY Package.swift ./Package.swift +COPY Sources ./Sources +COPY Tests ./Tests +RUN swift test --configuration debug diff --git a/Makefile b/Makefile index d7b81e6..a5c3597 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,9 @@ test: xcode: swift package generate-xcodeproj --xcconfig-overrides=overrides.xcconfig + +linux: + swift test --generate-linuxmain $(SWIFTC_FLAGS) clean: - swift build --clean \ No newline at end of file + swift build --clean diff --git a/Package.swift b/Package.swift index c6e1148..5f6ee0d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:4.0 +// swift-tools-version:4.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/Sources/XcodeProject/Objects/PBXFileType.swift b/Sources/XcodeProject/Objects/PBXFileType.swift index 1ecf70b..785a7fa 100644 --- a/Sources/XcodeProject/Objects/PBXFileType.swift +++ b/Sources/XcodeProject/Objects/PBXFileType.swift @@ -7,7 +7,9 @@ // import Foundation +#if canImport(CoreServices) import CoreServices +#endif public struct PBXFileType { static var fileTypes: [PBXFileType] = [ @@ -39,8 +41,28 @@ public struct PBXFileType { var isSource: Bool { return UTTypeConformsTo(uniformType as CFString, kUTTypeSourceCode) } + + static func fileType(filePath: String) -> PBXFileType? { + let fileExtension = (filePath as NSString).pathExtension + return fileType(fileExtension: fileExtension) + } + + static func fileType(reference: PBXFileReference) -> PBXFileType? { + return fileType(filePath: reference.fileType.type) + } +} + +#if !canImport(CoreServices) +typealias CFString = String +let kUTTypeCHeader: CFString = "public.c-header" +let kUTTypeCPlusPlusHeader: CFString = "public.c-plus-plus-header" +let kUTTypeSourceCode: CFString = "public.source-code" +func UTTypeConformsTo(_ inUTI: CFString, _ inConformsToUTI: CFString) -> Bool { + return true } +#endif +#if canImport(CoreServices) public extension PBXFileType { static func fileType(fileExtension: String) -> PBXFileType? { guard let uniformType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension as CFString, nil)?.takeUnretainedValue() as String? else { return nil } @@ -48,16 +70,14 @@ public extension PBXFileType { $0.uniformType == uniformType } } - - static func fileType(filePath: String) -> PBXFileType? { - let fileExtension = (filePath as NSString).pathExtension - return fileType(fileExtension: fileExtension) - } - - static func fileType(reference: PBXFileReference) -> PBXFileType? { - return fileType(filePath: reference.fileType.type) +} +#else +public extension PBXFileType { + static func fileType(fileExtension: String) -> PBXFileType? { + return nil } } +#endif extension PBXFileType { var buildPhaseType: PBXBuildPhase.Type? { diff --git a/Sources/XcodeProject/Objects/PBXGlobalID+Generator.swift b/Sources/XcodeProject/Objects/PBXGlobalID+Generator.swift index b09a52b..addf8fc 100644 --- a/Sources/XcodeProject/Objects/PBXGlobalID+Generator.swift +++ b/Sources/XcodeProject/Objects/PBXGlobalID+Generator.swift @@ -42,17 +42,13 @@ extension PBXGlobalID { private var lastTime: UInt32 = 0 private var firstSequence: UInt16 = 0 - init(userName: String = NSUserName(), processId: pid_t = getpid(), random: inout RandomNumberGenerator, referenceDateGenerator: @escaping () -> Date = Generator.referenceDate) { + init(userName: String = NSUserName(), processId: pid_t = getpid(), hostId: UInt32, random: UInt16, referenceDateGenerator: @escaping () -> Date = Generator.referenceDate) { self.referenceDateGenerator = referenceDateGenerator - var hostId: UInt32 = UInt32(gethostid()) - if hostId == 0 { - hostId = random.next() - } self.gid = GlobalIdentifier( userHash: Generator.userHash(userName: userName), pidByte: UInt8(truncatingIfNeeded: processId), - random: random.next(), + random: random, time: 0, hostShift: UInt8(truncatingIfNeeded: (hostId >> 0x10) & 0xff), hostHigh: UInt8(truncatingIfNeeded: (hostId & 0xff00) >> 0x8), diff --git a/Sources/XcodeProject/Objects/PBXGlobalID.swift b/Sources/XcodeProject/Objects/PBXGlobalID.swift index e60be67..6410a4f 100644 --- a/Sources/XcodeProject/Objects/PBXGlobalID.swift +++ b/Sources/XcodeProject/Objects/PBXGlobalID.swift @@ -27,13 +27,19 @@ public struct PBXGlobalID: RawRepresentable { return strings?.compactMap { return PBXGlobalID(rawValue: $0) } } - private static var randomNumberGenerator: RandomNumberGenerator = SystemRandomNumberGenerator() - private static var generator: PBXGlobalID.Generator = PBXGlobalID.Generator(random: &randomNumberGenerator) + private static var generator: PBXGlobalID.Generator = { + var randomNumberGenerator: RandomNumberGenerator = SystemRandomNumberGenerator() + var hostId: UInt32 = UInt32(gethostid()) + if hostId == 0 { + hostId = randomNumberGenerator.next() + } + return PBXGlobalID.Generator(hostId: hostId, random: randomNumberGenerator.next()) + }() } extension PBXGlobalID: Hashable { - public var hashValue: Int { - return rawValue.hashValue + public func hash(into hasher: inout Hasher) { + hasher.combine(rawValue) } public static func ==(lhs: PBXGlobalID, rhs: PBXGlobalID) -> Bool { diff --git a/Sources/XcodeProject/Objects/PBXObject.swift b/Sources/XcodeProject/Objects/PBXObject.swift index fb5c8ae..58780a0 100644 --- a/Sources/XcodeProject/Objects/PBXObject.swift +++ b/Sources/XcodeProject/Objects/PBXObject.swift @@ -67,8 +67,8 @@ public class PBXObject { } extension PBXObject: Hashable { - public var hashValue: Int { - return globalID.hashValue + public func hash(into hasher: inout Hasher) { + hasher.combine(globalID) } public static func ==(lhs: PBXObject, rhs: PBXObject) -> Bool { diff --git a/Sources/XcodeProject/Objects/References/PBXReference.swift b/Sources/XcodeProject/Objects/References/PBXReference.swift index e71917b..4b6e9b9 100644 --- a/Sources/XcodeProject/Objects/References/PBXReference.swift +++ b/Sources/XcodeProject/Objects/References/PBXReference.swift @@ -33,14 +33,30 @@ public class PBXReference: PBXObject { var indentWidth: Int? var buildFiles: [PBXBuildFile] { - return _buildFiles.allObjects + return _buildFiles.compactMap { $0.item } } - private var _buildFiles = NSHashTable.weakObjects() + + private struct WeakBuildFile: Hashable { + weak var item: PBXBuildFile? + + static func ==(lhs: WeakBuildFile, rhs: WeakBuildFile) -> Bool { + return lhs.item == rhs.item + } + + func hash(into hasher: inout Hasher) { + hasher.combine(item) + } + } + + private var _buildFiles: Array = [] + func register(buildFile: PBXBuildFile) { - _buildFiles.add(buildFile) + _buildFiles.removeAll { $0.item == nil } + _buildFiles.append(WeakBuildFile(item: buildFile)) } + func unregister(buildFile: PBXBuildFile) { - _buildFiles.remove(buildFile) + _buildFiles.removeAll { $0.item == nil || $0.item == buildFile } } public var displayName: String { diff --git a/Sources/XcodeProject/ProjectFile.swift b/Sources/XcodeProject/ProjectFile.swift index 3480ff7..eb75129 100644 --- a/Sources/XcodeProject/ProjectFile.swift +++ b/Sources/XcodeProject/ProjectFile.swift @@ -39,7 +39,6 @@ public final class ProjectFile { public private(set) var project: PBXProject let url: URL - let fileWrapper: FileWrapper /// Initializes a new project file from the given URL /// @@ -47,18 +46,8 @@ public final class ProjectFile { /// - Returns: A fully parsed project from the provided source or `nil` if an error happened public init(url: URL) throws { self.url = url - self.fileWrapper = try FileWrapper(url: url, options: []) - guard fileWrapper.isDirectory else { - throw CocoaError.error(.fileReadUnknown) - } - - guard let pbxproj = fileWrapper.fileWrappers?["project.pbxproj"], pbxproj.isRegularFile else { - throw CocoaError.error(.fileReadUnknown) - } - - guard let data = pbxproj.regularFileContents else { - throw Error.invalid - } + let pbxproj = URL(fileURLWithPath: "project.pbxproj", relativeTo: url) + let data = try Data(contentsOf: pbxproj) guard let plist = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] else { throw Error.invalidPlist @@ -85,23 +74,6 @@ public final class ProjectFile { project.path = url.path self.project = project } - - public func currentFileWrapper() throws -> FileWrapper { - let currentFileWrapper = fileWrapper - - let oldPbxproj = currentFileWrapper.fileWrappers!["project.pbxproj"]! - - let dataStream = DataStreamWriter() - let archiver = PBXPListArchiver(projectFile: self) - try archiver.write(stream: dataStream) - let newPbxproj = FileWrapper(regularFileWithContents: dataStream.data) - newPbxproj.preferredFilename = "project.pbxproj" - - currentFileWrapper.removeFileWrapper(oldPbxproj) - currentFileWrapper.addFileWrapper(newPbxproj) - - return currentFileWrapper - } } extension ProjectFile { @@ -112,19 +84,10 @@ extension ProjectFile { public func save(to destination: URL? = nil) throws { let destination = destination ?? url - guard let oldPbxproj = fileWrapper.fileWrappers?["project.pbxproj"], oldPbxproj.isRegularFile else { - throw CocoaError.error(.fileReadUnknown) - } - let dataStream = DataStreamWriter() let archiver = PBXPListArchiver(projectFile: self) try archiver.write(stream: dataStream) - let newPbxproj = FileWrapper(regularFileWithContents: dataStream.data) - newPbxproj.preferredFilename = "project.pbxproj" - - fileWrapper.removeFileWrapper(oldPbxproj) - fileWrapper.addFileWrapper(newPbxproj) - - try fileWrapper.write(to: destination, options: [.atomic], originalContentsURL: nil) + let pbxprojURL = URL(fileURLWithPath: "project.pbxproj", relativeTo: destination) + try dataStream.data.write(to: pbxprojURL, options: [.atomic]) } } diff --git a/Sources/XcodeProject/Workspace.swift b/Sources/XcodeProject/Workspace.swift index 1303c14..9a91e05 100644 --- a/Sources/XcodeProject/Workspace.swift +++ b/Sources/XcodeProject/Workspace.swift @@ -61,7 +61,6 @@ public final class Workspace: WorkspaceReference { } let url: URL public var referenceURL: URL? { return url.deletingLastPathComponent() } - public let fileWrapper: FileWrapper public var references: [WorkspaceItem] = [] public class FileReference: WorkspaceItem { @@ -88,15 +87,8 @@ public final class Workspace: WorkspaceReference { public init(url: URL) throws { self.url = url - self.fileWrapper = try FileWrapper(url: url, options: []) - if fileWrapper.isDirectory == false { - //throw - } - - guard let workspaceData = fileWrapper.fileWrappers?["contents.xcworkspacedata"], workspaceData.isRegularFile, - let data = workspaceData.regularFileContents else { - throw Error.invalid - } + let workspaceFileURL = URL(fileURLWithPath: "contents.xcworkspacedata", relativeTo: url) + let data = try Data(contentsOf: workspaceFileURL) let document = try XMLDocument(data: data, options: []) guard let children = document.rootElement()?.children as? [XMLElement] else { throw Error.invalid } diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..1a0c95c --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,8 @@ +import XCTest + +import XcodeProjectTests + +var tests = [XCTestCaseEntry]() +tests += XcodeProjectTests.__allTests() + +XCTMain(tests) diff --git a/Tests/XcodeProjectTests/PBXGlobalIDTests.swift b/Tests/XcodeProjectTests/PBXGlobalIDTests.swift index 45c6685..c69af45 100644 --- a/Tests/XcodeProjectTests/PBXGlobalIDTests.swift +++ b/Tests/XcodeProjectTests/PBXGlobalIDTests.swift @@ -10,14 +10,6 @@ import XCTest @testable import XcodeProject class PBXGlobalIDTests: XCTestCase { - struct FakeRandomGenerator: RandomNumberGenerator { - private var value: UInt64 = 1234567890 - mutating func next() -> UInt64 { - defer { value += 1 } - return value - } - } - func mockDateGenerator() -> Date { let components = DateComponents(timeZone: TimeZone(secondsFromGMT: 0), year: 2014, month: 07, day: 29, hour: 17, minute: 04, second: 20) let date = Calendar(identifier: .gregorian).date(from: components) @@ -25,9 +17,7 @@ class PBXGlobalIDTests: XCTestCase { } func testIDGeneration() { - let processId: pid_t = 50_000 - var randomNumberGenerator: RandomNumberGenerator = FakeRandomGenerator() - var generator = PBXGlobalID.Generator(userName: "XcodeProject", processId: processId, random: &randomNumberGenerator, referenceDateGenerator: mockDateGenerator) + var generator = PBXGlobalID.Generator(userName: "XcodeProject", processId: 50_000, hostId: 1234567890, random: 723, referenceDateGenerator: mockDateGenerator) XCTAssertEqual("8C5002D419880B94009602D2", generator.next()) XCTAssertEqual("8C5002D519880B94009602D2", generator.next()) XCTAssertEqual("8C5002D619880B94009602D2", generator.next()) diff --git a/Tests/XcodeProjectTests/XCTestManifests.swift b/Tests/XcodeProjectTests/XCTestManifests.swift new file mode 100644 index 0000000..364f0b7 --- /dev/null +++ b/Tests/XcodeProjectTests/XCTestManifests.swift @@ -0,0 +1,25 @@ +import XCTest + +extension PBXGlobalIDTests { + static let __allTests = [ + ("testIDGeneration", testIDGeneration), + ] +} + +extension XcodeProjectTests { + static let __allTests = [ + ("testAggregateTarget", testAggregateTarget), + ("testPrivateProjectFolder", testPrivateProjectFolder), + ("testSelfArchive", testSelfArchive), + ("testSelfSave", testSelfSave), + ] +} + +#if !os(macOS) +public func __allTests() -> [XCTestCaseEntry] { + return [ + testCase(PBXGlobalIDTests.__allTests), + testCase(XcodeProjectTests.__allTests), + ] +} +#endif diff --git a/Tests/XcodeProjectTests/XcodeProjectTests.swift b/Tests/XcodeProjectTests/XcodeProjectTests.swift index 3fe64ea..2744a63 100644 --- a/Tests/XcodeProjectTests/XcodeProjectTests.swift +++ b/Tests/XcodeProjectTests/XcodeProjectTests.swift @@ -21,27 +21,19 @@ class XcodeProjectTests: XCTestCase { private var selfPath: URL { let fileURL = URL(fileURLWithPath: #file) - let projectURL = URL(fileURLWithPath: "../../XcodeProject.xcodeproj", isDirectory: true, relativeTo: fileURL).standardizedFileURL + let projectURL = URL(fileURLWithPath: "../../XcodeProject.xcodeproj/", isDirectory: true, relativeTo: fileURL).standardizedFileURL return projectURL } @inline(__always) func assertReadWriteProject(url: URL) throws { - guard let projectFile = try? ProjectFile(url: url) else { - XCTFail("Failed to create ProjectFile") - return - } - + let projectFile = try ProjectFile(url: url) let archiver = PBXPListArchiver(projectFile: projectFile) let streamWriter = StringStreamWriter() - do { - try archiver.write(stream: streamWriter) - } catch { - XCTFail("Archiver write failed") - } + try archiver.write(stream: streamWriter) let url = URL(fileURLWithPath: "project.pbxproj", relativeTo: projectFile.url) - let data = try! Data(contentsOf: url) + let data = try Data(contentsOf: url) let string = String(data: data, encoding: .utf8) if streamWriter.string != string { @@ -57,17 +49,12 @@ class XcodeProjectTests: XCTestCase { //try assertReadWriteProject(url: selfPath) } - func testSelfSave() { - let copiedProjectURL = URL(fileURLWithPath: "\(NSTemporaryDirectory())\(UUID().uuidString).xcodeproj") - - do { - try FileManager.default.copyItem(at: selfPath, to: copiedProjectURL) - let projectFile = try ProjectFile(url: copiedProjectURL) - projectFile.project.mainGroup.sort(recursive: true, by: .type) - try projectFile.save() - } catch (let error) { - XCTFail(error.localizedDescription) - } + func testSelfSave() throws { + let copiedProjectURL = URL(fileURLWithPath: "\(NSTemporaryDirectory())\(UUID().uuidString).xcodeproj", isDirectory: true) + try FileManager.default.copyItem(at: selfPath, to: copiedProjectURL) + let projectFile = try ProjectFile(url: copiedProjectURL) + projectFile.project.mainGroup.sort(recursive: true, by: .type) + try projectFile.save() } func testAggregateTarget() throws { @@ -77,13 +64,9 @@ class XcodeProjectTests: XCTestCase { func testPrivateProjectFolder() throws { guard let path = ProcessInfo.processInfo.environment["PRIVATE_PROJECTS"], !path.isEmpty else { return } let privateProjectsDir = URL(fileURLWithPath: path, isDirectory: true) - do { - let files = try FileManager.default.contentsOfDirectory(at: privateProjectsDir, includingPropertiesForKeys: nil) - try files.filter { return $0.pathExtension == "xcodeproj" }.forEach { - try assertReadWriteProject(url: $0) - } - } catch { - XCTFail() + let files = try FileManager.default.contentsOfDirectory(at: privateProjectsDir, includingPropertiesForKeys: nil) + try files.filter { return $0.pathExtension == "xcodeproj" }.forEach { + try assertReadWriteProject(url: $0) } } } diff --git a/XcodeProject.xcodeproj/project.pbxproj b/XcodeProject.xcodeproj/project.pbxproj index 58a5dbe..251117f 100644 --- a/XcodeProject.xcodeproj/project.pbxproj +++ b/XcodeProject.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 6C2643E01E11C0E50057CFA4 /* GameplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6C2643DF1E11C0E50057CFA4 /* GameplayKit.framework */; }; 6C2643E21E11C16A0057CFA4 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6C2643E11E11C16A0057CFA4 /* Foundation.framework */; }; 6C2644091E11C80A0057CFA4 /* PBXPListArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2643E51E11C80A0057CFA4 /* PBXPListArchiver.swift */; }; 6C26440A1E11C80A0057CFA4 /* PBXPListUnarchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C2643E61E11C80A0057CFA4 /* PBXPListUnarchiver.swift */; }; @@ -118,7 +117,6 @@ buildActionMask = 2147483647; files = ( 6C2643E21E11C16A0057CFA4 /* Foundation.framework in Frameworks */, - 6C2643E01E11C0E50057CFA4 /* GameplayKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; };