From 06f36e33e3b35d7a31e2b9ffdfea423f3521e1de Mon Sep 17 00:00:00 2001 From: stevapple Date: Wed, 7 Jul 2021 16:49:37 +0800 Subject: [PATCH 01/23] Simple package attribute parser --- Package.swift | 12 ++++ Sources/ScriptParse/Models.swift | 62 ++++++++++++++++++ Sources/ScriptParse/StatementKind.swift | 30 +++++++++ Sources/ScriptParse/main.swift | 83 +++++++++++++++++++++++++ Sources/ScriptParse/misc.swift | 40 ++++++++++++ 5 files changed, 227 insertions(+) create mode 100644 Sources/ScriptParse/Models.swift create mode 100644 Sources/ScriptParse/StatementKind.swift create mode 100644 Sources/ScriptParse/main.swift create mode 100644 Sources/ScriptParse/misc.swift diff --git a/Package.swift b/Package.swift index 3746404c47b..941b1683ca7 100644 --- a/Package.swift +++ b/Package.swift @@ -100,6 +100,10 @@ let package = Package( "PackageModel", ] ), + .executable( + name: "package-parser", + targets: ["ScriptParse"] + ), ], targets: [ // The `PackageDescription` target provides the API that is available @@ -186,6 +190,12 @@ let package = Package( // MARK: Package Manager Functionality + .target( + /** Parse `@package` marks */ + name: "ScriptParse", + dependencies: ["SwiftSyntax", "SwiftToolsSupport-auto"] + ), + .target( /** Builds Modules and Products */ name: "SPMBuildCore", @@ -356,6 +366,7 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.4.3")), .package(url: "https://github.com/apple/swift-driver.git", .branch(relatedDependenciesBranch)), .package(url: "https://github.com/apple/swift-crypto.git", .upToNextMinor(from: "1.1.4")), + .package(url: "https://github.com/apple/swift-syntax.git", .branch("release/5.5")), ] } else { package.dependencies += [ @@ -363,5 +374,6 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { .package(path: "../swift-argument-parser"), .package(path: "../swift-driver"), .package(path: "../swift-crypto"), + .package(path: "../swift-syntax"), ] } diff --git a/Sources/ScriptParse/Models.swift b/Sources/ScriptParse/Models.swift new file mode 100644 index 00000000000..5078e05dcdb --- /dev/null +++ b/Sources/ScriptParse/Models.swift @@ -0,0 +1,62 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors + + FIXME: This is a temporary alternative of the frontend implementation. +*/ + +import struct Foundation.URL +import TSCBasic +import SwiftSyntax + +struct PackageModel: Codable { + let raw: String + let path: String? + let url: URL? + + init(_ raw: String, path: String? = nil, url: String? = nil) { + self.raw = raw + self.path = path + if let url = url { + self.url = URL(string: url) + } else { + self.url = nil + } + } +} + +struct PackageDependency: Codable { + let package: PackageModel + var modules: [String] = [] + + init(of package: PackageModel) { + self.package = package + } +} + +struct ScriptDependencies: Codable { + let sourceFile: AbsolutePath + let modules: [PackageDependency] +} + +enum ScriptParseError: Swift.Error, CustomStringConvertible { + case wrongSyntax + case unsupportedSyntax + case noFileSpecified + + var description: String { + switch self { + case .wrongSyntax: + return "Syntax error" + case .unsupportedSyntax: + return "Unsupported import syntax" + case .noFileSpecified: + return "Please specify a file" + } + } +} diff --git a/Sources/ScriptParse/StatementKind.swift b/Sources/ScriptParse/StatementKind.swift new file mode 100644 index 00000000000..92036dfc1c0 --- /dev/null +++ b/Sources/ScriptParse/StatementKind.swift @@ -0,0 +1,30 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors + + FIXME: This is a temporary alternative of the frontend implementation. +*/ + +import SwiftSyntax + +enum StatementKind { + case package + case `import` + case others +} + +extension CodeBlockItemSyntax { + var statementKind: StatementKind { + let tokens = tokens.map(\.tokenKind) + if tokens.starts(with: [.importKeyword]) { + return .import + } else if tokens.starts(with: [.atSign, .identifier("package")]) { + return .package + } else { return .others } + } +} diff --git a/Sources/ScriptParse/main.swift b/Sources/ScriptParse/main.swift new file mode 100644 index 00000000000..5cb504a13a7 --- /dev/null +++ b/Sources/ScriptParse/main.swift @@ -0,0 +1,83 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors + + FIXME: This is a temporary alternative of the frontend implementation. +*/ + +import SwiftSyntax +import TSCBasic +import Foundation + +guard CommandLine.argc > 1 else { + throw ScriptParseError.noFileSpecified +} + +let path = try AbsolutePath(validating: CommandLine.arguments[1]) +let syntaxTree = try SyntaxParser.parse(path.asURL) +let converter = SourceLocationConverter(file: path.basename, tree: syntaxTree) + +var inPackageScope = false +var keyStatements: [CodeBlockItemSyntax] = [] +for statement in syntaxTree.statements { + if statement.statementKind == .import && inPackageScope { + keyStatements.append(statement) + } else { + inPackageScope = false + } + if statement.statementKind == .package + && statement.nextToken?.tokenKind == .importKeyword { + inPackageScope = true + keyStatements.append(statement) + } +} + +var collected: [PackageDependency] = [] + +try keyStatements.forEach { + switch $0.statementKind { + case .package: + var tokens = [TokenSyntax]($0.tokens.dropFirst(2)) + guard tokens.first?.tokenKind == .leftParen, + tokens.last?.tokenKind == .rightParen else { + throw ScriptParseError.wrongSyntax + } + tokens.removeFirst() + tokens.removeLast() + let desc = tokens.map(\.text).joined() + // parse the first argument + if let path = try parseStringArgument(&tokens, label: "path") { + collected.append(PackageDependency(of: PackageModel(desc, path: path))) + } else if let url = try parseStringArgument(&tokens, label: "url") { + collected.append(PackageDependency(of: PackageModel(desc, url: url))) + } + // TODO: other parsing + else { + collected.append(PackageDependency(of: PackageModel(desc))) + } + + case .import: + let tokens = [TokenSyntax]($0.tokens.dropFirst()) + guard tokens.count == 1, + let moduleToken = tokens.first, + case .identifier(let moduleName) = moduleToken.tokenKind + else { throw ScriptParseError.unsupportedSyntax } + var model = collected.removeLast() + model.modules.append(moduleName) + collected.append(model) + + default: + fatalError() + } +} + +let encoder = JSONEncoder() +encoder.outputFormatting = .prettyPrinted + +let json = try encoder.encode(ScriptDependencies(sourceFile: path, modules: collected)) +print(String(data: json, encoding: .utf8)!) diff --git a/Sources/ScriptParse/misc.swift b/Sources/ScriptParse/misc.swift new file mode 100644 index 00000000000..d2c5f11b058 --- /dev/null +++ b/Sources/ScriptParse/misc.swift @@ -0,0 +1,40 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors + + FIXME: This is a temporary alternative of the frontend implementation. +*/ + +import SwiftSyntax +import Foundation + +func parseStringArgument(_ tokens: inout [TokenSyntax], label: String? = nil) throws -> String? { + if let label = label { + // parse label + guard case .identifier(let labelString) = tokens.first?.tokenKind, + labelString == label else { + return nil + } + tokens.removeFirst() + // parse colon + guard case .colon = tokens.removeFirst().tokenKind else { + throw ScriptParseError.wrongSyntax + } + } + guard case .stringLiteral(let string) = tokens.removeFirst().tokenKind else { + throw ScriptParseError.wrongSyntax + } + return string.unescaped() +} + +private extension String { + func unescaped() -> String { + let data = data(using: .utf8)! + return try! JSONDecoder().decode(String.self, from: data) + } +} From 36180cc9fe6f2b7af75e73a5ed5f7c214f35ae5b Mon Sep 17 00:00:00 2001 From: stevapple Date: Wed, 28 Jul 2021 00:29:03 +0800 Subject: [PATCH 02/23] Enhance package-parser --- Package.swift | 10 ++- Sources/ScriptParse/Models.swift | 12 ++- Sources/ScriptParse/ScriptParse.swift | 106 ++++++++++++++++++++++++++ Sources/ScriptParse/main.swift | 83 -------------------- Sources/package-parser/main.swift | 15 ++++ 5 files changed, 133 insertions(+), 93 deletions(-) create mode 100644 Sources/ScriptParse/ScriptParse.swift delete mode 100644 Sources/ScriptParse/main.swift create mode 100644 Sources/package-parser/main.swift diff --git a/Package.swift b/Package.swift index 941b1683ca7..4e2058c7c31 100644 --- a/Package.swift +++ b/Package.swift @@ -102,7 +102,7 @@ let package = Package( ), .executable( name: "package-parser", - targets: ["ScriptParse"] + targets: ["package-parser"] ), ], targets: [ @@ -193,7 +193,7 @@ let package = Package( .target( /** Parse `@package` marks */ name: "ScriptParse", - dependencies: ["SwiftSyntax", "SwiftToolsSupport-auto"] + dependencies: ["SwiftSyntax", "SwiftToolsSupport-auto", "ArgumentParser"] ), .target( @@ -223,7 +223,7 @@ let package = Package( .target( /** High-level commands */ name: "Commands", - dependencies: ["SwiftToolsSupport-auto", "Basics", "Build", "PackageGraph", "SourceControl", "Xcodeproj", "Workspace", "XCBuildSupport", "ArgumentParser", "PackageCollections"]), + dependencies: ["SwiftToolsSupport-auto", "Basics", "Build", "PackageGraph", "SourceControl", "Xcodeproj", "Workspace", "XCBuildSupport", "ArgumentParser", "PackageCollections", "ScriptParse"]), .target( /** The main executable provided by SwiftPM */ name: "swift-package", @@ -240,6 +240,10 @@ let package = Package( /** Runs an executable product */ name: "swift-run", dependencies: ["Commands"]), + .target( + /** Manages and runs a script */ + name: "package-parser", + dependencies: ["ScriptParse"]), .target( /** Interacts with package collections */ name: "swift-package-collection", diff --git a/Sources/ScriptParse/Models.swift b/Sources/ScriptParse/Models.swift index 5078e05dcdb..aa9bbd9185f 100644 --- a/Sources/ScriptParse/Models.swift +++ b/Sources/ScriptParse/Models.swift @@ -16,17 +16,15 @@ import SwiftSyntax struct PackageModel: Codable { let raw: String - let path: String? + let path: AbsolutePath? let url: URL? + let name: String? - init(_ raw: String, path: String? = nil, url: String? = nil) { + init(_ raw: String, path: AbsolutePath? = nil, url: URL? = nil, name: String? = nil) { self.raw = raw self.path = path - if let url = url { - self.url = URL(string: url) - } else { - self.url = nil - } + self.url = url + self.name = name } } diff --git a/Sources/ScriptParse/ScriptParse.swift b/Sources/ScriptParse/ScriptParse.swift new file mode 100644 index 00000000000..43f11c44240 --- /dev/null +++ b/Sources/ScriptParse/ScriptParse.swift @@ -0,0 +1,106 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors + + FIXME: This is a temporary alternative of the frontend implementation. +*/ + +import SwiftSyntax +import TSCBasic +import Foundation +import ArgumentParser + +public struct ScriptParse: ParsableCommand { + @Argument var file: String + + static let encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + return encoder + }() + + public init() {} +} + +extension ScriptParse { + static func parse(_ path: AbsolutePath) throws -> ScriptDependencies { + let syntaxTree = try SyntaxParser.parse(path.asURL) +// let converter = SourceLocationConverter(file: path.basename, tree: syntaxTree) + + var inPackageScope = false + var keyStatements: [CodeBlockItemSyntax] = [] + for statement in syntaxTree.statements { + if statement.statementKind == .import && inPackageScope { + keyStatements.append(statement) + } else { + inPackageScope = false + } + if statement.statementKind == .package + && statement.nextToken?.tokenKind == .importKeyword { + inPackageScope = true + keyStatements.append(statement) + } + } + + var collected: [PackageDependency] = [] + + try keyStatements.forEach { + switch $0.statementKind { + case .package: + var tokens = [TokenSyntax]($0.tokens.dropFirst(2)) + guard tokens.first?.tokenKind == .leftParen, + tokens.last?.tokenKind == .rightParen else { + throw ScriptParseError.wrongSyntax + } + tokens.removeFirst() + tokens.removeLast() + let desc = tokens.map(\.text).joined() + // parse the first argument + if let _path = try parseStringArgument(&tokens, label: "path"), + case let path = AbsolutePath(_path, relativeTo: path.parentDirectory) { + let name = try parseStringArgument(&tokens, label: "name") + collected.append(PackageDependency(of: PackageModel(desc.replacingOccurrences(of: _path, with: path.pathString), path: path, name: name))) + } else if let _url = try parseStringArgument(&tokens, label: "url"), + let url = URL(string: _url) { + collected.append(PackageDependency(of: PackageModel(desc, url: url))) + } + // TODO: other parsing + else { + collected.append(PackageDependency(of: PackageModel(desc))) + } + + case .import: + let tokens = [TokenSyntax]($0.tokens.dropFirst()) + guard tokens.count == 1, + let moduleToken = tokens.first, + case .identifier(let moduleName) = moduleToken.tokenKind + else { throw ScriptParseError.unsupportedSyntax } + var model = collected.removeLast() + model.modules.append(moduleName) + collected.append(model) + + default: + fatalError() + } + } + return ScriptDependencies(sourceFile: path, modules: collected) + } + + public func run() throws { + let path = try AbsolutePath(validating: file) + let json = try ScriptParse.manifest(for: path) + print(String(data: json, encoding: .utf8)!) + } + + // FIXME: This should be removed some day. + public static func manifest(for path: AbsolutePath) throws -> Data { + let output = try parse(path) + let json = try encoder.encode(output) + return json + } +} diff --git a/Sources/ScriptParse/main.swift b/Sources/ScriptParse/main.swift deleted file mode 100644 index 5cb504a13a7..00000000000 --- a/Sources/ScriptParse/main.swift +++ /dev/null @@ -1,83 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See http://swift.org/LICENSE.txt for license information - See http://swift.org/CONTRIBUTORS.txt for Swift project authors - - FIXME: This is a temporary alternative of the frontend implementation. -*/ - -import SwiftSyntax -import TSCBasic -import Foundation - -guard CommandLine.argc > 1 else { - throw ScriptParseError.noFileSpecified -} - -let path = try AbsolutePath(validating: CommandLine.arguments[1]) -let syntaxTree = try SyntaxParser.parse(path.asURL) -let converter = SourceLocationConverter(file: path.basename, tree: syntaxTree) - -var inPackageScope = false -var keyStatements: [CodeBlockItemSyntax] = [] -for statement in syntaxTree.statements { - if statement.statementKind == .import && inPackageScope { - keyStatements.append(statement) - } else { - inPackageScope = false - } - if statement.statementKind == .package - && statement.nextToken?.tokenKind == .importKeyword { - inPackageScope = true - keyStatements.append(statement) - } -} - -var collected: [PackageDependency] = [] - -try keyStatements.forEach { - switch $0.statementKind { - case .package: - var tokens = [TokenSyntax]($0.tokens.dropFirst(2)) - guard tokens.first?.tokenKind == .leftParen, - tokens.last?.tokenKind == .rightParen else { - throw ScriptParseError.wrongSyntax - } - tokens.removeFirst() - tokens.removeLast() - let desc = tokens.map(\.text).joined() - // parse the first argument - if let path = try parseStringArgument(&tokens, label: "path") { - collected.append(PackageDependency(of: PackageModel(desc, path: path))) - } else if let url = try parseStringArgument(&tokens, label: "url") { - collected.append(PackageDependency(of: PackageModel(desc, url: url))) - } - // TODO: other parsing - else { - collected.append(PackageDependency(of: PackageModel(desc))) - } - - case .import: - let tokens = [TokenSyntax]($0.tokens.dropFirst()) - guard tokens.count == 1, - let moduleToken = tokens.first, - case .identifier(let moduleName) = moduleToken.tokenKind - else { throw ScriptParseError.unsupportedSyntax } - var model = collected.removeLast() - model.modules.append(moduleName) - collected.append(model) - - default: - fatalError() - } -} - -let encoder = JSONEncoder() -encoder.outputFormatting = .prettyPrinted - -let json = try encoder.encode(ScriptDependencies(sourceFile: path, modules: collected)) -print(String(data: json, encoding: .utf8)!) diff --git a/Sources/package-parser/main.swift b/Sources/package-parser/main.swift new file mode 100644 index 00000000000..aac12531698 --- /dev/null +++ b/Sources/package-parser/main.swift @@ -0,0 +1,15 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors + + FIXME: This is a temporary alternative of the frontend implementation. +*/ + +import ScriptParse + +ScriptParse.main() From 0e50e43428277d59eddf1b8f06f4ef38a85ae224 Mon Sep 17 00:00:00 2001 From: stevapple Date: Wed, 28 Jul 2021 00:30:09 +0800 Subject: [PATCH 03/23] Introduce swift-script --- Package.swift | 10 +- Sources/Commands/SwiftScriptTool.swift | 138 +++++++++++++++++++++++++ Sources/Commands/SwiftTool.swift | 12 ++- Sources/ScriptingCore/Models.swift | 55 ++++++++++ Sources/ScriptingCore/Options.swift | 10 ++ Sources/ScriptingCore/utils.swift | 113 ++++++++++++++++++++ Sources/swift-script/CMakeLists.txt | 17 +++ Sources/swift-script/main.swift | 13 +++ 8 files changed, 365 insertions(+), 3 deletions(-) create mode 100644 Sources/Commands/SwiftScriptTool.swift create mode 100644 Sources/ScriptingCore/Models.swift create mode 100644 Sources/ScriptingCore/Options.swift create mode 100644 Sources/ScriptingCore/utils.swift create mode 100644 Sources/swift-script/CMakeLists.txt create mode 100644 Sources/swift-script/main.swift diff --git a/Package.swift b/Package.swift index 4e2058c7c31..3415d7c7cde 100644 --- a/Package.swift +++ b/Package.swift @@ -156,6 +156,10 @@ let package = Package( /** Package model conventions and loading support */ name: "PackageLoading", dependencies: ["SwiftToolsSupport-auto", "Basics", "PackageModel", "SourceControl"]), + .target( + name: "ScriptingCore", + /** Package models for scripting support */ + dependencies: ["SwiftToolsSupport-auto", "Basics"]), // MARK: Package Dependency Resolution @@ -223,7 +227,7 @@ let package = Package( .target( /** High-level commands */ name: "Commands", - dependencies: ["SwiftToolsSupport-auto", "Basics", "Build", "PackageGraph", "SourceControl", "Xcodeproj", "Workspace", "XCBuildSupport", "ArgumentParser", "PackageCollections", "ScriptParse"]), + dependencies: ["SwiftToolsSupport-auto", "Basics", "Build", "PackageGraph", "SourceControl", "Xcodeproj", "Workspace", "XCBuildSupport", "ArgumentParser", "PackageCollections", "ScriptParse", "ScriptingCore"]), .target( /** The main executable provided by SwiftPM */ name: "swift-package", @@ -240,6 +244,10 @@ let package = Package( /** Runs an executable product */ name: "swift-run", dependencies: ["Commands"]), + .target( + /** Manages and runs a script */ + name: "swift-script", + dependencies: ["Commands"]), .target( /** Manages and runs a script */ name: "package-parser", diff --git a/Sources/Commands/SwiftScriptTool.swift b/Sources/Commands/SwiftScriptTool.swift new file mode 100644 index 00000000000..9777380e375 --- /dev/null +++ b/Sources/Commands/SwiftScriptTool.swift @@ -0,0 +1,138 @@ +/* +This source file is part of the Swift.org open source project + +Copyright 2015 - 2016 Apple Inc. and the Swift project authors +Licensed under Apache License v2.0 with Runtime Library Exception + +See http://swift.org/LICENSE.txt for license information +See http://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import ArgumentParser +import Basics +import Build +import PackageGraph +import PackageModel +import TSCBasic +import ScriptParse +import ScriptingCore +import Foundation + +public extension SwiftToolOptions { + func redirect(to dir: AbsolutePath) throws -> SwiftToolOptions { + var options = self + options.packagePath = dir + options.buildPath = nil + try options.validate() + return options + } +} + +struct ScriptToolOptions: ParsableArguments { + /// If the executable product should be built before running. + @Flag(name: .customLong("skip-build"), help: "Skip building the executable product") + var shouldSkipBuild: Bool = false + + /// Whether to print build progress. + @Flag(help: "Print build progress") + var quiet: Bool = false + + var shouldBuild: Bool { !shouldSkipBuild } + + /// The script file to run. + @Argument(help: "The script file to run") + var file: String? + + /// The arguments to pass to the executable. + @Argument(parsing: .unconditionalRemaining, + help: "The arguments to pass to the executable") + var arguments: [String] = [] +} + +/// swift-script tool namespace +public struct SwiftScriptTool: ParsableCommand { + public static var configuration = CommandConfiguration( + commandName: "script", + _superCommandName: "swift", + abstract: "Manage and run Swift scripts", + discussion: "SEE ALSO: swift build, swift run, swift package, swift test", + version: SwiftVersion.currentVersion.completeDisplayString, + subcommands: [ + Run.self, + ], + helpNames: [.short, .long, .customLong("help", withSingleDash: true)]) + +// @OptionGroup() +// var swiftOptions: SwiftToolOptions +// +// @OptionGroup() +// var options: ScriptToolOptions + + public init() {} + + public static var _errorLabel: String { "error" } +} + +extension SwiftScriptTool { + static var cacheDir: AbsolutePath { localFileSystem.dotSwiftPM.appending(component: "scripts") } +} + +/// swift-run tool namespace +extension SwiftScriptTool { + struct Run: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Runs a script") + + @OptionGroup(_hiddenFromHelp: true) + var swiftOptions: SwiftToolOptions + + @OptionGroup() + var options: ScriptToolOptions + + func run() throws { + guard let file = options.file else { + throw ScriptError.fileNotFound("") + } + let (productName, cacheDirPath) = try prepareCache(for: file, at: SwiftScriptTool.cacheDir) + + let swiftTool = try SwiftTool(options: swiftOptions.redirect(to: cacheDirPath)) + swiftTool.redirectStdoutToStderr() + let output = BufferedOutputByteStream() + if options.quiet { + swiftTool.redirectStdoutTo(.init(output)) + } + + do { + let buildSystem = try swiftTool.createBuildSystem(explicitProduct: nil) + if options.shouldBuild { + try buildSystem.build(subset: .product(productName)) + } + + let executablePath = try swiftTool.buildParameters().buildPath.appending(component: productName) + try Commands.run(executablePath, + originalWorkingDirectory: swiftTool.originalWorkingDirectory, + arguments: options.arguments) + } catch let error as ScriptError { + swiftTool.diagnostics.emit(error) + stderrStream <<< output.bytes + stderrStream.flush() + throw ExitCode.failure + } + } + } +} + +/// Executes the executable at the specified path. +fileprivate func run( + _ excutablePath: AbsolutePath, + originalWorkingDirectory: AbsolutePath, + arguments: [String]) throws { + // Make sure we are running from the original working directory. + let cwd: AbsolutePath? = localFileSystem.currentWorkingDirectory + if cwd == nil || originalWorkingDirectory != cwd { + try ProcessEnv.chdir(originalWorkingDirectory) + } + + let pathRelativeToWorkingDirectory = excutablePath.relative(to: originalWorkingDirectory) + try exec(path: excutablePath.pathString, args: [pathRelativeToWorkingDirectory.pathString] + arguments) +} diff --git a/Sources/Commands/SwiftTool.swift b/Sources/Commands/SwiftTool.swift index 6eae2db5805..92230a30797 100644 --- a/Sources/Commands/SwiftTool.swift +++ b/Sources/Commands/SwiftTool.swift @@ -336,6 +336,9 @@ public class SwiftTool { /// The stream to print standard output on. fileprivate(set) var stdoutStream: OutputByteStream = TSCBasic.stdoutStream + + /// Whether not to print the stream if there's no error. + var quiet: Bool = false /// Holds the currently active workspace. /// @@ -574,8 +577,13 @@ public class SwiftTool { /// Start redirecting the standard output stream to the standard error stream. func redirectStdoutToStderr() { - self.stdoutStream = TSCBasic.stderrStream - DiagnosticsEngineHandler.default.stdoutStream = TSCBasic.stderrStream + redirectStdoutTo(TSCBasic.stderrStream) + } + + /// Start redirecting the standard output stream to another stream. + func redirectStdoutTo(_ stream: ThreadSafeOutputByteStream) { + self.stdoutStream = stream + DiagnosticsEngineHandler.default.stdoutStream = stream } /// Resolve the dependencies. diff --git a/Sources/ScriptingCore/Models.swift b/Sources/ScriptingCore/Models.swift new file mode 100644 index 00000000000..353bbf774f2 --- /dev/null +++ b/Sources/ScriptingCore/Models.swift @@ -0,0 +1,55 @@ +// +// File.swift +// File +// +// Created by C YR on 2021/7/19. +// + +import Foundation +import TSCBasic +import TSCUtility + +public struct PackageModel: Codable { + public let raw: String + public let url: Foundation.URL? + public let path: AbsolutePath? + public let _name: String? + + public var name: String { + if let name = _name { + return name + } + let name: String + if let path = path { + name = path.basename + } else if let url = url { + name = url.pathComponents.last!.spm_dropGitSuffix() + } else { + fatalError() + } + return name + } +} + +public struct PackageDependency: Codable { + public let package: PackageModel + public var modules: [String] = [] + + init(of package: PackageModel) { + self.package = package + } +} + +public struct ScriptDependencies: Codable { + public let sourceFile: AbsolutePath + public let modules: [PackageDependency] +} + +public extension PackageModel { + enum CodingKeys: String, CodingKey { + case raw + case url + case path + case _name = "name" + } +} diff --git a/Sources/ScriptingCore/Options.swift b/Sources/ScriptingCore/Options.swift new file mode 100644 index 00000000000..8a96cfc0363 --- /dev/null +++ b/Sources/ScriptingCore/Options.swift @@ -0,0 +1,10 @@ +// +// File.swift +// File +// +// Created by C YR on 2021/7/19. +// + +import TSCBasic + + diff --git a/Sources/ScriptingCore/utils.swift b/Sources/ScriptingCore/utils.swift new file mode 100644 index 00000000000..1632ec21755 --- /dev/null +++ b/Sources/ScriptingCore/utils.swift @@ -0,0 +1,113 @@ +/* +This source file is part of the Swift.org open source project + +Copyright 2015 - 2016 Apple Inc. and the Swift project authors +Licensed under Apache License v2.0 with Runtime Library Exception + +See http://swift.org/LICENSE.txt for license information +See http://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import Basics +import Foundation +import ScriptParse +import TSCBasic + +fileprivate extension AbsolutePath { + var sha256Hash: String { + String(ByteString(stringLiteral: pathString) + .sha256Checksum.prefix(6)) + } +} + +/// An enumeration of the errors that can be generated by the run tool. +public enum ScriptError: Swift.Error { + /// The specified file doesn't exist. + case fileNotFound(String) +} + +extension ScriptError: CustomStringConvertible { + public var description: String { + switch self { + case .fileNotFound(let path): + return "\(path) doesn't exist" + } + } +} + +/// Resolves a path string to `AbsolutePath`. +public func resolveFilePath(_ path: String) -> AbsolutePath? { + let absolutePath: AbsolutePath + if path.first == "/" { + absolutePath = AbsolutePath(path) + } else { + guard let cwd = localFileSystem.currentWorkingDirectory else { + return nil + } + absolutePath = AbsolutePath(cwd, path) + } + guard localFileSystem.isFile(absolutePath) else { + return nil + } + return absolutePath +} + +public func prepareCache(for file: String, at dirPath: AbsolutePath) throws -> (productName: String, cacheDirPath: AbsolutePath) { + if let scriptPath = resolveFilePath(file) { + let json = try ScriptParse.manifest(for: scriptPath) + let decoder = JSONDecoder() + let manifest = try decoder.decode(ScriptDependencies.self, from: json) + + let productName = scriptPath.basename.spm_dropSuffix(".swift").spm_mangledToBundleIdentifier() + let cacheDirPath = dirPath.appending(component: "\(productName)-\(scriptPath.sha256Hash)") + try localFileSystem.createDirectory(cacheDirPath, recursive: true) + + let sourceDirPath = cacheDirPath.appending(components: "Sources", productName) + try localFileSystem.createDirectory(sourceDirPath, recursive: true) + if !localFileSystem.exists(sourceDirPath.appending(component: "main.swift")) { + try localFileSystem.createSymbolicLink(sourceDirPath.appending(component: "main.swift"), pointingAt: scriptPath, relative: false) + } + + var targets: [(name: String, package: String)] = [] + for package in manifest.modules { + let packageName = package.package.name + for target in package.modules { + targets.append((target, packageName)) + } + } + + let packageSwift = """ + // swift-tools-version:5.4 + import PackageDescription + let package = Package( + name: "\(productName)", + products: [ + .executable( + name: "\(productName)", + targets: ["\(productName)"]), + ], + dependencies: [\(manifest.modules.map{".package(\($0.package.raw))"} + .joined(separator: ", "))], + targets: [ + .executableTarget( + name: "\(productName)", + dependencies: [\(targets.map {".product\($0)"}.joined(separator: ", "))], + swiftSettings: [.unsafeFlags(["-Xfrontend", "-ignore-package-declarations"])] + )] + ) + + """ + + try localFileSystem.writeIfChanged( + path: cacheDirPath.appending(component: "Package.swift"), + bytes: ByteString(stringLiteral: packageSwift) + ) + return (productName, cacheDirPath) + } else { + let cacheDirPath = dirPath.appending(component: file) + guard localFileSystem.isDirectory(cacheDirPath) else { + throw ScriptError.fileNotFound(file) + } + return (String(file.dropLast(7)), cacheDirPath) + } +} diff --git a/Sources/swift-script/CMakeLists.txt b/Sources/swift-script/CMakeLists.txt new file mode 100644 index 00000000000..d033ae9c788 --- /dev/null +++ b/Sources/swift-script/CMakeLists.txt @@ -0,0 +1,17 @@ +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors +# Licensed under Apache License v2.0 with Runtime Library Exception +# +# See http://swift.org/LICENSE.txt for license information +# See http://swift.org/CONTRIBUTORS.txt for Swift project authors + +add_executable(swift-script + main.swift) +target_link_libraries(swift-script PRIVATE + Commands) + +if(USE_CMAKE_INSTALL) +install(TARGETS swift-script + RUNTIME DESTINATION bin) +endif() diff --git a/Sources/swift-script/main.swift b/Sources/swift-script/main.swift new file mode 100644 index 00000000000..003476492a3 --- /dev/null +++ b/Sources/swift-script/main.swift @@ -0,0 +1,13 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import Commands + +SwiftScriptTool.main() From d2f747eb8d9fefe9a15621edd28b4c729cf77357 Mon Sep 17 00:00:00 2001 From: stevapple Date: Mon, 2 Aug 2021 23:35:33 +0800 Subject: [PATCH 04/23] Add some more commands --- Package.swift | 2 +- Sources/Commands/SwiftScriptTool.swift | 229 ++++++++++++++++++++++--- Sources/Commands/SwiftTool.swift | 29 ++++ Sources/ScriptingCore/utils.swift | 2 +- 4 files changed, 233 insertions(+), 29 deletions(-) diff --git a/Package.swift b/Package.swift index 3415d7c7cde..c00d9810e80 100644 --- a/Package.swift +++ b/Package.swift @@ -378,7 +378,7 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.4.3")), .package(url: "https://github.com/apple/swift-driver.git", .branch(relatedDependenciesBranch)), .package(url: "https://github.com/apple/swift-crypto.git", .upToNextMinor(from: "1.1.4")), - .package(url: "https://github.com/apple/swift-syntax.git", .branch("release/5.5")), + .package(url: "https://github.com/apple/swift-syntax.git", .branch(relatedDependenciesBranch)), ] } else { package.dependencies += [ diff --git a/Sources/Commands/SwiftScriptTool.swift b/Sources/Commands/SwiftScriptTool.swift index 9777380e375..b539d4cc440 100644 --- a/Sources/Commands/SwiftScriptTool.swift +++ b/Sources/Commands/SwiftScriptTool.swift @@ -10,32 +10,16 @@ See http://swift.org/CONTRIBUTORS.txt for Swift project authors import ArgumentParser import Basics -import Build -import PackageGraph import PackageModel import TSCBasic import ScriptParse import ScriptingCore -import Foundation - -public extension SwiftToolOptions { - func redirect(to dir: AbsolutePath) throws -> SwiftToolOptions { - var options = self - options.packagePath = dir - options.buildPath = nil - try options.validate() - return options - } -} +import Workspace struct ScriptToolOptions: ParsableArguments { /// If the executable product should be built before running. @Flag(name: .customLong("skip-build"), help: "Skip building the executable product") var shouldSkipBuild: Bool = false - - /// Whether to print build progress. - @Flag(help: "Print build progress") - var quiet: Bool = false var shouldBuild: Bool { !shouldSkipBuild } @@ -59,6 +43,10 @@ public struct SwiftScriptTool: ParsableCommand { version: SwiftVersion.currentVersion.completeDisplayString, subcommands: [ Run.self, + Build.self, + Clean.self, + Reset.self, + Resolve.self, ], helpNames: [.short, .long, .customLong("help", withSingleDash: true)]) @@ -79,7 +67,7 @@ extension SwiftScriptTool { /// swift-run tool namespace extension SwiftScriptTool { - struct Run: ParsableCommand { + struct Run: ScriptCommand { static let configuration = CommandConfiguration( abstract: "Runs a script") @@ -88,21 +76,22 @@ extension SwiftScriptTool { @OptionGroup() var options: ScriptToolOptions - - func run() throws { - guard let file = options.file else { - throw ScriptError.fileNotFound("") - } - let (productName, cacheDirPath) = try prepareCache(for: file, at: SwiftScriptTool.cacheDir) - let swiftTool = try SwiftTool(options: swiftOptions.redirect(to: cacheDirPath)) - swiftTool.redirectStdoutToStderr() + /// Whether to print build progress. + @Flag(help: "Print build progress") + var quiet: Bool = false + + func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws { let output = BufferedOutputByteStream() - if options.quiet { + if quiet { swiftTool.redirectStdoutTo(.init(output)) + } else { + swiftTool.redirectStdoutToStderr() } do { + // FIXME: How to hide the note? + swiftTool.diagnostics.emit(note: "Using cache: \(cacheDirPath.basename)") let buildSystem = try swiftTool.createBuildSystem(explicitProduct: nil) if options.shouldBuild { try buildSystem.build(subset: .product(productName)) @@ -120,6 +109,164 @@ extension SwiftScriptTool { } } } + + struct Build: ScriptCommand { + static let configuration = CommandConfiguration( + abstract: "Prebuild a script") + + @OptionGroup(_hiddenFromHelp: true) + var swiftOptions: SwiftToolOptions + + @OptionGroup() + var options: ScriptToolOptions + + func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws { + swiftTool.redirectStdoutToStderr() + + do { + swiftTool.diagnostics.emit(note: "Using cache: \(cacheDirPath.basename)") + let buildSystem = try swiftTool.createBuildSystem(explicitProduct: nil) + if options.shouldBuild { + try buildSystem.build(subset: .product(productName)) + } + } catch let error as ScriptError { + swiftTool.diagnostics.emit(error) + throw ExitCode.failure + } + } + } +} + +extension SwiftScriptTool { + struct Clean: ScriptCommand { + static let configuration = CommandConfiguration( + abstract: "Delete build artifacts") + + @OptionGroup(_hiddenFromHelp: true) + var swiftOptions: SwiftToolOptions + + @OptionGroup() + var options: ScriptToolOptions + + func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws { + try swiftTool.getActiveWorkspace().clean(with: swiftTool.diagnostics) + } + } + + struct Reset: ScriptCommand { + static let configuration = CommandConfiguration( + abstract: "Reset the complete cache directory") + + @OptionGroup(_hiddenFromHelp: true) + var swiftOptions: SwiftToolOptions + + @OptionGroup() + var options: ScriptToolOptions + + func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws { + try localFileSystem.removeFileTree(cacheDirPath) + } + } + + struct Update: ScriptCommand { + static let configuration = CommandConfiguration( + abstract: "Update package dependencies") + + @OptionGroup(_hiddenFromHelp: true) + var swiftOptions: SwiftToolOptions + + @OptionGroup() + var options: ScriptToolOptions + + @Flag(name: [.long, .customShort("n")], + help: "Display the list of dependencies that can be updated") + var dryRun: Bool = false + + @Argument(help: "The packages to update") + var packages: [String] = [] + + func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws { + let workspace = try swiftTool.getActiveWorkspace() + + let changes = try workspace.updateDependencies( + root: swiftTool.getWorkspaceRoot(), + packages: packages, + diagnostics: swiftTool.diagnostics, + dryRun: dryRun + ) + + // try to load the graph which will emit any errors + if !swiftTool.diagnostics.hasErrors { + _ = try workspace.loadPackageGraph( + rootInput: swiftTool.getWorkspaceRoot(), + diagnostics: swiftTool.diagnostics + ) + } + + if let pinsStore = swiftTool.diagnostics.wrap({ try workspace.pinsStore.load() }), + let changes = changes, dryRun { + logPackageChanges(changes: changes, pins: pinsStore) + } + + if !dryRun { + // Throw if there were errors when loading the graph. + // The actual errors will be printed before exiting. + guard !swiftTool.diagnostics.hasErrors else { + throw ExitCode.failure + } + } + } + } +} + +extension SwiftScriptTool { + struct ResolveOptions: ParsableArguments { + @Option(help: "The version to resolve at", transform: { Version(string: $0) }) + var version: Version? + + @Option(help: "The branch to resolve at") + var branch: String? + + @Option(help: "The revision to resolve at") + var revision: String? + + @Argument(help: "The name of the package to resolve") + var packageName: String? + } + + struct Resolve: ScriptCommand { + static let configuration = CommandConfiguration( + abstract: "Resolve package dependencies") + + @OptionGroup(_hiddenFromHelp: true) + var swiftOptions: SwiftToolOptions + + @OptionGroup() + var options: ScriptToolOptions + + @OptionGroup() + var resolveOptions: ResolveOptions + + func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws { + // If a package is provided, use that to resolve the dependencies. + if let packageName = resolveOptions.packageName { + let workspace = try swiftTool.getActiveWorkspace() + try workspace.resolve( + packageName: packageName, + root: swiftTool.getWorkspaceRoot(), + version: resolveOptions.version, + branch: resolveOptions.branch, + revision: resolveOptions.revision, + diagnostics: swiftTool.diagnostics) + if swiftTool.diagnostics.hasErrors { + throw ExitCode.failure + } + } else { + // Otherwise, run a normal resolve. + try swiftTool.resolve() + } + } + } } /// Executes the executable at the specified path. @@ -136,3 +283,31 @@ fileprivate func run( let pathRelativeToWorkingDirectory = excutablePath.relative(to: originalWorkingDirectory) try exec(path: excutablePath.pathString, args: [pathRelativeToWorkingDirectory.pathString] + arguments) } + +/// Logs all changed dependencies to a stream +/// - Parameter changes: Changes to log +/// - Parameter pins: PinsStore with currently pinned packages to compare changed packages to. +/// - Parameter stream: Stream used for logging +fileprivate func logPackageChanges(changes: [(PackageReference, Workspace.PackageStateChange)], pins: PinsStore, on stream: OutputByteStream = TSCBasic.stdoutStream) { + let changes = changes.filter { $0.1 != .unchanged } + + stream <<< "\n" + stream <<< "\(changes.count) dependenc\(changes.count == 1 ? "y has" : "ies have") changed\(changes.count > 0 ? ":" : ".")" + stream <<< "\n" + + for (package, change) in changes { + let currentVersion = pins.pinsMap[package.identity]?.state.description ?? "" + switch change { + case let .added(state): + stream <<< "+ \(package.name) \(state.requirement.prettyPrinted)" + case let .updated(state): + stream <<< "~ \(package.name) \(currentVersion) -> \(package.name) \(state.requirement.prettyPrinted)" + case .removed: + stream <<< "- \(package.name) \(currentVersion)" + case .unchanged: + continue + } + stream <<< "\n" + } + stream.flush() +} diff --git a/Sources/Commands/SwiftTool.swift b/Sources/Commands/SwiftTool.swift index 92230a30797..469beace341 100644 --- a/Sources/Commands/SwiftTool.swift +++ b/Sources/Commands/SwiftTool.swift @@ -26,6 +26,7 @@ import Build import XCBuildSupport import Workspace import Basics +import ScriptingCore typealias Diagnostic = TSCBasic.Diagnostic @@ -283,6 +284,34 @@ extension SwiftCommand { public static var _errorLabel: String { "error" } } +protocol ScriptCommand: ParsableCommand { + var swiftOptions: SwiftToolOptions { get } + var options: ScriptToolOptions { get } + + func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws +} + +extension ScriptCommand { + public func run() throws { + guard let file = options.file else { + throw ScriptError.fileNotFound("") + } + let (productName, cacheDirPath) = try checkAndPerformCache(for: file, at: SwiftScriptTool.cacheDir) + + var swiftOptions = swiftOptions + swiftOptions.packagePath = cacheDirPath + swiftOptions.buildPath = nil + let swiftTool = try SwiftTool(options: swiftOptions) + + try self.run(swiftTool, as: productName, at: cacheDirPath) + if swiftTool.diagnostics.hasErrors || swiftTool.executionStatus == .failure { + throw ExitCode.failure + } + } + + public static var _errorLabel: String { "error" } +} + public class SwiftTool { /// The original working directory. let originalWorkingDirectory: AbsolutePath diff --git a/Sources/ScriptingCore/utils.swift b/Sources/ScriptingCore/utils.swift index 1632ec21755..2e5a4399541 100644 --- a/Sources/ScriptingCore/utils.swift +++ b/Sources/ScriptingCore/utils.swift @@ -52,7 +52,7 @@ public func resolveFilePath(_ path: String) -> AbsolutePath? { return absolutePath } -public func prepareCache(for file: String, at dirPath: AbsolutePath) throws -> (productName: String, cacheDirPath: AbsolutePath) { +public func checkAndPerformCache(for file: String, at dirPath: AbsolutePath) throws -> (productName: String, cacheDirPath: AbsolutePath) { if let scriptPath = resolveFilePath(file) { let json = try ScriptParse.manifest(for: scriptPath) let decoder = JSONDecoder() From 027bdc5b0cdfe2414f37d7c7c526878abf5151a5 Mon Sep 17 00:00:00 2001 From: stevapple Date: Tue, 3 Aug 2021 01:09:57 +0800 Subject: [PATCH 05/23] Minor bugfix --- Sources/Commands/SwiftScriptTool.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Sources/Commands/SwiftScriptTool.swift b/Sources/Commands/SwiftScriptTool.swift index b539d4cc440..6949edd1c39 100644 --- a/Sources/Commands/SwiftScriptTool.swift +++ b/Sources/Commands/SwiftScriptTool.swift @@ -50,12 +50,6 @@ public struct SwiftScriptTool: ParsableCommand { ], helpNames: [.short, .long, .customLong("help", withSingleDash: true)]) -// @OptionGroup() -// var swiftOptions: SwiftToolOptions -// -// @OptionGroup() -// var options: ScriptToolOptions - public init() {} public static var _errorLabel: String { "error" } @@ -88,10 +82,11 @@ extension SwiftScriptTool { } else { swiftTool.redirectStdoutToStderr() } + // FIXME: More elegant solution? + print(diagnostic: .init(message: .note("Using cache: \(cacheDirPath.basename)")), + stdoutStream: swiftTool.stdoutStream) do { - // FIXME: How to hide the note? - swiftTool.diagnostics.emit(note: "Using cache: \(cacheDirPath.basename)") let buildSystem = try swiftTool.createBuildSystem(explicitProduct: nil) if options.shouldBuild { try buildSystem.build(subset: .product(productName)) @@ -122,9 +117,9 @@ extension SwiftScriptTool { func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws { swiftTool.redirectStdoutToStderr() + swiftTool.diagnostics.emit(note: "Using cache: \(cacheDirPath.basename)") do { - swiftTool.diagnostics.emit(note: "Using cache: \(cacheDirPath.basename)") let buildSystem = try swiftTool.createBuildSystem(explicitProduct: nil) if options.shouldBuild { try buildSystem.build(subset: .product(productName)) From e117920041fedfebbe894c0362c34aabe8b493b5 Mon Sep 17 00:00:00 2001 From: stevapple Date: Tue, 10 Aug 2021 01:30:48 +0800 Subject: [PATCH 06/23] Fix CMake build --- Sources/Basics/FileSystem+Extensions.swift | 30 +++++++++++++++++- Sources/CMakeLists.txt | 3 ++ Sources/Commands/CMakeLists.txt | 5 ++- Sources/Commands/SwiftScriptTool.swift | 36 ++++++++++++++++++---- Sources/Commands/SwiftTool.swift | 29 ----------------- Sources/ScriptParse/CMakeLists.txt | 32 +++++++++++++++++++ Sources/ScriptingCore/CMakeLists.txt | 28 +++++++++++++++++ Sources/ScriptingCore/Models.swift | 15 +++++---- Sources/ScriptingCore/Options.swift | 10 ------ Sources/ScriptingCore/utils.swift | 20 ++++++++---- Sources/package-parser/CMakeLists.txt | 17 ++++++++++ Sources/swift-script/CMakeLists.txt | 2 +- 12 files changed, 167 insertions(+), 60 deletions(-) create mode 100644 Sources/ScriptParse/CMakeLists.txt create mode 100644 Sources/ScriptingCore/CMakeLists.txt delete mode 100644 Sources/ScriptingCore/Options.swift create mode 100644 Sources/package-parser/CMakeLists.txt diff --git a/Sources/Basics/FileSystem+Extensions.swift b/Sources/Basics/FileSystem+Extensions.swift index 2d306539e07..8411a9f0cd1 100644 --- a/Sources/Basics/FileSystem+Extensions.swift +++ b/Sources/Basics/FileSystem+Extensions.swift @@ -1,7 +1,7 @@ /* This source file is part of the Swift.org open source project - Copyright (c) 2020 Apple Inc. and the Swift project authors + Copyright (c) 2020 - 2021 Apple Inc. and the Swift project authors Licensed under Apache License v2.0 with Runtime Library Exception See http://swift.org/LICENSE.txt for license information @@ -110,3 +110,31 @@ extension FileSystem { return idiomaticConfigDirectory } } + +// MARK: - script cache + +extension FileSystem { + /// SwiftPM cache directory under user's caches directory (if exists) + public var swiftScriptCacheDirectory: AbsolutePath { + return self.dotSwiftScriptCachesDirectory + } + + fileprivate var dotSwiftScriptCachesDirectory: AbsolutePath { + return self.dotSwiftPM.appending(component: "scripts") + } +} + +extension FileSystem { + public func getOrCreateSwiftScriptCacheDirectory() throws -> AbsolutePath { + let idiomaticCacheDirectory = self.swiftScriptCacheDirectory + // Create idiomatic if necessary + if !self.exists(idiomaticCacheDirectory) { + try self.createDirectory(idiomaticCacheDirectory, recursive: true) + } + // Create ~/.swiftpm if necessary + if !self.exists(self.dotSwiftPM) { + try self.createDirectory(self.dotSwiftPM, recursive: true) + } + return idiomaticCacheDirectory + } +} diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index 14370edcab7..5188b862cd9 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -22,9 +22,12 @@ add_subdirectory(PackagePlugin) add_subdirectory(SPMBuildCore) add_subdirectory(SPMLLBuild) add_subdirectory(SourceControl) +#add_subdirectory(ScriptingCore) +#add_subdirectory(ScriptParse) add_subdirectory(swift-build) add_subdirectory(swift-package) add_subdirectory(swift-run) +#add_subdirectory(swift-script) add_subdirectory(swift-test) add_subdirectory(Workspace) add_subdirectory(XCBuildSupport) diff --git a/Sources/Commands/CMakeLists.txt b/Sources/Commands/CMakeLists.txt index 8a067c7f340..58114d2b32b 100644 --- a/Sources/Commands/CMakeLists.txt +++ b/Sources/Commands/CMakeLists.txt @@ -1,6 +1,6 @@ # This source file is part of the Swift.org open source project # -# Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors +# Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors # Licensed under Apache License v2.0 with Runtime Library Exception # # See http://swift.org/LICENSE.txt for license information @@ -18,6 +18,7 @@ add_library(Commands SwiftPackageCollectionsTool.swift SwiftPackageTool.swift SwiftRunTool.swift +# SwiftScriptTool.swift SwiftTestTool.swift SwiftTool.swift SymbolGraphExtract.swift @@ -28,6 +29,8 @@ target_link_libraries(Commands PUBLIC Build PackageCollections PackageGraph +# ScriptingCore +# ScriptParse SourceControl TSCBasic TSCUtility diff --git a/Sources/Commands/SwiftScriptTool.swift b/Sources/Commands/SwiftScriptTool.swift index 6949edd1c39..02be39f7ed3 100644 --- a/Sources/Commands/SwiftScriptTool.swift +++ b/Sources/Commands/SwiftScriptTool.swift @@ -11,11 +11,39 @@ See http://swift.org/CONTRIBUTORS.txt for Swift project authors import ArgumentParser import Basics import PackageModel -import TSCBasic import ScriptParse import ScriptingCore +import TSCBasic import Workspace +protocol ScriptCommand: ParsableCommand { + var swiftOptions: SwiftToolOptions { get } + var options: ScriptToolOptions { get } + + func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws +} + +extension ScriptCommand { + public func run() throws { + guard let file = options.file else { + throw ScriptError.fileNotFound("") + } + let (productName, cacheDirPath) = try checkAndPerformCache(for: file) + + var swiftOptions = swiftOptions + swiftOptions.packagePath = cacheDirPath + swiftOptions.buildPath = nil + let swiftTool = try SwiftTool(options: swiftOptions) + + try self.run(swiftTool, as: productName, at: cacheDirPath) + if swiftTool.diagnostics.hasErrors || swiftTool.executionStatus == .failure { + throw ExitCode.failure + } + } + + public static var _errorLabel: String { "error" } +} + struct ScriptToolOptions: ParsableArguments { /// If the executable product should be built before running. @Flag(name: .customLong("skip-build"), help: "Skip building the executable product") @@ -55,10 +83,6 @@ public struct SwiftScriptTool: ParsableCommand { public static var _errorLabel: String { "error" } } -extension SwiftScriptTool { - static var cacheDir: AbsolutePath { localFileSystem.dotSwiftPM.appending(component: "scripts") } -} - /// swift-run tool namespace extension SwiftScriptTool { struct Run: ScriptCommand { @@ -74,7 +98,7 @@ extension SwiftScriptTool { /// Whether to print build progress. @Flag(help: "Print build progress") var quiet: Bool = false - + func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws { let output = BufferedOutputByteStream() if quiet { diff --git a/Sources/Commands/SwiftTool.swift b/Sources/Commands/SwiftTool.swift index 469beace341..92230a30797 100644 --- a/Sources/Commands/SwiftTool.swift +++ b/Sources/Commands/SwiftTool.swift @@ -26,7 +26,6 @@ import Build import XCBuildSupport import Workspace import Basics -import ScriptingCore typealias Diagnostic = TSCBasic.Diagnostic @@ -284,34 +283,6 @@ extension SwiftCommand { public static var _errorLabel: String { "error" } } -protocol ScriptCommand: ParsableCommand { - var swiftOptions: SwiftToolOptions { get } - var options: ScriptToolOptions { get } - - func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws -} - -extension ScriptCommand { - public func run() throws { - guard let file = options.file else { - throw ScriptError.fileNotFound("") - } - let (productName, cacheDirPath) = try checkAndPerformCache(for: file, at: SwiftScriptTool.cacheDir) - - var swiftOptions = swiftOptions - swiftOptions.packagePath = cacheDirPath - swiftOptions.buildPath = nil - let swiftTool = try SwiftTool(options: swiftOptions) - - try self.run(swiftTool, as: productName, at: cacheDirPath) - if swiftTool.diagnostics.hasErrors || swiftTool.executionStatus == .failure { - throw ExitCode.failure - } - } - - public static var _errorLabel: String { "error" } -} - public class SwiftTool { /// The original working directory. let originalWorkingDirectory: AbsolutePath diff --git a/Sources/ScriptParse/CMakeLists.txt b/Sources/ScriptParse/CMakeLists.txt new file mode 100644 index 00000000000..9be88f8dbf9 --- /dev/null +++ b/Sources/ScriptParse/CMakeLists.txt @@ -0,0 +1,32 @@ +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +# Licensed under Apache License v2.0 with Runtime Library Exception +# +# See http://swift.org/LICENSE.txt for license information +# See http://swift.org/CONTRIBUTORS.txt for Swift project authors + +add_library(ScriptParse + Models.swift + ScriptParse.swift + StatementKind.swift + misc.swift) + +target_link_libraries(ScriptParse PUBLIC + TSCBasic + TSCUtility + Basics + ArgumentParser + SwiftSyntax) + +# NOTE(compnerd) workaround for CMake not setting up include flags yet +set_target_properties(ScriptParse PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) + +if(USE_CMAKE_INSTALL) + install(TARGETS ScriptParse + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin) +endif() +set_property(GLOBAL APPEND PROPERTY SwiftPM_EXPORTS ScriptParse) diff --git a/Sources/ScriptingCore/CMakeLists.txt b/Sources/ScriptingCore/CMakeLists.txt new file mode 100644 index 00000000000..429b393818b --- /dev/null +++ b/Sources/ScriptingCore/CMakeLists.txt @@ -0,0 +1,28 @@ +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +# Licensed under Apache License v2.0 with Runtime Library Exception +# +# See http://swift.org/LICENSE.txt for license information +# See http://swift.org/CONTRIBUTORS.txt for Swift project authors + +add_library(ScriptingCore + Models.swift + utils.swift) + +target_link_libraries(ScriptingCore PUBLIC + TSCBasic + TSCUtility + Basics) + +# NOTE(compnerd) workaround for CMake not setting up include flags yet +set_target_properties(ScriptingCore PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) + +if(USE_CMAKE_INSTALL) + install(TARGETS ScriptingCore + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin) +endif() +set_property(GLOBAL APPEND PROPERTY SwiftPM_EXPORTS ScriptingCore) diff --git a/Sources/ScriptingCore/Models.swift b/Sources/ScriptingCore/Models.swift index 353bbf774f2..44ea828df84 100644 --- a/Sources/ScriptingCore/Models.swift +++ b/Sources/ScriptingCore/Models.swift @@ -1,9 +1,12 @@ -// -// File.swift -// File -// -// Created by C YR on 2021/7/19. -// +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors + */ import Foundation import TSCBasic diff --git a/Sources/ScriptingCore/Options.swift b/Sources/ScriptingCore/Options.swift deleted file mode 100644 index 8a96cfc0363..00000000000 --- a/Sources/ScriptingCore/Options.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// File.swift -// File -// -// Created by C YR on 2021/7/19. -// - -import TSCBasic - - diff --git a/Sources/ScriptingCore/utils.swift b/Sources/ScriptingCore/utils.swift index 2e5a4399541..faef57fa72f 100644 --- a/Sources/ScriptingCore/utils.swift +++ b/Sources/ScriptingCore/utils.swift @@ -1,12 +1,12 @@ /* -This source file is part of the Swift.org open source project + This source file is part of the Swift.org open source project -Copyright 2015 - 2016 Apple Inc. and the Swift project authors -Licensed under Apache License v2.0 with Runtime Library Exception + Copyright 2015 - 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception -See http://swift.org/LICENSE.txt for license information -See http://swift.org/CONTRIBUTORS.txt for Swift project authors -*/ + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors + */ import Basics import Foundation @@ -24,6 +24,8 @@ fileprivate extension AbsolutePath { public enum ScriptError: Swift.Error { /// The specified file doesn't exist. case fileNotFound(String) + /// The target path is directory + case isDirectory(String) } extension ScriptError: CustomStringConvertible { @@ -31,6 +33,8 @@ extension ScriptError: CustomStringConvertible { switch self { case .fileNotFound(let path): return "\(path) doesn't exist" + case .isDirectory(let path): + return "\(path) is a directory" } } } @@ -111,3 +115,7 @@ public func checkAndPerformCache(for file: String, at dirPath: AbsolutePath) thr return (String(file.dropLast(7)), cacheDirPath) } } + +public func checkAndPerformCache(for file: String) throws -> (productName: String, cacheDirPath: AbsolutePath) { + try checkAndPerformCache(for: file, at: localFileSystem.getOrCreateSwiftScriptCacheDirectory()) +} diff --git a/Sources/package-parser/CMakeLists.txt b/Sources/package-parser/CMakeLists.txt new file mode 100644 index 00000000000..ef8972dcc01 --- /dev/null +++ b/Sources/package-parser/CMakeLists.txt @@ -0,0 +1,17 @@ +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors +# Licensed under Apache License v2.0 with Runtime Library Exception +# +# See http://swift.org/LICENSE.txt for license information +# See http://swift.org/CONTRIBUTORS.txt for Swift project authors + +add_executable(package-parser + main.swift) +target_link_libraries(package-parser PRIVATE + ScriptParse) + +if(USE_CMAKE_INSTALL) +install(TARGETS package-parser + DESTINATION bin) +endif() diff --git a/Sources/swift-script/CMakeLists.txt b/Sources/swift-script/CMakeLists.txt index d033ae9c788..7e57f0cb432 100644 --- a/Sources/swift-script/CMakeLists.txt +++ b/Sources/swift-script/CMakeLists.txt @@ -1,6 +1,6 @@ # This source file is part of the Swift.org open source project # -# Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors +# Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors # Licensed under Apache License v2.0 with Runtime Library Exception # # See http://swift.org/LICENSE.txt for license information From 195db6ccadd27790bdc83e258173877941308340 Mon Sep 17 00:00:00 2001 From: stevapple Date: Tue, 10 Aug 2021 01:31:17 +0800 Subject: [PATCH 07/23] Add functionality to swift-script --- Sources/Commands/SwiftScriptTool.swift | 44 +++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/Sources/Commands/SwiftScriptTool.swift b/Sources/Commands/SwiftScriptTool.swift index 02be39f7ed3..5222956953f 100644 --- a/Sources/Commands/SwiftScriptTool.swift +++ b/Sources/Commands/SwiftScriptTool.swift @@ -75,6 +75,7 @@ public struct SwiftScriptTool: ParsableCommand { Clean.self, Reset.self, Resolve.self, + List.self, ], helpNames: [.short, .long, .customLong("help", withSingleDash: true)]) @@ -138,7 +139,10 @@ extension SwiftScriptTool { @OptionGroup() var options: ScriptToolOptions - + + @Option(name: .shortAndLong, help: "Save the prebuilt script binary", transform: AbsolutePath.init) + var output: AbsolutePath? + func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws { swiftTool.redirectStdoutToStderr() swiftTool.diagnostics.emit(note: "Using cache: \(cacheDirPath.basename)") @@ -148,6 +152,15 @@ extension SwiftScriptTool { if options.shouldBuild { try buildSystem.build(subset: .product(productName)) } + + if let output = output { + let executablePath = try swiftTool.buildParameters().buildPath.appending(component: productName) + guard !localFileSystem.isDirectory(output) else { + throw ScriptError.isDirectory(output.pathString) + } + try? localFileSystem.removeFileTree(output) + try localFileSystem.copy(from: executablePath, to: output) + } } catch let error as ScriptError { swiftTool.diagnostics.emit(error) throw ExitCode.failure @@ -238,6 +251,35 @@ extension SwiftScriptTool { } } +extension SwiftScriptTool { + struct List: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "List script caches") + + @OptionGroup(_hiddenFromHelp: true) + var swiftOptions: SwiftToolOptions + + func run() throws { + let cacheDir = try localFileSystem.getOrCreateSwiftScriptCacheDirectory() + let scripts = try localFileSystem.getDirectoryContents(cacheDir) + let resolved = try scripts.map { script -> (String, AbsolutePath) in + let sourceDir = cacheDir.appending(components: script, "Sources") + guard localFileSystem.isDirectory(sourceDir), + let name = try localFileSystem.getDirectoryContents(sourceDir).first, + case let realpath = resolveSymlinks(sourceDir.appending(components: name, "main.swift")) else { + throw Diagnostics.fatalError + } + return (script, realpath) + } + print("\(scripts.count) script\(scripts.count > 1 ? "s" : "") cached at \(cacheDir)") + guard let maxLength = resolved.map(\.0.count).max() else { return } + resolved.forEach { (name, path) in + print(name + String(repeating: " ", count: maxLength - name.count + 2) + path.pathString) + } + } + } +} + extension SwiftScriptTool { struct ResolveOptions: ParsableArguments { @Option(help: "The version to resolve at", transform: { Version(string: $0) }) From 7a937c7d35a62a118fda68f59a951794e331883b Mon Sep 17 00:00:00 2001 From: stevapple Date: Tue, 10 Aug 2021 01:52:15 +0800 Subject: [PATCH 08/23] Minor fixes --- Package.swift | 4 +-- Sources/Commands/SwiftScriptTool.swift | 34 ++++++++++++------------- Sources/ScriptParse/Models.swift | 4 +-- Sources/ScriptParse/ScriptParse.swift | 9 +++---- Sources/ScriptParse/StatementKind.swift | 2 +- Sources/ScriptParse/misc.swift | 2 +- Sources/ScriptingCore/Models.swift | 4 +-- 7 files changed, 29 insertions(+), 30 deletions(-) diff --git a/Package.swift b/Package.swift index c00d9810e80..83af9137ba2 100644 --- a/Package.swift +++ b/Package.swift @@ -197,7 +197,7 @@ let package = Package( .target( /** Parse `@package` marks */ name: "ScriptParse", - dependencies: ["SwiftSyntax", "SwiftToolsSupport-auto", "ArgumentParser"] + dependencies: ["SwiftToolsSupport-auto", "ArgumentParser", "SwiftSyntax"] ), .target( @@ -249,7 +249,7 @@ let package = Package( name: "swift-script", dependencies: ["Commands"]), .target( - /** Manages and runs a script */ + /** Parse `@package` marks */ name: "package-parser", dependencies: ["ScriptParse"]), .target( diff --git a/Sources/Commands/SwiftScriptTool.swift b/Sources/Commands/SwiftScriptTool.swift index 5222956953f..5a6189f9781 100644 --- a/Sources/Commands/SwiftScriptTool.swift +++ b/Sources/Commands/SwiftScriptTool.swift @@ -1,12 +1,12 @@ /* -This source file is part of the Swift.org open source project + This source file is part of the Swift.org open source project -Copyright 2015 - 2016 Apple Inc. and the Swift project authors -Licensed under Apache License v2.0 with Runtime Library Exception + Copyright 2015 - 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception -See http://swift.org/LICENSE.txt for license information -See http://swift.org/CONTRIBUTORS.txt for Swift project authors -*/ + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors + */ import ArgumentParser import Basics @@ -179,7 +179,7 @@ extension SwiftScriptTool { @OptionGroup() var options: ScriptToolOptions - + func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws { try swiftTool.getActiveWorkspace().clean(with: swiftTool.diagnostics) } @@ -194,12 +194,12 @@ extension SwiftScriptTool { @OptionGroup() var options: ScriptToolOptions - + func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws { try localFileSystem.removeFileTree(cacheDirPath) } } - + struct Update: ScriptCommand { static let configuration = CommandConfiguration( abstract: "Update package dependencies") @@ -209,11 +209,11 @@ extension SwiftScriptTool { @OptionGroup() var options: ScriptToolOptions - + @Flag(name: [.long, .customShort("n")], help: "Display the list of dependencies that can be updated") var dryRun: Bool = false - + @Argument(help: "The packages to update") var packages: [String] = [] @@ -284,10 +284,10 @@ extension SwiftScriptTool { struct ResolveOptions: ParsableArguments { @Option(help: "The version to resolve at", transform: { Version(string: $0) }) var version: Version? - + @Option(help: "The branch to resolve at") var branch: String? - + @Option(help: "The revision to resolve at") var revision: String? @@ -298,7 +298,7 @@ extension SwiftScriptTool { struct Resolve: ScriptCommand { static let configuration = CommandConfiguration( abstract: "Resolve package dependencies") - + @OptionGroup(_hiddenFromHelp: true) var swiftOptions: SwiftToolOptions @@ -307,7 +307,7 @@ extension SwiftScriptTool { @OptionGroup() var resolveOptions: ResolveOptions - + func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws { // If a package is provided, use that to resolve the dependencies. if let packageName = resolveOptions.packageName { @@ -351,11 +351,11 @@ fileprivate func run( /// - Parameter stream: Stream used for logging fileprivate func logPackageChanges(changes: [(PackageReference, Workspace.PackageStateChange)], pins: PinsStore, on stream: OutputByteStream = TSCBasic.stdoutStream) { let changes = changes.filter { $0.1 != .unchanged } - + stream <<< "\n" stream <<< "\(changes.count) dependenc\(changes.count == 1 ? "y has" : "ies have") changed\(changes.count > 0 ? ":" : ".")" stream <<< "\n" - + for (package, change) in changes { let currentVersion = pins.pinsMap[package.identity]?.state.description ?? "" switch change { diff --git a/Sources/ScriptParse/Models.swift b/Sources/ScriptParse/Models.swift index aa9bbd9185f..e1d7d3c1fc7 100644 --- a/Sources/ScriptParse/Models.swift +++ b/Sources/ScriptParse/Models.swift @@ -8,7 +8,7 @@ See http://swift.org/CONTRIBUTORS.txt for Swift project authors FIXME: This is a temporary alternative of the frontend implementation. -*/ + */ import struct Foundation.URL import TSCBasic @@ -31,7 +31,7 @@ struct PackageModel: Codable { struct PackageDependency: Codable { let package: PackageModel var modules: [String] = [] - + init(of package: PackageModel) { self.package = package } diff --git a/Sources/ScriptParse/ScriptParse.swift b/Sources/ScriptParse/ScriptParse.swift index 43f11c44240..2a7d9af786c 100644 --- a/Sources/ScriptParse/ScriptParse.swift +++ b/Sources/ScriptParse/ScriptParse.swift @@ -8,7 +8,7 @@ See http://swift.org/CONTRIBUTORS.txt for Swift project authors FIXME: This is a temporary alternative of the frontend implementation. -*/ + */ import SwiftSyntax import TSCBasic @@ -23,14 +23,13 @@ public struct ScriptParse: ParsableCommand { encoder.outputFormatting = .prettyPrinted return encoder }() - + public init() {} } extension ScriptParse { static func parse(_ path: AbsolutePath) throws -> ScriptDependencies { let syntaxTree = try SyntaxParser.parse(path.asURL) -// let converter = SourceLocationConverter(file: path.basename, tree: syntaxTree) var inPackageScope = false var keyStatements: [CodeBlockItemSyntax] = [] @@ -90,13 +89,13 @@ extension ScriptParse { } return ScriptDependencies(sourceFile: path, modules: collected) } - + public func run() throws { let path = try AbsolutePath(validating: file) let json = try ScriptParse.manifest(for: path) print(String(data: json, encoding: .utf8)!) } - + // FIXME: This should be removed some day. public static func manifest(for path: AbsolutePath) throws -> Data { let output = try parse(path) diff --git a/Sources/ScriptParse/StatementKind.swift b/Sources/ScriptParse/StatementKind.swift index 92036dfc1c0..e4918c5824a 100644 --- a/Sources/ScriptParse/StatementKind.swift +++ b/Sources/ScriptParse/StatementKind.swift @@ -8,7 +8,7 @@ See http://swift.org/CONTRIBUTORS.txt for Swift project authors FIXME: This is a temporary alternative of the frontend implementation. -*/ + */ import SwiftSyntax diff --git a/Sources/ScriptParse/misc.swift b/Sources/ScriptParse/misc.swift index d2c5f11b058..276a0f046f3 100644 --- a/Sources/ScriptParse/misc.swift +++ b/Sources/ScriptParse/misc.swift @@ -8,7 +8,7 @@ See http://swift.org/CONTRIBUTORS.txt for Swift project authors FIXME: This is a temporary alternative of the frontend implementation. -*/ + */ import SwiftSyntax import Foundation diff --git a/Sources/ScriptingCore/Models.swift b/Sources/ScriptingCore/Models.swift index 44ea828df84..591e034aa54 100644 --- a/Sources/ScriptingCore/Models.swift +++ b/Sources/ScriptingCore/Models.swift @@ -16,7 +16,7 @@ public struct PackageModel: Codable { public let raw: String public let url: Foundation.URL? public let path: AbsolutePath? - public let _name: String? + private let _name: String? public var name: String { if let name = _name { @@ -37,7 +37,7 @@ public struct PackageModel: Codable { public struct PackageDependency: Codable { public let package: PackageModel public var modules: [String] = [] - + init(of package: PackageModel) { self.package = package } From 811f8870a4aef901c22283df3557d1e884ee0e8d Mon Sep 17 00:00:00 2001 From: stevapple Date: Thu, 12 Aug 2021 16:03:22 +0800 Subject: [PATCH 09/23] Fix typo --- Sources/Commands/Error.swift | 1 - Sources/Commands/SwiftRunTool.swift | 6 +++--- Sources/Commands/SwiftScriptTool.swift | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Sources/Commands/Error.swift b/Sources/Commands/Error.swift index 14af126d0a8..509228714cc 100644 --- a/Sources/Commands/Error.swift +++ b/Sources/Commands/Error.swift @@ -37,7 +37,6 @@ extension Error: CustomStringConvertible { // The name has underscore because of SR-4015. func handle(error: Swift.Error) { - switch error { case Diagnostics.fatalError: break diff --git a/Sources/Commands/SwiftRunTool.swift b/Sources/Commands/SwiftRunTool.swift index 201d86b6408..60ff76f8ac1 100644 --- a/Sources/Commands/SwiftRunTool.swift +++ b/Sources/Commands/SwiftRunTool.swift @@ -236,7 +236,7 @@ public struct SwiftRunTool: SwiftCommand { /// Executes the executable at the specified path. private func run( - _ excutablePath: AbsolutePath, + _ executablePath: AbsolutePath, originalWorkingDirectory: AbsolutePath, arguments: [String]) throws { @@ -246,8 +246,8 @@ public struct SwiftRunTool: SwiftCommand { try ProcessEnv.chdir(originalWorkingDirectory) } - let pathRelativeToWorkingDirectory = excutablePath.relative(to: originalWorkingDirectory) - try exec(path: excutablePath.pathString, args: [pathRelativeToWorkingDirectory.pathString] + arguments) + let pathRelativeToWorkingDirectory = executablePath.relative(to: originalWorkingDirectory) + try exec(path: executablePath.pathString, args: [pathRelativeToWorkingDirectory.pathString] + arguments) } /// Determines if a path points to a valid swift file. diff --git a/Sources/Commands/SwiftScriptTool.swift b/Sources/Commands/SwiftScriptTool.swift index 5a6189f9781..a6a2b131452 100644 --- a/Sources/Commands/SwiftScriptTool.swift +++ b/Sources/Commands/SwiftScriptTool.swift @@ -332,7 +332,7 @@ extension SwiftScriptTool { /// Executes the executable at the specified path. fileprivate func run( - _ excutablePath: AbsolutePath, + _ executablePath: AbsolutePath, originalWorkingDirectory: AbsolutePath, arguments: [String]) throws { // Make sure we are running from the original working directory. @@ -341,8 +341,8 @@ fileprivate func run( try ProcessEnv.chdir(originalWorkingDirectory) } - let pathRelativeToWorkingDirectory = excutablePath.relative(to: originalWorkingDirectory) - try exec(path: excutablePath.pathString, args: [pathRelativeToWorkingDirectory.pathString] + arguments) + let pathRelativeToWorkingDirectory = executablePath.relative(to: originalWorkingDirectory) + try exec(path: executablePath.pathString, args: [pathRelativeToWorkingDirectory.pathString] + arguments) } /// Logs all changed dependencies to a stream From 29e7e41611ea7c4e6b306527e2d359c5c40a8dad Mon Sep 17 00:00:00 2001 From: stevapple Date: Thu, 12 Aug 2021 16:03:51 +0800 Subject: [PATCH 10/23] Rearrange arguments --- Sources/Commands/SwiftScriptTool.swift | 90 +++++++++++++------------- 1 file changed, 44 insertions(+), 46 deletions(-) diff --git a/Sources/Commands/SwiftScriptTool.swift b/Sources/Commands/SwiftScriptTool.swift index a6a2b131452..3e2efaf0fb3 100644 --- a/Sources/Commands/SwiftScriptTool.swift +++ b/Sources/Commands/SwiftScriptTool.swift @@ -18,16 +18,13 @@ import Workspace protocol ScriptCommand: ParsableCommand { var swiftOptions: SwiftToolOptions { get } - var options: ScriptToolOptions { get } + var file: String { get } func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws } extension ScriptCommand { public func run() throws { - guard let file = options.file else { - throw ScriptError.fileNotFound("") - } let (productName, cacheDirPath) = try checkAndPerformCache(for: file) var swiftOptions = swiftOptions @@ -51,10 +48,6 @@ struct ScriptToolOptions: ParsableArguments { var shouldBuild: Bool { !shouldSkipBuild } - /// The script file to run. - @Argument(help: "The script file to run") - var file: String? - /// The arguments to pass to the executable. @Argument(parsing: .unconditionalRemaining, help: "The arguments to pass to the executable") @@ -84,7 +77,6 @@ public struct SwiftScriptTool: ParsableCommand { public static var _errorLabel: String { "error" } } -/// swift-run tool namespace extension SwiftScriptTool { struct Run: ScriptCommand { static let configuration = CommandConfiguration( @@ -92,6 +84,9 @@ extension SwiftScriptTool { @OptionGroup(_hiddenFromHelp: true) var swiftOptions: SwiftToolOptions + + @Argument(help: "The script file to run") + var file: String @OptionGroup() var options: ScriptToolOptions @@ -136,6 +131,9 @@ extension SwiftScriptTool { @OptionGroup(_hiddenFromHelp: true) var swiftOptions: SwiftToolOptions + + @Argument(help: "The script file to build") + var file: String @OptionGroup() var options: ScriptToolOptions @@ -177,8 +175,8 @@ extension SwiftScriptTool { @OptionGroup(_hiddenFromHelp: true) var swiftOptions: SwiftToolOptions - @OptionGroup() - var options: ScriptToolOptions + @Argument(help: "The script file to clean build cache") + var file: String func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws { try swiftTool.getActiveWorkspace().clean(with: swiftTool.diagnostics) @@ -192,8 +190,8 @@ extension SwiftScriptTool { @OptionGroup(_hiddenFromHelp: true) var swiftOptions: SwiftToolOptions - @OptionGroup() - var options: ScriptToolOptions + @Argument(help: "The script file to reset build cache") + var file: String func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws { try localFileSystem.removeFileTree(cacheDirPath) @@ -207,8 +205,8 @@ extension SwiftScriptTool { @OptionGroup(_hiddenFromHelp: true) var swiftOptions: SwiftToolOptions - @OptionGroup() - var options: ScriptToolOptions + @Argument(help: "The script file to update dependencies") + var file: String @Flag(name: [.long, .customShort("n")], help: "Display the list of dependencies that can be updated") @@ -251,35 +249,6 @@ extension SwiftScriptTool { } } -extension SwiftScriptTool { - struct List: ParsableCommand { - static let configuration = CommandConfiguration( - abstract: "List script caches") - - @OptionGroup(_hiddenFromHelp: true) - var swiftOptions: SwiftToolOptions - - func run() throws { - let cacheDir = try localFileSystem.getOrCreateSwiftScriptCacheDirectory() - let scripts = try localFileSystem.getDirectoryContents(cacheDir) - let resolved = try scripts.map { script -> (String, AbsolutePath) in - let sourceDir = cacheDir.appending(components: script, "Sources") - guard localFileSystem.isDirectory(sourceDir), - let name = try localFileSystem.getDirectoryContents(sourceDir).first, - case let realpath = resolveSymlinks(sourceDir.appending(components: name, "main.swift")) else { - throw Diagnostics.fatalError - } - return (script, realpath) - } - print("\(scripts.count) script\(scripts.count > 1 ? "s" : "") cached at \(cacheDir)") - guard let maxLength = resolved.map(\.0.count).max() else { return } - resolved.forEach { (name, path) in - print(name + String(repeating: " ", count: maxLength - name.count + 2) + path.pathString) - } - } - } -} - extension SwiftScriptTool { struct ResolveOptions: ParsableArguments { @Option(help: "The version to resolve at", transform: { Version(string: $0) }) @@ -302,8 +271,8 @@ extension SwiftScriptTool { @OptionGroup(_hiddenFromHelp: true) var swiftOptions: SwiftToolOptions - @OptionGroup() - var options: ScriptToolOptions + @Argument(help: "The script file to clean") + var file: String @OptionGroup() var resolveOptions: ResolveOptions @@ -330,6 +299,35 @@ extension SwiftScriptTool { } } +extension SwiftScriptTool { + struct List: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "List script caches") + + @OptionGroup(_hiddenFromHelp: true) + var swiftOptions: SwiftToolOptions + + func run() throws { + let cacheDir = try localFileSystem.getOrCreateSwiftScriptCacheDirectory() + let scripts = try localFileSystem.getDirectoryContents(cacheDir) + let resolved = try scripts.map { script -> (String, AbsolutePath) in + let sourceDir = cacheDir.appending(components: script, "Sources") + guard localFileSystem.isDirectory(sourceDir), + let name = try localFileSystem.getDirectoryContents(sourceDir).first, + case let realpath = resolveSymlinks(sourceDir.appending(components: name, "main.swift")) else { + throw Diagnostics.fatalError + } + return (script, realpath) + } + print("\(scripts.count) script\(scripts.count > 1 ? "s" : "") cached at \(cacheDir)") + guard let maxLength = resolved.map(\.0.count).max() else { return } + resolved.forEach { (name, path) in + print(name + String(repeating: " ", count: maxLength - name.count + 2) + path.pathString) + } + } + } +} + /// Executes the executable at the specified path. fileprivate func run( _ executablePath: AbsolutePath, From b5824784bb02de827e9e1e7c27e0fe49f75b3703 Mon Sep 17 00:00:00 2001 From: stevapple Date: Thu, 12 Aug 2021 17:32:38 +0800 Subject: [PATCH 11/23] Minor fixes --- Sources/Basics/FileSystem+Extensions.swift | 6 +----- Sources/Commands/Error.swift | 8 +++++++- Sources/Commands/SwiftScriptTool.swift | 8 +++----- Sources/ScriptParse/ScriptParse.swift | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Sources/Basics/FileSystem+Extensions.swift b/Sources/Basics/FileSystem+Extensions.swift index 8411a9f0cd1..0129187e43f 100644 --- a/Sources/Basics/FileSystem+Extensions.swift +++ b/Sources/Basics/FileSystem+Extensions.swift @@ -127,14 +127,10 @@ extension FileSystem { extension FileSystem { public func getOrCreateSwiftScriptCacheDirectory() throws -> AbsolutePath { let idiomaticCacheDirectory = self.swiftScriptCacheDirectory - // Create idiomatic if necessary + // Create if necessary if !self.exists(idiomaticCacheDirectory) { try self.createDirectory(idiomaticCacheDirectory, recursive: true) } - // Create ~/.swiftpm if necessary - if !self.exists(self.dotSwiftPM) { - try self.createDirectory(self.dotSwiftPM, recursive: true) - } return idiomaticCacheDirectory } } diff --git a/Sources/Commands/Error.swift b/Sources/Commands/Error.swift index 509228714cc..110be3ff0c8 100644 --- a/Sources/Commands/Error.swift +++ b/Sources/Commands/Error.swift @@ -54,7 +54,7 @@ func print(error: Any) { func print(diagnostic: Diagnostic, stdoutStream: OutputByteStream) { - let writer = InteractiveWriter.stderr + let writer = InteractiveWriter(stream: stdoutStream) if !(diagnostic.location is UnknownLocation) { writer.write(diagnostic.location.description) @@ -78,6 +78,12 @@ func print(diagnostic: Diagnostic, stdoutStream: OutputByteStream) { writer.write("\n") } +extension Diagnostic: ByteStreamable { + public func write(to stream: WritableByteStream) { + print(diagnostic: self, stdoutStream: stream) + } +} + /// This class is used to write on the underlying stream. /// /// If underlying stream is a not tty, the string will be written in without any diff --git a/Sources/Commands/SwiftScriptTool.swift b/Sources/Commands/SwiftScriptTool.swift index 3e2efaf0fb3..0e4ceb06922 100644 --- a/Sources/Commands/SwiftScriptTool.swift +++ b/Sources/Commands/SwiftScriptTool.swift @@ -91,8 +91,8 @@ extension SwiftScriptTool { @OptionGroup() var options: ScriptToolOptions - /// Whether to print build progress. - @Flag(help: "Print build progress") + /// Whether to show build output. + @Flag(help: "Print build output") var quiet: Bool = false func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws { @@ -102,9 +102,7 @@ extension SwiftScriptTool { } else { swiftTool.redirectStdoutToStderr() } - // FIXME: More elegant solution? - print(diagnostic: .init(message: .note("Using cache: \(cacheDirPath.basename)")), - stdoutStream: swiftTool.stdoutStream) + swiftTool.stdoutStream <<< Diagnostic(message: .note("Using cache: \(cacheDirPath.basename)")) do { let buildSystem = try swiftTool.createBuildSystem(explicitProduct: nil) diff --git a/Sources/ScriptParse/ScriptParse.swift b/Sources/ScriptParse/ScriptParse.swift index 2a7d9af786c..071cdad876b 100644 --- a/Sources/ScriptParse/ScriptParse.swift +++ b/Sources/ScriptParse/ScriptParse.swift @@ -93,7 +93,7 @@ extension ScriptParse { public func run() throws { let path = try AbsolutePath(validating: file) let json = try ScriptParse.manifest(for: path) - print(String(data: json, encoding: .utf8)!) + print(String(decoding: json, as: UTF8.self)) } // FIXME: This should be removed some day. From 22e5601f37e98f0df4a2ea1b02aac6d40465c460 Mon Sep 17 00:00:00 2001 From: stevapple Date: Fri, 13 Aug 2021 01:12:40 +0800 Subject: [PATCH 12/23] Minor fixes --- Sources/Commands/SwiftScriptTool.swift | 4 ++-- Sources/ScriptingCore/utils.swift | 18 +++++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Sources/Commands/SwiftScriptTool.swift b/Sources/Commands/SwiftScriptTool.swift index 0e4ceb06922..06cd863f1e4 100644 --- a/Sources/Commands/SwiftScriptTool.swift +++ b/Sources/Commands/SwiftScriptTool.swift @@ -308,12 +308,12 @@ extension SwiftScriptTool { func run() throws { let cacheDir = try localFileSystem.getOrCreateSwiftScriptCacheDirectory() let scripts = try localFileSystem.getDirectoryContents(cacheDir) - let resolved = try scripts.map { script -> (String, AbsolutePath) in + let resolved = try scripts.compactMap { script -> (String, AbsolutePath)? in let sourceDir = cacheDir.appending(components: script, "Sources") guard localFileSystem.isDirectory(sourceDir), let name = try localFileSystem.getDirectoryContents(sourceDir).first, case let realpath = resolveSymlinks(sourceDir.appending(components: name, "main.swift")) else { - throw Diagnostics.fatalError + return nil } return (script, realpath) } diff --git a/Sources/ScriptingCore/utils.swift b/Sources/ScriptingCore/utils.swift index faef57fa72f..4d72ac8343b 100644 --- a/Sources/ScriptingCore/utils.swift +++ b/Sources/ScriptingCore/utils.swift @@ -10,7 +10,6 @@ import Basics import Foundation -import ScriptParse import TSCBasic fileprivate extension AbsolutePath { @@ -58,20 +57,24 @@ public func resolveFilePath(_ path: String) -> AbsolutePath? { public func checkAndPerformCache(for file: String, at dirPath: AbsolutePath) throws -> (productName: String, cacheDirPath: AbsolutePath) { if let scriptPath = resolveFilePath(file) { - let json = try ScriptParse.manifest(for: scriptPath) + let json: Data = try { + let proc = Process(args: "package-parser", scriptPath.pathString) + try proc.launch() + return try Data(proc.waitUntilExit().output.get()) + }() let decoder = JSONDecoder() let manifest = try decoder.decode(ScriptDependencies.self, from: json) - + let productName = scriptPath.basename.spm_dropSuffix(".swift").spm_mangledToBundleIdentifier() let cacheDirPath = dirPath.appending(component: "\(productName)-\(scriptPath.sha256Hash)") try localFileSystem.createDirectory(cacheDirPath, recursive: true) - + let sourceDirPath = cacheDirPath.appending(components: "Sources", productName) try localFileSystem.createDirectory(sourceDirPath, recursive: true) if !localFileSystem.exists(sourceDirPath.appending(component: "main.swift")) { try localFileSystem.createSymbolicLink(sourceDirPath.appending(component: "main.swift"), pointingAt: scriptPath, relative: false) } - + var targets: [(name: String, package: String)] = [] for package in manifest.modules { let packageName = package.package.name @@ -79,7 +82,7 @@ public func checkAndPerformCache(for file: String, at dirPath: AbsolutePath) thr targets.append((target, packageName)) } } - + let packageSwift = """ // swift-tools-version:5.4 import PackageDescription @@ -101,7 +104,7 @@ public func checkAndPerformCache(for file: String, at dirPath: AbsolutePath) thr ) """ - + try localFileSystem.writeIfChanged( path: cacheDirPath.appending(component: "Package.swift"), bytes: ByteString(stringLiteral: packageSwift) @@ -112,6 +115,7 @@ public func checkAndPerformCache(for file: String, at dirPath: AbsolutePath) thr guard localFileSystem.isDirectory(cacheDirPath) else { throw ScriptError.fileNotFound(file) } + // eg. Dropping "-abc123" from "test-abc123" return (String(file.dropLast(7)), cacheDirPath) } } From d82b0c7452a0269f8bb5ec06021d53f2bfb68e59 Mon Sep 17 00:00:00 2001 From: stevapple Date: Fri, 13 Aug 2021 01:12:54 +0800 Subject: [PATCH 13/23] Separate package-parser --- Package.swift | 18 +--- Sources/CMakeLists.txt | 5 +- Sources/Commands/CMakeLists.txt | 5 +- Sources/ScriptParse/CMakeLists.txt | 32 -------- Sources/ScriptParse/Models.swift | 60 -------------- Sources/ScriptParse/ScriptParse.swift | 105 ------------------------ Sources/ScriptParse/StatementKind.swift | 30 ------- Sources/ScriptParse/misc.swift | 40 --------- Sources/package-parser/CMakeLists.txt | 17 ---- Sources/package-parser/main.swift | 15 ---- 10 files changed, 5 insertions(+), 322 deletions(-) delete mode 100644 Sources/ScriptParse/CMakeLists.txt delete mode 100644 Sources/ScriptParse/Models.swift delete mode 100644 Sources/ScriptParse/ScriptParse.swift delete mode 100644 Sources/ScriptParse/StatementKind.swift delete mode 100644 Sources/ScriptParse/misc.swift delete mode 100644 Sources/package-parser/CMakeLists.txt delete mode 100644 Sources/package-parser/main.swift diff --git a/Package.swift b/Package.swift index 83af9137ba2..a83c71a3622 100644 --- a/Package.swift +++ b/Package.swift @@ -100,10 +100,6 @@ let package = Package( "PackageModel", ] ), - .executable( - name: "package-parser", - targets: ["package-parser"] - ), ], targets: [ // The `PackageDescription` target provides the API that is available @@ -194,12 +190,6 @@ let package = Package( // MARK: Package Manager Functionality - .target( - /** Parse `@package` marks */ - name: "ScriptParse", - dependencies: ["SwiftToolsSupport-auto", "ArgumentParser", "SwiftSyntax"] - ), - .target( /** Builds Modules and Products */ name: "SPMBuildCore", @@ -227,7 +217,7 @@ let package = Package( .target( /** High-level commands */ name: "Commands", - dependencies: ["SwiftToolsSupport-auto", "Basics", "Build", "PackageGraph", "SourceControl", "Xcodeproj", "Workspace", "XCBuildSupport", "ArgumentParser", "PackageCollections", "ScriptParse", "ScriptingCore"]), + dependencies: ["SwiftToolsSupport-auto", "Basics", "Build", "PackageGraph", "SourceControl", "Xcodeproj", "Workspace", "XCBuildSupport", "ArgumentParser", "PackageCollections", "ScriptingCore"]), .target( /** The main executable provided by SwiftPM */ name: "swift-package", @@ -248,10 +238,6 @@ let package = Package( /** Manages and runs a script */ name: "swift-script", dependencies: ["Commands"]), - .target( - /** Parse `@package` marks */ - name: "package-parser", - dependencies: ["ScriptParse"]), .target( /** Interacts with package collections */ name: "swift-package-collection", @@ -378,7 +364,6 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.4.3")), .package(url: "https://github.com/apple/swift-driver.git", .branch(relatedDependenciesBranch)), .package(url: "https://github.com/apple/swift-crypto.git", .upToNextMinor(from: "1.1.4")), - .package(url: "https://github.com/apple/swift-syntax.git", .branch(relatedDependenciesBranch)), ] } else { package.dependencies += [ @@ -386,6 +371,5 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { .package(path: "../swift-argument-parser"), .package(path: "../swift-driver"), .package(path: "../swift-crypto"), - .package(path: "../swift-syntax"), ] } diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index 5188b862cd9..23c18190306 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -22,12 +22,11 @@ add_subdirectory(PackagePlugin) add_subdirectory(SPMBuildCore) add_subdirectory(SPMLLBuild) add_subdirectory(SourceControl) -#add_subdirectory(ScriptingCore) -#add_subdirectory(ScriptParse) +add_subdirectory(ScriptingCore) add_subdirectory(swift-build) add_subdirectory(swift-package) add_subdirectory(swift-run) -#add_subdirectory(swift-script) +add_subdirectory(swift-script) add_subdirectory(swift-test) add_subdirectory(Workspace) add_subdirectory(XCBuildSupport) diff --git a/Sources/Commands/CMakeLists.txt b/Sources/Commands/CMakeLists.txt index 58114d2b32b..f27260cd830 100644 --- a/Sources/Commands/CMakeLists.txt +++ b/Sources/Commands/CMakeLists.txt @@ -18,7 +18,7 @@ add_library(Commands SwiftPackageCollectionsTool.swift SwiftPackageTool.swift SwiftRunTool.swift -# SwiftScriptTool.swift + SwiftScriptTool.swift SwiftTestTool.swift SwiftTool.swift SymbolGraphExtract.swift @@ -29,8 +29,7 @@ target_link_libraries(Commands PUBLIC Build PackageCollections PackageGraph -# ScriptingCore -# ScriptParse + ScriptingCore SourceControl TSCBasic TSCUtility diff --git a/Sources/ScriptParse/CMakeLists.txt b/Sources/ScriptParse/CMakeLists.txt deleted file mode 100644 index 9be88f8dbf9..00000000000 --- a/Sources/ScriptParse/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -# This source file is part of the Swift.org open source project -# -# Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors -# Licensed under Apache License v2.0 with Runtime Library Exception -# -# See http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors - -add_library(ScriptParse - Models.swift - ScriptParse.swift - StatementKind.swift - misc.swift) - -target_link_libraries(ScriptParse PUBLIC - TSCBasic - TSCUtility - Basics - ArgumentParser - SwiftSyntax) - -# NOTE(compnerd) workaround for CMake not setting up include flags yet -set_target_properties(ScriptParse PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) - -if(USE_CMAKE_INSTALL) - install(TARGETS ScriptParse - ARCHIVE DESTINATION lib - LIBRARY DESTINATION lib - RUNTIME DESTINATION bin) -endif() -set_property(GLOBAL APPEND PROPERTY SwiftPM_EXPORTS ScriptParse) diff --git a/Sources/ScriptParse/Models.swift b/Sources/ScriptParse/Models.swift deleted file mode 100644 index e1d7d3c1fc7..00000000000 --- a/Sources/ScriptParse/Models.swift +++ /dev/null @@ -1,60 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See http://swift.org/LICENSE.txt for license information - See http://swift.org/CONTRIBUTORS.txt for Swift project authors - - FIXME: This is a temporary alternative of the frontend implementation. - */ - -import struct Foundation.URL -import TSCBasic -import SwiftSyntax - -struct PackageModel: Codable { - let raw: String - let path: AbsolutePath? - let url: URL? - let name: String? - - init(_ raw: String, path: AbsolutePath? = nil, url: URL? = nil, name: String? = nil) { - self.raw = raw - self.path = path - self.url = url - self.name = name - } -} - -struct PackageDependency: Codable { - let package: PackageModel - var modules: [String] = [] - - init(of package: PackageModel) { - self.package = package - } -} - -struct ScriptDependencies: Codable { - let sourceFile: AbsolutePath - let modules: [PackageDependency] -} - -enum ScriptParseError: Swift.Error, CustomStringConvertible { - case wrongSyntax - case unsupportedSyntax - case noFileSpecified - - var description: String { - switch self { - case .wrongSyntax: - return "Syntax error" - case .unsupportedSyntax: - return "Unsupported import syntax" - case .noFileSpecified: - return "Please specify a file" - } - } -} diff --git a/Sources/ScriptParse/ScriptParse.swift b/Sources/ScriptParse/ScriptParse.swift deleted file mode 100644 index 071cdad876b..00000000000 --- a/Sources/ScriptParse/ScriptParse.swift +++ /dev/null @@ -1,105 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See http://swift.org/LICENSE.txt for license information - See http://swift.org/CONTRIBUTORS.txt for Swift project authors - - FIXME: This is a temporary alternative of the frontend implementation. - */ - -import SwiftSyntax -import TSCBasic -import Foundation -import ArgumentParser - -public struct ScriptParse: ParsableCommand { - @Argument var file: String - - static let encoder: JSONEncoder = { - let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted - return encoder - }() - - public init() {} -} - -extension ScriptParse { - static func parse(_ path: AbsolutePath) throws -> ScriptDependencies { - let syntaxTree = try SyntaxParser.parse(path.asURL) - - var inPackageScope = false - var keyStatements: [CodeBlockItemSyntax] = [] - for statement in syntaxTree.statements { - if statement.statementKind == .import && inPackageScope { - keyStatements.append(statement) - } else { - inPackageScope = false - } - if statement.statementKind == .package - && statement.nextToken?.tokenKind == .importKeyword { - inPackageScope = true - keyStatements.append(statement) - } - } - - var collected: [PackageDependency] = [] - - try keyStatements.forEach { - switch $0.statementKind { - case .package: - var tokens = [TokenSyntax]($0.tokens.dropFirst(2)) - guard tokens.first?.tokenKind == .leftParen, - tokens.last?.tokenKind == .rightParen else { - throw ScriptParseError.wrongSyntax - } - tokens.removeFirst() - tokens.removeLast() - let desc = tokens.map(\.text).joined() - // parse the first argument - if let _path = try parseStringArgument(&tokens, label: "path"), - case let path = AbsolutePath(_path, relativeTo: path.parentDirectory) { - let name = try parseStringArgument(&tokens, label: "name") - collected.append(PackageDependency(of: PackageModel(desc.replacingOccurrences(of: _path, with: path.pathString), path: path, name: name))) - } else if let _url = try parseStringArgument(&tokens, label: "url"), - let url = URL(string: _url) { - collected.append(PackageDependency(of: PackageModel(desc, url: url))) - } - // TODO: other parsing - else { - collected.append(PackageDependency(of: PackageModel(desc))) - } - - case .import: - let tokens = [TokenSyntax]($0.tokens.dropFirst()) - guard tokens.count == 1, - let moduleToken = tokens.first, - case .identifier(let moduleName) = moduleToken.tokenKind - else { throw ScriptParseError.unsupportedSyntax } - var model = collected.removeLast() - model.modules.append(moduleName) - collected.append(model) - - default: - fatalError() - } - } - return ScriptDependencies(sourceFile: path, modules: collected) - } - - public func run() throws { - let path = try AbsolutePath(validating: file) - let json = try ScriptParse.manifest(for: path) - print(String(decoding: json, as: UTF8.self)) - } - - // FIXME: This should be removed some day. - public static func manifest(for path: AbsolutePath) throws -> Data { - let output = try parse(path) - let json = try encoder.encode(output) - return json - } -} diff --git a/Sources/ScriptParse/StatementKind.swift b/Sources/ScriptParse/StatementKind.swift deleted file mode 100644 index e4918c5824a..00000000000 --- a/Sources/ScriptParse/StatementKind.swift +++ /dev/null @@ -1,30 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See http://swift.org/LICENSE.txt for license information - See http://swift.org/CONTRIBUTORS.txt for Swift project authors - - FIXME: This is a temporary alternative of the frontend implementation. - */ - -import SwiftSyntax - -enum StatementKind { - case package - case `import` - case others -} - -extension CodeBlockItemSyntax { - var statementKind: StatementKind { - let tokens = tokens.map(\.tokenKind) - if tokens.starts(with: [.importKeyword]) { - return .import - } else if tokens.starts(with: [.atSign, .identifier("package")]) { - return .package - } else { return .others } - } -} diff --git a/Sources/ScriptParse/misc.swift b/Sources/ScriptParse/misc.swift deleted file mode 100644 index 276a0f046f3..00000000000 --- a/Sources/ScriptParse/misc.swift +++ /dev/null @@ -1,40 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See http://swift.org/LICENSE.txt for license information - See http://swift.org/CONTRIBUTORS.txt for Swift project authors - - FIXME: This is a temporary alternative of the frontend implementation. - */ - -import SwiftSyntax -import Foundation - -func parseStringArgument(_ tokens: inout [TokenSyntax], label: String? = nil) throws -> String? { - if let label = label { - // parse label - guard case .identifier(let labelString) = tokens.first?.tokenKind, - labelString == label else { - return nil - } - tokens.removeFirst() - // parse colon - guard case .colon = tokens.removeFirst().tokenKind else { - throw ScriptParseError.wrongSyntax - } - } - guard case .stringLiteral(let string) = tokens.removeFirst().tokenKind else { - throw ScriptParseError.wrongSyntax - } - return string.unescaped() -} - -private extension String { - func unescaped() -> String { - let data = data(using: .utf8)! - return try! JSONDecoder().decode(String.self, from: data) - } -} diff --git a/Sources/package-parser/CMakeLists.txt b/Sources/package-parser/CMakeLists.txt deleted file mode 100644 index ef8972dcc01..00000000000 --- a/Sources/package-parser/CMakeLists.txt +++ /dev/null @@ -1,17 +0,0 @@ -# This source file is part of the Swift.org open source project -# -# Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors -# Licensed under Apache License v2.0 with Runtime Library Exception -# -# See http://swift.org/LICENSE.txt for license information -# See http://swift.org/CONTRIBUTORS.txt for Swift project authors - -add_executable(package-parser - main.swift) -target_link_libraries(package-parser PRIVATE - ScriptParse) - -if(USE_CMAKE_INSTALL) -install(TARGETS package-parser - DESTINATION bin) -endif() diff --git a/Sources/package-parser/main.swift b/Sources/package-parser/main.swift deleted file mode 100644 index aac12531698..00000000000 --- a/Sources/package-parser/main.swift +++ /dev/null @@ -1,15 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See http://swift.org/LICENSE.txt for license information - See http://swift.org/CONTRIBUTORS.txt for Swift project authors - - FIXME: This is a temporary alternative of the frontend implementation. -*/ - -import ScriptParse - -ScriptParse.main() From a6eab23f9865f5c57506b727a858a95100dfacf8 Mon Sep 17 00:00:00 2001 From: stevapple Date: Fri, 13 Aug 2021 13:17:36 +0800 Subject: [PATCH 14/23] Fix building --- Sources/Commands/SwiftScriptTool.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Commands/SwiftScriptTool.swift b/Sources/Commands/SwiftScriptTool.swift index 06cd863f1e4..697455bbd63 100644 --- a/Sources/Commands/SwiftScriptTool.swift +++ b/Sources/Commands/SwiftScriptTool.swift @@ -11,7 +11,6 @@ import ArgumentParser import Basics import PackageModel -import ScriptParse import ScriptingCore import TSCBasic import Workspace From c6192bf6bfd6f639eddabf67f09f60a17fa8dcfd Mon Sep 17 00:00:00 2001 From: stevapple Date: Fri, 13 Aug 2021 13:36:23 +0800 Subject: [PATCH 15/23] Update tool name --- Sources/ScriptingCore/utils.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ScriptingCore/utils.swift b/Sources/ScriptingCore/utils.swift index 4d72ac8343b..fb7d52efc2d 100644 --- a/Sources/ScriptingCore/utils.swift +++ b/Sources/ScriptingCore/utils.swift @@ -58,7 +58,8 @@ public func resolveFilePath(_ path: String) -> AbsolutePath? { public func checkAndPerformCache(for file: String, at dirPath: AbsolutePath) throws -> (productName: String, cacheDirPath: AbsolutePath) { if let scriptPath = resolveFilePath(file) { let json: Data = try { - let proc = Process(args: "package-parser", scriptPath.pathString) + // See: https://github.com/stevapple/package-syntax-parser + let proc = Process(args: "package-syntax-parser", scriptPath.pathString) try proc.launch() return try Data(proc.waitUntilExit().output.get()) }() From 847b584fb692bb3489549f7ae5789c573efeb698 Mon Sep 17 00:00:00 2001 From: stevapple Date: Fri, 13 Aug 2021 17:06:07 +0800 Subject: [PATCH 16/23] Toolchain support --- Sources/swift-package/main.swift | 2 ++ Utilities/bootstrap | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/swift-package/main.swift b/Sources/swift-package/main.swift index e206219e1b7..1bfec937730 100644 --- a/Sources/swift-package/main.swift +++ b/Sources/swift-package/main.swift @@ -24,6 +24,8 @@ case "swift-test": SwiftTestTool.main() case "swift-run": SwiftRunTool.main() +case "swift-script": + SwiftScriptTool.main() case "swift-package-collection": SwiftPackageCollectionsTool.main() default: diff --git a/Utilities/bootstrap b/Utilities/bootstrap index 59d9b99fda4..ab743ae7272 100755 --- a/Utilities/bootstrap +++ b/Utilities/bootstrap @@ -393,7 +393,7 @@ def install_swiftpm(prefix, args): # Install the swift-package tool and create symlinks to it. cli_tool_dest = os.path.join(prefix, "bin") install_binary(args, "swift-package", cli_tool_dest) - for tool in ["swift-build", "swift-test", "swift-run", "swift-package-collection"]: + for tool in ["swift-build", "swift-test", "swift-run", "swift-script", "swift-package-collection"]: src = "swift-package" dest = os.path.join(cli_tool_dest, tool) note("Creating tool symlink from %s to %s" % (src, dest)) From 67e494c374bea48ccedb49c16f9aa8574e03ab50 Mon Sep 17 00:00:00 2001 From: stevapple Date: Mon, 16 Aug 2021 23:22:17 +0800 Subject: [PATCH 17/23] Update ScriptingCore to keep up with package-syntax-parser --- Sources/ScriptingCore/Models.swift | 28 ++++++++++++++++++++-------- Sources/ScriptingCore/utils.swift | 15 +++++++++++---- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/Sources/ScriptingCore/Models.swift b/Sources/ScriptingCore/Models.swift index 591e034aa54..e144f24c53f 100644 --- a/Sources/ScriptingCore/Models.swift +++ b/Sources/ScriptingCore/Models.swift @@ -12,12 +12,18 @@ import Foundation import TSCBasic import TSCUtility +/// The model that represents a SwiftPM package dependency, or `PackageDescription.Dependency`. public struct PackageModel: Codable { - public let raw: String - public let url: Foundation.URL? - public let path: AbsolutePath? + /// The raw body of `@package(...)`. + let raw: String + /// The path to the local directory of the package. + let path: AbsolutePath? + /// The URL of a remote package. + let url: Foundation.URL? + /// The user-defined name for a local package. private let _name: String? + /// The resolved name of the package. public var name: String { if let name = _name { return name @@ -34,18 +40,24 @@ public struct PackageModel: Codable { } } +/// The model that represents a SwiftPM package dependency and modules from it. public struct PackageDependency: Codable { - public let package: PackageModel - public var modules: [String] = [] + /// The package dependency. + let package: PackageModel + /// Modules imported from the package. + var modules: [String] = [] init(of package: PackageModel) { self.package = package } } -public struct ScriptDependencies: Codable { - public let sourceFile: AbsolutePath - public let modules: [PackageDependency] +/// The model that represents parsed SwiftPM dependency info from a script. +struct ScriptDependencies: Codable { + /// The path to the script file. + let sourceFile: AbsolutePath + /// The parsed dependencies. + let dependencies: [PackageDependency] } public extension PackageModel { diff --git a/Sources/ScriptingCore/utils.swift b/Sources/ScriptingCore/utils.swift index fb7d52efc2d..98d05cf548b 100644 --- a/Sources/ScriptingCore/utils.swift +++ b/Sources/ScriptingCore/utils.swift @@ -19,11 +19,11 @@ fileprivate extension AbsolutePath { } } -/// An enumeration of the errors that can be generated by the run tool. +/// An enumeration of the errors that can be generated by the script tool. public enum ScriptError: Swift.Error { /// The specified file doesn't exist. case fileNotFound(String) - /// The target path is directory + /// The target path is directory. case isDirectory(String) } @@ -55,6 +55,7 @@ public func resolveFilePath(_ path: String) -> AbsolutePath? { return absolutePath } +/// Perform cache for a script and returns cache info. public func checkAndPerformCache(for file: String, at dirPath: AbsolutePath) throws -> (productName: String, cacheDirPath: AbsolutePath) { if let scriptPath = resolveFilePath(file) { let json: Data = try { @@ -66,18 +67,21 @@ public func checkAndPerformCache(for file: String, at dirPath: AbsolutePath) thr let decoder = JSONDecoder() let manifest = try decoder.decode(ScriptDependencies.self, from: json) + // Resolve product name and create cache directory. let productName = scriptPath.basename.spm_dropSuffix(".swift").spm_mangledToBundleIdentifier() let cacheDirPath = dirPath.appending(component: "\(productName)-\(scriptPath.sha256Hash)") try localFileSystem.createDirectory(cacheDirPath, recursive: true) + // Build package structure. let sourceDirPath = cacheDirPath.appending(components: "Sources", productName) try localFileSystem.createDirectory(sourceDirPath, recursive: true) if !localFileSystem.exists(sourceDirPath.appending(component: "main.swift")) { try localFileSystem.createSymbolicLink(sourceDirPath.appending(component: "main.swift"), pointingAt: scriptPath, relative: false) } + // Build target dependencies. var targets: [(name: String, package: String)] = [] - for package in manifest.modules { + for package in manifest.dependencies { let packageName = package.package.name for target in package.modules { targets.append((target, packageName)) @@ -94,7 +98,7 @@ public func checkAndPerformCache(for file: String, at dirPath: AbsolutePath) thr name: "\(productName)", targets: ["\(productName)"]), ], - dependencies: [\(manifest.modules.map{".package(\($0.package.raw))"} + dependencies: [\(manifest.dependencies.map{".package(\($0.package.raw))"} .joined(separator: ", "))], targets: [ .executableTarget( @@ -112,15 +116,18 @@ public func checkAndPerformCache(for file: String, at dirPath: AbsolutePath) thr ) return (productName, cacheDirPath) } else { + // Check if the specified cache exists. let cacheDirPath = dirPath.appending(component: file) guard localFileSystem.isDirectory(cacheDirPath) else { throw ScriptError.fileNotFound(file) } + // Drop the hash suffix. // eg. Dropping "-abc123" from "test-abc123" return (String(file.dropLast(7)), cacheDirPath) } } +/// Perform cache for a script and returns cache info. public func checkAndPerformCache(for file: String) throws -> (productName: String, cacheDirPath: AbsolutePath) { try checkAndPerformCache(for: file, at: localFileSystem.getOrCreateSwiftScriptCacheDirectory()) } From 3a7490bdcc4ebe6ccf229fb39d4e033fbda11a3c Mon Sep 17 00:00:00 2001 From: stevapple Date: Mon, 16 Aug 2021 23:32:42 +0800 Subject: [PATCH 18/23] Add document and comments --- Sources/Commands/SwiftScriptTool.swift | 5 +++++ Sources/ScriptingCore/README.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 Sources/ScriptingCore/README.md diff --git a/Sources/Commands/SwiftScriptTool.swift b/Sources/Commands/SwiftScriptTool.swift index 697455bbd63..9e5969c0527 100644 --- a/Sources/Commands/SwiftScriptTool.swift +++ b/Sources/Commands/SwiftScriptTool.swift @@ -26,6 +26,7 @@ extension ScriptCommand { public func run() throws { let (productName, cacheDirPath) = try checkAndPerformCache(for: file) + // Redirect to the cache directory. var swiftOptions = swiftOptions swiftOptions.packagePath = cacheDirPath swiftOptions.buildPath = nil @@ -96,6 +97,7 @@ extension SwiftScriptTool { func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws { let output = BufferedOutputByteStream() + // Mute build system if `-quiet` is set. if quiet { swiftTool.redirectStdoutTo(.init(output)) } else { @@ -148,6 +150,7 @@ extension SwiftScriptTool { try buildSystem.build(subset: .product(productName)) } + // Copy the prebuilt binary if output is set. if let output = output { let executablePath = try swiftTool.buildParameters().buildPath.appending(component: productName) guard !localFileSystem.isDirectory(output) else { @@ -307,6 +310,7 @@ extension SwiftScriptTool { func run() throws { let cacheDir = try localFileSystem.getOrCreateSwiftScriptCacheDirectory() let scripts = try localFileSystem.getDirectoryContents(cacheDir) + // Walk through the cache and find origin script paths. let resolved = try scripts.compactMap { script -> (String, AbsolutePath)? in let sourceDir = cacheDir.appending(components: script, "Sources") guard localFileSystem.isDirectory(sourceDir), @@ -316,6 +320,7 @@ extension SwiftScriptTool { } return (script, realpath) } + // Print the resolved cache info. print("\(scripts.count) script\(scripts.count > 1 ? "s" : "") cached at \(cacheDir)") guard let maxLength = resolved.map(\.0.count).max() else { return } resolved.forEach { (name, path) in diff --git a/Sources/ScriptingCore/README.md b/Sources/ScriptingCore/README.md new file mode 100644 index 00000000000..bdc86d83ac8 --- /dev/null +++ b/Sources/ScriptingCore/README.md @@ -0,0 +1,5 @@ +# ScriptingCore Library + +This library defines the support for the `swift-script` tool, +including resolving the output from `package-syntax-parser` and +cache support. From 9baf3769242b1d1577e1f4a31459469f2d2861d9 Mon Sep 17 00:00:00 2001 From: stevapple Date: Sun, 22 Aug 2021 16:27:02 +0800 Subject: [PATCH 19/23] improve ScriptingCore --- Sources/ScriptingCore/Models.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ScriptingCore/Models.swift b/Sources/ScriptingCore/Models.swift index e144f24c53f..8e9558245a2 100644 --- a/Sources/ScriptingCore/Models.swift +++ b/Sources/ScriptingCore/Models.swift @@ -20,7 +20,7 @@ public struct PackageModel: Codable { let path: AbsolutePath? /// The URL of a remote package. let url: Foundation.URL? - /// The user-defined name for a local package. + /// The user-defined name for a package. private let _name: String? /// The resolved name of the package. @@ -34,7 +34,7 @@ public struct PackageModel: Codable { } else if let url = url { name = url.pathComponents.last!.spm_dropGitSuffix() } else { - fatalError() + preconditionFailure("Invalid package model") } return name } From 934eea491e070504c688cfe498d17f29fb34e56b Mon Sep 17 00:00:00 2001 From: stevapple Date: Sun, 22 Aug 2021 18:02:48 +0800 Subject: [PATCH 20/23] Add tests for swift-script --- .../Scripts/EchoArguments/EchoArguments.swift | 2 + .../Scripts/EchoArguments/PackageSyntax.txt | 6 + Fixtures/Scripts/EchoCWD/EchoCWD.swift | 4 + Fixtures/Scripts/EchoCWD/PackageSyntax.txt | 16 +++ .../Scripts/EchoCWD/cwd-dump/Package.swift | 19 +++ .../cwd-dump/Sources/CwdDump/CwdDump.swift | 7 ++ Sources/SPMTestSupport/SwiftPMProduct.swift | 3 + Sources/SPMTestSupport/misc.swift | 27 ++++- Tests/CommandsTests/ScriptToolTests.swift | 111 ++++++++++++++++++ 9 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 Fixtures/Scripts/EchoArguments/EchoArguments.swift create mode 100644 Fixtures/Scripts/EchoArguments/PackageSyntax.txt create mode 100644 Fixtures/Scripts/EchoCWD/EchoCWD.swift create mode 100644 Fixtures/Scripts/EchoCWD/PackageSyntax.txt create mode 100644 Fixtures/Scripts/EchoCWD/cwd-dump/Package.swift create mode 100644 Fixtures/Scripts/EchoCWD/cwd-dump/Sources/CwdDump/CwdDump.swift create mode 100644 Tests/CommandsTests/ScriptToolTests.swift diff --git a/Fixtures/Scripts/EchoArguments/EchoArguments.swift b/Fixtures/Scripts/EchoArguments/EchoArguments.swift new file mode 100644 index 00000000000..d3dedd1c1e5 --- /dev/null +++ b/Fixtures/Scripts/EchoArguments/EchoArguments.swift @@ -0,0 +1,2 @@ +let arguments = CommandLine.arguments.dropFirst() +print(arguments.map{"\"\($0)\""}.joined(separator: " ")) diff --git a/Fixtures/Scripts/EchoArguments/PackageSyntax.txt b/Fixtures/Scripts/EchoArguments/PackageSyntax.txt new file mode 100644 index 00000000000..3a4674c79b7 --- /dev/null +++ b/Fixtures/Scripts/EchoArguments/PackageSyntax.txt @@ -0,0 +1,6 @@ +{ + "dependencies" : [ + + ], + "sourceFile" : "\/Users\/yr.chen\/Developer\/GSoC\/swift-package-manager\/Fixtures\/Scripts\/EchoArguments\/EchoArguments.swift" +} diff --git a/Fixtures/Scripts/EchoCWD/EchoCWD.swift b/Fixtures/Scripts/EchoCWD/EchoCWD.swift new file mode 100644 index 00000000000..dae616178e6 --- /dev/null +++ b/Fixtures/Scripts/EchoCWD/EchoCWD.swift @@ -0,0 +1,4 @@ +@package(name: "CwdDump", path: "cwd-dump") +import CwdDump + +CwdDump.main() diff --git a/Fixtures/Scripts/EchoCWD/PackageSyntax.txt b/Fixtures/Scripts/EchoCWD/PackageSyntax.txt new file mode 100644 index 00000000000..04d6e5c7f96 --- /dev/null +++ b/Fixtures/Scripts/EchoCWD/PackageSyntax.txt @@ -0,0 +1,16 @@ +{ + "dependencies" : [ + { + "package" : { + "raw" : "name:\"CwdDump\",path:\"\/Users\/yr.chen\/Developer\/GSoC\/swift-package-manager\/Fixtures\/Scripts\/EchoCWD\/cwd-dump\"", + "path" : "\/Users\/yr.chen\/Developer\/GSoC\/swift-package-manager\/Fixtures\/Scripts\/EchoCWD\/cwd-dump", + "name" : "CwdDump" + }, + "modules" : [ + "CwdDump" + ] + } + ], + "sourceFile" : "\/Users\/yr.chen\/Developer\/GSoC\/swift-package-manager\/Fixtures\/Scripts\/EchoCWD\/EchoCWD.swift" +} + diff --git a/Fixtures/Scripts/EchoCWD/cwd-dump/Package.swift b/Fixtures/Scripts/EchoCWD/cwd-dump/Package.swift new file mode 100644 index 00000000000..2581d5a1d5d --- /dev/null +++ b/Fixtures/Scripts/EchoCWD/cwd-dump/Package.swift @@ -0,0 +1,19 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "CwdDump", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "CwdDump", + targets: ["CwdDump"]) + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target(name: "CwdDump") + ] +) diff --git a/Fixtures/Scripts/EchoCWD/cwd-dump/Sources/CwdDump/CwdDump.swift b/Fixtures/Scripts/EchoCWD/cwd-dump/Sources/CwdDump/CwdDump.swift new file mode 100644 index 00000000000..2f94e0f0549 --- /dev/null +++ b/Fixtures/Scripts/EchoCWD/cwd-dump/Sources/CwdDump/CwdDump.swift @@ -0,0 +1,7 @@ +import Foundation + +public struct CwdDump { + public static func main() { + print(FileManager().currentDirectoryPath) + } +} diff --git a/Sources/SPMTestSupport/SwiftPMProduct.swift b/Sources/SPMTestSupport/SwiftPMProduct.swift index d6d41fd2905..89086aa4529 100644 --- a/Sources/SPMTestSupport/SwiftPMProduct.swift +++ b/Sources/SPMTestSupport/SwiftPMProduct.swift @@ -17,6 +17,7 @@ public enum SwiftPMProduct: Product { case SwiftPackage case SwiftTest case SwiftRun + case SwiftScript case XCTestHelper /// Executable name. @@ -30,6 +31,8 @@ public enum SwiftPMProduct: Product { return RelativePath("swift-test") case .SwiftRun: return RelativePath("swift-run") + case .SwiftScript: + return RelativePath("swift-script") case .XCTestHelper: return RelativePath("swiftpm-xctest-helper") } diff --git a/Sources/SPMTestSupport/misc.swift b/Sources/SPMTestSupport/misc.swift index 27bcdd7c9cf..02ab23b6ed4 100644 --- a/Sources/SPMTestSupport/misc.swift +++ b/Sources/SPMTestSupport/misc.swift @@ -62,7 +62,7 @@ public func fixture( return } - // The fixture contains either a checkout or just a Git directory. + // The fixture contains either a checkout, a script or just a Git directory. if localFileSystem.isFile(fixtureDir.appending(component: "Package.swift")) { // It's a single package, so copy the whole directory as-is. let dstDir = tmpDirPath.appending(component: copyName) @@ -70,6 +70,15 @@ public func fixture( // Invoke the block, passing it the path of the copied fixture. try body(dstDir) + } else if localFileSystem.isFile(fixtureDir.appending(component: "PackageSyntax.txt")) { + // It's a directory with scripts, so copy the whole directory as-is. + let dstDir = tmpDirPath.appending(component: copyName) + try systemQuietly("cp", "-R", "-H", fixtureDir.pathString, dstDir.pathString) + + // Mock package-syntax-parser output with PackageSyntax.txt + try withPackageSyntax(from: dstDir.appending(component: "PackageSyntax.txt")) { + try body(dstDir) + } } else { // Copy each of the package directories and construct a git repo in it. for fileName in try localFileSystem.getDirectoryContents(fixtureDir).sorted() { @@ -249,3 +258,19 @@ public func loadPackageGraph( createREPLProduct: createREPLProduct ) } + +private func withPackageSyntax(from file: AbsolutePath, _ body: () throws -> Void) throws -> Void { + var pathSuffix = "" + if let PATH = ProcessEnv.path { + pathSuffix += ":" + PATH + } + try withTemporaryDirectory { path in + let parser = path.appending(component: "package-syntax-parser") + try localFileSystem.writeFileContents(parser) { + $0.write("cat \(file.toJSON())\n") + $0.flush() + } + try localFileSystem.chmod(.executable, path: parser) + try withCustomEnv(["PATH": path.pathString + pathSuffix], body: body) + } +} diff --git a/Tests/CommandsTests/ScriptToolTests.swift b/Tests/CommandsTests/ScriptToolTests.swift new file mode 100644 index 00000000000..98c9083b2fd --- /dev/null +++ b/Tests/CommandsTests/ScriptToolTests.swift @@ -0,0 +1,111 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +import XCTest + +import SPMTestSupport +import Commands +import TSCBasic + +final class ScriptToolTests: XCTestCase { + private func execute(_ args: [String]) throws -> (stdout: String, stderr: String) { + return try SwiftPMProduct.SwiftScript.execute(args) + } + + private func build(_ args: String...) throws -> (stdout: String, stderr: String) { + return try execute(["build"] + args) + } + + private func run(_ args: String...) throws -> (stdout: String, stderr: String) { + return try execute(["run"] + args) + } + + func testUsage() throws { + let stdout = try execute(["-help"]).stdout + XCTAssert(stdout.contains("USAGE: swift script "), "got stdout:\n" + stdout) + } + + func testSeeAlso() throws { + let stdout = try execute(["--help"]).stdout + XCTAssert(stdout.contains("SEE ALSO: swift build, swift run, swift package, swift test"), "got stdout:\n" + stdout) + } + + func testVersion() throws { + let stdout = try execute(["--version"]).stdout + XCTAssert(stdout.contains("Swift Package Manager"), "got stdout:\n" + stdout) + } + + func testWrongScriptPath() throws { + try withTemporaryDirectory { path in + let pathString = path.appending(component: "EchoArguments").pathString + do { + _ = try run(pathString) + XCTFail("Unexpected success") + } catch SwiftPMProductError.executionFailure(_, _, let stderr) { + XCTExpectFailure() // FIXME: Seems an implementation bug of TSC? + XCTAssertEqual(stderr, "error: \(pathString) doesn't exist\n") + } + } + } + + func testArgumentPassing() throws { + fixture(name: "Scripts/EchoArguments") { path in + let result = try run(path.appending(component: "EchoArguments.swift").pathString, + "1", "--hello", "world") + + // We only expect tool's output on the stdout stream. + XCTAssertEqual(result.stdout.trimmingCharacters(in: .whitespacesAndNewlines), + #""1" "--hello" "world""#) + + // swift-build-tool output should go to stderr. + XCTAssertMatch(result.stderr, .contains("Using cache: EchoArguments-")) + XCTAssertMatch(result.stderr, .contains("Compiling")) + XCTAssertMatch(result.stderr, .contains("Linking")) + } + } + + func testCurrentWorkingDirectoryWithLocalDependency() throws { + fixture(name: "Scripts/EchoCWD") { path in + let result = try run(path.appending(component: "EchoCWD.swift").pathString) + + XCTAssertEqual(result.stdout.trimmingCharacters(in: .whitespacesAndNewlines), + localFileSystem.currentWorkingDirectory?.pathString) + } + } + + func testPrebuild() throws { + fixture(name: "Scripts/EchoArguments") { path in + let result = try build(path.appending(component: "EchoArguments.swift").pathString) + + // swift-build-tool output should go to stderr. + XCTAssertMatch(result.stderr, .contains("Using cache: EchoArguments-")) + XCTAssertMatch(result.stderr, .contains("Compiling")) + XCTAssertMatch(result.stderr, .contains("Linking")) + } + } + + func testQuietMode() throws { + fixture(name: "Scripts/EchoArguments") { path in + let result = try run(path.appending(component: "EchoArguments.swift").pathString, "--quiet", + "1", "--hello", "world") + + // We only expect tool's output on the stdout stream. + XCTAssertEqual(result.stdout.trimmingCharacters(in: .whitespacesAndNewlines), + #""1" "--hello" "world""#) + + // swift-build-tool output should be muted. + XCTAssertNoMatch(result.stderr, .contains("Using cache: EchoArguments-")) + XCTAssertNoMatch(result.stderr, .contains("Compiling")) + XCTAssertNoMatch(result.stderr, .contains("Linking")) + } + } + + // TODO: Tests for other swift-script tools +} From edd3056a3e53339a56dca2b36ee073c06728dbc4 Mon Sep 17 00:00:00 2001 From: stevapple Date: Tue, 24 Aug 2021 02:54:41 +0800 Subject: [PATCH 21/23] Fix cache checking bug --- Sources/ScriptingCore/utils.swift | 7 ++++--- Tests/CommandsTests/ScriptToolTests.swift | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ScriptingCore/utils.swift b/Sources/ScriptingCore/utils.swift index 98d05cf548b..1c6fe6c538e 100644 --- a/Sources/ScriptingCore/utils.swift +++ b/Sources/ScriptingCore/utils.swift @@ -47,7 +47,7 @@ public func resolveFilePath(_ path: String) -> AbsolutePath? { guard let cwd = localFileSystem.currentWorkingDirectory else { return nil } - absolutePath = AbsolutePath(cwd, path) + absolutePath = AbsolutePath(path, relativeTo: cwd) } guard localFileSystem.isFile(absolutePath) else { return nil @@ -117,8 +117,9 @@ public func checkAndPerformCache(for file: String, at dirPath: AbsolutePath) thr return (productName, cacheDirPath) } else { // Check if the specified cache exists. - let cacheDirPath = dirPath.appending(component: file) - guard localFileSystem.isDirectory(cacheDirPath) else { + guard !file.contains("/"), + case let cacheDirPath = dirPath.appending(component: file), + localFileSystem.isDirectory(cacheDirPath) else { throw ScriptError.fileNotFound(file) } // Drop the hash suffix. diff --git a/Tests/CommandsTests/ScriptToolTests.swift b/Tests/CommandsTests/ScriptToolTests.swift index 98c9083b2fd..4bc7d34cd5b 100644 --- a/Tests/CommandsTests/ScriptToolTests.swift +++ b/Tests/CommandsTests/ScriptToolTests.swift @@ -49,7 +49,6 @@ final class ScriptToolTests: XCTestCase { _ = try run(pathString) XCTFail("Unexpected success") } catch SwiftPMProductError.executionFailure(_, _, let stderr) { - XCTExpectFailure() // FIXME: Seems an implementation bug of TSC? XCTAssertEqual(stderr, "error: \(pathString) doesn't exist\n") } } From ee778d99b2519e0388071f27bf8083b5ceb71495 Mon Sep 17 00:00:00 2001 From: stevapple Date: Tue, 24 Aug 2021 02:55:48 +0800 Subject: [PATCH 22/23] Use SCRIPT_DIR placeholder in fixtures --- Fixtures/Scripts/EchoArguments/PackageSyntax.txt | 2 +- Fixtures/Scripts/EchoCWD/PackageSyntax.txt | 6 +++--- Sources/SPMTestSupport/misc.swift | 5 ++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Fixtures/Scripts/EchoArguments/PackageSyntax.txt b/Fixtures/Scripts/EchoArguments/PackageSyntax.txt index 3a4674c79b7..20a041ca6af 100644 --- a/Fixtures/Scripts/EchoArguments/PackageSyntax.txt +++ b/Fixtures/Scripts/EchoArguments/PackageSyntax.txt @@ -2,5 +2,5 @@ "dependencies" : [ ], - "sourceFile" : "\/Users\/yr.chen\/Developer\/GSoC\/swift-package-manager\/Fixtures\/Scripts\/EchoArguments\/EchoArguments.swift" + "sourceFile" : "SCRIPT_DIR\/EchoArguments.swift" } diff --git a/Fixtures/Scripts/EchoCWD/PackageSyntax.txt b/Fixtures/Scripts/EchoCWD/PackageSyntax.txt index 04d6e5c7f96..1b4bcef0cd8 100644 --- a/Fixtures/Scripts/EchoCWD/PackageSyntax.txt +++ b/Fixtures/Scripts/EchoCWD/PackageSyntax.txt @@ -2,8 +2,8 @@ "dependencies" : [ { "package" : { - "raw" : "name:\"CwdDump\",path:\"\/Users\/yr.chen\/Developer\/GSoC\/swift-package-manager\/Fixtures\/Scripts\/EchoCWD\/cwd-dump\"", - "path" : "\/Users\/yr.chen\/Developer\/GSoC\/swift-package-manager\/Fixtures\/Scripts\/EchoCWD\/cwd-dump", + "raw" : "name:\"CwdDump\",path:\"SCRIPT_DIR\/cwd-dump\"", + "path" : "SCRIPT_DIR\/cwd-dump", "name" : "CwdDump" }, "modules" : [ @@ -11,6 +11,6 @@ ] } ], - "sourceFile" : "\/Users\/yr.chen\/Developer\/GSoC\/swift-package-manager\/Fixtures\/Scripts\/EchoCWD\/EchoCWD.swift" + "sourceFile" : "SCRIPT_DIR\/EchoCWD.swift" } diff --git a/Sources/SPMTestSupport/misc.swift b/Sources/SPMTestSupport/misc.swift index 02ab23b6ed4..2cbf0d7b3f2 100644 --- a/Sources/SPMTestSupport/misc.swift +++ b/Sources/SPMTestSupport/misc.swift @@ -267,7 +267,10 @@ private func withPackageSyntax(from file: AbsolutePath, _ body: () throws -> Voi try withTemporaryDirectory { path in let parser = path.appending(component: "package-syntax-parser") try localFileSystem.writeFileContents(parser) { - $0.write("cat \(file.toJSON())\n") + // Manually escape the separator. + let dirname = file.dirname.replacingOccurrences(of: "/", with: "\\/") + // Replace placeholder with the real dirpath. + $0.write("cat \(file.pathString.spm_shellEscaped()) | sed 's$SCRIPT_DIR$\(dirname)$g' \n") $0.flush() } try localFileSystem.chmod(.executable, path: parser) From 13ac15181c28cf50b76d44946df722f2c530b8f1 Mon Sep 17 00:00:00 2001 From: stevapple Date: Tue, 24 Aug 2021 03:48:57 +0800 Subject: [PATCH 23/23] Fix symlink resolution --- Sources/Commands/SwiftScriptTool.swift | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Sources/Commands/SwiftScriptTool.swift b/Sources/Commands/SwiftScriptTool.swift index 9e5969c0527..bc956c5bd33 100644 --- a/Sources/Commands/SwiftScriptTool.swift +++ b/Sources/Commands/SwiftScriptTool.swift @@ -10,6 +10,7 @@ import ArgumentParser import Basics +import Foundation import PackageModel import ScriptingCore import TSCBasic @@ -310,21 +311,27 @@ extension SwiftScriptTool { func run() throws { let cacheDir = try localFileSystem.getOrCreateSwiftScriptCacheDirectory() let scripts = try localFileSystem.getDirectoryContents(cacheDir) - // Walk through the cache and find origin script paths. - let resolved = try scripts.compactMap { script -> (String, AbsolutePath)? in + // Walk through the cache and find original script paths. + let resolved = try scripts.compactMap { script -> (String, String)? in let sourceDir = cacheDir.appending(components: script, "Sources") guard localFileSystem.isDirectory(sourceDir), let name = try localFileSystem.getDirectoryContents(sourceDir).first, - case let realpath = resolveSymlinks(sourceDir.appending(components: name, "main.swift")) else { + case let path = sourceDir.appending(components: name, "main.swift"), + let original = try? FileManager.default.destinationOfSymbolicLink(atPath: path.pathString) else { return nil } - return (script, realpath) + // Check if the original script still exists. + if localFileSystem.exists(path, followSymlink: true) { + return (script, original) + } else { + return (script, "\(original) (removed)") + } } // Print the resolved cache info. print("\(scripts.count) script\(scripts.count > 1 ? "s" : "") cached at \(cacheDir)") guard let maxLength = resolved.map(\.0.count).max() else { return } - resolved.forEach { (name, path) in - print(name + String(repeating: " ", count: maxLength - name.count + 2) + path.pathString) + resolved.forEach { (name, desc) in + print(name + String(repeating: " ", count: maxLength - name.count + 2) + desc) } } }