diff --git a/CMakeLists.txt b/CMakeLists.txt index c514239c..6d13e2aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,10 @@ set(CMAKE_DISABLE_IN_SOURCE_BUILD YES) option(BUILD_SHARED_LIBS "Build shared libraries by default" YES) +if(FIND_PM_DEPS) + find_package(SwiftSystem CONFIG REQUIRED) +endif() + find_package(dispatch QUIET) find_package(Foundation QUIET) find_package(Threads QUIET) diff --git a/Package.swift b/Package.swift index 6c115e4a..1c95c490 100644 --- a/Package.swift +++ b/Package.swift @@ -44,7 +44,6 @@ let package = Package( name: "TSCTestSupport", targets: ["TSCTestSupport"]), ], - dependencies: [], targets: [ // MARK: Tools support core targets @@ -67,7 +66,8 @@ let package = Package( "TSCclibc", .product(name: "SystemPackage", package: "swift-system"), ], - exclude: CMakeFiles + ["README.md"]), + exclude: CMakeFiles + ["README.md"], + cxxSettings: [.define("_CRT_SECURE_NO_WARNINGS")]), .target( /** Abstractions for common operations, should migrate to TSCBasic */ name: "TSCUtility", @@ -102,27 +102,14 @@ let package = Package( ) /// When not using local dependencies, the branch to use for llbuild and TSC repositories. - let relatedDependenciesBranch = "main" - - if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { - package.dependencies += [ - .package(url: "https://github.com/apple/swift-system.git", .upToNextMinor(from: "1.1.1")), - ] - } else { - package.dependencies += [ - .package(path: "../swift-system"), - ] - } - -// FIXME: conditionalise these flags since SwiftPM 5.3 and earlier will crash -// for platforms they don't know about. -#if os(Windows) - if let TSCBasic = package.targets.first(where: { $0.name == "TSCBasic" }) { - TSCBasic.cxxSettings = [ - .define("_CRT_SECURE_NO_WARNINGS", .when(platforms: [.windows])), +let relatedDependenciesBranch = "main" + +if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { + package.dependencies += [ + .package(url: "https://github.com/apple/swift-system.git", .upToNextMinor(from: "1.1.1")), ] - TSCBasic.linkerSettings = [ - .linkedLibrary("Pathcch", .when(platforms: [.windows])), +} else { + package.dependencies += [ + .package(path: "../swift-system"), ] - } -#endif +} diff --git a/Sources/TSCBasic/CMakeLists.txt b/Sources/TSCBasic/CMakeLists.txt index 73517a6a..c15384af 100644 --- a/Sources/TSCBasic/CMakeLists.txt +++ b/Sources/TSCBasic/CMakeLists.txt @@ -60,15 +60,13 @@ target_link_libraries(TSCBasic PUBLIC SwiftSystem::SystemPackage TSCLibc) target_link_libraries(TSCBasic PRIVATE - TSCclibc) + TSCclibc) if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin) if(Foundation_FOUND) target_link_libraries(TSCBasic PUBLIC Foundation) endif() endif() -target_link_libraries(TSCBasic PRIVATE - $<$:Pathcch>) # NOTE(compnerd) workaround for CMake not setting up include flags yet set_target_properties(TSCBasic PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) diff --git a/Sources/TSCBasic/FileSystem.swift b/Sources/TSCBasic/FileSystem.swift index 03fdd8ed..cf34a1c7 100644 --- a/Sources/TSCBasic/FileSystem.swift +++ b/Sources/TSCBasic/FileSystem.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors + Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors Licensed under Apache License v2.0 with Runtime Library Exception See http://swift.org/LICENSE.txt for license information @@ -409,7 +409,7 @@ private class LocalFileSystem: FileSystem { } func createSymbolicLink(_ path: AbsolutePath, pointingAt destination: AbsolutePath, relative: Bool) throws { - let destString = relative ? destination.relative(to: path.parentDirectory).pathString : destination.pathString + let destString = relative ? try destination.relative(to: path.parentDirectory).pathString : destination.pathString try FileManager.default.createSymbolicLink(atPath: path.pathString, withDestinationPath: destString) } @@ -846,7 +846,7 @@ public class InMemoryFileSystem: FileSystem { throw FileSystemError(.alreadyExistsAtDestination, path) } - let destination = relative ? destination.relative(to: path.parentDirectory).pathString : destination.pathString + let destination = relative ? try destination.relative(to: path.parentDirectory).pathString : destination.pathString contents.entries[path.basename] = Node(.symlink(destination)) } @@ -1024,11 +1024,11 @@ public class RerootedFileSystemView: FileSystem { /// Adjust the input path for the underlying file system. private func formUnderlyingPath(_ path: AbsolutePath) -> AbsolutePath { - if path == AbsolutePath.root { + if path.isRoot { return root } else { // FIXME: Optimize? - return root.appending(RelativePath(String(path.pathString.dropFirst(1)))) + return root.appending(RelativePath(path.filepath.removingRoot())) } } diff --git a/Sources/TSCBasic/Path.swift b/Sources/TSCBasic/Path.swift index 71a3732b..ec7180d1 100644 --- a/Sources/TSCBasic/Path.swift +++ b/Sources/TSCBasic/Path.swift @@ -1,333 +1,167 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors + Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors Licensed under Apache License v2.0 with Runtime Library Exception See http://swift.org/LICENSE.txt for license information See http://swift.org/CONTRIBUTORS.txt for Swift project authors */ -#if os(Windows) -import Foundation -import WinSDK -#endif - -#if os(Windows) -private typealias PathImpl = UNIXPath -#else -private typealias PathImpl = UNIXPath -#endif +import SystemPackage import protocol Foundation.CustomNSError import var Foundation.NSLocalizedDescriptionKey -/// Represents an absolute file system path, independently of what (or whether -/// anything at all) exists at that path in the file system at any given time. -/// An absolute path always starts with a `/` character, and holds a normalized -/// string representation. This normalization is strictly syntactic, and does -/// not access the file system in any way. -/// -/// The absolute path string is normalized by: -/// - Collapsing `..` path components -/// - Removing `.` path components -/// - Removing any trailing path separator -/// - Removing any redundant path separators -/// -/// This string manipulation may change the meaning of a path if any of the -/// path components are symbolic links on disk. However, the file system is -/// never accessed in any way when initializing an AbsolutePath. -/// -/// Note that `~` (home directory resolution) is *not* done as part of path -/// normalization, because it is normally the responsibility of the shell and -/// not the program being invoked (e.g. when invoking `cd ~`, it is the shell -/// that evaluates the tilde; the `cd` command receives an absolute path). -public struct AbsolutePath: Hashable { - /// Check if the given name is a valid individual path component. - /// - /// This only checks with regard to the semantics enforced by `AbsolutePath` - /// and `RelativePath`; particular file systems may have their own - /// additional requirements. - static func isValidComponent(_ name: String) -> Bool { - return PathImpl.isValidComponent(name) - } - - /// Private implementation details, shared with the RelativePath struct. - private let _impl: PathImpl - - /// Private initializer when the backing storage is known. - private init(_ impl: PathImpl) { - _impl = impl - } +public protocol Path: Hashable, Codable, CustomStringConvertible { + /// Underlying type, based on SwiftSystem. + var filepath: FilePath { get } - /// Initializes the AbsolutePath from `absStr`, which must be an absolute - /// path (i.e. it must begin with a path separator; this initializer does - /// not interpret leading `~` characters as home directory specifiers). - /// The input string will be normalized if needed, as described in the - /// documentation for AbsolutePath. - public init(_ absStr: String) { - self.init(PathImpl(normalizingAbsolutePath: absStr)) - } + /// Public initializer from FilePath. + init(_ filepath: FilePath) - /// Initializes an AbsolutePath from a string that may be either absolute - /// or relative; if relative, `basePath` is used as the anchor; if absolute, - /// it is used as is, and in this case `basePath` is ignored. - public init(_ str: String, relativeTo basePath: AbsolutePath) { - if PathImpl(string: str).isAbsolute { - self.init(str) - } else { - self.init(basePath, RelativePath(str)) - } - } + /// Public initializer from String. + init(_ string: String) - /// Initializes the AbsolutePath by concatenating a relative path to an - /// existing absolute path, and renormalizing if necessary. - public init(_ absPath: AbsolutePath, _ relPath: RelativePath) { - self.init(absPath._impl.appending(relativePath: relPath._impl)) - } + /// Convenience initializer that verifies that the path lexically. + init(validating path: String) throws - /// Convenience initializer that appends a string to a relative path. - public init(_ absPath: AbsolutePath, _ relStr: String) { - self.init(absPath, RelativePath(relStr)) - } + /// Normalized string representation (the normalization rules are described + /// in the documentation of the initializer). This string is never empty. + var pathString: String { get } - /// Convenience initializer that verifies that the path is absolute. - public init(validating path: String) throws { - try self.init(PathImpl(validatingAbsolutePath: path)) - } + /// The root of a path. + var root: String? { get } /// Directory component. An absolute path always has a non-empty directory /// component (the directory component of the root path is the root itself). - public var dirname: String { - return _impl.dirname - } + var dirname: String { get } - /// Last path component (including the suffix, if any). it is never empty. - public var basename: String { - return _impl.basename - } + /// Last path component (including the suffix, if any). + var basename: String { get } /// Returns the basename without the extension. - public var basenameWithoutExt: String { - if let ext = self.extension { - return String(basename.dropLast(ext.count + 1)) - } - return basename - } - - /// Suffix (including leading `.` character) if any. Note that a basename - /// that starts with a `.` character is not considered a suffix, nor is a - /// trailing `.` character. - public var suffix: String? { - return _impl.suffix - } + var basenameWithoutExt: String { get } /// Extension of the give path's basename. This follow same rules as /// suffix except that it doesn't include leading `.` character. - public var `extension`: String? { - return _impl.extension - } + var `extension`: String? { get } - /// Absolute path of parent directory. This always returns a path, because - /// every directory has a parent (the parent directory of the root directory - /// is considered to be the root directory itself). - public var parentDirectory: AbsolutePath { - return AbsolutePath(_impl.parentDirectory) - } - - /// True if the path is the root directory. - public var isRoot: Bool { -#if os(Windows) - return _impl.string.withCString(encodedAs: UTF16.self, PathCchIsRoot) -#else - return _impl == PathImpl.root -#endif - } + /// Suffix (including leading `.` character) if any. Note that a basename + /// that starts with a `.` character is not considered a suffix, nor is a + /// trailing `.` character. + var suffix: String? { get } - /// Returns the absolute path with the relative path applied. - public func appending(_ subpath: RelativePath) -> AbsolutePath { - return AbsolutePath(self, subpath) - } + /// True if the path is a root directory. + var isRoot: Bool { get } - /// Returns the absolute path with an additional literal component appended. + /// Returns the path with an additional literal component appended. /// /// This method accepts pseudo-path like '.' or '..', but should not contain "/". - public func appending(component: String) -> AbsolutePath { - return AbsolutePath(_impl.appending(component: component)) - } + func appending(component: String) -> Self - /// Returns the absolute path with additional literal components appended. + /// Returns the relative path with additional literal components appended. /// /// This method should only be used in cases where the input is guaranteed /// to be a valid path component (i.e., it cannot be empty, contain a path /// separator, or be a pseudo-path like '.' or '..'). - public func appending(components names: [String]) -> AbsolutePath { - // FIXME: This doesn't seem a particularly efficient way to do this. - return names.reduce(self, { path, name in - path.appending(component: name) - }) - } - - public func appending(components names: String...) -> AbsolutePath { - appending(components: names) - } - - /// NOTE: We will most likely want to add other `appending()` methods, such - /// as `appending(suffix:)`, and also perhaps `replacing()` methods, - /// such as `replacing(suffix:)` or `replacing(basename:)` for some - /// of the more common path operations. - - /// NOTE: We may want to consider adding operators such as `+` for appending - /// a path component. - - /// NOTE: We will want to add a method to return the lowest common ancestor - /// path. - - /// Root directory (whose string representation is just a path separator). - public static let root = AbsolutePath(PathImpl.root) - - /// Normalized string representation (the normalization rules are described - /// in the documentation of the initializer). This string is never empty. - public var pathString: String { - return _impl.string - } + func appending(components names: [String]) -> Self + func appending(components names: String...) -> Self /// Returns an array of strings that make up the path components of the - /// absolute path. This is the same sequence of strings as the basenames - /// of each successive path component, starting from the root. Therefore - /// the first path component of an absolute path is always `/`. - public var components: [String] { - return _impl.components - } + /// path. This is the same sequence of strings as the basenames of each + /// successive path component. An empty path has a single path + /// component: the `.` string. + /// + /// NOTE: Path components no longer include the root. Use `root` instead. + var components: [String] { get } } -/// Represents a relative file system path. A relative path never starts with -/// a `/` character, and holds a normalized string representation. As with -/// AbsolutePath, the normalization is strictly syntactic, and does not access -/// the file system in any way. -/// -/// The relative path string is normalized by: -/// - Collapsing `..` path components that aren't at the beginning -/// - Removing extraneous `.` path components -/// - Removing any trailing path separator -/// - Removing any redundant path separators -/// - Replacing a completely empty path with a `.` -/// -/// This string manipulation may change the meaning of a path if any of the -/// path components are symbolic links on disk. However, the file system is -/// never accessed in any way when initializing a RelativePath. -public struct RelativePath: Hashable { - /// Private implementation details, shared with the AbsolutePath struct. - fileprivate let _impl: PathImpl - - /// Private initializer when the backing storage is known. - private init(_ impl: PathImpl) { - _impl = impl - } - - /// Initializes the RelativePath from `str`, which must be a relative path - /// (which means that it must not begin with a path separator or a tilde). - /// An empty input path is allowed, but will be normalized to a single `.` - /// character. The input string will be normalized if needed, as described - /// in the documentation for RelativePath. - public init(_ string: String) { - // Normalize the relative string and store it as our Path. - self.init(PathImpl(normalizingRelativePath: string)) +/// Default implementations of some protocol stubs. +extension Path { + public var pathString: String { + if filepath.string.isEmpty { + return "." + } + return filepath.string } - /// Convenience initializer that verifies that the path is relative. - public init(validating path: String) throws { - try self.init(PathImpl(validatingRelativePath: path)) + public var root: String? { + return filepath.root?.string } - /// Directory component. For a relative path without any path separators, - /// this is the `.` string instead of the empty string. public var dirname: String { - return _impl.dirname + let dirname = filepath.removingLastComponent().string + if dirname.isEmpty { + return "." + } + return dirname } - /// Last path component (including the suffix, if any). It is never empty. public var basename: String { - return _impl.basename + return filepath.lastComponent?.string ?? root ?? "." } - /// Returns the basename without the extension. public var basenameWithoutExt: String { - if let ext = self.extension { - return String(basename.dropLast(ext.count + 1)) - } - return basename + return filepath.lastComponent?.stem ?? root ?? "." } - /// Suffix (including leading `.` character) if any. Note that a basename - /// that starts with a `.` character is not considered a suffix, nor is a - /// trailing `.` character. - public var suffix: String? { - return _impl.suffix - } - - /// Extension of the give path's basename. This follow same rules as - /// suffix except that it doesn't include leading `.` character. public var `extension`: String? { - return _impl.extension + guard let ext = filepath.extension, + !ext.isEmpty else { + return nil + } + return filepath.extension } - /// Normalized string representation (the normalization rules are described - /// in the documentation of the initializer). This string is never empty. - public var pathString: String { - return _impl.string + public var suffix: String? { + if let ext = self.extension { + return "." + ext + } else { + return nil + } } - /// Returns an array of strings that make up the path components of the - /// relative path. This is the same sequence of strings as the basenames - /// of each successive path component. Therefore the returned array of - /// path components is never empty; even an empty path has a single path - /// component: the `.` string. - public var components: [String] { - return _impl.components + public var isRoot: Bool { + return filepath.isRoot } - /// Returns the relative path with the given relative path applied. - public func appending(_ subpath: RelativePath) -> RelativePath { - return RelativePath(_impl.appending(relativePath: subpath._impl)) + public func appending(component: String) -> Self { + return Self(filepath.appending( + FilePath.Component(stringLiteral: component))) } - /// Returns the relative path with an additional literal component appended. - /// - /// This method accepts pseudo-path like '.' or '..', but should not contain "/". - public func appending(component: String) -> RelativePath { - return RelativePath(_impl.appending(component: component)) + public func appending(components names: [String]) -> Self { + let components = names.map(FilePath.Component.init) + return Self(filepath.appending(components)) } - /// Returns the relative path with additional literal components appended. - /// - /// This method should only be used in cases where the input is guaranteed - /// to be a valid path component (i.e., it cannot be empty, contain a path - /// separator, or be a pseudo-path like '.' or '..'). - public func appending(components names: [String]) -> RelativePath { - // FIXME: This doesn't seem a particularly efficient way to do this. - return names.reduce(self, { path, name in - path.appending(component: name) - }) + public func appending(components names: String...) -> Self { + appending(components: names) } - public func appending(components names: String...) -> RelativePath { - appending(components: names) + public var components: [String] { + var components = filepath.components.map(\.string) + if filepath.isRelative && components.isEmpty { + components.append(".") + } + return components } } -extension AbsolutePath: Codable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(pathString) +/// Default implementation of `CustomStringConvertible`. +extension Path { + public var description: String { + return pathString } - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - try self.init(validating: container.decode(String.self)) + public var debugDescription: String { + // FIXME: We should really be escaping backslashes and quotes here. + return "<\(Self.self):\"\(pathString)\">" } } -extension RelativePath: Codable { +/// Default implementation of `Codable`. +extension Path { public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(pathString) @@ -339,521 +173,209 @@ extension RelativePath: Codable { } } -// Make absolute paths Comparable. -extension AbsolutePath: Comparable { - public static func < (lhs: AbsolutePath, rhs: AbsolutePath) -> Bool { - return lhs.pathString < rhs.pathString - } -} - -/// Make absolute paths CustomStringConvertible and CustomDebugStringConvertible. -extension AbsolutePath: CustomStringConvertible, CustomDebugStringConvertible { - public var description: String { - return pathString - } - - public var debugDescription: String { - // FIXME: We should really be escaping backslashes and quotes here. - return "" - } -} - -/// Make relative paths CustomStringConvertible and CustomDebugStringConvertible. -extension RelativePath: CustomStringConvertible { - public var description: String { - return _impl.string - } - - public var debugDescription: String { - // FIXME: We should really be escaping backslashes and quotes here. - return "" - } -} - -/// Private implementation shared between AbsolutePath and RelativePath. -protocol Path: Hashable { - - /// Root directory. - static var root: Self { get } - - /// Checks if a string is a valid component. - static func isValidComponent(_ name: String) -> Bool - - /// Normalized string of the (absolute or relative) path. Never empty. - var string: String { get } - - /// Returns whether the path is an absolute path. - var isAbsolute: Bool { get } - - /// Returns the directory part of the stored path (relying on the fact that it has been normalized). Returns a - /// string consisting of just `.` if there is no directory part (which is the case if and only if there is no path - /// separator). - var dirname: String { get } - - /// Returns the last past component. - var basename: String { get } - - /// Returns the components of the path between each path separator. - var components: [String] { get } - - /// Path of parent directory. This always returns a path, because every directory has a parent (the parent - /// directory of the root directory is considered to be the root directory itself). - var parentDirectory: Self { get } - - /// Creates a path from its normalized string representation. - init(string: String) - - /// Creates a path from an absolute string representation and normalizes it. - init(normalizingAbsolutePath: String) - - /// Creates a path from an relative string representation and normalizes it. - init(normalizingRelativePath: String) - - /// Creates a path from a string representation, validates that it is a valid absolute path and normalizes it. - init(validatingAbsolutePath: String) throws - - /// Creates a path from a string representation, validates that it is a valid relative path and normalizes it. - init(validatingRelativePath: String) throws - - /// Returns suffix with leading `.` if withDot is true otherwise without it. - func suffix(withDot: Bool) -> String? - - /// Returns a new Path by appending the path component. - func appending(component: String) -> Self - - /// Returns a path by concatenating a relative path and renormalizing if necessary. - func appending(relativePath: Self) -> Self -} - -extension Path { - var suffix: String? { - return suffix(withDot: true) - } +/// Represents an absolute file system path, independently of what (or whether +/// anything at all) exists at that path in the file system at any given time. +/// An absolute path always holds a normalized string representation. This +/// normalization is strictly syntactic, and does not access the file system +/// in any way. +/// +/// The absolute path string is normalized by: +/// - Collapsing `..` path components +/// - Removing `.` path components +/// - Removing any trailing path separator +/// - Removing any redundant path separators +/// +/// This string manipulation may change the meaning of a path if any of the +/// path components are symbolic links on disk. However, the file system is +/// never accessed in any way when initializing an AbsolutePath. +/// +/// Note that `~` (home directory resolution) is *not* done as part of path +/// normalization, because it is normally the responsibility of the shell and +/// not the program being invoked (e.g. when invoking `cd ~`, it is the shell +/// that evaluates the tilde; the `cd` command receives an absolute path). +public struct AbsolutePath: Path { + /// Underlying type, based on SwiftSystem. + public let filepath: FilePath - var `extension`: String? { - return suffix(withDot: false) + /// Public initializer with `FilePath``. + public init(_ filepath: FilePath) { +#if os(Windows) + if filepath.isAbsolute { + self.filepath = filepath.lexicallyNormalized() + return + } + var normalizedFilePath = filepath.lexicallyNormalized() + guard filepath.root?.string == "\\" else { + preconditionFailure("\(filepath) is not a valid absolute path") + } + normalizedFilePath.root = AbsolutePath.root.filepath.root + self.filepath = normalizedFilePath.lexicallyNormalized() +#else + precondition(filepath.isAbsolute) + self.filepath = filepath.lexicallyNormalized() +#endif } -} - -private struct UNIXPath: Path { - let string: String - - static let root = UNIXPath(string: "/") - static func isValidComponent(_ name: String) -> Bool { - return name != "" && name != "." && name != ".." && !name.contains("/") + /// Initializes the AbsolutePath from `absStr`, which must be an absolute + /// path (i.e. it must begin with a path separator; this initializer does + /// not interpret leading `~` characters as home directory specifiers). + /// The input string will be normalized if needed, as described in the + /// documentation for AbsolutePath. + /// + /// On Unix-like systems, an absolute path always starts with a `/` + /// character. Windows normally regards `/` as a relative root, but for + /// compatibility, system drive letter will be appended. Use + /// `try AbsolutePath(validating:)` to avoid such convention. + public init(_ absStr: String) { + self.init(FilePath(absStr)) } -#if os(Windows) - static func isAbsolutePath(_ path: String) -> Bool { - return !path.withCString(encodedAs: UTF16.self, PathIsRelativeW) + /// Initializes an AbsolutePath from a string that may be either absolute + /// or relative; if relative, `basePath` is used as the anchor; if absolute, + /// it is used as is, and in this case `basePath` is ignored. + public init(_ str: String, relativeTo basePath: AbsolutePath) { + self.init(basePath.filepath.pushing(FilePath(str))) } -#endif - var dirname: String { -#if os(Windows) - let fsr: UnsafePointer = string.fileSystemRepresentation - defer { fsr.deallocate() } - - let path: String = String(cString: fsr) - return path.withCString(encodedAs: UTF16.self) { - let data = UnsafeMutablePointer(mutating: $0) - PathCchRemoveFileSpec(data, path.count) - return String(decodingCString: data, as: UTF16.self) - } -#else - // FIXME: This method seems too complicated; it should be simplified, - // if possible, and certainly optimized (using UTF8View). - // Find the last path separator. - guard let idx = string.lastIndex(of: "/") else { - // No path separators, so the directory name is `.`. - return "." - } - // Check if it's the only one in the string. - if idx == string.startIndex { - // Just one path separator, so the directory name is `/`. - return "/" - } - // Otherwise, it's the string up to (but not including) the last path - // separator. - return String(string.prefix(upTo: idx)) -#endif + /// Initializes the AbsolutePath by concatenating a relative path to an + /// existing absolute path, and renormalizing if necessary. + public init(_ absPath: AbsolutePath, _ relPath: RelativePath) { + self.init(absPath.filepath.pushing(relPath.filepath)) } - var isAbsolute: Bool { -#if os(Windows) - return UNIXPath.isAbsolutePath(string) -#else - return string.hasPrefix("/") -#endif + /// Convenience initializer that appends a string to a relative path. + public init(_ absPath: AbsolutePath, _ relStr: String) { + self.init(absPath.filepath.pushing(FilePath(relStr))) } - var basename: String { -#if os(Windows) - let path: String = self.string - return path.withCString(encodedAs: UTF16.self) { - PathStripPathW(UnsafeMutablePointer(mutating: $0)) - return String(decodingCString: $0, as: UTF16.self) - } -#else - // FIXME: This method seems too complicated; it should be simplified, - // if possible, and certainly optimized (using UTF8View). - // Check for a special case of the root directory. - if string.spm_only == "/" { - // Root directory, so the basename is a single path separator (the - // root directory is special in this regard). - return "/" - } - // Find the last path separator. - guard let idx = string.lastIndex(of: "/") else { - // No path separators, so the basename is the whole string. - return string - } - // Otherwise, it's the string from (but not including) the last path - // separator. - return String(string.suffix(from: string.index(after: idx))) -#endif + /// Convenience initializer that verifies that the path is absolute. + public init(validating path: String) throws { + try self.init(FilePath(validatingAbsolutePath: path)) } - // FIXME: We should investigate if it would be more efficient to instead - // return a path component iterator that does all its work lazily, moving - // from one path separator to the next on-demand. - // - var components: [String] { -#if os(Windows) - return string.components(separatedBy: "\\").filter { !$0.isEmpty } -#else - // FIXME: This isn't particularly efficient; needs optimization, and - // in fact, it might well be best to return a custom iterator so we - // don't have to allocate everything up-front. It would be backed by - // the path string and just return a slice at a time. - let components = string.components(separatedBy: "/").filter({ !$0.isEmpty }) - - if string.hasPrefix("/") { - return ["/"] + components - } else { - return components - } -#endif + /// Absolute path of parent directory. This always returns a path, because + /// every directory has a parent (the parent directory of the root directory + /// is considered to be the root directory itself). + public var parentDirectory: AbsolutePath { + return AbsolutePath(filepath.removingLastComponent()) } - var parentDirectory: UNIXPath { - return self == .root ? self : Self(string: dirname) + /// Returns the absolute path with the relative path applied. + public func appending(_ subpath: RelativePath) -> AbsolutePath { + return AbsolutePath(self, subpath) } - init(string: String) { - self.string = string - } + /// NOTE: We will most likely want to add other `appending()` methods, such + /// as `appending(suffix:)`, and also perhaps `replacing()` methods, + /// such as `replacing(suffix:)` or `replacing(basename:)` for some + /// of the more common path operations. - init(normalizingAbsolutePath path: String) { - #if os(Windows) - var buffer: [WCHAR] = Array(repeating: 0, count: Int(MAX_PATH + 1)) - _ = path.withCString(encodedAs: UTF16.self) { - PathCanonicalizeW(&buffer, $0) - } - self.init(string: String(decodingCString: buffer, as: UTF16.self)) - #else - precondition(path.first == "/", "Failure normalizing \(path), absolute paths should start with '/'") - - // At this point we expect to have a path separator as first character. - assert(path.first == "/") - // Fast path. - if !mayNeedNormalization(absolute: path) { - self.init(string: path) - } + /// NOTE: We may want to consider adding operators such as `+` for appending + /// a path component. - // Split the character array into parts, folding components as we go. - // As we do so, we count the number of characters we'll end up with in - // the normalized string representation. - var parts: [String] = [] - var capacity = 0 - for part in path.split(separator: "/") { - switch part.count { - case 0: - // Ignore empty path components. - continue - case 1 where part.first == ".": - // Ignore `.` path components. - continue - case 2 where part.first == "." && part.last == ".": - // If there's a previous part, drop it; otherwise, do nothing. - if let prev = parts.last { - parts.removeLast() - capacity -= prev.count - } - default: - // Any other component gets appended. - parts.append(String(part)) - capacity += part.count - } + /// Returns the lowest common ancestor path. + public func lowestCommonAncestor(with path: AbsolutePath) -> AbsolutePath? { + guard root == path.root else { + return nil } - capacity += max(parts.count, 1) - - // Create an output buffer using the capacity we've calculated. - // FIXME: Determine the most efficient way to reassemble a string. - var result = "" - result.reserveCapacity(capacity) - - // Put the normalized parts back together again. - var iter = parts.makeIterator() - result.append("/") - if let first = iter.next() { - result.append(contentsOf: first) - while let next = iter.next() { - result.append("/") - result.append(contentsOf: next) + var filepath = path.filepath + while (!filepath.isRoot) { + if self.filepath.starts(with: filepath) { + break } + filepath.removeLastComponent() } - - // Sanity-check the result (including the capacity we reserved). - assert(!result.isEmpty, "unexpected empty string") - assert(result.count == capacity, "count: " + - "\(result.count), cap: \(capacity)") - - // Use the result as our stored string. - self.init(string: result) - #endif + return AbsolutePath(filepath) } - init(normalizingRelativePath path: String) { - #if os(Windows) - if path.isEmpty || path == "." { - self.init(string: ".") + /// The root directory. It is always `/` on UNIX, but may vary on Windows. + @available(*, deprecated, message: "root is not a static value, use the instance property instead") + public static var root: AbsolutePath { + if let rootPath = localFileSystem.currentWorkingDirectory?.root { + return AbsolutePath(rootPath) } else { - var buffer: [WCHAR] = Array(repeating: 0, count: Int(MAX_PATH + 1)) - _ = path.replacingOccurrences(of: "/", with: "\\").withCString(encodedAs: UTF16.self) { - PathCanonicalizeW(&buffer, $0) - } - self.init(string: String(decodingCString: buffer, as: UTF16.self)) - } - #else - precondition(path.first != "/") - - // FIXME: Here we should also keep track of whether anything actually has - // to be changed in the string, and if not, just return the existing one. - - // Split the character array into parts, folding components as we go. - // As we do so, we count the number of characters we'll end up with in - // the normalized string representation. - var parts: [String] = [] - var capacity = 0 - for part in path.split(separator: "/") { - switch part.count { - case 0: - // Ignore empty path components. - continue - case 1 where part.first == ".": - // Ignore `.` path components. - continue - case 2 where part.first == "." && part.last == ".": - // If at beginning, fall through to treat the `..` literally. - guard let prev = parts.last else { - fallthrough - } - // If previous component is anything other than `..`, drop it. - if !(prev.count == 2 && prev.first == "." && prev.last == ".") { - parts.removeLast() - capacity -= prev.count - continue - } - // Otherwise, fall through to treat the `..` literally. - fallthrough - default: - // Any other component gets appended. - parts.append(String(part)) - capacity += part.count - } - } - capacity += max(parts.count - 1, 0) - - // Create an output buffer using the capacity we've calculated. - // FIXME: Determine the most efficient way to reassemble a string. - var result = "" - result.reserveCapacity(capacity) - - // Put the normalized parts back together again. - var iter = parts.makeIterator() - if let first = iter.next() { - result.append(contentsOf: first) - while let next = iter.next() { - result.append("/") - result.append(contentsOf: next) +#if !os(Windows) + return AbsolutePath("/") +#else + if let drive = ProcessEnv.vars["SystemDrive"] ?? ProcessEnv.vars["HomeDrive"] { + return AbsolutePath(drive + "\\") + } else { + fatalError("cannot determine the drive") } +#endif } - - // Sanity-check the result (including the capacity we reserved). - assert(result.count == capacity, "count: " + - "\(result.count), cap: \(capacity)") - - // If the result is empty, return `.`, otherwise we return it as a string. - self.init(string: result.isEmpty ? "." : result) - #endif } +} - init(validatingAbsolutePath path: String) throws { - #if os(Windows) - let fsr: UnsafePointer = path.fileSystemRepresentation - defer { fsr.deallocate() } +/// Represents a relative file system path. A relative path never starts with +/// a `/` character, and holds a normalized string representation. As with +/// AbsolutePath, the normalization is strictly syntactic, and does not access +/// the file system in any way. +/// +/// The relative path string is normalized by: +/// - Collapsing `..` path components that aren't at the beginning +/// - Removing extraneous `.` path components +/// - Removing any trailing path separator +/// - Removing any redundant path separators +/// - Replacing a completely empty path with a `.` +/// +/// This string manipulation may change the meaning of a path if any of the +/// path components are symbolic links on disk. However, the file system is +/// never accessed in any way when initializing a RelativePath. +public struct RelativePath: Path { + /// Underlying type, based on SwiftSystem. + public let filepath: FilePath - let realpath = String(cString: fsr) - if !UNIXPath.isAbsolutePath(realpath) { - throw PathValidationError.invalidAbsolutePath(path) - } - self.init(normalizingAbsolutePath: path) - #else - switch path.first { - case "/": - self.init(normalizingAbsolutePath: path) - case "~": - throw PathValidationError.startsWithTilde(path) - default: - throw PathValidationError.invalidAbsolutePath(path) - } - #endif + /// Public initializer with FilePath. + public init(_ filepath: FilePath) { + precondition(filepath.isRelative) + self.filepath = filepath.lexicallyNormalized() } - init(validatingRelativePath path: String) throws { - #if os(Windows) - let fsr: UnsafePointer = path.fileSystemRepresentation - defer { fsr.deallocate() } - - let realpath: String = String(cString: fsr) - if UNIXPath.isAbsolutePath(realpath) { - throw PathValidationError.invalidRelativePath(path) - } - self.init(normalizingRelativePath: path) - #else - switch path.first { - case "/", "~": - throw PathValidationError.invalidRelativePath(path) - default: - self.init(normalizingRelativePath: path) - } - #endif + /// Initializes the RelativePath from `str`, which must be a relative path + /// (which means that it must not begin with a path separator or a tilde). + /// An empty input path is allowed, but will be normalized to a single `.` + /// character. The input string will be normalized if needed, as described + /// in the documentation for RelativePath. + public init(_ string: String) { + self.init(FilePath(string)) } - func suffix(withDot: Bool) -> String? { -#if os(Windows) - return self.string.withCString(encodedAs: UTF16.self) { - if let pointer = PathFindExtensionW($0) { - let substring = String(decodingCString: pointer, as: UTF16.self) - guard substring.length > 0 else { return nil } - return withDot ? substring : String(substring.dropFirst(1)) - } - return nil - } -#else - // FIXME: This method seems too complicated; it should be simplified, - // if possible, and certainly optimized (using UTF8View). - // Find the last path separator, if any. - let sIdx = string.lastIndex(of: "/") - // Find the start of the basename. - let bIdx = (sIdx != nil) ? string.index(after: sIdx!) : string.startIndex - // Find the last `.` (if any), starting from the second character of - // the basename (a leading `.` does not make the whole path component - // a suffix). - let fIdx = string.index(bIdx, offsetBy: 1, limitedBy: string.endIndex) ?? string.startIndex - if let idx = string[fIdx...].lastIndex(of: ".") { - // Unless it's just a `.` at the end, we have found a suffix. - if string.distance(from: idx, to: string.endIndex) > 1 { - let fromIndex = withDot ? idx : string.index(idx, offsetBy: 1) - return String(string.suffix(from: fromIndex)) - } else { - return nil - } - } - // If we get this far, there is no suffix. - return nil -#endif + /// Convenience initializer that verifies that the path is relative. + public init(validating path: String) throws { + try self.init(FilePath(validatingRelativePath: path)) } - func appending(component name: String) -> UNIXPath { -#if os(Windows) - var result: PWSTR? - _ = string.withCString(encodedAs: UTF16.self) { root in - name.withCString(encodedAs: UTF16.self) { path in - PathAllocCombine(root, path, ULONG(PATHCCH_ALLOW_LONG_PATHS.rawValue), &result) - } - } - defer { LocalFree(result) } - return PathImpl(string: String(decodingCString: result!, as: UTF16.self)) -#else - assert(!name.contains("/"), "\(name) is invalid path component") - - // Handle pseudo paths. - switch name { - case "", ".": - return self - case "..": - return self.parentDirectory - default: - break - } - - if self == Self.root { - return PathImpl(string: "/" + name) - } else { - return PathImpl(string: string + "/" + name) - } -#endif + /// Returns the relative path with the given relative path applied. + public func appending(_ subpath: RelativePath) -> RelativePath { + return + RelativePath(filepath.pushing(subpath.filepath)) } +} - func appending(relativePath: UNIXPath) -> UNIXPath { -#if os(Windows) - var result: PWSTR? - _ = string.withCString(encodedAs: UTF16.self) { root in - relativePath.string.withCString(encodedAs: UTF16.self) { path in - PathAllocCombine(root, path, ULONG(PATHCCH_ALLOW_LONG_PATHS.rawValue), &result) - } - } - defer { LocalFree(result) } - return PathImpl(string: String(decodingCString: result!, as: UTF16.self)) -#else - // Both paths are already normalized. The only case in which we have - // to renormalize their concatenation is if the relative path starts - // with a `..` path component. - var newPathString = string - if self != .root { - newPathString.append("/") - } - - let relativePathString = relativePath.string - newPathString.append(relativePathString) - - // If the relative string starts with `.` or `..`, we need to normalize - // the resulting string. - // FIXME: We can actually optimize that case, since we know that the - // normalization of a relative path can leave `..` path components at - // the beginning of the path only. - if relativePathString.hasPrefix(".") { - if newPathString.hasPrefix("/") { - return PathImpl(normalizingAbsolutePath: newPathString) - } else { - return PathImpl(normalizingRelativePath: newPathString) - } - } else { - return PathImpl(string: newPathString) - } -#endif +// Make absolute paths Comparable. +extension AbsolutePath: Comparable { + public static func < (lhs: AbsolutePath, rhs: AbsolutePath) -> Bool { + return lhs.pathString < rhs.pathString } } /// Describes the way in which a path is invalid. public enum PathValidationError: Error { - case startsWithTilde(String) case invalidAbsolutePath(String) case invalidRelativePath(String) + case differentRoot(String, String) } extension PathValidationError: CustomStringConvertible { public var description: String { switch self { - case .startsWithTilde(let path): - return "invalid absolute path '\(path)'; absolute path must begin with '/'" case .invalidAbsolutePath(let path): return "invalid absolute path '\(path)'" case .invalidRelativePath(let path): - return "invalid relative path '\(path)'; relative path should not begin with '/' or '~'" + return "invalid relative path '\(path)'" + case .differentRoot(let pathA, let pathB): + return "absolute paths '\(pathA)' and '\(pathB)' have different roots" } } } @@ -870,45 +392,32 @@ extension AbsolutePath { /// /// This method is strictly syntactic and does not access the file system /// in any way. Therefore, it does not take symbolic links into account. - public func relative(to base: AbsolutePath) -> RelativePath { - let result: RelativePath - // Split the two paths into their components. - // FIXME: The is needs to be optimized to avoid unncessary copying. - let pathComps = self.components - let baseComps = base.components - - // It's common for the base to be an ancestor, so try that first. - if pathComps.starts(with: baseComps) { - // Special case, which is a plain path without `..` components. It - // might be an empty path (when self and the base are equal). - let relComps = pathComps.dropFirst(baseComps.count) + public func relative(to base: AbsolutePath) throws -> RelativePath { + var relFilePath = FilePath() + var filepath = filepath #if os(Windows) - result = RelativePath(relComps.joined(separator: "\\")) -#else - result = RelativePath(relComps.joined(separator: "/")) -#endif - } else { - // General case, in which we might well need `..` components to go - // "up" before we can go "down" the directory tree. - var newPathComps = ArraySlice(pathComps) - var newBaseComps = ArraySlice(baseComps) - while newPathComps.prefix(1) == newBaseComps.prefix(1) { - // First component matches, so drop it. - newPathComps = newPathComps.dropFirst() - newBaseComps = newBaseComps.dropFirst() + if self.root != base.root { + guard self.root?.count == 3, + self.root!.hasSuffix(":\\") else { + throw PathValidationError.differentRoot(pathString, base.pathString) } - // Now construct a path consisting of as many `..`s as are in the - // `newBaseComps` followed by what remains in `newPathComps`. - var relComps = Array(repeating: "..", count: newBaseComps.count) - relComps.append(contentsOf: newPathComps) -#if os(Windows) - result = RelativePath(relComps.joined(separator: "\\")) -#else - result = RelativePath(relComps.joined(separator: "/")) -#endif + relFilePath.root = .init(String(self.root!.dropLast())) } - assert(base.appending(result) == self) - return result +#endif + filepath.root = base.filepath.root + + let commonAncestor = AbsolutePath(filepath).lowestCommonAncestor(with: base)! + let walkbackDepth: Int = { + var baseFilepath = base.filepath + precondition(baseFilepath.removePrefix(commonAncestor.filepath)) + return baseFilepath.components.count + }() + precondition(filepath.removePrefix(commonAncestor.filepath)) + + relFilePath.append(Array(repeating: FilePath.Component(".."), count: walkbackDepth)) + relFilePath.push(filepath) + + return RelativePath(relFilePath) } /// Returns true if the path contains the given path. @@ -925,7 +434,7 @@ extension AbsolutePath { /// This method is strictly syntactic and does not access the file system /// in any way. public func isAncestor(of descendant: AbsolutePath) -> Bool { - return descendant.components.dropLast().starts(with: self.components) + return descendant.filepath.removingLastComponent().starts(with: self.filepath) } /// Returns true if the path is an ancestor of or equal to the given path. @@ -933,7 +442,7 @@ extension AbsolutePath { /// This method is strictly syntactic and does not access the file system /// in any way. public func isAncestorOfOrEqual(to descendant: AbsolutePath) -> Bool { - return descendant.components.starts(with: self.components) + return descendant.filepath.starts(with: self.filepath) } /// Returns true if the path is a descendant of the given path. @@ -941,7 +450,7 @@ extension AbsolutePath { /// This method is strictly syntactic and does not access the file system /// in any way. public func isDescendant(of ancestor: AbsolutePath) -> Bool { - return self.components.dropLast().starts(with: ancestor.components) + return self.filepath.removingLastComponent().starts(with: ancestor.filepath) } /// Returns true if the path is a descendant of or equal to the given path. @@ -949,7 +458,7 @@ extension AbsolutePath { /// This method is strictly syntactic and does not access the file system /// in any way. public func isDescendantOfOrEqual(to ancestor: AbsolutePath) -> Bool { - return self.components.starts(with: ancestor.components) + return self.filepath.starts(with: ancestor.filepath) } } @@ -959,31 +468,27 @@ extension PathValidationError: CustomNSError { } } -// FIXME: We should consider whether to merge the two `normalize()` functions. -// The argument for doing so is that some of the code is repeated; the argument -// against doing so is that some of the details are different, and since any -// given path is either absolute or relative, it's wasteful to keep checking -// for whether it's relative or absolute. Possibly we can do both by clever -// use of generics that abstract away the differences. +extension FilePath { + init(validatingAbsolutePath path: String) throws { + self.init(path) + guard self.isAbsolute else { + throw PathValidationError.invalidAbsolutePath(path) + } + } -/// Fast check for if a string might need normalization. -/// -/// This assumes that paths containing dotfiles are rare: -private func mayNeedNormalization(absolute string: String) -> Bool { - var last = UInt8(ascii: "0") - for c in string.utf8 { - switch c { - case UInt8(ascii: "/") where last == UInt8(ascii: "/"): - return true - case UInt8(ascii: ".") where last == UInt8(ascii: "/"): - return true - default: - break + init(validatingRelativePath path: String) throws { + self.init(path) + guard self.isRelative else { + throw PathValidationError.invalidRelativePath(path) + } +#if !os(Windows) + guard self.components.first?.string != "~" else { + throw PathValidationError.invalidRelativePath(path) } - last = c +#endif } - if last == UInt8(ascii: "/") { - return true + + var isRoot: Bool { + root != nil && components.isEmpty } - return false } diff --git a/Sources/TSCBasic/PathShims.swift b/Sources/TSCBasic/PathShims.swift index bae0210d..0c51225a 100644 --- a/Sources/TSCBasic/PathShims.swift +++ b/Sources/TSCBasic/PathShims.swift @@ -29,7 +29,7 @@ public func resolveSymlinks(_ path: AbsolutePath) -> AbsolutePath { } return resolved.standardized.withUnsafeFileSystemRepresentation { - try! AbsolutePath(validating: String(cString: $0!)) + AbsolutePath(String(cString: $0!)) } #else let pathStr = path.pathString @@ -63,7 +63,7 @@ public func makeDirectories(_ path: AbsolutePath) throws { /// be a relative path, otherwise it will be absolute. @available(*, deprecated, renamed: "localFileSystem.createSymbolicLink") public func createSymlink(_ path: AbsolutePath, pointingAt dest: AbsolutePath, relative: Bool = true) throws { - let destString = relative ? dest.relative(to: path.parentDirectory).pathString : dest.pathString + let destString = relative ? try dest.relative(to: path.parentDirectory).pathString : dest.pathString try FileManager.default.createSymbolicLink(atPath: path.pathString, withDestinationPath: destString) } @@ -168,18 +168,20 @@ extension AbsolutePath { /// Returns a path suitable for display to the user (if possible, it is made /// to be relative to the current working directory). public func prettyPath(cwd: AbsolutePath? = localFileSystem.currentWorkingDirectory) -> String { - guard let dir = cwd else { - // No current directory, display as is. + guard let dir = cwd, + let rel = try? relative(to: dir) else { + // Cannot create relative path, display as is. return self.pathString } - // FIXME: Instead of string prefix comparison we should add a proper API - // to AbsolutePath to determine ancestry. - if self == dir { - return "." - } else if self.pathString.hasPrefix(dir.pathString + "/") { - return "./" + self.relative(to: dir).pathString + if let first = rel.components.first, + first != ".." { +#if os(Windows) + return ".\\" + rel.pathString +#else + return "./" + rel.pathString +#endif } else { - return self.pathString + return rel.pathString } } } diff --git a/Sources/TSCTestSupport/FileSystemExtensions.swift b/Sources/TSCTestSupport/FileSystemExtensions.swift index 248edf2d..4feeb1c8 100644 --- a/Sources/TSCTestSupport/FileSystemExtensions.swift +++ b/Sources/TSCTestSupport/FileSystemExtensions.swift @@ -52,7 +52,8 @@ extension FileSystem { do { try createDirectory(root, recursive: true) for path in files { - let path = root.appending(RelativePath(String(path.dropFirst()))) + let components = AbsolutePath(path).components + let path = root.appending(components: components) try createDirectory(path.parentDirectory, recursive: true) try writeFileContents(path, bytes: "") } diff --git a/Sources/TSCTestSupport/misc.swift b/Sources/TSCTestSupport/misc.swift index 6d58aa17..58a32705 100644 --- a/Sources/TSCTestSupport/misc.swift +++ b/Sources/TSCTestSupport/misc.swift @@ -9,6 +9,7 @@ */ import func XCTest.XCTFail +import struct XCTest.XCTSkip import class Foundation.NSDate import class Foundation.Thread @@ -61,6 +62,9 @@ public func systemQuietly(_ args: String...) throws { /// from different threads, the environment will neither be setup nor restored /// correctly. public func withCustomEnv(_ env: [String: String], body: () throws -> Void) throws { + #if os(Windows) + throw XCTSkip("'withCustomEnv(_:body:)' is broken on Windows") + #endif let state = Array(env.keys).map({ ($0, ProcessEnv.vars[$0]) }) let restore = { for (key, value) in state { diff --git a/Sources/TSCUtility/Archiver.swift b/Sources/TSCUtility/Archiver.swift index 357a4ce6..c84024f0 100644 --- a/Sources/TSCUtility/Archiver.swift +++ b/Sources/TSCUtility/Archiver.swift @@ -66,7 +66,7 @@ public struct ZipArchiver: Archiver { DispatchQueue.global(qos: .userInitiated).async { do { - let result = try Process.popen(args: "unzip", archivePath.pathString, "-d", destinationPath.pathString) + let result = try Process.popen(args: "unzip\(executableFileSuffix)", archivePath.pathString, "-d", destinationPath.pathString) guard result.exitStatus == .terminated(code: 0) else { throw try StringError(result.utf8stderrOutput()) } diff --git a/Sources/TSCUtility/Platform.swift b/Sources/TSCUtility/Platform.swift index f60455a1..a258301c 100644 --- a/Sources/TSCUtility/Platform.swift +++ b/Sources/TSCUtility/Platform.swift @@ -105,8 +105,8 @@ public enum Platform: Equatable { defer { tmp.deallocate() } guard confstr(name, tmp.baseAddress, len) == len else { return nil } let value = String(cString: tmp.baseAddress!) - guard value.hasSuffix(AbsolutePath.root.pathString) else { return nil } - return resolveSymlinks(AbsolutePath(value)) + guard let path = try? AbsolutePath(validating: value) else { return nil } + return resolveSymlinks(path) } #endif } diff --git a/Tests/TSCBasicPerformanceTests/SynchronizedQueuePerfTests.swift b/Tests/TSCBasicPerformanceTests/SynchronizedQueuePerfTests.swift index a68a5281..83c57a37 100644 --- a/Tests/TSCBasicPerformanceTests/SynchronizedQueuePerfTests.swift +++ b/Tests/TSCBasicPerformanceTests/SynchronizedQueuePerfTests.swift @@ -14,7 +14,7 @@ import TSCBasic import TSCTestSupport class SynchronizedQueuePerfTests: XCTestCasePerf { - + // Mock the UnitTest struct in SwiftPM/SwiftTestTool.swift struct Item { let productPath: AbsolutePath @@ -28,7 +28,6 @@ class SynchronizedQueuePerfTests: XCTestCasePerf { } } - func testEnqueueDequeue_10000() { let queue = SynchronizedQueue() let test = Item(productPath: AbsolutePath.root, name: "TestName", testCase: "TestCaseName") @@ -42,7 +41,7 @@ class SynchronizedQueuePerfTests: XCTestCasePerf { } } } - + func testEnqueueDequeue_1000() { let queue = SynchronizedQueue() let test = Item(productPath: AbsolutePath.root, name: "TestName", testCase: "TestCaseName") @@ -56,7 +55,7 @@ class SynchronizedQueuePerfTests: XCTestCasePerf { } } } - + func testEnqueueDequeue_100() { let queue = SynchronizedQueue() let test = Item(productPath: AbsolutePath.root, name: "TestName", testCase: "TestCaseName") diff --git a/Tests/TSCBasicTests/FileSystemTests.swift b/Tests/TSCBasicTests/FileSystemTests.swift index 55c8fed4..6fd04f8a 100644 --- a/Tests/TSCBasicTests/FileSystemTests.swift +++ b/Tests/TSCBasicTests/FileSystemTests.swift @@ -17,9 +17,12 @@ import TSCLibc class FileSystemTests: XCTestCase { // MARK: LocalFS Tests - func testLocalBasics() throws { let fs = TSCBasic.localFileSystem +#if os(Windows) + XCTSkip("FIXME: withTemporaryDirectory(removeTreeOnDeinit: true) will throw on Windows") + return +#endif try! withTemporaryFile { file in try! withTemporaryDirectory(removeTreeOnDeinit: true) { tempDirPath in // exists() @@ -46,7 +49,7 @@ class FileSystemTests: XCTestCase { // isExecutableFile let executable = tempDirPath.appending(component: "exec-foo") let executableSym = tempDirPath.appending(component: "exec-sym") - try! fs.createSymbolicLink(executableSym, pointingAt: executable, relative: false) + try fs.createSymbolicLink(executableSym, pointingAt: executable, relative: false) let stream = BufferedOutputByteStream() stream <<< """ #!/bin/sh @@ -84,7 +87,10 @@ class FileSystemTests: XCTestCase { } } - func testResolvingSymlinks() { + func testResolvingSymlinks() throws { + #if os(Windows) + throw XCTSkip("Symlink resolving on Windows often crashes due to some Foundation bugs.") + #endif // Make sure the root path resolves to itself. XCTAssertEqual(resolveSymlinks(AbsolutePath.root), AbsolutePath.root) @@ -183,6 +189,7 @@ class FileSystemTests: XCTestCase { } } + #if !os(Windows) func testLocalReadableWritable() throws { try testWithTemporaryDirectory { tmpdir in let fs = localFileSystem @@ -250,6 +257,7 @@ class FileSystemTests: XCTestCase { } } } + #endif func testLocalCreateDirectory() throws { let fs = TSCBasic.localFileSystem diff --git a/Tests/TSCBasicTests/PathShimTests.swift b/Tests/TSCBasicTests/PathShimTests.swift index 9d79535a..f7a2dbf7 100644 --- a/Tests/TSCBasicTests/PathShimTests.swift +++ b/Tests/TSCBasicTests/PathShimTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors + Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors Licensed under Apache License v2.0 with Runtime Library Exception See http://swift.org/LICENSE.txt for license information @@ -54,9 +54,9 @@ class WalkTests : XCTestCase { expected.remove(at: i) } #if os(Android) - XCTAssertEqual(3, x.components.count) - #else XCTAssertEqual(2, x.components.count) + #else + XCTAssertEqual(1, x.components.count) #endif } XCTAssertEqual(expected.count, 0) diff --git a/Tests/TSCBasicTests/PathTests.swift b/Tests/TSCBasicTests/PathTests.swift index c3b08e4c..1ca7cb3f 100644 --- a/Tests/TSCBasicTests/PathTests.swift +++ b/Tests/TSCBasicTests/PathTests.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors + Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors Licensed under Apache License v2.0 with Runtime Library Exception See http://swift.org/LICENSE.txt for license information @@ -12,6 +12,7 @@ import XCTest import Foundation import TSCBasic +import TSCTestSupport class PathTests: XCTestCase { @@ -150,10 +151,10 @@ class PathTests: XCTestCase { } func testBaseNameWithoutExt() { - XCTAssertEqual(AbsolutePath("/").basenameWithoutExt, "/") + XCTAssertEqual(AbsolutePath("/").basenameWithoutExt, AbsolutePath.root.pathString) XCTAssertEqual(AbsolutePath("/a").basenameWithoutExt, "a") XCTAssertEqual(AbsolutePath("/./a").basenameWithoutExt, "a") - XCTAssertEqual(AbsolutePath("/../..").basenameWithoutExt, "/") + XCTAssertEqual(AbsolutePath("/../..").basenameWithoutExt, AbsolutePath.root.pathString) XCTAssertEqual(RelativePath("../..").basenameWithoutExt, "..") XCTAssertEqual(RelativePath("../a").basenameWithoutExt, "a") XCTAssertEqual(RelativePath("../a/..").basenameWithoutExt, "..") @@ -229,8 +230,6 @@ class PathTests: XCTestCase { XCTAssertEqual(AbsolutePath("/").appending(components: "a", "b").pathString, "/a/b") XCTAssertEqual(AbsolutePath("/a").appending(components: "b", "c").pathString, "/a/b/c") - XCTAssertEqual(AbsolutePath("/a/b/c").appending(components: "", "c").pathString, "/a/b/c/c") - XCTAssertEqual(AbsolutePath("/a/b/c").appending(components: "").pathString, "/a/b/c") XCTAssertEqual(AbsolutePath("/a/b/c").appending(components: ".").pathString, "/a/b/c") XCTAssertEqual(AbsolutePath("/a/b/c").appending(components: "..").pathString, "/a/b") XCTAssertEqual(AbsolutePath("/a/b/c").appending(components: "..", "d").pathString, "/a/b/d") @@ -243,14 +242,14 @@ class PathTests: XCTestCase { } func testPathComponents() { - XCTAssertEqual(AbsolutePath("/").components, ["/"]) - XCTAssertEqual(AbsolutePath("/.").components, ["/"]) - XCTAssertEqual(AbsolutePath("/..").components, ["/"]) - XCTAssertEqual(AbsolutePath("/bar").components, ["/", "bar"]) - XCTAssertEqual(AbsolutePath("/foo/bar/..").components, ["/", "foo"]) - XCTAssertEqual(AbsolutePath("/bar/../foo").components, ["/", "foo"]) - XCTAssertEqual(AbsolutePath("/bar/../foo/..//").components, ["/"]) - XCTAssertEqual(AbsolutePath("/bar/../foo/..//yabba/a/b/").components, ["/", "yabba", "a", "b"]) + XCTAssertEqual(AbsolutePath("/").components, []) + XCTAssertEqual(AbsolutePath("/.").components, []) + XCTAssertEqual(AbsolutePath("/..").components, []) + XCTAssertEqual(AbsolutePath("/bar").components, ["bar"]) + XCTAssertEqual(AbsolutePath("/foo/bar/..").components, ["foo"]) + XCTAssertEqual(AbsolutePath("/bar/../foo").components, ["foo"]) + XCTAssertEqual(AbsolutePath("/bar/../foo/..//").components, []) + XCTAssertEqual(AbsolutePath("/bar/../foo/..//yabba/a/b/").components, ["yabba", "a", "b"]) XCTAssertEqual(RelativePath("").components, ["."]) XCTAssertEqual(RelativePath(".").components, ["."]) @@ -271,13 +270,13 @@ class PathTests: XCTestCase { } func testRelativePathFromAbsolutePaths() { - XCTAssertEqual(AbsolutePath("/").relative(to: AbsolutePath("/")), RelativePath(".")); - XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/")), RelativePath("a/b/c/d")); - XCTAssertEqual(AbsolutePath("/").relative(to: AbsolutePath("/a/b/c")), RelativePath("../../..")); - XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/b")), RelativePath("c/d")); - XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/b/c")), RelativePath("d")); - XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/c/d")), RelativePath("../../b/c/d")); - XCTAssertEqual(AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/b/c/d")), RelativePath("../../../a/b/c/d")); + XCTAssertEqual(try! AbsolutePath("/").relative(to: AbsolutePath("/")), RelativePath(".")); + XCTAssertEqual(try! AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/")), RelativePath("a/b/c/d")); + XCTAssertEqual(try! AbsolutePath("/").relative(to: AbsolutePath("/a/b/c")), RelativePath("../../..")); + XCTAssertEqual(try! AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/b")), RelativePath("c/d")); + XCTAssertEqual(try! AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/b/c")), RelativePath("d")); + XCTAssertEqual(try! AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/a/c/d")), RelativePath("../../b/c/d")); + XCTAssertEqual(try! AbsolutePath("/a/b/c/d").relative(to: AbsolutePath("/b/c/d")), RelativePath("../../../a/b/c/d")); } func testComparison() { @@ -312,10 +311,15 @@ class PathTests: XCTestCase { } func testAbsolutePathValidation() { - XCTAssertNoThrow(try AbsolutePath(validating: "/a/b/c/d")) + #if os(Windows) + let pathString = #"C:\a\b\c\d"# + #else + let pathString = "/a/b/c/d" + #endif + XCTAssertNoThrow(try AbsolutePath(validating: pathString)) XCTAssertThrowsError(try AbsolutePath(validating: "~/a/b/d")) { error in - XCTAssertEqual("\(error)", "invalid absolute path '~/a/b/d'; absolute path must begin with '/'") + XCTAssertEqual("\(error)", "invalid absolute path '~/a/b/d'") } XCTAssertThrowsError(try AbsolutePath(validating: "a/b/d")) { error in @@ -327,11 +331,11 @@ class PathTests: XCTestCase { XCTAssertNoThrow(try RelativePath(validating: "a/b/c/d")) XCTAssertThrowsError(try RelativePath(validating: "/a/b/d")) { error in - XCTAssertEqual("\(error)", "invalid relative path '/a/b/d'; relative path should not begin with '/' or '~'") + XCTAssertEqual("\(error)", "invalid relative path '/a/b/d'") } XCTAssertThrowsError(try RelativePath(validating: "~/a/b/d")) { error in - XCTAssertEqual("\(error)", "invalid relative path '~/a/b/d'; relative path should not begin with '/' or '~'") + XCTAssertEqual("\(error)", "invalid relative path '~/a/b/d'") } } diff --git a/Tests/TSCBasicTests/miscTests.swift b/Tests/TSCBasicTests/miscTests.swift index 92b8c204..cc447c88 100644 --- a/Tests/TSCBasicTests/miscTests.swift +++ b/Tests/TSCBasicTests/miscTests.swift @@ -15,8 +15,11 @@ import TSCBasic class miscTests: XCTestCase { func testExecutableLookup() throws { + #if os(Windows) + throw XCTSkip("TODO") + #endif try testWithTemporaryDirectory { path in - + let pathEnv1 = path.appending(component: "pathEnv1") try localFileSystem.createDirectory(pathEnv1) let pathEnvClang = pathEnv1.appending(component: "clang") @@ -28,15 +31,15 @@ class miscTests: XCTestCase { // nil and empty string should fail. XCTAssertNil(lookupExecutablePath(filename: nil, currentWorkingDirectory: path, searchPaths: pathEnv)) XCTAssertNil(lookupExecutablePath(filename: "", currentWorkingDirectory: path, searchPaths: pathEnv)) - + // Absolute path to a binary should return it. var exec = lookupExecutablePath(filename: pathEnvClang.pathString, currentWorkingDirectory: path, searchPaths: pathEnv) XCTAssertEqual(exec, pathEnvClang) - + // This should lookup from PATH variable since executable is not present in cwd. exec = lookupExecutablePath(filename: "clang", currentWorkingDirectory: path, searchPaths: pathEnv) XCTAssertEqual(exec, pathEnvClang) - + // Create the binary relative to cwd and make it executable. let clang = path.appending(component: "clang") try localFileSystem.writeFileContents(clang, bytes: "") @@ -46,18 +49,23 @@ class miscTests: XCTestCase { XCTAssertEqual(exec, clang) } } - + func testEnvSearchPaths() throws { + #if os(Windows) + let pathString = "something;.;abc/../.build/debug;/usr/bin:/bin/" + #else + let pathString = "something:.:abc/../.build/debug:/usr/bin:/bin/" + #endif let cwd = AbsolutePath("/dummy") - let paths = getEnvSearchPaths(pathString: "something:.:abc/../.build/debug:/usr/bin:/bin/", currentWorkingDirectory: cwd) + let paths = getEnvSearchPaths(pathString: pathString, currentWorkingDirectory: cwd) XCTAssertEqual(paths, ["/dummy/something", "/dummy", "/dummy/.build/debug", "/usr/bin", "/bin"].map({AbsolutePath($0)})) } - + func testEmptyEnvSearchPaths() throws { let cwd = AbsolutePath("/dummy") let paths = getEnvSearchPaths(pathString: "", currentWorkingDirectory: cwd) XCTAssertEqual(paths, []) - + let nilPaths = getEnvSearchPaths(pathString: nil, currentWorkingDirectory: cwd) XCTAssertEqual(nilPaths, []) } diff --git a/Tests/TSCUtilityTests/PkgConfigParserTests.swift b/Tests/TSCUtilityTests/PkgConfigParserTests.swift index f8b28ba2..46b3dcb2 100644 --- a/Tests/TSCUtilityTests/PkgConfigParserTests.swift +++ b/Tests/TSCUtilityTests/PkgConfigParserTests.swift @@ -107,14 +107,14 @@ final class PkgConfigParserTests: XCTestCase { "/usr/lib/pkgconfig/foo.pc", "/usr/local/opt/foo/lib/pkgconfig/foo.pc", "/custom/foo.pc") - XCTAssertEqual("/custom/foo.pc", try PCFileFinder(diagnostics: diagnostics, brewPrefix: nil).locatePCFile(name: "foo", customSearchPaths: [AbsolutePath("/custom")], fileSystem: fs).pathString) - XCTAssertEqual("/custom/foo.pc", try PkgConfig(name: "foo", additionalSearchPaths: [AbsolutePath("/custom")], diagnostics: diagnostics, fileSystem: fs, brewPrefix: nil).pcFile.pathString) - XCTAssertEqual("/usr/lib/pkgconfig/foo.pc", try PCFileFinder(diagnostics: diagnostics, brewPrefix: nil).locatePCFile(name: "foo", customSearchPaths: [], fileSystem: fs).pathString) + XCTAssertEqual(AbsolutePath("/custom/foo.pc"), try PCFileFinder(diagnostics: diagnostics, brewPrefix: nil).locatePCFile(name: "foo", customSearchPaths: [AbsolutePath("/custom")], fileSystem: fs)) + XCTAssertEqual(AbsolutePath("/custom/foo.pc"), try PkgConfig(name: "foo", additionalSearchPaths: [AbsolutePath("/custom")], diagnostics: diagnostics, fileSystem: fs, brewPrefix: nil).pcFile) + XCTAssertEqual(AbsolutePath("/usr/lib/pkgconfig/foo.pc"), try PCFileFinder(diagnostics: diagnostics, brewPrefix: nil).locatePCFile(name: "foo", customSearchPaths: [], fileSystem: fs)) try withCustomEnv(["PKG_CONFIG_PATH": "/usr/local/opt/foo/lib/pkgconfig"]) { - XCTAssertEqual("/usr/local/opt/foo/lib/pkgconfig/foo.pc", try PkgConfig(name: "foo", diagnostics: diagnostics, fileSystem: fs, brewPrefix: nil).pcFile.pathString) + XCTAssertEqual(AbsolutePath("/usr/local/opt/foo/lib/pkgconfig/foo.pc"), try PkgConfig(name: "foo", diagnostics: diagnostics, fileSystem: fs, brewPrefix: nil).pcFile) } try withCustomEnv(["PKG_CONFIG_PATH": "/usr/local/opt/foo/lib/pkgconfig:/usr/lib/pkgconfig"]) { - XCTAssertEqual("/usr/local/opt/foo/lib/pkgconfig/foo.pc", try PkgConfig(name: "foo", diagnostics: diagnostics, fileSystem: fs, brewPrefix: nil).pcFile.pathString) + XCTAssertEqual(AbsolutePath("/usr/local/opt/foo/lib/pkgconfig/foo.pc"), try PkgConfig(name: "foo", diagnostics: diagnostics, fileSystem: fs, brewPrefix: nil).pcFile) } } @@ -142,6 +142,7 @@ final class PkgConfigParserTests: XCTestCase { XCTAssertEqual(PCFileFinder.pkgConfigPaths, [AbsolutePath("/Volumes/BestDrive/pkgconfig")]) } + #if !os(Windows) // pkg-config is not compatible with Windows paths. func testAbsolutePathDependency() throws { let libffiPath = "/usr/local/opt/libffi/lib/pkgconfig/libffi.pc" @@ -171,6 +172,7 @@ final class PkgConfigParserTests: XCTestCase { fileSystem: fileSystem, brewPrefix: AbsolutePath("/usr/local"))) } + #endif func testUnevenQuotes() throws { do {