Move carton command implementation into CartonDriver module (#459)
This commit is contained in:
parent
e030a494f9
commit
bb9a91ce0b
|
@ -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(
|
||||
|
|
|
@ -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!?")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue