Reorganize Core Data containers (#1017)
Before anything, remove any code related to App Group containers from tvOS target because they are not available. Include the beta receipt override, it's broken for that reason. In short: - Store all Core Data containers locally. Do not use the App Group for Core Data for consistency across platforms. - Store logs in the App Group on iOS/macOS, but locally on tvOS (see `urlForCaches`). Then, rather than one container per model, merge models into: - Local: Providers - Remote: Profiles + Preferences (now in the same CloudKit container) Reuse the remote model for backups too. This change is safe because: - Local profiles are stored via Network Extension in the keychain, not Core Data - Remote profiles are re-imported via CloudKit sync - Providers are re-downloaded on first use - Preferences are lost, but they are "cheap" data - Profile backups are lost, but they were hidden anyway
This commit is contained in:
parent
1751c94891
commit
ffb8829f4f
|
@ -49,10 +49,7 @@ let package = Package(
|
||||||
),
|
),
|
||||||
.library(
|
.library(
|
||||||
name: "TunnelLibrary",
|
name: "TunnelLibrary",
|
||||||
targets: [
|
targets: ["CommonLibrary"]
|
||||||
"AppDataPreferences",
|
|
||||||
"CommonLibrary"
|
|
||||||
]
|
|
||||||
),
|
),
|
||||||
.library(
|
.library(
|
||||||
name: "UIAccessibility",
|
name: "UIAccessibility",
|
||||||
|
|
|
@ -24,14 +24,8 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import AppData
|
import AppData
|
||||||
import CoreData
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension AppData {
|
extension AppData {
|
||||||
public static var cdPreferencesModel: NSManagedObjectModel {
|
public static let preferencesBundle: Bundle = .module
|
||||||
guard let model: NSManagedObjectModel = .mergedModel(from: [.module]) else {
|
|
||||||
fatalError("Unable to build Core Data model (Preferences v3)")
|
|
||||||
}
|
|
||||||
return model
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,16 +24,8 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import AppData
|
import AppData
|
||||||
import CoreData
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension AppData {
|
extension AppData {
|
||||||
|
public static let profilesBundle: Bundle = .module
|
||||||
@MainActor
|
|
||||||
public static let cdProfilesModel: NSManagedObjectModel = {
|
|
||||||
guard let model: NSManagedObjectModel = .mergedModel(from: [.module]) else {
|
|
||||||
fatalError("Unable to build Core Data model (Profiles v3)")
|
|
||||||
}
|
|
||||||
return model
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,16 +24,8 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import AppData
|
import AppData
|
||||||
import CoreData
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension AppData {
|
extension AppData {
|
||||||
|
public static let providersBundle: Bundle = .module
|
||||||
@MainActor
|
|
||||||
public static let cdProvidersModel: NSManagedObjectModel = {
|
|
||||||
guard let model: NSManagedObjectModel = .mergedModel(from: [.module]) else {
|
|
||||||
fatalError("Unable to build Core Data model (Providers v3)")
|
|
||||||
}
|
|
||||||
return model
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,28 +30,44 @@ import PassepartoutKit
|
||||||
|
|
||||||
extension BundleConfiguration {
|
extension BundleConfiguration {
|
||||||
public static var urlForAppLog: URL {
|
public static var urlForAppLog: URL {
|
||||||
urlForGroupCaches.appending(path: Constants.shared.log.appPath)
|
urlForCaches.appending(path: Constants.shared.log.appPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var urlForTunnelLog: URL {
|
public static var urlForTunnelLog: URL {
|
||||||
urlForGroupCaches.appending(path: Constants.shared.log.tunnelPath)
|
urlForCaches.appending(path: Constants.shared.log.tunnelPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var urlForBetaReceipt: URL {
|
public static var urlForBetaReceipt: URL? {
|
||||||
urlForGroupCaches.appending(path: Constants.shared.tunnel.betaReceiptPath)
|
#if os(iOS)
|
||||||
|
urlForCaches.appending(path: Constants.shared.tunnel.betaReceiptPath)
|
||||||
|
#else
|
||||||
|
nil
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// App Group container is not available on tvOS (#1007)
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
|
|
||||||
extension BundleConfiguration {
|
extension BundleConfiguration {
|
||||||
public static var urlForGroupCaches: URL {
|
public static var urlForCaches: URL {
|
||||||
let url = appGroupURL.appending(components: "Library", "Caches")
|
let url = appGroupURL.appending(components: "Library", "Caches")
|
||||||
try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
do {
|
||||||
|
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
||||||
|
} catch {
|
||||||
|
pp_log(.app, .fault, "Unable to create group caches directory: \(error)")
|
||||||
|
}
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var urlForGroupDocuments: URL {
|
public static var urlForDocuments: URL {
|
||||||
let url = appGroupURL.appending(components: "Library", "Documents")
|
let url = appGroupURL.appending(components: "Library", "Documents")
|
||||||
try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
do {
|
||||||
|
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
||||||
|
} catch {
|
||||||
|
pp_log(.app, .fault, "Unable to create group documents directory: \(error)")
|
||||||
|
}
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,3 +82,27 @@ private extension BundleConfiguration {
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
extension BundleConfiguration {
|
||||||
|
public static var urlForCaches: URL {
|
||||||
|
do {
|
||||||
|
return try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||||
|
} catch {
|
||||||
|
pp_log(.app, .fault, "Unable to create user documents directory: \(error)")
|
||||||
|
return URL(fileURLWithPath: NSTemporaryDirectory())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var urlForDocuments: URL {
|
||||||
|
do {
|
||||||
|
return try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||||
|
} catch {
|
||||||
|
pp_log(.app, .fault, "Unable to create user documents directory: \(error)")
|
||||||
|
return URL(fileURLWithPath: NSTemporaryDirectory())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -34,8 +34,6 @@ extension BundleConfiguration {
|
||||||
|
|
||||||
case cloudKitId
|
case cloudKitId
|
||||||
|
|
||||||
case cloudKitPreferencesId
|
|
||||||
|
|
||||||
case userLevel
|
case userLevel
|
||||||
|
|
||||||
case groupId
|
case groupId
|
||||||
|
|
|
@ -28,13 +28,11 @@ import PassepartoutKit
|
||||||
|
|
||||||
public struct Constants: Decodable, Sendable {
|
public struct Constants: Decodable, Sendable {
|
||||||
public struct Containers: Decodable, Sendable {
|
public struct Containers: Decodable, Sendable {
|
||||||
public let localProfiles: String
|
public let local: String
|
||||||
|
|
||||||
public let remoteProfiles: String
|
public let remote: String
|
||||||
|
|
||||||
public let providers: String
|
public let backup: String
|
||||||
|
|
||||||
public let preferences: String
|
|
||||||
|
|
||||||
public let legacyV2: String
|
public let legacyV2: String
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"bundleKey": "AppConfig",
|
"bundleKey": "AppConfig",
|
||||||
"containers": {
|
"containers": {
|
||||||
"localProfiles": "Profiles-v3",
|
"local": "Local",
|
||||||
"remoteProfiles": "Profiles-v3.remote",
|
"remote": "Remote",
|
||||||
"providers": "Providers-v3",
|
"backup": "Backup",
|
||||||
"preferences": "Preferences-v3",
|
"preferences": "Preferences-v3",
|
||||||
"legacyV2": "Profiles",
|
"legacyV2": "Profiles",
|
||||||
"legacyV2TV": "SharedProfiles"
|
"legacyV2TV": "SharedProfiles"
|
||||||
|
|
|
@ -46,7 +46,7 @@ public final class AppContext: ObservableObject, Sendable {
|
||||||
|
|
||||||
public let tunnel: ExtendedTunnel
|
public let tunnel: ExtendedTunnel
|
||||||
|
|
||||||
private let tunnelReceiptURL: URL
|
private let tunnelReceiptURL: URL?
|
||||||
|
|
||||||
private var launchTask: Task<Void, Error>?
|
private var launchTask: Task<Void, Error>?
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ public final class AppContext: ObservableObject, Sendable {
|
||||||
preferencesManager: PreferencesManager,
|
preferencesManager: PreferencesManager,
|
||||||
registry: Registry,
|
registry: Registry,
|
||||||
tunnel: ExtendedTunnel,
|
tunnel: ExtendedTunnel,
|
||||||
tunnelReceiptURL: URL
|
tunnelReceiptURL: URL?
|
||||||
) {
|
) {
|
||||||
self.iapManager = iapManager
|
self.iapManager = iapManager
|
||||||
self.migrationManager = migrationManager
|
self.migrationManager = migrationManager
|
||||||
|
@ -128,7 +128,8 @@ private extension AppContext {
|
||||||
.store(in: &subscriptions)
|
.store(in: &subscriptions)
|
||||||
|
|
||||||
// copy release receipt to tunnel for TestFlight eligibility (once is enough, it won't change)
|
// copy release receipt to tunnel for TestFlight eligibility (once is enough, it won't change)
|
||||||
if let appReceiptURL = Bundle.main.appStoreProductionReceiptURL {
|
if let tunnelReceiptURL,
|
||||||
|
let appReceiptURL = Bundle.main.appStoreProductionReceiptURL {
|
||||||
do {
|
do {
|
||||||
pp_log(.App.iap, .info, "\tCopy release receipt to tunnel...")
|
pp_log(.App.iap, .info, "\tCopy release receipt to tunnel...")
|
||||||
try? FileManager.default.removeItem(at: tunnelReceiptURL)
|
try? FileManager.default.removeItem(at: tunnelReceiptURL)
|
||||||
|
|
|
@ -85,7 +85,7 @@
|
||||||
</CommandLineArgument>
|
</CommandLineArgument>
|
||||||
<CommandLineArgument
|
<CommandLineArgument
|
||||||
argument = "-pp_ui_testing"
|
argument = "-pp_ui_testing"
|
||||||
isEnabled = "YES">
|
isEnabled = "NO">
|
||||||
</CommandLineArgument>
|
</CommandLineArgument>
|
||||||
<CommandLineArgument
|
<CommandLineArgument
|
||||||
argument = "-pp_fake_migration"
|
argument = "-pp_fake_migration"
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||||
<array>
|
<array>
|
||||||
<string>$(CFG_CLOUDKIT_ID)</string>
|
<string>$(CFG_CLOUDKIT_ID)</string>
|
||||||
<string>$(CFG_CLOUDKIT_PREFERENCES_ID)</string>
|
|
||||||
<string>$(CFG_LEGACY_V2_CLOUDKIT_ID)</string>
|
<string>$(CFG_LEGACY_V2_CLOUDKIT_ID)</string>
|
||||||
<string>$(CFG_LEGACY_V2_TV_CLOUDKIT_ID)</string>
|
<string>$(CFG_LEGACY_V2_TV_CLOUDKIT_ID)</string>
|
||||||
</array>
|
</array>
|
||||||
|
|
|
@ -8,8 +8,6 @@
|
||||||
<string>$(CFG_APP_STORE_ID)</string>
|
<string>$(CFG_APP_STORE_ID)</string>
|
||||||
<key>cloudKitId</key>
|
<key>cloudKitId</key>
|
||||||
<string>$(CFG_CLOUDKIT_ID)</string>
|
<string>$(CFG_CLOUDKIT_ID)</string>
|
||||||
<key>cloudKitPreferencesId</key>
|
|
||||||
<string>$(CFG_CLOUDKIT_PREFERENCES_ID)</string>
|
|
||||||
<key>groupId</key>
|
<key>groupId</key>
|
||||||
<string>$(CFG_GROUP_ID)</string>
|
<string>$(CFG_GROUP_ID)</string>
|
||||||
<key>iapBundlePrefix</key>
|
<key>iapBundlePrefix</key>
|
||||||
|
|
|
@ -29,6 +29,7 @@ import AppDataProfiles
|
||||||
import AppDataProviders
|
import AppDataProviders
|
||||||
import CommonLibrary
|
import CommonLibrary
|
||||||
import CommonUtils
|
import CommonUtils
|
||||||
|
import CoreData
|
||||||
import Foundation
|
import Foundation
|
||||||
import LegacyV2
|
import LegacyV2
|
||||||
import PassepartoutKit
|
import PassepartoutKit
|
||||||
|
@ -39,6 +40,18 @@ extension AppContext {
|
||||||
static let shared: AppContext = {
|
static let shared: AppContext = {
|
||||||
let dependencies: Dependencies = .shared
|
let dependencies: Dependencies = .shared
|
||||||
|
|
||||||
|
guard let cdRemoteModel = NSManagedObjectModel.mergedModel(from: [
|
||||||
|
AppData.profilesBundle,
|
||||||
|
AppData.preferencesBundle
|
||||||
|
]) else {
|
||||||
|
fatalError("Unable to load remote model")
|
||||||
|
}
|
||||||
|
guard let cdLocalModel = NSManagedObjectModel.mergedModel(from: [
|
||||||
|
AppData.providersBundle
|
||||||
|
]) else {
|
||||||
|
fatalError("Unable to load local model")
|
||||||
|
}
|
||||||
|
|
||||||
let iapManager = IAPManager(
|
let iapManager = IAPManager(
|
||||||
customUserLevel: dependencies.customUserLevel,
|
customUserLevel: dependencies.customUserLevel,
|
||||||
inAppHelper: dependencies.simulatedAppProductHelper(),
|
inAppHelper: dependencies.simulatedAppProductHelper(),
|
||||||
|
@ -54,8 +67,8 @@ extension AppContext {
|
||||||
let remoteRepositoryBlock: (Bool) -> ProfileRepository = {
|
let remoteRepositoryBlock: (Bool) -> ProfileRepository = {
|
||||||
let remoteStore = CoreDataPersistentStore(
|
let remoteStore = CoreDataPersistentStore(
|
||||||
logger: dependencies.coreDataLogger(),
|
logger: dependencies.coreDataLogger(),
|
||||||
containerName: Constants.shared.containers.remoteProfiles,
|
containerName: Constants.shared.containers.remote,
|
||||||
model: AppData.cdProfilesModel,
|
model: cdRemoteModel,
|
||||||
cloudKitIdentifier: $0 ? BundleConfiguration.mainString(for: .cloudKitId) : nil,
|
cloudKitIdentifier: $0 ? BundleConfiguration.mainString(for: .cloudKitId) : nil,
|
||||||
author: nil
|
author: nil
|
||||||
)
|
)
|
||||||
|
@ -71,8 +84,13 @@ extension AppContext {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return ProfileManager(
|
return ProfileManager(
|
||||||
repository: dependencies.mainProfileRepository(environment: tunnelEnvironment),
|
repository: dependencies.mainProfileRepository(
|
||||||
backupRepository: dependencies.backupProfileRepository(),
|
model: cdRemoteModel,
|
||||||
|
environment: tunnelEnvironment
|
||||||
|
),
|
||||||
|
backupRepository: dependencies.backupProfileRepository(
|
||||||
|
model: cdRemoteModel
|
||||||
|
),
|
||||||
remoteRepositoryBlock: remoteRepositoryBlock,
|
remoteRepositoryBlock: remoteRepositoryBlock,
|
||||||
mirrorsRemoteRepository: dependencies.mirrorsRemoteRepository,
|
mirrorsRemoteRepository: dependencies.mirrorsRemoteRepository,
|
||||||
processor: processor
|
processor: processor
|
||||||
|
@ -89,8 +107,8 @@ extension AppContext {
|
||||||
let providerManager: ProviderManager = {
|
let providerManager: ProviderManager = {
|
||||||
let store = CoreDataPersistentStore(
|
let store = CoreDataPersistentStore(
|
||||||
logger: dependencies.coreDataLogger(),
|
logger: dependencies.coreDataLogger(),
|
||||||
containerName: Constants.shared.containers.providers,
|
containerName: Constants.shared.containers.local,
|
||||||
model: AppData.cdProvidersModel,
|
model: cdLocalModel,
|
||||||
cloudKitIdentifier: nil,
|
cloudKitIdentifier: nil,
|
||||||
author: nil
|
author: nil
|
||||||
)
|
)
|
||||||
|
@ -126,9 +144,9 @@ extension AppContext {
|
||||||
let preferencesManager: PreferencesManager = {
|
let preferencesManager: PreferencesManager = {
|
||||||
let preferencesStore = CoreDataPersistentStore(
|
let preferencesStore = CoreDataPersistentStore(
|
||||||
logger: dependencies.coreDataLogger(),
|
logger: dependencies.coreDataLogger(),
|
||||||
containerName: Constants.shared.containers.preferences,
|
containerName: Constants.shared.containers.remote,
|
||||||
model: AppData.cdPreferencesModel,
|
model: cdRemoteModel,
|
||||||
cloudKitIdentifier: BundleConfiguration.mainString(for: .cloudKitPreferencesId),
|
cloudKitIdentifier: BundleConfiguration.mainString(for: .cloudKitId),
|
||||||
author: nil
|
author: nil
|
||||||
)
|
)
|
||||||
return PreferencesManager(
|
return PreferencesManager(
|
||||||
|
@ -207,11 +225,11 @@ private extension Dependencies {
|
||||||
FakeTunnelStrategy(environment: environment, dataCountInterval: 1000)
|
FakeTunnelStrategy(environment: environment, dataCountInterval: 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mainProfileRepository(environment: TunnelEnvironment) -> ProfileRepository {
|
func mainProfileRepository(model: NSManagedObjectModel, environment: TunnelEnvironment) -> ProfileRepository {
|
||||||
coreDataProfileRepository(observingResults: true)
|
coreDataProfileRepository(model: model, observingResults: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func backupProfileRepository() -> ProfileRepository? {
|
func backupProfileRepository(model: NSManagedObjectModel) -> ProfileRepository? {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -225,12 +243,12 @@ private extension Dependencies {
|
||||||
neStrategy(environment: environment)
|
neStrategy(environment: environment)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mainProfileRepository(environment: TunnelEnvironment) -> ProfileRepository {
|
func mainProfileRepository(model: NSManagedObjectModel, environment: TunnelEnvironment) -> ProfileRepository {
|
||||||
neProfileRepository(environment: environment)
|
neProfileRepository(environment: environment)
|
||||||
}
|
}
|
||||||
|
|
||||||
func backupProfileRepository() -> ProfileRepository? {
|
func backupProfileRepository(model: NSManagedObjectModel) -> ProfileRepository? {
|
||||||
coreDataProfileRepository(observingResults: false)
|
coreDataProfileRepository(model: model, observingResults: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,11 +279,11 @@ private extension Dependencies {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func coreDataProfileRepository(observingResults: Bool) -> ProfileRepository {
|
func coreDataProfileRepository(model: NSManagedObjectModel, observingResults: Bool) -> ProfileRepository {
|
||||||
let store = CoreDataPersistentStore(
|
let store = CoreDataPersistentStore(
|
||||||
logger: coreDataLogger(),
|
logger: coreDataLogger(),
|
||||||
containerName: Constants.shared.containers.localProfiles,
|
containerName: Constants.shared.containers.backup,
|
||||||
model: AppData.cdProfilesModel,
|
model: model,
|
||||||
cloudKitIdentifier: nil,
|
cloudKitIdentifier: nil,
|
||||||
author: nil
|
author: nil
|
||||||
)
|
)
|
||||||
|
|
|
@ -47,7 +47,6 @@ CFG_LOGIN_ITEM_ID = $(CFG_APP_ID).LoginItem
|
||||||
CFG_TUNNEL_ID = $(CFG_APP_ID).Tunnel
|
CFG_TUNNEL_ID = $(CFG_APP_ID).Tunnel
|
||||||
|
|
||||||
CFG_CLOUDKIT_ID = $(CFG_CLOUDKIT_ROOT).v3
|
CFG_CLOUDKIT_ID = $(CFG_CLOUDKIT_ROOT).v3
|
||||||
CFG_CLOUDKIT_PREFERENCES_ID = $(CFG_CLOUDKIT_ROOT).v3.Preferences
|
|
||||||
CFG_LEGACY_V2_CLOUDKIT_ID = $(CFG_CLOUDKIT_ROOT)
|
CFG_LEGACY_V2_CLOUDKIT_ID = $(CFG_CLOUDKIT_ROOT)
|
||||||
CFG_LEGACY_V2_TV_CLOUDKIT_ID = $(CFG_CLOUDKIT_ROOT).Shared
|
CFG_LEGACY_V2_TV_CLOUDKIT_ID = $(CFG_CLOUDKIT_ROOT).Shared
|
||||||
|
|
||||||
|
|
|
@ -51,11 +51,17 @@ private extension Dependencies {
|
||||||
func tunnelReceiptReader() -> AppReceiptReader {
|
func tunnelReceiptReader() -> AppReceiptReader {
|
||||||
FallbackReceiptReader(
|
FallbackReceiptReader(
|
||||||
main: StoreKitReceiptReader(),
|
main: StoreKitReceiptReader(),
|
||||||
beta: KvittoReceiptReader(url: betaReceiptURL)
|
beta: betaReceiptURL.map {
|
||||||
|
KvittoReceiptReader(url: $0)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var betaReceiptURL: URL {
|
var betaReceiptURL: URL? {
|
||||||
|
#if !os(tvOS)
|
||||||
BundleConfiguration.urlForBetaReceipt // copied by AppContext.onLaunch
|
BundleConfiguration.urlForBetaReceipt // copied by AppContext.onLaunch
|
||||||
|
#else
|
||||||
|
nil
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,6 @@
|
||||||
<string>$(CFG_KEYCHAIN_GROUP_ID)</string>
|
<string>$(CFG_KEYCHAIN_GROUP_ID)</string>
|
||||||
<key>tunnelId</key>
|
<key>tunnelId</key>
|
||||||
<string>$(CFG_TUNNEL_ID)</string>
|
<string>$(CFG_TUNNEL_ID)</string>
|
||||||
<key>cloudKitPreferencesId</key>
|
|
||||||
<string>$(CFG_CLOUDKIT_PREFERENCES_ID)</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
|
Loading…
Reference in New Issue