diff --git a/Sources/Commands/PackageCommands/Init.swift b/Sources/Commands/PackageCommands/Init.swift index 6dbb7a6e9f3..3cc7f87fa24 100644 --- a/Sources/Commands/PackageCommands/Init.swift +++ b/Sources/Commands/PackageCommands/Init.swift @@ -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") diff --git a/Sources/CoreCommands/SwiftCommandState.swift b/Sources/CoreCommands/SwiftCommandState.swift index 141ffeee9da..f75b68e24e6 100644 --- a/Sources/CoreCommands/SwiftCommandState.swift +++ b/Sources/CoreCommands/SwiftCommandState.swift @@ -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 @@ -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 { @@ -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. @@ -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. @@ -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 @@ -295,7 +307,8 @@ public final class SwiftCommandState { options: options, toolWorkspaceConfiguration: toolWorkspaceConfiguration, workspaceDelegateProvider: workspaceDelegateProvider, - workspaceLoaderProvider: workspaceLoaderProvider + workspaceLoaderProvider: workspaceLoaderProvider, + createPackagePath: createPackagePath ) } @@ -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 @@ -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) @@ -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 { diff --git a/Tests/BuildTests/PrepareForIndexTests.swift b/Tests/BuildTests/PrepareForIndexTests.swift index a8701d52dce..16e8cf10a7e 100644 --- a/Tests/BuildTests/PrepareForIndexTests.swift +++ b/Tests/BuildTests/PrepareForIndexTests.swift @@ -208,6 +208,7 @@ class PrepareForIndexTests: XCTestCase { observabilityScope: $1 ) }, + createPackagePath: false, hostTriple: .arm64Linux, fileSystem: localFileSystem, environment: .current diff --git a/Tests/CommandsTests/SwiftCommandStateTests.swift b/Tests/CommandsTests/SwiftCommandStateTests.swift index 77e6b80d6ca..3edd6113a26 100644 --- a/Tests/CommandsTests/SwiftCommandStateTests.swift +++ b/Tests/CommandsTests/SwiftCommandStateTests.swift @@ -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 { @@ -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 { @@ -512,6 +534,7 @@ extension SwiftCommandState { observabilityScope: $1 ) }, + createPackagePath: createPackagePath, hostTriple: .arm64Linux, fileSystem: fileSystem, environment: environment