Flatpak build scripts

This commit is contained in:
Miroslav Mazel
2023-06-30 15:01:10 +02:00
parent 50b5cc7292
commit 894ba11cac
6 changed files with 820 additions and 0 deletions

38
flatpak/flatpak_meta.json Normal file
View File

@ -0,0 +1,38 @@
{
"appId": "ca.hamaluik.Timecop",
"lowercaseAppName": "timecop",
"githubReleaseOrganization": "hamaluik",
"githubReleaseProject": "timecop",
"localReleases": [
{
"version": "1.0.0",
"date": "2020-03-19"
}
],
"localReleaseAssets": [
{
"arch": "aarch64",
"tarballPath": "scripts/flatpak_generator_exports/timecop-linux-aarch64.tar.gz"
}
],
"localLinuxBuildDir": "../build/linux",
"appStreamPath": "ca.hamaluik.Timecop.appdata.xml",
"desktopPath": "ca.hamaluik.Timecop.desktop",
"icons": {
"symbolic": "ca.hamaluik.Timecop-symbolic.svg",
"scalable": "ca.hamaluik.Timecop.svg"
},
"freedesktopRuntime": "22.08",
"buildCommandsAfterUnpack": [
"ln -s /usr/lib/x86_64-linux-gnu/libsqlite3.so.0 /app/lib/libsqlite3.so",
"if [ ! -e '/app/lib/libsqlite3.so' ]; then ln -s -f /usr/lib/aarch64-linux-gnu/libsqlite3.so.0 /app/lib/libsqlite3.so; fi",
"if [ $(arch)=\"aarch64\" ]; then sed -si \"/Exec=/cExec=env LIBGL_ALWAYS_SOFTWARE=1 timecop\" timecop/ca.hamaluik.Timecop.desktop; fi"
],
"finishArgs": [
"--share=ipc",
"--socket=fallback-x11",
"--socket=wayland",
"--device=dri",
"--talk-name=org.freedesktop.Notifications"
]
}

View File

@ -0,0 +1,182 @@
// ignore_for_file: avoid_print
import 'dart:io';
import 'flatpak_shared.dart';
/// arguments:
/// --meta [file]
/// Required argument for providing the metadata file for this script.
/// --github
/// Use this option to pull release info from Github rather than the metadata file.
/// --addTodaysVersion [version]
/// If pulling data from Github, this provides a way to specify the release to be released today.
void main(List<String> arguments) async {
if (!Platform.isLinux) {
throw Exception('Must be run under Linux');
}
// PARSE ARGUMENTS
final metaIndex = arguments.indexOf('--meta');
if (metaIndex == -1) {
throw Exception(
'You must run this script with a metadata file argument, using the --meta flag.');
}
if (arguments.length == metaIndex + 1) {
throw Exception(
'The --meta flag must be followed by the path to the metadata file.');
}
final metaFile = File(arguments[metaIndex + 1]);
if (!(await metaFile.exists())) {
throw Exception('The provided metadata file does not exist.');
}
final fetchFromGithub = arguments.contains('--github');
final addTodaysVersionIndex = arguments.indexOf('--addTodaysVersion');
if (addTodaysVersionIndex != -1 &&
arguments.length == addTodaysVersionIndex + 1) {
throw Exception(
'The --addTodaysVersion flag must be followed by the version name.');
}
final addedTodaysVersion =
addTodaysVersionIndex != -1 ? arguments[addTodaysVersionIndex + 1] : null;
// GENERATE PACKAGE
final meta =
FlatpakMeta.fromJson(metaFile, skipLocalReleases: fetchFromGithub);
final outputDir =
Directory('${Directory.current.path}/flatpak_generator_exports');
await outputDir.create();
final packageGenerator = PackageGenerator(
inputDir: metaFile.parent,
meta: meta,
addedTodaysVersion: addedTodaysVersion);
await packageGenerator.generatePackage(
outputDir,
await PackageGenerator.runningOnARM()
? CPUArchitecture.aarch64
: CPUArchitecture.x86_64,
fetchFromGithub);
}
class PackageGenerator {
final Directory inputDir;
final FlatpakMeta meta;
final String? addedTodaysVersion;
PackageGenerator(
{required this.inputDir,
required this.meta,
required this.addedTodaysVersion});
Future<void> generatePackage(Directory outputDir, CPUArchitecture arch,
bool fetchReleasesFromGithub) async {
final tempDir = await outputDir.createTemp('flutter_generator_temp');
final appId = meta.appId;
// desktop file
final desktopFile = File('${inputDir.path}/${meta.desktopPath}');
if (!(await desktopFile.exists())) {
throw Exception(
'The desktop file does not exist under the specified path: ${desktopFile.path}');
}
await desktopFile.copy('${tempDir.path}/$appId.desktop');
// icons
final iconTempDir = Directory('${tempDir.path}/icons');
for (final icon in meta.icons) {
final iconFile = File('${inputDir.path}/${icon.path}');
if (!(await iconFile.exists())) {
throw Exception('The icon file ${iconFile.path} does not exist.');
}
final iconSubdir = Directory('${iconTempDir.path}/${icon.type}');
await iconSubdir.create(recursive: true);
await iconFile.copy('${iconSubdir.path}/${icon.getFilename(appId)}');
}
// AppStream metainfo file
final origAppStreamFile = File('${inputDir.path}/${meta.appStreamPath}');
if (!(await origAppStreamFile.exists())) {
throw Exception(
'The app data file does not exist under the specified path: ${origAppStreamFile.path}');
}
final editedAppStreamContent = AppStreamModifier.replaceVersions(
await origAppStreamFile.readAsString(),
await meta.getReleases(fetchReleasesFromGithub, addedTodaysVersion));
final editedAppStreamFile = File('${tempDir.path}/$appId.metainfo.xml');
await editedAppStreamFile.writeAsString(editedAppStreamContent);
// build files
final bundlePath =
'${inputDir.path}/${meta.localLinuxBuildDir}/${arch.flutterDirName}/release/bundle';
final buildDir = Directory(bundlePath);
if (!(await buildDir.exists())) {
throw Exception(
'The linux build directory does not exist under the specified path: ${buildDir.path}');
}
final destDir = Directory('${tempDir.path}/bin');
await destDir.create();
final baseFilename =
'${meta.lowercaseAppName}-linux-${arch.flatpakArchCode}';
final packagePath = '${outputDir.absolute.path}/$baseFilename.tar.gz';
final shaPath = '${outputDir.absolute.path}/$baseFilename.sha256';
await Process.run(
'cp', ['-r', '${buildDir.absolute.path}/.', destDir.absolute.path]);
await Process.run('tar', ['-czvf', packagePath, '.'],
workingDirectory: tempDir.absolute.path);
print('Generated $packagePath');
final preShasum = await Process.run('shasum', ['-a', '256', packagePath]);
final shasum = preShasum.stdout.toString().split(' ').first;
await File(shaPath).writeAsString(shasum);
print('Generated $shaPath');
await tempDir.delete(recursive: true);
}
static Future<bool> runningOnARM() async {
final unameRes = await Process.run('uname', ['-m']);
final unameString = unameRes.stdout.toString().trimLeft();
return unameString.startsWith('arm') || unameString.startsWith('aarch');
}
}
// Updates releases in ${appName}.metainfo.xml
class AppStreamModifier {
static String replaceVersions(
String origAppStreamContent, List<Release> versions) {
final joinedReleases = versions
.map((v) => '\t\t<release version="${v.version}" date="${v.date}" />')
.join('\n');
final releasesSection =
'<releases>\n$joinedReleases\n\t</releases>'; //TODO check this
if (origAppStreamContent.contains('<releases')) {
return origAppStreamContent
.replaceAll('\n', '<~>')
.replaceFirst(RegExp('<releases.*</releases>'), releasesSection)
.replaceAll('<~>', '\n');
} else {
return origAppStreamContent.replaceFirst(
'</component>', '\n\t$releasesSection\n</component>');
}
}
}

View File

@ -0,0 +1,337 @@
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');
}
}
}

View File

@ -0,0 +1,170 @@
// ignore_for_file: avoid_print
import 'dart:convert';
import 'dart:io';
import 'flatpak_shared.dart';
/// arguments:
/// --meta [file]
/// Required argument for providing the metadata file for this script.
/// --github
/// Use this option to pull release info from Github rather than the metadata file.
void main(List<String> arguments) async {
if (Platform.isWindows) {
throw Exception('Must be run under a UNIX-like operating system.');
}
final metaIndex = arguments.indexOf('--meta');
if (metaIndex == -1) {
throw Exception(
'You must run this script with a metadata file argument, using the --meta flag.');
}
if (arguments.length == metaIndex + 1) {
throw Exception(
'The --meta flag must be followed by the path to the metadata file.');
}
final metaFile = File(arguments[metaIndex + 1]);
if (!metaFile.existsSync()) {
throw Exception('The provided metadata file does not exist.');
}
final fetchFromGithub = arguments.contains('--github');
final meta =
FlatpakMeta.fromJson(metaFile, skipLocalReleases: fetchFromGithub);
final outputDir =
Directory('${Directory.current.path}/flatpak_generator_exports');
outputDir.createSync();
final manifestGenerator = FlatpakManifestGenerator(meta);
final manifestContent =
await manifestGenerator.generateFlatpakManifest(fetchFromGithub);
final manifestPath = '${outputDir.path}/${meta.appId}.json';
final manifestFile = File(manifestPath);
manifestFile.writeAsStringSync(manifestContent);
print('Generated $manifestPath');
final flathubJsonContent =
await manifestGenerator.generateFlathubJson(fetchFromGithub);
if (flathubJsonContent != null) {
final flathubJsonPath = '${outputDir.path}/flathub.json';
final flathubJsonFile = File(flathubJsonPath);
flathubJsonFile.writeAsStringSync(flathubJsonContent);
print('Generated $flathubJsonPath');
}
}
// ${appId}.json
class FlatpakManifestGenerator {
final FlatpakMeta meta;
Map<CPUArchitecture, bool>? _githubArchSupport;
Map<CPUArchitecture, bool>? _localArchSupport;
FlatpakManifestGenerator(this.meta);
Future<String> generateFlatpakManifest(bool fetchFromGithub) async {
final appName = meta.lowercaseAppName;
final appId = meta.appId;
final assets = await meta.getLatestReleaseAssets(fetchFromGithub);
if (assets == null) {
throw Exception('There are no associated assets.');
}
_lazyGenerateArchSupportMap(fetchFromGithub, assets);
const encoder = JsonEncoder.withIndent(' ');
return encoder.convert({
'app-id': appId,
'runtime': 'org.freedesktop.Platform',
'runtime-version': meta.freedesktopRuntime,
'sdk': 'org.freedesktop.Sdk',
'command': appName,
'separate-locales': false,
'finish-args': meta.finishArgs,
'modules': <dynamic>[
...meta.extraModules ?? <dynamic>[],
{
'name': appName,
'buildsystem': 'simple',
'build-commands': <dynamic>[
'cp -R $appName/bin/ /app/$appName',
'chmod +x /app/$appName/$appName',
'mkdir -p /app/bin/',
'mkdir -p /app/lib/',
'ln -s /app/$appName/$appName /app/bin/$appName',
...meta.buildCommandsAfterUnpack ?? [],
...meta.icons.map((icon) =>
'install -Dm644 $appName/icons/${icon.type}/${icon.getFilename(appId)} /app/share/icons/hicolor/${icon.type}/apps/${icon.getFilename(appId)}'),
'install -Dm644 $appName/$appId.desktop /app/share/applications/$appId.desktop',
'install -Dm644 $appName/$appId.metainfo.xml /app/share/metainfo/$appId.metainfo.xml'
],
'sources': assets
.map((a) => {
'type': 'archive',
'only-arches': [a.arch.flatpakArchCode],
(fetchFromGithub ? 'url' : 'path'): a.tarballUrlOrPath,
'sha256': a.tarballSha256,
'dest': meta.lowercaseAppName
})
.toList()
}
]
});
}
Future<String?> generateFlathubJson(bool fetchFromGithub) async {
final assets = await meta.getLatestReleaseAssets(fetchFromGithub);
if (assets == null) {
throw Exception('There are no associated assets.');
}
_lazyGenerateArchSupportMap(fetchFromGithub, assets);
const encoder = JsonEncoder.withIndent(' ');
final onlyArchListInput =
fetchFromGithub ? _githubArchSupport! : _localArchSupport!;
final onlyArchList = List<String>.empty(growable: true);
for (final e in onlyArchListInput.entries) {
if (e.value == true) {
onlyArchList.add(e.key.flatpakArchCode);
}
}
if (onlyArchList.length == CPUArchitecture.values.length) {
return null;
} else {
return encoder.convert({'only-arches': onlyArchList});
}
}
void _lazyGenerateArchSupportMap(
bool fetchFromGithub, List<ReleaseAsset> assets) {
if (fetchFromGithub) {
if (_githubArchSupport == null) {
_githubArchSupport = <CPUArchitecture, bool>{
for (final arch in CPUArchitecture.values) arch: false
};
for (final a in assets) {
_githubArchSupport![a.arch] = true;
}
}
} else {
if (_localArchSupport == null) {
_localArchSupport = <CPUArchitecture, bool>{
for (final arch in CPUArchitecture.values) arch: false
};
for (final a in assets) {
_localArchSupport![a.arch] = true;
}
}
}
}
}

View File

@ -0,0 +1,85 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
source: hosted
version: "2.11.0"
collection:
dependency: transitive
description:
name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
url: "https://pub.dev"
source: hosted
version: "1.17.1"
http:
dependency: "direct main"
description:
name: http
sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
url: "https://pub.dev"
source: hosted
version: "0.13.6"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
meta:
dependency: transitive
description:
name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path:
dependency: transitive
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
url: "https://pub.dev"
source: hosted
version: "1.8.3"
source_span:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.1"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev"
source: hosted
version: "1.3.2"
sdks:
dart: ">=2.19.0 <4.0.0"

View File

@ -0,0 +1,8 @@
name: flatpak_generator
version: 1.0.0
environment:
sdk: '>=2.18.5'
dependencies:
http: ^0.13.5