Parcourir la source

Release/0.1.2 (#10)

* Set pump initial settings and pump popover

* Check reservoir

* fix initial setup

* open CGM source

* fix initial pump setup

* Hide debug options

* fix initial preferences

* use xcconfig

* Autoupdate build number

* Create README_RU.md

* Display version and build number

* English README

* Disclaimer

* disallow bolus 0

* possible fix wrong layout after start

* fix glucose Y range

* Optional api secret

* fix suggestion parsing

* Face id access

* fix clock

* build autoupdate

* Fix temp targets layout and pump view

* Fix pump updating logic

* fix omnipod reservooir

* Background timers

* legend

* Remove unused dependecies

* show temp target range

* FAQ_RU

* FAQ eng

* bump buils number

* clear signing

* fix reservoir bug

* Libre 2 glucose filter

* fix glucose filter logic

* skip_neutral_temps option fix

* Fix bild number update script

* Fix BloodGlucose parsing

* update charts on UIApplication.willEnterForegroundNotification

* Fix autotune deletion

* README updated

* Bump version

* Heartbeat based on glucose, not pump

* set pump heartbeat to true

* fix omnipod time

* set loop color to yellow if suggestion not found

* Rewrite sync logic again

* heartbeat if no new glucose

* set heartbeat date on force

* Bump version

* Change versioning to manual

* fix version in settings

* check glucose date < 12 min old

* remove signing
Ivan il y a 5 ans
Parent
commit
045c478e64

+ 4 - 23
FreeAPS.xcodeproj/project.pbxproj

@@ -1445,7 +1445,6 @@
 				388E595525AD948C0019842D /* Frameworks */,
 				388E595625AD948C0019842D /* Resources */,
 				3821ECD025DC703C00BC42AD /* Embed Frameworks */,
-				38F3783826135440009DB701 /* Build Number Update */,
 			);
 			buildRules = (
 			);
@@ -1562,24 +1561,6 @@
 			shellPath = /bin/sh;
 			shellScript = "source \"${SRCROOT}\"/scripts/swiftformat.sh\n\n";
 		};
-		38F3783826135440009DB701 /* Build Number Update */ = {
-			isa = PBXShellScriptBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-			);
-			inputFileListPaths = (
-			);
-			inputPaths = (
-			);
-			name = "Build Number Update";
-			outputFileListPaths = (
-			);
-			outputPaths = (
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-			shellPath = /bin/sh;
-			shellScript = "\"${SRCROOT}\"/scripts/build-number-update.swift\n";
-		};
 /* End PBXShellScriptBuildPhase section */
 
 /* Begin PBXSourcesBuildPhase section */
@@ -1948,7 +1929,7 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = FreeAPS/Resources/FreeAPS.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
 				DEVELOPMENT_ASSET_PATHS = "";
 				DEVELOPMENT_TEAM = "";
 				ENABLE_PREVIEWS = YES;
@@ -1958,7 +1939,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 0.1.1;
+				MARKETING_VERSION = "$(CURRENT_PROJECT_VERSION)";
 				PRODUCT_BUNDLE_IDENTIFIER = ru.artpancreas.FreeAPS;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_VERSION = 5.0;
@@ -1973,7 +1954,7 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = FreeAPS/Resources/FreeAPS.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
 				DEVELOPMENT_ASSET_PATHS = "";
 				DEVELOPMENT_TEAM = "";
 				ENABLE_PREVIEWS = YES;
@@ -1983,7 +1964,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 0.1.1;
+				MARKETING_VERSION = "$(CURRENT_PROJECT_VERSION)";
 				PRODUCT_BUNDLE_IDENTIFIER = ru.artpancreas.FreeAPS;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_VERSION = 5.0;

+ 1 - 1
FreeAPS/Resources/Config.xcconfig

@@ -1 +1 @@
-CURRENT_PROJECT_VERSION = 0.1.1
+BUILD_VERSION = 0.1.2

+ 1 - 1
FreeAPS/Resources/Info.plist

@@ -17,7 +17,7 @@
 	<key>CFBundleShortVersionString</key>
 	<string>$(MARKETING_VERSION)</string>
 	<key>CFBundleVersion</key>
-	<string>259</string>
+	<string>$(BUILD_VERSION)</string>
 	<key>LSApplicationQueriesSchemes</key>
 	<array>
 		<string>dexcomg6</string>

+ 10 - 3
FreeAPS/Sources/APS/APSManager.swift

@@ -6,7 +6,7 @@ import SwiftDate
 import Swinject
 
 protocol APSManager {
-    func heartbeatNow()
+    func heartbeat(force: Bool)
     func autotune() -> AnyPublisher<Autotune?, Never>
     func enactBolus(amount: Double)
     var pumpManager: PumpManagerUI? { get set }
@@ -78,8 +78,8 @@ final class BaseAPSManager: APSManager, Injectable {
         pumpManager?.addStatusObserver(self, queue: processQueue)
     }
 
-    func heartbeatNow() {
-        deviceDataManager.heartbeat()
+    func heartbeat(force: Bool) {
+        deviceDataManager.heartbeat(force: force)
     }
 
     private func fetchAndLoop() {
@@ -103,6 +103,7 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     private func loop() {
+        debug(.apsManager, "Starting loop")
         isLooping.send(true)
         Publishers.CombineLatest(
             nightscout.fetchCarbs(),
@@ -165,6 +166,12 @@ final class BaseAPSManager: APSManager, Injectable {
             return Just(false).eraseToAnyPublisher()
         }
 
+        let lastGlucoseDate = glucoseStorage.lastGlucoseDate()
+        guard lastGlucoseDate >= Date().addingTimeInterval(-12.minutes.timeInterval) else {
+            debug(.apsManager, "Glucose data is stale")
+            return Just(false).eraseToAnyPublisher()
+        }
+
         let now = Date()
         let temp = currentTemp(date: now)
 

+ 15 - 9
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -15,7 +15,7 @@ protocol DeviceDataManager {
     var recommendsLoop: PassthroughSubject<Void, Never> { get }
     var pumpName: CurrentValueSubject<String, Never> { get }
     var pumpExpiresAtDate: CurrentValueSubject<Date?, Never> { get }
-    func heartbeat()
+    func heartbeat(force: Bool)
 }
 
 private let staticPumpManagers: [PumpManagerUI.Type] = [
@@ -73,7 +73,6 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
         injectServices(resolver)
         setupPumpManager()
         UIDevice.current.isBatteryMonitoringEnabled = true
-        updatePumpData()
     }
 
     func setupPumpManager() {
@@ -82,7 +81,15 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
         }
     }
 
-    private func updatePumpData() {
+    @SyncAccess(lock: accessLock) private var pumpUpdateInProgress = false
+
+    func heartbeat(force: Bool) {
+        if force {
+            lastHeartBeatTime = Date()
+            updatePumpData()
+            return
+        }
+
         let now = Date()
         var updateInterval: TimeInterval = 5.minutes.timeInterval
 
@@ -101,12 +108,11 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
             return
         }
 
-        heartbeat()
+        lastHeartBeatTime = Date()
+        updatePumpData()
     }
 
-    @SyncAccess(lock: accessLock) private var pumpUpdateInProgress = false
-
-    func heartbeat() {
+    private func updatePumpData() {
         guard let pumpManager = pumpManager else {
             debug(.deviceManager, "Pump is not set, skip updating")
             return
@@ -117,7 +123,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
         }
         debug(.deviceManager, "Start updating the pump data")
         pumpUpdateInProgress = true
-        lastHeartBeatTime = Date()
+
         pumpManager.ensureCurrentPumpData {
             debug(.deviceManager, "Pump Data updated")
             self.pumpUpdateInProgress = false
@@ -158,7 +164,7 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
 
     func pumpManagerBLEHeartbeatDidFire(_: PumpManager) {
         debug(.deviceManager, "Pump Heartbeat")
-        updatePumpData()
+//        heartbeat(force: false)
     }
 
     func pumpManagerMustProvideBLEHeartbeat(_: PumpManager) -> Bool {

+ 15 - 10
FreeAPS/Sources/APS/GlucoseManager.swift

@@ -12,7 +12,7 @@ final class BaseGlucoseManager: GlucoseManager, Injectable {
     @Injected() var apsManager: APSManager!
 
     private var lifetime = Set<AnyCancellable>()
-    private let timer = DispatchTimer(timeInterval: 10)
+    private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
 
     init(resolver: Resolver) {
         injectServices(resolver)
@@ -22,16 +22,21 @@ final class BaseGlucoseManager: GlucoseManager, Injectable {
     private func subscribe() {
         timer.publisher
             .receive(on: processQueue)
-            .flatMap { date -> AnyPublisher<[BloodGlucose], Never> in
-                guard self.glucoseStogare.syncDate().timeIntervalSince1970 <= date.timeIntervalSince1970
-                else {
-                    return Just([]).eraseToAnyPublisher()
-                }
-                return self.nightscoutManager.fetchGlucose()
+            .flatMap { _ -> AnyPublisher<(Date, [BloodGlucose]), Never> in
+                debug(.nightscout, "Glucose manager heartbeat")
+                debug(.nightscout, "Start fetching glucose")
+                return Publishers.CombineLatest(Just(self.glucoseStogare.syncDate()), self.nightscoutManager.fetchGlucose())
+                    .eraseToAnyPublisher()
             }
-            .sink { glucose in
-                if !self.glucoseStogare.filterTooFrequentGlucose(glucose).isEmpty {
-                    self.apsManager.heartbeatNow()
+            .sink { syncDate, glucose in
+                // Because of Spike dosn't respect a date query
+                let filteredByDate = glucose.filter { $0.dateString > syncDate }
+                let filtered = self.glucoseStogare.filterTooFrequentGlucose(filteredByDate)
+                if !filtered.isEmpty {
+                    debug(.nightscout, "New glucose found")
+                    self.apsManager.heartbeat(force: true)
+                } else {
+                    self.apsManager.heartbeat(force: false)
                 }
             }
             .store(in: &lifetime)

+ 3 - 6
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -62,8 +62,7 @@ final class OpenAPS {
                     autosens: autosens.isEmpty ? .null : autosens,
                     meal: meal,
                     microBolusAllowed: true,
-                    reservoir: reservoir,
-                    clock: clock
+                    reservoir: reservoir
                 )
                 debug(.openAPS, "SUGGESTED: \(suggested)")
 
@@ -278,8 +277,7 @@ final class OpenAPS {
         autosens: JSON,
         meal: JSON,
         microBolusAllowed: Bool,
-        reservoir: JSON,
-        clock: JSON
+        reservoir: JSON
     ) -> RawJSON {
         dispatchPrecondition(condition: .onQueue(processQueue))
         return jsWorker.inCommonContext { worker in
@@ -297,8 +295,7 @@ final class OpenAPS {
                     autosens,
                     meal,
                     microBolusAllowed,
-                    reservoir,
-                    clock
+                    reservoir
                 ]
             )
         }

+ 6 - 1
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -7,6 +7,7 @@ protocol GlucoseStorage {
     func recent() -> [BloodGlucose]
     func syncDate() -> Date
     func filterTooFrequentGlucose(_ glucose: [BloodGlucose]) -> [BloodGlucose]
+    func lastGlucoseDate() -> Date
 }
 
 final class BaseGlucoseStorage: GlucoseStorage, Injectable {
@@ -56,8 +57,12 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self)?.reversed() ?? []
     }
 
+    func lastGlucoseDate() -> Date {
+        recent().last?.dateString ?? .distantPast
+    }
+
     func filterTooFrequentGlucose(_ glucose: [BloodGlucose]) -> [BloodGlucose] {
-        var lastDate = recent().first?.dateString ?? .distantPast
+        var lastDate = lastGlucoseDate()
         var filtered: [BloodGlucose] = []
 
         for entry in glucose.reversed() {

+ 1 - 1
FreeAPS/Sources/Modules/Home/HomeProvider.swift

@@ -19,7 +19,7 @@ extension Home {
         }
 
         func heartbeatNow() {
-            apsManager.heartbeatNow()
+            apsManager.heartbeat(force: true)
         }
 
         func filteredGlucose(hours: Int) -> [BloodGlucose] {

+ 3 - 0
FreeAPS/Sources/Modules/Home/View/Header/LoopView.swift

@@ -50,6 +50,9 @@ struct LoopView: View {
         let delta = timerDate.timeIntervalSince(lastLoopDate) - Config.lag
 
         if delta <= 5.minutes.timeInterval {
+            guard actualSuggestion?.deliverAt != nil else {
+                return .loopYellow
+            }
             return .loopGreen
         } else if delta <= 10.minutes.timeInterval {
             return .loopYellow

+ 2 - 2
FreeAPS/Sources/Modules/Home/View/Header/PumpView.swift

@@ -73,11 +73,11 @@ struct PumpView: View {
         time -= hours.hours.timeInterval
         let minutes = Int(time / 1.minutes.timeInterval)
 
-        if days > 1 {
+        if days >= 1 {
             return "\(days)d \(hours)h"
         }
 
-        if hours > 1 {
+        if hours >= 1 {
             return "\(hours)h"
         }
 

+ 0 - 2
FreeAPS/Sources/Modules/Settings/SettingsViewModel.swift

@@ -8,7 +8,6 @@ extension Settings {
 
         @Published var debugOptions = false
 
-        private(set) var appVersion = ""
         private(set) var buildNumber = ""
 
         override func subscribe() {
@@ -23,7 +22,6 @@ extension Settings {
 
             broadcaster.register(SettingsObserver.self, observer: self)
 
-            appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
             buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown"
         }
     }

+ 1 - 1
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -6,7 +6,7 @@ extension Settings {
 
         var body: some View {
             Form {
-                Section(header: Text("FreeAPS X \(viewModel.appVersion) (\(viewModel.buildNumber))")) {
+                Section(header: Text("FreeAPS X v\(viewModel.buildNumber)")) {
                     Toggle("Closed loop", isOn: $viewModel.closedLoop)
                 }
 

+ 1 - 1
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -57,7 +57,7 @@ extension NightscoutAPI {
         components.host = url.host
         components.port = url.port
         components.path = Config.entriesPath
-        components.queryItems = [URLQueryItem(name: "count", value: "\(2000)")]
+        components.queryItems = [URLQueryItem(name: "count", value: "\(1600)")]
         if let date = sinceDate {
             let dateItem = URLQueryItem(
                 name: "find[dateString][$gte]",

+ 0 - 236
scripts/build-number-update.swift

@@ -1,236 +0,0 @@
-#!/usr/bin/env xcrun --sdk macosx swift
-
-import Foundation
-
-// MARK: Types
-
-struct InfoPlist {
-    private typealias Plist = (dict: [String: Any], format: PropertyListSerialization.PropertyListFormat)
-
-    private var fileURL: URL
-    private var plist: Plist
-
-    enum Const: String {
-        case version = "CFBundleShortVersionString"
-        case build = "CFBundleVersion"
-        case settings = "PreferenceSpecifiers"
-        case settingsValue = "DefaultValue"
-    }
-
-    var version: String? {
-        get {
-            plist.dict[InfoPlist.Const.version.rawValue] as? String
-        }
-        set {
-            plist.dict[InfoPlist.Const.version.rawValue] = newValue
-        }
-    }
-
-    var build: String? {
-        get {
-            plist.dict[InfoPlist.Const.build.rawValue] as? String
-        }
-        set {
-            plist.dict[InfoPlist.Const.build.rawValue] = newValue
-        }
-    }
-
-    var settingsVersion: String? {
-        get {
-            (
-                plist
-                    .dict[InfoPlist.Const.settings.rawValue] as! [[String: Any]]
-            )[1][
-                InfoPlist.Const.settingsValue
-                    .rawValue
-            ] as? String
-        }
-        set {
-            var dictCopy = plist.dict
-            var specs = dictCopy[InfoPlist.Const.settings.rawValue] as! [[String: Any]]
-            specs[1][InfoPlist.Const.settingsValue.rawValue] = newValue ?? ""
-            dictCopy[InfoPlist.Const.settings.rawValue] = specs
-            plist.dict = dictCopy
-        }
-    }
-
-    private init(fileURL: URL, plist: Plist) {
-        self.fileURL = fileURL
-        self.plist = plist
-    }
-
-    init?(fromFileAtURL fileURL: URL) {
-        guard let plist = InfoPlist.readPlist(fromFileAtURL: fileURL) else { return nil }
-
-        self.init(fileURL: fileURL, plist: plist)
-    }
-
-    func save() {
-        InfoPlist.writePlist(plist, toFileAtURL: fileURL)
-    }
-
-    // MARK: Plist file read/write
-
-    private static func readPlist(fromFileAtURL fileURL: URL) -> Plist? {
-        var data: Data
-        do {
-            data = try Data(contentsOf: fileURL)
-        } catch {
-            return nil
-        }
-
-        var format: PropertyListSerialization.PropertyListFormat = .xml
-        let dict: [String: Any]
-        do {
-            dict = try PropertyListSerialization.propertyList(from: data, format: &format) as! [String: Any]
-        } catch {
-            print("error: Failed to deserialize plist read from file: \(fileURL.absoluteString)")
-            print("Error details: \(error)")
-            return nil
-        }
-
-        return (dict: dict, format: format)
-    }
-
-    private static func writePlist(_ plist: Plist, toFileAtURL fileURL: URL) {
-        let data: Data
-        do {
-            data = try PropertyListSerialization.data(fromPropertyList: plist.dict, format: plist.format, options: 0)
-        } catch {
-            print("error: Failed to serialize plist!")
-            return
-        }
-
-        do {
-            try data.write(to: fileURL)
-        } catch {
-            print("error: Failed to write file: \(fileURL.absoluteString)")
-            print("Error details: \(error)")
-            return
-        }
-    }
-}
-
-enum Branch: CustomStringConvertible {
-    private static var releasePrefix = "release"
-
-    case Release(version: String)
-    case Other(name: String)
-
-    init(_ string: String) {
-        let parts = string.components(separatedBy: "/")
-        if parts.count >= 2, parts[0] == Branch.releasePrefix {
-            self = .Release(version: parts[1])
-        } else {
-            self = .Other(name: string)
-        }
-    }
-
-    var description: String {
-        switch self {
-        case let .Release(version):
-            return "\(Branch.releasePrefix)/\(version)"
-        case let .Other(name):
-            return name
-        }
-    }
-}
-
-// MARK: Helpers
-
-func execute(command: String, args: [String]) -> String? {
-    let process = Process()
-    process.launchPath = command
-    process.arguments = args
-
-    let pipe = Pipe()
-    process.standardOutput = pipe
-    process.launch()
-
-    let data = pipe.fileHandleForReading.readDataToEndOfFile()
-
-    return String(data: data, encoding: .utf8)?
-        .trimmingCharacters(in: .newlines)
-}
-
-func checkdSYM(buildNumber: String?) {
-    // Не правильная версия в dSYM может привести к проблемам в работе с сервисами, которые эти dSYM используют (например: Crashlytics)
-    // http://tgoode.com/2014/06/05/sensible-way-increment-bundle-version-cfbundleversion-xcode/#comment-600
-    // http://tgoode.com/2014/06/05/sensible-way-increment-bundle-version-cfbundleversion-xcode/#comment-2704
-    // http://stackoverflow.com/q/13323728
-    // http://stackoverflow.com/a/22460268
-    if let dsymFolderPath = ProcessInfo.processInfo.environment["DWARF_DSYM_FOLDER_PATH"], !dsymFolderPath.isEmpty,
-       let productName = ProcessInfo.processInfo.environment["PRODUCT_NAME"], !productName.isEmpty
-    {
-        let dsymPlistURL = URL(fileURLWithPath: "\(dsymFolderPath)/\(productName).app.dSYM/Contents/Info.plist")
-
-        guard var dsymInfoPlist = InfoPlist(fromFileAtURL: dsymPlistURL) else {
-            print("Failed to read dsym at url \(dsymPlistURL). This is ok for debug builds.")
-            return
-        }
-
-        dsymInfoPlist.build = buildNumber
-        dsymInfoPlist.save()
-    }
-}
-
-// MARK: Implementation
-
-guard let currentBranch = execute(command: "/usr/bin/git", args: ["rev-parse", "--abbrev-ref", "HEAD"]).map(Branch.init) else {
-    print("Can't determine current branch! Skiping...")
-    exit(0)
-}
-
-guard let commitsCount = execute(command: "/usr/bin/git", args: ["rev-list", "--count", "HEAD"]), !commitsCount.isEmpty else {
-    print("Can't determine commits count! Skiping..")
-    exit(0)
-}
-
-guard let targetBuildDir = ProcessInfo.processInfo.environment["TARGET_BUILD_DIR"], !targetBuildDir.isEmpty else {
-    print("error: TARGET_BUILD_DIR environment variable is empty!")
-    exit(1)
-}
-
-guard let infoPlistPath = ProcessInfo.processInfo.environment["INFOPLIST_PATH"], !infoPlistPath.isEmpty else {
-    print("error: INFOPLIST_PATH environment variable is empty!")
-    exit(1)
-}
-
-guard let infoPlistFile = ProcessInfo.processInfo.environment["INFOPLIST_FILE"], !infoPlistFile.isEmpty else {
-    print("error: INFOPLIST_FILE environment variable is empty!")
-    exit(1)
-}
-
-guard let currentVersion = ProcessInfo.processInfo.environment["CURRENT_PROJECT_VERSION"] else {
-    print("error: Current version can't be determined!")
-    exit(1)
-}
-
-guard var buildInfoPlist = InfoPlist(fromFileAtURL: URL(fileURLWithPath: "\(targetBuildDir)/\(infoPlistPath)")) else {
-    print("error: Build Info Plist cannot be load!")
-    exit(1)
-}
-
-guard let scrRoot = ProcessInfo.processInfo.environment["SRCROOT"], !targetBuildDir.isEmpty else {
-    print("error: SRCROOT environment variable is empty!")
-    exit(1)
-}
-
-guard var sourceInfoPlist = InfoPlist(fromFileAtURL: URL(fileURLWithPath: "\(scrRoot)/FreeAPS/Resources/Info.plist")) else {
-    print("error: Source Info Plist cannot be load!")
-    exit(1)
-}
-
-switch currentBranch {
-case let .Release(version):
-    buildInfoPlist.version = version
-    buildInfoPlist.build = commitsCount
-    sourceInfoPlist.build = commitsCount
-default:
-    buildInfoPlist.build = "\(commitsCount)"
-}
-
-buildInfoPlist.save()
-sourceInfoPlist.save()
-
-checkdSYM(buildNumber: buildInfoPlist.build)