diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Serial.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Serial.xcscheme
new file mode 100644
index 0000000..91ae4b5
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/Serial.xcscheme
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SerialDSL-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SerialDSL-Package.xcscheme
new file mode 100644
index 0000000..b2d25d6
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/SerialDSL-Package.xcscheme
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SerialDSL.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SerialDSL.xcscheme
new file mode 100644
index 0000000..908356f
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/SerialDSL.xcscheme
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..d0fb04c
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,22 @@
+{
+ "configurations": [
+ {
+ "type": "lldb",
+ "request": "launch",
+ "name": "Debug Serial",
+ "program": "${workspaceFolder:swift-Serial-DSL}/.build/debug/Serial",
+ "args": [],
+ "cwd": "${workspaceFolder:swift-Serial-DSL}",
+ "preLaunchTask": "swift: Build Debug Serial"
+ },
+ {
+ "type": "lldb",
+ "request": "launch",
+ "name": "Release Serial",
+ "program": "${workspaceFolder:swift-Serial-DSL}/.build/release/Serial",
+ "args": [],
+ "cwd": "${workspaceFolder:swift-Serial-DSL}",
+ "preLaunchTask": "swift: Build Release Serial"
+ }
+ ]
+}
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..e13c7ac
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,17 @@
+export EXECUTABLE_NAME = serial
+
+PREFIX = /usr/local
+INSTALL_PATH = $(PREFIX)/bin/$(EXECUTABLE_NAME)
+SHARE_PATH = $(PREFIX)/share/$(EXECUTABLE_NAME)
+CURRENT_PATH = $(PWD)
+SWIFT_BUILD_FLAGS = --disable-sandbox -c release --arch arm64 --arch x86_64
+EXECUTABLE_PATH = $(shell swift build $(SWIFT_BUILD_FLAGS) --show-bin-path)/$(EXECUTABLE_NAME)
+
+.PHONY: install build uninstall format_code brew release
+
+install: build
+ mkdir -p $(PREFIX)/bin
+ cp -f $(EXECUTABLE_PATH) $(INSTALL_PATH)
+
+build:
+ swift build $(SWIFT_BUILD_FLAGS)
diff --git a/Package.resolved b/Package.resolved
new file mode 100644
index 0000000..0d2f30a
--- /dev/null
+++ b/Package.resolved
@@ -0,0 +1,32 @@
+{
+ "pins" : [
+ {
+ "identity" : "swift-argument-parser",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-argument-parser",
+ "state" : {
+ "revision" : "9f39744e025c7d377987f30b03770805dcb0bcd1",
+ "version" : "1.1.4"
+ }
+ },
+ {
+ "identity" : "swift-system",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-system.git",
+ "state" : {
+ "revision" : "836bc4557b74fe6d2660218d56e3ce96aff76574",
+ "version" : "1.1.1"
+ }
+ },
+ {
+ "identity" : "swift-tools-support-core",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-tools-support-core.git",
+ "state" : {
+ "revision" : "0b77e67c484e532444ceeab60119b8536f8cd648",
+ "version" : "0.3.0"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/Package.swift b/Package.swift
index aecb681..a7d5975 100644
--- a/Package.swift
+++ b/Package.swift
@@ -6,26 +6,40 @@ import PackageDescription
let package = Package(
name: "SerialDSL",
platforms: [
- .macOS(.v10_13),
+ .macOS(.v10_15),
.iOS(.v13),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "SerialDSL",
+ type: .dynamic,
targets: ["SerialDSL"]
- )
+ ),
+ .executable(name: "Serial", targets: ["Serial", "SerialDSL"])
],
dependencies: [
- // Dependencies declare other packages that this package depends on.
- // .package(url: /* package url */, from: "1.0.0"),
+ .package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.0"),
+// .package(url: "https://github.com/apple/swift-package-manager", branch: "main"),
+ .package(url: "https://github.com/apple/swift-tools-support-core.git", from: "0.2.7"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "SerialDSL",
- dependencies: []
+ dependencies: [],
+ swiftSettings: [
+ .unsafeFlags(["-enable-library-evolution"])
+ ]
+ ),
+ .executableTarget(
+ name: "Serial",
+ dependencies: [
+ "SerialDSL",
+ .product(name: "ArgumentParser", package: "swift-argument-parser"),
+ .product(name: "SwiftToolsSupport", package: "swift-tools-support-core"),
+ ]
),
.testTarget(
name: "SerialDSLTests",
diff --git a/Sources/Fixture.swift b/Sources/Fixture.swift
new file mode 100644
index 0000000..d6a954e
--- /dev/null
+++ b/Sources/Fixture.swift
@@ -0,0 +1,49 @@
+import SerialDSL
+import Foundation
+
+struct Record: SerialView {
+
+ let name: String
+ let age: Int
+
+ var body: some SerialView {
+ Object {
+ Member("name") {
+ name
+ }
+ Member("age") {
+ age
+ }
+ }
+ }
+
+}
+
+struct Results: SerialView {
+
+ let records: [Record]
+
+ var body: some SerialView {
+ Object {
+ Member("results") {
+ records
+ }
+ }
+ }
+
+}
+
+let results = Results(records: [
+ .init(name: "A", age: 1),
+ .init(name: "B", age: 2),
+])
+
+serialize {
+
+ SerialObject {
+ SerialMember("data") {
+ results
+ }
+ }
+
+}
diff --git a/Sources/Serial/CLI.swift b/Sources/Serial/CLI.swift
new file mode 100644
index 0000000..afdd47e
--- /dev/null
+++ b/Sources/Serial/CLI.swift
@@ -0,0 +1,365 @@
+import ArgumentParser
+import Foundation
+import TSCBasic
+import TSCUtility
+
+let RUNTIME_NAME = "SerialDSL"
+
+enum Log {
+
+ static func debug(
+ file: StaticString = #file,
+ line: UInt = #line,
+ _ log: OSLog,
+ _ object: @autoclosure () -> Any
+ ) {
+ print(object())
+ }
+
+ static func error(
+ file: StaticString = #file,
+ line: UInt = #line,
+ _ log: OSLog,
+ _ object: @autoclosure () -> Any
+ ) {
+ os_log(.info, log: log, "%{public}@\n%{public}@:%{public}@", "\(object())", "\(file)", "\(line.description)")
+ }
+
+}
+
+extension OSLog {
+
+ @inline(__always)
+ private static func makeOSLogInDebug(isEnabled: Bool = true, _ factory: () -> OSLog) -> OSLog {
+#if DEBUG
+ return factory()
+#else
+ return .disabled
+#endif
+ }
+
+ static let generic: OSLog = makeOSLogInDebug { OSLog.init(subsystem: "app.muukii", category: "generic") }
+}
+
+struct CLIError: Swift.Error, LocalizedError, Equatable {
+
+ var errorDescription: String?
+
+ static var fileNotFound: Self = .init(errorDescription: "File not found")
+ static var runtimeNotFound: Self = .init(errorDescription: "Runtime not found")
+
+}
+
+struct CLI: AsyncParsableCommand {
+
+ static var configuration: CommandConfiguration = .init(subcommands: [Gen.self])
+
+ struct Gen: AsyncParsableCommand {
+
+ struct DomainError: Swift.Error, LocalizedError, Equatable {
+
+ var errorDescription: String?
+
+ static var failedToCompile: Self = .init(errorDescription: "Failed to compile")
+ static var couldNotCreateOutputFile: Self = .init(errorDescription: "Could not create output file")
+ static var failureInMakingOutput: Self = .init(errorDescription: "Failure in makiing output")
+
+ }
+
+ @Argument var targetFilePath: String
+
+ mutating func run() async throws {
+
+ let filePath = localFileSystem.currentWorkingDirectory!.appending(
+ RelativePath(targetFilePath)
+ )
+
+ guard localFileSystem.exists(filePath) else {
+ throw CLIError.fileNotFound
+ }
+
+ let foundPath = try TSCBasic.Process.checkNonZeroExit(arguments: [
+ "/usr/bin/xcrun", "--find", "swiftc",
+ ]).spm_chomp()
+
+ let swiftc = try AbsolutePath(validating: foundPath)
+
+ let applicationPath = try Utils.hostBinDir(fileSystem: localFileSystem)
+
+ var runtimeFrameworksPath: AbsolutePath {
+
+ if localFileSystem.exists(applicationPath.appending(component: "lib\(RUNTIME_NAME).dylib")) {
+ return applicationPath
+ }
+
+ return applicationPath.appending(
+ components: "PackageFrameworks",
+ "\(RUNTIME_NAME).framework"
+ )
+ }
+
+ var libraryPath: AbsolutePath {
+ if runtimeFrameworksPath.extension == "framework" {
+ return runtimeFrameworksPath.appending(component: RUNTIME_NAME)
+ } else {
+ // note: this is not correct for all platforms, but we only actually use it on macOS.
+ return runtimeFrameworksPath.appending(component: "lib\(RUNTIME_NAME).dylib")
+ }
+ }
+
+// Log.debug(.generic, """
+//applicationPath: \(applicationPath)
+//runtimeFrameworksPath: \(runtimeFrameworksPath)
+//""")
+
+ guard localFileSystem.exists(libraryPath) else {
+ throw CLIError.runtimeNotFound
+ }
+
+ let target = try Utils.computeMinimumDeploymentTarget(
+ of: libraryPath
+ )
+
+ let sdkPath = try Utils.sdk()
+
+ var cmd: [String] = []
+ cmd += [swiftc.pathString]
+
+ if runtimeFrameworksPath.extension == "framework" {
+ cmd += [
+ "-F", runtimeFrameworksPath.parentDirectory.pathString,
+ "-framework", RUNTIME_NAME,
+ "-Xlinker", "-rpath", "-Xlinker", runtimeFrameworksPath.parentDirectory.pathString,
+ ]
+ } else {
+ cmd += [
+ "-L", runtimeFrameworksPath.pathString,
+ "-l\(RUNTIME_NAME)",
+ "-Xlinker", "-rpath", "-Xlinker", runtimeFrameworksPath.pathString
+ ]
+ }
+ cmd += ["-target", "arm64-apple-macosx\(target!.versionString)"]
+
+ cmd += ["-sdk", sdkPath.pathString]
+ cmd += Utils.flags()
+
+ cmd += [filePath.pathString]
+ cmd += [
+ "-Xfrontend", "-disable-implicit-concurrency-module-import",
+ "-Xfrontend", "-disable-implicit-string-processing-module-import",
+ "-I", applicationPath.pathString,
+ ]
+
+ try await withTemporaryDirectory { workingPath in
+
+ let compiledFile = workingPath.appending(component: "compiled")
+
+ // make a binary
+ do {
+
+ cmd += ["-o", compiledFile.pathString]
+
+ // compile
+ let result = try await TSCBasic.Process.popen(
+ arguments: cmd,
+ environment: ProcessInfo.processInfo.environment,
+ loggingHandler: { log in }
+ )
+
+ // Return now if there was an error.
+ if result.exitStatus != .terminated(code: 0) {
+ let output = try result.utf8stderrOutput()
+ Log.debug(.generic, "\(output)\n\(cmd.joined(separator: " "))")
+ throw DomainError.failedToCompile
+ }
+
+ }
+
+ // make an output
+ do {
+
+ let outputFile = workingPath.appending(component: "output")
+
+ guard let outputFileDesc = fopen(outputFile.pathString, "w") else {
+ throw DomainError.couldNotCreateOutputFile
+ }
+
+ var cmd: [String] = []
+
+ cmd += [compiledFile.pathString]
+
+ cmd += ["-fileno", "\(fileno(outputFileDesc))"]
+
+ let result = try await TSCBasic.Process.popen(arguments: cmd, environment: ProcessInfo.processInfo.environment, loggingHandler: { log in })
+
+ fclose(outputFileDesc)
+
+ // Return now if there was an error.
+ if result.exitStatus != .terminated(code: 0) {
+
+ let output = try result.utf8stderrOutput()
+ Log.debug(.generic, "\(output)\n\(cmd.joined(separator: " "))")
+
+ throw DomainError.failureInMakingOutput
+ }
+
+ let output: String = try localFileSystem.readFileContents(outputFile)
+
+ print(output)
+
+ }
+
+ }
+ }
+
+ }
+
+}
+
+func withTemporaryDirectory(_ work: @escaping (AbsolutePath) async throws -> Void) async throws {
+
+ try await withCheckedThrowingContinuation { continuation in
+
+ do {
+ try withTemporaryDirectory { path, completion -> Void in
+ Task {
+ do {
+ try await work(path)
+ completion(path)
+ continuation.resume()
+ } catch {
+ print(error)
+ // handle error
+ completion(path)
+ continuation.resume(throwing: error)
+ }
+ }
+ }
+ } catch {
+ continuation.resume(throwing: error)
+ }
+ }
+
+}
+
+extension TSCBasic.Process {
+
+ static public func popen(
+ arguments: [String],
+ environment: [String: String] = ProcessEnv.vars,
+ loggingHandler: LoggingHandler? = .none
+ ) async throws -> ProcessResult {
+
+ try await withCheckedThrowingContinuation { continuation in
+
+ self.popen(
+ arguments: arguments,
+ environment: environment,
+ loggingHandler: loggingHandler,
+ queue: nil
+ ) { result in
+
+ switch result {
+ case .success(let r):
+ continuation.resume(returning: r)
+ case .failure(let e):
+ continuation.resume(throwing: e)
+ }
+
+ }
+
+ }
+
+ }
+
+}
+
+public typealias EnvironmentVariables = [String: String]
+
+enum Utils {
+
+ static func computeMinimumDeploymentTarget(of binaryPath: AbsolutePath) throws -> PlatformVersion?
+ {
+
+ let platformName = "MACOS"
+
+ let runResult = try Process.popen(arguments: [
+ "/usr/bin/xcrun", "vtool", "-show-build", binaryPath.pathString,
+ ])
+ var lines = try runResult.utf8Output().components(separatedBy: "\n")
+ while !lines.isEmpty {
+ let first = lines.removeFirst()
+ if first.contains("platform \(platformName)"), let line = lines.first, line.contains("minos")
+ {
+ return line.components(separatedBy: " ").last.map(PlatformVersion.init(stringLiteral:))
+ }
+ }
+ return nil
+ }
+
+ static func flags() -> [String] {
+ // Compute common arguments for clang and swift.
+ var extraCCFlags: [String] = []
+ var extraSwiftCFlags: [String] = []
+
+ if let sdkPaths = sdkPlatformFrameworkPaths(environment: ProcessInfo.processInfo.environment) {
+ extraCCFlags += ["-F", sdkPaths.fwk.pathString]
+ extraSwiftCFlags += ["-F", sdkPaths.fwk.pathString]
+ extraSwiftCFlags += ["-I", sdkPaths.lib.pathString]
+ extraSwiftCFlags += ["-L", sdkPaths.lib.pathString]
+ }
+ return extraSwiftCFlags
+
+ }
+
+ static func sdkPlatformFrameworkPaths(
+ environment: EnvironmentVariables = ProcessInfo.processInfo.environment
+ ) -> (fwk: AbsolutePath, lib: AbsolutePath)? {
+
+ let platformPath = try? TSCBasic.Process.checkNonZeroExit(
+ arguments: ["/usr/bin/xcrun", "--sdk", "macosx", "--show-sdk-platform-path"],
+ environment: environment
+ ).spm_chomp()
+
+ if let platformPath = platformPath, !platformPath.isEmpty {
+ // For XCTest framework.
+ let fwk = AbsolutePath(platformPath).appending(
+ components: "Developer",
+ "Library",
+ "Frameworks"
+ )
+
+ // For XCTest Swift library.
+ let lib = AbsolutePath(platformPath).appending(
+ components: "Developer",
+ "usr",
+ "lib"
+ )
+
+ return (fwk, lib)
+ }
+ return nil
+ }
+
+ static func sdk() throws -> AbsolutePath {
+ let path = try TSCBasic.Process.checkNonZeroExit(
+ arguments: ["/usr/bin/xcrun", "--sdk", "macosx", "--show-sdk-path"],
+ environment: ProcessEnv.vars
+ ).spm_chomp()
+ return AbsolutePath(path)
+ }
+
+ static func hostBinDir(
+ fileSystem: FileSystem
+ ) throws -> AbsolutePath {
+
+ return try AbsolutePath(validating: (Bundle.main.executablePath! as NSString).resolvingSymlinksInPath).parentDirectory
+ }
+}
+
+@main
+enum Main {
+ static func main() async {
+ await CLI.main()
+ }
+}
diff --git a/Sources/Serial/FileSystem+.swift b/Sources/Serial/FileSystem+.swift
new file mode 100644
index 0000000..dc45f2d
--- /dev/null
+++ b/Sources/Serial/FileSystem+.swift
@@ -0,0 +1,24 @@
+import TSCBasic
+import Foundation
+
+extension FileSystem {
+ public func readFileContents(_ path: AbsolutePath) throws -> Data {
+ return try Data(self.readFileContents(path).contents)
+ }
+
+ public func readFileContents(_ path: AbsolutePath) throws -> String {
+ return try String(decoding: self.readFileContents(path), as: UTF8.self)
+ }
+
+ public func writeFileContents(_ path: AbsolutePath, data: Data) throws {
+ return try self.writeFileContents(path, bytes: .init(data))
+ }
+
+ public func writeFileContents(_ path: AbsolutePath, string: String) throws {
+ return try self.writeFileContents(path, bytes: .init(encodingAsUTF8: string))
+ }
+
+ public func writeFileContents(_ path: AbsolutePath, provider: () -> String) throws {
+ return try self.writeFileContents(path, string: provider())
+ }
+}
diff --git a/Sources/Serial/PlatformVersion.swift b/Sources/Serial/PlatformVersion.swift
new file mode 100644
index 0000000..91cdc55
--- /dev/null
+++ b/Sources/Serial/PlatformVersion.swift
@@ -0,0 +1,55 @@
+import TSCBasic
+import TSCUtility
+
+/// Represents a platform version.
+public struct PlatformVersion: Equatable, Hashable, Codable {
+
+ /// The unknown platform version.
+ public static let unknown: PlatformVersion = .init("0.0.0")
+
+ /// The underlying version storage.
+ private let version: Version
+
+ /// The string representation of the version.
+ public var versionString: String {
+ var str = "\(version.major).\(version.minor)"
+ if version.patch != 0 {
+ str += ".\(version.patch)"
+ }
+ return str
+ }
+
+ public var major: Int { version.major }
+ public var minor: Int { version.minor }
+ public var patch: Int { version.patch }
+
+ /// Create a platform version given a string.
+ ///
+ /// The platform version is expected to be in format: X.X.X
+ public init(_ version: String) {
+ let components = version.split(separator: ".").compactMap({ Int($0) })
+ assert(!components.isEmpty && components.count <= 3, version)
+ switch components.count {
+ case 1:
+ self.version = Version(components[0], 0, 0)
+ case 2:
+ self.version = Version(components[0], components[1], 0)
+ case 3:
+ self.version = Version(components[0], components[1], components[2])
+ default:
+ fatalError("Unexpected number of components \(components)")
+ }
+ }
+}
+
+extension PlatformVersion: Comparable {
+ public static func < (lhs: PlatformVersion, rhs: PlatformVersion) -> Bool {
+ return lhs.version < rhs.version
+ }
+}
+
+extension PlatformVersion: ExpressibleByStringLiteral {
+ public init(stringLiteral value: String) {
+ self.init(value)
+ }
+}
diff --git a/Sources/SerialDSL/Entrypoint.swift b/Sources/SerialDSL/Entrypoint.swift
new file mode 100644
index 0000000..d267f0b
--- /dev/null
+++ b/Sources/SerialDSL/Entrypoint.swift
@@ -0,0 +1,21 @@
+@_implementationOnly import Darwin.C
+
+public func serialize(@ValueBuilder _ thunk: () -> some SerialView) {
+
+ let value = thunk()
+
+ let text = value.renderJSON()
+
+ if let optIdx = CommandLine.arguments.firstIndex(of: "-fileno") {
+
+ if let outputFileDesc = Int32(CommandLine.arguments[optIdx + 1]) {
+ guard let fd = fdopen(outputFileDesc, "w") else {
+ return
+ }
+ fputs(text, fd)
+ fclose(fd)
+ }
+ } else {
+ print(text)
+ }
+}