Files
timecop/flatpak/scripts/flatpak_shared.dart
2023-06-30 15:01:10 +02:00

338 lines
11 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
class Release {
final String version;
final String date;
Release({required this.version, required this.date});
}
enum CPUArchitecture {
x86_64('x86_64', 'x64'),
aarch64('aarch64', 'arm64');
final String flatpakArchCode;
final String flutterDirName;
const CPUArchitecture(this.flatpakArchCode, this.flutterDirName);
}
class ReleaseAsset {
final CPUArchitecture arch;
final String tarballUrlOrPath;
final bool isRelativeLocalPath;
final String tarballSha256;
ReleaseAsset(
{required this.arch,
required this.tarballUrlOrPath,
required this.isRelativeLocalPath,
required this.tarballSha256});
}
class Icon {
static const _symbolicType = 'symbolic';
final String type;
final String path;
late final String _fileExtension;
Icon({required this.type, required this.path}) {
_fileExtension = path.split('.').last;
}
String getFilename(String appId) => (type == _symbolicType)
? '$appId-symbolic.$_fileExtension'
: '$appId.$_fileExtension';
}
class GithubReleases {
final String githubReleaseOrganization;
final String githubReleaseProject;
List<Release>? _releases;
List<ReleaseAsset>? _latestReleaseAssets;
GithubReleases(this.githubReleaseOrganization, this.githubReleaseProject);
Future<List<Release>> getReleases(bool canBeEmpty) async {
if (_releases == null) {
await _fetchReleasesAndAssets(canBeEmpty);
}
return _releases!;
}
Future<List<ReleaseAsset>?> getLatestReleaseAssets() async {
if (_releases == null) {
await _fetchReleasesAndAssets(false);
}
return _latestReleaseAssets;
}
Future<void> _fetchReleasesAndAssets(bool canBeEmpty) async {
final releaseJsonContent = (await http.get(Uri(
scheme: 'https',
host: 'api.github.com',
path:
'/repos/$githubReleaseOrganization/$githubReleaseProject/releases')))
.body;
final decodedJson = jsonDecode(releaseJsonContent) as List;
DateTime? latestReleaseAssetDate;
final releases = List<Release>.empty(growable: true);
await Future.forEach<dynamic>(decodedJson, (dynamic releaseDynamic) async {
final releaseMap = releaseDynamic as Map;
final releaseDateAndTime =
DateTime.parse(releaseMap['published_at'] as String);
final releaseDateString =
releaseDateAndTime.toIso8601String().split('T').first;
if (latestReleaseAssetDate == null ||
(latestReleaseAssetDate?.compareTo(releaseDateAndTime) == -1)) {
final assets =
await _parseGithubReleaseAssets(releaseMap['assets'] as List);
if (assets != null) {
_latestReleaseAssets = assets;
latestReleaseAssetDate = releaseDateAndTime;
}
}
releases.add(Release(
version: releaseMap['name'] as String, date: releaseDateString));
});
if (releases.isNotEmpty || canBeEmpty) {
_releases = releases;
} else {
throw Exception("Github must contain at least 1 release.");
}
}
Future<List<ReleaseAsset>?> _parseGithubReleaseAssets(List assetMaps) async {
String? x64TarballUrl;
String? x64Sha;
String? aarch64TarballUrl;
String? aarch64Sha;
for (final am in assetMaps) {
final amMap = am as Map;
final downloadUrl = amMap['browser_download_url'] as String;
final filename = amMap['name'] as String;
final fileExtension = filename.substring(filename.indexOf('.') + 1);
final filenameWithoutExtension =
filename.substring(0, filename.indexOf('.'));
final arch = filenameWithoutExtension.endsWith('aarch64')
? CPUArchitecture.aarch64
: CPUArchitecture.x86_64;
switch (fileExtension) {
case 'sha256':
if (arch == CPUArchitecture.aarch64) {
aarch64Sha = await _readSha(downloadUrl);
} else {
x64Sha = await _readSha(downloadUrl);
}
break;
case 'tar':
case 'tar.gz':
case 'tgz':
case 'tar.xz':
case 'txz':
case 'tar.bz2':
case 'tbz2':
case 'zip':
case '7z':
if (arch == CPUArchitecture.aarch64) {
aarch64TarballUrl = downloadUrl;
} else {
x64TarballUrl = downloadUrl;
}
break;
default:
break;
}
}
final res = List<ReleaseAsset>.empty(growable: true);
if (x64TarballUrl != null && x64Sha != null) {
res.add(ReleaseAsset(
arch: CPUArchitecture.x86_64,
tarballUrlOrPath: x64TarballUrl,
isRelativeLocalPath: false,
tarballSha256: x64Sha));
}
if (aarch64TarballUrl != null && aarch64Sha != null) {
res.add(ReleaseAsset(
arch: CPUArchitecture.aarch64,
tarballUrlOrPath: aarch64TarballUrl,
isRelativeLocalPath: false,
tarballSha256: aarch64Sha));
}
return res.isEmpty ? null : res;
}
Future<String> _readSha(String shaUrl) async =>
(await http.get(Uri.parse(shaUrl))).body.split(' ').first;
}
class FlatpakMeta {
final String appId;
final String lowercaseAppName;
final String appStreamPath;
final String desktopPath;
final List<Icon> icons;
// Flatpak manifest releated properties
final String freedesktopRuntime;
final List<String>? buildCommandsAfterUnpack;
final List<dynamic>? extraModules;
final List<String> finishArgs;
// Properties relevant only for local releases
final List<Release>? _localReleases;
final List<ReleaseAsset>? _localReleaseAssets;
final String localLinuxBuildDir;
// Properties relevant only for releases fetched from Github
final String? githubReleaseOrganization;
final String? githubReleaseProject;
late final GithubReleases? _githubReleases;
FlatpakMeta(
{required this.appId,
required this.lowercaseAppName,
required this.githubReleaseOrganization,
required this.githubReleaseProject,
required List<Release>? localReleases,
required List<ReleaseAsset>? localReleaseAssets,
required this.localLinuxBuildDir,
required this.appStreamPath,
required this.desktopPath,
required this.icons,
required this.freedesktopRuntime,
required this.buildCommandsAfterUnpack,
required this.extraModules,
required this.finishArgs})
: _localReleases = localReleases,
_localReleaseAssets = localReleaseAssets {
if (githubReleaseOrganization != null && githubReleaseProject != null) {
_githubReleases =
GithubReleases(githubReleaseOrganization!, githubReleaseProject!);
}
}
Future<List<Release>> getReleases(
bool fetchReleasesFromGithub, String? addedTodaysVersion) async {
final releases = List<Release>.empty(growable: true);
if (addedTodaysVersion != null) {
releases.add(Release(
version: addedTodaysVersion,
date: DateTime.now().toIso8601String().split("T").first));
}
if (fetchReleasesFromGithub) {
if (_githubReleases == null) {
throw Exception(
'Metadata must include Github repository info if fetching releases from Github.');
}
releases.addAll(
await _githubReleases!.getReleases(addedTodaysVersion != null));
} else {
if (_localReleases == null && addedTodaysVersion == null) {
throw Exception(
'Metadata must include releases if not fetching releases from Github.');
}
if (_localReleases?.isNotEmpty ?? false) {
releases.addAll(_localReleases!);
}
}
return releases;
}
Future<List<ReleaseAsset>?> getLatestReleaseAssets(
bool fetchReleasesFromGithub) async {
if (fetchReleasesFromGithub) {
if (_githubReleases == null) {
throw Exception(
'Metadata must include Github repository info if fetching releases from Github.');
}
return await _githubReleases!.getLatestReleaseAssets();
} else {
if (_localReleases == null) {
throw Exception(
'Metadata must include releases if not fetching releases from Github.');
}
return _localReleaseAssets;
}
}
static FlatpakMeta fromJson(File jsonFile, {bool skipLocalReleases = false}) {
try {
final dynamic json = jsonDecode(jsonFile.readAsStringSync());
return FlatpakMeta(
appId: json['appId'] as String,
lowercaseAppName: json['lowercaseAppName'] as String,
githubReleaseOrganization:
json['githubReleaseOrganization'] as String?,
githubReleaseProject: json['githubReleaseProject'] as String?,
localReleases: skipLocalReleases
? null
: (json['localReleases'] as List?)?.map((dynamic r) {
final rMap = r as Map;
return Release(
version: rMap['version'] as String,
date: rMap['date'] as String);
}).toList(),
localReleaseAssets: skipLocalReleases
? null
: (json['localReleaseAssets'] as List?)?.map((dynamic ra) {
final raMap = ra as Map;
final archString = raMap['arch'] as String;
final arch = (archString ==
CPUArchitecture.x86_64.flatpakArchCode)
? CPUArchitecture.x86_64
: (archString == CPUArchitecture.aarch64.flatpakArchCode)
? CPUArchitecture.aarch64
: null;
if (arch == null) {
throw Exception(
'Architecture must be either "${CPUArchitecture.x86_64.flatpakArchCode}" or "${CPUArchitecture.aarch64.flatpakArchCode}"');
}
final tarballFile = File(
'${jsonFile.parent.path}/${raMap['tarballPath'] as String}');
final tarballPath = tarballFile.absolute.path;
final preShasum =
Process.runSync('shasum', ['-a', '256', tarballPath]);
final shasum = preShasum.stdout.toString().split(' ').first;
if (preShasum.exitCode != 0) {
throw Exception(preShasum.stderr);
}
return ReleaseAsset(
arch: arch,
tarballUrlOrPath: tarballPath,
isRelativeLocalPath: true,
tarballSha256: shasum);
}).toList(),
localLinuxBuildDir: json['localLinuxBuildDir'] as String,
appStreamPath: json['appStreamPath'] as String,
desktopPath: json['desktopPath'] as String,
icons: (json['icons'] as Map).entries.map((mapEntry) {
return Icon(
type: mapEntry.key as String, path: mapEntry.value as String);
}).toList(),
freedesktopRuntime: json['freedesktopRuntime'] as String,
buildCommandsAfterUnpack: (json['buildCommandsAfterUnpack'] as List?)
?.map((dynamic bc) => bc as String)
.toList(),
extraModules: json['extraModules'] as List?,
finishArgs: (json['finishArgs'] as List)
.map((dynamic fa) => fa as String)
.toList());
} catch (e) {
throw Exception('Could not parse JSON file, due to this error:\n$e');
}
}
}