diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 342d21a6c..9ac8f649b 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -137,7 +137,7 @@ jobs: https://github.com/apple/swift-package-manager/issues/6595" && false) ) build-native-windows: - name: "Native, no testing: windows-latest/release" + name: "Native: windows-latest, debug" strategy: fail-fast: false runs-on: windows-latest @@ -145,8 +145,10 @@ jobs: - name: Setup swift uses: compnerd/gha-setup-swift@main with: - branch: swift-5.8.1-release - tag: 5.8.1-RELEASE + github-repo: thebrowsercompany/swift-build + release-asset-name: installer-amd64.exe + release-tag-name: 20231010.3 + github-token: ${{ secrets.GITHUB_TOKEN }} - uses: actions/checkout@v3 @@ -159,26 +161,25 @@ jobs: 7z x llvm-15.0.6-windows-x86-msvc17-msvcrt.7z -oC:\ Add-Content $env:GITHUB_PATH 'C:\llvm-15.0.6-windows-x86-msvc17-msvcrt\bin' - - name: Copy LLVM's include and lib to include and lib folder of MSVC - run: | - xcopy c:\llvm-15.0.6-windows-x86-msvc17-msvcrt\include\*.* c:\program" "files\microsoft" "visual" "studio\2022\enterprise\vc\tools\msvc\${{ env.VCToolsVersion }}\include\ /s /h - xcopy c:\llvm-15.0.6-windows-x86-msvc17-msvcrt\lib\*.* c:\program" "files\microsoft" "visual" "studio\2022\enterprise\vc\tools\msvc\${{ env.VCToolsVersion }}\lib\x64\ /s /h - - run: llvm-config --version + - name: Generate LLVM pkgconfig file + run: | + swift package resolve + & "C:\Program Files\Git\bin\bash.exe" .build/checkouts/Swifty-LLVM/Tools/make-pkgconfig.sh llvm.pc + type llvm.pc + - name: Build support library run: clang -c ./Library/Hylo/LibC.c -o HyloLibC.lib - - name: Copy support library - run: xcopy HyloLibC.lib c:\program" "files\microsoft" "visual" "studio\2022\enterprise\vc\tools\msvc\${{ env.VCToolsVersion }}\lib\x64\ + - name: Prevent reentrant builds for speed + run: echo 'HYLO_NO_REENTRANT_BUILD=1' >> $env:GITHUB_ENV - - name: Build (Release) - id: build - continue-on-error: true - run: swift build -v -c release + - name: Non-test build ensuring that build tools have been built + run: swift build --pkg-config-path . - - name: Retry on failure - continue-on-error: false - if: steps.build.outcome != 'success' - run: swift build -v -c release + - name: Build tests + run: swift build --build-tests --pkg-config-path . + - name: Test + run: swift test --skip-build --pkg-config-path . diff --git a/Package.resolved b/Package.resolved index a19da3a35..7b86310e1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -119,4 +119,4 @@ } ], "version" : 2 -} +} \ No newline at end of file diff --git a/Package.swift b/Package.swift index d02d2d675..052d9d7de 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,12 @@ // swift-tools-version:5.7 import PackageDescription +#if os(Windows) +let onWindows = true +#else +let onWindows = false +#endif + /// Settings to be passed to swiftc for all targets. let allTargetsSwiftSettings: [SwiftSetting] = [ .unsafeFlags(["-warnings-as-errors"]) @@ -126,7 +132,8 @@ let package = Package( .plugin( name: "TestGeneratorPlugin", capability: .buildTool(), - dependencies: [.target(name: "GenerateHyloFileTests")]), + // Workaround for SPM bug; see PortableBuildToolPlugin.swift + dependencies: onWindows ? [] : ["GenerateHyloFileTests"]), .executableTarget( name: "GenerateHyloFileTests", @@ -134,7 +141,7 @@ let package = Package( .product(name: "ArgumentParser", package: "swift-argument-parser"), "Utils", ], - swiftSettings: allTargetsSwiftSettings), + swiftSettings: allTargetsSwiftSettings + [ .unsafeFlags(["-parse-as-library"]) ]), // Test targets. .testTarget( @@ -144,7 +151,7 @@ let package = Package( .testTarget( name: "DriverTests", - dependencies: ["Driver"], + dependencies: ["Driver", "TestUtils"], swiftSettings: allTargetsSwiftSettings), .testTarget( diff --git a/Plugins/TestGeneratorPlugin/PortableBuildToolPlugin.swift b/Plugins/TestGeneratorPlugin/PortableBuildToolPlugin.swift new file mode 100644 index 000000000..5be52281b --- /dev/null +++ b/Plugins/TestGeneratorPlugin/PortableBuildToolPlugin.swift @@ -0,0 +1,380 @@ +import PackagePlugin +import Foundation +#if os(Windows) +import WinSDK +#endif + +#if os(Windows) +/// The name of the environment variable containing the executable search path. +fileprivate let pathEnvironmentVariable = "Path" +/// The separator between elements of the executable search path. +fileprivate let pathEnvironmentSeparator: Character = ";" +/// The file extension applied to binary executables +fileprivate let executableSuffix = ".exe" + +fileprivate extension URL { + + /// Returns the URL given by removing all the elements of `suffix` + /// from the tail of `pathComponents`, or` `nil` if `suffix` is not + /// a suffix of `pathComponents`. + func sansPathComponentSuffix>(_ suffix: Suffix) + -> URL? + { + var r = self + var remainingSuffix = suffix[...] + while let x = remainingSuffix.popLast() { + if r.lastPathComponent != x { return nil } + r.deleteLastPathComponent() + } + return r + } + + /// The representation used by the native filesystem. + var fileSystemPath: String { + self.withUnsafeFileSystemRepresentation { String(cString: $0!) } + } +} + +fileprivate extension PackagePlugin.Target { + + /// The source files. + var allSourceFiles: [URL] { + return (self as? PackagePlugin.SourceModuleTarget)?.sourceFiles(withSuffix: "").map(\.path.url) ?? [] + } + +} + +fileprivate extension PackagePlugin.Package { + + /// The source files in this package on which the given executable depends. + func sourceDependencies(ofProductNamed productName: String) throws -> [URL] { + var result: Set = [] + let p = products.first { $0.name == productName }! + var visitedTargets = Set() + + for t0 in p.targets { + if visitedTargets.insert(t0.id).inserted { + result.formUnion(t0.allSourceFiles) + } + + for t1 in t0.recursiveTargetDependencies { + if visitedTargets.insert(t1.id).inserted { + result.formUnion(t1.allSourceFiles) + } + } + } + return Array(result) + } + +} +#endif + +// Workarounds for SPM's buggy `Path` type on Windows. +// +// SPM `PackagePlugin.Path` uses a representation that—if not repaired before used by a +// `BuildToolPlugin` on Windows—will cause files not to be found. +public extension Path { + + /// A string representation appropriate to the platform. + var platformString: String { + #if os(Windows) + string.withCString(encodedAs: UTF16.self) { pwszPath in + // Allocate a buffer for the repaired UTF-16. + let bufferSize = Int(GetFullPathNameW(pwszPath, 0, nil, nil)) + var buffer = Array(repeating: 0, count: bufferSize) + // Actually do the repair + _ = GetFullPathNameW(pwszPath, DWORD(bufferSize), &buffer, nil) + // Drop the zero terminator and convert back to a Swift string. + return String(decoding: buffer.dropLast(), as: UTF16.self) + } + #else + string + #endif + } + + /// A `URL` referring to the same location. + var url: URL { URL(fileURLWithPath: platformString) } + + /// A representation of `Self` that works on all platforms. + var portable: Self { + #if os(Windows) + Path(self.platformString) + #else + self + #endif + } +} + +public extension URL { + + /// A Swift Package Manager-compatible representation. + var spmPath: Path { Path(self.path) } + + /// Returns `self` with the relative file path `suffix` appended. + /// + /// This is a portable version of `self.appending(path:)`, which is only available on recent + /// macOSes. + func appendingPath(_ suffix: String) -> URL { + +#if os(macOS) + if #available(macOS 13.0, *) { return self.appending(path: suffix) } +#endif + + return (suffix as NSString).pathComponents + .reduce(into: self) { $0.appendPathComponent($1) } + } + +} + +/// Defines functionality for all plugins having a `buildTool` capability. +public protocol PortableBuildToolPlugin: BuildToolPlugin { + + /// Returns the build commands for `target` in `context`. + func portableBuildCommands( + context: PackagePlugin.PluginContext, + target: PackagePlugin.Target + ) async throws -> [PortableBuildCommand] + +} + +extension PortableBuildToolPlugin { + + public func createBuildCommands(context: PluginContext, target: Target) async throws + -> [PackagePlugin.Command] + { + + return try await portableBuildCommands(context: context, target: target).map { + try $0.spmCommand(in: context) + } + + } + +} + +public extension PortableBuildCommand.Tool { + + /// A partial translation to SPM plugin inputs of an invocation. + struct SPMInvocation { + /// The executable that will actually run. + let executable: PackagePlugin.Path + /// The command-line arguments that must precede the ones specified by the caller. + let argumentPrefix: [String] + /// The source files that must be added as build dependencies if we want the tool + /// to be re-run when its sources change. + let additionalSources: [URL] + } + + fileprivate func spmInvocation(in context: PackagePlugin.PluginContext) throws -> SPMInvocation { + switch self { + case .preInstalled(file: let pathToExecutable): + return .init(executable: pathToExecutable.portable, argumentPrefix: [], additionalSources: []) + + case .executableProduct(name: let productName): + #if !os(Windows) + return try .init( + executable: context.tool(named: productName).path.portable, + argumentPrefix: [], additionalSources: []) + #else + // Instead of depending on context.tool(named:), which demands a declared dependency on the + // tool, which causes link errors on Windows + // (https://github.com/apple/swift-package-manager/issues/6859#issuecomment-1720371716), + // Invoke swift reentrantly to run the GenerateResoure tool. + + // + // If a likely candidate for the current toolchain can be found in the `Path`, prepend its + // `bin/` dfirectory. + // + var searchPath = ProcessInfo.processInfo.environment[pathEnvironmentVariable]! + .split(separator: pathEnvironmentSeparator).map { URL(fileURLWithPath: String($0)) } + + // SwiftPM seems to put a descendant of the currently-running Swift Toolchain/ directory having + // this component suffix into the executable search path when plugins are run. + let pluginAPISuffix = ["lib", "swift", "pm", "PluginAPI"] + + if let p = searchPath.lazy.compactMap({ $0.sansPathComponentSuffix(pluginAPISuffix) }).first { + searchPath = [ p.appendingPathComponent("bin") ] + searchPath + } + + // Try searchPath first, then fall back to SPM's `tool(named:)` + let swift = try searchPath.lazy.map { $0.appendingPathComponent("swift" + executableSuffix) } + .first { FileManager().isExecutableFile(atPath: $0.path) } + ?? context.tool(named: "swift").path.url + + let noReentrantBuild = ProcessInfo.processInfo.environment["HYLO_NO_REENTRANT_BUILD"] != nil + let packageDirectory = context.package.directory.url + + // Locate the scratch directory for reentrant builds inside the package directory to work + // around SPM's broken Windows path handling + let conditionalOptions = noReentrantBuild + ? [ "--skip-build" ] + : [ + "--scratch-path", + packageDirectory.appendingPathComponent(".build") + .appendingPathComponent(UUID().uuidString) + .fileSystemPath + ] + + return .init( + executable: swift.spmPath, + argumentPrefix: [ + "run", + // Only Macs currently use sandboxing, but nested sandboxes are prohibited, so for future + // resilience in case Windows gets a sandbox, disable it on these reentrant builds. + // + // Currently if we run this code on a Mac, disabling the sandbox on this inner build is + // enough to allow us to write on the scratchPath, which is outside any _outer_ sandbox. + // I think that's an SPM bug. If they fix it, we'll need to nest scratchPath in + // context.workDirectory and add an explicit build step to delete it to keep its contents + // from being incorporated into the resources of the target we're building. + "--disable-sandbox", + "--package-path", packageDirectory.fileSystemPath] + + conditionalOptions + + [ productName ], + additionalSources: + try context.package.sourceDependencies(ofProductNamed: productName)) + #endif + } + } +} + +fileprivate extension PortableBuildCommand { + + /// Returns a representation of `self` for the result of a `BuildToolPlugin.createBuildCommands` + /// invocation with the given `context` parameter. + func spmCommand(in context: PackagePlugin.PluginContext) throws -> PackagePlugin.Command { + + switch self { + case .buildCommand( + displayName: let displayName, + tool: let tool, + arguments: let arguments, + environment: let environment, + inputFiles: let inputFiles, + outputFiles: let outputFiles, + pluginSourceFile: let pluginSourceFile): + + let i = try tool.spmInvocation(in: context) + + /// Guess at files that constitute this plugin, the changing of which should cause outputs to be + /// regenerated (workaround for https://github.com/apple/swift-package-manager/issues/6936). + let pluginSourceDirectory = URL(fileURLWithPath: pluginSourceFile).deletingLastPathComponent() + + // We could filter out directories, but why bother? + let pluginSources = try FileManager() + .subpathsOfDirectory(atPath: pluginSourceDirectory.path) + .map { pluginSourceDirectory.appendingPath($0) } + + return .buildCommand( + displayName: displayName, + executable: i.executable, + arguments: i.argumentPrefix + arguments, + environment: environment, + inputFiles: inputFiles.map(\.portable) + (pluginSources + i.additionalSources).map(\.spmPath), + outputFiles: outputFiles.map(\.portable)) + + case .prebuildCommand( + displayName: let displayName, + tool: let tool, + arguments: let arguments, + environment: let environment, + outputFilesDirectory: let outputFilesDirectory): + + let i = try tool.spmInvocation(in: context) + + return .prebuildCommand( + displayName: displayName, + executable: i.executable, + arguments: i.argumentPrefix + arguments, + environment: environment, + outputFilesDirectory: outputFilesDirectory.portable) + } + } + +} + + +/// A command to run during the build. +public enum PortableBuildCommand { + + /// A command-line tool to be invoked. + public enum Tool { + + /// The executable product named `name` in this package + case executableProduct(name: String) + + /// The executable at `file`, an absolute path outside the build directory of the package being + /// built. + case preInstalled(file: PackagePlugin.Path) + } + + /// A command that runs when any of its output files are needed by + /// the build, but out-of-date. + /// + /// An output file is out-of-date if it doesn't exist, or if any + /// input files have changed since the command was last run. + /// + /// - Note: the paths in the list of output files may depend on the list of + /// input file paths, but **must not** depend on reading the contents of + /// any input files. Such cases must be handled using a `prebuildCommand`. + /// + /// - Parameters: + /// - displayName: An optional string to show in build logs and other + /// status areas. + /// - tool: The command-line tool invoked to build the output files. + /// - arguments: Command-line arguments to be passed to the tool. + /// - environment: Environment variable assignments visible to the + /// tool. + /// - inputFiles: Files on which the contents of output files may depend. + /// Any paths passed as `arguments` should typically be passed here as + /// well. + /// - outputFiles: Files to be generated or updated by the tool. + /// Any files recognizable by their extension as source files + /// (e.g. `.swift`) are compiled into the target for which this command + /// was generated as if in its source directory; other files are treated + /// as resources as if explicitly listed in `Package.swift` using + /// `.process(...)`. + /// - pluginSourceFile: the path to a source file of the PortableBuildToolPlugin; allow the + /// default to take effect. + case buildCommand( + displayName: String?, + tool: Tool, + arguments: [String], + environment: [String: String] = [:], + inputFiles: [Path] = [], + outputFiles: [Path] = [], + pluginSourceFile: String = #filePath + ) + + /// A command that runs unconditionally before every build. + /// + /// Prebuild commands can have a significant performance impact + /// and should only be used when there would be no way to know the + /// list of output file paths without first reading the contents + /// of one or more input files. Typically there is no way to + /// determine this list without first running the command, so + /// instead of encoding that list, the caller supplies an + /// `outputFilesDirectory` parameter, and all files in that + /// directory after the command runs are treated as output files. + /// + /// - Parameters: + /// - displayName: An optional string to show in build logs and other + /// status areas. + /// - tool: The command-line tool invoked to build the output files. + /// - arguments: Command-line arguments to be passed to the tool. + /// - environment: Environment variable assignments visible to the tool. + /// - workingDirectory: Optional initial working directory when the tool + /// runs. + /// - outputFilesDirectory: A directory into which the command writes its + /// output files. Any files there recognizable by their extension as + /// source files (e.g. `.swift`) are compiled into the target for which + /// this command was generated as if in its source directory; other + /// files are treated as resources as if explicitly listed in + /// `Package.swift` using `.process(...)`. + case prebuildCommand( + displayName: String?, + tool: Tool, + arguments: [String], + environment: [String: String] = [:], + outputFilesDirectory: Path) + +} diff --git a/Plugins/TestGeneratorPlugin/TestGeneratorPlugin.swift b/Plugins/TestGeneratorPlugin/TestGeneratorPlugin.swift index daecabf9c..60100753a 100644 --- a/Plugins/TestGeneratorPlugin/TestGeneratorPlugin.swift +++ b/Plugins/TestGeneratorPlugin/TestGeneratorPlugin.swift @@ -3,20 +3,24 @@ import PackagePlugin /// The Swift Package Manager plugin that generates XCTest cases for annotated ".hylo" files as /// part of our build process. @main -struct TestGeneratorPlugin: BuildToolPlugin { +struct TestGeneratorPlugin: PortableBuildToolPlugin { - func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { + func portableBuildCommands( + context: PackagePlugin.PluginContext, target: PackagePlugin.Target + ) throws -> [PortableBuildCommand] + { guard let target = target as? SourceModuleTarget else { return [] } - let inputPaths = target.sourceFiles(withSuffix: "hylo").map(\.path) + let inputPaths = target.sourceFiles(withSuffix: ".hylo").map(\.path) let outputPath = context.pluginWorkDirectory.appending("HyloFileTests.swift") - let cmd = Command.buildCommand( + return [ + .buildCommand( displayName: "Generating XCTestCases into \(outputPath)", - executable: try context.tool(named: "GenerateHyloFileTests").path, - arguments: ["-o", outputPath, "-n", target.moduleName] + inputPaths, + tool: .executableProduct(name: "GenerateHyloFileTests"), + arguments: ["-o", outputPath.platformString, "-n", target.moduleName] + + inputPaths.map(\.platformString), inputFiles: inputPaths, - outputFiles: [outputPath]) - return [cmd] + outputFiles: [outputPath])] } } diff --git a/Sources/CodeGen/LLVM/LLVMProgram.swift b/Sources/CodeGen/LLVM/LLVMProgram.swift index b422143a2..c399714de 100644 --- a/Sources/CodeGen/LLVM/LLVMProgram.swift +++ b/Sources/CodeGen/LLVM/LLVMProgram.swift @@ -60,7 +60,7 @@ public struct LLVMProgram { var result: [URL] = [] for m in llvmModules.values { let f = directory.appendingPathComponent(m.name).appendingPathExtension("o") - try m.write(type, for: target, to: f.path) + try m.write(type, for: target, to: f.fileSystemPath) result.append(f) } return result diff --git a/Sources/Core/Diagnostic+LoggingOrder.swift b/Sources/Core/Diagnostic+LoggingOrder.swift index dff40b471..b5aa00921 100644 --- a/Sources/Core/Diagnostic+LoggingOrder.swift +++ b/Sources/Core/Diagnostic+LoggingOrder.swift @@ -8,7 +8,7 @@ extension Diagnostic { if lhs.file == rhs.file { return lhs.first() < rhs.first() } else { - return lhs.file.url.path.lexicographicallyPrecedes(rhs.file.url.path) + return lhs.file.url.fileSystemPath.lexicographicallyPrecedes(rhs.file.url.path) } } diff --git a/Sources/Core/Diagnostic.swift b/Sources/Core/Diagnostic.swift index 235c2a36e..f1fdb612b 100644 --- a/Sources/Core/Diagnostic.swift +++ b/Sources/Core/Diagnostic.swift @@ -73,7 +73,7 @@ extension Diagnostic: CustomStringConvertible { let prefix: String let l = site.first() let (line, column) = l.lineAndColumn - prefix = "\(l.file.url.standardizedFileURL.path):\(line):\(column): " + prefix = "\(l.file.url.fileSystemPath):\(line):\(column): " return prefix + "\(level): \(message)" } diff --git a/Sources/Driver/Driver.swift b/Sources/Driver/Driver.swift index b41ea4761..aa36ce1ca 100644 --- a/Sources/Driver/Driver.swift +++ b/Sources/Driver/Driver.swift @@ -122,12 +122,18 @@ public struct Driver: ParsableCommand { } public func run() throws { - let (exitCode, diagnostics) = try execute() + do { + let (exitCode, diagnostics) = try execute() - diagnostics.render( - into: &standardError, style: ProcessInfo.ansiTerminalIsConnected ? .styled : .unstyled) + diagnostics.render( + into: &standardError, style: ProcessInfo.ansiTerminalIsConnected ? .styled : .unstyled) - Driver.exit(withError: exitCode) + Driver.exit(withError: exitCode) + } + catch let e { + print("Unexpected error\n\(e)") + Driver.exit(withError: e) + } } /// Executes the command, returning its exit status and any generated diagnostics. @@ -182,19 +188,44 @@ public struct Driver: ParsableCommand { // LLVM + if verbose { + standardError.write("begin depolymorphization pass.\n") + } ir.applyPass(.depolymorphize) + if verbose { + standardError.write("create LLVM target machine.\n") + } + #if os(Windows) + let target = try LLVM.TargetMachine(for: .host()) + #else let target = try LLVM.TargetMachine(for: .host(), relocation: .pic) + #endif + if verbose { + standardError.write("create LLVM program.\n") + } var llvmProgram = try LLVMProgram(ir, mainModule: sourceModule, for: target) if optimize { + if verbose { + standardError.write("LLVM optimization.\n") + } llvmProgram.optimize() } else { + if verbose { + standardError.write("LLVM mandatory passes.\n") + } llvmProgram.applyMandatoryPasses() } + if verbose { + standardError.write("LLVM processing complete.\n") + } if outputType == .llvm { let m = llvmProgram.llvmModules[sourceModule]! + if verbose { + standardError.write("writing LLVM output.") + } try m.description.write(to: llvmFile(productName), atomically: true, encoding: .utf8) return } @@ -203,7 +234,7 @@ public struct Driver: ParsableCommand { if outputType == .intelAsm { try llvmProgram.llvmModules[sourceModule]!.write( - .assembly, for: target, to: intelASMFile(productName).path) + .assembly, for: target, to: intelASMFile(productName).fileSystemPath) return } @@ -276,7 +307,7 @@ public struct Driver: ParsableCommand { private func makeMacOSExecutable( at binaryPath: String, linking objects: [URL], diagnostics: inout DiagnosticSet ) throws { - let xcrun = try find("xcrun") + let xcrun = try findExecutable(invokedAs: "xcrun").fileSystemPath let sdk = try runCommandLine(xcrun, ["--sdk", "macosx", "--show-sdk-path"], diagnostics: &diagnostics) ?? "" @@ -286,7 +317,7 @@ public struct Driver: ParsableCommand { "-L\(sdk)/usr/lib", ] arguments.append(contentsOf: librarySearchPaths.map({ "-L\($0)" })) - arguments.append(contentsOf: objects.map(\.path)) + arguments.append(contentsOf: objects.map(\.fileSystemPath)) arguments.append("-lSystem") arguments.append(contentsOf: libraries.map({ "-l\($0)" })) @@ -304,12 +335,12 @@ public struct Driver: ParsableCommand { "-o", binaryPath, ] arguments.append(contentsOf: librarySearchPaths.map({ "-L\($0)" })) - arguments.append(contentsOf: objects.map(\.path)) + arguments.append(contentsOf: objects.map(\.fileSystemPath)) arguments.append(contentsOf: libraries.map({ "-l\($0)" })) // Note: We use "clang" rather than "ld" so that to deal with the entry point of the program. // See https://stackoverflow.com/questions/51677440 - try runCommandLine(find("clang++"), arguments, diagnostics: &diagnostics) + try runCommandLine(findExecutable(invokedAs: "clang++").fileSystemPath, arguments, diagnostics: &diagnostics) } /// Combines the object files located at `objects` into an executable file at `binaryPath`, @@ -320,15 +351,15 @@ public struct Driver: ParsableCommand { diagnostics: inout DiagnosticSet ) throws { try runCommandLine( - find("lld-link"), - ["-defaultlib:HyloLibC", "-defaultlib:msvcrt", "-out:" + binaryPath] + objects.map(\.path), + findExecutable(invokedAs: "lld-link").fileSystemPath, + ["-defaultlib:HyloLibC", "-defaultlib:msvcrt", "-out:" + binaryPath] + objects.map(\.fileSystemPath), diagnostics: &diagnostics) } /// Returns `self.outputURL` transformed as a suitable executable file path, using `productName` /// as a default name if `outputURL` is `nil`. private func executableOutputPath(default productName: String) -> String { - var binaryPath = outputURL?.path ?? URL(fileURLWithPath: productName).path + var binaryPath = outputURL?.path ?? URL(fileURLWithPath: productName).fileSystemPath if !binaryPath.hasSuffix(HostPlatform.executableSuffix) { binaryPath += HostPlatform.executableSuffix } @@ -360,29 +391,33 @@ public struct Driver: ParsableCommand { UNIMPLEMENTED() } - /// Returns the path of the specified executable. - private func find(_ executable: String) throws -> String { - if let path = Driver.executableLocationCache[executable] { return path } + /// Returns the path of the executable that is invoked at the command-line with the name given by + /// `invocationName`. + private func findExecutable(invokedAs invocationName: String) throws -> URL { + if let cached = Driver.executableLocationCache[invocationName] { return cached } + + let executableFileName + = invocationName.hasSuffix(executableSuffix) ? invocationName : invocationName + executableSuffix // Search in the current working directory. - var candidate = currentDirectory.appendingPathComponent(executable) - if FileManager.default.fileExists(atPath: candidate.path) { - Driver.executableLocationCache[executable] = candidate.path - return candidate.path + var candidate = currentDirectory.appendingPathComponent(executableFileName) + if FileManager.default.fileExists(atPath: candidate.fileSystemPath) { + Driver.executableLocationCache[invocationName] = candidate + return candidate } // Search in the PATH. let environment = ProcessInfo.processInfo.environment[HostPlatform.pathEnvironmentVariable] ?? "" for root in environment.split(separator: HostPlatform.pathEnvironmentSeparator) { - candidate = URL(fileURLWithPath: String(root)).appendingPathComponent(executable) - if FileManager.default.fileExists(atPath: candidate.path + HostPlatform.executableSuffix) { - Driver.executableLocationCache[executable] = candidate.path - return candidate.path + candidate = URL(fileURLWithPath: String(root)).appendingPathComponent(invocationName + HostPlatform.executableSuffix) + if FileManager.default.fileExists(atPath: candidate.fileSystemPath) { + Driver.executableLocationCache[invocationName] = candidate + return candidate } } - throw EnvironmentError("executable not found: \(executable)") + throw EnvironmentError("not found: executable invoked as \(invocationName)") } /// Runs the executable at `path`, passing `arguments` on the command line, and returns @@ -402,8 +437,8 @@ public struct Driver: ParsableCommand { return r.standardOutput.readUTF8().trimmingCharacters(in: .whitespacesAndNewlines) } - /// A map from executable name to path of the named binary. - private static var executableLocationCache: [String: String] = [:] + /// A map from the name by which an executable is invoked to path of the named binary. + private static var executableLocationCache: [String: URL] = [:] /// Writes a textual description of `input` to the given `output` file. func write(_ input: AST, to output: URL) throws { diff --git a/Sources/GenerateHyloFileTests/GenerateHyloFileTests.swift b/Sources/GenerateHyloFileTests/GenerateHyloFileTests.swift index 128eef177..395d0726b 100644 --- a/Sources/GenerateHyloFileTests/GenerateHyloFileTests.swift +++ b/Sources/GenerateHyloFileTests/GenerateHyloFileTests.swift @@ -4,6 +4,7 @@ import Utils /// A command-line tool that generates XCTest cases for a list of annotated ".hylo" /// files as part of our build process. +@main struct GenerateHyloFileTests: ParsableCommand { @Option( @@ -24,9 +25,7 @@ struct GenerateHyloFileTests: ParsableCommand { /// Returns the Swift source of the test function for the Hylo file at `source`. func swiftFunctionTesting(valAt source: URL) throws -> String { - let firstLine = - try String(contentsOf: source) - .split(separator: "\n", maxSplits: 1).first ?? "" + let firstLine = try String(contentsOf: source).prefix { !$0.isNewline } let parsed = try firstLine.parsedAsFirstLineOfAnnotatedHyloFileTest() let testID = source.deletingPathExtension().lastPathComponent.asSwiftIdentifier @@ -34,7 +33,7 @@ struct GenerateHyloFileTests: ParsableCommand { func test_\(parsed.methodName)_\(testID)() throws { try \(parsed.methodName)( - \(String(reflecting: source.path)), expectSuccess: \(parsed.expectSuccess)) + \(String(reflecting: source.fileSystemPath)), expectSuccess: \(parsed.expectSuccess)) } """ @@ -54,7 +53,7 @@ struct GenerateHyloFileTests: ParsableCommand { output += try swiftFunctionTesting(valAt: f) } catch let e as FirstLineError { try! FileHandle.standardError.write( - contentsOf: Data("\(f.path):1: error: \(e.details)\n".utf8)) + contentsOf: Data("\(f.fileSystemPath):1: error: \(e.details)\n".utf8)) GenerateHyloFileTests.exit(withError: ExitCode(-1)) } } diff --git a/Sources/GenerateHyloFileTests/main.swift b/Sources/GenerateHyloFileTests/main.swift deleted file mode 100644 index d94d589b2..000000000 --- a/Sources/GenerateHyloFileTests/main.swift +++ /dev/null @@ -1 +0,0 @@ -GenerateHyloFileTests.main() diff --git a/Sources/IR/Emitter.swift b/Sources/IR/Emitter.swift index 285c7b6cc..888d3697c 100644 --- a/Sources/IR/Emitter.swift +++ b/Sources/IR/Emitter.swift @@ -1492,7 +1492,7 @@ struct Emitter { let anchor = site ?? ast[e].site switch ast[e].kind { case .file: - emitStore(string: anchor.file.url.absoluteURL.path, to: storage, at: anchor) + emitStore(string: anchor.file.url.absoluteURL.fileSystemPath, to: storage, at: anchor) case .line: emitStore(int: anchor.first().line.number, to: storage, at: anchor) } diff --git a/Sources/TestUtils/AnnotatedHyloFileTest.swift b/Sources/TestUtils/AnnotatedHyloFileTest.swift index 87b3e9f3f..d56e42d9f 100644 --- a/Sources/TestUtils/AnnotatedHyloFileTest.swift +++ b/Sources/TestUtils/AnnotatedHyloFileTest.swift @@ -150,6 +150,7 @@ extension XCTestCase { /// Compiles and runs the val file at `hyloFilePath`, `XCTAssert`ing that diagnostics and exit /// codes match annotated expectations. public func compileAndRun(_ hyloFilePath: String, expectSuccess: Bool) throws { + if swiftyLLVMMandatoryPassesCrash { return } try checkAnnotatedHyloFileDiagnostics(inFileAt: hyloFilePath, expectSuccess: expectSuccess) { (hyloSource, diagnostics) in diff --git a/Sources/TestUtils/KnownBugs.swift b/Sources/TestUtils/KnownBugs.swift new file mode 100644 index 000000000..ffc293c7b --- /dev/null +++ b/Sources/TestUtils/KnownBugs.swift @@ -0,0 +1,7 @@ + +/// Whether Swifty-LLVM crashes when running mandatory passes: (https://github.com/hylo-lang/Swifty-LLVM/issues/24). +#if os(Windows) +public let swiftyLLVMMandatoryPassesCrash = true +#else +public let swiftyLLVMMandatoryPassesCrash = false +#endif diff --git a/Sources/TestUtils/TestAnnotation.swift b/Sources/TestUtils/TestAnnotation.swift index f4ad3d95d..04248abfe 100644 --- a/Sources/TestUtils/TestAnnotation.swift +++ b/Sources/TestUtils/TestAnnotation.swift @@ -1,5 +1,6 @@ import Core import XCTest +import Utils /// A test annotation in a source file. /// @@ -58,8 +59,7 @@ public struct TestAnnotation: Hashable { /// - Parameters: /// - location: The line location of the annotation. /// - body: A collection of characters representing an annotation body. - init(in url: URL, atLine line: Int, parsing body: S) - where S.Element == Character { + init(in url: URL, atLine line: Int, parsing body: S) { var s = body.drop(while: { $0.isWhitespace }) // Parse the line offset, if any. @@ -87,7 +87,7 @@ public struct TestAnnotation: Hashable { let indentation = s.prefix(while: { $0.isWhitespace && !$0.isNewline }) // Parse the argument. - let lines = s.split(separator: "\n") + let lines = s.lineContents() if lines.isEmpty { self.argument = nil } else { diff --git a/Sources/TestUtils/XCTest+Shims.swift b/Sources/TestUtils/XCTest+Shims.swift index f2d623293..8717b59c7 100644 --- a/Sources/TestUtils/XCTest+Shims.swift +++ b/Sources/TestUtils/XCTest+Shims.swift @@ -96,7 +96,7 @@ import XCTest let location = issue.sourceCodeContext!.location! recordFailure( withDescription: issue.compactDescription, - inFile: location.fileURL.path, atLine: location.lineNumber, + inFile: location.fileURL.fileSystemPath, atLine: location.lineNumber, expected: true) } diff --git a/Sources/Utils/HostOS.swift b/Sources/Utils/HostOS.swift new file mode 100644 index 000000000..f770a9c2b --- /dev/null +++ b/Sources/Utils/HostOS.swift @@ -0,0 +1,15 @@ +#if os(Windows) +/// The name of the environment variable containing the executable search path. +public let pathEnvironmentVariable = "Path" +/// The separator between elements of the executable search path. +public let pathEnvironmentSeparator: Character = ";" +/// The file extension applied to binary executables +public let executableSuffix = ".exe" +#else +/// The name of the environment variable containing the executable search path. +public let pathEnvironmentVariable = "PATH" +/// The separator between elements of the executable search path. +public let pathEnvironmentSeparator: Character = ":" +/// The file extension applied to binary executables +public let executableSuffix = "" +#endif diff --git a/Sources/Utils/String+Extensions.swift b/Sources/Utils/String+Extensions.swift index 17297c5ee..95ad8fc25 100644 --- a/Sources/Utils/String+Extensions.swift +++ b/Sources/Utils/String+Extensions.swift @@ -36,7 +36,7 @@ extension StringProtocol { public func lineBoundaries() -> [Index] { var r = [startIndex] var remainder = self[...] - while !remainder.isEmpty, let i = remainder.firstIndex(of: "\n") { + while !remainder.isEmpty, let i = remainder.firstIndex(where: \.isNewline) { let j = index(after: i) r.append(j) remainder = remainder[j...] @@ -44,6 +44,18 @@ extension StringProtocol { return r } + /// Returns each line of text in order, sans terminating newline. + public func lineContents() -> [SubSequence] { + var result: [SubSequence] = [] + var remainder = self[...] + while !remainder.isEmpty { + let i = remainder.firstIndex(where: \.isNewline) ?? remainder.endIndex + result.append(remainder[.. String { diff --git a/Sources/Utils/URL+Extensions.swift b/Sources/Utils/URL+Extensions.swift new file mode 100644 index 000000000..90e52224f --- /dev/null +++ b/Sources/Utils/URL+Extensions.swift @@ -0,0 +1,16 @@ +import Foundation + +extension URL { + + /// The path in the filesystem. + /// + /// - Precondition: `self` is a file scheme or file reference URL. + public var fileSystemPath: String { + self.standardizedFileURL.withUnsafeFileSystemRepresentation { (name: UnsafePointer?) in + FileManager().string( + withFileSystemRepresentation: name!, + length: (0...).first(where: { i in name![i] == 0 })!) + } + } + +} diff --git a/Tests/DriverTests/DriverTests.swift b/Tests/DriverTests/DriverTests.swift index ea82f3a86..819426f83 100644 --- a/Tests/DriverTests/DriverTests.swift +++ b/Tests/DriverTests/DriverTests.swift @@ -3,6 +3,7 @@ import Core import Driver import Utils import XCTest +import TestUtils final class DriverTests: XCTestCase { @@ -53,6 +54,8 @@ final class DriverTests: XCTestCase { } func testLLVM() throws { + if swiftyLLVMMandatoryPassesCrash { return } + let result = try compile(["--emit", "llvm"], newFile(containing: "public fun main() {}")) XCTAssert(result.status.isSuccess) result.checkDiagnosticText(is: "") @@ -60,6 +63,8 @@ final class DriverTests: XCTestCase { } func testIntelASM() throws { + if swiftyLLVMMandatoryPassesCrash { return } + let result = try compile(["--emit", "intel-asm"], newFile(containing: "public fun main() {}")) XCTAssert(result.status.isSuccess) result.checkDiagnosticText(is: "") @@ -67,6 +72,8 @@ final class DriverTests: XCTestCase { } func testBinary() throws { + if swiftyLLVMMandatoryPassesCrash { return } + let result = try compile(["--emit", "binary"], newFile(containing: "public fun main() {}")) XCTAssert(result.status.isSuccess) result.checkDiagnosticText(is: "") diff --git a/Tests/EndToEndTests/ExecutionTests.swift b/Tests/EndToEndTests/ExecutionTests.swift index 9e72a82ef..5739d25e2 100644 --- a/Tests/EndToEndTests/ExecutionTests.swift +++ b/Tests/EndToEndTests/ExecutionTests.swift @@ -4,6 +4,8 @@ import XCTest final class ExecutionTests: XCTestCase { func testHelloWorld() throws { + if swiftyLLVMMandatoryPassesCrash { return } + let f = FileManager.default.makeTemporaryFileURL() let s = #"public fun main() { print("Hello, World!") }"# try s.write(to: f, atomically: true, encoding: .utf8)