Compare commits

..

26 Commits

Author SHA1 Message Date
Imran
0e2a0b65ec Merge pull request #1990 from ImranR98/dev
- Improved XAPK Support (#682)
- Custom user-agent for APKMirror (as per feedback in #1973)
- Minor change to German translation (#1986)
2024-11-23 16:31:46 -05:00
Imran Remtulla
5b79f399d1 Minor change to German translation (#1986) 2024-11-23 16:29:10 -05:00
Imran Remtulla
2961d5ac17 Merge remote-tracking branch 'origin/main' into dev 2024-11-23 16:27:21 -05:00
Imran Remtulla
4af4160aaa Increment version 2024-11-23 16:27:11 -05:00
Imran Remtulla
327f73cc9e More changes for XAPK support (#682) 2024-11-23 16:26:11 -05:00
Imran Remtulla
e82170fec6 Improved XAPK Support (#682) 2024-11-23 16:05:02 -05:00
Imran Remtulla
8922b1c048 Custom user-agent for APKMirror (as per feedback in #1973) 2024-11-23 15:26:58 -05:00
Imran
e9550c6ff0 Merge pull request #1980 from Hamster45105/wiki-links
Update wiki links
2024-11-23 01:53:07 -05:00
Hamish
890c3682c4 Update wiki links 2024-11-23 17:17:04 +11:00
Imran
a2c38968e1 Merge pull request #1977 from summoner001/main
Update hu.json
2024-11-15 13:14:29 -05:00
summoner001
a9c3ee4c54 Update hu.json
Correcting translation.
2024-11-15 14:34:01 +01:00
Imran Remtulla
36db226024 README updates
Signed-off-by: Imran Remtulla <contact@imranr.dev>
2024-11-14 21:42:25 -05:00
Imran
6fbdf62afa Merge pull request #1975 from ImranR98/dev
Bugfixes for the "download release assets" button (#1968)
2024-11-14 21:34:36 -05:00
Imran Remtulla
9344ebbb06 Upgrade Flutter + packages, increment version 2024-11-14 21:31:47 -05:00
Imran Remtulla
51b66ab983 Merge remote-tracking branch 'origin/main' into dev 2024-11-14 21:31:17 -05:00
Imran Remtulla
7f2a9b6fa5 Bugfix for "download release assets" (#1968) 2024-11-14 21:21:32 -05:00
Imran Remtulla
ac6bc4d786 Bugfix in "download release assets" button (#1968) 2024-11-14 20:50:06 -05:00
Imran
acd30b516c Merge pull request #1972 from DwainZwerg/patch-1
Update README.md
2024-11-14 20:23:41 -05:00
Imran
5bc9234101 Merge pull request #1965 from bluefly000/japanese-translation
Update ja.json
2024-11-14 20:23:28 -05:00
Imran
80d15f5232 Merge pull request #1964 from summoner001/main
update hu.json
2024-11-14 20:23:15 -05:00
DwainZwerg
eb7c2a9fc4 Update README.md 2024-11-14 15:04:14 +01:00
DwainZwerg
40175468b2 Update README.md
Add list of all repositories
2024-11-14 15:01:48 +01:00
bluefly000
814b5a71bd Update ja.json 2024-11-11 14:47:17 +09:00
bluefly000
38a1e43116 Update ja.json 2024-11-11 14:27:20 +09:00
summoner001
5064b78c79 update hu.json 2024-11-10 18:42:08 +01:00
summoner001
767350e4e3 update hu.json 2024-11-10 18:39:41 +01:00
13 changed files with 126 additions and 79 deletions

View File

@@ -7,10 +7,12 @@ Get Android app updates straight from the source.
Obtainium allows you to install and update apps directly from their releases pages, and receive notifications when new releases are made available.
More info:
- [Obtainium/wiki](https://github.com/ImranR98/Obtainium/wiki)
- [Obtainium Wiki](https://wiki.obtainium.imranr.dev/) ([repository](https://github.com/ImranR98/Obtainium-Wiki))
- [AppVerifier](https://github.com/soupslurpr/AppVerifier) - App verification tool (recommended, integrates with Obtainium)
- [apps.obtainium.imranr.dev](https://apps.obtainium.imranr.dev/) - Crowdsourced app configurations
- [apps.obtainium.imranr.dev](https://apps.obtainium.imranr.dev/) - Crowdsourced app configurations ([repository](https://github.com/ImranR98/apps.obtainium.imranr.dev))
- [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0) - Original motivation for this app
- [Website](https://obtainium.imranr.dev) ([repository](https://github.com/ImranR98/obtainium.imranr.dev))
- [Source code](https://github.com/ImranR98/Obtainium)
Currently supported App sources:
- Open Source - General:
@@ -64,8 +66,6 @@ Verification info:
- Note: The above signature is also valid for the F-Droid flavour of Obtainium, thanks to [reproducible builds](https://f-droid.org/docs/Reproducible_Builds/).
- [PGP Public Key](https://keyserver.ubuntu.com/pks/lookup?search=contact%40imranr.dev&fingerprint=on&op=index) (to verify APK hashes)
## Limitations
- For some sources, data is gathered using Web scraping and can easily break due to changes in website design. In such cases, more reliable methods may be unavailable.

View File

@@ -114,7 +114,7 @@
"light": "Hell",
"followSystem": "System folgen",
"followSystemThemeExplanation": "Das Folgen des Systemthemes ist unter Android < 10 nur mit Hilfe von Drittanbieterapps möglich",
"useBlackTheme": "Pure Black Dark Theme verwenden",
"useBlackTheme": "Rein schwarzen Hintergrund verwenden",
"appSortBy": "App sortieren nach",
"authorName": "Autor/Name",
"nameAuthor": "Name/Autor",

View File

@@ -213,7 +213,7 @@
"releaseDateAsVersion": "Használja a kiadás dátumát verzió-karakterláncként",
"releaseTitleAsVersion": "Használja a kiadás címét verzió-karakterláncként",
"releaseDateAsVersionExplanation": "Ezt a beállítást csak olyan alkalmazásoknál szabad használni, ahol a verzió-érzékelés nem működik megfelelően, de elérhető a kiadás dátuma.",
"changes": "Változások",
"changes": "Változáslista",
"releaseDate": "Kiadás dátuma",
"importFromURLsInFile": "Importálás fájlban található webcímből (pl. OPML)",
"versionDetectionExplanation": "A verzió-karakterlánc egyeztetése az rendszer által érzékelt verzióval",

View File

@@ -147,21 +147,21 @@
"noNewUpdates": "新しいアップデートはありません",
"xHasAnUpdate": "{} のアップデートが利用可能です。",
"appsUpdated": "アプリをアップデートしました",
"appsNotUpdated": "アプリケーションの更新に失敗",
"appsNotUpdated": "アプリのアップデートに失敗しました",
"appsUpdatedNotifDescription": "1つまたは複数のAppのアップデートがバックグラウンドで適用されたことをユーザーに通知する",
"xWasUpdatedToY": "{} が {} にアップデートされました",
"xWasNotUpdatedToY": "への更新に失敗しました",
"xWasNotUpdatedToY": "{} の {} へのアップデートに失敗しました",
"errorCheckingUpdates": "アップデート確認中のエラー",
"errorCheckingUpdatesNotifDescription": "バックグラウンドでのアップデート確認に失敗した際に表示される通知",
"appsRemoved": "削除されたアプリ",
"appsRemovedNotifDescription": "アプリの読み込み中にエラーが発生したため、1つまたは複数のアプリが削除されたことをユーザーに通知する",
"xWasRemovedDueToErrorY": "このエラーのため、{} は削除されました: {}",
"completeAppInstallation": "アプリのインストールを完了する",
"obtainiumMustBeOpenToInstallApps": "アプリをインストールするにはObtainiumが開いている必要があります",
"obtainiumMustBeOpenToInstallApps": "アプリをインストールするにはObtainiumを開く必要があります",
"completeAppInstallationNotifDescription": "アプリのインストールを完了するために、Obtainiumに戻る必要があります",
"checkingForUpdates": "アップデートを確認中",
"checkingForUpdatesNotifDescription": "アップデートを確認する際に表示される一時的な通知",
"pleaseAllowInstallPerm": "Obtainiumによるアプリのインストールを許可してください",
"pleaseAllowInstallPerm": "Obtainiumによるアプリのインストールを許可してください",
"trackOnly": "追跡のみ",
"errorWithHttpStatusCode": "エラー {}",
"versionCorrectionDisabled": "バージョン補正無効 (プラグインが動作していません)",
@@ -211,7 +211,7 @@
"uninstallFromDevice": "デバイスからアンインストールする",
"onlyWorksWithNonVersionDetectApps": "バージョン検出を無効にしているアプリにのみ動作します。",
"releaseDateAsVersion": "リリース日をバージョンとして使用する",
"releaseTitleAsVersion": "リリースタイトルをバージョン文字列として使用",
"releaseTitleAsVersion": "リリースタイトルをバージョンとして使用する",
"releaseDateAsVersionExplanation": "このオプションは、バージョン検出が正しく機能しないアプリで、リリース日が利用可能な場合にのみ使用する必要があります。",
"changes": "変更点",
"releaseDate": "リリース日",
@@ -256,7 +256,7 @@
"intermediateLink": "中間リンク",
"exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)",
"bgUpdatesOnWiFiOnly": "WiFiを使用していない場合、バックグラウンドアップデートを無効にする",
"bgUpdatesWhileChargingOnly": "非充電時にバックグラウンド更新を無効にする",
"bgUpdatesWhileChargingOnly": "非充電時にバックグラウンドアップデートを無効にする",
"autoSelectHighestVersionCode": "最も高いバージョンコードのAPKを自動で選択する",
"versionExtractionRegEx": "バージョン抽出の正規表現",
"trimVersionString": "正規表現でバージョン文字列をトリムする",
@@ -316,7 +316,7 @@
"crowdsourcedConfigsLabel": "クラウドソーシングによるアプリの設定(利用は自己責任で)",
"crowdsourcedConfigsShort": "クラウドソーシングによるアプリの設定",
"allowInsecure": "安全でないHTTPリクエストを許可する",
"stayOneVersionBehind": "最新バージョンから1つ遅れ",
"stayOneVersionBehind": "最新バージョンから1つ前のものを使用する",
"removeAppQuestion": {
"one": "アプリを削除しますか?",
"other": "アプリを削除しますか?"
@@ -358,23 +358,23 @@
"other": "{n} 個のログをクリアしました (前 = {before}, 後 = {after})"
},
"xAndNMoreUpdatesAvailable": {
"one": "{} とさらに {} 個のアプリのアップデートが利用可能です。",
"one": "{} とさらに 1 個のアプリのアップデートが利用可能です。",
"other": "{} とさらに {} 個のアプリのアップデートが利用可能です。"
},
"xAndNMoreUpdatesInstalled": {
"one": "{} とさらに {} 個のアプリがアップデートされました。",
"one": "{} とさらに 1 個のアプリがアップデートされました。",
"other": "{} とさらに {} 個のアプリがアップデートされました。"
},
"xAndNMoreUpdatesFailed": {
"one": "更新に失敗しました。",
"other": "アプリのアップデートに失敗しました。"
"one": "{} とさらに 1 個のアプリのアップデートに失敗しました。",
"other": "{} とさらに {} 個のアプリのアップデートに失敗しました。"
},
"xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} とさらに 1 個のアプリがアップデートされた可能性があります。",
"other": "{} とさらに {} 個のアプリがアップデートされた可能性があります。"
},
"apk": {
"one": "{}APK",
"other": "{}APK"
"one": "{} APK",
"other": "{} APK"
}
}

View File

@@ -1,5 +1,5 @@
<p>Obtainium allows you to install and update Apps directly from their releases pages, and receive notifications when new releases are made available.</p>
<p>Read the <a href="https://github.com/ImranR98/Obtainium/wiki">Wiki</a></p>
<p>Read the <a href="https://wiki.obtainium.imranr.dev/">Wiki</a></p>
<p>
<b>Currently supported App sources:</b>
</p>

View File

@@ -1,5 +1,5 @@
<p>Obtainium позволяет вам устанавливать и обновлять приложения прямо с их объявлений о выпусках и получать уведомления о новых выпусках.</p>
<p>Для деталей читайте <a href="https://github.com/ImranR98/Obtainium/wiki">Вики</a></p>
<p>Для деталей читайте <a href="https://wiki.obtainium.imranr.dev/">Вики</a></p>
<p>
<b>Поддерживаемые источники приложений:</b>
</p>

View File

@@ -5,6 +5,8 @@ import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/apps_provider.dart';
import 'package:obtainium/providers/settings_provider.dart';
import 'package:obtainium/providers/source_provider.dart';
class APKMirror extends AppSource {
@@ -31,6 +33,16 @@ class APKMirror extends AppSource {
];
}
@override
Future<Map<String, String>?> getRequestHeaders(
Map<String, dynamic> additionalSettings,
{bool forAPKDownload = false}) async {
return {
"User-Agent":
"Obtainium/${(await getInstalledInfo(obtainiumId))?.versionName ?? '1.0.0'}"
};
}
@override
String sourceSpecificStandardizeURL(String url, {bool forSelection = false}) {
RegExp standardUrlRegEx = RegExp(

View File

@@ -105,11 +105,7 @@ class APKPure extends AppSource {
.map((e) => e.text.trim())
.map((t) => t == 'APKs' ? 'APK' : t) ??
[];
String type = types.isEmpty
? 'APK'
: types.length == 1
? types.first
: types.last;
String type = types.isEmpty ? 'APK' : types.first;
String? dateString = apkInfo
?.querySelector('div.info-bottom span.time')
?.text

View File

@@ -886,7 +886,7 @@ class _SettingsPageState extends State<SettingsPage> {
),
IconButton(
onPressed: () {
launchUrlString('${settingsProvider.sourceUrl}/wiki',
launchUrlString('https://wiki.obtainium.imranr.dev/',
mode: LaunchMode.externalApplication);
},
icon: const Icon(Icons.help_outline_rounded),

View File

@@ -516,11 +516,29 @@ class AppsProvider with ChangeNotifier {
.listSync()
.where((e) => e.path.toLowerCase().endsWith('.apk'))
.toList();
FileSystemEntity? temp;
apks.removeWhere((element) {
bool res = element.uri.pathSegments.last.startsWith(app.id);
if (res) {
temp = element;
}
return res;
});
if (temp != null) {
apks = [
temp!,
...apks,
];
}
for (var i = 0; i < apks.length; i++) {
try {
newInfo = await pm.getPackageArchiveInfo(
archiveFilePath: apks.first.path);
break;
newInfo =
await pm.getPackageArchiveInfo(archiveFilePath: apks[i].path);
if (newInfo != null) {
break;
}
} catch (e) {
if (i == apks.length - 1) {
rethrow;
@@ -644,28 +662,47 @@ class AppsProvider with ChangeNotifier {
var somethingInstalled = false;
try {
MultiAppMultiError errors = MultiAppMultiError();
List<File> APKFiles = [];
for (var file in dir.extracted
.listSync(recursive: true, followLinks: false)
.whereType<File>()) {
if (file.path.toLowerCase().endsWith('.apk')) {
try {
somethingInstalled = somethingInstalled ||
await installApk(
DownloadedApk(dir.appId, file), firstTimeWithContext,
needsBGWorkaround: needsBGWorkaround,
shizukuPretendToBeGooglePlay: shizukuPretendToBeGooglePlay);
} catch (e) {
logs.add(
'Could not install APK from XAPK \'${file.path}\': ${e.toString()}');
errors.add(dir.appId, e, appName: apps[dir.appId]?.name);
}
APKFiles.add(file);
} else if (file.path.toLowerCase().endsWith('.obb')) {
await moveObbFile(file, dir.appId);
}
}
if (somethingInstalled) {
File? temp;
APKFiles.removeWhere((element) {
bool res = element.uri.pathSegments.last.startsWith(dir.appId);
if (res) {
temp = element;
}
return res;
});
if (temp != null) {
APKFiles = [
temp!,
...APKFiles,
];
}
try {
await installApk(
DownloadedApk(dir.appId, APKFiles[0]), firstTimeWithContext,
needsBGWorkaround: needsBGWorkaround,
shizukuPretendToBeGooglePlay: shizukuPretendToBeGooglePlay,
additionalAPKs: APKFiles.sublist(1)
.map((a) => DownloadedApk(dir.appId, a))
.toList());
somethingInstalled = true;
dir.file.delete(recursive: true);
} else if (errors.idsByErrorString.isNotEmpty) {
} catch (e) {
logs.add('Could not install APKs from XAPK: ${e.toString()}');
errors.add(dir.appId, e, appName: apps[dir.appId]?.name);
}
if (errors.idsByErrorString.isNotEmpty) {
throw errors;
}
} finally {
@@ -677,7 +714,8 @@ class AppsProvider with ChangeNotifier {
Future<bool> installApk(
DownloadedApk file, BuildContext? firstTimeWithContext,
{bool needsBGWorkaround = false,
bool shizukuPretendToBeGooglePlay = false}) async {
bool shizukuPretendToBeGooglePlay = false,
List<DownloadedApk> additionalAPKs = const []}) async {
if (firstTimeWithContext != null &&
settingsProvider.beforeNewInstallsShareToAppVerifier &&
(await getInstalledInfo('dev.soupslurpr.appverifier')) != null) {
@@ -693,6 +731,7 @@ class AppsProvider with ChangeNotifier {
if (newInfo == null) {
try {
file.file.deleteSync(recursive: true);
additionalAPKs.forEach((a) => a.file.deleteSync(recursive: true));
} catch (e) {
//
} finally {
@@ -720,8 +759,10 @@ class AppsProvider with ChangeNotifier {
}
int? code;
if (!settingsProvider.useShizuku) {
code =
await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
var allAPKs = [file.file.path];
allAPKs.addAll(additionalAPKs.map((a) => a.file.path));
code = await AndroidPackageInstaller.installApk(
apkFilePath: allAPKs.join(','));
} else {
code = await ShizukuApkInstaller.installAPK(file.file.uri.toString(),
shizukuPretendToBeGooglePlay ? "com.android.vending" : "");
@@ -1036,8 +1077,17 @@ class AppsProvider with ChangeNotifier {
if (apps[id]!.app.apkUrls.isNotEmpty ||
apps[id]!.app.otherAssetUrls.isNotEmpty) {
// ignore: use_build_context_synchronously
fileUrl = await confirmAppFileUrl(apps[id]!.app, context, true,
MapEntry<String, String>? tempFileUrl = await confirmAppFileUrl(
apps[id]!.app, context, true,
evenIfSingleChoice: true);
if (tempFileUrl != null) {
fileUrl = MapEntry(
tempFileUrl.key,
await (SourceProvider().getSource(apps[id]!.app.url,
overrideSource: apps[id]!.app.overrideSource))
.apkUrlPrefetchModifier(tempFileUrl.value, apps[id]!.app.url,
apps[id]!.app.additionalSettings));
}
}
if (fileUrl != null) {
filesToDownload.add(MapEntry(fileUrl, apps[id]!.app));
@@ -1050,18 +1100,7 @@ class AppsProvider with ChangeNotifier {
Future<void> downloadFn(MapEntry<String, String> fileUrl, App app) async {
try {
var exportDir = await settingsProvider.getExportDir();
String downloadPath = '${await getStorageRootPath()}/Download';
bool downloadsAccessible = false;
try {
Directory(downloadPath).listSync();
downloadsAccessible = true;
} catch (e) {
//
}
if (!downloadsAccessible && exportDir != null) {
downloadPath = exportDir.path;
}
await downloadFile(fileUrl.value, fileUrl.key, true,
(double? progress) {
notificationsProvider

View File

@@ -14,7 +14,7 @@ packages:
description:
path: "."
ref: main
resolved-ref: ba2aa7a11edc2649d1d80c25ed9291521262f714
resolved-ref: bcad19e964d377da8816718032e5dbf6dd16ba3a
url: "https://github.com/ImranR98/android_package_installer"
source: git
version: "0.0.1"
@@ -303,10 +303,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: aac85f20436608e01a6ffd1fdd4e746a7f33c93a2c83752e626bdfaea139b877
sha256: "16dc141db5a2ccc6520ebb6a2eb5945b1b09e95085c021d9f914f8ded7f1465c"
url: "https://pub.dev"
source: hosted
version: "8.1.3"
version: "8.1.4"
fixnum:
dependency: transitive
description:
@@ -420,10 +420,10 @@ packages:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "725145682706fb0e5a30f93e5cb64f3df7ed7743de749bd555b22bf75ee718c0"
sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610
url: "https://pub.dev"
source: hosted
version: "18.0.0"
version: "18.0.1"
flutter_local_notifications_linux:
dependency: transitive
description:
@@ -449,10 +449,10 @@ packages:
dependency: "direct main"
description:
name: flutter_markdown
sha256: f0e599ba89c9946c8e051780f0ec99aba4ba15895e0380a7ab68f420046fc44e
sha256: "999a4e3cb3e1532a971c86d6c73a480264f6a687959d4887cb4e2990821827e4"
url: "https://pub.dev"
source: hosted
version: "0.7.4+1"
version: "0.7.4+2"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@@ -731,10 +731,10 @@ packages:
dependency: transitive
description:
name: permission_handler_html
sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.dev"
source: hosted
version: "0.1.3+2"
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
@@ -945,10 +945,10 @@ packages:
dependency: transitive
description:
name: sqflite_common
sha256: "4468b24876d673418a7b7147e5a08a715b4998a7ae69227acafaab762e0e5490"
sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"
url: "https://pub.dev"
source: hosted
version: "2.5.4+5"
version: "2.5.4+6"
sqflite_darwin:
dependency: transitive
description:
@@ -1017,10 +1017,10 @@ packages:
dependency: transitive
description:
name: timezone
sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d"
sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d
url: "https://pub.dev"
source: hosted
version: "0.9.4"
version: "0.10.0"
typed_data:
dependency: transitive
description:
@@ -1065,10 +1065,10 @@ packages:
dependency: transitive
description:
name: url_launcher_linux
sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
version: "3.2.1"
url_launcher_macos:
dependency: transitive
description:
@@ -1145,10 +1145,10 @@ packages:
dependency: transitive
description:
name: webview_flutter_android
sha256: dec83a8da0a2dcd8a25418534cc59348dbc2855fa1dd0cc929c62b6029fde392
sha256: "285cedfd9441267f6cca8843458620b5fda1af75b04f5818d0441acda5d7df19"
url: "https://pub.dev"
source: hosted
version: "4.0.1"
version: "4.1.0"
webview_flutter_platform_interface:
dependency: transitive
description:
@@ -1161,10 +1161,10 @@ packages:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: f14ee08021772fed913da8daebcfdeb46be457081e521e93e9918fe6cd1ce9e8
sha256: b7e92f129482460951d96ef9a46b49db34bd2e1621685de26e9eaafd9674e7eb
url: "https://pub.dev"
source: hosted
version: "3.16.1"
version: "3.16.3"
win32:
dependency: transitive
description:

View File

@@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.1.30+2287
version: 1.1.32+2289
environment:
sdk: '>=3.0.0 <4.0.0'