diff --git a/.github/actions/uitests/action.yml b/.github/actions/uitests/action.yml index f1f22ee00..595d3f3fc 100644 --- a/.github/actions/uitests/action.yml +++ b/.github/actions/uitests/action.yml @@ -55,6 +55,14 @@ runs: run: Scripts/CI/darwin_setup_build.sh shell: bash + - name: Disable LookInside & LookIn servers + shell: bash + run: | + { + echo "OPENSWIFTUI_EXAMPLE_LOOKINSIDE_SERVER=0" + echo "OPENSWIFTUI_EXAMPLE_LOOKIN_SERVER=0" + } >> "$GITHUB_ENV" + - name: Generate Example project shell: bash run: | diff --git a/Example/Project.swift b/Example/Project.swift index a3faac921..3841c8426 100644 --- a/Example/Project.swift +++ b/Example/Project.swift @@ -1,7 +1,13 @@ import ProjectDescription +import ProjectDescriptionHelpers // MARK: - Constants +configureOpenSwiftUIEnvironment() +let exampleServerConfiguration = OpenSwiftUIExampleServerConfiguration.resolve() +let enableLookInsideServer = exampleServerConfiguration.enableLookInsideServer +let enableLookInServer = exampleServerConfiguration.enableLookInServer + let destinations: Destinations = [.iPhone, .iPad, .mac, .appleVision] let deploymentTargets = DeploymentTargets.multiplatform( @@ -50,12 +56,18 @@ let openSwiftUIDebugModeSettings = openSwiftUIModeSettings.merging([ "SWIFT_OPTIMIZATION_LEVEL": "-Onone", ]) +let inspectUIServerReleaseExclusionSettings: SettingsDictionary = (enableLookInsideServer || enableLookInServer) + ? [ + "EXCLUDED_SOURCE_FILE_NAMES": "$(inherited) LookInsideServer* LookinServer*", + ] + : [:] + func targetConfigurations(_ xcconfig: Path) -> [Configuration] { [ .debug(name: swiftUIDebug, settings: swiftUIModeSettings.merging(debugModeSettings), xcconfig: xcconfig), - .release(name: swiftUIRelease, settings: swiftUIModeSettings, xcconfig: xcconfig), + .release(name: swiftUIRelease, settings: swiftUIModeSettings.merging(inspectUIServerReleaseExclusionSettings), xcconfig: xcconfig), .debug(name: openSwiftUIDebug, settings: openSwiftUIDebugModeSettings, xcconfig: xcconfig), - .release(name: openSwiftUIRelease, settings: openSwiftUIModeSettings, xcconfig: xcconfig), + .release(name: openSwiftUIRelease, settings: openSwiftUIModeSettings.merging(inspectUIServerReleaseExclusionSettings), xcconfig: xcconfig), ] } @@ -184,10 +196,23 @@ let privateFrameworkDependencies: [TargetDependency] = [ .external(name: "BacklightServices", condition: .when([.ios, .visionos])), ] +let debugServerDependencies: [TargetDependency] +if enableLookInsideServer { + debugServerDependencies = [ + .external(name: "LookInsideServer", condition: .when([.ios, .macos])), + ] +} else if enableLookInServer { + debugServerDependencies = [ + .external(name: "LookinServer", condition: .when([.ios])), + ] +} else { + debugServerDependencies = [] +} + let appDependencies: [TargetDependency] = [ .external(name: "OpenSwiftUI"), .external(name: "Equatable"), -] + privateFrameworkDependencies +] + privateFrameworkDependencies + debugServerDependencies let testArguments = Arguments.arguments( environmentVariables: [ diff --git a/Example/README.md b/Example/README.md index b3bec6b53..dcfc6ffc3 100644 --- a/Example/README.md +++ b/Example/README.md @@ -31,6 +31,24 @@ mise exec -- tuist install mise exec -- tuist generate --no-open ``` +By default, the generated Debug Example targets include LookInsideServer. You can switch the debug inspector server before running `tuist install` and `tuist generate`: + +```shell +# Default +export OPENSWIFTUI_EXAMPLE_LOOKINSIDE_SERVER=1 +export OPENSWIFTUI_EXAMPLE_LOOKIN_SERVER=0 + +# Use Lookin instead +export OPENSWIFTUI_EXAMPLE_LOOKINSIDE_SERVER=0 +export OPENSWIFTUI_EXAMPLE_LOOKIN_SERVER=1 + +# Disable both +export OPENSWIFTUI_EXAMPLE_LOOKINSIDE_SERVER=0 +export OPENSWIFTUI_EXAMPLE_LOOKIN_SERVER=0 +``` + +Do not enable both server variables at the same time. + ## Example A OpenSwiftUI/SwiftUI `App` lifecycle example. diff --git a/Example/Tuist/Package.resolved b/Example/Tuist/Package.resolved index b2741d55c..96d7f8fc3 100644 --- a/Example/Tuist/Package.resolved +++ b/Example/Tuist/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "154116076f9106391f665916f636f6142e20264d7bdbff93d21cf58c63c1c2fc", + "originHash" : "7ca1a1e556159cedef2e6b4648b27086aa27dbdb32eb4de468b034ec81c5b69f", "pins" : [ { "identity" : "equatable", diff --git a/Example/Tuist/Package.swift b/Example/Tuist/Package.swift index ac4bbdabe..af026f0cf 100644 --- a/Example/Tuist/Package.swift +++ b/Example/Tuist/Package.swift @@ -2,20 +2,50 @@ import PackageDescription -let package = Package( +#if TUIST +import ProjectDescriptionHelpers + +public struct PackageContextEnvironmentProvider: EnvironmentProvider { + public init() {} + + public func value(forKey key: String) -> String? { + Context.environment[key] + } +} + +configureOpenSwiftUIEnvironment(provider: PackageContextEnvironmentProvider()) +let exampleServerConfiguration = OpenSwiftUIExampleServerConfiguration.resolve() +let enableLookInsideServer = exampleServerConfiguration.enableLookInsideServer +let enableLookInServer = exampleServerConfiguration.enableLookInServer +#else +let enableLookInsideServer = false +let enableLookInServer = false +#endif + +var dependencies: [PackageDescription.Package.Dependency] = [ + .package(path: "../../"), + .package(path: "../../../OpenAttributeGraph"), + .package(path: "../../../OpenRenderBox"), + .package(path: "../../../DarwinPrivateFrameworks"), + .package(url: "https://github.com/apple/swift-collections", from: "1.1.0"), + .package(url: "https://github.com/apple/swift-numerics", from: "1.0.3"), + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "601.0.0"), + .package(url: "https://github.com/OpenSwiftUIProject/equatable.git", branch: "main"), + .package(url: "https://github.com/OpenSwiftUIProject/SymbolLocator.git", from: "0.2.0"), + .package(url: "https://github.com/OpenSwiftUIProject/swift-snapshot-testing", exact: "1.18.9-osui"), +] + +if enableLookInsideServer { + dependencies.append(.package(url: "https://github.com/LookInsideApp/LookInside-Release.git", from: "0.2.2")) +} + +if enableLookInServer { + dependencies.append(.package(url: "https://github.com/QMUI/LookinServer.git", from: "1.2.8")) +} + +let package = PackageDescription.Package( name: "ExampleDependencies", - dependencies: [ - .package(path: "../../"), - .package(path: "../../../OpenAttributeGraph"), - .package(path: "../../../OpenRenderBox"), - .package(path: "../../../DarwinPrivateFrameworks"), - .package(url: "https://github.com/apple/swift-collections", from: "1.1.0"), - .package(url: "https://github.com/apple/swift-numerics", from: "1.0.3"), - .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "601.0.0"), - .package(url: "https://github.com/OpenSwiftUIProject/equatable.git", branch: "main"), - .package(url: "https://github.com/OpenSwiftUIProject/SymbolLocator.git", from: "0.2.0"), - .package(url: "https://github.com/OpenSwiftUIProject/swift-snapshot-testing", exact: "1.18.9-osui"), - ] + dependencies: dependencies ) #if TUIST @@ -40,33 +70,47 @@ let openSwiftUITargetSettings: SettingsDictionary = [ "DYLIB_INSTALL_NAME_BASE": "@rpath", ] +var packageProductTypes: [String: ProjectDescription.Product] = [ + "OpenSwiftUI": ProjectDescription.Product.framework, + "OpenSwiftUICore": ProjectDescription.Product.staticFramework, + "OpenSwiftUI_SPI": ProjectDescription.Product.staticFramework, + "COpenSwiftUI": ProjectDescription.Product.staticFramework, + "OpenSwiftUIMacros": ProjectDescription.Product.macro, + "OpenSwiftUITestsSupport": ProjectDescription.Product.staticFramework, + "OpenSwiftUISymbolDualTestsSupport": ProjectDescription.Product.staticFramework, + "OpenAttributeGraphShims": ProjectDescription.Product.staticFramework, + "OpenCoreGraphicsShims": ProjectDescription.Product.staticFramework, + "OpenObservation": ProjectDescription.Product.staticFramework, + "OpenQuartzCoreShims": ProjectDescription.Product.staticFramework, + "OpenRenderBoxShims": ProjectDescription.Product.staticFramework, + "SymbolLocator": ProjectDescription.Product.staticFramework, +] + +var packageProductDestinations: [String: Destinations] = [ + "OpenSwiftUI": examplePackageDestinations, + "OpenSwiftUICore": examplePackageDestinations, + "OpenSwiftUI_SPI": examplePackageDestinations, + "OpenSwiftUIExtension": examplePackageDestinations, + "OpenSwiftUIBridge": examplePackageDestinations, + "OpenAttributeGraph": examplePackageDestinations, + "OpenAttributeGraphShims": examplePackageDestinations, + "OpenRenderBox": examplePackageDestinations, + "OpenRenderBoxShims": examplePackageDestinations, +] + +if enableLookInsideServer { + packageProductTypes["LookInsideServer"] = ProjectDescription.Product.framework + packageProductDestinations["LookInsideServer"] = [.iPhone, .iPad, .mac] +} + +if enableLookInServer { + packageProductTypes["LookinServer"] = ProjectDescription.Product.framework + packageProductDestinations["LookinServer"] = [.iPhone, .iPad] +} + let packageSettings = PackageSettings( - productTypes: [ - "OpenSwiftUI": ProjectDescription.Product.framework, - "OpenSwiftUICore": ProjectDescription.Product.staticFramework, - "OpenSwiftUI_SPI": ProjectDescription.Product.staticFramework, - "COpenSwiftUI": ProjectDescription.Product.staticFramework, - "OpenSwiftUIMacros": ProjectDescription.Product.macro, - "OpenSwiftUITestsSupport": ProjectDescription.Product.staticFramework, - "OpenSwiftUISymbolDualTestsSupport": ProjectDescription.Product.staticFramework, - "OpenAttributeGraphShims": ProjectDescription.Product.staticFramework, - "OpenCoreGraphicsShims": ProjectDescription.Product.staticFramework, - "OpenObservation": ProjectDescription.Product.staticFramework, - "OpenQuartzCoreShims": ProjectDescription.Product.staticFramework, - "OpenRenderBoxShims": ProjectDescription.Product.staticFramework, - "SymbolLocator": ProjectDescription.Product.staticFramework, - ], - productDestinations: [ - "OpenSwiftUI": examplePackageDestinations, - "OpenSwiftUICore": examplePackageDestinations, - "OpenSwiftUI_SPI": examplePackageDestinations, - "OpenSwiftUIExtension": examplePackageDestinations, - "OpenSwiftUIBridge": examplePackageDestinations, - "OpenAttributeGraph": examplePackageDestinations, - "OpenAttributeGraphShims": examplePackageDestinations, - "OpenRenderBox": examplePackageDestinations, - "OpenRenderBoxShims": examplePackageDestinations, - ], + productTypes: packageProductTypes, + productDestinations: packageProductDestinations, baseSettings: .settings( configurations: openSwiftUIPackageConfigurations, defaultSettings: .none, diff --git a/Example/Tuist/ProjectDescriptionHelpers/EnvManager.swift b/Example/Tuist/ProjectDescriptionHelpers/EnvManager.swift new file mode 100644 index 000000000..673a32208 --- /dev/null +++ b/Example/Tuist/ProjectDescriptionHelpers/EnvManager.swift @@ -0,0 +1,136 @@ +import Foundation + +// MARK: - EnvManager + +public protocol EnvironmentProvider { + func value(forKey key: String) -> String? +} + +public struct ProcessInfoEnvironmentProvider: EnvironmentProvider { + public init() {} + + public func value(forKey key: String) -> String? { + ProcessInfo.processInfo.environment[key] + } +} + +public final class EnvManager { + nonisolated(unsafe) public static let shared = EnvManager() + + private var domains: [String] = [] + private var environmentProvider: EnvironmentProvider + + /// When true, append raw key as fallback when searching in domains + public var includeFallbackToRawKey: Bool = false + + private init() { + self.environmentProvider = ProcessInfoEnvironmentProvider() + } + + /// Set a custom environment provider (useful for testing) + public func setEnvironmentProvider(_ provider: EnvironmentProvider) { + self.environmentProvider = provider + } + + /// Reset domains and environment provider (useful for testing) + public func reset() { + domains.removeAll() + includeFallbackToRawKey = false + self.environmentProvider = ProcessInfoEnvironmentProvider() + } + + public func register(domain: String) { + domains.append(domain) + } + + public func withDomain(_ domain: String, perform: () throws -> T) rethrows -> T { + domains.append(domain) + defer { domains.removeAll { $0 == domain } } + return try perform() + } + + private func envValue(rawKey: String, default defaultValue: T?, searchInDomain: Bool, parser: (String) -> T?) -> T? { + func parseEnvValue(_ key: String) -> (String, T)? { + guard let value = environmentProvider.value(forKey: key), + let result = parser(value) else { return nil } + return (value, result) + } + var keys: [String] = searchInDomain ? domains.map { "\($0.uppercased())_\(rawKey)" } : [] + if !searchInDomain || includeFallbackToRawKey { + keys.append(rawKey) + } + for key in keys { + if let (value, result) = parseEnvValue(key) { + print("[Env] \(key)=\(value) -> \(result)") + return result + } + } + let primaryKey = keys.first ?? rawKey + if let defaultValue { + print("[Env] \(primaryKey) not set -> \(defaultValue)(default)") + } + return defaultValue + } + + public func envBoolValue(rawKey: String, default defaultValue: Bool? = nil, searchInDomain: Bool) -> Bool? { + envValue(rawKey: rawKey, default: defaultValue, searchInDomain: searchInDomain) { value in + switch value { + case "1": true + case "0": false + default: nil + } + } + } + + public func envIntValue(rawKey: String, default defaultValue: Int? = nil, searchInDomain: Bool) -> Int? { + envValue(rawKey: rawKey, default: defaultValue, searchInDomain: searchInDomain) { Int($0) } + } + + public func envStringValue(rawKey: String, default defaultValue: String? = nil, searchInDomain: Bool) -> String? { + envValue(rawKey: rawKey, default: defaultValue, searchInDomain: searchInDomain) { $0 } + } +} + +public func envBoolValue(_ key: String, default defaultValue: Bool = false, searchInDomain: Bool = true) -> Bool { + EnvManager.shared.envBoolValue(rawKey: key, default: defaultValue, searchInDomain: searchInDomain)! +} + +public func envIntValue(_ key: String, default defaultValue: Int = 0, searchInDomain: Bool = true) -> Int { + EnvManager.shared.envIntValue(rawKey: key, default: defaultValue, searchInDomain: searchInDomain)! +} + +public func envStringValue(_ key: String, default defaultValue: String, searchInDomain: Bool = true) -> String { + EnvManager.shared.envStringValue(rawKey: key, default: defaultValue, searchInDomain: searchInDomain)! +} + +public func envStringValue(_ key: String, searchInDomain: Bool = true) -> String? { + EnvManager.shared.envStringValue(rawKey: key, searchInDomain: searchInDomain) +} + +public func configureOpenSwiftUIEnvironment(provider: EnvironmentProvider? = nil) { + EnvManager.shared.reset() + if let provider { + EnvManager.shared.setEnvironmentProvider(provider) + } + EnvManager.shared.register(domain: "OpenSwiftUI") +} + +public struct OpenSwiftUIExampleServerConfiguration { + public static let lookInsideServerEnvKey = "OPENSWIFTUI_EXAMPLE_LOOKINSIDE_SERVER" + public static let lookInServerEnvKey = "OPENSWIFTUI_EXAMPLE_LOOKIN_SERVER" + + public let enableLookInsideServer: Bool + public let enableLookInServer: Bool + + public static func resolve() -> OpenSwiftUIExampleServerConfiguration { + let enableLookInsideServer = envBoolValue("EXAMPLE_LOOKINSIDE_SERVER", default: true) + let enableLookInServer = envBoolValue("EXAMPLE_LOOKIN_SERVER", default: false) + if enableLookInsideServer && enableLookInServer { + fatalError("\(lookInsideServerEnvKey) and \(lookInServerEnvKey) cannot both be enabled") + } + return OpenSwiftUIExampleServerConfiguration( + enableLookInsideServer: enableLookInsideServer, + enableLookInServer: enableLookInServer + ) + } +}