diff --git a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift index 687820ec..2464e9c3 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift @@ -526,7 +526,7 @@ public final class SwiftCommandOutputParser: TaskOutputParser { serializedDiagnosticsPaths.filter { path in // rdar://91295617 (Swift produces empty serialized diagnostics if there are none which is not parseable by clang_loadDiagnostics) do { - return try fs.exists(path) && fs.getFileInfo(path).statBuf.st_size > 0 + return try fs.exists(path) && fs.getFileInfo(path).size > 0 } catch { return false } diff --git a/Sources/SWBLLBuild/LowLevelBuildSystem.swift b/Sources/SWBLLBuild/LowLevelBuildSystem.swift index e2193a66..d23d6f98 100644 --- a/Sources/SWBLLBuild/LowLevelBuildSystem.swift +++ b/Sources/SWBLLBuild/LowLevelBuildSystem.swift @@ -11,11 +11,7 @@ //===----------------------------------------------------------------------===// public import SWBUtil -#if os(Windows) -private import SWBLibc -#else public import SWBLibc -#endif // Re-export all APIs from llbuild bindings. @_exported public import llbuild @@ -25,7 +21,34 @@ public import SWBLibc #endif // Filesystem adaptors for SWBLLBuild.FileSystem. -extension SWBUtil.FileInfo: SWBLLBuild.FileInfo {} +extension SWBUtil.FileInfo: SWBLLBuild.FileInfo { + + public init(_ statBuf: stat) { + // This should be remove from llbuild FileInfo protocol as it just not needed, would also be nice to remove the stat requirement too. + preconditionFailure() + } + + public var statBuf: stat { + var statBuf: stat = stat() + + statBuf.st_dev = numericCast(self.deviceID) + statBuf.st_ino = numericCast(self.iNode) + statBuf.st_mode = numericCast(self.permissions) + statBuf.st_size = numericCast(self.size) + #if canImport(Darwin) + statBuf.st_mtimespec.tv_sec = numericCast(self.modificationTimestamp) + statBuf.st_mtimespec.tv_nsec = self.modificationNanoseconds + #elseif os(Windows) + statBuf.st_mtime = self.modificationTimestamp + #elseif canImport(Glibc) || canImport(Musl) || canImport(Android) + statBuf.st_mtim.tv_sec = numericCast(self.modificationTimestamp) + statBuf.st_mtim.tv_nsec = self.modificationNanoseconds + #else + #error("Not implemented for this platform") + #endif + return statBuf + } +} public final class FileSystemImpl: FileSystem { diff --git a/Sources/SWBTaskExecution/TaskActions/ODRAssetPackManifestTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/ODRAssetPackManifestTaskAction.swift index 72f1cfb3..24142a60 100644 --- a/Sources/SWBTaskExecution/TaskActions/ODRAssetPackManifestTaskAction.swift +++ b/Sources/SWBTaskExecution/TaskActions/ODRAssetPackManifestTaskAction.swift @@ -108,7 +108,7 @@ fileprivate extension FSProxy { try traverse(path) { subPath -> Void in let info = try getLinkFileInfo(subPath) - uncompressedSize += Int(info.statBuf.st_size) + uncompressedSize += Int(info.size) newestModTime = max(newestModTime, info.modificationDate) } diff --git a/Sources/SWBUtil/FSProxy.swift b/Sources/SWBUtil/FSProxy.swift index 9f0eb4c2..92a9bf56 100644 --- a/Sources/SWBUtil/FSProxy.swift +++ b/Sources/SWBUtil/FSProxy.swift @@ -10,7 +10,7 @@ // //===----------------------------------------------------------------------===// -public import SWBLibc +import SWBLibc #if canImport(System) public import System @@ -18,6 +18,7 @@ public import System public import SystemPackage #endif +public import struct Foundation.CocoaError public import struct Foundation.Data public import struct Foundation.Date public import struct Foundation.FileAttributeKey @@ -28,82 +29,91 @@ public import struct Foundation.URL public import struct Foundation.URLResourceKey public import struct Foundation.URLResourceValues public import struct Foundation.UUID +public import struct Foundation.FileAttributeType +public import struct Foundation.FileAttributeKey +public import struct Foundation.TimeInterval +public import class Foundation.NSDictionary +#if canImport(Darwin) +import struct ObjectiveC.ObjCBool +#endif #if os(Windows) -// Windows' POSIX layer does not have S_IFLNK, so define it. -// We only need it for PseudoFS. -fileprivate let S_IFLNK: Int32 = 0o0120000 +public import struct WinSDK.HANDLE #endif + /// File system information for a particular file. /// /// This is a simple wrapper for stat() information. public struct FileInfo: Equatable, Sendable { - public let statBuf: stat + public let fileAttrs: [FileAttributeKey: any Sendable] - public init(_ statBuf: stat) { - self.statBuf = statBuf + public init(_ fileAttrs: [FileAttributeKey: any Sendable]) { + self.fileAttrs = fileAttrs + } + + func _readFileAttributePrimitive(_ value: Any?, as type: T.Type) -> T? { + guard let value else { return nil } + if let exact = value as? T { + return exact + } else if let binInt = value as? (any BinaryInteger), let result = T(exactly: binInt) { + return result + } + return nil } public var isFile: Bool { - #if os(Windows) - return (statBuf.st_mode & UInt16(ucrt.S_IFREG)) != 0 - #else - return (statBuf.st_mode & S_IFREG) != 0 - #endif + return (fileAttrs[.type] as! FileAttributeType == .typeRegular) } public var isDirectory: Bool { - #if os(Windows) - return (statBuf.st_mode & UInt16(ucrt.S_IFDIR)) != 0 - #else - return (statBuf.st_mode & S_IFDIR) != 0 - #endif + return fileAttrs[.type] as! FileAttributeType == .typeDirectory } public var isSymlink: Bool { - #if os(Windows) - return (statBuf.st_mode & UInt16(S_IFLNK)) == S_IFLNK - #else - return (statBuf.st_mode & S_IFMT) == S_IFLNK - #endif + return fileAttrs[.type] as! FileAttributeType == .typeSymbolicLink } - public var isExecutable: Bool { - #if os(Windows) - // Per https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/stat-functions, "user execute bits are set according to the filename extension". - // Don't use FileManager.isExecutableFile due to https://github.com/swiftlang/swift-foundation/issues/860 - return (statBuf.st_mode & UInt16(_S_IEXEC)) != 0 - #else - return (statBuf.st_mode & S_IXUSR) != 0 - #endif + public var size: Int64 { + return _readFileAttributePrimitive(fileAttrs[.size], as: Int64.self) ?? 0 } - public var permissions: Int { - return Int(statBuf.st_mode & 0o777) + public var permissions: UInt16 { + return _readFileAttributePrimitive(fileAttrs[.posixPermissions], as: UInt16.self) ?? 0 } - public var owner: Int { - return Int(statBuf.st_uid) + public var owner: UInt { + return _readFileAttributePrimitive(fileAttrs[.ownerAccountID], as: UInt.self) ?? 0 } - public var group: Int { - return Int(statBuf.st_gid) + public var group: UInt { + return _readFileAttributePrimitive(fileAttrs[.groupOwnerAccountID], as: UInt.self) ?? 0 } - public var modificationTimestamp: time_t { - return statBuf.st_mtimespec.tv_sec + public var modificationDate: Date { + return fileAttrs[.modificationDate] as! Date } - public var modificationDate: Date { - let secs = statBuf.st_mtimespec.tv_sec - let nsecs = statBuf.st_mtimespec.tv_nsec - // Using reference date, instead of 1970, which offers a bit more nanosecond precision since it is a lower absolute number. - return Date(timeIntervalSinceReferenceDate: Double(secs) - Date.timeIntervalBetween1970AndReferenceDate + (1.0e-9 * Double(nsecs))) + public var modificationTimestamp: Int64 { + let date = fileAttrs[.modificationDate] as! Date + return Int64(date.timeIntervalSince1970) + } + + public var modificationNanoseconds: Int { + let date = fileAttrs[.modificationDate] as! Date + return Int(date.timeIntervalSince1970 * 1_000_000_000.0 - Double(date.timeIntervalSince1970) * 1_000_000_000.0) + } + + public var iNode: UInt64 { + return _readFileAttributePrimitive(fileAttrs[.systemFileNumber], as: UInt64.self) ?? 0 + } + + public var deviceID: Int32 { + return _readFileAttributePrimitive(fileAttrs[.systemNumber], as: Int32.self) ?? 0 } public static func ==(lhs: FileInfo, rhs: FileInfo) -> Bool { - return lhs.statBuf == rhs.statBuf + return NSDictionary(dictionary: lhs.fileAttrs).isEqual(NSDictionary(dictionary: rhs.fileAttrs)) } } @@ -297,19 +307,19 @@ public extension FSProxy { } func getFileSize(_ path: Path) throws -> ByteCount { - try ByteCount(Int64(getFileInfo(path).statBuf.st_size)) + try ByteCount(Int64(getFileInfo(path).size)) } } fileprivate extension FSProxy { - func createFileInfo(_ statBuf: stat) -> FileInfo { + func createFileInfo(_ fileAttrs: [FileAttributeKey: any Sendable]) -> FileInfo { if fileSystemMode == .deviceAgnostic { - var buf = statBuf - buf.st_ino = 0 - buf.st_dev = 0 + var buf = fileAttrs + buf[.systemFileNumber] = 0 + buf[.systemNumber] = 0 return FileInfo(buf) } - return FileInfo(statBuf) + return FileInfo(fileAttrs) } } @@ -348,28 +358,30 @@ class LocalFS: FSProxy, @unchecked Sendable { /// Check whether a filesystem entity exists at the given path. func exists(_ path: Path) -> Bool { - var statBuf = stat() - if stat(path.str, &statBuf) < 0 { - return false - } - return true + fileManager.fileExists(atPath: path.str) } /// Check whether the given path is a directory. /// /// If the given path is a symlink to a directory, then this will return true if the destination of the symlink is a directory. func isDirectory(_ path: Path) -> Bool { - var statBuf = stat() - if stat(path.str, &statBuf) < 0 { - return false +#if canImport(Darwin) + var isDirectory: ObjCBool = false + if fileManager.fileExists(atPath: path.str, isDirectory: &isDirectory) { + return isDirectory.boolValue } - return createFileInfo(statBuf).isDirectory +#else + var isDirectory = false + if fileManager.fileExists(atPath: path.str, isDirectory: &isDirectory) { + return isDirectory + } +#endif + return false } /// Check whether a given path is a symlink. /// - parameter destinationExists: If the path is a symlink, then this `inout` parameter will be set to `true` if the destination exists. Otherwise it will be set to `false`. func isSymlink(_ path: Path, _ destinationExists: inout Bool) -> Bool { - #if os(Windows) do { let destination = try fileManager.destinationOfSymbolicLink(atPath: path.str) destinationExists = exists((path.isAbsolute ? path.dirname : Path.currentDirectory).join(destination)) @@ -378,22 +390,6 @@ class LocalFS: FSProxy, @unchecked Sendable { destinationExists = false return false } - #else - destinationExists = false - var statBuf = stat() - if lstat(path.str, &statBuf) < 0 { - return false - } - guard createFileInfo(statBuf).isSymlink else { - return false - } - statBuf = stat() - if stat(path.str, &statBuf) < 0 { - return true - } - destinationExists = true - return true - #endif } func listdir(_ path: Path) throws -> [String] { @@ -403,71 +399,53 @@ class LocalFS: FSProxy, @unchecked Sendable { /// Creates a directory at the given path. Throws an exception if it cannot do so. /// - parameter recursive: If `false`, then the parent directory at `path` must already exist in order to create the directory. If it doesn't, then it will return without creating the directory (it will not throw an exception). If `true`, then the directory hierarchy of `path` will be created if possible. func createDirectory(_ path: Path, recursive: Bool) throws { - // Try to create the directory. - #if os(Windows) - do { - return try fileManager.createDirectory(atPath: path.str, withIntermediateDirectories: recursive) - } catch { - throw StubError.error("Could not create directory at path '\(path.str)': \(error)") - } - #else - let result = mkdir(path.str, S_IRWXU | S_IRWXG | S_IRWXO) - - // If it succeeded, we are done. - if result == 0 { - return + guard path.isAbsolute else { + throw StubError.error("Cannot recursively create directory at non-absolute path: \(path.str)") } - - // If the failure was because something exists at this path, then we examine it to see whether it means we're okay. - if errno == EEXIST { - var destinationExists = false - if isDirectory(path) { - // If the item at the path is a directory, then we're good. This includes if it's a symlink which points to a directory. - return - } - else if isSymlink(path, &destinationExists) { - // If the item at the path is a symlink, then we check whether it's a broken symlink or points to something that is not a directory. - if destinationExists { - // The destination does exist, so it's not a directory. - throw StubError.error("File is a symbolic link which references a path which is not a directory: \(path.str)") + // If something exists at this path, then we examine it to see whether it means we're okay. + do { + try fileManager.createDirectory(atPath: path.str, withIntermediateDirectories: false) + } catch let error as CocoaError { + if error.code == .fileWriteFileExists || error.code == .fileWriteUnknown { + var destinationExists = false + if isDirectory(path) { + // If the item at the path is a directory, then we're good. This includes if it's a symlink which points to a directory. + return + } + else if isSymlink(path, &destinationExists) { + // If the item at the path is a symlink, then we check whether it's a broken symlink or points to something that is not a directory. + if destinationExists { + // The destination does exist, so it's not a directory. + throw StubError.error("File is a symbolic link which references a path which is not a directory: \(path.str)") + } + else { + // The destination does not exist - throw an exception because we have a broken symlink. + throw StubError.error("File is a broken symbolic link: \(path.str)") + } } else { - // The destination does not exist - throw an exception because we have a broken symlink. - throw StubError.error("File is a broken symbolic link: \(path.str)") + /// The path exists but is not a directory + throw StubError.error("File exists but is not a directory: \(path.str)") } } - else { - /// The path exists but is not a directory - throw StubError.error("File exists but is not a directory: \(path.str)") - } - } - - // If we are recursive and not the root path, then... - if recursive && !path.isRoot { - // If it failed due to ENOENT (e.g., a missing parent), then attempt to create the parent and retry. - if errno == ENOENT { - // Attempt to create the parent. - guard path.isAbsolute else { - throw StubError.error("Cannot recursively create directory at non-absolute path: \(path.str)") - } - try createDirectory(path.dirname, recursive: true) - - // Re-attempt creation, non-recursively. - try createDirectory(path) + if recursive && !path.isRoot { + if error.code == .fileNoSuchFile { + // Attempt to create the parent. + try createDirectory(path.dirname, recursive: true) - // We are done. - return - } + // Re-attempt creation, non-recursively. + try createDirectory(path) - // If our parent is not a directory, then report that. - if !isDirectory(path.dirname) { - throw StubError.error("File exists but is not a directory: \(path.dirname.str)") + // We are done. + return + } + // If our parent is not a directory, then report that. + if !isDirectory(path.dirname) { + throw StubError.error("File exists but is not a directory: \(path.dirname.str)") + } } + throw error } - - // Otherwise, we failed due to some other error. Report it. - throw POSIXError(errno, context: "mkdir", path.str, "S_IRWXU | S_IRWXG | S_IRWXO") - #endif } func createTemporaryDirectory(parent: Path) throws -> Path { @@ -569,49 +547,21 @@ class LocalFS: FSProxy, @unchecked Sendable { } func remove(_ path: Path) throws { - guard unlink(path.str) == 0 else { - throw POSIXError(errno, context: "unlink", path.str) - } + try fileManager.removeItem(atPath: path.str) } func removeDirectory(_ path: Path) throws { if isDirectory(path) { - #if os(Windows) try fileManager.removeItem(atPath: path.str) - #else - var paths = [path] - try traverse(path) { paths.append($0) } - for path in paths.reversed() { - guard SWBLibc.remove(path.str) == 0 else { - throw POSIXError(errno, context: "remove", path.str) - } - } - #endif } } func setFilePermissions(_ path: Path, permissions: Int) throws { - #if os(Windows) - // permissions work differently on Windows - #else - try eintrLoop { - guard chmod(path.str, mode_t(permissions)) == 0 else { - throw POSIXError(errno, context: "chmod", path.str, String(mode_t(permissions))) - } - } - #endif + try fileManager.setAttributes([.posixPermissions: Int(permissions)], ofItemAtPath: path.str) } func setFileOwnership(_ path: Path, owner: Int, group: Int) throws { - #if os(Windows) - // permissions work differently on Windows - #else - try eintrLoop { - guard chown(path.str, uid_t(owner), gid_t(group)) == 0 else { - throw POSIXError(errno, context: "chown", path.str, String(uid_t(owner)), String(gid_t(group))) - } - } - #endif + try fileManager.setAttributes([.ownerAccountID: owner, .groupOwnerAccountID: group], ofItemAtPath: path.str) } func touch(_ path: Path) throws { @@ -627,24 +577,23 @@ class LocalFS: FSProxy, @unchecked Sendable { } func getFileInfo(_ path: Path) throws -> FileInfo { - var buf = stat() - - try eintrLoop { - guard stat(path.str, &buf) == 0 else { - throw POSIXError(errno, context: "stat", path.str) + if isSymlink(path) { + var destinationPath = try fileManager.destinationOfSymbolicLink(atPath: path.str) + if !Path(destinationPath).isAbsolute { + destinationPath = path.dirname.join(Path(destinationPath)).str } + return createFileInfo(try fileManager.attributesOfItem(atPath: destinationPath)) } - - return createFileInfo(buf) + return createFileInfo(try fileManager.attributesOfItem(atPath: path.str)) } func getFilePermissions(_ path: Path) throws -> Int { - return try getFileInfo(path).permissions + return try Int(getFileInfo(path).permissions) } func getFileOwnership(_ path: Path) throws -> (owner: Int, group: Int) { let fileInfo = try getFileInfo(path) - return (fileInfo.owner, fileInfo.group) + return (Int(fileInfo.owner), Int(fileInfo.group)) } func getFileTimestamp(_ path: Path) throws -> Int { @@ -652,7 +601,7 @@ class LocalFS: FSProxy, @unchecked Sendable { } func isExecutable(_ path: Path) throws -> Bool { - return try getFileInfo(path).isExecutable + return fileManager.isExecutableFile(atPath: path.str) } func isFile(_ path: Path) throws -> Bool { @@ -660,27 +609,7 @@ class LocalFS: FSProxy, @unchecked Sendable { } func getLinkFileInfo(_ path: Path) throws -> FileInfo { - var buf = stat() - #if os(Windows) - try eintrLoop { - guard stat(path.str, &buf) == 0 else { - throw POSIXError(errno, context: "lstat", path.str) - } - } - - var destinationExists = false - if isSymlink(path, &destinationExists) { - buf.st_mode &= ~UInt16(ucrt.S_IFREG) - buf.st_mode |= UInt16(S_IFLNK) - } - #else - try eintrLoop { - guard lstat(path.str, &buf) == 0 else { - throw POSIXError(errno, context: "lstat", path.str) - } - } - #endif - return createFileInfo(buf) + return try createFileInfo(fileManager.attributesOfItem(atPath: path.str)) } @discardableResult func traverse(_ path: Path, _ f: (Path) throws -> T?) throws -> [T] { @@ -700,11 +629,7 @@ class LocalFS: FSProxy, @unchecked Sendable { } func symlink(_ path: Path, target: Path) throws { - #if os(Windows) try fileManager.createSymbolicLink(atPath: path.str, withDestinationPath: target.str) - #else - guard SWBLibc.symlink(target.str, path.str) == 0 else { throw POSIXError(errno, context: "symlink", target.str, path.str) } - #endif } func setIsExcludedFromBackup(_ path: Path, _ value: Bool) throws { @@ -854,18 +779,7 @@ class LocalFS: FSProxy, @unchecked Sendable { } func readlink(_ path: Path) throws -> Path { - #if os(Windows) return try Path(fileManager.destinationOfSymbolicLink(atPath: path.str)) - #else - let buf = UnsafeMutablePointer.allocate(capacity: Int(PATH_MAX) + 1) - defer { buf.deallocate() } - let result = SWBLibc.readlink(path.str, buf, Int(PATH_MAX)) - guard result >= 0 else { - throw POSIXError(errno, context: "readlink", path.str) - } - buf[result] = 0 - return Path(String.init(cString: buf)) - #endif } func getFreeDiskSpace(_ path: Path) throws -> ByteCount? { @@ -1329,41 +1243,31 @@ public class PseudoFS: FSProxy, @unchecked Sendable { guard let node = getNode(path) else { throw POSIXError(ENOENT) } switch node.contents { case .file(let contents): - var info = stat() - #if os(Windows) - info.st_mtimespec = timespec(tv_sec: Int64(node.timestamp), tv_nsec: 0) - #else - info.st_mtimespec = timespec(tv_sec: time_t(node.timestamp), tv_nsec: 0) - #endif - info.st_size = off_t(contents.bytes.count) - info.st_dev = node.device - info.st_ino = node.inode + let info: [FileAttributeKey: any Sendable] = [ + .modificationDate : Date(timeIntervalSince1970: TimeInterval(node.timestamp)), + .type: FileAttributeType.typeRegular, + .size: contents.bytes.count, + .posixPermissions: 0, + .systemNumber: node.device, + .systemFileNumber: node.inode] return createFileInfo(info) case .directory(let dir): - var info = stat() - #if os(Windows) - info.st_mode = UInt16(ucrt.S_IFDIR) - info.st_mtimespec = timespec(tv_sec: Int64(node.timestamp), tv_nsec: 0) - #else - info.st_mode = S_IFDIR - info.st_mtimespec = timespec(tv_sec: time_t(node.timestamp), tv_nsec: 0) - #endif - info.st_size = off_t(dir.contents.count) - info.st_dev = node.device - info.st_ino = node.inode + let info: [FileAttributeKey: any Sendable] = [ + .modificationDate: Date(timeIntervalSince1970: TimeInterval(node.timestamp)), + .type: FileAttributeType.typeDirectory, + .size: dir.contents.count, + .posixPermissions: 0, + .systemNumber: node.device, + .systemFileNumber: node.inode] return createFileInfo(info) case .symlink(_): - var info = stat() - #if os(Windows) - info.st_mode = UInt16(S_IFLNK) - info.st_mtimespec = timespec(tv_sec: Int64(node.timestamp), tv_nsec: 0) - #else - info.st_mode = S_IFLNK - info.st_mtimespec = timespec(tv_sec: time_t(node.timestamp), tv_nsec: 0) - #endif - info.st_size = off_t(0) - info.st_dev = node.device - info.st_ino = node.inode + let info: [FileAttributeKey: any Sendable] = [ + .modificationDate: Date(timeIntervalSince1970: TimeInterval(node.timestamp)), + .type: FileAttributeType.typeSymbolicLink, + .size: 0, + .posixPermissions: 0, + .systemNumber: node.device, + .systemFileNumber: node.inode] return createFileInfo(info) } } @@ -1481,72 +1385,6 @@ public func createFS(simulated: Bool, ignoreFileSystemDeviceInodeChanges: Bool) } } -fileprivate extension stat { - static func ==(lhs: stat, rhs: stat) -> Bool { - return ( - lhs.st_dev == rhs.st_dev && - lhs.st_ino == rhs.st_ino && - lhs.st_mode == rhs.st_mode && - lhs.st_nlink == rhs.st_nlink && - lhs.st_uid == rhs.st_uid && - lhs.st_gid == rhs.st_gid && - lhs.st_rdev == rhs.st_rdev && - lhs.st_atimespec == rhs.st_atimespec && - lhs.st_mtimespec == rhs.st_mtimespec && - lhs.st_ctimespec == rhs.st_ctimespec && - lhs.st_size == rhs.st_size) - } -} - -extension timespec: Equatable { - public static func ==(lhs: timespec, rhs: timespec) -> Bool { - return lhs.tv_sec == rhs.tv_sec && lhs.tv_nsec == rhs.tv_nsec - } -} - -#if os(Windows) -public struct timespec: Sendable { - public let tv_sec: Int64 - public let tv_nsec: Int64 -} - -extension stat { - public var st_atim: timespec { - get { timespec(tv_sec: st_atime, tv_nsec: 0) } - set { st_atime = newValue.tv_sec } - } - - public var st_mtim: timespec { - get { timespec(tv_sec: st_mtime, tv_nsec: 0) } - set { st_mtime = newValue.tv_sec } - } - - public var st_ctim: timespec { - get { timespec(tv_sec: st_ctime, tv_nsec: 0) } - set { st_ctime = newValue.tv_sec } - } -} -#endif - -#if !canImport(Darwin) -extension stat { - public var st_atimespec: timespec { - get { st_atim } - set { st_atim = newValue } - } - - public var st_mtimespec: timespec { - get { st_mtim } - set { st_mtim = newValue } - } - - public var st_ctimespec: timespec { - get { st_ctim } - set { st_ctim = newValue } - } -} -#endif - #if os(Windows) extension HANDLE { /// Runs a closure and then closes the HANDLE, even if an error occurs. diff --git a/Sources/SWBUtil/FilesSignature.swift b/Sources/SWBUtil/FilesSignature.swift index 8cbc25c7..6b716f5d 100644 --- a/Sources/SWBUtil/FilesSignature.swift +++ b/Sources/SWBUtil/FilesSignature.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import SWBLibc +internal import Foundation /// Represents an opaque signature of a list of files. /// @@ -51,18 +52,18 @@ fileprivate extension FSProxy { /// /// The signature returned is a byte string constructed from an MD5 of properties of all of the files, so the order of `paths` is significant, and a different signature may be returned for different orderings. func filesSignature(_ paths: [Path]) -> ByteString { - var stats: [(Path, stat?)] = [] + var stats: [(Path, FileInfo?)] = [] for path in paths { if isDirectory(path) { do { try traverse(path) { subPath in - stats.append((subPath, try? getFileInfo(subPath).statBuf)) + stats.append((subPath, try? getFileInfo(subPath))) } } catch { stats.append((path, nil)) } } else { - stats.append((path, try? getFileInfo(path).statBuf)) + stats.append((path, try? getFileInfo(path))) } } @@ -70,17 +71,17 @@ fileprivate extension FSProxy { } /// Returns the signature of a list of files. - func filesSignature(_ statInfos: [(Path, stat?)]) -> ByteString { + func filesSignature(_ statInfos: [(Path, FileInfo?)]) -> ByteString { let md5Context = InsecureHashContext() for (path, statInfo) in statInfos { md5Context.add(string: path.str) if let statInfo { md5Context.add(string: "stat") - md5Context.add(number: statInfo.st_ino) - md5Context.add(number: statInfo.st_dev) - md5Context.add(number: statInfo.st_size) - md5Context.add(number: statInfo.st_mtimespec.tv_sec) - md5Context.add(number: statInfo.st_mtimespec.tv_nsec) + md5Context.add(number: statInfo.iNode) + md5Context.add(number: statInfo.deviceID) + md5Context.add(number: statInfo.size) + md5Context.add(number: statInfo.modificationTimestamp) + md5Context.add(number: statInfo.modificationNanoseconds) } else { md5Context.add(string: "") } diff --git a/Sources/SWBUtil/PbxCp.swift b/Sources/SWBUtil/PbxCp.swift index f5be8a02..ee9b67a3 100644 --- a/Sources/SWBUtil/PbxCp.swift +++ b/Sources/SWBUtil/PbxCp.swift @@ -404,14 +404,14 @@ fileprivate func copyEntry(_ srcPath: Path, _ srcTopLevelPath: Path, _ srcParent } else if fileInfo.isFile { try await copyRegular(srcPath, srcParentPath, dstPath, options: options, verbose: verbose, indentationLevel: indentationLevel, outStream: outStream) if verbose { - let size = fileInfo.statBuf.st_size + let size = fileInfo.size textOutput(" \(size) bytes", indentTo: indentationLevel, outStream: outStream) } return 1 } else if fileInfo.isDirectory { return try await copyDirectory(srcPath, srcTopLevelPath, srcParentPath, dstPath, options: options, verbose: verbose, indentationLevel: indentationLevel, outStream: outStream) } else { - throw StubError.error("\(srcPath): unsupported or unknown stat mode (0x\(String(format: "%02x", fileInfo.statBuf.st_mode))") + throw StubError.error("\(srcPath): unsupported or unknown file type: \(fileInfo.fileAttrs[.type] as! String)") } } diff --git a/Tests/SWBBuildSystemTests/BuildOperationTests.swift b/Tests/SWBBuildSystemTests/BuildOperationTests.swift index f3a0c446..8b0c82a4 100644 --- a/Tests/SWBBuildSystemTests/BuildOperationTests.swift +++ b/Tests/SWBBuildSystemTests/BuildOperationTests.swift @@ -4782,7 +4782,7 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script try await tester.checkBuild(runDestination: .macOS, persistent: true) { results in if !SWBFeatureFlag.performOwnershipAnalysis.value { for _ in 0..<4 { - results.checkError(.contains("No such file or directory (2) (for task: [\"Copy\"")) + results.checkError(.contains("couldn’t be opened because there is no such file. (for task: [\"Copy\"")) } } results.checkError(.contains("unterminated string literal")) @@ -5076,7 +5076,7 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script } if !SWBFeatureFlag.performOwnershipAnalysis.value { for fname in ["aFramework.swiftmodule", "aFramework.swiftdoc", "aFramework.swiftsourceinfo", "aFramework.abi.json"] { - results.checkError(.contains("\(tmpDirPath.str)/Test/aProject/build/aProject.build/Debug/aFramework.build/Objects-normal/x86_64/\(fname)): No such file or directory (2)")) + results.checkError(.contains("The file “\(fname)” couldn’t be opened because there is no such file.")) } } results.checkError("Build input file cannot be found: \'\(tmpDirPath.str)/Test/aProject/File.swift\'. Did you forget to declare this file as an output of a script phase or custom build rule which produces it? (for task: [\"ExtractAppIntentsMetadata\"])") diff --git a/Tests/SWBBuildSystemTests/SwiftDriverTests.swift b/Tests/SWBBuildSystemTests/SwiftDriverTests.swift index 062e17ea..e6209702 100644 --- a/Tests/SWBBuildSystemTests/SwiftDriverTests.swift +++ b/Tests/SWBBuildSystemTests/SwiftDriverTests.swift @@ -2489,10 +2489,10 @@ fileprivate struct SwiftDriverTests: CoreBasedTests { if !SWBFeatureFlag.performOwnershipAnalysis.value { results.checkErrors([ - .contains("No such file or directory"), - .contains("No such file or directory"), - .contains("No such file or directory"), - .contains("No such file or directory"), + .contains("couldn’t be opened because there is no such file."), + .contains("couldn’t be opened because there is no such file."), + .contains("couldn’t be opened because there is no such file."), + .contains("couldn’t be opened because there is no such file."), ]) } @@ -2585,10 +2585,10 @@ fileprivate struct SwiftDriverTests: CoreBasedTests { if !SWBFeatureFlag.performOwnershipAnalysis.value { results.checkErrors([ - .contains("No such file or directory"), - .contains("No such file or directory"), - .contains("No such file or directory"), - .contains("No such file or directory"), + .contains("couldn’t be opened because there is no such file."), + .contains("couldn’t be opened because there is no such file."), + .contains("couldn’t be opened because there is no such file."), + .contains("couldn’t be opened because there is no such file."), ]) } diff --git a/Tests/SWBTaskExecutionTests/FileCopyTaskTests.swift b/Tests/SWBTaskExecutionTests/FileCopyTaskTests.swift index da70238e..d27708d8 100644 --- a/Tests/SWBTaskExecutionTests/FileCopyTaskTests.swift +++ b/Tests/SWBTaskExecutionTests/FileCopyTaskTests.swift @@ -122,7 +122,11 @@ fileprivate struct FileCopyTaskTests { #expect(result == .failed) // Examine the error messages. - XCTAssertMatch(outputDelegate.errors, [.suffix("MissingFile.bogus): No such file or directory (2)")]) + #if canImport(Darwin) + XCTAssertMatch(outputDelegate.errors, [.suffix("The file “MissingFile.bogus” couldn’t be opened because there is no such file.")]) + #else + XCTAssertMatch(outputDelegate.errors, [.suffix("The operation could not be completed. The file doesn’t exist.")]) + #endif } } } diff --git a/Tests/SWBUtilTests/FSProxyTests.swift b/Tests/SWBUtilTests/FSProxyTests.swift index 1f56717d..7a1aa90a 100644 --- a/Tests/SWBUtilTests/FSProxyTests.swift +++ b/Tests/SWBUtilTests/FSProxyTests.swift @@ -36,7 +36,7 @@ import SWBTestSupport // MARK: LocalFS Tests - @Test(.skipHostOS(.windows)) // FIXME: error handling is different on Windows + @Test func localCreateDirectory() throws { try withTemporaryDirectory { (tmpDir: Path) in // Create a directory inside the tmpDir. @@ -111,7 +111,11 @@ import SWBTestSupport } catch { didThrow = true + #if os(Windows) #expect(error.localizedDescription == "File exists but is not a directory: \(filePath.str)") + #else + #expect(error.localizedDescription == "File exists but is not a directory: \(dirPath.str)") + #endif } #expect(didThrow) } @@ -179,7 +183,11 @@ import SWBTestSupport } catch { didThrow = true - #expect(error.localizedDescription == "File exists but is not a directory: \(symlinkPath.str)") + #if os(Windows) + #expect(error.localizedDescription == "File is a symbolic link which references a path which is not a directory: \(symlinkPath.str)") + #else + #expect(error.localizedDescription == "File exists but is not a directory: \(dirPath.str)") + #endif } #expect(didThrow) } @@ -200,7 +208,7 @@ import SWBTestSupport #expect { try localFS.createDirectory(filePath, recursive: true) } throws: { error in - error.localizedDescription == "Cannot recursively create directory at non-absolute path: foo/bar/baz" + error.localizedDescription == "Cannot recursively create directory at non-absolute path: \(filePath.str)" } } } @@ -390,7 +398,7 @@ import SWBTestSupport #expect(!fileInfo.isSymlink) let linkFileInfo = try localFS.getLinkFileInfo(file) - #expect(fileInfo.statBuf.st_ino == linkFileInfo.statBuf.st_ino) + #expect(fileInfo.iNode == linkFileInfo.iNode) #expect(!linkFileInfo.isSymlink) // Test absolute and relative targets @@ -406,7 +414,7 @@ import SWBTestSupport #expect(try localFS.read(sym) == data) let symFileInfo = try localFS.getFileInfo(sym) - #expect(symFileInfo.statBuf.st_ino == fileInfo.statBuf.st_ino) + #expect(symFileInfo.iNode == fileInfo.iNode) #expect(!symFileInfo.isSymlink) let symLinkFileInfo = try localFS.getLinkFileInfo(sym) @@ -450,9 +458,7 @@ import SWBTestSupport // not working on Windows for some reason let hostOS = try ProcessInfo.processInfo.hostOperatingSystem() - withKnownIssue { - #expect(fsModDate == fileMgrModDate) - } when: { hostOS == .windows } + #expect(fsModDate == fileMgrModDate) } } @@ -849,18 +855,20 @@ import SWBTestSupport // Check that file stat information differs. #expect(try fs.getFileInfo(Path.root.join("subdir/a.txt")) != fs.getFileInfo(Path.root.join("subdir/b.txt"))) -#if !os(Windows) // Check that we can get stat info on the directory. let s = try fs.getFileInfo(Path.root.join("subdir")) - #expect(s.statBuf.st_mode & S_IFDIR == S_IFDIR) - #expect(s.statBuf.st_size == 2) + #expect(s.isDirectory) + #expect(s.size == 2) // Check that the stat info changes if we mutate the directory. try fs.remove(Path.root.join("subdir/b.txt")) try fs.write(Path.root.join("subdir/c.txt"), contents: "c") let s2 = try fs.getFileInfo(Path.root.join("subdir")) #expect(s != s2) -#endif + + let f = try fs.getFileInfo(Path.root.join("subdir")) + let f2 = try fs.getFileInfo(Path.root.join("subdir")) + #expect(f == f2) } @Test @@ -1141,10 +1149,13 @@ import SWBTestSupport } func _testCopyTree(_ fs: any FSProxy, basePath: Path) throws { - func compareFileInfo(_ lhs: FileInfo, _ rhs: FileInfo, sourceLocation: SourceLocation = #_sourceLocation) { + func compareFileInfo(_ lhsPath: Path, _ rhsPAth: Path, sourceLocation: SourceLocation = #_sourceLocation) throws { + let lhs = try fs.getFileInfo(lhsPath) + let rhs = try fs.getFileInfo(rhsPAth) + + #expect(FileManager.default.isExecutableFile(atPath: lhsPath.str) == FileManager.default.isExecutableFile(atPath: rhsPAth.str), sourceLocation: sourceLocation) #expect(lhs.group == rhs.group, sourceLocation: sourceLocation) #expect(lhs.isDirectory == rhs.isDirectory, sourceLocation: sourceLocation) - #expect(lhs.isExecutable == rhs.isExecutable, sourceLocation: sourceLocation) #expect(lhs.isSymlink == rhs.isSymlink, sourceLocation: sourceLocation) if fs is PseudoFS { // There is no guarantee that the implementation of copy() will preserve the modification timestamp on either files and/or directories, on any real filesystem, so only make this assertion for the pseudo filesystem which we wholly control. @@ -1188,11 +1199,11 @@ import SWBTestSupport #expect(try fs.getFilePermissions(subdirDst.join("dir0/file0")) == file0Perms) #expect(try fs.getFilePermissions(subdirDst.join("dir0/dir0_0/file1")) == file1Perms) } - compareFileInfo(try fs.getFileInfo(subdirDst.join("dir0")), try fs.getFileInfo(subdir.join("dir0"))) - compareFileInfo(try fs.getFileInfo(subdirDst.join("dir0/file0")), try fs.getFileInfo(subdir.join("dir0/file0"))) - compareFileInfo(try fs.getFileInfo(subdirDst.join("dir0/dir0_0/file1")), try fs.getFileInfo(subdir.join("dir0/dir0_0/file1"))) - compareFileInfo(try fs.getFileInfo(subdirDst.join("dir0/dir0_0")), try fs.getFileInfo(subdir.join("dir0/dir0_0"))) - compareFileInfo(try fs.getFileInfo(subdirDst.join("dir1")), try fs.getFileInfo(subdir.join("dir1"))) + try compareFileInfo(subdirDst.join("dir0"), subdir.join("dir0")) + try compareFileInfo(subdirDst.join("dir0/file0"), subdir.join("dir0/file0")) + try compareFileInfo(subdirDst.join("dir0/dir0_0/file1"), subdir.join("dir0/dir0_0/file1")) + try compareFileInfo(subdirDst.join("dir0/dir0_0"), subdir.join("dir0/dir0_0")) + try compareFileInfo(subdirDst.join("dir1"), subdir.join("dir1")) // Test the file contents. #expect(try ByteString(data0) == fs.read(subdirDst.join("dir0/file0"))) @@ -1212,9 +1223,9 @@ import SWBTestSupport let sig0a_orig = fs.filesSignature([file0]) // Validate that the inode/device info is only 0 when the info should be ignored. - let inode = try fs.getFileInfo(file0).statBuf.st_ino + let inode = try fs.getFileInfo(file0).iNode #expect((inode == 0) == shouldIgnoreDeviceInodeChanges) - let device = try fs.getFileInfo(file0).statBuf.st_dev + let device = try fs.getFileInfo(file0).deviceID #expect((device == 0) == shouldIgnoreDeviceInodeChanges) // Copy the file and copy it back, keeping the attributes of the file intact. NOTE!! Do not change this from copy/remove to move as that will **not** necessarily change the st_ino value. By copying the file, we can guarantee that a new file inode must be created. @@ -1255,9 +1266,9 @@ import SWBTestSupport // Validate that the inode/device info is only 0 when the info should be ignored. for file in [dir0, dir1, file0] { - let inode = try fs.getFileInfo(file).statBuf.st_ino + let inode = try fs.getFileInfo(file).iNode #expect((inode == 0) == shouldIgnoreDeviceInodeChanges) - let device = try fs.getFileInfo(file).statBuf.st_dev + let device = try fs.getFileInfo(file).deviceID #expect((device == 0) == shouldIgnoreDeviceInodeChanges) }