Move carton command implementation into CartonDriver module (#459)

This commit is contained in:
omochimetaru 2024-05-20 14:31:00 +09:00 committed by GitHub
parent e030a494f9
commit bb9a91ce0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 230 additions and 190 deletions

View File

@ -13,6 +13,7 @@ let package = Package(
products: [
.library(name: "SwiftToolchain", targets: ["SwiftToolchain"]),
.library(name: "CartonHelpers", targets: ["CartonHelpers"]),
.library(name: "CartonDriver", targets: ["CartonDriver"]),
.library(name: "CartonKit", targets: ["CartonKit"]),
.library(name: "CartonFrontend", targets: ["CartonFrontend"]),
.executable(name: "carton", targets: ["carton"]),
@ -35,11 +36,17 @@ let package = Package(
),
],
targets: [
.target(
name: "CartonDriver",
dependencies: [
"SwiftToolchain",
"CartonHelpers"
]
),
.executableTarget(
name: "carton",
dependencies: [
"SwiftToolchain",
"CartonHelpers",
"CartonDriver"
]
),
.executableTarget(

View File

@ -0,0 +1,220 @@
// Copyright 2024 Carton contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This executable is a thin wrapper around the Swift Package Manager and the Carton's SwiftPM Plugins.
// The responsibilities of this executable are:
// - to install appropriate SwiftWasm toolchain if it's not installed and to use it for the later invocations
// * This step will be eventually removed once SwiftPM provides a good way to manage Swift SDKs declaratively
// and Xcode toolchain provides WebAssembly target. (OSS toolchain already provides it)
// - to grant the SwiftPM Plugin process appropriate permissions to write to the file system
// * "dev" and "test" subcommands require listening TCP sockets but SwiftPM doesn't provide a way to
// express this requirement in the package manifest
// * "bundle" subcommand requires writing to the file system to "./Bundle" directory. This is to keep
// soft compatibility with the default behavior of the previous version of Carton
// - to give the SwiftPM build system the target triple by default
// * SwiftPM doesn't provide a way to control the target triple from plugin process
// - to pre-build "{package-name}PackageTests" product before running plugin process
// * SwiftPM doesn't support building only "all tests" product from plugin process, so we have to
// build it before running the CartonTest plugin process
//
// This executable should be eventually removed once SwiftPM provides a way to express those requirements.
import CartonHelpers
import Foundation
import SwiftToolchain
struct CartonCommandError: Error & CustomStringConvertible {
init(_ description: String) {
self.description = description
}
var description: String
}
extension Foundation.Process {
internal static func checkRun(
_ executableURL: URL, arguments: [String], forwardExit: Bool = false
) throws {
fputs(
"Running \(([executableURL.path] + arguments).map { "\"\($0)\"" }.joined(separator: " "))\n",
stderr)
fflush(stderr)
let process = Foundation.Process()
process.executableURL = executableURL
process.arguments = arguments
// Monitor termination/interrruption signals to forward them to child process
func setSignalForwarding(_ signalNo: Int32) {
signal(signalNo, SIG_IGN)
let signalSource = DispatchSource.makeSignalSource(signal: signalNo)
signalSource.setEventHandler {
signalSource.cancel()
process.interrupt()
}
signalSource.resume()
}
setSignalForwarding(SIGINT)
setSignalForwarding(SIGTERM)
if forwardExit {
process.terminationHandler = {
// Exit plugin process itself when child process exited
exit($0.terminationStatus)
}
}
try process.run()
process.waitUntilExit()
if process.terminationStatus != 0 {
exit(process.terminationStatus)
}
}
}
func derivePackageCommandArguments(
swiftExec: URL,
subcommand: String,
scratchPath: String,
extraArguments: [String]
) throws -> [String] {
var packageArguments: [String] = [
"package", "--triple", "wasm32-unknown-wasi", "--scratch-path", scratchPath,
]
let pluginArguments: [String] = ["plugin"]
var cartonPluginArguments: [String] = extraArguments
switch subcommand {
case "bundle":
packageArguments += ["--disable-sandbox"]
// TODO: Uncomment this line once we stop creating .carton directory in the home directory
// pluginArguments += ["--allow-writing-to-package-directory"]
// Place before user-given extra arguments to allow overriding default options
cartonPluginArguments = ["--output", "Bundle"] + cartonPluginArguments
case "dev":
packageArguments += ["--disable-sandbox"]
case "test":
// 1. Ask the plugin process to generate the build command based on the given options
let commandFile = try makeTemporaryFile(prefix: "test-build")
try Foundation.Process.checkRun(
swiftExec,
arguments: packageArguments + pluginArguments + [
"carton-test",
"internal-get-build-command",
] + cartonPluginArguments + ["--output", commandFile.path]
)
// 2. Build the test product
let buildArguments = try String(contentsOf: commandFile).split(separator: "\n")
if !buildArguments.isEmpty {
let buildCommand = buildArguments.map(String.init) + [
// NOTE: "swift-build" uses llbuild manifest cache by default even though
// target triple changed.
"--disable-build-manifest-caching",
"--triple", "wasm32-unknown-wasi", "--scratch-path", scratchPath,
]
try Foundation.Process.checkRun(
swiftExec,
arguments: buildCommand
)
}
// "--environment browser" launches a http server
packageArguments += ["--disable-sandbox"]
default: break
}
return packageArguments + pluginArguments + ["carton-\(subcommand)"] + cartonPluginArguments
}
var errnoString: String {
String(cString: strerror(errno))
}
var temporaryDirectory: URL {
URL(fileURLWithPath: NSTemporaryDirectory())
}
func makeTemporaryFile(prefix: String, in directory: URL? = nil) throws -> URL {
let directory = directory ?? temporaryDirectory
var template = directory.appendingPathComponent("\(prefix)XXXXXX").path
let result = try template.withUTF8 { template in
let copy = UnsafeMutableBufferPointer<CChar>.allocate(capacity: template.count + 1)
defer { copy.deallocate() }
template.copyBytes(to: copy)
copy[template.count] = 0
guard mkstemp(copy.baseAddress!) != -1 else {
let error = errnoString
throw CartonCommandError("Failed to make a temporary file at \(template): \(error)")
}
return String(cString: copy.baseAddress!)
}
return URL(fileURLWithPath: result)
}
func pluginSubcommand(subcommand: String, argv0: String, arguments: [String]) async throws {
let scratchPath = URL(fileURLWithPath: ".build/carton")
if FileManager.default.fileExists(atPath: scratchPath.path) {
try FileManager.default.createDirectory(at: scratchPath, withIntermediateDirectories: true)
}
let terminal = InteractiveWriter.stdout
let toolchainSystem = try ToolchainSystem(fileSystem: localFileSystem)
let (swiftPath, _) = try await toolchainSystem.inferSwiftPath(terminal)
let extraArguments = arguments
let swiftExec = URL(fileURLWithPath: swiftPath.pathString)
let pluginArguments = try derivePackageCommandArguments(
swiftExec: swiftExec,
subcommand: subcommand,
scratchPath: scratchPath.path,
extraArguments: extraArguments
)
try Foundation.Process.checkRun(swiftExec, arguments: pluginArguments, forwardExit: true)
}
public func main(arguments: [String]) async throws {
let argv0 = arguments[0]
let arguments = arguments.dropFirst()
let pluginSubcommands = ["bundle", "dev", "test"]
let subcommands = pluginSubcommands + ["package", "--version"]
guard let subcommand = arguments.first, subcommands.contains(subcommand) else {
if arguments.first == "init" {
print(
"Warning: 'init' subcommand has been removed, use 'swift package init' and add 'carton' as a dependency in Package.swift instead."
)
}
print("Usage: swift run carton <subcommand> [options]")
print("Available subcommands: \(subcommands.joined(separator: ", "))")
exit(1)
}
switch subcommand {
case _ where pluginSubcommands.contains(subcommand):
try await pluginSubcommand(
subcommand: subcommand, argv0: argv0, arguments: Array(arguments.dropFirst()))
case "package":
let terminal = InteractiveWriter.stdout
let toolchainSystem = try ToolchainSystem(fileSystem: localFileSystem)
let (swiftPath, _) = try await toolchainSystem.inferSwiftPath(terminal)
try Foundation.Process.checkRun(
URL(fileURLWithPath: swiftPath.pathString),
arguments: ["package"] + arguments.dropFirst(), forwardExit: true
)
case "--version":
print(cartonVersion)
default: fatalError("Unimplemented subcommand!?")
}
}

View File

@ -30,193 +30,6 @@
//
// This executable should be eventually removed once SwiftPM provides a way to express those requirements.
import CartonHelpers
import Foundation
import SwiftToolchain
struct CartonCommandError: Error & CustomStringConvertible {
init(_ description: String) {
self.description = description
}
var description: String
}
extension Foundation.Process {
internal static func checkRun(
_ executableURL: URL, arguments: [String], forwardExit: Bool = false
) throws {
fputs(
"Running \(([executableURL.path] + arguments).map { "\"\($0)\"" }.joined(separator: " "))\n",
stderr)
fflush(stderr)
let process = Foundation.Process()
process.executableURL = executableURL
process.arguments = arguments
// Monitor termination/interrruption signals to forward them to child process
func setSignalForwarding(_ signalNo: Int32) {
signal(signalNo, SIG_IGN)
let signalSource = DispatchSource.makeSignalSource(signal: signalNo)
signalSource.setEventHandler {
signalSource.cancel()
process.interrupt()
}
signalSource.resume()
}
setSignalForwarding(SIGINT)
setSignalForwarding(SIGTERM)
if forwardExit {
process.terminationHandler = {
// Exit plugin process itself when child process exited
exit($0.terminationStatus)
}
}
try process.run()
process.waitUntilExit()
if process.terminationStatus != 0 {
exit(process.terminationStatus)
}
}
}
func derivePackageCommandArguments(
swiftExec: URL,
subcommand: String,
scratchPath: String,
extraArguments: [String]
) throws -> [String] {
var packageArguments: [String] = [
"package", "--triple", "wasm32-unknown-wasi", "--scratch-path", scratchPath,
]
let pluginArguments: [String] = ["plugin"]
var cartonPluginArguments: [String] = extraArguments
switch subcommand {
case "bundle":
packageArguments += ["--disable-sandbox"]
// TODO: Uncomment this line once we stop creating .carton directory in the home directory
// pluginArguments += ["--allow-writing-to-package-directory"]
// Place before user-given extra arguments to allow overriding default options
cartonPluginArguments = ["--output", "Bundle"] + cartonPluginArguments
case "dev":
packageArguments += ["--disable-sandbox"]
case "test":
// 1. Ask the plugin process to generate the build command based on the given options
let commandFile = try makeTemporaryFile(prefix: "test-build")
try Foundation.Process.checkRun(
swiftExec,
arguments: packageArguments + pluginArguments + [
"carton-test",
"internal-get-build-command",
] + cartonPluginArguments + ["--output", commandFile.path]
)
// 2. Build the test product
let buildArguments = try String(contentsOf: commandFile).split(separator: "\n")
if !buildArguments.isEmpty {
let buildCommand = buildArguments.map(String.init) + [
// NOTE: "swift-build" uses llbuild manifest cache by default even though
// target triple changed.
"--disable-build-manifest-caching",
"--triple", "wasm32-unknown-wasi", "--scratch-path", scratchPath,
]
try Foundation.Process.checkRun(
swiftExec,
arguments: buildCommand
)
}
// "--environment browser" launches a http server
packageArguments += ["--disable-sandbox"]
default: break
}
return packageArguments + pluginArguments + ["carton-\(subcommand)"] + cartonPluginArguments
}
var errnoString: String {
String(cString: strerror(errno))
}
var temporaryDirectory: URL {
URL(fileURLWithPath: NSTemporaryDirectory())
}
func makeTemporaryFile(prefix: String, in directory: URL? = nil) throws -> URL {
let directory = directory ?? temporaryDirectory
var template = directory.appendingPathComponent("\(prefix)XXXXXX").path
let result = try template.withUTF8 { template in
let copy = UnsafeMutableBufferPointer<CChar>.allocate(capacity: template.count + 1)
defer { copy.deallocate() }
template.copyBytes(to: copy)
copy[template.count] = 0
guard mkstemp(copy.baseAddress!) != -1 else {
let error = errnoString
throw CartonCommandError("Failed to make a temporary file at \(template): \(error)")
}
return String(cString: copy.baseAddress!)
}
return URL(fileURLWithPath: result)
}
func pluginSubcommand(subcommand: String, argv0: String, arguments: [String]) async throws {
let scratchPath = URL(fileURLWithPath: ".build/carton")
if FileManager.default.fileExists(atPath: scratchPath.path) {
try FileManager.default.createDirectory(at: scratchPath, withIntermediateDirectories: true)
}
let terminal = InteractiveWriter.stdout
let toolchainSystem = try ToolchainSystem(fileSystem: localFileSystem)
let (swiftPath, _) = try await toolchainSystem.inferSwiftPath(terminal)
let extraArguments = arguments
let swiftExec = URL(fileURLWithPath: swiftPath.pathString)
let pluginArguments = try derivePackageCommandArguments(
swiftExec: swiftExec,
subcommand: subcommand,
scratchPath: scratchPath.path,
extraArguments: extraArguments
)
try Foundation.Process.checkRun(swiftExec, arguments: pluginArguments, forwardExit: true)
}
func main(arguments: [String]) async throws {
let argv0 = arguments[0]
let arguments = arguments.dropFirst()
let pluginSubcommands = ["bundle", "dev", "test"]
let subcommands = pluginSubcommands + ["package", "--version"]
guard let subcommand = arguments.first, subcommands.contains(subcommand) else {
if arguments.first == "init" {
print(
"Warning: 'init' subcommand has been removed, use 'swift package init' and add 'carton' as a dependency in Package.swift instead."
)
}
print("Usage: swift run carton <subcommand> [options]")
print("Available subcommands: \(subcommands.joined(separator: ", "))")
exit(1)
}
switch subcommand {
case _ where pluginSubcommands.contains(subcommand):
try await pluginSubcommand(
subcommand: subcommand, argv0: argv0, arguments: Array(arguments.dropFirst()))
case "package":
let terminal = InteractiveWriter.stdout
let toolchainSystem = try ToolchainSystem(fileSystem: localFileSystem)
let (swiftPath, _) = try await toolchainSystem.inferSwiftPath(terminal)
try Foundation.Process.checkRun(
URL(fileURLWithPath: swiftPath.pathString),
arguments: ["package"] + arguments.dropFirst(), forwardExit: true
)
case "--version":
print(cartonVersion)
default: fatalError("Unimplemented subcommand!?")
}
}
import CartonDriver
try await main(arguments: CommandLine.arguments)