Use `async`/`await` and actors instead of Combine (#283)
This makes our codebase smaller by ~80 lines and arguably more readable. Also removes OpenCombine dependency. * Use `async`/`await` and actors instead of Combine * Remove OpenCombine dependency * Fix progress animation not updated * Stop building with Swift 5.4, clean up terminal output * Update requirements in `README.md` * Add `description` to `InvalidResponseCode` * Fix Linux build error * Rename `main.swift` to `Main.swift` * Work around IR/TDB warnings * Pass IR/TDB arguments correctly to `swift build` * Support `async` process runner in `carton-release` * Use Xcode 13.2.1 on macOS * Pass TDB/IR flags to `swift test` as well * Make `Install` command async * Add doc comments, handle subsequent rebuilds
This commit is contained in:
parent
6c6e78eb0d
commit
74a49c1aa8
|
@ -11,12 +11,9 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-11
|
||||
swift_version: 5.4
|
||||
xcode: /Applications/Xcode_12.5.1.app/Contents/Developer
|
||||
- os: macos-11
|
||||
swift_version: 5.5
|
||||
xcode: /Applications/Xcode_13.1.app/Contents/Developer
|
||||
xcode: /Applications/Xcode_13.2.1.app/Contents/Developer
|
||||
- os: ubuntu-18.04
|
||||
swift_version: 5.5
|
||||
- os: ubuntu-20.04
|
||||
|
@ -45,7 +42,9 @@ jobs:
|
|||
curl https://get.wasmer.io -sSfL | sh
|
||||
|
||||
- name: Build the project
|
||||
run: swift build
|
||||
run: |
|
||||
swift -v
|
||||
swift build -Xswiftc -Xfrontend -Xswiftc -validate-tbd-against-ir=none
|
||||
|
||||
- name: Build and install JavaScript and sanitizer resources
|
||||
run: |
|
||||
|
@ -61,7 +60,7 @@ jobs:
|
|||
if [ -e /home/runner/.wasmer/wasmer.sh ]; then
|
||||
source /home/runner/.wasmer/wasmer.sh
|
||||
fi
|
||||
swift test
|
||||
swift test -Xswiftc -Xfrontend -Xswiftc -validate-tbd-against-ir=none
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
|
|
@ -37,15 +37,6 @@
|
|||
"version": "4.5.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "OpenCombine",
|
||||
"repositoryURL": "https://github.com/OpenCombine/OpenCombine.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "28993ae57de5a4ea7e164787636cafad442d568c",
|
||||
"version": "0.12.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "routing-kit",
|
||||
"repositoryURL": "https://github.com/vapor/routing-kit.git",
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
// swift-tools-version:5.4
|
||||
// swift-tools-version:5.5
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let openCombineProduct = Target.Dependency.product(
|
||||
name: "OpenCombine",
|
||||
package: "OpenCombine",
|
||||
condition: .when(platforms: [.linux])
|
||||
)
|
||||
|
||||
let package = Package(
|
||||
name: "carton",
|
||||
platforms: [.macOS(.v10_15)],
|
||||
|
@ -39,7 +33,6 @@ let package = Package(
|
|||
url: "https://github.com/apple/swift-tools-support-core.git",
|
||||
.branch("release/5.5")
|
||||
),
|
||||
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.12.0"),
|
||||
.package(url: "https://github.com/vapor/vapor.git", from: "4.53.0"),
|
||||
.package(url: "https://github.com/apple/swift-crypto.git", from: "1.1.0"),
|
||||
.package(url: "https://github.com/JohnSundell/Splash.git", from: "0.16.0"),
|
||||
|
@ -65,12 +58,10 @@ let package = Package(
|
|||
.target(
|
||||
name: "CartonKit",
|
||||
dependencies: [
|
||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||
.product(name: "AsyncHTTPClient", package: "async-http-client"),
|
||||
.product(name: "Crypto", package: "swift-crypto"),
|
||||
.product(name: "Vapor", package: "vapor"),
|
||||
"CartonHelpers",
|
||||
openCombineProduct,
|
||||
"SwiftToolchain",
|
||||
]
|
||||
),
|
||||
|
@ -81,7 +72,6 @@ let package = Package(
|
|||
.product(name: "NIOFoundationCompat", package: "swift-nio"),
|
||||
.product(name: "SwiftPMDataModel-auto", package: "SwiftPM"),
|
||||
"CartonHelpers",
|
||||
openCombineProduct,
|
||||
"WasmTransformer",
|
||||
]
|
||||
),
|
||||
|
@ -89,7 +79,7 @@ let package = Package(
|
|||
name: "CartonHelpers",
|
||||
dependencies: [
|
||||
.product(name: "AsyncHTTPClient", package: "async-http-client"),
|
||||
openCombineProduct,
|
||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||
"Splash",
|
||||
"WasmTransformer",
|
||||
]
|
||||
|
|
|
@ -24,8 +24,8 @@ development workflow such as toolchain and SDK installations.
|
|||
|
||||
### Requirements
|
||||
|
||||
- macOS 11 and Xcode 12.5.1 or later. macOS 10.15 may work, but is untested.
|
||||
- [Swift 5.4 or later](https://swift.org/download/) and Ubuntu 18.04 or 20.04 for Linux users.
|
||||
- macOS 11 and Xcode 13.2.1 or later. macOS 10.15 may work, but is untested.
|
||||
- [Swift 5.5 or later](https://swift.org/download/) and Ubuntu 18.04 or 20.04 for Linux users.
|
||||
|
||||
### Installation
|
||||
|
||||
|
@ -112,9 +112,8 @@ is that your `Package.swift` contains at least a single executable product, whic
|
|||
for WebAssembly and served when you start `carton dev` in the directory where `Package.swift` is located.
|
||||
|
||||
`carton` is built with [Vapor](https://vapor.codes/), [SwiftNIO](https://github.com/apple/swift-nio),
|
||||
[swift-tools-support-core](https://github.com/apple/swift-tools-support-core), and
|
||||
[OpenCombine](https://github.com/OpenCombine/OpenCombine), and supports both macOS and Linux. (Many
|
||||
thanks to everyone supporting and maintaining those projects!)
|
||||
[swift-tools-support-core](https://github.com/apple/swift-tools-support-core), and supports both
|
||||
macOS and Linux. (Many thanks to everyone supporting and maintaining those projects!)
|
||||
|
||||
### Running `carton dev` with the `release` configuration
|
||||
|
||||
|
|
|
@ -13,5 +13,9 @@
|
|||
// limitations under the License.
|
||||
|
||||
import CartonCLI
|
||||
import CartonHelpers
|
||||
|
||||
Carton.main()
|
||||
@main
|
||||
struct Main: AsyncMain {
|
||||
typealias Command = Carton
|
||||
}
|
|
@ -26,7 +26,7 @@ private let dependency = Entrypoint(
|
|||
sha256: bundleEntrypointSHA256
|
||||
)
|
||||
|
||||
struct Bundle: ParsableCommand {
|
||||
struct Bundle: AsyncParsableCommand {
|
||||
@Option(help: "Specify name of an executable product to produce the bundle for.")
|
||||
var product: String?
|
||||
|
||||
|
@ -50,15 +50,15 @@ struct Bundle: ParsableCommand {
|
|||
)
|
||||
}
|
||||
|
||||
func run() throws {
|
||||
func run() async throws {
|
||||
let terminal = InteractiveWriter.stdout
|
||||
|
||||
try dependency.check(on: localFileSystem, terminal)
|
||||
|
||||
let toolchain = try Toolchain(localFileSystem, terminal)
|
||||
let toolchain = try await Toolchain(localFileSystem, terminal)
|
||||
|
||||
let flavor = buildFlavor()
|
||||
let build = try toolchain.buildCurrentProject(
|
||||
let build = try await toolchain.buildCurrentProject(
|
||||
product: product,
|
||||
flavor: flavor
|
||||
)
|
||||
|
@ -80,10 +80,10 @@ struct Bundle: ParsableCommand {
|
|||
try localFileSystem.removeFileTree(bundleDirectory)
|
||||
try localFileSystem.createDirectory(bundleDirectory)
|
||||
let optimizedPath = AbsolutePath(bundleDirectory, "main.wasm")
|
||||
try ProcessRunner(
|
||||
try await Process.run(
|
||||
["wasm-opt", "-Os", build.mainWasmPath.pathString, "-o", optimizedPath.pathString],
|
||||
terminal
|
||||
).waitUntilFinished()
|
||||
)
|
||||
try terminal.logLookup(
|
||||
"After stripping debug info the main binary size is ",
|
||||
localFileSystem.humanReadableFileSize(optimizedPath),
|
||||
|
|
|
@ -14,17 +14,12 @@
|
|||
|
||||
import ArgumentParser
|
||||
import CartonHelpers
|
||||
import Foundation
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
import CartonKit
|
||||
import Foundation
|
||||
import SwiftToolchain
|
||||
import TSCBasic
|
||||
|
||||
struct Dev: ParsableCommand {
|
||||
struct Dev: AsyncParsableCommand {
|
||||
static let entrypoint = Entrypoint(fileName: "dev.js", sha256: devEntrypointSHA256)
|
||||
|
||||
@Option(help: "Specify name of an executable product in development.")
|
||||
|
@ -71,12 +66,12 @@ struct Dev: ParsableCommand {
|
|||
)
|
||||
}
|
||||
|
||||
func run() throws {
|
||||
func run() async throws {
|
||||
let terminal = InteractiveWriter.stdout
|
||||
|
||||
try Self.entrypoint.check(on: localFileSystem, terminal)
|
||||
|
||||
let toolchain = try Toolchain(localFileSystem, terminal)
|
||||
let toolchain = try await Toolchain(localFileSystem, terminal)
|
||||
|
||||
if !verbose {
|
||||
terminal.clearWindow()
|
||||
|
@ -97,7 +92,7 @@ struct Dev: ParsableCommand {
|
|||
}
|
||||
|
||||
let flavor = buildFlavor()
|
||||
let build = try toolchain.buildCurrentProject(
|
||||
let build = try await toolchain.buildCurrentProject(
|
||||
product: product,
|
||||
flavor: flavor
|
||||
)
|
||||
|
@ -113,7 +108,7 @@ struct Dev: ParsableCommand {
|
|||
|
||||
let sources = try paths.flatMap { try localFileSystem.traverseRecursively($0) }
|
||||
|
||||
try Server(
|
||||
try await Server(
|
||||
.init(
|
||||
builder: Builder(
|
||||
arguments: build.arguments,
|
||||
|
@ -125,16 +120,16 @@ struct Dev: ParsableCommand {
|
|||
),
|
||||
mainWasmPath: build.mainWasmPath,
|
||||
verbose: verbose,
|
||||
skipAutoOpen: skipAutoOpen,
|
||||
shouldSkipAutoOpen: skipAutoOpen,
|
||||
port: port,
|
||||
host: host,
|
||||
customIndexContent: HTML.readCustomIndexPage(at: customIndexPage, on: localFileSystem),
|
||||
// swiftlint:disable:next force_try
|
||||
manifest: try! toolchain.manifest.get(),
|
||||
product: build.product,
|
||||
entrypoint: Self.entrypoint
|
||||
),
|
||||
terminal
|
||||
entrypoint: Self.entrypoint,
|
||||
terminal: terminal
|
||||
)
|
||||
).run()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import Foundation
|
|||
import SwiftToolchain
|
||||
import TSCBasic
|
||||
|
||||
struct Init: ParsableCommand {
|
||||
struct Init: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Create a Swift package for a new SwiftWasm project.",
|
||||
subcommands: [ListTemplates.self]
|
||||
|
@ -33,7 +33,7 @@ struct Init: ParsableCommand {
|
|||
@Option(name: .long,
|
||||
help: "The name of the project") var name: String?
|
||||
|
||||
func run() throws {
|
||||
func run() async throws {
|
||||
let terminal = InteractiveWriter.stdout
|
||||
|
||||
guard let name = name ?? localFileSystem.currentWorkingDirectory?.basename else {
|
||||
|
@ -58,7 +58,7 @@ struct Init: ParsableCommand {
|
|||
return
|
||||
}
|
||||
try localFileSystem.createDirectory(packagePath)
|
||||
try template.template.create(
|
||||
try await template.template.create(
|
||||
on: localFileSystem,
|
||||
project: .init(name: name, path: packagePath, inPlace: self.name == nil),
|
||||
terminal
|
||||
|
|
|
@ -19,7 +19,7 @@ import SwiftToolchain
|
|||
import TSCBasic
|
||||
|
||||
/// Proxy swift-package command to locally pinned toolchain version.
|
||||
struct Package: ParsableCommand {
|
||||
struct Package: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(abstract: """
|
||||
Perform operations on Swift packages.
|
||||
""")
|
||||
|
@ -27,10 +27,10 @@ struct Package: ParsableCommand {
|
|||
@Argument(wrappedValue: [], parsing: .remaining)
|
||||
var arguments: [String]
|
||||
|
||||
func run() throws {
|
||||
func run() async throws {
|
||||
let terminal = InteractiveWriter.stdout
|
||||
|
||||
let toolchain = try Toolchain(localFileSystem, terminal)
|
||||
try toolchain.runPackage(arguments)
|
||||
let toolchain = try await Toolchain(localFileSystem, terminal)
|
||||
try await toolchain.runPackage(arguments)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,17 +17,17 @@ import CartonHelpers
|
|||
import SwiftToolchain
|
||||
import TSCBasic
|
||||
|
||||
struct Install: ParsableCommand {
|
||||
struct Install: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Install new Swift toolchain/SDK."
|
||||
)
|
||||
|
||||
@Argument() var version: String?
|
||||
|
||||
func run() throws {
|
||||
func run() async throws {
|
||||
let terminal = InteractiveWriter.stdout
|
||||
|
||||
_ = try Toolchain(for: version, localFileSystem, terminal)
|
||||
_ = try await Toolchain(for: version, localFileSystem, terminal)
|
||||
terminal.write("\nSDK successfully installed!\n", inColor: .green)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,11 +13,6 @@
|
|||
// limitations under the License.
|
||||
|
||||
import ArgumentParser
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
import CartonHelpers
|
||||
import CartonKit
|
||||
import SwiftToolchain
|
||||
|
@ -39,7 +34,7 @@ private enum Environment: String, CaseIterable, ExpressibleByArgument {
|
|||
|
||||
extension SanitizeVariant: ExpressibleByArgument {}
|
||||
|
||||
struct Test: ParsableCommand {
|
||||
struct Test: AsyncParsableCommand {
|
||||
static let entrypoint = Entrypoint(fileName: "test.js", sha256: testEntrypointSHA256)
|
||||
|
||||
static let configuration = CommandConfiguration(abstract: "Run the tests in a WASI environment.")
|
||||
|
@ -78,13 +73,13 @@ struct Test: ParsableCommand {
|
|||
)
|
||||
}
|
||||
|
||||
func run() throws {
|
||||
func run() async throws {
|
||||
let terminal = InteractiveWriter.stdout
|
||||
|
||||
try Self.entrypoint.check(on: localFileSystem, terminal)
|
||||
let toolchain = try Toolchain(localFileSystem, terminal)
|
||||
let toolchain = try await Toolchain(localFileSystem, terminal)
|
||||
let flavor = buildFlavor()
|
||||
let testBundlePath = try toolchain.buildTestBundle(flavor: flavor)
|
||||
let testBundlePath = try await toolchain.buildTestBundle(flavor: flavor)
|
||||
|
||||
if environment == .wasmer {
|
||||
terminal.write("\nRunning the test bundle with wasmer:\n", inColor: .yellow)
|
||||
|
@ -95,25 +90,24 @@ struct Test: ParsableCommand {
|
|||
wasmerArguments.append("--")
|
||||
wasmerArguments.append(contentsOf: testCases)
|
||||
}
|
||||
let runner = ProcessRunner(wasmerArguments, parser: TestsParser(), terminal)
|
||||
|
||||
try runner.waitUntilFinished()
|
||||
try await Process.run(wasmerArguments, parser: TestsParser(), terminal)
|
||||
} else {
|
||||
try Server(
|
||||
try await Server(
|
||||
.init(
|
||||
builder: nil,
|
||||
mainWasmPath: testBundlePath,
|
||||
verbose: true,
|
||||
skipAutoOpen: false,
|
||||
shouldSkipAutoOpen: false,
|
||||
port: port,
|
||||
host: host,
|
||||
customIndexContent: nil,
|
||||
// swiftlint:disable:next force_try
|
||||
manifest: try! toolchain.manifest.get(),
|
||||
product: nil,
|
||||
entrypoint: Self.entrypoint
|
||||
),
|
||||
terminal
|
||||
entrypoint: Self.entrypoint,
|
||||
terminal: terminal
|
||||
)
|
||||
).run()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright 2021 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.
|
||||
|
||||
import ArgumentParser
|
||||
|
||||
public extension Sequence {
|
||||
func asyncMap<T>(
|
||||
_ transform: (Element) async throws -> T
|
||||
) async rethrows -> [T] {
|
||||
var values = [T]()
|
||||
|
||||
for element in self {
|
||||
try await values.append(transform(element))
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that can be executed as part of a nested tree of commands.
|
||||
public protocol AsyncParsableCommand: ParsableCommand {
|
||||
mutating func run() async throws
|
||||
}
|
||||
|
||||
public extension AsyncParsableCommand {
|
||||
mutating func run() throws {
|
||||
throw CleanExit.helpRequest(self)
|
||||
}
|
||||
}
|
||||
|
||||
public protocol AsyncMain {
|
||||
associatedtype Command: ParsableCommand
|
||||
}
|
||||
|
||||
public extension AsyncMain {
|
||||
static func main() async {
|
||||
do {
|
||||
var command = try Command.parseAsRoot()
|
||||
if var command = command as? AsyncParsableCommand {
|
||||
try await command.run()
|
||||
} else {
|
||||
try command.run()
|
||||
}
|
||||
} catch {
|
||||
Command.exit(withError: error)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright 2022 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.
|
||||
|
||||
import AsyncHTTPClient
|
||||
import Foundation
|
||||
|
||||
public struct InvalidResponseCode: Error {
|
||||
let code: UInt
|
||||
|
||||
var description: String {
|
||||
"""
|
||||
While attempting to download an archive, the server returned an invalid response code \(code)
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
public final class AsyncFileDownload {
|
||||
public let progressStream: AsyncThrowingStream<FileDownloadDelegate.Progress, Error>
|
||||
|
||||
public init(path: String, _ url: URL, _ client: HTTPClient, onTotalBytes: @escaping (Int) -> ()) {
|
||||
progressStream = .init { continuation in
|
||||
do {
|
||||
let request = try HTTPClient.Request.get(url: url)
|
||||
|
||||
let delegate = try FileDownloadDelegate(
|
||||
path: path,
|
||||
reportHead: {
|
||||
guard $0.status == .ok,
|
||||
let totalBytes = $0.headers.first(name: "Content-Length").flatMap(Int.init)
|
||||
else {
|
||||
continuation
|
||||
.finish(throwing: InvalidResponseCode(code: $0.status.code))
|
||||
return
|
||||
}
|
||||
onTotalBytes(totalBytes)
|
||||
},
|
||||
reportProgress: {
|
||||
continuation.yield($0)
|
||||
}
|
||||
)
|
||||
|
||||
Task {
|
||||
_ = try await client.execute(request: request, delegate: delegate)
|
||||
.futureResult
|
||||
.get()
|
||||
|
||||
continuation.finish()
|
||||
}
|
||||
} catch {
|
||||
continuation.finish(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,26 +12,157 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import Dispatch
|
||||
import Foundation
|
||||
import TSCBasic
|
||||
|
||||
public func processDataOutput(_ arguments: [String]) throws -> [UInt8] {
|
||||
let process = Process(arguments: arguments, startNewProcessGroup: false)
|
||||
let process = TSCBasic.Process(arguments: arguments, startNewProcessGroup: false)
|
||||
try process.launch()
|
||||
let result = try process.waitUntilExit()
|
||||
|
||||
guard case .terminated(code: EXIT_SUCCESS) = result.exitStatus else {
|
||||
var description = "Process failed with non-zero exit status"
|
||||
let stdout: String?
|
||||
if let output = try ByteString(result.output.get()).validDescription, !output.isEmpty {
|
||||
description += " and following output:\n\(output)"
|
||||
stdout = output
|
||||
} else {
|
||||
stdout = nil
|
||||
}
|
||||
|
||||
var stderr: String?
|
||||
if let output = try ByteString(result.stderrOutput.get()).validDescription {
|
||||
description += " and following error output:\n\(output)"
|
||||
stderr = output
|
||||
} else {
|
||||
stderr = nil
|
||||
}
|
||||
|
||||
throw ProcessRunnerError(description: description)
|
||||
throw ProcessError(stderr: stderr, stdout: stdout)
|
||||
}
|
||||
|
||||
return try result.output.get()
|
||||
}
|
||||
|
||||
struct ProcessError: Error {
|
||||
let stderr: String?
|
||||
let stdout: String?
|
||||
}
|
||||
|
||||
extension ProcessError: CustomStringConvertible {
|
||||
var description: String {
|
||||
var result = "Process failed with non-zero exit status"
|
||||
if let stdout = stdout {
|
||||
result += " and following output:\n\(stdout)"
|
||||
}
|
||||
|
||||
if let stderr = stderr {
|
||||
result += " and following error output:\n\(stderr)"
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
public extension TSCBasic.Process {
|
||||
// swiftlint:disable:next function_body_length
|
||||
static func run(
|
||||
_ arguments: [String],
|
||||
loadingMessage: String = "Running...",
|
||||
parser: ProcessOutputParser? = nil,
|
||||
_ terminal: InteractiveWriter
|
||||
) async throws {
|
||||
terminal.clearLine()
|
||||
terminal.write("\(loadingMessage)\n", inColor: .yellow)
|
||||
|
||||
let processName = arguments[0].first == "/" ?
|
||||
AbsolutePath(arguments[0]).basename : arguments[0]
|
||||
|
||||
do {
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<(), Swift.Error>) in
|
||||
DispatchQueue.global().async {
|
||||
var stdoutBuffer = ""
|
||||
|
||||
let stdout: TSCBasic.Process.OutputClosure = {
|
||||
guard let string = String(data: Data($0), encoding: .utf8) else { return }
|
||||
if parser != nil {
|
||||
// Aggregate this for formatting later
|
||||
stdoutBuffer += string
|
||||
} else {
|
||||
terminal.write(string)
|
||||
}
|
||||
}
|
||||
|
||||
var stderrBuffer = [UInt8]()
|
||||
|
||||
let stderr: TSCBasic.Process.OutputClosure = {
|
||||
stderrBuffer.append(contentsOf: $0)
|
||||
}
|
||||
|
||||
let process = Process(
|
||||
arguments: arguments,
|
||||
outputRedirection: .stream(stdout: stdout, stderr: stderr),
|
||||
verbose: true,
|
||||
startNewProcessGroup: true
|
||||
)
|
||||
|
||||
let result = Result<ProcessResult, Swift.Error> {
|
||||
try process.launch()
|
||||
return try process.waitUntilExit()
|
||||
}
|
||||
|
||||
switch result.map(\.exitStatus) {
|
||||
case .success(.terminated(code: EXIT_SUCCESS)):
|
||||
terminal.write("\n")
|
||||
if let parser = parser {
|
||||
if parser.parsingConditions.contains(.success) {
|
||||
parser.parse(stdoutBuffer, terminal)
|
||||
}
|
||||
} else {
|
||||
terminal.write(stdoutBuffer)
|
||||
}
|
||||
terminal.write(
|
||||
"\n`\(processName)` process finished successfully\n",
|
||||
inColor: .green,
|
||||
bold: false
|
||||
)
|
||||
continuation.resume()
|
||||
|
||||
case let .failure(error):
|
||||
continuation.resume(throwing: error)
|
||||
default:
|
||||
continuation.resume(
|
||||
throwing: ProcessError(
|
||||
stderr: String(data: Data(stderrBuffer), encoding: .utf8) ?? "",
|
||||
stdout: stdoutBuffer
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
let errorString = String(describing: error)
|
||||
if errorString.isEmpty {
|
||||
terminal.clearLine()
|
||||
terminal.write(
|
||||
"\(processName) process failed.\n\n",
|
||||
inColor: .red
|
||||
)
|
||||
if let error = error as? ProcessError, let stdout = error.stdout {
|
||||
if let parser = parser {
|
||||
if parser.parsingConditions.contains(.failure) {
|
||||
parser.parse(stdout, terminal)
|
||||
}
|
||||
} else {
|
||||
terminal.write(stdout)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
terminal.write(
|
||||
"\nProcess failed and produced following output: \n",
|
||||
inColor: .red
|
||||
)
|
||||
print(error)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,157 +0,0 @@
|
|||
// Copyright 2020 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.
|
||||
|
||||
import Dispatch
|
||||
import Foundation
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
import TSCBasic
|
||||
|
||||
public extension Subscribers.Completion {
|
||||
var result: Result<(), Failure> {
|
||||
switch self {
|
||||
case let .failure(error):
|
||||
return .failure(error)
|
||||
case .finished:
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProcessRunnerError: Error, CustomStringConvertible {
|
||||
let description: String
|
||||
}
|
||||
|
||||
public final class ProcessRunner {
|
||||
public let publisher: AnyPublisher<String, Error>
|
||||
|
||||
private var subscription: AnyCancellable?
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
public init(
|
||||
_ arguments: [String],
|
||||
loadingMessage: String = "Running...",
|
||||
parser: ProcessOutputParser? = nil,
|
||||
_ terminal: InteractiveWriter
|
||||
) {
|
||||
let subject = PassthroughSubject<String, Error>()
|
||||
var tmpOutput = ""
|
||||
publisher = subject
|
||||
.handleEvents(
|
||||
receiveSubscription: { _ in
|
||||
terminal.clearLine()
|
||||
terminal.write("\(loadingMessage)\n", inColor: .yellow)
|
||||
},
|
||||
receiveOutput: {
|
||||
if parser != nil {
|
||||
// Aggregate this for formatting later
|
||||
tmpOutput += $0
|
||||
} else {
|
||||
terminal.write($0)
|
||||
}
|
||||
}, receiveCompletion: {
|
||||
switch $0 {
|
||||
case .finished:
|
||||
let processName = arguments[0].first == "/" ?
|
||||
AbsolutePath(arguments[0]).basename : arguments[0]
|
||||
terminal.write("\n")
|
||||
if let parser = parser {
|
||||
if parser.parsingConditions.contains(.success) {
|
||||
parser.parse(tmpOutput, terminal)
|
||||
}
|
||||
} else {
|
||||
terminal.write(tmpOutput)
|
||||
}
|
||||
terminal.write(
|
||||
"\n`\(processName)` process finished successfully\n",
|
||||
inColor: .green,
|
||||
bold: false
|
||||
)
|
||||
case let .failure(error):
|
||||
let errorString = String(describing: error)
|
||||
if errorString.isEmpty {
|
||||
terminal.clearLine()
|
||||
terminal.write(
|
||||
"Compilation failed.\n\n",
|
||||
inColor: .red
|
||||
)
|
||||
if let parser = parser {
|
||||
if parser.parsingConditions.contains(.failure) {
|
||||
parser.parse(tmpOutput, terminal)
|
||||
}
|
||||
} else {
|
||||
terminal.write(tmpOutput)
|
||||
}
|
||||
} else {
|
||||
terminal.write(
|
||||
"\nProcess failed and produced following output: \n",
|
||||
inColor: .red
|
||||
)
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
DispatchQueue.global().async {
|
||||
let stdout: TSCBasic.Process.OutputClosure = {
|
||||
guard let string = String(data: Data($0), encoding: .utf8) else { return }
|
||||
subject.send(string)
|
||||
}
|
||||
|
||||
var stderrBuffer = [UInt8]()
|
||||
|
||||
let stderr: TSCBasic.Process.OutputClosure = {
|
||||
stderrBuffer.append(contentsOf: $0)
|
||||
}
|
||||
|
||||
let process = Process(
|
||||
arguments: arguments,
|
||||
outputRedirection: .stream(stdout: stdout, stderr: stderr),
|
||||
verbose: true,
|
||||
startNewProcessGroup: true
|
||||
)
|
||||
|
||||
let result = Result<ProcessResult, Error> {
|
||||
try process.launch()
|
||||
return try process.waitUntilExit()
|
||||
}
|
||||
|
||||
switch result.map(\.exitStatus) {
|
||||
case .success(.terminated(code: EXIT_SUCCESS)):
|
||||
subject.send(completion: .finished)
|
||||
case let .failure(error):
|
||||
subject.send(completion: .failure(error))
|
||||
default:
|
||||
let errorDescription = String(data: Data(stderrBuffer), encoding: .utf8) ?? ""
|
||||
return subject
|
||||
.send(completion: .failure(ProcessRunnerError(description: errorDescription)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func waitUntilFinished() throws {
|
||||
try tsc_await { completion in
|
||||
subscription = publisher
|
||||
.sink(
|
||||
receiveCompletion: { completion($0.result) },
|
||||
receiveValue: { _ in }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
// Copyright 2020 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.
|
||||
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
import TSCBasic
|
||||
import TSCUtility
|
||||
|
||||
final class Watcher {
|
||||
private let subject = PassthroughSubject<[AbsolutePath], Never>()
|
||||
private var fsWatch: FSWatch!
|
||||
let publisher: AnyPublisher<[AbsolutePath], Never>
|
||||
|
||||
init(_ paths: [AbsolutePath]) throws {
|
||||
publisher = subject.eraseToAnyPublisher()
|
||||
|
||||
guard !paths.isEmpty else { return }
|
||||
|
||||
fsWatch = FSWatch(paths: paths, latency: 0.1) { [weak self] in
|
||||
self?.subject.send($0)
|
||||
}
|
||||
try fsWatch.start()
|
||||
}
|
||||
}
|
|
@ -34,7 +34,7 @@ public protocol Template {
|
|||
on fileSystem: FileSystem,
|
||||
project: Project,
|
||||
_ terminal: InteractiveWriter
|
||||
) throws
|
||||
) async throws
|
||||
}
|
||||
|
||||
enum TemplateError: Error {
|
||||
|
@ -78,9 +78,9 @@ extension Template {
|
|||
fileSystem: FileSystem,
|
||||
project: Project,
|
||||
_ terminal: InteractiveWriter
|
||||
) throws {
|
||||
try Toolchain(fileSystem, terminal)
|
||||
.packageInit(name: project.name, type: type, inPlace: project.inPlace)
|
||||
) async throws {
|
||||
try await Toolchain(fileSystem, terminal)
|
||||
.runPackageInit(name: project.name, type: type, inPlace: project.inPlace)
|
||||
}
|
||||
|
||||
static func createManifest(
|
||||
|
@ -135,9 +135,9 @@ extension Templates {
|
|||
on fileSystem: FileSystem,
|
||||
project: Project,
|
||||
_ terminal: InteractiveWriter
|
||||
) throws {
|
||||
) async throws {
|
||||
try fileSystem.changeCurrentWorkingDirectory(to: project.path)
|
||||
try createPackage(type: .executable, fileSystem: fileSystem, project: project, terminal)
|
||||
try await createPackage(type: .executable, fileSystem: fileSystem, project: project, terminal)
|
||||
try createManifest(
|
||||
fileSystem: fileSystem,
|
||||
project: project,
|
||||
|
@ -165,9 +165,9 @@ extension Templates {
|
|||
on fileSystem: FileSystem,
|
||||
project: Project,
|
||||
_ terminal: InteractiveWriter
|
||||
) throws {
|
||||
) async throws {
|
||||
try fileSystem.changeCurrentWorkingDirectory(to: project.path)
|
||||
try createPackage(type: .executable,
|
||||
try await createPackage(type: .executable,
|
||||
fileSystem: fileSystem,
|
||||
project: project,
|
||||
terminal)
|
||||
|
|
|
@ -27,8 +27,8 @@ extension Application {
|
|||
let manifest: Manifest
|
||||
let product: ProductDescription?
|
||||
let entrypoint: Entrypoint
|
||||
let onWebSocketOpen: (WebSocket, DestinationEnvironment) -> ()
|
||||
let onWebSocketClose: (WebSocket) -> ()
|
||||
let onWebSocketOpen: (WebSocket, DestinationEnvironment) async -> ()
|
||||
let onWebSocketClose: (WebSocket) async -> ()
|
||||
}
|
||||
|
||||
func configure(_ configuration: Configuration) {
|
||||
|
@ -53,8 +53,8 @@ extension Application {
|
|||
let environment = request.headers["User-Agent"].compactMap(DestinationEnvironment.init).first
|
||||
?? .other
|
||||
|
||||
configuration.onWebSocketOpen(ws, environment)
|
||||
ws.onClose.whenComplete { _ in configuration.onWebSocketClose(ws) }
|
||||
Task { await configuration.onWebSocketOpen(ws, environment) }
|
||||
ws.onClose.whenComplete { _ in Task { await configuration.onWebSocketClose(ws) } }
|
||||
}
|
||||
|
||||
get("main.wasm") {
|
||||
|
|
|
@ -13,14 +13,10 @@
|
|||
// limitations under the License.
|
||||
|
||||
import CartonHelpers
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
import PackageModel
|
||||
import SwiftToolchain
|
||||
import TSCBasic
|
||||
import TSCUtility
|
||||
import Vapor
|
||||
|
||||
private enum Event {
|
||||
|
@ -56,7 +52,7 @@ extension Event: Decodable {
|
|||
}
|
||||
}
|
||||
|
||||
/// This `Hashable` conformance is required to handle simulatenous connections with `Set<WebSocket>`
|
||||
/// This `Hashable` conformance is required to handle simultaneous connections with `Set<WebSocket>`
|
||||
extension WebSocket: Hashable {
|
||||
public static func == (lhs: WebSocket, rhs: WebSocket) -> Bool {
|
||||
lhs === rhs
|
||||
|
@ -67,71 +63,85 @@ extension WebSocket: Hashable {
|
|||
}
|
||||
}
|
||||
|
||||
public final class Server {
|
||||
public actor Server {
|
||||
/// Used for decoding `Event` values sent from the WebSocket client.
|
||||
private let decoder = JSONDecoder()
|
||||
|
||||
/// A set of connected WebSocket clients currently connected to this server.
|
||||
private var connections = Set<WebSocket>()
|
||||
private var subscriptions = [AnyCancellable]()
|
||||
private let watcher: Watcher?
|
||||
|
||||
/// Filesystem watcher monitoring relevant source files for changes.
|
||||
private var watcher: FSWatch?
|
||||
|
||||
/// An instance of Vapor server application.
|
||||
private let app: Application
|
||||
|
||||
/// Local URL of this server, `https://128.0.0.1:8080/` by default.
|
||||
private let localURL: String
|
||||
private let skipAutoOpen: Bool
|
||||
|
||||
/// Whether a browser tab should automatically open as soon as the server is ready.
|
||||
private let shouldSkipAutoOpen: Bool
|
||||
|
||||
/// Whether a build that could be triggered by this server is currently running.
|
||||
private var isBuildCurrentlyRunning = false
|
||||
|
||||
/// Whether a subsequent build is currently scheduled on top of a currently running build.
|
||||
private var isSubsequentBuildScheduled = false
|
||||
|
||||
public struct Configuration {
|
||||
let builder: Builder?
|
||||
let mainWasmPath: AbsolutePath
|
||||
let verbose: Bool
|
||||
let skipAutoOpen: Bool
|
||||
let shouldSkipAutoOpen: Bool
|
||||
let port: Int
|
||||
let host: String
|
||||
let customIndexContent: String?
|
||||
let manifest: Manifest
|
||||
let product: ProductDescription?
|
||||
let entrypoint: Entrypoint
|
||||
let terminal: InteractiveWriter
|
||||
|
||||
public init(
|
||||
builder: Builder?,
|
||||
mainWasmPath: AbsolutePath,
|
||||
verbose: Bool,
|
||||
skipAutoOpen: Bool,
|
||||
shouldSkipAutoOpen: Bool,
|
||||
port: Int,
|
||||
host: String,
|
||||
customIndexContent: String?,
|
||||
manifest: Manifest,
|
||||
product: ProductDescription?,
|
||||
entrypoint: Entrypoint
|
||||
entrypoint: Entrypoint,
|
||||
terminal: InteractiveWriter
|
||||
) {
|
||||
self.builder = builder
|
||||
self.mainWasmPath = mainWasmPath
|
||||
self.verbose = verbose
|
||||
self.skipAutoOpen = skipAutoOpen
|
||||
self.shouldSkipAutoOpen = shouldSkipAutoOpen
|
||||
self.port = port
|
||||
self.host = host
|
||||
self.customIndexContent = customIndexContent
|
||||
self.manifest = manifest
|
||||
self.product = product
|
||||
self.entrypoint = entrypoint
|
||||
self.terminal = terminal
|
||||
}
|
||||
}
|
||||
|
||||
public init(
|
||||
_ configuration: Configuration,
|
||||
_ terminal: InteractiveWriter
|
||||
) throws {
|
||||
if let builder = configuration.builder {
|
||||
watcher = try Watcher(builder.pathsToWatch)
|
||||
} else {
|
||||
watcher = nil
|
||||
}
|
||||
|
||||
_ configuration: Configuration
|
||||
) async throws {
|
||||
var env = Environment(
|
||||
name: configuration.verbose ? "development" : "production",
|
||||
arguments: ["vapor"]
|
||||
)
|
||||
localURL = "http://\(configuration.host):\(configuration.port)/"
|
||||
skipAutoOpen = configuration.skipAutoOpen
|
||||
shouldSkipAutoOpen = configuration.shouldSkipAutoOpen
|
||||
|
||||
try LoggingSystem.bootstrap(from: &env)
|
||||
app = Application(env)
|
||||
watcher = nil
|
||||
|
||||
app.configure(
|
||||
.init(
|
||||
port: configuration.port,
|
||||
|
@ -142,39 +152,76 @@ public final class Server {
|
|||
product: configuration.product,
|
||||
entrypoint: configuration.entrypoint,
|
||||
onWebSocketOpen: { [weak self] ws, environment in
|
||||
if let handler = self?.createWSHandler(
|
||||
if let handler = await self?.createWSHandler(
|
||||
configuration,
|
||||
in: environment,
|
||||
terminal: terminal
|
||||
terminal: configuration.terminal
|
||||
) {
|
||||
ws.onText(handler)
|
||||
}
|
||||
self?.connections.insert(ws)
|
||||
|
||||
await self?.add(connection: ws)
|
||||
},
|
||||
onWebSocketClose: { [weak self] in self?.connections.remove($0) }
|
||||
onWebSocketClose: { [weak self] in await self?.remove(connection: $0) }
|
||||
)
|
||||
)
|
||||
// Listen to Vapor App lifecycle events
|
||||
app.lifecycle.use(self)
|
||||
|
||||
guard let builder = configuration.builder, let watcher = watcher else {
|
||||
guard let builder = configuration.builder else {
|
||||
return
|
||||
}
|
||||
|
||||
watcher.publisher
|
||||
.flatMap(maxPublishers: .max(1)) { changes -> AnyPublisher<String, Never> in
|
||||
if !configuration.verbose {
|
||||
terminal.clearWindow()
|
||||
}
|
||||
terminal.write("\nThese paths have changed, rebuilding...\n", inColor: .yellow)
|
||||
for change in changes.map(\.pathString) {
|
||||
terminal.write("- \(change)\n", inColor: .cyan)
|
||||
}
|
||||
watcher = FSWatch(paths: builder.pathsToWatch, latency: 0.1) { [weak self] changes in
|
||||
guard let self = self, !changes.isEmpty else { return }
|
||||
Task { try await self.onChange(changes, configuration) }
|
||||
}
|
||||
try watcher?.start()
|
||||
}
|
||||
|
||||
return self.run(builder, terminal)
|
||||
private func onChange(_ changes: [AbsolutePath], _ configuration: Configuration) async throws {
|
||||
guard !isBuildCurrentlyRunning else {
|
||||
if !isSubsequentBuildScheduled {
|
||||
isSubsequentBuildScheduled = true
|
||||
}
|
||||
.sink(receiveValue: { _ in })
|
||||
.store(in: &subscriptions)
|
||||
return
|
||||
}
|
||||
|
||||
if !configuration.verbose {
|
||||
configuration.terminal.clearWindow()
|
||||
}
|
||||
configuration.terminal.write(
|
||||
"\nThese paths have changed, rebuilding...\n",
|
||||
inColor: .yellow
|
||||
)
|
||||
for change in changes.map(\.pathString) {
|
||||
configuration.terminal.write("- \(change)\n", inColor: .cyan)
|
||||
}
|
||||
|
||||
isBuildCurrentlyRunning = true
|
||||
// `configuration.builder` is guaranteed to be non-nil here as its presence is checked in `init`
|
||||
try await run(configuration.builder!, configuration.terminal)
|
||||
|
||||
if isSubsequentBuildScheduled {
|
||||
configuration.terminal.write(
|
||||
"\nMore paths have changed during the build, rebuilding again...\n",
|
||||
inColor: .yellow
|
||||
)
|
||||
try await run(configuration.builder!, configuration.terminal)
|
||||
}
|
||||
|
||||
isSubsequentBuildScheduled = false
|
||||
isBuildCurrentlyRunning = false
|
||||
}
|
||||
|
||||
private func add(pendingChanges: [AbsolutePath]) {}
|
||||
|
||||
private func add(connection: WebSocket) {
|
||||
connections.insert(connection)
|
||||
}
|
||||
|
||||
private func remove(connection: WebSocket) {
|
||||
connections.remove(connection)
|
||||
}
|
||||
|
||||
/// Blocking function that starts the HTTP server.
|
||||
|
@ -189,18 +236,12 @@ public final class Server {
|
|||
private func run(
|
||||
_ builder: Builder,
|
||||
_ terminal: InteractiveWriter
|
||||
) -> AnyPublisher<String, Never> {
|
||||
builder
|
||||
.run()
|
||||
.handleEvents(receiveCompletion: { [weak self] in
|
||||
guard case .finished = $0, let self = self else { return }
|
||||
) async throws {
|
||||
try await builder.run()
|
||||
|
||||
terminal.write("\nBuild completed successfully\n", inColor: .green, bold: false)
|
||||
terminal.logLookup("The app is currently hosted at ", self.localURL)
|
||||
self.connections.forEach { $0.send("reload") }
|
||||
})
|
||||
.catch { _ in Empty().eraseToAnyPublisher() }
|
||||
.eraseToAnyPublisher()
|
||||
terminal.write("\nBuild completed successfully\n", inColor: .green, bold: false)
|
||||
terminal.logLookup("The app is currently hosted at ", localURL)
|
||||
connections.forEach { $0.send("reload") }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,15 +289,15 @@ extension Server {
|
|||
}
|
||||
|
||||
extension Server: LifecycleHandler {
|
||||
public func didBoot(_ application: Application) throws {
|
||||
guard !skipAutoOpen else { return }
|
||||
public nonisolated func didBoot(_ application: Application) throws {
|
||||
guard !shouldSkipAutoOpen else { return }
|
||||
openInSystemBrowser(url: localURL)
|
||||
}
|
||||
|
||||
/// Attempts to open the specified URL string in system browser on macOS and Linux.
|
||||
/// - Returns: true if launching command returns successfully.
|
||||
@discardableResult
|
||||
private func openInSystemBrowser(url: String) -> Bool {
|
||||
private nonisolated func openInSystemBrowser(url: String) -> Bool {
|
||||
#if os(macOS)
|
||||
let openCommand = "open"
|
||||
#elseif os(Linux)
|
||||
|
|
|
@ -13,11 +13,6 @@
|
|||
// limitations under the License.
|
||||
|
||||
import CartonHelpers
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
import Foundation
|
||||
import TSCBasic
|
||||
import WasmTransformer
|
||||
|
@ -25,13 +20,10 @@ import WasmTransformer
|
|||
public final class Builder {
|
||||
public let mainWasmPath: AbsolutePath
|
||||
public let pathsToWatch: [AbsolutePath]
|
||||
|
||||
private var currentProcess: ProcessRunner?
|
||||
private let arguments: [String]
|
||||
private let flavor: BuildFlavor
|
||||
private let terminal: InteractiveWriter
|
||||
private let fileSystem: FileSystem
|
||||
private var subscription: AnyCancellable?
|
||||
|
||||
public init(
|
||||
arguments: [String],
|
||||
|
@ -49,70 +41,57 @@ public final class Builder {
|
|||
self.fileSystem = fileSystem
|
||||
}
|
||||
|
||||
private func processPublisher(builderArguments: [String]) -> AnyPublisher<String, Error> {
|
||||
private func buildWithoutSanitizing(builderArguments: [String]) async throws {
|
||||
let buildStarted = Date()
|
||||
let process = ProcessRunner(
|
||||
try await Process.run(
|
||||
builderArguments,
|
||||
loadingMessage: "Compiling...",
|
||||
parser: nil,
|
||||
terminal
|
||||
)
|
||||
currentProcess = process
|
||||
|
||||
return process
|
||||
.publisher
|
||||
.handleEvents(receiveCompletion: { [weak self] in
|
||||
guard case .finished = $0, let self = self else { return }
|
||||
self.terminal.logLookup(
|
||||
"`swift build` completed in ",
|
||||
String(format: "%.2f seconds", abs(buildStarted.timeIntervalSinceNow))
|
||||
)
|
||||
|
||||
self.terminal.logLookup(
|
||||
"`swift build` completed in ",
|
||||
String(format: "%.2f seconds", abs(buildStarted.timeIntervalSinceNow))
|
||||
)
|
||||
var transformers: [(inout InputByteStream, inout InMemoryOutputWriter) throws -> ()] = []
|
||||
if self.flavor.environment != .other {
|
||||
transformers.append(I64ImportTransformer().transform)
|
||||
}
|
||||
|
||||
var transformers: [(inout InputByteStream, inout InMemoryOutputWriter) throws -> ()] = []
|
||||
if self.flavor.environment != .other {
|
||||
transformers.append(I64ImportTransformer().transform)
|
||||
}
|
||||
switch self.flavor.sanitize {
|
||||
case .stackOverflow:
|
||||
transformers.append(StackOverflowSanitizer().transform)
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
|
||||
switch self.flavor.sanitize {
|
||||
case .stackOverflow:
|
||||
transformers.append(StackOverflowSanitizer().transform)
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
guard !transformers.isEmpty else { return }
|
||||
|
||||
guard !transformers.isEmpty else { return }
|
||||
let binary = try self.fileSystem.readFileContents(self.mainWasmPath)
|
||||
|
||||
// FIXME: errors from these `try` expressions should be recoverable, not sure how to
|
||||
// do that in `handleEvents`, and `flatMap` doesn't fit here as we need to track
|
||||
// publisher completion.
|
||||
// swiftlint:disable force_try
|
||||
let binary = try! self.fileSystem.readFileContents(self.mainWasmPath)
|
||||
let transformStarted = Date()
|
||||
var inputBinary = binary.contents
|
||||
for transformer in transformers {
|
||||
var input = InputByteStream(bytes: inputBinary)
|
||||
var writer = InMemoryOutputWriter(reservingCapacity: inputBinary.count)
|
||||
try! transformer(&input, &writer)
|
||||
inputBinary = writer.bytes()
|
||||
}
|
||||
|
||||
let transformStarted = Date()
|
||||
var inputBinary = binary.contents
|
||||
for transformer in transformers {
|
||||
var input = InputByteStream(bytes: inputBinary)
|
||||
var writer = InMemoryOutputWriter(reservingCapacity: inputBinary.count)
|
||||
try! transformer(&input, &writer)
|
||||
inputBinary = writer.bytes()
|
||||
}
|
||||
self.terminal.logLookup(
|
||||
"Binary transformation for Safari compatibility completed in ",
|
||||
String(format: "%.2f seconds", abs(transformStarted.timeIntervalSinceNow))
|
||||
)
|
||||
|
||||
self.terminal.logLookup(
|
||||
"Binary transformation for Safari compatibility completed in ",
|
||||
String(format: "%.2f seconds", abs(transformStarted.timeIntervalSinceNow))
|
||||
)
|
||||
|
||||
try! self.fileSystem.writeFileContents(self.mainWasmPath, bytes: .init(inputBinary))
|
||||
// swiftlint:enable force_try
|
||||
})
|
||||
.eraseToAnyPublisher()
|
||||
try self.fileSystem.writeFileContents(self.mainWasmPath, bytes: .init(inputBinary))
|
||||
}
|
||||
|
||||
public func run() -> AnyPublisher<String, Error> {
|
||||
public func run() async throws {
|
||||
switch flavor.sanitize {
|
||||
case .none:
|
||||
return processPublisher(builderArguments: arguments)
|
||||
return try await buildWithoutSanitizing(builderArguments: arguments)
|
||||
case .stackOverflow:
|
||||
let sanitizerFile =
|
||||
fileSystem.homeDirectory.appending(components: ".carton", "static", "so_sanitizer.wasm")
|
||||
|
@ -123,17 +102,7 @@ public final class Builder {
|
|||
// stack-overflow-sanitizer depends on "--stack-first"
|
||||
"-Xlinker", "--stack-first",
|
||||
])
|
||||
return processPublisher(builderArguments: modifiedArguments)
|
||||
}
|
||||
}
|
||||
|
||||
public func runAndWaitUntilFinished() throws {
|
||||
try tsc_await { completion in
|
||||
subscription = run()
|
||||
.sink(
|
||||
receiveCompletion: { completion($0.result) },
|
||||
receiveValue: { _ in }
|
||||
)
|
||||
return try await buildWithoutSanitizing(builderArguments: modifiedArguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
// Copyright 2020 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.
|
||||
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
import TSCBasic
|
||||
import TSCUtility
|
||||
|
||||
struct Progress {
|
||||
let step: Int
|
||||
let total: Int
|
||||
let text: String
|
||||
}
|
||||
|
||||
extension Publisher where Output == Progress {
|
||||
func handle(
|
||||
with progressAnimation: ProgressAnimationProtocol
|
||||
) -> Publishers.HandleEvents<Self> {
|
||||
handleEvents(
|
||||
receiveOutput: {
|
||||
progressAnimation.update(step: $0.step, total: $0.total, text: $0.text)
|
||||
},
|
||||
receiveCompletion: {
|
||||
switch $0 {
|
||||
case .finished:
|
||||
progressAnimation.complete(success: true)
|
||||
case .failure:
|
||||
progressAnimation.complete(success: false)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -23,7 +23,6 @@ public let compatibleJSKitVersion = Version(0, 11, 1)
|
|||
|
||||
enum ToolchainError: Error, CustomStringConvertible {
|
||||
case directoryDoesNotExist(AbsolutePath)
|
||||
case invalidResponseCode(UInt)
|
||||
case invalidInstallationArchive(AbsolutePath)
|
||||
case noExecutableProduct
|
||||
case failedToBuild(product: String)
|
||||
|
@ -38,10 +37,6 @@ enum ToolchainError: Error, CustomStringConvertible {
|
|||
switch self {
|
||||
case let .directoryDoesNotExist(path):
|
||||
return "Directory at path \(path.pathString) does not exist and could not be created"
|
||||
case let .invalidResponseCode(code):
|
||||
return """
|
||||
While attempting to download an archive, the server returned an invalid response code \(code)
|
||||
"""
|
||||
case let .invalidInstallationArchive(path):
|
||||
return "Invalid toolchain/SDK archive was installed at path \(path)"
|
||||
case .noExecutableProduct:
|
||||
|
@ -105,9 +100,9 @@ public final class Toolchain {
|
|||
for versionSpec: String? = nil,
|
||||
_ fileSystem: FileSystem,
|
||||
_ terminal: InteractiveWriter
|
||||
) throws {
|
||||
) async throws {
|
||||
let toolchainSystem = ToolchainSystem(fileSystem: fileSystem)
|
||||
let (swiftPath, version) = try toolchainSystem.inferSwiftPath(from: versionSpec, terminal)
|
||||
let (swiftPath, version) = try await toolchainSystem.inferSwiftPath(from: versionSpec, terminal)
|
||||
self.swiftPath = swiftPath
|
||||
self.version = version
|
||||
self.fileSystem = fileSystem
|
||||
|
@ -207,7 +202,7 @@ public final class Toolchain {
|
|||
public func buildCurrentProject(
|
||||
product: String?,
|
||||
flavor: BuildFlavor
|
||||
) throws -> BuildDescription {
|
||||
) async throws -> BuildDescription {
|
||||
guard let product = try inferDevProduct(hint: product)
|
||||
else { throw ToolchainError.noExecutableProduct }
|
||||
|
||||
|
@ -270,14 +265,13 @@ public final class Toolchain {
|
|||
builderArguments.append(contentsOf: ["-Xlinker", "-licuuc", "-Xlinker", "-licui18n"])
|
||||
}
|
||||
|
||||
try Builder(
|
||||
try await Builder(
|
||||
arguments: builderArguments,
|
||||
mainWasmPath: mainWasmPath,
|
||||
flavor,
|
||||
fileSystem,
|
||||
terminal
|
||||
)
|
||||
.runAndWaitUntilFinished()
|
||||
).run()
|
||||
|
||||
guard fileSystem.exists(mainWasmPath) else {
|
||||
terminal.write(
|
||||
|
@ -293,7 +287,7 @@ public final class Toolchain {
|
|||
/// Returns an absolute path to the resulting test bundle
|
||||
public func buildTestBundle(
|
||||
flavor: BuildFlavor
|
||||
) throws -> AbsolutePath {
|
||||
) async throws -> AbsolutePath {
|
||||
let manifest = try self.manifest.get()
|
||||
let binPath = try inferBinPath(isRelease: flavor.isRelease)
|
||||
let testProductName = "\(manifest.name)PackageTests"
|
||||
|
@ -308,7 +302,7 @@ public final class Toolchain {
|
|||
var builderArguments = [
|
||||
swiftPath.pathString, "build", "-c", flavor.isRelease ? "release" : "debug",
|
||||
"--product", testProductName, "--triple", "wasm32-unknown-wasi",
|
||||
"-Xswiftc", "-color-diagnostics"
|
||||
"-Xswiftc", "-color-diagnostics",
|
||||
]
|
||||
|
||||
// Versions later than 5.3.x have test discovery enabled by default and the explicit flag
|
||||
|
@ -323,14 +317,13 @@ public final class Toolchain {
|
|||
builderArguments.append(contentsOf: ["-Xlinker", "-licuuc", "-Xlinker", "-licui18n"])
|
||||
}
|
||||
|
||||
try Builder(
|
||||
try await Builder(
|
||||
arguments: builderArguments,
|
||||
mainWasmPath: testBundlePath,
|
||||
flavor,
|
||||
fileSystem,
|
||||
terminal
|
||||
)
|
||||
.runAndWaitUntilFinished()
|
||||
).run()
|
||||
|
||||
guard fileSystem.exists(testBundlePath) else {
|
||||
terminal.write(
|
||||
|
@ -343,7 +336,7 @@ public final class Toolchain {
|
|||
return testBundlePath
|
||||
}
|
||||
|
||||
public func packageInit(name: String, type: PackageType, inPlace: Bool) throws {
|
||||
public func runPackageInit(name: String, type: PackageType, inPlace: Bool) async throws {
|
||||
var initArgs = [
|
||||
swiftPath.pathString, "package", "init",
|
||||
"--type", type.rawValue,
|
||||
|
@ -351,13 +344,11 @@ public final class Toolchain {
|
|||
if !inPlace {
|
||||
initArgs.append(contentsOf: ["--name", name])
|
||||
}
|
||||
try ProcessRunner(initArgs, terminal)
|
||||
.waitUntilFinished()
|
||||
try await TSCBasic.Process.run(initArgs, terminal)
|
||||
}
|
||||
|
||||
public func runPackage(_ arguments: [String]) throws {
|
||||
public func runPackage(_ arguments: [String]) async throws {
|
||||
let args = [swiftPath.pathString, "package"] + arguments
|
||||
try ProcessRunner(args, terminal)
|
||||
.waitUntilFinished()
|
||||
try await TSCBasic.Process.run(args, terminal)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,16 +15,17 @@
|
|||
import AsyncHTTPClient
|
||||
import CartonHelpers
|
||||
import Foundation
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
import TSCBasic
|
||||
import TSCUtility
|
||||
|
||||
private let expectedArchiveSize = 891_856_371
|
||||
|
||||
private extension FileDownloadDelegate.Progress {
|
||||
var totalOrEstimatedBytes: Int {
|
||||
totalBytes ?? expectedArchiveSize
|
||||
}
|
||||
}
|
||||
|
||||
extension ToolchainSystem {
|
||||
func installSDK(
|
||||
version: String,
|
||||
|
@ -32,7 +33,7 @@ extension ToolchainSystem {
|
|||
to sdkPath: AbsolutePath,
|
||||
_ client: HTTPClient,
|
||||
_ terminal: InteractiveWriter
|
||||
) throws -> AbsolutePath {
|
||||
) async throws -> AbsolutePath {
|
||||
if !fileSystem.exists(sdkPath, followSymlink: true) {
|
||||
try fileSystem.createDirectory(sdkPath, recursive: true)
|
||||
}
|
||||
|
@ -44,10 +45,6 @@ extension ToolchainSystem {
|
|||
let ext = url.pathExtension
|
||||
|
||||
let archivePath = sdkPath.appending(component: "\(version).\(ext)")
|
||||
let (delegate, subject) = try downloadDelegate(path: archivePath.pathString, terminal)
|
||||
|
||||
var subscriptions = [AnyCancellable]()
|
||||
let request = try HTTPClient.Request.get(url: url)
|
||||
|
||||
// Clean up the downloaded file (especially important for failed downloads, otherwise running
|
||||
// `carton` again will fail trying to pick up the broken download).
|
||||
|
@ -59,40 +56,41 @@ extension ToolchainSystem {
|
|||
}
|
||||
}
|
||||
|
||||
_ = try tsc_await { (completion: @escaping (Result<(), Error>) -> ()) in
|
||||
client.execute(request: request, delegate: delegate).futureResult.whenComplete {
|
||||
switch $0 {
|
||||
case .success:
|
||||
subject.send(completion: .finished)
|
||||
case let .failure(error):
|
||||
subject.send(completion: .failure(error))
|
||||
do {
|
||||
let fileDownload = AsyncFileDownload(
|
||||
path: archivePath.pathString,
|
||||
url,
|
||||
client,
|
||||
onTotalBytes: {
|
||||
terminal.write("Archive size is \($0 / 1_000_000) MB\n", inColor: .yellow)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
subject
|
||||
.removeDuplicates {
|
||||
// only report values that differ in more than 1%
|
||||
$1.step - $0.step < ($0.total / 100)
|
||||
let animation = PercentProgressAnimation(
|
||||
stream: stdoutStream,
|
||||
header: "Downloading the archive"
|
||||
)
|
||||
var previouslyReceived = 0
|
||||
for try await progress in fileDownload.progressStream {
|
||||
guard progress.receivedBytes - previouslyReceived >= (progress.totalOrEstimatedBytes / 100)
|
||||
else {
|
||||
continue
|
||||
}
|
||||
.handle(
|
||||
with: PercentProgressAnimation(stream: stdoutStream, header: "Downloading the archive")
|
||||
defer { previouslyReceived = progress.receivedBytes }
|
||||
|
||||
animation.update(
|
||||
step: progress.receivedBytes,
|
||||
total: progress.totalOrEstimatedBytes,
|
||||
text: "saving to \(archivePath.pathString)"
|
||||
)
|
||||
.sink(
|
||||
receiveCompletion: {
|
||||
switch $0 {
|
||||
case .finished:
|
||||
terminal.write("Download completed successfully\n", inColor: .green)
|
||||
completion(.success(()))
|
||||
case let .failure(error):
|
||||
terminal.write("Download failed\n", inColor: .red)
|
||||
completion(.failure(error))
|
||||
}
|
||||
},
|
||||
receiveValue: { _ in }
|
||||
)
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
} catch {
|
||||
terminal.write("Download failed with error \(error)\n", inColor: .red)
|
||||
throw error
|
||||
}
|
||||
|
||||
terminal.write("Download completed successfully\n", inColor: .green)
|
||||
|
||||
let installationPath: AbsolutePath
|
||||
|
||||
let arguments: [String]
|
||||
|
@ -118,30 +116,4 @@ extension ToolchainSystem {
|
|||
|
||||
return installationPath
|
||||
}
|
||||
|
||||
private func downloadDelegate(
|
||||
path: String,
|
||||
_ terminal: InteractiveWriter
|
||||
) throws -> (FileDownloadDelegate, PassthroughSubject<Progress, Error>) {
|
||||
let subject = PassthroughSubject<Progress, Error>()
|
||||
return try (FileDownloadDelegate(
|
||||
path: path,
|
||||
reportHead: {
|
||||
guard $0.status == .ok,
|
||||
let totalBytes = $0.headers.first(name: "Content-Length").flatMap(Int.init)
|
||||
else {
|
||||
subject.send(completion: .failure(ToolchainError.invalidResponseCode($0.status.code)))
|
||||
return
|
||||
}
|
||||
terminal.write("Archive size is \(totalBytes / 1_000_000) MB\n", inColor: .yellow)
|
||||
},
|
||||
reportProgress: {
|
||||
subject.send(.init(
|
||||
step: $0.receivedBytes,
|
||||
total: $0.totalBytes ?? expectedArchiveSize,
|
||||
text: "saving to \(path)"
|
||||
))
|
||||
}
|
||||
), subject)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -210,7 +210,7 @@ public class ToolchainSystem {
|
|||
func inferSwiftPath(
|
||||
from versionSpec: String? = nil,
|
||||
_ terminal: InteractiveWriter
|
||||
) throws -> (AbsolutePath, String) {
|
||||
) async throws -> (AbsolutePath, String) {
|
||||
let specURL = versionSpec.flatMap { (string: String) -> Foundation.URL? in
|
||||
guard
|
||||
let url = Foundation.URL(string: string),
|
||||
|
@ -251,7 +251,7 @@ public class ToolchainSystem {
|
|||
inColor: .yellow
|
||||
)
|
||||
terminal.logLookup("Swift toolchain/SDK download URL: ", downloadURL)
|
||||
let installationPath = try installSDK(
|
||||
let installationPath = try await installSDK(
|
||||
version: swiftVersion,
|
||||
from: downloadURL,
|
||||
to: cartonToolchainResolver.cartonSDKPath,
|
||||
|
|
|
@ -17,15 +17,14 @@ import CartonHelpers
|
|||
import TSCBasic
|
||||
import WasmTransformer
|
||||
|
||||
struct HashArchive: ParsableCommand {
|
||||
/** Converts a hexadecimal hash string to Swift code that represents a static
|
||||
struct HashArchive: AsyncParsableCommand {
|
||||
/** Converts a hexadecimal hash string to Swift code that represents an archive of static assets.
|
||||
*/
|
||||
private func arrayString(from hash: String) -> String {
|
||||
precondition(hash.count == 64)
|
||||
|
||||
let commaSeparated = stride(from: 0, to: hash.count, by: 2)
|
||||
.map { hash.dropLast(hash.count - $0 - 2).suffix(2) }
|
||||
.map { "0x\($0)" }
|
||||
.map { "0x\(hash.dropLast(hash.count - $0 - 2).suffix(2))" }
|
||||
.joined(separator: ", ")
|
||||
|
||||
precondition(commaSeparated.count == 190)
|
||||
|
@ -36,7 +35,7 @@ struct HashArchive: ParsableCommand {
|
|||
"""
|
||||
}
|
||||
|
||||
func run() throws {
|
||||
func run() async throws {
|
||||
let terminal = InteractiveWriter.stdout
|
||||
let cwd = localFileSystem.currentWorkingDirectory!
|
||||
let staticPath = AbsolutePath(cwd, "static")
|
||||
|
@ -46,8 +45,8 @@ struct HashArchive: ParsableCommand {
|
|||
)
|
||||
|
||||
try localFileSystem.createDirectory(dotFilesStaticPath, recursive: true)
|
||||
let hashes = try ["dev", "bundle", "test"].map { entrypoint -> (String, String) in
|
||||
try ProcessRunner(["npm", "run", entrypoint], terminal).waitUntilFinished()
|
||||
let hashes = try await ["dev", "bundle", "test"].asyncMap { entrypoint -> (String, String) in
|
||||
try await Process.run(["npm", "run", entrypoint], terminal)
|
||||
let entrypointPath = AbsolutePath(staticPath, "\(entrypoint).js")
|
||||
let dotFilesEntrypointPath = dotFilesStaticPath.appending(component: "\(entrypoint).js")
|
||||
try localFileSystem.removeFileTree(dotFilesEntrypointPath)
|
||||
|
@ -68,7 +67,7 @@ struct HashArchive: ParsableCommand {
|
|||
.dropFirst()
|
||||
.map(\.pathString)
|
||||
|
||||
try ProcessRunner(["zip", "-j", "static.zip"] + archiveSources, terminal).waitUntilFinished()
|
||||
try await Process.run(["zip", "-j", "static.zip"] + archiveSources, terminal)
|
||||
|
||||
let archiveHash = try SHA256().hash(
|
||||
localFileSystem.readFileContents(AbsolutePath(
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import ArgumentParser
|
||||
import CartonHelpers
|
||||
|
||||
struct CartonRelease: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
|
@ -21,4 +22,7 @@ struct CartonRelease: ParsableCommand {
|
|||
)
|
||||
}
|
||||
|
||||
CartonRelease.main()
|
||||
@main
|
||||
struct Main: AsyncMain {
|
||||
typealias Command = CartonRelease
|
||||
}
|
|
@ -6,8 +6,8 @@
|
|||
"repositoryURL": "https://github.com/swiftwasm/JavaScriptKit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b19e7c8b10a2750ed47753e31ed13613171f3294",
|
||||
"version": "0.10.1"
|
||||
"revision": "309e63c03d8116210ad0437f5d1f09a26d4de48b",
|
||||
"version": "0.11.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -24,8 +24,8 @@
|
|||
"repositoryURL": "https://github.com/swiftwasm/OpenCombineJS.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "eaf324ce78710f53b52fb82e9a8de4693633e33a",
|
||||
"version": "0.1.1"
|
||||
"revision": "f1f1799ddbb9876a0ef8c5700a3b78d352d0b969",
|
||||
"version": "0.1.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -51,8 +51,8 @@
|
|||
"repositoryURL": "https://github.com/TokamakUI/Tokamak",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "c2ed28ca40445b1b63bfdfe45de2507e5eb24a21",
|
||||
"version": "0.8.0"
|
||||
"revision": "32616fe9d4d8dcdd6a48519e2ce8d7b0ba744434",
|
||||
"version": "0.9.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue