Skip to content

Commit d2f747e

Browse files
committed
Add some more commands
1 parent 0e50e43 commit d2f747e

File tree

4 files changed

+233
-29
lines changed

4 files changed

+233
-29
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil {
378378
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.4.3")),
379379
.package(url: "https://github.com/apple/swift-driver.git", .branch(relatedDependenciesBranch)),
380380
.package(url: "https://github.com/apple/swift-crypto.git", .upToNextMinor(from: "1.1.4")),
381-
.package(url: "https://github.com/apple/swift-syntax.git", .branch("release/5.5")),
381+
.package(url: "https://github.com/apple/swift-syntax.git", .branch(relatedDependenciesBranch)),
382382
]
383383
} else {
384384
package.dependencies += [

Sources/Commands/SwiftScriptTool.swift

Lines changed: 202 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,16 @@ See http://swift.org/CONTRIBUTORS.txt for Swift project authors
1010

1111
import ArgumentParser
1212
import Basics
13-
import Build
14-
import PackageGraph
1513
import PackageModel
1614
import TSCBasic
1715
import ScriptParse
1816
import ScriptingCore
19-
import Foundation
20-
21-
public extension SwiftToolOptions {
22-
func redirect(to dir: AbsolutePath) throws -> SwiftToolOptions {
23-
var options = self
24-
options.packagePath = dir
25-
options.buildPath = nil
26-
try options.validate()
27-
return options
28-
}
29-
}
17+
import Workspace
3018

3119
struct ScriptToolOptions: ParsableArguments {
3220
/// If the executable product should be built before running.
3321
@Flag(name: .customLong("skip-build"), help: "Skip building the executable product")
3422
var shouldSkipBuild: Bool = false
35-
36-
/// Whether to print build progress.
37-
@Flag(help: "Print build progress")
38-
var quiet: Bool = false
3923

4024
var shouldBuild: Bool { !shouldSkipBuild }
4125

@@ -59,6 +43,10 @@ public struct SwiftScriptTool: ParsableCommand {
5943
version: SwiftVersion.currentVersion.completeDisplayString,
6044
subcommands: [
6145
Run.self,
46+
Build.self,
47+
Clean.self,
48+
Reset.self,
49+
Resolve.self,
6250
],
6351
helpNames: [.short, .long, .customLong("help", withSingleDash: true)])
6452

@@ -79,7 +67,7 @@ extension SwiftScriptTool {
7967

8068
/// swift-run tool namespace
8169
extension SwiftScriptTool {
82-
struct Run: ParsableCommand {
70+
struct Run: ScriptCommand {
8371
static let configuration = CommandConfiguration(
8472
abstract: "Runs a script")
8573

@@ -88,21 +76,22 @@ extension SwiftScriptTool {
8876

8977
@OptionGroup()
9078
var options: ScriptToolOptions
91-
92-
func run() throws {
93-
guard let file = options.file else {
94-
throw ScriptError.fileNotFound("")
95-
}
96-
let (productName, cacheDirPath) = try prepareCache(for: file, at: SwiftScriptTool.cacheDir)
9779

98-
let swiftTool = try SwiftTool(options: swiftOptions.redirect(to: cacheDirPath))
99-
swiftTool.redirectStdoutToStderr()
80+
/// Whether to print build progress.
81+
@Flag(help: "Print build progress")
82+
var quiet: Bool = false
83+
84+
func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws {
10085
let output = BufferedOutputByteStream()
101-
if options.quiet {
86+
if quiet {
10287
swiftTool.redirectStdoutTo(.init(output))
88+
} else {
89+
swiftTool.redirectStdoutToStderr()
10390
}
10491

10592
do {
93+
// FIXME: How to hide the note?
94+
swiftTool.diagnostics.emit(note: "Using cache: \(cacheDirPath.basename)")
10695
let buildSystem = try swiftTool.createBuildSystem(explicitProduct: nil)
10796
if options.shouldBuild {
10897
try buildSystem.build(subset: .product(productName))
@@ -120,6 +109,164 @@ extension SwiftScriptTool {
120109
}
121110
}
122111
}
112+
113+
struct Build: ScriptCommand {
114+
static let configuration = CommandConfiguration(
115+
abstract: "Prebuild a script")
116+
117+
@OptionGroup(_hiddenFromHelp: true)
118+
var swiftOptions: SwiftToolOptions
119+
120+
@OptionGroup()
121+
var options: ScriptToolOptions
122+
123+
func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws {
124+
swiftTool.redirectStdoutToStderr()
125+
126+
do {
127+
swiftTool.diagnostics.emit(note: "Using cache: \(cacheDirPath.basename)")
128+
let buildSystem = try swiftTool.createBuildSystem(explicitProduct: nil)
129+
if options.shouldBuild {
130+
try buildSystem.build(subset: .product(productName))
131+
}
132+
} catch let error as ScriptError {
133+
swiftTool.diagnostics.emit(error)
134+
throw ExitCode.failure
135+
}
136+
}
137+
}
138+
}
139+
140+
extension SwiftScriptTool {
141+
struct Clean: ScriptCommand {
142+
static let configuration = CommandConfiguration(
143+
abstract: "Delete build artifacts")
144+
145+
@OptionGroup(_hiddenFromHelp: true)
146+
var swiftOptions: SwiftToolOptions
147+
148+
@OptionGroup()
149+
var options: ScriptToolOptions
150+
151+
func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws {
152+
try swiftTool.getActiveWorkspace().clean(with: swiftTool.diagnostics)
153+
}
154+
}
155+
156+
struct Reset: ScriptCommand {
157+
static let configuration = CommandConfiguration(
158+
abstract: "Reset the complete cache directory")
159+
160+
@OptionGroup(_hiddenFromHelp: true)
161+
var swiftOptions: SwiftToolOptions
162+
163+
@OptionGroup()
164+
var options: ScriptToolOptions
165+
166+
func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws {
167+
try localFileSystem.removeFileTree(cacheDirPath)
168+
}
169+
}
170+
171+
struct Update: ScriptCommand {
172+
static let configuration = CommandConfiguration(
173+
abstract: "Update package dependencies")
174+
175+
@OptionGroup(_hiddenFromHelp: true)
176+
var swiftOptions: SwiftToolOptions
177+
178+
@OptionGroup()
179+
var options: ScriptToolOptions
180+
181+
@Flag(name: [.long, .customShort("n")],
182+
help: "Display the list of dependencies that can be updated")
183+
var dryRun: Bool = false
184+
185+
@Argument(help: "The packages to update")
186+
var packages: [String] = []
187+
188+
func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws {
189+
let workspace = try swiftTool.getActiveWorkspace()
190+
191+
let changes = try workspace.updateDependencies(
192+
root: swiftTool.getWorkspaceRoot(),
193+
packages: packages,
194+
diagnostics: swiftTool.diagnostics,
195+
dryRun: dryRun
196+
)
197+
198+
// try to load the graph which will emit any errors
199+
if !swiftTool.diagnostics.hasErrors {
200+
_ = try workspace.loadPackageGraph(
201+
rootInput: swiftTool.getWorkspaceRoot(),
202+
diagnostics: swiftTool.diagnostics
203+
)
204+
}
205+
206+
if let pinsStore = swiftTool.diagnostics.wrap({ try workspace.pinsStore.load() }),
207+
let changes = changes, dryRun {
208+
logPackageChanges(changes: changes, pins: pinsStore)
209+
}
210+
211+
if !dryRun {
212+
// Throw if there were errors when loading the graph.
213+
// The actual errors will be printed before exiting.
214+
guard !swiftTool.diagnostics.hasErrors else {
215+
throw ExitCode.failure
216+
}
217+
}
218+
}
219+
}
220+
}
221+
222+
extension SwiftScriptTool {
223+
struct ResolveOptions: ParsableArguments {
224+
@Option(help: "The version to resolve at", transform: { Version(string: $0) })
225+
var version: Version?
226+
227+
@Option(help: "The branch to resolve at")
228+
var branch: String?
229+
230+
@Option(help: "The revision to resolve at")
231+
var revision: String?
232+
233+
@Argument(help: "The name of the package to resolve")
234+
var packageName: String?
235+
}
236+
237+
struct Resolve: ScriptCommand {
238+
static let configuration = CommandConfiguration(
239+
abstract: "Resolve package dependencies")
240+
241+
@OptionGroup(_hiddenFromHelp: true)
242+
var swiftOptions: SwiftToolOptions
243+
244+
@OptionGroup()
245+
var options: ScriptToolOptions
246+
247+
@OptionGroup()
248+
var resolveOptions: ResolveOptions
249+
250+
func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws {
251+
// If a package is provided, use that to resolve the dependencies.
252+
if let packageName = resolveOptions.packageName {
253+
let workspace = try swiftTool.getActiveWorkspace()
254+
try workspace.resolve(
255+
packageName: packageName,
256+
root: swiftTool.getWorkspaceRoot(),
257+
version: resolveOptions.version,
258+
branch: resolveOptions.branch,
259+
revision: resolveOptions.revision,
260+
diagnostics: swiftTool.diagnostics)
261+
if swiftTool.diagnostics.hasErrors {
262+
throw ExitCode.failure
263+
}
264+
} else {
265+
// Otherwise, run a normal resolve.
266+
try swiftTool.resolve()
267+
}
268+
}
269+
}
123270
}
124271

125272
/// Executes the executable at the specified path.
@@ -136,3 +283,31 @@ fileprivate func run(
136283
let pathRelativeToWorkingDirectory = excutablePath.relative(to: originalWorkingDirectory)
137284
try exec(path: excutablePath.pathString, args: [pathRelativeToWorkingDirectory.pathString] + arguments)
138285
}
286+
287+
/// Logs all changed dependencies to a stream
288+
/// - Parameter changes: Changes to log
289+
/// - Parameter pins: PinsStore with currently pinned packages to compare changed packages to.
290+
/// - Parameter stream: Stream used for logging
291+
fileprivate func logPackageChanges(changes: [(PackageReference, Workspace.PackageStateChange)], pins: PinsStore, on stream: OutputByteStream = TSCBasic.stdoutStream) {
292+
let changes = changes.filter { $0.1 != .unchanged }
293+
294+
stream <<< "\n"
295+
stream <<< "\(changes.count) dependenc\(changes.count == 1 ? "y has" : "ies have") changed\(changes.count > 0 ? ":" : ".")"
296+
stream <<< "\n"
297+
298+
for (package, change) in changes {
299+
let currentVersion = pins.pinsMap[package.identity]?.state.description ?? ""
300+
switch change {
301+
case let .added(state):
302+
stream <<< "+ \(package.name) \(state.requirement.prettyPrinted)"
303+
case let .updated(state):
304+
stream <<< "~ \(package.name) \(currentVersion) -> \(package.name) \(state.requirement.prettyPrinted)"
305+
case .removed:
306+
stream <<< "- \(package.name) \(currentVersion)"
307+
case .unchanged:
308+
continue
309+
}
310+
stream <<< "\n"
311+
}
312+
stream.flush()
313+
}

Sources/Commands/SwiftTool.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import Build
2626
import XCBuildSupport
2727
import Workspace
2828
import Basics
29+
import ScriptingCore
2930

3031
typealias Diagnostic = TSCBasic.Diagnostic
3132

@@ -283,6 +284,34 @@ extension SwiftCommand {
283284
public static var _errorLabel: String { "error" }
284285
}
285286

287+
protocol ScriptCommand: ParsableCommand {
288+
var swiftOptions: SwiftToolOptions { get }
289+
var options: ScriptToolOptions { get }
290+
291+
func run(_ swiftTool: SwiftTool, as productName: String, at cacheDirPath: AbsolutePath) throws
292+
}
293+
294+
extension ScriptCommand {
295+
public func run() throws {
296+
guard let file = options.file else {
297+
throw ScriptError.fileNotFound("")
298+
}
299+
let (productName, cacheDirPath) = try checkAndPerformCache(for: file, at: SwiftScriptTool.cacheDir)
300+
301+
var swiftOptions = swiftOptions
302+
swiftOptions.packagePath = cacheDirPath
303+
swiftOptions.buildPath = nil
304+
let swiftTool = try SwiftTool(options: swiftOptions)
305+
306+
try self.run(swiftTool, as: productName, at: cacheDirPath)
307+
if swiftTool.diagnostics.hasErrors || swiftTool.executionStatus == .failure {
308+
throw ExitCode.failure
309+
}
310+
}
311+
312+
public static var _errorLabel: String { "error" }
313+
}
314+
286315
public class SwiftTool {
287316
/// The original working directory.
288317
let originalWorkingDirectory: AbsolutePath

Sources/ScriptingCore/utils.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public func resolveFilePath(_ path: String) -> AbsolutePath? {
5252
return absolutePath
5353
}
5454

55-
public func prepareCache(for file: String, at dirPath: AbsolutePath) throws -> (productName: String, cacheDirPath: AbsolutePath) {
55+
public func checkAndPerformCache(for file: String, at dirPath: AbsolutePath) throws -> (productName: String, cacheDirPath: AbsolutePath) {
5656
if let scriptPath = resolveFilePath(file) {
5757
let json = try ScriptParse.manifest(for: scriptPath)
5858
let decoder = JSONDecoder()

0 commit comments

Comments
 (0)