Skip to content

Allow creation of non-existent directory during swift package init #8401

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 3 commits into from
Mar 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Sources/Commands/PackageCommands/Init.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ extension SwiftPackageCommand {
@Option(name: .customLong("name"), help: "Provide custom package name")
var packageName: String?

// This command should support creating the supplied --package-path if it isn't created.
var createPackagePath = true

func run(_ swiftCommandState: SwiftCommandState) throws {
guard let cwd = swiftCommandState.fileSystem.currentWorkingDirectory else {
throw InternalError("Could not find the current working directory")
Expand Down
56 changes: 44 additions & 12 deletions Sources/CoreCommands/SwiftCommandState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import enum TSCBasic.ProcessLockError
import var TSCBasic.stderrStream
import class TSCBasic.TerminalController
import class TSCBasic.ThreadSafeOutputByteStream
import enum TSCBasic.SystemError

import var TSCUtility.verbosity

Expand Down Expand Up @@ -90,12 +91,20 @@ public protocol _SwiftCommand {
var workspaceDelegateProvider: WorkspaceDelegateProvider { get }
var workspaceLoaderProvider: WorkspaceLoaderProvider { get }
func buildSystemProvider(_ swiftCommandState: SwiftCommandState) throws -> BuildSystemProvider

// If a packagePath is specificed, this indicates that the command allows
// creating the directory if it doesn't exist.
var createPackagePath: Bool { get }
}

extension _SwiftCommand {
public var toolWorkspaceConfiguration: ToolWorkspaceConfiguration {
.init()
}

public var createPackagePath: Bool {
return false
}
}

public protocol SwiftCommand: ParsableCommand, _SwiftCommand {
Expand All @@ -110,7 +119,8 @@ extension SwiftCommand {
options: globalOptions,
toolWorkspaceConfiguration: self.toolWorkspaceConfiguration,
workspaceDelegateProvider: self.workspaceDelegateProvider,
workspaceLoaderProvider: self.workspaceLoaderProvider
workspaceLoaderProvider: self.workspaceLoaderProvider,
createPackagePath: self.createPackagePath
)

// We use this to attempt to catch misuse of the locking APIs since we only release the lock from here.
Expand Down Expand Up @@ -151,7 +161,8 @@ extension AsyncSwiftCommand {
options: globalOptions,
toolWorkspaceConfiguration: self.toolWorkspaceConfiguration,
workspaceDelegateProvider: self.workspaceDelegateProvider,
workspaceLoaderProvider: self.workspaceLoaderProvider
workspaceLoaderProvider: self.workspaceLoaderProvider,
createPackagePath: self.createPackagePath
)

// We use this to attempt to catch misuse of the locking APIs since we only release the lock from here.
Expand Down Expand Up @@ -283,7 +294,8 @@ public final class SwiftCommandState {
options: GlobalOptions,
toolWorkspaceConfiguration: ToolWorkspaceConfiguration = .init(),
workspaceDelegateProvider: @escaping WorkspaceDelegateProvider,
workspaceLoaderProvider: @escaping WorkspaceLoaderProvider
workspaceLoaderProvider: @escaping WorkspaceLoaderProvider,
createPackagePath: Bool
) throws {
// output from background activities goes to stderr, this includes diagnostics and output from build operations,
// package resolution that take place as part of another action
Expand All @@ -295,7 +307,8 @@ public final class SwiftCommandState {
options: options,
toolWorkspaceConfiguration: toolWorkspaceConfiguration,
workspaceDelegateProvider: workspaceDelegateProvider,
workspaceLoaderProvider: workspaceLoaderProvider
workspaceLoaderProvider: workspaceLoaderProvider,
createPackagePath: createPackagePath
)
}

Expand All @@ -306,6 +319,7 @@ public final class SwiftCommandState {
toolWorkspaceConfiguration: ToolWorkspaceConfiguration,
workspaceDelegateProvider: @escaping WorkspaceDelegateProvider,
workspaceLoaderProvider: @escaping WorkspaceLoaderProvider,
createPackagePath: Bool,
hostTriple: Basics.Triple? = nil,
fileSystem: any FileSystem = localFileSystem,
environment: Environment = .current
Expand Down Expand Up @@ -341,19 +355,20 @@ public final class SwiftCommandState {
self.options = options

// Honor package-path option is provided.
if let packagePath = options.locations.packageDirectory {
try ProcessEnv.chdir(packagePath)
}

if toolWorkspaceConfiguration.shouldInstallSignalHandlers {
cancellator.installSignalHandlers()
}
self.cancellator = cancellator
try Self.chdirIfNeeded(
packageDirectory: self.options.locations.packageDirectory,
createPackagePath: createPackagePath
)
} catch {
self.observabilityScope.emit(error)
throw ExitCode.failure
}

if toolWorkspaceConfiguration.shouldInstallSignalHandlers {
cancellator.installSignalHandlers()
}
self.cancellator = cancellator

// Create local variables to use while finding build path to avoid capture self before init error.
let packageRoot = findPackageRoot(fileSystem: fileSystem)

Expand Down Expand Up @@ -529,6 +544,23 @@ public final class SwiftCommandState {
return (identities, targets)
}

private static func chdirIfNeeded(packageDirectory: AbsolutePath?, createPackagePath: Bool) throws {
if let packagePath = packageDirectory {
do {
try ProcessEnv.chdir(packagePath)
} catch let SystemError.chdir(errorCode, path) {
// If the command allows for the directory at the package path
// to not be present then attempt to create it and chdir again.
if createPackagePath {
try makeDirectories(packagePath)
try ProcessEnv.chdir(packagePath)
} else {
throw SystemError.chdir(errorCode, path)
}
}
}
}

private func getEditsDirectory() throws -> AbsolutePath {
// TODO: replace multiroot-data-file with explicit overrides
if let multiRootPackageDataFile = options.locations.multirootPackageDataFile {
Expand Down
1 change: 1 addition & 0 deletions Tests/BuildTests/PrepareForIndexTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ class PrepareForIndexTests: XCTestCase {
observabilityScope: $1
)
},
createPackagePath: false,
hostTriple: .arm64Linux,
fileSystem: localFileSystem,
environment: .current
Expand Down
23 changes: 23 additions & 0 deletions Tests/CommandsTests/SwiftCommandStateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ import func PackageGraph.loadModulesGraph
import _InternalTestSupport
import XCTest

import ArgumentParser
import class TSCBasic.BufferedOutputByteStream
import protocol TSCBasic.OutputByteStream
import enum TSCBasic.SystemError
import var TSCBasic.stderrStream

final class SwiftCommandStateTests: CommandsTestCase {
Expand Down Expand Up @@ -485,12 +487,32 @@ final class SwiftCommandStateTests: CommandsTestCase {
XCTAssertEqual(try targetToolchain.getClangCompiler(), targetClangPath)
XCTAssertEqual(targetToolchain.librarianPath, targetArPath)
}

func testPackagePathWithMissingFolder() async throws {
try withTemporaryDirectory { fixturePath in
let packagePath = fixturePath.appending(component: "Foo")
let options = try GlobalOptions.parse(["--package-path", packagePath.pathString])

do {
let outputStream = BufferedOutputByteStream()
XCTAssertThrowsError(try SwiftCommandState.makeMockState(outputStream: outputStream, options: options), "error expected")
}

do {
let outputStream = BufferedOutputByteStream()
let tool = try SwiftCommandState.makeMockState(outputStream: outputStream, options: options, createPackagePath: true)
tool.waitForObservabilityEvents(timeout: .now() + .seconds(1))
XCTAssertNoMatch(outputStream.bytes.validDescription, .contains("error:"))
}
}
}
}

extension SwiftCommandState {
static func makeMockState(
outputStream: OutputByteStream = stderrStream,
options: GlobalOptions,
createPackagePath: Bool = false,
fileSystem: any FileSystem = localFileSystem,
environment: Environment = .current
) throws -> SwiftCommandState {
Expand All @@ -512,6 +534,7 @@ extension SwiftCommandState {
observabilityScope: $1
)
},
createPackagePath: createPackagePath,
hostTriple: .arm64Linux,
fileSystem: fileSystem,
environment: environment
Expand Down