carton/Sources/CartonDriver/CartonDriverCommand.swift

201 lines
7.7 KiB
Swift

// 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 CartonCore
import CartonHelpers
import Foundation
import SwiftToolchain
struct CartonDriverError: Error & CustomStringConvertible {
init(_ description: String) {
self.description = description
}
var description: String
}
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
let pid = ProcessInfo.processInfo.processIdentifier
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"]
cartonPluginArguments += ["--pid", pid.description]
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"]
cartonPluginArguments += ["--pid", pid.description]
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 CartonDriverError("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.swift.pathString)
let pluginArguments = try derivePackageCommandArguments(
swiftExec: swiftExec,
subcommand: subcommand,
scratchPath: scratchPath.path,
extraArguments: extraArguments
)
var env: [String: String] = ProcessInfo.processInfo.environment
if ToolchainSystem.isSnapshotVersion(swiftPath.version),
swiftPath.toolchain.extension == "xctoolchain"
{
env["DYLD_LIBRARY_PATH"] = swiftPath.toolchain.appending(
components: ["usr", "lib", "swift", "macosx"]
).pathString
}
try Foundation.Process.checkRun(
swiftExec,
arguments: pluginArguments,
environment: env,
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.swift.pathString),
arguments: ["package"] + arguments.dropFirst(),
forwardExit: true
)
case "--version":
print(cartonVersion)
default: fatalError("Unimplemented subcommand!?")
}
}