diff --git a/examples/hello_world/distribute_options.yaml b/examples/hello_world/distribute_options.yaml index 059cdb34..2eb47c54 100644 --- a/examples/hello_world/distribute_options.yaml +++ b/examples/hello_world/distribute_options.yaml @@ -38,6 +38,12 @@ releases: target: deb build_args: profile: true + - name: linux-pacman + package: + platform: linux + target: pacman + build_args: + profile: true - name: linux-zip package: platform: linux diff --git a/examples/hello_world/linux/packaging/pacman/make_config.yaml b/examples/hello_world/linux/packaging/pacman/make_config.yaml new file mode 100644 index 00000000..aa6ddcfb --- /dev/null +++ b/examples/hello_world/linux/packaging/pacman/make_config.yaml @@ -0,0 +1,100 @@ +# the name used to display in the OS. Specifically desktop +# entry name +display_name: Hello World + +# package name for debian/apt repository +# the name should be all lowercase with -+. +package_name: hello-world + +maintainer: + name: LiJianying + email: lijy91@foxmail.com + +# the size of binary in kilobyte +installed_size: 6604 + +# direct dependencies required by the application +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html +# dependencies: +# - libkeybinder-3.0-0 (>= 0.3.2) + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html +# build_dependencies_indep: +# - texinfo + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html +# build_dependencies: +# - kernel-headers-2.2.10 [!hurd-i386] +# - gnumach-dev [hurd-i386] +# - libluajit5.1-dev [i386 amd64 kfreebsd-i386 armel armhf powerpc mips] + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html +# recommended_dependencies: +# - neofetch + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html +# suggested_dependencies: +# - libkeybinder-3.0-0 (>= 0.3.2) + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html +# enhances: +# - spotube + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html +# pre_dependencies: +# - libc6 + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html#packages-which-break-other-packages-breaks +# breaks: +# - libspotify (<< 3.0.0) + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html#conflicting-binary-packages-conflicts +# conflicts: +# - spotify + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html#virtual-packages-provides +# provides: +# - libx11 + +# refer: https://www.debian.org/doc/debian-policy/ch-relationships.html#overwriting-files-and-replacing-packages-replaces +# replaces: +# - spotify + +postinstall_scripts: + - echo `Installed my awesome app` +postuninstall_scripts: + - echo `Surprised Pickachu face` + +# application icon path relative to project url +icon: assets/logo.png + +keywords: + - Hello + - World + - Test + - Application + +# a name to categorize the app into a section of application +generic_name: Music Application + +# supported mime types that can be opened using this application +# supported_mime_type: +# - audio/mpeg + +metainfo: linux/packaging/helloworld.appdata.xml + +# shown when right clicked the desktop entry icons +# actions: +# - Gallery +# - Create + +# the categories the application belong to +# refer: https://specifications.freedesktop.org/menu-spec/latest/ +categories: + - Music + - Media + +# let OS know if the application can be run on start_up. If it's false +# the application will deny to the OS if it was added as a start_up +# application +startup_notify: true diff --git a/packages/flutter_app_builder/pubspec.yaml b/packages/flutter_app_builder/pubspec.yaml index 6049c246..1fb10210 100644 --- a/packages/flutter_app_builder/pubspec.yaml +++ b/packages/flutter_app_builder/pubspec.yaml @@ -13,7 +13,8 @@ dependencies: pub_semver: ^2.1.0 pubspec_parse: ^1.1.0 recase: ^4.1.0 - shell_executor: ^0.1.5 + shell_executor: + path: ../shell_executor dev_dependencies: dependency_validator: ^3.0.0 diff --git a/packages/flutter_app_packager/lib/src/flutter_app_packager.dart b/packages/flutter_app_packager/lib/src/flutter_app_packager.dart index 123b3c9d..6345f460 100644 --- a/packages/flutter_app_packager/lib/src/flutter_app_packager.dart +++ b/packages/flutter_app_packager/lib/src/flutter_app_packager.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter_app_packager/src/api/app_package_maker.dart'; import 'package:flutter_app_packager/src/makers/makers.dart'; +import 'package:flutter_app_packager/src/makers/pacman/app_package_maker_pacman.dart'; class FlutterAppPackager { final List _makers = [ @@ -18,6 +19,7 @@ class FlutterAppPackager { AppPackageMakerMsix(), AppPackageMakerPkg(), AppPackageMakerRPM(), + AppPackageMakerPacman(), AppPackageMakerZip('linux'), AppPackageMakerZip('macos'), AppPackageMakerZip('windows'), diff --git a/packages/flutter_app_packager/lib/src/makers/pacman/app_package_maker_pacman.dart b/packages/flutter_app_packager/lib/src/makers/pacman/app_package_maker_pacman.dart new file mode 100644 index 00000000..400f492d --- /dev/null +++ b/packages/flutter_app_packager/lib/src/makers/pacman/app_package_maker_pacman.dart @@ -0,0 +1,197 @@ +import 'dart:io'; + +import 'package:flutter_app_packager/src/api/app_package_maker.dart'; +import 'package:flutter_app_packager/src/makers/pacman/make_pacman_config.dart'; +import 'package:path/path.dart' as path; +import 'package:shell_executor/shell_executor.dart'; + +class AppPackageMakerPacman extends AppPackageMaker { + @override + String get name => 'pacman'; + @override + String get platform => 'linux'; + @override + bool get isSupportedOnCurrentPlatform => Platform.isLinux; + @override + String get packageFormat => 'pacman'; + + @override + MakeConfigLoader get configLoader { + return MakePacmanConfigLoader() + ..platform = platform + ..packageFormat = packageFormat; + } + + @override + Future make(MakeConfig config) { + return _make( + config.buildOutputDirectory, + outputDirectory: config.outputDirectory, + makeConfig: config as MakePacmanConfig, + ); + } + + Future _make( + Directory appDirectory, { + required Directory outputDirectory, + required MakePacmanConfig makeConfig, + }) async { + final files = makeConfig.toFilesString(); + + Directory packagingDirectory = makeConfig.packagingDirectory; + + /// Need to create following directories + /// /usr/share/$appBinaryName + /// /usr/share/applications + /// /usr/share/icons/hicolor/128x128/apps + /// /usr/share/icons/hicolor/256x256/apps + + final applicationsDir = + path.join(packagingDirectory.path, 'usr/share/applications'); + final icon128Dir = path.join( + packagingDirectory.path, + 'usr/share/icons/hicolor/128x128/apps', + ); + final icon256Dir = path.join( + packagingDirectory.path, + 'usr/share/icons/hicolor/256x256/apps', + ); + final metainfoDir = + path.join(packagingDirectory.path, 'usr/share/metainfo'); + final mkdirProcessResult = await $('mkdir', [ + '-p', + path.join(packagingDirectory.path, 'usr/share', makeConfig.appBinaryName), + applicationsDir, + if (makeConfig.metainfo != null) metainfoDir, + if (makeConfig.icon != null) ...[icon128Dir, icon256Dir], + ]); + + if (mkdirProcessResult.exitCode != 0) throw MakeError(); + + if (makeConfig.icon != null) { + final iconFile = File(makeConfig.icon!); + if (!iconFile.existsSync()) { + throw MakeError("provided icon ${makeConfig.icon} path wasn't found"); + } + + await iconFile.copy( + path.join( + icon128Dir, + makeConfig.appBinaryName + path.extension(makeConfig.icon!), + ), + ); + await iconFile.copy( + path.join( + icon256Dir, + makeConfig.appBinaryName + path.extension(makeConfig.icon!), + ), + ); + } + if (makeConfig.metainfo != null) { + final metainfoPath = + path.join(Directory.current.path, makeConfig.metainfo!); + final metainfoFile = File(metainfoPath); + if (!metainfoFile.existsSync()) { + throw MakeError("Metainfo $metainfoPath path wasn't found"); + } + await metainfoFile.copy( + path.join( + metainfoDir, + makeConfig.appBinaryName + path.extension(makeConfig.metainfo!, 2), + ), + ); + } + + // create & write the files got from makeConfig + final installFile = File(path.join(packagingDirectory.path, '.INSTALL')); + final pkgInfoFile = File(path.join(packagingDirectory.path, '.PKGINFO')); + final desktopEntryFile = + File(path.join(applicationsDir, '${makeConfig.appBinaryName}.desktop')); + + if (!installFile.existsSync()) installFile.createSync(); + if (!pkgInfoFile.existsSync()) pkgInfoFile.createSync(); + if (!desktopEntryFile.existsSync()) desktopEntryFile.createSync(); + + await installFile.writeAsString(files['INSTALL']!); + await pkgInfoFile.writeAsString(files['PKGINFO']!); + await desktopEntryFile.writeAsString(files['DESKTOP']!); + + // copy the application binary to /usr/share/$appBinaryName + await $('cp', [ + '-fr', + '${appDirectory.path}/.', + '${packagingDirectory.path}/usr/share/${makeConfig.appBinaryName}/', + ]); + + // MTREE Metadata using bsdtar and fakeroot + ProcessResult mtreeResult = await $( + 'bsdtar', + [ + '-czf', + '.MTREE', + '--format=mtree', + '--options=!all,use-set,type,uid,gid,mode,time,size,md5,sha256,link', + '.PKGINFO', + '.INSTALL', + 'usr', + ], + environment: { + 'LANG': 'C', + }, + workingDirectory: packagingDirectory.path, + ); + if (mtreeResult.exitCode != 0) { + throw MakeError(mtreeResult.stderr); + } + + // create the pacman package using fakeroot and bsdtar + // fakeroot -- env LANG=C bsdtar -cf - .MTREE .PKGINFO * | xz -c -z - > $pkgname-$pkgver-$pkgrel-$arch.tar.xz + + ProcessResult archiveResult = await $( + 'bsdtar', + [ + '-cf', + 'temptar', + '.MTREE', + '.INSTALL', + '.PKGINFO', + 'usr', + ], + environment: { + 'LANG': 'C', + }, + workingDirectory: packagingDirectory.path, + ); + if (archiveResult.exitCode != 0) { + throw MakeError(archiveResult.stderr); + } + + ProcessResult processResult = await $( + 'xz', + [ + '-z', + 'temptar', + ], + workingDirectory: packagingDirectory.path, + ); + + if (processResult.exitCode != 0) { + throw MakeError(processResult.stderr); + } + + // copy file from temptar.xz to the makeConfig.outputFile.path + final copyResult = await $( + 'mv', + [ + '${packagingDirectory.path}/temptar.xz', + makeConfig.outputFile.path, + ], + ); + if (copyResult.exitCode != 0) { + throw MakeError(copyResult.stderr); + } + + packagingDirectory.deleteSync(recursive: true); + return MakeResult(makeConfig); + } +} diff --git a/packages/flutter_app_packager/lib/src/makers/pacman/make_pacman_config.dart b/packages/flutter_app_packager/lib/src/makers/pacman/make_pacman_config.dart new file mode 100644 index 00000000..76270b67 --- /dev/null +++ b/packages/flutter_app_packager/lib/src/makers/pacman/make_pacman_config.dart @@ -0,0 +1,333 @@ +import 'dart:io'; + +import 'package:flutter_app_packager/src/api/app_package_maker.dart'; + +// Ported from https://gist.github.com/Earnestly/bebad057f40a662b5cc3 +// format of make_config for pacman +/* +# the name used to display in the OS. Specifically desktop +# entry name +display_name: Hola Amigos + +# package name for arch repository +# the name should be all lowercase with -+. +package_name: hola-amigos + +licenses: + - MIT + +maintainer: + name: Gamer Boy 69 + email: rickastley@gmail.lol + +# the size of binary in kilobyte +installed_size: 24400 + +# direct dependencies required by the application +# refer: https://man.archlinux.org/man/PKGBUILD.5#OPTIONS_AND_DIRECTIVES +dependencies: + - mysupercooldep + +# optional dependencies not so much required by the application +# refer: https://man.archlinux.org/man/PKGBUILD.5#OPTIONS_AND_DIRECTIVES +optional_dependencies: + - iamalwaysoptional + +# refer: https://man.archlinux.org/man/PKGBUILD.5#OPTIONS_AND_DIRECTIVES +provides: + - whatsup + +# refer: https://man.archlinux.org/man/PKGBUILD.5#OPTIONS_AND_DIRECTIVES +options: + - zipman + +# refer: https://man.archlinux.org/man/PKGBUILD.5#OPTIONS_AND_DIRECTIVES +conflicts: + - libwhatsup + +# refer: https://man.archlinux.org/man/PKGBUILD.5#OPTIONS_AND_DIRECTIVES +replaces: + - yourdep + +# refer: https://man.archlinux.org/man/PKGBUILD.5#OPTIONS_AND_DIRECTIVES +provides: + - libx11 + +postinstall_scripts: + - echo `Installed my awesome app` +postupgrade_scripts: + - echo `Supercharger my awesome app` +postuninstall_scripts: + - echo `Surprised Pickachu face` + + +# application icon path relative to project url +icon: assets/logo.png + +keywords: + - Hello + - World + - Test + - Application + +# a name to categorize the app into a section of application +generic_name: Hobby Application + +# supported mime types that can be opened using this application +supported_mime_type: + - audio/mpeg + +# shown when right clicked the desktop entry icons +actions: + - Gallery + - Create + +# the categories the application belong to +# refer: https://specifications.freedesktop.org/menu-spec/latest/ +categories: + - Music + - Media + +# let OS know if the application can be run on start_up. If it's false +# the application will deny to the OS if it was added as a start_up +# application +startup_notify: true +*/ + +class MakePacmanConfig extends MakeLinuxPackageConfig { + MakePacmanConfig({ + required this.displayName, + required this.packageName, + this.installedSize, + required this.maintainer, + this.packageRelease = 1, + List? postinstallScripts, + List? postupgradeScripts, + List? postuninstallScripts, + this.actions, + this.categories, + this.dependencies, + this.genericName, + this.optDependencies, + this.options, + this.startupNotify = false, + this.groups = const ['default'], + this.licenses = const ['unknown'], + this.icon, + this.metainfo, + this.keywords, + this.provides, + this.conflicts, + this.replaces, + this.supportedMimeType, + }) : _postinstallScripts = postinstallScripts ?? [], + _postupgradeScripts = postupgradeScripts ?? [], + _postremoveScripts = postuninstallScripts ?? []; + + factory MakePacmanConfig.fromJson(Map map) { + return MakePacmanConfig( + displayName: map['display_name'], + packageName: map['package_name'], // + packageRelease: int.tryParse(map['package_release'] ?? '1') ?? 1, + maintainer: + "${map['maintainer']['name']} <${map['maintainer']['email']}>", + dependencies: map['dependencies'] != null + ? List.castFrom(map['dependencies']) + : null, + conflicts: map['conflicts'] != null + ? List.castFrom(map['conflicts']) + : null, + replaces: map['replaces'] != null + ? List.castFrom(map['replaces']) + : null, + options: map['options'] != null + ? List.castFrom(map['options']) + : null, + optDependencies: map['optional_dependencies'] != null + ? List.castFrom(map['optional_dependencies']) + : null, + licenses: map['licenses'] != null + ? List.castFrom(map['licenses']) + : ["unknown"], + groups: map['groups'] != null + ? List.castFrom(map['groups']) + : ["default"], + provides: map['provides'] != null + ? List.castFrom(map['provides']) + : null, + postinstallScripts: map['postinstall_scripts'] != null + ? List.castFrom(map['postinstall_scripts']) + : null, + postuninstallScripts: map['postuninstall_scripts'] != null + ? List.castFrom(map['postuninstall_scripts']) + : null, + postupgradeScripts: map['postupgrade_scripts'] != null + ? List.castFrom(map['postupgrade_scripts']) + : null, + keywords: map['keywords'] != null + ? List.castFrom(map['keywords']) + : null, + supportedMimeType: map['supported_mime_type'] != null + ? List.castFrom(map['supported_mime_type']) + : null, + actions: map['actions'] != null + ? List.castFrom(map['actions']) + : null, + categories: map['categories'] != null + ? List.castFrom(map['categories']) + : null, + startupNotify: map['startup_notify'], + genericName: map['generic_name'], + installedSize: map['installed_size'], + icon: map['icon'], + metainfo: map['metainfo'], + ); + } + + String displayName; + String packageName; + String maintainer; + int packageRelease; + int? installedSize; + List licenses; + List groups; + String? icon; + String? metainfo; + String? genericName; + bool startupNotify; + List? options; + List? dependencies; + List? optDependencies; + List? conflicts; + List? replaces; + List? provides; + List _postinstallScripts; + List _postupgradeScripts; + List _postremoveScripts; + List? keywords; + List? supportedMimeType; + List? actions; + List? categories; + + List get postinstallScripts => [ + 'ln -s /usr/share/$appBinaryName/$appBinaryName /usr/bin/$appBinaryName', + 'chmod +x /usr/bin/$appBinaryName', + ..._postinstallScripts, + ]; + + List get postuninstallScripts => [ + 'rm /usr/bin/$appBinaryName', + ..._postremoveScripts, + ]; + + List get postupgradeScripts => _postupgradeScripts; + + @override + Map toJson() { + return { + 'PKGINFO': { + 'pkgname': packageName, + 'pkgver': appVersion.toString(), + 'pkgdesc': pubspec.description, + 'packager': maintainer, + 'size': installedSize, + 'license': '(${licenses.join(', ')})', + 'groups': '(${groups.join(', ')})', + 'arch': '(${_getArchitecture()})', + 'url': pubspec.homepage, + 'options': options != null ? "(${options!.join(', ')})" : null, + 'depends': + dependencies != null ? "(${dependencies!.join(', ')})" : null, + 'optdepends': + optDependencies != null ? "(${optDependencies!.join(', ')})" : null, + 'conflicts': conflicts != null ? "(${conflicts!.join(', ')})" : null, + 'replaces': replaces != null ? "(${replaces!.join(', ')})" : null, + 'provides': provides != null ? "(${provides!.join(', ')})" : null, + }..removeWhere((key, value) => value == null), + 'DESKTOP': { + 'Type': 'Application', + 'Version': appVersion.toString(), + 'Name': displayName, + 'GenericName': genericName, + 'Icon': appBinaryName, + 'Exec': '$appBinaryName %U', + 'Actions': actions != null && actions!.isNotEmpty + ? '${actions!.join(';')};' + : null, + 'MimeType': supportedMimeType != null && supportedMimeType!.isNotEmpty + ? '${supportedMimeType!.join(';')};' + : null, + 'Categories': categories != null && categories!.isNotEmpty + ? '${categories!.join(';')};' + : null, + 'Keywords': keywords != null && keywords!.isNotEmpty + ? '${keywords!.join(';')};' + : null, + 'StartupNotify': startupNotify, + }..removeWhere((key, value) => value == null), + }; + } + + Map toFilesString() { + final json = toJson(); + final pkginfoFile = + '${(json['PKGINFO'] as Map).entries.map( + (e) => '${e.key}=${e.value}', + ).join('\n')}\n'; + final installFileMap = { + "post_install": postinstallScripts.join('\n\t'), + "post_upgrade": + postupgradeScripts.isNotEmpty ? postupgradeScripts.join('\n') : null, + "post_remove": postuninstallScripts.join('\n'), + }..removeWhere((key, value) => value == null); + + final installFile = installFileMap.entries + .map( + (e) => '${e.key}() {\n\t${e.value}\n}', + ) + .join('\n'); + + final desktopFile = [ + '[Desktop Entry]', + ...(json['DESKTOP'] as Map).entries.map( + (e) => '${e.key}=${e.value}', + ), + ].join('\n'); + final map = { + 'PKGINFO': pkginfoFile, + 'DESKTOP': desktopFile, + 'INSTALL': installFile, + }; + return map; + } +} + +class MakePacmanConfigLoader extends DefaultMakeConfigLoader { + @override + MakeConfig load( + Map? arguments, + Directory outputDirectory, { + required Directory buildOutputDirectory, + required List buildOutputFiles, + }) { + final baseMakeConfig = super.load( + arguments, + outputDirectory, + buildOutputDirectory: buildOutputDirectory, + buildOutputFiles: buildOutputFiles, + ); + final map = loadMakeConfigYaml( + '$platform/packaging/$packageFormat/make_config.yaml', + ); + return MakePacmanConfig.fromJson(map).copyWith(baseMakeConfig); + } +} + +String _getArchitecture() { + final result = Process.runSync('uname', ['-m']); + if ('${result.stdout}'.trim() == 'aarch64') { + return 'aarch64'; + } else { + return 'x86_64'; + } +} diff --git a/packages/flutter_app_packager/pubspec.yaml b/packages/flutter_app_packager/pubspec.yaml index 420ae9ec..1f8dcdce 100644 --- a/packages/flutter_app_packager/pubspec.yaml +++ b/packages/flutter_app_packager/pubspec.yaml @@ -15,7 +15,8 @@ dependencies: path: ^1.8.1 pub_semver: ^2.1.0 pubspec_parse: ^1.1.0 - shell_executor: ^0.1.5 + shell_executor: + path: ../shell_executor yaml: ^3.1.0 dev_dependencies: diff --git a/packages/flutter_app_publisher/pubspec.yaml b/packages/flutter_app_publisher/pubspec.yaml index 6ff31417..a58a4be1 100644 --- a/packages/flutter_app_publisher/pubspec.yaml +++ b/packages/flutter_app_publisher/pubspec.yaml @@ -11,10 +11,12 @@ dependencies: dio: ^5.3.4 googleapis: ^13.2.0 googleapis_auth: ^1.6.0 - parse_app_package: ^0.4.0 + parse_app_package: + path: ../parse_app_package pubspec_parse: ^1.1.0 qiniu_sdk_base: ^0.5.0 - shell_executor: ^0.1.5 + shell_executor: + path: ../shell_executor dev_dependencies: dependency_validator: ^3.0.0 diff --git a/packages/flutter_distributor/pubspec.yaml b/packages/flutter_distributor/pubspec.yaml index 75d46582..ef8d5fc2 100644 --- a/packages/flutter_distributor/pubspec.yaml +++ b/packages/flutter_distributor/pubspec.yaml @@ -18,13 +18,17 @@ dependencies: args: ^2.2.0 charset: ^2.0.1 dio: ^5.3.4 - flutter_app_builder: ^0.4.4 - flutter_app_packager: ^0.4.4 - flutter_app_publisher: ^0.4.4 + flutter_app_builder: + path: ../flutter_app_builder + flutter_app_packager: + path: ../flutter_app_packager + flutter_app_publisher: + path: ../flutter_app_publisher logging: ^1.0.2 path: ^1.8.1 pubspec_parse: ^1.1.0 - shell_executor: ^0.1.5 + shell_executor: + path: ../shell_executor shell_uikit: ^0.1.1 yaml: ^3.1.0 diff --git a/packages/parse_app_package/pubspec.yaml b/packages/parse_app_package/pubspec.yaml index 89b71adc..88c7a8fe 100644 --- a/packages/parse_app_package/pubspec.yaml +++ b/packages/parse_app_package/pubspec.yaml @@ -11,7 +11,8 @@ dependencies: archive: ^3.4.10 args: ^2.2.0 plist_parser: ^0.0.11 - shell_executor: ^0.1.5 + shell_executor: + path: ../shell_executor executables: parse_app_package: main