Skip to content

Commit 6f40b31

Browse files
authored
Allow creation of non-existent directory during swift package init (#8401)
Running `swift package init --package-path <dir-does-not-exists>` results in an error when SwiftPM attempts to change directories in to the directory that doesn't exist. ### Motivation: It is rare that a user wants to do a `swift package init` in a folder that already has content. A typical pattern is to `mkdir mypackage && cd my package && swift package init`. SwiftPM already has a `--package-path` flag indicating the package folder that the command should operate on, but attempting to use this to create a folder that doesn't exist during `swift package init` results in an error when SwiftPM attempts to chdir to the --package-path that doesn't exist. ### Modifications: 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. ### Result: `swift package init --package-path ./mypackage` successfully initializes a package in `./mypackage`, even if `./mypackage` doesn't exist. Issue: #8393
1 parent 6c179de commit 6f40b31

File tree

4 files changed

+71
-12
lines changed

4 files changed

+71
-12
lines changed

Sources/Commands/PackageCommands/Init.swift

Lines changed: 3 additions & 0 deletions
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

Lines changed: 44 additions & 12 deletions
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,12 +91,20 @@ 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 {
@@ -110,7 +119,8 @@ extension SwiftCommand {
110119
options: globalOptions,
111120
toolWorkspaceConfiguration: self.toolWorkspaceConfiguration,
112121
workspaceDelegateProvider: self.workspaceDelegateProvider,
113-
workspaceLoaderProvider: self.workspaceLoaderProvider
122+
workspaceLoaderProvider: self.workspaceLoaderProvider,
123+
createPackagePath: self.createPackagePath
114124
)
115125

116126
// We use this to attempt to catch misuse of the locking APIs since we only release the lock from here.
@@ -151,7 +161,8 @@ extension AsyncSwiftCommand {
151161
options: globalOptions,
152162
toolWorkspaceConfiguration: self.toolWorkspaceConfiguration,
153163
workspaceDelegateProvider: self.workspaceDelegateProvider,
154-
workspaceLoaderProvider: self.workspaceLoaderProvider
164+
workspaceLoaderProvider: self.workspaceLoaderProvider,
165+
createPackagePath: self.createPackagePath
155166
)
156167

157168
// We use this to attempt to catch misuse of the locking APIs since we only release the lock from here.
@@ -283,7 +294,8 @@ public final class SwiftCommandState {
283294
options: GlobalOptions,
284295
toolWorkspaceConfiguration: ToolWorkspaceConfiguration = .init(),
285296
workspaceDelegateProvider: @escaping WorkspaceDelegateProvider,
286-
workspaceLoaderProvider: @escaping WorkspaceLoaderProvider
297+
workspaceLoaderProvider: @escaping WorkspaceLoaderProvider,
298+
createPackagePath: Bool
287299
) throws {
288300
// output from background activities goes to stderr, this includes diagnostics and output from build operations,
289301
// package resolution that take place as part of another action
@@ -295,7 +307,8 @@ public final class SwiftCommandState {
295307
options: options,
296308
toolWorkspaceConfiguration: toolWorkspaceConfiguration,
297309
workspaceDelegateProvider: workspaceDelegateProvider,
298-
workspaceLoaderProvider: workspaceLoaderProvider
310+
workspaceLoaderProvider: workspaceLoaderProvider,
311+
createPackagePath: createPackagePath
299312
)
300313
}
301314

@@ -306,6 +319,7 @@ public final class SwiftCommandState {
306319
toolWorkspaceConfiguration: ToolWorkspaceConfiguration,
307320
workspaceDelegateProvider: @escaping WorkspaceDelegateProvider,
308321
workspaceLoaderProvider: @escaping WorkspaceLoaderProvider,
322+
createPackagePath: Bool,
309323
hostTriple: Basics.Triple? = nil,
310324
fileSystem: any FileSystem = localFileSystem,
311325
environment: Environment = .current
@@ -342,19 +356,20 @@ public final class SwiftCommandState {
342356
self.options = options
343357

344358
// Honor package-path option is provided.
345-
if let packagePath = options.locations.packageDirectory {
346-
try ProcessEnv.chdir(packagePath)
347-
}
348-
349-
if toolWorkspaceConfiguration.shouldInstallSignalHandlers {
350-
cancellator.installSignalHandlers()
351-
}
352-
self.cancellator = cancellator
359+
try Self.chdirIfNeeded(
360+
packageDirectory: self.options.locations.packageDirectory,
361+
createPackagePath: createPackagePath
362+
)
353363
} catch {
354364
self.observabilityScope.emit(error)
355365
throw ExitCode.failure
356366
}
357367

368+
if toolWorkspaceConfiguration.shouldInstallSignalHandlers {
369+
cancellator.installSignalHandlers()
370+
}
371+
self.cancellator = cancellator
372+
358373
// Create local variables to use while finding build path to avoid capture self before init error.
359374
let packageRoot = findPackageRoot(fileSystem: fileSystem)
360375

@@ -530,6 +545,23 @@ public final class SwiftCommandState {
530545
return (identities, targets)
531546
}
532547

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

Tests/BuildTests/PrepareForIndexTests.swift

Lines changed: 1 addition & 0 deletions
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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import _InternalTestSupport
2222
@testable import PackageModel
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 {
@@ -512,12 +514,32 @@ final class SwiftCommandStateTests: CommandsTestCase {
512514
XCTAssertEqual(try targetToolchain.getClangCompiler(), targetClangPath)
513515
XCTAssertEqual(targetToolchain.librarianPath, targetArPath)
514516
}
517+
518+
func testPackagePathWithMissingFolder() async throws {
519+
try withTemporaryDirectory { fixturePath in
520+
let packagePath = fixturePath.appending(component: "Foo")
521+
let options = try GlobalOptions.parse(["--package-path", packagePath.pathString])
522+
523+
do {
524+
let outputStream = BufferedOutputByteStream()
525+
XCTAssertThrowsError(try SwiftCommandState.makeMockState(outputStream: outputStream, options: options), "error expected")
526+
}
527+
528+
do {
529+
let outputStream = BufferedOutputByteStream()
530+
let tool = try SwiftCommandState.makeMockState(outputStream: outputStream, options: options, createPackagePath: true)
531+
tool.waitForObservabilityEvents(timeout: .now() + .seconds(1))
532+
XCTAssertNoMatch(outputStream.bytes.validDescription, .contains("error:"))
533+
}
534+
}
535+
}
515536
}
516537

517538
extension SwiftCommandState {
518539
static func makeMockState(
519540
outputStream: OutputByteStream = stderrStream,
520541
options: GlobalOptions,
542+
createPackagePath: Bool = false,
521543
fileSystem: any FileSystem = localFileSystem,
522544
environment: Environment = .current
523545
) throws -> SwiftCommandState {
@@ -539,6 +561,7 @@ extension SwiftCommandState {
539561
observabilityScope: $1
540562
)
541563
},
564+
createPackagePath: createPackagePath,
542565
hostTriple: .arm64Linux,
543566
fileSystem: fileSystem,
544567
environment: environment

0 commit comments

Comments
 (0)