From 427374a651a8f868bc64c0a8c16e1036f9a27400 Mon Sep 17 00:00:00 2001 From: Dave Inglis Date: Fri, 6 Jun 2025 09:18:01 -0400 Subject: [PATCH] Update LocalFS to use FileManager for all operations LocalFS was using some posix APIs (stat, mkdir, chmod, chown, etc) with some being used in Windows which mean long filenames would fail in some cases, this cleans that up making all platforms that same --- .../Tools/SwiftCompiler.swift | 2 +- Sources/SWBLLBuild/LowLevelBuildSystem.swift | 33 +- .../ODRAssetPackManifestTaskAction.swift | 2 +- Sources/SWBUtil/FSProxy.swift | 450 ++++++------------ Sources/SWBUtil/FilesSignature.swift | 19 +- Sources/SWBUtil/PbxCp.swift | 4 +- .../BuildOperationTests.swift | 4 +- .../SwiftDriverTests.swift | 16 +- .../FileCopyTaskTests.swift | 6 +- Tests/SWBUtilTests/FSProxyTests.swift | 57 ++- 10 files changed, 235 insertions(+), 358 deletions(-) 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) }