refactor: split utilities
This commit is contained in:
17
apps/macos/Sources/Clawdis/AgeFormatting.swift
Normal file
17
apps/macos/Sources/Clawdis/AgeFormatting.swift
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
// Human-friendly age string (e.g., "2m ago").
|
||||||
|
func age(from date: Date, now: Date = .init()) -> String {
|
||||||
|
let seconds = max(0, Int(now.timeIntervalSince(date)))
|
||||||
|
let minutes = seconds / 60
|
||||||
|
let hours = minutes / 60
|
||||||
|
let days = hours / 24
|
||||||
|
|
||||||
|
if seconds < 60 { return "just now" }
|
||||||
|
if minutes == 1 { return "1 minute ago" }
|
||||||
|
if minutes < 60 { return "\(minutes)m ago" }
|
||||||
|
if hours == 1 { return "1 hour ago" }
|
||||||
|
if hours < 24 { return "\(hours)h ago" }
|
||||||
|
if days == 1 { return "yesterday" }
|
||||||
|
return "\(days)d ago"
|
||||||
|
}
|
||||||
102
apps/macos/Sources/Clawdis/CLIInstaller.swift
Normal file
102
apps/macos/Sources/Clawdis/CLIInstaller.swift
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
enum CLIInstaller {
|
||||||
|
private static func embeddedHelperURL() -> URL {
|
||||||
|
Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/Relay/clawdis")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func installedLocation() -> String? {
|
||||||
|
self.installedLocation(
|
||||||
|
searchPaths: cliHelperSearchPaths,
|
||||||
|
embeddedHelper: self.embeddedHelperURL(),
|
||||||
|
fileManager: .default)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func installedLocation(
|
||||||
|
searchPaths: [String],
|
||||||
|
embeddedHelper: URL,
|
||||||
|
fileManager: FileManager) -> String?
|
||||||
|
{
|
||||||
|
let embedded = embeddedHelper.resolvingSymlinksInPath()
|
||||||
|
|
||||||
|
for basePath in searchPaths {
|
||||||
|
let candidate = URL(fileURLWithPath: basePath).appendingPathComponent("clawdis").path
|
||||||
|
var isDirectory: ObjCBool = false
|
||||||
|
|
||||||
|
guard fileManager.fileExists(atPath: candidate, isDirectory: &isDirectory),
|
||||||
|
!isDirectory.boolValue
|
||||||
|
else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
guard fileManager.isExecutableFile(atPath: candidate) else { continue }
|
||||||
|
|
||||||
|
let resolved = URL(fileURLWithPath: candidate).resolvingSymlinksInPath()
|
||||||
|
if resolved == embedded {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
static func isInstalled() -> Bool {
|
||||||
|
self.installedLocation() != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
static func install(statusHandler: @escaping @Sendable (String) async -> Void) async {
|
||||||
|
let helper = self.embeddedHelperURL()
|
||||||
|
guard FileManager.default.isExecutableFile(atPath: helper.path) else {
|
||||||
|
await statusHandler(
|
||||||
|
"Embedded CLI missing in bundle; repackage via scripts/package-mac-app.sh " +
|
||||||
|
"(or restart-mac.sh without SKIP_GATEWAY_PACKAGE=1).")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let targets = cliHelperSearchPaths.map { "\($0)/clawdis" }
|
||||||
|
let result = await self.privilegedSymlink(source: helper.path, targets: targets)
|
||||||
|
await statusHandler(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func privilegedSymlink(source: String, targets: [String]) async -> String {
|
||||||
|
let escapedSource = self.shellEscape(source)
|
||||||
|
let targetList = targets.map(self.shellEscape).joined(separator: " ")
|
||||||
|
let cmds = [
|
||||||
|
"mkdir -p /usr/local/bin /opt/homebrew/bin",
|
||||||
|
targets.map { "ln -sf \(escapedSource) \($0)" }.joined(separator: "; "),
|
||||||
|
].joined(separator: "; ")
|
||||||
|
|
||||||
|
let script = """
|
||||||
|
do shell script "\(cmds)" with administrator privileges
|
||||||
|
"""
|
||||||
|
|
||||||
|
let proc = Process()
|
||||||
|
proc.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
||||||
|
proc.arguments = ["-e", script]
|
||||||
|
|
||||||
|
let pipe = Pipe()
|
||||||
|
proc.standardOutput = pipe
|
||||||
|
proc.standardError = pipe
|
||||||
|
|
||||||
|
do {
|
||||||
|
try proc.run()
|
||||||
|
proc.waitUntilExit()
|
||||||
|
let data = pipe.fileHandleForReading.readToEndSafely()
|
||||||
|
let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
|
if proc.terminationStatus == 0 {
|
||||||
|
return output.isEmpty ? "CLI helper linked into \(targetList)" : output
|
||||||
|
}
|
||||||
|
if output.lowercased().contains("user canceled") {
|
||||||
|
return "Install canceled"
|
||||||
|
}
|
||||||
|
return "Failed to install CLI helper: \(output)"
|
||||||
|
} catch {
|
||||||
|
return "Failed to run installer: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func shellEscape(_ path: String) -> String {
|
||||||
|
"'" + path.replacingOccurrences(of: "'", with: "'\"'\"'") + "'"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,237 +1,5 @@
|
|||||||
import AppKit
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension ProcessInfo {
|
|
||||||
var isPreview: Bool {
|
|
||||||
self.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
var isRunningTests: Bool {
|
|
||||||
// SwiftPM tests load one or more `.xctest` bundles. With Swift Testing, `Bundle.main` is not
|
|
||||||
// guaranteed to be the `.xctest` bundle, so check all loaded bundles.
|
|
||||||
if Bundle.allBundles.contains(where: { $0.bundleURL.pathExtension == "xctest" }) { return true }
|
|
||||||
if Bundle.main.bundleURL.pathExtension == "xctest" { return true }
|
|
||||||
|
|
||||||
// Backwards-compatible fallbacks for runners that still set XCTest env vars.
|
|
||||||
return self.environment["XCTestConfigurationFilePath"] != nil
|
|
||||||
|| self.environment["XCTestBundlePath"] != nil
|
|
||||||
|| self.environment["XCTestSessionIdentifier"] != nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum LaunchdManager {
|
|
||||||
private static func runLaunchctl(_ args: [String]) {
|
|
||||||
let process = Process()
|
|
||||||
process.launchPath = "/bin/launchctl"
|
|
||||||
process.arguments = args
|
|
||||||
try? process.run()
|
|
||||||
}
|
|
||||||
|
|
||||||
static func startClawdis() {
|
|
||||||
let userTarget = "gui/\(getuid())/\(launchdLabel)"
|
|
||||||
self.runLaunchctl(["kickstart", "-k", userTarget])
|
|
||||||
}
|
|
||||||
|
|
||||||
static func stopClawdis() {
|
|
||||||
let userTarget = "gui/\(getuid())/\(launchdLabel)"
|
|
||||||
self.runLaunchctl(["stop", userTarget])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum LaunchAgentManager {
|
|
||||||
private static var plistURL: URL {
|
|
||||||
FileManager.default.homeDirectoryForCurrentUser
|
|
||||||
.appendingPathComponent("Library/LaunchAgents/com.steipete.clawdis.plist")
|
|
||||||
}
|
|
||||||
|
|
||||||
static func status() async -> Bool {
|
|
||||||
guard FileManager.default.fileExists(atPath: self.plistURL.path) else { return false }
|
|
||||||
let result = await self.runLaunchctl(["print", "gui/\(getuid())/\(launchdLabel)"])
|
|
||||||
return result == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
static func set(enabled: Bool, bundlePath: String) async {
|
|
||||||
if enabled {
|
|
||||||
self.writePlist(bundlePath: bundlePath)
|
|
||||||
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"])
|
|
||||||
_ = await self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path])
|
|
||||||
_ = await self.runLaunchctl(["kickstart", "-k", "gui/\(getuid())/\(launchdLabel)"])
|
|
||||||
} else {
|
|
||||||
// Disable autostart going forward but leave the current app running.
|
|
||||||
// bootout would terminate the launchd job immediately (and crash the app if launched via agent).
|
|
||||||
try? FileManager.default.removeItem(at: self.plistURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func writePlist(bundlePath: String) {
|
|
||||||
let plist = """
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>Label</key>
|
|
||||||
<string>com.steipete.clawdis</string>
|
|
||||||
<key>ProgramArguments</key>
|
|
||||||
<array>
|
|
||||||
<string>\(bundlePath)/Contents/MacOS/Clawdis</string>
|
|
||||||
</array>
|
|
||||||
<key>WorkingDirectory</key>
|
|
||||||
<string>\(FileManager.default.homeDirectoryForCurrentUser.path)</string>
|
|
||||||
<key>RunAtLoad</key>
|
|
||||||
<true/>
|
|
||||||
<key>KeepAlive</key>
|
|
||||||
<true/>
|
|
||||||
<key>EnvironmentVariables</key>
|
|
||||||
<dict>
|
|
||||||
<key>PATH</key>
|
|
||||||
<string>\(CommandResolver.preferredPaths().joined(separator: ":"))</string>
|
|
||||||
</dict>
|
|
||||||
<key>StandardOutPath</key>
|
|
||||||
<string>\(LogLocator.launchdLogPath)</string>
|
|
||||||
<key>StandardErrorPath</key>
|
|
||||||
<string>\(LogLocator.launchdLogPath)</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
"""
|
|
||||||
try? plist.write(to: self.plistURL, atomically: true, encoding: .utf8)
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
private static func runLaunchctl(_ args: [String]) async -> Int32 {
|
|
||||||
await Task.detached(priority: .utility) { () -> Int32 in
|
|
||||||
let process = Process()
|
|
||||||
process.launchPath = "/bin/launchctl"
|
|
||||||
process.arguments = args
|
|
||||||
process.standardOutput = Pipe()
|
|
||||||
process.standardError = Pipe()
|
|
||||||
do {
|
|
||||||
try process.run()
|
|
||||||
process.waitUntilExit()
|
|
||||||
return process.terminationStatus
|
|
||||||
} catch {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
}.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Human-friendly age string (e.g., "2m ago").
|
|
||||||
func age(from date: Date, now: Date = .init()) -> String {
|
|
||||||
let seconds = max(0, Int(now.timeIntervalSince(date)))
|
|
||||||
let minutes = seconds / 60
|
|
||||||
let hours = minutes / 60
|
|
||||||
let days = hours / 24
|
|
||||||
|
|
||||||
if seconds < 60 { return "just now" }
|
|
||||||
if minutes == 1 { return "1 minute ago" }
|
|
||||||
if minutes < 60 { return "\(minutes)m ago" }
|
|
||||||
if hours == 1 { return "1 hour ago" }
|
|
||||||
if hours < 24 { return "\(hours)h ago" }
|
|
||||||
if days == 1 { return "yesterday" }
|
|
||||||
return "\(days)d ago"
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
enum CLIInstaller {
|
|
||||||
private static func embeddedHelperURL() -> URL {
|
|
||||||
Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/Relay/clawdis")
|
|
||||||
}
|
|
||||||
|
|
||||||
static func installedLocation() -> String? {
|
|
||||||
self.installedLocation(
|
|
||||||
searchPaths: cliHelperSearchPaths,
|
|
||||||
embeddedHelper: self.embeddedHelperURL(),
|
|
||||||
fileManager: .default)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func installedLocation(
|
|
||||||
searchPaths: [String],
|
|
||||||
embeddedHelper: URL,
|
|
||||||
fileManager: FileManager) -> String?
|
|
||||||
{
|
|
||||||
let embedded = embeddedHelper.resolvingSymlinksInPath()
|
|
||||||
|
|
||||||
for basePath in searchPaths {
|
|
||||||
let candidate = URL(fileURLWithPath: basePath).appendingPathComponent("clawdis").path
|
|
||||||
var isDirectory: ObjCBool = false
|
|
||||||
|
|
||||||
guard fileManager.fileExists(atPath: candidate, isDirectory: &isDirectory),
|
|
||||||
!isDirectory.boolValue
|
|
||||||
else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
guard fileManager.isExecutableFile(atPath: candidate) else { continue }
|
|
||||||
|
|
||||||
let resolved = URL(fileURLWithPath: candidate).resolvingSymlinksInPath()
|
|
||||||
if resolved == embedded {
|
|
||||||
return candidate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
static func isInstalled() -> Bool {
|
|
||||||
self.installedLocation() != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
static func install(statusHandler: @escaping @Sendable (String) async -> Void) async {
|
|
||||||
let helper = self.embeddedHelperURL()
|
|
||||||
guard FileManager.default.isExecutableFile(atPath: helper.path) else {
|
|
||||||
await statusHandler(
|
|
||||||
"Embedded CLI missing in bundle; repackage via scripts/package-mac-app.sh " +
|
|
||||||
"(or restart-mac.sh without SKIP_GATEWAY_PACKAGE=1).")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let targets = cliHelperSearchPaths.map { "\($0)/clawdis" }
|
|
||||||
let result = await self.privilegedSymlink(source: helper.path, targets: targets)
|
|
||||||
await statusHandler(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func privilegedSymlink(source: String, targets: [String]) async -> String {
|
|
||||||
let escapedSource = self.shellEscape(source)
|
|
||||||
let targetList = targets.map(self.shellEscape).joined(separator: " ")
|
|
||||||
let cmds = [
|
|
||||||
"mkdir -p /usr/local/bin /opt/homebrew/bin",
|
|
||||||
targets.map { "ln -sf \(escapedSource) \($0)" }.joined(separator: "; "),
|
|
||||||
].joined(separator: "; ")
|
|
||||||
|
|
||||||
let script = """
|
|
||||||
do shell script "\(cmds)" with administrator privileges
|
|
||||||
"""
|
|
||||||
|
|
||||||
let proc = Process()
|
|
||||||
proc.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
|
||||||
proc.arguments = ["-e", script]
|
|
||||||
|
|
||||||
let pipe = Pipe()
|
|
||||||
proc.standardOutput = pipe
|
|
||||||
proc.standardError = pipe
|
|
||||||
|
|
||||||
do {
|
|
||||||
try proc.run()
|
|
||||||
proc.waitUntilExit()
|
|
||||||
let data = pipe.fileHandleForReading.readToEndSafely()
|
|
||||||
let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
||||||
if proc.terminationStatus == 0 {
|
|
||||||
return output.isEmpty ? "CLI helper linked into \(targetList)" : output
|
|
||||||
}
|
|
||||||
if output.lowercased().contains("user canceled") {
|
|
||||||
return "Install canceled"
|
|
||||||
}
|
|
||||||
return "Failed to install CLI helper: \(output)"
|
|
||||||
} catch {
|
|
||||||
return "Failed to run installer: \(error.localizedDescription)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func shellEscape(_ path: String) -> String {
|
|
||||||
"'" + path.replacingOccurrences(of: "'", with: "'\"'\"'") + "'"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum CommandResolver {
|
enum CommandResolver {
|
||||||
private static let projectRootDefaultsKey = "clawdis.gatewayProjectRootPath"
|
private static let projectRootDefaultsKey = "clawdis.gatewayProjectRootPath"
|
||||||
private static let helperName = "clawdis"
|
private static let helperName = "clawdis"
|
||||||
78
apps/macos/Sources/Clawdis/LaunchAgentManager.swift
Normal file
78
apps/macos/Sources/Clawdis/LaunchAgentManager.swift
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum LaunchAgentManager {
|
||||||
|
private static var plistURL: URL {
|
||||||
|
FileManager.default.homeDirectoryForCurrentUser
|
||||||
|
.appendingPathComponent("Library/LaunchAgents/com.steipete.clawdis.plist")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func status() async -> Bool {
|
||||||
|
guard FileManager.default.fileExists(atPath: self.plistURL.path) else { return false }
|
||||||
|
let result = await self.runLaunchctl(["print", "gui/\(getuid())/\(launchdLabel)"])
|
||||||
|
return result == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
static func set(enabled: Bool, bundlePath: String) async {
|
||||||
|
if enabled {
|
||||||
|
self.writePlist(bundlePath: bundlePath)
|
||||||
|
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"])
|
||||||
|
_ = await self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path])
|
||||||
|
_ = await self.runLaunchctl(["kickstart", "-k", "gui/\(getuid())/\(launchdLabel)"])
|
||||||
|
} else {
|
||||||
|
// Disable autostart going forward but leave the current app running.
|
||||||
|
// bootout would terminate the launchd job immediately (and crash the app if launched via agent).
|
||||||
|
try? FileManager.default.removeItem(at: self.plistURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func writePlist(bundlePath: String) {
|
||||||
|
let plist = """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.steipete.clawdis</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>\(bundlePath)/Contents/MacOS/Clawdis</string>
|
||||||
|
</array>
|
||||||
|
<key>WorkingDirectory</key>
|
||||||
|
<string>\(FileManager.default.homeDirectoryForCurrentUser.path)</string>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<key>EnvironmentVariables</key>
|
||||||
|
<dict>
|
||||||
|
<key>PATH</key>
|
||||||
|
<string>\(CommandResolver.preferredPaths().joined(separator: ":"))</string>
|
||||||
|
</dict>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>\(LogLocator.launchdLogPath)</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>\(LogLocator.launchdLogPath)</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
"""
|
||||||
|
try? plist.write(to: self.plistURL, atomically: true, encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
private static func runLaunchctl(_ args: [String]) async -> Int32 {
|
||||||
|
await Task.detached(priority: .utility) { () -> Int32 in
|
||||||
|
let process = Process()
|
||||||
|
process.launchPath = "/bin/launchctl"
|
||||||
|
process.arguments = args
|
||||||
|
process.standardOutput = Pipe()
|
||||||
|
process.standardError = Pipe()
|
||||||
|
do {
|
||||||
|
try process.run()
|
||||||
|
process.waitUntilExit()
|
||||||
|
return process.terminationStatus
|
||||||
|
} catch {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}.value
|
||||||
|
}
|
||||||
|
}
|
||||||
20
apps/macos/Sources/Clawdis/LaunchdManager.swift
Normal file
20
apps/macos/Sources/Clawdis/LaunchdManager.swift
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum LaunchdManager {
|
||||||
|
private static func runLaunchctl(_ args: [String]) {
|
||||||
|
let process = Process()
|
||||||
|
process.launchPath = "/bin/launchctl"
|
||||||
|
process.arguments = args
|
||||||
|
try? process.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func startClawdis() {
|
||||||
|
let userTarget = "gui/\(getuid())/\(launchdLabel)"
|
||||||
|
self.runLaunchctl(["kickstart", "-k", userTarget])
|
||||||
|
}
|
||||||
|
|
||||||
|
static func stopClawdis() {
|
||||||
|
let userTarget = "gui/\(getuid())/\(launchdLabel)"
|
||||||
|
self.runLaunchctl(["stop", userTarget])
|
||||||
|
}
|
||||||
|
}
|
||||||
19
apps/macos/Sources/Clawdis/ProcessInfo+Clawdis.swift
Normal file
19
apps/macos/Sources/Clawdis/ProcessInfo+Clawdis.swift
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension ProcessInfo {
|
||||||
|
var isPreview: Bool {
|
||||||
|
self.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
var isRunningTests: Bool {
|
||||||
|
// SwiftPM tests load one or more `.xctest` bundles. With Swift Testing, `Bundle.main` is not
|
||||||
|
// guaranteed to be the `.xctest` bundle, so check all loaded bundles.
|
||||||
|
if Bundle.allBundles.contains(where: { $0.bundleURL.pathExtension == "xctest" }) { return true }
|
||||||
|
if Bundle.main.bundleURL.pathExtension == "xctest" { return true }
|
||||||
|
|
||||||
|
// Backwards-compatible fallbacks for runners that still set XCTest env vars.
|
||||||
|
return self.environment["XCTestConfigurationFilePath"] != nil
|
||||||
|
|| self.environment["XCTestBundlePath"] != nil
|
||||||
|
|| self.environment["XCTestSessionIdentifier"] != nil
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user