Comments 28
Рекомендую открыть для себя https://github.com/apple/swift-argument-parser.
... а так же NSPropertyListSerialization
Спасибо за совет! Это будет получше регулярок.
Тут дополню, а разве при использовании метода + writePropertyList:toStream:format:options:error:
у нас порядок элементов не изменится в Info.plist, так как корневой элемент это Dictionary? Тогда, возможно, что регулярка будет всё же лучше.
Про ArgumentParser я упомянул в статье, что если менеджер зависимостей не SPM и подключать его не хочется, то можно воспользоваться уже доступными способами.
но зачем вообще что-то парсить, если есть тот же Environment с именованными параметрами?
URL="https://..."" INFO_PLIST="Info.plist" updater update
или же можно использовать UserDefaults: аргументы вида "-arg value" доступны из UserDefaults.standard
updater -arg value
print(UserDefaults.standard.value(forKey: "arg")) -> Optional(value)
Возможность использовать UserDefaults
, как я понял, не указана явно в публичной документации. Но способ получше парсинга.
У UserDefaults есть разные "domains", и это так называемый Argument Domain: https://developer.apple.com/documentation/foundation/userdefaults/1410665-argumentdomain
Ещё рекомендую посмотреть man plutil
и man PlistBuddy
. PlistBuddy почему-то никогда не попадает в $PATH, но он всегда есть в /usr/libexec/PlistBuddy
.
Как я понимаю, задача заточена чисто под iOS, и а раз так, то надо и делать всё так, чтобы было максимально удобно для имеющихся инструментов. Например вместо json с самодельным форматом можно было бы сразу хранить нужную ветку Info.plist в формате plist (или json), и делать PlistBuddy -x "Merge remote.plist"
. Но у PlistBuddy есть фатальный недостаток: он когда видит несуществующий ключ, возвращает ненулевой код ошибки. Это неудобно.
Но можно сделать почти идеально с plutil: plutil -replace 'NSAppTransportSecurity' -json "{ ... }" -- Info.plist
Вот формат, который мы на самом деле хотим видеть в "настроечном" удалённом файле:
{
"NSExceptionDomains": {
"dev.app_main.com": {
"NSExceptionAllowsInsecureHTTPLoads" : true
},
"dev.app_quicksearch.com": {
"NSExceptionAllowsInsecureHTTPLoads" : true
},
"dev.app_cached.com": {
"NSExceptionAllowsInsecureHTTPLoads" : true
},
"stage.app_main.com": {
"NSExceptionAllowsInsecureHTTPLoads" : false
},
"stage.app_quicksearch.com": {
"NSExceptionAllowsInsecureHTTPLoads" : false
},
"stage.app_cached.com": {
"NSExceptionAllowsInsecureHTTPLoads" : false
}
}
}
Остаётся только скачать его и обновить ключ в Info.plist используя plutil:
plutil -replace 'NSAppTransportSecurity' -json "$(curl https://raw.githubusercontent.com/Streetmage/DomainsList/main/remote_domains.json)" -- Info.plist
Чтобы очистить ключ:
plutil -replace 'NSAppTransportSecurity' -json '{}' -- Info.plist
Можно было бы делать и plutil -remove 'NSAppTransportSecurity' -- Info.plist
, но plutil тоже будет возвращать код ошибки, если ключа не было, это может быть неудобно.
$ plutil -remove 'NSAppTransportSecurity' -- Info.plist
Info.plist: Could not modify plist, error: No value to remove at key path NSAppTransportSecurity
$ echo $?
1
$ plutil -replace 'NSAppTransportSecurity' -json '{}' -- Info.plist
$ echo $?
0
Вместо отдельного "файла-справочника" в приложении, можно читать эти данные прямо из своего же Info.plist. Я подозреваю, что iOS не обидится, если помимо NSExceptionAllowsInsecureHTTPLoads
записать ещё и description.
Но вообще, я не понимаю суть проблемы. Скорее всего это всё находится на подконтрольных серверах, обновляют этот json люди, которые знают и о приложении, и о доменах, и о разработчиках. Info.plist модифицируется при сборке, нужен для отладки. Так может надо просто научить тех, кто меняет домены, пойти в репозиторий приложения и закомитить изменения в Info.plist? Зачем нужна вся эта шайтан-машина со скриптами?
Если вы не хотите, чтобы эти домены попадали в релизную сборку, можно включить в Build Settings настройку Preprocess Info.plist File (INFOPLIST_PREPROCESS)
, добавить в Info.plist Preprocessor Definitions (INFOPLIST_PREPROCESSOR_DEFINITIONS)
строчку типа "DEBUG" для Debug версии, и тогда в Info.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>
#if DEBUG
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>mydomain.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>MyDescription</key>
<string>Hello World</string>
</dict>
</dict>
</dict>
#endif
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
...
Я сейчас впервые в жизни это попробовал сделать, и у меня заработало. Однако, боюсь, что plutil с макросами в теле файла уже не сможет работать. Но я и склоняю к тому, чтобы это менять руками, такие обновления происходят очень редко, и на мой взгляд лучше бы их было видно в истории изменений.
Пример здесь абстрактный и больше был призван показать сам процесс возможности использования утилиты на Swift-е. Он упрощен специально, чтобы не вносить доп. условий.
В исходной задаче файл json привести сразу к формату plist-а было невозможно, это был сторонний сервис. Плюс был нюанс с тем, что не все поля нужно заменять в массиве, а только те, у которых в названии есть определенный признак, то есть ещё дополнительно предварительно отфильтровать.
На этом этапе я решил, что использование консольных утилит типа PlistBuddy потребует их более глубокого изучения и встанет вопрос поддержки другими iOS разработчиками. Например, выше вроде бы простое решение с утилитами plutil и PlistBuddy всё равно требует знания нюансов и опыта работы с этими утилитами.
А вот ваше решение с тем, чтобы хранить дополнительную инфу сразу в plist и не хранить отдельно JSON интересное. Хотя для нас бы не сработало, потому что массив состоял из строк, а не из словарей, куда можно ещё дополнительно закинуть тот же description.
Хотя для нас бы не сработало, потому что массив состоял из строк, а не из словарей, куда можно ещё дополнительно закинуть тот же description.
Как это понять?
Например, вот так:
<array>
<string>value_without_filter_tag</string>
<string>value_with_filter_tag_1</string>
<string>value_with_filter_tag_2</string>
<string>value_with_filter_tag_3</string>
<array>
И нужно было оставить всё, что без value_without_filter_tag. А все другие значение с подстрокой "with_filter_tag" заменить.
Я не понимаю, какой массив, из каких строк, и почему что-то нельзя с ним сделать?
Я предлагал читать данные о доменах (видимо для отображения в UI) прямо из Info.plist, причём туда можно записать не только имя домена и флаг NSExceptionAllowsInsecureHTTPLoads, но так же и любые другие пользовательские данные, как тот же description. Да, структура будет чуть другая, ну и что? Вместо массива объектов типа [{ domain, description, flag }]
будет структура { domain: { description, flag} }
. Попарсить её нет никакой проблемы, зато есть один источник правды.
Глядя на код в репозитории, я увидел множество проблем с этим примером. Какие-то касаются кода, какие-то более концептуальные. Я даже и не знаю с чего начать.
Начну пожалуй с любимого:
static func fetchDomainsJson(remotePath: String) -> [Domain] {
guard let url = URL(string: remotePath) else { fatalError(.remotePathIsInvalid) }
do {
let data = try Data(contentsOf: url)
return try JSONDecoder().decode([Domain].self, from: data)
} catch {
fatalError(.failedToLoadRemoteFile(error))
}
}
Обожаю эти do { try f() } catch { fatalError(error) }
. Почему бы просто не сделать try!
– система точно так же "упадёт", и точно так же напечатает ошибку.
То же касается и url. По-моему, хорошим тоном было бы сразу дать функции нормальные данные (готовый URL), а не заставлять её пытаться из потенциального мусора создать себе нормальное окружение.
Напоследок, раз уж тут есть какие-то try, то пусть бы функция была throws, а делать ли try! пусть решают уровнем выше.
Похожий, но другой пример проблемы:
guard let data = try? Data(contentsOf: url),
let infoPlist = String(data: data, encoding: .utf8)
else { fatalError(.infoPlistNotFound) }
Если do { try f() } catch { fatalError() }
и f()!
это в принципе одно и то же (потому что одинаково напечатается одинаковая ошибка и одинаково упадёт), то здесь же стало хуже. try! Data(...) напечатал бы гораздо больше полезной информации о том, почему же не получилось прочитать данные, чем просто "infoPlistNotFound". Что ещё и не факт, что правда. Файл-то может быть found, но не смочь прочитаться по тысяче причин: недостаточно прав, прочитался только до половины, да чёрт ещё знает почему. Полезная информация, которая могла бы быть в логах, и помочь расследованию, оказывается просто потеряна.
Итак, вот мой вариант:
import Foundation
let infoPlistURL = URL(fileURLWithPath: UserDefaults.standard.string(forKey: "infoPlist") ?? "Info.plist")
var format: PropertyListSerialization.PropertyListFormat = .xml
var plist = try! PropertyListSerialization
.propertyList(from: try! Data(contentsOf: infoPlistURL),
options: .mutableContainersAndLeaves,
format: &format) as! NSMutableDictionary
let kvPairs = try JSONDecoder()
.decode([DomainInfo].self, from: try readDomainsData())
.map {
($0.name, [
"NSExceptionAllowsInsecureHTTPLoads": $0.allowsInsecureHTTPLoads,
"MyDescription" : $0.description
])
}
plist["NSAppTransportSecurity"] = [
"NSExceptionDomains": Dictionary(uniqueKeysWithValues: kvPairs)
]
try! PropertyListSerialization
.data(fromPropertyList: plist, format: format, options: 0)
.write(to: infoPlistURL)
struct DomainInfo: Decodable {
let name: String
let allowsInsecureHTTPLoads: Bool
let description: String
}
func readDomainsData() throws -> Data {
if let url = UserDefaults.standard.string(forKey: "from"), url != "-" {
if url.hasPrefix("http://") || url.hasPrefix("https://") {
return try Data(contentsOf: URL(string: url)!)
}
return try Data(contentsOf: URL(fileURLWithPath: url))
}
return FileHandle.standardInput.readDataToEndOfFile()
}
мы вроде согласились, что отдельный файл не нужен, поэтому обновляется только Info.plist;
Info.plist по дефолту ищется в текущей рабочей директории, или его можно передать аргументом
-infoPlist
;json с данными может читаться из трёх источников:
updater -from https://....
– URL в интернете;updater -from /local/path/on/disk
– файл на диске;updater -from -
(dash) или же без аргументов – прочитается из stdin.curl url | updater
;cat file | updater
;updater < file
;updater
и ввод с завершающим Ctrd+D;
команда для удаления тоже не нужна, т.к. это просто "записать пустой массив", и это можно сделать как
echo "[]" | updater
;сохраняет тот же формат файла Info.plist (xml или бинарный).
Использование Command Line Tool на Swift в iOS проекте