Skip to content

Commit aeb6d4a

Browse files
committed
Allow creation of non-existant directory during swift package init
Running `swift package init --package-path <dir-does-not-exists>` results in an error when SPM attempts to change directories in to the directory that doesn't exist. Add a new boolean to the `_SwiftCommand` protocol that lets commands opt in to creating the directory at `--package-path` if it doesn't exist. Opt in `InitCommand` to this behaviour. Issue: swiftlang#8393
1 parent 19ce521 commit aeb6d4a

File tree

4 files changed

+73
-13
lines changed

4 files changed

+73
-13
lines changed

Sources/Commands/PackageCommands/Init.swift

+3
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ extension SwiftPackageCommand {
4949
@Option(name: .customLong("name"), help: "Provide custom package name")
5050
var packageName: String?
5151

52+
// This command should support creating the supplied --package-path if it isn't created.
53+
var createPackagePath = true
54+
5255
func run(_ swiftCommandState: SwiftCommandState) throws {
5356
guard let cwd = swiftCommandState.fileSystem.currentWorkingDirectory else {
5457
throw InternalError("Could not find the current working directory")

Sources/CoreCommands/SwiftCommandState.swift

+44-13
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import enum TSCBasic.ProcessLockError
5353
import var TSCBasic.stderrStream
5454
import class TSCBasic.TerminalController
5555
import class TSCBasic.ThreadSafeOutputByteStream
56+
import enum TSCBasic.SystemError
5657

5758
import var TSCUtility.verbosity
5859

@@ -90,18 +91,25 @@ public protocol _SwiftCommand {
9091
var workspaceDelegateProvider: WorkspaceDelegateProvider { get }
9192
var workspaceLoaderProvider: WorkspaceLoaderProvider { get }
9293
func buildSystemProvider(_ swiftCommandState: SwiftCommandState) throws -> BuildSystemProvider
94+
95+
// If a packagePath is specificed, this indicates that the command allows
96+
// creating the directory if it doesn't exist.
97+
var createPackagePath: Bool { get }
9398
}
9499

95100
extension _SwiftCommand {
96101
public var toolWorkspaceConfiguration: ToolWorkspaceConfiguration {
97102
.init()
98103
}
104+
105+
public var createPackagePath: Bool {
106+
return false
107+
}
99108
}
100109

101110
public protocol SwiftCommand: ParsableCommand, _SwiftCommand {
102111
func run(_ swiftCommandState: SwiftCommandState) throws
103112
}
104-
105113
extension SwiftCommand {
106114
public static var _errorLabel: String { "error" }
107115

@@ -110,7 +118,8 @@ extension SwiftCommand {
110118
options: globalOptions,
111119
toolWorkspaceConfiguration: self.toolWorkspaceConfiguration,
112120
workspaceDelegateProvider: self.workspaceDelegateProvider,
113-
workspaceLoaderProvider: self.workspaceLoaderProvider
121+
workspaceLoaderProvider: self.workspaceLoaderProvider,
122+
createPackagePath: self.createPackagePath
114123
)
115124

116125
// We use this to attempt to catch misuse of the locking APIs since we only release the lock from here.
@@ -151,7 +160,8 @@ extension AsyncSwiftCommand {
151160
options: globalOptions,
152161
toolWorkspaceConfiguration: self.toolWorkspaceConfiguration,
153162
workspaceDelegateProvider: self.workspaceDelegateProvider,
154-
workspaceLoaderProvider: self.workspaceLoaderProvider
163+
workspaceLoaderProvider: self.workspaceLoaderProvider,
164+
createPackagePath: self.createPackagePath
155165
)
156166

157167
// We use this to attempt to catch misuse of the locking APIs since we only release the lock from here.
@@ -283,7 +293,8 @@ public final class SwiftCommandState {
283293
options: GlobalOptions,
284294
toolWorkspaceConfiguration: ToolWorkspaceConfiguration = .init(),
285295
workspaceDelegateProvider: @escaping WorkspaceDelegateProvider,
286-
workspaceLoaderProvider: @escaping WorkspaceLoaderProvider
296+
workspaceLoaderProvider: @escaping WorkspaceLoaderProvider,
297+
createPackagePath: Bool
287298
) throws {
288299
// output from background activities goes to stderr, this includes diagnostics and output from build operations,
289300
// package resolution that take place as part of another action
@@ -295,7 +306,8 @@ public final class SwiftCommandState {
295306
options: options,
296307
toolWorkspaceConfiguration: toolWorkspaceConfiguration,
297308
workspaceDelegateProvider: workspaceDelegateProvider,
298-
workspaceLoaderProvider: workspaceLoaderProvider
309+
workspaceLoaderProvider: workspaceLoaderProvider,
310+
createPackagePath: createPackagePath
299311
)
300312
}
301313

@@ -306,6 +318,7 @@ public final class SwiftCommandState {
306318
toolWorkspaceConfiguration: ToolWorkspaceConfiguration,
307319
workspaceDelegateProvider: @escaping WorkspaceDelegateProvider,
308320
workspaceLoaderProvider: @escaping WorkspaceLoaderProvider,
321+
createPackagePath: Bool,
309322
hostTriple: Basics.Triple? = nil,
310323
fileSystem: any FileSystem = localFileSystem,
311324
environment: Environment = .current
@@ -341,19 +354,20 @@ public final class SwiftCommandState {
341354
self.options = options
342355

343356
// Honor package-path option is provided.
344-
if let packagePath = options.locations.packageDirectory {
345-
try ProcessEnv.chdir(packagePath)
346-
}
347-
348-
if toolWorkspaceConfiguration.shouldInstallSignalHandlers {
349-
cancellator.installSignalHandlers()
350-
}
351-
self.cancellator = cancellator
357+
try Self.chdirIfNeeded(
358+
packageDirectory: self.options.locations.packageDirectory,
359+
createPackagePath: createPackagePath
360+
)
352361
} catch {
353362
self.observabilityScope.emit(error)
354363
throw ExitCode.failure
355364
}
356365

366+
if toolWorkspaceConfiguration.shouldInstallSignalHandlers {
367+
cancellator.installSignalHandlers()
368+
}
369+
self.cancellator = cancellator
370+
357371
// Create local variables to use while finding build path to avoid capture self before init error.
358372
let packageRoot = findPackageRoot(fileSystem: fileSystem)
359373

@@ -529,6 +543,23 @@ public final class SwiftCommandState {
529543
return (identities, targets)
530544
}
531545

546+
private static func chdirIfNeeded(packageDirectory: AbsolutePath?, createPackagePath: Bool) throws {
547+
if let packagePath = packageDirectory {
548+
do {
549+
try ProcessEnv.chdir(packagePath)
550+
} catch let SystemError.chdir(errorCode, path) {
551+
// If the command allows for the directory at the package path
552+
// to not be present then attempt to create it and chdir again.
553+
if createPackagePath {
554+
try makeDirectories(packagePath)
555+
try ProcessEnv.chdir(packagePath)
556+
} else {
557+
throw SystemError.chdir(errorCode, path)
558+
}
559+
}
560+
}
561+
}
562+
532563
private func getEditsDirectory() throws -> AbsolutePath {
533564
// TODO: replace multiroot-data-file with explicit overrides
534565
if let multiRootPackageDataFile = options.locations.multirootPackageDataFile {

Tests/BuildTests/PrepareForIndexTests.swift

+1
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ class PrepareForIndexTests: XCTestCase {
208208
observabilityScope: $1
209209
)
210210
},
211+
createPackagePath: false,
211212
hostTriple: .arm64Linux,
212213
fileSystem: localFileSystem,
213214
environment: .current

Tests/CommandsTests/SwiftCommandStateTests.swift

+25
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import func PackageGraph.loadModulesGraph
2222
import _InternalTestSupport
2323
import XCTest
2424

25+
import ArgumentParser
2526
import class TSCBasic.BufferedOutputByteStream
2627
import protocol TSCBasic.OutputByteStream
28+
import enum TSCBasic.SystemError
2729
import var TSCBasic.stderrStream
2830

2931
final class SwiftCommandStateTests: CommandsTestCase {
@@ -485,12 +487,34 @@ final class SwiftCommandStateTests: CommandsTestCase {
485487
XCTAssertEqual(try targetToolchain.getClangCompiler(), targetClangPath)
486488
XCTAssertEqual(targetToolchain.librarianPath, targetArPath)
487489
}
490+
491+
func testPackagePathWithMissingFolder() async throws {
492+
try fixture(name: "Miscellaneous/Simple") { fixturePath in
493+
let packagePath = fixturePath.appending(component: "Foo")
494+
let options = try GlobalOptions.parse(["--package-path", packagePath.pathString])
495+
496+
do {
497+
let outputStream = BufferedOutputByteStream()
498+
XCTAssertThrowsError(try SwiftCommandState.makeMockState(outputStream: outputStream, options: options), "error expected") { error in
499+
XCTAssertMatch(outputStream.bytes.validDescription, .contains("error: No such file or directory"))
500+
}
501+
}
502+
503+
do {
504+
let outputStream = BufferedOutputByteStream()
505+
let tool = try SwiftCommandState.makeMockState(outputStream: outputStream, options: options, createPackagePath: true)
506+
tool.waitForObservabilityEvents(timeout: .now() + .seconds(1))
507+
XCTAssertNoMatch(outputStream.bytes.validDescription, .contains("error:"))
508+
}
509+
}
510+
}
488511
}
489512

490513
extension SwiftCommandState {
491514
static func makeMockState(
492515
outputStream: OutputByteStream = stderrStream,
493516
options: GlobalOptions,
517+
createPackagePath: Bool = false,
494518
fileSystem: any FileSystem = localFileSystem,
495519
environment: Environment = .current
496520
) throws -> SwiftCommandState {
@@ -512,6 +536,7 @@ extension SwiftCommandState {
512536
observabilityScope: $1
513537
)
514538
},
539+
createPackagePath: createPackagePath,
515540
hostTriple: .arm64Linux,
516541
fileSystem: fileSystem,
517542
environment: environment

0 commit comments

Comments
 (0)