├── .github └── workflows │ ├── ci-linux.yml │ ├── ci-macos.yml │ └── ci-windows.yml ├── .gitignore ├── .swift-format ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── SwiftReload │ ├── Build │ │ ├── SwiftBuildCommand.swift │ │ └── SwiftBuildManifest.swift │ ├── LocalSwiftReloader.swift │ ├── Patcher │ │ ├── CommandPatcher.swift │ │ ├── Patcher.swift │ │ └── PatcherState.swift │ ├── ProjectExtractor │ │ ├── ProjectExtractor.swift │ │ └── SwiftPMProjectExtractor.swift │ ├── SyntaxDiff │ │ └── SyntaxDiff.swift │ └── Watcher │ │ ├── ByteString.swift │ │ ├── Condition.swift │ │ ├── FSWatch.swift │ │ ├── FileInfo.swift │ │ ├── FileSystem.swift │ │ ├── PathUtils.swift │ │ ├── ProcessEnv.swift │ │ ├── RecursiveWatcher.swift │ │ ├── Thread.swift │ │ ├── WatcherUtils.swift │ │ └── WritableByteStream.swift └── SwiftReloadExample │ └── main.swift └── Tests └── SwiftReloadTests └── SwiftReloadTests.swift /.github/workflows/ci-linux.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Linux 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | pull_request: 10 | branches: ["main"] 11 | 12 | jobs: 13 | linux: 14 | runs-on: ubuntu-24.04 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - uses: SwiftyLab/setup-swift@latest 20 | 21 | - name: Get swift version 22 | run: swift --version 23 | 24 | - name: Build 25 | run: swift build 26 | 27 | - name: Run tests 28 | run: swift test 29 | -------------------------------------------------------------------------------- /.github/workflows/ci-macos.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: MacOS 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | pull_request: 10 | branches: ["main"] 11 | 12 | jobs: 13 | macos: 14 | runs-on: macos-15 15 | 16 | steps: 17 | - uses: maxim-lobanov/setup-xcode@v1 18 | with: 19 | xcode-version: "16.1.0" 20 | 21 | - uses: actions/checkout@v3 22 | 23 | - name: Build 24 | run: swift build 25 | 26 | - name: Run tests 27 | run: swift test 28 | -------------------------------------------------------------------------------- /.github/workflows/ci-windows.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Windows 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | pull_request: 10 | branches: ["main"] 11 | 12 | jobs: 13 | windows: 14 | runs-on: windows-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - uses: SwiftyLab/setup-swift@latest 20 | with: 21 | swift-version: https://download.swift.org/swift-6.1-branch/windows10/swift-6.1-DEVELOPMENT-SNAPSHOT-2025-02-20-a/swift-6.1-DEVELOPMENT-SNAPSHOT-2025-02-20-a-windows10.exe 22 | development: true 23 | 24 | - name: Get swift version 25 | run: swift --version 26 | 27 | - name: Build 28 | run: swift build 29 | 30 | # - name: Run tests 31 | # run: swift test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.index-build 4 | /Packages 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/configuration/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | .vscode -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "lineLength": 100, 4 | "indentation": { 5 | "spaces": 4 6 | }, 7 | "maximumBlankLines": 1, 8 | "respectsExistingLineBreaks": true, 9 | "lineBreakBeforeControlFlowKeywords": false, 10 | "lineBreakBeforeEachArgument": true 11 | } -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "72afbb20ee9a458170033da59afcf78c56bfb9defcde99431584f23c3db6c189", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-syntax", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/swiftlang/swift-syntax.git", 8 | "state" : { 9 | "revision" : "0687f71944021d616d34d922343dcef086855920", 10 | "version" : "600.0.1" 11 | } 12 | }, 13 | { 14 | "identity" : "yams", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/jpsim/Yams.git", 17 | "state" : { 18 | "revision" : "2688707e563b44d7d87c29ba6c5ca04ce86ae58b", 19 | "version" : "5.3.0" 20 | } 21 | } 22 | ], 23 | "version" : 3 24 | } 25 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftReload", 8 | 9 | platforms: [ 10 | .macOS(.v14), 11 | .iOS(.v15), 12 | .watchOS(.v8), 13 | .tvOS(.v15), 14 | ], 15 | 16 | products: [ 17 | .library( 18 | name: "SwiftReload", 19 | targets: ["SwiftReload"] 20 | ) 21 | ], 22 | dependencies: [ 23 | .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.0"), 24 | .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.1"), 25 | ], 26 | targets: [ 27 | .target( 28 | name: "SwiftReload", 29 | dependencies: [ 30 | "Yams", 31 | .product(name: "SwiftParser", package: "swift-syntax"), 32 | .product(name: "SwiftSyntax", package: "swift-syntax"), 33 | .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), 34 | ] 35 | ), 36 | .executableTarget( 37 | name: "SwiftReloadExample", 38 | dependencies: [ 39 | "SwiftReload", 40 | .product(name: "SwiftParser", package: "swift-syntax"), 41 | .product(name: "SwiftSyntax", package: "swift-syntax"), 42 | ], 43 | swiftSettings: [ 44 | .unsafeFlags(["-Xfrontend", "-enable-private-imports"]), 45 | .unsafeFlags(["-Xfrontend", "-enable-implicit-dynamic"]), 46 | ], 47 | linkerSettings: [ 48 | .unsafeFlags( 49 | ["-Xlinker", "--export-dynamic"], 50 | .when(platforms: [.linux, .android]) 51 | ) 52 | ] 53 | ), 54 | .testTarget( 55 | name: "SwiftReloadTests", 56 | dependencies: ["SwiftReload"], 57 | swiftSettings: [ 58 | .unsafeFlags(["-Xfrontend", "-enable-private-imports"]), 59 | .unsafeFlags(["-Xfrontend", "-enable-implicit-dynamic"]), 60 | ], 61 | linkerSettings: [ 62 | .unsafeFlags( 63 | ["-Xlinker", "--export-dynamic"], 64 | .when(platforms: [.linux, .android]) 65 | ) 66 | ] 67 | ), 68 | ] 69 | ) 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftReload 2 | 3 | This is an experimental project that enables hot reloading of Swift code in SwiftPM based projects. 4 | 5 | ## Platforms 6 | 7 | | **Platform** | **CI Status** | **Support Status** | 8 | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | 9 | | macOS | [![MacOS](https://github.com/ShaftUI/SwiftReload/actions/workflows/ci-macos.yml/badge.svg)](https://github.com/ShaftUI/SwiftReload/actions/workflows/ci-macos.yml) | ✅ | 10 | | Linux | [![Linux](https://github.com/ShaftUI/SwiftReload/actions/workflows/ci-linux.yml/badge.svg)](https://github.com/ShaftUI/SwiftReload/actions/workflows/ci-linux.yml) | ✅ | 11 | | Windows | [![Swift](https://github.com/ShaftUI/SwiftReload/actions/workflows/ci-windows.yml/badge.svg)](https://github.com/ShaftUI/SwiftReload/actions/workflows/ci-windows.yml) | 🚧 | 12 | 13 | ## Quick Start 14 | 15 | 1. Add SwiftReload to your project's dependencies in `Package.swift`: 16 | 17 | ```swift 18 | dependencies: [ 19 | .package(url: "https://github.com/ShaftUI/SwiftReload.git", branch: "main") 20 | ] 21 | ``` 22 | 23 | 2. Add SwiftReload to your target's dependencies: 24 | 25 | ```swift 26 | .executableTarget( 27 | name: "MyApp", 28 | dependencies: [ 29 | "SwiftReload" 30 | ] 31 | ) 32 | ``` 33 | 34 | 1. Add `-enable-private-imports` and `-enable-implicit-dynamic` flag to your target's build settings: 35 | 36 | ```swift 37 | .executableTarget( 38 | name: "MyApp", 39 | dependencies: [ 40 | "SwiftReload" 41 | ], 42 | swiftSettings: [ 43 | .unsafeFlags( 44 | ["-Xfrontend", "-enable-private-imports"], 45 | .when(configuration: .debug) 46 | ), 47 | .unsafeFlags( 48 | ["-Xfrontend", "-enable-implicit-dynamic"], 49 | .when(configuration: .debug) 50 | ), 51 | ], 52 | linkerSettings: [ 53 | .unsafeFlags( 54 | ["-Xlinker", "--export-dynamic"], 55 | .when(platforms: [.linux, .android], configuration: .debug) 56 | ), 57 | ] 58 | ) 59 | ``` 60 | 61 | This enables method swizzling, which SwiftReload uses to replace code at runtime. 62 | 63 | > On Linux, you also need to add the `-Xlinker --export-dynamic` flag to the linker settings to export all symbols from the executable. 64 | 65 | 3. Add the following code at the beginning of your `main.swift`: 66 | 67 | ```swift 68 | import SwiftReload 69 | 70 | LocalSwiftReloader().start() 71 | ``` 72 | 73 | > For complete example, see the [`Sources/SwiftReloadExample`](https://github.com/ShaftUI/SwiftReload/tree/main/Sources/SwiftReloadExample) directory. 74 | 75 | 76 | ## With [ShaftUI](https://github.com/ShaftUI/Shaft) 77 | 78 | The `LocalSwiftReloader` has a `onReload` callback that is called when the code reload is triggered. You can call `backend.scheduleReassemble` in the callback to rebuild the UI. 79 | 80 | ```swift 81 | #if DEBUG 82 | import SwiftReload 83 | LocalSwiftReloader(onReload: backend.scheduleReassemble).start() 84 | #endif 85 | ``` 86 | 87 | ## How it works 88 | 89 | SwiftReload monitors changes to the source files of your project. When a change is detected, it recompiles the updated source files to a dynamic library and loads it into the running process. The dynamic library then replaces the existing code in the process, effectively enabling hot reloading of Swift code. -------------------------------------------------------------------------------- /Sources/SwiftReload/Build/SwiftBuildCommand.swift: -------------------------------------------------------------------------------- 1 | /// Wrapper around build commands generated by SwiftPM with additional utility 2 | /// methods. 3 | public struct SwiftBuildCommand { 4 | public init(from args: [String]) { 5 | self.args = args 6 | } 7 | 8 | public private(set) var args: [String] 9 | 10 | public func findModuleName() -> String? { 11 | guard let index = args.firstIndex(of: "-module-name") else { 12 | return nil 13 | } 14 | return args[index + 1] 15 | } 16 | 17 | /// Remove `count` args starting from the first occurence of `arg`. 18 | public mutating func remove(_ arg: String, count: Int = 1) { 19 | if let index = args.firstIndex(of: arg) { 20 | args.removeSubrange(index..<(index + count)) 21 | } 22 | } 23 | 24 | /// Add `arg` to the end of the args. 25 | public mutating func append(_ arg: String) { 26 | args.append(arg) 27 | } 28 | 29 | /// Add multiple args to the end of the args. 30 | public mutating func append(_ args: [String]) { 31 | self.args.append(contentsOf: args) 32 | } 33 | } 34 | 35 | extension SwiftBuildCommand: CustomStringConvertible { 36 | public var description: String { 37 | args.joined(separator: " ") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/SwiftReload/Build/SwiftBuildManifest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Yams 3 | 4 | /// commands: 5 | /// "/Users/mac/code/PlayGradient/.build/arm64-apple-macosx/debug/CLib3.build/test3.cpp.o": 6 | /// tool: clang 7 | /// inputs: ["/Users/mac/code/PlayGradient/.build/arm64-apple-macosx/debug/Modules/SLib.swiftmodule","/Users/mac/code/PlayGradient/Sources/CLib3/test3.cpp"] 8 | /// outputs: ["/Users/mac/code/PlayGradient/.build/arm64-apple-macosx/debug/CLib3.build/test3.cpp.o"] 9 | /// description: "Compiling CLib3 test3.cpp" 10 | /// args: ["/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang","-fobjc-arc","-target","arm64-apple-macosx14.0","-O0","-DSWIFT_PACKAGE=1","-DDEBUG=1","-fblocks","-index-store-path","/Users/mac/code/PlayGradient/.build/arm64-apple-macosx/debug/index/store","-I","/Users/mac/code/PlayGradient/Sources/CLib3/include","-fmodule-map-file=/Users/mac/code/PlayGradient/.build/arm64-apple-macosx/debug/SLib.build/module.modulemap","-I","/Users/mac/code/PlayGradient/Sources/CLib2","-fmodule-map-file=/Users/mac/code/PlayGradient/Sources/CLib2/module.modulemap","-isysroot","/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.2.sdk","-F","/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks","-fPIC","-g","-g","-MD","-MT","dependencies","-MF","/Users/mac/code/PlayGradient/.build/arm64-apple-macosx/debug/CLib3.build/test3.cpp.d","-std=c++20","-c","/Users/mac/code/PlayGradient/Sources/CLib3/test3.cpp","-o","/Users/mac/code/PlayGradient/.build/arm64-apple-macosx/debug/CLib3.build/test3.cpp.o"] 11 | /// deps: "/Users/mac/code/PlayGradient/.build/arm64-apple-macosx/debug/CLib3.build/test3.cpp.d" 12 | struct BuildManifest: Codable { 13 | let commands: [String: Command] 14 | 15 | struct Command: Codable { 16 | let tool: String 17 | let inputs: [String] 18 | let outputs: [String] 19 | let description: String? 20 | let args: [String]? 21 | let deps: String? 22 | } 23 | } 24 | 25 | extension BuildManifest { 26 | static func load(from url: URL) throws -> BuildManifest { 27 | let data = try Data(contentsOf: url) 28 | return try YAMLDecoder().decode(BuildManifest.self, from: data) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/SwiftReload/LocalSwiftReloader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if os(Windows) 4 | import WinSDK 5 | #endif 6 | 7 | public class LocalSwiftReloader { 8 | public init(entryPoint: String = #filePath, onReload: @escaping () -> Void = {}) { 9 | self.onReload = onReload 10 | self.entryPoint = URL(fileURLWithPath: entryPoint) 11 | self.projectExtractor = SwiftPMProjectExtractor(entryPoint: self.entryPoint) 12 | self.sourceRoot = self.entryPoint.deletingLastPathComponent() 13 | self.projectRoot = projectExtractor.projectRoot 14 | self.buildRoot = projectRoot.appendingPathComponent(".build") 15 | self.patchBuildRoot = buildRoot.appendingPathComponent("patches") 16 | try? FileManager.default.removeItem(at: patchBuildRoot) 17 | initState() 18 | } 19 | 20 | /// The callback to run when a patch is applied. Note that there is no 21 | /// guarantee that the callback will be run on the main thread. 22 | public let onReload: () -> Void 23 | 24 | /// The entry point of the Swift project, usually the main Swift file. This 25 | /// is used to locate the project root. 26 | public let entryPoint: URL 27 | 28 | /// The root directory of the Swift source files, usually the directory 29 | /// containing the entry point. 30 | public let sourceRoot: URL 31 | 32 | /// The root directory of the Swift project. This is the directory 33 | /// containing the `Package.swift` file. 34 | public let projectRoot: URL 35 | 36 | /// The root of the build directory of the Swift project. This is usually 37 | /// the `.build` directory in the project root. 38 | public let buildRoot: URL 39 | 40 | /// The directory to store the generated patches. 41 | public let patchBuildRoot: URL 42 | 43 | private lazy var watcher: RecursiveFileWatcher = RecursiveFileWatcher( 44 | sourceRoot, 45 | filter: { $0.pathExtension == "swift" }, 46 | callback: onFileChange 47 | ) 48 | 49 | /// The project extractor used to extract build commands and various 50 | /// additional information from the Swift project. 51 | private let projectExtractor: ProjectExtractor 52 | 53 | private var patchID: Int { state.patchID } 54 | 55 | private let state = PatcherState() 56 | 57 | /// Load all source files into the patcher state for later diffing and 58 | /// patching. 59 | private func initState() { 60 | enumerateFiles(at: sourceRoot) { url in 61 | guard let content = try? String(contentsOf: url) else { 62 | return 63 | } 64 | state.loadFile(path: url, content: content) 65 | } 66 | print("🚀 Reloader loaded. Watching \(state.count) files") 67 | } 68 | 69 | private func onFileChange(_ files: [URL]) { 70 | for file in files { 71 | patchFile(file) 72 | } 73 | } 74 | 75 | /// Generate a patch for a file, compile the patch into a dylib, and load 76 | /// the patch dylib. 77 | private func patchFile(_ file: URL) { 78 | print("🛠️ Patching \(file.path)") 79 | 80 | guard let content = try? String(contentsOf: file) else { 81 | print("🛑 Failed to read file \(file.path)") 82 | return 83 | } 84 | guard let command = projectExtractor.findBuildCommand(for: file) else { 85 | print("🛑 Failed to find build command for \(file.path)") 86 | return 87 | } 88 | guard let moduleName = command.findModuleName() else { 89 | print("🛑 Failed to find module name for \(file.path)") 90 | return 91 | } 92 | guard 93 | let patched = state.updateAndPatch( 94 | path: file, 95 | content: content, 96 | moduleName: moduleName 97 | ) 98 | else { 99 | print("🛑 Failed to generate patch for \(file.path)") 100 | return 101 | } 102 | 103 | let filename = file.deletingPathExtension().lastPathComponent 104 | let patchFile = patchBuildRoot.appendingPathComponent("\(filename).patch_\(patchID).swift") 105 | // try? patched.write(to: patchFile, atomically: true, encoding: .utf8) 106 | try! FileManager.default.createDirectory( 107 | at: patchBuildRoot, 108 | withIntermediateDirectories: true 109 | ) 110 | FileManager.default.createFile(atPath: patchFile.path, contents: patched.data(using: .utf8)) 111 | print("🛠️ Patch generated at \(patchFile.path)") 112 | 113 | let outputFile = patchBuildRoot.appendingPathComponent("\(filename).patch_\(patchID).dylib") 114 | let patchedCommand = CommandPatcher.patchCommand( 115 | command, 116 | inputFile: patchFile, 117 | outputFile: outputFile, 118 | moduleName: moduleName, 119 | patchID: patchID 120 | ) 121 | print("🛠️ Compiling with '\(patchedCommand.args.first!)'") 122 | 123 | executeCommand(patchedCommand) 124 | 125 | // load the patch dylib 126 | do { 127 | try loadLibrary(path: outputFile.path) 128 | print("✅ Patch loaded successfully") 129 | onReload() 130 | } catch LoadLibraryError.win32Error(let code) { 131 | print("🛑 Failed to load \(outputFile.path): Win32 error \(code)") 132 | } catch LoadLibraryError.dlopenError(let error) { 133 | print("🛑 Failed to load \(outputFile.path): dlopen \(error)") 134 | } catch { 135 | print("🛑 Failed to load \(outputFile.path): \(error)") 136 | } 137 | } 138 | 139 | public func start() { 140 | watcher.start() 141 | } 142 | } 143 | 144 | /// Execute a Swift build command. 145 | private func executeCommand(_ command: SwiftBuildCommand) { 146 | let process = try! Process.run( 147 | URL(fileURLWithPath: command.args.first!), 148 | arguments: command.args.dropFirst().map { $0 } 149 | ) 150 | process.waitUntilExit() 151 | } 152 | 153 | enum LoadLibraryError: Error { 154 | case win32Error(Int) 155 | case dlopenError(String) 156 | } 157 | 158 | private func loadLibrary(path: String) throws { 159 | #if os(Windows) 160 | let result = path.withCString(encodedAs: UTF16.self) { LoadLibraryW($0) } 161 | guard result != nil else { 162 | throw LoadLibraryError.win32Error(Int(GetLastError())) 163 | } 164 | return 165 | #else 166 | let loadResult = dlopen(path, RTLD_NOW) 167 | if loadResult == nil { 168 | let error = String(cString: dlerror()) 169 | throw LoadLibraryError.dlopenError(error) 170 | } 171 | #endif 172 | } 173 | -------------------------------------------------------------------------------- /Sources/SwiftReload/Patcher/CommandPatcher.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct CommandPatcher { 4 | /// Transform a Swift build command to a patch command. 5 | static func patchCommand( 6 | _ command: SwiftBuildCommand, 7 | inputFile: URL, 8 | outputFile: URL, 9 | moduleName: String, 10 | patchID: Int 11 | ) -> SwiftBuildCommand { 12 | var patchCommand = command 13 | patchCommand.remove("-c", count: 2) 14 | patchCommand.remove("-module-name", count: 2) 15 | patchCommand.remove("-emit-module") 16 | patchCommand.remove("-emit-dependencies") 17 | patchCommand.remove("-emit-module-path", count: 2) 18 | patchCommand.remove("-module-cache-path", count: 2) 19 | patchCommand.remove("-output-file-map", count: 2) 20 | patchCommand.remove("-index-store-path", count: 2) 21 | patchCommand.remove("-package-name", count: 2) 22 | patchCommand.remove("-parseable-output") 23 | patchCommand.remove("-incremental") 24 | patchCommand.remove("-enable-batch-mode") 25 | // patchCommand.remove("-color-diagnostics") 26 | 27 | // patchCommand.append("-v") 28 | patchCommand.append(["-module-name", "\(moduleName)_patch_\(patchID)"]) 29 | patchCommand.append(["-c", inputFile.path]) 30 | patchCommand.append(["-o", outputFile.path]) 31 | patchCommand.append("-emit-library") 32 | patchCommand.append(["-Xfrontend", "-disable-access-control"]) 33 | patchCommand.append(["-Xlinker", "-flat_namespace"]) 34 | patchCommand.append(["-Xlinker", "-undefined"]) 35 | patchCommand.append(["-Xlinker", "dynamic_lookup"]) 36 | patchCommand.append(["-Xfrontend", "-enable-implicit-dynamic"]) 37 | patchCommand.append(["-enable-private-imports"]) 38 | 39 | return patchCommand 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/SwiftReload/Patcher/Patcher.swift: -------------------------------------------------------------------------------- 1 | import SwiftParser 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | 5 | class Pacher { 6 | public init(patchID: Int = 0) { 7 | self.patchID = patchID 8 | } 9 | 10 | /// The ID of the patch. Used to generate unique patch function names. 11 | public let patchID: Int 12 | 13 | /// Generate a patch file that contains the diff between the old and new syntax. 14 | public func patch(oldSyntax: SourceFileSyntax, newSyntax: SourceFileSyntax, moduleName: String) 15 | -> SourceFileSyntax 16 | { 17 | var result = SourceFileSyntax {} 18 | patchImport(oldSyntax, moduleName: moduleName, result: &result) 19 | 20 | let oldDeclarations = toDeclarations(file: oldSyntax) 21 | let newDeclarations = toDeclarations(file: newSyntax) 22 | let diffs = performDiff(oldDeclarations, newDeclarations) 23 | patchCode(diffs, result: &result) 24 | 25 | return result 26 | } 27 | 28 | /// Add nessessary import for the patch module. 29 | private func patchImport( 30 | _ oldSyntax: SourceFileSyntax, 31 | moduleName: String, 32 | result: inout SourceFileSyntax 33 | ) { 34 | let importDecl = ImportDeclSyntax( 35 | path: .init( 36 | [.init(name: .init(stringLiteral: moduleName))] 37 | ) 38 | ) 39 | result.statements.append(.init(item: .init(importDecl))) 40 | 41 | for stmt in oldSyntax.statements { 42 | if let importDecl = ImportDeclSyntax(stmt.item) { 43 | result.statements.append(.init(item: .init(importDecl))) 44 | } 45 | } 46 | } 47 | 48 | /// Generate code for diffed declarations. 49 | private func patchCode(_ diffs: [SyntaxDiff], result: inout SourceFileSyntax) { 50 | for diff in diffs { 51 | switch diff { 52 | case .added(let decl): 53 | result.statements.append(.init(item: .init(decl.syntax))) 54 | case .changed(_, let newDecl): 55 | var patched = patchFunction(newDecl.syntax as! FunctionDeclSyntax) 56 | if !newDecl.scope.isEmpty { 57 | var extensionScope = makeExtensionScope(newDecl.scope) 58 | extensionScope.memberBlock.members.append(.init(decl: patched)) 59 | patched = extensionScope 60 | } 61 | result.statements.append(.init(item: .init(patched))) 62 | case .removed(_): 63 | break 64 | } 65 | } 66 | } 67 | 68 | /// Add `_dynamicReplacement` attribute to the function. 69 | private func patchFunction(_ node: FunctionDeclSyntax) -> DeclSyntaxProtocol { 70 | var result = node 71 | 72 | /// Remove `override` modifier. 73 | result.modifiers = result.modifiers.filter { modifier in 74 | return modifier.name.text != "override" && modifier.name.text != "dynamic" 75 | } 76 | 77 | /// Add `_dynamicReplacement` attribute. 78 | let dynamicReplacement = AttributeSyntax( 79 | attributeName: "_dynamicReplacement" as TypeSyntax, 80 | leftParen: .leftParenToken(), 81 | arguments: .dynamicReplacementArguments( 82 | .init(declName: DeclReferenceExprSyntax(baseName: node.name)) 83 | ), 84 | rightParen: .rightParenToken() 85 | ) 86 | result.attributes.append(.attribute(dynamicReplacement)) 87 | 88 | /// Assign a random name to avoid conflict. 89 | result.name = getPatchName(node.name.text) 90 | 91 | return result 92 | } 93 | 94 | private func makeExtensionScope(_ scope: [String]) -> ExtensionDeclSyntax { 95 | return try! ExtensionDeclSyntax("extension \(raw: scope.joined(separator: ".")) {}") 96 | } 97 | 98 | private func getPatchName(_ originalName: String) -> TokenSyntax { 99 | return "\(raw: originalName)_patch_\(raw: patchID)" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/SwiftReload/Patcher/PatcherState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftParser 3 | import SwiftSyntax 4 | 5 | /// A stateful wrapper for the patcher that has caches intermidiate results for 6 | /// better performance. 7 | class PatcherState { 8 | public private(set) var patchID: Int = 0 9 | 10 | private var files = PatcherFiles() 11 | 12 | /// The number of files in the patcher state. 13 | var count: Int { files.count } 14 | 15 | /// Load a file into the patcher without patching it. 16 | func loadFile(path: URL, content: String) { 17 | files.update(path: path, content: content) 18 | } 19 | 20 | /// Update a file and generate a patch for it. 21 | func updateAndPatch(path: URL, content: String, moduleName: String) -> String? { 22 | let oldSyntax = files.getSyntax(for: path) 23 | files.update(path: path, content: content) 24 | let newSyntax = files.getSyntax(for: path)! 25 | let patch = Pacher(patchID: patchID).patch( 26 | oldSyntax: oldSyntax!, 27 | newSyntax: newSyntax, 28 | moduleName: moduleName 29 | ) 30 | patchID += 1 31 | return patch.formatted().description 32 | } 33 | } 34 | 35 | private class PatcherFiles { 36 | private var files: [URL: String] = [:] 37 | 38 | private var syntaxCache: [URL: SourceFileSyntax] = [:] 39 | 40 | var count: Int { files.count } 41 | 42 | func update(path: URL, content: String) { 43 | if let oldContent = files[path] { 44 | if oldContent != content { 45 | files[path] = content 46 | revokeSyntaxCache(for: path) 47 | } 48 | } else { 49 | files[path] = content 50 | } 51 | } 52 | 53 | func remove(path: URL) { 54 | files.removeValue(forKey: path) 55 | revokeSyntaxCache(for: path) 56 | } 57 | 58 | private func revokeSyntaxCache(for path: URL) { 59 | syntaxCache.removeValue(forKey: path) 60 | } 61 | 62 | func getSyntax(for path: URL) -> SourceFileSyntax? { 63 | if let syntax = syntaxCache[path] { 64 | return syntax 65 | } else { 66 | if let content = files[path] { 67 | let syntax = Parser.parse(source: content) 68 | syntaxCache[path] = syntax 69 | return syntax 70 | } else { 71 | return nil 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/SwiftReload/ProjectExtractor/ProjectExtractor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol ProjectExtractor { 4 | init(entryPoint: URL) 5 | 6 | var projectRoot: URL { get } 7 | 8 | func findBuildCommand(for path: URL) -> SwiftBuildCommand? 9 | } 10 | -------------------------------------------------------------------------------- /Sources/SwiftReload/ProjectExtractor/SwiftPMProjectExtractor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class SwiftPMProjectExtractor: ProjectExtractor { 4 | public required init(entryPoint: URL) { 5 | self.entryPoint = entryPoint 6 | } 7 | 8 | public let entryPoint: URL 9 | 10 | public lazy var projectRoot: URL = findProjectRoot(of: entryPoint)! 11 | 12 | private lazy var buildManifest: BuildManifest = loadBuildManifest(projectRoot: projectRoot) 13 | 14 | public func findBuildCommand(for file: URL) -> SwiftBuildCommand? { 15 | let path = file.fileSystemRepresentation 16 | for command in buildManifest.commands.values { 17 | if command.inputs.contains(path) && command.tool == "shell" { 18 | return SwiftBuildCommand(from: command.args!) 19 | } 20 | } 21 | return nil 22 | } 23 | } 24 | 25 | private func loadBuildManifest(projectRoot: URL) -> BuildManifest { 26 | let buildManifestURL = projectRoot.appendingPathComponent(".build/debug.yaml") 27 | return try! BuildManifest.load(from: buildManifestURL) 28 | } 29 | 30 | private func findProjectRoot(of swiftSource: URL) -> URL? { 31 | var current = swiftSource.deletingLastPathComponent() 32 | while current.path != "/" { 33 | let files = try! FileManager.default.contentsOfDirectory(atPath: current.path) 34 | if files.contains("Package.swift") { 35 | return current 36 | } 37 | current = current.deletingLastPathComponent() 38 | } 39 | return nil 40 | } 41 | 42 | extension URL { 43 | var fileSystemRepresentation: String { 44 | return withUnsafeFileSystemRepresentation { String(cString: $0!) } 45 | } 46 | } -------------------------------------------------------------------------------- /Sources/SwiftReload/SyntaxDiff/SyntaxDiff.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | 3 | /// Represents a declaration in the source code. 4 | public class Declaration { 5 | init( 6 | _ declaration: DeclSyntaxProtocol, 7 | name: String, 8 | scope: [String] = [], 9 | signature: String = "", 10 | code: String = "" 11 | ) { 12 | self.syntax = declaration 13 | self.name = name 14 | self.scope = scope 15 | self.signature = signature 16 | self.code = code 17 | } 18 | 19 | /// The name of the declaration. 20 | let name: String 21 | 22 | /// The signature of the declaration. Only available for functions. 23 | let signature: String 24 | 25 | /// The code of the declaration. Only available for functions. 26 | let code: String 27 | 28 | /// The scope of the declaration. Top level declarations have an empty 29 | /// scope. 30 | let scope: [String] 31 | 32 | /// The syntax node of the declaration. 33 | let syntax: DeclSyntaxProtocol 34 | } 35 | 36 | extension Declaration: CustomReflectable { 37 | public var customMirror: Mirror { 38 | Mirror( 39 | self, 40 | children: [ 41 | "declaration": type(of: syntax), 42 | "name": name, 43 | "scope": scope, 44 | "signature": signature, 45 | "code": code, 46 | ], 47 | displayStyle: .class 48 | ) 49 | } 50 | } 51 | 52 | /// Recursively extract all declarations from a source file. 53 | public func toDeclarations(file: SourceFileSyntax) -> [Declaration] { 54 | let builder = DeclarationNodeBuilder() 55 | return builder.build(file: file) 56 | } 57 | 58 | private class DeclarationNodeBuilder: SyntaxVisitor { 59 | init() { 60 | super.init(viewMode: .fixedUp) 61 | } 62 | 63 | var result = [Declaration]() 64 | var stack = [Declaration]() 65 | 66 | private var currentScope: [String] { 67 | stack.map(\.name) 68 | } 69 | 70 | private func push(_ node: Declaration) { 71 | result.append(node) 72 | stack.append(node) 73 | } 74 | 75 | private func pop() { 76 | stack.removeLast() 77 | } 78 | 79 | override func visit(_ decl: FunctionDeclSyntax) -> SyntaxVisitorContinueKind { 80 | let name = decl.name.text 81 | let signature = decl.signature.description 82 | let code = decl.body!.formatted().description 83 | let node = Declaration( 84 | decl, 85 | name: name, 86 | scope: currentScope, 87 | signature: signature, 88 | code: code 89 | ) 90 | push(node) 91 | return .skipChildren 92 | } 93 | 94 | override func visitPost(_ node: FunctionDeclSyntax) { 95 | pop() 96 | } 97 | 98 | override func visit(_ decl: ClassDeclSyntax) -> SyntaxVisitorContinueKind { 99 | let name = decl.name.text 100 | let node = Declaration(decl, name: name, scope: currentScope) 101 | push(node) 102 | return .visitChildren 103 | } 104 | 105 | override func visitPost(_ node: ClassDeclSyntax) { 106 | pop() 107 | } 108 | 109 | override func visit(_ decl: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind { 110 | return .skipChildren 111 | } 112 | 113 | func build(file: SourceFileSyntax) -> [Declaration] { 114 | result = [] 115 | stack = [] 116 | walk(file) 117 | return result 118 | } 119 | } 120 | 121 | /// Represents the different types of changes that can occur between two sets of 122 | /// declarations. 123 | public enum SyntaxDiff { 124 | /// A new declaration has been added. 125 | case added(Declaration) 126 | 127 | /// An existing declaration has been removed. 128 | case removed(Declaration) 129 | 130 | /// An existing declaration has been changed. 131 | case changed(Declaration, Declaration) 132 | } 133 | 134 | /// Perform a diff between two sets of declarations. 135 | public func performDiff(_ oldDeclarations: [Declaration], _ newDeclarations: [Declaration]) 136 | -> [SyntaxDiff] 137 | { 138 | var result = [SyntaxDiff]() 139 | 140 | for old in oldDeclarations { 141 | if let new = newDeclarations.first(where: { 142 | $0.scope == old.scope && $0.name == old.name && $0.signature == old.signature 143 | }) { 144 | if old.code != new.code { 145 | result.append(.changed(old, new)) 146 | } 147 | } else { 148 | result.append(.removed(old)) 149 | } 150 | } 151 | 152 | for new in newDeclarations { 153 | if !oldDeclarations.contains(where: { $0.name == new.name }) { 154 | result.append(.added(new)) 155 | } 156 | } 157 | 158 | return result 159 | } 160 | -------------------------------------------------------------------------------- /Sources/SwiftReload/Watcher/ByteString.swift: -------------------------------------------------------------------------------- 1 | /* 2 | This source file is part of the Swift.org open source project 3 | 4 | Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors 5 | Licensed under Apache License v2.0 with Runtime Library Exception 6 | 7 | See http://swift.org/LICENSE.txt for license information 8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors 9 | */ 10 | 11 | import Foundation 12 | 13 | /// A `ByteString` represents a sequence of bytes. 14 | /// 15 | /// This struct provides useful operations for working with buffers of 16 | /// bytes. Conceptually it is just a contiguous array of bytes (UInt8), but it 17 | /// contains methods and default behavior suitable for common operations done 18 | /// using bytes strings. 19 | /// 20 | /// This struct *is not* intended to be used for significant mutation of byte 21 | /// strings, we wish to retain the flexibility to micro-optimize the memory 22 | /// allocation of the storage (for example, by inlining the storage for small 23 | /// strings or and by eliminating wasted space in growable arrays). For 24 | /// construction of byte arrays, clients should use the `WritableByteStream` class 25 | /// and then convert to a `ByteString` when complete. 26 | public struct ByteString: ExpressibleByArrayLiteral, Hashable, Sendable { 27 | /// The buffer contents. 28 | @usableFromInline 29 | internal var _bytes: [UInt8] 30 | 31 | /// Create an empty byte string. 32 | @inlinable 33 | public init() { 34 | _bytes = [] 35 | } 36 | 37 | /// Create a byte string from a byte array literal. 38 | @inlinable 39 | public init(arrayLiteral contents: UInt8...) { 40 | _bytes = contents 41 | } 42 | 43 | /// Create a byte string from an array of bytes. 44 | @inlinable 45 | public init(_ contents: [UInt8]) { 46 | _bytes = contents 47 | } 48 | 49 | /// Create a byte string from an array slice. 50 | @inlinable 51 | public init(_ contents: ArraySlice) { 52 | _bytes = Array(contents) 53 | } 54 | 55 | /// Create a byte string from an byte buffer. 56 | @inlinable 57 | public init(_ contents: S) where S.Iterator.Element == UInt8 { 58 | _bytes = [UInt8](contents) 59 | } 60 | 61 | /// Create a byte string from the UTF8 encoding of a string. 62 | @inlinable 63 | public init(encodingAsUTF8 string: String) { 64 | _bytes = [UInt8](string.utf8) 65 | } 66 | 67 | /// Access the byte string contents as an array. 68 | @inlinable 69 | public var contents: [UInt8] { 70 | return _bytes 71 | } 72 | 73 | /// Return the byte string size. 74 | @inlinable 75 | public var count: Int { 76 | return _bytes.count 77 | } 78 | 79 | /// Gives a non-escaping closure temporary access to an immutable `Data` instance wrapping the `ByteString` without 80 | /// copying any memory around. 81 | /// 82 | /// - Parameters: 83 | /// - closure: The closure that will have access to a `Data` instance for the duration of its lifetime. 84 | @inlinable 85 | public func withData(_ closure: (Data) throws -> T) rethrows -> T { 86 | return try _bytes.withUnsafeBytes { pointer -> T in 87 | let mutatingPointer = UnsafeMutableRawPointer(mutating: pointer.baseAddress!) 88 | let data = Data(bytesNoCopy: mutatingPointer, count: pointer.count, deallocator: .none) 89 | return try closure(data) 90 | } 91 | } 92 | 93 | /// Returns a `String` lowercase hexadecimal representation of the contents of the `ByteString`. 94 | @inlinable 95 | public var hexadecimalRepresentation: String { 96 | _bytes.reduce("") { 97 | var str = String($1, radix: 16) 98 | // The above method does not do zero padding. 99 | if str.count == 1 { 100 | str = "0" + str 101 | } 102 | return $0 + str 103 | } 104 | } 105 | } 106 | 107 | /// Conform to CustomDebugStringConvertible. 108 | extension ByteString: CustomStringConvertible { 109 | /// Return the string decoded as a UTF8 sequence, or traps if not possible. 110 | public var description: String { 111 | return cString 112 | } 113 | 114 | /// Return the string decoded as a UTF8 sequence, if possible. 115 | @inlinable 116 | public var validDescription: String? { 117 | // FIXME: This is very inefficient, we need a way to pass a buffer. It 118 | // is also wrong if the string contains embedded '\0' characters. 119 | let tmp = _bytes + [UInt8(0)] 120 | return tmp.withUnsafeBufferPointer { ptr in 121 | return String(validatingUTF8: unsafeBitCast(ptr.baseAddress, to: UnsafePointer.self)) 122 | } 123 | } 124 | 125 | /// Return the string decoded as a UTF8 sequence, substituting replacement 126 | /// characters for ill-formed UTF8 sequences. 127 | @inlinable 128 | public var cString: String { 129 | return String(decoding: _bytes, as: Unicode.UTF8.self) 130 | } 131 | 132 | @available(*, deprecated, message: "use description or validDescription instead") 133 | public var asString: String? { 134 | return validDescription 135 | } 136 | } 137 | 138 | /// ByteStreamable conformance for a ByteString. 139 | extension ByteString: ByteStreamable { 140 | @inlinable 141 | public func write(to stream: WritableByteStream) { 142 | stream.write(_bytes) 143 | } 144 | } 145 | 146 | /// StringLiteralConvertable conformance for a ByteString. 147 | extension ByteString: ExpressibleByStringLiteral { 148 | public typealias UnicodeScalarLiteralType = StringLiteralType 149 | public typealias ExtendedGraphemeClusterLiteralType = StringLiteralType 150 | 151 | public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) { 152 | _bytes = [UInt8](value.utf8) 153 | } 154 | public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) { 155 | _bytes = [UInt8](value.utf8) 156 | } 157 | public init(stringLiteral value: StringLiteralType) { 158 | _bytes = [UInt8](value.utf8) 159 | } 160 | } -------------------------------------------------------------------------------- /Sources/SwiftReload/Watcher/Condition.swift: -------------------------------------------------------------------------------- 1 | /* 2 | This source file is part of the Swift.org open source project 3 | 4 | Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors 5 | Licensed under Apache License v2.0 with Runtime Library Exception 6 | 7 | See http://swift.org/LICENSE.txt for license information 8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors 9 | */ 10 | #if !_runtime(_ObjC) 11 | @preconcurrency import Foundation 12 | #else 13 | import Foundation 14 | #endif 15 | 16 | /// Simple wrapper around NSCondition. 17 | /// - SeeAlso: NSCondition 18 | public struct Condition { 19 | private let _condition = NSCondition() 20 | 21 | /// Create a new condition. 22 | public init() {} 23 | 24 | /// Wait for the condition to become available. 25 | public func wait() { 26 | _condition.wait() 27 | } 28 | 29 | /// Blocks the current thread until the condition is signaled or the specified time limit is reached. 30 | /// 31 | /// - Returns: true if the condition was signaled; otherwise, false if the time limit was reached. 32 | public func wait(until limit: Date) -> Bool { 33 | return _condition.wait(until: limit) 34 | } 35 | 36 | /// Signal the availability of the condition (awake one thread waiting on 37 | /// the condition). 38 | public func signal() { 39 | _condition.signal() 40 | } 41 | 42 | /// Broadcast the availability of the condition (awake all threads waiting 43 | /// on the condition). 44 | public func broadcast() { 45 | _condition.broadcast() 46 | } 47 | 48 | /// A helper method to execute the given body while condition is locked. 49 | /// - Note: Will ensure condition unlocks even if `body` throws. 50 | public func whileLocked(_ body: () throws -> T) rethrows -> T { 51 | _condition.lock() 52 | defer { _condition.unlock() } 53 | return try body() 54 | } 55 | } 56 | 57 | #if compiler(>=5.7) 58 | extension Condition: Sendable {} 59 | #endif -------------------------------------------------------------------------------- /Sources/SwiftReload/Watcher/FSWatch.swift: -------------------------------------------------------------------------------- 1 | /* 2 | This source file is part of the Swift.org open source project 3 | 4 | Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors 5 | Licensed under Apache License v2.0 with Runtime Library Exception 6 | 7 | See http://swift.org/LICENSE.txt for license information 8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors 9 | */ 10 | 11 | import Dispatch 12 | import Foundation 13 | 14 | #if os(Windows) 15 | import WinSDK 16 | #endif 17 | 18 | /// FSWatch is a cross-platform filesystem watching utility. 19 | public class FSWatch { 20 | 21 | public typealias EventReceivedBlock = (_ paths: [AbsolutePath]) -> Void 22 | 23 | /// Delegate for handling events from the underling watcher. 24 | fileprivate struct _WatcherDelegate { 25 | let block: EventReceivedBlock 26 | 27 | func pathsDidReceiveEvent(_ paths: [AbsolutePath]) { 28 | block(paths) 29 | } 30 | } 31 | 32 | /// The paths being watched. 33 | public let paths: [AbsolutePath] 34 | 35 | /// The underlying file watching utility. 36 | /// 37 | /// This is FSEventStream on macOS and inotify on linux. 38 | private var _watcher: _FileWatcher! 39 | 40 | /// The number of seconds the watcher should wait before passing the 41 | /// collected events to the clients. 42 | let latency: Double 43 | 44 | /// Create an instance with given paths. 45 | /// 46 | /// Paths can be files or directories. Directories are watched recursively. 47 | public init(paths: [AbsolutePath], latency: Double = 0.2, block: @escaping EventReceivedBlock) { 48 | precondition(!paths.isEmpty) 49 | self.paths = paths 50 | self.latency = latency 51 | 52 | #if os(OpenBSD) 53 | self._watcher = NoOpWatcher( 54 | paths: paths, 55 | latency: latency, 56 | delegate: _WatcherDelegate(block: block) 57 | ) 58 | #elseif os(Windows) 59 | self._watcher = RDCWatcher( 60 | paths: paths, 61 | latency: latency, 62 | delegate: _WatcherDelegate(block: block) 63 | ) 64 | #elseif canImport(Glibc) || canImport(Musl) 65 | var ipaths: [AbsolutePath: Inotify.WatchOptions] = [:] 66 | 67 | // FIXME: We need to recurse here. 68 | for path in paths { 69 | if localFileSystem.isDirectory(path) { 70 | ipaths[path] = .defaultDirectoryWatchOptions 71 | } else if localFileSystem.isFile(path) { 72 | ipaths[path] = .defaultFileWatchOptions 73 | // Watch files. 74 | } else { 75 | // FIXME: Report errors 76 | } 77 | } 78 | 79 | self._watcher = Inotify( 80 | paths: ipaths, 81 | latency: latency, 82 | delegate: _WatcherDelegate(block: block) 83 | ) 84 | #elseif os(macOS) 85 | self._watcher = FSEventStream( 86 | paths: paths, 87 | latency: latency, 88 | delegate: _WatcherDelegate(block: block) 89 | ) 90 | #else 91 | fatalError("Unsupported platform") 92 | #endif 93 | } 94 | 95 | /// Start watching the filesystem for events. 96 | /// 97 | /// This method should be called only once. 98 | public func start() throws { 99 | // FIXME: Write precondition to ensure its called only once. 100 | try _watcher.start() 101 | } 102 | 103 | /// Stop watching the filesystem. 104 | /// 105 | /// This method should be called after start() and the object should be thrown away. 106 | public func stop() { 107 | // FIXME: Write precondition to ensure its called after start() and once only. 108 | _watcher.stop() 109 | } 110 | } 111 | 112 | /// Protocol to which the different file watcher implementations should conform. 113 | private protocol _FileWatcher { 114 | func start() throws 115 | func stop() 116 | } 117 | 118 | #if os(OpenBSD) || (!os(macOS) && canImport(Darwin)) 119 | extension FSWatch._WatcherDelegate: NoOpWatcherDelegate {} 120 | extension NoOpWatcher: _FileWatcher {} 121 | #elseif os(Windows) 122 | extension FSWatch._WatcherDelegate: RDCWatcherDelegate {} 123 | extension RDCWatcher: _FileWatcher {} 124 | #elseif canImport(Glibc) || canImport(Musl) 125 | extension FSWatch._WatcherDelegate: InotifyDelegate {} 126 | extension Inotify: _FileWatcher {} 127 | #elseif os(macOS) 128 | extension FSWatch._WatcherDelegate: FSEventStreamDelegate {} 129 | extension FSEventStream: _FileWatcher {} 130 | #else 131 | #error("Implementation required") 132 | #endif 133 | 134 | // MARK:- inotify 135 | 136 | #if os(OpenBSD) || (!os(macOS) && canImport(Darwin)) 137 | 138 | public protocol NoOpWatcherDelegate { 139 | func pathsDidReceiveEvent(_ paths: [AbsolutePath]) 140 | } 141 | 142 | public final class NoOpWatcher { 143 | public init(paths: [AbsolutePath], latency: Double, delegate: NoOpWatcherDelegate? = nil) { 144 | } 145 | 146 | public func start() throws {} 147 | 148 | public func stop() {} 149 | } 150 | 151 | #elseif os(Windows) 152 | 153 | public protocol RDCWatcherDelegate { 154 | func pathsDidReceiveEvent(_ paths: [AbsolutePath]) 155 | } 156 | 157 | /// Bindings for `ReadDirectoryChangesW` C APIs. 158 | public final class RDCWatcher { 159 | class Watch { 160 | var hDirectory: HANDLE 161 | let path: String 162 | let originalPath: AbsolutePath 163 | var overlapped: OVERLAPPED 164 | var terminate: HANDLE 165 | var buffer: UnsafeMutableBufferPointer // buffer must be DWORD-aligned 166 | var thread: Thread? 167 | 168 | public init(directory handle: HANDLE, _ path: String, _ originalPath: AbsolutePath) { 169 | self.hDirectory = handle 170 | self.path = path 171 | self.originalPath = originalPath 172 | self.overlapped = OVERLAPPED() 173 | self.overlapped.hEvent = CreateEventW(nil, false, false, nil) 174 | self.terminate = CreateEventW(nil, true, false, nil) 175 | 176 | let EntrySize: Int = 177 | MemoryLayout.stride 178 | + (Int(MAX_PATH) * MemoryLayout.stride) 179 | self.buffer = 180 | UnsafeMutableBufferPointer.allocate( 181 | capacity: EntrySize * 4 / MemoryLayout.stride 182 | ) 183 | } 184 | 185 | deinit { 186 | SetEvent(self.terminate) 187 | CloseHandle(self.terminate) 188 | CloseHandle(self.overlapped.hEvent) 189 | CloseHandle(hDirectory) 190 | self.buffer.deallocate() 191 | } 192 | } 193 | 194 | /// The paths being watched. 195 | private let paths: [AbsolutePath] 196 | 197 | /// The settle period (in seconds). 198 | private let settle: Double 199 | 200 | /// The watcher delegate. 201 | private let delegate: RDCWatcherDelegate? 202 | 203 | private let watches: [Watch] 204 | private let queue: DispatchQueue = 205 | DispatchQueue(label: "org.swift.swiftpm.\(RDCWatcher.self).callback") 206 | 207 | public init(paths: [AbsolutePath], latency: Double, delegate: RDCWatcherDelegate? = nil) { 208 | self.paths = paths 209 | self.settle = latency 210 | self.delegate = delegate 211 | 212 | self.watches = paths.map { originalPath in 213 | originalPath.pathString.withCString(encodedAs: UTF16.self) { 214 | let dwDesiredAccess: DWORD = DWORD(FILE_LIST_DIRECTORY) 215 | let dwShareMode: DWORD = DWORD( 216 | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE 217 | ) 218 | let dwCreationDisposition: DWORD = DWORD(OPEN_EXISTING) 219 | let dwFlags: DWORD = DWORD(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED) 220 | 221 | let handle: HANDLE = 222 | CreateFileW( 223 | $0, 224 | dwDesiredAccess, 225 | dwShareMode, 226 | nil, 227 | dwCreationDisposition, 228 | dwFlags, 229 | nil 230 | ) 231 | assert(!(handle == INVALID_HANDLE_VALUE)) 232 | 233 | let dwSize: DWORD = GetFinalPathNameByHandleW(handle, nil, 0, 0) 234 | let path = withUnsafeTemporaryAllocation( 235 | of: WCHAR.self, 236 | capacity: Int(dwSize) + 1 237 | ) { 238 | let _ = GetFinalPathNameByHandleW( 239 | handle, 240 | $0.baseAddress, 241 | DWORD($0.count), 242 | 0 243 | ) 244 | return String(decodingCString: $0.baseAddress!, as: UTF16.self) 245 | } 246 | 247 | return Watch(directory: handle, path, originalPath) 248 | } 249 | } 250 | } 251 | 252 | public func start() throws { 253 | // TODO(compnerd) can we compress the threads to a single worker thread 254 | self.watches.forEach { watch in 255 | watch.thread = Thread { 256 | [delegate = self.delegate, queue = self.queue, weak watch] in 257 | guard let watch = watch else { return } 258 | 259 | while true { 260 | let dwNotifyFilter: DWORD = 261 | DWORD(FILE_NOTIFY_CHANGE_FILE_NAME) 262 | | DWORD(FILE_NOTIFY_CHANGE_DIR_NAME) 263 | | DWORD(FILE_NOTIFY_CHANGE_SIZE) 264 | | DWORD(FILE_NOTIFY_CHANGE_LAST_WRITE) 265 | | DWORD(FILE_NOTIFY_CHANGE_CREATION) 266 | var dwBytesReturned: DWORD = 0 267 | if !ReadDirectoryChangesW( 268 | watch.hDirectory, 269 | UnsafeMutableRawPointer(watch.buffer.baseAddress), 270 | DWORD(watch.buffer.count * MemoryLayout.stride), 271 | true, 272 | dwNotifyFilter, 273 | &dwBytesReturned, 274 | &watch.overlapped, 275 | nil 276 | ) { 277 | return 278 | } 279 | 280 | var handles: (HANDLE?, HANDLE?) = (watch.terminate, watch.overlapped.hEvent) 281 | switch WaitForMultipleObjects(2, &handles.0, false, INFINITE) { 282 | case WAIT_OBJECT_0 + 1: 283 | break 284 | case DWORD(WAIT_TIMEOUT): // Spurious Wakeup? 285 | continue 286 | case WAIT_FAILED, WAIT_OBJECT_0: // Terminate Request 287 | fallthrough 288 | default: 289 | CloseHandle(watch.hDirectory) 290 | watch.hDirectory = INVALID_HANDLE_VALUE 291 | return 292 | } 293 | 294 | if !GetOverlappedResult( 295 | watch.hDirectory, 296 | &watch.overlapped, 297 | &dwBytesReturned, 298 | false 299 | ) { 300 | queue.async { 301 | delegate?.pathsDidReceiveEvent([AbsolutePath(watch.path)]) 302 | } 303 | return 304 | } 305 | 306 | // There was a buffer underrun on the kernel side. We may 307 | // have lost events, please re-synchronize. 308 | if dwBytesReturned == 0 { 309 | return 310 | } 311 | 312 | var paths: [AbsolutePath] = [] 313 | watch.buffer.withMemoryRebound(to: FILE_NOTIFY_INFORMATION.self) { 314 | var pNotify: UnsafeMutablePointer = 315 | $0.baseAddress! 316 | while true { 317 | // FIXME(compnerd) do we care what type of event was received? 318 | let file: String = 319 | String( 320 | utf16CodeUnitsNoCopy: &pNotify.pointee.FileName, 321 | count: Int(pNotify.pointee.FileNameLength) 322 | / MemoryLayout.stride, 323 | freeWhenDone: false 324 | ) 325 | 326 | var url = URL(fileURLWithPath: watch.originalPath.pathString) 327 | url.appendPathComponent(file) 328 | paths.append(try! AbsolutePath(validating: url.path)) 329 | 330 | pNotify = 331 | (UnsafeMutableRawPointer(pNotify) 332 | + Int(pNotify.pointee.NextEntryOffset)) 333 | .assumingMemoryBound(to: FILE_NOTIFY_INFORMATION.self) 334 | 335 | if pNotify.pointee.NextEntryOffset == 0 { 336 | break 337 | } 338 | } 339 | } 340 | 341 | queue.async { 342 | delegate?.pathsDidReceiveEvent(paths) 343 | } 344 | } 345 | } 346 | watch.thread?.start() 347 | } 348 | } 349 | 350 | public func stop() { 351 | self.watches.forEach { 352 | SetEvent($0.terminate) 353 | $0.thread?.join() 354 | } 355 | } 356 | } 357 | 358 | #elseif canImport(Glibc) || canImport(Musl) 359 | 360 | /// The delegate for receiving inotify events. 361 | public protocol InotifyDelegate { 362 | func pathsDidReceiveEvent(_ paths: [AbsolutePath]) 363 | } 364 | 365 | /// Bindings for inotify C APIs. 366 | public final class Inotify { 367 | 368 | /// The errors encountered during inotify operations. 369 | public enum Error: Swift.Error { 370 | case invalidFD 371 | case failedToWatch(AbsolutePath) 372 | } 373 | 374 | /// The available options for a particular path. 375 | public struct WatchOptions: OptionSet { 376 | public let rawValue: Int32 377 | 378 | public init(rawValue: Int32) { 379 | self.rawValue = rawValue 380 | } 381 | 382 | // File/directory created in watched directory (e.g., open(2) 383 | // O_CREAT, mkdir(2), link(2), symlink(2), bind(2) on a UNIX 384 | // domain socket). 385 | public static let create = WatchOptions(rawValue: IN_CREATE) 386 | 387 | // File/directory deleted from watched directory. 388 | public static let delete = WatchOptions(rawValue: IN_DELETE) 389 | 390 | // Watched file/directory was itself deleted. (This event 391 | // also occurs if an object is moved to another filesystem, 392 | // since mv(1) in effect copies the file to the other 393 | // filesystem and then deletes it from the original filesys‐ 394 | // tem.) In addition, an IN_IGNORED event will subsequently 395 | // be generated for the watch descriptor. 396 | public static let deleteSelf = WatchOptions(rawValue: IN_DELETE_SELF) 397 | 398 | public static let move = WatchOptions(rawValue: IN_MOVE) 399 | 400 | /// Watched file/directory was itself moved. 401 | public static let moveSelf = WatchOptions(rawValue: IN_MOVE_SELF) 402 | 403 | /// File was modified (e.g., write(2), truncate(2)). 404 | public static let modify = WatchOptions(rawValue: IN_MODIFY) 405 | 406 | // File or directory was opened. 407 | public static let open = WatchOptions(rawValue: IN_OPEN) 408 | 409 | // Metadata changed—for example, permissions (e.g., 410 | // chmod(2)), timestamps (e.g., utimensat(2)), extended 411 | // attributes (setxattr(2)), link count (since Linux 2.6.25; 412 | // e.g., for the target of link(2) and for unlink(2)), and 413 | // user/group ID (e.g., chown(2)). 414 | public static let attrib = WatchOptions(rawValue: IN_ATTRIB) 415 | 416 | // File opened for writing was closed. 417 | public static let closeWrite = WatchOptions(rawValue: IN_CLOSE_WRITE) 418 | 419 | // File or directory not opened for writing was closed. 420 | public static let closeNoWrite = WatchOptions(rawValue: IN_CLOSE_NOWRITE) 421 | 422 | // File was accessed (e.g., read(2), execve(2)). 423 | public static let access = WatchOptions(rawValue: IN_ACCESS) 424 | 425 | /// The list of default options that can be used for watching files. 426 | public static let defaultFileWatchOptions: WatchOptions = [ 427 | .deleteSelf, .moveSelf, .modify, 428 | ] 429 | 430 | /// The list of default options that can be used for watching directories. 431 | public static let defaultDirectoryWatchOptions: WatchOptions = [ 432 | .create, .delete, .deleteSelf, .move, .moveSelf, 433 | ] 434 | 435 | /// List of all available events. 436 | public static let all: [WatchOptions] = [ 437 | .create, 438 | .delete, 439 | .deleteSelf, 440 | .move, 441 | .moveSelf, 442 | .modify, 443 | .open, 444 | .attrib, 445 | .closeWrite, 446 | .closeNoWrite, 447 | .access, 448 | ] 449 | } 450 | 451 | // Sizeof inotify_event + max len of filepath + 1 (for null char). 452 | private static let eventSize = MemoryLayout.size + Int(NAME_MAX) + 1 453 | 454 | /// The paths being watched. 455 | public let paths: [AbsolutePath: WatchOptions] 456 | 457 | /// The delegate. 458 | private let delegate: InotifyDelegate? 459 | 460 | /// The settle period (in seconds). 461 | public let settle: Double 462 | 463 | /// Internal properties. 464 | private var fd: Int32? 465 | 466 | /// The list of watched directories/files. 467 | private var wds: [Int32: AbsolutePath] = [:] 468 | 469 | /// The queue on which we read the events. 470 | private let readQueue = DispatchQueue(label: "org.swift.swiftpm.\(Inotify.self).read") 471 | 472 | /// Callback queue for the delegate. 473 | private let callbacksQueue = DispatchQueue( 474 | label: "org.swift.swiftpm.\(Inotify.self).callback" 475 | ) 476 | 477 | /// Condition for handling event reporting. 478 | private var reportCondition = Condition() 479 | 480 | // Should be read or written to using the report condition only. 481 | private var collectedEvents: [AbsolutePath] = [] 482 | 483 | // Should be read or written to using the report condition only. 484 | private var lastEventTime: Date? = nil 485 | 486 | // Should be read or written to using the report condition only. 487 | private var cancelled = false 488 | 489 | /// Pipe for waking up the read loop. 490 | private var cancellationPipe: [Int32] = [0, 0] 491 | 492 | /// Create a inotify instance. 493 | /// 494 | /// The paths are not watched recursively. 495 | public init( 496 | paths: [AbsolutePath: WatchOptions], 497 | latency: Double, 498 | delegate: InotifyDelegate? = nil 499 | ) { 500 | self.paths = paths 501 | self.delegate = delegate 502 | self.settle = latency 503 | } 504 | 505 | /// Start the watch operation. 506 | public func start() throws { 507 | 508 | // Create the file descriptor. 509 | let fd = inotify_init1(Int32(IN_NONBLOCK)) 510 | 511 | guard fd != -1 else { 512 | throw Error.invalidFD 513 | } 514 | self.fd = fd 515 | 516 | /// Add watch for each path. 517 | for (path, _) in paths { 518 | try listenDirectory(path) 519 | } 520 | 521 | // Start the report thread. 522 | startReportThread() 523 | 524 | readQueue.async { 525 | self.startRead() 526 | } 527 | } 528 | 529 | private func listenDirectory(_ directory: AbsolutePath) throws { 530 | // print("Listening to directory: \(directory)") 531 | 532 | guard localFileSystem.isDirectory(directory) else { 533 | throw Error.failedToWatch(directory) 534 | } 535 | 536 | let wd = inotify_add_watch( 537 | fd!, 538 | directory.pathString, 539 | UInt32(WatchOptions.defaultDirectoryWatchOptions.rawValue) 540 | ) 541 | guard wd != -1 else { 542 | throw Error.failedToWatch(directory) 543 | } 544 | self.wds[wd] = directory 545 | 546 | for entry in try localFileSystem.getDirectoryContents(directory) { 547 | let entryPath = directory.appending(component: entry) 548 | if localFileSystem.isDirectory(entryPath) { 549 | try listenDirectory(entryPath) 550 | } else if localFileSystem.isFile(entryPath) { 551 | try listenFile(entryPath) 552 | } 553 | } 554 | } 555 | 556 | private func listenFile(_ file: AbsolutePath) throws { 557 | // print("Listening to file: \(file)") 558 | 559 | guard localFileSystem.isFile(file) else { 560 | throw Error.failedToWatch(file) 561 | } 562 | 563 | let wd = inotify_add_watch( 564 | fd!, 565 | file.pathString, 566 | UInt32(WatchOptions.defaultFileWatchOptions.rawValue) 567 | ) 568 | guard wd != -1 else { 569 | throw Error.failedToWatch(file) 570 | } 571 | self.wds[wd] = file 572 | } 573 | 574 | /// End the watch operation. 575 | public func stop() { 576 | // FIXME: Write precondition to ensure this is called only once. 577 | guard let fd = fd else { 578 | assertionFailure("end called without a fd") 579 | return 580 | } 581 | 582 | // Shutdown the report thread. 583 | reportCondition.whileLocked { 584 | cancelled = true 585 | reportCondition.signal() 586 | } 587 | 588 | // Wakeup the read loop by writing on the cancellation pipe. 589 | let writtenData = write(cancellationPipe[1], "", 1) 590 | assert(writtenData == 1) 591 | 592 | // FIXME: We need to remove the watches. 593 | close(fd) 594 | } 595 | 596 | private func startRead() { 597 | guard let fd = fd else { 598 | fatalError("unexpected call to startRead without fd") 599 | } 600 | 601 | // Create a pipe that we can use to get notified when we're cancelled. 602 | let pipeRv = pipe(&cancellationPipe) 603 | // FIXME: We don't see pipe2 for some reason. 604 | let f = fcntl(cancellationPipe[0], F_SETFL, O_NONBLOCK) 605 | assert(f != -1) 606 | assert(pipeRv == 0) 607 | 608 | while true { 609 | // The read fd set. Contains the inotify and cancellation fd. 610 | var rfds = fd_set() 611 | FD_ZERO(&rfds) 612 | 613 | FD_SET(fd, &rfds) 614 | FD_SET(cancellationPipe[0], &rfds) 615 | 616 | let nfds = [fd, cancellationPipe[0]].reduce(0, max) + 1 617 | // num fds, read fds, write fds, except fds, timeout 618 | let selectRet = select(nfds, &rfds, nil, nil, nil) 619 | // FIXME: Check for int signal. 620 | assert(selectRet != -1) 621 | 622 | // Return if we're cancelled. 623 | if FD_ISSET(cancellationPipe[0], &rfds) { 624 | return 625 | } 626 | assert(FD_ISSET(fd, &rfds)) 627 | 628 | let buf = UnsafeMutablePointer.allocate(capacity: Inotify.eventSize) 629 | // FIXME: We need to free the buffer. 630 | 631 | let readLength = read(fd, buf, Inotify.eventSize) 632 | // FIXME: Check for int signal. 633 | 634 | // Consume events. 635 | var idx = 0 636 | while idx < readLength { 637 | let event = withUnsafePointer(to: &buf[idx]) { 638 | $0.withMemoryRebound(to: inotify_event.self, capacity: 1) { 639 | $0.pointee 640 | } 641 | } 642 | 643 | // Get the associated with the event. 644 | var path = wds[event.wd]! 645 | 646 | // FIXME: We need extract information from the event mask and 647 | // create a data structure. 648 | // FIXME: Do we need to detect and remove watch for directories 649 | // that are deleted? 650 | 651 | // Get the relative base name from the event if present. 652 | if event.len > 0 { 653 | // Get the basename of the file that had the event. 654 | let basename = String(cString: buf + idx + MemoryLayout.size) 655 | 656 | // Construct the full path. 657 | // FIXME: We should report this path separately. 658 | path = path.appending(component: basename) 659 | } 660 | 661 | // Signal the reporter. 662 | reportCondition.whileLocked { 663 | lastEventTime = Date() 664 | collectedEvents.append(path) 665 | reportCondition.signal() 666 | } 667 | 668 | // Add to watch list if it's newly created. 669 | if event.mask & UInt32(IN_CREATE) != 0 { 670 | if localFileSystem.isDirectory(path) { 671 | try? listenDirectory(path) 672 | } else if localFileSystem.isFile(path) { 673 | try? listenFile(path) 674 | } 675 | } 676 | 677 | idx += MemoryLayout.size + Int(event.len) 678 | } 679 | } 680 | } 681 | 682 | /// Spawns a thread that collects events and reports them after the settle period. 683 | private func startReportThread() { 684 | let thread = Thread { 685 | var endLoop = false 686 | while !endLoop { 687 | 688 | // Block until we timeout or get signalled. 689 | self.reportCondition.whileLocked { 690 | var performReport = false 691 | 692 | // Block until timeout expires or wait forever until we get some event. 693 | if let lastEventTime = self.lastEventTime { 694 | let timeout = lastEventTime + Double(self.settle) 695 | let timeLimitReached = !self.reportCondition.wait(until: timeout) 696 | 697 | if timeLimitReached { 698 | self.lastEventTime = nil 699 | performReport = true 700 | } 701 | } else { 702 | self.reportCondition.wait() 703 | } 704 | 705 | // If we're cancelled, just return. 706 | if self.cancelled { 707 | endLoop = true 708 | return 709 | } 710 | 711 | // Report the events if we're asked to. 712 | if performReport && !self.collectedEvents.isEmpty { 713 | let events = self.collectedEvents 714 | self.collectedEvents = [] 715 | self.callbacksQueue.async { 716 | self.report(events) 717 | } 718 | } 719 | } 720 | } 721 | } 722 | 723 | thread.start() 724 | } 725 | 726 | private func report(_ paths: [AbsolutePath]) { 727 | delegate?.pathsDidReceiveEvent(paths) 728 | } 729 | } 730 | 731 | // FIXME: Swift should provide shims for FD_ macros 732 | 733 | private func FD_ZERO(_ set: inout fd_set) { 734 | #if os(Android) || canImport(Musl) 735 | #if arch(arm) 736 | set.fds_bits = ( 737 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 738 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 739 | ) 740 | #else 741 | set.fds_bits = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 742 | #endif 743 | #else 744 | #if arch(arm) 745 | set.__fds_bits = ( 746 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 747 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 748 | ) 749 | #else 750 | set.__fds_bits = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 751 | #endif 752 | #endif 753 | } 754 | 755 | private func FD_SET(_ fd: Int32, _ set: inout fd_set) { 756 | let intOffset = Int(fd / 16) 757 | let bitOffset = Int(fd % 16) 758 | #if os(Android) || canImport(Musl) 759 | var fd_bits = set.fds_bits 760 | let mask: UInt = 1 << bitOffset 761 | #else 762 | var fd_bits = set.__fds_bits 763 | let mask = 1 << bitOffset 764 | #endif 765 | switch intOffset { 766 | case 0: fd_bits.0 = fd_bits.0 | mask 767 | case 1: fd_bits.1 = fd_bits.1 | mask 768 | case 2: fd_bits.2 = fd_bits.2 | mask 769 | case 3: fd_bits.3 = fd_bits.3 | mask 770 | case 4: fd_bits.4 = fd_bits.4 | mask 771 | case 5: fd_bits.5 = fd_bits.5 | mask 772 | case 6: fd_bits.6 = fd_bits.6 | mask 773 | case 7: fd_bits.7 = fd_bits.7 | mask 774 | case 8: fd_bits.8 = fd_bits.8 | mask 775 | case 9: fd_bits.9 = fd_bits.9 | mask 776 | case 10: fd_bits.10 = fd_bits.10 | mask 777 | case 11: fd_bits.11 = fd_bits.11 | mask 778 | case 12: fd_bits.12 = fd_bits.12 | mask 779 | case 13: fd_bits.13 = fd_bits.13 | mask 780 | case 14: fd_bits.14 = fd_bits.14 | mask 781 | case 15: fd_bits.15 = fd_bits.15 | mask 782 | #if arch(arm) 783 | case 16: fd_bits.16 = fd_bits.16 | mask 784 | case 17: fd_bits.17 = fd_bits.17 | mask 785 | case 18: fd_bits.18 = fd_bits.18 | mask 786 | case 19: fd_bits.19 = fd_bits.19 | mask 787 | case 20: fd_bits.20 = fd_bits.20 | mask 788 | case 21: fd_bits.21 = fd_bits.21 | mask 789 | case 22: fd_bits.22 = fd_bits.22 | mask 790 | case 23: fd_bits.23 = fd_bits.23 | mask 791 | case 24: fd_bits.24 = fd_bits.24 | mask 792 | case 25: fd_bits.25 = fd_bits.25 | mask 793 | case 26: fd_bits.26 = fd_bits.26 | mask 794 | case 27: fd_bits.27 = fd_bits.27 | mask 795 | case 28: fd_bits.28 = fd_bits.28 | mask 796 | case 29: fd_bits.29 = fd_bits.29 | mask 797 | case 30: fd_bits.30 = fd_bits.30 | mask 798 | case 31: fd_bits.31 = fd_bits.31 | mask 799 | #endif 800 | default: break 801 | } 802 | #if os(Android) || canImport(Musl) 803 | set.fds_bits = fd_bits 804 | #else 805 | set.__fds_bits = fd_bits 806 | #endif 807 | } 808 | 809 | private func FD_ISSET(_ fd: Int32, _ set: inout fd_set) -> Bool { 810 | let intOffset = Int(fd / 32) 811 | let bitOffset = Int(fd % 32) 812 | #if os(Android) || canImport(Musl) 813 | let fd_bits = set.fds_bits 814 | let mask: UInt = 1 << bitOffset 815 | #else 816 | let fd_bits = set.__fds_bits 817 | let mask = 1 << bitOffset 818 | #endif 819 | switch intOffset { 820 | case 0: return fd_bits.0 & mask != 0 821 | case 1: return fd_bits.1 & mask != 0 822 | case 2: return fd_bits.2 & mask != 0 823 | case 3: return fd_bits.3 & mask != 0 824 | case 4: return fd_bits.4 & mask != 0 825 | case 5: return fd_bits.5 & mask != 0 826 | case 6: return fd_bits.6 & mask != 0 827 | case 7: return fd_bits.7 & mask != 0 828 | case 8: return fd_bits.8 & mask != 0 829 | case 9: return fd_bits.9 & mask != 0 830 | case 10: return fd_bits.10 & mask != 0 831 | case 11: return fd_bits.11 & mask != 0 832 | case 12: return fd_bits.12 & mask != 0 833 | case 13: return fd_bits.13 & mask != 0 834 | case 14: return fd_bits.14 & mask != 0 835 | case 15: return fd_bits.15 & mask != 0 836 | #if arch(arm) 837 | case 16: return fd_bits.16 & mask != 0 838 | case 17: return fd_bits.17 & mask != 0 839 | case 18: return fd_bits.18 & mask != 0 840 | case 19: return fd_bits.19 & mask != 0 841 | case 20: return fd_bits.20 & mask != 0 842 | case 21: return fd_bits.21 & mask != 0 843 | case 22: return fd_bits.22 & mask != 0 844 | case 23: return fd_bits.23 & mask != 0 845 | case 24: return fd_bits.24 & mask != 0 846 | case 25: return fd_bits.25 & mask != 0 847 | case 26: return fd_bits.26 & mask != 0 848 | case 27: return fd_bits.27 & mask != 0 849 | case 28: return fd_bits.28 & mask != 0 850 | case 29: return fd_bits.29 & mask != 0 851 | case 30: return fd_bits.30 & mask != 0 852 | case 31: return fd_bits.31 & mask != 0 853 | #endif 854 | default: return false 855 | } 856 | } 857 | 858 | #endif 859 | 860 | // MARK:- FSEventStream 861 | 862 | #if os(macOS) 863 | 864 | private func callback( 865 | streamRef: ConstFSEventStreamRef, 866 | clientCallBackInfo: UnsafeMutableRawPointer?, 867 | numEvents: Int, 868 | eventPaths: UnsafeMutableRawPointer, 869 | eventFlags: UnsafePointer, 870 | eventIds: UnsafePointer 871 | ) { 872 | let eventStream = unsafeBitCast(clientCallBackInfo, to: FSEventStream.self) 873 | 874 | // We expect the paths to be reported in an NSArray because we requested CFTypes. 875 | let eventPaths = unsafeBitCast(eventPaths, to: NSArray.self) as? [String] ?? [] 876 | 877 | // Compute the set of paths that were changed. 878 | let paths = eventPaths.compactMap({ try? AbsolutePath(validating: $0) }) 879 | 880 | eventStream.callbacksQueue.async { 881 | eventStream.delegate.pathsDidReceiveEvent(paths) 882 | } 883 | } 884 | 885 | public protocol FSEventStreamDelegate { 886 | func pathsDidReceiveEvent(_ paths: [AbsolutePath]) 887 | } 888 | 889 | /// Wrapper for Darwin's FSEventStream API. 890 | public final class FSEventStream { 891 | 892 | /// The errors encountered during fs event watching. 893 | public enum Error: Swift.Error { 894 | case unknownError 895 | } 896 | 897 | /// Reference to the underlying event stream. 898 | /// 899 | /// This is var and implicitly unwrapped optional because 900 | /// we need to capture self for the context. 901 | private var stream: FSEventStreamRef! 902 | 903 | /// Reference to the handler that should be called. 904 | let delegate: FSEventStreamDelegate 905 | 906 | /// The thread on which the stream is running. 907 | private var thread: Foundation.Thread? 908 | 909 | /// The run loop attached to the stream. 910 | private var runLoop: CFRunLoop? 911 | 912 | /// Callback queue for the delegate. 913 | fileprivate let callbacksQueue = DispatchQueue( 914 | label: "org.swift.swiftpm.\(FSEventStream.self).callback" 915 | ) 916 | 917 | public init( 918 | paths: [AbsolutePath], 919 | latency: Double, 920 | delegate: FSEventStreamDelegate, 921 | flags: FSEventStreamCreateFlags = FSEventStreamCreateFlags( 922 | kFSEventStreamCreateFlagUseCFTypes 923 | ) 924 | ) { 925 | self.delegate = delegate 926 | 927 | // Create the context that needs to be passed to the callback. 928 | var callbackContext = FSEventStreamContext() 929 | callbackContext.info = unsafeBitCast(self, to: UnsafeMutableRawPointer.self) 930 | 931 | // Create the stream. 932 | self.stream = FSEventStreamCreate( 933 | nil, 934 | callback, 935 | &callbackContext, 936 | paths.map({ $0.pathString }) as CFArray, 937 | FSEventStreamEventId(kFSEventStreamEventIdSinceNow), 938 | latency, 939 | flags 940 | ) 941 | } 942 | 943 | // Start the runloop. 944 | public func start() throws { 945 | let thread = Foundation.Thread { [weak self] in 946 | guard let `self` = self else { return } 947 | self.runLoop = CFRunLoopGetCurrent() 948 | let queue = DispatchQueue(label: "org.swiftwasm.carton.FSWatch") 949 | queue.sync { 950 | // Schedule the run loop. 951 | FSEventStreamSetDispatchQueue(self.stream, queue) 952 | // Start the stream. 953 | FSEventStreamSetDispatchQueue(self.stream, queue) 954 | FSEventStreamStart(self.stream) 955 | } 956 | } 957 | thread.start() 958 | self.thread = thread 959 | } 960 | 961 | /// Stop watching the events. 962 | public func stop() { 963 | // FIXME: This is probably not thread safe? 964 | if let runLoop = self.runLoop { 965 | CFRunLoopStop(runLoop) 966 | } 967 | } 968 | } 969 | #endif 970 | -------------------------------------------------------------------------------- /Sources/SwiftReload/Watcher/FileInfo.swift: -------------------------------------------------------------------------------- 1 | /* 2 | This source file is part of the Swift.org open source project 3 | 4 | Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors 5 | Licensed under Apache License v2.0 with Runtime Library Exception 6 | 7 | See http://swift.org/LICENSE.txt for license information 8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors 9 | */ 10 | 11 | #if !_runtime(_ObjC) 12 | @preconcurrency import Foundation 13 | #else 14 | import Foundation 15 | #endif 16 | 17 | #if swift(<5.6) 18 | extension FileAttributeType: UnsafeSendable {} 19 | extension Date: UnsafeSendable {} 20 | #endif 21 | 22 | /// File system information for a particular file. 23 | public struct FileInfo: Equatable, Codable, Sendable { 24 | 25 | /// The device number. 26 | public let device: UInt64 27 | 28 | /// The inode number. 29 | public let inode: UInt64 30 | 31 | /// The size of the file. 32 | public let size: UInt64 33 | 34 | /// The modification time of the file. 35 | public let modTime: Date 36 | 37 | /// Kind of file system entity. 38 | public let posixPermissions: Int16 39 | 40 | /// Kind of file system entity. 41 | public let fileType: FileAttributeType 42 | 43 | public init(_ attrs: [FileAttributeKey: Any]) { 44 | let device = (attrs[.systemNumber] as? NSNumber)?.uint64Value 45 | assert(device != nil) 46 | self.device = device! 47 | 48 | let inode = attrs[.systemFileNumber] as? UInt64 49 | assert(inode != nil) 50 | self.inode = inode! 51 | 52 | let posixPermissions = (attrs[.posixPermissions] as? NSNumber)?.int16Value 53 | assert(posixPermissions != nil) 54 | self.posixPermissions = posixPermissions! 55 | 56 | let fileType = attrs[.type] as? FileAttributeType 57 | assert(fileType != nil) 58 | self.fileType = fileType! 59 | 60 | let size = attrs[.size] as? UInt64 61 | assert(size != nil) 62 | self.size = size! 63 | 64 | let modTime = attrs[.modificationDate] as? Date 65 | assert(modTime != nil) 66 | self.modTime = modTime! 67 | } 68 | } 69 | 70 | extension FileAttributeType: Codable {} -------------------------------------------------------------------------------- /Sources/SwiftReload/Watcher/FileSystem.swift: -------------------------------------------------------------------------------- 1 | /* 2 | This source file is part of the Swift.org open source project 3 | 4 | Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors 5 | Licensed under Apache License v2.0 with Runtime Library Exception 6 | 7 | See http://swift.org/LICENSE.txt for license information 8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors 9 | */ 10 | 11 | import Dispatch 12 | import Foundation 13 | 14 | public struct FileSystemError: Error, Equatable, Sendable { 15 | public enum Kind: Equatable, Sendable { 16 | /// Access to the path is denied. 17 | /// 18 | /// This is used when an operation cannot be completed because a component of 19 | /// the path cannot be accessed. 20 | /// 21 | /// Used in situations that correspond to the POSIX EACCES error code. 22 | case invalidAccess 23 | 24 | /// IO Error encoding 25 | /// 26 | /// This is used when an operation cannot be completed due to an otherwise 27 | /// unspecified IO error. 28 | case ioError(code: Int32) 29 | 30 | /// Is a directory 31 | /// 32 | /// This is used when an operation cannot be completed because a component 33 | /// of the path which was expected to be a file was not. 34 | /// 35 | /// Used in situations that correspond to the POSIX EISDIR error code. 36 | case isDirectory 37 | 38 | /// No such path exists. 39 | /// 40 | /// This is used when a path specified does not exist, but it was expected 41 | /// to. 42 | /// 43 | /// Used in situations that correspond to the POSIX ENOENT error code. 44 | case noEntry 45 | 46 | /// Not a directory 47 | /// 48 | /// This is used when an operation cannot be completed because a component 49 | /// of the path which was expected to be a directory was not. 50 | /// 51 | /// Used in situations that correspond to the POSIX ENOTDIR error code. 52 | case notDirectory 53 | 54 | /// Unsupported operation 55 | /// 56 | /// This is used when an operation is not supported by the concrete file 57 | /// system implementation. 58 | case unsupported 59 | 60 | /// An unspecific operating system error at a given path. 61 | case unknownOSError 62 | 63 | /// File or folder already exists at destination. 64 | /// 65 | /// This is thrown when copying or moving a file or directory but the destination 66 | /// path already contains a file or folder. 67 | case alreadyExistsAtDestination 68 | 69 | /// If an unspecified error occurs when trying to change directories. 70 | case couldNotChangeDirectory 71 | 72 | /// If a mismatch is detected in byte count when writing to a file. 73 | case mismatchedByteCount(expected: Int, actual: Int) 74 | } 75 | 76 | /// The kind of the error being raised. 77 | public let kind: Kind 78 | 79 | /// The absolute path to the file associated with the error, if available. 80 | public let path: AbsolutePath? 81 | 82 | public init(_ kind: Kind, _ path: AbsolutePath? = nil) { 83 | self.kind = kind 84 | self.path = path 85 | } 86 | } 87 | 88 | extension FileSystemError: CustomNSError { 89 | public var errorUserInfo: [String: Any] { 90 | return [NSLocalizedDescriptionKey: "\(self)"] 91 | } 92 | } 93 | 94 | extension FileSystemError { 95 | public init(errno: Int32, _ path: AbsolutePath) { 96 | switch errno { 97 | case EACCES: 98 | self.init(.invalidAccess, path) 99 | case EISDIR: 100 | self.init(.isDirectory, path) 101 | case ENOENT: 102 | self.init(.noEntry, path) 103 | case ENOTDIR: 104 | self.init(.notDirectory, path) 105 | case EEXIST: 106 | self.init(.alreadyExistsAtDestination, path) 107 | default: 108 | self.init(.ioError(code: errno), path) 109 | } 110 | } 111 | } 112 | 113 | /// Defines the file modes. 114 | public enum FileMode: Sendable { 115 | 116 | public enum Option: Int, Sendable { 117 | case recursive 118 | case onlyFiles 119 | } 120 | 121 | case userUnWritable 122 | case userWritable 123 | case executable 124 | 125 | public func setMode(_ originalMode: Int16) -> Int16 { 126 | switch self { 127 | case .userUnWritable: 128 | // r-x rwx rwx 129 | return originalMode & 0o577 130 | case .userWritable: 131 | // -w- --- --- 132 | return originalMode | 0o200 133 | case .executable: 134 | // --x --x --x 135 | return originalMode | 0o111 136 | } 137 | } 138 | } 139 | 140 | /// Extended file system attributes that can applied to a given file path. See also ``FileSystem/hasAttribute(_:_:)``. 141 | public enum FileSystemAttribute: RawRepresentable { 142 | #if canImport(Darwin) 143 | case quarantine 144 | #endif 145 | 146 | public init?(rawValue: String) { 147 | switch rawValue { 148 | #if canImport(Darwin) 149 | case "com.apple.quarantine": 150 | self = .quarantine 151 | #endif 152 | default: 153 | return nil 154 | } 155 | } 156 | 157 | public var rawValue: String { 158 | switch self { 159 | #if canImport(Darwin) 160 | case .quarantine: 161 | return "com.apple.quarantine" 162 | #endif 163 | } 164 | } 165 | } 166 | 167 | // FIXME: Design an asynchronous story? 168 | // 169 | /// Abstracted access to file system operations. 170 | /// 171 | /// This protocol is used to allow most of the codebase to interact with a 172 | /// natural filesystem interface, while still allowing clients to transparently 173 | /// substitute a virtual file system or redirect file system operations. 174 | /// 175 | /// - Note: All of these APIs are synchronous and can block. 176 | public protocol FileSystem: Sendable { 177 | /// Check whether the given path exists and is accessible. 178 | @_disfavoredOverload 179 | func exists(_ path: AbsolutePath, followSymlink: Bool) -> Bool 180 | 181 | /// Check whether the given path is accessible and a directory. 182 | func isDirectory(_ path: AbsolutePath) -> Bool 183 | 184 | /// Check whether the given path is accessible and a file. 185 | func isFile(_ path: AbsolutePath) -> Bool 186 | 187 | /// Check whether the given path is an accessible and executable file. 188 | func isExecutableFile(_ path: AbsolutePath) -> Bool 189 | 190 | /// Check whether the given path is accessible and is a symbolic link. 191 | func isSymlink(_ path: AbsolutePath) -> Bool 192 | 193 | /// Check whether the given path is accessible and readable. 194 | func isReadable(_ path: AbsolutePath) -> Bool 195 | 196 | /// Check whether the given path is accessible and writable. 197 | func isWritable(_ path: AbsolutePath) -> Bool 198 | 199 | /// Returns any known item replacement directories for a given path. These may be used by platform-specific 200 | /// libraries to handle atomic file system operations, such as deletion. 201 | func itemReplacementDirectories(for path: AbsolutePath) throws -> [AbsolutePath] 202 | 203 | @available(*, deprecated, message: "use `hasAttribute(_:_:)` instead") 204 | func hasQuarantineAttribute(_ path: AbsolutePath) -> Bool 205 | 206 | /// Returns `true` if a given path has an attribute with a given name applied when file system supports this 207 | /// attribute. Returns `false` if such attribute is not applied or it isn't supported. 208 | func hasAttribute(_ name: FileSystemAttribute, _ path: AbsolutePath) -> Bool 209 | 210 | // FIXME: Actual file system interfaces will allow more efficient access to 211 | // more data than just the name here. 212 | // 213 | /// Get the contents of the given directory, in an undefined order. 214 | func getDirectoryContents(_ path: AbsolutePath) throws -> [String] 215 | 216 | /// Get the current working directory (similar to `getcwd(3)`), which can be 217 | /// different for different (virtualized) implementations of a FileSystem. 218 | /// The current working directory can be empty if e.g. the directory became 219 | /// unavailable while the current process was still working in it. 220 | /// This follows the POSIX `getcwd(3)` semantics. 221 | @_disfavoredOverload 222 | var currentWorkingDirectory: AbsolutePath? { get } 223 | 224 | /// Change the current working directory. 225 | /// - Parameters: 226 | /// - path: The path to the directory to change the current working directory to. 227 | func changeCurrentWorkingDirectory(to path: AbsolutePath) throws 228 | 229 | /// Get the home directory of current user 230 | @_disfavoredOverload 231 | var homeDirectory: AbsolutePath { get throws } 232 | 233 | /// Get the caches directory of current user 234 | @_disfavoredOverload 235 | var cachesDirectory: AbsolutePath? { get } 236 | 237 | /// Get the temp directory 238 | @_disfavoredOverload 239 | var tempDirectory: AbsolutePath { get throws } 240 | 241 | /// Create the given directory. 242 | func createDirectory(_ path: AbsolutePath) throws 243 | 244 | /// Create the given directory. 245 | /// 246 | /// - recursive: If true, create missing parent directories if possible. 247 | func createDirectory(_ path: AbsolutePath, recursive: Bool) throws 248 | 249 | /// Creates a symbolic link of the source path at the target path 250 | /// - Parameters: 251 | /// - path: The path at which to create the link. 252 | /// - destination: The path to which the link points to. 253 | /// - relative: If `relative` is true, the symlink contents will be a relative path, otherwise it will be absolute. 254 | func createSymbolicLink( 255 | _ path: AbsolutePath, pointingAt destination: AbsolutePath, relative: Bool) throws 256 | 257 | // FIXME: This is obviously not a very efficient or flexible API. 258 | // 259 | /// Get the contents of a file. 260 | /// 261 | /// - Returns: The file contents as bytes, or nil if missing. 262 | func readFileContents(_ path: AbsolutePath) throws -> ByteString 263 | 264 | // FIXME: This is obviously not a very efficient or flexible API. 265 | // 266 | /// Write the contents of a file. 267 | func writeFileContents(_ path: AbsolutePath, bytes: ByteString) throws 268 | 269 | // FIXME: This is obviously not a very efficient or flexible API. 270 | // 271 | /// Write the contents of a file. 272 | func writeFileContents(_ path: AbsolutePath, bytes: ByteString, atomically: Bool) throws 273 | 274 | /// Recursively deletes the file system entity at `path`. 275 | /// 276 | /// If there is no file system entity at `path`, this function does nothing (in particular, this is not considered 277 | /// to be an error). 278 | func removeFileTree(_ path: AbsolutePath) throws 279 | 280 | /// Change file mode. 281 | func chmod(_ mode: FileMode, path: AbsolutePath, options: Set) throws 282 | 283 | /// Returns the file info of the given path. 284 | /// 285 | /// The method throws if the underlying stat call fails. 286 | func getFileInfo(_ path: AbsolutePath) throws -> FileInfo 287 | 288 | /// Copy a file or directory. 289 | func copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws 290 | 291 | /// Move a file or directory. 292 | func move(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws 293 | } 294 | 295 | /// Convenience implementations (default arguments aren't permitted in protocol 296 | /// methods). 297 | extension FileSystem { 298 | /// exists override with default value. 299 | @_disfavoredOverload 300 | public func exists(_ path: AbsolutePath) -> Bool { 301 | return exists(path, followSymlink: true) 302 | } 303 | 304 | /// Default implementation of createDirectory(_:) 305 | public func createDirectory(_ path: AbsolutePath) throws { 306 | try createDirectory(path, recursive: false) 307 | } 308 | 309 | // Change file mode. 310 | public func chmod(_ mode: FileMode, path: AbsolutePath) throws { 311 | try chmod(mode, path: path, options: []) 312 | } 313 | 314 | // Unless the file system type provides an override for this method, throw 315 | // if `atomically` is `true`, otherwise fall back to whatever implementation already exists. 316 | @_disfavoredOverload 317 | public func writeFileContents(_ path: AbsolutePath, bytes: ByteString, atomically: Bool) throws { 318 | guard !atomically else { 319 | throw FileSystemError(.unsupported, path) 320 | } 321 | try writeFileContents(path, bytes: bytes) 322 | } 323 | 324 | /// Write to a file from a stream producer. 325 | @_disfavoredOverload 326 | public func writeFileContents(_ path: AbsolutePath, body: (WritableByteStream) -> Void) throws { 327 | let contents = BufferedOutputByteStream() 328 | body(contents) 329 | try createDirectory(path.parentDirectory, recursive: true) 330 | try writeFileContents(path, bytes: contents.bytes) 331 | } 332 | 333 | public func getFileInfo(_ path: AbsolutePath) throws -> FileInfo { 334 | throw FileSystemError(.unsupported, path) 335 | } 336 | 337 | public func hasQuarantineAttribute(_ path: AbsolutePath) -> Bool { false } 338 | 339 | public func hasAttribute(_ name: FileSystemAttribute, _ path: AbsolutePath) -> Bool { 340 | #if canImport(Darwin) 341 | false 342 | #endif 343 | } 344 | 345 | public func itemReplacementDirectories(for path: AbsolutePath) throws -> [AbsolutePath] { [] } 346 | } 347 | 348 | /// Concrete FileSystem implementation which communicates with the local file system. 349 | private struct LocalFileSystem: FileSystem { 350 | func isExecutableFile(_ path: AbsolutePath) -> Bool { 351 | // Our semantics doesn't consider directories. 352 | return (self.isFile(path) || self.isSymlink(path)) 353 | && FileManager.default.isExecutableFile(atPath: path.pathString) 354 | } 355 | 356 | func exists(_ path: AbsolutePath, followSymlink: Bool) -> Bool { 357 | if followSymlink { 358 | return FileManager.default.fileExists(atPath: path.pathString) 359 | } 360 | return (try? FileManager.default.attributesOfItem(atPath: path.pathString)) != nil 361 | } 362 | 363 | func isDirectory(_ path: AbsolutePath) -> Bool { 364 | var isDirectory: ObjCBool = false 365 | let exists: Bool = FileManager.default.fileExists( 366 | atPath: path.pathString, isDirectory: &isDirectory) 367 | return exists && isDirectory.boolValue 368 | } 369 | 370 | func isFile(_ path: AbsolutePath) -> Bool { 371 | guard let path = try? resolveSymlinks(path) else { 372 | return false 373 | } 374 | let attrs = try? FileManager.default.attributesOfItem(atPath: path.pathString) 375 | return attrs?[.type] as? FileAttributeType == .typeRegular 376 | } 377 | 378 | func isSymlink(_ path: AbsolutePath) -> Bool { 379 | let url = NSURL(fileURLWithPath: path.pathString) 380 | // We are intentionally using `NSURL.resourceValues(forKeys:)` here since it improves performance on Darwin platforms. 381 | let result = try? url.resourceValues(forKeys: [.isSymbolicLinkKey]) 382 | return (result?[.isSymbolicLinkKey] as? Bool) == true 383 | } 384 | 385 | func isReadable(_ path: AbsolutePath) -> Bool { 386 | FileManager.default.isReadableFile(atPath: path.pathString) 387 | } 388 | 389 | func isWritable(_ path: AbsolutePath) -> Bool { 390 | FileManager.default.isWritableFile(atPath: path.pathString) 391 | } 392 | 393 | func getFileInfo(_ path: AbsolutePath) throws -> FileInfo { 394 | let attrs = try FileManager.default.attributesOfItem(atPath: path.pathString) 395 | return FileInfo(attrs) 396 | } 397 | 398 | func hasAttribute(_ name: FileSystemAttribute, _ path: AbsolutePath) -> Bool { 399 | #if canImport(Darwin) 400 | let bufLength = getxattr(path.pathString, name.rawValue, nil, 0, 0, 0) 401 | 402 | return bufLength > 0 403 | #endif 404 | } 405 | 406 | var currentWorkingDirectory: AbsolutePath? { 407 | let cwdStr = FileManager.default.currentDirectoryPath 408 | 409 | #if _runtime(_ObjC) 410 | // The ObjC runtime indicates that the underlying Foundation has ObjC 411 | // interoperability in which case the return type of 412 | // `fileSystemRepresentation` is different from the Swift implementation 413 | // of Foundation. 414 | return try? AbsolutePath(validating: cwdStr) 415 | #else 416 | let fsr: UnsafePointer = cwdStr.fileSystemRepresentation 417 | defer { fsr.deallocate() } 418 | 419 | return try? AbsolutePath(validating: String(cString: fsr)) 420 | #endif 421 | } 422 | 423 | func changeCurrentWorkingDirectory(to path: AbsolutePath) throws { 424 | guard isDirectory(path) else { 425 | throw FileSystemError(.notDirectory, path) 426 | } 427 | 428 | guard FileManager.default.changeCurrentDirectoryPath(path.pathString) else { 429 | throw FileSystemError(.couldNotChangeDirectory, path) 430 | } 431 | } 432 | 433 | var homeDirectory: AbsolutePath { 434 | get throws { 435 | return try AbsolutePath(validating: NSHomeDirectory()) 436 | } 437 | } 438 | 439 | var cachesDirectory: AbsolutePath? { 440 | return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first.flatMap { 441 | try? AbsolutePath(validating: $0.path) 442 | } 443 | } 444 | 445 | var tempDirectory: AbsolutePath { 446 | get throws { 447 | let override = 448 | ProcessEnv.block["TMPDIR"] ?? ProcessEnv.block["TEMP"] ?? ProcessEnv.block["TMP"] 449 | if let path = override.flatMap({ try? AbsolutePath(validating: $0) }) { 450 | return path 451 | } 452 | return try AbsolutePath(validating: NSTemporaryDirectory()) 453 | } 454 | } 455 | 456 | func getDirectoryContents(_ path: AbsolutePath) throws -> [String] { 457 | #if canImport(Darwin) 458 | return try FileManager.default.contentsOfDirectory(atPath: path.pathString) 459 | #else 460 | do { 461 | return try FileManager.default.contentsOfDirectory(atPath: path.pathString) 462 | } catch let error as NSError { 463 | // Fixup error from corelibs-foundation. 464 | if error.code == CocoaError.fileReadNoSuchFile.rawValue, 465 | !error.userInfo.keys.contains(NSLocalizedDescriptionKey) 466 | { 467 | var userInfo = error.userInfo 468 | userInfo[NSLocalizedDescriptionKey] = "The folder “\(path.basename)” doesn’t exist." 469 | throw NSError(domain: error.domain, code: error.code, userInfo: userInfo) 470 | } 471 | throw error 472 | } 473 | #endif 474 | } 475 | 476 | func createDirectory(_ path: AbsolutePath, recursive: Bool) throws { 477 | // Don't fail if path is already a directory. 478 | if isDirectory(path) { return } 479 | 480 | try FileManager.default.createDirectory( 481 | atPath: path.pathString, withIntermediateDirectories: recursive, attributes: [:]) 482 | } 483 | 484 | func createSymbolicLink( 485 | _ path: AbsolutePath, pointingAt destination: AbsolutePath, relative: Bool 486 | ) throws { 487 | let destString = 488 | relative ? destination.relative(to: path.parentDirectory).pathString : destination.pathString 489 | try FileManager.default.createSymbolicLink( 490 | atPath: path.pathString, withDestinationPath: destString) 491 | } 492 | 493 | func readFileContents(_ path: AbsolutePath) throws -> ByteString { 494 | // Open the file. 495 | guard let fp = fopen(path.pathString, "rb") else { 496 | throw FileSystemError(errno: errno, path) 497 | } 498 | defer { fclose(fp) } 499 | 500 | // Read the data one block at a time. 501 | let data = BufferedOutputByteStream() 502 | var tmpBuffer = [UInt8](repeating: 0, count: 1 << 12) 503 | while true { 504 | let n = fread(&tmpBuffer, 1, tmpBuffer.count, fp) 505 | if n < 0 { 506 | if errno == EINTR { continue } 507 | throw FileSystemError(.ioError(code: errno), path) 508 | } 509 | if n == 0 { 510 | let errno = ferror(fp) 511 | if errno != 0 { 512 | throw FileSystemError(.ioError(code: errno), path) 513 | } 514 | break 515 | } 516 | data.send(tmpBuffer[0..) throws { 567 | guard exists(path) else { return } 568 | func setMode(path: String) throws { 569 | let attrs = try FileManager.default.attributesOfItem(atPath: path) 570 | // Skip if only files should be changed. 571 | if options.contains(.onlyFiles) && attrs[.type] as? FileAttributeType != .typeRegular { 572 | return 573 | } 574 | 575 | // Compute the new mode for this file. 576 | let currentMode = attrs[.posixPermissions] as! Int16 577 | let newMode = mode.setMode(currentMode) 578 | guard newMode != currentMode else { return } 579 | try FileManager.default.setAttributes( 580 | [.posixPermissions: newMode], 581 | ofItemAtPath: path) 582 | } 583 | 584 | try setMode(path: path.pathString) 585 | guard isDirectory(path) else { return } 586 | 587 | guard 588 | let traverse = FileManager.default.enumerator( 589 | at: URL(fileURLWithPath: path.pathString), 590 | includingPropertiesForKeys: nil) 591 | else { 592 | throw FileSystemError(.noEntry, path) 593 | } 594 | 595 | if !options.contains(.recursive) { 596 | traverse.skipDescendants() 597 | } 598 | 599 | while let path = traverse.nextObject() { 600 | try setMode(path: (path as! URL).path) 601 | } 602 | } 603 | 604 | func copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws { 605 | guard exists(sourcePath) else { throw FileSystemError(.noEntry, sourcePath) } 606 | guard !exists(destinationPath) 607 | else { throw FileSystemError(.alreadyExistsAtDestination, destinationPath) } 608 | try FileManager.default.copyItem(at: sourcePath.asURL, to: destinationPath.asURL) 609 | } 610 | 611 | func move(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws { 612 | guard exists(sourcePath) else { throw FileSystemError(.noEntry, sourcePath) } 613 | guard !exists(destinationPath) 614 | else { throw FileSystemError(.alreadyExistsAtDestination, destinationPath) } 615 | try FileManager.default.moveItem(at: sourcePath.asURL, to: destinationPath.asURL) 616 | } 617 | 618 | func itemReplacementDirectories(for path: AbsolutePath) throws -> [AbsolutePath] { 619 | let result = try FileManager.default.url( 620 | for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: path.asURL, create: false 621 | ) 622 | let path = try AbsolutePath(validating: result.path) 623 | // Foundation returns a path that is unique every time, so we return both that path, as well as its parent. 624 | return [path, path.parentDirectory] 625 | } 626 | } 627 | 628 | private var _localFileSystem: FileSystem = LocalFileSystem() 629 | 630 | /// Public access to the local FS proxy. 631 | public var localFileSystem: FileSystem { 632 | get { 633 | return _localFileSystem 634 | } 635 | 636 | @available( 637 | *, deprecated, 638 | message: 639 | "This global should never be mutable and is supposed to be read-only. Deprecated in Apr 2023." 640 | ) 641 | set { 642 | _localFileSystem = newValue 643 | } 644 | } 645 | 646 | extension FileSystem { 647 | /// Print the filesystem tree of the given path. 648 | /// 649 | /// For debugging only. 650 | public func dumpTree(at path: AbsolutePath = .root) { 651 | print(".") 652 | do { 653 | try recurse(fs: self, path: path) 654 | } catch { 655 | print("\(error)") 656 | } 657 | } 658 | 659 | /// Write bytes to the path if the given contents are different. 660 | public func writeIfChanged(path: AbsolutePath, bytes: ByteString) throws { 661 | try createDirectory(path.parentDirectory, recursive: true) 662 | 663 | // Return if the contents are same. 664 | if isFile(path), try readFileContents(path) == bytes { 665 | return 666 | } 667 | 668 | try writeFileContents(path, bytes: bytes) 669 | } 670 | 671 | /// Helper method to recurse and print the tree. 672 | private func recurse(fs: FileSystem, path: AbsolutePath, prefix: String = "") throws { 673 | let contents = try fs.getDirectoryContents(path) 674 | 675 | for (idx, entry) in contents.enumerated() { 676 | let isLast = idx == contents.count - 1 677 | let line = prefix + (isLast ? "└── " : "├── ") + entry 678 | print(line) 679 | 680 | let entryPath = path.appending(component: entry) 681 | if fs.isDirectory(entryPath) { 682 | let childPrefix = prefix + (isLast ? " " : "│ ") 683 | try recurse(fs: fs, path: entryPath, prefix: String(childPrefix)) 684 | } 685 | } 686 | } 687 | } 688 | 689 | #if !os(Windows) 690 | extension dirent { 691 | /// Get the directory name. 692 | /// 693 | /// This returns nil if the name is not valid UTF8. 694 | public var name: String? { 695 | var d_name = self.d_name 696 | return withUnsafePointer(to: &d_name) { 697 | String(validatingUTF8: UnsafeRawPointer($0).assumingMemoryBound(to: CChar.self)) 698 | } 699 | } 700 | } 701 | #endif -------------------------------------------------------------------------------- /Sources/SwiftReload/Watcher/PathUtils.swift: -------------------------------------------------------------------------------- 1 | import protocol Foundation.CustomNSError 2 | import var Foundation.NSLocalizedDescriptionKey 3 | 4 | /* 5 | This source file is part of the Swift.org open source project 6 | 7 | Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors 8 | Licensed under Apache License v2.0 with Runtime Library Exception 9 | 10 | See http://swift.org/LICENSE.txt for license information 11 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors 12 | */ 13 | #if os(Windows) 14 | import Foundation 15 | import WinSDK 16 | #endif 17 | 18 | #if os(Windows) 19 | private typealias PathImpl = WindowsPath 20 | #else 21 | private typealias PathImpl = UNIXPath 22 | #endif 23 | 24 | /// Represents an absolute file system path, independently of what (or whether 25 | /// anything at all) exists at that path in the file system at any given time. 26 | /// An absolute path always starts with a `/` character, and holds a normalized 27 | /// string representation. This normalization is strictly syntactic, and does 28 | /// not access the file system in any way. 29 | /// 30 | /// The absolute path string is normalized by: 31 | /// - Collapsing `..` path components 32 | /// - Removing `.` path components 33 | /// - Removing any trailing path separator 34 | /// - Removing any redundant path separators 35 | /// 36 | /// This string manipulation may change the meaning of a path if any of the 37 | /// path components are symbolic links on disk. However, the file system is 38 | /// never accessed in any way when initializing an AbsolutePath. 39 | /// 40 | /// Note that `~` (home directory resolution) is *not* done as part of path 41 | /// normalization, because it is normally the responsibility of the shell and 42 | /// not the program being invoked (e.g. when invoking `cd ~`, it is the shell 43 | /// that evaluates the tilde; the `cd` command receives an absolute path). 44 | public struct AbsolutePath: Hashable, Sendable { 45 | /// Check if the given name is a valid individual path component. 46 | /// 47 | /// This only checks with regard to the semantics enforced by `AbsolutePath` 48 | /// and `RelativePath`; particular file systems may have their own 49 | /// additional requirements. 50 | static func isValidComponent(_ name: String) -> Bool { 51 | return PathImpl.isValidComponent(name) 52 | } 53 | 54 | /// Private implementation details, shared with the RelativePath struct. 55 | private let _impl: PathImpl 56 | 57 | /// Private initializer when the backing storage is known. 58 | private init(_ impl: PathImpl) { 59 | _impl = impl 60 | } 61 | 62 | /// Initializes an AbsolutePath from a string that may be either absolute 63 | /// or relative; if relative, `basePath` is used as the anchor; if absolute, 64 | /// it is used as is, and in this case `basePath` is ignored. 65 | public init(validating str: String, relativeTo basePath: AbsolutePath) throws { 66 | if PathImpl(string: str).isAbsolute { 67 | try self.init(validating: str) 68 | } else { 69 | #if os(Windows) 70 | assert(!basePath.pathString.isEmpty) 71 | guard !str.isEmpty else { 72 | self.init(basePath._impl) 73 | return 74 | } 75 | 76 | let base: UnsafePointer = 77 | basePath.pathString.fileSystemRepresentation 78 | defer { base.deallocate() } 79 | 80 | let path: UnsafePointer = str.fileSystemRepresentation 81 | defer { path.deallocate() } 82 | 83 | var pwszResult: PWSTR! 84 | _ = String(cString: base).withCString(encodedAs: UTF16.self) { pwszBase in 85 | String(cString: path).withCString(encodedAs: UTF16.self) { pwszPath in 86 | PathAllocCombine( 87 | pwszBase, 88 | pwszPath, 89 | ULONG(PATHCCH_ALLOW_LONG_PATHS.rawValue), 90 | &pwszResult 91 | ) 92 | } 93 | } 94 | defer { LocalFree(pwszResult) } 95 | 96 | self.init(String(decodingCString: pwszResult, as: UTF16.self)) 97 | #else 98 | try self.init(basePath, RelativePath(validating: str)) 99 | #endif 100 | } 101 | } 102 | 103 | /// Initializes the AbsolutePath by concatenating a relative path to an 104 | /// existing absolute path, and renormalizing if necessary. 105 | public init(_ absPath: AbsolutePath, _ relPath: RelativePath) { 106 | self.init(absPath._impl.appending(relativePath: relPath._impl)) 107 | } 108 | 109 | /// Convenience initializer that appends a string to a relative path. 110 | public init(_ absPath: AbsolutePath, validating relStr: String) throws { 111 | try self.init(absPath, RelativePath(validating: relStr)) 112 | } 113 | 114 | /// Initializes the AbsolutePath from `absStr`, which must be an absolute 115 | /// path (i.e. it must begin with a path separator; this initializer does 116 | /// not interpret leading `~` characters as home directory specifiers). 117 | /// The input string will be normalized if needed, as described in the 118 | /// documentation for AbsolutePath. 119 | public init(validating path: String) throws { 120 | try self.init(PathImpl(validatingAbsolutePath: path)) 121 | } 122 | 123 | /// Directory component. An absolute path always has a non-empty directory 124 | /// component (the directory component of the root path is the root itself). 125 | public var dirname: String { 126 | return _impl.dirname 127 | } 128 | 129 | /// Last path component (including the suffix, if any). it is never empty. 130 | public var basename: String { 131 | return _impl.basename 132 | } 133 | 134 | /// Returns the basename without the extension. 135 | public var basenameWithoutExt: String { 136 | if let ext = self.extension { 137 | return String(basename.dropLast(ext.count + 1)) 138 | } 139 | return basename 140 | } 141 | 142 | /// Suffix (including leading `.` character) if any. Note that a basename 143 | /// that starts with a `.` character is not considered a suffix, nor is a 144 | /// trailing `.` character. 145 | public var suffix: String? { 146 | return _impl.suffix 147 | } 148 | 149 | /// Extension of the give path's basename. This follow same rules as 150 | /// suffix except that it doesn't include leading `.` character. 151 | public var `extension`: String? { 152 | return _impl.extension 153 | } 154 | 155 | /// Absolute path of parent directory. This always returns a path, because 156 | /// every directory has a parent (the parent directory of the root directory 157 | /// is considered to be the root directory itself). 158 | public var parentDirectory: AbsolutePath { 159 | return AbsolutePath(_impl.parentDirectory) 160 | } 161 | 162 | /// True if the path is the root directory. 163 | public var isRoot: Bool { 164 | return _impl.isRoot 165 | } 166 | 167 | /// Returns the absolute path with the relative path applied. 168 | public func appending(_ subpath: RelativePath) -> AbsolutePath { 169 | return AbsolutePath(self, subpath) 170 | } 171 | 172 | /// Returns the absolute path with an additional literal component appended. 173 | /// 174 | /// This method accepts pseudo-path like '.' or '..', but should not contain "/". 175 | public func appending(component: String) -> AbsolutePath { 176 | return AbsolutePath(_impl.appending(component: component)) 177 | } 178 | 179 | /// Returns the absolute path with additional literal components appended. 180 | /// 181 | /// This method should only be used in cases where the input is guaranteed 182 | /// to be a valid path component (i.e., it cannot be empty, contain a path 183 | /// separator, or be a pseudo-path like '.' or '..'). 184 | public func appending(components names: [String]) -> AbsolutePath { 185 | // FIXME: This doesn't seem a particularly efficient way to do this. 186 | return names.reduce( 187 | self, 188 | { path, name in 189 | path.appending(component: name) 190 | } 191 | ) 192 | } 193 | 194 | public func appending(components names: String...) -> AbsolutePath { 195 | appending(components: names) 196 | } 197 | 198 | /// NOTE: We will most likely want to add other `appending()` methods, such 199 | /// as `appending(suffix:)`, and also perhaps `replacing()` methods, 200 | /// such as `replacing(suffix:)` or `replacing(basename:)` for some 201 | /// of the more common path operations. 202 | 203 | /// NOTE: We may want to consider adding operators such as `+` for appending 204 | /// a path component. 205 | 206 | /// NOTE: We will want to add a method to return the lowest common ancestor 207 | /// path. 208 | 209 | /// Root directory (whose string representation is just a path separator). 210 | public static let root = AbsolutePath(PathImpl.root) 211 | 212 | /// Normalized string representation (the normalization rules are described 213 | /// in the documentation of the initializer). This string is never empty. 214 | public var pathString: String { 215 | return _impl.string 216 | } 217 | 218 | /// Returns an array of strings that make up the path components of the 219 | /// absolute path. This is the same sequence of strings as the basenames 220 | /// of each successive path component, starting from the root. Therefore 221 | /// the first path component of an absolute path is always `/`. 222 | public var components: [String] { 223 | return _impl.components 224 | } 225 | } 226 | 227 | /// Represents a relative file system path. A relative path never starts with 228 | /// a `/` character, and holds a normalized string representation. As with 229 | /// AbsolutePath, the normalization is strictly syntactic, and does not access 230 | /// the file system in any way. 231 | /// 232 | /// The relative path string is normalized by: 233 | /// - Collapsing `..` path components that aren't at the beginning 234 | /// - Removing extraneous `.` path components 235 | /// - Removing any trailing path separator 236 | /// - Removing any redundant path separators 237 | /// - Replacing a completely empty path with a `.` 238 | /// 239 | /// This string manipulation may change the meaning of a path if any of the 240 | /// path components are symbolic links on disk. However, the file system is 241 | /// never accessed in any way when initializing a RelativePath. 242 | public struct RelativePath: Hashable, Sendable { 243 | /// Private implementation details, shared with the AbsolutePath struct. 244 | fileprivate let _impl: PathImpl 245 | 246 | /// Private initializer when the backing storage is known. 247 | private init(_ impl: PathImpl) { 248 | _impl = impl 249 | } 250 | 251 | /// Convenience initializer that verifies that the path is relative. 252 | public init(validating path: String) throws { 253 | try self.init(PathImpl(validatingRelativePath: path)) 254 | } 255 | 256 | /// Directory component. For a relative path without any path separators, 257 | /// this is the `.` string instead of the empty string. 258 | public var dirname: String { 259 | return _impl.dirname 260 | } 261 | 262 | /// Last path component (including the suffix, if any). It is never empty. 263 | public var basename: String { 264 | return _impl.basename 265 | } 266 | 267 | /// Returns the basename without the extension. 268 | public var basenameWithoutExt: String { 269 | if let ext = self.extension { 270 | return String(basename.dropLast(ext.count + 1)) 271 | } 272 | return basename 273 | } 274 | 275 | /// Suffix (including leading `.` character) if any. Note that a basename 276 | /// that starts with a `.` character is not considered a suffix, nor is a 277 | /// trailing `.` character. 278 | public var suffix: String? { 279 | return _impl.suffix 280 | } 281 | 282 | /// Extension of the give path's basename. This follow same rules as 283 | /// suffix except that it doesn't include leading `.` character. 284 | public var `extension`: String? { 285 | return _impl.extension 286 | } 287 | 288 | /// Normalized string representation (the normalization rules are described 289 | /// in the documentation of the initializer). This string is never empty. 290 | public var pathString: String { 291 | return _impl.string 292 | } 293 | 294 | /// Returns an array of strings that make up the path components of the 295 | /// relative path. This is the same sequence of strings as the basenames 296 | /// of each successive path component. Therefore the returned array of 297 | /// path components is never empty; even an empty path has a single path 298 | /// component: the `.` string. 299 | public var components: [String] { 300 | return _impl.components 301 | } 302 | 303 | /// Returns the relative path with the given relative path applied. 304 | public func appending(_ subpath: RelativePath) -> RelativePath { 305 | return RelativePath(_impl.appending(relativePath: subpath._impl)) 306 | } 307 | 308 | /// Returns the relative path with an additional literal component appended. 309 | /// 310 | /// This method accepts pseudo-path like '.' or '..', but should not contain "/". 311 | public func appending(component: String) -> RelativePath { 312 | return RelativePath(_impl.appending(component: component)) 313 | } 314 | 315 | /// Returns the relative path with additional literal components appended. 316 | /// 317 | /// This method should only be used in cases where the input is guaranteed 318 | /// to be a valid path component (i.e., it cannot be empty, contain a path 319 | /// separator, or be a pseudo-path like '.' or '..'). 320 | public func appending(components names: [String]) -> RelativePath { 321 | // FIXME: This doesn't seem a particularly efficient way to do this. 322 | return names.reduce( 323 | self, 324 | { path, name in 325 | path.appending(component: name) 326 | } 327 | ) 328 | } 329 | 330 | public func appending(components names: String...) -> RelativePath { 331 | appending(components: names) 332 | } 333 | } 334 | 335 | extension AbsolutePath: Codable { 336 | public func encode(to encoder: Encoder) throws { 337 | var container = encoder.singleValueContainer() 338 | try container.encode(pathString) 339 | } 340 | 341 | public init(from decoder: Decoder) throws { 342 | let container = try decoder.singleValueContainer() 343 | try self.init(validating: container.decode(String.self)) 344 | } 345 | } 346 | 347 | extension RelativePath: Codable { 348 | public func encode(to encoder: Encoder) throws { 349 | var container = encoder.singleValueContainer() 350 | try container.encode(pathString) 351 | } 352 | 353 | public init(from decoder: Decoder) throws { 354 | let container = try decoder.singleValueContainer() 355 | try self.init(validating: container.decode(String.self)) 356 | } 357 | } 358 | 359 | // Make absolute paths Comparable. 360 | extension AbsolutePath: Comparable { 361 | public static func < (lhs: AbsolutePath, rhs: AbsolutePath) -> Bool { 362 | return lhs.pathString < rhs.pathString 363 | } 364 | } 365 | 366 | /// Make absolute paths CustomStringConvertible and CustomDebugStringConvertible. 367 | extension AbsolutePath: CustomStringConvertible, CustomDebugStringConvertible { 368 | public var description: String { 369 | return pathString 370 | } 371 | 372 | public var debugDescription: String { 373 | // FIXME: We should really be escaping backslashes and quotes here. 374 | return "" 375 | } 376 | } 377 | 378 | /// Make relative paths CustomStringConvertible and CustomDebugStringConvertible. 379 | extension RelativePath: CustomStringConvertible { 380 | public var description: String { 381 | return _impl.string 382 | } 383 | 384 | public var debugDescription: String { 385 | // FIXME: We should really be escaping backslashes and quotes here. 386 | return "" 387 | } 388 | } 389 | 390 | /// Private implementation shared between AbsolutePath and RelativePath. 391 | protocol Path: Hashable { 392 | 393 | /// Root directory. 394 | static var root: Self { get } 395 | 396 | /// Checks if a string is a valid component. 397 | static func isValidComponent(_ name: String) -> Bool 398 | 399 | /// Normalized string of the (absolute or relative) path. Never empty. 400 | var string: String { get } 401 | 402 | /// Returns whether the path is the root path. 403 | var isRoot: Bool { get } 404 | 405 | /// Returns whether the path is an absolute path. 406 | var isAbsolute: Bool { get } 407 | 408 | /// Returns the directory part of the stored path (relying on the fact that it has been normalized). Returns a 409 | /// string consisting of just `.` if there is no directory part (which is the case if and only if there is no path 410 | /// separator). 411 | var dirname: String { get } 412 | 413 | /// Returns the last past component. 414 | var basename: String { get } 415 | 416 | /// Returns the components of the path between each path separator. 417 | var components: [String] { get } 418 | 419 | /// Path of parent directory. This always returns a path, because every directory has a parent (the parent 420 | /// directory of the root directory is considered to be the root directory itself). 421 | var parentDirectory: Self { get } 422 | 423 | /// Creates a path from its normalized string representation. 424 | init(string: String) 425 | 426 | /// Creates a path from a string representation, validates that it is a valid absolute path and normalizes it. 427 | init(validatingAbsolutePath: String) throws 428 | 429 | /// Creates a path from a string representation, validates that it is a valid relative path and normalizes it. 430 | init(validatingRelativePath: String) throws 431 | 432 | /// Returns suffix with leading `.` if withDot is true otherwise without it. 433 | func suffix(withDot: Bool) -> String? 434 | 435 | /// Returns a new Path by appending the path component. 436 | func appending(component: String) -> Self 437 | 438 | /// Returns a path by concatenating a relative path and renormalizing if necessary. 439 | func appending(relativePath: Self) -> Self 440 | } 441 | 442 | extension Path { 443 | var suffix: String? { 444 | return suffix(withDot: true) 445 | } 446 | 447 | var `extension`: String? { 448 | return suffix(withDot: false) 449 | } 450 | } 451 | 452 | #if os(Windows) 453 | private struct WindowsPath: Path, Sendable { 454 | let string: String 455 | 456 | // NOTE: this is *NOT* a root path. It is a drive-relative path that needs 457 | // to be specified due to assumptions in the APIs. Use the platform 458 | // specific path separator as we should be normalizing the path normally. 459 | // This is required to make the `InMemoryFileSystem` correctly iterate 460 | // paths. 461 | static let root = Self(string: "\\") 462 | 463 | static func isValidComponent(_ name: String) -> Bool { 464 | return name != "" && name != "." && name != ".." && !name.contains("/") 465 | } 466 | 467 | static func isAbsolutePath(_ path: String) -> Bool { 468 | return !path.withCString(encodedAs: UTF16.self, PathIsRelativeW) 469 | } 470 | 471 | var dirname: String { 472 | let fsr: UnsafePointer = self.string.fileSystemRepresentation 473 | defer { fsr.deallocate() } 474 | 475 | let path: String = String(cString: fsr) 476 | return path.withCString(encodedAs: UTF16.self) { 477 | let data = UnsafeMutablePointer(mutating: $0) 478 | PathCchRemoveFileSpec(data, path.count) 479 | return String(decodingCString: data, as: UTF16.self) 480 | } 481 | } 482 | 483 | var isAbsolute: Bool { 484 | return Self.isAbsolutePath(self.string) 485 | } 486 | 487 | public var isRoot: Bool { 488 | return self.string.withCString(encodedAs: UTF16.self, PathCchIsRoot) 489 | } 490 | 491 | var basename: String { 492 | let path: String = self.string 493 | return path.withCString(encodedAs: UTF16.self) { 494 | PathStripPathW(UnsafeMutablePointer(mutating: $0)) 495 | return String(decodingCString: $0, as: UTF16.self) 496 | } 497 | } 498 | 499 | // FIXME: We should investigate if it would be more efficient to instead 500 | // return a path component iterator that does all its work lazily, moving 501 | // from one path separator to the next on-demand. 502 | // 503 | var components: [String] { 504 | let normalized: UnsafePointer = string.fileSystemRepresentation 505 | defer { normalized.deallocate() } 506 | 507 | return String(cString: normalized).components(separatedBy: "\\").filter { !$0.isEmpty } 508 | } 509 | 510 | var parentDirectory: Self { 511 | return self == .root ? self : Self(string: dirname) 512 | } 513 | 514 | init(string: String) { 515 | if string.first?.isASCII ?? false, string.first?.isLetter ?? false, 516 | string.first?.isLowercase ?? false, 517 | string.count > 1, string[string.index(string.startIndex, offsetBy: 1)] == ":" 518 | { 519 | self.string = "\(string.first!.uppercased())\(string.dropFirst(1))" 520 | } else { 521 | self.string = string 522 | } 523 | } 524 | 525 | private static func repr(_ path: String) -> String { 526 | guard !path.isEmpty else { return "" } 527 | let representation: UnsafePointer = path.fileSystemRepresentation 528 | defer { representation.deallocate() } 529 | return String(cString: representation) 530 | } 531 | 532 | init(validatingAbsolutePath path: String) throws { 533 | let realpath = Self.repr(path) 534 | if !Self.isAbsolutePath(realpath) { 535 | throw PathValidationError.invalidAbsolutePath(path) 536 | } 537 | self.init(string: realpath) 538 | } 539 | 540 | init(validatingRelativePath path: String) throws { 541 | if path.isEmpty || path == "." { 542 | self.init(string: ".") 543 | } else { 544 | let realpath: String = Self.repr(path) 545 | // Treat a relative path as an invalid relative path... 546 | if Self.isAbsolutePath(realpath) || realpath.first == "\\" { 547 | throw PathValidationError.invalidRelativePath(path) 548 | } 549 | self.init(string: realpath) 550 | } 551 | } 552 | 553 | func suffix(withDot: Bool) -> String? { 554 | return self.string.withCString(encodedAs: UTF16.self) { 555 | if let pointer = PathFindExtensionW($0) { 556 | let substring = String(decodingCString: pointer, as: UTF16.self) 557 | guard substring.length > 0 else { return nil } 558 | return withDot ? substring : String(substring.dropFirst(1)) 559 | } 560 | return nil 561 | } 562 | } 563 | 564 | func appending(component name: String) -> Self { 565 | var result: PWSTR? 566 | _ = string.withCString(encodedAs: UTF16.self) { root in 567 | name.withCString(encodedAs: UTF16.self) { path in 568 | PathAllocCombine(root, path, ULONG(PATHCCH_ALLOW_LONG_PATHS.rawValue), &result) 569 | } 570 | } 571 | defer { LocalFree(result) } 572 | return Self(string: String(decodingCString: result!, as: UTF16.self)) 573 | } 574 | 575 | func appending(relativePath: Self) -> Self { 576 | var result: PWSTR? 577 | _ = string.withCString(encodedAs: UTF16.self) { root in 578 | relativePath.string.withCString(encodedAs: UTF16.self) { path in 579 | PathAllocCombine(root, path, ULONG(PATHCCH_ALLOW_LONG_PATHS.rawValue), &result) 580 | } 581 | } 582 | defer { LocalFree(result) } 583 | return Self(string: String(decodingCString: result!, as: UTF16.self)) 584 | } 585 | } 586 | #else 587 | private struct UNIXPath: Path, Sendable { 588 | let string: String 589 | 590 | static let root = Self(string: "/") 591 | 592 | static func isValidComponent(_ name: String) -> Bool { 593 | return name != "" && name != "." && name != ".." && !name.contains("/") 594 | } 595 | 596 | var dirname: String { 597 | // FIXME: This method seems too complicated; it should be simplified, 598 | // if possible, and certainly optimized (using UTF8View). 599 | // Find the last path separator. 600 | guard let idx = string.lastIndex(of: "/") else { 601 | // No path separators, so the directory name is `.`. 602 | return "." 603 | } 604 | // Check if it's the only one in the string. 605 | if idx == string.startIndex { 606 | // Just one path separator, so the directory name is `/`. 607 | return "/" 608 | } 609 | // Otherwise, it's the string up to (but not including) the last path 610 | // separator. 611 | return String(string.prefix(upTo: idx)) 612 | } 613 | 614 | var isAbsolute: Bool { 615 | return string.hasPrefix("/") 616 | } 617 | 618 | var isRoot: Bool { 619 | return self == Self.root 620 | } 621 | 622 | var basename: String { 623 | // FIXME: This method seems too complicated; it should be simplified, 624 | // if possible, and certainly optimized (using UTF8View). 625 | // Check for a special case of the root directory. 626 | if string.spm_only == "/" { 627 | // Root directory, so the basename is a single path separator (the 628 | // root directory is special in this regard). 629 | return "/" 630 | } 631 | // Find the last path separator. 632 | guard let idx = string.lastIndex(of: "/") else { 633 | // No path separators, so the basename is the whole string. 634 | return string 635 | } 636 | // Otherwise, it's the string from (but not including) the last path 637 | // separator. 638 | return String(string.suffix(from: string.index(after: idx))) 639 | } 640 | 641 | // FIXME: We should investigate if it would be more efficient to instead 642 | // return a path component iterator that does all its work lazily, moving 643 | // from one path separator to the next on-demand. 644 | // 645 | var components: [String] { 646 | // FIXME: This isn't particularly efficient; needs optimization, and 647 | // in fact, it might well be best to return a custom iterator so we 648 | // don't have to allocate everything up-front. It would be backed by 649 | // the path string and just return a slice at a time. 650 | let components = string.components(separatedBy: "/").filter({ !$0.isEmpty }) 651 | 652 | if string.hasPrefix("/") { 653 | return ["/"] + components 654 | } else { 655 | return components 656 | } 657 | } 658 | 659 | var parentDirectory: Self { 660 | return self == .root ? self : Self(string: dirname) 661 | } 662 | 663 | init(string: String) { 664 | self.string = string 665 | } 666 | 667 | init(normalizingAbsolutePath path: String) { 668 | precondition( 669 | path.first == "/", 670 | "Failure normalizing \(path), absolute paths should start with '/'" 671 | ) 672 | 673 | // At this point we expect to have a path separator as first character. 674 | assert(path.first == "/") 675 | // Fast path. 676 | if !mayNeedNormalization(absolute: path) { 677 | self.init(string: path) 678 | } 679 | 680 | // Split the character array into parts, folding components as we go. 681 | // As we do so, we count the number of characters we'll end up with in 682 | // the normalized string representation. 683 | var parts: [String] = [] 684 | var capacity = 0 685 | for part in path.split(separator: "/") { 686 | switch part.count { 687 | case 0: 688 | // Ignore empty path components. 689 | continue 690 | case 1 where part.first == ".": 691 | // Ignore `.` path components. 692 | continue 693 | case 2 where part.first == "." && part.last == ".": 694 | // If there's a previous part, drop it; otherwise, do nothing. 695 | if let prev = parts.last { 696 | parts.removeLast() 697 | capacity -= prev.count 698 | } 699 | default: 700 | // Any other component gets appended. 701 | parts.append(String(part)) 702 | capacity += part.count 703 | } 704 | } 705 | capacity += max(parts.count, 1) 706 | 707 | // Create an output buffer using the capacity we've calculated. 708 | // FIXME: Determine the most efficient way to reassemble a string. 709 | var result = "" 710 | result.reserveCapacity(capacity) 711 | 712 | // Put the normalized parts back together again. 713 | var iter = parts.makeIterator() 714 | result.append("/") 715 | if let first = iter.next() { 716 | result.append(contentsOf: first) 717 | while let next = iter.next() { 718 | result.append("/") 719 | result.append(contentsOf: next) 720 | } 721 | } 722 | 723 | // Sanity-check the result (including the capacity we reserved). 724 | assert(!result.isEmpty, "unexpected empty string") 725 | assert(result.count == capacity, "count: " + "\(result.count), cap: \(capacity)") 726 | 727 | // Use the result as our stored string. 728 | self.init(string: result) 729 | } 730 | 731 | init(normalizingRelativePath path: String) { 732 | precondition(path.first != "/") 733 | 734 | // FIXME: Here we should also keep track of whether anything actually has 735 | // to be changed in the string, and if not, just return the existing one. 736 | 737 | // Split the character array into parts, folding components as we go. 738 | // As we do so, we count the number of characters we'll end up with in 739 | // the normalized string representation. 740 | var parts: [String] = [] 741 | var capacity = 0 742 | for part in path.split(separator: "/") { 743 | switch part.count { 744 | case 0: 745 | // Ignore empty path components. 746 | continue 747 | case 1 where part.first == ".": 748 | // Ignore `.` path components. 749 | continue 750 | case 2 where part.first == "." && part.last == ".": 751 | // If at beginning, fall through to treat the `..` literally. 752 | guard let prev = parts.last else { 753 | fallthrough 754 | } 755 | // If previous component is anything other than `..`, drop it. 756 | if !(prev.count == 2 && prev.first == "." && prev.last == ".") { 757 | parts.removeLast() 758 | capacity -= prev.count 759 | continue 760 | } 761 | // Otherwise, fall through to treat the `..` literally. 762 | fallthrough 763 | default: 764 | // Any other component gets appended. 765 | parts.append(String(part)) 766 | capacity += part.count 767 | } 768 | } 769 | capacity += max(parts.count - 1, 0) 770 | 771 | // Create an output buffer using the capacity we've calculated. 772 | // FIXME: Determine the most efficient way to reassemble a string. 773 | var result = "" 774 | result.reserveCapacity(capacity) 775 | 776 | // Put the normalized parts back together again. 777 | var iter = parts.makeIterator() 778 | if let first = iter.next() { 779 | result.append(contentsOf: first) 780 | while let next = iter.next() { 781 | result.append("/") 782 | result.append(contentsOf: next) 783 | } 784 | } 785 | 786 | // Sanity-check the result (including the capacity we reserved). 787 | assert(result.count == capacity, "count: " + "\(result.count), cap: \(capacity)") 788 | 789 | // If the result is empty, return `.`, otherwise we return it as a string. 790 | self.init(string: result.isEmpty ? "." : result) 791 | } 792 | 793 | init(validatingAbsolutePath path: String) throws { 794 | switch path.first { 795 | case "/": 796 | self.init(normalizingAbsolutePath: path) 797 | case "~": 798 | throw PathValidationError.startsWithTilde(path) 799 | default: 800 | throw PathValidationError.invalidAbsolutePath(path) 801 | } 802 | } 803 | 804 | init(validatingRelativePath path: String) throws { 805 | switch path.first { 806 | case "/": 807 | throw PathValidationError.invalidRelativePath(path) 808 | default: 809 | self.init(normalizingRelativePath: path) 810 | } 811 | } 812 | 813 | func suffix(withDot: Bool) -> String? { 814 | // FIXME: This method seems too complicated; it should be simplified, 815 | // if possible, and certainly optimized (using UTF8View). 816 | // Find the last path separator, if any. 817 | let sIdx = string.lastIndex(of: "/") 818 | // Find the start of the basename. 819 | let bIdx = (sIdx != nil) ? string.index(after: sIdx!) : string.startIndex 820 | // Find the last `.` (if any), starting from the second character of 821 | // the basename (a leading `.` does not make the whole path component 822 | // a suffix). 823 | let fIdx = 824 | string.index(bIdx, offsetBy: 1, limitedBy: string.endIndex) ?? string.startIndex 825 | if let idx = string[fIdx...].lastIndex(of: ".") { 826 | // Unless it's just a `.` at the end, we have found a suffix. 827 | if string.distance(from: idx, to: string.endIndex) > 1 { 828 | let fromIndex = withDot ? idx : string.index(idx, offsetBy: 1) 829 | return String(string.suffix(from: fromIndex)) 830 | } else { 831 | return nil 832 | } 833 | } 834 | // If we get this far, there is no suffix. 835 | return nil 836 | } 837 | 838 | func appending(component name: String) -> Self { 839 | assert(!name.contains("/"), "\(name) is invalid path component") 840 | 841 | // Handle pseudo paths. 842 | switch name { 843 | case "", ".": 844 | return self 845 | case "..": 846 | return self.parentDirectory 847 | default: 848 | break 849 | } 850 | 851 | if self == Self.root { 852 | return Self(string: "/" + name) 853 | } else { 854 | return Self(string: string + "/" + name) 855 | } 856 | } 857 | 858 | func appending(relativePath: Self) -> Self { 859 | // Both paths are already normalized. The only case in which we have 860 | // to renormalize their concatenation is if the relative path starts 861 | // with a `..` path component. 862 | var newPathString = string 863 | if self != .root { 864 | newPathString.append("/") 865 | } 866 | 867 | let relativePathString = relativePath.string 868 | newPathString.append(relativePathString) 869 | 870 | // If the relative string starts with `.` or `..`, we need to normalize 871 | // the resulting string. 872 | // FIXME: We can actually optimize that case, since we know that the 873 | // normalization of a relative path can leave `..` path components at 874 | // the beginning of the path only. 875 | if relativePathString.hasPrefix(".") { 876 | if newPathString.hasPrefix("/") { 877 | return Self(normalizingAbsolutePath: newPathString) 878 | } else { 879 | return Self(normalizingRelativePath: newPathString) 880 | } 881 | } else { 882 | return Self(string: newPathString) 883 | } 884 | } 885 | } 886 | #endif 887 | 888 | /// Describes the way in which a path is invalid. 889 | public enum PathValidationError: Error { 890 | case startsWithTilde(String) 891 | case invalidAbsolutePath(String) 892 | case invalidRelativePath(String) 893 | } 894 | 895 | extension PathValidationError: CustomStringConvertible { 896 | public var description: String { 897 | switch self { 898 | case .startsWithTilde(let path): 899 | return "invalid absolute path '\(path)'; absolute path must begin with '/'" 900 | case .invalidAbsolutePath(let path): 901 | return "invalid absolute path '\(path)'" 902 | case .invalidRelativePath(let path): 903 | return 904 | "invalid relative path '\(path)'; relative path should not begin with '\(AbsolutePath.root.pathString)'" 905 | } 906 | } 907 | } 908 | 909 | extension AbsolutePath { 910 | /// Returns a relative path that, when concatenated to `base`, yields the 911 | /// callee path itself. If `base` is not an ancestor of the callee, the 912 | /// returned path will begin with one or more `..` path components. 913 | /// 914 | /// Because both paths are absolute, they always have a common ancestor 915 | /// (the root path, if nothing else). Therefore, any path can be made 916 | /// relative to any other path by using a sufficient number of `..` path 917 | /// components. 918 | /// 919 | /// This method is strictly syntactic and does not access the file system 920 | /// in any way. Therefore, it does not take symbolic links into account. 921 | public func relative(to base: AbsolutePath) -> RelativePath { 922 | let result: RelativePath 923 | // Split the two paths into their components. 924 | // FIXME: The is needs to be optimized to avoid unncessary copying. 925 | let pathComps = self.components 926 | let baseComps = base.components 927 | 928 | // It's common for the base to be an ancestor, so try that first. 929 | if pathComps.starts(with: baseComps) { 930 | // Special case, which is a plain path without `..` components. It 931 | // might be an empty path (when self and the base are equal). 932 | let relComps = pathComps.dropFirst(baseComps.count) 933 | #if os(Windows) 934 | let pathString = relComps.joined(separator: "\\") 935 | #else 936 | let pathString = relComps.joined(separator: "/") 937 | #endif 938 | do { 939 | result = try RelativePath(validating: pathString) 940 | } catch { 941 | preconditionFailure("invalid relative path computed from \(pathString)") 942 | } 943 | 944 | } else { 945 | // General case, in which we might well need `..` components to go 946 | // "up" before we can go "down" the directory tree. 947 | var newPathComps = ArraySlice(pathComps) 948 | var newBaseComps = ArraySlice(baseComps) 949 | while newPathComps.prefix(1) == newBaseComps.prefix(1) { 950 | // First component matches, so drop it. 951 | newPathComps = newPathComps.dropFirst() 952 | newBaseComps = newBaseComps.dropFirst() 953 | } 954 | // Now construct a path consisting of as many `..`s as are in the 955 | // `newBaseComps` followed by what remains in `newPathComps`. 956 | var relComps = Array(repeating: "..", count: newBaseComps.count) 957 | relComps.append(contentsOf: newPathComps) 958 | #if os(Windows) 959 | let pathString = relComps.joined(separator: "\\") 960 | #else 961 | let pathString = relComps.joined(separator: "/") 962 | #endif 963 | do { 964 | result = try RelativePath(validating: pathString) 965 | } catch { 966 | preconditionFailure("invalid relative path computed from \(pathString)") 967 | } 968 | } 969 | 970 | assert(AbsolutePath(base, result) == self) 971 | return result 972 | } 973 | 974 | /// Returns true if the path contains the given path. 975 | /// 976 | /// This method is strictly syntactic and does not access the file system 977 | /// in any way. 978 | @available(*, deprecated, renamed: "isDescendantOfOrEqual(to:)") 979 | public func contains(_ other: AbsolutePath) -> Bool { 980 | return isDescendantOfOrEqual(to: other) 981 | } 982 | 983 | /// Returns true if the path is an ancestor of the given path. 984 | /// 985 | /// This method is strictly syntactic and does not access the file system 986 | /// in any way. 987 | public func isAncestor(of descendant: AbsolutePath) -> Bool { 988 | return descendant.components.dropLast().starts(with: self.components) 989 | } 990 | 991 | /// Returns true if the path is an ancestor of or equal to the given path. 992 | /// 993 | /// This method is strictly syntactic and does not access the file system 994 | /// in any way. 995 | public func isAncestorOfOrEqual(to descendant: AbsolutePath) -> Bool { 996 | return descendant.components.starts(with: self.components) 997 | } 998 | 999 | /// Returns true if the path is a descendant of the given path. 1000 | /// 1001 | /// This method is strictly syntactic and does not access the file system 1002 | /// in any way. 1003 | public func isDescendant(of ancestor: AbsolutePath) -> Bool { 1004 | return self.components.dropLast().starts(with: ancestor.components) 1005 | } 1006 | 1007 | /// Returns true if the path is a descendant of or equal to the given path. 1008 | /// 1009 | /// This method is strictly syntactic and does not access the file system 1010 | /// in any way. 1011 | public func isDescendantOfOrEqual(to ancestor: AbsolutePath) -> Bool { 1012 | return self.components.starts(with: ancestor.components) 1013 | } 1014 | } 1015 | 1016 | extension PathValidationError: CustomNSError { 1017 | public var errorUserInfo: [String: Any] { 1018 | return [NSLocalizedDescriptionKey: self.description] 1019 | } 1020 | } 1021 | 1022 | // FIXME: We should consider whether to merge the two `normalize()` functions. 1023 | // The argument for doing so is that some of the code is repeated; the argument 1024 | // against doing so is that some of the details are different, and since any 1025 | // given path is either absolute or relative, it's wasteful to keep checking 1026 | // for whether it's relative or absolute. Possibly we can do both by clever 1027 | // use of generics that abstract away the differences. 1028 | 1029 | /// Fast check for if a string might need normalization. 1030 | /// 1031 | /// This assumes that paths containing dotfiles are rare: 1032 | private func mayNeedNormalization(absolute string: String) -> Bool { 1033 | var last = UInt8(ascii: "0") 1034 | for c in string.utf8 { 1035 | switch c { 1036 | case UInt8(ascii: "/") where last == UInt8(ascii: "/"): 1037 | return true 1038 | case UInt8(ascii: ".") where last == UInt8(ascii: "/"): 1039 | return true 1040 | default: 1041 | break 1042 | } 1043 | last = c 1044 | } 1045 | if last == UInt8(ascii: "/") { 1046 | return true 1047 | } 1048 | return false 1049 | } 1050 | 1051 | // MARK: - `AbsolutePath` backwards compatibility, delete after deprecation period. 1052 | 1053 | extension AbsolutePath { 1054 | @_disfavoredOverload 1055 | @available(*, deprecated, message: "use throwing `init(validating:)` variant instead") 1056 | public init(_ absStr: String) { 1057 | try! self.init(validating: absStr) 1058 | } 1059 | 1060 | @_disfavoredOverload 1061 | @available( 1062 | *, 1063 | deprecated, 1064 | message: "use throwing `init(validating:relativeTo:)` variant instead" 1065 | ) 1066 | public init(_ str: String, relativeTo basePath: AbsolutePath) { 1067 | try! self.init(validating: str, relativeTo: basePath) 1068 | } 1069 | 1070 | @_disfavoredOverload 1071 | @available(*, deprecated, message: "use throwing variant instead") 1072 | public init(_ absPath: AbsolutePath, _ relStr: String) { 1073 | try! self.init(absPath, validating: relStr) 1074 | } 1075 | } 1076 | 1077 | // MARK: - `AbsolutePath` backwards compatibility, delete after deprecation period. 1078 | 1079 | extension RelativePath { 1080 | @_disfavoredOverload 1081 | @available(*, deprecated, message: "use throwing variant instead") 1082 | public init(_ string: String) { 1083 | try! self.init(validating: string) 1084 | } 1085 | } 1086 | -------------------------------------------------------------------------------- /Sources/SwiftReload/Watcher/ProcessEnv.swift: -------------------------------------------------------------------------------- 1 | /* 2 | This source file is part of the Swift.org open source project 3 | 4 | Copyright (c) 2019 Apple Inc. and the Swift project authors 5 | Licensed under Apache License v2.0 with Runtime Library Exception 6 | 7 | See http://swift.org/LICENSE.txt for license information 8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors 9 | */ 10 | 11 | import Foundation 12 | 13 | public struct ProcessEnvironmentKey: CustomStringConvertible { 14 | public let value: String 15 | public init(_ value: String) { 16 | self.value = value 17 | } 18 | 19 | public var description: String { value } 20 | } 21 | 22 | extension ProcessEnvironmentKey: Encodable { 23 | public func encode(to encoder: any Encoder) throws { 24 | var container = encoder.singleValueContainer() 25 | try container.encode(self.value) 26 | } 27 | } 28 | 29 | extension ProcessEnvironmentKey: Decodable { 30 | public init(from decoder: any Decoder) throws { 31 | let container = try decoder.singleValueContainer() 32 | self.value = try container.decode(String.self) 33 | } 34 | } 35 | 36 | extension ProcessEnvironmentKey: Equatable { 37 | public static func == (_ lhs: Self, _ rhs: Self) -> Bool { 38 | #if os(Windows) 39 | // TODO: is this any faster than just doing a lowercased conversion and compare? 40 | return lhs.value.caseInsensitiveCompare(rhs.value) == .orderedSame 41 | #else 42 | return lhs.value == rhs.value 43 | #endif 44 | } 45 | } 46 | 47 | extension ProcessEnvironmentKey: ExpressibleByStringLiteral { 48 | public init(stringLiteral value: String) { 49 | self.init(value) 50 | } 51 | } 52 | 53 | extension ProcessEnvironmentKey: Hashable { 54 | public func hash(into hasher: inout Hasher) { 55 | #if os(Windows) 56 | self.value.lowercased().hash(into: &hasher) 57 | #else 58 | self.value.hash(into: &hasher) 59 | #endif 60 | } 61 | } 62 | 63 | extension ProcessEnvironmentKey: Sendable {} 64 | 65 | public typealias ProcessEnvironmentBlock = [ProcessEnvironmentKey: String] 66 | extension ProcessEnvironmentBlock { 67 | public init(_ dictionary: [String: String]) { 68 | self.init(uniqueKeysWithValues: dictionary.map { (ProcessEnvironmentKey($0.key), $0.value) }) 69 | } 70 | } 71 | 72 | extension ProcessEnvironmentBlock: Sendable {} 73 | 74 | /// Provides functionality related a process's environment. 75 | public enum ProcessEnv { 76 | 77 | @available(*, deprecated, message: "Use `block` instead") 78 | public static var vars: [String: String] { 79 | [String: String](uniqueKeysWithValues: _vars.map { ($0.key.value, $0.value) }) 80 | } 81 | 82 | /// Returns a dictionary containing the current environment. 83 | public static var block: ProcessEnvironmentBlock { _vars } 84 | 85 | private static var _vars = ProcessEnvironmentBlock( 86 | uniqueKeysWithValues: ProcessInfo.processInfo.environment.map { 87 | (ProcessEnvironmentBlock.Key($0.key), $0.value) 88 | } 89 | ) 90 | 91 | /// Invalidate the cached env. 92 | public static func invalidateEnv() { 93 | _vars = ProcessEnvironmentBlock( 94 | uniqueKeysWithValues: ProcessInfo.processInfo.environment.map { 95 | (ProcessEnvironmentKey($0.key), $0.value) 96 | } 97 | ) 98 | } 99 | 100 | /// `PATH` variable in the process's environment (`Path` under Windows). 101 | public static var path: String? { 102 | return block["PATH"] 103 | } 104 | 105 | /// The current working directory of the process. 106 | public static var cwd: AbsolutePath? { 107 | return localFileSystem.currentWorkingDirectory 108 | } 109 | } -------------------------------------------------------------------------------- /Sources/SwiftReload/Watcher/RecursiveWatcher.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A file watcher that watches a directory recursively with optional filtering. 4 | public class RecursiveFileWatcher { 5 | public init( 6 | _ directory: Foundation.URL, 7 | filter: @escaping (Foundation.URL) -> Bool = { _ in true }, 8 | callback: @escaping ([Foundation.URL]) -> Void 9 | ) { 10 | assert(directory.isFileURL) 11 | self.directory = directory 12 | self.filter = filter 13 | self.callback = callback 14 | } 15 | 16 | /// The directory to watch. 17 | public let directory: Foundation.URL 18 | 19 | /// The filter to select files to watch. Return true to watch a file. 20 | public let filter: (Foundation.URL) -> Bool 21 | 22 | /// Recording the modification time of files. Used to determine if a file 23 | /// has been changed. 24 | private var modifyTime = [Foundation.URL: Date]() 25 | 26 | /// The callback invoked when some files are changed. The argument is the 27 | /// list of changed files. There is no guarantee which thread the callback 28 | /// is called on. 29 | private let callback: ([Foundation.URL]) -> Void 30 | 31 | private lazy var watcher: FSWatch = FSWatch( 32 | paths: [try! AbsolutePath(validating: directory.path)], 33 | block: onFileChange 34 | ) 35 | 36 | /// Start watching the directory for file changes. 37 | public func start() { 38 | initModifyTime() 39 | try! watcher.start() 40 | } 41 | 42 | /// Populate the modify time records for all files in the directory. 43 | private func initModifyTime() { 44 | enumerateFilesFiltered(at: directory) { url in 45 | modifyTime[url] = getModifyTime(url) 46 | } 47 | } 48 | 49 | private func onFileChange(_ changedPath: [AbsolutePath]) { 50 | var changedFiles = [Foundation.URL]() 51 | 52 | for path in changedPath { 53 | let url = URL(fileURLWithPath: path.pathString) 54 | 55 | if !FileManager.default.fileExists(atPath: url.path) { 56 | // The file is deleted. 57 | modifyTime.removeValue(forKey: url) 58 | continue 59 | } 60 | 61 | if url.isDirectory { 62 | enumerateFilesFiltered(at: url) { url in 63 | let newModifyTime = getModifyTime(url) 64 | if let oldModifyTime = modifyTime[url], oldModifyTime != newModifyTime { 65 | changedFiles.append(url) 66 | modifyTime[url] = newModifyTime 67 | } 68 | } 69 | } else { 70 | let newModifyTime = getModifyTime(url) 71 | let oldModifyTime = modifyTime[url] 72 | if oldModifyTime != newModifyTime { 73 | changedFiles.append(url) 74 | modifyTime[url] = newModifyTime 75 | } 76 | } 77 | 78 | } 79 | 80 | callback(changedFiles) 81 | } 82 | 83 | /// Enumerate all files recursively in a directory that pass the filter. 84 | private func enumerateFilesFiltered( 85 | at directory: Foundation.URL, 86 | block: (Foundation.URL) -> Void 87 | ) { 88 | enumerateFiles(at: directory) { url in 89 | if filter(url) { 90 | block(url) 91 | } 92 | } 93 | } 94 | } 95 | 96 | /// Enumerate all files recursively in a directory. 97 | internal func enumerateFiles(at url: Foundation.URL, _ block: (Foundation.URL) -> Void) { 98 | let fm = FileManager.default 99 | let enumerator = fm.enumerator(at: url, includingPropertiesForKeys: nil) 100 | for case let url as Foundation.URL in enumerator! { 101 | block(url) 102 | } 103 | } 104 | 105 | /// Get the modification time of a file. 106 | private func getModifyTime(_ url: Foundation.URL) -> Date { 107 | let fm = FileManager.default 108 | let attributes = try! fm.attributesOfItem(atPath: url.path) 109 | return attributes[.modificationDate] as! Date 110 | } 111 | -------------------------------------------------------------------------------- /Sources/SwiftReload/Watcher/Thread.swift: -------------------------------------------------------------------------------- 1 | /* 2 | This source file is part of the Swift.org open source project 3 | 4 | Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors 5 | Licensed under Apache License v2.0 with Runtime Library Exception 6 | 7 | See http://swift.org/LICENSE.txt for license information 8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors 9 | */ 10 | 11 | import Foundation 12 | #if os(Windows) 13 | import WinSDK 14 | #endif 15 | 16 | /// This class bridges the gap between Darwin and Linux Foundation Threading API. 17 | /// It provides closure based execution and a join method to block the calling thread 18 | /// until the thread is finished executing. 19 | final public class Thread { 20 | 21 | /// The thread implementation which is Foundation.Thread on Linux and 22 | /// a Thread subclass which provides closure support on Darwin. 23 | private var thread: ThreadImpl! 24 | 25 | /// Condition variable to support blocking other threads using join when this thread has not finished executing. 26 | private var finishedCondition: Condition 27 | 28 | /// A boolean variable to track if this thread has finished executing its task. 29 | private var isFinished: Bool 30 | 31 | /// Creates an instance of thread class with closure to be executed when start() is called. 32 | public init(task: @escaping () -> Void) { 33 | isFinished = false 34 | finishedCondition = Condition() 35 | 36 | // Wrap the task with condition notifying any other threads blocked due to this thread. 37 | // Capture self weakly to avoid reference cycle. In case Thread is deinited before the task 38 | // runs, skip the use of finishedCondition. 39 | let theTask = { [weak self] in 40 | if let strongSelf = self { 41 | precondition(!strongSelf.isFinished) 42 | strongSelf.finishedCondition.whileLocked { 43 | task() 44 | strongSelf.isFinished = true 45 | strongSelf.finishedCondition.broadcast() 46 | } 47 | } else { 48 | // If the containing thread has been destroyed, we can ignore the finished condition and just run the 49 | // task. 50 | task() 51 | } 52 | } 53 | 54 | self.thread = ThreadImpl(block: theTask) 55 | } 56 | 57 | /// Starts the thread execution. 58 | public func start() { 59 | thread.start() 60 | } 61 | 62 | /// Blocks the calling thread until this thread is finished execution. 63 | public func join() { 64 | finishedCondition.whileLocked { 65 | while !isFinished { 66 | finishedCondition.wait() 67 | } 68 | } 69 | } 70 | 71 | /// Causes the calling thread to yield execution to another thread. 72 | public static func yield() { 73 | #if os(Windows) 74 | SwitchToThread() 75 | #else 76 | sched_yield() 77 | #endif 78 | } 79 | } 80 | 81 | #if canImport(Darwin) 82 | /// A helper subclass of Foundation's Thread with closure support. 83 | final private class ThreadImpl: Foundation.Thread { 84 | 85 | /// The task to be executed. 86 | private let task: () -> Void 87 | 88 | override func main() { 89 | task() 90 | } 91 | 92 | init(block task: @escaping () -> Void) { 93 | self.task = task 94 | } 95 | } 96 | #else 97 | // Thread on Linux supports closure so just use it directly. 98 | typealias ThreadImpl = Foundation.Thread 99 | #endif -------------------------------------------------------------------------------- /Sources/SwiftReload/Watcher/WatcherUtils.swift: -------------------------------------------------------------------------------- 1 | /* 2 | This source file is part of the Swift.org open source project 3 | 4 | Copyright (c) 2020 Apple Inc. and the Swift project authors 5 | Licensed under Apache License v2.0 with Runtime Library Exception 6 | 7 | See http://swift.org/LICENSE.txt for license information 8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors 9 | */ 10 | 11 | import Foundation 12 | 13 | #if os(Windows) 14 | import WinSDK 15 | #endif 16 | 17 | /// Closable entity is one that manages underlying resources and needs to be closed for cleanup 18 | /// The intent of this method is for the sole owner of the refernece/handle of the resource to close it completely, comapred to releasing a shared resource. 19 | public protocol Closable { 20 | func close() throws 21 | } 22 | 23 | extension URL { 24 | var isDirectory: Bool { 25 | (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true 26 | } 27 | } 28 | 29 | extension Collection { 30 | /// Returns the only element of the collection or nil. 31 | public var spm_only: Element? { 32 | return count == 1 ? self[startIndex] : nil 33 | } 34 | } 35 | 36 | extension AbsolutePath { 37 | /// File URL created from the normalized string representation of the path. 38 | public var asURL: Foundation.URL { 39 | return URL(fileURLWithPath: pathString) 40 | } 41 | } 42 | 43 | /// Returns the "real path" corresponding to `path` by resolving any symbolic links. 44 | public func resolveSymlinks(_ path: AbsolutePath) throws -> AbsolutePath { 45 | #if os(Windows) 46 | let handle: HANDLE = path.pathString.withCString(encodedAs: UTF16.self) { 47 | CreateFileW( 48 | $0, GENERIC_READ, DWORD(FILE_SHARE_READ), nil, 49 | DWORD(OPEN_EXISTING), DWORD(FILE_FLAG_BACKUP_SEMANTICS), nil) 50 | } 51 | if handle == INVALID_HANDLE_VALUE { return path } 52 | defer { CloseHandle(handle) } 53 | return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: 261) { 54 | let dwLength: DWORD = 55 | GetFinalPathNameByHandleW( 56 | handle, $0.baseAddress!, DWORD($0.count), 57 | DWORD(FILE_NAME_NORMALIZED)) 58 | let path = String(decodingCString: $0.baseAddress!, as: UTF16.self) 59 | return try AbsolutePath(path) 60 | } 61 | #else 62 | let pathStr = path.pathString 63 | 64 | // FIXME: We can't use FileManager's destinationOfSymbolicLink because 65 | // that implements readlink and not realpath. 66 | if let resultPtr = realpath(pathStr, nil) { 67 | let result = String(cString: resultPtr) 68 | // If `resolved_path` is specified as NULL, then `realpath` uses 69 | // malloc(3) to allocate a buffer [...]. The caller should deallocate 70 | // this buffer using free(3). 71 | // 72 | // String.init(cString:) creates a new string by copying the 73 | // null-terminated UTF-8 data referenced by the given pointer. 74 | resultPtr.deallocate() 75 | // FIXME: We should measure if it's really more efficient to compare the strings first. 76 | return result == pathStr ? path : try AbsolutePath(validating: result) 77 | } 78 | 79 | return path 80 | #endif 81 | } -------------------------------------------------------------------------------- /Sources/SwiftReload/Watcher/WritableByteStream.swift: -------------------------------------------------------------------------------- 1 | /* 2 | This source file is part of the Swift.org open source project 3 | 4 | Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors 5 | Licensed under Apache License v2.0 with Runtime Library Exception 6 | 7 | See http://swift.org/LICENSE.txt for license information 8 | See http://swift.org/CONTRIBUTORS.txt for Swift project authors 9 | */ 10 | 11 | import Dispatch 12 | import Foundation 13 | 14 | /// Convert an integer in 0..<16 to its hexadecimal ASCII character. 15 | private func hexdigit(_ value: UInt8) -> UInt8 { 16 | return value < 10 ? (0x30 + value) : (0x41 + value - 10) 17 | } 18 | 19 | /// Describes a type which can be written to a byte stream. 20 | public protocol ByteStreamable { 21 | func write(to stream: WritableByteStream) 22 | } 23 | 24 | /// An output byte stream. 25 | /// 26 | /// This protocol is designed to be able to support efficient streaming to 27 | /// different output destinations, e.g., a file or an in memory buffer. This is 28 | /// loosely modeled on LLVM's llvm::raw_ostream class. 29 | /// 30 | /// The stream is generally used in conjunction with the `appending` function. 31 | /// For example: 32 | /// 33 | /// let stream = BufferedOutputByteStream() 34 | /// stream.appending("Hello, world!") 35 | /// 36 | /// would write the UTF8 encoding of "Hello, world!" to the stream. 37 | /// 38 | /// The stream accepts a number of custom formatting operators which are defined 39 | /// in the `Format` struct (used for namespacing purposes). For example: 40 | /// 41 | /// let items = ["hello", "world"] 42 | /// stream.appending(Format.asSeparatedList(items, separator: " ")) 43 | /// 44 | /// would write each item in the list to the stream, separating them with a 45 | /// space. 46 | public protocol WritableByteStream: AnyObject, TextOutputStream, Closable { 47 | /// The current offset within the output stream. 48 | var position: Int { get } 49 | 50 | /// Write an individual byte to the buffer. 51 | func write(_ byte: UInt8) 52 | 53 | /// Write a collection of bytes to the buffer. 54 | func write(_ bytes: C) where C.Element == UInt8 55 | 56 | /// Flush the stream's buffer. 57 | func flush() 58 | } 59 | 60 | // Default noop implementation of close to avoid source-breaking downstream dependents with the addition of the close 61 | // API. 62 | extension WritableByteStream { 63 | public func close() throws {} 64 | } 65 | 66 | // Public alias to the old name to not introduce API compatibility. 67 | public typealias OutputByteStream = WritableByteStream 68 | 69 | #if os(Android) || canImport(Musl) 70 | public typealias FILEPointer = OpaquePointer 71 | #else 72 | public typealias FILEPointer = UnsafeMutablePointer 73 | #endif 74 | 75 | extension WritableByteStream { 76 | /// Write a sequence of bytes to the buffer. 77 | public func write(sequence: S) where S.Iterator.Element == UInt8 { 78 | // Iterate the sequence and append byte by byte since sequence's append 79 | // is not performant anyway. 80 | for byte in sequence { 81 | write(byte) 82 | } 83 | } 84 | 85 | /// Write a string to the buffer (as UTF8). 86 | public func write(_ string: String) { 87 | // FIXME(performance): Use `string.utf8._copyContents(initializing:)`. 88 | write(string.utf8) 89 | } 90 | 91 | /// Write a string (as UTF8) to the buffer, with escaping appropriate for 92 | /// embedding within a JSON document. 93 | /// 94 | /// - Note: This writes the literal data applying JSON string escaping, but 95 | /// does not write any other characters (like the quotes that would surround 96 | /// a JSON string). 97 | public func writeJSONEscaped(_ string: String) { 98 | // See RFC7159 for reference: https://tools.ietf.org/html/rfc7159 99 | for character in string.utf8 { 100 | // Handle string escapes; we use constants here to directly match the RFC. 101 | switch character { 102 | // Literal characters. 103 | case 0x20...0x21, 0x23...0x5B, 0x5D...0xFF: 104 | write(character) 105 | 106 | // Single-character escaped characters. 107 | case 0x22: // '"' 108 | write(0x5C) // '\' 109 | write(0x22) // '"' 110 | case 0x5C: // '\\' 111 | write(0x5C) // '\' 112 | write(0x5C) // '\' 113 | case 0x08: // '\b' 114 | write(0x5C) // '\' 115 | write(0x62) // 'b' 116 | case 0x0C: // '\f' 117 | write(0x5C) // '\' 118 | write(0x66) // 'b' 119 | case 0x0A: // '\n' 120 | write(0x5C) // '\' 121 | write(0x6E) // 'n' 122 | case 0x0D: // '\r' 123 | write(0x5C) // '\' 124 | write(0x72) // 'r' 125 | case 0x09: // '\t' 126 | write(0x5C) // '\' 127 | write(0x74) // 't' 128 | 129 | // Multi-character escaped characters. 130 | default: 131 | write(0x5C) // '\' 132 | write(0x75) // 'u' 133 | write(hexdigit(0)) 134 | write(hexdigit(0)) 135 | write(hexdigit(character >> 4)) 136 | write(hexdigit(character & 0xF)) 137 | } 138 | } 139 | } 140 | 141 | // MARK: helpers that return `self` 142 | 143 | // FIXME: This override shouldn't be necesary but removing it causes a 30% performance regression. This problem is 144 | // tracked by the following bug: https://bugs.swift.org/browse/SR-8535 145 | @discardableResult 146 | public func send(_ value: ArraySlice) -> WritableByteStream { 147 | value.write(to: self) 148 | return self 149 | } 150 | 151 | @discardableResult 152 | public func send(_ value: ByteStreamable) -> WritableByteStream { 153 | value.write(to: self) 154 | return self 155 | } 156 | 157 | @discardableResult 158 | public func send(_ value: CustomStringConvertible) -> WritableByteStream { 159 | value.description.write(to: self) 160 | return self 161 | } 162 | 163 | @discardableResult 164 | public func send(_ value: ByteStreamable & CustomStringConvertible) -> WritableByteStream { 165 | value.write(to: self) 166 | return self 167 | } 168 | } 169 | 170 | /// The `WritableByteStream` base class. 171 | /// 172 | /// This class provides a base and efficient implementation of the `WritableByteStream` 173 | /// protocol. It can not be used as is-as subclasses as several functions need to be 174 | /// implemented in subclasses. 175 | public class _WritableByteStreamBase: WritableByteStream { 176 | /// If buffering is enabled 177 | @usableFromInline let _buffered: Bool 178 | 179 | /// The data buffer. 180 | /// - Note: Minimum Buffer size should be one. 181 | @usableFromInline var _buffer: [UInt8] 182 | 183 | /// Default buffer size of the data buffer. 184 | private static let bufferSize = 1024 185 | 186 | /// Queue to protect mutating operation. 187 | fileprivate let queue = DispatchQueue(label: "org.swift.swiftpm.basic.stream") 188 | 189 | init(buffered: Bool) { 190 | self._buffered = buffered 191 | self._buffer = [] 192 | 193 | // When not buffered we still reserve 1 byte, as it is used by the 194 | // by the single byte write() variant. 195 | self._buffer.reserveCapacity(buffered ? _WritableByteStreamBase.bufferSize : 1) 196 | } 197 | 198 | // MARK: Data Access API 199 | 200 | /// The current offset within the output stream. 201 | public var position: Int { 202 | return _buffer.count 203 | } 204 | 205 | /// Currently available buffer size. 206 | @usableFromInline var _availableBufferSize: Int { 207 | return _buffer.capacity - _buffer.count 208 | } 209 | 210 | /// Clears the buffer maintaining current capacity. 211 | @usableFromInline func _clearBuffer() { 212 | _buffer.removeAll(keepingCapacity: true) 213 | } 214 | 215 | // MARK: Data Output API 216 | 217 | public final func flush() { 218 | writeImpl(ArraySlice(_buffer)) 219 | _clearBuffer() 220 | flushImpl() 221 | } 222 | 223 | @usableFromInline func flushImpl() { 224 | // Do nothing. 225 | } 226 | 227 | public final func close() throws { 228 | try closeImpl() 229 | } 230 | 231 | @usableFromInline func closeImpl() throws { 232 | fatalError("Subclasses must implement this") 233 | } 234 | 235 | @usableFromInline func writeImpl(_ bytes: C) where C.Iterator.Element == UInt8 { 236 | fatalError("Subclasses must implement this") 237 | } 238 | 239 | @usableFromInline func writeImpl(_ bytes: ArraySlice) { 240 | fatalError("Subclasses must implement this") 241 | } 242 | 243 | /// Write an individual byte to the buffer. 244 | public final func write(_ byte: UInt8) { 245 | guard _buffered else { 246 | _buffer.append(byte) 247 | writeImpl(ArraySlice(_buffer)) 248 | flushImpl() 249 | _clearBuffer() 250 | return 251 | } 252 | 253 | // If buffer is full, write and clear it. 254 | if _availableBufferSize == 0 { 255 | writeImpl(ArraySlice(_buffer)) 256 | _clearBuffer() 257 | } 258 | 259 | // This will need to change change if we ever have unbuffered stream. 260 | precondition(_availableBufferSize > 0) 261 | _buffer.append(byte) 262 | } 263 | 264 | /// Write a collection of bytes to the buffer. 265 | @inlinable public final func write(_ bytes: C) where C.Element == UInt8 { 266 | guard _buffered else { 267 | if let b = bytes as? ArraySlice { 268 | // Fast path for unbuffered ArraySlice 269 | writeImpl(b) 270 | } else if let b = bytes as? [UInt8] { 271 | // Fast path for unbuffered Array 272 | writeImpl(ArraySlice(b)) 273 | } else { 274 | // generic collection unfortunately must be temporarily buffered 275 | writeImpl(bytes) 276 | } 277 | flushImpl() 278 | return 279 | } 280 | 281 | // This is based on LLVM's raw_ostream. 282 | let availableBufferSize = self._availableBufferSize 283 | let byteCount = Int(bytes.count) 284 | 285 | // If we have to insert more than the available space in buffer. 286 | if byteCount > availableBufferSize { 287 | // If buffer is empty, start writing and keep the last chunk in buffer. 288 | if _buffer.isEmpty { 289 | let bytesToWrite = byteCount - (byteCount % availableBufferSize) 290 | let writeUptoIndex = bytes.index(bytes.startIndex, offsetBy: numericCast(bytesToWrite)) 291 | writeImpl(bytes.prefix(upTo: writeUptoIndex)) 292 | 293 | // If remaining bytes is more than buffer size write everything. 294 | let bytesRemaining = byteCount - bytesToWrite 295 | if bytesRemaining > availableBufferSize { 296 | writeImpl(bytes.suffix(from: writeUptoIndex)) 297 | return 298 | } 299 | // Otherwise keep remaining in buffer. 300 | _buffer += bytes.suffix(from: writeUptoIndex) 301 | return 302 | } 303 | 304 | let writeUptoIndex = bytes.index(bytes.startIndex, offsetBy: numericCast(availableBufferSize)) 305 | // Append whatever we can accommodate. 306 | _buffer += bytes.prefix(upTo: writeUptoIndex) 307 | 308 | writeImpl(ArraySlice(_buffer)) 309 | _clearBuffer() 310 | 311 | // FIXME: We should start again with remaining chunk but this doesn't work. Write everything for now. 312 | //write(collection: bytes.suffix(from: writeUptoIndex)) 313 | writeImpl(bytes.suffix(from: writeUptoIndex)) 314 | return 315 | } 316 | _buffer += bytes 317 | } 318 | } 319 | 320 | /// The thread-safe wrapper around output byte streams. 321 | /// 322 | /// This class wraps any `WritableByteStream` conforming type to provide a type-safe 323 | /// access to its operations. If the provided stream inherits from `_WritableByteStreamBase`, 324 | /// it will also ensure it is type-safe will all other `ThreadSafeOutputByteStream` instances 325 | /// around the same stream. 326 | public final class ThreadSafeOutputByteStream: WritableByteStream { 327 | private static let defaultQueue = DispatchQueue( 328 | label: "org.swift.swiftpm.basic.thread-safe-output-byte-stream") 329 | public let stream: WritableByteStream 330 | private let queue: DispatchQueue 331 | 332 | public var position: Int { 333 | return queue.sync { 334 | stream.position 335 | } 336 | } 337 | 338 | public init(_ stream: WritableByteStream) { 339 | self.stream = stream 340 | self.queue = 341 | (stream as? _WritableByteStreamBase)?.queue ?? ThreadSafeOutputByteStream.defaultQueue 342 | } 343 | 344 | public func write(_ byte: UInt8) { 345 | queue.sync { 346 | stream.write(byte) 347 | } 348 | } 349 | 350 | public func write(_ bytes: C) where C.Element == UInt8 { 351 | queue.sync { 352 | stream.write(bytes) 353 | } 354 | } 355 | 356 | public func flush() { 357 | queue.sync { 358 | stream.flush() 359 | } 360 | } 361 | 362 | public func write(sequence: S) where S.Iterator.Element == UInt8 { 363 | queue.sync { 364 | stream.write(sequence: sequence) 365 | } 366 | } 367 | 368 | public func writeJSONEscaped(_ string: String) { 369 | queue.sync { 370 | stream.writeJSONEscaped(string) 371 | } 372 | } 373 | 374 | public func close() throws { 375 | try queue.sync { 376 | try stream.close() 377 | } 378 | } 379 | } 380 | 381 | #if swift(<5.6) 382 | extension ThreadSafeOutputByteStream: UnsafeSendable {} 383 | #else 384 | extension ThreadSafeOutputByteStream: @unchecked Sendable {} 385 | #endif 386 | 387 | /// Define an output stream operator. We need it to be left associative, so we 388 | /// use `<<<`. 389 | infix operator <<< : StreamingPrecedence 390 | precedencegroup StreamingPrecedence { 391 | associativity: left 392 | } 393 | 394 | // MARK: Output Operator Implementations 395 | 396 | // FIXME: This override shouldn't be necesary but removing it causes a 30% performance regression. This problem is 397 | // tracked by the following bug: https://bugs.swift.org/browse/SR-8535 398 | 399 | @available(*, deprecated, message: "use send(_:) function on WritableByteStream instead") 400 | @discardableResult 401 | public func <<< (stream: WritableByteStream, value: ArraySlice) -> WritableByteStream { 402 | value.write(to: stream) 403 | return stream 404 | } 405 | 406 | @available(*, deprecated, message: "use send(_:) function on WritableByteStream instead") 407 | @discardableResult 408 | public func <<< (stream: WritableByteStream, value: ByteStreamable) -> WritableByteStream { 409 | value.write(to: stream) 410 | return stream 411 | } 412 | 413 | @available(*, deprecated, message: "use send(_:) function on WritableByteStream instead") 414 | @discardableResult 415 | public func <<< (stream: WritableByteStream, value: CustomStringConvertible) -> WritableByteStream { 416 | value.description.write(to: stream) 417 | return stream 418 | } 419 | 420 | @available(*, deprecated, message: "use send(_:) function on WritableByteStream instead") 421 | @discardableResult 422 | public func <<< (stream: WritableByteStream, value: ByteStreamable & CustomStringConvertible) 423 | -> WritableByteStream 424 | { 425 | value.write(to: stream) 426 | return stream 427 | } 428 | 429 | extension UInt8: ByteStreamable { 430 | public func write(to stream: WritableByteStream) { 431 | stream.write(self) 432 | } 433 | } 434 | 435 | extension Character: ByteStreamable { 436 | public func write(to stream: WritableByteStream) { 437 | stream.write(String(self)) 438 | } 439 | } 440 | 441 | extension String: ByteStreamable { 442 | public func write(to stream: WritableByteStream) { 443 | stream.write(self.utf8) 444 | } 445 | } 446 | 447 | extension Substring: ByteStreamable { 448 | public func write(to stream: WritableByteStream) { 449 | stream.write(self.utf8) 450 | } 451 | } 452 | 453 | extension StaticString: ByteStreamable { 454 | public func write(to stream: WritableByteStream) { 455 | withUTF8Buffer { stream.write($0) } 456 | } 457 | } 458 | 459 | extension Array: ByteStreamable where Element == UInt8 { 460 | public func write(to stream: WritableByteStream) { 461 | stream.write(self) 462 | } 463 | } 464 | 465 | extension ArraySlice: ByteStreamable where Element == UInt8 { 466 | public func write(to stream: WritableByteStream) { 467 | stream.write(self) 468 | } 469 | } 470 | 471 | extension ContiguousArray: ByteStreamable where Element == UInt8 { 472 | public func write(to stream: WritableByteStream) { 473 | stream.write(self) 474 | } 475 | } 476 | 477 | // MARK: Formatted Streaming Output 478 | 479 | /// Provides operations for returning derived streamable objects to implement various forms of formatted output. 480 | public struct Format { 481 | /// Write the input boolean encoded as a JSON object. 482 | static public func asJSON(_ value: Bool) -> ByteStreamable { 483 | return JSONEscapedBoolStreamable(value: value) 484 | } 485 | private struct JSONEscapedBoolStreamable: ByteStreamable { 486 | let value: Bool 487 | 488 | func write(to stream: WritableByteStream) { 489 | stream.send(value ? "true" : "false") 490 | } 491 | } 492 | 493 | /// Write the input integer encoded as a JSON object. 494 | static public func asJSON(_ value: Int) -> ByteStreamable { 495 | return JSONEscapedIntStreamable(value: value) 496 | } 497 | private struct JSONEscapedIntStreamable: ByteStreamable { 498 | let value: Int 499 | 500 | func write(to stream: WritableByteStream) { 501 | // FIXME: Diagnose integers which cannot be represented in JSON. 502 | stream.send(value.description) 503 | } 504 | } 505 | 506 | /// Write the input double encoded as a JSON object. 507 | static public func asJSON(_ value: Double) -> ByteStreamable { 508 | return JSONEscapedDoubleStreamable(value: value) 509 | } 510 | private struct JSONEscapedDoubleStreamable: ByteStreamable { 511 | let value: Double 512 | 513 | func write(to stream: WritableByteStream) { 514 | // FIXME: What should we do about NaN, etc.? 515 | // 516 | // FIXME: Is Double.debugDescription the best representation? 517 | stream.send(value.debugDescription) 518 | } 519 | } 520 | 521 | /// Write the input CustomStringConvertible encoded as a JSON object. 522 | static public func asJSON(_ value: T) -> ByteStreamable { 523 | return JSONEscapedStringStreamable(value: value.description) 524 | } 525 | /// Write the input string encoded as a JSON object. 526 | static public func asJSON(_ string: String) -> ByteStreamable { 527 | return JSONEscapedStringStreamable(value: string) 528 | } 529 | private struct JSONEscapedStringStreamable: ByteStreamable { 530 | let value: String 531 | 532 | func write(to stream: WritableByteStream) { 533 | stream.send(UInt8(ascii: "\"")) 534 | stream.writeJSONEscaped(value) 535 | stream.send(UInt8(ascii: "\"")) 536 | } 537 | } 538 | 539 | /// Write the input string list encoded as a JSON object. 540 | static public func asJSON(_ items: [T]) -> ByteStreamable { 541 | return JSONEscapedStringListStreamable(items: items.map({ $0.description })) 542 | } 543 | /// Write the input string list encoded as a JSON object. 544 | // 545 | // FIXME: We might be able to make this more generic through the use of a "JSONEncodable" protocol. 546 | static public func asJSON(_ items: [String]) -> ByteStreamable { 547 | return JSONEscapedStringListStreamable(items: items) 548 | } 549 | private struct JSONEscapedStringListStreamable: ByteStreamable { 550 | let items: [String] 551 | 552 | func write(to stream: WritableByteStream) { 553 | stream.send(UInt8(ascii: "[")) 554 | for (i, item) in items.enumerated() { 555 | if i != 0 { stream.send(",") } 556 | stream.send(Format.asJSON(item)) 557 | } 558 | stream.send(UInt8(ascii: "]")) 559 | } 560 | } 561 | 562 | /// Write the input dictionary encoded as a JSON object. 563 | static public func asJSON(_ items: [String: String]) -> ByteStreamable { 564 | return JSONEscapedDictionaryStreamable(items: items) 565 | } 566 | private struct JSONEscapedDictionaryStreamable: ByteStreamable { 567 | let items: [String: String] 568 | 569 | func write(to stream: WritableByteStream) { 570 | stream.send(UInt8(ascii: "{")) 571 | for (offset:i, element:(key:key, value:value)) in items.enumerated() { 572 | if i != 0 { stream.send(",") } 573 | stream.send(Format.asJSON(key)).send(":").send(Format.asJSON(value)) 574 | } 575 | stream.send(UInt8(ascii: "}")) 576 | } 577 | } 578 | 579 | /// Write the input list (after applying a transform to each item) encoded as a JSON object. 580 | // 581 | // FIXME: We might be able to make this more generic through the use of a "JSONEncodable" protocol. 582 | static public func asJSON(_ items: [T], transform: @escaping (T) -> String) -> ByteStreamable { 583 | return JSONEscapedTransformedStringListStreamable(items: items, transform: transform) 584 | } 585 | private struct JSONEscapedTransformedStringListStreamable: ByteStreamable { 586 | let items: [T] 587 | let transform: (T) -> String 588 | 589 | func write(to stream: WritableByteStream) { 590 | stream.send(UInt8(ascii: "[")) 591 | for (i, item) in items.enumerated() { 592 | if i != 0 { stream.send(",") } 593 | stream.send(Format.asJSON(transform(item))) 594 | } 595 | stream.send(UInt8(ascii: "]")) 596 | } 597 | } 598 | 599 | /// Write the input list to the stream with the given separator between items. 600 | static public func asSeparatedList(_ items: [T], separator: String) 601 | -> ByteStreamable 602 | { 603 | return SeparatedListStreamable(items: items, separator: separator) 604 | } 605 | private struct SeparatedListStreamable: ByteStreamable { 606 | let items: [T] 607 | let separator: String 608 | 609 | func write(to stream: WritableByteStream) { 610 | for (i, item) in items.enumerated() { 611 | // Add the separator, if necessary. 612 | if i != 0 { 613 | stream.send(separator) 614 | } 615 | 616 | stream.send(item) 617 | } 618 | } 619 | } 620 | 621 | /// Write the input list to the stream (after applying a transform to each item) with the given separator between 622 | /// items. 623 | static public func asSeparatedList( 624 | _ items: [T], 625 | transform: @escaping (T) -> ByteStreamable, 626 | separator: String 627 | ) -> ByteStreamable { 628 | return TransformedSeparatedListStreamable( 629 | items: items, transform: transform, separator: separator) 630 | } 631 | private struct TransformedSeparatedListStreamable: ByteStreamable { 632 | let items: [T] 633 | let transform: (T) -> ByteStreamable 634 | let separator: String 635 | 636 | func write(to stream: WritableByteStream) { 637 | for (i, item) in items.enumerated() { 638 | if i != 0 { stream.send(separator) } 639 | stream.send(transform(item)) 640 | } 641 | } 642 | } 643 | 644 | static public func asRepeating(string: String, count: Int) -> ByteStreamable { 645 | return RepeatingStringStreamable(string: string, count: count) 646 | } 647 | private struct RepeatingStringStreamable: ByteStreamable { 648 | let string: String 649 | let count: Int 650 | 651 | init(string: String, count: Int) { 652 | precondition(count >= 0, "Count should be >= zero") 653 | self.string = string 654 | self.count = count 655 | } 656 | 657 | func write(to stream: WritableByteStream) { 658 | for _ in 0..(_ bytes: C) where C.Iterator.Element == UInt8 { 695 | contents += bytes 696 | } 697 | override final func writeImpl(_ bytes: ArraySlice) { 698 | contents += bytes 699 | } 700 | 701 | override final func closeImpl() throws { 702 | // Do nothing. The protocol does not require to stop receiving writes, close only signals that resources could 703 | // be released at this point should we need to. 704 | } 705 | } 706 | 707 | /// Represents a stream which is backed to a file. Not for instantiating. 708 | public class FileOutputByteStream: _WritableByteStreamBase { 709 | 710 | public override final func closeImpl() throws { 711 | flush() 712 | try fileCloseImpl() 713 | } 714 | 715 | /// Closes the file flushing any buffered data. 716 | func fileCloseImpl() throws { 717 | fatalError("fileCloseImpl() should be implemented by a subclass") 718 | } 719 | } 720 | 721 | /// Implements file output stream for local file system. 722 | public final class LocalFileOutputByteStream: FileOutputByteStream { 723 | 724 | /// The pointer to the file. 725 | let filePointer: FILEPointer 726 | 727 | /// Set to an error value if there were any IO error during writing. 728 | private var error: FileSystemError? 729 | 730 | /// Closes the file on deinit if true. 731 | private var closeOnDeinit: Bool 732 | 733 | /// Path to the file this stream should operate on. 734 | private let path: AbsolutePath? 735 | 736 | /// Instantiate using the file pointer. 737 | public init(filePointer: FILEPointer, closeOnDeinit: Bool = true, buffered: Bool = true) throws { 738 | self.filePointer = filePointer 739 | self.closeOnDeinit = closeOnDeinit 740 | self.path = nil 741 | super.init(buffered: buffered) 742 | } 743 | 744 | /// Opens the file for writing at the provided path. 745 | /// 746 | /// - Parameters: 747 | /// - path: Path to the file this stream should operate on. 748 | /// - closeOnDeinit: If true closes the file on deinit. clients can use 749 | /// close() if they want to close themselves or catch 750 | /// errors encountered during writing to the file. 751 | /// Default value is true. 752 | /// - buffered: If true buffers writes in memory until full or flush(). 753 | /// Otherwise, writes are processed and flushed immediately. 754 | /// Default value is true. 755 | /// 756 | /// - Throws: FileSystemError 757 | public init(_ path: AbsolutePath, closeOnDeinit: Bool = true, buffered: Bool = true) throws { 758 | guard let filePointer = fopen(path.pathString, "wb") else { 759 | throw FileSystemError(errno: errno, path) 760 | } 761 | self.path = path 762 | self.filePointer = filePointer 763 | self.closeOnDeinit = closeOnDeinit 764 | super.init(buffered: buffered) 765 | } 766 | 767 | deinit { 768 | if closeOnDeinit { 769 | fclose(filePointer) 770 | } 771 | } 772 | 773 | func errorDetected(code: Int32?) { 774 | if let code = code { 775 | error = .init(.ioError(code: code), path) 776 | } else { 777 | error = .init(.unknownOSError, path) 778 | } 779 | } 780 | 781 | override final func writeImpl(_ bytes: C) where C.Iterator.Element == UInt8 { 782 | // FIXME: This will be copying bytes but we don't have option currently. 783 | var contents = [UInt8](bytes) 784 | while true { 785 | let n = fwrite(&contents, 1, contents.count, filePointer) 786 | if n < 0 { 787 | if errno == EINTR { continue } 788 | errorDetected(code: errno) 789 | } else if n != contents.count { 790 | errorDetected(code: nil) 791 | } 792 | break 793 | } 794 | } 795 | 796 | override final func writeImpl(_ bytes: ArraySlice) { 797 | bytes.withUnsafeBytes { bytesPtr in 798 | while true { 799 | let n = fwrite(bytesPtr.baseAddress!, 1, bytesPtr.count, filePointer) 800 | if n < 0 { 801 | if errno == EINTR { continue } 802 | errorDetected(code: errno) 803 | } else if n != bytesPtr.count { 804 | errorDetected(code: nil) 805 | } 806 | break 807 | } 808 | } 809 | } 810 | 811 | override final func flushImpl() { 812 | fflush(filePointer) 813 | } 814 | 815 | override final func fileCloseImpl() throws { 816 | defer { 817 | fclose(filePointer) 818 | // If clients called close we shouldn't call fclose again in deinit. 819 | closeOnDeinit = false 820 | } 821 | // Throw if errors were found during writing. 822 | if let error = error { 823 | throw error 824 | } 825 | } 826 | } 827 | 828 | /// Public stdout stream instance. 829 | public var stdoutStream: ThreadSafeOutputByteStream = try! ThreadSafeOutputByteStream( 830 | LocalFileOutputByteStream( 831 | filePointer: stdout, 832 | closeOnDeinit: false)) 833 | 834 | /// Public stderr stream instance. 835 | public var stderrStream: ThreadSafeOutputByteStream = try! ThreadSafeOutputByteStream( 836 | LocalFileOutputByteStream( 837 | filePointer: stderr, 838 | closeOnDeinit: false)) -------------------------------------------------------------------------------- /Sources/SwiftReloadExample/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftParser 3 | import SwiftReload 4 | import SwiftSyntax 5 | 6 | LocalSwiftReloader().start() 7 | 8 | let counter = FastCounter() 9 | 10 | func hello() { 11 | print("hello") 12 | counter.tick() 13 | } 14 | 15 | class Counter { 16 | var count = 0 17 | 18 | func tick() { 19 | count += 1 20 | print("count = \(count)") 21 | } 22 | } 23 | 24 | class FastCounter: Counter { 25 | override func tick() { 26 | count += 10 27 | print("count = \(count)") 28 | } 29 | } 30 | 31 | Task { 32 | while true { 33 | try await Task.sleep(for: .milliseconds(1000)) 34 | hello() 35 | } 36 | } 37 | 38 | RunLoop.main.run() 39 | -------------------------------------------------------------------------------- /Tests/SwiftReloadTests/SwiftReloadTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | 4 | @testable import SwiftReload 5 | 6 | class LocalSwiftReloaderTests { 7 | init() { 8 | saveSelf() 9 | } 10 | 11 | deinit { 12 | restoreSelf() 13 | } 14 | 15 | @Test func example() async throws { 16 | var resume: () -> Void = {} 17 | 18 | LocalSwiftReloader(onReload: { 19 | resume() 20 | }).start() 21 | 22 | print(greet("Alice")) 23 | #expect(greet("Alice") == "Hello, Alice!") 24 | 25 | // replace self file "Hello" with "Hi" 26 | try await replaceSelf("Hello", with: "Hi") 27 | 28 | // wait for the reloader to reload the module 29 | // try await Task.sleep(for: .milliseconds(2000)) 30 | await withCheckedContinuation { continuation in 31 | print("waiting for reload") 32 | resume = continuation.resume 33 | print("resumed") 34 | } 35 | 36 | // check if the changes are reflected 37 | print(greet("Alice")) 38 | #expect(greet("Alice") == "Hi, Alice!") 39 | } 40 | 41 | func greet(_ name: String) -> String { 42 | return "Hello, \(name)!" 43 | } 44 | 45 | } 46 | 47 | var savedSelf: String? 48 | 49 | /// Save the current file content 50 | func saveSelf() { 51 | let file = URL(fileURLWithPath: #filePath) 52 | let content = try! String(contentsOf: file) 53 | savedSelf = content 54 | } 55 | 56 | /// Restore previous file content 57 | func restoreSelf() { 58 | guard let content = savedSelf else { 59 | return 60 | } 61 | let file = URL(fileURLWithPath: #filePath) 62 | try! content.write(to: file, atomically: true, encoding: .utf8) 63 | } 64 | 65 | /// Replace a string in the current file 66 | func replaceSelf(_ old: String, with new: String) async throws { 67 | let file = URL(fileURLWithPath: #filePath) 68 | let content = try String(contentsOf: file) 69 | let newContent = content.replacingOccurrences(of: old, with: new) 70 | try newContent.write(to: file, atomically: true, encoding: .utf8) 71 | } 72 | --------------------------------------------------------------------------------